feat: add CCashClient methods

This commit is contained in:
Luke Bennett 2021-06-13 03:01:37 +01:00
parent 145c9507b4
commit 9e3a08f307
No known key found for this signature in database
GPG key ID: A738E9C68D3BF31A
18 changed files with 328 additions and 15 deletions

View file

@ -15,9 +15,11 @@ npm install ccash-client-js
```js ```js
import { CCashClient } from 'ccash-client-js'; import { CCashClient } from 'ccash-client-js';
process.env.CCASH_API_BASE_URL = 'https://your.ccash.api';
const client = new CCashClient(); const client = new CCashClient();
client.balance('twix'); console.log(await client.balance('blinkblinko'));
``` ```
## Examples ## Examples

1
examples/node/.env Normal file
View file

@ -0,0 +1 @@
CCASH_API_BASE_URL=https://wtfisthis.tech/BankF

View file

@ -1,10 +1,20 @@
require('dotenv').config();
const { CCashClient } = require('ccash-client-js'); const { CCashClient } = require('ccash-client-js');
const client = new CCashClient() const user = 'blinkblinko';
const pass = 'TestPassword';
const client = new CCashClient();
async function main() { async function main() {
const balance = await client.balance('twix'); const createdUser = await client.addUser(user, pass);
console.log(`Balance: ${balance}`) console.log('User created', createdUser);
const balance = await client.balance(user);
console.log(`Balance: ${balance}`);
const deletedUser = await client.deleteUser(user, pass);
console.log('User deleated', deletedUser);
} }
main() main();

View file

@ -7,6 +7,7 @@
"start": "node index.js" "start": "node index.js"
}, },
"dependencies": { "dependencies": {
"ccash-client-js": "file:../.." "ccash-client-js": "file:../..",
"dotenv": "^10.0.0"
} }
} }

View file

@ -14,6 +14,11 @@ axios@^0.21.1:
dependencies: dependencies:
axios "^0.21.1" axios "^0.21.1"
dotenv@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
follow-redirects@^1.10.0: follow-redirects@^1.10.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"

2
examples/web/.env Normal file
View file

@ -0,0 +1,2 @@
SKIP_PREFLIGHT_CHECK=true
CCASH_API_BASE_URL=https://wtfisthis.tech/BankF

View file

