@12-apps/prisma 2.0.0 → 4.0.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.
@@ -11,8 +11,63 @@
11
11
  */
12
12
  import type { PrismaClient } from '@prisma/client';
13
13
  /**
14
- * Wrap a client so tracked-model writes are attributed to the current actor.
14
+ * WHICH models are stamped, and why that is the HOST's to say.
15
+ *
16
+ * This was a hard-coded set of five model names from one application —
17
+ * `MenuItem`, `InventoryItem`, `ProductCategory`, `Supplier`, `Discount` — none
18
+ * of which exist in this package's own schema. `applyAuditStamps` took no
19
+ * config and `getPrismaClient` wrapped EVERY client with it, so the list was not
20
+ * an available default: it was the only behaviour on offer.
21
+ *
22
+ * That fails in both directions at once for anyone else:
23
+ *
24
+ * - **Silently inert.** A host whose models are named anything else gets no
25
+ * attribution at all. Nothing throws; `created_by` and `updated_by` simply
26
+ * stay NULL forever, on a trail whose entire purpose is saying who did it.
27
+ * - **Actively broken.** A host that HAS a `Supplier` or `Discount` — hardly
28
+ * exotic names — but without those columns gets every create/update/upsert on
29
+ * it rewritten to carry arguments its schema does not have, and Prisma
30
+ * rejects the call.
31
+ *
32
+ * The `name` → `searchName` denormalisation had the same problem and is now a
33
+ * SEPARATE list, because the two coincided only in that one application. A host
34
+ * can attribute a model without keeping a normalised search column on it, and
35
+ * the reverse.
36
+ */
37
+ export interface AuditStampConfig {
38
+ /** Models carrying `createdBy` / `updatedBy`. Empty means stamp nothing. */
39
+ trackedModels: readonly string[];
40
+ /**
41
+ * Models that also keep `searchName` in sync with `name`. Defaults to none —
42
+ * a host that wants it says so, rather than inheriting it from whichever
43
+ * models it happened to list above.
44
+ */
45
+ searchNameModels?: readonly string[];
46
+ }
47
+ /**
48
+ * Declare the models this host stamps. Call once, before the first client is
49
+ * built.
50
+ *
51
+ * Nothing is stamped until this is called. That is deliberate and it is the
52
+ * safe direction: attribution that never appears is a visibly empty column,
53
+ * whereas guessing a foreign host's model names writes to tables the package
54
+ * knows nothing about.
55
+ */
56
+ export declare function configureAuditStamps(config: AuditStampConfig): void;
57
+ /** The declared config — diagnostics and tests. */
58
+ export declare const auditStampConfig: () => AuditStampConfig;
59
+ /**
60
+ * Wrap a client so declared-model writes are attributed to the current actor.
15
61
  * Returns the client typed as {@link PrismaClient}: the extension only adds query
16
62
  * middleware (no new delegates), so every existing call site stays valid.
63
+ *
64
+ * `config` defaults to whatever {@link configureAuditStamps} declared, so the
65
+ * common path is one declaration at the composition root and untouched call
66
+ * sites. Passing it explicitly is for tests and for a host building more than
67
+ * one client with different model sets.
68
+ *
69
+ * The two model lists are read ONCE here rather than per query: a declaration
70
+ * that changed under a live client would stamp inconsistently across the same
71
+ * request, which is worse than either setting.
17
72
  */
18
- export declare function applyAuditStamps(client: PrismaClient): PrismaClient;
73
+ export declare function applyAuditStamps(client: PrismaClient, config?: AuditStampConfig): PrismaClient;
@@ -11,17 +11,36 @@
11
11
  * always wins.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.auditStampConfig = void 0;
15
+ exports.configureAuditStamps = configureAuditStamps;
14
16
  exports.applyAuditStamps = applyAuditStamps;
15
17
  const actor_context_1 = require("./actor-context");
16
18
  const search_normalize_1 = require("./search-normalize");
