@mionkit/client
Advanced tools
Comparing version 0.4.2 to 0.6.0
@@ -17,4 +17,8 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./src/types"), exports); | ||
__exportStar(require("./src/constants"), exports); | ||
__exportStar(require("./src/reflection"), exports); | ||
__exportStar(require("./src/clientMethodsMetadata"), exports); | ||
__exportStar(require("./src/request"), exports); | ||
__exportStar(require("./src/client"), exports); | ||
__exportStar(require("./src/types"), exports); | ||
//# sourceMappingURL=index.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.prefillData = exports.remote = exports.setClientOptions = void 0; | ||
const node_fetch_1 = require("node-fetch"); | ||
exports.initClient = void 0; | ||
const constants_1 = require("./constants"); | ||
let clientOptions = { | ||
...constants_1.DEFAULT_PREFILL_OPTIONS, | ||
}; | ||
const setClientOptions = (prefillOptions_ = {}) => { | ||
clientOptions = { | ||
...clientOptions, | ||
...prefillOptions_, | ||
const core_1 = require("@mionkit/core"); | ||
const request_1 = require("./request"); | ||
function initClient(options) { | ||
const clientOptions = { | ||
...constants_1.DEFAULT_PREFILL_OPTIONS, | ||
...options, | ||
}; | ||
}; | ||
exports.setClientOptions = setClientOptions; | ||
const remote = (path, ...args) => { | ||
let requestData = { | ||
...(args.length ? { path: args } : {}), | ||
}; | ||
const remoteCall = { | ||
data: (reqData = {}) => { | ||
requestData = { | ||
...reqData, | ||
...requestData, | ||
}; | ||
return remoteCall; | ||
}, | ||
call: async () => { | ||
const headers = getHeaders(requestData, null); | ||
const body = getBody(requestData, null); | ||
const response = await (0, node_fetch_1.default)(clientOptions.apiURL, { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
...headers, | ||
}, | ||
body: Object.keys(body).length ? JSON.stringify(body) : '', | ||
}); | ||
return response.json(); | ||
}, | ||
}; | ||
return remoteCall; | ||
}; | ||
exports.remote = remote; | ||
const getHeaders = (requestData, remoteExecutable) => { | ||
return {}; | ||
}; | ||
const getBody = (requestData, remoteExecutable) => { | ||
return {}; | ||
}; | ||
const prefillData = (fieldName, ...args) => { }; | ||
exports.prefillData = prefillData; | ||
const client = new MionClient(clientOptions); | ||
const rootProxy = new MethodProxy([], client, clientOptions); | ||
return { client, methods: rootProxy.proxy }; | ||
} | ||
exports.initClient = initClient; | ||
class MionClient { | ||
constructor(clientOptions) { | ||
this.clientOptions = clientOptions; | ||
this.metadataById = new Map(); | ||
this.reflectionById = new Map(); | ||
} | ||
call(routeSubRequest, ...hookSubRequests) { | ||
const request = new request_1.MionRequest(this.clientOptions, this.metadataById, this.reflectionById, routeSubRequest, hookSubRequests); | ||
return request | ||
.call() | ||
.then(() => [routeSubRequest.return, ...hookSubRequests.map((hook) => hook.return)]); | ||
} | ||
validate(...subRequest) { | ||
const request = new request_1.MionRequest(this.clientOptions, this.metadataById, this.reflectionById); | ||
return request.validateParams(subRequest); | ||
} | ||
prefill(...subRequest) { | ||
const request = new request_1.MionRequest(this.clientOptions, this.metadataById, this.reflectionById); | ||
return request.prefill(subRequest); | ||
} | ||
removePrefill(...subRequest) { | ||
const request = new request_1.MionRequest(this.clientOptions, this.metadataById, this.reflectionById); | ||
return request.removePrefill(subRequest); | ||
} | ||
} | ||
class MethodProxy { | ||
constructor(parentProps, client, clientOptions) { | ||
this.parentProps = parentProps; | ||
this.client = client; | ||
this.clientOptions = clientOptions; | ||
this.propsProxies = {}; | ||
this.handler = { | ||
apply: (target, thisArg, argArray) => { | ||
const subRequest = { | ||
pointer: [...this.parentProps], | ||
id: (0, core_1.getRouterItemId)(this.parentProps), | ||
isResolved: false, | ||
params: argArray, | ||
return: undefined, | ||
error: undefined, | ||
prefill: () => { | ||
return this.client.prefill(subRequest).catch((errors) => Promise.reject(findError(subRequest, errors))); | ||
}, | ||
removePrefill: () => { | ||
return this.client.removePrefill(subRequest).catch((errors) => Promise.reject(findError(subRequest, errors))); | ||
}, | ||
call: (...hooks) => { | ||
return this.client | ||
.call(subRequest, ...hooks) | ||
.then(() => subRequest.return) | ||
.catch((errors) => Promise.reject(findError(subRequest, errors))); | ||
}, | ||
validate: () => { | ||
return this.client | ||
.validate(subRequest) | ||
.then((responses) => responses[0]) | ||
.catch((errors) => Promise.reject(findError(subRequest, errors))); | ||
}, | ||
}; | ||
return subRequest; | ||
}, | ||
get: (target, prop) => { | ||
const existing = this.propsProxies[prop]; | ||
if (existing) | ||
return existing.proxy; | ||
const newMethodProxy = new MethodProxy([...this.parentProps, prop], this.client, this.clientOptions); | ||
this.propsProxies[prop] = newMethodProxy; | ||
return newMethodProxy.proxy; | ||
}, | ||
}; | ||
const target = () => null; | ||
this.proxy = new Proxy(target, this.handler); | ||
} | ||
} | ||
function findError(req, errors) { | ||
return errors.get(req.id) || errors.values().next().value; | ||
} | ||
//# sourceMappingURL=client.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.DEFAULT_PREFILL_OPTIONS = void 0; | ||
exports.STORAGE_KEY = exports.DEFAULT_PREFILL_OPTIONS = void 0; | ||
const reflection_1 = require("@mionkit/reflection"); | ||
exports.DEFAULT_PREFILL_OPTIONS = { | ||
apiURL: window.location.origin, | ||
prefillStorage: 'localStorage', | ||
baseURL: '', | ||
storage: 'localStorage', | ||
fetchOptions: { | ||
method: 'PUT', | ||
headers: { 'Content-Type': 'application/json' }, | ||
}, | ||
prefix: '', | ||
suffix: '', | ||
enableValidation: true, | ||
enableSerialization: true, | ||
reflectionOptions: reflection_1.DEFAULT_REFLECTION_OPTIONS, | ||
bodyParser: JSON, | ||
autoGenerateErrorId: false, | ||
}; | ||
exports.STORAGE_KEY = 'mionkit:client'; | ||
//# sourceMappingURL=constants.js.map |
@@ -0,3 +1,7 @@ | ||
export * from './src/types'; | ||
export * from './src/constants'; | ||
export * from './src/reflection'; | ||
export * from './src/clientMethodsMetadata'; | ||
export * from './src/request'; | ||
export * from './src/client'; | ||
export * from './src/types'; | ||
//# sourceMappingURL=index.js.map |
@@ -1,46 +0,90 @@ | ||
import fetch from 'node-fetch'; | ||
import { DEFAULT_PREFILL_OPTIONS } from './constants'; | ||
let clientOptions = { | ||
...DEFAULT_PREFILL_OPTIONS, | ||
}; | ||
export const setClientOptions = (prefillOptions_ = {}) => { | ||
clientOptions = { | ||
...clientOptions, | ||
...prefillOptions_, | ||
import { getRouterItemId } from '@mionkit/core'; | ||
import { MionRequest } from './request'; | ||
export function initClient(options) { | ||
const clientOptions = { | ||
...DEFAULT_PREFILL_OPTIONS, | ||
...options, | ||
}; | ||
}; | ||
export const remote = (path, ...args) => { | ||
let requestData = { | ||
...(args.length ? { path: args } : {}), | ||
}; | ||
const remoteCall = { | ||
data: (reqData = {}) => { | ||
requestData = { | ||
...reqData, | ||
...requestData, | ||
}; | ||
return remoteCall; | ||
}, | ||
call: async () => { | ||
const headers = getHeaders(requestData, null); | ||
const body = getBody(requestData, null); | ||
const response = await fetch(clientOptions.apiURL, { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
...headers, | ||
}, | ||
body: Object.keys(body).length ? JSON.stringify(body) : '', | ||
}); | ||
return response.json(); | ||
}, | ||
}; | ||
return remoteCall; | ||
}; | ||
const getHeaders = (requestData, remoteExecutable) => { | ||
return {}; | ||
}; | ||
const getBody = (requestData, remoteExecutable) => { | ||
return {}; | ||
}; | ||
export const prefillData = (fieldName, ...args) => { }; | ||
const client = new MionClient(clientOptions); | ||
const rootProxy = new MethodProxy([], client, clientOptions); | ||
return { client, methods: rootProxy.proxy }; | ||
} | ||
class MionClient { | ||
constructor(clientOptions) { | ||
this.clientOptions = clientOptions; | ||
this.metadataById = new Map(); | ||
this.reflectionById = new Map(); | ||
} | ||
call(routeSubRequest, ...hookSubRequests) { | ||
const request = new MionRequest(this.clientOptions, this.metadataById, this.reflectionById, routeSubRequest, hookSubRequests); | ||
return request | ||
.call() | ||
.then(() => [routeSubRequest.return, ...hookSubRequests.map((hook) => hook.return)]); | ||
} | ||
validate(...subRequest) { | ||
const request = new MionRequest(this.clientOptions, this.metadataById, this.reflectionById); | ||
return request.validateParams(subRequest); | ||
} | ||
prefill(...subRequest) { | ||
const request = new MionRequest(this.clientOptions, this.metadataById, this.reflectionById); | ||
return request.prefill(subRequest); | ||
} | ||
removePrefill(...subRequest) { | ||
const request = new MionRequest(this.clientOptions, this.metadataById, this.reflectionById); | ||
return request.removePrefill(subRequest); | ||
} | ||
} | ||
class MethodProxy { | ||
constructor(parentProps, client, clientOptions) { | ||
this.parentProps = parentProps; | ||
this.client = client; | ||
this.clientOptions = clientOptions; | ||
this.propsProxies = {}; | ||
this.handler = { | ||
apply: (target, thisArg, argArray) => { | ||
const subRequest = { | ||
pointer: [...this.parentProps], | ||
id: getRouterItemId(this.parentProps), | ||
isResolved: false, | ||
params: argArray, | ||
return: undefined, | ||
error: undefined, | ||
prefill: () => { | ||
return this.client.prefill(subRequest).catch((errors) => Promise.reject(findError(subRequest, errors))); | ||
}, | ||
removePrefill: () => { | ||
return this.client.removePrefill(subRequest).catch((errors) => Promise.reject(findError(subRequest, errors))); | ||
}, | ||
call: (...hooks) => { | ||
return this.client | ||
.call(subRequest, ...hooks) | ||
.then(() => subRequest.return) | ||
.catch((errors) => Promise.reject(findError(subRequest, errors))); | ||
}, | ||
validate: () => { | ||
return this.client | ||
.validate(subRequest) | ||
.then((responses) => responses[0]) | ||
.catch((errors) => Promise.reject(findError(subRequest, errors))); | ||
}, | ||
}; | ||
return subRequest; | ||
}, | ||
get: (target, prop) => { | ||
const existing = this.propsProxies[prop]; | ||
if (existing) | ||
return existing.proxy; | ||
const newMethodProxy = new MethodProxy([...this.parentProps, prop], this.client, this.clientOptions); | ||
this.propsProxies[prop] = newMethodProxy; | ||
return newMethodProxy.proxy; | ||
}, | ||
}; | ||
const target = () => null; | ||
this.proxy = new Proxy(target, this.handler); | ||
} | ||
} | ||
function findError(req, errors) { | ||
return errors.get(req.id) || errors.values().next().value; | ||
} | ||
//# sourceMappingURL=client.js.map |
@@ -0,5 +1,18 @@ | ||
import { DEFAULT_REFLECTION_OPTIONS } from '@mionkit/reflection'; | ||
export const DEFAULT_PREFILL_OPTIONS = { | ||
apiURL: window.location.origin, | ||
prefillStorage: 'localStorage', | ||
baseURL: '', | ||
storage: 'localStorage', | ||
fetchOptions: { | ||
method: 'PUT', | ||
headers: { 'Content-Type': 'application/json' }, | ||
}, | ||
prefix: '', | ||
suffix: '', | ||
enableValidation: true, | ||
enableSerialization: true, | ||
reflectionOptions: DEFAULT_REFLECTION_OPTIONS, | ||
bodyParser: JSON, | ||
autoGenerateErrorId: false, | ||
}; | ||
export const STORAGE_KEY = 'mionkit:client'; | ||
//# sourceMappingURL=constants.js.map |
{ | ||
"name": "@mionkit/client", | ||
"version": "0.4.2", | ||
"version": "0.6.0", | ||
"description": "Browser client for mion Apps.", | ||
@@ -20,5 +20,5 @@ "keywords": [ | ||
"license": "MIT", | ||
"main": "./.dist/cjs/index.js", | ||
"module": "./.dist/esm/index.js", | ||
"types": "./.dist/cjs/index.d.ts", | ||
"main": ".dist/cjs/index.js", | ||
"module": ".dist/esm/index.js", | ||
"types": ".dist/types/index.d.ts", | ||
"exports": { | ||
@@ -45,9 +45,10 @@ ".": { | ||
"test": "jest", | ||
"dev": "rimraf .dist && tsc --build tsconfig.build.json --watch", | ||
"dev": "rimraf .dist && tsc --build tsconfig.json --watch", | ||
"dev:test": "jest --watch", | ||
"lint": "eslint src --ext .ts", | ||
"format": "prettier --write src/**/*.ts", | ||
"build:csj": "rimraf .dist/csj && tsc --project tsconfig.build.json", | ||
"build:esm": "rimraf .dist/esm && tsc --project tsconfig.build-esm.json", | ||
"build": "npm run build:csj && npm run build:esm", | ||
"build:cjs": "tsc --project tsconfig.build.json", | ||
"build:esm": "tsc --project tsconfig.build.json --module ES2020 --outDir .dist/esm", | ||
"build:types": "tsc --project tsconfig.build.json --outDir .dist/types --emitDeclarationOnly --declaration true --declarationMap true", | ||
"build": "npm run build:cjs && npm run build:esm && npm run build:types", | ||
"auto-readme": "embedme **/*.md && prettier --write **/*.md", | ||
@@ -61,8 +62,12 @@ "clean": "rimraf .dist & rimraf .coverage" | ||
"@deepkit/type": "^1.0.1-alpha.97", | ||
"@mionkit/router": "^0.4.2" | ||
"@mionkit/core": "^0.6.0", | ||
"@mionkit/reflection": "^0.6.0" | ||
}, | ||
"gitHead": "b5ad878a3ef17318fe4a23f44453cbf0c10bb2f5", | ||
"devDependencies": { | ||
"@serverless/event-mocks": "^1.1.1" | ||
}, | ||
"gitHead": "e93a50bbd278753fccb43ce99cdd9cf5536552f6" | ||
"@mionkit/http": "^0.6.0", | ||
"@mionkit/router": "^0.6.0", | ||
"dom-storage": "^2.1.0", | ||
"jest-environment-jsdom": "^29.6.2" | ||
} | ||
} |
152
README.md
@@ -9,3 +9,3 @@ <p align="center"> | ||
<p align="center"> | ||
<strong>Browser client for mion Apps. | ||
<strong>Fully typed client for mion Apis | ||
</strong> | ||
@@ -20,6 +20,152 @@ </p> | ||
Browser client for mion Apis | ||
Modern client for mion Apis: | ||
## Work in progress 🛠️ | ||
- Strongly typed apis with autocompletion ans static type checking. | ||
- Fully typed list of remote methods with it's parameters and return values. | ||
- Automattic Validation and Serialization out of the box. | ||
- Local Validation (no need to make a server request to validate parameters) | ||
- Prefill request data to persist across multiple calls. | ||
- No compilation needed | ||
## Setting up the server | ||
To be able to use the client the server must register a couple of routes required by the client to request remote methods metadata for validation and serialization. This routes are part of the `@mionkit/commons` package. | ||
It is also required to export the **type** of the registered routes in the server. | ||
```ts | ||
// examples/server.routes.ts | ||
import {RpcError} from '@mionkit/core'; | ||
import {Routes, registerRoutes} from '@mionkit/router'; | ||
import {clientRoutes} from '@mionkit/common'; | ||
import {Logger} from 'Logger'; | ||
export type User = {id: string; name: string; surname: string}; | ||
export type Order = {id: string; date: Date; userId: string; totalUSD: number}; | ||
const routes = { | ||
auth: { | ||
headerName: 'authorization', | ||
headerHook: (ctx, token: string): void | RpcError => { | ||
if (!token) return new RpcError({statusCode: 401, message: 'Not Authorized', name: ' Not Authorized'}); | ||
}, | ||
}, | ||
users: { | ||
getById: (ctx, id: string): User | RpcError => ({id, name: 'John', surname: 'Smith'}), | ||
delete: (ctx, id: string): string | RpcError => id, | ||
create: (ctx, user: Omit<User, 'id'>): User | RpcError => ({id: 'USER-123', ...user}), | ||
}, | ||
orders: { | ||
getById: (ctx, id: string): Order | RpcError => ({id, date: new Date(), userId: 'USER-123', totalUSD: 120}), | ||
delete: (ctx, id: string): string | RpcError => id, | ||
create: (ctx, order: Omit<Order, 'id'>): Order | RpcError => ({id: 'ORDER-123', ...order}), | ||
}, | ||
utils: { | ||
sum: (ctx, a: number, b: number): number => a + b, | ||
sayHello: (ctx, user: User): string => `Hello ${user.name} ${user.surname}`, | ||
}, | ||
log: { | ||
forceRunOnError: true, | ||
hook: (ctx): any => { | ||
Logger.log(ctx.path, ctx.request.headers, ctx.request.body); | ||
}, | ||
}, | ||
} satisfies Routes; | ||
// init server or serverless router | ||
// initHttpRouter(...); | ||
// initAwsLambdaRouter(...); | ||
// register routes and exporting the type of the Api to be used by client | ||
const myApi = registerRoutes(routes); | ||
export type MyApi = typeof myApi; | ||
// register routes required by client, (these routes serve metadata, for validation and serialization) | ||
registerRoutes(clientRoutes); | ||
``` | ||
## Using the client | ||
To use the client we just need to import the **type** of the registered routes, and initialize the client. | ||
The `methods` object returned when initializing the client is a fully typed `RemoteApi`` object that contains all the remote methods with parameter types and return values. | ||
```ts | ||
// examples/client.ts | ||
import {initClient} from '@mionkit/client'; | ||
// importing type only from server | ||
import type {MyApi} from './server.routes'; | ||
import {ParamsValidationResponse} from '@mionkit/reflection'; | ||
const port = 8076; | ||
const baseURL = `http://localhost:${port}`; | ||
const {methods, client} = initClient<MyApi>({baseURL}); | ||
// prefills the token for any future requests, value is stored in localStorage | ||
await methods.auth('myToken-XYZ').prefill(); | ||
// calls sayHello route in the server | ||
const sayHello = await methods.utils.sayHello({id: '123', name: 'John', surname: 'Doe'}).call(); | ||
console.log(sayHello); // Hello John Doe | ||
// calls sumTwo route in the server | ||
const sumTwoResp = await methods.utils.sum(5, 2).call(); | ||
console.log(sumTwoResp); // 7 | ||
// validate parameters locally without calling the server | ||
const validationResp: ParamsValidationResponse = await methods.utils | ||
.sayHello({id: '123', name: 'John', surname: 'Doe'}) | ||
.validate(); | ||
console.log(validationResp); // {hasErrors: false, totalErrors: 0, errors: []} | ||
``` | ||
#### Fully Typed Client | ||
![autocomplete](./assets/autocomplete.gif) | ||
## Handling Errors | ||
When a remote route call fails, it always throws a `RpcError` this can be the error from the route or any other error thrown from the route's hooks. | ||
All the `methods` operations: `call`, `validate`, `prefill`, `removePrefill` are async and throw a `RpcError` if something fails including validation and serialization. | ||
As catch blocks are always of type `any`, the Type guard `isRpcError` can be used to check the correct type of the error. | ||
```ts | ||
// examples/handling-errors.ts | ||
import {initClient} from '@mionkit/client'; | ||
// importing type only from server | ||
import type {MyApi} from './server.routes'; | ||
import {isRpcError, RpcError} from '@mionkit/core'; | ||
const port = 8076; | ||
const baseURL = `http://localhost:${port}`; | ||
const {methods, client} = initClient<MyApi>({baseURL}); | ||
try { | ||
// calls sayHello route in the server | ||
const sayHello = await methods.utils.sayHello({id: '123', name: 'John', surname: 'Doe'}).call(); | ||
console.log(sayHello); // Hello John Doe | ||
} catch (error: RpcError | any) { | ||
// in this case the request has failed because the authorization hook is missing | ||
console.log(error); // {statusCode: 400, name: 'Validation Error', message: `Invalid params for Route or Hook 'auth'.`} | ||
if (isRpcError(error)) { | ||
// ... handle the error as required | ||
} | ||
} | ||
try { | ||
// Validation throws an error when validation fails | ||
const sayHello = await methods.utils.sayHello(null as any).validate(); | ||
console.log(sayHello); // Hello John Doe | ||
} catch (error: RpcError | any) { | ||
console.log(error); // { statusCode: 400, name: 'Validation Error', message: `Invalid params ...`, errorData : {...}} | ||
} | ||
``` | ||
_[MIT](../../LICENSE) LICENSE_ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
310494
48
1361
170
3
4
5
+ Added@mionkit/core@^0.6.0
+ Added@mionkit/reflection@^0.6.0
+ Added@mionkit/core@0.6.0(transitive)
+ Added@mionkit/reflection@0.6.0(transitive)
+ Added@mionkit/runtype@0.6.0(transitive)
- Removed@mionkit/router@^0.4.2
- Removed@mionkit/router@0.4.2(transitive)
- Removed@mionkit/runtype@0.4.2(transitive)