@getstrata/core 2.0.3 → 2.0.5
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/CHANGELOG.md +8 -0
- package/dist/core/database/baseRepository.d.ts +2 -0
- package/dist/core/events/eventBus.d.ts +4 -0
- package/dist/core/events/outbox/index.d.ts +1 -0
- package/dist/core/http/corsMiddleware.d.ts +7 -2
- package/dist/core/openapi/registeredRoute.d.ts +40 -1
- package/dist/entries/audit/exportAuditLogs.js +18 -0
- package/dist/entries/auth/scimAuthMiddleware.js +18 -0
- package/dist/entries/http/corsMiddleware.js +31 -8
- package/dist/entries/jobs/exportAuditLogsJob.js +18 -0
- package/dist/entries/openapi/generator.js +42 -12
- package/dist/entries/openapi/validate.js +10 -0
- package/dist/entries/tenant/databaseTenantContext.js +18 -0
- package/dist/entries/tenant/tenantDatabaseScope.js +18 -0
- package/dist/framework/public-api.d.ts +1 -1
- package/dist/index.js +89 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.5
|
|
4
|
+
|
|
5
|
+
- Allow explicitly approved application headers in CORS
|
|
6
|
+
|
|
7
|
+
## 2.0.4
|
|
8
|
+
|
|
9
|
+
- Place cache rollout guidance beside outbox docs without overlapping API additions
|
|
10
|
+
|
|
3
11
|
## 2.0.3
|
|
4
12
|
|
|
5
13
|
- Persist transactional events with leased tenant-aware SQL outbox workers
|
|
@@ -21,6 +21,8 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
|
|
|
21
21
|
protected readonly table: TableDefinition<TEntity, PrimaryKey>;
|
|
22
22
|
protected readonly connection: DatabaseConnection;
|
|
23
23
|
constructor(table: TableDefinition<TEntity, PrimaryKey>, connection?: DatabaseConnection);
|
|
24
|
+
private assertTransactionalEvent;
|
|
25
|
+
private runModelMutation;
|
|
24
26
|
count(options?: ExtendedQueryOptions<TEntity>): Promise<number>;
|
|
25
27
|
findAll(options?: ExtendedQueryOptions<TEntity>): Promise<TEntity[]>;
|
|
26
28
|
paginate(options: {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
type EventListener = (payload: unknown) => void | Promise<void>;
|
|
2
2
|
declare class EventBus {
|
|
3
3
|
constructor();
|
|
4
|
+
private readonly transactionalListeners;
|
|
5
|
+
listenTransactional(event: string, listener: EventListener): () => void;
|
|
6
|
+
hasTransactionalListeners(event: string): boolean;
|
|
7
|
+
dispatchTransactional(event: string, payload: unknown): Promise<void>;
|
|
4
8
|
private readonly listeners;
|
|
5
9
|
on(event: string, listener: EventListener): () => void;
|
|
6
10
|
listen(event: string, listener: EventListener): () => void;
|
|
@@ -30,6 +30,7 @@ declare class SqlOutbox {
|
|
|
30
30
|
private readonly maxPayloadBytes;
|
|
31
31
|
private readonly tenantResolver;
|
|
32
32
|
constructor(options: SqlOutboxOptions);
|
|
33
|
+
hasListener(name: string, event?: string): boolean;
|
|
33
34
|
publish(name: string, payload: unknown, options?: {
|
|
34
35
|
id?: string;
|
|
35
36
|
version?: number;
|
|
@@ -5,6 +5,11 @@ interface CorsConfig {
|
|
|
5
5
|
allowedHeaders: string[];
|
|
6
6
|
maxAgeSeconds: number;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** Additional application-approved request headers; defaults and origins remain unchanged. */
|
|
9
|
+
interface CorsOptions {
|
|
10
|
+
additionalAllowedHeaders?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
declare function resolveCorsConfig(options?: CorsOptions): CorsConfig;
|
|
13
|
+
declare function createCorsMiddleware(options?: CorsOptions): Middleware;
|
|
14
|
+
export type { CorsConfig, CorsOptions };
|
|
10
15
|
export { createCorsMiddleware, resolveCorsConfig };
|
|
@@ -1,6 +1,45 @@
|
|
|
1
|
+
/** JSON Schema keywords remain extensible; metadata cannot contain executable values. */
|
|
2
|
+
type OpenApiJson = null | boolean | number | string | OpenApiJson[] | {
|
|
3
|
+
[key: string]: OpenApiJson | undefined;
|
|
4
|
+
};
|
|
5
|
+
type OpenApiSchema = boolean | {
|
|
6
|
+
[keyword: string]: OpenApiJson | undefined;
|
|
7
|
+
};
|
|
8
|
+
interface OpenApiParameter {
|
|
9
|
+
name: string;
|
|
10
|
+
in: "header" | "query" | "path" | "cookie";
|
|
11
|
+
required?: boolean;
|
|
12
|
+
description?: string;
|
|
13
|
+
schema: OpenApiSchema;
|
|
14
|
+
}
|
|
15
|
+
interface OpenApiMediaType {
|
|
16
|
+
schema?: OpenApiSchema;
|
|
17
|
+
example?: OpenApiJson;
|
|
18
|
+
}
|
|
19
|
+
interface OpenApiResponse {
|
|
20
|
+
description: string;
|
|
21
|
+
content?: Record<string, OpenApiMediaType>;
|
|
22
|
+
}
|
|
23
|
+
interface OpenApiOperation {
|
|
24
|
+
summary?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
operationId?: string;
|
|
27
|
+
tags?: string[];
|
|
28
|
+
deprecated?: boolean;
|
|
29
|
+
parameters?: OpenApiParameter[];
|
|
30
|
+
requestBody?: {
|
|
31
|
+
required?: boolean;
|
|
32
|
+
description?: string;
|
|
33
|
+
content: Record<string, OpenApiMediaType>;
|
|
34
|
+
};
|
|
35
|
+
responses?: Record<string, OpenApiResponse>;
|
|
36
|
+
security?: Array<Record<string, string[]>>;
|
|
37
|
+
}
|
|
38
|
+
type OpenApiRouteMap = Record<string, Partial<Record<"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS", OpenApiOperation>>>;
|
|
1
39
|
interface RegisteredRoute {
|
|
2
40
|
method: string;
|
|
3
41
|
path: string;
|
|
4
42
|
middleware: string[];
|
|
43
|
+
openApi?: OpenApiOperation;
|
|
5
44
|
}
|
|
6
|
-
export type { RegisteredRoute };
|
|
45
|
+
export type { OpenApiJson, OpenApiMediaType, OpenApiOperation, OpenApiParameter, OpenApiResponse, OpenApiRouteMap, OpenApiSchema, RegisteredRoute, };
|
|
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
|
|
|
58
58
|
// ../../src/core/events/eventBus.ts
|
|
59
59
|
class EventBus {
|
|
60
60
|
constructor() {}
|
|
61
|
+
transactionalListeners = new Map;
|
|
62
|
+
listenTransactional(event, listener) {
|
|
63
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
64
|
+
handlers.add(listener);
|
|
65
|
+
this.transactionalListeners.set(event, handlers);
|
|
66
|
+
return () => {
|
|
67
|
+
handlers.delete(listener);
|
|
68
|
+
if (!handlers.size)
|
|
69
|
+
this.transactionalListeners.delete(event);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
hasTransactionalListeners(event) {
|
|
73
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
74
|
+
}
|
|
75
|
+
async dispatchTransactional(event, payload) {
|
|
76
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
77
|
+
await listener(payload);
|
|
78
|
+
}
|
|
61
79
|
listeners = new Map;
|
|
62
80
|
on(event, listener) {
|
|
63
81
|
return this.listen(event, listener);
|
|
@@ -155,6 +155,24 @@ var init_tenantContext = __esm(() => {
|
|
|
155
155
|
// ../../src/core/events/eventBus.ts
|
|
156
156
|
class EventBus {
|
|
157
157
|
constructor() {}
|
|
158
|
+
transactionalListeners = new Map;
|
|
159
|
+
listenTransactional(event, listener) {
|
|
160
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
161
|
+
handlers.add(listener);
|
|
162
|
+
this.transactionalListeners.set(event, handlers);
|
|
163
|
+
return () => {
|
|
164
|
+
handlers.delete(listener);
|
|
165
|
+
if (!handlers.size)
|
|
166
|
+
this.transactionalListeners.delete(event);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
hasTransactionalListeners(event) {
|
|
170
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
171
|
+
}
|
|
172
|
+
async dispatchTransactional(event, payload) {
|
|
173
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
174
|
+
await listener(payload);
|
|
175
|
+
}
|
|
158
176
|
listeners = new Map;
|
|
159
177
|
on(event, listener) {
|
|
160
178
|
return this.listen(event, listener);
|
|
@@ -86,10 +86,28 @@ function sdkClientClassName() {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// ../../src/core/http/corsMiddleware.ts
|
|
89
|
+
function additionalHeaders(options) {
|
|
90
|
+
const configured = process.env.CORS_ADDITIONAL_ALLOWED_HEADERS?.trim();
|
|
91
|
+
const supplied = options.additionalAllowedHeaders === undefined ? [] : options.additionalAllowedHeaders;
|
|
92
|
+
if (!Array.isArray(supplied)) {
|
|
93
|
+
throw new TypeError("CORS additionalAllowedHeaders must be an array of header names.");
|
|
94
|
+
}
|
|
95
|
+
const names = [
|
|
96
|
+
...configured ? configured.split(",").map((name) => name.trim()) : [],
|
|
97
|
+
...supplied
|
|
98
|
+
];
|
|
99
|
+
return names.map((name) => {
|
|
100
|
+
if (typeof name !== "string" || name === "*" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
|
|
101
|
+
throw new TypeError("CORS additional headers must be explicit valid HTTP header names; wildcards are not allowed.");
|
|
102
|
+
}
|
|
103
|
+
return name;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
89
106
|
function defaultAllowedOrigins() {
|
|
90
107
|
return appUrl();
|
|
91
108
|
}
|
|
92
|
-
function resolveCorsConfig() {
|
|
109
|
+
function resolveCorsConfig(options = {}) {
|
|
110
|
+
const extraHeaders = additionalHeaders(options);
|
|
93
111
|
return {
|
|
94
112
|
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? defaultAllowedOrigins()).split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
95
113
|
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
@@ -102,22 +120,27 @@ function resolveCorsConfig() {
|
|
|
102
120
|
"X-Authenticated-User-Role",
|
|
103
121
|
"If-Match",
|
|
104
122
|
"If-None-Match",
|
|
105
|
-
"X-CSRF-Token"
|
|
106
|
-
|
|
123
|
+
"X-CSRF-Token",
|
|
124
|
+
...extraHeaders
|
|
125
|
+
].filter((name, index, names) => names.findIndex((other) => other.toLowerCase() === name.toLowerCase()) === index),
|
|
107
126
|
maxAgeSeconds: 86400
|
|
108
127
|
};
|
|
109
128
|
}
|
|
110
|
-
function createCorsMiddleware() {
|
|
129
|
+
function createCorsMiddleware(options = {}) {
|
|
130
|
+
resolveCorsConfig(options);
|
|
131
|
+
const captured = {
|
|
132
|
+
additionalAllowedHeaders: [...options.additionalAllowedHeaders ?? []]
|
|
133
|
+
};
|
|
111
134
|
return async (request, next) => {
|
|
112
135
|
if (request.method === "OPTIONS") {
|
|
113
136
|
return new Response(null, {
|
|
114
137
|
status: 204,
|
|
115
|
-
headers: buildCorsHeaders(request)
|
|
138
|
+
headers: buildCorsHeaders(request, captured)
|
|
116
139
|
});
|
|
117
140
|
}
|
|
118
141
|
const response = await next();
|
|
119
142
|
const headers = new Headers(response.headers);
|
|
120
|
-
for (const [key, value] of buildCorsHeaders(request)) {
|
|
143
|
+
for (const [key, value] of buildCorsHeaders(request, captured)) {
|
|
121
144
|
headers.set(key, value);
|
|
122
145
|
}
|
|
123
146
|
return new Response(response.body, {
|
|
@@ -127,10 +150,10 @@ function createCorsMiddleware() {
|
|
|
127
150
|
});
|
|
128
151
|
};
|
|
129
152
|
}
|
|
130
|
-
function buildCorsHeaders(request) {
|
|
153
|
+
function buildCorsHeaders(request, options) {
|
|
131
154
|
const headers = new Headers;
|
|
132
155
|
const origin = request.headers.get("origin");
|
|
133
|
-
const corsConfig = resolveCorsConfig();
|
|
156
|
+
const corsConfig = resolveCorsConfig(options);
|
|
134
157
|
const allowedOrigins = corsConfig.allowedOrigins;
|
|
135
158
|
headers.set("Vary", "Origin");
|
|
136
159
|
const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
|
|
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
|
|
|
58
58
|
// ../../src/core/events/eventBus.ts
|
|
59
59
|
class EventBus {
|
|
60
60
|
constructor() {}
|
|
61
|
+
transactionalListeners = new Map;
|
|
62
|
+
listenTransactional(event, listener) {
|
|
63
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
64
|
+
handlers.add(listener);
|
|
65
|
+
this.transactionalListeners.set(event, handlers);
|
|
66
|
+
return () => {
|
|
67
|
+
handlers.delete(listener);
|
|
68
|
+
if (!handlers.size)
|
|
69
|
+
this.transactionalListeners.delete(event);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
hasTransactionalListeners(event) {
|
|
73
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
74
|
+
}
|
|
75
|
+
async dispatchTransactional(event, payload) {
|
|
76
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
77
|
+
await listener(payload);
|
|
78
|
+
}
|
|
61
79
|
listeners = new Map;
|
|
62
80
|
on(event, listener) {
|
|
63
81
|
return this.listen(event, listener);
|
|
@@ -168,7 +168,7 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
|
|
|
168
168
|
"GET /scim/v2/Groups": "SCIM list groups (departments)"
|
|
169
169
|
};
|
|
170
170
|
function toOpenApiPath(path) {
|
|
171
|
-
return path.replace(/:([A-Za-z_]
|
|
171
|
+
return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
|
|
172
172
|
}
|
|
173
173
|
function toRelativeApiPath(path) {
|
|
174
174
|
const prefix = apiPrefix();
|
|
@@ -196,6 +196,16 @@ function requiresBearerAuth(path, method) {
|
|
|
196
196
|
}
|
|
197
197
|
return relative.startsWith("/auth/") || relative.startsWith("/users/me") || path.startsWith("/users/me") || relative === "/user" || relative.startsWith("/integrations/") || relative.startsWith("/audit-logs") || ["POST", "PATCH", "PUT", "DELETE"].includes(method);
|
|
198
198
|
}
|
|
199
|
+
var DEFAULT_RESPONSES = {
|
|
200
|
+
"200": { description: "OK" },
|
|
201
|
+
"201": { description: "Created" },
|
|
202
|
+
"204": { description: "No Content" },
|
|
203
|
+
"400": { description: "Bad Request" },
|
|
204
|
+
"401": { description: "Unauthorized" },
|
|
205
|
+
"403": { description: "Forbidden" },
|
|
206
|
+
"404": { description: "Not Found" },
|
|
207
|
+
"422": { description: "Validation Error" }
|
|
208
|
+
};
|
|
199
209
|
function generateOpenApiSpec(routes) {
|
|
200
210
|
const paths = {};
|
|
201
211
|
for (const route of routes) {
|
|
@@ -206,16 +216,8 @@ function generateOpenApiSpec(routes) {
|
|
|
206
216
|
paths[openApiPath][method] = {
|
|
207
217
|
summary: description,
|
|
208
218
|
...requiresBearerAuth(route.path, route.method) ? { security: [{ bearerAuth: [] }] } : {},
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"201": { description: "Created" },
|
|
212
|
-
"204": { description: "No Content" },
|
|
213
|
-
"400": { description: "Bad Request" },
|
|
214
|
-
"401": { description: "Unauthorized" },
|
|
215
|
-
"403": { description: "Forbidden" },
|
|
216
|
-
"404": { description: "Not Found" },
|
|
217
|
-
"422": { description: "Validation Error" }
|
|
218
|
-
}
|
|
219
|
+
...route.openApi,
|
|
220
|
+
responses: route.openApi?.responses ?? structuredClone(DEFAULT_RESPONSES)
|
|
219
221
|
};
|
|
220
222
|
}
|
|
221
223
|
return {
|
|
@@ -297,10 +299,38 @@ function renderTypeScriptSdk(spec, prefix = apiPrefix()) {
|
|
|
297
299
|
" }",
|
|
298
300
|
""
|
|
299
301
|
];
|
|
302
|
+
const contracts = Object.fromEntries(Object.entries(spec.paths).map(([path, methods]) => [
|
|
303
|
+
path,
|
|
304
|
+
Object.fromEntries(Object.entries(methods).filter(([, value]) => {
|
|
305
|
+
const operation = value;
|
|
306
|
+
return operation.parameters || operation.requestBody || operation.operationId || JSON.stringify(operation.responses) !== JSON.stringify(DEFAULT_RESPONSES);
|
|
307
|
+
}))
|
|
308
|
+
]).filter(([, methods]) => Object.keys(methods).length));
|
|
309
|
+
if (Object.keys(contracts).length)
|
|
310
|
+
lines.splice(1, 0, ` static readonly operationContracts = ${JSON.stringify(contracts)} as const;`);
|
|
300
311
|
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
301
312
|
const requestPath = toRequestPath(path, prefix);
|
|
302
|
-
for (const method of Object.
|
|
313
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
314
|
+
const metadata = operation;
|
|
315
|
+
const headers = metadata.parameters?.filter((parameter) => parameter.in === "header") ?? [];
|
|
303
316
|
const functionName = toMethodName(method, path, prefix);
|
|
317
|
+
const pathParameters = metadata.parameters?.filter((parameter) => parameter.in === "path") ?? [];
|
|
318
|
+
const queryParameters = metadata.parameters?.filter((parameter) => parameter.in === "query") ?? [];
|
|
319
|
+
const groups = [
|
|
320
|
+
{ field: "operationHeaders", parameters: headers },
|
|
321
|
+
{ field: "operationPath", parameters: pathParameters },
|
|
322
|
+
{ field: "operationQuery", parameters: queryParameters }
|
|
323
|
+
].filter((group) => group.parameters.length);
|
|
324
|
+
if (groups.length) {
|
|
325
|
+
const type = groups.map((group) => `${group.field}${group.parameters.some((parameter) => parameter.required) ? "" : "?"}: { ${group.parameters.map((parameter) => `${JSON.stringify(parameter.name)}${parameter.required ? "" : "?"}: ${parameter.schema?.type === "integer" || parameter.schema?.type === "number" ? "number" : parameter.schema?.type === "boolean" ? "boolean" : "string"}`).join("; ")} }`).join("; ");
|
|
326
|
+
const required = groups.some((group) => group.parameters.some((parameter) => parameter.required));
|
|
327
|
+
lines.push(` async ${functionName}(init: RequestInit & { ${type} }${required ? "" : " = {}"}): Promise<Response> {`, " const headers = new Headers(init.headers);", ` let path = ${JSON.stringify(requestPath)};`, ...groups.flatMap((group) => group.parameters.filter((parameter) => parameter.required).map((parameter) => ` if (init.${group.field}?.[${JSON.stringify(parameter.name)}] === undefined) throw new TypeError(${JSON.stringify(`Required operation ${group.field === "operationHeaders" ? "header" : group.field === "operationPath" ? "path parameter" : "query parameter"}: ${parameter.name}`)});`)), ...headers.map((parameter) => ` if (init.operationHeaders?.[${JSON.stringify(parameter.name)}] !== undefined) headers.set(${JSON.stringify(parameter.name)}, String(init.operationHeaders[${JSON.stringify(parameter.name)}]));`), ...pathParameters.map((parameter) => ` if (init.operationPath?.[${JSON.stringify(parameter.name)}] !== undefined) path = path.replaceAll(${JSON.stringify(`{${parameter.name}}`)}, encodeURIComponent(String(init.operationPath[${JSON.stringify(parameter.name)}])));`), ...queryParameters.length ? [
|
|
328
|
+
" const query = new URLSearchParams();",
|
|
329
|
+
...queryParameters.map((parameter) => ` if (init.operationQuery?.[${JSON.stringify(parameter.name)}] !== undefined) query.set(${JSON.stringify(parameter.name)}, String(init.operationQuery[${JSON.stringify(parameter.name)}]));`),
|
|
330
|
+
" if (query.size) path += '?' + query.toString();"
|
|
331
|
+
] : [], ` const { ${groups.map((group) => `${group.field}: _${group.field}`).join(", ")}, ...requestInit } = init;`, ` return await this.request(path, { ...requestInit, headers, method: ${JSON.stringify(method.toUpperCase())} });`, " }", "");
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
304
334
|
lines.push(` async ${functionName}(init: RequestInit = {}): Promise<Response> {`, ` return await this.request("${requestPath}", { ...init, method: "${method.toUpperCase()}" });`, " }", "");
|
|
305
335
|
}
|
|
306
336
|
}
|
|
@@ -13,6 +13,16 @@ function validateOpenApiSpec(spec) {
|
|
|
13
13
|
errors.push(`Path must start with '/': ${path}`);
|
|
14
14
|
}
|
|
15
15
|
for (const [method, operation] of Object.entries(methods)) {
|
|
16
|
+
const details = operation;
|
|
17
|
+
const seen = new Set;
|
|
18
|
+
for (const parameter of details.parameters ?? []) {
|
|
19
|
+
const identity = `${parameter.in}:${parameter.in === "header" ? parameter.name.toLowerCase() : parameter.name}`;
|
|
20
|
+
if (seen.has(identity))
|
|
21
|
+
errors.push(`Duplicate parameter ${parameter.name} on ${method.toUpperCase()} ${path}.`);
|
|
22
|
+
seen.add(identity);
|
|
23
|
+
if (parameter.in === "path" && (!parameter.required || !path.includes(`{${parameter.name}}`)))
|
|
24
|
+
errors.push(`Path parameter ${parameter.name} must be required and match ${path}.`);
|
|
25
|
+
}
|
|
16
26
|
if (!("responses" in operation)) {
|
|
17
27
|
errors.push(`Operation ${method.toUpperCase()} ${path} is missing responses.`);
|
|
18
28
|
}
|
|
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
|
|
|
58
58
|
// ../../src/core/events/eventBus.ts
|
|
59
59
|
class EventBus {
|
|
60
60
|
constructor() {}
|
|
61
|
+
transactionalListeners = new Map;
|
|
62
|
+
listenTransactional(event, listener) {
|
|
63
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
64
|
+
handlers.add(listener);
|
|
65
|
+
this.transactionalListeners.set(event, handlers);
|
|
66
|
+
return () => {
|
|
67
|
+
handlers.delete(listener);
|
|
68
|
+
if (!handlers.size)
|
|
69
|
+
this.transactionalListeners.delete(event);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
hasTransactionalListeners(event) {
|
|
73
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
74
|
+
}
|
|
75
|
+
async dispatchTransactional(event, payload) {
|
|
76
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
77
|
+
await listener(payload);
|
|
78
|
+
}
|
|
61
79
|
listeners = new Map;
|
|
62
80
|
on(event, listener) {
|
|
63
81
|
return this.listen(event, listener);
|
|
@@ -133,6 +133,24 @@ var init_tenantContext = __esm(() => {
|
|
|
133
133
|
// ../../src/core/events/eventBus.ts
|
|
134
134
|
class EventBus {
|
|
135
135
|
constructor() {}
|
|
136
|
+
transactionalListeners = new Map;
|
|
137
|
+
listenTransactional(event, listener) {
|
|
138
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
139
|
+
handlers.add(listener);
|
|
140
|
+
this.transactionalListeners.set(event, handlers);
|
|
141
|
+
return () => {
|
|
142
|
+
handlers.delete(listener);
|
|
143
|
+
if (!handlers.size)
|
|
144
|
+
this.transactionalListeners.delete(event);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
hasTransactionalListeners(event) {
|
|
148
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
149
|
+
}
|
|
150
|
+
async dispatchTransactional(event, payload) {
|
|
151
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
152
|
+
await listener(payload);
|
|
153
|
+
}
|
|
136
154
|
listeners = new Map;
|
|
137
155
|
on(event, listener) {
|
|
138
156
|
return this.listen(event, listener);
|
|
@@ -81,7 +81,7 @@ export { conditionalJsonResponse } from "../core/http/conditionalResponse.ts";
|
|
|
81
81
|
export type { ContentSecurityPolicyDirectives, ContentSecurityPolicyOptions, } from "../core/http/contentSecurityPolicy.ts";
|
|
82
82
|
export { configureContentSecurityPolicy, generateCspNonce, HTMX_2_0_4_INDICATOR_STYLE_HASH, resetContentSecurityPolicyForTests, resolveContentSecurityPolicy, resolveHtmlContentSecurityPolicy, serverHtmxContentSecurityPolicy, spaContentSecurityPolicy, strictApiContentSecurityPolicy, } from "../core/http/contentSecurityPolicy.ts";
|
|
83
83
|
export { readBunRequestCookie, readRequestCookie } from "../core/http/cookies.ts";
|
|
84
|
-
export { createCorsMiddleware } from "../core/http/corsMiddleware.ts";
|
|
84
|
+
export { type CorsConfig, type CorsOptions, createCorsMiddleware, } from "../core/http/corsMiddleware.ts";
|
|
85
85
|
export { createCsrfMiddleware } from "../core/http/csrfMiddleware.ts";
|
|
86
86
|
export { createCsrfProtection } from "../core/http/csrfProtection.ts";
|
|
87
87
|
export { createCsrfTokenCookie, readSubmittedCsrfToken, readSubmittedCsrfTokenFromBody, resolveCsrfToken, resolveCsrfTokenForRequest, verifyCsrfToken, } from "../core/http/csrfToken.ts";
|
package/dist/index.js
CHANGED
|
@@ -535,6 +535,24 @@ var init_transactionControl = __esm(() => {
|
|
|
535
535
|
// ../../src/core/events/eventBus.ts
|
|
536
536
|
class EventBus {
|
|
537
537
|
constructor() {}
|
|
538
|
+
transactionalListeners = new Map;
|
|
539
|
+
listenTransactional(event, listener) {
|
|
540
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
541
|
+
handlers.add(listener);
|
|
542
|
+
this.transactionalListeners.set(event, handlers);
|
|
543
|
+
return () => {
|
|
544
|
+
handlers.delete(listener);
|
|
545
|
+
if (!handlers.size)
|
|
546
|
+
this.transactionalListeners.delete(event);
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
hasTransactionalListeners(event) {
|
|
550
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
551
|
+
}
|
|
552
|
+
async dispatchTransactional(event, payload) {
|
|
553
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
554
|
+
await listener(payload);
|
|
555
|
+
}
|
|
538
556
|
listeners = new Map;
|
|
539
557
|
on(event, listener) {
|
|
540
558
|
return this.listen(event, listener);
|
|
@@ -621,6 +639,7 @@ async function dispatchQueuedModelEvents(queued) {
|
|
|
621
639
|
}
|
|
622
640
|
}
|
|
623
641
|
async function dispatchModelEvent(event, payload) {
|
|
642
|
+
await eventBus.dispatchTransactional(event, payload);
|
|
624
643
|
const state = deferredModelEvents.getStore();
|
|
625
644
|
const frame = state?.frames[state.frames.length - 1];
|
|
626
645
|
if (frame) {
|
|
@@ -3810,6 +3829,9 @@ class ConfigStore {
|
|
|
3810
3829
|
return this.values.has(key);
|
|
3811
3830
|
}
|
|
3812
3831
|
}
|
|
3832
|
+
// ../../src/core/database/baseRepository.ts
|
|
3833
|
+
init_eventBus();
|
|
3834
|
+
|
|
3813
3835
|
// ../../src/core/events/index.ts
|
|
3814
3836
|
init_deferredModelEvents();
|
|
3815
3837
|
init_eventBus();
|
|
@@ -3828,6 +3850,9 @@ function buildPaginationMeta(input) {
|
|
|
3828
3850
|
};
|
|
3829
3851
|
}
|
|
3830
3852
|
|
|
3853
|
+
// ../../src/core/database/baseRepository.ts
|
|
3854
|
+
init_connectionContext();
|
|
3855
|
+
|
|
3831
3856
|
// ../../src/core/database/query.ts
|
|
3832
3857
|
init_dialect();
|
|
3833
3858
|
function quoteIdentifier(identifier) {
|
|
@@ -4952,6 +4977,9 @@ class RepositoryQuery {
|
|
|
4952
4977
|
}
|
|
4953
4978
|
|
|
4954
4979
|
// ../../src/core/database/baseRepository.ts
|
|
4980
|
+
init_transaction();
|
|
4981
|
+
init_transactionControl();
|
|
4982
|
+
|
|
4955
4983
|
class BaseRepository {
|
|
4956
4984
|
table;
|
|
4957
4985
|
connection;
|
|
@@ -4959,6 +4987,17 @@ class BaseRepository {
|
|
|
4959
4987
|
this.table = table;
|
|
4960
4988
|
this.connection = connection;
|
|
4961
4989
|
}
|
|
4990
|
+
assertTransactionalEvent(action) {
|
|
4991
|
+
if (!eventBus.hasTransactionalListeners(modelEventName(this.table.name, action)))
|
|
4992
|
+
return;
|
|
4993
|
+
if (!hasActiveTransaction() || this.connection !== repositoryConnection && this.connection !== getActiveDatabaseConnection(repositoryConnection))
|
|
4994
|
+
throw new Error("Transactional model listeners require runInTransaction and the framework-bound repository connection before writing.");
|
|
4995
|
+
}
|
|
4996
|
+
async runModelMutation(action, operation) {
|
|
4997
|
+
if (!eventBus.hasTransactionalListeners(modelEventName(this.table.name, action)))
|
|
4998
|
+
return operation();
|
|
4999
|
+
return runInTransaction(operation);
|
|
5000
|
+
}
|
|
4962
5001
|
async count(options = {}) {
|
|
4963
5002
|
const { whereNodes, where, ...rest } = options;
|
|
4964
5003
|
return await this.countWhere(where ?? {}, rest, whereNodes ?? []);
|
|
@@ -5116,7 +5155,8 @@ class BaseRepository {
|
|
|
5116
5155
|
return await this.incrementById(id, column, -amount, extra);
|
|
5117
5156
|
}
|
|
5118
5157
|
async create(values) {
|
|
5119
|
-
|
|
5158
|
+
this.assertTransactionalEvent("created");
|
|
5159
|
+
return await this.runModelMutation("created", () => withDatabaseErrorHandling(async () => {
|
|
5120
5160
|
const { text, params } = buildInsertQuery(this.table, values);
|
|
5121
5161
|
const [record] = await this.connection.unsafe(text, params);
|
|
5122
5162
|
if (!record) {
|
|
@@ -5125,10 +5165,11 @@ class BaseRepository {
|
|
|
5125
5165
|
const entity = record;
|
|
5126
5166
|
await dispatchModelEvent(modelEventName(this.table.name, "created"), entity);
|
|
5127
5167
|
return entity;
|
|
5128
|
-
});
|
|
5168
|
+
}));
|
|
5129
5169
|
}
|
|
5130
5170
|
async updateById(id, changes) {
|
|
5131
|
-
|
|
5171
|
+
this.assertTransactionalEvent("updated");
|
|
5172
|
+
return await this.runModelMutation("updated", () => withDatabaseErrorHandling(async () => {
|
|
5132
5173
|
const { text, params } = buildUpdateQuery(this.table, id, changes);
|
|
5133
5174
|
const [record] = await this.connection.unsafe(text, params);
|
|
5134
5175
|
const entity = record ?? null;
|
|
@@ -5136,7 +5177,7 @@ class BaseRepository {
|
|
|
5136
5177
|
await dispatchModelEvent(modelEventName(this.table.name, "updated"), entity);
|
|
5137
5178
|
}
|
|
5138
5179
|
return entity;
|
|
5139
|
-
});
|
|
5180
|
+
}));
|
|
5140
5181
|
}
|
|
5141
5182
|
async updateByIdOrThrow(id, changes, errorFactory) {
|
|
5142
5183
|
const record = await this.updateById(id, changes);
|
|
@@ -5152,7 +5193,8 @@ class BaseRepository {
|
|
|
5152
5193
|
return await this.forceDeleteById(id);
|
|
5153
5194
|
}
|
|
5154
5195
|
async softDeleteById(id) {
|
|
5155
|
-
|
|
5196
|
+
this.assertTransactionalEvent("deleted");
|
|
5197
|
+
return await this.runModelMutation("deleted", () => withDatabaseErrorHandling(async () => {
|
|
5156
5198
|
const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
|
|
5157
5199
|
const [record] = await this.connection.unsafe(text, params);
|
|
5158
5200
|
if (!record) {
|
|
@@ -5160,10 +5202,11 @@ class BaseRepository {
|
|
|
5160
5202
|
}
|
|
5161
5203
|
await dispatchModelEvent(modelEventName(this.table.name, "deleted"), record);
|
|
5162
5204
|
return true;
|
|
5163
|
-
});
|
|
5205
|
+
}));
|
|
5164
5206
|
}
|
|
5165
5207
|
async forceDeleteById(id) {
|
|
5166
|
-
|
|
5208
|
+
this.assertTransactionalEvent("force-deleted");
|
|
5209
|
+
return await this.runModelMutation("force-deleted", () => withDatabaseErrorHandling(async () => {
|
|
5167
5210
|
const { text, params } = buildDeleteByIdQuery(this.table, id);
|
|
5168
5211
|
const [row] = await this.connection.unsafe(text, params);
|
|
5169
5212
|
if (!row) {
|
|
@@ -5173,10 +5216,11 @@ class BaseRepository {
|
|
|
5173
5216
|
id
|
|
5174
5217
|
});
|
|
5175
5218
|
return true;
|
|
5176
|
-
});
|
|
5219
|
+
}));
|
|
5177
5220
|
}
|
|
5178
5221
|
async restoreById(id) {
|
|
5179
|
-
|
|
5222
|
+
this.assertTransactionalEvent("restored");
|
|
5223
|
+
return await this.runModelMutation("restored", () => withDatabaseErrorHandling(async () => {
|
|
5180
5224
|
const { text, params } = buildRestoreByIdQuery(this.table, id);
|
|
5181
5225
|
const [record] = await this.connection.unsafe(text, params);
|
|
5182
5226
|
if (!record) {
|
|
@@ -5185,7 +5229,7 @@ class BaseRepository {
|
|
|
5185
5229
|
const entity = record;
|
|
5186
5230
|
await dispatchModelEvent(modelEventName(this.table.name, "restored"), entity);
|
|
5187
5231
|
return entity;
|
|
5188
|
-
});
|
|
5232
|
+
}));
|
|
5189
5233
|
}
|
|
5190
5234
|
withConnection(connection) {
|
|
5191
5235
|
const clone = Object.create(Object.getPrototypeOf(this));
|
|
@@ -8417,6 +8461,10 @@ class SqlOutbox {
|
|
|
8417
8461
|
this.listeners.set(listener.name, listener);
|
|
8418
8462
|
}
|
|
8419
8463
|
}
|
|
8464
|
+
hasListener(name, event) {
|
|
8465
|
+
const listener = this.listeners.get(name);
|
|
8466
|
+
return listener !== undefined && (event === undefined || listener.event === event);
|
|
8467
|
+
}
|
|
8420
8468
|
async publish(name, payload, options = {}) {
|
|
8421
8469
|
if (!hasActiveTransaction())
|
|
8422
8470
|
throw new Error("Durable publication requires an active business transaction.");
|
|
@@ -9418,10 +9466,28 @@ function readBunRequestCookie(request, name) {
|
|
|
9418
9466
|
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
9419
9467
|
}
|
|
9420
9468
|
// ../../src/core/http/corsMiddleware.ts
|
|
9469
|
+
function additionalHeaders(options) {
|
|
9470
|
+
const configured = process.env.CORS_ADDITIONAL_ALLOWED_HEADERS?.trim();
|
|
9471
|
+
const supplied = options.additionalAllowedHeaders === undefined ? [] : options.additionalAllowedHeaders;
|
|
9472
|
+
if (!Array.isArray(supplied)) {
|
|
9473
|
+
throw new TypeError("CORS additionalAllowedHeaders must be an array of header names.");
|
|
9474
|
+
}
|
|
9475
|
+
const names = [
|
|
9476
|
+
...configured ? configured.split(",").map((name) => name.trim()) : [],
|
|
9477
|
+
...supplied
|
|
9478
|
+
];
|
|
9479
|
+
return names.map((name) => {
|
|
9480
|
+
if (typeof name !== "string" || name === "*" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
|
|
9481
|
+
throw new TypeError("CORS additional headers must be explicit valid HTTP header names; wildcards are not allowed.");
|
|
9482
|
+
}
|
|
9483
|
+
return name;
|
|
9484
|
+
});
|
|
9485
|
+
}
|
|
9421
9486
|
function defaultAllowedOrigins() {
|
|
9422
9487
|
return appUrl();
|
|
9423
9488
|
}
|
|
9424
|
-
function resolveCorsConfig() {
|
|
9489
|
+
function resolveCorsConfig(options = {}) {
|
|
9490
|
+
const extraHeaders = additionalHeaders(options);
|
|
9425
9491
|
return {
|
|
9426
9492
|
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? defaultAllowedOrigins()).split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
9427
9493
|
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
@@ -9434,22 +9500,27 @@ function resolveCorsConfig() {
|
|
|
9434
9500
|
"X-Authenticated-User-Role",
|
|
9435
9501
|
"If-Match",
|
|
9436
9502
|
"If-None-Match",
|
|
9437
|
-
"X-CSRF-Token"
|
|
9438
|
-
|
|
9503
|
+
"X-CSRF-Token",
|
|
9504
|
+
...extraHeaders
|
|
9505
|
+
].filter((name, index, names) => names.findIndex((other) => other.toLowerCase() === name.toLowerCase()) === index),
|
|
9439
9506
|
maxAgeSeconds: 86400
|
|
9440
9507
|
};
|
|
9441
9508
|
}
|
|
9442
|
-
function createCorsMiddleware() {
|
|
9509
|
+
function createCorsMiddleware(options = {}) {
|
|
9510
|
+
resolveCorsConfig(options);
|
|
9511
|
+
const captured = {
|
|
9512
|
+
additionalAllowedHeaders: [...options.additionalAllowedHeaders ?? []]
|
|
9513
|
+
};
|
|
9443
9514
|
return async (request, next) => {
|
|
9444
9515
|
if (request.method === "OPTIONS") {
|
|
9445
9516
|
return new Response(null, {
|
|
9446
9517
|
status: 204,
|
|
9447
|
-
headers: buildCorsHeaders(request)
|
|
9518
|
+
headers: buildCorsHeaders(request, captured)
|
|
9448
9519
|
});
|
|
9449
9520
|
}
|
|
9450
9521
|
const response = await next();
|
|
9451
9522
|
const headers = new Headers(response.headers);
|
|
9452
|
-
for (const [key, value] of buildCorsHeaders(request)) {
|
|
9523
|
+
for (const [key, value] of buildCorsHeaders(request, captured)) {
|
|
9453
9524
|
headers.set(key, value);
|
|
9454
9525
|
}
|
|
9455
9526
|
return new Response(response.body, {
|
|
@@ -9459,10 +9530,10 @@ function createCorsMiddleware() {
|
|
|
9459
9530
|
});
|
|
9460
9531
|
};
|
|
9461
9532
|
}
|
|
9462
|
-
function buildCorsHeaders(request) {
|
|
9533
|
+
function buildCorsHeaders(request, options) {
|
|
9463
9534
|
const headers = new Headers;
|
|
9464
9535
|
const origin = request.headers.get("origin");
|
|
9465
|
-
const corsConfig = resolveCorsConfig();
|
|
9536
|
+
const corsConfig = resolveCorsConfig(options);
|
|
9466
9537
|
const allowedOrigins = corsConfig.allowedOrigins;
|
|
9467
9538
|
headers.set("Vary", "Origin");
|
|
9468
9539
|
const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
|