@getstrata/bootstrap 2.0.2 → 2.0.4
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/bootstrap/buildModuleRoutes.d.ts +2 -1
- package/dist/bootstrap/contracts.d.ts +4 -0
- package/dist/bootstrap/httpKernel.d.ts +1 -1
- package/dist/bootstrap/listeners/invalidateCacheOnModelWrite.d.ts +11 -2
- package/dist/entries/buildModuleRoutes.js +31 -15
- package/dist/entries/buildWebModuleRoutes.js +32 -16
- package/dist/entries/context.js +83 -7
- package/dist/entries/createWebRoutes.js +32 -16
- package/dist/entries/dependencies.js +83 -7
- package/dist/entries/httpKernel.js +8 -2
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +84 -7
- package/dist/entries/providers.js +83 -7
- package/dist/entries/web/routing.js +8 -2
- package/dist/index.js +100 -23
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @getstrata/bootstrap changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.4
|
|
4
|
+
|
|
5
|
+
- Place cache rollout guidance beside outbox docs without overlapping API additions
|
|
6
|
+
|
|
7
|
+
## 2.0.3
|
|
8
|
+
|
|
9
|
+
- Persist transactional events with leased tenant-aware SQL outbox workers
|
|
10
|
+
|
|
3
11
|
## 2.0.2
|
|
4
12
|
|
|
5
13
|
- Adopt TypeScript 7.0.2 in framework and generated apps; preserve tested TypeScript 5.9 and 6.x consumer compatibility.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { OpenApiRouteMap } from "@getstrata/core/openapi/registeredRoute";
|
|
1
2
|
import type { AppDependencies, AppModule, AppRouteMap } from "./contracts";
|
|
2
3
|
interface BuildModuleRoutesOptions {
|
|
3
4
|
apiPrefix?: string;
|
|
4
5
|
modules?: AppModule[];
|
|
5
6
|
clearRegistry?: boolean;
|
|
6
7
|
}
|
|
7
|
-
declare function registerOpenApiRouteMap(routes: Record<string, unknown>, middleware: string[]): Record<string, unknown>;
|
|
8
|
+
declare function registerOpenApiRouteMap(routes: Record<string, unknown>, middleware: string[], metadata?: OpenApiRouteMap): Record<string, unknown>;
|
|
8
9
|
declare function buildModuleRoutes(dependencies: AppDependencies, options?: BuildModuleRoutesOptions): AppRouteMap;
|
|
9
10
|
export type { BuildModuleRoutesOptions };
|
|
10
11
|
export { buildModuleRoutes, registerOpenApiRouteMap };
|
|
@@ -8,7 +8,10 @@ interface ModuleRouteContext {
|
|
|
8
8
|
cachedJson: CachedJson;
|
|
9
9
|
kernel: HttpKernel;
|
|
10
10
|
}
|
|
11
|
+
import type { OpenApiRouteMap } from "@getstrata/core/openapi/registeredRoute";
|
|
11
12
|
interface AppModule {
|
|
13
|
+
/** Paths match this module's API route map before apiPrefix is applied. */
|
|
14
|
+
openApi?: OpenApiRouteMap;
|
|
12
15
|
name: string;
|
|
13
16
|
order?: number;
|
|
14
17
|
tableName?: string;
|
|
@@ -18,4 +21,5 @@ interface AppModule {
|
|
|
18
21
|
routes?(context: ModuleRouteContext): AppRouteMap;
|
|
19
22
|
webRoutes?(context: ModuleRouteContext): AppRouteMap;
|
|
20
23
|
}
|
|
24
|
+
export type { OpenApiOperation, OpenApiRouteMap, OpenApiSchema, } from "@getstrata/core/openapi/registeredRoute";
|
|
21
25
|
export type { AppModule, ModuleRouteContext };
|
|
@@ -7,7 +7,7 @@ type WebGuestHome = string | ((user: AuthUser) => string | Promise<string>);
|
|
|
7
7
|
declare class HttpKernel {
|
|
8
8
|
private readonly dependencies;
|
|
9
9
|
constructor(dependencies: AppDependencies);
|
|
10
|
-
globalMiddleware(): Middleware[];
|
|
10
|
+
globalMiddleware(errorFormat?: "json" | "web"): Middleware[];
|
|
11
11
|
group(name: MiddlewareGroupName): Middleware[];
|
|
12
12
|
wrap(groups: MiddlewareGroupName | MiddlewareGroupName[], handler: RouteHandler): RouteHandler;
|
|
13
13
|
wrapApi(handler: RouteHandler): RouteHandler;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import { type EventBus } from "@getstrata/core/events";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { DurableListener, SqlOutbox } from "@getstrata/core/events/outbox";
|
|
3
|
+
import type { CacheLike } from "../../types/services";
|
|
4
|
+
interface ModelCacheInvalidationOptions {
|
|
5
|
+
outbox?: SqlOutbox;
|
|
6
|
+
}
|
|
7
|
+
/** Include in the application's one outbox registry; never dispatch through Redis queues. */
|
|
8
|
+
declare function createModelCacheInvalidationListener(resolveCache?: () => CacheLike): DurableListener;
|
|
9
|
+
/** Durable mode requires the outbox schema and an explicit business transaction. */
|
|
10
|
+
declare function registerInvalidateCacheOnModelWriteListeners(bus?: EventBus, options?: ModelCacheInvalidationOptions): () => void;
|
|
11
|
+
export type { ModelCacheInvalidationOptions };
|
|
12
|
+
export { createModelCacheInvalidationListener, registerInvalidateCacheOnModelWriteListeners };
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
3
3
|
import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
|
|
4
4
|
import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
|
|
5
|
-
import { createJsonErrorMiddleware } from "@getstrata/core/http/response";
|
|
6
5
|
|
|
7
6
|
// ../../src/bootstrap/httpKernel.ts
|
|
8
7
|
import {
|
|
@@ -27,7 +26,12 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
27
26
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
28
27
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
29
28
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
30
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createJsonErrorMiddleware,
|
|
31
|
+
createWebErrorMiddleware,
|
|
32
|
+
withErrorHandling,
|
|
33
|
+
withJsonErrorHandling
|
|
34
|
+
} from "@getstrata/core/http/response";
|
|
31
35
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
32
36
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
33
37
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -116,7 +120,7 @@ class HttpKernel {
|
|
|
116
120
|
constructor(dependencies) {
|
|
117
121
|
this.dependencies = dependencies;
|
|
118
122
|
}
|
|
119
|
-
globalMiddleware() {
|
|
123
|
+
globalMiddleware(errorFormat = "json") {
|
|
120
124
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
121
125
|
return [
|
|
122
126
|
createCorsMiddleware(),
|
|
@@ -126,6 +130,7 @@ class HttpKernel {
|
|
|
126
130
|
createMetricsMiddleware(),
|
|
127
131
|
createRequestLoggingMiddleware(),
|
|
128
132
|
requestIdMiddleware,
|
|
133
|
+
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
129
134
|
createAuthMiddleware(auth),
|
|
130
135
|
createMembershipMiddleware(),
|
|
131
136
|
createTenantMiddleware()
|
|
@@ -423,22 +428,22 @@ function readSharedRouteRegistry() {
|
|
|
423
428
|
var routeRegistry = readSharedRouteRegistry();
|
|
424
429
|
|
|
425
430
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
426
|
-
function registerOpenApiRoute(method, path, middleware) {
|
|
427
|
-
routeRegistry.register({ method, path, middleware });
|
|
431
|
+
function registerOpenApiRoute(method, path, middleware, openApi) {
|
|
432
|
+
routeRegistry.register({ method, path, middleware, ...openApi ? { openApi } : {} });
|
|
428
433
|
}
|
|
429
|
-
function registerOpenApiRouteMap(routes, middleware) {
|
|
434
|
+
function registerOpenApiRouteMap(routes, middleware, metadata = {}) {
|
|
430
435
|
const registered = {};
|
|
431
436
|
for (const [path, handler] of Object.entries(routes)) {
|
|
432
437
|
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
433
438
|
const methodMap = handler;
|
|
434
439
|
registered[path] = methodMap;
|
|
435
440
|
for (const method of Object.keys(methodMap)) {
|
|
436
|
-
registerOpenApiRoute(method.toUpperCase(), path, middleware);
|
|
441
|
+
registerOpenApiRoute(method.toUpperCase(), path, middleware, metadata[path]?.[method.toUpperCase()]);
|
|
437
442
|
}
|
|
438
443
|
continue;
|
|
439
444
|
}
|
|
440
445
|
registered[path] = handler;
|
|
441
|
-
registerOpenApiRoute("GET", path, middleware);
|
|
446
|
+
registerOpenApiRoute("GET", path, middleware, metadata[path]?.GET);
|
|
442
447
|
}
|
|
443
448
|
return registered;
|
|
444
449
|
}
|
|
@@ -454,21 +459,32 @@ function buildModuleRoutes(dependencies, options = {}) {
|
|
|
454
459
|
routeRegistry.clear();
|
|
455
460
|
}
|
|
456
461
|
const kernel = createHttpKernel(dependencies);
|
|
457
|
-
const middleware = [
|
|
458
|
-
...kernel.globalMiddleware(),
|
|
459
|
-
createJsonErrorMiddleware(),
|
|
460
|
-
...kernel.group("api")
|
|
461
|
-
];
|
|
462
|
+
const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
|
|
462
463
|
const cachedJson = createCachedJson(dependencies);
|
|
463
464
|
const moduleRoutes = {};
|
|
465
|
+
const metadata = {};
|
|
464
466
|
for (const module of modules) {
|
|
465
467
|
if (!module.routes) {
|
|
466
468
|
continue;
|
|
467
469
|
}
|
|
468
|
-
|
|
470
|
+
const routes = module.routes({ dependencies, cachedJson, kernel });
|
|
471
|
+
for (const [path, operations] of Object.entries(module.openApi ?? {})) {
|
|
472
|
+
if (!(path in routes))
|
|
473
|
+
throw new Error(`OpenAPI metadata has no API route: ${module.name} ${path}`);
|
|
474
|
+
const methods = typeof routes[path] === "function" ? ["GET"] : Object.keys(routes[path]).map((method) => method.toUpperCase());
|
|
475
|
+
for (const method of Object.keys(operations))
|
|
476
|
+
if (!methods.includes(method))
|
|
477
|
+
throw new Error(`OpenAPI metadata has no API handler: ${method} ${path}`);
|
|
478
|
+
}
|
|
479
|
+
for (const path of Object.keys(routes)) {
|
|
480
|
+
delete metadata[path];
|
|
481
|
+
if (module.openApi?.[path])
|
|
482
|
+
metadata[path] = module.openApi[path];
|
|
483
|
+
}
|
|
484
|
+
Object.assign(moduleRoutes, routes);
|
|
469
485
|
}
|
|
470
486
|
const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
|
|
471
|
-
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
|
|
487
|
+
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"], prefixRouteMap(apiPrefix, metadata)), middleware);
|
|
472
488
|
}
|
|
473
489
|
export {
|
|
474
490
|
buildModuleRoutes,
|
|
@@ -5,7 +5,6 @@ import { applyMiddlewareToRoutes as applyMiddlewareToRoutes2 } from "@getstrata/
|
|
|
5
5
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
6
6
|
import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
|
|
7
7
|
import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
|
|
8
|
-
import { createJsonErrorMiddleware } from "@getstrata/core/http/response";
|
|
9
8
|
|
|
10
9
|
// ../../src/bootstrap/httpKernel.ts
|
|
11
10
|
import {
|
|
@@ -30,7 +29,12 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
30
29
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
31
30
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
32
31
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
33
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
createJsonErrorMiddleware,
|
|
34
|
+
createWebErrorMiddleware,
|
|
35
|
+
withErrorHandling,
|
|
36
|
+
withJsonErrorHandling
|
|
37
|
+
} from "@getstrata/core/http/response";
|
|
34
38
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
35
39
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
36
40
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -119,7 +123,7 @@ class HttpKernel {
|
|
|
119
123
|
constructor(dependencies) {
|
|
120
124
|
this.dependencies = dependencies;
|
|
121
125
|
}
|
|
122
|
-
globalMiddleware() {
|
|
126
|
+
globalMiddleware(errorFormat = "json") {
|
|
123
127
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
124
128
|
return [
|
|
125
129
|
createCorsMiddleware(),
|
|
@@ -129,6 +133,7 @@ class HttpKernel {
|
|
|
129
133
|
createMetricsMiddleware(),
|
|
130
134
|
createRequestLoggingMiddleware(),
|
|
131
135
|
requestIdMiddleware,
|
|
136
|
+
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
132
137
|
createAuthMiddleware(auth),
|
|
133
138
|
createMembershipMiddleware(),
|
|
134
139
|
createTenantMiddleware()
|
|
@@ -426,22 +431,22 @@ function readSharedRouteRegistry() {
|
|
|
426
431
|
var routeRegistry = readSharedRouteRegistry();
|
|
427
432
|
|
|
428
433
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
429
|
-
function registerOpenApiRoute(method, path, middleware) {
|
|
430
|
-
routeRegistry.register({ method, path, middleware });
|
|
434
|
+
function registerOpenApiRoute(method, path, middleware, openApi) {
|
|
435
|
+
routeRegistry.register({ method, path, middleware, ...openApi ? { openApi } : {} });
|
|
431
436
|
}
|
|
432
|
-
function registerOpenApiRouteMap(routes, middleware) {
|
|
437
|
+
function registerOpenApiRouteMap(routes, middleware, metadata = {}) {
|
|
433
438
|
const registered = {};
|
|
434
439
|
for (const [path, handler] of Object.entries(routes)) {
|
|
435
440
|
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
436
441
|
const methodMap = handler;
|
|
437
442
|
registered[path] = methodMap;
|
|
438
443
|
for (const method of Object.keys(methodMap)) {
|
|
439
|
-
registerOpenApiRoute(method.toUpperCase(), path, middleware);
|
|
444
|
+
registerOpenApiRoute(method.toUpperCase(), path, middleware, metadata[path]?.[method.toUpperCase()]);
|
|
440
445
|
}
|
|
441
446
|
continue;
|
|
442
447
|
}
|
|
443
448
|
registered[path] = handler;
|
|
444
|
-
registerOpenApiRoute("GET", path, middleware);
|
|
449
|
+
registerOpenApiRoute("GET", path, middleware, metadata[path]?.GET);
|
|
445
450
|
}
|
|
446
451
|
return registered;
|
|
447
452
|
}
|
|
@@ -457,21 +462,32 @@ function buildModuleRoutes(dependencies, options = {}) {
|
|
|
457
462
|
routeRegistry.clear();
|
|
458
463
|
}
|
|
459
464
|
const kernel = createHttpKernel(dependencies);
|
|
460
|
-
const middleware = [
|
|
461
|
-
...kernel.globalMiddleware(),
|
|
462
|
-
createJsonErrorMiddleware(),
|
|
463
|
-
...kernel.group("api")
|
|
464
|
-
];
|
|
465
|
+
const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
|
|
465
466
|
const cachedJson = createCachedJson(dependencies);
|
|
466
467
|
const moduleRoutes = {};
|
|
468
|
+
const metadata = {};
|
|
467
469
|
for (const module of modules) {
|
|
468
470
|
if (!module.routes) {
|
|
469
471
|
continue;
|
|
470
472
|
}
|
|
471
|
-
|
|
473
|
+
const routes = module.routes({ dependencies, cachedJson, kernel });
|
|
474
|
+
for (const [path, operations] of Object.entries(module.openApi ?? {})) {
|
|
475
|
+
if (!(path in routes))
|
|
476
|
+
throw new Error(`OpenAPI metadata has no API route: ${module.name} ${path}`);
|
|
477
|
+
const methods = typeof routes[path] === "function" ? ["GET"] : Object.keys(routes[path]).map((method) => method.toUpperCase());
|
|
478
|
+
for (const method of Object.keys(operations))
|
|
479
|
+
if (!methods.includes(method))
|
|
480
|
+
throw new Error(`OpenAPI metadata has no API handler: ${method} ${path}`);
|
|
481
|
+
}
|
|
482
|
+
for (const path of Object.keys(routes)) {
|
|
483
|
+
delete metadata[path];
|
|
484
|
+
if (module.openApi?.[path])
|
|
485
|
+
metadata[path] = module.openApi[path];
|
|
486
|
+
}
|
|
487
|
+
Object.assign(moduleRoutes, routes);
|
|
472
488
|
}
|
|
473
489
|
const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
|
|
474
|
-
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
|
|
490
|
+
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"], prefixRouteMap(apiPrefix, metadata)), middleware);
|
|
475
491
|
}
|
|
476
492
|
|
|
477
493
|
// ../../src/bootstrap/buildWebModuleRoutes.ts
|
|
@@ -481,7 +497,7 @@ function buildWebModuleRoutes(dependencies, options = {}) {
|
|
|
481
497
|
routeRegistry.clear();
|
|
482
498
|
}
|
|
483
499
|
const kernel = createHttpKernel(dependencies);
|
|
484
|
-
const middleware = kernel.globalMiddleware();
|
|
500
|
+
const middleware = kernel.globalMiddleware("web");
|
|
485
501
|
const moduleRoutes = { ...seedRoutes };
|
|
486
502
|
for (const module of modules) {
|
|
487
503
|
if (!module.webRoutes) {
|
package/dist/entries/context.js
CHANGED
|
@@ -437,14 +437,88 @@ function discoverModelTableNames() {
|
|
|
437
437
|
|
|
438
438
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
439
439
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
440
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
441
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
442
|
+
function registrations() {
|
|
443
|
+
const global = globalThis;
|
|
444
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
445
|
+
return global[STATE_KEY];
|
|
446
|
+
}
|
|
447
|
+
function requireCacheTags(value) {
|
|
448
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
449
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
function createModelCacheInvalidationListener(resolveCache = resolveApplicationCache) {
|
|
453
|
+
return {
|
|
454
|
+
name: "strata.cache.invalidate-tags.v1",
|
|
455
|
+
event: CACHE_EVENT,
|
|
456
|
+
async handle(event, { signal }) {
|
|
457
|
+
const payload = event.payload;
|
|
458
|
+
if (event.version !== 1)
|
|
459
|
+
throw new Error("Invalid durable cache invalidation version.");
|
|
460
|
+
const tags = requireCacheTags(payload?.tags);
|
|
461
|
+
signal.throwIfAborted();
|
|
462
|
+
await new InvalidateCacheTagsJob(resolveCache()).handle({ tags });
|
|
463
|
+
signal.throwIfAborted();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options = {}) {
|
|
468
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
469
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
470
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
471
|
+
tableName,
|
|
472
|
+
action,
|
|
473
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
474
|
+
})));
|
|
475
|
+
if (options.outbox) {
|
|
476
|
+
for (const write of writes)
|
|
477
|
+
if (write.tags.length)
|
|
478
|
+
requireCacheTags(write.tags);
|
|
479
|
+
}
|
|
480
|
+
const state = registrations();
|
|
481
|
+
const previous = state.get(bus);
|
|
482
|
+
if (previous?.durable && !options.outbox)
|
|
483
|
+
return () => {};
|
|
484
|
+
previous?.suspend();
|
|
485
|
+
const unsubscribe = [];
|
|
486
|
+
const subscribe = [];
|
|
487
|
+
const registration = {
|
|
488
|
+
durable: !!options.outbox,
|
|
489
|
+
suspend() {
|
|
490
|
+
for (const stop of unsubscribe.splice(0))
|
|
491
|
+
stop();
|
|
492
|
+
},
|
|
493
|
+
resume() {
|
|
494
|
+
for (const start of subscribe)
|
|
495
|
+
unsubscribe.push(start());
|
|
496
|
+
state.set(bus, registration);
|
|
497
|
+
},
|
|
498
|
+
dispose() {
|
|
499
|
+
if (state.get(bus) !== registration)
|
|
500
|
+
return;
|
|
501
|
+
registration.suspend();
|
|
502
|
+
if (previous)
|
|
503
|
+
previous.resume();
|
|
504
|
+
else
|
|
505
|
+
state.delete(bus);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
509
|
+
const event = modelEventName(tableName, action);
|
|
510
|
+
if (options.outbox) {
|
|
511
|
+
if (!durableTags.length)
|
|
512
|
+
continue;
|
|
513
|
+
const outbox = options.outbox;
|
|
514
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
515
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
516
|
+
}));
|
|
517
|
+
} else {
|
|
518
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
444
519
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
445
|
-
if (tags.length
|
|
520
|
+
if (!tags.length)
|
|
446
521
|
return;
|
|
447
|
-
}
|
|
448
522
|
let cache;
|
|
449
523
|
let queue;
|
|
450
524
|
try {
|
|
@@ -455,9 +529,11 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
|
455
529
|
}
|
|
456
530
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
457
531
|
await queue.dispatch(job, { tags });
|
|
458
|
-
});
|
|
532
|
+
}));
|
|
459
533
|
}
|
|
460
534
|
}
|
|
535
|
+
registration.resume();
|
|
536
|
+
return registration.dispose;
|
|
461
537
|
}
|
|
462
538
|
|
|
463
539
|
// ../../src/bootstrap/providers/listeners.ts
|
|
@@ -11,7 +11,6 @@ import { applyMiddlewareToRoutes as applyMiddlewareToRoutes2 } from "@getstrata/
|
|
|
11
11
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
12
12
|
import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
|
|
13
13
|
import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
|
|
14
|
-
import { createJsonErrorMiddleware } from "@getstrata/core/http/response";
|
|
15
14
|
|
|
16
15
|
// ../../src/bootstrap/httpKernel.ts
|
|
17
16
|
import {
|
|
@@ -36,7 +35,12 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
36
35
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
37
36
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
38
37
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
39
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
createJsonErrorMiddleware,
|
|
40
|
+
createWebErrorMiddleware,
|
|
41
|
+
withErrorHandling,
|
|
42
|
+
withJsonErrorHandling
|
|
43
|
+
} from "@getstrata/core/http/response";
|
|
40
44
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
41
45
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
42
46
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -125,7 +129,7 @@ class HttpKernel {
|
|
|
125
129
|
constructor(dependencies) {
|
|
126
130
|
this.dependencies = dependencies;
|
|
127
131
|
}
|
|
128
|
-
globalMiddleware() {
|
|
132
|
+
globalMiddleware(errorFormat = "json") {
|
|
129
133
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
130
134
|
return [
|
|
131
135
|
createCorsMiddleware(),
|
|
@@ -135,6 +139,7 @@ class HttpKernel {
|
|
|
135
139
|
createMetricsMiddleware(),
|
|
136
140
|
createRequestLoggingMiddleware(),
|
|
137
141
|
requestIdMiddleware,
|
|
142
|
+
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
138
143
|
createAuthMiddleware(auth),
|
|
139
144
|
createMembershipMiddleware(),
|
|
140
145
|
createTenantMiddleware()
|
|
@@ -432,22 +437,22 @@ function readSharedRouteRegistry() {
|
|
|
432
437
|
var routeRegistry = readSharedRouteRegistry();
|
|
433
438
|
|
|
434
439
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
435
|
-
function registerOpenApiRoute(method, path, middleware) {
|
|
436
|
-
routeRegistry.register({ method, path, middleware });
|
|
440
|
+
function registerOpenApiRoute(method, path, middleware, openApi) {
|
|
441
|
+
routeRegistry.register({ method, path, middleware, ...openApi ? { openApi } : {} });
|
|
437
442
|
}
|
|
438
|
-
function registerOpenApiRouteMap(routes, middleware) {
|
|
443
|
+
function registerOpenApiRouteMap(routes, middleware, metadata = {}) {
|
|
439
444
|
const registered = {};
|
|
440
445
|
for (const [path, handler] of Object.entries(routes)) {
|
|
441
446
|
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
442
447
|
const methodMap = handler;
|
|
443
448
|
registered[path] = methodMap;
|
|
444
449
|
for (const method of Object.keys(methodMap)) {
|
|
445
|
-
registerOpenApiRoute(method.toUpperCase(), path, middleware);
|
|
450
|
+
registerOpenApiRoute(method.toUpperCase(), path, middleware, metadata[path]?.[method.toUpperCase()]);
|
|
446
451
|
}
|
|
447
452
|
continue;
|
|
448
453
|
}
|
|
449
454
|
registered[path] = handler;
|
|
450
|
-
registerOpenApiRoute("GET", path, middleware);
|
|
455
|
+
registerOpenApiRoute("GET", path, middleware, metadata[path]?.GET);
|
|
451
456
|
}
|
|
452
457
|
return registered;
|
|
453
458
|
}
|
|
@@ -463,21 +468,32 @@ function buildModuleRoutes(dependencies, options = {}) {
|
|
|
463
468
|
routeRegistry.clear();
|
|
464
469
|
}
|
|
465
470
|
const kernel = createHttpKernel(dependencies);
|
|
466
|
-
const middleware = [
|
|
467
|
-
...kernel.globalMiddleware(),
|
|
468
|
-
createJsonErrorMiddleware(),
|
|
469
|
-
...kernel.group("api")
|
|
470
|
-
];
|
|
471
|
+
const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
|
|
471
472
|
const cachedJson = createCachedJson(dependencies);
|
|
472
473
|
const moduleRoutes = {};
|
|
474
|
+
const metadata = {};
|
|
473
475
|
for (const module of modules) {
|
|
474
476
|
if (!module.routes) {
|
|
475
477
|
continue;
|
|
476
478
|
}
|
|
477
|
-
|
|
479
|
+
const routes = module.routes({ dependencies, cachedJson, kernel });
|
|
480
|
+
for (const [path, operations] of Object.entries(module.openApi ?? {})) {
|
|
481
|
+
if (!(path in routes))
|
|
482
|
+
throw new Error(`OpenAPI metadata has no API route: ${module.name} ${path}`);
|
|
483
|
+
const methods = typeof routes[path] === "function" ? ["GET"] : Object.keys(routes[path]).map((method) => method.toUpperCase());
|
|
484
|
+
for (const method of Object.keys(operations))
|
|
485
|
+
if (!methods.includes(method))
|
|
486
|
+
throw new Error(`OpenAPI metadata has no API handler: ${method} ${path}`);
|
|
487
|
+
}
|
|
488
|
+
for (const path of Object.keys(routes)) {
|
|
489
|
+
delete metadata[path];
|
|
490
|
+
if (module.openApi?.[path])
|
|
491
|
+
metadata[path] = module.openApi[path];
|
|
492
|
+
}
|
|
493
|
+
Object.assign(moduleRoutes, routes);
|
|
478
494
|
}
|
|
479
495
|
const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
|
|
480
|
-
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
|
|
496
|
+
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"], prefixRouteMap(apiPrefix, metadata)), middleware);
|
|
481
497
|
}
|
|
482
498
|
|
|
483
499
|
// ../../src/bootstrap/buildWebModuleRoutes.ts
|
|
@@ -487,7 +503,7 @@ function buildWebModuleRoutes(dependencies, options = {}) {
|
|
|
487
503
|
routeRegistry.clear();
|
|
488
504
|
}
|
|
489
505
|
const kernel = createHttpKernel(dependencies);
|
|
490
|
-
const middleware = kernel.globalMiddleware();
|
|
506
|
+
const middleware = kernel.globalMiddleware("web");
|
|
491
507
|
const moduleRoutes = { ...seedRoutes };
|
|
492
508
|
for (const module of modules) {
|
|
493
509
|
if (!module.webRoutes) {
|
|
@@ -437,14 +437,88 @@ function discoverModelTableNames() {
|
|
|
437
437
|
|
|
438
438
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
439
439
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
440
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
441
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
442
|
+
function registrations() {
|
|
443
|
+
const global = globalThis;
|
|
444
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
445
|
+
return global[STATE_KEY];
|
|
446
|
+
}
|
|
447
|
+
function requireCacheTags(value) {
|
|
448
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
449
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
function createModelCacheInvalidationListener(resolveCache = resolveApplicationCache) {
|
|
453
|
+
return {
|
|
454
|
+
name: "strata.cache.invalidate-tags.v1",
|
|
455
|
+
event: CACHE_EVENT,
|
|
456
|
+
async handle(event, { signal }) {
|
|
457
|
+
const payload = event.payload;
|
|
458
|
+
if (event.version !== 1)
|
|
459
|
+
throw new Error("Invalid durable cache invalidation version.");
|
|
460
|
+
const tags = requireCacheTags(payload?.tags);
|
|
461
|
+
signal.throwIfAborted();
|
|
462
|
+
await new InvalidateCacheTagsJob(resolveCache()).handle({ tags });
|
|
463
|
+
signal.throwIfAborted();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options = {}) {
|
|
468
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
469
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
470
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
471
|
+
tableName,
|
|
472
|
+
action,
|
|
473
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
474
|
+
})));
|
|
475
|
+
if (options.outbox) {
|
|
476
|
+
for (const write of writes)
|
|
477
|
+
if (write.tags.length)
|
|
478
|
+
requireCacheTags(write.tags);
|
|
479
|
+
}
|
|
480
|
+
const state = registrations();
|
|
481
|
+
const previous = state.get(bus);
|
|
482
|
+
if (previous?.durable && !options.outbox)
|
|
483
|
+
return () => {};
|
|
484
|
+
previous?.suspend();
|
|
485
|
+
const unsubscribe = [];
|
|
486
|
+
const subscribe = [];
|
|
487
|
+
const registration = {
|
|
488
|
+
durable: !!options.outbox,
|
|
489
|
+
suspend() {
|
|
490
|
+
for (const stop of unsubscribe.splice(0))
|
|
491
|
+
stop();
|
|
492
|
+
},
|
|
493
|
+
resume() {
|
|
494
|
+
for (const start of subscribe)
|
|
495
|
+
unsubscribe.push(start());
|
|
496
|
+
state.set(bus, registration);
|
|
497
|
+
},
|
|
498
|
+
dispose() {
|
|
499
|
+
if (state.get(bus) !== registration)
|
|
500
|
+
return;
|
|
501
|
+
registration.suspend();
|
|
502
|
+
if (previous)
|
|
503
|
+
previous.resume();
|
|
504
|
+
else
|
|
505
|
+
state.delete(bus);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
509
|
+
const event = modelEventName(tableName, action);
|
|
510
|
+
if (options.outbox) {
|
|
511
|
+
if (!durableTags.length)
|
|
512
|
+
continue;
|
|
513
|
+
const outbox = options.outbox;
|
|
514
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
515
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
516
|
+
}));
|
|
517
|
+
} else {
|
|
518
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
444
519
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
445
|
-
if (tags.length
|
|
520
|
+
if (!tags.length)
|
|
446
521
|
return;
|
|
447
|
-
}
|
|
448
522
|
let cache;
|
|
449
523
|
let queue;
|
|
450
524
|
try {
|
|
@@ -455,9 +529,11 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
|
455
529
|
}
|
|
456
530
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
457
531
|
await queue.dispatch(job, { tags });
|
|
458
|
-
});
|
|
532
|
+
}));
|
|
459
533
|
}
|
|
460
534
|
}
|
|
535
|
+
registration.resume();
|
|
536
|
+
return registration.dispose;
|
|
461
537
|
}
|
|
462
538
|
|
|
463
539
|
// ../../src/bootstrap/providers/listeners.ts
|
|
@@ -22,7 +22,12 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
22
22
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
23
23
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
24
24
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createJsonErrorMiddleware,
|
|
27
|
+
createWebErrorMiddleware,
|
|
28
|
+
withErrorHandling,
|
|
29
|
+
withJsonErrorHandling
|
|
30
|
+
} from "@getstrata/core/http/response";
|
|
26
31
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
27
32
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
28
33
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -111,7 +116,7 @@ class HttpKernel {
|
|
|
111
116
|
constructor(dependencies) {
|
|
112
117
|
this.dependencies = dependencies;
|
|
113
118
|
}
|
|
114
|
-
globalMiddleware() {
|
|
119
|
+
globalMiddleware(errorFormat = "json") {
|
|
115
120
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
116
121
|
return [
|
|
117
122
|
createCorsMiddleware(),
|
|
@@ -121,6 +126,7 @@ class HttpKernel {
|
|
|
121
126
|
createMetricsMiddleware(),
|
|
122
127
|
createRequestLoggingMiddleware(),
|
|
123
128
|
requestIdMiddleware,
|
|
129
|
+
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
124
130
|
createAuthMiddleware(auth),
|
|
125
131
|
createMembershipMiddleware(),
|
|
126
132
|
createTenantMiddleware()
|
|
@@ -102,14 +102,88 @@ function discoverModelTableNames() {
|
|
|
102
102
|
|
|
103
103
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
104
104
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
106
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
107
|
+
function registrations() {
|
|
108
|
+
const global = globalThis;
|
|
109
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
110
|
+
return global[STATE_KEY];
|
|
111
|
+
}
|
|
112
|
+
function requireCacheTags(value) {
|
|
113
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
114
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function createModelCacheInvalidationListener(resolveCache = resolveApplicationCache) {
|
|
118
|
+
return {
|
|
119
|
+
name: "strata.cache.invalidate-tags.v1",
|
|
120
|
+
event: CACHE_EVENT,
|
|
121
|
+
async handle(event, { signal }) {
|
|
122
|
+
const payload = event.payload;
|
|
123
|
+
if (event.version !== 1)
|
|
124
|
+
throw new Error("Invalid durable cache invalidation version.");
|
|
125
|
+
const tags = requireCacheTags(payload?.tags);
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
await new InvalidateCacheTagsJob(resolveCache()).handle({ tags });
|
|
128
|
+
signal.throwIfAborted();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus, options = {}) {
|
|
133
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
134
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
135
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
136
|
+
tableName,
|
|
137
|
+
action,
|
|
138
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
139
|
+
})));
|
|
140
|
+
if (options.outbox) {
|
|
141
|
+
for (const write of writes)
|
|
142
|
+
if (write.tags.length)
|
|
143
|
+
requireCacheTags(write.tags);
|
|
144
|
+
}
|
|
145
|
+
const state = registrations();
|
|
146
|
+
const previous = state.get(bus);
|
|
147
|
+
if (previous?.durable && !options.outbox)
|
|
148
|
+
return () => {};
|
|
149
|
+
previous?.suspend();
|
|
150
|
+
const unsubscribe = [];
|
|
151
|
+
const subscribe = [];
|
|
152
|
+
const registration = {
|
|
153
|
+
durable: !!options.outbox,
|
|
154
|
+
suspend() {
|
|
155
|
+
for (const stop of unsubscribe.splice(0))
|
|
156
|
+
stop();
|
|
157
|
+
},
|
|
158
|
+
resume() {
|
|
159
|
+
for (const start of subscribe)
|
|
160
|
+
unsubscribe.push(start());
|
|
161
|
+
state.set(bus, registration);
|
|
162
|
+
},
|
|
163
|
+
dispose() {
|
|
164
|
+
if (state.get(bus) !== registration)
|
|
165
|
+
return;
|
|
166
|
+
registration.suspend();
|
|
167
|
+
if (previous)
|
|
168
|
+
previous.resume();
|
|
169
|
+
else
|
|
170
|
+
state.delete(bus);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
174
|
+
const event = modelEventName(tableName, action);
|
|
175
|
+
if (options.outbox) {
|
|
176
|
+
if (!durableTags.length)
|
|
177
|
+
continue;
|
|
178
|
+
const outbox = options.outbox;
|
|
179
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
180
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
181
|
+
}));
|
|
182
|
+
} else {
|
|
183
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
109
184
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
110
|
-
if (tags.length
|
|
185
|
+
if (!tags.length)
|
|
111
186
|
return;
|
|
112
|
-
}
|
|
113
187
|
let cache;
|
|
114
188
|
let queue;
|
|
115
189
|
try {
|
|
@@ -120,10 +194,13 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
|
120
194
|
}
|
|
121
195
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
122
196
|
await queue.dispatch(job, { tags });
|
|
123
|
-
});
|
|
197
|
+
}));
|
|
124
198
|
}
|
|
125
199
|
}
|
|
200
|
+
registration.resume();
|
|
201
|
+
return registration.dispose;
|
|
126
202
|
}
|
|
127
203
|
export {
|
|
204
|
+
createModelCacheInvalidationListener,
|
|
128
205
|
registerInvalidateCacheOnModelWriteListeners
|
|
129
206
|
};
|
|
@@ -427,14 +427,88 @@ function discoverModelTableNames() {
|
|
|
427
427
|
|
|
428
428
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
429
429
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
430
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
431
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
432
|
+
function registrations() {
|
|
433
|
+
const global = globalThis;
|
|
434
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
435
|
+
return global[STATE_KEY];
|
|
436
|
+
}
|
|
437
|
+
function requireCacheTags(value) {
|
|
438
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
439
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
function createModelCacheInvalidationListener(resolveCache = resolveApplicationCache) {
|
|
443
|
+
return {
|
|
444
|
+
name: "strata.cache.invalidate-tags.v1",
|
|
445
|
+
event: CACHE_EVENT,
|
|
446
|
+
async handle(event, { signal }) {
|
|
447
|
+
const payload = event.payload;
|
|
448
|
+
if (event.version !== 1)
|
|
449
|
+
throw new Error("Invalid durable cache invalidation version.");
|
|
450
|
+
const tags = requireCacheTags(payload?.tags);
|
|
451
|
+
signal.throwIfAborted();
|
|
452
|
+
await new InvalidateCacheTagsJob(resolveCache()).handle({ tags });
|
|
453
|
+
signal.throwIfAborted();
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options = {}) {
|
|
458
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
459
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
460
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
461
|
+
tableName,
|
|
462
|
+
action,
|
|
463
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
464
|
+
})));
|
|
465
|
+
if (options.outbox) {
|
|
466
|
+
for (const write of writes)
|
|
467
|
+
if (write.tags.length)
|
|
468
|
+
requireCacheTags(write.tags);
|
|
469
|
+
}
|
|
470
|
+
const state = registrations();
|
|
471
|
+
const previous = state.get(bus);
|
|
472
|
+
if (previous?.durable && !options.outbox)
|
|
473
|
+
return () => {};
|
|
474
|
+
previous?.suspend();
|
|
475
|
+
const unsubscribe = [];
|
|
476
|
+
const subscribe = [];
|
|
477
|
+
const registration = {
|
|
478
|
+
durable: !!options.outbox,
|
|
479
|
+
suspend() {
|
|
480
|
+
for (const stop of unsubscribe.splice(0))
|
|
481
|
+
stop();
|
|
482
|
+
},
|
|
483
|
+
resume() {
|
|
484
|
+
for (const start of subscribe)
|
|
485
|
+
unsubscribe.push(start());
|
|
486
|
+
state.set(bus, registration);
|
|
487
|
+
},
|
|
488
|
+
dispose() {
|
|
489
|
+
if (state.get(bus) !== registration)
|
|
490
|
+
return;
|
|
491
|
+
registration.suspend();
|
|
492
|
+
if (previous)
|
|
493
|
+
previous.resume();
|
|
494
|
+
else
|
|
495
|
+
state.delete(bus);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
499
|
+
const event = modelEventName(tableName, action);
|
|
500
|
+
if (options.outbox) {
|
|
501
|
+
if (!durableTags.length)
|
|
502
|
+
continue;
|
|
503
|
+
const outbox = options.outbox;
|
|
504
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
505
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
506
|
+
}));
|
|
507
|
+
} else {
|
|
508
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
434
509
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
435
|
-
if (tags.length
|
|
510
|
+
if (!tags.length)
|
|
436
511
|
return;
|
|
437
|
-
}
|
|
438
512
|
let cache;
|
|
439
513
|
let queue;
|
|
440
514
|
try {
|
|
@@ -445,9 +519,11 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
|
445
519
|
}
|
|
446
520
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
447
521
|
await queue.dispatch(job, { tags });
|
|
448
|
-
});
|
|
522
|
+
}));
|
|
449
523
|
}
|
|
450
524
|
}
|
|
525
|
+
registration.resume();
|
|
526
|
+
return registration.dispose;
|
|
451
527
|
}
|
|
452
528
|
|
|
453
529
|
// ../../src/bootstrap/providers/listeners.ts
|
|
@@ -32,7 +32,12 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
32
32
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
33
33
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
34
34
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
createJsonErrorMiddleware,
|
|
37
|
+
createWebErrorMiddleware,
|
|
38
|
+
withErrorHandling,
|
|
39
|
+
withJsonErrorHandling
|
|
40
|
+
} from "@getstrata/core/http/response";
|
|
36
41
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
37
42
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
38
43
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -121,7 +126,7 @@ class HttpKernel {
|
|
|
121
126
|
constructor(dependencies) {
|
|
122
127
|
this.dependencies = dependencies;
|
|
123
128
|
}
|
|
124
|
-
globalMiddleware() {
|
|
129
|
+
globalMiddleware(errorFormat = "json") {
|
|
125
130
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
126
131
|
return [
|
|
127
132
|
createCorsMiddleware(),
|
|
@@ -131,6 +136,7 @@ class HttpKernel {
|
|
|
131
136
|
createMetricsMiddleware(),
|
|
132
137
|
createRequestLoggingMiddleware(),
|
|
133
138
|
requestIdMiddleware,
|
|
139
|
+
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
134
140
|
createAuthMiddleware(auth),
|
|
135
141
|
createMembershipMiddleware(),
|
|
136
142
|
createTenantMiddleware()
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
18
18
|
import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
|
|
19
19
|
import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
|
|
20
|
-
import { createJsonErrorMiddleware } from "@getstrata/core/http/response";
|
|
21
20
|
|
|
22
21
|
// ../../src/bootstrap/httpKernel.ts
|
|
23
22
|
import {
|
|
@@ -42,7 +41,12 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
42
41
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
43
42
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
44
43
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
45
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
createJsonErrorMiddleware,
|
|
46
|
+
createWebErrorMiddleware,
|
|
47
|
+
withErrorHandling,
|
|
48
|
+
withJsonErrorHandling
|
|
49
|
+
} from "@getstrata/core/http/response";
|
|
46
50
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
47
51
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
48
52
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -130,7 +134,7 @@ class HttpKernel {
|
|
|
130
134
|
constructor(dependencies) {
|
|
131
135
|
this.dependencies = dependencies;
|
|
132
136
|
}
|
|
133
|
-
globalMiddleware() {
|
|
137
|
+
globalMiddleware(errorFormat = "json") {
|
|
134
138
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
135
139
|
return [
|
|
136
140
|
createCorsMiddleware(),
|
|
@@ -140,6 +144,7 @@ class HttpKernel {
|
|
|
140
144
|
createMetricsMiddleware(),
|
|
141
145
|
createRequestLoggingMiddleware(),
|
|
142
146
|
requestIdMiddleware,
|
|
147
|
+
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
143
148
|
createAuthMiddleware(auth),
|
|
144
149
|
createMembershipMiddleware(),
|
|
145
150
|
createTenantMiddleware()
|
|
@@ -431,22 +436,22 @@ function readSharedRouteRegistry() {
|
|
|
431
436
|
var routeRegistry = readSharedRouteRegistry();
|
|
432
437
|
|
|
433
438
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
434
|
-
function registerOpenApiRoute(method, path, middleware) {
|
|
435
|
-
routeRegistry.register({ method, path, middleware });
|
|
439
|
+
function registerOpenApiRoute(method, path, middleware, openApi) {
|
|
440
|
+
routeRegistry.register({ method, path, middleware, ...openApi ? { openApi } : {} });
|
|
436
441
|
}
|
|
437
|
-
function registerOpenApiRouteMap(routes, middleware) {
|
|
442
|
+
function registerOpenApiRouteMap(routes, middleware, metadata = {}) {
|
|
438
443
|
const registered = {};
|
|
439
444
|
for (const [path, handler] of Object.entries(routes)) {
|
|
440
445
|
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
441
446
|
const methodMap = handler;
|
|
442
447
|
registered[path] = methodMap;
|
|
443
448
|
for (const method of Object.keys(methodMap)) {
|
|
444
|
-
registerOpenApiRoute(method.toUpperCase(), path, middleware);
|
|
449
|
+
registerOpenApiRoute(method.toUpperCase(), path, middleware, metadata[path]?.[method.toUpperCase()]);
|
|
445
450
|
}
|
|
446
451
|
continue;
|
|
447
452
|
}
|
|
448
453
|
registered[path] = handler;
|
|
449
|
-
registerOpenApiRoute("GET", path, middleware);
|
|
454
|
+
registerOpenApiRoute("GET", path, middleware, metadata[path]?.GET);
|
|
450
455
|
}
|
|
451
456
|
return registered;
|
|
452
457
|
}
|
|
@@ -462,21 +467,32 @@ function buildModuleRoutes(dependencies, options = {}) {
|
|
|
462
467
|
routeRegistry.clear();
|
|
463
468
|
}
|
|
464
469
|
const kernel = createHttpKernel(dependencies);
|
|
465
|
-
const middleware = [
|
|
466
|
-
...kernel.globalMiddleware(),
|
|
467
|
-
createJsonErrorMiddleware(),
|
|
468
|
-
...kernel.group("api")
|
|
469
|
-
];
|
|
470
|
+
const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
|
|
470
471
|
const cachedJson = createCachedJson(dependencies);
|
|
471
472
|
const moduleRoutes = {};
|
|
473
|
+
const metadata = {};
|
|
472
474
|
for (const module of modules) {
|
|
473
475
|
if (!module.routes) {
|
|
474
476
|
continue;
|
|
475
477
|
}
|
|
476
|
-
|
|
478
|
+
const routes = module.routes({ dependencies, cachedJson, kernel });
|
|
479
|
+
for (const [path, operations] of Object.entries(module.openApi ?? {})) {
|
|
480
|
+
if (!(path in routes))
|
|
481
|
+
throw new Error(`OpenAPI metadata has no API route: ${module.name} ${path}`);
|
|
482
|
+
const methods = typeof routes[path] === "function" ? ["GET"] : Object.keys(routes[path]).map((method) => method.toUpperCase());
|
|
483
|
+
for (const method of Object.keys(operations))
|
|
484
|
+
if (!methods.includes(method))
|
|
485
|
+
throw new Error(`OpenAPI metadata has no API handler: ${method} ${path}`);
|
|
486
|
+
}
|
|
487
|
+
for (const path of Object.keys(routes)) {
|
|
488
|
+
delete metadata[path];
|
|
489
|
+
if (module.openApi?.[path])
|
|
490
|
+
metadata[path] = module.openApi[path];
|
|
491
|
+
}
|
|
492
|
+
Object.assign(moduleRoutes, routes);
|
|
477
493
|
}
|
|
478
494
|
const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
|
|
479
|
-
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
|
|
495
|
+
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"], prefixRouteMap(apiPrefix, metadata)), middleware);
|
|
480
496
|
}
|
|
481
497
|
// ../../src/bootstrap/buildWebModuleRoutes.ts
|
|
482
498
|
import { applyMiddlewareToRoutes as applyMiddlewareToRoutes2 } from "@getstrata/core/http/middleware";
|
|
@@ -486,7 +502,7 @@ function buildWebModuleRoutes(dependencies, options = {}) {
|
|
|
486
502
|
routeRegistry.clear();
|
|
487
503
|
}
|
|
488
504
|
const kernel = createHttpKernel(dependencies);
|
|
489
|
-
const middleware = kernel.globalMiddleware();
|
|
505
|
+
const middleware = kernel.globalMiddleware("web");
|
|
490
506
|
const moduleRoutes = { ...seedRoutes };
|
|
491
507
|
for (const module of modules) {
|
|
492
508
|
if (!module.webRoutes) {
|
|
@@ -822,14 +838,73 @@ import { eventBus as eventBus2, modelEventName } from "@getstrata/core/events";
|
|
|
822
838
|
import { InvalidateCacheTagsJob } from "@getstrata/core/jobs/invalidateCacheTagsJob";
|
|
823
839
|
import { createTrackedJob } from "@getstrata/core/queue/createAppQueue";
|
|
824
840
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
841
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
842
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
843
|
+
function registrations() {
|
|
844
|
+
const global = globalThis;
|
|
845
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
846
|
+
return global[STATE_KEY];
|
|
847
|
+
}
|
|
848
|
+
function requireCacheTags(value) {
|
|
849
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
850
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
851
|
+
return value;
|
|
852
|
+
}
|
|
853
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options = {}) {
|
|
854
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
855
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
856
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
857
|
+
tableName,
|
|
858
|
+
action,
|
|
859
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
860
|
+
})));
|
|
861
|
+
if (options.outbox) {
|
|
862
|
+
for (const write of writes)
|
|
863
|
+
if (write.tags.length)
|
|
864
|
+
requireCacheTags(write.tags);
|
|
865
|
+
}
|
|
866
|
+
const state = registrations();
|
|
867
|
+
const previous = state.get(bus);
|
|
868
|
+
if (previous?.durable && !options.outbox)
|
|
869
|
+
return () => {};
|
|
870
|
+
previous?.suspend();
|
|
871
|
+
const unsubscribe = [];
|
|
872
|
+
const subscribe = [];
|
|
873
|
+
const registration = {
|
|
874
|
+
durable: !!options.outbox,
|
|
875
|
+
suspend() {
|
|
876
|
+
for (const stop of unsubscribe.splice(0))
|
|
877
|
+
stop();
|
|
878
|
+
},
|
|
879
|
+
resume() {
|
|
880
|
+
for (const start of subscribe)
|
|
881
|
+
unsubscribe.push(start());
|
|
882
|
+
state.set(bus, registration);
|
|
883
|
+
},
|
|
884
|
+
dispose() {
|
|
885
|
+
if (state.get(bus) !== registration)
|
|
886
|
+
return;
|
|
887
|
+
registration.suspend();
|
|
888
|
+
if (previous)
|
|
889
|
+
previous.resume();
|
|
890
|
+
else
|
|
891
|
+
state.delete(bus);
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
895
|
+
const event = modelEventName(tableName, action);
|
|
896
|
+
if (options.outbox) {
|
|
897
|
+
if (!durableTags.length)
|
|
898
|
+
continue;
|
|
899
|
+
const outbox = options.outbox;
|
|
900
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
901
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
902
|
+
}));
|
|
903
|
+
} else {
|
|
904
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
829
905
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
830
|
-
if (tags.length
|
|
906
|
+
if (!tags.length)
|
|
831
907
|
return;
|
|
832
|
-
}
|
|
833
908
|
let cache;
|
|
834
909
|
let queue;
|
|
835
910
|
try {
|
|
@@ -840,9 +915,11 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
|
840
915
|
}
|
|
841
916
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
842
917
|
await queue.dispatch(job, { tags });
|
|
843
|
-
});
|
|
918
|
+
}));
|
|
844
919
|
}
|
|
845
920
|
}
|
|
921
|
+
registration.resume();
|
|
922
|
+
return registration.dispose;
|
|
846
923
|
}
|
|
847
924
|
|
|
848
925
|
// ../../src/bootstrap/providers/listeners.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/bootstrap",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Strata application bootstrap: HttpKernel, DI, web session helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"access": "public"
|
|
186
186
|
},
|
|
187
187
|
"peerDependencies": {
|
|
188
|
-
"@getstrata/core": "^2.0.
|
|
188
|
+
"@getstrata/core": "^2.0.4",
|
|
189
189
|
"typescript": "^5.9.0 || ^6.0.0 || ^7.0.0"
|
|
190
190
|
},
|
|
191
191
|
"engines": {
|