17
- /** Prisma model names that carry `created_by`/`updated_by` + `search_name`. */
18
- const TRACKED_MODELS = new Set([
19
- 'MenuItem',
20
- 'InventoryItem',
21
- 'ProductCategory',
22
- 'Supplier',
23
- 'Discount',
24
- ]);
19
+ /**
20
+ * The configuration in force, declared ONCE at a host's composition root.
21
+ *
22
+ * A module-level declaration rather than a parameter on `getPrismaClient()`
23
+ * because that function is called bare from hundreds of call sites in an
24
+ * adopting host, and threading config through all of them would be a migration
25
+ * out of proportion to the fix. Same shape as `declareActorContextKey` in
26
+ * `@12-apps/audit`, for the same reason.
27
+ */
28
+ const declared = { config: { trackedModels: [] } };
29
+ /**
30
+ * Declare the models this host stamps. Call once, before the first client is
31
+ * built.
32
+ *
33
+ * Nothing is stamped until this is called. That is deliberate and it is the
34
+ * safe direction: attribution that never appears is a visibly empty column,
35
+ * whereas guessing a foreign host's model names writes to tables the package
36
+ * knows nothing about.
37
+ */
38
+ function configureAuditStamps(config) {
39
+ declared.config = config;
40
+ }
41
+ /** The declared config — diagnostics and tests. */
42
+ const auditStampConfig = () => declared.config;
43
+ exports.auditStampConfig = auditStampConfig;
25
44
  /** Fill created_by + updated_by on a create payload (without clobbering overrides). */
26
45
  function stampCreate(data, userId) {
27
46
  if (data.createdBy === undefined)
@@ -46,73 +65,72 @@ function stampSearchName(data) {
46
65
  }
47
66
  }
48
67
  /**
49
- * Wrap a client so tracked-model writes are attributed to the current actor.
68
+ * Wrap a client so declared-model writes are attributed to the current actor.
50
69
  * Returns the client typed as {@link PrismaClient}: the extension only adds query
51
70
  * middleware (no new delegates), so every existing call site stays valid.
71
+ *
72
+ * `config` defaults to whatever {@link configureAuditStamps} declared, so the
73
+ * common path is one declaration at the composition root and untouched call
74
+ * sites. Passing it explicitly is for tests and for a host building more than
75
+ * one client with different model sets.
76
+ *
77
+ * The two model lists are read ONCE here rather than per query: a declaration
78
+ * that changed under a live client would stamp inconsistently across the same
79
+ * request, which is worse than either setting.
52
80
  */
