vovk 3.1.0 → 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.
@@ -13,15 +13,12 @@ export function compose(...args) {
13
13
  }
14
14
  // Detect class: native ES class constructors' toString() starts with "class"
15
15
  if (last.toString().startsWith('class ')) {
16
- // Apply class decorators immediately in reverse order (bottom-up, matching stacked decorator semantics)
17
- let result = last;
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
18
  for (let i = decoratorFns.length - 1; i >= 0; i--) {
19
- const transformed = decoratorFns[i](result);
20
- if (transformed !== undefined) {
21
- result = transformed;
22
- }
19
+ decoratorFns[i](last);
23
20
  }
24
- return result;
21
+ return last;
25
22
  }
26
23
  // Method-level compose: store decorator appliers for deferred execution by initSegment
27
24
  const handler = last;
@@ -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
  }
@@ -5,41 +5,37 @@ export const initSegment = (options) => {
5
5
  const segmentName = trimPath(options.segmentName ?? '');
6
6
  options.segmentName = segmentName;
7
7
  const controllerEntries = Object.entries(options.controllers ?? {});
8
- // Phase 1: Apply compose metadata for all 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
+ });
9
14
  for (const [rpcModuleName, controller] of controllerEntries) {
10
15
  controller._segmentName = segmentName;
11
16
  controller._rpcModuleName = rpcModuleName;
12
17
  controller._onError = options?.onError;
13
18
  controller._onSuccess = options?.onSuccess;
14
19
  controller._onBefore = options?.onBefore;
15
- // Apply compose() metadata: call decorator appliers for each composed handler
20
+ // Apply deferred decorate() decorator appliers in reverse order (bottom-up, matching stacked decorator semantics)
16
21
  for (const key of Object.getOwnPropertyNames(controller)) {
17
- const method = controller[key];
18
- if (typeof method === 'function' && method._composeMetadata) {
19
- const metadata = method._composeMetadata;
20
- if (metadata.decoratorAppliers) {
21
- // Apply in reverse order to match decorator semantics (bottom-up)
22
- for (let i = metadata.decoratorAppliers.length - 1; i >= 0; i--) {
23
- // Call decorator function with (controller, propertyKey) simulating experimental decorator context
24
- metadata.decoratorAppliers[i](controller, key);
25
- }
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);
26
27
  }
27
28
  }
28
29
  }
29
- }
30
- // Phase 2: Re-clone metadata for controllers that extend another registered controller.
31
- // This is needed because cloneControllerMetadata() may run before compose metadata is applied
32
- // (e.g., when using class-level compose with a parent that uses method-level compose).
33
- const controllerSet = new Set(controllerEntries.map(([, c]) => c));
34
- for (const [, controller] of controllerEntries) {
30
+ // Re-clone metadata if this controller extends another registered controller
31
+ // (cloneControllerMetadata() runs at class-definition time, before decorate() metadata is applied)
35
32
  const parent = Object.getPrototypeOf(controller);
36
- if (parent && controllerSet.has(parent) && parent._handlers) {
33
+ if (controllerSet.has(parent) && parent._handlers) {
37
34
  controller._handlers = { ...parent._handlers, ...controller._handlers };
38
35
  controller._handlersMetadata = { ...parent._handlersMetadata, ...controller._handlersMetadata };
39
- Object.values(vovkApp.routes).forEach((methods) => {
40
- const parentMethods = methods.get(parent) ?? {};
41
- methods.set(controller, { ...parentMethods, ...methods.get(controller) });
42
- });
36
+ for (const methods of Object.values(vovkApp.routes)) {
37
+ methods.set(controller, { ...(methods.get(parent) ?? {}), ...methods.get(controller) });
38
+ }
43
39
  }
44
40
  }
45
41
  async function GET_DEV(req, data) {
@@ -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,7 +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 { compose } from './core/compose.js';
8
+ export { decorate } from './core/decorate.js';
9
9
  export { progressive } from './client/progressive.js';
10
10
  export { fetcher, createFetcher } from './client/fetcher.js';
11
11
  export { initSegment } from './core/initSegment.js';
package/dist/index.js CHANGED
@@ -6,7 +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 { compose } from './core/compose.js';
9
+ export { decorate } from './core/decorate.js';
10
10
  // client
11
11
  export { progressive } from './client/progressive.js';
12
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.1.0",
10
+ "version": "3.1.1",
11
11
  "bin": {
12
12
  "vovk-cli-npx": "./bin/index.mjs"
13
13
  },