@willyim/drizzle-audit 0.3.0 → 0.5.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/README.md +102 -21
- package/dist/src/d1/audit-log-schema.d.ts +3 -2
- package/dist/src/d1/audit-log-schema.d.ts.map +1 -1
- package/dist/src/d1/audit-log-schema.js +14 -4
- package/dist/src/d1/index.d.ts +2 -1
- package/dist/src/d1/index.d.ts.map +1 -1
- package/dist/src/d1/runtime.d.ts +11 -11
- package/dist/src/d1/runtime.d.ts.map +1 -1
- package/dist/src/d1/runtime.js +26 -9
- package/dist/src/d1/sql.d.ts +2 -2
- package/dist/src/d1/sql.d.ts.map +1 -1
- package/dist/src/d1/sql.js +61 -29
- package/dist/src/d1/types.d.ts +10 -2
- package/dist/src/d1/types.d.ts.map +1 -1
- package/dist/src/d1-runtime/with-audit.d.ts +15 -11
- package/dist/src/d1-runtime/with-audit.d.ts.map +1 -1
- package/dist/src/d1-runtime/with-audit.js +53 -60
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/postgres/audit-log-schema.d.ts +7 -14
- package/dist/src/postgres/audit-log-schema.d.ts.map +1 -1
- package/dist/src/postgres/audit-log-schema.js +14 -4
- package/dist/src/postgres/index.d.ts +3 -2
- package/dist/src/postgres/index.d.ts.map +1 -1
- package/dist/src/postgres/index.js +1 -1
- package/dist/src/postgres/runtime.d.ts +6 -8
- package/dist/src/postgres/runtime.d.ts.map +1 -1
- package/dist/src/postgres/runtime.js +15 -3
- package/dist/src/postgres/sql.d.ts +10 -7
- package/dist/src/postgres/sql.d.ts.map +1 -1
- package/dist/src/postgres/sql.js +72 -50
- package/dist/src/postgres/types.d.ts +10 -2
- package/dist/src/postgres/types.d.ts.map +1 -1
- package/dist/test/d1-async.integration.test.d.ts +13 -0
- package/dist/test/d1-async.integration.test.d.ts.map +1 -0
- package/dist/test/d1-async.integration.test.js +159 -0
- package/dist/test/d1.integration.test.js +71 -4
- package/dist/test/sqlite.integration.test.d.ts +2 -0
- package/dist/test/sqlite.integration.test.d.ts.map +1 -0
- package/dist/test/{d1-runtime.integration.test.js → sqlite.integration.test.js} +82 -25
- package/package.json +2 -1
- package/dist/test/d1-runtime.integration.test.d.ts +0 -2
- package/dist/test/d1-runtime.integration.test.d.ts.map +0 -1
- package/dist/test/postgres.integration.test.d.ts +0 -2
- package/dist/test/postgres.integration.test.d.ts.map +0 -1
- package/dist/test/postgres.integration.test.js +0 -286
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ await withAuditedTransaction(db, currentUser.id, async (tx) => {
|
|
|
43
43
|
|
|
44
44
|
Postgres triggers capture full row snapshots (`old_data`/`new_data` as JSONB) automatically.
|
|
45
45
|
|
|
46
|
-
### D1/SQLite — App-Level Wrapper
|
|
46
|
+
### D1/SQLite — App-Level Wrapper
|
|
47
47
|
|
|
48
48
|
For D1 and SQLite, the `withAudit` wrapper is the simplest approach. No triggers, no context tables — it intercepts operations in your JS code where you already have the user session.
|
|
49
49
|
|
|
@@ -69,15 +69,17 @@ export const auditLogs = d1AuditLogTable()
|
|
|
69
69
|
// 3. Use in your app (e.g. React Router action)
|
|
70
70
|
const audit = withAudit(db, auditLogs, { userId: session.userId })
|
|
71
71
|
|
|
72
|
-
audit.insert(users, { id: "u1", name: "Ada" })
|
|
73
|
-
audit.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" })
|
|
74
|
-
audit.delete(users, eq(users.id, "u1"))
|
|
72
|
+
await audit.insert(users, { id: "u1", name: "Ada" })
|
|
73
|
+
await audit.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" })
|
|
74
|
+
await audit.delete(users, eq(users.id, "u1"))
|
|
75
75
|
|
|
76
76
|
// Non-audited access is still available
|
|
77
77
|
audit.db.select().from(users).all()
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
The wrapper auto-detects primary keys from your Drizzle table schema
|
|
80
|
+
The wrapper auto-detects primary keys from your Drizzle table schema and captures old/new row data. All methods return Promises and must be awaited.
|
|
81
|
+
|
|
82
|
+
> **Note:** Each operation runs the data write and audit log insert as sequential statements with no transaction wrapper. This is required for D1 compatibility (D1 does not support `BEGIN`/`COMMIT` over the prepared-statement API). In the unlikely event of a failure between the two writes, one may succeed without the other. If you need atomic auditing, use the trigger-based approach instead (see below).
|
|
81
83
|
|
|
82
84
|
### D1/SQLite — Triggers
|
|
83
85
|
|
|
@@ -118,47 +120,62 @@ sqlite.exec(createAttachD1AuditTriggersSqlWithColumns([
|
|
|
118
120
|
]))
|
|
119
121
|
```
|
|
120
122
|
|
|
121
|
-
##
|
|
123
|
+
## Context Columns
|
|
124
|
+
|
|
125
|
+
Beyond `user_id`, you can declare arbitrary extra context columns (e.g.
|
|
126
|
+
`workspace_id`, `tenant_id`, `request_id`). Each is an extra `TEXT` column on
|
|
127
|
+
`audit_logs`, populated at write-time from a named runtime context value (a
|
|
128
|
+
Postgres session GUC, or a `_audit_context` KV row in D1).
|
|
122
129
|
|
|
123
|
-
|
|
130
|
+
```ts
|
|
131
|
+
type AuditContextColumn = {
|
|
132
|
+
column: string // audit table column (TEXT, nullable)
|
|
133
|
+
sessionKey?: string // Postgres GUC the trigger reads — default `app.${column}`
|
|
134
|
+
// D1: the _audit_context key — default `${column}`
|
|
135
|
+
index?: boolean // create an index on the column — default true
|
|
136
|
+
}
|
|
137
|
+
```
|
|
124
138
|
|
|
125
139
|
### Postgres
|
|
126
140
|
|
|
127
141
|
```ts
|
|
128
|
-
|
|
129
|
-
createAuditInstallSql({ workspaceIdColumn: "workspace_id" })
|
|
130
|
-
export const auditLogs = pgAuditLogTable({ workspaceIdColumn: "workspace_id" })
|
|
142
|
+
const contextColumns = [{ column: "workspace_id" }, { column: "tenant_id" }]
|
|
131
143
|
|
|
132
|
-
|
|
144
|
+
createAuditInstallSql({ contextColumns })
|
|
145
|
+
export const auditLogs = pgAuditLogTable({ contextColumns })
|
|
146
|
+
|
|
147
|
+
// Pass context at runtime (GUC name → value)
|
|
133
148
|
await withAuditedTransaction(
|
|
134
149
|
db, userId, async (tx) => { /* ... */ },
|
|
135
150
|
"app.user_id",
|
|
136
|
-
{
|
|
151
|
+
{ context: { "app.workspace_id": "ws_1", "app.tenant_id": "t_1" } },
|
|
137
152
|
)
|
|
138
153
|
```
|
|
139
154
|
|
|
140
|
-
To add
|
|
155
|
+
To add context columns to an existing install, use
|
|
156
|
+
`createAuditAddContextColumnsSql({ contextColumns })` — it adds each column and
|
|
157
|
+
regenerates the trigger over the full set. Pass the complete list of columns the
|
|
158
|
+
table should have.
|
|
141
159
|
|
|
142
160
|
### D1 Runtime
|
|
143
161
|
|
|
144
162
|
```ts
|
|
145
163
|
const audit = withAudit(db, auditLogs, {
|
|
146
164
|
userId: "user_1",
|
|
147
|
-
|
|
165
|
+
context: { workspace_id: "ws_1", tenant_id: "t_1" },
|
|
148
166
|
})
|
|
149
167
|
```
|
|
150
168
|
|
|
151
169
|
### D1 Triggers
|
|
152
170
|
|
|
153
171
|
```ts
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
172
|
+
const contextColumns = [{ column: "workspace_id" }, { column: "tenant_id" }]
|
|
173
|
+
|
|
174
|
+
createD1AuditInstallSql({ contextColumns })
|
|
175
|
+
createAttachD1AuditTriggersSql([{ table: "users" }], { contextColumns })
|
|
159
176
|
|
|
160
177
|
withD1AuditedTransaction(db, "user_1", (tx) => { /* ... */ }, {
|
|
161
|
-
|
|
178
|
+
context: { workspace_id: "ws_1", tenant_id: "t_1" },
|
|
162
179
|
})
|
|
163
180
|
```
|
|
164
181
|
|
|
@@ -200,10 +217,16 @@ export function createAuditSql() {
|
|
|
200
217
|
| `createAuditInstallSql(options?)` | SQL to create the audit table, indexes, and trigger function |
|
|
201
218
|
| `createAttachAuditTriggerSql(target, options?)` | SQL to attach audit trigger to one table |
|
|
202
219
|
| `createAttachAuditTriggersSql(targets, options?)` | Same, for multiple tables |
|
|
203
|
-
| `
|
|
220
|
+
| `createAuditAddContextColumnsSql(options)` | SQL to add context columns + regenerate trigger on existing install |
|
|
204
221
|
| `setAuditContext(db, actorId, contextKey?, options?)` | Set actor context in current transaction |
|
|
205
222
|
| `withAuditedTransaction(db, actorId, callback, contextKey?, options?)` | Transaction wrapper with actor context |
|
|
206
223
|
|
|
224
|
+
### `@willyim/drizzle-audit`
|
|
225
|
+
|
|
226
|
+
| Export | Description |
|
|
227
|
+
|---|---|
|
|
228
|
+
| `computeDiff(operation, oldData, newData, options?)` | Compute field-by-field diff from old/new row data |
|
|
229
|
+
|
|
207
230
|
### `@willyim/drizzle-audit/d1`
|
|
208
231
|
|
|
209
232
|
| Export | Description |
|
|
@@ -231,6 +254,43 @@ export function createAuditSql() {
|
|
|
231
254
|
- `.delete(table, where)` — Fetch old rows, delete, audit log (per row)
|
|
232
255
|
- `.db` — Raw Drizzle instance for non-audited operations
|
|
233
256
|
|
|
257
|
+
## Computing Diffs
|
|
258
|
+
|
|
259
|
+
The `computeDiff` utility produces field-by-field diffs from the `old_data`/`new_data` captured by audit triggers. It works with any operation type and requires no Drizzle dependency.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
import { computeDiff } from "@willyim/drizzle-audit"
|
|
263
|
+
|
|
264
|
+
const result = computeDiff(
|
|
265
|
+
"UPDATE",
|
|
266
|
+
{ id: "u1", name: "Ada", email: "ada@example.com" },
|
|
267
|
+
{ id: "u1", name: "Ada Lovelace", email: "ada@example.com" },
|
|
268
|
+
)
|
|
269
|
+
// {
|
|
270
|
+
// operation: "UPDATE",
|
|
271
|
+
// changes: [{ field: "name", old: "Ada", new: "Ada Lovelace" }]
|
|
272
|
+
// }
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
For INSERT operations, pass `null` as `oldData` — all fields appear as additions. For DELETE, pass `null` as `newData` — all fields appear as removals.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
// INSERT — every field is new
|
|
279
|
+
computeDiff("INSERT", null, { id: "u1", name: "Ada" })
|
|
280
|
+
|
|
281
|
+
// DELETE — every field is removed
|
|
282
|
+
computeDiff("DELETE", { id: "u1", name: "Ada" }, null)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
By default, `updated_at` and `created_at` fields are excluded. Override with `ignoreFields`:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
computeDiff("UPDATE", oldData, newData, { ignoreFields: [] }) // include all fields
|
|
289
|
+
computeDiff("UPDATE", oldData, newData, { ignoreFields: ["internal_note"] })
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Nested objects are compared using deep equality. Fields are returned sorted alphabetically.
|
|
293
|
+
|
|
234
294
|
## Audit Log Schema
|
|
235
295
|
|
|
236
296
|
### Postgres
|
|
@@ -273,6 +333,27 @@ CREATE TABLE audit_logs (
|
|
|
273
333
|
| **Bypass risk** | Low (DB-level) | Medium (must use wrapper) | Low (DB-level) |
|
|
274
334
|
| **Best for** | Postgres apps | D1/Cloudflare Workers | SQLite apps needing DB-level guarantees |
|
|
275
335
|
|
|
336
|
+
## Migrating from 0.2.x → 0.3.0
|
|
337
|
+
|
|
338
|
+
The `workspace_id`-specific options were removed in favor of the generic
|
|
339
|
+
[Context Columns](#context-columns) API. Mechanical replacements:
|
|
340
|
+
|
|
341
|
+
| Removed | Replacement |
|
|
342
|
+
|---|---|
|
|
343
|
+
| `pgAuditLogTable({ workspaceIdColumn: "workspace_id" })` | `pgAuditLogTable({ contextColumns: [{ column: "workspace_id" }] })` |
|
|
344
|
+
| `d1AuditLogTable({ workspaceIdColumn: "workspace_id" })` | `d1AuditLogTable({ contextColumns: [{ column: "workspace_id" }] })` |
|
|
345
|
+
| `createAuditInstallSql({ workspaceIdColumn: "workspace_id" })` | `createAuditInstallSql({ contextColumns: [{ column: "workspace_id" }] })` |
|
|
346
|
+
| `createD1AuditInstallSql({ workspaceIdColumn })` | `createD1AuditInstallSql({ contextColumns: [{ column }] })` |
|
|
347
|
+
| `createAttachD1AuditTriggersSql(targets, { workspaceIdColumn })` | `createAttachD1AuditTriggersSql(targets, { contextColumns: [{ column }] })` |
|
|
348
|
+
| `createAuditAddWorkspaceColumnSql(options)` | `createAuditAddContextColumnsSql({ contextColumns: [...] })` |
|
|
349
|
+
| Postgres runtime `{ workspaceId: v }` | `{ context: { "app.workspace_id": v } }` |
|
|
350
|
+
| Postgres runtime `{ workspaceId: v, workspaceContextKey: "app.tenant_id" }` | `{ context: { "app.tenant_id": v } }` |
|
|
351
|
+
| D1 runtime `{ workspaceId: v }` | `{ context: { workspace_id: v } }` |
|
|
352
|
+
| `withAudit(db, table, { userId, workspaceId: v })` | `withAudit(db, table, { userId, context: { workspace_id: v } })` |
|
|
353
|
+
|
|
354
|
+
The on-disk column name (`workspace_id`) is unchanged, so no data migration is
|
|
355
|
+
needed — only call sites change.
|
|
356
|
+
|
|
276
357
|
## License
|
|
277
358
|
|
|
278
359
|
MIT
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { AuditContextColumn } from "./types.js";
|
|
1
2
|
export type D1AuditLogTableOptions = {
|
|
2
|
-
/**
|
|
3
|
-
|
|
3
|
+
/** Extra context columns to include in the table definition, matching the install. */
|
|
4
|
+
contextColumns?: AuditContextColumn[];
|
|
4
5
|
};
|
|
5
6
|
export declare function d1AuditLogTable(options?: D1AuditLogTableOptions): import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
6
7
|
name: "audit_logs";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-log-schema.d.ts","sourceRoot":"","sources":["../../../src/d1/audit-log-schema.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,sBAAsB,GAAG;IACnC,
|
|
1
|
+
{"version":3,"file":"audit-log-schema.d.ts","sourceRoot":"","sources":["../../../src/d1/audit-log-schema.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAEpD,MAAM,MAAM,sBAAsB,GAAG;IACnC,sFAAsF;IACtF,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAA;CACtC,CAAA;AAiBD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuB/D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAMtE"}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { index, integer, sqliteTable, text, } from "drizzle-orm/sqlite-core";
|
|
2
|
+
function resolveColumns(options) {
|
|
3
|
+
const columns = [];
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
for (const entry of options?.contextColumns ?? []) {
|
|
6
|
+
const column = entry.column?.trim();
|
|
7
|
+
if (column && !seen.has(column)) {
|
|
8
|
+
seen.add(column);
|
|
9
|
+
columns.push(column);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return columns;
|
|
13
|
+
}
|
|
2
14
|
export function d1AuditLogTable(options) {
|
|
3
|
-
const
|
|
15
|
+
const contextColumns = resolveColumns(options);
|
|
4
16
|
const columns = {
|
|
5
17
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
6
18
|
table_name: text("table_name").notNull(),
|
|
7
19
|
operation: text("operation").notNull(),
|
|
8
20
|
row_id: text("row_id"),
|
|
9
21
|
user_id: text("user_id"),
|
|
10
|
-
...(
|
|
11
|
-
? { [workspaceIdColumn]: text(workspaceIdColumn) }
|
|
12
|
-
: {}),
|
|
22
|
+
...Object.fromEntries(contextColumns.map((c) => [c, text(c)])),
|
|
13
23
|
old_data: text("old_data"),
|
|
14
24
|
new_data: text("new_data"),
|
|
15
25
|
created_at: text("created_at").notNull().default("(datetime('now'))"),
|
package/dist/src/d1/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export type { D1AuditLogTableOptions } from "./audit-log-schema.js";
|
|
|
3
3
|
export { createAttachD1AuditTriggerSql, createAttachD1AuditTriggersSql, createAttachD1AuditTriggerSqlWithColumns, createAttachD1AuditTriggersSqlWithColumns, createD1AuditInstallSql, } from "./sql.js";
|
|
4
4
|
export type { D1AuditTriggerTargetWithColumns } from "./sql.js";
|
|
5
5
|
export { clearD1AuditContext, setD1AuditContext, withD1AuditedTransaction, } from "./runtime.js";
|
|
6
|
-
export type {
|
|
6
|
+
export type { D1AuditContextOptions } from "./runtime.js";
|
|
7
|
+
export type { AuditContextColumn, D1AuditInstallOptions, D1AuditSqlExecutor, D1AuditTriggerTarget, } from "./types.js";
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/d1/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC5E,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,EAC9B,wCAAwC,EACxC,yCAAyC,EACzC,uBAAuB,GACxB,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,+BAA+B,EAAE,MAAM,UAAU,CAAA;AAC/D,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/d1/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC5E,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,EAC9B,wCAAwC,EACxC,yCAAyC,EACzC,uBAAuB,GACxB,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,+BAA+B,EAAE,MAAM,UAAU,CAAA;AAC/D,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAEzD,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,YAAY,CAAA"}
|
package/dist/src/d1/runtime.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { D1AuditSqlExecutor } from "./types.js";
|
|
2
|
+
export type D1AuditContextOptions = {
|
|
3
|
+
/** Map of context key → value written as KV rows the triggers read. */
|
|
4
|
+
context?: Record<string, string>;
|
|
5
|
+
contextTable?: string;
|
|
6
|
+
};
|
|
2
7
|
/**
|
|
3
8
|
* Sets the audit context for the current transaction by writing to the
|
|
4
9
|
* _audit_context table. Must be called inside a transaction before any
|
|
@@ -6,17 +11,15 @@ import type { D1AuditSqlExecutor } from "./types.js";
|
|
|
6
11
|
*
|
|
7
12
|
* D1/SQLite has no session variables, so triggers read context from this table.
|
|
8
13
|
*/
|
|
9
|
-
export declare function setD1AuditContext(db: D1AuditSqlExecutor, actorId: string, options?:
|
|
10
|
-
workspaceId?: string;
|
|
11
|
-
contextTable?: string;
|
|
12
|
-
}): Promise<unknown> | undefined;
|
|
14
|
+
export declare function setD1AuditContext(db: D1AuditSqlExecutor, actorId: string, options?: D1AuditContextOptions): Promise<unknown> | undefined;
|
|
13
15
|
/**
|
|
14
16
|
* Clears the audit context after a transaction completes.
|
|
15
17
|
* Called automatically by withD1AuditedTransaction.
|
|
18
|
+
*
|
|
19
|
+
* Clears `user_id` plus any keys set via `context` so the next transaction
|
|
20
|
+
* starts clean.
|
|
16
21
|
*/
|
|
17
|
-
export declare function clearD1AuditContext(db: D1AuditSqlExecutor, options?:
|
|
18
|
-
contextTable?: string;
|
|
19
|
-
}): unknown;
|
|
22
|
+
export declare function clearD1AuditContext(db: D1AuditSqlExecutor, options?: D1AuditContextOptions): unknown;
|
|
20
23
|
/**
|
|
21
24
|
* Wraps a Drizzle SQLite transaction with audit context. Sets the actor
|
|
22
25
|
* before the callback and clears the context after (success or failure).
|
|
@@ -37,8 +40,5 @@ export declare function clearD1AuditContext(db: D1AuditSqlExecutor, options?: {
|
|
|
37
40
|
*/
|
|
38
41
|
export declare function withD1AuditedTransaction<TDb extends D1AuditSqlExecutor, TResult>(db: TDb & {
|
|
39
42
|
transaction: (cb: (tx: any) => TResult) => TResult;
|
|
40
|
-
}, actorId: string, callback: (tx: TDb) => TResult, options?:
|
|
41
|
-
workspaceId?: string;
|
|
42
|
-
contextTable?: string;
|
|
43
|
-
}): TResult;
|
|
43
|
+
}, actorId: string, callback: (tx: TDb) => TResult, options?: D1AuditContextOptions): TResult;
|
|
44
44
|
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/d1/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAUpD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,kBAAkB,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/d1/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAUpD,MAAM,MAAM,qBAAqB,GAAG;IAClC,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAkBD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,kBAAkB,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,qBAAqB,gCAwBhC;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,kBAAkB,EACtB,OAAO,CAAC,EAAE,qBAAqB,WAYhC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,SAAS,kBAAkB,EAAE,OAAO,EAC9E,EAAE,EAAE,GAAG,GAAG;IAAE,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,KAAK,OAAO,CAAA;CAAE,EAChE,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,EAC9B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAWT"}
|
package/dist/src/d1/runtime.js
CHANGED
|
@@ -5,6 +5,19 @@ function assertActorId(actorId) {
|
|
|
5
5
|
throw new Error("actorId must not be empty");
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Builds the KV map (key → value) to write. Empty/undefined values are dropped
|
|
10
|
+
* so the corresponding column stays NULL.
|
|
11
|
+
*/
|
|
12
|
+
function resolveContext(options) {
|
|
13
|
+
const context = {};
|
|
14
|
+
for (const [key, value] of Object.entries(options?.context ?? {})) {
|
|
15
|
+
if (value !== undefined && value !== "") {
|
|
16
|
+
context[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return context;
|
|
20
|
+
}
|
|
8
21
|
/**
|
|
9
22
|
* Sets the audit context for the current transaction by writing to the
|
|
10
23
|
* _audit_context table. Must be called inside a transaction before any
|
|
@@ -15,26 +28,30 @@ function assertActorId(actorId) {
|
|
|
15
28
|
export function setD1AuditContext(db, actorId, options) {
|
|
16
29
|
assertActorId(actorId);
|
|
17
30
|
const table = options?.contextTable ?? DEFAULT_CONTEXT_TABLE;
|
|
18
|
-
const
|
|
31
|
+
const context = resolveContext(options);
|
|
32
|
+
const writeKv = (key, value) => db.run(sql `INSERT OR REPLACE INTO ${sql.identifier(table)} (key, value) VALUES (${key}, ${value})`);
|
|
33
|
+
const result = writeKv("user_id", actorId);
|
|
19
34
|
// Handle async drivers (D1) by chaining if result is a Promise
|
|
20
35
|
if (result && typeof result.then === "function") {
|
|
21
|
-
return
|
|
22
|
-
if (options?.workspaceId !== undefined && options.workspaceId !== "") {
|
|
23
|
-
return db.run(sql `INSERT OR REPLACE INTO ${sql.identifier(table)} (key, value) VALUES ('workspace_id', ${options.workspaceId})`);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
36
|
+
return Object.entries(context).reduce((prev, [key, value]) => prev.then(() => writeKv(key, value)), result);
|
|
26
37
|
}
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
for (const [key, value] of Object.entries(context)) {
|
|
39
|
+
writeKv(key, value);
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
/**
|
|
32
43
|
* Clears the audit context after a transaction completes.
|
|
33
44
|
* Called automatically by withD1AuditedTransaction.
|
|
45
|
+
*
|
|
46
|
+
* Clears `user_id` plus any keys set via `context` so the next transaction
|
|
47
|
+
* starts clean.
|
|
34
48
|
*/
|
|
35
49
|
export function clearD1AuditContext(db, options) {
|
|
36
50
|
const table = options?.contextTable ?? DEFAULT_CONTEXT_TABLE;
|
|
37
|
-
|
|
51
|
+
const keys = ["user_id", ...Object.keys(options?.context ?? {})];
|
|
52
|
+
const uniqueKeys = [...new Set(keys)];
|
|
53
|
+
const keyList = sql.join(uniqueKeys.map((k) => sql `${k}`), sql `, `);
|
|
54
|
+
return db.run(sql `DELETE FROM ${sql.identifier(table)} WHERE key IN (${keyList})`);
|
|
38
55
|
}
|
|
39
56
|
/**
|
|
40
57
|
* Wraps a Drizzle SQLite transaction with audit context. Sets the actor
|
package/dist/src/d1/sql.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type { D1AuditInstallOptions, D1AuditTriggerTarget } from "./types.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Generates SQL to install the audit_logs table and _audit_context table.
|
|
4
4
|
*
|
|
5
|
-
* The _audit_context table stores user_id (and
|
|
6
|
-
* the current transaction. Since D1/SQLite has no session variables, triggers
|
|
5
|
+
* The _audit_context table stores user_id (and any configured context columns)
|
|
6
|
+
* for the current transaction. Since D1/SQLite has no session variables, triggers
|
|
7
7
|
* read context from this table instead.
|
|
8
8
|
*/
|
|
9
9
|
export declare function createD1AuditInstallSql(options?: D1AuditInstallOptions): string;
|
package/dist/src/d1/sql.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../../src/d1/sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../../src/d1/sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAA;AAyEnB;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,qBAA0B,UA8C1E;AAoHD;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE,qBAA0B,UAOpC;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,oBAAoB,EAAE,EAC/B,OAAO,GAAE,qBAA0B,UAQpC;AAID,MAAM,MAAM,+BAA+B,GAAG,oBAAoB,GAAG;IACnE,kEAAkE;IAClE,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB,CAAA;AA4HD;;;;;;;;;;;GAWG;AACH,wBAAgB,wCAAwC,CACtD,MAAM,EAAE,+BAA+B,EACvC,OAAO,GAAE,qBAA0B,UAUpC;AAED,wBAAgB,yCAAyC,CACvD,OAAO,EAAE,+BAA+B,EAAE,EAC1C,OAAO,GAAE,qBAA0B,UAQpC"}
|
package/dist/src/d1/sql.js
CHANGED
|
@@ -13,29 +13,63 @@ function assertNonEmpty(value, label) {
|
|
|
13
13
|
}
|
|
14
14
|
return value;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes the context columns from the install options, de-duplicating by
|
|
18
|
+
* column name. For D1 the KV key defaults to the column name itself.
|
|
19
|
+
*/
|
|
20
|
+
function normalizeContextColumns(options) {
|
|
21
|
+
const resolved = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
for (const entry of options.contextColumns ?? []) {
|
|
24
|
+
const column = entry.column?.trim();
|
|
25
|
+
if (!column) {
|
|
26
|
+
throw new Error("contextColumns[].column must not be empty");
|
|
27
|
+
}
|
|
28
|
+
if (seen.has(column)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
seen.add(column);
|
|
32
|
+
resolved.push({
|
|
33
|
+
column,
|
|
34
|
+
sessionKey: entry.sessionKey?.trim() || column,
|
|
35
|
+
index: entry.index ?? true,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
}
|
|
40
|
+
/** Builds the column-name suffix for an INSERT column list. */
|
|
41
|
+
function contextColsClause(contextColumns) {
|
|
42
|
+
return contextColumns.map((c) => `, ${quoteIdent(c.column)}`).join("");
|
|
43
|
+
}
|
|
44
|
+
/** Builds the matching VALUES suffix that reads each KV row from the context table. */
|
|
45
|
+
function contextValuesClause(contextColumns, contextTable) {
|
|
46
|
+
return contextColumns
|
|
47
|
+
.map((c) => `,\n (SELECT value FROM ${quoteIdent(contextTable)} WHERE key = ${quoteLiteral(c.sessionKey)})`)
|
|
48
|
+
.join("");
|
|
49
|
+
}
|
|
16
50
|
/**
|
|
17
51
|
* Generates SQL to install the audit_logs table and _audit_context table.
|
|
18
52
|
*
|
|
19
|
-
* The _audit_context table stores user_id (and
|
|
20
|
-
* the current transaction. Since D1/SQLite has no session variables, triggers
|
|
53
|
+
* The _audit_context table stores user_id (and any configured context columns)
|
|
54
|
+
* for the current transaction. Since D1/SQLite has no session variables, triggers
|
|
21
55
|
* read context from this table instead.
|
|
22
56
|
*/
|
|
23
57
|
export function createD1AuditInstallSql(options = {}) {
|
|
24
58
|
const auditTable = assertNonEmpty(options.auditTable ?? DEFAULT_AUDIT_TABLE, "auditTable");
|
|
25
59
|
const contextTable = assertNonEmpty(options.contextTable ?? DEFAULT_CONTEXT_TABLE, "contextTable");
|
|
26
|
-
const
|
|
60
|
+
const contextColumns = normalizeContextColumns(options);
|
|
27
61
|
const auditColumns = [
|
|
28
62
|
"id INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
29
63
|
"table_name TEXT NOT NULL",
|
|
30
64
|
"operation TEXT NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE'))",
|
|
31
65
|
"row_id TEXT",
|
|
32
66
|
"user_id TEXT",
|
|
33
|
-
...(
|
|
67
|
+
...contextColumns.map((c) => `${quoteIdent(c.column)} TEXT`),
|
|
34
68
|
"old_data TEXT",
|
|
35
69
|
"new_data TEXT",
|
|
36
70
|
"created_at TEXT NOT NULL DEFAULT (datetime('now'))",
|
|
37
71
|
];
|
|
38
|
-
const
|
|
72
|
+
const contextTableColumns = [
|
|
39
73
|
"key TEXT PRIMARY KEY",
|
|
40
74
|
"value TEXT",
|
|
41
75
|
];
|
|
@@ -43,16 +77,14 @@ export function createD1AuditInstallSql(options = {}) {
|
|
|
43
77
|
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_table_name_idx`)} ON ${quoteIdent(auditTable)} (table_name);`,
|
|
44
78
|
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_row_id_idx`)} ON ${quoteIdent(auditTable)} (row_id);`,
|
|
45
79
|
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_user_id_idx`)} ON ${quoteIdent(auditTable)} (user_id);`,
|
|
46
|
-
...
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
]
|
|
50
|
-
: []),
|
|
80
|
+
...contextColumns
|
|
81
|
+
.filter((c) => c.index)
|
|
82
|
+
.map((c) => `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_${c.column}_idx`)} ON ${quoteIdent(auditTable)} (${quoteIdent(c.column)});`),
|
|
51
83
|
`CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_created_at_idx`)} ON ${quoteIdent(auditTable)} (created_at);`,
|
|
52
84
|
];
|
|
53
85
|
return [
|
|
54
86
|
`CREATE TABLE IF NOT EXISTS ${quoteIdent(auditTable)} (\n ${auditColumns.join(",\n ")}\n);`,
|
|
55
|
-
`CREATE TABLE IF NOT EXISTS ${quoteIdent(contextTable)} (\n ${
|
|
87
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdent(contextTable)} (\n ${contextTableColumns.join(",\n ")}\n);`,
|
|
56
88
|
...indexStatements,
|
|
57
89
|
].join("\n\n");
|
|
58
90
|
}
|
|
@@ -63,7 +95,7 @@ function buildInsertTriggerSql(target, options) {
|
|
|
63
95
|
const contextTable = assertNonEmpty(options.contextTable ?? DEFAULT_CONTEXT_TABLE, "contextTable");
|
|
64
96
|
const triggerPrefix = target.triggerPrefix ?? table;
|
|
65
97
|
const triggerName = `${triggerPrefix}_audit_insert`;
|
|
66
|
-
const
|
|
98
|
+
const contextColumns = normalizeContextColumns(options);
|
|
67
99
|
return `
|
|
68
100
|
DROP TRIGGER IF EXISTS ${quoteIdent(triggerName)};
|
|
69
101
|
|
|
@@ -71,12 +103,12 @@ CREATE TRIGGER ${quoteIdent(triggerName)}
|
|
|
71
103
|
AFTER INSERT ON ${quoteIdent(table)}
|
|
72
104
|
FOR EACH ROW
|
|
73
105
|
BEGIN
|
|
74
|
-
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${
|
|
106
|
+
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${contextColsClause(contextColumns)})
|
|
75
107
|
VALUES (
|
|
76
108
|
${quoteLiteral(table)},
|
|
77
109
|
'INSERT',
|
|
78
110
|
NEW.${quoteIdent(rowIdColumn)},
|
|
79
|
-
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${
|
|
111
|
+
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${contextValuesClause(contextColumns, contextTable)}
|
|
80
112
|
);
|
|
81
113
|
END;`.trim();
|
|
82
114
|
}
|
|
@@ -87,7 +119,7 @@ function buildUpdateTriggerSql(target, options) {
|
|
|
87
119
|
const contextTable = assertNonEmpty(options.contextTable ?? DEFAULT_CONTEXT_TABLE, "contextTable");
|
|
88
120
|
const triggerPrefix = target.triggerPrefix ?? table;
|
|
89
121
|
const triggerName = `${triggerPrefix}_audit_update`;
|
|
90
|
-
const
|
|
122
|
+
const contextColumns = normalizeContextColumns(options);
|
|
91
123
|
return `
|
|
92
124
|
DROP TRIGGER IF EXISTS ${quoteIdent(triggerName)};
|
|
93
125
|
|
|
@@ -95,12 +127,12 @@ CREATE TRIGGER ${quoteIdent(triggerName)}
|
|
|
95
127
|
AFTER UPDATE ON ${quoteIdent(table)}
|
|
96
128
|
FOR EACH ROW
|
|
97
129
|
BEGIN
|
|
98
|
-
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${
|
|
130
|
+
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${contextColsClause(contextColumns)})
|
|
99
131
|
VALUES (
|
|
100
132
|
${quoteLiteral(table)},
|
|
101
133
|
'UPDATE',
|
|
102
134
|
NEW.${quoteIdent(rowIdColumn)},
|
|
103
|
-
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${
|
|
135
|
+
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${contextValuesClause(contextColumns, contextTable)}
|
|
104
136
|
);
|
|
105
137
|
END;`.trim();
|
|
106
138
|
}
|
|
@@ -111,7 +143,7 @@ function buildDeleteTriggerSql(target, options) {
|
|
|
111
143
|
const contextTable = assertNonEmpty(options.contextTable ?? DEFAULT_CONTEXT_TABLE, "contextTable");
|
|
112
144
|
const triggerPrefix = target.triggerPrefix ?? table;
|
|
113
145
|
const triggerName = `${triggerPrefix}_audit_delete`;
|
|
114
|
-
const
|
|
146
|
+
const contextColumns = normalizeContextColumns(options);
|
|
115
147
|
return `
|
|
116
148
|
DROP TRIGGER IF EXISTS ${quoteIdent(triggerName)};
|
|
117
149
|
|
|
@@ -119,12 +151,12 @@ CREATE TRIGGER ${quoteIdent(triggerName)}
|
|
|
119
151
|
AFTER DELETE ON ${quoteIdent(table)}
|
|
120
152
|
FOR EACH ROW
|
|
121
153
|
BEGIN
|
|
122
|
-
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${
|
|
154
|
+
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${contextColsClause(contextColumns)})
|
|
123
155
|
VALUES (
|
|
124
156
|
${quoteLiteral(table)},
|
|
125
157
|
'DELETE',
|
|
126
158
|
OLD.${quoteIdent(rowIdColumn)},
|
|
127
|
-
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${
|
|
159
|
+
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${contextValuesClause(contextColumns, contextTable)}
|
|
128
160
|
);
|
|
129
161
|
END;`.trim();
|
|
130
162
|
}
|
|
@@ -160,7 +192,7 @@ function buildInsertTriggerWithColumnsSql(target, options) {
|
|
|
160
192
|
const contextTable = assertNonEmpty(options.contextTable ?? DEFAULT_CONTEXT_TABLE, "contextTable");
|
|
161
193
|
const triggerPrefix = target.triggerPrefix ?? table;
|
|
162
194
|
const triggerName = `${triggerPrefix}_audit_insert`;
|
|
163
|
-
const
|
|
195
|
+
const contextColumns = normalizeContextColumns(options);
|
|
164
196
|
return `
|
|
165
197
|
DROP TRIGGER IF EXISTS ${quoteIdent(triggerName)};
|
|
166
198
|
|
|
@@ -168,12 +200,12 @@ CREATE TRIGGER ${quoteIdent(triggerName)}
|
|
|
168
200
|
AFTER INSERT ON ${quoteIdent(table)}
|
|
169
201
|
FOR EACH ROW
|
|
170
202
|
BEGIN
|
|
171
|
-
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${
|
|
203
|
+
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${contextColsClause(contextColumns)}, new_data)
|
|
172
204
|
VALUES (
|
|
173
205
|
${quoteLiteral(table)},
|
|
174
206
|
'INSERT',
|
|
175
207
|
NEW.${quoteIdent(rowIdColumn)},
|
|
176
|
-
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${
|
|
208
|
+
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${contextValuesClause(contextColumns, contextTable)},
|
|
177
209
|
${buildJsonObjectExpr(target.columns, "NEW")}
|
|
178
210
|
);
|
|
179
211
|
END;`.trim();
|
|
@@ -185,7 +217,7 @@ function buildUpdateTriggerWithColumnsSql(target, options) {
|
|
|
185
217
|
const contextTable = assertNonEmpty(options.contextTable ?? DEFAULT_CONTEXT_TABLE, "contextTable");
|
|
186
218
|
const triggerPrefix = target.triggerPrefix ?? table;
|
|
187
219
|
const triggerName = `${triggerPrefix}_audit_update`;
|
|
188
|
-
const
|
|
220
|
+
const contextColumns = normalizeContextColumns(options);
|
|
189
221
|
return `
|
|
190
222
|
DROP TRIGGER IF EXISTS ${quoteIdent(triggerName)};
|
|
191
223
|
|
|
@@ -193,12 +225,12 @@ CREATE TRIGGER ${quoteIdent(triggerName)}
|
|
|
193
225
|
AFTER UPDATE ON ${quoteIdent(table)}
|
|
194
226
|
FOR EACH ROW
|
|
195
227
|
BEGIN
|
|
196
|
-
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${
|
|
228
|
+
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${contextColsClause(contextColumns)}, old_data, new_data)
|
|
197
229
|
VALUES (
|
|
198
230
|
${quoteLiteral(table)},
|
|
199
231
|
'UPDATE',
|
|
200
232
|
NEW.${quoteIdent(rowIdColumn)},
|
|
201
|
-
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${
|
|
233
|
+
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${contextValuesClause(contextColumns, contextTable)},
|
|
202
234
|
${buildJsonObjectExpr(target.columns, "OLD")},
|
|
203
235
|
${buildJsonObjectExpr(target.columns, "NEW")}
|
|
204
236
|
);
|
|
@@ -211,7 +243,7 @@ function buildDeleteTriggerWithColumnsSql(target, options) {
|
|
|
211
243
|
const contextTable = assertNonEmpty(options.contextTable ?? DEFAULT_CONTEXT_TABLE, "contextTable");
|
|
212
244
|
const triggerPrefix = target.triggerPrefix ?? table;
|
|
213
245
|
const triggerName = `${triggerPrefix}_audit_delete`;
|
|
214
|
-
const
|
|
246
|
+
const contextColumns = normalizeContextColumns(options);
|
|
215
247
|
return `
|
|
216
248
|
DROP TRIGGER IF EXISTS ${quoteIdent(triggerName)};
|
|
217
249
|
|
|
@@ -219,12 +251,12 @@ CREATE TRIGGER ${quoteIdent(triggerName)}
|
|
|
219
251
|
AFTER DELETE ON ${quoteIdent(table)}
|
|
220
252
|
FOR EACH ROW
|
|
221
253
|
BEGIN
|
|
222
|
-
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${
|
|
254
|
+
INSERT INTO ${quoteIdent(auditTable)} (table_name, operation, row_id, user_id${contextColsClause(contextColumns)}, old_data)
|
|
223
255
|
VALUES (
|
|
224
256
|
${quoteLiteral(table)},
|
|
225
257
|
'DELETE',
|
|
226
258
|
OLD.${quoteIdent(rowIdColumn)},
|
|
227
|
-
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${
|
|
259
|
+
(SELECT value FROM ${quoteIdent(contextTable)} WHERE key = 'user_id')${contextValuesClause(contextColumns, contextTable)},
|
|
228
260
|
${buildJsonObjectExpr(target.columns, "OLD")}
|
|
229
261
|
);
|
|
230
262
|
END;`.trim();
|
package/dist/src/d1/types.d.ts
CHANGED
|
@@ -6,13 +6,21 @@ import type { SQL } from "drizzle-orm";
|
|
|
6
6
|
export type D1AuditSqlExecutor = {
|
|
7
7
|
run: (query: SQL) => unknown;
|
|
8
8
|
};
|
|
9
|
+
export type AuditContextColumn = {
|
|
10
|
+
/** Column added to the audit table (TEXT, nullable). */
|
|
11
|
+
column: string;
|
|
12
|
+
/** The `_audit_context` key the trigger reads. Default `${column}`. */
|
|
13
|
+
sessionKey?: string;
|
|
14
|
+
/** Create an index on the column. Default true. */
|
|
15
|
+
index?: boolean;
|
|
16
|
+
};
|
|
9
17
|
export type D1AuditInstallOptions = {
|
|
10
18
|
/** Name of the audit log table (default: "audit_logs") */
|
|
11
19
|
auditTable?: string;
|
|
12
20
|
/** Name of the context table used to pass user_id to triggers (default: "_audit_context") */
|
|
13
21
|
contextTable?: string;
|
|
14
|
-
/**
|
|
15
|
-
|
|
22
|
+
/** Extra context columns added to the audit table and populated by triggers from the _audit_context KV table. */
|
|
23
|
+
contextColumns?: AuditContextColumn[];
|
|
16
24
|
};
|
|
17
25
|
export type D1AuditTriggerTarget = {
|
|
18
26
|
table: string;
|