@willyim/drizzle-audit 0.1.3
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 +278 -0
- package/dist/src/cli/generate-migration.d.ts +14 -0
- package/dist/src/cli/generate-migration.d.ts.map +1 -0
- package/dist/src/cli/generate-migration.js +118 -0
- package/dist/src/cli/runner.d.ts +2 -0
- package/dist/src/cli/runner.d.ts.map +1 -0
- package/dist/src/cli/runner.js +23 -0
- package/dist/src/d1/audit-log-schema.d.ts +181 -0
- package/dist/src/d1/audit-log-schema.d.ts.map +1 -0
- package/dist/src/d1/audit-log-schema.js +30 -0
- package/dist/src/d1/index.d.ts +7 -0
- package/dist/src/d1/index.d.ts.map +1 -0
- package/dist/src/d1/index.js +3 -0
- package/dist/src/d1/runtime.d.ts +44 -0
- package/dist/src/d1/runtime.d.ts.map +1 -0
- package/dist/src/d1/runtime.js +68 -0
- package/dist/src/d1/sql.d.ts +37 -0
- package/dist/src/d1/sql.d.ts.map +1 -0
- package/dist/src/d1/sql.js +261 -0
- package/dist/src/d1/types.d.ts +22 -0
- package/dist/src/d1/types.d.ts.map +1 -0
- package/dist/src/d1/types.js +1 -0
- package/dist/src/d1-runtime/index.d.ts +3 -0
- package/dist/src/d1-runtime/index.d.ts.map +1 -0
- package/dist/src/d1-runtime/index.js +1 -0
- package/dist/src/d1-runtime/with-audit.d.ts +77 -0
- package/dist/src/d1-runtime/with-audit.d.ts.map +1 -0
- package/dist/src/d1-runtime/with-audit.js +130 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/postgres/audit-log-schema.d.ts +140 -0
- package/dist/src/postgres/audit-log-schema.d.ts.map +1 -0
- package/dist/src/postgres/audit-log-schema.js +25 -0
- package/dist/src/postgres/index.d.ts +6 -0
- package/dist/src/postgres/index.d.ts.map +1 -0
- package/dist/src/postgres/index.js +3 -0
- package/dist/src/postgres/runtime.d.ts +10 -0
- package/dist/src/postgres/runtime.d.ts.map +1 -0
- package/dist/src/postgres/runtime.js +21 -0
- package/dist/src/postgres/sql.d.ts +14 -0
- package/dist/src/postgres/sql.d.ts.map +1 -0
- package/dist/src/postgres/sql.js +190 -0
- package/dist/src/postgres/types.d.ts +22 -0
- package/dist/src/postgres/types.d.ts.map +1 -0
- package/dist/src/postgres/types.js +1 -0
- package/dist/test/d1-runtime.integration.test.d.ts +2 -0
- package/dist/test/d1-runtime.integration.test.d.ts.map +1 -0
- package/dist/test/d1-runtime.integration.test.js +222 -0
- package/dist/test/d1.integration.test.d.ts +2 -0
- package/dist/test/d1.integration.test.d.ts.map +1 -0
- package/dist/test/d1.integration.test.js +223 -0
- package/dist/test/postgres.integration.test.d.ts +2 -0
- package/dist/test/postgres.integration.test.d.ts.map +1 -0
- package/dist/test/postgres.integration.test.js +286 -0
- package/package.json +66 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import { asc, eq, isNull } from "drizzle-orm";
|
|
5
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
6
|
+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
7
|
+
import { d1AuditLogTable } from "../src/d1/index.js";
|
|
8
|
+
import { withAudit } from "../src/d1-runtime/index.js";
|
|
9
|
+
const users = sqliteTable("users", {
|
|
10
|
+
id: text("id").primaryKey(),
|
|
11
|
+
name: text("name").notNull(),
|
|
12
|
+
email: text("email"),
|
|
13
|
+
});
|
|
14
|
+
const invoices = sqliteTable("invoices", {
|
|
15
|
+
invoice_id: text("invoice_id").primaryKey(),
|
|
16
|
+
amount: integer("amount").notNull(),
|
|
17
|
+
status: text("status").notNull().default("pending"),
|
|
18
|
+
});
|
|
19
|
+
const auditLogs = d1AuditLogTable();
|
|
20
|
+
function setupDb() {
|
|
21
|
+
const sqlite = new Database(":memory:");
|
|
22
|
+
const db = drizzle({ client: sqlite, schema: { auditLogs, users, invoices } });
|
|
23
|
+
sqlite.exec(`
|
|
24
|
+
CREATE TABLE audit_logs (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
table_name TEXT NOT NULL,
|
|
27
|
+
operation TEXT NOT NULL,
|
|
28
|
+
row_id TEXT,
|
|
29
|
+
user_id TEXT,
|
|
30
|
+
old_data TEXT,
|
|
31
|
+
new_data TEXT,
|
|
32
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE users (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
email TEXT
|
|
38
|
+
);
|
|
39
|
+
CREATE TABLE invoices (
|
|
40
|
+
invoice_id TEXT PRIMARY KEY,
|
|
41
|
+
amount INTEGER NOT NULL,
|
|
42
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
43
|
+
);
|
|
44
|
+
`);
|
|
45
|
+
return { db, sqlite };
|
|
46
|
+
}
|
|
47
|
+
test("withAudit insert logs audit row with new_data", () => {
|
|
48
|
+
const { db, sqlite } = setupDb();
|
|
49
|
+
try {
|
|
50
|
+
const audited = withAudit(db, auditLogs, { userId: "user_1" });
|
|
51
|
+
const row = audited.insert(users, { id: "u1", name: "Ada", email: "ada@example.com" });
|
|
52
|
+
assert.equal(row.id, "u1");
|
|
53
|
+
assert.equal(row.name, "Ada");
|
|
54
|
+
const logs = db.select().from(auditLogs).all();
|
|
55
|
+
assert.equal(logs.length, 1);
|
|
56
|
+
assert.equal(logs[0]?.table_name, "users");
|
|
57
|
+
assert.equal(logs[0]?.operation, "INSERT");
|
|
58
|
+
assert.equal(logs[0]?.row_id, "u1");
|
|
59
|
+
assert.equal(logs[0]?.user_id, "user_1");
|
|
60
|
+
assert.equal(logs[0]?.old_data, null);
|
|
61
|
+
assert.deepEqual(JSON.parse(logs[0]?.new_data), {
|
|
62
|
+
id: "u1",
|
|
63
|
+
name: "Ada",
|
|
64
|
+
email: "ada@example.com",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
sqlite.close();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
test("withAudit update captures old and new data", () => {
|
|
72
|
+
const { db, sqlite } = setupDb();
|
|
73
|
+
try {
|
|
74
|
+
// Seed a row
|
|
75
|
+
db.insert(users).values({ id: "u1", name: "Ada", email: "ada@example.com" }).run();
|
|
76
|
+
const audited = withAudit(db, auditLogs, { userId: "user_2" });
|
|
77
|
+
const rows = audited.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" });
|
|
78
|
+
assert.equal(rows.length, 1);
|
|
79
|
+
assert.equal(rows[0]?.name, "Ada Lovelace");
|
|
80
|
+
const logs = db.select().from(auditLogs).all();
|
|
81
|
+
assert.equal(logs.length, 1);
|
|
82
|
+
assert.equal(logs[0]?.operation, "UPDATE");
|
|
83
|
+
assert.equal(logs[0]?.row_id, "u1");
|
|
84
|
+
assert.equal(logs[0]?.user_id, "user_2");
|
|
85
|
+
const oldData = JSON.parse(logs[0]?.old_data);
|
|
86
|
+
assert.equal(oldData.name, "Ada");
|
|
87
|
+
assert.equal(oldData.email, "ada@example.com");
|
|
88
|
+
const newData = JSON.parse(logs[0]?.new_data);
|
|
89
|
+
assert.equal(newData.name, "Ada Lovelace");
|
|
90
|
+
assert.equal(newData.email, "ada@example.com");
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
sqlite.close();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
test("withAudit delete captures old data", () => {
|
|
97
|
+
const { db, sqlite } = setupDb();
|
|
98
|
+
try {
|
|
99
|
+
db.insert(users).values({ id: "u1", name: "Ada" }).run();
|
|
100
|
+
const audited = withAudit(db, auditLogs, { userId: "user_3" });
|
|
101
|
+
const deleted = audited.delete(users, eq(users.id, "u1"));
|
|
102
|
+
assert.equal(deleted.length, 1);
|
|
103
|
+
assert.equal(deleted[0]?.id, "u1");
|
|
104
|
+
// Verify row is gone
|
|
105
|
+
const remaining = db.select().from(users).all();
|
|
106
|
+
assert.equal(remaining.length, 0);
|
|
107
|
+
// Verify audit log
|
|
108
|
+
const logs = db.select().from(auditLogs).all();
|
|
109
|
+
assert.equal(logs.length, 1);
|
|
110
|
+
assert.equal(logs[0]?.operation, "DELETE");
|
|
111
|
+
assert.equal(logs[0]?.row_id, "u1");
|
|
112
|
+
assert.equal(logs[0]?.user_id, "user_3");
|
|
113
|
+
assert.deepEqual(JSON.parse(logs[0]?.old_data), {
|
|
114
|
+
id: "u1",
|
|
115
|
+
name: "Ada",
|
|
116
|
+
email: null,
|
|
117
|
+
});
|
|
118
|
+
assert.equal(logs[0]?.new_data, null);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
sqlite.close();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
test("withAudit works with custom primary key column", () => {
|
|
125
|
+
const { db, sqlite } = setupDb();
|
|
126
|
+
try {
|
|
127
|
+
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"));
|
|
131
|
+
const logs = db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
|
|
132
|
+
assert.equal(logs.length, 3);
|
|
133
|
+
assert.equal(logs[0]?.table_name, "invoices");
|
|
134
|
+
assert.equal(logs[0]?.row_id, "inv_1");
|
|
135
|
+
assert.equal(logs[1]?.operation, "UPDATE");
|
|
136
|
+
assert.equal(logs[1]?.row_id, "inv_1");
|
|
137
|
+
assert.equal(JSON.parse(logs[1]?.old_data).amount, 100);
|
|
138
|
+
assert.equal(JSON.parse(logs[1]?.new_data).amount, 200);
|
|
139
|
+
assert.equal(logs[2]?.operation, "DELETE");
|
|
140
|
+
assert.equal(logs[2]?.row_id, "inv_1");
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
sqlite.close();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
test("withAudit handles multi-row update", () => {
|
|
147
|
+
const { db, sqlite } = setupDb();
|
|
148
|
+
try {
|
|
149
|
+
db.insert(users).values([
|
|
150
|
+
{ id: "u1", name: "Ada" },
|
|
151
|
+
{ id: "u2", name: "Bob" },
|
|
152
|
+
{ id: "u3", name: "Carol" },
|
|
153
|
+
]).run();
|
|
154
|
+
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" });
|
|
157
|
+
assert.equal(rows.length, 3);
|
|
158
|
+
const logs = db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
|
|
159
|
+
assert.equal(logs.length, 3);
|
|
160
|
+
for (const log of logs) {
|
|
161
|
+
assert.equal(log.operation, "UPDATE");
|
|
162
|
+
assert.equal(log.user_id, "admin");
|
|
163
|
+
const newData = JSON.parse(log.new_data);
|
|
164
|
+
assert.equal(newData.email, "bulk@example.com");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
sqlite.close();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
test("withAudit with workspace_id", () => {
|
|
172
|
+
const sqlite = new Database(":memory:");
|
|
173
|
+
const auditLogsWithWs = d1AuditLogTable({ workspaceIdColumn: "workspace_id" });
|
|
174
|
+
const db = drizzle({ client: sqlite, schema: { auditLogs: auditLogsWithWs, users } });
|
|
175
|
+
try {
|
|
176
|
+
sqlite.exec(`
|
|
177
|
+
CREATE TABLE audit_logs (
|
|
178
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
179
|
+
table_name TEXT NOT NULL,
|
|
180
|
+
operation TEXT NOT NULL,
|
|
181
|
+
row_id TEXT,
|
|
182
|
+
user_id TEXT,
|
|
183
|
+
workspace_id TEXT,
|
|
184
|
+
old_data TEXT,
|
|
185
|
+
new_data TEXT,
|
|
186
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
187
|
+
);
|
|
188
|
+
CREATE TABLE users (
|
|
189
|
+
id TEXT PRIMARY KEY,
|
|
190
|
+
name TEXT NOT NULL,
|
|
191
|
+
email TEXT
|
|
192
|
+
);
|
|
193
|
+
`);
|
|
194
|
+
const audited = withAudit(db, auditLogsWithWs, {
|
|
195
|
+
userId: "user_1",
|
|
196
|
+
workspaceId: "ws_1",
|
|
197
|
+
});
|
|
198
|
+
audited.insert(users, { id: "u1", name: "Ada" });
|
|
199
|
+
const logs = db.select().from(auditLogsWithWs).all();
|
|
200
|
+
assert.equal(logs.length, 1);
|
|
201
|
+
assert.equal(logs[0]?.user_id, "user_1");
|
|
202
|
+
assert.equal(logs[0].workspace_id, "ws_1");
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
sqlite.close();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
test("withAudit.db gives access to raw db for non-audited ops", () => {
|
|
209
|
+
const { db, sqlite } = setupDb();
|
|
210
|
+
try {
|
|
211
|
+
const audited = withAudit(db, auditLogs, { userId: "user_1" });
|
|
212
|
+
// Direct insert — no audit
|
|
213
|
+
audited.db.insert(users).values({ id: "u1", name: "Ada" }).run();
|
|
214
|
+
const logs = db.select().from(auditLogs).all();
|
|
215
|
+
assert.equal(logs.length, 0);
|
|
216
|
+
const rows = db.select().from(users).all();
|
|
217
|
+
assert.equal(rows.length, 1);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
sqlite.close();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"d1.integration.test.d.ts","sourceRoot":"","sources":["../../test/d1.integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import { asc, eq, sql } from "drizzle-orm";
|
|
5
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
6
|
+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
7
|
+
import { createAttachD1AuditTriggersSql, createAttachD1AuditTriggersSqlWithColumns, createD1AuditInstallSql, d1AuditLogTable, d1AuditContextTable, withD1AuditedTransaction, } from "../src/d1/index.js";
|
|
8
|
+
const users = sqliteTable("users", {
|
|
9
|
+
id: text("id").primaryKey(),
|
|
10
|
+
name: text("name").notNull(),
|
|
11
|
+
});
|
|
12
|
+
const invoices = sqliteTable("invoices", {
|
|
13
|
+
invoice_id: text("invoice_id").primaryKey(),
|
|
14
|
+
amount: integer("amount").notNull(),
|
|
15
|
+
});
|
|
16
|
+
const auditLogs = d1AuditLogTable();
|
|
17
|
+
const auditContext = d1AuditContextTable();
|
|
18
|
+
function setupDb() {
|
|
19
|
+
const sqlite = new Database(":memory:");
|
|
20
|
+
const db = drizzle({ client: sqlite, schema: { auditLogs, auditContext, users, invoices } });
|
|
21
|
+
sqlite.exec(createD1AuditInstallSql());
|
|
22
|
+
sqlite.exec(`
|
|
23
|
+
CREATE TABLE users (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
name TEXT NOT NULL
|
|
26
|
+
);
|
|
27
|
+
CREATE TABLE invoices (
|
|
28
|
+
invoice_id TEXT PRIMARY KEY,
|
|
29
|
+
amount INTEGER NOT NULL
|
|
30
|
+
);
|
|
31
|
+
`);
|
|
32
|
+
sqlite.exec(createAttachD1AuditTriggersSql([
|
|
33
|
+
{ table: "users" },
|
|
34
|
+
{ table: "invoices", rowIdColumn: "invoice_id" },
|
|
35
|
+
]));
|
|
36
|
+
return { db, sqlite };
|
|
37
|
+
}
|
|
38
|
+
test("d1 auditing works end to end (without row data)", () => {
|
|
39
|
+
const { db, sqlite } = setupDb();
|
|
40
|
+
try {
|
|
41
|
+
// Insert without audit context: user_id = NULL
|
|
42
|
+
db.insert(users).values({ id: "user_0", name: "No Context" }).run();
|
|
43
|
+
const noContextLogs = db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
|
|
44
|
+
assert.equal(noContextLogs.length, 1);
|
|
45
|
+
assert.equal(noContextLogs[0]?.user_id, null);
|
|
46
|
+
assert.equal(noContextLogs[0]?.table_name, "users");
|
|
47
|
+
assert.equal(noContextLogs[0]?.operation, "INSERT");
|
|
48
|
+
assert.equal(noContextLogs[0]?.row_id, "user_0");
|
|
49
|
+
// With audit context
|
|
50
|
+
withD1AuditedTransaction(db, "user_123", (tx) => {
|
|
51
|
+
tx.insert(users).values({ id: "user_1", name: "Ada" }).run();
|
|
52
|
+
tx.update(users).set({ name: "Ada Lovelace" }).where(eq(users.id, "user_1")).run();
|
|
53
|
+
tx.insert(invoices).values({ invoice_id: "inv_1", amount: 42 }).run();
|
|
54
|
+
tx.delete(users).where(eq(users.id, "user_1")).run();
|
|
55
|
+
});
|
|
56
|
+
const logs = db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
|
|
57
|
+
assert.equal(logs.length, 5);
|
|
58
|
+
// INSERT user
|
|
59
|
+
assert.equal(logs[1]?.table_name, "users");
|
|
60
|
+
assert.equal(logs[1]?.operation, "INSERT");
|
|
61
|
+
assert.equal(logs[1]?.row_id, "user_1");
|
|
62
|
+
assert.equal(logs[1]?.user_id, "user_123");
|
|
63
|
+
// UPDATE user
|
|
64
|
+
assert.equal(logs[2]?.table_name, "users");
|
|
65
|
+
assert.equal(logs[2]?.operation, "UPDATE");
|
|
66
|
+
assert.equal(logs[2]?.row_id, "user_1");
|
|
67
|
+
assert.equal(logs[2]?.user_id, "user_123");
|
|
68
|
+
// INSERT invoice (custom rowIdColumn)
|
|
69
|
+
assert.equal(logs[3]?.table_name, "invoices");
|
|
70
|
+
assert.equal(logs[3]?.operation, "INSERT");
|
|
71
|
+
assert.equal(logs[3]?.row_id, "inv_1");
|
|
72
|
+
// DELETE user
|
|
73
|
+
assert.equal(logs[4]?.table_name, "users");
|
|
74
|
+
assert.equal(logs[4]?.operation, "DELETE");
|
|
75
|
+
assert.equal(logs[4]?.row_id, "user_1");
|
|
76
|
+
assert.equal(logs[4]?.user_id, "user_123");
|
|
77
|
+
// Context table should be clean after transaction
|
|
78
|
+
const contextRows = db.select().from(auditContext).all();
|
|
79
|
+
assert.equal(contextRows.length, 0);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
sqlite.close();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
test("d1 column-aware triggers capture full row data", () => {
|
|
86
|
+
const sqlite = new Database(":memory:");
|
|
87
|
+
const db = drizzle({ client: sqlite, schema: { auditLogs, auditContext, users } });
|
|
88
|
+
try {
|
|
89
|
+
sqlite.exec(createD1AuditInstallSql());
|
|
90
|
+
sqlite.exec(`
|
|
91
|
+
CREATE TABLE users (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
name TEXT NOT NULL
|
|
94
|
+
);
|
|
95
|
+
`);
|
|
96
|
+
sqlite.exec(createAttachD1AuditTriggersSqlWithColumns([
|
|
97
|
+
{ table: "users", columns: ["id", "name"] },
|
|
98
|
+
]));
|
|
99
|
+
withD1AuditedTransaction(db, "user_1", (tx) => {
|
|
100
|
+
tx.insert(users).values({ id: "u1", name: "Ada" }).run();
|
|
101
|
+
tx.update(users).set({ name: "Ada Lovelace" }).where(eq(users.id, "u1")).run();
|
|
102
|
+
tx.delete(users).where(eq(users.id, "u1")).run();
|
|
103
|
+
});
|
|
104
|
+
const logs = db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
|
|
105
|
+
assert.equal(logs.length, 3);
|
|
106
|
+
// INSERT: new_data captured
|
|
107
|
+
assert.equal(logs[0]?.operation, "INSERT");
|
|
108
|
+
assert.deepEqual(JSON.parse(logs[0]?.new_data), { id: "u1", name: "Ada" });
|
|
109
|
+
assert.equal(logs[0]?.old_data, null);
|
|
110
|
+
// UPDATE: old_data and new_data captured
|
|
111
|
+
assert.equal(logs[1]?.operation, "UPDATE");
|
|
112
|
+
assert.deepEqual(JSON.parse(logs[1]?.old_data), { id: "u1", name: "Ada" });
|
|
113
|
+
assert.deepEqual(JSON.parse(logs[1]?.new_data), { id: "u1", name: "Ada Lovelace" });
|
|
114
|
+
// DELETE: old_data captured
|
|
115
|
+
assert.equal(logs[2]?.operation, "DELETE");
|
|
116
|
+
assert.deepEqual(JSON.parse(logs[2]?.old_data), { id: "u1", name: "Ada Lovelace" });
|
|
117
|
+
assert.equal(logs[2]?.new_data, null);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
sqlite.close();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
test("d1 workspace_id column and context are stored when enabled", () => {
|
|
124
|
+
const sqlite = new Database(":memory:");
|
|
125
|
+
const auditLogsWithWorkspace = d1AuditLogTable({ workspaceIdColumn: "workspace_id" });
|
|
126
|
+
const db = drizzle({ client: sqlite, schema: { auditLogs: auditLogsWithWorkspace, auditContext, users } });
|
|
127
|
+
try {
|
|
128
|
+
sqlite.exec(createD1AuditInstallSql({ workspaceIdColumn: "workspace_id" }));
|
|
129
|
+
sqlite.exec(`
|
|
130
|
+
CREATE TABLE users (
|
|
131
|
+
id TEXT PRIMARY KEY,
|
|
132
|
+
name TEXT NOT NULL
|
|
133
|
+
);
|
|
134
|
+
`);
|
|
135
|
+
sqlite.exec(createAttachD1AuditTriggersSql([{ table: "users" }], { workspaceIdColumn: "workspace_id" }));
|
|
136
|
+
withD1AuditedTransaction(db, "user_1", (tx) => {
|
|
137
|
+
tx.insert(users).values({ id: "u1", name: "Alice" }).run();
|
|
138
|
+
}, { workspaceId: "ws_1" });
|
|
139
|
+
const logs = db.select().from(auditLogsWithWorkspace).all();
|
|
140
|
+
assert.equal(logs.length, 1);
|
|
141
|
+
assert.equal(logs[0]?.user_id, "user_1");
|
|
142
|
+
assert.equal(logs[0].workspace_id, "ws_1");
|
|
143
|
+
// Without workspace
|
|
144
|
+
withD1AuditedTransaction(db, "user_2", (tx) => {
|
|
145
|
+
tx.insert(users).values({ id: "u2", name: "Bob" }).run();
|
|
146
|
+
});
|
|
147
|
+
const logs2 = db
|
|
148
|
+
.select()
|
|
149
|
+
.from(auditLogsWithWorkspace)
|
|
150
|
+
.orderBy(asc(auditLogsWithWorkspace.id))
|
|
151
|
+
.all();
|
|
152
|
+
assert.equal(logs2.length, 2);
|
|
153
|
+
assert.equal(logs2[1]?.user_id, "user_2");
|
|
154
|
+
assert.equal(logs2[1].workspace_id, null);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
sqlite.close();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
test("d1 writes without audit context produce rows with user_id = NULL", () => {
|
|
161
|
+
const { db, sqlite } = setupDb();
|
|
162
|
+
try {
|
|
163
|
+
db.insert(users).values({ id: "u1", name: "Alice" }).run();
|
|
164
|
+
db.update(users).set({ name: "Alice Updated" }).where(eq(users.id, "u1")).run();
|
|
165
|
+
db.delete(users).where(eq(users.id, "u1")).run();
|
|
166
|
+
const logs = db.select().from(auditLogs).orderBy(asc(auditLogs.id)).all();
|
|
167
|
+
assert.equal(logs.length, 3);
|
|
168
|
+
assert.equal(logs[0]?.operation, "INSERT");
|
|
169
|
+
assert.equal(logs[0]?.user_id, null);
|
|
170
|
+
assert.equal(logs[0]?.row_id, "u1");
|
|
171
|
+
assert.equal(logs[1]?.operation, "UPDATE");
|
|
172
|
+
assert.equal(logs[1]?.user_id, null);
|
|
173
|
+
assert.equal(logs[2]?.operation, "DELETE");
|
|
174
|
+
assert.equal(logs[2]?.user_id, null);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
sqlite.close();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
test("d1 trigger SQL handles table names with special characters", () => {
|
|
181
|
+
const sqlite = new Database(":memory:");
|
|
182
|
+
const db = drizzle({ client: sqlite, schema: { auditLogs, auditContext } });
|
|
183
|
+
try {
|
|
184
|
+
sqlite.exec(createD1AuditInstallSql());
|
|
185
|
+
// Table name with a single quote — the quoteLiteral fix prevents SQL breakage
|
|
186
|
+
const tableName = "user's_data";
|
|
187
|
+
sqlite.exec(`CREATE TABLE "${tableName}" (id TEXT PRIMARY KEY, val TEXT);`);
|
|
188
|
+
sqlite.exec(createAttachD1AuditTriggersSql([{ table: tableName }]));
|
|
189
|
+
// Seed context and insert
|
|
190
|
+
withD1AuditedTransaction(db, "tester", (tx) => {
|
|
191
|
+
tx.run(sql `INSERT INTO "${sql.raw(tableName)}" (id, val) VALUES ('r1', 'hello')`);
|
|
192
|
+
});
|
|
193
|
+
const logs = db.select().from(auditLogs).all();
|
|
194
|
+
assert.equal(logs.length, 1);
|
|
195
|
+
assert.equal(logs[0]?.table_name, tableName);
|
|
196
|
+
assert.equal(logs[0]?.row_id, "r1");
|
|
197
|
+
assert.equal(logs[0]?.user_id, "tester");
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
sqlite.close();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
test("d1 column-aware triggers handle column names with special characters", () => {
|
|
204
|
+
const sqlite = new Database(":memory:");
|
|
205
|
+
const db = drizzle({ client: sqlite, schema: { auditLogs, auditContext } });
|
|
206
|
+
try {
|
|
207
|
+
sqlite.exec(createD1AuditInstallSql());
|
|
208
|
+
sqlite.exec(`CREATE TABLE items (id TEXT PRIMARY KEY, "user's name" TEXT);`);
|
|
209
|
+
sqlite.exec(createAttachD1AuditTriggersSqlWithColumns([
|
|
210
|
+
{ table: "items", columns: ["id", "user's name"] },
|
|
211
|
+
]));
|
|
212
|
+
withD1AuditedTransaction(db, "tester", (tx) => {
|
|
213
|
+
tx.run(sql `INSERT INTO items (id, "user's name") VALUES ('i1', 'Ada')`);
|
|
214
|
+
});
|
|
215
|
+
const logs = db.select().from(auditLogs).all();
|
|
216
|
+
assert.equal(logs.length, 1);
|
|
217
|
+
const newData = JSON.parse(logs[0]?.new_data);
|
|
218
|
+
assert.equal(newData["user's name"], "Ada");
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
sqlite.close();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.integration.test.d.ts","sourceRoot":"","sources":["../../test/postgres.integration.test.ts"],"names":[],"mappings":""}
|