vovk 3.0.0-draft.154 → 3.0.0-draft.156

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.
@@ -1,3 +1,3 @@
1
1
  import type { KnownAny, VovkSchema } from '../types.js';
2
2
  import type { VovkClientOptions, VovkClient, VovkDefaultFetcherOptions } from './types.js';
3
- export declare const createRPC: <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions<Record<string, never>>>(schema: VovkSchema, givenSegmentName: string, rpcModuleName: string, options?: VovkClientOptions<OPTS>) => VovkClient<T, OPTS>;
3
+ export declare const createRPC: <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions<Record<string, never>>>(schema: VovkSchema, segmentName: string, rpcModuleName: string, options?: VovkClientOptions<OPTS>) => VovkClient<T, OPTS>;
@@ -17,8 +17,8 @@ const getHandlerPath = (endpoint, params, query) => {
17
17
  }
18
18
  return `${result}${queryStr ? '?' : ''}${queryStr}`;
19
19
  };
20
- const createRPC = (schema, givenSegmentName, rpcModuleName, options) => {
21
- const segmentName = options?.segmentNameOverride ?? givenSegmentName;
20
+ const createRPC = (schema, segmentName, rpcModuleName, options) => {
21
+ const segmentNamePath = options?.segmentNameOverride ?? segmentName;
22
22
  const segmentSchema = schema.segments[segmentName];
23
23
  if (!segmentSchema)
24
24
  throw new Error(`Unable to create RPC object. Segment schema is missing. Check client template.`);
@@ -34,10 +34,10 @@ const createRPC = (schema, givenSegmentName, rpcModuleName, options) => {
34
34
  const mainPrefix = [
35
35
  apiRoot.startsWith('http://') || apiRoot.startsWith('https://') || apiRoot.startsWith('/') ? '' : '/',
36
36
  apiRoot,
37
- segmentName,
37
+ segmentNamePath,
38
38
  ]
39
39
  .filter(Boolean)
40
- .join('/');
40
+ .join('/') + '/';
41
41
  return mainPrefix + getHandlerPath([controllerPrefix, path].filter(Boolean).join('/'), params, query);
42
42
  };
43
43
  const handler = (input = {}) => {
@@ -83,7 +83,7 @@ const createRPC = (schema, givenSegmentName, rpcModuleName, options) => {
83
83
  handler.segmentSchema = segmentSchema;
84
84
  handler.fullSchema = schema;
85
85
  handler.isRPC = true;
86
- handler.path = [segmentName, controllerPrefix, path].filter(Boolean).join('/');
86
+ handler.path = [segmentNamePath, controllerPrefix, path].filter(Boolean).join('/');
87
87
  // @ts-expect-error TODO
88
88
  client[staticMethodName] = handler;
89
89
  }
package/cjs/index.d.ts CHANGED
@@ -7,7 +7,8 @@ import { createDecorator } from './createDecorator.js';
7
7
  import { JSONLinesResponse } from './JSONLinesResponse.js';
8
8
  import { generateStaticAPI } from './utils/generateStaticAPI.js';
9
9
  import { withValidation } from './utils/withValidation.js';
10
- export { type KnownAny, type VovkClient, type VovkClientFetcher, type VovkDefaultFetcherOptions, type VovkStreamAsyncIterable, type VovkValidateOnClient, type VovkSegmentSchema, type VovkErrorResponse, type VovkRequest, type VovkOutput, type VovkIteration, type VovkBody, type VovkQuery, type VovkParams, type VovkYieldType, type VovkReturnType, type VovkClientOptions, type VovkControllerSchema, type VovkHandlerSchema, type VovkSchema, type VovkConfig, type VovkStrictConfig, type VovkValidationType, VovkSchemaIdEnum, JSONLinesResponse, HttpException, HttpStatus, HttpMethod, createVovkApp, createDecorator, createRPC, fetcher, createFetcher, generateStaticAPI, withValidation, };
10
+ import { multitenant } from './utils/multitenant.js';
11
+ export { type KnownAny, type VovkClient, type VovkClientFetcher, type VovkDefaultFetcherOptions, type VovkStreamAsyncIterable, type VovkValidateOnClient, type VovkSegmentSchema, type VovkErrorResponse, type VovkRequest, type VovkOutput, type VovkIteration, type VovkBody, type VovkQuery, type VovkParams, type VovkYieldType, type VovkReturnType, type VovkClientOptions, type VovkControllerSchema, type VovkHandlerSchema, type VovkSchema, type VovkConfig, type VovkStrictConfig, type VovkValidationType, VovkSchemaIdEnum, JSONLinesResponse, HttpException, HttpStatus, HttpMethod, createVovkApp, createDecorator, createRPC, fetcher, createFetcher, generateStaticAPI, withValidation, multitenant, };
11
12
  export declare const get: {
12
13
  (givenPath?: string | undefined, options?: import("./types.js").DecoratorOptions | undefined): ReturnType<(givenPath?: string, options?: import("./types.js").DecoratorOptions) => (givenTarget: KnownAny, propertyKey: string) => void>;
13
14
  auto: (options?: import("./types.js").DecoratorOptions) => (givenTarget: KnownAny, propertyKey: string) => void;
package/cjs/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.initVovk = exports.prefix = exports.options = exports.head = exports.del = exports.patch = exports.put = exports.post = exports.get = exports.withValidation = exports.generateStaticAPI = exports.createFetcher = exports.fetcher = exports.createRPC = exports.createDecorator = exports.createVovkApp = exports.HttpMethod = exports.HttpStatus = exports.HttpException = exports.JSONLinesResponse = exports.VovkSchemaIdEnum = void 0;
4
+ exports.initVovk = exports.prefix = exports.options = exports.head = exports.del = exports.patch = exports.put = exports.post = exports.get = exports.multitenant = exports.withValidation = exports.generateStaticAPI = exports.createFetcher = exports.fetcher = exports.createRPC = exports.createDecorator = exports.createVovkApp = exports.HttpMethod = exports.HttpStatus = exports.HttpException = exports.JSONLinesResponse = exports.VovkSchemaIdEnum = void 0;
5
5
  const createVovkApp_js_1 = require("./createVovkApp.js");
6
6
  Object.defineProperty(exports, "createVovkApp", { enumerable: true, get: function () { return createVovkApp_js_1.createVovkApp; } });
7
7
  const types_js_1 = require("./types.js");
@@ -22,4 +22,6 @@ const generateStaticAPI_js_1 = require("./utils/generateStaticAPI.js");
22
22
  Object.defineProperty(exports, "generateStaticAPI", { enumerable: true, get: function () { return generateStaticAPI_js_1.generateStaticAPI; } });
23
23
  const withValidation_js_1 = require("./utils/withValidation.js");
24
24
  Object.defineProperty(exports, "withValidation", { enumerable: true, get: function () { return withValidation_js_1.withValidation; } });
25
+ const multitenant_js_1 = require("./utils/multitenant.js");
26
+ Object.defineProperty(exports, "multitenant", { enumerable: true, get: function () { return multitenant_js_1.multitenant; } });
25
27
  _a = (0, createVovkApp_js_1.createVovkApp)(), exports.get = _a.get, exports.post = _a.post, exports.put = _a.put, exports.patch = _a.patch, exports.del = _a.del, exports.head = _a.head, exports.options = _a.options, exports.prefix = _a.prefix, exports.initVovk = _a.initVovk;
@@ -0,0 +1,24 @@
1
+ type Override = {
2
+ from: string;
3
+ to: string;
4
+ };
5
+ type Config = {
6
+ requestUrl: string;
7
+ requestHost: string;
8
+ targetHost: string;
9
+ overrides: {
10
+ [key: string]: Override[];
11
+ };
12
+ };
13
+ export declare function multitenant(config: Config): {
14
+ action: null;
15
+ destination: null;
16
+ message: string;
17
+ subdomains: null;
18
+ } | {
19
+ action: string;
20
+ destination: string;
21
+ message: string;
22
+ subdomains: Record<string, string> | null;
23
+ };
24
+ export {};
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ /*
3
+ Function "multitenant" accepts an object with "requestUrl", "requestHost", "targetHost" and "overrides" properties.
4
+ The requestHost parameter comes from request headers and should be used instead of the hostname in the requestUrl.
5
+ The keys in overrides are subdomains relative to the "targetHost". They can include a dot to indicate lower-level
6
+ subdomains, and can use square brackets to indicate wildcard values. The values are arrays of objects with "from"
7
+ and "to" properties, which are strings indicating the override paths for Next.js.
8
+
9
+ If a path matches a specific subdomain name (e.g., "/admin" matches the "admin" subdomain),
10
+ the request will be redirected to that specific subdomain instead of being processed on the current subdomain.
11
+
12
+ If a path ends with "_schema_", it will not be processed by any overrides and will be passed through as-is.
13
+
14
+ Example:
15
+
16
+ const { action, destination, message, subdomains } = multitenant({
17
+ requestUrl: request.url,
18
+ requestHost: request.headers.get('host'),
19
+ targetHost: "localhost:3000",
20
+ overrides: {
21
+ "[customer_name].customer": [
22
+ { from: "api", to: "api/customer" }, // API
23
+ { from: "", to: "[customer_name]" } // UI
24
+ ],
25
+ "pro.[customer_name].customer": [
26
+ { from: "api", to: "api/customer/pro" }, // API
27
+ { from: "", to: "pro/[customer_name]" } // UI
28
+ ],
29
+ "admin": [
30
+ { from: "api", to: "api/admin" }, // API
31
+ { from: "", to: "admin" } // UI
32
+ ],
33
+ },
34
+ });
35
+
36
+ subdomains is a Record<string, string> | null where keys are subdomain names and values are their corresponding values. If non-wildcard subdomains are used, should be equal to null.
37
+ action is one of "rewrite", "redirect", "notfound", or null.
38
+ message is a string describing the action taken
39
+ destination is a string URL to redirect or rewrite to, or null if no action is taken or "notfound"
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.multitenant = multitenant;
43
+ // Get the reserved paths from the overrides configuration
44
+ const getReservedPaths = (overrides) => {
45
+ return Object.keys(overrides).filter((key) => !key.includes('[') && !key.includes(']')); // Filter out dynamic paths
46
+ };
47
+ /**
48
+ * Convert a pattern with [placeholders] to a regex pattern and extract placeholder names
49
+ */
50
+ const patternToRegex = (pattern) => {
51
+ const paramNames = [];
52
+ const regexPattern = pattern
53
+ .replace(/\[([^\]]+)\]/g, (_, name) => {
54
+ paramNames.push(name);
55
+ return '([^.]+)';
56
+ })
57
+ .replace(/\./g, '\\.'); // Escape dots in the pattern
58
+ return {
59
+ regex: new RegExp(`^${regexPattern}$`),
60
+ paramNames,
61
+ };
62
+ };
63
+ function multitenant(config) {
64
+ const { requestUrl, requestHost, targetHost, overrides } = config;
65
+ // Parse the URL
66
+ const urlObj = new URL(requestUrl);
67
+ const pathname = urlObj.pathname.slice(1); // Remove leading slash
68
+ // Skip processing for paths ending with "_schema_"
69
+ if (pathname.endsWith('_schema_')) {
70
+ return {
71
+ action: null,
72
+ destination: null,
73
+ message: 'Schema endpoint, bypassing overrides',
74
+ subdomains: null,
75
+ };
76
+ }
77
+ const pathSegments = pathname.split('/').filter(Boolean);
78
+ // Get reserved paths
79
+ const reservedPaths = getReservedPaths(overrides);
80
+ // Check if any path segment matches a reserved path (e.g., "admin")
81
+ for (let i = 0; i < pathSegments.length; i++) {
82
+ const segment = pathSegments[i];
83
+ if (reservedPaths.includes(segment)) {
84
+ // Create the destination URL with the reserved path as subdomain
85
+ const destinationHost = `${segment}.${targetHost}`;
86
+ // Keep path segments before the reserved path
87
+ const beforeSegments = pathSegments.slice(0, i);
88
+ // Keep path segments after the reserved path
89
+ const afterSegments = pathSegments.slice(i + 1);
90
+ // Construct the new path
91
+ const newPath = [...beforeSegments, ...afterSegments].join('/');
92
+ const destinationUrl = new URL(`${urlObj.protocol}//${destinationHost}`);
93
+ if (newPath) {
94
+ destinationUrl.pathname = `/${newPath}`;
95
+ }
96
+ // Keep any query parameters
97
+ destinationUrl.search = urlObj.search;
98
+ return {
99
+ action: 'redirect',
100
+ destination: destinationUrl.toString(),
101
+ message: `Redirecting to ${segment} subdomain`,
102
+ subdomains: null, // No wildcards used
103
+ };
104
+ }
105
+ }
106
+ // Process based on host and subdomains
107
+ for (const pattern in overrides) {
108
+ const fullPattern = `${pattern}.${targetHost}`;
109
+ const { regex, paramNames } = patternToRegex(fullPattern);
110
+ const match = requestHost.match(regex);
111
+ if (match) {
112
+ const overrideRules = overrides[pattern];
113
+ // Extract parameters from the match
114
+ const params = {};
115
+ if (match.length > 1) {
116
+ for (let i = 0; i < paramNames.length; i++) {
117
+ params[paramNames[i]] = match[i + 1];
118
+ }
119
+ }
120
+ // Find the appropriate rule based on the path
121
+ for (const rule of overrideRules) {
122
+ if (pathname === rule.from || pathname.startsWith(`${rule.from}/`)) {
123
+ // Replace path with the destination
124
+ let destination = pathname.replace(rule.from, rule.to);
125
+ // Replace any dynamic parameters in destination
126
+ if (Object.keys(params).length > 0) {
127
+ Object.entries(params).forEach(([key, value]) => {
128
+ destination = destination.replace(`[${key}]`, value);
129
+ });
130
+ }
131
+ // Only return non-null subdomains if we have wildcard parameters
132
+ const wildcardSubdomains = paramNames.length > 0 ? params : null;
133
+ return {
134
+ action: 'rewrite',
135
+ destination: `${urlObj.protocol}//${urlObj.host}/${destination}${urlObj.search}`,
136
+ message: `Rewriting to ${destination}`,
137
+ subdomains: wildcardSubdomains,
138
+ };
139
+ }
140
+ }
141
+ }
142
+ }
143
+ // Handle cases where a customer subdomain tries to access reserved paths
144
+ if (pathSegments.length > 0 && reservedPaths.includes(pathSegments[0])) {
145
+ const reservedPath = pathSegments[0];
146
+ const restPath = pathSegments.slice(1).join('/');
147
+ // Create the destination URL with the reserved path as subdomain
148
+ const destinationHost = `${reservedPath}.${targetHost}`;
149
+ const destinationUrl = new URL(`${urlObj.protocol}//${destinationHost}`);
150
+ // Only add remaining path segments if they exist
151
+ if (restPath) {
152
+ destinationUrl.pathname = `/${restPath}`;
153
+ }
154
+ // Keep any query parameters
155
+ destinationUrl.search = urlObj.search;
156
+ return {
157
+ action: 'redirect',
158
+ destination: destinationUrl.toString(),
159
+ message: `Redirecting to ${reservedPath} subdomain`,
160
+ subdomains: null, // No wildcards used for reserved paths
161
+ };
162
+ }
163
+ // Default case - pass through
164
+ return {
165
+ action: null,
166
+ destination: null,
167
+ message: 'No action',
168
+ subdomains: null, // No wildcards matched
169
+ };
170
+ }
@@ -1,3 +1,3 @@
1
1
  import type { KnownAny, VovkSchema } from '../types.js';
2
2
  import type { VovkClientOptions, VovkClient, VovkDefaultFetcherOptions } from './types.js';
3
- export declare const createRPC: <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions<Record<string, never>>>(schema: VovkSchema, givenSegmentName: string, rpcModuleName: string, options?: VovkClientOptions<OPTS>) => VovkClient<T, OPTS>;
3
+ export declare const createRPC: <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions<Record<string, never>>>(schema: VovkSchema, segmentName: string, rpcModuleName: string, options?: VovkClientOptions<OPTS>) => VovkClient<T, OPTS>;
@@ -17,8 +17,8 @@ const getHandlerPath = (endpoint, params, query) => {
17
17
  }
18
18
  return `${result}${queryStr ? '?' : ''}${queryStr}`;
19
19
  };
20
- const createRPC = (schema, givenSegmentName, rpcModuleName, options) => {
21
- const segmentName = options?.segmentNameOverride ?? givenSegmentName;
20
+ const createRPC = (schema, segmentName, rpcModuleName, options) => {
21
+ const segmentNamePath = options?.segmentNameOverride ?? segmentName;
22
22
  const segmentSchema = schema.segments[segmentName];
23
23
  if (!segmentSchema)
24
24
  throw new Error(`Unable to create RPC object. Segment schema is missing. Check client template.`);
@@ -34,10 +34,10 @@ const createRPC = (schema, givenSegmentName, rpcModuleName, options) => {
34
34
  const mainPrefix = [
35
35
  apiRoot.startsWith('http://') || apiRoot.startsWith('https://') || apiRoot.startsWith('/') ? '' : '/',
36
36
  apiRoot,
37
- segmentName,
37
+ segmentNamePath,
38
38
  ]
39
39
  .filter(Boolean)
40
- .join('/');
40
+ .join('/') + '/';
41
41
  return mainPrefix + getHandlerPath([controllerPrefix, path].filter(Boolean).join('/'), params, query);
42
42
  };
43
43
  const handler = (input = {}) => {
@@ -83,7 +83,7 @@ const createRPC = (schema, givenSegmentName, rpcModuleName, options) => {
83
83
  handler.segmentSchema = segmentSchema;
84
84
  handler.fullSchema = schema;
85
85
  handler.isRPC = true;
86
- handler.path = [segmentName, controllerPrefix, path].filter(Boolean).join('/');
86
+ handler.path = [segmentNamePath, controllerPrefix, path].filter(Boolean).join('/');
87
87
  // @ts-expect-error TODO
88
88
  client[staticMethodName] = handler;
89
89
  }
package/mjs/index.d.ts CHANGED
@@ -7,7 +7,8 @@ import { createDecorator } from './createDecorator.js';
7
7
  import { JSONLinesResponse } from './JSONLinesResponse.js';
8
8
  import { generateStaticAPI } from './utils/generateStaticAPI.js';
9
9
  import { withValidation } from './utils/withValidation.js';
10
- export { type KnownAny, type VovkClient, type VovkClientFetcher, type VovkDefaultFetcherOptions, type VovkStreamAsyncIterable, type VovkValidateOnClient, type VovkSegmentSchema, type VovkErrorResponse, type VovkRequest, type VovkOutput, type VovkIteration, type VovkBody, type VovkQuery, type VovkParams, type VovkYieldType, type VovkReturnType, type VovkClientOptions, type VovkControllerSchema, type VovkHandlerSchema, type VovkSchema, type VovkConfig, type VovkStrictConfig, type VovkValidationType, VovkSchemaIdEnum, JSONLinesResponse, HttpException, HttpStatus, HttpMethod, createVovkApp, createDecorator, createRPC, fetcher, createFetcher, generateStaticAPI, withValidation, };
10
+ import { multitenant } from './utils/multitenant.js';
11
+ export { type KnownAny, type VovkClient, type VovkClientFetcher, type VovkDefaultFetcherOptions, type VovkStreamAsyncIterable, type VovkValidateOnClient, type VovkSegmentSchema, type VovkErrorResponse, type VovkRequest, type VovkOutput, type VovkIteration, type VovkBody, type VovkQuery, type VovkParams, type VovkYieldType, type VovkReturnType, type VovkClientOptions, type VovkControllerSchema, type VovkHandlerSchema, type VovkSchema, type VovkConfig, type VovkStrictConfig, type VovkValidationType, VovkSchemaIdEnum, JSONLinesResponse, HttpException, HttpStatus, HttpMethod, createVovkApp, createDecorator, createRPC, fetcher, createFetcher, generateStaticAPI, withValidation, multitenant, };
11
12
  export declare const get: {
12
13
  (givenPath?: string | undefined, options?: import("./types.js").DecoratorOptions | undefined): ReturnType<(givenPath?: string, options?: import("./types.js").DecoratorOptions) => (givenTarget: KnownAny, propertyKey: string) => void>;
13
14
  auto: (options?: import("./types.js").DecoratorOptions) => (givenTarget: KnownAny, propertyKey: string) => void;
package/mjs/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.initVovk = exports.prefix = exports.options = exports.head = exports.del = exports.patch = exports.put = exports.post = exports.get = exports.withValidation = exports.generateStaticAPI = exports.createFetcher = exports.fetcher = exports.createRPC = exports.createDecorator = exports.createVovkApp = exports.HttpMethod = exports.HttpStatus = exports.HttpException = exports.JSONLinesResponse = exports.VovkSchemaIdEnum = void 0;
4
+ exports.initVovk = exports.prefix = exports.options = exports.head = exports.del = exports.patch = exports.put = exports.post = exports.get = exports.multitenant = exports.withValidation = exports.generateStaticAPI = exports.createFetcher = exports.fetcher = exports.createRPC = exports.createDecorator = exports.createVovkApp = exports.HttpMethod = exports.HttpStatus = exports.HttpException = exports.JSONLinesResponse = exports.VovkSchemaIdEnum = void 0;
5
5
  const createVovkApp_js_1 = require("./createVovkApp.js");
6
6
  Object.defineProperty(exports, "createVovkApp", { enumerable: true, get: function () { return createVovkApp_js_1.createVovkApp; } });
7
7
  const types_js_1 = require("./types.js");
@@ -22,4 +22,6 @@ const generateStaticAPI_js_1 = require("./utils/generateStaticAPI.js");
22
22
  Object.defineProperty(exports, "generateStaticAPI", { enumerable: true, get: function () { return generateStaticAPI_js_1.generateStaticAPI; } });
23
23
  const withValidation_js_1 = require("./utils/withValidation.js");
24
24
  Object.defineProperty(exports, "withValidation", { enumerable: true, get: function () { return withValidation_js_1.withValidation; } });
25
+ const multitenant_js_1 = require("./utils/multitenant.js");
26
+ Object.defineProperty(exports, "multitenant", { enumerable: true, get: function () { return multitenant_js_1.multitenant; } });
25
27
  _a = (0, createVovkApp_js_1.createVovkApp)(), exports.get = _a.get, exports.post = _a.post, exports.put = _a.put, exports.patch = _a.patch, exports.del = _a.del, exports.head = _a.head, exports.options = _a.options, exports.prefix = _a.prefix, exports.initVovk = _a.initVovk;
@@ -0,0 +1,24 @@
1
+ type Override = {
2
+ from: string;
3
+ to: string;
4
+ };
5
+ type Config = {
6
+ requestUrl: string;
7
+ requestHost: string;
8
+ targetHost: string;
9
+ overrides: {
10
+ [key: string]: Override[];
11
+ };
12
+ };
13
+ export declare function multitenant(config: Config): {
14
+ action: null;
15
+ destination: null;
16
+ message: string;
17
+ subdomains: null;
18
+ } | {
19
+ action: string;
20
+ destination: string;
21
+ message: string;
22
+ subdomains: Record<string, string> | null;
23
+ };
24
+ export {};
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ /*
3
+ Function "multitenant" accepts an object with "requestUrl", "requestHost", "targetHost" and "overrides" properties.
4
+ The requestHost parameter comes from request headers and should be used instead of the hostname in the requestUrl.
5
+ The keys in overrides are subdomains relative to the "targetHost". They can include a dot to indicate lower-level
6
+ subdomains, and can use square brackets to indicate wildcard values. The values are arrays of objects with "from"
7
+ and "to" properties, which are strings indicating the override paths for Next.js.
8
+
9
+ If a path matches a specific subdomain name (e.g., "/admin" matches the "admin" subdomain),
10
+ the request will be redirected to that specific subdomain instead of being processed on the current subdomain.
11
+
12
+ If a path ends with "_schema_", it will not be processed by any overrides and will be passed through as-is.
13
+
14
+ Example:
15
+
16
+ const { action, destination, message, subdomains } = multitenant({
17
+ requestUrl: request.url,
18
+ requestHost: request.headers.get('host'),
19
+ targetHost: "localhost:3000",
20
+ overrides: {
21
+ "[customer_name].customer": [
22
+ { from: "api", to: "api/customer" }, // API
23
+ { from: "", to: "[customer_name]" } // UI
24
+ ],
25
+ "pro.[customer_name].customer": [
26
+ { from: "api", to: "api/customer/pro" }, // API
27
+ { from: "", to: "pro/[customer_name]" } // UI
28
+ ],
29
+ "admin": [
30
+ { from: "api", to: "api/admin" }, // API
31
+ { from: "", to: "admin" } // UI
32
+ ],
33
+ },
34
+ });
35
+
36
+ subdomains is a Record<string, string> | null where keys are subdomain names and values are their corresponding values. If non-wildcard subdomains are used, should be equal to null.
37
+ action is one of "rewrite", "redirect", "notfound", or null.
38
+ message is a string describing the action taken
39
+ destination is a string URL to redirect or rewrite to, or null if no action is taken or "notfound"
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.multitenant = multitenant;
43
+ // Get the reserved paths from the overrides configuration
44
+ const getReservedPaths = (overrides) => {
45
+ return Object.keys(overrides).filter((key) => !key.includes('[') && !key.includes(']')); // Filter out dynamic paths
46
+ };
47
+ /**
48
+ * Convert a pattern with [placeholders] to a regex pattern and extract placeholder names
49
+ */
50
+ const patternToRegex = (pattern) => {
51
+ const paramNames = [];
52
+ const regexPattern = pattern
53
+ .replace(/\[([^\]]+)\]/g, (_, name) => {
54
+ paramNames.push(name);
55
+ return '([^.]+)';
56
+ })
57
+ .replace(/\./g, '\\.'); // Escape dots in the pattern
58
+ return {
59
+ regex: new RegExp(`^${regexPattern}$`),
60
+ paramNames,
61
+ };
62
+ };
63
+ function multitenant(config) {
64
+ const { requestUrl, requestHost, targetHost, overrides } = config;
65
+ // Parse the URL
66
+ const urlObj = new URL(requestUrl);
67
+ const pathname = urlObj.pathname.slice(1); // Remove leading slash
68
+ // Skip processing for paths ending with "_schema_"
69
+ if (pathname.endsWith('_schema_')) {
70
+ return {
71
+ action: null,
72
+ destination: null,
73
+ message: 'Schema endpoint, bypassing overrides',
74
+ subdomains: null,
75
+ };
76
+ }
77
+ const pathSegments = pathname.split('/').filter(Boolean);
78
+ // Get reserved paths
79
+ const reservedPaths = getReservedPaths(overrides);
80
+ // Check if any path segment matches a reserved path (e.g., "admin")
81
+ for (let i = 0; i < pathSegments.length; i++) {
82
+ const segment = pathSegments[i];
83
+ if (reservedPaths.includes(segment)) {
84
+ // Create the destination URL with the reserved path as subdomain
85
+ const destinationHost = `${segment}.${targetHost}`;
86
+ // Keep path segments before the reserved path
87
+ const beforeSegments = pathSegments.slice(0, i);
88
+ // Keep path segments after the reserved path
89
+ const afterSegments = pathSegments.slice(i + 1);
90
+ // Construct the new path
91
+ const newPath = [...beforeSegments, ...afterSegments].join('/');
92
+ const destinationUrl = new URL(`${urlObj.protocol}//${destinationHost}`);
93
+ if (newPath) {
94
+ destinationUrl.pathname = `/${newPath}`;
95
+ }
96
+ // Keep any query parameters
97
+ destinationUrl.search = urlObj.search;
98
+ return {
99
+ action: 'redirect',
100
+ destination: destinationUrl.toString(),
101
+ message: `Redirecting to ${segment} subdomain`,
102
+ subdomains: null, // No wildcards used
103
+ };
104
+ }
105
+ }
106
+ // Process based on host and subdomains
107
+ for (const pattern in overrides) {
108
+ const fullPattern = `${pattern}.${targetHost}`;
109
+ const { regex, paramNames } = patternToRegex(fullPattern);
110
+ const match = requestHost.match(regex);
111
+ if (match) {
112
+ const overrideRules = overrides[pattern];
113
+ // Extract parameters from the match
114
+ const params = {};
115
+ if (match.length > 1) {
116
+ for (let i = 0; i < paramNames.length; i++) {
117
+ params[paramNames[i]] = match[i + 1];
118
+ }
119
+ }
120
+ // Find the appropriate rule based on the path
121
+ for (const rule of overrideRules) {
122
+ if (pathname === rule.from || pathname.startsWith(`${rule.from}/`)) {
123
+ // Replace path with the destination
124
+ let destination = pathname.replace(rule.from, rule.to);
125
+ // Replace any dynamic parameters in destination
126
+ if (Object.keys(params).length > 0) {
127
+ Object.entries(params).forEach(([key, value]) => {
128
+ destination = destination.replace(`[${key}]`, value);
129
+ });
130
+ }
131
+ // Only return non-null subdomains if we have wildcard parameters
132
+ const wildcardSubdomains = paramNames.length > 0 ? params : null;
133
+ return {
134
+ action: 'rewrite',
135
+ destination: `${urlObj.protocol}//${urlObj.host}/${destination}${urlObj.search}`,
136
+ message: `Rewriting to ${destination}`,
137
+ subdomains: wildcardSubdomains,
138
+ };
139
+ }
140
+ }
141
+ }
142
+ }
143
+ // Handle cases where a customer subdomain tries to access reserved paths
144
+ if (pathSegments.length > 0 && reservedPaths.includes(pathSegments[0])) {
145
+ const reservedPath = pathSegments[0];
146
+ const restPath = pathSegments.slice(1).join('/');
147
+ // Create the destination URL with the reserved path as subdomain
148
+ const destinationHost = `${reservedPath}.${targetHost}`;
149
+ const destinationUrl = new URL(`${urlObj.protocol}//${destinationHost}`);
150
+ // Only add remaining path segments if they exist
151
+ if (restPath) {
152
+ destinationUrl.pathname = `/${restPath}`;
153
+ }
154
+ // Keep any query parameters
155
+ destinationUrl.search = urlObj.search;
156
+ return {
157
+ action: 'redirect',
158
+ destination: destinationUrl.toString(),
159
+ message: `Redirecting to ${reservedPath} subdomain`,
160
+ subdomains: null, // No wildcards used for reserved paths
161
+ };
162
+ }
163
+ // Default case - pass through
164
+ return {
165
+ action: null,
166
+ destination: null,
167
+ message: 'No action',
168
+ subdomains: null, // No wildcards matched
169
+ };
170
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk",
3
- "version": "3.0.0-draft.154",
3
+ "version": "3.0.0-draft.156",
4
4
  "main": "./cjs/index.js",
5
5
  "module": "./mjs/index.js",
6
6
  "types": "./mjs/index.d.ts",