@constructive-io/express-context 0.2.0 → 0.2.1
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/loaders/create-loader.js +11 -4
- package/loaders/create-loader.d.ts +1 -1
- package/loaders/create-loader.js +11 -4
- package/package.json +2 -2
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
|
|
|
@@ -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();
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/express-context",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
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": "ccc303b2a2d1d9701648efc124ea1d9f2714711f"
|
|
63
63
|
}
|