@constructive-io/express-context 0.2.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.
Files changed (51) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +95 -0
  3. package/context.d.ts +61 -0
  4. package/context.js +119 -0
  5. package/esm/context.js +115 -0
  6. package/esm/index.js +45 -0
  7. package/esm/loaders/auth-settings.js +62 -0
  8. package/esm/loaders/cors.js +55 -0
  9. package/esm/loaders/create-loader.js +53 -0
  10. package/esm/loaders/database-settings.js +52 -0
  11. package/esm/loaders/index.js +56 -0
  12. package/esm/loaders/pubkey.js +69 -0
  13. package/esm/loaders/registry.js +69 -0
  14. package/esm/loaders/rls.js +93 -0
  15. package/esm/loaders/types.js +12 -0
  16. package/esm/loaders/webauthn.js +54 -0
  17. package/esm/pg-client.js +8 -0
  18. package/esm/pg-settings.js +44 -0
  19. package/esm/request-id.js +19 -0
  20. package/esm/types.js +9 -0
  21. package/index.d.ts +46 -0
  22. package/index.js +62 -0
  23. package/loaders/auth-settings.d.ts +14 -0
  24. package/loaders/auth-settings.js +65 -0
  25. package/loaders/cors.d.ts +9 -0
  26. package/loaders/cors.js +58 -0
  27. package/loaders/create-loader.d.ts +19 -0
  28. package/loaders/create-loader.js +56 -0
  29. package/loaders/database-settings.d.ts +10 -0
  30. package/loaders/database-settings.js +55 -0
  31. package/loaders/index.d.ts +39 -0
  32. package/loaders/index.js +68 -0
  33. package/loaders/pubkey.d.ts +10 -0
  34. package/loaders/pubkey.js +72 -0
  35. package/loaders/registry.d.ts +37 -0
  36. package/loaders/registry.js +72 -0
  37. package/loaders/rls.d.ts +10 -0
  38. package/loaders/rls.js +96 -0
  39. package/loaders/types.d.ts +43 -0
  40. package/loaders/types.js +13 -0
  41. package/loaders/webauthn.d.ts +9 -0
  42. package/loaders/webauthn.js +57 -0
  43. package/package.json +63 -0
  44. package/pg-client.d.ts +8 -0
  45. package/pg-client.js +12 -0
  46. package/pg-settings.d.ts +29 -0
  47. package/pg-settings.js +47 -0
  48. package/request-id.d.ts +14 -0
  49. package/request-id.js +22 -0
  50. package/types.d.ts +197 -0
  51. package/types.js +10 -0
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Constructive <developers@constructive.io>
5
+ Copyright (c) 2020-present, Interweb, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # @constructive-io/express-context
2
+
3
+ Extractable Express middleware for Constructive tenant context — domain resolution, JWT auth, pgSettings, and withPgClient.
4
+
5
+ ## Usage
6
+
7
+ ```typescript
8
+ import {
9
+ createContextMiddleware,
10
+ requestIdMiddleware
11
+ } from '@constructive-io/express-context';
12
+
13
+ const app = express();
14
+
15
+ app.use(requestIdMiddleware());
16
+ app.use(apiMiddleware); // sets req.api
17
+ app.use(authMiddleware); // sets req.token
18
+ app.use(createContextMiddleware()); // builds req.constructive
19
+
20
+ app.post('/v1/chat', async (req, res) => {
21
+ const { withPgClient, pgSettings, userId, databaseId } = req.constructive;
22
+
23
+ const result = await withPgClient(async (client) => {
24
+ return client.query('SELECT current_user_id()');
25
+ });
26
+
27
+ res.json(result.rows);
28
+ });
29
+ ```
30
+
31
+ ## What it provides
32
+
33
+ - **Types** — `ApiStructure`, `RlsModule`, `AuthSettings`, `ConstructiveContext`, etc.
34
+ - **pgSettings builder** — Constructs SET LOCAL key-value pairs from API + token
35
+ - **withPgClient** — Tenant-scoped RLS transaction helper (BEGIN → SET LOCAL → fn → COMMIT)
36
+ - **requestId middleware** — UUID correlation ID (from X-Request-Id header or generated)
37
+ - **Context middleware** — Composes all of the above into `req.constructive`
38
+
39
+ ---
40
+
41
+ ## Education and Tutorials
42
+
43
+ 1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
44
+ Get started with modular databases in minutes. Install prerequisites and deploy your first module.
45
+
46
+ 2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
47
+ Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
48
+
49
+ 3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
50
+ Master the workflow for adding, organizing, and managing database changes with pgpm.
51
+
52
+ 4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
53
+ Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
54
+
55
+ 5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
56
+ Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
57
+
58
+ 6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
59
+ Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
60
+
61
+ 7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
62
+ Common issues and solutions for pgpm, PostgreSQL, and testing.
63
+
64
+ ## Related Constructive Tooling
65
+
66
+ ### 📦 Package Management
67
+
68
+ * [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
69
+
70
+ ### 🧪 Testing
71
+
72
+ * [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
73
+ * [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
74
+ * [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
75
+ * [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
76
+ * [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
77
+
78
+ ### 🧠 Parsing & AST
79
+
80
+ * [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
81
+ * [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
82
+ * [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
83
+ * [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
84
+ * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
85
+ * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
86
+
87
+ ## Credits
88
+
89
+ **🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
90
+
91
+ ## Disclaimer
92
+
93
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
94
+
95
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
package/context.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * context — Builds `req.constructive` from resolved API + auth token
3
+ *
4
+ * This middleware runs AFTER the API resolver and auth middleware have
5
+ * populated `req.api` and `req.token`. It composes:
6
+ *
7
+ * - pgSettings (role, claims, request_id, database_id)
8
+ * - Tenant database pool (via pg-cache)
9
+ * - withPgClient (transaction-scoped RLS helper)
10
+ * - Convenience fields (userId, databaseId, requestId)
11
+ * - useModule (lazy, on-demand per-database module resolution)
12
+ *
13
+ * The result is a single `req.constructive` object that any downstream
14
+ * route handler can use for tenant-scoped database operations.
15
+ */
16
+ import type { Request, RequestHandler } from 'express';
17
+ import type { PgpmOptions } from '@pgpmjs/types';
18
+ import type { LoaderRegistry } from './loaders/registry';
19
+ import type { ConstructiveContext } from './types';
20
+ export interface ContextMiddlewareOptions {
21
+ /** Base PG options for pool creation (host, port, user, password) */
22
+ pg?: PgpmOptions['pg'];
23
+ /** Module loader registry for per-database cached lookups */
24
+ loaders?: LoaderRegistry;
25
+ }
26
+ /**
27
+ * Build the ConstructiveContext from the current request state.
28
+ *
29
+ * Requires `req.api` and `req.requestId` to be set by upstream middleware.
30
+ * `req.token` is optional (anonymous requests get null).
31
+ *
32
+ * Module loaders are NOT resolved eagerly. Instead, `ctx.useModule(name)`
33
+ * resolves them on demand — only the modules that middleware actually
34
+ * needs will fire SQL queries.
35
+ */
36
+ export declare function buildContext(req: Request, opts?: ContextMiddlewareOptions): ConstructiveContext | null;
37
+ /**
38
+ * Express middleware that builds `req.constructive` from the resolved
39
+ * API config and auth token.
40
+ *
41
+ * Mount AFTER the API resolver and auth middleware:
42
+ *
43
+ * ```typescript
44
+ * import { createContextMiddleware, createDefaultRegistry } from '@constructive-io/express-context';
45
+ *
46
+ * const loaders = createDefaultRegistry();
47
+ *
48
+ * app.use(apiMiddleware); // sets req.api
49
+ * app.use(authMiddleware); // sets req.token
50
+ * app.use(createContextMiddleware({ loaders }));
51
+ *
52
+ * // Downstream middleware/routes call useModule on demand:
53
+ * app.post('/v1/chat', async (req, res) => {
54
+ * const ctx = req.constructive;
55
+ * const rls = await ctx.useModule('rlsModule'); // only fires if not cached
56
+ * const auth = await ctx.useModule('authSettings'); // only fires if not cached
57
+ * // webauthnSettings loader never fires if nobody asks for it
58
+ * });
59
+ * ```
60
+ */
61
+ export declare function createContextMiddleware(opts?: ContextMiddlewareOptions): RequestHandler;
package/context.js ADDED
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * context — Builds `req.constructive` from resolved API + auth token
4
+ *
5
+ * This middleware runs AFTER the API resolver and auth middleware have
6
+ * populated `req.api` and `req.token`. It composes:
7
+ *
8
+ * - pgSettings (role, claims, request_id, database_id)
9
+ * - Tenant database pool (via pg-cache)
10
+ * - withPgClient (transaction-scoped RLS helper)
11
+ * - Convenience fields (userId, databaseId, requestId)
12
+ * - useModule (lazy, on-demand per-database module resolution)
13
+ *
14
+ * The result is a single `req.constructive` object that any downstream
15
+ * route handler can use for tenant-scoped database operations.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.buildContext = buildContext;
19
+ exports.createContextMiddleware = createContextMiddleware;
20
+ const pg_cache_1 = require("pg-cache");
21
+ const pg_client_1 = require("./pg-client");
22
+ const pg_settings_1 = require("./pg-settings");
23
+ /**
24
+ * Create a `useModule` function bound to the given loader context.
25
+ *
26
+ * Calling `useModule('rlsModule')` lazily resolves the RLS loader,
27
+ * hitting the DB only on cache miss. The function is a no-op (returns
28
+ * undefined) when no registry is configured.
29
+ */
30
+ function createUseModule(registry, loaderCtx) {
31
+ return (async (name) => {
32
+ if (!registry || !loaderCtx)
33
+ return undefined;
34
+ return registry.resolve(name, loaderCtx);
35
+ });
36
+ }
37
+ /**
38
+ * Build the ConstructiveContext from the current request state.
39
+ *
40
+ * Requires `req.api` and `req.requestId` to be set by upstream middleware.
41
+ * `req.token` is optional (anonymous requests get null).
42
+ *
43
+ * Module loaders are NOT resolved eagerly. Instead, `ctx.useModule(name)`
44
+ * resolves them on demand — only the modules that middleware actually
45
+ * needs will fire SQL queries.
46
+ */
47
+ function buildContext(req, opts = {}) {
48
+ const api = req.api;
49
+ if (!api)
50
+ return null;
51
+ const token = req.token ?? null;
52
+ const requestId = req.requestId || '';
53
+ const pgSettings = (0, pg_settings_1.buildPgSettings)({
54
+ api,
55
+ token,
56
+ requestId,
57
+ clientIp: req.clientIp,
58
+ });
59
+ const tenantPool = (0, pg_cache_1.getPgPool)({
60
+ ...opts.pg,
61
+ database: api.dbname,
62
+ });
63
+ // Build loader context (if registry provided and databaseId known)
64
+ let loaderCtx = null;
65
+ if (opts.loaders && api.databaseId) {
66
+ const servicesPool = (0, pg_cache_1.getPgPool)(opts.pg);
67
+ loaderCtx = {
68
+ servicesPool,
69
+ tenantPool,
70
+ databaseId: api.databaseId,
71
+ apiId: api.apiId,
72
+ dbname: api.dbname,
73
+ };
74
+ }
75
+ return {
76
+ api,
77
+ token,
78
+ pgSettings,
79
+ databaseId: api.databaseId ?? null,
80
+ userId: token?.user_id ?? null,
81
+ requestId,
82
+ pool: tenantPool,
83
+ withPgClient: (fn) => (0, pg_client_1.withPgClient)(tenantPool, pgSettings, fn),
84
+ useModule: createUseModule(opts.loaders, loaderCtx),
85
+ };
86
+ }
87
+ /**
88
+ * Express middleware that builds `req.constructive` from the resolved
89
+ * API config and auth token.
90
+ *
91
+ * Mount AFTER the API resolver and auth middleware:
92
+ *
93
+ * ```typescript
94
+ * import { createContextMiddleware, createDefaultRegistry } from '@constructive-io/express-context';
95
+ *
96
+ * const loaders = createDefaultRegistry();
97
+ *
98
+ * app.use(apiMiddleware); // sets req.api
99
+ * app.use(authMiddleware); // sets req.token
100
+ * app.use(createContextMiddleware({ loaders }));
101
+ *
102
+ * // Downstream middleware/routes call useModule on demand:
103
+ * app.post('/v1/chat', async (req, res) => {
104
+ * const ctx = req.constructive;
105
+ * const rls = await ctx.useModule('rlsModule'); // only fires if not cached
106
+ * const auth = await ctx.useModule('authSettings'); // only fires if not cached
107
+ * // webauthnSettings loader never fires if nobody asks for it
108
+ * });
109
+ * ```
110
+ */
111
+ function createContextMiddleware(opts = {}) {
112
+ return (req, _res, next) => {
113
+ const ctx = buildContext(req, opts);
114
+ if (ctx) {
115
+ req.constructive = ctx;
116
+ }
117
+ next();
118
+ };
119
+ }
package/esm/context.js ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * context — Builds `req.constructive` from resolved API + auth token
3
+ *
4
+ * This middleware runs AFTER the API resolver and auth middleware have
5
+ * populated `req.api` and `req.token`. It composes:
6
+ *
7
+ * - pgSettings (role, claims, request_id, database_id)
8
+ * - Tenant database pool (via pg-cache)
9
+ * - withPgClient (transaction-scoped RLS helper)
10
+ * - Convenience fields (userId, databaseId, requestId)
11
+ * - useModule (lazy, on-demand per-database module resolution)
12
+ *
13
+ * The result is a single `req.constructive` object that any downstream
14
+ * route handler can use for tenant-scoped database operations.
15
+ */
16
+ import { getPgPool } from 'pg-cache';
17
+ import { withPgClient as withPgClientFn } from './pg-client';
18
+ import { buildPgSettings } from './pg-settings';
19
+ /**
20
+ * Create a `useModule` function bound to the given loader context.
21
+ *
22
+ * Calling `useModule('rlsModule')` lazily resolves the RLS loader,
23
+ * hitting the DB only on cache miss. The function is a no-op (returns
24
+ * undefined) when no registry is configured.
25
+ */
26
+ function createUseModule(registry, loaderCtx) {
27
+ return (async (name) => {
28
+ if (!registry || !loaderCtx)
29
+ return undefined;
30
+ return registry.resolve(name, loaderCtx);
31
+ });
32
+ }
33
+ /**
34
+ * Build the ConstructiveContext from the current request state.
35
+ *
36
+ * Requires `req.api` and `req.requestId` to be set by upstream middleware.
37
+ * `req.token` is optional (anonymous requests get null).
38
+ *
39
+ * Module loaders are NOT resolved eagerly. Instead, `ctx.useModule(name)`
40
+ * resolves them on demand — only the modules that middleware actually
41
+ * needs will fire SQL queries.
42
+ */
43
+ export function buildContext(req, opts = {}) {
44
+ const api = req.api;
45
+ if (!api)
46
+ return null;
47
+ const token = req.token ?? null;
48
+ const requestId = req.requestId || '';
49
+ const pgSettings = buildPgSettings({
50
+ api,
51
+ token,
52
+ requestId,
53
+ clientIp: req.clientIp,
54
+ });
55
+ const tenantPool = getPgPool({
56
+ ...opts.pg,
57
+ database: api.dbname,
58
+ });
59
+ // Build loader context (if registry provided and databaseId known)
60
+ let loaderCtx = null;
61
+ if (opts.loaders && api.databaseId) {
62
+ const servicesPool = getPgPool(opts.pg);
63
+ loaderCtx = {
64
+ servicesPool,
65
+ tenantPool,
66
+ databaseId: api.databaseId,
67
+ apiId: api.apiId,
68
+ dbname: api.dbname,
69
+ };
70
+ }
71
+ return {
72
+ api,
73
+ token,
74
+ pgSettings,
75
+ databaseId: api.databaseId ?? null,
76
+ userId: token?.user_id ?? null,
77
+ requestId,
78
+ pool: tenantPool,
79
+ withPgClient: (fn) => withPgClientFn(tenantPool, pgSettings, fn),
80
+ useModule: createUseModule(opts.loaders, loaderCtx),
81
+ };
82
+ }
83
+ /**
84
+ * Express middleware that builds `req.constructive` from the resolved
85
+ * API config and auth token.
86
+ *
87
+ * Mount AFTER the API resolver and auth middleware:
88
+ *
89
+ * ```typescript
90
+ * import { createContextMiddleware, createDefaultRegistry } from '@constructive-io/express-context';
91
+ *
92
+ * const loaders = createDefaultRegistry();
93
+ *
94
+ * app.use(apiMiddleware); // sets req.api
95
+ * app.use(authMiddleware); // sets req.token
96
+ * app.use(createContextMiddleware({ loaders }));
97
+ *
98
+ * // Downstream middleware/routes call useModule on demand:
99
+ * app.post('/v1/chat', async (req, res) => {
100
+ * const ctx = req.constructive;
101
+ * const rls = await ctx.useModule('rlsModule'); // only fires if not cached
102
+ * const auth = await ctx.useModule('authSettings'); // only fires if not cached
103
+ * // webauthnSettings loader never fires if nobody asks for it
104
+ * });
105
+ * ```
106
+ */
107
+ export function createContextMiddleware(opts = {}) {
108
+ return (req, _res, next) => {
109
+ const ctx = buildContext(req, opts);
110
+ if (ctx) {
111
+ req.constructive = ctx;
112
+ }
113
+ next();
114
+ };
115
+ }
package/esm/index.js ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @constructive-io/express-context
3
+ *
4
+ * Extractable Express middleware for Constructive tenant context.
5
+ *
6
+ * Provides:
7
+ * - Shared types (ApiStructure, RlsModule, AuthSettings, etc.)
8
+ * - pgSettings builder (role, JWT claims, request_id, database_id)
9
+ * - withPgClient (tenant-scoped RLS transaction helper)
10
+ * - requestId middleware (UUID correlation ID)
11
+ * - Context middleware (composes all of the above into req.constructive)
12
+ * - Module loaders (pluggable per-database cached lookups)
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import {
17
+ * createContextMiddleware,
18
+ * requestIdMiddleware,
19
+ * createDefaultRegistry,
20
+ * } from '@constructive-io/express-context';
21
+ *
22
+ * const loaders = createDefaultRegistry();
23
+ *
24
+ * app.use(requestIdMiddleware());
25
+ * app.use(apiMiddleware); // sets req.api (your domain resolver)
26
+ * app.use(authMiddleware); // sets req.token (your JWT verifier)
27
+ * app.use(createContextMiddleware({ loaders })); // builds req.constructive
28
+ *
29
+ * app.post('/v1/chat', async (req, res) => {
30
+ * const ctx = req.constructive;
31
+ * const rls = await ctx.useModule('rlsModule'); // only fires if not cached
32
+ * const auth = await ctx.useModule('authSettings'); // only fires if not cached
33
+ * // webauthnSettings loader never fires if nobody asks for it
34
+ * });
35
+ * ```
36
+ */
37
+ export { buildPgSettings } from './pg-settings';
38
+ // withPgClient helper
39
+ export { withPgClient } from './pg-client';
40
+ // Request ID middleware
41
+ export { requestIdMiddleware } from './request-id';
42
+ export { buildContext, createContextMiddleware } from './context';
43
+ export { authSettingsLoader, corsLoader, createDefaultRegistry, createLoaderRegistry, createModuleLoader, databaseSettingsLoader, pubkeyLoader, rlsLoader, webauthnLoader, } from './loaders';
44
+ // Side-effect: Express type augmentation
45
+ import './types';
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Auth Settings Loader (Tier 2 — tenant DB)
3
+ *
4
+ * Two-step discovery:
5
+ * 1. Query metaschema_modules_public.sessions_module to find the
6
+ * schema name and table name for auth settings
7
+ * 2. Query the discovered <schema>.<table> in the tenant DB
8
+ *
9
+ * This is the pattern for any module whose config lives in the tenant
10
+ * database rather than the services database.
11
+ */
12
+ import { createModuleLoader } from './create-loader';
13
+ // ─── SQL ────────────────────────────────────────────────────────────────────
14
+ const AUTH_SETTINGS_DISCOVERY_SQL = `
15
+ SELECT s.schema_name, sm.auth_settings_table AS table_name
16
+ FROM metaschema_modules_public.sessions_module sm
17
+ JOIN metaschema_public.schema s ON s.id = sm.schema_id
18
+ LIMIT 1
19
+ `;
20
+ const buildAuthSettingsQuery = (schemaName, tableName) => `
21
+ SELECT
22
+ cookie_secure,
23
+ cookie_samesite,
24
+ cookie_domain,
25
+ cookie_httponly,
26
+ cookie_max_age,
27
+ cookie_path,
28
+ remember_me_duration,
29
+ enable_captcha,
30
+ captcha_site_key
31
+ FROM "${schemaName}"."${tableName}"
32
+ LIMIT 1
33
+ `;
34
+ // ─── Loader ─────────────────────────────────────────────────────────────────
35
+ export const authSettingsLoader = createModuleLoader({
36
+ name: 'authSettings',
37
+ ttlMs: 5 * 60_000,
38
+ async resolve(ctx) {
39
+ const { tenantPool } = ctx;
40
+ // Step 1: Discover schema + table from sessions_module
41
+ const discovery = await tenantPool.query(AUTH_SETTINGS_DISCOVERY_SQL);
42
+ const resolved = discovery.rows[0];
43
+ if (!resolved)
44
+ return undefined;
45
+ // Step 2: Query the actual auth settings table
46
+ const result = await tenantPool.query(buildAuthSettingsQuery(resolved.schema_name, resolved.table_name));
47
+ const row = result.rows[0];
48
+ if (!row)
49
+ return undefined;
50
+ return {
51
+ cookieSecure: row.cookie_secure,
52
+ cookieSamesite: row.cookie_samesite,
53
+ cookieDomain: row.cookie_domain,
54
+ cookieHttponly: row.cookie_httponly,
55
+ cookieMaxAge: row.cookie_max_age,
56
+ cookiePath: row.cookie_path,
57
+ rememberMeDuration: row.remember_me_duration,
58
+ enableCaptcha: row.enable_captcha,
59
+ captchaSiteKey: row.captcha_site_key,
60
+ };
61
+ },
62
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * CORS Origins Loader
3
+ *
4
+ * Resolves allowed CORS origins for a database+API combination.
5
+ * Checks per-API settings first, falls back to database-level default,
6
+ * then to the legacy api_modules approach.
7
+ */
8
+ import { createModuleLoader } from './create-loader';
9
+ // ─── SQL ────────────────────────────────────────────────────────────────────
10
+ const CORS_SETTINGS_SQL = `
11
+ SELECT allowed_origins
12
+ FROM services_public.cors_settings
13
+ WHERE database_id = $1 AND api_id = $2
14
+ LIMIT 1
15
+ `;
16
+ const CORS_SETTINGS_DB_DEFAULT_SQL = `
17
+ SELECT allowed_origins
18
+ FROM services_public.cors_settings
19
+ WHERE database_id = $1 AND api_id IS NULL
20
+ LIMIT 1
21
+ `;
22
+ const CORS_MODULE_SQL = `
23
+ SELECT data
24
+ FROM services_public.api_modules
25
+ WHERE api_id = $1 AND name = 'cors'
26
+ LIMIT 1
27
+ `;
28
+ // ─── Loader ─────────────────────────────────────────────────────────────────
29
+ export const corsLoader = createModuleLoader({
30
+ name: 'corsOrigins',
31
+ ttlMs: 5 * 60_000,
32
+ async resolve(ctx) {
33
+ const { servicesPool, databaseId, apiId } = ctx;
34
+ // Try per-API cors_settings first
35
+ try {
36
+ if (apiId) {
37
+ const perApi = await servicesPool.query(CORS_SETTINGS_SQL, [databaseId, apiId]);
38
+ if (perApi.rows[0])
39
+ return perApi.rows[0].allowed_origins;
40
+ }
41
+ const dbDefault = await servicesPool.query(CORS_SETTINGS_DB_DEFAULT_SQL, [databaseId]);
42
+ if (dbDefault.rows[0])
43
+ return dbDefault.rows[0].allowed_origins;
44
+ }
45
+ catch {
46
+ // Table may not exist yet
47
+ }
48
+ // Fall back to legacy api_modules
49
+ if (apiId) {
50
+ const result = await servicesPool.query(CORS_MODULE_SQL, [apiId]);
51
+ return result.rows[0]?.data?.urls;
52
+ }
53
+ return undefined;
54
+ },
55
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * create-loader — Factory for building cached ModuleLoader instances.
3
+ *
4
+ * Wraps a raw resolve function with an LRU cache keyed by databaseId.
5
+ * Each loader gets its own independent cache with configurable TTL and
6
+ * max entries.
7
+ */
8
+ import { LRUCache } from 'lru-cache';
9
+ import { Logger } from '@pgpmjs/logger';
10
+ const DEFAULT_TTL_MS = 60_000;
11
+ const DEFAULT_MAX = 100;
12
+ export function createModuleLoader(opts) {
13
+ const log = new Logger(`loader:${opts.name}`);
14
+ const cache = new LRUCache({
15
+ max: opts.max ?? DEFAULT_MAX,
16
+ ttl: opts.ttlMs ?? DEFAULT_TTL_MS,
17
+ updateAgeOnGet: true,
18
+ allowStale: false,
19
+ });
20
+ return {
21
+ name: opts.name,
22
+ async resolve(ctx) {
23
+ const key = ctx.databaseId;
24
+ if (cache.has(key)) {
25
+ log.debug(`Cache HIT databaseId=${key}`);
26
+ return cache.get(key);
27
+ }
28
+ log.debug(`Cache MISS databaseId=${key}, resolving`);
29
+ try {
30
+ const value = await opts.resolve(ctx);
31
+ cache.set(key, value);
32
+ return value;
33
+ }
34
+ catch (e) {
35
+ log.warn(`Failed to resolve databaseId=${key}: ${e.message}`);
36
+ return undefined;
37
+ }
38
+ },
39
+ invalidate(databaseId) {
40
+ if (databaseId) {
41
+ cache.delete(databaseId);
42
+ log.debug(`Invalidated databaseId=${databaseId}`);
43
+ }
44
+ else {
45
+ cache.clear();
46
+ log.debug(`Invalidated all entries (was size=${cache.size})`);
47
+ }
48
+ },
49
+ get cacheSize() {
50
+ return cache.size;
51
+ },
52
+ };
53
+ }