@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.
- package/dist/audit-extension.d.ts +57 -2
- package/dist/audit-extension.js +66 -48
- package/dist/index.d.ts +7 -0
- package/dist/index.js +10 -1
- package/package.json +6 -6
- package/src/audit-extension.ts +100 -42
- package/src/index.ts +7 -0
|
@@ -11,8 +11,63 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { PrismaClient } from '@prisma/client';
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
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;
|
package/dist/audit-extension.js
CHANGED
|
@@ -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
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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 (
|
|
60
|
-
|
|
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 (
|
|
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 (
|
|
83
|
-
|
|
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 (
|
|
93
|
-
|
|
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 (
|
|
128
|
+
if (touched(model)) {
|
|
103
129
|
const userId = (0, actor_context_1.getActorUserId)();
|
|
104
|
-
if (args.create)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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": "
|
|
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": "^
|
|
62
|
+
"@12-apps/audit": "^3.0.0",
|
|
63
63
|
"@12-apps/entitlements": "^2.0.0",
|
|
64
|
-
"@12-apps/entity-lifecycle": "^
|
|
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": "^
|
|
68
|
-
"@12-apps/notifications": "^
|
|
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": "^
|
|
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",
|
package/src/audit-extension.ts
CHANGED
|
@@ -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
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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(
|
|
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 (
|
|
64
|
-
|
|
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 (
|
|
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 (
|
|
85
|
-
|
|
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 (
|
|
94
|
-
|
|
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 (
|
|
168
|
+
if (touched(model)) {
|
|
103
169
|
const userId = getActorUserId();
|
|
104
|
-
if (args.create)
|
|
105
|
-
|
|
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
|