53
- function applyAuditStamps(client) {
81
+ function applyAuditStamps(client, config = declared.config) {
82
+ const tracked = new Set(config.trackedModels);
83
+ const searchable = new Set(config.searchNameModels ?? []);
84
+ /** Both stamps for one payload, each gated on its OWN list. */
85
+ const stampRow = (model, data, userId, create) => {
86
+ if (searchable.has(model))
87
+ stampSearchName(data);
88
+ if (!userId || !tracked.has(model))
89
+ return;
90
+ if (create)
91
+ stampCreate(data, userId);
92
+ else
93
+ stampUpdate(data, userId);
94
+ };
95
+ /** Whether this model is in either list — the cheap early-out. */
96
+ const touched = (model) => tracked.has(model) || searchable.has(model);
54
97
  const extended = client.$extends({
55
98
  name: 'auditStamps',
56
99
  query: {
57
100
  $allModels: {
58
101
  create({ model, args, query }) {
59
- if (TRACKED_MODELS.has(model) && args.data) {
60
- const data = args.data;
61
- stampSearchName(data);
62
- const userId = (0, actor_context_1.getActorUserId)();
63
- if (userId)
64
- stampCreate(data, userId);
102
+ if (touched(model) && args.data) {
103
+ stampRow(model, args.data, (0, actor_context_1.getActorUserId)(), true);
65
104
  }
66
105
  return query(args);
67
106
  },
68
107
  createMany({ model, args, query }) {
69
- if (TRACKED_MODELS.has(model) && args.data) {
108
+ if (touched(model) && args.data) {
70
109
  const userId = (0, actor_context_1.getActorUserId)();
71
110
  const rows = Array.isArray(args.data) ? args.data : [args.data];
72
- rows.forEach((row) => {
73
- const data = row;
74
- stampSearchName(data);
75
- if (userId)
76
- stampCreate(data, userId);
77
- });
111
+ rows.forEach((row) => stampRow(model, row, userId, true));
78
112
  }
79
113
  return query(args);
80
114
  },
81
115
  update({ model, args, query }) {
82
- if (TRACKED_MODELS.has(model) && args.data) {
83
- const data = args.data;
84
- stampSearchName(data);
85
- const userId = (0, actor_context_1.getActorUserId)();
86
- if (userId)
87
- stampUpdate(data, userId);
116
+ if (touched(model) && args.data) {
117
+ stampRow(model, args.data, (0, actor_context_1.getActorUserId)(), false);
88
118
  }
89
119
  return query(args);
90
120
  },
91
121
  updateMany({ model, args, query }) {
92
- if (TRACKED_MODELS.has(model) && args.data) {
93
- const data = args.data;
94
- stampSearchName(data);
95
- const userId = (0, actor_context_1.getActorUserId)();
96
- if (userId)
97
- stampUpdate(data, userId);
122
+ if (touched(model) && args.data) {
123
+ stampRow(model, args.data, (0, actor_context_1.getActorUserId)(), false);
98
124
  }
99
125
  return query(args);
100
126
  },
101
127
  upsert({ model, args, query }) {
102
- if (TRACKED_MODELS.has(model)) {
128
+ if (touched(model)) {
103
129
  const userId = (0, actor_context_1.getActorUserId)();
104
- if (args.create) {
105
- const data = args.create;
106
- stampSearchName(data);
107
- if (userId)
108
- stampCreate(data, userId);
109
- }
110
- if (args.update) {
111
- const data = args.update;
112
- stampSearchName(data);
113
- if (userId)
114
- stampUpdate(data, userId);
115
- }
130
+ if (args.create)
131
+ stampRow(model, args.create, userId, true);
132
+ if (args.update)
133
+ stampRow(model, args.update, userId, false);
116
134
  }
117
135
  return query(args);
118
136
  },
package/dist/index.d.ts CHANGED
@@ -25,6 +25,13 @@
25
25
  export type { PrismaClient } from '@prisma/client';
26
26
  import type { PrismaClient } from '@prisma/client';
27
27
  export { AppendOnlyViolationError } from './append-only-extension';
28
+ /**
29
+ * Declare which models carry attribution, and which keep a normalised search
30
+ * column. Call once at the composition root, before the first client is built —
31
+ * nothing is stamped until you do. See `audit-extension.ts` for why this is the
32
+ * host's to say rather than a list this package guesses.
33
+ */
34
+ export { configureAuditStamps, auditStampConfig, type AuditStampConfig } from './audit-extension';
28
35
  export { getActorAttribution, getActorUserId, runWithActor, runWithActorScope, setActor, type ActorAttribution, type ActorAttributionSnapshot, type ActorContext, } from './actor-context';
29
36
  export { normalizeSearchText } from './search-normalize';
30
37
  /**
package/dist/index.js CHANGED
@@ -57,13 +57,22 @@ 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.AppendOnlyViolationError = void 0;
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;
61
61
  const append_only_extension_1 = require("./append-only-extension");
62
62
  const audit_extension_1 = require("./audit-extension");
63
63
  // Append-only guard for the audit log (FUT-209): mutating the AuditLog model
64
64
  // throws. Re-exported so tests can assert on the error type.
65
65
  var append_only_extension_2 = require("./append-only-extension");
66
66
  Object.defineProperty(exports, "AppendOnlyViolationError", { enumerable: true, get: function () { return append_only_extension_2.AppendOnlyViolationError; } });
67
+ /**
68
+ * Declare which models carry attribution, and which keep a normalised search
69
+ * column. Call once at the composition root, before the first client is built —
70
+ * nothing is stamped until you do. See `audit-extension.ts` for why this is the
71
+ * host's to say rather than a list this package guesses.
72
+ */
73
+ var audit_extension_2 = require("./audit-extension");
74
+ Object.defineProperty(exports, "configureAuditStamps", { enumerable: true, get: function () { return audit_extension_2.configureAuditStamps; } });
75
+ Object.defineProperty(exports, "auditStampConfig", { enumerable: true, get: function () { return audit_extension_2.auditStampConfig; } });
67
76
  // Change-attribution context helpers (FUT-168): the auth layer calls `setActor`
68
77
  // once a request is authorized; the audit extension applied below reads it to
69
78
  // stamp created_by/updated_by. Re-exported here so consumers import them from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/prisma",
3
- "version": "2.0.0",
3
+ "version": "4.0.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": "^2.0.0",
62
+ "@12-apps/audit": "^3.0.0",
63
63
  "@12-apps/entitlements": "^2.0.0",
64
- "@12-apps/entity-lifecycle": "^2.1.0",
64
+ "@12-apps/entity-lifecycle": "^3.0.0",
65
65
  "@12-apps/eslint-config": "^1.20.0",
66
66
  "@12-apps/jobs": "^3.0.0",
67
- "@12-apps/mcp": "^1.20.0",
68
- "@12-apps/notifications": "^1.0.0",
67
+ "@12-apps/mcp": "^2.0.0",
68
+ "@12-apps/notifications": "^2.0.0",
69
69
  "@12-apps/onboarding": "^1.20.0",
70
- "@12-apps/payments-backend": "^2.1.0",
70
+ "@12-apps/payments-backend": "^3.0.0",
71
71
  "@12-apps/product-research": "^2.0.0",
72
72
  "@12-apps/rbac": "^3.0.0",
73
73
  "@12-apps/realtime": "^1.19.0",
@@ -15,14 +15,67 @@ import type { PrismaClient } from '@prisma/client';
15
15
  import { getActorUserId } from './actor-context';
16
16
  import { normalizeSearchText } from './search-normalize';
17
17
 
18
- /** Prisma model names that carry `created_by`/`updated_by` + `search_name`. */
19
- const TRACKED_MODELS = new Set([
20
- 'MenuItem',
21
- 'InventoryItem',
22
- 'ProductCategory',
23
- 'Supplier',
24
- 'Discount',
25
- ]);
18
+ /**
19
+ * WHICH models are stamped, and why that is the HOST's to say.
20
+ *
21
+ * This was a hard-coded set of five model names from one application —
22
+ * `MenuItem`, `InventoryItem`, `ProductCategory`, `Supplier`, `Discount` — none
23
+ * of which exist in this package's own schema. `applyAuditStamps` took no
24
+ * config and `getPrismaClient` wrapped EVERY client with it, so the list was not
25
+ * an available default: it was the only behaviour on offer.
26
+ *
27
+ * That fails in both directions at once for anyone else:
28
+ *
29
+ * - **Silently inert.** A host whose models are named anything else gets no
30
+ * attribution at all. Nothing throws; `created_by` and `updated_by` simply
31
+ * stay NULL forever, on a trail whose entire purpose is saying who did it.
32
+ * - **Actively broken.** A host that HAS a `Supplier` or `Discount` — hardly
33
+ * exotic names — but without those columns gets every create/update/upsert on
34
+ * it rewritten to carry arguments its schema does not have, and Prisma
35
+ * rejects the call.
36
+ *
37
+ * The `name` → `searchName` denormalisation had the same problem and is now a
38
+ * SEPARATE list, because the two coincided only in that one application. A host
39
+ * can attribute a model without keeping a normalised search column on it, and
40
+ * the reverse.
41
+ */
42
+ export interface AuditStampConfig {
43
+ /** Models carrying `createdBy` / `updatedBy`. Empty means stamp nothing. */
44
+ trackedModels: readonly string[];
45
+ /**
46
+ * Models that also keep `searchName` in sync with `name`. Defaults to none —
47
+ * a host that wants it says so, rather than inheriting it from whichever
48
+ * models it happened to list above.
49
+ */
50
+ searchNameModels?: readonly string[];
51
+ }
52
+
53
+ /**
54
+ * The configuration in force, declared ONCE at a host's composition root.
55
+ *
56
+ * A module-level declaration rather than a parameter on `getPrismaClient()`
57
+ * because that function is called bare from hundreds of call sites in an
58
+ * adopting host, and threading config through all of them would be a migration
59
+ * out of proportion to the fix. Same shape as `declareActorContextKey` in
60
+ * `@12-apps/audit`, for the same reason.
61
+ */
62
+ const declared: { config: AuditStampConfig } = { config: { trackedModels: [] } };
63
+
64
+ /**
65
+ * Declare the models this host stamps. Call once, before the first client is
66
+ * built.
67
+ *
68
+ * Nothing is stamped until this is called. That is deliberate and it is the
69
+ * safe direction: attribution that never appears is a visibly empty column,
70
+ * whereas guessing a foreign host's model names writes to tables the package
71
+ * knows nothing about.
72
+ */
73
+ export function configureAuditStamps(config: AuditStampConfig): void {
74
+ declared.config = config;
75
+ }
76
+
77
+ /** The declared config — diagnostics and tests. */
78
+ export const auditStampConfig = (): AuditStampConfig => declared.config;
26
79
 
27
80
  type MutableData = Record<string, unknown>;
28
81
 
@@ -50,67 +103,72 @@ function stampSearchName(data: MutableData): void {
50
103
  }
51
104
 
52
105
  /**
53
- * Wrap a client so tracked-model writes are attributed to the current actor.
106
+ * Wrap a client so declared-model writes are attributed to the current actor.
54
107
  * Returns the client typed as {@link PrismaClient}: the extension only adds query
55
108
  * middleware (no new delegates), so every existing call site stays valid.
109
+ *
110
+ * `config` defaults to whatever {@link configureAuditStamps} declared, so the
111
+ * common path is one declaration at the composition root and untouched call
112
+ * sites. Passing it explicitly is for tests and for a host building more than
113
+ * one client with different model sets.
114
+ *
115
+ * The two model lists are read ONCE here rather than per query: a declaration
116
+ * that changed under a live client would stamp inconsistently across the same
117
+ * request, which is worse than either setting.
56
118
  */
57
- export function applyAuditStamps(client: PrismaClient): PrismaClient {
119
+ export function applyAuditStamps(
120
+ client: PrismaClient,
121
+ config: AuditStampConfig = declared.config,
122
+ ): PrismaClient {
123
+ const tracked = new Set(config.trackedModels);
124
+ const searchable = new Set(config.searchNameModels ?? []);
125
+
126
+ /** Both stamps for one payload, each gated on its OWN list. */
127
+ const stampRow = (model: string, data: MutableData, userId: string | undefined, create: boolean) => {
128
+ if (searchable.has(model)) stampSearchName(data);
129
+ if (!userId || !tracked.has(model)) return;
130
+ if (create) stampCreate(data, userId);
131
+ else stampUpdate(data, userId);
132
+ };
133
+
134
+ /** Whether this model is in either list — the cheap early-out. */
135
+ const touched = (model: string) => tracked.has(model) || searchable.has(model);
136
+
58
137
  const extended = client.$extends({
59
138
  name: 'auditStamps',
60
139
  query: {
61
140
  $allModels: {
62
141
  create({ model, args, query }) {
63
- if (TRACKED_MODELS.has(model) && args.data) {
64
- const data = args.data as MutableData;
65
- stampSearchName(data);
66
- const userId = getActorUserId();
67
- if (userId) stampCreate(data, userId);
142
+ if (touched(model) && args.data) {
143
+ stampRow(model, args.data as MutableData, getActorUserId(), true);
68
144
  }
69
145
  return query(args);
70
146
  },
71
147
  createMany({ model, args, query }) {
72
- if (TRACKED_MODELS.has(model) && args.data) {
148
+ if (touched(model) && args.data) {
73
149
  const userId = getActorUserId();
74
150
  const rows = Array.isArray(args.data) ? args.data : [args.data];
75
- rows.forEach((row) => {
76
- const data = row as MutableData;
77
- stampSearchName(data);
78
- if (userId) stampCreate(data, userId);
79
- });
151
+ rows.forEach((row) => stampRow(model, row as MutableData, userId, true));
80
152
  }
81
153
  return query(args);
82
154
  },
83
155
  update({ model, args, query }) {
84
- if (TRACKED_MODELS.has(model) && args.data) {
85
- const data = args.data as MutableData;
86
- stampSearchName(data);
87
- const userId = getActorUserId();
88
- if (userId) stampUpdate(data, userId);
156
+ if (touched(model) && args.data) {
157
+ stampRow(model, args.data as MutableData, getActorUserId(), false);
89
158
  }
90
159
  return query(args);
91
160
  },
92
161
  updateMany({ model, args, query }) {
93
- if (TRACKED_MODELS.has(model) && args.data) {
94
- const data = args.data as MutableData;
95
- stampSearchName(data);
96
- const userId = getActorUserId();
97
- if (userId) stampUpdate(data, userId);
162
+ if (touched(model) && args.data) {
163
+ stampRow(model, args.data as MutableData, getActorUserId(), false);
98
164
  }
99
165
  return query(args);
100
166
  },
101
167
  upsert({ model, args, query }) {
102
- if (TRACKED_MODELS.has(model)) {
168
+ if (touched(model)) {
103
169
  const userId = getActorUserId();
104
- if (args.create) {
105
- const data = args.create as MutableData;
106
- stampSearchName(data);
107
- if (userId) stampCreate(data, userId);
108
- }
109
- if (args.update) {
110
- const data = args.update as MutableData;
111
- stampSearchName(data);
112
- if (userId) stampUpdate(data, userId);
113
- }
170
+ if (args.create) stampRow(model, args.create as MutableData, userId, true);
171
+ if (args.update) stampRow(model, args.update as MutableData, userId, false);
114
172
  }
115
173
  return query(args);
116
174
  },
package/src/index.ts CHANGED
@@ -34,6 +34,13 @@ import { applyAuditStamps } from './audit-extension';
34
34
  // Append-only guard for the audit log (FUT-209): mutating the AuditLog model
35
35
  // throws. Re-exported so tests can assert on the error type.
36
36
  export { AppendOnlyViolationError } from './append-only-extension';
37
+ /**
38
+ * Declare which models carry attribution, and which keep a normalised search
39
+ * column. Call once at the composition root, before the first client is built —
40
+ * nothing is stamped until you do. See `audit-extension.ts` for why this is the
41
+ * host's to say rather than a list this package guesses.
42
+ */
43
+ export { configureAuditStamps, auditStampConfig, type AuditStampConfig } from './audit-extension';
37
44
 
38
45
  // Change-attribution context helpers (FUT-168): the auth layer calls `setActor`
39
46
  // once a request is authorized; the audit extension applied below reads it to