vovk 3.0.0-draft.99 → 3.0.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.
Files changed (167) hide show
  1. package/README.md +22 -13
  2. package/bin/index.mjs +10 -0
  3. package/dist/client/createRPC.d.ts +13 -3
  4. package/dist/client/createRPC.js +112 -50
  5. package/dist/client/defaultHandler.d.ts +5 -1
  6. package/dist/client/defaultHandler.js +12 -9
  7. package/dist/client/defaultStreamHandler.d.ts +16 -4
  8. package/dist/client/defaultStreamHandler.js +259 -62
  9. package/dist/client/fetcher.d.ts +41 -3
  10. package/dist/client/fetcher.js +125 -60
  11. package/dist/client/progressive.d.ts +15 -0
  12. package/dist/client/progressive.js +56 -0
  13. package/dist/{utils → client}/serializeQuery.d.ts +2 -2
  14. package/dist/{utils → client}/serializeQuery.js +1 -4
  15. package/dist/core/HttpException.d.ts +16 -0
  16. package/dist/core/HttpException.js +26 -0
  17. package/dist/core/JSONLinesResponder.d.ts +42 -0
  18. package/dist/core/JSONLinesResponder.js +92 -0
  19. package/dist/core/controllersToStaticParams.d.ts +13 -0
  20. package/dist/core/controllersToStaticParams.js +36 -0
  21. package/dist/core/createDecorator.d.ts +12 -0
  22. package/dist/{createDecorator.js → core/createDecorator.js} +18 -12
  23. package/dist/core/decorators.d.ts +59 -0
  24. package/dist/core/decorators.js +132 -0
  25. package/dist/core/getSchema.d.ts +21 -0
  26. package/dist/core/getSchema.js +31 -0
  27. package/dist/core/initSegment.d.ts +33 -0
  28. package/dist/core/initSegment.js +35 -0
  29. package/dist/core/multitenant.d.ts +33 -0
  30. package/dist/core/multitenant.js +132 -0
  31. package/dist/core/resolveGeneratorConfigValues.d.ts +19 -0
  32. package/dist/core/resolveGeneratorConfigValues.js +59 -0
  33. package/dist/{utils → core}/setHandlerSchema.d.ts +2 -2
  34. package/dist/{utils → core}/setHandlerSchema.js +1 -4
  35. package/dist/core/toDownloadResponse.d.ts +11 -0
  36. package/dist/core/toDownloadResponse.js +25 -0
  37. package/dist/core/vovkApp.d.ts +36 -0
  38. package/dist/core/vovkApp.js +316 -0
  39. package/dist/index.d.ts +25 -59
  40. package/dist/index.js +23 -23
  41. package/dist/internal.d.ts +17 -0
  42. package/dist/internal.js +10 -0
  43. package/dist/openapi/error.d.ts +2 -0
  44. package/dist/openapi/error.js +97 -0
  45. package/dist/openapi/openAPIToVovkSchema/applyComponentsSchemas.d.ts +3 -0
  46. package/dist/openapi/openAPIToVovkSchema/applyComponentsSchemas.js +65 -0
  47. package/dist/openapi/openAPIToVovkSchema/index.d.ts +5 -0
  48. package/dist/openapi/openAPIToVovkSchema/index.js +153 -0
  49. package/dist/openapi/openAPIToVovkSchema/inlineRefs.d.ts +9 -0
  50. package/dist/openapi/openAPIToVovkSchema/inlineRefs.js +99 -0
  51. package/dist/openapi/operation.d.ts +10 -0
  52. package/dist/openapi/operation.js +19 -0
  53. package/dist/openapi/tool.d.ts +2 -0
  54. package/dist/openapi/tool.js +12 -0
  55. package/dist/openapi/vovkSchemaToOpenAPI.d.ts +21 -0
  56. package/dist/openapi/vovkSchemaToOpenAPI.js +250 -0
  57. package/dist/req/bufferBody.d.ts +1 -0
  58. package/dist/req/bufferBody.js +30 -0
  59. package/dist/req/parseBody.d.ts +4 -0
  60. package/dist/req/parseBody.js +49 -0
  61. package/dist/req/parseForm.d.ts +1 -0
  62. package/dist/req/parseForm.js +24 -0
  63. package/dist/{utils → req}/parseQuery.d.ts +1 -2
  64. package/dist/{utils → req}/parseQuery.js +2 -5
  65. package/dist/req/reqMeta.d.ts +2 -0
  66. package/dist/{utils → req}/reqMeta.js +1 -4
  67. package/dist/req/reqQuery.d.ts +2 -0
  68. package/dist/req/reqQuery.js +4 -0
  69. package/dist/req/validateContentType.d.ts +1 -0
  70. package/dist/req/validateContentType.js +32 -0
  71. package/dist/samples/createCodeSamples.d.ts +20 -0
  72. package/dist/samples/createCodeSamples.js +293 -0
  73. package/dist/samples/objectToCode.d.ts +8 -0
  74. package/dist/samples/objectToCode.js +38 -0
  75. package/dist/samples/schemaToCode.d.ts +11 -0
  76. package/dist/samples/schemaToCode.js +264 -0
  77. package/dist/samples/schemaToObject.d.ts +2 -0
  78. package/dist/samples/schemaToObject.js +164 -0
  79. package/dist/samples/schemaToTsType.d.ts +2 -0
  80. package/dist/samples/schemaToTsType.js +114 -0
  81. package/dist/tools/ToModelOutput.d.ts +8 -0
  82. package/dist/tools/ToModelOutput.js +10 -0
  83. package/dist/tools/createTool.d.ts +126 -0
  84. package/dist/tools/createTool.js +6 -0
  85. package/dist/tools/createToolFactory.d.ts +135 -0
  86. package/dist/tools/createToolFactory.js +61 -0
  87. package/dist/tools/deriveTools.d.ts +46 -0
  88. package/dist/tools/deriveTools.js +134 -0
  89. package/dist/tools/toModelOutputDefault.d.ts +7 -0
  90. package/dist/tools/toModelOutputDefault.js +7 -0
  91. package/dist/tools/toModelOutputMCP.d.ts +30 -0
  92. package/dist/tools/toModelOutputMCP.js +54 -0
  93. package/dist/tsconfig.tsbuildinfo +1 -0
  94. package/dist/types/client.d.ts +140 -0
  95. package/dist/types/client.js +1 -0
  96. package/dist/types/config.d.ts +151 -0
  97. package/dist/types/config.js +1 -0
  98. package/dist/types/core.d.ts +115 -0
  99. package/dist/types/core.js +1 -0
  100. package/dist/types/enums.d.ts +75 -0
  101. package/dist/{types.js → types/enums.js} +21 -9
  102. package/dist/types/inference.d.ts +117 -0
  103. package/dist/types/inference.js +1 -0
  104. package/dist/types/json-schema.d.ts +51 -0
  105. package/dist/types/json-schema.js +1 -0
  106. package/dist/types/operation.d.ts +5 -0
  107. package/dist/types/operation.js +1 -0
  108. package/dist/types/package.d.ts +544 -0
  109. package/dist/types/package.js +5 -0
  110. package/dist/types/request.d.ts +48 -0
  111. package/dist/types/request.js +1 -0
  112. package/dist/types/standard-schema.d.ts +117 -0
  113. package/dist/types/standard-schema.js +6 -0
  114. package/dist/types/tools.d.ts +43 -0
  115. package/dist/types/tools.js +1 -0
  116. package/dist/types/utils.d.ts +9 -0
  117. package/dist/types/utils.js +1 -0
  118. package/dist/types/validation.d.ts +48 -0
  119. package/dist/types/validation.js +1 -0
  120. package/dist/utils/camelCase.d.ts +6 -0
  121. package/dist/utils/camelCase.js +34 -0
  122. package/dist/utils/deepExtend.d.ts +53 -0
  123. package/dist/utils/deepExtend.js +128 -0
  124. package/dist/utils/fileNameToDisposition.d.ts +1 -0
  125. package/dist/utils/fileNameToDisposition.js +3 -0
  126. package/dist/utils/shim.d.ts +1 -0
  127. package/dist/utils/shim.js +1 -1
  128. package/dist/utils/toKebabCase.d.ts +1 -0
  129. package/dist/utils/toKebabCase.js +5 -0
  130. package/dist/utils/trimPath.d.ts +1 -0
  131. package/dist/utils/trimPath.js +1 -0
  132. package/dist/utils/upperFirst.d.ts +1 -0
  133. package/dist/utils/upperFirst.js +3 -0
  134. package/dist/validation/createStandardValidation.d.ts +268 -0
  135. package/dist/validation/createStandardValidation.js +45 -0
  136. package/dist/validation/createValidateOnClient.d.ts +14 -0
  137. package/dist/validation/createValidateOnClient.js +23 -0
  138. package/dist/validation/procedure.d.ts +261 -0
  139. package/dist/validation/procedure.js +8 -0
  140. package/dist/validation/withValidationLibrary.d.ts +119 -0
  141. package/dist/validation/withValidationLibrary.js +174 -0
  142. package/package.json +44 -10
  143. package/dist/HttpException.d.ts +0 -7
  144. package/dist/HttpException.js +0 -15
  145. package/dist/StreamJSONResponse.d.ts +0 -14
  146. package/dist/StreamJSONResponse.js +0 -57
  147. package/dist/VovkApp.d.ts +0 -29
  148. package/dist/VovkApp.js +0 -188
  149. package/dist/client/index.d.ts +0 -3
  150. package/dist/client/index.js +0 -7
  151. package/dist/client/types.d.ts +0 -104
  152. package/dist/client/types.js +0 -2
  153. package/dist/createDecorator.d.ts +0 -6
  154. package/dist/createVovkApp.d.ts +0 -62
  155. package/dist/createVovkApp.js +0 -118
  156. package/dist/types.d.ts +0 -220
  157. package/dist/utils/generateStaticAPI.d.ts +0 -4
  158. package/dist/utils/generateStaticAPI.js +0 -18
  159. package/dist/utils/getSchema.d.ts +0 -20
  160. package/dist/utils/getSchema.js +0 -33
  161. package/dist/utils/reqForm.d.ts +0 -2
  162. package/dist/utils/reqForm.js +0 -13
  163. package/dist/utils/reqMeta.d.ts +0 -2
  164. package/dist/utils/reqQuery.d.ts +0 -2
  165. package/dist/utils/reqQuery.js +0 -10
  166. package/dist/utils/withValidation.d.ts +0 -20
  167. package/dist/utils/withValidation.js +0 -72
