@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.
Files changed (56) hide show
  1. package/README.md +278 -0
  2. package/dist/src/cli/generate-migration.d.ts +14 -0
  3. package/dist/src/cli/generate-migration.d.ts.map +1 -0
  4. package/dist/src/cli/generate-migration.js +118 -0
  5. package/dist/src/cli/runner.d.ts +2 -0
  6. package/dist/src/cli/runner.d.ts.map +1 -0
  7. package/dist/src/cli/runner.js +23 -0
  8. package/dist/src/d1/audit-log-schema.d.ts +181 -0
  9. package/dist/src/d1/audit-log-schema.d.ts.map +1 -0
  10. package/dist/src/d1/audit-log-schema.js +30 -0
  11. package/dist/src/d1/index.d.ts +7 -0
  12. package/dist/src/d1/index.d.ts.map +1 -0
  13. package/dist/src/d1/index.js +3 -0
  14. package/dist/src/d1/runtime.d.ts +44 -0
  15. package/dist/src/d1/runtime.d.ts.map +1 -0
  16. package/dist/src/d1/runtime.js +68 -0
  17. package/dist/src/d1/sql.d.ts +37 -0
  18. package/dist/src/d1/sql.d.ts.map +1 -0
  19. package/dist/src/d1/sql.js +261 -0
  20. package/dist/src/d1/types.d.ts +22 -0
  21. package/dist/src/d1/types.d.ts.map +1 -0
  22. package/dist/src/d1/types.js +1 -0
  23. package/dist/src/d1-runtime/index.d.ts +3 -0
  24. package/dist/src/d1-runtime/index.d.ts.map +1 -0
  25. package/dist/src/d1-runtime/index.js +1 -0
  26. package/dist/src/d1-runtime/with-audit.d.ts +77 -0
  27. package/dist/src/d1-runtime/with-audit.d.ts.map +1 -0
  28. package/dist/src/d1-runtime/with-audit.js +130 -0
  29. package/dist/src/index.d.ts +4 -0
  30. package/dist/src/index.d.ts.map +1 -0
  31. package/dist/src/index.js +3 -0
  32. package/dist/src/postgres/audit-log-schema.d.ts +140 -0
  33. package/dist/src/postgres/audit-log-schema.d.ts.map +1 -0
  34. package/dist/src/postgres/audit-log-schema.js +25 -0
  35. package/dist/src/postgres/index.d.ts +6 -0
  36. package/dist/src/postgres/index.d.ts.map +1 -0
  37. package/dist/src/postgres/index.js +3 -0
  38. package/dist/src/postgres/runtime.d.ts +10 -0
  39. package/dist/src/postgres/runtime.d.ts.map +1 -0
  40. package/dist/src/postgres/runtime.js +21 -0
  41. package/dist/src/postgres/sql.d.ts +14 -0
  42. package/dist/src/postgres/sql.d.ts.map +1 -0
  43. package/dist/src/postgres/sql.js +190 -0
  44. package/dist/src/postgres/types.d.ts +22 -0
  45. package/dist/src/postgres/types.d.ts.map +1 -0
  46. package/dist/src/postgres/types.js +1 -0
  47. package/dist/test/d1-runtime.integration.test.d.ts +2 -0
  48. package/dist/test/d1-runtime.integration.test.d.ts.map +1 -0
  49. package/dist/test/d1-runtime.integration.test.js +222 -0
  50. package/dist/test/d1.integration.test.d.ts +2 -0
  51. package/dist/test/d1.integration.test.d.ts.map +1 -0
  52. package/dist/test/d1.integration.test.js +223 -0
  53. package/dist/test/postgres.integration.test.d.ts +2 -0
  54. package/dist/test/postgres.integration.test.d.ts.map +1 -0
  55. package/dist/test/postgres.integration.test.js +286 -0
  56. package/package.json +66 -0
@@ -0,0 +1,286 @@
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
+ });
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@willyim/drizzle-audit",
3
+ "version": "0.1.3",
4
+ "description": "Lightweight audit logging for Drizzle ORM using database triggers (Postgres + D1/SQLite)",
5
+ "type": "module",
6
+ "main": "./dist/src/index.js",
7
+ "types": "./dist/src/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/src/index.js",
10
+ "./postgres": "./dist/src/postgres/index.js",
11
+ "./d1": "./dist/src/d1/index.js",
12
+ "./d1-runtime": "./dist/src/d1-runtime/index.js"
13
+ },
14
+ "bin": {
15
+ "drizzle-audit": "./dist/src/cli/generate-migration.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "npm run build && node --test ./dist/test/**/*.test.js"
26
+ },
27
+ "keywords": [
28
+ "drizzle",
29
+ "drizzle-orm",
30
+ "audit",
31
+ "audit-log",
32
+ "postgres",
33
+ "d1",
34
+ "sqlite",
35
+ "cloudflare",
36
+ "triggers"
37
+ ],
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/wovalle/willy.im",
42
+ "directory": "packages/drizzle_audit"
43
+ },
44
+ "peerDependencies": {
45
+ "drizzle-kit": ">=1.0.0-0",
46
+ "drizzle-orm": ">=1.0.0-0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "drizzle-kit": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.19.7",
55
+ "drizzle-orm": "^1.0.0-beta.15-859cf75",
56
+ "typescript": "^5.9.3"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "optionalDependencies": {
62
+ "@electric-sql/pglite": "^0.3.16",
63
+ "@types/better-sqlite3": "^7.6.13",
64
+ "better-sqlite3": "^11.10.0"
65
+ }
66
+ }