@constructive-io/express-context 0.9.0 → 0.10.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/esm/index.js CHANGED
@@ -41,6 +41,6 @@ export { withPgClient } from './pg-client';
41
41
  // Request ID middleware
42
42
  export { requestIdMiddleware } from './request-id';
43
43
  export { buildContext, createContextMiddleware } from './context';
44
- export { agentChatLoader, authSettingsLoader, billingLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, inferenceLogLoader, pubkeyLoader, rlsLoader, llmLoader, webauthnLoader, } from './loaders';
44
+ export { agentChatLoader, authSettingsLoader, billingLoader, computeLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, inferenceLogLoader, pubkeyLoader, rlsLoader, llmLoader, webauthnLoader, } from './loaders';
45
45
  // Side-effect: Express type augmentation
46
46
  import './types';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Compute Module Loader
3
+ *
4
+ * Resolves per-database compute (functions) config from
5
+ * metaschema_modules_public.function_module and function_invocation_module.
6
+ * Returns the schema and table names for function definitions, API bindings,
7
+ * and invocations so REST routes can address the generated tables.
8
+ *
9
+ * Function modules are scoped: a database may provision one per scope, each
10
+ * with its own schema and tables. All of them are returned — REST routes and
11
+ * the GraphQL bindings plugin expose bindings from every module, and RLS on
12
+ * the underlying tables governs access.
13
+ */
14
+ import { createModuleLoader } from './create-loader';
15
+ // ─── SQL ────────────────────────────────────────────────────────────────────
16
+ const COMPUTE_MODULE_SQL = `
17
+ SELECT
18
+ fs.schema_name AS functions_schema_name,
19
+ fm.definitions_table_name,
20
+ fm.bindings_table_name,
21
+ ivs.schema_name AS invocations_schema_name,
22
+ ivm.invocations_table_name,
23
+ ivm.entity_field AS invocations_entity_field
24
+ FROM metaschema_modules_public.function_module fm
25
+ JOIN metaschema_public.schema fs ON fs.id = fm.schema_id
26
+ JOIN metaschema_modules_public.function_invocation_module ivm
27
+ ON ivm.database_id = fm.database_id AND ivm.scope = fm.scope
28
+ JOIN metaschema_public.schema ivs ON ivs.id = ivm.schema_id
29
+ WHERE fm.database_id = $1
30
+ ORDER BY fs.schema_name
31
+ `;
32
+ // ─── Loader ─────────────────────────────────────────────────────────────────
33
+ export const computeLoader = createModuleLoader({
34
+ name: 'compute',
35
+ ttlMs: 60_000,
36
+ async resolve(ctx) {
37
+ const { tenantPool, databaseId } = ctx;
38
+ const result = await tenantPool.query(COMPUTE_MODULE_SQL, [databaseId]);
39
+ if (result.rows.length === 0)
40
+ return undefined;
41
+ return {
42
+ modules: result.rows.map((row) => ({
43
+ schemaName: row.functions_schema_name,
44
+ definitionsTableName: row.definitions_table_name,
45
+ // Physical bindings table name, recorded by the metaschema generator
46
+ // on the function_module config row — read as a fact, never derived
47
+ // from the definitions table name.
48
+ bindingsTableName: row.bindings_table_name,
49
+ invocationsSchemaName: row.invocations_schema_name,
50
+ invocationsTableName: row.invocations_table_name,
51
+ // Scope-key column of the invocations table (entity_field), or NULL
52
+ // for global scopes. Consumers set this column on inserts.
53
+ invocationsEntityField: row.invocations_entity_field,
54
+ })),
55
+ };
56
+ },
57
+ });
@@ -26,14 +26,23 @@ export function createModuleLoader(opts) {
26
26
  return cache.get(key);
27
27
  }
28
28
  log.debug(`Cache MISS databaseId=${key}, resolving`);
29
+ // "Not provisioned" is expressed by the loader returning undefined, or
30
+ // by the module's tables not existing at all (42P01 undefined_table).
31
+ // Any other resolution error (bad query, ambiguous config) propagates —
32
+ // never silently coerced into "module absent".
29
33
  try {
30
34
  const value = await opts.resolve(ctx);
31
35
  cache.set(key, value);
32
36
  return value;
33
37
  }
