@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
|
@@ -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
|
|
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,25 +149,25 @@ 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
|
-
|
|
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
|
-
|
|
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
|
-
const auditLogsWithWs = d1AuditLogTable({
|
|
168
|
+
const auditLogsWithWs = d1AuditLogTable({
|
|
169
|
+
contextColumns: [{ column: "workspace_id" }],
|
|
170
|
+
});
|
|
174
171
|
const db = drizzle({ client: sqlite, schema: { auditLogs: auditLogsWithWs, users } });
|
|
175
172
|
try {
|
|
176
173
|
sqlite.exec(`
|
|
@@ -193,9 +190,9 @@ test("withAudit with workspace_id", () => {
|
|
|
193
190
|
`);
|
|
194
191
|
const audited = withAudit(db, auditLogsWithWs, {
|
|
195
192
|
userId: "user_1",
|
|
196
|
-
|
|
193
|
+
context: { workspace_id: "ws_1" },
|
|
197
194
|
});
|
|
198
|
-
audited.insert(users, { id: "u1", name: "Ada" });
|
|
195
|
+
await audited.insert(users, { id: "u1", name: "Ada" });
|
|
199
196
|
const logs = db.select().from(auditLogsWithWs).all();
|
|
200
197
|
assert.equal(logs.length, 1);
|
|
201
198
|
assert.equal(logs[0]?.user_id, "user_1");
|
|
@@ -205,11 +202,71 @@ test("withAudit with workspace_id", () => {
|
|
|
205
202
|
sqlite.close();
|
|
206
203
|
}
|
|
207
204
|
});
|
|
208
|
-
test("withAudit
|
|
205
|
+
test("withAudit with generic context columns", async () => {
|
|
206
|
+
const sqlite = new Database(":memory:");
|
|
207
|
+
const auditLogsWithCtx = d1AuditLogTable({
|
|
208
|
+
contextColumns: [
|
|
209
|
+
{ column: "workspace_id" },
|
|
210
|
+
{ column: "tenant_id" },
|
|
211
|
+
{ column: "request_id" },
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
const db = drizzle({ client: sqlite, schema: { auditLogs: auditLogsWithCtx, users } });
|
|
215
|
+
try {
|
|
216
|
+
sqlite.exec(`
|
|
217
|
+
CREATE TABLE audit_logs (
|
|
218
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
219
|
+
table_name TEXT NOT NULL,
|
|
220
|
+
operation TEXT NOT NULL,
|
|
221
|
+
row_id TEXT,
|
|
222
|
+
user_id TEXT,
|
|
223
|
+
workspace_id TEXT,
|
|
224
|
+
tenant_id TEXT,
|
|
225
|
+
request_id TEXT,
|
|
226
|
+
old_data TEXT,
|
|
227
|
+
new_data TEXT,
|
|
228
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
229
|
+
);
|
|
230
|
+
CREATE TABLE users (
|
|
231
|
+
id TEXT PRIMARY KEY,
|
|
232
|
+
name TEXT NOT NULL,
|
|
233
|
+
email TEXT
|
|
234
|
+
);
|
|
235
|
+
`);
|
|
236
|
+
const audited = withAudit(db, auditLogsWithCtx, {
|
|
237
|
+
userId: "user_1",
|
|
238
|
+
context: {
|
|
239
|
+
workspace_id: "ws_1",
|
|
240
|
+
tenant_id: "tenant_1",
|
|
241
|
+
request_id: "req_1",
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
await audited.insert(users, { id: "u1", name: "Ada" });
|
|
245
|
+
const logs = db.select().from(auditLogsWithCtx).all();
|
|
246
|
+
assert.equal(logs.length, 1);
|
|
247
|
+
const row = logs[0];
|
|
248
|
+
assert.equal(row.user_id, "user_1");
|
|
249
|
+
assert.equal(row.workspace_id, "ws_1");
|
|
250
|
+
assert.equal(row.tenant_id, "tenant_1");
|
|
251
|
+
assert.equal(row.request_id, "req_1");
|
|
252
|
+
// No context: extra columns stay NULL.
|
|
253
|
+
const auditedNoCtx = withAudit(db, auditLogsWithCtx, { userId: "user_2" });
|
|
254
|
+
await auditedNoCtx.insert(users, { id: "u2", name: "Bob" });
|
|
255
|
+
const all = db.select().from(auditLogsWithCtx).orderBy(asc(auditLogsWithCtx.id)).all();
|
|
256
|
+
const last = all[all.length - 1];
|
|
257
|
+
assert.equal(last.user_id, "user_2");
|
|
258
|
+
assert.equal(last.workspace_id, null);
|
|
259
|
+
assert.equal(last.tenant_id, null);
|
|
260
|
+
assert.equal(last.request_id, null);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
sqlite.close();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
test("withAudit.db gives access to raw db for non-audited ops", async () => {
|
|
209
267
|
const { db, sqlite } = setupDb();
|
|
210
268
|
try {
|
|
211
269
|
const audited = withAudit(db, auditLogs, { userId: "user_1" });
|
|
212
|
-
// Direct insert — no audit
|
|
213
270
|
audited.db.insert(users).values({ id: "u1", name: "Ada" }).run();
|
|
214
271
|
const logs = db.select().from(auditLogs).all();
|
|
215
272
|
assert.equal(logs.length, 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willyim/drizzle-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"d1-runtime.integration.test.d.ts","sourceRoot":"","sources":["../../test/d1-runtime.integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -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
|
-
});
|