vovk 3.0.4 → 3.1.0
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/dist/core/compose.d.ts +38 -0
- package/dist/core/compose.js +34 -0
- package/dist/core/initSegment.js +32 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -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,34 @@
|
|
|
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 immediately in reverse order (bottom-up, matching stacked decorator semantics)
|
|
17
|
+
let result = last;
|
|
18
|
+
for (let i = decoratorFns.length - 1; i >= 0; i--) {
|
|
19
|
+
const transformed = decoratorFns[i](result);
|
|
20
|
+
if (transformed !== undefined) {
|
|
21
|
+
result = transformed;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
// Method-level compose: store decorator appliers for deferred execution by initSegment
|
|
27
|
+
const handler = last;
|
|
28
|
+
handler._composeMetadata = handler._composeMetadata ?? {};
|
|
29
|
+
handler._composeMetadata.decoratorAppliers = handler._composeMetadata.decoratorAppliers ?? [];
|
|
30
|
+
for (const decoratorFn of decoratorFns) {
|
|
31
|
+
handler._composeMetadata.decoratorAppliers.push(decoratorFn);
|
|
32
|
+
}
|
|
33
|
+
return handler;
|
|
34
|
+
}
|
package/dist/core/initSegment.js
CHANGED
|
@@ -4,12 +4,43 @@ import { getSchema } from './getSchema.js';
|
|
|
4
4
|
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
|
|
9
|
+
for (const [rpcModuleName, controller] of controllerEntries) {
|
|
8
10
|
controller._segmentName = segmentName;
|
|
9
11
|
controller._rpcModuleName = rpcModuleName;
|
|
10
12
|
controller._onError = options?.onError;
|
|
11
13
|
controller._onSuccess = options?.onSuccess;
|
|
12
14
|
controller._onBefore = options?.onBefore;
|
|
15
|
+
// Apply compose() metadata: call decorator appliers for each composed handler
|
|
16
|
+
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
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
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) {
|
|
35
|
+
const parent = Object.getPrototypeOf(controller);
|
|
36
|
+
if (parent && controllerSet.has(parent) && parent._handlers) {
|
|
37
|
+
controller._handlers = { ...parent._handlers, ...controller._handlers };
|
|
38
|
+
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
|
+
});
|
|
43
|
+
}
|
|
13
44
|
}
|
|
14
45
|
async function GET_DEV(req, data) {
|
|
15
46
|
const params = await data.params;
|
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 { compose } from './core/compose.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 { compose } from './core/compose.js';
|
|
9
10
|
// client
|
|
10
11
|
export { progressive } from './client/progressive.js';
|
|
11
12
|
export { fetcher, createFetcher } from './client/fetcher.js';
|