@@ -0,0 +1,132 @@
1
+ // Get the reserved paths from the overrides configuration
2
+ const getReservedPaths = (overrides) => {
3
+ return Object.keys(overrides).filter((key) => !key.includes('[') && !key.includes(']')); // Filter out dynamic paths
4
+ };
5
+ /**
6
+ * Convert a pattern with [placeholders] to a regex pattern and extract placeholder names
7
+ */
8
+ const patternToRegex = (pattern) => {
9
+ const paramNames = [];
10
+ const regexPattern = pattern
11
+ .replace(/\[([^\]]+)\]/g, (_, name) => {
12
+ paramNames.push(name);
13
+ return '([^.]+)';
14
+ })
15
+ .replace(/\./g, '\\.'); // Escape dots in the pattern
16
+ return {
17
+ regex: new RegExp(`^${regexPattern}$`),
18
+ paramNames,
19
+ };
20
+ };
21
+ /**
22
+ * Multitenant function to handle subdomain and path-based routing overrides.
23
+ * @see https://vovk.dev/multitenant
24
+ */
25
+ export function multitenant(config) {
26
+ const { requestUrl, requestHost, targetHost, overrides } = config;
27
+ // Parse the URL
28
+ const urlObj = new URL(requestUrl);
29
+ const pathname = urlObj.pathname.slice(1); // Remove leading slash
30
+ // Skip processing for paths ending with "_schema_"
31
+ if (pathname.endsWith('_schema_')) {
32
+ return {
33
+ action: null,
34
+ destination: null,
35
+ message: 'Schema endpoint, bypassing overrides',
36
+ subdomains: null,
37
+ };
38
+ }
39
+ const pathSegments = pathname.split('/').filter(Boolean);
40
+ // Get reserved paths
41
+ const reservedPaths = getReservedPaths(overrides);
42
+ // Check if any path segment matches a reserved path (e.g., "admin")
43
+ for (let i = 0; i < pathSegments.length; i++) {
44
+ const segment = pathSegments[i];
45
+ if (reservedPaths.includes(segment)) {
46
+ // Create the destination URL with the reserved path as subdomain
47
+ const destinationHost = `${segment}.${targetHost}`;
48
+ // Keep path segments before the reserved path
49
+ const beforeSegments = pathSegments.slice(0, i);
50
+ // Keep path segments after the reserved path
51
+ const afterSegments = pathSegments.slice(i + 1);
52
+ // Construct the new path
53
+ const newPath = [...beforeSegments, ...afterSegments].join('/');
54
+ const destinationUrl = new URL(`${urlObj.protocol}//${destinationHost}`);
55
+ if (newPath) {
56
+ destinationUrl.pathname = `/${newPath}`;
57
+ }
58
+ // Keep any query parameters
59
+ destinationUrl.search = urlObj.search;
60
+ return {
61
+ action: 'redirect',
62
+ destination: destinationUrl.toString(),
63
+ message: `Redirecting to ${segment} subdomain`,
64
+ subdomains: null, // No wildcards used
65
+ };
66
+ }
67
+ }
68
+ // Process based on host and subdomains
69
+ for (const pattern in overrides) {
70
+ const fullPattern = `${pattern}.${targetHost}`;
71
+ const { regex, paramNames } = patternToRegex(fullPattern);
72
+ const match = requestHost.match(regex);
73
+ if (match) {
74
+ const overrideRules = overrides[pattern];
75
+ // Extract parameters from the match
76
+ const params = {};
77
+ if (match.length > 1) {
78
+ for (let i = 0; i < paramNames.length; i++) {
79
+ params[paramNames[i]] = match[i + 1];
80
+ }
81
+ }
82
+ // Find the appropriate rule based on the path
83
+ for (const rule of overrideRules) {
84
+ if (pathname === rule.from || pathname.startsWith(`${rule.from}/`)) {
85
+ // Replace path with the destination
86
+ let destination = pathname.replace(rule.from, rule.to);
87
+ // Replace any dynamic parameters in destination
88
+ if (Object.keys(params).length > 0) {
89
+ Object.entries(params).forEach(([key, value]) => {
90
+ destination = destination.replace(`[${key}]`, value);
91
+ });
92
+ }
93
+ // Only return non-null subdomains if we have wildcard parameters
94
+ const wildcardSubdomains = paramNames.length > 0 ? params : null;
95
+ return {
96
+ action: 'rewrite',
97
+ destination: `${urlObj.protocol}//${urlObj.host}/${destination}${urlObj.search}`,
98
+ message: `Rewriting to ${destination}`,
99
+ subdomains: wildcardSubdomains,
100
+ };
101
+ }
102
+ }
103
+ }
104
+ }
105
+ // Handle cases where a customer subdomain tries to access reserved paths
106
+ if (pathSegments.length > 0 && reservedPaths.includes(pathSegments[0])) {
107
+ const reservedPath = pathSegments[0];
108
+ const restPath = pathSegments.slice(1).join('/');
109
+ // Create the destination URL with the reserved path as subdomain
110
+ const destinationHost = `${reservedPath}.${targetHost}`;
111
+ const destinationUrl = new URL(`${urlObj.protocol}//${destinationHost}`);
112
+ // Only add remaining path segments if they exist
113
+ if (restPath) {
114
+ destinationUrl.pathname = `/${restPath}`;
115
+ }
116
+ // Keep any query parameters
117
+ destinationUrl.search = urlObj.search;
118
+ return {
119
+ action: 'redirect',
120
+ destination: destinationUrl.toString(),
121
+ message: `Redirecting to ${reservedPath} subdomain`,
122
+ subdomains: null, // No wildcards used for reserved paths
123
+ };
124
+ }
125
+ // Default case - pass through
126
+ return {
127
+ action: null,
128
+ destination: null,
129
+ message: 'No action',
130
+ subdomains: null, // No wildcards matched
131
+ };
132
+ }
@@ -0,0 +1,19 @@
1
+ import type { OpenAPIObject } from 'openapi3-ts/oas31';
2
+ import type { VovkConfig, VovkOutputConfig, VovkPackageJson, VovkReadmeConfig, VovkSamplesConfig } from '../types/config.js';
3
+ import type { PackageJson } from '../types/package.js';
4
+ export declare function resolveGeneratorConfigValues({ config, outputConfigs, forceOutputConfigs, segmentName, isBundle, projectPackageJson, }: {
5
+ config: VovkConfig | undefined;
6
+ outputConfigs: VovkOutputConfig[];
7
+ forceOutputConfigs?: VovkOutputConfig[];
8
+ segmentName: string | null;
9
+ isBundle: boolean;
10
+ projectPackageJson: PackageJson | undefined;
11
+ }): {
12
+ readme: VovkReadmeConfig;
13
+ openAPIObject: OpenAPIObject;
14
+ samples: VovkSamplesConfig;
15
+ origin: string;
16
+ package: VovkPackageJson;
17
+ imports: VovkOutputConfig['imports'];
18
+ reExports: VovkOutputConfig['reExports'];
19
+ };
@@ -0,0 +1,59 @@
1
+ import { deepExtend } from '../utils/deepExtend.js';
2
+ export function resolveGeneratorConfigValues({ config, outputConfigs, forceOutputConfigs, segmentName, isBundle, projectPackageJson, }) {
3
+ const packageJson = deepExtend(Object.fromEntries(Object.entries(projectPackageJson ?? {}).filter(([key]) => [
4
+ 'name',
5
+ 'version',
6
+ 'description',
7
+ 'license',
8
+ 'author',
9
+ 'contributors',
10
+ 'repository',
11
+ 'homepage',
12
+ 'bugs',
13
+ 'keywords',
14
+ ].includes(key))), config?.outputConfig?.package, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.package : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.package), {}), isBundle ? config?.bundle?.outputConfig?.package : undefined);
15
+ const openAPIObject = deepExtend({
16
+ openapi: '3.1.0',
17
+ info: {
18
+ title: packageJson.name,
19
+ version: packageJson.version,
20
+ description: packageJson.description,
21
+ },
22
+ }, config?.outputConfig?.openAPIObject, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.openAPIObject : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.openAPIObject), {}), isBundle ? config?.bundle?.outputConfig?.openAPIObject : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.openAPIObject), {}));
23
+ const samples = deepExtend({}, config?.outputConfig?.samples, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.samples : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.samples), {}), isBundle ? config?.bundle?.outputConfig?.samples : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.samples), {}));
24
+ const readme = deepExtend({}, config?.outputConfig?.readme, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.readme : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.readme), {}), isBundle ? config?.bundle?.outputConfig?.readme : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.readme), {}));
25
+ const origin = [
26
+ config?.outputConfig?.origin,
27
+ typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.origin : undefined,
28
+ ...(outputConfigs?.map((config) => config.origin) ?? []),
29
+ isBundle ? config?.bundle?.outputConfig?.origin : undefined,
30
+ ...(forceOutputConfigs?.map((config) => config.origin) ?? []),
31
+ ]
32
+ .filter(Boolean)
33
+ .at(-1)
34
+ // remove trailing slash if any
35
+ ?.replace(/\/$/, '') ?? '';
36
+ const imports = deepExtend({
37
+ fetcher: 'vovk/fetcher',
38
+ validateOnClient: null,
39
+ createRPC: 'vovk/createRPC',
40
+ }, config?.outputConfig?.imports, typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.imports : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.imports), {}), isBundle ? config?.bundle?.outputConfig?.imports : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.imports), {}));
41
+ const reExports = deepExtend(
42
+ // segmentName can be an empty string (for the root segment) and null (for composed clients)
43
+ // therefore, !segmentName indicates that this either a composed client or a root segment of a segmented client
44
+ {}, !segmentName && config?.outputConfig?.reExports,
45
+ // for segmented client, apply all reExports from all segments
46
+ typeof segmentName !== 'string' &&
47
+ Object.values(config?.outputConfig?.segments ?? {}).reduce((acc, segmentConfig) => deepExtend(acc, segmentConfig.reExports ?? {}), {}),
48
+ // for a specific segment, apply reExports from that segment
49
+ typeof segmentName === 'string' ? config?.outputConfig?.segments?.[segmentName]?.reExports : undefined, outputConfigs?.reduce((acc, config) => deepExtend(acc, config.reExports), {}), isBundle ? config?.bundle?.outputConfig?.reExports : undefined, forceOutputConfigs?.reduce((acc, config) => deepExtend(acc, config.reExports), {}));
50
+ return {
51
+ package: packageJson,
52
+ openAPIObject,
53
+ samples,
54
+ readme,
55
+ origin,
56
+ imports,
57
+ reExports,
58
+ };
59
+ }
@@ -1,4 +1,4 @@
1
- import type { KnownAny, VovkController, VovkHandlerSchema } from '../types';
2
- export declare function setHandlerSchema(h: ((...args: KnownAny[]) => KnownAny) & {
1
+ import type { VovkController, VovkHandlerSchema } from '../types/core.js';
2
+ export declare function setHandlerSchema(h: ((...args: unknown[]) => unknown) & {
3
3
  _getSchema?: (controller: VovkController) => Omit<VovkHandlerSchema, 'httpMethod' | 'path'>;
4
4
  }, schema: Omit<VovkHandlerSchema, 'httpMethod' | 'path'>): Promise<void>;
@@ -1,7 +1,4 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setHandlerSchema = setHandlerSchema;
4
- async function setHandlerSchema(h, schema) {
1
+ export async function setHandlerSchema(h, schema) {
5
2
  h._getSchema = (controller) => {
6
3
  if (!controller) {
7
4
  throw new Error('Error setting client validators. Controller not found. Did you forget to use an HTTP decorator?');
@@ -0,0 +1,11 @@
1
+ type BinaryData = Blob | File | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array> | string;
2
+ /**
3
+ * Creates a Response object for downloading binary data with appropriate headers.
4
+ * @see https://vovk.dev/response
5
+ */
6
+ export declare function toDownloadResponse(data: BinaryData, { filename, type, headers }?: {
7
+ filename?: string;
8
+ type?: string;
9
+ headers?: Record<string, string>;
10
+ }): Response;
11
+ export {};
@@ -0,0 +1,25 @@
1
+ import { fileNameToDisposition } from '../utils/fileNameToDisposition.js';
2
+ /**
3
+ * Creates a Response object for downloading binary data with appropriate headers.
4
+ * @see https://vovk.dev/response
5
+ */
6
+ export function toDownloadResponse(data, { filename, type, headers } = {}) {
7
+ const body = data instanceof Blob
8
+ ? data
9
+ : data instanceof ReadableStream
10
+ ? data
11
+ : new Blob([data], { type: type ?? 'application/octet-stream' });
12
+ const resolvedName = filename ?? (data instanceof File ? data.name : undefined);
13
+ const resolvedType = type ?? (body instanceof Blob ? body.type : undefined);
14
+ return new Response(body, {
15
+ headers: {
16
+ ...(resolvedType ? { 'Content-Type': resolvedType } : {}),
17
+ ...(resolvedName
18
+ ? {
19
+ 'Content-Disposition': fileNameToDisposition(resolvedName),
20
+ }
21
+ : {}),
22
+ ...headers,
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,36 @@
1
+ import { HttpMethod, HttpStatus } from '../types/enums.js';
2
+ import type { RouteHandler, VovkController, DecoratorOptions } from '../types/core.js';
3
+ declare class VovkApp {
4
+ #private;
5
+ private static getHeadersFromDecoratorOptions;
6
+ routes: Record<HttpMethod, Map<VovkController, Record<string, RouteHandler>>>;
7
+ GET: (req: Request, data: {
8
+ params: Promise<Record<string, string[]>>;
9
+ }, segmentName: string) => Promise<Response>;
10
+ POST: (req: Request, data: {
11
+ params: Promise<Record<string, string[]>>;
12
+ }, segmentName: string) => Promise<Response>;
13
+ PUT: (req: Request, data: {
14
+ params: Promise<Record<string, string[]>>;
15
+ }, segmentName: string) => Promise<Response>;
16
+ PATCH: (req: Request, data: {
17
+ params: Promise<Record<string, string[]>>;
18
+ }, segmentName: string) => Promise<Response>;
19
+ DELETE: (req: Request, data: {
20
+ params: Promise<Record<string, string[]>>;
21
+ }, segmentName: string) => Promise<Response>;
22
+ HEAD: (req: Request, data: {
23
+ params: Promise<Record<string, string[]>>;
24
+ }, segmentName: string) => Promise<Response>;
25
+ OPTIONS: (req: Request, data: {
26
+ params: Promise<Record<string, string[]>>;
27
+ }, segmentName: string) => Promise<Response>;
28
+ respond: ({ statusCode, responseBody, options, }: {
29
+ req: Request;
30
+ statusCode: HttpStatus;
31
+ responseBody: unknown;
32
+ options?: DecoratorOptions;
33
+ }) => Promise<Response>;
34
+ }
35
+ declare const vovkApp: VovkApp;
36
+ export { vovkApp };
@@ -0,0 +1,316 @@
1
+ var _a;
2
+ import { HttpException } from './HttpException.js';
3
+ import { JSONLinesResponder, Responder } from './JSONLinesResponder.js';
4
+ import { reqQuery } from '../req/reqQuery.js';
5
+ import { reqMeta } from '../req/reqMeta.js';
6
+ import { HttpMethod, HttpStatus } from '../types/enums.js';
7
+ import { parseBody } from '../req/parseBody.js';
8
+ class VovkApp {
9
+ static getHeadersFromDecoratorOptions(options) {
10
+ if (!options)
11
+ return {};
12
+ const corsHeaders = {
13
+ 'access-control-allow-origin': '*',
14
+ 'access-control-allow-methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD',
15
+ 'access-control-allow-headers': 'content-type, authorization',
16
+ };
17
+ const headers = {
18
+ ...(options.cors ? corsHeaders : {}),
19
+ ...(options.headers ?? {}),
20
+ };
21
+ return headers;
22
+ }
23
+ routes = {
24
+ GET: new Map(),
25
+ POST: new Map(),
26
+ PUT: new Map(),
27
+ PATCH: new Map(),
28
+ DELETE: new Map(),
29
+ HEAD: new Map(),
30
+ OPTIONS: new Map(),
31
+ };
32
+ GET = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.GET, req, params: await data.params, segmentName });
33
+ POST = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.POST, req, params: await data.params, segmentName });
34
+ PUT = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.PUT, req, params: await data.params, segmentName });
35
+ PATCH = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.PATCH, req, params: await data.params, segmentName });
36
+ DELETE = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.DELETE, req, params: await data.params, segmentName });
37
+ HEAD = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.HEAD, req, params: await data.params, segmentName });
38
+ OPTIONS = async (req, data, segmentName) => this.#callMethod({ httpMethod: HttpMethod.OPTIONS, req, params: await data.params, segmentName });
39
+ respond = async ({ statusCode, responseBody, options, }) => {
40
+ const response = new Response(JSON.stringify(responseBody), {
41
+ status: statusCode,
42
+ headers: {
43
+ 'content-type': 'application/json',
44
+ ..._a.getHeadersFromDecoratorOptions(options),
45
+ },
46
+ });
47
+ return response;
48
+ };
49
+ #respondWithError = ({ req, statusCode, message, options, cause, }) => {
50
+ return this.respond({
51
+ req,
52
+ statusCode,
53
+ responseBody: {
54
+ cause,
55
+ statusCode,
56
+ message,
57
+ isError: true,
58
+ },
59
+ options,
60
+ });
61
+ };
62
+ #routeRegexCache = new Map();
63
+ #routeSegmentsCache = new Map();
64
+ #routeParamPositionsCache = new Map();
65
+ #routeMatchCache = new Map();
66
+ #getHandler = ({ handlers, path, params, }) => {
67
+ let methodParams = {};
68
+ if (Object.keys(params).length === 0) {
69
+ return { handler: handlers[''], methodParams };
70
+ }
71
+ const pathStr = path.join('/');
72
+ // Fast path: Check if this exact path has been matched before
73
+ const cachedMatch = this.#routeMatchCache.get(pathStr);
74
+ if (cachedMatch) {
75
+ return {
76
+ handler: handlers[cachedMatch.route],
77
+ methodParams: cachedMatch.params,
78
+ };
79
+ }
80
+ // Check for direct static route match
81
+ let methodKey = handlers[pathStr] ? pathStr : null;
82
+ if (!methodKey) {
83
+ const methodKeys = [];
84
+ const pathLength = path.length;
85
+ // First pass: group routes by length for quick filtering
86
+ const routesByLength = new Map();
87
+ for (const p of Object.keys(handlers)) {
88
+ let routeSegments = this.#routeSegmentsCache.get(p);
89
+ if (!routeSegments) {
90
+ routeSegments = p.split('/');
91
+ this.#routeSegmentsCache.set(p, routeSegments);
92
+ // Pre-compute parameter positions for routes with parameters
93
+ if (p.includes('{')) {
94
+ const paramPositions = [];
95
+ for (let i = 0; i < routeSegments.length; i++) {
96
+ const segment = routeSegments[i];
97
+ if (segment.includes('{')) {
98
+ const paramMatch = segment.match(/\{(\w+)\}/);
99
+ if (paramMatch) {
100
+ paramPositions.push({ index: i, paramName: paramMatch[1] });
101
+ }
102
+ }
103
+ }
104
+ this.#routeParamPositionsCache.set(p, paramPositions);
105
+ }
106
+ }
107
+ const segmentLength = routeSegments.length;
108
+ if (segmentLength !== pathLength)
109
+ continue;
110
+ const lengthRoutes = routesByLength.get(segmentLength) || [];
111
+ lengthRoutes.push(p);
112
+ routesByLength.set(segmentLength, lengthRoutes);
113
+ }
114
+ // Only process routes with matching segment count
115
+ const candidateRoutes = routesByLength.get(pathLength) || [];
116
+ for (const p of candidateRoutes) {
117
+ const routeSegments = this.#routeSegmentsCache.get(p);
118
+ const params = {};
119
+ // Fast path for routes with parameters
120
+ const paramPositions = this.#routeParamPositionsCache.get(p);
121
+ if (paramPositions) {
122
+ let isMatch = true;
123
+ // First check all non-parameter segments for a quick fail
124
+ for (let i = 0; i < routeSegments.length; i++) {
125
+ const routeSegment = routeSegments[i];
126
+ if (!routeSegment.includes('{') && routeSegment !== path[i]) {
127
+ isMatch = false;
128
+ break;
129
+ }
130
+ }
131
+ if (!isMatch)
132
+ continue;
133
+ // Now process parameter segments
134
+ for (const { index, paramName } of paramPositions) {
135
+ const routeSegment = routeSegments[index];
136
+ const pathSegment = path[index];
137
+ let regex = this.#routeRegexCache.get(routeSegment);
138
+ if (!regex) {
139
+ const regexPattern = routeSegment
140
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
141
+ .replace(/\\{(\w+)\\}/g, '(?<$1>[^/]+)');
142
+ regex = new RegExp(`^${regexPattern}$`);
143
+ this.#routeRegexCache.set(routeSegment, regex);
144
+ }
145
+ const values = pathSegment.match(regex)?.groups;
146
+ if (!values) {
147
+ isMatch = false;
148
+ break;
149
+ }
150
+ if (paramName in params) {
151
+ throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Duplicate parameter "${paramName}" at ${p}`);
152
+ }
153
+ params[paramName] = values[paramName];
154
+ }
155
+ if (isMatch) {
156
+ methodParams = params;
157
+ methodKeys.push(p);
158
+ }
159
+ }
160
+ else {
161
+ // Static route - simple equality comparison for all segments
162
+ let isMatch = true;
163
+ for (let i = 0; i < routeSegments.length; i++) {
164
+ if (routeSegments[i] !== path[i]) {
165
+ isMatch = false;
166
+ break;
167
+ }
168
+ }
169
+ if (isMatch) {
170
+ methodKeys.push(p);
171
+ }
172
+ }
173
+ }
174
+ if (methodKeys.length > 1) {
175
+ throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Conflicting routes found: ${methodKeys.join(', ')}`);
176
+ }
177
+ [methodKey] = methodKeys;
178
+ // Cache successful matches
179
+ if (methodKey) {
180
+ this.#routeMatchCache.set(pathStr, { route: methodKey, params: methodParams });
181
+ }
182
+ }
183
+ if (methodKey) {
184
+ return { handler: handlers[methodKey], methodParams };
185
+ }
186
+ return { handler: null, methodParams };
187
+ };
188
+ #allHandlers = {};
189
+ #collectHandlers = (httpMethod, segmentName) => {
190
+ const controllers = this.routes[httpMethod];
191
+ const handlers = {};
192
+ controllers.forEach((staticMethods, controller) => {
193
+ if (segmentName !== controller._segmentName)
194
+ return;
195
+ const prefix = controller._prefix ?? '';
196
+ Object.entries(staticMethods ?? {}).forEach(([path, staticMethod]) => {
197
+ const fullPath = [prefix, path].filter(Boolean).join('/');
198
+ handlers[fullPath] = { staticMethod, controller };
199
+ });
200
+ });
201
+ return handlers;
202
+ };
203
+ #callMethod = async ({ httpMethod, req: request, params, segmentName, }) => {
204
+ const req = request;
205
+ const path = params[Object.keys(params)[0]] ?? [];
206
+ const handlers = this.#allHandlers[segmentName]?.[httpMethod] ?? this.#collectHandlers(httpMethod, segmentName);
207
+ this.#allHandlers[segmentName] ??= {};
208
+ this.#allHandlers[segmentName][httpMethod] = handlers;
209
+ let headerList;
210
+ try {
211
+ headerList = request.headers;
212
+ }
213
+ catch {
214
+ // this is static rendering environment, headers are not available
215
+ headerList = null;
216
+ }
217
+ const xMeta = headerList?.get('x-meta');
218
+ const xMetaHeader = xMeta && JSON.parse(xMeta);
219
+ if (xMetaHeader)
220
+ reqMeta(req, { xMetaHeader });
221
+ const { handler, methodParams } = this.#getHandler({ handlers, path, params });
222
+ if (!handler) {
223
+ return this.#respondWithError({
224
+ req,
225
+ statusCode: HttpStatus.NOT_FOUND,
226
+ message: `Route '${path.join('/')}' is not found for ${httpMethod} method at ${segmentName === '' ? 'the root segment' : `segment '${segmentName}'`}`,
227
+ });
228
+ }
229
+ const { staticMethod, controller } = handler;
230
+ const headersFromDecoratorOptions = _a.getHeadersFromDecoratorOptions(staticMethod._options);
231
+ const { _onSuccess: onSuccess, _onBefore: onBefore } = controller;
232
+ req.vovk = {
233
+ body: () => parseBody(req),
234
+ query: () => reqQuery(req),
235
+ meta: (meta) => reqMeta(req, meta),
236
+ params: () => methodParams,
237
+ };
238
+ try {
239
+ await staticMethod._options?.before?.call(controller, req);
240
+ await onBefore?.(req);
241
+ const result = await staticMethod.call(controller, req, methodParams);
242
+ if (result instanceof Response) {
243
+ await onSuccess?.(result, req);
244
+ // set headers from decorator options
245
+ for (const [key, value] of Object.entries(headersFromDecoratorOptions)) {
246
+ if (!result.headers.has(key)) {
247
+ result.headers.set(key, value);
248
+ }
249
+ }
250
+ return result;
251
+ }
252
+ if (result instanceof Responder) {
253
+ await onSuccess?.(result, req);
254
+ // set headers from decorator options
255
+ for (const [key, value] of Object.entries(headersFromDecoratorOptions)) {
256
+ if (!result.response.headers.has(key)) {
257
+ result.response.headers.set(key, value);
258
+ }
259
+ }
260
+ return result.response;
261
+ }
262
+ const isIterator = typeof result === 'object' &&
263
+ !!result &&
264
+ !(result instanceof Array) &&
265
+ ((Reflect.has(result, Symbol.iterator) &&
266
+ typeof result[Symbol.iterator] === 'function') ||
267
+ (Reflect.has(result, Symbol.asyncIterator) &&
268
+ typeof result[Symbol.asyncIterator] === 'function'));
269
+ if (isIterator) {
270
+ const responder = new JSONLinesResponder(req, ({ headers, readableStream }) => new Response(readableStream, {
271
+ headers: { ...headersFromDecoratorOptions, ...headers },
272
+ }));
273
+ void (async () => {
274
+ try {
275
+ for await (const chunk of result) {
276
+ await responder.send(chunk);
277
+ }
278
+ }
279
+ catch (e) {
280
+ return responder.throw(e);
281
+ }
282
+ return responder.close();
283
+ })();
284
+ await onSuccess?.(responder, req);
285
+ return responder.response;
286
+ }
287
+ const responseBody = result ?? null;
288
+ await onSuccess?.(responseBody, req);
289
+ return this.respond({ req, statusCode: 200, responseBody, options: staticMethod._options });
290
+ }
291
+ catch (e) {
292
+ const err = e;
293
+ try {
294
+ await controller._onError?.(err, req);
295
+ }
296
+ catch (onErrorError) {
297
+ // eslint-disable-next-line no-console
298
+ console.error('An error caught in onError handler:', onErrorError);
299
+ }
300
+ if (err.message !== 'NEXT_REDIRECT' && err.message !== 'NEXT_NOT_FOUND') {
301
+ const statusCode = err.statusCode || HttpStatus.INTERNAL_SERVER_ERROR;
302
+ return this.#respondWithError({
303
+ req,
304
+ statusCode,
305
+ message: err.message,
306
+ options: staticMethod._options,
307
+ cause: err.cause,
308
+ });
309
+ }
310
+ throw e; // if NEXT_REDIRECT or NEXT_NOT_FOUND, rethrow it
311
+ }
312
+ };
313
+ }
314
+ _a = VovkApp;
315
+ const vovkApp = new VovkApp();
316
+ export { vovkApp };