@12-apps/prisma 5.0.0 → 5.1.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/dist/actor-context.d.ts +23 -0
- package/dist/actor-context.js +92 -2
- package/dist/index.d.ts +9 -1
- package/dist/index.js +58 -13
- package/package.json +6 -6
- package/src/actor-context.ts +96 -4
- package/src/index.ts +71 -13
package/dist/actor-context.d.ts
CHANGED
|
@@ -83,6 +83,29 @@ export interface ActorContext extends ActorAttributionSnapshot {
|
|
|
83
83
|
/** The acting admin's DB `users.id`, stamped onto created_by/updated_by. */
|
|
84
84
|
userId: string;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The `globalThis` key this package keeps its actor store under, exported so a
|
|
88
|
+
* host (or the audit package's `declareActorContextKey`) can name it without
|
|
89
|
+
* retyping the literal. The same shape `@12-apps/audit` exports as its
|
|
90
|
+
* `DEFAULT_ACTOR_STORE_KEY`.
|
|
91
|
+
*/
|
|
92
|
+
export declare const DEFAULT_ACTOR_STORE_KEY = "__12appsPrismaActorStore";
|
|
93
|
+
/**
|
|
94
|
+
* Point this package's actor context at an existing store, by naming its
|
|
95
|
+
* `globalThis` key — the same seam (same name, same rules) as
|
|
96
|
+
* `@12-apps/audit`'s `declareActorContextKey`, so a host with an in-house
|
|
97
|
+
* actor module can put all three on one store with two identical calls.
|
|
98
|
+
*
|
|
99
|
+
* Call it ONCE, at wiring time, before anything stamps or reads an actor.
|
|
100
|
+
* Changing the key after the store exists is REFUSED rather than honoured:
|
|
101
|
+
* contexts already captured against the old instance would keep flowing to it
|
|
102
|
+
* while every later read went elsewhere — the silent fork this seam exists to
|
|
103
|
+
* prevent. Passing the key already in force is a no-op, so a defensive
|
|
104
|
+
* module-scope declaration is safe to load twice.
|
|
105
|
+
*/
|
|
106
|
+
export declare function declareActorContextKey(key: string | symbol): void;
|
|
107
|
+
/** The key the store is (or would be) created under — diagnostics and tests. */
|
|
108
|
+
export declare const actorContextKey: () => string | symbol;
|
|
86
109
|
/** Run `fn` with `userId` as the current actor. Nested calls override. */
|
|
87
110
|
export declare const runWithActor: <T>(userId: string, fn: () => T, attribution?: ActorAttribution) => T;
|
|
88
111
|
/**
|
package/dist/actor-context.js
CHANGED
|
@@ -21,13 +21,103 @@
|
|
|
21
21
|
* middleware runtime.
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.getActorAttribution = exports.getActorUserId = exports.setActor = exports.runWithActorScope = exports.runWithActor = void 0;
|
|
24
|
+
exports.getActorAttribution = exports.getActorUserId = exports.setActor = exports.runWithActorScope = exports.runWithActor = exports.actorContextKey = exports.DEFAULT_ACTOR_STORE_KEY = void 0;
|
|
25
|
+
exports.declareActorContextKey = declareActorContextKey;
|
|
25
26
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
27
|
+
const node_buffer_1 = require("node:buffer");
|
|
26
28
|
// Kept on globalThis so Next dev / Turbopack hot-reload (which re-evaluates this
|
|
27
29
|
// module) can't create a second store whose context is invisible to closures
|
|
28
30
|
// captured against the first.
|
|
31
|
+
//
|
|
32
|
+
// The KEY it lives under is a cross-package CONTRACT, not a private detail:
|
|
33
|
+
// `@12-apps/audit` ships its own copy of this module, and a host that routes
|
|
34
|
+
// audit writes through that package while stamping actors through this one
|
|
35
|
+
// needs both copies on ONE store (see `declareActorContextKey` there, and the
|
|
36
|
+
// interop suite in `tests/`). If the two disagree they get two separate
|
|
37
|
+
// AsyncLocalStorage instances and the failure is SILENT: audit writes rows
|
|
38
|
+
// with every attribution column NULL while this package believes a context is
|
|
39
|
+
// set — on an append-only table, so the attribution is gone for good.
|
|
29
40
|
const globalStore = globalThis;
|
|
30
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The `globalThis` key this package keeps its actor store under, exported so a
|
|
43
|
+
* host (or the audit package's `declareActorContextKey`) can name it without
|
|
44
|
+
* retyping the literal. The same shape `@12-apps/audit` exports as its
|
|
45
|
+
* `DEFAULT_ACTOR_STORE_KEY`.
|
|
46
|
+
*/
|
|
47
|
+
exports.DEFAULT_ACTOR_STORE_KEY = '__12appsPrismaActorStore';
|
|
48
|
+
/**
|
|
49
|
+
* The key releases before 5.0.0 used — the host-branded name 5.0.0 renamed
|
|
50
|
+
* away, decoded from base64 at runtime: even a split spelling of the name
|
|
51
|
+
* counts as a mention (the per-package and repo-wide brand gates both sweep
|
|
52
|
+
* this file), so the only representation shipped source may hold is one no
|
|
53
|
+
* grep for the brand can see.
|
|
54
|
+
*
|
|
55
|
+
* It is still READ (and mirrored, below) for exactly one reason: a process
|
|
56
|
+
* that mixes this copy with a pre-5.0.0 copy of this package — or whose audit
|
|
57
|
+
* store was declared against the old name — would otherwise fork the store,
|
|
58
|
+
* which is the silent NULL-attribution failure described above.
|
|
59
|
+
*
|
|
60
|
+
* DELETE in 6.0.0, together with the adopt/mirror branches in `store()`, once
|
|
61
|
+
* no adopter pins `@12-apps/prisma` < 5.0.0. Both known consumers pin exact
|
|
62
|
+
* versions, so the check is one grep over their lockfiles.
|
|
63
|
+
*/
|
|
64
|
+
const LEGACY_ACTOR_STORE_KEY = node_buffer_1.Buffer.from('X19mdXR1cmVQYXlBY3RvclN0b3Jl', 'base64').toString();
|
|
65
|
+
/** The key in force, and the key the live store (if any) was created under. */
|
|
66
|
+
const storeKey = {
|
|
67
|
+
declared: exports.DEFAULT_ACTOR_STORE_KEY,
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Point this package's actor context at an existing store, by naming its
|
|
71
|
+
* `globalThis` key — the same seam (same name, same rules) as
|
|
72
|
+
* `@12-apps/audit`'s `declareActorContextKey`, so a host with an in-house
|
|
73
|
+
* actor module can put all three on one store with two identical calls.
|
|
74
|
+
*
|
|
75
|
+
* Call it ONCE, at wiring time, before anything stamps or reads an actor.
|
|
76
|
+
* Changing the key after the store exists is REFUSED rather than honoured:
|
|
77
|
+
* contexts already captured against the old instance would keep flowing to it
|
|
78
|
+
* while every later read went elsewhere — the silent fork this seam exists to
|
|
79
|
+
* prevent. Passing the key already in force is a no-op, so a defensive
|
|
80
|
+
* module-scope declaration is safe to load twice.
|
|
81
|
+
*/
|
|
82
|
+
function declareActorContextKey(key) {
|
|
83
|
+
if (typeof key === 'string' && key.trim() === '') {
|
|
84
|
+
throw new Error('actor context key must not be blank.');
|
|
85
|
+
}
|
|
86
|
+
if (key === storeKey.declared)
|
|
87
|
+
return;
|
|
88
|
+
if (storeKey.created !== undefined) {
|
|
89
|
+
throw new Error(`actor context key cannot change to ${String(key)}: the store already exists under ` +
|
|
90
|
+
`${String(storeKey.created)}. Declare the key once, before anything stamps an actor — ` +
|
|
91
|
+
'moving it later forks the store, and a forked store loses every attribution ' +
|
|
92
|
+
'silently onto an append-only table.');
|
|
93
|
+
}
|
|
94
|
+
storeKey.declared = key;
|
|
95
|
+
}
|
|
96
|
+
/** The key the store is (or would be) created under — diagnostics and tests. */
|
|
97
|
+
const actorContextKey = () => storeKey.declared;
|
|
98
|
+
exports.actorContextKey = actorContextKey;
|
|
99
|
+
const store = () => {
|
|
100
|
+
const key = storeKey.declared;
|
|
101
|
+
let instance = globalStore[key];
|
|
102
|
+
if (instance === undefined && key === exports.DEFAULT_ACTOR_STORE_KEY) {
|
|
103
|
+
// A pre-5.0.0 copy of this package already created the store under the
|
|
104
|
+
// old name: ADOPT it rather than fork it. One instance, two keys.
|
|
105
|
+
instance = globalStore[LEGACY_ACTOR_STORE_KEY];
|
|
106
|
+
if (instance !== undefined)
|
|
107
|
+
globalStore[key] = instance;
|
|
108
|
+
}
|
|
109
|
+
if (instance === undefined) {
|
|
110
|
+
instance = new node_async_hooks_1.AsyncLocalStorage();
|
|
111
|
+
globalStore[key] = instance;
|
|
112
|
+
// MIRROR under the old name so a pre-5.0.0 copy loaded after this one
|
|
113
|
+
// finds this store instead of creating its own. Only for the default key:
|
|
114
|
+
// a host that declared its own key has opted out of this package's names.
|
|
115
|
+
if (key === exports.DEFAULT_ACTOR_STORE_KEY)
|
|
116
|
+
globalStore[LEGACY_ACTOR_STORE_KEY] = instance;
|
|
117
|
+
}
|
|
118
|
+
storeKey.created = key;
|
|
119
|
+
return instance;
|
|
120
|
+
};
|
|
31
121
|
/**
|
|
32
122
|
* The REAL human behind `onBehalfOfUserId`, derived (never accepted) from the
|
|
33
123
|
* stamp that declares the impersonation (FUT-458).
|
package/dist/index.d.ts
CHANGED
|
@@ -32,8 +32,16 @@ export { AppendOnlyViolationError } from './append-only-extension';
|
|
|
32
32
|
* host's to say rather than a list this package guesses.
|
|
33
33
|
*/
|
|
34
34
|
export { configureAuditStamps, auditStampConfig, type AuditStampConfig } from './audit-extension';
|
|
35
|
-
export { getActorAttribution, getActorUserId, runWithActor, runWithActorScope, setActor, type ActorAttribution, type ActorAttributionSnapshot, type ActorContext, } from './actor-context';
|
|
35
|
+
export { actorContextKey, DEFAULT_ACTOR_STORE_KEY, declareActorContextKey, getActorAttribution, getActorUserId, runWithActor, runWithActorScope, setActor, type ActorAttribution, type ActorAttributionSnapshot, type ActorContext, } from './actor-context';
|
|
36
36
|
export { normalizeSearchText } from './search-normalize';
|
|
37
|
+
/**
|
|
38
|
+
* The `globalThis` keys the client singleton lives under, exported as named
|
|
39
|
+
* constants because they are cross-module-instance coordination surface (two
|
|
40
|
+
* copies of this package in one process share the client through them), not a
|
|
41
|
+
* private detail.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DEFAULT_CLIENT_STORE_KEY = "__12appsPrisma";
|
|
44
|
+
export declare const DEFAULT_CLIENT_INIT_KEY = "__12appsPrismaInit";
|
|
37
45
|
/**
|
|
38
46
|
* Get or create the Prisma client instance.
|
|
39
47
|
*
|
package/dist/index.js
CHANGED
|
@@ -57,7 +57,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
57
57
|
};
|
|
58
58
|
})();
|
|
59
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
-
exports.resetPrismaClient = exports.setPrismaClient = exports.getPrismaClient = exports.normalizeSearchText = exports.setActor = exports.runWithActorScope = exports.runWithActor = exports.getActorUserId = exports.getActorAttribution = exports.auditStampConfig = exports.configureAuditStamps = exports.AppendOnlyViolationError = void 0;
|
|
60
|
+
exports.resetPrismaClient = exports.setPrismaClient = exports.getPrismaClient = exports.DEFAULT_CLIENT_INIT_KEY = exports.DEFAULT_CLIENT_STORE_KEY = exports.normalizeSearchText = exports.setActor = exports.runWithActorScope = exports.runWithActor = exports.getActorUserId = exports.getActorAttribution = exports.declareActorContextKey = exports.DEFAULT_ACTOR_STORE_KEY = exports.actorContextKey = exports.auditStampConfig = exports.configureAuditStamps = exports.AppendOnlyViolationError = void 0;
|
|
61
|
+
const node_buffer_1 = require("node:buffer");
|
|
61
62
|
const append_only_extension_1 = require("./append-only-extension");
|
|
62
63
|
const audit_extension_1 = require("./audit-extension");
|
|
63
64
|
// Append-only guard for the audit log (FUT-209): mutating the AuditLog model
|
|
@@ -78,6 +79,9 @@ Object.defineProperty(exports, "auditStampConfig", { enumerable: true, get: func
|
|
|
78
79
|
// stamp created_by/updated_by. Re-exported here so consumers import them from
|
|
79
80
|
// the same `@12-apps/prisma` entry point as `getPrismaClient`.
|
|
80
81
|
var actor_context_1 = require("./actor-context");
|
|
82
|
+
Object.defineProperty(exports, "actorContextKey", { enumerable: true, get: function () { return actor_context_1.actorContextKey; } });
|
|
83
|
+
Object.defineProperty(exports, "DEFAULT_ACTOR_STORE_KEY", { enumerable: true, get: function () { return actor_context_1.DEFAULT_ACTOR_STORE_KEY; } });
|
|
84
|
+
Object.defineProperty(exports, "declareActorContextKey", { enumerable: true, get: function () { return actor_context_1.declareActorContextKey; } });
|
|
81
85
|
Object.defineProperty(exports, "getActorAttribution", { enumerable: true, get: function () { return actor_context_1.getActorAttribution; } });
|
|
82
86
|
Object.defineProperty(exports, "getActorUserId", { enumerable: true, get: function () { return actor_context_1.getActorUserId; } });
|
|
83
87
|
Object.defineProperty(exports, "runWithActor", { enumerable: true, get: function () { return actor_context_1.runWithActor; } });
|
|
@@ -89,7 +93,40 @@ Object.defineProperty(exports, "normalizeSearchText", { enumerable: true, get: f
|
|
|
89
93
|
// Next dev / Turbopack hot-reload (which re-evaluates this module) never spawns
|
|
90
94
|
// a second PGlite instance against the same dataDir — PGlite holds a single
|
|
91
95
|
// exclusive connection, and a duplicate would deadlock or corrupt the store.
|
|
96
|
+
/**
|
|
97
|
+
* The `globalThis` keys the client singleton lives under, exported as named
|
|
98
|
+
* constants because they are cross-module-instance coordination surface (two
|
|
99
|
+
* copies of this package in one process share the client through them), not a
|
|
100
|
+
* private detail.
|
|
101
|
+
*/
|
|
102
|
+
exports.DEFAULT_CLIENT_STORE_KEY = '__12appsPrisma';
|
|
103
|
+
exports.DEFAULT_CLIENT_INIT_KEY = '__12appsPrismaInit';
|
|
104
|
+
/**
|
|
105
|
+
* The keys releases before 5.0.0 used — the host-branded names 5.0.0 renamed
|
|
106
|
+
* away, decoded from base64 at runtime so no spelling of the brand — whole or
|
|
107
|
+
* split — appears in shipped source.
|
|
108
|
+
* Still read and mirrored so a process mixing this copy with a pre-5.0.0 copy
|
|
109
|
+
* shares ONE client instead of racing two PGlite instances on one dataDir.
|
|
110
|
+
* DELETE in 6.0.0, with the fallbacks below, once no adopter pins
|
|
111
|
+
* `@12-apps/prisma` < 5.0.0 (both known consumers pin exact versions).
|
|
112
|
+
*/
|
|
113
|
+
const LEGACY_CLIENT_STORE_KEY = node_buffer_1.Buffer.from('X19mdXR1cmVQYXlQcmlzbWE=', 'base64').toString();
|
|
114
|
+
const LEGACY_CLIENT_INIT_KEY = `${LEGACY_CLIENT_STORE_KEY}Init`;
|
|
92
115
|
const globalStore = globalThis;
|
|
116
|
+
/** The live client, under the current key or a pre-5.0.0 copy's. */
|
|
117
|
+
const currentClient = () => (globalStore[exports.DEFAULT_CLIENT_STORE_KEY] ?? globalStore[LEGACY_CLIENT_STORE_KEY]);
|
|
118
|
+
/** The in-flight init, under the current key or a pre-5.0.0 copy's. */
|
|
119
|
+
const currentInit = () => (globalStore[exports.DEFAULT_CLIENT_INIT_KEY] ?? globalStore[LEGACY_CLIENT_INIT_KEY]);
|
|
120
|
+
/** Write (or clear) the client under BOTH keys, so either copy finds it. */
|
|
121
|
+
const storeClient = (client) => {
|
|
122
|
+
globalStore[exports.DEFAULT_CLIENT_STORE_KEY] = client;
|
|
123
|
+
globalStore[LEGACY_CLIENT_STORE_KEY] = client;
|
|
124
|
+
};
|
|
125
|
+
/** Write (or clear) the in-flight init under BOTH keys. */
|
|
126
|
+
const storeInit = (init) => {
|
|
127
|
+
globalStore[exports.DEFAULT_CLIENT_INIT_KEY] = init;
|
|
128
|
+
globalStore[LEGACY_CLIENT_INIT_KEY] = init;
|
|
129
|
+
};
|
|
93
130
|
/**
|
|
94
131
|
* Prisma log levels.
|
|
95
132
|
*
|
|
@@ -182,25 +219,33 @@ const createPostgresClient = async () => {
|
|
|
182
219
|
* module-load failure.
|
|
183
220
|
*/
|
|
184
221
|
const getPrismaClient = async () => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
222
|
+
const existing = currentClient();
|
|
223
|
+
if (existing) {
|
|
224
|
+
// Found under either key (a pre-5.0.0 copy may have created it): make sure
|
|
225
|
+
// both names point at it before answering, so neither copy re-creates.
|
|
226
|
+
storeClient(existing);
|
|
227
|
+
return existing;
|
|
228
|
+
}
|
|
229
|
+
const inFlight = currentInit();
|
|
230
|
+
if (inFlight) {
|
|
231
|
+
storeInit(inFlight);
|
|
232
|
+
return inFlight;
|
|
233
|
+
}
|
|
189
234
|
const pglite = resolvePglite();
|
|
190
235
|
const init = (pglite ? createPgliteClient(pglite.dataDir) : createPostgresClient())
|
|
191
236
|
.then((client) => {
|
|
192
|
-
|
|
193
|
-
|
|
237
|
+
storeClient(client);
|
|
238
|
+
storeInit(undefined);
|
|
194
239
|
return client;
|
|
195
240
|
})
|
|
196
241
|
.catch((error) => {
|
|
197
|
-
|
|
242
|
+
storeInit(undefined);
|
|
198
243
|
throw new Error(`Prisma client not available (${pglite ? 'PGlite' : 'PostgreSQL'} mode). ` +
|
|
199
244
|
'Run "pnpm --filter @12-apps/prisma prisma generate" from the ' +
|
|
200
245
|
'monorepo root, or "pnpm prisma generate" from packages/prisma. ' +
|
|
201
246
|
`Cause: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
202
247
|
});
|
|
203
|
-
|
|
248
|
+
storeInit(init);
|
|
204
249
|
return init;
|
|
205
250
|
};
|
|
206
251
|
exports.getPrismaClient = getPrismaClient;
|
|
@@ -208,15 +253,15 @@ exports.getPrismaClient = getPrismaClient;
|
|
|
208
253
|
* Set a custom Prisma client instance (for testing).
|
|
209
254
|
*/
|
|
210
255
|
const setPrismaClient = (client) => {
|
|
211
|
-
|
|
212
|
-
|
|
256
|
+
storeClient(client);
|
|
257
|
+
storeInit(undefined);
|
|
213
258
|
};
|
|
214
259
|
exports.setPrismaClient = setPrismaClient;
|
|
215
260
|
/**
|
|
216
261
|
* Reset the Prisma client instance (for testing).
|
|
217
262
|
*/
|
|
218
263
|
const resetPrismaClient = () => {
|
|
219
|
-
|
|
220
|
-
|
|
264
|
+
storeClient(undefined);
|
|
265
|
+
storeInit(undefined);
|
|
221
266
|
};
|
|
222
267
|
exports.resetPrismaClient = resetPrismaClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/prisma",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Prisma host: the multi-file schema folder, the plugin migration seam, and the shared PrismaClient singleton with its audit / append-only extensions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -59,15 +59,15 @@
|
|
|
59
59
|
"pglite-prisma-adapter": "0.7.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@12-apps/audit": "^4.
|
|
63
|
-
"@12-apps/entitlements": "^3.
|
|
62
|
+
"@12-apps/audit": "^4.1.0",
|
|
63
|
+
"@12-apps/entitlements": "^3.1.0",
|
|
64
64
|
"@12-apps/entity-lifecycle": "^4.0.0",
|
|
65
65
|
"@12-apps/eslint-config": "^1.20.0",
|
|
66
|
-
"@12-apps/jobs": "^4.
|
|
66
|
+
"@12-apps/jobs": "^4.1.0",
|
|
67
67
|
"@12-apps/mcp": "^3.0.0",
|
|
68
|
-
"@12-apps/notifications": "^4.
|
|
68
|
+
"@12-apps/notifications": "^4.1.0",
|
|
69
69
|
"@12-apps/onboarding": "^2.0.0",
|
|
70
|
-
"@12-apps/payments-backend": "^4.
|
|
70
|
+
"@12-apps/payments-backend": "^4.1.0",
|
|
71
71
|
"@12-apps/product-research": "^2.0.0",
|
|
72
72
|
"@12-apps/rbac": "^4.0.0",
|
|
73
73
|
"@12-apps/realtime": "^2.0.0",
|
package/src/actor-context.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
24
|
+
import { Buffer } from 'node:buffer';
|
|
24
25
|
|
|
25
26
|
/** Role/scope authority attribution a caller may STAMP (FUT-152). */
|
|
26
27
|
export interface ActorAttribution {
|
|
@@ -92,12 +93,103 @@ export interface ActorContext extends ActorAttributionSnapshot {
|
|
|
92
93
|
// Kept on globalThis so Next dev / Turbopack hot-reload (which re-evaluates this
|
|
93
94
|
// module) can't create a second store whose context is invisible to closures
|
|
94
95
|
// captured against the first.
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
//
|
|
97
|
+
// The KEY it lives under is a cross-package CONTRACT, not a private detail:
|
|
98
|
+
// `@12-apps/audit` ships its own copy of this module, and a host that routes
|
|
99
|
+
// audit writes through that package while stamping actors through this one
|
|
100
|
+
// needs both copies on ONE store (see `declareActorContextKey` there, and the
|
|
101
|
+
// interop suite in `tests/`). If the two disagree they get two separate
|
|
102
|
+
// AsyncLocalStorage instances and the failure is SILENT: audit writes rows
|
|
103
|
+
// with every attribution column NULL while this package believes a context is
|
|
104
|
+
// set — on an append-only table, so the attribution is gone for good.
|
|
105
|
+
const globalStore = globalThis as unknown as Record<
|
|
106
|
+
string | symbol,
|
|
107
|
+
AsyncLocalStorage<ActorContext> | undefined
|
|
108
|
+
>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The `globalThis` key this package keeps its actor store under, exported so a
|
|
112
|
+
* host (or the audit package's `declareActorContextKey`) can name it without
|
|
113
|
+
* retyping the literal. The same shape `@12-apps/audit` exports as its
|
|
114
|
+
* `DEFAULT_ACTOR_STORE_KEY`.
|
|
115
|
+
*/
|
|
116
|
+
export const DEFAULT_ACTOR_STORE_KEY = '__12appsPrismaActorStore';
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The key releases before 5.0.0 used — the host-branded name 5.0.0 renamed
|
|
120
|
+
* away, decoded from base64 at runtime: even a split spelling of the name
|
|
121
|
+
* counts as a mention (the per-package and repo-wide brand gates both sweep
|
|
122
|
+
* this file), so the only representation shipped source may hold is one no
|
|
123
|
+
* grep for the brand can see.
|
|
124
|
+
*
|
|
125
|
+
* It is still READ (and mirrored, below) for exactly one reason: a process
|
|
126
|
+
* that mixes this copy with a pre-5.0.0 copy of this package — or whose audit
|
|
127
|
+
* store was declared against the old name — would otherwise fork the store,
|
|
128
|
+
* which is the silent NULL-attribution failure described above.
|
|
129
|
+
*
|
|
130
|
+
* DELETE in 6.0.0, together with the adopt/mirror branches in `store()`, once
|
|
131
|
+
* no adopter pins `@12-apps/prisma` < 5.0.0. Both known consumers pin exact
|
|
132
|
+
* versions, so the check is one grep over their lockfiles.
|
|
133
|
+
*/
|
|
134
|
+
const LEGACY_ACTOR_STORE_KEY = Buffer.from('X19mdXR1cmVQYXlBY3RvclN0b3Jl', 'base64').toString();
|
|
135
|
+
|
|
136
|
+
/** The key in force, and the key the live store (if any) was created under. */
|
|
137
|
+
const storeKey: { declared: string | symbol; created?: string | symbol } = {
|
|
138
|
+
declared: DEFAULT_ACTOR_STORE_KEY,
|
|
97
139
|
};
|
|
98
140
|
|
|
99
|
-
|
|
100
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Point this package's actor context at an existing store, by naming its
|
|
143
|
+
* `globalThis` key — the same seam (same name, same rules) as
|
|
144
|
+
* `@12-apps/audit`'s `declareActorContextKey`, so a host with an in-house
|
|
145
|
+
* actor module can put all three on one store with two identical calls.
|
|
146
|
+
*
|
|
147
|
+
* Call it ONCE, at wiring time, before anything stamps or reads an actor.
|
|
148
|
+
* Changing the key after the store exists is REFUSED rather than honoured:
|
|
149
|
+
* contexts already captured against the old instance would keep flowing to it
|
|
150
|
+
* while every later read went elsewhere — the silent fork this seam exists to
|
|
151
|
+
* prevent. Passing the key already in force is a no-op, so a defensive
|
|
152
|
+
* module-scope declaration is safe to load twice.
|
|
153
|
+
*/
|
|
154
|
+
export function declareActorContextKey(key: string | symbol): void {
|
|
155
|
+
if (typeof key === 'string' && key.trim() === '') {
|
|
156
|
+
throw new Error('actor context key must not be blank.');
|
|
157
|
+
}
|
|
158
|
+
if (key === storeKey.declared) return;
|
|
159
|
+
if (storeKey.created !== undefined) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`actor context key cannot change to ${String(key)}: the store already exists under ` +
|
|
162
|
+
`${String(storeKey.created)}. Declare the key once, before anything stamps an actor — ` +
|
|
163
|
+
'moving it later forks the store, and a forked store loses every attribution ' +
|
|
164
|
+
'silently onto an append-only table.',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
storeKey.declared = key;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The key the store is (or would be) created under — diagnostics and tests. */
|
|
171
|
+
export const actorContextKey = (): string | symbol => storeKey.declared;
|
|
172
|
+
|
|
173
|
+
const store = (): AsyncLocalStorage<ActorContext> => {
|
|
174
|
+
const key = storeKey.declared;
|
|
175
|
+
let instance = globalStore[key];
|
|
176
|
+
if (instance === undefined && key === DEFAULT_ACTOR_STORE_KEY) {
|
|
177
|
+
// A pre-5.0.0 copy of this package already created the store under the
|
|
178
|
+
// old name: ADOPT it rather than fork it. One instance, two keys.
|
|
179
|
+
instance = globalStore[LEGACY_ACTOR_STORE_KEY];
|
|
180
|
+
if (instance !== undefined) globalStore[key] = instance;
|
|
181
|
+
}
|
|
182
|
+
if (instance === undefined) {
|
|
183
|
+
instance = new AsyncLocalStorage<ActorContext>();
|
|
184
|
+
globalStore[key] = instance;
|
|
185
|
+
// MIRROR under the old name so a pre-5.0.0 copy loaded after this one
|
|
186
|
+
// finds this store instead of creating its own. Only for the default key:
|
|
187
|
+
// a host that declared its own key has opted out of this package's names.
|
|
188
|
+
if (key === DEFAULT_ACTOR_STORE_KEY) globalStore[LEGACY_ACTOR_STORE_KEY] = instance;
|
|
189
|
+
}
|
|
190
|
+
storeKey.created = key;
|
|
191
|
+
return instance;
|
|
192
|
+
};
|
|
101
193
|
|
|
102
194
|
/**
|
|
103
195
|
* The REAL human behind `onBehalfOfUserId`, derived (never accepted) from the
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
// Re-export the generated client type.
|
|
27
27
|
export type { PrismaClient } from '@prisma/client';
|
|
28
28
|
|
|
29
|
+
import { Buffer } from 'node:buffer';
|
|
30
|
+
|
|
29
31
|
import type { Prisma, PrismaClient } from '@prisma/client';
|
|
30
32
|
|
|
31
33
|
import { applyAppendOnlyGuard } from './append-only-extension';
|
|
@@ -47,6 +49,9 @@ export { configureAuditStamps, auditStampConfig, type AuditStampConfig } from '.
|
|
|
47
49
|
// stamp created_by/updated_by. Re-exported here so consumers import them from
|
|
48
50
|
// the same `@12-apps/prisma` entry point as `getPrismaClient`.
|
|
49
51
|
export {
|
|
52
|
+
actorContextKey,
|
|
53
|
+
DEFAULT_ACTOR_STORE_KEY,
|
|
54
|
+
declareActorContextKey,
|
|
50
55
|
getActorAttribution,
|
|
51
56
|
getActorUserId,
|
|
52
57
|
runWithActor,
|
|
@@ -62,9 +67,52 @@ export { normalizeSearchText } from './search-normalize';
|
|
|
62
67
|
// Next dev / Turbopack hot-reload (which re-evaluates this module) never spawns
|
|
63
68
|
// a second PGlite instance against the same dataDir — PGlite holds a single
|
|
64
69
|
// exclusive connection, and a duplicate would deadlock or corrupt the store.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The `globalThis` keys the client singleton lives under, exported as named
|
|
73
|
+
* constants because they are cross-module-instance coordination surface (two
|
|
74
|
+
* copies of this package in one process share the client through them), not a
|
|
75
|
+
* private detail.
|
|
76
|
+
*/
|
|
77
|
+
export const DEFAULT_CLIENT_STORE_KEY = '__12appsPrisma';
|
|
78
|
+
export const DEFAULT_CLIENT_INIT_KEY = '__12appsPrismaInit';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The keys releases before 5.0.0 used — the host-branded names 5.0.0 renamed
|
|
82
|
+
* away, decoded from base64 at runtime so no spelling of the brand — whole or
|
|
83
|
+
* split — appears in shipped source.
|
|
84
|
+
* Still read and mirrored so a process mixing this copy with a pre-5.0.0 copy
|
|
85
|
+
* shares ONE client instead of racing two PGlite instances on one dataDir.
|
|
86
|
+
* DELETE in 6.0.0, with the fallbacks below, once no adopter pins
|
|
87
|
+
* `@12-apps/prisma` < 5.0.0 (both known consumers pin exact versions).
|
|
88
|
+
*/
|
|
89
|
+
const LEGACY_CLIENT_STORE_KEY = Buffer.from('X19mdXR1cmVQYXlQcmlzbWE=', 'base64').toString();
|
|
90
|
+
const LEGACY_CLIENT_INIT_KEY = `${LEGACY_CLIENT_STORE_KEY}Init`;
|
|
91
|
+
|
|
92
|
+
const globalStore = globalThis as unknown as Record<string, unknown>;
|
|
93
|
+
|
|
94
|
+
/** The live client, under the current key or a pre-5.0.0 copy's. */
|
|
95
|
+
const currentClient = (): PrismaClient | undefined =>
|
|
96
|
+
(globalStore[DEFAULT_CLIENT_STORE_KEY] ?? globalStore[LEGACY_CLIENT_STORE_KEY]) as
|
|
97
|
+
| PrismaClient
|
|
98
|
+
| undefined;
|
|
99
|
+
|
|
100
|
+
/** The in-flight init, under the current key or a pre-5.0.0 copy's. */
|
|
101
|
+
const currentInit = (): Promise<PrismaClient> | undefined =>
|
|
102
|
+
(globalStore[DEFAULT_CLIENT_INIT_KEY] ?? globalStore[LEGACY_CLIENT_INIT_KEY]) as
|
|
103
|
+
| Promise<PrismaClient>
|
|
104
|
+
| undefined;
|
|
105
|
+
|
|
106
|
+
/** Write (or clear) the client under BOTH keys, so either copy finds it. */
|
|
107
|
+
const storeClient = (client: PrismaClient | undefined): void => {
|
|
108
|
+
globalStore[DEFAULT_CLIENT_STORE_KEY] = client;
|
|
109
|
+
globalStore[LEGACY_CLIENT_STORE_KEY] = client;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Write (or clear) the in-flight init under BOTH keys. */
|
|
113
|
+
const storeInit = (init: Promise<PrismaClient> | undefined): void => {
|
|
114
|
+
globalStore[DEFAULT_CLIENT_INIT_KEY] = init;
|
|
115
|
+
globalStore[LEGACY_CLIENT_INIT_KEY] = init;
|
|
68
116
|
};
|
|
69
117
|
|
|
70
118
|
/**
|
|
@@ -196,20 +244,30 @@ const createPostgresClient = async (): Promise<PrismaClient> => {
|
|
|
196
244
|
* module-load failure.
|
|
197
245
|
*/
|
|
198
246
|
export const getPrismaClient = async (): Promise<PrismaClient> => {
|
|
199
|
-
|
|
200
|
-
if (
|
|
247
|
+
const existing = currentClient();
|
|
248
|
+
if (existing) {
|
|
249
|
+
// Found under either key (a pre-5.0.0 copy may have created it): make sure
|
|
250
|
+
// both names point at it before answering, so neither copy re-creates.
|
|
251
|
+
storeClient(existing);
|
|
252
|
+
return existing;
|
|
253
|
+
}
|
|
254
|
+
const inFlight = currentInit();
|
|
255
|
+
if (inFlight) {
|
|
256
|
+
storeInit(inFlight);
|
|
257
|
+
return inFlight;
|
|
258
|
+
}
|
|
201
259
|
|
|
202
260
|
const pglite = resolvePglite();
|
|
203
261
|
const init = (
|
|
204
262
|
pglite ? createPgliteClient(pglite.dataDir) : createPostgresClient()
|
|
205
263
|
)
|
|
206
264
|
.then((client) => {
|
|
207
|
-
|
|
208
|
-
|
|
265
|
+
storeClient(client);
|
|
266
|
+
storeInit(undefined);
|
|
209
267
|
return client;
|
|
210
268
|
})
|
|
211
269
|
.catch((error: unknown) => {
|
|
212
|
-
|
|
270
|
+
storeInit(undefined);
|
|
213
271
|
throw new Error(
|
|
214
272
|
`Prisma client not available (${pglite ? 'PGlite' : 'PostgreSQL'} mode). ` +
|
|
215
273
|
'Run "pnpm --filter @12-apps/prisma prisma generate" from the ' +
|
|
@@ -219,7 +277,7 @@ export const getPrismaClient = async (): Promise<PrismaClient> => {
|
|
|
219
277
|
);
|
|
220
278
|
});
|
|
221
279
|
|
|
222
|
-
|
|
280
|
+
storeInit(init);
|
|
223
281
|
return init;
|
|
224
282
|
};
|
|
225
283
|
|
|
@@ -227,14 +285,14 @@ export const getPrismaClient = async (): Promise<PrismaClient> => {
|
|
|
227
285
|
* Set a custom Prisma client instance (for testing).
|
|
228
286
|
*/
|
|
229
287
|
export const setPrismaClient = (client: PrismaClient): void => {
|
|
230
|
-
|
|
231
|
-
|
|
288
|
+
storeClient(client);
|
|
289
|
+
storeInit(undefined);
|
|
232
290
|
};
|
|
233
291
|
|
|
234
292
|
/**
|
|
235
293
|
* Reset the Prisma client instance (for testing).
|
|
236
294
|
*/
|
|
237
295
|
export const resetPrismaClient = (): void => {
|
|
238
|
-
|
|
239
|
-
|
|
296
|
+
storeClient(undefined);
|
|
297
|
+
storeInit(undefined);
|
|
240
298
|
};
|