@constructive-io/express-context 0.2.0 → 0.3.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 +57 -5
- package/esm/index.js +1 -1
- package/esm/loaders/agent-chat.js +36 -0
- package/esm/loaders/billing.js +37 -0
- package/esm/loaders/create-loader.js +11 -4
- package/esm/loaders/index.js +9 -0
- package/esm/loaders/inference-log.js +33 -0
- package/index.d.ts +2 -2
- package/index.js +4 -1
- package/loaders/agent-chat.d.ts +9 -0
- package/loaders/agent-chat.js +39 -0
- package/loaders/billing.d.ts +9 -0
- package/loaders/billing.js +40 -0
- package/loaders/create-loader.d.ts +1 -1
- package/loaders/create-loader.js +11 -4
- package/loaders/index.d.ts +3 -0
- package/loaders/index.js +13 -1
- package/loaders/inference-log.d.ts +9 -0
- package/loaders/inference-log.js +36 -0
- package/package.json +2 -2
- package/types.d.ts +19 -0
package/README.md
CHANGED
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
# @constructive-io/express-context
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center" width="100%">
|
|
4
|
+
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center" width="100%">
|
|
8
|
+
<a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
|
|
9
|
+
<img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/@constructive-io/express-context"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=packages%2Fexpress-context%2Fpackage.json"/></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
Extractable Express middleware for Constructive tenant context — domain resolution, JWT auth, pgSettings, withPgClient, and modular per-database cached lookups.
|
|
4
16
|
|
|
5
17
|
## Usage
|
|
6
18
|
|
|
7
19
|
```typescript
|
|
8
20
|
import {
|
|
9
21
|
createContextMiddleware,
|
|
10
|
-
requestIdMiddleware
|
|
22
|
+
requestIdMiddleware,
|
|
23
|
+
createDefaultRegistry,
|
|
11
24
|
} from '@constructive-io/express-context';
|
|
12
25
|
|
|
26
|
+
const loaders = createDefaultRegistry();
|
|
27
|
+
|
|
13
28
|
const app = express();
|
|
14
29
|
|
|
15
30
|
app.use(requestIdMiddleware());
|
|
16
31
|
app.use(apiMiddleware); // sets req.api
|
|
17
32
|
app.use(authMiddleware); // sets req.token
|
|
18
|
-
app.use(createContextMiddleware()); // builds req.constructive
|
|
33
|
+
app.use(createContextMiddleware({ loaders })); // builds req.constructive
|
|
19
34
|
|
|
20
35
|
app.post('/v1/chat', async (req, res) => {
|
|
21
|
-
const
|
|
36
|
+
const ctx = req.constructive;
|
|
37
|
+
const rls = await ctx.useModule('rlsModule'); // only fires if not cached
|
|
38
|
+
const auth = await ctx.useModule('authSettings'); // only fires if not cached
|
|
39
|
+
// webauthnSettings loader never fires if nobody asks for it
|
|
22
40
|
|
|
23
|
-
const result = await withPgClient(async (client) => {
|
|
41
|
+
const result = await ctx.withPgClient(async (client) => {
|
|
24
42
|
return client.query('SELECT current_user_id()');
|
|
25
43
|
});
|
|
26
44
|
|
|
@@ -35,6 +53,40 @@ app.post('/v1/chat', async (req, res) => {
|
|
|
35
53
|
- **withPgClient** — Tenant-scoped RLS transaction helper (BEGIN → SET LOCAL → fn → COMMIT)
|
|
36
54
|
- **requestId middleware** — UUID correlation ID (from X-Request-Id header or generated)
|
|
37
55
|
- **Context middleware** — Composes all of the above into `req.constructive`
|
|
56
|
+
- **Module loaders** — Pluggable per-database cached lookups with lazy on-demand resolution
|
|
57
|
+
|
|
58
|
+
## Module Loaders
|
|
59
|
+
|
|
60
|
+
Each loader encapsulates a SQL query + type transform + per-databaseId LRU cache for one piece of per-database configuration. Loaders are registered in a `LoaderRegistry` and resolved lazily via `useModule(name)`.
|
|
61
|
+
|
|
62
|
+
### Built-in loaders
|
|
63
|
+
|
|
64
|
+
| Loader | Source | Description |
|
|
65
|
+
|--------|--------|-------------|
|
|
66
|
+
| `rlsLoader` | `services_public.rls_settings` | RLS module (authenticate functions, schema refs) |
|
|
67
|
+
| `corsLoader` | `services_public.cors_settings` | CORS allowed origins |
|
|
68
|
+
| `databaseSettingsLoader` | `services_public.database_settings` | Feature flags (aggregates, search, uploads, etc.) |
|
|
69
|
+
| `pubkeyLoader` | `services_public.pubkey_settings` | Public key challenge auth settings |
|
|
70
|
+
| `webauthnLoader` | `services_public.webauthn_settings` | WebAuthn/passkey configuration |
|
|
71
|
+
| `authSettingsLoader` | `metaschema_modules_public.sessions_module` | Cookie/captcha settings (two-step tenant DB discovery) |
|
|
72
|
+
|
|
73
|
+
### Custom loaders
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { createModuleLoader, createLoaderRegistry } from '@constructive-io/express-context';
|
|
77
|
+
|
|
78
|
+
const myLoader = createModuleLoader({
|
|
79
|
+
name: 'myModule',
|
|
80
|
+
ttlMs: 60_000,
|
|
81
|
+
async resolve(ctx) {
|
|
82
|
+
const { rows } = await ctx.tenantPool.query(MY_SQL, [ctx.databaseId]);
|
|
83
|
+
return rows[0] ? transform(rows[0]) : undefined;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const registry = createLoaderRegistry();
|
|
88
|
+
registry.register(myLoader);
|
|
89
|
+
```
|
|
38
90
|
|
|
39
91
|
---
|
|
40
92
|
|
package/esm/index.js
CHANGED
|
@@ -40,6 +40,6 @@ export { withPgClient } from './pg-client';
|
|
|
40
40
|
// Request ID middleware
|
|
41
41
|
export { requestIdMiddleware } from './request-id';
|
|
42
42
|
export { buildContext, createContextMiddleware } from './context';
|
|
43
|
-
export { authSettingsLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, pubkeyLoader, rlsLoader, webauthnLoader, } from './loaders';
|
|
43
|
+
export { agentChatLoader, authSettingsLoader, billingLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, inferenceLogLoader, pubkeyLoader, rlsLoader, webauthnLoader, } from './loaders';
|
|
44
44
|
// Side-effect: Express type augmentation
|
|
45
45
|
import './types';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Chat Module Loader
|
|
3
|
+
*
|
|
4
|
+
* Resolves per-database agent chat config from metaschema_modules_public.agent_chat_module.
|
|
5
|
+
* Returns the schema and table names for threads, messages, and tasks.
|
|
6
|
+
*/
|
|
7
|
+
import { createModuleLoader } from './create-loader';
|
|
8
|
+
// ─── SQL ────────────────────────────────────────────────────────────────────
|
|
9
|
+
const AGENT_CHAT_MODULE_SQL = `
|
|
10
|
+
SELECT
|
|
11
|
+
s.schema_name,
|
|
12
|
+
acm.thread_table_name,
|
|
13
|
+
acm.message_table_name,
|
|
14
|
+
acm.task_table_name
|
|
15
|
+
FROM metaschema_modules_public.agent_chat_module acm
|
|
16
|
+
JOIN metaschema_public.schema s ON s.id = acm.schema_id
|
|
17
|
+
LIMIT 1
|
|
18
|
+
`;
|
|
19
|
+
// ─── Loader ─────────────────────────────────────────────────────────────────
|
|
20
|
+
export const agentChatLoader = createModuleLoader({
|
|
21
|
+
name: 'agentChat',
|
|
22
|
+
ttlMs: 60_000,
|
|
23
|
+
async resolve(ctx) {
|
|
24
|
+
const { tenantPool } = ctx;
|
|
25
|
+
const result = await tenantPool.query(AGENT_CHAT_MODULE_SQL);
|
|
26
|
+
const row = result.rows[0];
|
|
27
|
+
if (!row)
|
|
28
|
+
return undefined;
|
|
29
|
+
return {
|
|
30
|
+
schemaName: row.schema_name,
|
|
31
|
+
threadTableName: row.thread_table_name,
|
|
32
|
+
messageTableName: row.message_table_name,
|
|
33
|
+
taskTableName: row.task_table_name,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Module Loader
|
|
3
|
+
*
|
|
4
|
+
* Resolves per-database billing config from metaschema_modules_public.billing_module.
|
|
5
|
+
* Returns schema names and function names needed for quota checks and usage recording.
|
|
6
|
+
*/
|
|
7
|
+
import { createModuleLoader } from './create-loader';
|
|
8
|
+
// ─── SQL ────────────────────────────────────────────────────────────────────
|
|
9
|
+
const BILLING_MODULE_SQL = `
|
|
10
|
+
SELECT
|
|
11
|
+
s.schema_name AS public_schema,
|
|
12
|
+
ps.schema_name AS private_schema,
|
|
13
|
+
bm.record_usage_function
|
|
14
|
+
FROM metaschema_modules_public.billing_module bm
|
|
15
|
+
JOIN metaschema_public.schema s ON bm.schema_id = s.id
|
|
16
|
+
JOIN metaschema_public.schema ps ON bm.private_schema_id = ps.id
|
|
17
|
+
WHERE bm.database_id = $1
|
|
18
|
+
LIMIT 1
|
|
19
|
+
`;
|
|
20
|
+
// ─── Loader ─────────────────────────────────────────────────────────────────
|
|
21
|
+
export const billingLoader = createModuleLoader({
|
|
22
|
+
name: 'billing',
|
|
23
|
+
ttlMs: 5 * 60_000,
|
|
24
|
+
async resolve(ctx) {
|
|
25
|
+
const { tenantPool, databaseId } = ctx;
|
|
26
|
+
const result = await tenantPool.query(BILLING_MODULE_SQL, [databaseId]);
|
|
27
|
+
const row = result.rows[0];
|
|
28
|
+
if (!row?.record_usage_function)
|
|
29
|
+
return undefined;
|
|
30
|
+
return {
|
|
31
|
+
publicSchema: row.public_schema,
|
|
32
|
+
privateSchema: row.private_schema,
|
|
33
|
+
recordUsageFunction: row.record_usage_function,
|
|
34
|
+
checkBillingQuotaFunction: 'check_billing_quota',
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* create-loader — Factory for building cached ModuleLoader instances.
|
|
3
3
|
*
|
|
4
|
-
* Wraps a raw resolve function with an LRU cache keyed by databaseId.
|
|
4
|
+
* Wraps a raw resolve function with an LRU cache keyed by databaseId:apiId.
|
|
5
5
|
* Each loader gets its own independent cache with configurable TTL and
|
|
6
6
|
* max entries.
|
|
7
7
|
*/
|
|
@@ -20,7 +20,7 @@ export function createModuleLoader(opts) {
|
|
|
20
20
|
return {
|
|
21
21
|
name: opts.name,
|
|
22
22
|
async resolve(ctx) {
|
|
23
|
-
const key = ctx.databaseId;
|
|
23
|
+
const key = ctx.apiId ? `${ctx.databaseId}:${ctx.apiId}` : ctx.databaseId;
|
|
24
24
|
if (cache.has(key)) {
|
|
25
25
|
log.debug(`Cache HIT databaseId=${key}`);
|
|
26
26
|
return cache.get(key);
|
|
@@ -38,8 +38,15 @@ export function createModuleLoader(opts) {
|
|
|
38
38
|
},
|
|
39
39
|
invalidate(databaseId) {
|
|
40
40
|
if (databaseId) {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Clear the plain databaseId key and any composite databaseId:apiId keys
|
|
42
|
+
let cleared = 0;
|
|
43
|
+
for (const k of cache.keys()) {
|
|
44
|
+
if (k === databaseId || k.startsWith(`${databaseId}:`)) {
|
|
45
|
+
cache.delete(k);
|
|
46
|
+
cleared++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
log.debug(`Invalidated ${cleared} entries for databaseId=${databaseId}`);
|
|
43
50
|
}
|
|
44
51
|
else {
|
|
45
52
|
cache.clear();
|
package/esm/loaders/index.js
CHANGED
|
@@ -34,6 +34,9 @@ export { databaseSettingsLoader } from './database-settings';
|
|
|
34
34
|
export { pubkeyLoader } from './pubkey';
|
|
35
35
|
export { webauthnLoader } from './webauthn';
|
|
36
36
|
export { authSettingsLoader } from './auth-settings';
|
|
37
|
+
export { billingLoader } from './billing';
|
|
38
|
+
export { inferenceLogLoader } from './inference-log';
|
|
39
|
+
export { agentChatLoader } from './agent-chat';
|
|
37
40
|
/**
|
|
38
41
|
* Convenience: create a registry pre-loaded with all built-in loaders.
|
|
39
42
|
*/
|
|
@@ -44,6 +47,9 @@ import { databaseSettingsLoader } from './database-settings';
|
|
|
44
47
|
import { pubkeyLoader } from './pubkey';
|
|
45
48
|
import { webauthnLoader } from './webauthn';
|
|
46
49
|
import { authSettingsLoader } from './auth-settings';
|
|
50
|
+
import { billingLoader } from './billing';
|
|
51
|
+
import { inferenceLogLoader } from './inference-log';
|
|
52
|
+
import { agentChatLoader } from './agent-chat';
|
|
47
53
|
export function createDefaultRegistry() {
|
|
48
54
|
const registry = createLoaderRegistry();
|
|
49
55
|
registry.register(rlsLoader);
|
|
@@ -52,5 +58,8 @@ export function createDefaultRegistry() {
|
|
|
52
58
|
registry.register(pubkeyLoader);
|
|
53
59
|
registry.register(webauthnLoader);
|
|
54
60
|
registry.register(authSettingsLoader);
|
|
61
|
+
registry.register(billingLoader);
|
|
62
|
+
registry.register(inferenceLogLoader);
|
|
63
|
+
registry.register(agentChatLoader);
|
|
55
64
|
return registry;
|
|
56
65
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inference Log Module Loader
|
|
3
|
+
*
|
|
4
|
+
* Resolves per-database inference log config from metaschema_modules_public.inference_log_module.
|
|
5
|
+
* Returns the schema and table name for logging LLM inference usage.
|
|
6
|
+
*/
|
|
7
|
+
import { createModuleLoader } from './create-loader';
|
|
8
|
+
// ─── SQL ────────────────────────────────────────────────────────────────────
|
|
9
|
+
const INFERENCE_LOG_MODULE_SQL = `
|
|
10
|
+
SELECT
|
|
11
|
+
s.schema_name AS schema,
|
|
12
|
+
ilm.inference_log_table_name AS table_name
|
|
13
|
+
FROM metaschema_modules_public.inference_log_module ilm
|
|
14
|
+
JOIN metaschema_public.schema s ON ilm.schema_id = s.id
|
|
15
|
+
WHERE ilm.database_id = $1
|
|
16
|
+
LIMIT 1
|
|
17
|
+
`;
|
|
18
|
+
// ─── Loader ─────────────────────────────────────────────────────────────────
|
|
19
|
+
export const inferenceLogLoader = createModuleLoader({
|
|
20
|
+
name: 'inferenceLog',
|
|
21
|
+
ttlMs: 5 * 60_000,
|
|
22
|
+
async resolve(ctx) {
|
|
23
|
+
const { tenantPool, databaseId } = ctx;
|
|
24
|
+
const result = await tenantPool.query(INFERENCE_LOG_MODULE_SQL, [databaseId]);
|
|
25
|
+
const row = result.rows[0];
|
|
26
|
+
if (!row?.schema || !row?.table_name)
|
|
27
|
+
return undefined;
|
|
28
|
+
return {
|
|
29
|
+
schema: row.schema,
|
|
30
|
+
tableName: row.table_name,
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
});
|
package/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* });
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
export type { ApiConfigResult, ApiError, ApiModule, ApiStructure, AuthSettings, ConstructiveAPIToken, ConstructiveContext, CorsModuleData, DatabaseSettings, GenericModuleData, PublicKeyChallengeData, PubkeyChallengeSettings,
|
|
37
|
+
export type { AgentChatConfig, ApiConfigResult, ApiError, ApiModule, ApiStructure, AuthSettings, BillingConfig, BuiltinModuleMap, ConstructiveAPIToken, ConstructiveContext, CorsModuleData, DatabaseSettings, GenericModuleData, InferenceLogConfig, PublicKeyChallengeData, PubkeyChallengeSettings, RlsModule, WebauthnSettings, WithPgClient, } from './types';
|
|
38
38
|
export type { PgSettingsInput } from './pg-settings';
|
|
39
39
|
export { buildPgSettings } from './pg-settings';
|
|
40
40
|
export { withPgClient } from './pg-client';
|
|
@@ -42,5 +42,5 @@ export { requestIdMiddleware } from './request-id';
|
|
|
42
42
|
export type { ContextMiddlewareOptions } from './context';
|
|
43
43
|
export { buildContext, createContextMiddleware } from './context';
|
|
44
44
|
export type { CreateLoaderOptions, LoaderContext, LoaderRegistry, ModuleLoader, } from './loaders';
|
|
45
|
-
export { authSettingsLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, pubkeyLoader, rlsLoader, webauthnLoader, } from './loaders';
|
|
45
|
+
export { agentChatLoader, authSettingsLoader, billingLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, inferenceLogLoader, pubkeyLoader, rlsLoader, webauthnLoader, } from './loaders';
|
|
46
46
|
import './types';
|
package/index.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.webauthnLoader = exports.rlsLoader = exports.pubkeyLoader = exports.databaseSettingsLoader = exports.createModuleLoader = exports.createLoaderRegistry = exports.createDefaultRegistry = exports.corsLoader = exports.authSettingsLoader = exports.createContextMiddleware = exports.buildContext = exports.requestIdMiddleware = exports.withPgClient = exports.buildPgSettings = void 0;
|
|
39
|
+
exports.webauthnLoader = exports.rlsLoader = exports.pubkeyLoader = exports.inferenceLogLoader = exports.databaseSettingsLoader = exports.createModuleLoader = exports.createLoaderRegistry = exports.createDefaultRegistry = exports.corsLoader = exports.billingLoader = exports.authSettingsLoader = exports.agentChatLoader = exports.createContextMiddleware = exports.buildContext = exports.requestIdMiddleware = exports.withPgClient = exports.buildPgSettings = void 0;
|
|
40
40
|
var pg_settings_1 = require("./pg-settings");
|
|
41
41
|
Object.defineProperty(exports, "buildPgSettings", { enumerable: true, get: function () { return pg_settings_1.buildPgSettings; } });
|
|
42
42
|
// withPgClient helper
|
|
@@ -49,12 +49,15 @@ var context_1 = require("./context");
|
|
|
49
49
|
Object.defineProperty(exports, "buildContext", { enumerable: true, get: function () { return context_1.buildContext; } });
|
|
50
50
|
Object.defineProperty(exports, "createContextMiddleware", { enumerable: true, get: function () { return context_1.createContextMiddleware; } });
|
|
51
51
|
var loaders_1 = require("./loaders");
|
|
52
|
+
Object.defineProperty(exports, "agentChatLoader", { enumerable: true, get: function () { return loaders_1.agentChatLoader; } });
|
|
52
53
|
Object.defineProperty(exports, "authSettingsLoader", { enumerable: true, get: function () { return loaders_1.authSettingsLoader; } });
|
|
54
|
+
Object.defineProperty(exports, "billingLoader", { enumerable: true, get: function () { return loaders_1.billingLoader; } });
|
|
53
55
|
Object.defineProperty(exports, "corsLoader", { enumerable: true, get: function () { return loaders_1.corsLoader; } });
|
|
54
56
|
Object.defineProperty(exports, "createDefaultRegistry", { enumerable: true, get: function () { return loaders_1.createDefaultRegistry; } });
|
|
55
57
|
Object.defineProperty(exports, "createLoaderRegistry", { enumerable: true, get: function () { return loaders_1.createLoaderRegistry; } });
|
|
56
58
|
Object.defineProperty(exports, "createModuleLoader", { enumerable: true, get: function () { return loaders_1.createModuleLoader; } });
|
|
57
59
|
Object.defineProperty(exports, "databaseSettingsLoader", { enumerable: true, get: function () { return loaders_1.databaseSettingsLoader; } });
|
|
60
|
+
Object.defineProperty(exports, "inferenceLogLoader", { enumerable: true, get: function () { return loaders_1.inferenceLogLoader; } });
|
|
58
61
|
Object.defineProperty(exports, "pubkeyLoader", { enumerable: true, get: function () { return loaders_1.pubkeyLoader; } });
|
|
59
62
|
Object.defineProperty(exports, "rlsLoader", { enumerable: true, get: function () { return loaders_1.rlsLoader; } });
|
|
60
63
|
Object.defineProperty(exports, "webauthnLoader", { enumerable: true, get: function () { return loaders_1.webauthnLoader; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Chat Module Loader
|
|
3
|
+
*
|
|
4
|
+
* Resolves per-database agent chat config from metaschema_modules_public.agent_chat_module.
|
|
5
|
+
* Returns the schema and table names for threads, messages, and tasks.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentChatConfig } from '../types';
|
|
8
|
+
import type { ModuleLoader } from './types';
|
|
9
|
+
export declare const agentChatLoader: ModuleLoader<AgentChatConfig>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent Chat Module Loader
|
|
4
|
+
*
|
|
5
|
+
* Resolves per-database agent chat config from metaschema_modules_public.agent_chat_module.
|
|
6
|
+
* Returns the schema and table names for threads, messages, and tasks.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.agentChatLoader = void 0;
|
|
10
|
+
const create_loader_1 = require("./create-loader");
|
|
11
|
+
// ─── SQL ────────────────────────────────────────────────────────────────────
|
|
12
|
+
const AGENT_CHAT_MODULE_SQL = `
|
|
13
|
+
SELECT
|
|
14
|
+
s.schema_name,
|
|
15
|
+
acm.thread_table_name,
|
|
16
|
+
acm.message_table_name,
|
|
17
|
+
acm.task_table_name
|
|
18
|
+
FROM metaschema_modules_public.agent_chat_module acm
|
|
19
|
+
JOIN metaschema_public.schema s ON s.id = acm.schema_id
|
|
20
|
+
LIMIT 1
|
|
21
|
+
`;
|
|
22
|
+
// ─── Loader ─────────────────────────────────────────────────────────────────
|
|
23
|
+
exports.agentChatLoader = (0, create_loader_1.createModuleLoader)({
|
|
24
|
+
name: 'agentChat',
|
|
25
|
+
ttlMs: 60_000,
|
|
26
|
+
async resolve(ctx) {
|
|
27
|
+
const { tenantPool } = ctx;
|
|
28
|
+
const result = await tenantPool.query(AGENT_CHAT_MODULE_SQL);
|
|
29
|
+
const row = result.rows[0];
|
|
30
|
+
if (!row)
|
|
31
|
+
return undefined;
|
|
32
|
+
return {
|
|
33
|
+
schemaName: row.schema_name,
|
|
34
|
+
threadTableName: row.thread_table_name,
|
|
35
|
+
messageTableName: row.message_table_name,
|
|
36
|
+
taskTableName: row.task_table_name,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Module Loader
|
|
3
|
+
*
|
|
4
|
+
* Resolves per-database billing config from metaschema_modules_public.billing_module.
|
|
5
|
+
* Returns schema names and function names needed for quota checks and usage recording.
|
|
6
|
+
*/
|
|
7
|
+
import type { BillingConfig } from '../types';
|
|
8
|
+
import type { ModuleLoader } from './types';
|
|
9
|
+
export declare const billingLoader: ModuleLoader<BillingConfig>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Billing Module Loader
|
|
4
|
+
*
|
|
5
|
+
* Resolves per-database billing config from metaschema_modules_public.billing_module.
|
|
6
|
+
* Returns schema names and function names needed for quota checks and usage recording.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.billingLoader = void 0;
|
|
10
|
+
const create_loader_1 = require("./create-loader");
|
|
11
|
+
// ─── SQL ────────────────────────────────────────────────────────────────────
|
|
12
|
+
const BILLING_MODULE_SQL = `
|
|
13
|
+
SELECT
|
|
14
|
+
s.schema_name AS public_schema,
|
|
15
|
+
ps.schema_name AS private_schema,
|
|
16
|
+
bm.record_usage_function
|
|
17
|
+
FROM metaschema_modules_public.billing_module bm
|
|
18
|
+
JOIN metaschema_public.schema s ON bm.schema_id = s.id
|
|
19
|
+
JOIN metaschema_public.schema ps ON bm.private_schema_id = ps.id
|
|
20
|
+
WHERE bm.database_id = $1
|
|
21
|
+
LIMIT 1
|
|
22
|
+
`;
|
|
23
|
+
// ─── Loader ─────────────────────────────────────────────────────────────────
|
|
24
|
+
exports.billingLoader = (0, create_loader_1.createModuleLoader)({
|
|
25
|
+
name: 'billing',
|
|
26
|
+
ttlMs: 5 * 60_000,
|
|
27
|
+
async resolve(ctx) {
|
|
28
|
+
const { tenantPool, databaseId } = ctx;
|
|
29
|
+
const result = await tenantPool.query(BILLING_MODULE_SQL, [databaseId]);
|
|
30
|
+
const row = result.rows[0];
|
|
31
|
+
if (!row?.record_usage_function)
|
|
32
|
+
return undefined;
|
|
33
|
+
return {
|
|
34
|
+
publicSchema: row.public_schema,
|
|
35
|
+
privateSchema: row.private_schema,
|
|
36
|
+
recordUsageFunction: row.record_usage_function,
|
|
37
|
+
checkBillingQuotaFunction: 'check_billing_quota',
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* create-loader — Factory for building cached ModuleLoader instances.
|
|
3
3
|
*
|
|
4
|
-
* Wraps a raw resolve function with an LRU cache keyed by databaseId.
|
|
4
|
+
* Wraps a raw resolve function with an LRU cache keyed by databaseId:apiId.
|
|
5
5
|
* Each loader gets its own independent cache with configurable TTL and
|
|
6
6
|
* max entries.
|
|
7
7
|
*/
|
package/loaders/create-loader.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* create-loader — Factory for building cached ModuleLoader instances.
|
|
4
4
|
*
|
|
5
|
-
* Wraps a raw resolve function with an LRU cache keyed by databaseId.
|
|
5
|
+
* Wraps a raw resolve function with an LRU cache keyed by databaseId:apiId.
|
|
6
6
|
* Each loader gets its own independent cache with configurable TTL and
|
|
7
7
|
* max entries.
|
|
8
8
|
*/
|
|
@@ -23,7 +23,7 @@ function createModuleLoader(opts) {
|
|
|
23
23
|
return {
|
|
24
24
|
name: opts.name,
|
|
25
25
|
async resolve(ctx) {
|
|
26
|
-
const key = ctx.databaseId;
|
|
26
|
+
const key = ctx.apiId ? `${ctx.databaseId}:${ctx.apiId}` : ctx.databaseId;
|
|
27
27
|
if (cache.has(key)) {
|
|
28
28
|
log.debug(`Cache HIT databaseId=${key}`);
|
|
29
29
|
return cache.get(key);
|
|
@@ -41,8 +41,15 @@ function createModuleLoader(opts) {
|
|
|
41
41
|
},
|
|
42
42
|
invalidate(databaseId) {
|
|
43
43
|
if (databaseId) {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// Clear the plain databaseId key and any composite databaseId:apiId keys
|
|
45
|
+
let cleared = 0;
|
|
46
|
+
for (const k of cache.keys()) {
|
|
47
|
+
if (k === databaseId || k.startsWith(`${databaseId}:`)) {
|
|
48
|
+
cache.delete(k);
|
|
49
|
+
cleared++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
log.debug(`Invalidated ${cleared} entries for databaseId=${databaseId}`);
|
|
46
53
|
}
|
|
47
54
|
else {
|
|
48
55
|
cache.clear();
|
package/loaders/index.d.ts
CHANGED
|
@@ -36,4 +36,7 @@ export { databaseSettingsLoader } from './database-settings';
|
|
|
36
36
|
export { pubkeyLoader } from './pubkey';
|
|
37
37
|
export { webauthnLoader } from './webauthn';
|
|
38
38
|
export { authSettingsLoader } from './auth-settings';
|
|
39
|
+
export { billingLoader } from './billing';
|
|
40
|
+
export { inferenceLogLoader } from './inference-log';
|
|
41
|
+
export { agentChatLoader } from './agent-chat';
|
|
39
42
|
export declare function createDefaultRegistry(): import("./registry").LoaderRegistry;
|
package/loaders/index.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* registry.register(myLoader);
|
|
28
28
|
*/
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
-
exports.authSettingsLoader = exports.webauthnLoader = exports.pubkeyLoader = exports.databaseSettingsLoader = exports.corsLoader = exports.rlsLoader = exports.createLoaderRegistry = exports.createModuleLoader = void 0;
|
|
30
|
+
exports.agentChatLoader = exports.inferenceLogLoader = exports.billingLoader = exports.authSettingsLoader = exports.webauthnLoader = exports.pubkeyLoader = exports.databaseSettingsLoader = exports.corsLoader = exports.rlsLoader = exports.createLoaderRegistry = exports.createModuleLoader = void 0;
|
|
31
31
|
exports.createDefaultRegistry = createDefaultRegistry;
|
|
32
32
|
var create_loader_1 = require("./create-loader");
|
|
33
33
|
Object.defineProperty(exports, "createModuleLoader", { enumerable: true, get: function () { return create_loader_1.createModuleLoader; } });
|
|
@@ -46,6 +46,12 @@ var webauthn_1 = require("./webauthn");
|
|
|
46
46
|
Object.defineProperty(exports, "webauthnLoader", { enumerable: true, get: function () { return webauthn_1.webauthnLoader; } });
|
|
47
47
|
var auth_settings_1 = require("./auth-settings");
|
|
48
48
|
Object.defineProperty(exports, "authSettingsLoader", { enumerable: true, get: function () { return auth_settings_1.authSettingsLoader; } });
|
|
49
|
+
var billing_1 = require("./billing");
|
|
50
|
+
Object.defineProperty(exports, "billingLoader", { enumerable: true, get: function () { return billing_1.billingLoader; } });
|
|
51
|
+
var inference_log_1 = require("./inference-log");
|
|
52
|
+
Object.defineProperty(exports, "inferenceLogLoader", { enumerable: true, get: function () { return inference_log_1.inferenceLogLoader; } });
|
|
53
|
+
var agent_chat_1 = require("./agent-chat");
|
|
54
|
+
Object.defineProperty(exports, "agentChatLoader", { enumerable: true, get: function () { return agent_chat_1.agentChatLoader; } });
|
|
49
55
|
/**
|
|
50
56
|
* Convenience: create a registry pre-loaded with all built-in loaders.
|
|
51
57
|
*/
|
|
@@ -56,6 +62,9 @@ const database_settings_2 = require("./database-settings");
|
|
|
56
62
|
const pubkey_2 = require("./pubkey");
|
|
57
63
|
const webauthn_2 = require("./webauthn");
|
|
58
64
|
const auth_settings_2 = require("./auth-settings");
|
|
65
|
+
const billing_2 = require("./billing");
|
|
66
|
+
const inference_log_2 = require("./inference-log");
|
|
67
|
+
const agent_chat_2 = require("./agent-chat");
|
|
59
68
|
function createDefaultRegistry() {
|
|
60
69
|
const registry = (0, registry_2.createLoaderRegistry)();
|
|
61
70
|
registry.register(rls_2.rlsLoader);
|
|
@@ -64,5 +73,8 @@ function createDefaultRegistry() {
|
|
|
64
73
|
registry.register(pubkey_2.pubkeyLoader);
|
|
65
74
|
registry.register(webauthn_2.webauthnLoader);
|
|
66
75
|
registry.register(auth_settings_2.authSettingsLoader);
|
|
76
|
+
registry.register(billing_2.billingLoader);
|
|
77
|
+
registry.register(inference_log_2.inferenceLogLoader);
|
|
78
|
+
registry.register(agent_chat_2.agentChatLoader);
|
|
67
79
|
return registry;
|
|
68
80
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inference Log Module Loader
|
|
3
|
+
*
|
|
4
|
+
* Resolves per-database inference log config from metaschema_modules_public.inference_log_module.
|
|
5
|
+
* Returns the schema and table name for logging LLM inference usage.
|
|
6
|
+
*/
|
|
7
|
+
import type { InferenceLogConfig } from '../types';
|
|
8
|
+
import type { ModuleLoader } from './types';
|
|
9
|
+
export declare const inferenceLogLoader: ModuleLoader<InferenceLogConfig>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Inference Log Module Loader
|
|
4
|
+
*
|
|
5
|
+
* Resolves per-database inference log config from metaschema_modules_public.inference_log_module.
|
|
6
|
+
* Returns the schema and table name for logging LLM inference usage.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.inferenceLogLoader = void 0;
|
|
10
|
+
const create_loader_1 = require("./create-loader");
|
|
11
|
+
// ─── SQL ────────────────────────────────────────────────────────────────────
|
|
12
|
+
const INFERENCE_LOG_MODULE_SQL = `
|
|
13
|
+
SELECT
|
|
14
|
+
s.schema_name AS schema,
|
|
15
|
+
ilm.inference_log_table_name AS table_name
|
|
16
|
+
FROM metaschema_modules_public.inference_log_module ilm
|
|
17
|
+
JOIN metaschema_public.schema s ON ilm.schema_id = s.id
|
|
18
|
+
WHERE ilm.database_id = $1
|
|
19
|
+
LIMIT 1
|
|
20
|
+
`;
|
|
21
|
+
// ─── Loader ─────────────────────────────────────────────────────────────────
|
|
22
|
+
exports.inferenceLogLoader = (0, create_loader_1.createModuleLoader)({
|
|
23
|
+
name: 'inferenceLog',
|
|
24
|
+
ttlMs: 5 * 60_000,
|
|
25
|
+
async resolve(ctx) {
|
|
26
|
+
const { tenantPool, databaseId } = ctx;
|
|
27
|
+
const result = await tenantPool.query(INFERENCE_LOG_MODULE_SQL, [databaseId]);
|
|
28
|
+
const row = result.rows[0];
|
|
29
|
+
if (!row?.schema || !row?.table_name)
|
|
30
|
+
return undefined;
|
|
31
|
+
return {
|
|
32
|
+
schema: row.schema,
|
|
33
|
+
tableName: row.table_name,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/express-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"jwt",
|
|
60
60
|
"pgSettings"
|
|
61
61
|
],
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "0d63238f2186b997a69a3287fa6cd35ebd93dda6"
|
|
63
63
|
}
|
package/types.d.ts
CHANGED
|
@@ -119,6 +119,22 @@ export type ConstructiveAPIToken = {
|
|
|
119
119
|
kind?: string;
|
|
120
120
|
[key: string]: unknown;
|
|
121
121
|
};
|
|
122
|
+
export interface BillingConfig {
|
|
123
|
+
publicSchema: string;
|
|
124
|
+
privateSchema: string;
|
|
125
|
+
recordUsageFunction: string;
|
|
126
|
+
checkBillingQuotaFunction: string;
|
|
127
|
+
}
|
|
128
|
+
export interface InferenceLogConfig {
|
|
129
|
+
schema: string;
|
|
130
|
+
tableName: string;
|
|
131
|
+
}
|
|
132
|
+
export interface AgentChatConfig {
|
|
133
|
+
schemaName: string;
|
|
134
|
+
threadTableName: string | null;
|
|
135
|
+
messageTableName: string | null;
|
|
136
|
+
taskTableName: string | null;
|
|
137
|
+
}
|
|
122
138
|
/**
|
|
123
139
|
* Maps built-in loader names to their resolved types.
|
|
124
140
|
*
|
|
@@ -134,6 +150,9 @@ export interface BuiltinModuleMap {
|
|
|
134
150
|
authSettings: AuthSettings;
|
|
135
151
|
pubkeyChallengeSettings: PubkeyChallengeSettings;
|
|
136
152
|
webauthnSettings: WebauthnSettings;
|
|
153
|
+
billing: BillingConfig;
|
|
154
|
+
inferenceLog: InferenceLogConfig;
|
|
155
|
+
agentChat: AgentChatConfig;
|
|
137
156
|
}
|
|
138
157
|
/**
|
|
139
158
|
* Callback for executing queries within a tenant-scoped transaction
|