vovk 3.0.4 → 3.1.1

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.
@@ -0,0 +1,38 @@
1
+ import type { KnownAny } from '../types/utils.js';
2
+ /**
3
+ * Metadata stored on a handler by HTTP decorators and custom decorators when used outside decorator context (via compose).
4
+ */
5
+ export type ComposeMetadata = {
6
+ httpMethod?: string;
7
+ path?: string;
8
+ options?: KnownAny;
9
+ decoratorAppliers?: ((controller: KnownAny, propertyKey: string) => void)[];
10
+ };
11
+ /**
12
+ * Composes decorators and a handler/class into a single value.
13
+ *
14
+ * For method-level composition, decorators are stored and applied later by initSegment.
15
+ * For class-level composition, decorators like prefix() and cloneControllerMetadata()
16
+ * are applied immediately to the class in reverse order (matching stacked decorator semantics).
17
+ *
18
+ * @example Method-level
19
+ * ```ts
20
+ * static handleParams = compose(
21
+ * put('x/{foo}/{bar}/y'),
22
+ * authGuard(null),
23
+ * procedure({ params: z.object({ foo: z.string(), bar: z.string() }) })
24
+ * .handle(async (req) => req.vovk.params())
25
+ * );
26
+ * ```
27
+ *
28
+ * @example Class-level
29
+ * ```ts
30
+ * const MyController = compose(
31
+ * prefix('users'),
32
+ * cloneControllerMetadata(),
33
+ * class extends ParentController {}
34
+ * );
35
+ * export default MyController;
36
+ * ```
37
+ */
38
+ export declare function compose<T>(...args: [...unknown[], T]): T;
@@ -0,0 +1,31 @@
1
+ export function compose(...args) {
2
+ if (args.length === 0)
3
+ throw new Error('compose() requires at least one argument');
4
+ const last = args[args.length - 1];
5
+ const decoratorFns = args.slice(0, -1);
6
+ if (typeof last !== 'function') {
7
+ throw new Error('The last argument to compose() must be a function, handler, or class');
8
+ }
9
+ for (const decoratorFn of decoratorFns) {
10
+ if (typeof decoratorFn !== 'function') {
11
+ throw new Error('All arguments to compose() except the last must be decorator functions');
12
+ }
13
+ }
14
+ // Detect class: native ES class constructors' toString() starts with "class"
15
+ if (last.toString().startsWith('class ')) {
16
+ // Apply class decorators in reverse order (bottom-up, matching stacked decorator semantics).
17
+ // Decorators mutate and return the same class, so .name is preserved.
18
+ for (let i = decoratorFns.length - 1; i >= 0; i--) {
19
+ decoratorFns[i](last);
20
+ }
21
+ return last;
22
+ }
23
+ // Method-level compose: store decorator appliers for deferred execution by initSegment
24
+ const handler = last;
25
+ handler._composeMetadata = handler._composeMetadata ?? {};
26
+ handler._composeMetadata.decoratorAppliers = handler._composeMetadata.decoratorAppliers ?? [];
27
+ for (const decoratorFn of decoratorFns) {
28
+ handler._composeMetadata.decoratorAppliers.push(decoratorFn);
29
+ }
30
+ return handler;
31
+ }
@@ -13,7 +13,7 @@ export function controllersToStaticParams(c, slug = 'vovk') {
13
13
  { [slug]: ['_schema_'] },
14
14
  ...Object.values(controllers).flatMap((controller) => {
15
15
  const handlers = controller._handlers;
16
- const splitPrefix = controller._prefix?.split('/') ?? [];
16
+ const splitPrefix = controller.prefix?.split('/') ?? [];
17
17
  return Object.entries(handlers ?? {}).flatMap(([name, handler]) => {
18
18
  const staticParams = controller._handlersMetadata?.[name]?.staticParams;
19
19
  if (staticParams?.length) {
@@ -0,0 +1,43 @@
1
+ import type { KnownAny } from '../types/utils.js';
2
+ /**
3
+ * Metadata stored on a handler by HTTP decorators and custom decorators when used outside decorator context (via decorate).
4
+ */
5
+ export type DecorateMetadata = {
6
+ httpMethod?: string;
7
+ path?: string;
8
+ options?: KnownAny;
9
+ decoratorAppliers?: ((controller: KnownAny, propertyKey: string) => void)[];
10
+ };
11
+ /**
12
+ * Applies decorators to a handler without using decorator syntax.
13
+ * Returns an object with `.handle()` to register the handler function.
14
+ *
15
+ * When the last argument is a procedure result (has `.handle`), its `.handle()` is proxied.
16
+ * Otherwise, `.handle()` wraps a plain handler directly.
17
+ *
18
+ * @example With procedure
19
+ * ```ts
20
+ * static handleParams = decorate(
21
+ * put('x/{foo}/{bar}/y'),
22
+ * authGuard(null),
23
+ * procedure({ params: z.object({ foo: z.string(), bar: z.string() }) })
24
+ * ).handle(async (req) => req.vovk.params());
25
+ * ```
26
+ *
27
+ * @example Without procedure
28
+ * ```ts
29
+ * static getMethod = decorate(
30
+ * get(),
31
+ * ).handle(async () => {
32
+ * return { method: 'get' };
33
+ * });
34
+ * ```
35
+ */
36
+ export declare function decorate<H extends {
37
+ handle: (...args: KnownAny[]) => KnownAny;
38
+ }>(...args: [...unknown[], H]): {
39
+ handle: H['handle'];
40
+ };
41
+ export declare function decorate(...args: unknown[]): {
42
+ handle: <T extends (...args: KnownAny[]) => KnownAny>(fn: T) => T;
43
+ };
@@ -0,0 +1,24 @@
1
+ export function decorate(...args) {
2
+ if (args.length === 0)
3
+ throw new Error('decorate() requires at least one argument');
4
+ const last = args[args.length - 1];
5
+ const hasProcedure = typeof last === 'function' && 'handle' in last && typeof last.handle === 'function';
6
+ const procedureResult = hasProcedure ? last : null;
7
+ const decoratorFns = (hasProcedure ? args.slice(0, -1) : args);
8
+ for (const decoratorFn of decoratorFns) {
9
+ if (typeof decoratorFn !== 'function') {
10
+ throw new Error('All decorator arguments to decorate() must be functions');
11
+ }
12
+ }
13
+ return {
14
+ handle(fn) {
15
+ const handler = procedureResult ? procedureResult.handle(fn) : fn;
16
+ handler._decorateMetadata = handler._decorateMetadata ?? {};
17
+ handler._decorateMetadata.decoratorAppliers = handler._decorateMetadata.decoratorAppliers ?? [];
18
+ for (const decoratorFn of decoratorFns) {
19
+ handler._decorateMetadata.decoratorAppliers.push(decoratorFn);
20
+ }
21
+ return handler;
22
+ },
23
+ };
24
+ }
@@ -84,7 +84,7 @@ export const prefix = (givenPath = '') => {
84
84
  const path = trimPath(givenPath);
85
85
  return (givenTarget, _context) => {
86
86
  const controller = givenTarget;
87
- controller._prefix = path;
87
+ controller.prefix = path;
88
88
  return givenTarget;
89
89
  };
90
90
  };
@@ -8,7 +8,7 @@ export async function getControllerSchema(controller, rpcModuleName, exposeValid
8
8
  return {
9
9
  rpcModuleName,
10
10
  originalControllerName: controller.name,
11
- prefix: controller._prefix ?? '',
11
+ prefix: controller.prefix ?? '',
12
12
  handlers,
13
13
  };
14
14
  }
@@ -4,12 +4,39 @@ import { getSchema } from './getSchema.js';
4
4
  export const initSegment = (options) => {
5
5
  const segmentName = trimPath(options.segmentName ?? '');
6
6
  options.segmentName = segmentName;
7
- for (const [rpcModuleName, controller] of Object.entries(options.controllers ?? {})) {
7
+ const controllerEntries = Object.entries(options.controllers ?? {});
8
+ const controllerSet = new Set(controllerEntries.map(([, c]) => c));
9
+ // Sort so parent controllers are initialized before their children
10
+ controllerEntries.sort(([, a], [, b]) => {
11
+ return (Number(controllerSet.has(Object.getPrototypeOf(a))) -
12
+ Number(controllerSet.has(Object.getPrototypeOf(b))));
13
+ });
14
+ for (const [rpcModuleName, controller] of controllerEntries) {
8
15
  controller._segmentName = segmentName;
9
16
  controller._rpcModuleName = rpcModuleName;
10
17
  controller._onError = options?.onError;
11
18
  controller._onSuccess = options?.onSuccess;
12
19
  controller._onBefore = options?.onBefore;
20
+ // Apply deferred decorate() decorator appliers in reverse order (bottom-up, matching stacked decorator semantics)
21
+ for (const key of Object.getOwnPropertyNames(controller)) {
22
+ const appliers = controller[key]?._decorateMetadata
23
+ ?.decoratorAppliers;
24
+ if (appliers) {
25
+ for (let i = appliers.length - 1; i >= 0; i--) {
26
+ appliers[i](controller, key);
27
+ }
28
+ }
29
+ }
30
+ // Re-clone metadata if this controller extends another registered controller
31
+ // (cloneControllerMetadata() runs at class-definition time, before decorate() metadata is applied)
32
+ const parent = Object.getPrototypeOf(controller);
33
+ if (controllerSet.has(parent) && parent._handlers) {
34
+ controller._handlers = { ...parent._handlers, ...controller._handlers };
35
+ controller._handlersMetadata = { ...parent._handlersMetadata, ...controller._handlersMetadata };
36
+ for (const methods of Object.values(vovkApp.routes)) {
37
+ methods.set(controller, { ...(methods.get(parent) ?? {}), ...methods.get(controller) });
38
+ }
39
+ }
13
40
  }
14
41
  async function GET_DEV(req, data) {
15
42
  const params = await data.params;
@@ -194,7 +194,7 @@ class VovkApp {
194
194
  controllers.forEach((staticMethods, controller) => {
195
195
  if (segmentName !== controller._segmentName)
196
196
  return;
197
- const prefix = controller._prefix ?? '';
197
+ const prefix = controller.prefix ?? '';
198
198
  Object.entries(staticMethods ?? {}).forEach(([path, staticMethod]) => {
199
199
  const fullPath = [prefix, path].filter(Boolean).join('/');
200
200
  handlers[fullPath] = { staticMethod, controller };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { multitenant } from './core/multitenant.js';
5
5
  export { JSONLinesResponder } from './core/JSONLinesResponder.js';
6
6
  export { toDownloadResponse } from './core/toDownloadResponse.js';
7
7
  export { get, post, put, patch, del, head, options, prefix, cloneControllerMetadata } from './core/decorators.js';
8
+ export { decorate } from './core/decorate.js';
8
9
  export { progressive } from './client/progressive.js';
9
10
  export { fetcher, createFetcher } from './client/fetcher.js';
10
11
  export { initSegment } from './core/initSegment.js';
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { multitenant } from './core/multitenant.js';
6
6
  export { JSONLinesResponder } from './core/JSONLinesResponder.js';
7
7
  export { toDownloadResponse } from './core/toDownloadResponse.js';
8
8
  export { get, post, put, patch, del, head, options, prefix, cloneControllerMetadata } from './core/decorators.js';
9
+ export { decorate } from './core/decorate.js';
9
10
  // client
10
11
  export { progressive } from './client/progressive.js';
11
12
  export { fetcher, createFetcher } from './client/fetcher.js';
@@ -86,7 +86,7 @@ export type StreamAbortMessage = {
86
86
  export type VovkControllerInternal = {
87
87
  _segmentName: string;
88
88
  _rpcModuleName?: VovkControllerSchema['rpcModuleName'];
89
- _prefix?: VovkControllerSchema['prefix'];
89
+ prefix?: VovkControllerSchema['prefix'];
90
90
  _handlers: VovkControllerSchema['handlers'];
91
91
  _handlersMetadata?: Record<string, {
92
92
  staticParams?: Record<string, string>[];
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  ],
8
8
  "main": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
- "version": "3.0.4",
10
+ "version": "3.1.1",
11
11
  "bin": {
12
12
  "vovk-cli-npx": "./bin/index.mjs"
13
13
  },