@kibinrpc/server 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @kibinrpc/server
2
+
3
+ Server router for [kibinrpc](../../README.md) — register actions, mount a single handler.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install @kibinrpc/server
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { ServerAction, defineActions, createRouter, KibinError } from '@kibinrpc/server'
15
+
16
+ class UserActions {
17
+ @ServerAction()
18
+ async getUser(id: string) {
19
+ const user = await db.users.find(id)
20
+ if (!user) throw new KibinError('NOT_FOUND', 'User not found')
21
+ return user
22
+ }
23
+ }
24
+
25
+ const postActions = defineActions({
26
+ async listPosts() {
27
+ return db.posts.findAll()
28
+ },
29
+ })
30
+
31
+ export const router = createRouter({
32
+ user: new UserActions(),
33
+ post: postActions,
34
+ })
35
+
36
+ export type AppRouter = typeof router
37
+ ```
38
+
39
+ Mount on any framework that supports the Web `Request`/`Response` API:
40
+
41
+ ```ts
42
+ // Hono
43
+ app.post('/api/rpc', (c) => router.handler(c.req.raw))
44
+
45
+ // Next.js App Router
46
+ export const POST = router.handler
47
+ ```
48
+
49
+ The handler automatically detects single vs. batched requests — no second endpoint needed.
50
+
51
+ ## Registering actions
52
+
53
+ Only explicitly registered functions are callable. Everything else is rejected with `METHOD_NOT_FOUND`.
54
+
55
+ ### Class decorator
56
+
57
+ ```ts
58
+ import { ServerAction } from '@kibinrpc/server'
59
+
60
+ class UserActions {
61
+ @ServerAction()
62
+ async listUsers() { ... }
63
+
64
+ // Not callable from the client — not decorated
65
+ private helper() { ... }
66
+ }
67
+ ```
68
+
69
+ ### Functional
70
+
71
+ ```ts
72
+ import { defineActions, serverAction } from '@kibinrpc/server'
73
+
74
+ // Register a whole namespace at once
75
+ const postActions = defineActions({
76
+ async listPosts() { ... },
77
+ async createPost(data: NewPost) { ... },
78
+ })
79
+
80
+ // Or mark individual functions
81
+ const deletePost = serverAction(async (id: string) => { ... })
82
+ ```
83
+
84
+ ## Interceptors
85
+
86
+ Interceptors run for every call — including each item inside a batched request.
87
+
88
+ ```ts
89
+ const router = createRouter({ user, post }, {
90
+ beforeAction({ namespace, method, args, request }) {
91
+ const token = request.headers.get('Authorization')
92
+ if (!token) throw new KibinError('UNAUTHORIZED', 'Missing token')
93
+ },
94
+
95
+ afterAction({ namespace, method, result }) {
96
+ return result // optionally transform the result
97
+ },
98
+
99
+ onError({ namespace, method, error }) {
100
+ console.error(`[RPC] ${namespace}.${method} failed`, error)
101
+ },
102
+ })
103
+ ```
104
+
105
+ ## Error handling
106
+
107
+ Throw `KibinError` to send a structured error to the client:
108
+
109
+ ```ts
110
+ import { KibinError } from '@kibinrpc/server'
111
+
112
+ throw new KibinError('NOT_FOUND', 'User not found')
113
+ throw new KibinError('UNAUTHORIZED', 'Invalid token')
114
+ throw new KibinError('BAD_REQUEST', 'Invalid input')
115
+ ```
116
+
117
+ Any other thrown error becomes `{ code: 'INTERNAL_ERROR' }` — the original message is not leaked.
118
+
119
+ ## HTTP status codes
120
+
121
+ | Error code | HTTP status |
122
+ |---|---|
123
+ | `NOT_FOUND`, `METHOD_NOT_FOUND` | 404 |
124
+ | `BAD_REQUEST` | 400 |
125
+ | everything else | 500 |
126
+
127
+ For batched requests, each item carries its own `status` field. The overall HTTP status is `200` if all items succeed, `207 Multi-Status` for partial failures.
128
+
129
+ ## API
130
+
131
+ ### `createRouter(services, interceptors?)`
132
+
133
+ ```ts
134
+ const router = createRouter(
135
+ { user: new UserActions(), post: postActions },
136
+ { beforeAction, afterAction, onError },
137
+ )
138
+
139
+ router.handler // (request: Request) => Promise<Response>
140
+ router.services // the original services object
141
+ ```
142
+
143
+ ### `KibinError`
144
+
145
+ ```ts
146
+ new KibinError(code: string, message: string)
147
+ ```
148
+
149
+ ### Exported types
150
+
151
+ ```ts
152
+ import type {
153
+ RouterInterceptors,
154
+ ActionCtx,
155
+ AfterActionCtx,
156
+ ActionErrorCtx,
157
+ RpcRequest,
158
+ RpcResponse,
159
+ RpcBatchItemResponse,
160
+ } from '@kibinrpc/server'
161
+ ```
package/dist/index.d.mts CHANGED
@@ -12,13 +12,6 @@ declare class KibinError extends Error {
12
12
  declare function serverAction<T extends (...args: never[]) => unknown>(fn: T): T;
13
13
  declare function defineActions<T extends Record<string, (...args: never[]) => unknown>>(actions: T): T;
14
14
  //#endregion
15
- //#region src/router.d.ts
16
- type Services = Record<string, object>;
17
- declare function createRouter<T extends Services>(services: T): {
18
- services: T;
19
- handler: (request: Request) => Promise<Response>;
20
- };
21
- //#endregion
22
15
  //#region src/types.d.ts
23
16
  interface RpcRequest {
24
17
  namespace: string;
@@ -32,6 +25,33 @@ interface RpcResponse<T = unknown> {
32
25
  message: string;
33
26
  };
34
27
  }
28
+ interface RpcBatchItemResponse extends RpcResponse {
29
+ status: number;
30
+ }
31
+ interface ActionCtx {
32
+ namespace: string;
33
+ method: string;
34
+ args: unknown[];
35
+ request: Request;
36
+ }
37
+ interface AfterActionCtx extends ActionCtx {
38
+ result: unknown;
39
+ }
40
+ interface ActionErrorCtx extends ActionCtx {
41
+ error: unknown;
42
+ }
43
+ interface RouterInterceptors {
44
+ beforeAction?: (ctx: ActionCtx) => void | Promise<void>;
45
+ afterAction?: (ctx: AfterActionCtx) => unknown | Promise<unknown>;
46
+ onError?: (ctx: ActionErrorCtx) => void | Promise<void>;
47
+ }
48
+ //#endregion
49
+ //#region src/router.d.ts
50
+ type Services = Record<string, object>;
51
+ declare function createRouter<T extends Services>(services: T, interceptors?: RouterInterceptors): {
52
+ services: T;
53
+ handler: (request: Request) => Promise<Response>;
54
+ };
35
55
  //#endregion
36
- export { KibinError, type RpcRequest, type RpcResponse, ServerAction, createRouter, defineActions, serverAction };
56
+ export { type ActionCtx, type ActionErrorCtx, type AfterActionCtx, KibinError, type RouterInterceptors, type RpcBatchItemResponse, type RpcRequest, type RpcResponse, ServerAction, createRouter, defineActions, serverAction };
37
57
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/decorator.ts","../src/errors.ts","../src/fn.ts","../src/router.ts","../src/types.ts"],"mappings":";iBAEgB,YAAA,CAAA,0CAEd,MAAA,GAAS,IAAA,EAAM,IAAA,KAAS,IAAA,EAAM,IAAA,KAAS,MAAA,EACvC,OAAA,EAAS,2BAAA,CAA4B,IAAA,GAAO,IAAA,EAAM,IAAA,KAAS,IAAA,EAAM,IAAA,KAAS,MAAA,OAAO,IAAA,EADlE,IAAA,KAAI,IAAA,EAAW,IAAA,KAAS,MAAA;;;cCJ5B,UAAA,SAAmB,KAAK;EAAA,SAC3B,IAAA;EAAA,SACA,UAAA;cAEG,IAAA,UAAc,OAAA,UAAiB,UAAA;AAAA;;;iBCF5B,YAAA,eAA2B,IAAA,sBAAA,CAA2B,EAAA,EAAI,CAAA,GAAI,CAAC;AAAA,iBAK/D,aAAA,WAAwB,MAAA,aAAmB,IAAA,uBAAA,CAC1D,OAAA,EAAS,CAAA,GACP,CAAA;;;KCLE,QAAA,GAAW,MAAM;AAAA,iBASN,YAAA,WAAuB,QAAA,CAAA,CAAU,QAAA,EAAU,CAAA;;qBAC1B,OAAA,KAAU,OAAA,CAAQ,QAAA;AAAA;;;UCdlC,UAAA;EAChB,SAAA;EACA,MAAA;EACA,IAAA;AAAA;AAAA,UAGgB,WAAA;EAChB,IAAA,GAAO,CAAC;EACR,KAAA;IAAU,IAAA;IAAc,OAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/decorator.ts","../src/errors.ts","../src/fn.ts","../src/types.ts","../src/router.ts"],"mappings":";iBAEgB,YAAA,CAAA,0CAEd,MAAA,GAAS,IAAA,EAAM,IAAA,KAAS,IAAA,EAAM,IAAA,KAAS,MAAA,EACvC,OAAA,EAAS,2BAAA,CAA4B,IAAA,GAAO,IAAA,EAAM,IAAA,KAAS,IAAA,EAAM,IAAA,KAAS,MAAA,OAAO,IAAA,EADlE,IAAA,KAAI,IAAA,EAAW,IAAA,KAAS,MAAA;;;cCJ5B,UAAA,SAAmB,KAAK;EAAA,SAC3B,IAAA;EAAA,SACA,UAAA;cAEG,IAAA,UAAc,OAAA,UAAiB,UAAA;AAAA;;;iBCF5B,YAAA,eAA2B,IAAA,sBAAA,CAA2B,EAAA,EAAI,CAAA,GAAI,CAAC;AAAA,iBAK/D,aAAA,WAAwB,MAAA,aAAmB,IAAA,uBAAA,CAC1D,OAAA,EAAS,CAAA,GACP,CAAA;;;UCTc,UAAA;EAChB,SAAA;EACA,MAAA;EACA,IAAA;AAAA;AAAA,UAGgB,WAAA;EAChB,IAAA,GAAO,CAAC;EACR,KAAA;IAAU,IAAA;IAAc,OAAA;EAAA;AAAA;AAAA,UAGR,oBAAA,SAA6B,WAAW;EACxD,MAAM;AAAA;AAAA,UAGU,SAAA;EAChB,SAAA;EACA,MAAA;EACA,IAAA;EACA,OAAA,EAAS,OAAO;AAAA;AAAA,UAGA,cAAA,SAAuB,SAAS;EAChD,MAAM;AAAA;AAAA,UAGU,cAAA,SAAuB,SAAS;EAChD,KAAK;AAAA;AAAA,UAGW,kBAAA;EAChB,YAAA,IAAgB,GAAA,EAAK,SAAA,YAAqB,OAAA;EAC1C,WAAA,IAAe,GAAA,EAAK,cAAA,eAA6B,OAAA;EACjD,OAAA,IAAW,GAAA,EAAK,cAAA,YAA0B,OAAA;AAAA;;;KC7BtC,QAAA,GAAW,MAAM;AAAA,iBAwEN,YAAA,WAAuB,QAAA,CAAA,CAAU,QAAA,EAAU,CAAA,EAAG,YAAA,GAAe,kBAAA;;qBAC5C,OAAA,KAAU,OAAA,CAAQ,QAAA;AAAA"}
package/dist/index.mjs CHANGED
@@ -48,7 +48,62 @@ function jsonResponse(body, status) {
48
48
  headers: { "Content-Type": "application/json" }
49
49
  });
50
50
  }
51
- function createRouter(services) {
51
+ function statusFromResponse(result) {
52
+ if (!result.error) return 200;
53
+ if (result.error.code === "NOT_FOUND" || result.error.code === "METHOD_NOT_FOUND") return 404;
54
+ if (result.error.code === "BAD_REQUEST") return 400;
55
+ return 500;
56
+ }
57
+ async function executeRpcCall(body, request, services, interceptors) {
58
+ const { namespace, method, args } = body;
59
+ const service = services[namespace];
60
+ if (!service) return { error: {
61
+ code: "NOT_FOUND",
62
+ message: `Namespace "${namespace}" not found`
63
+ } };
64
+ const fn = service[method];
65
+ if (!(getRegisteredActions(service).has(method) || isBrandedAction(fn))) return { error: {
66
+ code: "METHOD_NOT_FOUND",
67
+ message: `"${method}" is not a registered server action`
68
+ } };
69
+ if (typeof fn !== "function") return { error: {
70
+ code: "METHOD_NOT_FOUND",
71
+ message: `Method "${method}" not found`
72
+ } };
73
+ const ctx = {
74
+ namespace,
75
+ method,
76
+ args,
77
+ request
78
+ };
79
+ try {
80
+ if (interceptors?.beforeAction) await interceptors.beforeAction(ctx);
81
+ const result = await fn.apply(service, args);
82
+ let finalResult = result;
83
+ if (interceptors?.afterAction) {
84
+ const transformed = await interceptors.afterAction({
85
+ ...ctx,
86
+ result
87
+ });
88
+ if (transformed !== void 0) finalResult = transformed;
89
+ }
90
+ return { data: finalResult };
91
+ } catch (error) {
92
+ if (interceptors?.onError) await interceptors.onError({
93
+ ...ctx,
94
+ error
95
+ });
96
+ if (error instanceof KibinError) return { error: {
97
+ code: error.code,
98
+ message: error.message
99
+ } };
100
+ return { error: {
101
+ code: "INTERNAL_ERROR",
102
+ message: "Internal server error"
103
+ } };
104
+ }
105
+ }
106
+ function createRouter(services, interceptors) {
52
107
  async function handler(request) {
53
108
  let body;
54
109
  try {
@@ -59,33 +114,15 @@ function createRouter(services) {
59
114
  message: "Invalid JSON body"
60
115
  } }, 400);
61
116
  }
62
- const { namespace, method, args } = body;
63
- const service = services[namespace];
64
- if (!service) return jsonResponse({ error: {
65
- code: "NOT_FOUND",
66
- message: `Namespace "${namespace}" not found`
67
- } }, 404);
68
- const fn = service[method];
69
- if (!(getRegisteredActions(service).has(method) || isBrandedAction(fn))) return jsonResponse({ error: {
70
- code: "METHOD_NOT_FOUND",
71
- message: `"${method}" is not a registered server action`
72
- } }, 404);
73
- if (typeof fn !== "function") return jsonResponse({ error: {
74
- code: "METHOD_NOT_FOUND",
75
- message: `Method "${method}" not found`
76
- } }, 404);
77
- try {
78
- return jsonResponse({ data: await fn.apply(service, args) }, 200);
79
- } catch (error) {
80
- if (error instanceof KibinError) return jsonResponse({ error: {
81
- code: error.code,
82
- message: error.message
83
- } }, error.statusCode);
84
- return jsonResponse({ error: {
85
- code: "INTERNAL_ERROR",
86
- message: "Internal server error"
87
- } }, 500);
117
+ if (Array.isArray(body)) {
118
+ const responses = (await Promise.all(body.map((b) => executeRpcCall(b, request, services, interceptors)))).map((r) => ({
119
+ ...r,
120
+ status: statusFromResponse(r)
121
+ }));
122
+ return jsonResponse(responses, responses.every((r) => r.status === 200) ? 200 : 207);
88
123
  }
124
+ const result = await executeRpcCall(body, request, services, interceptors);
125
+ return jsonResponse(result, statusFromResponse(result));
89
126
  }
90
127
  return {
91
128
  services,
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/registry.ts","../src/decorator.ts","../src/errors.ts","../src/fn.ts","../src/router.ts"],"sourcesContent":["export const SERVER_ACTIONS_KEY = Symbol('serverActions');\nexport const FUNCTION_ACTION_KEY = Symbol('functionAction');\n\nexport function getRegisteredActions(instance: object): Set<string> {\n\treturn (Reflect.get(instance, SERVER_ACTIONS_KEY) as Set<string> | undefined) ?? new Set();\n}\n\nexport function isBrandedAction(fn: unknown): boolean {\n\treturn typeof fn === 'function' && Reflect.get(fn, FUNCTION_ACTION_KEY) === true;\n}\n","import { SERVER_ACTIONS_KEY } from './registry.js';\n\nexport function ServerAction() {\n\treturn <This, Args extends unknown[], Return>(\n\t\ttarget: (this: This, ...args: Args) => Return,\n\t\tcontext: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,\n\t) => {\n\t\tcontext.addInitializer(function (this: This) {\n\t\t\tif (!Reflect.has(this as object, SERVER_ACTIONS_KEY)) {\n\t\t\t\tReflect.set(this as object, SERVER_ACTIONS_KEY, new Set<string>());\n\t\t\t}\n\t\t\t(Reflect.get(this as object, SERVER_ACTIONS_KEY) as Set<string>).add(String(context.name));\n\t\t});\n\t\treturn target;\n\t};\n}\n","export class KibinError extends Error {\n\treadonly code: string;\n\treadonly statusCode: number;\n\n\tconstructor(code: string, message: string, statusCode = 400) {\n\t\tsuper(message);\n\t\tthis.name = 'KibinError';\n\t\tthis.code = code;\n\t\tthis.statusCode = statusCode;\n\t}\n}\n","import { FUNCTION_ACTION_KEY } from './registry.js';\n\nexport function serverAction<T extends (...args: never[]) => unknown>(fn: T): T {\n\tReflect.set(fn, FUNCTION_ACTION_KEY, true);\n\treturn fn;\n}\n\nexport function defineActions<T extends Record<string, (...args: never[]) => unknown>>(\n\tactions: T,\n): T {\n\tfor (const key of Object.keys(actions)) {\n\t\tserverAction(actions[key]);\n\t}\n\treturn actions;\n}\n","import { KibinError } from './errors.js';\nimport { getRegisteredActions, isBrandedAction } from './registry.js';\nimport type { RpcRequest, RpcResponse } from './types.js';\n\ntype Services = Record<string, object>;\n\nfunction jsonResponse(body: RpcResponse, status: number): Response {\n\treturn new Response(JSON.stringify(body), {\n\t\tstatus,\n\t\theaders: { 'Content-Type': 'application/json' },\n\t});\n}\n\nexport function createRouter<T extends Services>(services: T) {\n\tasync function handler(request: Request): Promise<Response> {\n\t\tlet body: RpcRequest;\n\t\ttry {\n\t\t\tbody = (await request.json()) as RpcRequest;\n\t\t} catch {\n\t\t\treturn jsonResponse({ error: { code: 'BAD_REQUEST', message: 'Invalid JSON body' } }, 400);\n\t\t}\n\n\t\tconst { namespace, method, args } = body;\n\n\t\tconst service = services[namespace];\n\t\tif (!service) {\n\t\t\treturn jsonResponse(\n\t\t\t\t{ error: { code: 'NOT_FOUND', message: `Namespace \"${namespace}\" not found` } },\n\t\t\t\t404,\n\t\t\t);\n\t\t}\n\n\t\tconst fn = (service as Record<string, unknown>)[method];\n\t\tconst isAllowed = getRegisteredActions(service).has(method) || isBrandedAction(fn);\n\t\tif (!isAllowed) {\n\t\t\treturn jsonResponse(\n\t\t\t\t{\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: 'METHOD_NOT_FOUND',\n\t\t\t\t\t\tmessage: `\"${method}\" is not a registered server action`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t404,\n\t\t\t);\n\t\t}\n\n\t\tif (typeof fn !== 'function') {\n\t\t\treturn jsonResponse(\n\t\t\t\t{ error: { code: 'METHOD_NOT_FOUND', message: `Method \"${method}\" not found` } },\n\t\t\t\t404,\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await fn.apply(service, args);\n\t\t\treturn jsonResponse({ data: result }, 200);\n\t\t} catch (error) {\n\t\t\tif (error instanceof KibinError) {\n\t\t\t\treturn jsonResponse(\n\t\t\t\t\t{ error: { code: error.code, message: error.message } },\n\t\t\t\t\terror.statusCode,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn jsonResponse(\n\t\t\t\t{ error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } },\n\t\t\t\t500,\n\t\t\t);\n\t\t}\n\t}\n\n\treturn { services, handler };\n}\n"],"mappings":";AAAA,MAAa,qBAAqB,OAAO,eAAe;AACxD,MAAa,sBAAsB,OAAO,gBAAgB;AAE1D,SAAgB,qBAAqB,UAA+B;CACnE,OAAQ,QAAQ,IAAI,UAAU,kBAAkB,qBAAiC,IAAI,IAAI;AAC1F;AAEA,SAAgB,gBAAgB,IAAsB;CACrD,OAAO,OAAO,OAAO,cAAc,QAAQ,IAAI,IAAI,mBAAmB,MAAM;AAC7E;;;ACPA,SAAgB,eAAe;CAC9B,QACC,QACA,YACI;EACJ,QAAQ,eAAe,WAAsB;GAC5C,IAAI,CAAC,QAAQ,IAAI,MAAgB,kBAAkB,GAClD,QAAQ,IAAI,MAAgB,oCAAoB,IAAI,IAAY,CAAC;GAElE,QAAS,IAAI,MAAgB,kBAAkB,EAAkB,IAAI,OAAO,QAAQ,IAAI,CAAC;EAC1F,CAAC;EACD,OAAO;CACR;AACD;;;ACfA,IAAa,aAAb,cAAgC,MAAM;CACrC;CACA;CAEA,YAAY,MAAc,SAAiB,aAAa,KAAK;EAC5D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,aAAa;CACnB;AACD;;;ACRA,SAAgB,aAAsD,IAAU;CAC/E,QAAQ,IAAI,IAAI,qBAAqB,IAAI;CACzC,OAAO;AACR;AAEA,SAAgB,cACf,SACI;CACJ,KAAK,MAAM,OAAO,OAAO,KAAK,OAAO,GACpC,aAAa,QAAQ,IAAI;CAE1B,OAAO;AACR;;;ACRA,SAAS,aAAa,MAAmB,QAA0B;CAClE,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EACzC;EACA,SAAS,EAAE,gBAAgB,mBAAmB;CAC/C,CAAC;AACF;AAEA,SAAgB,aAAiC,UAAa;CAC7D,eAAe,QAAQ,SAAqC;EAC3D,IAAI;EACJ,IAAI;GACH,OAAQ,MAAM,QAAQ,KAAK;EAC5B,QAAQ;GACP,OAAO,aAAa,EAAE,OAAO;IAAE,MAAM;IAAe,SAAS;GAAoB,EAAE,GAAG,GAAG;EAC1F;EAEA,MAAM,EAAE,WAAW,QAAQ,SAAS;EAEpC,MAAM,UAAU,SAAS;EACzB,IAAI,CAAC,SACJ,OAAO,aACN,EAAE,OAAO;GAAE,MAAM;GAAa,SAAS,cAAc,UAAU;EAAa,EAAE,GAC9E,GACD;EAGD,MAAM,KAAM,QAAoC;EAEhD,IAAI,EADc,qBAAqB,OAAO,EAAE,IAAI,MAAM,KAAK,gBAAgB,EAAE,IAEhF,OAAO,aACN,EACC,OAAO;GACN,MAAM;GACN,SAAS,IAAI,OAAO;EACrB,EACD,GACA,GACD;EAGD,IAAI,OAAO,OAAO,YACjB,OAAO,aACN,EAAE,OAAO;GAAE,MAAM;GAAoB,SAAS,WAAW,OAAO;EAAa,EAAE,GAC/E,GACD;EAGD,IAAI;GAEH,OAAO,aAAa,EAAE,MAAM,MADP,GAAG,MAAM,SAAS,IAAI,EACR,GAAG,GAAG;EAC1C,SAAS,OAAO;GACf,IAAI,iBAAiB,YACpB,OAAO,aACN,EAAE,OAAO;IAAE,MAAM,MAAM;IAAM,SAAS,MAAM;GAAQ,EAAE,GACtD,MAAM,UACP;GAED,OAAO,aACN,EAAE,OAAO;IAAE,MAAM;IAAkB,SAAS;GAAwB,EAAE,GACtE,GACD;EACD;CACD;CAEA,OAAO;EAAE;EAAU;CAAQ;AAC5B"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/registry.ts","../src/decorator.ts","../src/errors.ts","../src/fn.ts","../src/router.ts"],"sourcesContent":["export const SERVER_ACTIONS_KEY = Symbol('serverActions');\nexport const FUNCTION_ACTION_KEY = Symbol('functionAction');\n\nexport function getRegisteredActions(instance: object): Set<string> {\n\treturn (Reflect.get(instance, SERVER_ACTIONS_KEY) as Set<string> | undefined) ?? new Set();\n}\n\nexport function isBrandedAction(fn: unknown): boolean {\n\treturn typeof fn === 'function' && Reflect.get(fn, FUNCTION_ACTION_KEY) === true;\n}\n","import { SERVER_ACTIONS_KEY } from './registry.js';\n\nexport function ServerAction() {\n\treturn <This, Args extends unknown[], Return>(\n\t\ttarget: (this: This, ...args: Args) => Return,\n\t\tcontext: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,\n\t) => {\n\t\tcontext.addInitializer(function (this: This) {\n\t\t\tif (!Reflect.has(this as object, SERVER_ACTIONS_KEY)) {\n\t\t\t\tReflect.set(this as object, SERVER_ACTIONS_KEY, new Set<string>());\n\t\t\t}\n\t\t\t(Reflect.get(this as object, SERVER_ACTIONS_KEY) as Set<string>).add(String(context.name));\n\t\t});\n\t\treturn target;\n\t};\n}\n","export class KibinError extends Error {\n\treadonly code: string;\n\treadonly statusCode: number;\n\n\tconstructor(code: string, message: string, statusCode = 400) {\n\t\tsuper(message);\n\t\tthis.name = 'KibinError';\n\t\tthis.code = code;\n\t\tthis.statusCode = statusCode;\n\t}\n}\n","import { FUNCTION_ACTION_KEY } from './registry.js';\n\nexport function serverAction<T extends (...args: never[]) => unknown>(fn: T): T {\n\tReflect.set(fn, FUNCTION_ACTION_KEY, true);\n\treturn fn;\n}\n\nexport function defineActions<T extends Record<string, (...args: never[]) => unknown>>(\n\tactions: T,\n): T {\n\tfor (const key of Object.keys(actions)) {\n\t\tserverAction(actions[key]);\n\t}\n\treturn actions;\n}\n","import { KibinError } from './errors.js';\nimport { getRegisteredActions, isBrandedAction } from './registry.js';\nimport type { RouterInterceptors, RpcBatchItemResponse, RpcRequest, RpcResponse } from './types.js';\n\ntype Services = Record<string, object>;\n\nfunction jsonResponse(body: RpcResponse | RpcBatchItemResponse[], status: number): Response {\n\treturn new Response(JSON.stringify(body), {\n\t\tstatus,\n\t\theaders: { 'Content-Type': 'application/json' },\n\t});\n}\n\nfunction statusFromResponse(result: RpcResponse): number {\n\tif (!result.error) return 200;\n\tif (result.error.code === 'NOT_FOUND' || result.error.code === 'METHOD_NOT_FOUND') return 404;\n\tif (result.error.code === 'BAD_REQUEST') return 400;\n\treturn 500;\n}\n\nasync function executeRpcCall(\n\tbody: RpcRequest,\n\trequest: Request,\n\tservices: Services,\n\tinterceptors: RouterInterceptors | undefined,\n): Promise<RpcResponse> {\n\tconst { namespace, method, args } = body;\n\n\tconst service = services[namespace];\n\tif (!service) {\n\t\treturn { error: { code: 'NOT_FOUND', message: `Namespace \"${namespace}\" not found` } };\n\t}\n\n\tconst fn = (service as Record<string, unknown>)[method];\n\tconst isAllowed = getRegisteredActions(service).has(method) || isBrandedAction(fn);\n\tif (!isAllowed) {\n\t\treturn {\n\t\t\terror: {\n\t\t\t\tcode: 'METHOD_NOT_FOUND',\n\t\t\t\tmessage: `\"${method}\" is not a registered server action`,\n\t\t\t},\n\t\t};\n\t}\n\n\tif (typeof fn !== 'function') {\n\t\treturn { error: { code: 'METHOD_NOT_FOUND', message: `Method \"${method}\" not found` } };\n\t}\n\n\tconst ctx = { namespace, method, args, request };\n\n\ttry {\n\t\tif (interceptors?.beforeAction) {\n\t\t\tawait interceptors.beforeAction(ctx);\n\t\t}\n\n\t\tconst result = await fn.apply(service, args);\n\n\t\tlet finalResult = result;\n\t\tif (interceptors?.afterAction) {\n\t\t\tconst transformed = await interceptors.afterAction({ ...ctx, result });\n\t\t\tif (transformed !== undefined) finalResult = transformed;\n\t\t}\n\n\t\treturn { data: finalResult };\n\t} catch (error) {\n\t\tif (interceptors?.onError) {\n\t\t\tawait interceptors.onError({ ...ctx, error });\n\t\t}\n\n\t\tif (error instanceof KibinError) {\n\t\t\treturn { error: { code: error.code, message: error.message } };\n\t\t}\n\t\treturn { error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } };\n\t}\n}\n\nexport function createRouter<T extends Services>(services: T, interceptors?: RouterInterceptors) {\n\tasync function handler(request: Request): Promise<Response> {\n\t\tlet body: RpcRequest | RpcRequest[];\n\t\ttry {\n\t\t\tbody = (await request.json()) as RpcRequest | RpcRequest[];\n\t\t} catch {\n\t\t\treturn jsonResponse({ error: { code: 'BAD_REQUEST', message: 'Invalid JSON body' } }, 400);\n\t\t}\n\n\t\tif (Array.isArray(body)) {\n\t\t\tconst results = await Promise.all(\n\t\t\t\tbody.map((b) => executeRpcCall(b, request, services, interceptors)),\n\t\t\t);\n\t\t\tconst responses: RpcBatchItemResponse[] = results.map((r) => ({\n\t\t\t\t...r,\n\t\t\t\tstatus: statusFromResponse(r),\n\t\t\t}));\n\t\t\tconst httpStatus = responses.every((r) => r.status === 200) ? 200 : 207;\n\t\t\treturn jsonResponse(responses, httpStatus);\n\t\t}\n\n\t\tconst result = await executeRpcCall(body, request, services, interceptors);\n\t\treturn jsonResponse(result, statusFromResponse(result));\n\t}\n\n\treturn { services, handler };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kibinrpc/server",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Devloper-friendly RPC server router with full type inference",
5
5
  "license": "MIT",
6
6
  "author": "ixexel661",