34
38
  catch (e) {
39
+ if (e.code === '42P01') {
40
+ log.debug(`Module tables absent for databaseId=${key}: ${e.message}`);
41
+ cache.set(key, undefined);
42
+ return undefined;
43
+ }
35
44
  log.warn(`Failed to resolve databaseId=${key}: ${e.message}`);
36
- return undefined;
45
+ throw e;
37
46
  }
38
47
  },
39
48
  invalidate(databaseId) {
@@ -38,6 +38,7 @@ export { billingLoader } from './billing';
38
38
  export { inferenceLogLoader } from './inference-log';
39
39
  export { agentChatLoader } from './agent-chat';
40
40
  export { llmLoader } from './llm';
41
+ export { computeLoader } from './compute';
41
42
  /**
42
43
  * Convenience: create a registry pre-loaded with all built-in loaders.
43
44
  */
@@ -52,6 +53,7 @@ import { billingLoader } from './billing';
52
53
  import { inferenceLogLoader } from './inference-log';
53
54
  import { agentChatLoader } from './agent-chat';
54
55
  import { llmLoader } from './llm';
56
+ import { computeLoader } from './compute';
55
57
  export function createDefaultRegistry() {
56
58
  const registry = createLoaderRegistry();
57
59
  registry.register(rlsLoader);
@@ -64,5 +66,6 @@ export function createDefaultRegistry() {
64
66
  registry.register(inferenceLogLoader);
65
67
  registry.register(agentChatLoader);
66
68
  registry.register(llmLoader);
69
+ registry.register(computeLoader);
67
70
  return registry;
68
71
  }
@@ -38,6 +38,12 @@ export function buildPgSettings(input) {
38
38
  if (api.databaseId) {
39
39
  settings['jwt.claims.database_id'] = api.databaseId;
40
40
  }
41
+ // API provenance — which API surface this request arrived through.
42
+ // Derived server-side from hostname -> services_public.domains -> api_id;
43
+ // never taken from client-supplied headers, body, or token payload.
44
+ if (api.apiId) {
45
+ settings['jwt.claims.api_id'] = api.apiId;
46
+ }
41
47
  // Distributed tracing
42
48
  settings['request.id'] = requestId;
43
49
  // Client metadata (for audit functions)
package/index.d.ts CHANGED
@@ -34,7 +34,7 @@
34
34
  * });
35
35
  * ```
36
36
  */
37
- export type { AgentChatConfig, ApiConfigResult, ApiError, ApiModule, ApiStructure, AuthSettings, BillingConfig, BuiltinModuleMap, ConstructiveAPIToken, ConstructiveContext, CorsModuleData, DatabaseSettings, GenericModuleData, InferenceLogConfig, LlmConfig, PublicKeyChallengeData, PubkeyChallengeSettings, RlsModule, WebauthnSettings, WithPgClient, } from './types';
37
+ export type { AgentChatConfig, ApiConfigResult, ApiError, ApiModule, ApiStructure, AuthSettings, BillingConfig, BuiltinModuleMap, ComputeConfig, ComputeModuleConfig, ConstructiveAPIToken, ConstructiveContext, CorsModuleData, DatabaseSettings, GenericModuleData, InferenceLogConfig, LlmConfig, PublicKeyChallengeData, 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';
@@ -44,5 +44,5 @@ export { requestIdMiddleware } from './request-id';
44
44
  export type { ContextMiddlewareOptions } from './context';
45
45
  export { buildContext, createContextMiddleware } from './context';
46
46
  export type { CreateLoaderOptions, LoaderContext, LoaderRegistry, ModuleLoader, } from './loaders';
47
- export { agentChatLoader, authSettingsLoader, billingLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, inferenceLogLoader, pubkeyLoader, rlsLoader, llmLoader, webauthnLoader, } from './loaders';
47
+ export { agentChatLoader, authSettingsLoader, billingLoader, computeLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, inferenceLogLoader, pubkeyLoader, rlsLoader, llmLoader, webauthnLoader, } from './loaders';
48
48
  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.llmLoader = 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 = exports.createBillingClient = void 0;
39
+ exports.webauthnLoader = exports.llmLoader = exports.rlsLoader = exports.pubkeyLoader = exports.inferenceLogLoader = exports.databaseSettingsLoader = exports.createModuleLoader = exports.createLoaderRegistry = exports.createDefaultRegistry = exports.corsLoader = exports.computeLoader = exports.billingLoader = exports.authSettingsLoader = exports.agentChatLoader = exports.createContextMiddleware = exports.buildContext = exports.requestIdMiddleware = exports.withPgClient = exports.buildPgSettings = exports.createBillingClient = void 0;
40
40
  var billing_client_1 = require("./billing-client");
41
41
  Object.defineProperty(exports, "createBillingClient", { enumerable: true, get: function () { return billing_client_1.createBillingClient; } });
42
42
  var pg_settings_1 = require("./pg-settings");
@@ -54,6 +54,7 @@ var loaders_1 = require("./loaders");
54
54
  Object.defineProperty(exports, "agentChatLoader", { enumerable: true, get: function () { return loaders_1.agentChatLoader; } });
55
55
  Object.defineProperty(exports, "authSettingsLoader", { enumerable: true, get: function () { return loaders_1.authSettingsLoader; } });
56
56
  Object.defineProperty(exports, "billingLoader", { enumerable: true, get: function () { return loaders_1.billingLoader; } });
57
+ Object.defineProperty(exports, "computeLoader", { enumerable: true, get: function () { return loaders_1.computeLoader; } });
57
58
  Object.defineProperty(exports, "corsLoader", { enumerable: true, get: function () { return loaders_1.corsLoader; } });
58
59
  Object.defineProperty(exports, "createDefaultRegistry", { enumerable: true, get: function () { return loaders_1.createDefaultRegistry; } });
59
60
  Object.defineProperty(exports, "createLoaderRegistry", { enumerable: true, get: function () { return loaders_1.createLoaderRegistry; } });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Compute Module Loader
3
+ *
4
+ * Resolves per-database compute (functions) config from
5
+ * metaschema_modules_public.function_module and function_invocation_module.
6
+ * Returns the schema and table names for function definitions, API bindings,
7
+ * and invocations so REST routes can address the generated tables.
8
+ *
9
+ * Function modules are scoped: a database may provision one per scope, each
10
+ * with its own schema and tables. All of them are returned — REST routes and
11
+ * the GraphQL bindings plugin expose bindings from every module, and RLS on
12
+ * the underlying tables governs access.
13
+ */
14
+ import type { ComputeConfig } from '../types';
15
+ import type { ModuleLoader } from './types';
16
+ export declare const computeLoader: ModuleLoader<ComputeConfig>;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Compute Module Loader
4
+ *
5
+ * Resolves per-database compute (functions) config from
6
+ * metaschema_modules_public.function_module and function_invocation_module.
7
+ * Returns the schema and table names for function definitions, API bindings,
8
+ * and invocations so REST routes can address the generated tables.
9
+ *
10
+ * Function modules are scoped: a database may provision one per scope, each
11
+ * with its own schema and tables. All of them are returned — REST routes and
12
+ * the GraphQL bindings plugin expose bindings from every module, and RLS on
13
+ * the underlying tables governs access.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.computeLoader = void 0;
17
+ const create_loader_1 = require("./create-loader");
18
+ // ─── SQL ────────────────────────────────────────────────────────────────────
19
+ const COMPUTE_MODULE_SQL = `
20
+ SELECT
21
+ fs.schema_name AS functions_schema_name,
22
+ fm.definitions_table_name,
23
+ fm.bindings_table_name,
24
+ ivs.schema_name AS invocations_schema_name,
25
+ ivm.invocations_table_name,
26
+ ivm.entity_field AS invocations_entity_field
27
+ FROM metaschema_modules_public.function_module fm
28
+ JOIN metaschema_public.schema fs ON fs.id = fm.schema_id
29
+ JOIN metaschema_modules_public.function_invocation_module ivm
30
+ ON ivm.database_id = fm.database_id AND ivm.scope = fm.scope
31
+ JOIN metaschema_public.schema ivs ON ivs.id = ivm.schema_id
32
+ WHERE fm.database_id = $1
33
+ ORDER BY fs.schema_name
34
+ `;
35
+ // ─── Loader ─────────────────────────────────────────────────────────────────
36
+ exports.computeLoader = (0, create_loader_1.createModuleLoader)({
37
+ name: 'compute',
38
+ ttlMs: 60_000,
39
+ async resolve(ctx) {
40
+ const { tenantPool, databaseId } = ctx;
41
+ const result = await tenantPool.query(COMPUTE_MODULE_SQL, [databaseId]);
42
+ if (result.rows.length === 0)
43
+ return undefined;
44
+ return {
45
+ modules: result.rows.map((row) => ({
46
+ schemaName: row.functions_schema_name,
47
+ definitionsTableName: row.definitions_table_name,
48
+ // Physical bindings table name, recorded by the metaschema generator
49
+ // on the function_module config row — read as a fact, never derived
50
+ // from the definitions table name.
51
+ bindingsTableName: row.bindings_table_name,
52
+ invocationsSchemaName: row.invocations_schema_name,
53
+ invocationsTableName: row.invocations_table_name,
54
+ // Scope-key column of the invocations table (entity_field), or NULL
55
+ // for global scopes. Consumers set this column on inserts.
56
+ invocationsEntityField: row.invocations_entity_field,
57
+ })),
58
+ };
59
+ },
60
+ });
@@ -29,14 +29,23 @@ function createModuleLoader(opts) {
29
29
  return cache.get(key);
30
30
  }
31
31
  log.debug(`Cache MISS databaseId=${key}, resolving`);
32
+ // "Not provisioned" is expressed by the loader returning undefined, or
33
+ // by the module's tables not existing at all (42P01 undefined_table).
34
+ // Any other resolution error (bad query, ambiguous config) propagates —
35
+ // never silently coerced into "module absent".
32
36
  try {
33
37
  const value = await opts.resolve(ctx);
34
38
  cache.set(key, value);
35
39
  return value;
36
40
  }
37
41
  catch (e) {
42
+ if (e.code === '42P01') {
43
+ log.debug(`Module tables absent for databaseId=${key}: ${e.message}`);
44
+ cache.set(key, undefined);
45
+ return undefined;
46
+ }
38
47
  log.warn(`Failed to resolve databaseId=${key}: ${e.message}`);
39
- return undefined;
48
+ throw e;
40
49
  }
41
50
  },
42
51
  invalidate(databaseId) {
@@ -40,4 +40,5 @@ export { billingLoader } from './billing';
40
40
  export { inferenceLogLoader } from './inference-log';
41
41
  export { agentChatLoader } from './agent-chat';
42
42
  export { llmLoader } from './llm';
43
+ export { computeLoader } from './compute';
43
44
  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.llmLoader = exports.agentChatLoader = exports.inferenceLogLoader = exports.billingLoader = exports.authSettingsLoader = exports.webauthnLoader = exports.pubkeyLoader = exports.databaseSettingsLoader = exports.corsLoader = exports.rlsLoader = exports.createLoaderRegistry = exports.createModuleLoader = void 0;
30
+ exports.computeLoader = exports.llmLoader = 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; } });
@@ -54,6 +54,8 @@ var agent_chat_1 = require("./agent-chat");
54
54
  Object.defineProperty(exports, "agentChatLoader", { enumerable: true, get: function () { return agent_chat_1.agentChatLoader; } });
55
55
  var llm_1 = require("./llm");
56
56
  Object.defineProperty(exports, "llmLoader", { enumerable: true, get: function () { return llm_1.llmLoader; } });
57
+ var compute_1 = require("./compute");
58
+ Object.defineProperty(exports, "computeLoader", { enumerable: true, get: function () { return compute_1.computeLoader; } });
57
59
  /**
58
60
  * Convenience: create a registry pre-loaded with all built-in loaders.
59
61
  */
@@ -68,6 +70,7 @@ const billing_2 = require("./billing");
68
70
  const inference_log_2 = require("./inference-log");
69
71
  const agent_chat_2 = require("./agent-chat");
70
72
  const llm_2 = require("./llm");
73
+ const compute_2 = require("./compute");
71
74
  function createDefaultRegistry() {
72
75
  const registry = (0, registry_2.createLoaderRegistry)();
73
76
  registry.register(rls_2.rlsLoader);
@@ -80,5 +83,6 @@ function createDefaultRegistry() {
80
83
  registry.register(inference_log_2.inferenceLogLoader);
81
84
  registry.register(agent_chat_2.agentChatLoader);
82
85
  registry.register(llm_2.llmLoader);
86
+ registry.register(compute_2.computeLoader);
83
87
  return registry;
84
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/express-context",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",
@@ -32,7 +32,7 @@
32
32
  "@constructive-io/url-domains": "^2.19.0",
33
33
  "@pgpmjs/env": "^2.27.0",
34
34
  "@pgpmjs/logger": "^2.14.0",
35
- "@pgpmjs/server-utils": "^3.15.0",
35
+ "@pgpmjs/server-utils": "^3.15.1",
36
36
  "@pgpmjs/types": "^2.34.0",
37
37
  "lru-cache": "^11.2.7",
38
38
  "pg": "^8.21.0",
@@ -59,5 +59,5 @@
59
59
  "jwt",
60
60
  "pgSettings"
61
61
  ],
62
- "gitHead": "7eb34a3baa10ba3476575967b87ff51b606e042a"
62
+ "gitHead": "cd21f62bb64bdc7f4f40f6cef6af73f8f1c8f8dc"
63
63
  }
package/pg-settings.js CHANGED
@@ -41,6 +41,12 @@ function buildPgSettings(input) {
41
41
  if (api.databaseId) {
42
42
  settings['jwt.claims.database_id'] = api.databaseId;
43
43
  }
44
+ // API provenance — which API surface this request arrived through.
45
+ // Derived server-side from hostname -> services_public.domains -> api_id;
46
+ // never taken from client-supplied headers, body, or token payload.
47
+ if (api.apiId) {
48
+ settings['jwt.claims.api_id'] = api.apiId;
49
+ }
44
50
  // Distributed tracing
45
51
  settings['request.id'] = requestId;
46
52
  // Client metadata (for audit functions)
package/types.d.ts CHANGED
@@ -138,6 +138,29 @@ export interface AgentChatConfig {
138
138
  messageTableName: string | null;
139
139
  taskTableName: string | null;
140
140
  }
141
+ /** One provisioned function-module scope's compute table names. */
142
+ export interface ComputeModuleConfig {
143
+ schemaName: string;
144
+ definitionsTableName: string;
145
+ bindingsTableName: string;
146
+ invocationsSchemaName: string;
147
+ invocationsTableName: string;
148
+ /**
149
+ * Scope-key column of the invocations table, read from the metaschema
150
+ * (`function_invocation_module.entity_field`): `database_id` for the
151
+ * database scope, `NULL` for global scopes (platform/app), or the entity
152
+ * key column for entity scopes. Consumers set this column on invocation
153
+ * inserts instead of switching on scope name.
154
+ */
155
+ invocationsEntityField: string | null;
156
+ }
157
+ /**
158
+ * All function modules provisioned on the database. A database may have one
159
+ * per scope; every module is exposed and RLS governs access to each.
160
+ */
161
+ export interface ComputeConfig {
162
+ modules: ComputeModuleConfig[];
163
+ }
141
164
  export interface LlmConfig {
142
165
  embeddingProvider: string;
143
166
  embeddingModel: string;
@@ -169,6 +192,7 @@ export interface BuiltinModuleMap {
169
192
  inferenceLog: InferenceLogConfig;
170
193
  agentChat: AgentChatConfig;
171
194
  llm: LlmConfig;
195
+ compute: ComputeConfig;
172
196
  }
173
197
  /**
174
198
  * Callback for executing queries within a tenant-scoped transaction