mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
core&ui: drop graphql apis
This commit is contained in:
1 parent
0f7961d675
commit
6e16d03f67
52 files changed
+485
-583
No files matched your search
@@ -4,19 +4,19 @@
|
||||
"license": "MIT",
|
||||
"main": "base.mjs",
|
||||
"dependencies": {
|
||||
"@eslint-react/eslint-plugin": "^1.48.4",
|
||||
"@eslint-react/eslint-plugin": "^1.48.5",
|
||||
"@eslint/compat": "^1.2.8",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@stylistic/eslint-plugin": "^4.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
||||
"@typescript-eslint/parser": "^8.30.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-de-morgan": "^1.2.1",
|
||||
"eslint-plugin-github": "^6.0.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^16.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { Context, Service } from 'cordis';
|
||||
import Schema from 'schemastery';
|
||||
import { param } from './decorators';
|
||||
import { BadRequestError, MethodNotAllowedError, NotFoundError } from './error';
|
||||
import { } from './interface';
|
||||
import { Handler } from './server';
|
||||
import { Types } from './validator';
|
||||
|
||||
const BINARY = Symbol.for('hydro.api.response.binary');
|
||||
const REDIRECT = Symbol.for('hydro.api.response.redirect');
|
||||
|
||||
export type ApiCall<Arg, Res> = {
|
||||
readonly type: 'Query' | 'Mutation';
|
||||
readonly input: Schema<Arg>;
|
||||
readonly func: (context: any, args: Arg) => Promise<Res> | Res;
|
||||
readonly hooks: ApiCall<Arg, void>[];
|
||||
};
|
||||
|
||||
export const _get = (type: 'Query' | 'Mutation') => <Arg, Res>(
|
||||
schema: Schema<Arg>,
|
||||
func: (context: Handler, args: Arg) => Promise<Res> | Res,
|
||||
hooks: ApiCall<any, void>[] = [],
|
||||
): ApiCall<Arg, Res> => ({ input: schema, func, hooks, type } as const); // eslint-disable-line
|
||||
|
||||
export const Query = _get('Query');
|
||||
export const Mutation = _get('Mutation');
|
||||
|
||||
export class BinaryResponse {
|
||||
[BINARY]: true;
|
||||
constructor(public readonly data: Buffer, public filename: string) { }
|
||||
static check(value: any): value is BinaryResponse {
|
||||
return value && typeof value === 'object' && BINARY in value && value[BINARY] === true;
|
||||
}
|
||||
}
|
||||
|
||||
export class RedirectResponse {
|
||||
[REDIRECT]: true;
|
||||
constructor(public readonly url: string) { }
|
||||
static check(value: any): value is RedirectResponse {
|
||||
return value && typeof value === 'object' && REDIRECT in value && value[REDIRECT] === true;
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated TODO */
|
||||
export const NOP = Query(Schema.any(), () => { });
|
||||
|
||||
export const APIS = {
|
||||
'query.batch': Query(Schema.array(Schema.object({ op: Schema.string(), args: Schema.any() })), () => ({})),
|
||||
'mutation.batch': Mutation(Schema.array(Schema.object({ op: Schema.string(), args: Schema.any() })), () => ({})),
|
||||
} as const;
|
||||
export interface Apis {
|
||||
builtin: {
|
||||
'query.batch': ApiCall<{ op: string, args: any }[], { [key: string]: any }>;
|
||||
'mutation.batch': ApiCall<{ op: string, args: any }[], { [key: string]: any }>;
|
||||
}
|
||||
}
|
||||
export type FlattenedApis = Apis[keyof Apis];
|
||||
|
||||
// Thanks to @ForkKILLET for the projection function
|
||||
type ProjectionSchemaId = 1;
|
||||
type MKeyOf<T> = T extends any ? keyof T : never;
|
||||
type MId<T> = { [K in MKeyOf<T>]: T[K] } & {};
|
||||
type ProjectionSchema<T> = T extends Array<infer U>
|
||||
? ProjectionSchema<U>
|
||||
: | { [K in keyof T]?: ProjectionSchemaId | ProjectionSchema<T[K]> }
|
||||
| Record<keyof any, ProjectionSchemaId | object>;
|
||||
type AsKeys<T> = T extends Array<infer U extends string> ? Record<U, 1> : T;
|
||||
type Projection<T, S> = S extends ProjectionSchemaId
|
||||
? T : T extends Array<infer U> ? Array<MId<Projection<U, S>>> : {
|
||||
[K in keyof T & keyof S]: K extends keyof AsKeys<S> ? Projection<T[K], AsKeys<S>[K]> : never
|
||||
};
|
||||
|
||||
export const projection = <T, S extends ProjectionSchema<T>>(input: T, schema: S): Projection<T, S> => {
|
||||
if (typeof input !== 'object' || input === null) throw new Error('Input must be an object.');
|
||||
type R = Projection<T, S>;
|
||||
if (Array.isArray(schema)) schema = Object.fromEntries(schema.map((s) => [s, 1])) as S;
|
||||
if (Array.isArray(input)) {
|
||||
return input.map((item) => projection(item, schema)) as R;
|
||||
}
|
||||
const result = {} as R;
|
||||
for (const key of Reflect.ownKeys(input)) {
|
||||
const schemaIt = schema[key];
|
||||
if (!schemaIt) continue;
|
||||
if (schemaIt === 1) result[key] = input[key];
|
||||
else result[key] = projection(input[key], schemaIt);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export interface ApiExecutionContext {
|
||||
}
|
||||
|
||||
export class ApiService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'api');
|
||||
}
|
||||
|
||||
provide(calls: Partial<FlattenedApis>) {
|
||||
this.ctx.effect(() => {
|
||||
for (const key in calls) {
|
||||
APIS[key] = calls[key];
|
||||
}
|
||||
return () => {
|
||||
for (const key in calls) {
|
||||
delete APIS[key];
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async execute(context: ApiExecutionContext, callOrName: ApiCall<any, any> | string, rawArgs: any, emitHook?: any, project?: any) {
|
||||
const call = typeof callOrName === 'string' ? APIS[callOrName] : callOrName;
|
||||
if (!call) throw new NotFoundError(callOrName);
|
||||
const { input, func, hooks } = call;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
for (const hook of hooks) await this.execute(context, hook, rawArgs);
|
||||
|
||||
let args: any;
|
||||
try {
|
||||
args = input ? input(rawArgs as any) : rawArgs;
|
||||
} catch (e) {
|
||||
throw new BadRequestError(e.message);
|
||||
}
|
||||
if (typeof callOrName === 'string') {
|
||||
await emitHook?.('api/before', args);
|
||||
await emitHook?.(`api/before/${callOrName}`, args);
|
||||
}
|
||||
const result = await func(context, args as any);
|
||||
return project ? projection(result, project) : result;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
api: ApiService;
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiHandler<C extends Context> extends Handler<C> {
|
||||
@param('op', Types.String)
|
||||
async all({ }, op: string) {
|
||||
if (!['get', 'post'].includes(this.request.method.toLowerCase())) {
|
||||
throw new MethodNotAllowedError(this.request.method);
|
||||
}
|
||||
if (!APIS[op]) throw new BadRequestError('Invalid operation');
|
||||
if (APIS[op].type === 'Mutation' && this.request.method.toLowerCase() === 'get') {
|
||||
throw new BadRequestError('Mutation operation cannot be called with GET method');
|
||||
}
|
||||
// @ts-ignore
|
||||
await this.ctx.parallel('handler/api/before', this);
|
||||
// @ts-ignore
|
||||
await this.ctx.parallel(`handler/api/before/${op}`, this);
|
||||
const result = await this.ctx.api.execute(
|
||||
this, op, { domainId: this.args.domainId, ...(this.args.args || {}) },
|
||||
(m, args) => (this.ctx.parallel as any)(m, args), this.args.projection,
|
||||
);
|
||||
if (BinaryResponse.check(result)) {
|
||||
this.binary(result.data, result.filename);
|
||||
} else if (RedirectResponse.check(result)) {
|
||||
this.response.redirect = result.url;
|
||||
} else {
|
||||
this.response.body = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function applyApiHandler(ctx: Context, name: string, path: string) {
|
||||
ctx.plugin(ApiService);
|
||||
ctx.inject(['server', 'api'], ({ Route }) => {
|
||||
Route(name, path, ApiHandler);
|
||||
});
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from './validator';
|
||||
export * from './error';
|
||||
export * from './router';
|
||||
export * from './interface';
|
||||
export * from './api';
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Context } from 'cordis';
|
||||
import type { ApiHandler } from './api';
|
||||
import type { ConnectionHandler, Handler, NotFoundHandler } from './server';
|
||||
|
||||
export interface KnownHandlers {
|
||||
@@ -21,16 +23,20 @@ export type HookType = 'before-prepare' | 'before' | 'before-operation' | 'after
|
||||
export type Methods = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||
export type HookWithMethod = Exclude<HookType, 'before-operation'>;
|
||||
|
||||
export interface ServerEvents<C> extends KnownHandlerEvents, HandlerEvents<C> {
|
||||
export interface ServerEvents<C extends Context> extends KnownHandlerEvents, HandlerEvents<C> {
|
||||
'handler/create': (thisArg: Handler<C> | ConnectionHandler<C>, type: 'ws' | 'http') => VoidReturn
|
||||
'handler/create/http': (thisArg: Handler<C>) => VoidReturn
|
||||
'handler/create/ws': (thisArg: ConnectionHandler<C>) => VoidReturn
|
||||
'handler/init': (thisArg: Handler<C>) => VoidReturn
|
||||
'handler/error': (thisArg: Handler<C> | ConnectionHandler<C>, e: Error) => VoidReturn
|
||||
'handler/api/before': (thisArg: ApiHandler<C>) => VoidReturn
|
||||
'api/before': (args: any) => VoidReturn
|
||||
[k: `handler/${HookType}/${string}`]: (thisArg: any) => VoidReturn
|
||||
[k: `handler/${HookWithMethod}/${string}/${Methods}`]: (thisArg: any) => VoidReturn
|
||||
[k: `handler/register/${string}`]: (HandlerClass: new (...args: any[]) => any) => VoidReturn
|
||||
[k: `handler/error/${string}`]: (thisArg: Handler<C>, e: Error) => VoidReturn
|
||||
[k: `handler/api/before/${string}`]: (thisArg: ApiHandler<C>) => VoidReturn
|
||||
[k: `api/before/${string}`]: (args: any) => VoidReturn
|
||||
}
|
||||
|
||||
declare module 'cordis' {
|
||||
|
||||
@@ -502,16 +502,18 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
||||
await (this.ctx.parallel as any)('handler/create/http', h);
|
||||
|
||||
if (checker) checker.call(h);
|
||||
if (method === 'post') {
|
||||
if (operation) {
|
||||
if (typeof h[`post${operation}`] !== 'function') {
|
||||
throw new InvalidOperationError(operation);
|
||||
if (typeof h.all !== 'function') {
|
||||
if (method === 'post') {
|
||||
if (operation) {
|
||||
if (typeof h[`post${operation}`] !== 'function') {
|
||||
throw new InvalidOperationError(operation);
|
||||
}
|
||||
} else if (typeof h.post !== 'function') {
|
||||
throw new MethodNotAllowedError(method);
|
||||
}
|
||||
} else if (typeof h.post !== 'function') {
|
||||
} else if (typeof h[method] !== 'function') {
|
||||
throw new MethodNotAllowedError(method);
|
||||
}
|
||||
} else if (typeof h[method] !== 'function' && typeof h.all !== 'function') {
|
||||
throw new MethodNotAllowedError(method);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'node:test';
|
||||
import { projection } from '../api';
|
||||
|
||||
const input = {
|
||||
foo: 1,
|
||||
bar: 'string',
|
||||
arr: [
|
||||
{ sub: 1, another: 'key' },
|
||||
{ sub: 2 },
|
||||
],
|
||||
obj: {
|
||||
sub: 3,
|
||||
},
|
||||
};
|
||||
|
||||
describe('projection', () => {
|
||||
it('pick', () => {
|
||||
expect(projection(input, { foo: 1, bar: 1 })).to.deep.equal({ foo: 1, bar: 'string' });
|
||||
});
|
||||
it('array', () => {
|
||||
expect(projection(input, { arr: { sub: 1 } })).to.deep.equal({ arr: [{ sub: 1 }, { sub: 2 }] });
|
||||
});
|
||||
it('non-exist', () => {
|
||||
expect(projection(input, { 'non-exist': 1 })).to.deep.equal({});
|
||||
});
|
||||
it('partial', () => {
|
||||
expect(projection(input, { arr: { sub: 1, another: 1 } })).to.deep.equal({ arr: [{ sub: 1, another: 'key' }, { sub: 2 }] });
|
||||
});
|
||||
it('object', () => {
|
||||
expect(projection(input, { obj: { sub: 1 } })).to.deep.equal({ obj: { sub: 3 } });
|
||||
});
|
||||
});
|
||||
describe('safety', () => {
|
||||
it('prototype', () => {
|
||||
// eslint-disable-next-line
|
||||
expect(projection({}, { __proto__: 1 })).to.deep.equal({});
|
||||
expect(projection({}, { prototype: 1 })).to.deep.equal({});
|
||||
});
|
||||
it('circular', () => {
|
||||
const circular = {};
|
||||
// @ts-ignore
|
||||
circular.circular = circular;
|
||||
expect(projection(circular, { circular: 1 })).to.deep.equal({ circular });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user