@ -2,14 +2,16 @@ import React, { useEffect, useState } from 'react';
import { CCashClient } from 'ccash-client-js'; import { CCashClient } from 'ccash-client-js';
import './App.css'; import './App.css';
const client = new CCashClient(); const client = new CCashClient(
process.env.CCASH_API_BASE_URL || 'https://wtfisthis.tech/BankF'
);
function App() { function App() {
const [balance, setBalance] = useState(0); const [balance, setBalance] = useState(0);
useEffect(() => { useEffect(() => {
(async function getBalance() { (async function getBalance() {
setBalance(await client.balance('twix')); setBalance(await client.balance('blinkblinko'));
})(); })();
}, []); }, []);

View file

@ -3163,6 +3163,7 @@ case-sensitive-paths-webpack-plugin@2.3.0:
version "0.0.0" version "0.0.0"
dependencies: dependencies:
axios "^0.21.1" axios "^0.21.1"
class-transformer "^0.4.0"
chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2" version "2.4.2"
@ -3266,6 +3267,11 @@ cjs-module-lexer@^0.6.0:
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f"
integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==
class-transformer@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.4.0.tgz#b52144117b423c516afb44cc1c76dbad31c2165b"
integrity sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==
class-utils@^0.3.5: class-utils@^0.3.5:
version "0.3.6" version "0.3.6"
resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"

View file

@ -18,7 +18,8 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.1" "axios": "^0.21.1",
"class-transformer": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.5", "@babel/core": "^7.14.5",

View file

@ -0,0 +1,69 @@
import { Exception } from './Exception';
export class BaseUrlMissingException extends Exception {
constructor() {
super('base url missing');
}
}
export class UserNotFoundException extends Exception {
constructor() {
super('user not found');
}
}
export class WrongPasswordException extends Exception {
constructor() {
super('wrong password');
}
}
export class InvalidRequestException extends Exception {
constructor() {
super('invalid request');
}
}
export class WrongAdminPasswordException extends Exception {
constructor() {
super('wrong admin password');
}
}
export class NameTooLongException extends Exception {
constructor() {
super('name too long');
}
}
export class UserAlreadyExistsException extends Exception {
constructor() {
super('user already exists');
}
}
export class InsufficientFundsException extends Exception {
constructor() {
super('insufficient funds');
}
}
export enum ErrorCodes {
UserNotFound = -1,
WrongPassword = -2,
InvalidRequest = -3,
WrongAdminPassword = -4,
NameTooLong = -5,
UserAlreadyExists = -6,
InsufficientFunds = -7,
}
export const ExceptionMap = {
[ErrorCodes.UserNotFound]: UserNotFoundException,
[ErrorCodes.WrongPassword]: WrongPasswordException,
[ErrorCodes.InvalidRequest]: InvalidRequestException,
[ErrorCodes.WrongAdminPassword]: WrongAdminPasswordException,
[ErrorCodes.NameTooLong]: NameTooLongException,
[ErrorCodes.UserAlreadyExists]: UserAlreadyExistsException,
[ErrorCodes.InsufficientFunds]: InsufficientFundsException,
};

View file

@ -1,5 +1,166 @@
export class CCashClient { import axios, { AxiosInstance, AxiosResponse } from 'axios';
balance(user: string): Promise<number> { import { plainToClass } from 'class-transformer';
return Promise.resolve(10); import { ICCashClient, User } from './CCashClient.types';
import {
BaseUrlMissingException,
ExceptionMap,
ErrorCodes,
} from './CCashClient.exceptions';
export class CCashClient implements Partial<ICCashClient> {
/** TODO: not partial **/
http: AxiosInstance;
constructor(baseURL: string | undefined = process.env.CCASH_API_BASE_URL) {
if (!baseURL) {
throw new BaseUrlMissingException();
} }
this.http = axios.create({
baseURL,
});
}
balance(user: string): Promise<number> {
return this.http
.get(`/${user}/bal`)
.then((response) => this.handleError(response) || response.data.value);
}
log(
user: string,
pass: string,
transactionCount: number = 10
): Promise<number[]> {
return this.http
.get(`/${user}/bal`, {
headers: { Password: pass },
params: { n: transactionCount },
})
.then((response) => this.handleError(response) || response.data.value);
}
sendFunds(
user: string,
pass: string,
to: string,
amount: number
): Promise<number> {
return this.http
.post(`/${user}/send/${to}`, {
headers: { Password: pass },
params: { amount },
})
.then((response) => this.handleError(response) || amount);
}
verifyPassword(user: string, pass: string): Promise<boolean> {
return this.http
.get(`/${user}/pass/verify`, { headers: { Password: pass } })
.then((response) => this.handleError(response) || response.data.value);
}
changePassword(user: string, pass: string, newPass: string): Promise<User> {
return this.http
.patch(
`/${user}/pass/change`,
{ password: newPass },
{ headers: { Password: pass } }
)
.then(
(response) =>
this.handleError(response) || this.serialize(User, { user })
);
}
setBal(user: string, pass: string, amount: number): Promise<number> {
return this.http
.patch(`/admin/${user}/bal`, undefined, {
headers: { Password: pass },
params: { amount },
})
.then((response) => this.handleError(response) || amount);
}
help(): Promise<string> {
return this.http.get('/help').then((response) => response.data);
}
close(pass: string): Promise<boolean> {
return this.http
.post('/close', undefined, { headers: { Password: pass } })
.then((response) => this.handleError(response) || true);
}
contains(user: string): Promise<boolean> {
return this.http
.get(`/contains/${user}`)
.then(
(response) => this.handleError(response) || response.data.value || false
);
}
adminVerifyPass(pass: string): Promise<boolean> {
return this.http
.get('/admin/verify')
.then(
(response) => this.handleError(response) || response.data.value || false
);
}
addUser(user: string, pass: string): Promise<User> {
return this.http
.post(`/user/${user}`, undefined, { headers: { Password: pass } })
.then(
(response) =>
this.handleError(response) || this.serialize(User, { user })
);
}
adminAddUser(
user: string,
pass: string,
initialBalance: number
): Promise<User> {
return this.http
.post(`/user/${user}`, undefined, {
headers: { Password: pass },
params: { init_bal: initialBalance },
})
.then(
(response) =>
this.handleError(response) || this.serialize(User, { user })
);
}
deleteUser(user: string, pass: string): Promise<User> {
return this.http
.delete(`/user/${user}`, { headers: { Password: pass } })
.then(
(response) =>
this.handleError(response) || this.serialize(User, { user })
);
}
adminDeleteUser(user: string, pass: string): Promise<User> {
return this.http
.delete(`/user/${user}`, { headers: { Password: pass } })
.then(
(response) =>
this.handleError(response) || this.serialize(User, { user })
);
}
private handleError(response: AxiosResponse<any>): false {
if (
response.data.value &&
Object.values(ErrorCodes).includes(response.data.value)
) {
throw new ExceptionMap[response.data.value as ErrorCodes]();
}
return false;
}
private serialize = plainToClass;
} }

36
src/CCashClient.types.ts Normal file
View file

@ -0,0 +1,36 @@
import { User } from './User';
export { User };
export interface ICCashClient {
// Usage
balance(user: string): Promise<number>;
log(user: string, pass: string, transactionCount?: number): Promise<number[]>;
sendFunds(
user: string,
pass: string,
to: string,
amount: number
): Promise<number>;
verifyPassword(user: string, pass: string): Promise<boolean>;
// Meta usage
changePassword(user: string, pass: string, newPass: string): Promise<User>;
setBal(user: string, pass: string, amount: number): Promise<number>;
// System usage
help(): Promise<string>;
close(pass: string): Promise<boolean>;
contains(user: string): Promise<boolean>;
adminVerifyPass(pass: string): Promise<boolean>;
// User management
addUser(user: string, pass: string): Promise<User>;
adminAddUser(
user: string,
pass: string,
initialBalance: number
): Promise<User>;
deleteUser(user: string, pass: string): Promise<User>;
adminDeleteUser(user: string, pass: string): Promise<User>;
}

1
src/Exception.ts Normal file
View file

@ -0,0 +1 @@
export class Exception extends Error {}

7
src/User.ts Normal file
View file

@ -0,0 +1,7 @@
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class User {
@Expose()
user!: string;
}

View file

@ -1 +1,3 @@
export { CCashClient } from './CCashClient'; export * from './CCashClient';
export * from './CCashClient.types';
export * from './CCashClient.exceptions';

View file

@ -12,7 +12,7 @@ describe('CCashClient', () => {
describe('balance', () => { describe('balance', () => {
it('returns an integer', async () => { it('returns an integer', async () => {
expect(await client.balance('twix')).toEqual(10); expect(await client.balance('blinkblinko')).toEqual(10);
}); });
}); });
}); });

View file

@ -3,11 +3,13 @@
"outDir": "./dist/cjs", "outDir": "./dist/cjs",
"target": "es2015", "target": "es2015",
"module": "commonjs", "module": "commonjs",
"declaration": true,
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "node",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true,
"experimentalDecorators": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"paths": { "paths": {

View file

@ -1546,6 +1546,11 @@ cjs-module-lexer@^1.0.0:
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.1.tgz#2fd46d9906a126965aa541345c499aaa18e8cd73" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.1.tgz#2fd46d9906a126965aa541345c499aaa18e8cd73"
integrity sha512-jVamGdJPDeuQilKhvVn1h3knuMOZzr8QDnpk+M9aMlCaMkTDd6fBWPhiDqFvFZ07pL0liqabAiuy8SY4jGHeaw== integrity sha512-jVamGdJPDeuQilKhvVn1h3knuMOZzr8QDnpk+M9aMlCaMkTDd6fBWPhiDqFvFZ07pL0liqabAiuy8SY4jGHeaw==
class-transformer@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.4.0.tgz#b52144117b423c516afb44cc1c76dbad31c2165b"
integrity sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==
cliui@^7.0.2: cliui@^7.0.2:
version "7.0.4" version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"