@constructive-io/express-context 0.30.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/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/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 +12 -0
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/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/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
|
@@ -221,6 +221,17 @@ export interface LlmConfig {
|
|
|
221
221
|
maxTokensPerRequest: number | null;
|
|
222
222
|
ragContextLimit: number | null;
|
|
223
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
|
+
}
|
|
224
235
|
/**
|
|
225
236
|
* Maps built-in loader names to their resolved types.
|
|
226
237
|
*
|
|
@@ -245,6 +256,7 @@ export interface BuiltinModuleMap {
|
|
|
245
256
|
compute: ComputeConfig;
|
|
246
257
|
events: EventsConfig;
|
|
247
258
|
requestProtection: RequestProtection;
|
|
259
|
+
standing: DatabaseStanding;
|
|
248
260
|
}
|
|
249
261
|
/**
|
|
250
262
|
* Callback for executing queries within a tenant-scoped transaction
|