@constructive-io/express-context 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/esm/index.js +1 -1
- package/esm/loaders/create-loader.js +58 -29
- package/esm/loaders/events.js +7 -5
- package/esm/loaders/index.js +4 -0
- package/esm/loaders/registry.js +2 -2
- package/esm/loaders/standing.js +44 -0
- package/esm/loaders/types.js +3 -3
- package/index.d.ts +2 -2
- package/index.js +5 -2
- package/loaders/create-loader.d.ts +2 -2
- package/loaders/create-loader.js +58 -29
- package/loaders/events.d.ts +3 -3
- package/loaders/events.js +7 -5
- package/loaders/index.d.ts +2 -0
- package/loaders/index.js +8 -1
- package/loaders/registry.d.ts +4 -4
- package/loaders/registry.js +2 -2
- package/loaders/standing.d.ts +22 -0
- package/loaders/standing.js +47 -0
- package/loaders/types.d.ts +9 -6
- package/loaders/types.js +3 -3
- package/package.json +6 -6
- package/types.d.ts +18 -1
package/README.md
CHANGED
|
@@ -57,7 +57,31 @@ app.post('/v1/chat', async (req, res) => {
|
|
|
57
57
|
|
|
58
58
|
## Module Loaders
|
|
59
59
|
|
|
60
|
-
Each loader encapsulates a SQL query + type transform +
|
|
60
|
+
Each loader encapsulates a SQL query + type transform + bounded LRU cache for
|
|
61
|
+
one piece of per-database configuration. The existing cache key is retained:
|
|
62
|
+
`databaseId:apiId` when an API ID is present, otherwise `databaseId`. TTLs are
|
|
63
|
+
hard expiry bounds, and concurrent misses for the same key share a single
|
|
64
|
+
resolution. Loaders are registered in a `LoaderRegistry` and resolved lazily via
|
|
65
|
+
`useModule(name)`.
|
|
66
|
+
|
|
67
|
+
Logical database IDs already distinguish tenant databases, and the optional API
|
|
68
|
+
ID distinguishes their API configuration. Pool identities and routing schemas
|
|
69
|
+
are not added to the key; extending cache identity is not required for these
|
|
70
|
+
TTL, concurrency, and invalidation improvements.
|
|
71
|
+
|
|
72
|
+
The existing `invalidate(databaseId?)` API is retained by design. Passing a
|
|
73
|
+
database ID invalidates that database and all of its API entries; omitting it
|
|
74
|
+
clears the loader's entire cache. Registry invalidation applies the same operation
|
|
75
|
+
to every registered loader. Matching in-flight resolutions
|
|
76
|
+
cannot repopulate an invalidated cache, although their existing callers can still
|
|
77
|
+
receive the results. Global and database-level invalidation are sufficient;
|
|
78
|
+
additional invalidation parameters are not required for this change.
|
|
79
|
+
|
|
80
|
+
Absence remains uncached by design: neither `undefined` results nor PostgreSQL
|
|
81
|
+
`42P01` (undefined table) results are stored, so subsequent calls can discover
|
|
82
|
+
newly available configuration. Concurrent calls may still share the same
|
|
83
|
+
in-flight resolution. Negative caching is not required for this change; other
|
|
84
|
+
resolution errors continue to propagate without being cached.
|
|
61
85
|
|
|
62
86
|
### Built-in loaders
|
|
63
87
|
|
package/esm/index.js
CHANGED
|
@@ -45,6 +45,6 @@ export { BoundedCounter, CounterFlusher } from './usage-counter';
|
|
|
45
45
|
// Request ID middleware
|
|
46
46
|
export { requestIdMiddleware } from './request-id';
|
|
47
47
|
export { buildContext, createContextMiddleware } from './context';
|
|
48
|
-
export { agentChatLoader, authSettingsLoader, authSurfaceLoader, billingLoader, computeLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, eventsLoader, identityProvidersLoader, inferenceLogLoader, llmLoader, pubkeyLoader, requestProtectionLoader, requireDatabaseId, requireIdentityProvider, rlsLoader, webauthnLoader, } from './loaders';
|
|
48
|
+
export { agentChatLoader, authSettingsLoader, authSurfaceLoader, billingLoader, computeLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, DATABASE_STANDING_SQL, DATABASE_STANDING_TTL_MS, databaseSettingsLoader, eventsLoader, identityProvidersLoader, inferenceLogLoader, llmLoader, pubkeyLoader, requestProtectionLoader, requireDatabaseId, requireIdentityProvider, rlsLoader, standingLoader, webauthnLoader, } from './loaders';
|
|
49
49
|
// Side-effect: Express type augmentation
|
|
50
50
|
import './types';
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* create-loader — Factory for building cached ModuleLoader instances.
|
|
3
3
|
*
|
|
4
4
|
* Wraps a raw resolve function with an LRU cache keyed by databaseId:apiId.
|
|
5
|
-
* Each loader gets its own independent cache with configurable TTL and
|
|
6
|
-
*
|
|
5
|
+
* Each loader gets its own independent cache with a configurable hard TTL and
|
|
6
|
+
* maximum size.
|
|
7
7
|
*/
|
|
8
8
|
import { Logger } from '@pgpmjs/logger';
|
|
9
9
|
import { LRUCache } from 'lru-cache';
|
|
@@ -14,53 +14,82 @@ export function createModuleLoader(opts) {
|
|
|
14
14
|
const cache = new LRUCache({
|
|
15
15
|
max: opts.max ?? DEFAULT_MAX,
|
|
16
16
|
ttl: opts.ttlMs ?? DEFAULT_TTL_MS,
|
|
17
|
-
updateAgeOnGet:
|
|
17
|
+
updateAgeOnGet: false,
|
|
18
18
|
allowStale: false,
|
|
19
19
|
});
|
|
20
|
+
const pending = new Map();
|
|
20
21
|
return {
|
|
21
22
|
name: opts.name,
|
|
22
23
|
async resolve(ctx) {
|
|
23
24
|
const key = ctx.apiId ? `${ctx.databaseId}:${ctx.apiId}` : ctx.databaseId;
|
|
24
|
-
|
|
25
|
+
const cached = cache.get(key);
|
|
26
|
+
if (cached !== undefined) {
|
|
25
27
|
log.debug(`Cache HIT databaseId=${key}`);
|
|
26
|
-
return
|
|
28
|
+
return cached;
|
|
29
|
+
}
|
|
30
|
+
const existing = pending.get(key);
|
|
31
|
+
if (existing && !existing.invalidated) {
|
|
32
|
+
log.debug(`Cache COALESCE databaseId=${key}`);
|
|
33
|
+
return existing.promise;
|
|
27
34
|
}
|
|
28
35
|
log.debug(`Cache MISS databaseId=${key}, resolving`);
|
|
29
36
|
// "Not provisioned" is expressed by the loader returning undefined, or
|
|
30
37
|
// by the module's tables not existing at all (42P01 undefined_table).
|
|
31
38
|
// Any other resolution error (bad query, ambiguous config) propagates —
|
|
32
39
|
// never silently coerced into "module absent".
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const resolution = {
|
|
41
|
+
databaseId: ctx.databaseId,
|
|
42
|
+
invalidated: false,
|
|
43
|
+
promise: Promise.resolve(undefined)
|
|
44
|
+
};
|
|
45
|
+
resolution.promise = Promise.resolve().then(async () => {
|
|
46
|
+
try {
|
|
47
|
+
const value = await opts.resolve(ctx);
|
|
48
|
+
// Keep absence uncached so subsequent calls can discover new config.
|
|
49
|
+
if (!resolution.invalidated && value !== undefined) {
|
|
50
|
+
cache.set(key, value);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
43
53
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
catch (e) {
|
|
55
|
+
if (e.code === '42P01') {
|
|
56
|
+
log.debug(`Module tables absent for databaseId=${key}: ${e.message}`);
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
log.warn(`Failed to resolve databaseId=${key}: ${e.message}`);
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (pending.get(key) === resolution) {
|
|
64
|
+
pending.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
pending.set(key, resolution);
|
|
69
|
+
return resolution.promise;
|
|
47
70
|
},
|
|
48
71
|
invalidate(databaseId) {
|
|
49
|
-
if (databaseId) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
if (!databaseId) {
|
|
73
|
+
const previousSize = cache.size;
|
|
74
|
+
cache.clear();
|
|
75
|
+
for (const resolution of pending.values()) {
|
|
76
|
+
resolution.invalidated = true;
|
|
77
|
+
}
|
|
78
|
+
log.debug(`Invalidated all entries (was size=${previousSize})`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let cleared = 0;
|
|
82
|
+
for (const key of cache.keys()) {
|
|
83
|
+
if (key === databaseId || key.startsWith(`${databaseId}:`)) {
|
|
84
|
+
if (cache.delete(key))
|
|
55
85
|
cleared++;
|
|
56
|
-
}
|
|
57
86
|
}
|
|
58
|
-
log.debug(`Invalidated ${cleared} entries for databaseId=${databaseId}`);
|
|
59
87
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
for (const resolution of pending.values()) {
|
|
89
|
+
if (resolution.databaseId === databaseId)
|
|
90
|
+
resolution.invalidated = true;
|
|
63
91
|
}
|
|
92
|
+
log.debug(`Invalidated ${cleared} entries for databaseId=${databaseId}`);
|
|
64
93
|
},
|
|
65
94
|
get cacheSize() {
|
|
66
95
|
return cache.size;
|
package/esm/loaders/events.js
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
* Events Module Loader
|
|
3
3
|
*
|
|
4
4
|
* Resolves the tenant's app-scoped events module from
|
|
5
|
-
* metaschema_modules_public.events_module: the private schema and the
|
|
6
|
-
* its `record_event`
|
|
7
|
-
* hard-coding the generated schema or function names.
|
|
5
|
+
* metaschema_modules_public.events_module: the private schema and the names of
|
|
6
|
+
* its `record_event` and `record_error` functions, so the server can record
|
|
7
|
+
* events without hard-coding the generated schema or function names.
|
|
8
8
|
*/
|
|
9
9
|
import { createModuleLoader } from './create-loader';
|
|
10
10
|
// ─── SQL ────────────────────────────────────────────────────────────────────
|
|
11
11
|
const EVENTS_MODULE_SQL = `
|
|
12
12
|
SELECT
|
|
13
13
|
s.schema_name AS private_schema_name,
|
|
14
|
-
em.record_event
|
|
14
|
+
em.record_event,
|
|
15
|
+
em.record_error
|
|
15
16
|
FROM metaschema_modules_public.events_module em
|
|
16
17
|
JOIN metaschema_public.schema s ON s.id = em.private_schema_id
|
|
17
18
|
WHERE em.database_id = $1
|
|
@@ -30,7 +31,8 @@ export const eventsLoader = createModuleLoader({
|
|
|
30
31
|
return undefined;
|
|
31
32
|
return {
|
|
32
33
|
privateSchemaName: row.private_schema_name,
|
|
33
|
-
recordEvent: row.record_event
|
|
34
|
+
recordEvent: row.record_event,
|
|
35
|
+
recordError: row.record_error ?? null
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
});
|
package/esm/loaders/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - corsOrigins (routing-plane cors_settings)
|
|
11
11
|
* - databaseSettings(routing-plane database_settings)
|
|
12
12
|
* - requestProtection (routing-plane database_settings/api_settings bounds)
|
|
13
|
+
* - standing (metaschema_public.database suspended_at/suspended_reason, 5s TTL)
|
|
13
14
|
* - pubkeyChallengeSettings (routing-plane pubkey_settings)
|
|
14
15
|
* - webauthnSettings(routing-plane webauthn_settings)
|
|
15
16
|
* - authSettings (metaschema_modules_public.sessions_module → tenant DB)
|
|
@@ -49,6 +50,7 @@ export { llmLoader } from './llm';
|
|
|
49
50
|
export { pubkeyLoader } from './pubkey';
|
|
50
51
|
export { requestProtectionLoader } from './request-protection';
|
|
51
52
|
export { rlsLoader } from './rls';
|
|
53
|
+
export { DATABASE_STANDING_SQL, DATABASE_STANDING_TTL_MS, standingLoader } from './standing';
|
|
52
54
|
export { webauthnLoader } from './webauthn';
|
|
53
55
|
/**
|
|
54
56
|
* Convenience: create a registry pre-loaded with all built-in loaders.
|
|
@@ -67,6 +69,7 @@ import { pubkeyLoader } from './pubkey';
|
|
|
67
69
|
import { createLoaderRegistry } from './registry';
|
|
68
70
|
import { requestProtectionLoader } from './request-protection';
|
|
69
71
|
import { rlsLoader } from './rls';
|
|
72
|
+
import { standingLoader } from './standing';
|
|
70
73
|
import { webauthnLoader } from './webauthn';
|
|
71
74
|
export function createDefaultRegistry() {
|
|
72
75
|
const registry = createLoaderRegistry();
|
|
@@ -84,5 +87,6 @@ export function createDefaultRegistry() {
|
|
|
84
87
|
registry.register(computeLoader);
|
|
85
88
|
registry.register(eventsLoader);
|
|
86
89
|
registry.register(requestProtectionLoader);
|
|
90
|
+
registry.register(standingLoader);
|
|
87
91
|
return registry;
|
|
88
92
|
}
|
package/esm/loaders/registry.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* parallel. Useful for pre-warming or migration from the monolithic
|
|
10
10
|
* svcCache pattern.
|
|
11
11
|
*
|
|
12
|
-
* Each loader's result is independently cached per databaseId
|
|
13
|
-
* one module never invalidates another.
|
|
12
|
+
* Each loader's result is independently cached per databaseId and optional
|
|
13
|
+
* apiId — resolving one module never invalidates another.
|
|
14
14
|
*/
|
|
15
15
|
import { Logger } from '@pgpmjs/logger';
|
|
16
16
|
const log = new Logger('loader-registry');
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Standing Loader
|
|
3
|
+
*
|
|
4
|
+
* Whether a database may be served right now. The state is the two
|
|
5
|
+
* system-controlled columns on `metaschema_public.database`
|
|
6
|
+
* (`suspended_at`, `suspended_reason`) that billing and platform admins write;
|
|
7
|
+
* every lane reads it through this loader so the policy behind "suspended" can
|
|
8
|
+
* change without touching a serving path.
|
|
9
|
+
*
|
|
10
|
+
* The TTL is deliberately short: it bounds how long an established client
|
|
11
|
+
* keeps being served after its database is suspended.
|
|
12
|
+
*
|
|
13
|
+
* A plane without `metaschema_public.database` resolves to `undefined` like any
|
|
14
|
+
* other absent module (the factory's undefined_table handling); any other read
|
|
15
|
+
* failure propagates so the caller fails closed. A database the plane does not
|
|
16
|
+
* know is *not* servable — a request pinned to it has nothing to run against.
|
|
17
|
+
*/
|
|
18
|
+
import { createModuleLoader } from './create-loader';
|
|
19
|
+
// ─── SQL ────────────────────────────────────────────────────────────────────
|
|
20
|
+
export const DATABASE_STANDING_SQL = `
|
|
21
|
+
SELECT suspended_at, suspended_reason
|
|
22
|
+
FROM metaschema_public.database
|
|
23
|
+
WHERE id = $1
|
|
24
|
+
`;
|
|
25
|
+
export const DATABASE_STANDING_TTL_MS = 5_000;
|
|
26
|
+
// ─── Loader ─────────────────────────────────────────────────────────────────
|
|
27
|
+
export const standingLoader = createModuleLoader({
|
|
28
|
+
name: 'standing',
|
|
29
|
+
ttlMs: DATABASE_STANDING_TTL_MS,
|
|
30
|
+
async resolve(ctx) {
|
|
31
|
+
const { routingPool, databaseId } = ctx;
|
|
32
|
+
const { rows } = await routingPool.query(DATABASE_STANDING_SQL, [databaseId]);
|
|
33
|
+
const row = rows[0];
|
|
34
|
+
if (!row)
|
|
35
|
+
return { exists: false, suspended: false, suspendedAt: null, reason: null };
|
|
36
|
+
const suspendedAt = row.suspended_at === null ? null : new Date(row.suspended_at);
|
|
37
|
+
return {
|
|
38
|
+
exists: true,
|
|
39
|
+
suspended: suspendedAt !== null,
|
|
40
|
+
suspendedAt,
|
|
41
|
+
reason: row.suspended_reason
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
package/esm/loaders/types.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module Loader Types
|
|
3
3
|
*
|
|
4
|
-
* A ModuleLoader is a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A ModuleLoader is a cached lookup that resolves config from the routing DB
|
|
5
|
+
* or tenant DB. Each loader owns an independent, bounded LRU keyed by
|
|
6
|
+
* databaseId and optional apiId.
|
|
7
7
|
*
|
|
8
8
|
* Loaders are registered in a LoaderRegistry and resolved in parallel
|
|
9
9
|
* during context building. The result is a typed modules map on
|
package/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* });
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
export type { AgentChatConfig, ApiConfigResult, ApiError, ApiStructure, AuthSettings, AuthSurface, BillingConfig, BuiltinModuleMap, ComputeConfig, ComputeModuleConfig, ConstructiveAPIToken, ConstructiveContext, DatabaseSettings, EventsConfig, IdentityProviderConfig, IdentityProvidersModule, InferenceLogConfig, LlmConfig, PubkeyChallengeSettings, RlsModule, WebauthnSettings, WithPgClient, } from './types';
|
|
37
|
+
export type { AgentChatConfig, ApiConfigResult, ApiError, ApiStructure, AuthSettings, AuthSurface, BillingConfig, BuiltinModuleMap, ComputeConfig, ComputeModuleConfig, ConstructiveAPIToken, ConstructiveContext, DatabaseSettings, DatabaseStanding, EventsConfig, IdentityProviderConfig, IdentityProvidersModule, InferenceLogConfig, LlmConfig, PubkeyChallengeSettings, RlsModule, WebauthnSettings, WithPgClient, } from './types';
|
|
38
38
|
export type { BillingClient, InferenceLogEntry } from './billing-client';
|
|
39
39
|
export { createBillingClient } from './billing-client';
|
|
40
40
|
export type { PgSettingsInput } from './pg-settings';
|
|
@@ -52,5 +52,5 @@ export { requestIdMiddleware } from './request-id';
|
|
|
52
52
|
export type { ContextMiddlewareOptions } from './context';
|
|
53
53
|
export { buildContext, createContextMiddleware } from './context';
|
|
54
54
|
export type { CreateLoaderOptions, LoaderContext, LoaderRegistry, ModuleLoader, } from './loaders';
|
|
55
|
-
export { agentChatLoader, authSettingsLoader, authSurfaceLoader, billingLoader, computeLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, eventsLoader, identityProvidersLoader, inferenceLogLoader, llmLoader, pubkeyLoader, requestProtectionLoader, requireDatabaseId, requireIdentityProvider, rlsLoader, webauthnLoader, } from './loaders';
|
|
55
|
+
export { agentChatLoader, authSettingsLoader, authSurfaceLoader, billingLoader, computeLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, DATABASE_STANDING_SQL, DATABASE_STANDING_TTL_MS, databaseSettingsLoader, eventsLoader, identityProvidersLoader, inferenceLogLoader, llmLoader, pubkeyLoader, requestProtectionLoader, requireDatabaseId, requireIdentityProvider, rlsLoader, standingLoader, webauthnLoader, } from './loaders';
|
|
56
56
|
import './types';
|
package/index.js
CHANGED
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
40
|
-
exports.webauthnLoader = void 0;
|
|
39
|
+
exports.requireDatabaseId = exports.requestProtectionLoader = exports.pubkeyLoader = exports.llmLoader = exports.inferenceLogLoader = exports.identityProvidersLoader = exports.eventsLoader = exports.databaseSettingsLoader = exports.DATABASE_STANDING_TTL_MS = exports.DATABASE_STANDING_SQL = exports.createModuleLoader = exports.createLoaderRegistry = exports.createDefaultRegistry = exports.corsLoader = exports.computeLoader = exports.billingLoader = exports.authSurfaceLoader = exports.authSettingsLoader = exports.agentChatLoader = exports.createContextMiddleware = exports.buildContext = exports.requestIdMiddleware = exports.CounterFlusher = exports.BoundedCounter = exports.UNKNOWN_SOURCE = exports.sourceBucket = exports.refusalRows = exports.RefusalRecorder = exports.refusalOverflowKey = exports.refusalKeyOf = exports.REFUSAL_REASONS = exports.OVERFLOW_SOURCE = exports.OVERFLOW_ROUTE = exports.DEFAULT_REFUSAL_MAX_KEYS = exports.DEFAULT_REFUSAL_FLUSH_JITTER_MS = exports.DEFAULT_REFUSAL_FLUSH_INTERVAL_MS = exports.createRecordRefusalsSink = exports.trustedProxyHops = exports.RateWindow = exports.ConcurrencyLimiter = exports.clientIpFrom = exports.resolveRequestProtection = exports.protectionPgSettings = exports.PROTECTION_BOUNDS = exports.INTROSPECTION_BOUND = exports.DEFAULT_REQUEST_PROTECTION = exports.ASSUMED_PAGE_SIZE = exports.withPgClient = exports.buildPgSettings = exports.createBillingClient = void 0;
|
|
40
|
+
exports.webauthnLoader = exports.standingLoader = exports.rlsLoader = exports.requireIdentityProvider = void 0;
|
|
41
41
|
var billing_client_1 = require("./billing-client");
|
|
42
42
|
Object.defineProperty(exports, "createBillingClient", { enumerable: true, get: function () { return billing_client_1.createBillingClient; } });
|
|
43
43
|
var pg_settings_1 = require("./pg-settings");
|
|
@@ -90,6 +90,8 @@ Object.defineProperty(exports, "corsLoader", { enumerable: true, get: function (
|
|
|
90
90
|
Object.defineProperty(exports, "createDefaultRegistry", { enumerable: true, get: function () { return loaders_1.createDefaultRegistry; } });
|
|
91
91
|
Object.defineProperty(exports, "createLoaderRegistry", { enumerable: true, get: function () { return loaders_1.createLoaderRegistry; } });
|
|
92
92
|
Object.defineProperty(exports, "createModuleLoader", { enumerable: true, get: function () { return loaders_1.createModuleLoader; } });
|
|
93
|
+
Object.defineProperty(exports, "DATABASE_STANDING_SQL", { enumerable: true, get: function () { return loaders_1.DATABASE_STANDING_SQL; } });
|
|
94
|
+
Object.defineProperty(exports, "DATABASE_STANDING_TTL_MS", { enumerable: true, get: function () { return loaders_1.DATABASE_STANDING_TTL_MS; } });
|
|
93
95
|
Object.defineProperty(exports, "databaseSettingsLoader", { enumerable: true, get: function () { return loaders_1.databaseSettingsLoader; } });
|
|
94
96
|
Object.defineProperty(exports, "eventsLoader", { enumerable: true, get: function () { return loaders_1.eventsLoader; } });
|
|
95
97
|
Object.defineProperty(exports, "identityProvidersLoader", { enumerable: true, get: function () { return loaders_1.identityProvidersLoader; } });
|
|
@@ -100,6 +102,7 @@ Object.defineProperty(exports, "requestProtectionLoader", { enumerable: true, ge
|
|
|
100
102
|
Object.defineProperty(exports, "requireDatabaseId", { enumerable: true, get: function () { return loaders_1.requireDatabaseId; } });
|
|
101
103
|
Object.defineProperty(exports, "requireIdentityProvider", { enumerable: true, get: function () { return loaders_1.requireIdentityProvider; } });
|
|
102
104
|
Object.defineProperty(exports, "rlsLoader", { enumerable: true, get: function () { return loaders_1.rlsLoader; } });
|
|
105
|
+
Object.defineProperty(exports, "standingLoader", { enumerable: true, get: function () { return loaders_1.standingLoader; } });
|
|
103
106
|
Object.defineProperty(exports, "webauthnLoader", { enumerable: true, get: function () { return loaders_1.webauthnLoader; } });
|
|
104
107
|
// Side-effect: Express type augmentation
|
|
105
108
|
require("./types");
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* create-loader — Factory for building cached ModuleLoader instances.
|
|
3
3
|
*
|
|
4
4
|
* Wraps a raw resolve function with an LRU cache keyed by databaseId:apiId.
|
|
5
|
-
* Each loader gets its own independent cache with configurable TTL and
|
|
6
|
-
*
|
|
5
|
+
* Each loader gets its own independent cache with a configurable hard TTL and
|
|
6
|
+
* maximum size.
|
|
7
7
|
*/
|
|
8
8
|
import type { LoaderContext, ModuleLoader } from './types';
|
|
9
9
|
export interface CreateLoaderOptions<T> {
|
package/loaders/create-loader.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* create-loader — Factory for building cached ModuleLoader instances.
|
|
4
4
|
*
|
|
5
5
|
* Wraps a raw resolve function with an LRU cache keyed by databaseId:apiId.
|
|
6
|
-
* Each loader gets its own independent cache with configurable TTL and
|
|
7
|
-
*
|
|
6
|
+
* Each loader gets its own independent cache with a configurable hard TTL and
|
|
7
|
+
* maximum size.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.createModuleLoader = createModuleLoader;
|
|
@@ -17,53 +17,82 @@ function createModuleLoader(opts) {
|
|
|
17
17
|
const cache = new lru_cache_1.LRUCache({
|
|
18
18
|
max: opts.max ?? DEFAULT_MAX,
|
|
19
19
|
ttl: opts.ttlMs ?? DEFAULT_TTL_MS,
|
|
20
|
-
updateAgeOnGet:
|
|
20
|
+
updateAgeOnGet: false,
|
|
21
21
|
allowStale: false,
|
|
22
22
|
});
|
|
23
|
+
const pending = new Map();
|
|
23
24
|
return {
|
|
24
25
|
name: opts.name,
|
|
25
26
|
async resolve(ctx) {
|
|
26
27
|
const key = ctx.apiId ? `${ctx.databaseId}:${ctx.apiId}` : ctx.databaseId;
|
|
27
|
-
|
|
28
|
+
const cached = cache.get(key);
|
|
29
|
+
if (cached !== undefined) {
|
|
28
30
|
log.debug(`Cache HIT databaseId=${key}`);
|
|
29
|
-
return
|
|
31
|
+
return cached;
|
|
32
|
+
}
|
|
33
|
+
const existing = pending.get(key);
|
|
34
|
+
if (existing && !existing.invalidated) {
|
|
35
|
+
log.debug(`Cache COALESCE databaseId=${key}`);
|
|
36
|
+
return existing.promise;
|
|
30
37
|
}
|
|
31
38
|
log.debug(`Cache MISS databaseId=${key}, resolving`);
|
|
32
39
|
// "Not provisioned" is expressed by the loader returning undefined, or
|
|
33
40
|
// by the module's tables not existing at all (42P01 undefined_table).
|
|
34
41
|
// Any other resolution error (bad query, ambiguous config) propagates —
|
|
35
42
|
// never silently coerced into "module absent".
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const resolution = {
|
|
44
|
+
databaseId: ctx.databaseId,
|
|
45
|
+
invalidated: false,
|
|
46
|
+
promise: Promise.resolve(undefined)
|
|
47
|
+
};
|
|
48
|
+
resolution.promise = Promise.resolve().then(async () => {
|
|
49
|
+
try {
|
|
50
|
+
const value = await opts.resolve(ctx);
|
|
51
|
+
// Keep absence uncached so subsequent calls can discover new config.
|
|
52
|
+
if (!resolution.invalidated && value !== undefined) {
|
|
53
|
+
cache.set(key, value);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
46
56
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
catch (e) {
|
|
58
|
+
if (e.code === '42P01') {
|
|
59
|
+
log.debug(`Module tables absent for databaseId=${key}: ${e.message}`);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
log.warn(`Failed to resolve databaseId=${key}: ${e.message}`);
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
if (pending.get(key) === resolution) {
|
|
67
|
+
pending.delete(key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
pending.set(key, resolution);
|
|
72
|
+
return resolution.promise;
|
|
50
73
|
},
|
|
51
74
|
invalidate(databaseId) {
|
|
52
|
-
if (databaseId) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for (const
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
if (!databaseId) {
|
|
76
|
+
const previousSize = cache.size;
|
|
77
|
+
cache.clear();
|
|
78
|
+
for (const resolution of pending.values()) {
|
|
79
|
+
resolution.invalidated = true;
|
|
80
|
+
}
|
|
81
|
+
log.debug(`Invalidated all entries (was size=${previousSize})`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
let cleared = 0;
|
|
85
|
+
for (const key of cache.keys()) {
|
|
86
|
+
if (key === databaseId || key.startsWith(`${databaseId}:`)) {
|
|
87
|
+
if (cache.delete(key))
|
|
58
88
|
cleared++;
|
|
59
|
-
}
|
|
60
89
|
}
|
|
61
|
-
log.debug(`Invalidated ${cleared} entries for databaseId=${databaseId}`);
|
|
62
90
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
91
|
+
for (const resolution of pending.values()) {
|
|
92
|
+
if (resolution.databaseId === databaseId)
|
|
93
|
+
resolution.invalidated = true;
|
|
66
94
|
}
|
|
95
|
+
log.debug(`Invalidated ${cleared} entries for databaseId=${databaseId}`);
|
|
67
96
|
},
|
|
68
97
|
get cacheSize() {
|
|
69
98
|
return cache.size;
|
package/loaders/events.d.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Events Module Loader
|
|
3
3
|
*
|
|
4
4
|
* Resolves the tenant's app-scoped events module from
|
|
5
|
-
* metaschema_modules_public.events_module: the private schema and the
|
|
6
|
-
* its `record_event`
|
|
7
|
-
* hard-coding the generated schema or function names.
|
|
5
|
+
* metaschema_modules_public.events_module: the private schema and the names of
|
|
6
|
+
* its `record_event` and `record_error` functions, so the server can record
|
|
7
|
+
* events without hard-coding the generated schema or function names.
|
|
8
8
|
*/
|
|
9
9
|
import type { EventsConfig } from '../types';
|
|
10
10
|
import type { ModuleLoader } from './types';
|
package/loaders/events.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Events Module Loader
|
|
4
4
|
*
|
|
5
5
|
* Resolves the tenant's app-scoped events module from
|
|
6
|
-
* metaschema_modules_public.events_module: the private schema and the
|
|
7
|
-
* its `record_event`
|
|
8
|
-
* hard-coding the generated schema or function names.
|
|
6
|
+
* metaschema_modules_public.events_module: the private schema and the names of
|
|
7
|
+
* its `record_event` and `record_error` functions, so the server can record
|
|
8
|
+
* events without hard-coding the generated schema or function names.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.eventsLoader = void 0;
|
|
@@ -14,7 +14,8 @@ const create_loader_1 = require("./create-loader");
|
|
|
14
14
|
const EVENTS_MODULE_SQL = `
|
|
15
15
|
SELECT
|
|
16
16
|
s.schema_name AS private_schema_name,
|
|
17
|
-
em.record_event
|
|
17
|
+
em.record_event,
|
|
18
|
+
em.record_error
|
|
18
19
|
FROM metaschema_modules_public.events_module em
|
|
19
20
|
JOIN metaschema_public.schema s ON s.id = em.private_schema_id
|
|
20
21
|
WHERE em.database_id = $1
|
|
@@ -33,7 +34,8 @@ exports.eventsLoader = (0, create_loader_1.createModuleLoader)({
|
|
|
33
34
|
return undefined;
|
|
34
35
|
return {
|
|
35
36
|
privateSchemaName: row.private_schema_name,
|
|
36
|
-
recordEvent: row.record_event
|
|
37
|
+
recordEvent: row.record_event,
|
|
38
|
+
recordError: row.record_error ?? null
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
});
|
package/loaders/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - corsOrigins (routing-plane cors_settings)
|
|
11
11
|
* - databaseSettings(routing-plane database_settings)
|
|
12
12
|
* - requestProtection (routing-plane database_settings/api_settings bounds)
|
|
13
|
+
* - standing (metaschema_public.database suspended_at/suspended_reason, 5s TTL)
|
|
13
14
|
* - pubkeyChallengeSettings (routing-plane pubkey_settings)
|
|
14
15
|
* - webauthnSettings(routing-plane webauthn_settings)
|
|
15
16
|
* - authSettings (metaschema_modules_public.sessions_module → tenant DB)
|
|
@@ -51,5 +52,6 @@ export { llmLoader } from './llm';
|
|
|
51
52
|
export { pubkeyLoader } from './pubkey';
|
|
52
53
|
export { requestProtectionLoader } from './request-protection';
|
|
53
54
|
export { rlsLoader } from './rls';
|
|
55
|
+
export { DATABASE_STANDING_SQL, DATABASE_STANDING_TTL_MS, standingLoader } from './standing';
|
|
54
56
|
export { webauthnLoader } from './webauthn';
|
|
55
57
|
export declare function createDefaultRegistry(): import("./registry").LoaderRegistry;
|
package/loaders/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - corsOrigins (routing-plane cors_settings)
|
|
12
12
|
* - databaseSettings(routing-plane database_settings)
|
|
13
13
|
* - requestProtection (routing-plane database_settings/api_settings bounds)
|
|
14
|
+
* - standing (metaschema_public.database suspended_at/suspended_reason, 5s TTL)
|
|
14
15
|
* - pubkeyChallengeSettings (routing-plane pubkey_settings)
|
|
15
16
|
* - webauthnSettings(routing-plane webauthn_settings)
|
|
16
17
|
* - authSettings (metaschema_modules_public.sessions_module → tenant DB)
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
* registry.register(myLoader);
|
|
34
35
|
*/
|
|
35
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.webauthnLoader = exports.rlsLoader = exports.requestProtectionLoader = exports.pubkeyLoader = exports.llmLoader = exports.inferenceLogLoader = exports.requireIdentityProvider = exports.identityProvidersLoader = exports.eventsLoader = exports.databaseSettingsLoader = exports.corsLoader = exports.computeLoader = exports.billingLoader = exports.authSurfaceLoader = exports.authSettingsLoader = exports.agentChatLoader = exports.createLoaderRegistry = exports.createModuleLoader = exports.requireDatabaseId = void 0;
|
|
37
|
+
exports.webauthnLoader = exports.standingLoader = exports.DATABASE_STANDING_TTL_MS = exports.DATABASE_STANDING_SQL = exports.rlsLoader = exports.requestProtectionLoader = exports.pubkeyLoader = exports.llmLoader = exports.inferenceLogLoader = exports.requireIdentityProvider = exports.identityProvidersLoader = exports.eventsLoader = exports.databaseSettingsLoader = exports.corsLoader = exports.computeLoader = exports.billingLoader = exports.authSurfaceLoader = exports.authSettingsLoader = exports.agentChatLoader = exports.createLoaderRegistry = exports.createModuleLoader = exports.requireDatabaseId = void 0;
|
|
37
38
|
exports.createDefaultRegistry = createDefaultRegistry;
|
|
38
39
|
var types_1 = require("./types");
|
|
39
40
|
Object.defineProperty(exports, "requireDatabaseId", { enumerable: true, get: function () { return types_1.requireDatabaseId; } });
|
|
@@ -71,6 +72,10 @@ var request_protection_1 = require("./request-protection");
|
|
|
71
72
|
Object.defineProperty(exports, "requestProtectionLoader", { enumerable: true, get: function () { return request_protection_1.requestProtectionLoader; } });
|
|
72
73
|
var rls_1 = require("./rls");
|
|
73
74
|
Object.defineProperty(exports, "rlsLoader", { enumerable: true, get: function () { return rls_1.rlsLoader; } });
|
|
75
|
+
var standing_1 = require("./standing");
|
|
76
|
+
Object.defineProperty(exports, "DATABASE_STANDING_SQL", { enumerable: true, get: function () { return standing_1.DATABASE_STANDING_SQL; } });
|
|
77
|
+
Object.defineProperty(exports, "DATABASE_STANDING_TTL_MS", { enumerable: true, get: function () { return standing_1.DATABASE_STANDING_TTL_MS; } });
|
|
78
|
+
Object.defineProperty(exports, "standingLoader", { enumerable: true, get: function () { return standing_1.standingLoader; } });
|
|
74
79
|
var webauthn_1 = require("./webauthn");
|
|
75
80
|
Object.defineProperty(exports, "webauthnLoader", { enumerable: true, get: function () { return webauthn_1.webauthnLoader; } });
|
|
76
81
|
/**
|
|
@@ -90,6 +95,7 @@ const pubkey_2 = require("./pubkey");
|
|
|
90
95
|
const registry_2 = require("./registry");
|
|
91
96
|
const request_protection_2 = require("./request-protection");
|
|
92
97
|
const rls_2 = require("./rls");
|
|
98
|
+
const standing_2 = require("./standing");
|
|
93
99
|
const webauthn_2 = require("./webauthn");
|
|
94
100
|
function createDefaultRegistry() {
|
|
95
101
|
const registry = (0, registry_2.createLoaderRegistry)();
|
|
@@ -107,5 +113,6 @@ function createDefaultRegistry() {
|
|
|
107
113
|
registry.register(compute_2.computeLoader);
|
|
108
114
|
registry.register(events_2.eventsLoader);
|
|
109
115
|
registry.register(request_protection_2.requestProtectionLoader);
|
|
116
|
+
registry.register(standing_2.standingLoader);
|
|
110
117
|
return registry;
|
|
111
118
|
}
|
package/loaders/registry.d.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* parallel. Useful for pre-warming or migration from the monolithic
|
|
10
10
|
* svcCache pattern.
|
|
11
11
|
*
|
|
12
|
-
* Each loader's result is independently cached per databaseId
|
|
13
|
-
* one module never invalidates another.
|
|
12
|
+
* Each loader's result is independently cached per databaseId and optional
|
|
13
|
+
* apiId — resolving one module never invalidates another.
|
|
14
14
|
*/
|
|
15
15
|
import type { LoaderContext, ModuleLoader } from './types';
|
|
16
16
|
export interface LoaderRegistry {
|
|
@@ -19,8 +19,8 @@ export interface LoaderRegistry {
|
|
|
19
19
|
/**
|
|
20
20
|
* Resolve a single loader by name (lazy, on-demand).
|
|
21
21
|
* Returns undefined if the loader isn't registered or the module
|
|
22
|
-
* isn't provisioned for this database. Results are cached per databaseId
|
|
23
|
-
* inside the loader's own LRU — repeated calls are cheap.
|
|
22
|
+
* isn't provisioned for this database. Results are cached per databaseId and
|
|
23
|
+
* optional apiId inside the loader's own LRU — repeated calls are cheap.
|
|
24
24
|
*/
|
|
25
25
|
resolve<T = unknown>(name: string, ctx: LoaderContext): Promise<T | undefined>;
|
|
26
26
|
/** Resolve all registered loaders in parallel (eager, for migration/pre-warm). */
|
package/loaders/registry.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* parallel. Useful for pre-warming or migration from the monolithic
|
|
11
11
|
* svcCache pattern.
|
|
12
12
|
*
|
|
13
|
-
* Each loader's result is independently cached per databaseId
|
|
14
|
-
* one module never invalidates another.
|
|
13
|
+
* Each loader's result is independently cached per databaseId and optional
|
|
14
|
+
* apiId — resolving one module never invalidates another.
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.createLoaderRegistry = createLoaderRegistry;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Standing Loader
|
|
3
|
+
*
|
|
4
|
+
* Whether a database may be served right now. The state is the two
|
|
5
|
+
* system-controlled columns on `metaschema_public.database`
|
|
6
|
+
* (`suspended_at`, `suspended_reason`) that billing and platform admins write;
|
|
7
|
+
* every lane reads it through this loader so the policy behind "suspended" can
|
|
8
|
+
* change without touching a serving path.
|
|
9
|
+
*
|
|
10
|
+
* The TTL is deliberately short: it bounds how long an established client
|
|
11
|
+
* keeps being served after its database is suspended.
|
|
12
|
+
*
|
|
13
|
+
* A plane without `metaschema_public.database` resolves to `undefined` like any
|
|
14
|
+
* other absent module (the factory's undefined_table handling); any other read
|
|
15
|
+
* failure propagates so the caller fails closed. A database the plane does not
|
|
16
|
+
* know is *not* servable — a request pinned to it has nothing to run against.
|
|
17
|
+
*/
|
|
18
|
+
import type { DatabaseStanding } from '../types';
|
|
19
|
+
import type { ModuleLoader } from './types';
|
|
20
|
+
export declare const DATABASE_STANDING_SQL = "\n SELECT suspended_at, suspended_reason\n FROM metaschema_public.database\n WHERE id = $1\n";
|
|
21
|
+
export declare const DATABASE_STANDING_TTL_MS = 5000;
|
|
22
|
+
export declare const standingLoader: ModuleLoader<DatabaseStanding>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Database Standing Loader
|
|
4
|
+
*
|
|
5
|
+
* Whether a database may be served right now. The state is the two
|
|
6
|
+
* system-controlled columns on `metaschema_public.database`
|
|
7
|
+
* (`suspended_at`, `suspended_reason`) that billing and platform admins write;
|
|
8
|
+
* every lane reads it through this loader so the policy behind "suspended" can
|
|
9
|
+
* change without touching a serving path.
|
|
10
|
+
*
|
|
11
|
+
* The TTL is deliberately short: it bounds how long an established client
|
|
12
|
+
* keeps being served after its database is suspended.
|
|
13
|
+
*
|
|
14
|
+
* A plane without `metaschema_public.database` resolves to `undefined` like any
|
|
15
|
+
* other absent module (the factory's undefined_table handling); any other read
|
|
16
|
+
* failure propagates so the caller fails closed. A database the plane does not
|
|
17
|
+
* know is *not* servable — a request pinned to it has nothing to run against.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.standingLoader = exports.DATABASE_STANDING_TTL_MS = exports.DATABASE_STANDING_SQL = void 0;
|
|
21
|
+
const create_loader_1 = require("./create-loader");
|
|
22
|
+
// ─── SQL ────────────────────────────────────────────────────────────────────
|
|
23
|
+
exports.DATABASE_STANDING_SQL = `
|
|
24
|
+
SELECT suspended_at, suspended_reason
|
|
25
|
+
FROM metaschema_public.database
|
|
26
|
+
WHERE id = $1
|
|
27
|
+
`;
|
|
28
|
+
exports.DATABASE_STANDING_TTL_MS = 5_000;
|
|
29
|
+
// ─── Loader ─────────────────────────────────────────────────────────────────
|
|
30
|
+
exports.standingLoader = (0, create_loader_1.createModuleLoader)({
|
|
31
|
+
name: 'standing',
|
|
32
|
+
ttlMs: exports.DATABASE_STANDING_TTL_MS,
|
|
33
|
+
async resolve(ctx) {
|
|
34
|
+
const { routingPool, databaseId } = ctx;
|
|
35
|
+
const { rows } = await routingPool.query(exports.DATABASE_STANDING_SQL, [databaseId]);
|
|
36
|
+
const row = rows[0];
|
|
37
|
+
if (!row)
|
|
38
|
+
return { exists: false, suspended: false, suspendedAt: null, reason: null };
|
|
39
|
+
const suspendedAt = row.suspended_at === null ? null : new Date(row.suspended_at);
|
|
40
|
+
return {
|
|
41
|
+
exists: true,
|
|
42
|
+
suspended: suspendedAt !== null,
|
|
43
|
+
suspendedAt,
|
|
44
|
+
reason: row.suspended_reason
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
package/loaders/types.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module Loader Types
|
|
3
3
|
*
|
|
4
|
-
* A ModuleLoader is a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A ModuleLoader is a cached lookup that resolves config from the routing DB
|
|
5
|
+
* or tenant DB. Each loader owns an independent, bounded LRU keyed by
|
|
6
|
+
* databaseId and optional apiId.
|
|
7
7
|
*
|
|
8
8
|
* Loaders are registered in a LoaderRegistry and resolved in parallel
|
|
9
9
|
* during context building. The result is a typed modules map on
|
|
@@ -48,15 +48,18 @@ export interface LoaderContext {
|
|
|
48
48
|
dbname: string;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* A single module loader. Encapsulates the SQL query, type transform,
|
|
52
|
-
*
|
|
51
|
+
* A single module loader. Encapsulates the SQL query, type transform, and
|
|
52
|
+
* per-database/API LRU cache for one piece of per-database config.
|
|
53
53
|
*/
|
|
54
54
|
export interface ModuleLoader<T = unknown> {
|
|
55
55
|
/** Unique name (used in log prefix and as the key in the modules map) */
|
|
56
56
|
readonly name: string;
|
|
57
57
|
/** Resolve the module config for a given database. Returns undefined if not provisioned. */
|
|
58
58
|
resolve(ctx: LoaderContext): Promise<T | undefined>;
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Invalidate one logical database, including all of its API entries.
|
|
61
|
+
* Omitting the database ID clears everything.
|
|
62
|
+
*/
|
|
60
63
|
invalidate(databaseId?: string): void;
|
|
61
64
|
/** Current number of cached entries */
|
|
62
65
|
readonly cacheSize: number;
|
package/loaders/types.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Module Loader Types
|
|
4
4
|
*
|
|
5
|
-
* A ModuleLoader is a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* A ModuleLoader is a cached lookup that resolves config from the routing DB
|
|
6
|
+
* or tenant DB. Each loader owns an independent, bounded LRU keyed by
|
|
7
|
+
* databaseId and optional apiId.
|
|
8
8
|
*
|
|
9
9
|
* Loaders are registered in a LoaderRegistry and resolved in parallel
|
|
10
10
|
* during context building. The result is a typed modules map on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/express-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Extractable Express middleware for Constructive tenant context — domain resolution, JWT auth, pgSettings, and withPgClient",
|
|
5
5
|
"author": "Constructive <developers@constructive.io>",
|
|
6
6
|
"homepage": "https://github.com/constructive-io/constructive",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@constructive-io/url-domains": "^2.30.1",
|
|
33
|
-
"@pgpmjs/env": "^2.43.
|
|
33
|
+
"@pgpmjs/env": "^2.43.5",
|
|
34
34
|
"@pgpmjs/logger": "^2.25.1",
|
|
35
|
-
"@pgpmjs/server-utils": "^3.27.
|
|
36
|
-
"@pgpmjs/types": "^2.53.
|
|
35
|
+
"@pgpmjs/server-utils": "^3.27.5",
|
|
36
|
+
"@pgpmjs/types": "^2.53.3",
|
|
37
37
|
"lru-cache": "^11.2.7",
|
|
38
38
|
"pg": "^8.21.0",
|
|
39
|
-
"pg-cache": "^3.27.
|
|
39
|
+
"pg-cache": "^3.27.5",
|
|
40
40
|
"pg-query-context": "^2.30.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"jwt",
|
|
60
60
|
"pgSettings"
|
|
61
61
|
],
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "27be33dc1ba8351e2a2c2bbb99b9c0798c0813b9"
|
|
63
63
|
}
|
package/types.d.ts
CHANGED
|
@@ -199,10 +199,15 @@ export interface ComputeModuleConfig {
|
|
|
199
199
|
export interface ComputeConfig {
|
|
200
200
|
modules: ComputeModuleConfig[];
|
|
201
201
|
}
|
|
202
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* The tenant's app-scoped events module: where `record_event` and
|
|
204
|
+
* `record_error` live. `recordError` is null on a tenant whose events module
|
|
205
|
+
* predates the function; callers must report that rather than skip silently.
|
|
206
|
+
*/
|
|
203
207
|
export interface EventsConfig {
|
|
204
208
|
privateSchemaName: string;
|
|
205
209
|
recordEvent: string;
|
|
210
|
+
recordError: string | null;
|
|
206
211
|
}
|
|
207
212
|
export interface LlmConfig {
|
|
208
213
|
embeddingProvider: string;
|
|
@@ -216,6 +221,17 @@ export interface LlmConfig {
|
|
|
216
221
|
maxTokensPerRequest: number | null;
|
|
217
222
|
ragContextLimit: number | null;
|
|
218
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Whether a database may be served right now, as read from
|
|
226
|
+
* `metaschema_public.database`. A database the plane does not know is not
|
|
227
|
+
* servable either (`exists: false`).
|
|
228
|
+
*/
|
|
229
|
+
export interface DatabaseStanding {
|
|
230
|
+
exists: boolean;
|
|
231
|
+
suspended: boolean;
|
|
232
|
+
suspendedAt: Date | null;
|
|
233
|
+
reason: string | null;
|
|
234
|
+
}
|
|
219
235
|
/**
|
|
220
236
|
* Maps built-in loader names to their resolved types.
|
|
221
237
|
*
|
|
@@ -240,6 +256,7 @@ export interface BuiltinModuleMap {
|
|
|
240
256
|
compute: ComputeConfig;
|
|
241
257
|
events: EventsConfig;
|
|
242
258
|
requestProtection: RequestProtection;
|
|
259
|
+
standing: DatabaseStanding;
|
|
243
260
|
}
|
|
244
261
|
/**
|
|
245
262
|
* Callback for executing queries within a tenant-scoped transaction
|