@willyim/drizzle-audit 0.2.1 → 0.4.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 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 (recommended)
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, captures old/new row data, and runs each operation + audit log insert in a transaction.
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
 
@@ -204,6 +206,12 @@ export function createAuditSql() {
204
206
  | `setAuditContext(db, actorId, contextKey?, options?)` | Set actor context in current transaction |
205
207
  | `withAuditedTransaction(db, actorId, callback, contextKey?, options?)` | Transaction wrapper with actor context |
206
208
 
209
+ ### `@willyim/drizzle-audit`
210
+
211
+ | Export | Description |
212
+ |---|---|
213
+ | `computeDiff(operation, oldData, newData, options?)` | Compute field-by-field diff from old/new row data |
214
+
207
215
  ### `@willyim/drizzle-audit/d1`
208
216
 
209
217
  | Export | Description |
@@ -231,6 +239,43 @@ export function createAuditSql() {
231
239
  - `.delete(table, where)` — Fetch old rows, delete, audit log (per row)
232
240
  - `.db` — Raw Drizzle instance for non-audited operations
233
241
 
242
+ ## Computing Diffs
243
+
244
+ 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.
245
+
246
+ ```ts
247
+ import { computeDiff } from "@willyim/drizzle-audit"
248
+
249
+ const result = computeDiff(
250
+ "UPDATE",
251
+ { id: "u1", name: "Ada", email: "ada@example.com" },
252
+ { id: "u1", name: "Ada Lovelace", email: "ada@example.com" },
253
+ )
254
+ // {
255
+ // operation: "UPDATE",
256
+ // changes: [{ field: "name", old: "Ada", new: "Ada Lovelace" }]
257
+ // }
258
+ ```
259
+
260
+ For INSERT operations, pass `null` as `oldData` — all fields appear as additions. For DELETE, pass `null` as `newData` — all fields appear as removals.
261
+
262
+ ```ts
263
+ // INSERT — every field is new
264
+ computeDiff("INSERT", null, { id: "u1", name: "Ada" })
265
+
266
+ // DELETE — every field is removed
267
+ computeDiff("DELETE", { id: "u1", name: "Ada" }, null)
268
+ ```
269
+
270
+ By default, `updated_at` and `created_at` fields are excluded. Override with `ignoreFields`:
271
+
272
+ ```ts
273
+ computeDiff("UPDATE", oldData, newData, { ignoreFields: [] }) // include all fields
274
+ computeDiff("UPDATE", oldData, newData, { ignoreFields: ["internal_note"] })
275
+ ```
276
+
277
+ Nested objects are compared using deep equality. Fields are returned sorted alphabetically.
278
+
234
279
  ## Audit Log Schema
235
280
 
236
281
  ### Postgres
@@ -0,0 +1,15 @@
1
+ export type DiffEntry = {
2
+ field: string;
3
+ old: unknown;
4
+ new: unknown;
5
+ };
6
+ export type DiffResult = {
7
+ operation: string;
8
+ changes: DiffEntry[];
9
+ };
10
+ export type ComputeDiffOptions = {
11
+ /** Fields to exclude from the diff (default: ["updated_at", "created_at"]) */
12
+ ignoreFields?: string[];
13
+ };
14
+ export declare function computeDiff(operation: string, oldData: Record<string, unknown> | null, newData: Record<string, unknown> | null, options?: ComputeDiffOptions): DiffResult;
15
+ //# sourceMappingURL=compute-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compute-diff.d.ts","sourceRoot":"","sources":["../../src/compute-diff.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,CAAC;AAQF,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACvC,OAAO,CAAC,EAAE,kBAAkB,GAC3B,UAAU,CAyBZ"}
@@ -0,0 +1,22 @@
1
+ const DEFAULT_IGNORE = ["updated_at", "created_at"];
2
+ function deepEqual(a, b) {
3
+ return JSON.stringify(a) === JSON.stringify(b);
4
+ }
5
+ export function computeDiff(operation, oldData, newData, options) {
6
+ const ignore = options?.ignoreFields ?? DEFAULT_IGNORE;
7
+ if (oldData === null && newData === null) {
8
+ return { operation, changes: [] };
9
+ }
10
+ const allKeys = Array.from(new Set([...Object.keys(oldData ?? {}), ...Object.keys(newData ?? {})]))
11
+ .filter((k) => !ignore.includes(k))
12
+ .sort();
13
+ const changes = [];
14
+ for (const field of allKeys) {
15
+ const oldVal = oldData?.[field] ?? null;
16
+ const newVal = newData?.[field] ?? null;
17
+ if (!deepEqual(oldVal, newVal)) {
18
+ changes.push({ field, old: oldVal, new: newVal });
19
+ }
20
+ }
21
+ return { operation, changes };
22
+ }
@@ -22,33 +22,36 @@ export type DrizzleSQLiteDb = {
22
22
  update: (table: any) => any;
23
23
  delete: (table: any) => any;
24
24
  select: (fields?: any) => any;
25
- transaction: (cb: (tx: any) => any) => any;
26
25
  };
27
26
  export type AuditedDb<TDb extends DrizzleSQLiteDb> = {
28
27
  /**
29
28
  * Insert a row and log an INSERT audit event.
30
29
  * Returns the inserted row.
31
30
  */
32
- insert: <T extends SQLiteTable>(table: T, data: InferInsertModel<T>) => InferSelectModel<T>;
31
+ insert: <T extends SQLiteTable>(table: T, data: InferInsertModel<T>) => Promise<InferSelectModel<T>>;
33
32
  /**
34
33
  * Update rows matching `where` and log an UPDATE audit event for each affected row.
35
34
  * Captures old_data (before) and new_data (after).
36
35
  * Returns the updated rows.
37
36
  */
38
- update: <T extends SQLiteTable>(table: T, where: SQL, data: Partial<InferInsertModel<T>>) => InferSelectModel<T>[];
37
+ update: <T extends SQLiteTable>(table: T, where: SQL, data: Partial<InferInsertModel<T>>) => Promise<InferSelectModel<T>[]>;
39
38
  /**
40
39
  * Delete rows matching `where` and log a DELETE audit event for each affected row.
41
40
  * Captures old_data.
42
41
  * Returns the deleted rows.
43
42
  */
44
- delete: <T extends SQLiteTable>(table: T, where: SQL) => InferSelectModel<T>[];
43
+ delete: <T extends SQLiteTable>(table: T, where: SQL) => Promise<InferSelectModel<T>[]>;
45
44
  /** Access the underlying db for non-audited operations. */
46
45
  db: TDb;
47
46
  };
48
47
  /**
49
48
  * Creates an audited wrapper around a Drizzle SQLite database.
50
- * Each insert/update/delete is wrapped in a transaction that atomically
51
- * writes to both the target table and the audit_logs table.
49
+ * Each insert/update/delete runs the data write and audit log insert sequentially
50
+ * as individual statements (no transaction). This works with both D1 (async) and
51
+ * better-sqlite3 (sync), but writes are best-effort: a failure between the data
52
+ * write and the audit insert can leave one without the other.
53
+ *
54
+ * For atomic auditing on D1, use the trigger-based approach from `@willyim/drizzle-audit/d1`.
52
55
  *
53
56
  * @param db - A Drizzle SQLite database instance (D1, better-sqlite3, libsql)
54
57
  * @param auditTable - The Drizzle table definition for audit_logs
@@ -62,13 +65,13 @@ export type AuditedDb<TDb extends DrizzleSQLiteDb> = {
62
65
  * const audited = withAudit(db, auditLogs, { userId: session.userId })
63
66
  *
64
67
  * // Audited insert
65
- * audited.insert(users, { id: "u1", name: "Ada" })
68
+ * await audited.insert(users, { id: "u1", name: "Ada" })
66
69
  *
67
70
  * // Audited update — captures old + new data
68
- * audited.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" })
71
+ * await audited.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" })
69
72
  *
70
73
  * // Audited delete — captures deleted data
71
- * audited.delete(users, eq(users.id, "u1"))
74
+ * await audited.delete(users, eq(users.id, "u1"))
72
75
  *
73
76
  * // Non-audited access
74
77
  * audited.db.select().from(users).all()
@@ -1 +1 @@
1
- {"version":3,"file":"with-audit.d.ts","sourceRoot":"","sources":["../../../src/d1-runtime/with-audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACT,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAIxE,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IAC3B,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IAC3B,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IAC3B,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,GAAG,CAAA;IAC7B,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,GAAG,KAAK,GAAG,CAAA;CAC3C,CAAA;AAgBD,MAAM,MAAM,SAAS,CAAC,GAAG,SAAS,eAAe,IAAI;IACnD;;;OAGG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,WAAW,EAC5B,KAAK,EAAE,CAAC,EACR,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,KACtB,gBAAgB,CAAC,CAAC,CAAC,CAAA;IAExB;;;;OAIG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,WAAW,EAC5B,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,GAAG,EACV,IAAI,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAC/B,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAA;IAE1B;;;;OAIG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,WAAW,EAC5B,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,GAAG,KACP,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAA;IAE1B,2DAA2D;IAC3D,EAAE,EAAE,GAAG,CAAA;CACR,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,SAAS,CAAC,GAAG,SAAS,eAAe,EACnD,EAAE,EAAE,GAAG,EACP,UAAU,EAAE,WAAW,EACvB,OAAO,EAAE,YAAY,GACpB,SAAS,CAAC,GAAG,CAAC,CAoHhB"}
1
+ {"version":3,"file":"with-audit.d.ts","sourceRoot":"","sources":["../../../src/d1-runtime/with-audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACT,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAIxE,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IAC3B,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IAC3B,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IAC3B,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,GAAG,CAAA;CAC9B,CAAA;AAgBD,MAAM,MAAM,SAAS,CAAC,GAAG,SAAS,eAAe,IAAI;IACnD;;;OAGG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,WAAW,EAC5B,KAAK,EAAE,CAAC,EACR,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,KACtB,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAA;IAEjC;;;;OAIG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,WAAW,EAC5B,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,GAAG,EACV,IAAI,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAC/B,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAEnC;;;;OAIG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,WAAW,EAC5B,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,GAAG,KACP,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAEnC,2DAA2D;IAC3D,EAAE,EAAE,GAAG,CAAA;CACR,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,SAAS,CAAC,GAAG,SAAS,eAAe,EACnD,EAAE,EAAE,GAAG,EACP,UAAU,EAAE,WAAW,EACvB,OAAO,EAAE,YAAY,GACpB,SAAS,CAAC,GAAG,CAAC,CA8FhB"}
@@ -12,8 +12,12 @@ function getRowId(row, pk) {
12
12
  }
13
13
  /**
14
14
  * Creates an audited wrapper around a Drizzle SQLite database.
15
- * Each insert/update/delete is wrapped in a transaction that atomically
16
- * writes to both the target table and the audit_logs table.
15
+ * Each insert/update/delete runs the data write and audit log insert sequentially
16
+ * as individual statements (no transaction). This works with both D1 (async) and
17
+ * better-sqlite3 (sync), but writes are best-effort: a failure between the data
18
+ * write and the audit insert can leave one without the other.
19
+ *
20
+ * For atomic auditing on D1, use the trigger-based approach from `@willyim/drizzle-audit/d1`.
17
21
  *
18
22
  * @param db - A Drizzle SQLite database instance (D1, better-sqlite3, libsql)
19
23
  * @param auditTable - The Drizzle table definition for audit_logs
@@ -27,13 +31,13 @@ function getRowId(row, pk) {
27
31
  * const audited = withAudit(db, auditLogs, { userId: session.userId })
28
32
  *
29
33
  * // Audited insert
30
- * audited.insert(users, { id: "u1", name: "Ada" })
34
+ * await audited.insert(users, { id: "u1", name: "Ada" })
31
35
  *
32
36
  * // Audited update — captures old + new data
33
- * audited.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" })
37
+ * await audited.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" })
34
38
  *
35
39
  * // Audited delete — captures deleted data
36
- * audited.delete(users, eq(users.id, "u1"))
40
+ * await audited.delete(users, eq(users.id, "u1"))
37
41
  *
38
42
  * // Non-audited access
39
43
  * audited.db.select().from(users).all()
@@ -69,62 +73,46 @@ export function withAudit(db, auditTable, context) {
69
73
  }
70
74
  return {
71
75
  db,
72
- insert(table, data) {
76
+ async insert(table, data) {
73
77
  const tableName = getTableName(table);
74
78
  const pk = getPrimaryKeyColumn(table);
75
- return db.transaction((tx) => {
76
- const [row] = tx.insert(table).values(data).returning().all();
77
- const rowId = getRowId(row, pk);
78
- tx.insert(auditTable)
79
- .values(buildAuditRow(tableName, "INSERT", rowId, null, row))
80
- .run();
81
- return row;
82
- });
79
+ const [row] = await db.insert(table).values(data).returning().all();
80
+ const rowId = getRowId(row, pk);
81
+ await db
82
+ .insert(auditTable)
83
+ .values(buildAuditRow(tableName, "INSERT", rowId, null, row))
84
+ .run();
85
+ return row;
83
86
  },
84
- update(table, where, data) {
87
+ async update(table, where, data) {
85
88
  const tableName = getTableName(table);
86
89
  const pk = getPrimaryKeyColumn(table);
87
- return db.transaction((tx) => {
88
- const oldRows = tx
89
- .select()
90
- .from(table)
91
- .where(where)
92
- .all();
93
- const newRows = tx
94
- .update(table)
95
- .set(data)
96
- .where(where)
97
- .returning()
98
- .all();
99
- for (let i = 0; i < newRows.length; i++) {
100
- const oldRow = oldRows[i] ?? null;
101
- const newRow = newRows[i];
102
- const rowId = getRowId(newRow, pk);
103
- tx.insert(auditTable)
104
- .values(buildAuditRow(tableName, "UPDATE", rowId, oldRow, newRow))
105
- .run();
106
- }
107
- return newRows;
108
- });
90
+ const oldRows = await db.select().from(table).where(where).all();
91
+ const newRows = await db.update(table).set(data).where(where).returning().all();
92
+ for (let i = 0; i < newRows.length; i++) {
93
+ const oldRow = oldRows[i] ?? null;
94
+ const newRow = newRows[i];
95
+ const rowId = getRowId(newRow, pk);
96
+ await db
97
+ .insert(auditTable)
98
+ .values(buildAuditRow(tableName, "UPDATE", rowId, oldRow, newRow))
99
+ .run();
100
+ }
101
+ return newRows;
109
102
  },
110
- delete(table, where) {
103
+ async delete(table, where) {
111
104
  const tableName = getTableName(table);
112
105
  const pk = getPrimaryKeyColumn(table);
113
- return db.transaction((tx) => {
114
- const oldRows = tx
115
- .select()
116
- .from(table)
117
- .where(where)
118
- .all();
119
- tx.delete(table).where(where).run();
120
- for (const oldRow of oldRows) {
121
- const rowId = getRowId(oldRow, pk);
122
- tx.insert(auditTable)
123
- .values(buildAuditRow(tableName, "DELETE", rowId, oldRow, null))
124
- .run();
125
- }
126
- return oldRows;
127
- });
106
+ const oldRows = await db.select().from(table).where(where).all();
107
+ await db.delete(table).where(where).run();
108
+ for (const oldRow of oldRows) {
109
+ const rowId = getRowId(oldRow, pk);
110
+ await db
111
+ .insert(auditTable)
112
+ .values(buildAuditRow(tableName, "DELETE", rowId, oldRow, null))
113
+ .run();
114
+ }
115
+ return oldRows;
128
116
  },
129
117
  };
130
118
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./postgres/index.js";
2
2
  export * from "./d1/index.js";
3
3
  export * from "./d1-runtime/index.js";
4
+ export * from "./compute-diff.js";
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,mBAAmB,CAAA"}
package/dist/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./postgres/index.js";
2
2
  export * from "./d1/index.js";
3
3
  export * from "./d1-runtime/index.js";
4
+ export * from "./compute-diff.js";
@@ -20,7 +20,6 @@ export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): impor
20
20
  enumValues: undefined;
21
21
  identity: undefined;
22
22
  generated: undefined;
23
- insertType: unknown;
24
23
  }>;
25
24
  new_data: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").PgJsonbBuilder, {
26
25
  name: string;
@@ -36,7 +35,6 @@ export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): impor
36
35
  enumValues: undefined;
37
36
  identity: undefined;
38
37
  generated: undefined;
39
- insertType: unknown;
40
38
  }>;
41
39
  created_at: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").SetHasDefault<import("drizzle-orm/pg-core").PgTimestampBuilder>>, {
42
40
  name: string;
@@ -52,7 +50,6 @@ export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): impor
52
50
  enumValues: undefined;
53
51
  identity: undefined;
54
52
  generated: undefined;
55
- insertType: Date | undefined;
56
53
  }>;
57
54
  id: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").SetIsPrimaryKey<import("drizzle-orm/pg-core").PgBigSerial53Builder>, {
58
55
  name: string;
@@ -68,9 +65,8 @@ export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): impor
68
65
  enumValues: undefined;
69
66
  identity: undefined;
70
67
  generated: undefined;
71
- insertType: number | undefined;
72
68
  }>;
73
- table_name: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<undefined>>, {
69
+ table_name: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
74
70
  name: string;
75
71
  tableName: "audit_logs";
76
72
  dataType: "string";
@@ -84,9 +80,8 @@ export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): impor
84
80
  enumValues: undefined;
85
81
  identity: undefined;
86
82
  generated: undefined;
87
- insertType: string;
88
83
  }>;
89
- operation: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<undefined>>, {
84
+ operation: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
90
85
  name: string;
91
86
  tableName: "audit_logs";
92
87
  dataType: "string";
@@ -100,9 +95,8 @@ export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): impor
100
95
  enumValues: undefined;
101
96
  identity: undefined;
102
97
  generated: undefined;
103
- insertType: string;
104
98
  }>;
105
- row_id: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").PgTextBuilder<undefined>, {
99
+ row_id: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>, {
106
100
  name: string;
107
101
  tableName: "audit_logs";
108
102
  dataType: "string";
@@ -116,9 +110,8 @@ export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): impor
116
110
  enumValues: undefined;
117
111
  identity: undefined;
118
112
  generated: undefined;
119
- insertType: string | null | undefined;
120
113
  }>;
121
- user_id: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").PgTextBuilder<undefined>, {
114
+ user_id: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>, {
122
115
  name: string;
123
116
  tableName: "audit_logs";
124
117
  dataType: "string";
@@ -132,7 +125,6 @@ export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): impor
132
125
  enumValues: undefined;
133
126
  identity: undefined;
134
127
  generated: undefined;
135
- insertType: string | null | undefined;
136
128
  }>;
137
129
  };
138
130
  dialect: "pg";
@@ -1 +1 @@
1
- {"version":3,"file":"audit-log-schema.d.ts","sourceRoot":"","sources":["../../../src/postgres/audit-log-schema.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,sBAAsB,GAAG;IACnC,+GAA+G;IAC/G,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2B/D"}
1
+ {"version":3,"file":"audit-log-schema.d.ts","sourceRoot":"","sources":["../../../src/postgres/audit-log-schema.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,sBAAsB,GAAG;IACnC,+GAA+G;IAC/G,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2B/D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compute-diff.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compute-diff.test.d.ts","sourceRoot":"","sources":["../../test/compute-diff.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,60 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { computeDiff } from "../src/compute-diff.js";
4
+ test("UPDATE — only changed fields are emitted", () => {
5
+ const result = computeDiff("UPDATE", { name: "Juan", phone: "809", status: "current" }, { name: "Juan Carlos", phone: "809", status: "current" });
6
+ assert.deepEqual(result, {
7
+ operation: "UPDATE",
8
+ changes: [{ field: "name", old: "Juan", new: "Juan Carlos" }],
9
+ });
10
+ });
11
+ test("UPDATE — ignores updated_at by default", () => {
12
+ const result = computeDiff("UPDATE", { name: "A", updated_at: "2026-01-01" }, { name: "B", updated_at: "2026-04-15" });
13
+ assert.deepEqual(result, {
14
+ operation: "UPDATE",
15
+ changes: [{ field: "name", old: "A", new: "B" }],
16
+ });
17
+ });
18
+ test("UPDATE — ignores created_at by default", () => {
19
+ const result = computeDiff("UPDATE", { name: "A", created_at: "2026-01-01" }, { name: "A", created_at: "2026-04-15" });
20
+ assert.deepEqual(result, { operation: "UPDATE", changes: [] });
21
+ });
22
+ test("INSERT — all fields with old: null", () => {
23
+ const result = computeDiff("INSERT", null, { name: "María", phone: "809" });
24
+ assert.deepEqual(result, {
25
+ operation: "INSERT",
26
+ changes: [
27
+ { field: "name", old: null, new: "María" },
28
+ { field: "phone", old: null, new: "809" },
29
+ ],
30
+ });
31
+ });
32
+ test("DELETE — all fields with new: null", () => {
33
+ const result = computeDiff("DELETE", { name: "Pedro", phone: "809" }, null);
34
+ assert.deepEqual(result, {
35
+ operation: "DELETE",
36
+ changes: [
37
+ { field: "name", old: "Pedro", new: null },
38
+ { field: "phone", old: "809", new: null },
39
+ ],
40
+ });
41
+ });
42
+ test("custom ignoreFields", () => {
43
+ const result = computeDiff("UPDATE", { a: 1, b: 2 }, { a: 1, b: 3 }, { ignoreFields: ["b"] });
44
+ assert.deepEqual(result, { operation: "UPDATE", changes: [] });
45
+ });
46
+ test("deep equality for nested objects", () => {
47
+ const result = computeDiff("UPDATE", { payload: { x: 1 } }, { payload: { x: 2 } });
48
+ assert.deepEqual(result, {
49
+ operation: "UPDATE",
50
+ changes: [{ field: "payload", old: { x: 1 }, new: { x: 2 } }],
51
+ });
52
+ });
53
+ test("both null — returns empty changes", () => {
54
+ const result = computeDiff("UPDATE", null, null);
55
+ assert.deepEqual(result, { operation: "UPDATE", changes: [] });
56
+ });
57
+ test("changes are sorted alphabetically by field", () => {
58
+ const result = computeDiff("UPDATE", { zebra: 1, apple: 2, mango: 3 }, { zebra: 2, apple: 3, mango: 4 });
59
+ assert.deepEqual(result.changes.map((c) => c.field), ["apple", "mango", "zebra"]);
60
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Integration tests for withAudit against a real async D1 driver (miniflare v4).
3
+ *
4
+ * These tests exercise the actual failure mode described in issue #29:
5
+ * - drizzle-orm/d1 .all()/.run() return Promises, not plain values.
6
+ * - drizzle-orm/d1 db.transaction() issues a raw BEGIN which D1 rejects.
7
+ *
8
+ * All tests here FAIL on the current sync implementation and must PASS after the fix.
9
+ *
10
+ * The existing sqlite.integration.test.ts covers the better-sqlite3 (sync) path.
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=d1-async.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-async.integration.test.d.ts","sourceRoot":"","sources":["../../test/d1-async.integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Integration tests for withAudit against a real async D1 driver (miniflare v4).
3
+ *
4
+ * These tests exercise the actual failure mode described in issue #29:
5
+ * - drizzle-orm/d1 .all()/.run() return Promises, not plain values.
6
+ * - drizzle-orm/d1 db.transaction() issues a raw BEGIN which D1 rejects.
7
+ *
8
+ * All tests here FAIL on the current sync implementation and must PASS after the fix.
9
+ *
10
+ * The existing sqlite.integration.test.ts covers the better-sqlite3 (sync) path.
11
+ */
12
+ import assert from "node:assert/strict";
13
+ import { after, before, test } from "node:test";
14
+ import { asc, eq, isNull } from "drizzle-orm";
15
+ import { drizzle } from "drizzle-orm/d1";
16
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
17
+ import { Miniflare } from "miniflare";
18
+ import { d1AuditLogTable } from "../src/d1/index.js";
19
+ import { withAudit } from "../src/d1-runtime/index.js";
20
+ const users = sqliteTable("users", {
21
+ id: text("id").primaryKey(),
22
+ name: text("name").notNull(),
23
+ email: text("email"),
24
+ });
25
+ const invoices = sqliteTable("invoices", {
26
+ invoice_id: text("invoice_id").primaryKey(),
27
+ amount: integer("amount").notNull(),
28
+ status: text("status").notNull().default("pending"),
29
+ });
30
+ const auditLogs = d1AuditLogTable();
31
+ let mf;
32
+ before(async () => {
33
+ mf = new Miniflare({
34
+ modules: true,
35
+ script: `export default { fetch() { return new Response("ok") } }`,
36
+ d1Databases: { DB: "drizzle-audit-test" },
37
+ });
38
+ });
39
+ after(async () => {
40
+ await mf.dispose();
41
+ });
42
+ async function setupDb() {
43
+ const d1 = await mf.getD1Database("DB");
44
+ // D1 exec() treats each newline as a statement separator — use prepare().run() for DDL
45
+ for (const sql of [
46
+ "DROP TABLE IF EXISTS audit_logs",
47
+ "DROP TABLE IF EXISTS users",
48
+ "DROP TABLE IF EXISTS invoices",
49
+ "CREATE TABLE audit_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, table_name TEXT NOT NULL, operation TEXT NOT NULL, row_id TEXT, user_id TEXT, old_data TEXT, new_data TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')))",
50
+ "CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT)",
51
+ "CREATE TABLE invoices (invoice_id TEXT PRIMARY KEY, amount INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'pending')",
52
+ ]) {
53
+ await d1.prepare(sql).run();
54
+ }
55
+ return drizzle(d1, { schema: { auditLogs, users, invoices } });
56
+ }
57
+ test("withAudit insert logs audit row with new_data (real D1)", async () => {
58
+ const db = await setupDb();
59
+ const audited = withAudit(db, auditLogs, { userId: "user_1" });
60
+ const row = await audited.insert(users, { id: "u1", name: "Ada", email: "ada@example.com" });
61
+ assert.equal(row.id, "u1");
62
+ assert.equal(row.name, "Ada");
63
+ const logs = await db.select().from(auditLogs).all();
64
+ assert.equal(logs.length, 1);
65
+ assert.equal(logs[0]?.table_name, "users");
66
+ assert.equal(logs[0]?.operation, "INSERT");
67
+ assert.equal(logs[0]?.row_id, "u1");
68
+ assert.equal(logs[0]?.user_id, "user_1");
69
+ assert.equal(logs[0]?.old_data, null);
70
+ assert.deepEqual(JSON.parse(logs[0]?.new_data), {
71
+ id: "u1",
72
+ name: "Ada",
73
+ email: "ada@example.com",
74
+ });
75
+ });
76
+ test("withAudit update captures old and new data (real D1)", async () => {
77
+ const db = await setupDb();
78
+ await db.insert(users).values({ id: "u1", name: "Ada", email: "ada@example.com" });
79
+ const audited = withAudit(db, auditLogs, { userId: "user_2" });
80
+ const rows = await audited.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" });
81
+ assert.equal(rows.length, 1);
82
+ assert.equal(rows[0]?.name, "Ada Lovelace");
83
+ const logs = await db.select().from(auditLogs).all();
84
+ assert.equal(logs.length, 1);
85
+ assert.equal(logs[0]?.operation, "UPDATE");
86
+ assert.equal(logs[0]?.row_id, "u1");
87
+ assert.equal(logs[0]?.user_id, "user_2");
88
+ const oldData = JSON.parse(logs[0]?.old_data);
89
+ assert.equal(oldData.name, "Ada");
90
+ assert.equal(oldData.email, "ada@example.com");
91
+ const newData = JSON.parse(logs[0]?.new_data);
92
+ assert.equal(newData.name, "Ada Lovelace");
93
+ assert.equal(newData.email, "ada@example.com");
94
+ });
95
+ test("withAudit delete captures old data (real D1)", async () => {
96
+ const db = await setupDb();
97
+ await db.insert(users).values({ id: "u1", name: "Ada" });
98
+ const audited = withAudit(db, auditLogs, { userId: "user_3" });
99
+ const deleted = await audited.delete(users, eq(users.id, "u1"));
100
+ assert.equal(deleted.length, 1);
101
+ assert.equal(deleted[0]?.id, "u1");
102
+ const remaining = await db.select().from(users).all();
103
+ assert.equal(remaining.length, 0);
104
+ const logs = await db.select().from(auditLogs).all();
105
+ assert.equal(logs.length, 1);
106
+ assert.equal(logs[0]?.operation, "DELETE");
107
+ assert.equal(logs[0]?.row_id, "u1");
108
+ assert.equal(logs[0]?.user_id, "user_3");
109
+ assert.deepEqual(JSON.parse(logs[0]?.old_data), {
110
+ id: "u1",
111
+ name: "Ada",
112
+ email: null,
113
+ });
114
+ assert.equal(logs[0]?.new_data, null);
115
+ });
116
+ test("withAudit works with custom primary key column (real D1)", async () => {
117
+ const db = await setupDb();
118
+ const audited = withAudit(db, auditLogs, { userId: "user_1" });
119
+ await audited.insert(invoices, { invoice_id: "inv_1", amount: 100 });
120
+ await audited.update(invoices, eq(invoices.invoice_id, "inv_1"), { amount: 200 });
121
+ await audited.delete(invoices, eq(invoices.invoice_id, "inv_1"));
122
+ const logs = await db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
123
+ assert.equal(logs.length, 3);
124
+ assert.equal(logs[0]?.table_name, "invoices");
125
+ assert.equal(logs[0]?.row_id, "inv_1");
126
+ assert.equal(logs[1]?.operation, "UPDATE");
127
+ assert.equal(logs[1]?.row_id, "inv_1");
128
+ assert.equal(JSON.parse(logs[1]?.old_data).amount, 100);
129
+ assert.equal(JSON.parse(logs[1]?.new_data).amount, 200);
130
+ assert.equal(logs[2]?.operation, "DELETE");
131
+ assert.equal(logs[2]?.row_id, "inv_1");
132
+ });
133
+ test("withAudit handles multi-row update (real D1)", async () => {
134
+ const db = await setupDb();
135
+ await db.insert(users).values([
136
+ { id: "u1", name: "Ada" },
137
+ { id: "u2", name: "Bob" },
138
+ { id: "u3", name: "Carol" },
139
+ ]);
140
+ const audited = withAudit(db, auditLogs, { userId: "admin" });
141
+ const rows = await audited.update(users, isNull(users.email), { email: "bulk@example.com" });
142
+ assert.equal(rows.length, 3);
143
+ const logs = await db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
144
+ assert.equal(logs.length, 3);
145
+ for (const log of logs) {
146
+ assert.equal(log.operation, "UPDATE");
147
+ assert.equal(log.user_id, "admin");
148
+ assert.equal(JSON.parse(log.new_data).email, "bulk@example.com");
149
+ }
150
+ });
151
+ test("withAudit.db gives access to raw db for non-audited ops (real D1)", async () => {
152
+ const db = await setupDb();
153
+ const audited = withAudit(db, auditLogs, { userId: "user_1" });
154
+ await audited.db.insert(users).values({ id: "u1", name: "Ada" });
155
+ const logs = await db.select().from(auditLogs).all();
156
+ assert.equal(logs.length, 0);
157
+ const rows = await db.select().from(users).all();
158
+ assert.equal(rows.length, 1);
159
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sqlite.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.integration.test.d.ts","sourceRoot":"","sources":["../../test/sqlite.integration.test.ts"],"names":[],"mappings":""}
@@ -3,7 +3,7 @@ import test from "node:test";
3
3
  import Database from "better-sqlite3";
4
4
  import { asc, eq, isNull } from "drizzle-orm";
5
5
  import { drizzle } from "drizzle-orm/better-sqlite3";
6
- import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
6
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
7
7
  import { d1AuditLogTable } from "../src/d1/index.js";
8
8
  import { withAudit } from "../src/d1-runtime/index.js";
9
9
  const users = sqliteTable("users", {
@@ -44,11 +44,11 @@ function setupDb() {
44
44
  `);
45
45
  return { db, sqlite };
46
46
  }
47
- test("withAudit insert logs audit row with new_data", () => {
47
+ test("withAudit insert logs audit row with new_data", async () => {
48
48
  const { db, sqlite } = setupDb();
49
49
  try {
50
50
  const audited = withAudit(db, auditLogs, { userId: "user_1" });
51
- const row = audited.insert(users, { id: "u1", name: "Ada", email: "ada@example.com" });
51
+ const row = await audited.insert(users, { id: "u1", name: "Ada", email: "ada@example.com" });
52
52
  assert.equal(row.id, "u1");
53
53
  assert.equal(row.name, "Ada");
54
54
  const logs = db.select().from(auditLogs).all();
@@ -68,13 +68,12 @@ test("withAudit insert logs audit row with new_data", () => {
68
68
  sqlite.close();
69
69
  }
70
70
  });
71
- test("withAudit update captures old and new data", () => {
71
+ test("withAudit update captures old and new data", async () => {
72
72
  const { db, sqlite } = setupDb();
73
73
  try {
74
- // Seed a row
75
74
  db.insert(users).values({ id: "u1", name: "Ada", email: "ada@example.com" }).run();
76
75
  const audited = withAudit(db, auditLogs, { userId: "user_2" });
77
- const rows = audited.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" });
76
+ const rows = await audited.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" });
78
77
  assert.equal(rows.length, 1);
79
78
  assert.equal(rows[0]?.name, "Ada Lovelace");
80
79
  const logs = db.select().from(auditLogs).all();
@@ -93,18 +92,16 @@ test("withAudit update captures old and new data", () => {
93
92
  sqlite.close();
94
93
  }
95
94
  });
96
- test("withAudit delete captures old data", () => {
95
+ test("withAudit delete captures old data", async () => {
97
96
  const { db, sqlite } = setupDb();
98
97
  try {
99
98
  db.insert(users).values({ id: "u1", name: "Ada" }).run();
100
99
  const audited = withAudit(db, auditLogs, { userId: "user_3" });
101
- const deleted = audited.delete(users, eq(users.id, "u1"));
100
+ const deleted = await audited.delete(users, eq(users.id, "u1"));
102
101
  assert.equal(deleted.length, 1);
103
102
  assert.equal(deleted[0]?.id, "u1");
104
- // Verify row is gone
105
103
  const remaining = db.select().from(users).all();
106
104
  assert.equal(remaining.length, 0);
107
- // Verify audit log
108
105
  const logs = db.select().from(auditLogs).all();
109
106
  assert.equal(logs.length, 1);
110
107
  assert.equal(logs[0]?.operation, "DELETE");
@@ -121,13 +118,13 @@ test("withAudit delete captures old data", () => {
121
118
  sqlite.close();
122
119
  }
123
120
  });
124
- test("withAudit works with custom primary key column", () => {
121
+ test("withAudit works with custom primary key column", async () => {
125
122
  const { db, sqlite } = setupDb();
126
123
  try {
127
124
  const audited = withAudit(db, auditLogs, { userId: "user_1" });
128
- audited.insert(invoices, { invoice_id: "inv_1", amount: 100 });
129
- audited.update(invoices, eq(invoices.invoice_id, "inv_1"), { amount: 200 });
130
- audited.delete(invoices, eq(invoices.invoice_id, "inv_1"));
125
+ await audited.insert(invoices, { invoice_id: "inv_1", amount: 100 });
126
+ await audited.update(invoices, eq(invoices.invoice_id, "inv_1"), { amount: 200 });
127
+ await audited.delete(invoices, eq(invoices.invoice_id, "inv_1"));
131
128
  const logs = db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
132
129
  assert.equal(logs.length, 3);
133
130
  assert.equal(logs[0]?.table_name, "invoices");
@@ -143,7 +140,7 @@ test("withAudit works with custom primary key column", () => {
143
140
  sqlite.close();
144
141
  }
145
142
  });
146
- test("withAudit handles multi-row update", () => {
143
+ test("withAudit handles multi-row update", async () => {
147
144
  const { db, sqlite } = setupDb();
148
145
  try {
149
146
  db.insert(users).values([
@@ -152,23 +149,21 @@ test("withAudit handles multi-row update", () => {
152
149
  { id: "u3", name: "Carol" },
153
150
  ]).run();
154
151
  const audited = withAudit(db, auditLogs, { userId: "admin" });
155
- // Update all users (no where = all rows, but let's use a broader condition)
156
- const rows = audited.update(users, isNull(users.email), { email: "bulk@example.com" });
152
+ const rows = await audited.update(users, isNull(users.email), { email: "bulk@example.com" });
157
153
  assert.equal(rows.length, 3);
158
154
  const logs = db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
159
155
  assert.equal(logs.length, 3);
160
156
  for (const log of logs) {
161
157
  assert.equal(log.operation, "UPDATE");
162
158
  assert.equal(log.user_id, "admin");
163
- const newData = JSON.parse(log.new_data);
164
- assert.equal(newData.email, "bulk@example.com");
159
+ assert.equal(JSON.parse(log.new_data).email, "bulk@example.com");
165
160
  }
166
161
  }
167
162
  finally {
168
163
  sqlite.close();
169
164
  }
170
165
  });
171
- test("withAudit with workspace_id", () => {
166
+ test("withAudit with workspace_id", async () => {
172
167
  const sqlite = new Database(":memory:");
173
168
  const auditLogsWithWs = d1AuditLogTable({ workspaceIdColumn: "workspace_id" });
174
169
  const db = drizzle({ client: sqlite, schema: { auditLogs: auditLogsWithWs, users } });
@@ -195,7 +190,7 @@ test("withAudit with workspace_id", () => {
195
190
  userId: "user_1",
196
191
  workspaceId: "ws_1",
197
192
  });
198
- audited.insert(users, { id: "u1", name: "Ada" });
193
+ await audited.insert(users, { id: "u1", name: "Ada" });
199
194
  const logs = db.select().from(auditLogsWithWs).all();
200
195
  assert.equal(logs.length, 1);
201
196
  assert.equal(logs[0]?.user_id, "user_1");
@@ -205,11 +200,10 @@ test("withAudit with workspace_id", () => {
205
200
  sqlite.close();
206
201
  }
207
202
  });
208
- test("withAudit.db gives access to raw db for non-audited ops", () => {
203
+ test("withAudit.db gives access to raw db for non-audited ops", async () => {
209
204
  const { db, sqlite } = setupDb();
210
205
  try {
211
206
  const audited = withAudit(db, auditLogs, { userId: "user_1" });
212
- // Direct insert — no audit
213
207
  audited.db.insert(users).values({ id: "u1", name: "Ada" }).run();
214
208
  const logs = db.select().from(auditLogs).all();
215
209
  assert.equal(logs.length, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willyim/drizzle-audit",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Lightweight audit logging for Drizzle ORM using database triggers (Postgres + D1/SQLite)",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -53,6 +53,7 @@
53
53
  "devDependencies": {
54
54
  "@types/node": "^22.19.7",
55
55
  "drizzle-orm": "^1.0.0-beta.15-859cf75",
56
+ "miniflare": "^4.20260410.0",
56
57
  "typescript": "^5.9.3"
57
58
  },
58
59
  "publishConfig": {
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=d1-runtime.integration.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"d1-runtime.integration.test.d.ts","sourceRoot":"","sources":["../../test/d1-runtime.integration.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=postgres.integration.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"postgres.integration.test.d.ts","sourceRoot":"","sources":["../../test/postgres.integration.test.ts"],"names":[],"mappings":""}
@@ -1,286 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { PGlite } from "@electric-sql/pglite";
4
- import { asc, eq } from "drizzle-orm";
5
- import { drizzle } from "drizzle-orm/pglite";
6
- import { integer, pgTable, text } from "drizzle-orm/pg-core";
7
- import { createAttachAuditTriggersSql, createAuditInstallSql, pgAuditLogTable, withAuditedTransaction, } from "../src/postgres/index.js";
8
- const users = pgTable("users", {
9
- id: text("id").primaryKey(),
10
- name: text("name").notNull(),
11
- });
12
- const invoices = pgTable("invoices", {
13
- invoice_id: text("invoice_id").primaryKey(),
14
- amount: integer("amount").notNull(),
15
- });
16
- const auditLogs = pgAuditLogTable();
17
- test("postgres auditing works end to end", async () => {
18
- const client = new PGlite();
19
- const db = drizzle({
20
- client,
21
- schema: {
22
- auditLogs,
23
- users,
24
- invoices,
25
- },
26
- });
27
- try {
28
- await client.exec(createAuditInstallSql());
29
- await client.exec(`
30
- CREATE TABLE users (
31
- id TEXT PRIMARY KEY,
32
- name TEXT NOT NULL
33
- );
34
-
35
- CREATE TABLE invoices (
36
- invoice_id TEXT PRIMARY KEY,
37
- amount INTEGER NOT NULL
38
- );
39
- `);
40
- await client.exec(createAttachAuditTriggersSql([
41
- { table: "users" },
42
- { table: "invoices", rowIdColumn: "invoice_id" },
43
- ]));
44
- // Insert without audit context should succeed with user_id = NULL
45
- await db.insert(users).values({ id: "user_0", name: "No Context" });
46
- const existingUsers = await db.select().from(users);
47
- assert.equal(existingUsers.length, 1);
48
- const noContextLogs = await db.select().from(auditLogs).orderBy(asc(auditLogs.id));
49
- assert.equal(noContextLogs.length, 1);
50
- assert.equal(noContextLogs[0]?.user_id, null);
51
- assert.equal(noContextLogs[0]?.table_name, "users");
52
- assert.equal(noContextLogs[0]?.operation, "INSERT");
53
- await withAuditedTransaction(db, "user_123", async (tx) => {
54
- await tx.insert(users).values({ id: "user_1", name: "Ada" });
55
- await tx
56
- .update(users)
57
- .set({ name: "Ada Lovelace" })
58
- .where(eq(users.id, "user_1"));
59
- await tx.insert(invoices).values({ invoice_id: "inv_1", amount: 42 });
60
- await tx.delete(users).where(eq(users.id, "user_1"));
61
- });
62
- const logs = await db
63
- .select()
64
- .from(auditLogs)
65
- .orderBy(asc(auditLogs.id));
66
- assert.equal(logs.length, 5);
67
- assert.equal(logs[1]?.table_name, "users");
68
- assert.equal(logs[1]?.operation, "INSERT");
69
- assert.equal(logs[1]?.row_id, "user_1");
70
- assert.equal(logs[1]?.user_id, "user_123");
71
- assert.deepEqual(logs[1]?.new_data, { id: "user_1", name: "Ada" });
72
- assert.equal(logs[2]?.table_name, "users");
73
- assert.equal(logs[2]?.operation, "UPDATE");
74
- assert.equal(logs[2]?.row_id, "user_1");
75
- assert.deepEqual(logs[2]?.old_data, { id: "user_1", name: "Ada" });
76
- assert.deepEqual(logs[2]?.new_data, {
77
- id: "user_1",
78
- name: "Ada Lovelace",
79
- });
80
- assert.equal(logs[3]?.table_name, "invoices");
81
- assert.equal(logs[3]?.operation, "INSERT");
82
- assert.equal(logs[3]?.row_id, "inv_1");
83
- assert.deepEqual(logs[3]?.new_data, {
84
- invoice_id: "inv_1",
85
- amount: 42,
86
- });
87
- assert.equal(logs[4]?.table_name, "users");
88
- assert.equal(logs[4]?.operation, "DELETE");
89
- assert.equal(logs[4]?.row_id, "user_1");
90
- assert.deepEqual(logs[4]?.old_data, {
91
- id: "user_1",
92
- name: "Ada Lovelace",
93
- });
94
- }
95
- finally {
96
- await client.close();
97
- }
98
- });
99
- test("migration SQL bundle installs and enforces audit context", async () => {
100
- const client = new PGlite();
101
- const db = drizzle({
102
- client,
103
- schema: {
104
- auditLogs,
105
- users,
106
- invoices,
107
- },
108
- });
109
- try {
110
- await client.exec(`
111
- CREATE TABLE users (
112
- id TEXT PRIMARY KEY,
113
- name TEXT NOT NULL
114
- );
115
-
116
- CREATE TABLE invoices (
117
- invoice_id TEXT PRIMARY KEY,
118
- amount INTEGER NOT NULL
119
- );
120
- `);
121
- const migrationBundle = [
122
- createAuditInstallSql(),
123
- createAttachAuditTriggersSql([
124
- { table: "users" },
125
- { table: "invoices", rowIdColumn: "invoice_id" },
126
- ]),
127
- ].join("\n\n");
128
- await client.exec(migrationBundle);
129
- // Insert without audit context should succeed with user_id = NULL
130
- await db.insert(users).values({ id: "u_no_ctx", name: "No Context" });
131
- const noCtxLogs = await db.select().from(auditLogs);
132
- assert.equal(noCtxLogs.length, 1);
133
- assert.equal(noCtxLogs[0]?.user_id, null);
134
- assert.equal(noCtxLogs[0]?.operation, "INSERT");
135
- await withAuditedTransaction(db, "system:test", async (tx) => {
136
- await tx.insert(users).values({ id: "u", name: "With Context" });
137
- });
138
- const logs = await db.select().from(auditLogs).orderBy(asc(auditLogs.id));
139
- assert.equal(logs.length, 2);
140
- assert.equal(logs[1]?.table_name, "users");
141
- assert.equal(logs[1]?.operation, "INSERT");
142
- assert.equal(logs[1]?.user_id, "system:test");
143
- }
144
- finally {
145
- await client.close();
146
- }
147
- });
148
- test("writes without audit context produce audit rows with user_id = NULL", async () => {
149
- const client = new PGlite();
150
- const db = drizzle({
151
- client,
152
- schema: {
153
- auditLogs,
154
- users,
155
- },
156
- });
157
- try {
158
- await client.exec(createAuditInstallSql());
159
- await client.exec(`
160
- CREATE TABLE users (
161
- id TEXT PRIMARY KEY,
162
- name TEXT NOT NULL
163
- );
164
- `);
165
- await client.exec(createAttachAuditTriggersSql([{ table: "users" }]));
166
- // INSERT without audit context
167
- await db.insert(users).values({ id: "u1", name: "Alice" });
168
- // UPDATE without audit context
169
- await db.update(users).set({ name: "Alice Updated" }).where(eq(users.id, "u1"));
170
- // DELETE without audit context
171
- await db.delete(users).where(eq(users.id, "u1"));
172
- const logs = await db.select().from(auditLogs).orderBy(asc(auditLogs.id));
173
- assert.equal(logs.length, 3);
174
- assert.equal(logs[0]?.operation, "INSERT");
175
- assert.equal(logs[0]?.user_id, null);
176
- assert.equal(logs[0]?.row_id, "u1");
177
- assert.deepEqual(logs[0]?.new_data, { id: "u1", name: "Alice" });
178
- assert.equal(logs[1]?.operation, "UPDATE");
179
- assert.equal(logs[1]?.user_id, null);
180
- assert.deepEqual(logs[1]?.old_data, { id: "u1", name: "Alice" });
181
- assert.deepEqual(logs[1]?.new_data, { id: "u1", name: "Alice Updated" });
182
- assert.equal(logs[2]?.operation, "DELETE");
183
- assert.equal(logs[2]?.user_id, null);
184
- assert.deepEqual(logs[2]?.old_data, { id: "u1", name: "Alice Updated" });
185
- }
186
- finally {
187
- await client.close();
188
- }
189
- });
190
- test("workspace_id column and context are stored when enabled", async () => {
191
- const client = new PGlite();
192
- const auditLogsWithWorkspace = pgAuditLogTable({ workspaceIdColumn: "workspace_id" });
193
- const db = drizzle({
194
- client,
195
- schema: {
196
- auditLogs: auditLogsWithWorkspace,
197
- users,
198
- invoices,
199
- },
200
- });
201
- try {
202
- await client.exec(createAuditInstallSql({ workspaceIdColumn: "workspace_id" }));
203
- await client.exec(`
204
- CREATE TABLE users (
205
- id TEXT PRIMARY KEY,
206
- name TEXT NOT NULL
207
- );
208
- `);
209
- await client.exec(createAttachAuditTriggersSql([{ table: "users" }]));
210
- await withAuditedTransaction(db, "user_1", async (tx) => {
211
- await tx.insert(users).values({ id: "u1", name: "Alice" });
212
- }, "app.user_id", { workspaceId: "ws_1" });
213
- const logs = await db.select().from(auditLogsWithWorkspace);
214
- assert.equal(logs.length, 1);
215
- assert.equal(logs[0]?.user_id, "user_1");
216
- assert.equal(logs[0].workspace_id, "ws_1");
217
- await withAuditedTransaction(db, "user_2", async (tx) => {
218
- await tx.insert(users).values({ id: "u2", name: "Bob" });
219
- });
220
- const logs2 = await db.select().from(auditLogsWithWorkspace).orderBy(asc(auditLogsWithWorkspace.id));
221
- assert.equal(logs2.length, 2);
222
- assert.equal(logs2[1]?.user_id, "user_2");
223
- assert.equal(logs2[1].workspace_id, null);
224
- }
225
- finally {
226
- await client.close();
227
- }
228
- });
229
- test("custom workspace column name uses matching context key", async () => {
230
- const client = new PGlite();
231
- const auditLogsWithTenant = pgAuditLogTable({ workspaceIdColumn: "tenant_id" });
232
- const db = drizzle({
233
- client,
234
- schema: {
235
- auditLogs: auditLogsWithTenant,
236
- users,
237
- },
238
- });
239
- try {
240
- await client.exec(createAuditInstallSql({ workspaceIdColumn: "tenant_id" }));
241
- await client.exec(`
242
- CREATE TABLE users (
243
- id TEXT PRIMARY KEY,
244
- name TEXT NOT NULL
245
- );
246
- `);
247
- await client.exec(createAttachAuditTriggersSql([{ table: "users" }]));
248
- // Use workspaceContextKey matching the trigger's "app.tenant_id"
249
- await withAuditedTransaction(db, "user_1", async (tx) => {
250
- await tx.insert(users).values({ id: "u1", name: "Alice" });
251
- }, "app.user_id", { workspaceId: "tenant_abc", workspaceContextKey: "app.tenant_id" });
252
- const logs = await db.select().from(auditLogsWithTenant);
253
- assert.equal(logs.length, 1);
254
- assert.equal(logs[0]?.user_id, "user_1");
255
- assert.equal(logs[0].tenant_id, "tenant_abc");
256
- }
257
- finally {
258
- await client.close();
259
- }
260
- });
261
- test("custom context key for user_id works", async () => {
262
- const client = new PGlite();
263
- const db = drizzle({
264
- client,
265
- schema: { auditLogs, users },
266
- });
267
- try {
268
- await client.exec(createAuditInstallSql({ contextKey: "myapp.actor" }));
269
- await client.exec(`
270
- CREATE TABLE users (
271
- id TEXT PRIMARY KEY,
272
- name TEXT NOT NULL
273
- );
274
- `);
275
- await client.exec(createAttachAuditTriggersSql([{ table: "users" }], { contextKey: "myapp.actor" }));
276
- await withAuditedTransaction(db, "custom_user", async (tx) => {
277
- await tx.insert(users).values({ id: "u1", name: "Alice" });
278
- }, "myapp.actor");
279
- const logs = await db.select().from(auditLogs).orderBy(asc(auditLogs.id));
280
- assert.equal(logs.length, 1);
281
- assert.equal(logs[0]?.user_id, "custom_user");
282
- }
283
- finally {
284
- await client.close();
285
- }
286
- });