@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,130 @@
1
+ import { getTableColumns, getTableName, } from "drizzle-orm";
2
+ function getPrimaryKeyColumn(table) {
3
+ const cols = getTableColumns(table);
4
+ const pk = Object.values(cols).find((c) => c.primary);
5
+ return pk ?? null;
6
+ }
7
+ function getRowId(row, pk) {
8
+ if (!pk)
9
+ return null;
10
+ const val = row[pk.name];
11
+ return val != null ? String(val) : null;
12
+ }
13
+ /**
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.
17
+ *
18
+ * @param db - A Drizzle SQLite database instance (D1, better-sqlite3, libsql)
19
+ * @param auditTable - The Drizzle table definition for audit_logs
20
+ * @param context - The audit context (userId, optional workspaceId)
21
+ *
22
+ * @example
23
+ * import { withAudit } from "drizzle-audit/d1-runtime"
24
+ * import { d1AuditLogTable } from "drizzle-audit/d1"
25
+ *
26
+ * const auditLogs = d1AuditLogTable()
27
+ * const audited = withAudit(db, auditLogs, { userId: session.userId })
28
+ *
29
+ * // Audited insert
30
+ * audited.insert(users, { id: "u1", name: "Ada" })
31
+ *
32
+ * // Audited update — captures old + new data
33
+ * audited.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" })
34
+ *
35
+ * // Audited delete — captures deleted data
36
+ * audited.delete(users, eq(users.id, "u1"))
37
+ *
38
+ * // Non-audited access
39
+ * audited.db.select().from(users).all()
40
+ */
41
+ export function withAudit(db, auditTable, context) {
42
+ const workspaceIdColumn = (() => {
43
+ const cols = getTableColumns(auditTable);
44
+ const known = new Set([
45
+ "id",
46
+ "table_name",
47
+ "operation",
48
+ "row_id",
49
+ "user_id",
50
+ "old_data",
51
+ "new_data",
52
+ "created_at",
53
+ ]);
54
+ const extra = Object.keys(cols).find((k) => !known.has(k));
55
+ return extra ?? null;
56
+ })();
57
+ function buildAuditRow(tableName, operation, rowId, oldData, newData) {
58
+ return {
59
+ table_name: tableName,
60
+ operation,
61
+ row_id: rowId,
62
+ user_id: context.userId,
63
+ old_data: oldData ? JSON.stringify(oldData) : null,
64
+ new_data: newData ? JSON.stringify(newData) : null,
65
+ ...(workspaceIdColumn && context.workspaceId
66
+ ? { [workspaceIdColumn]: context.workspaceId }
67
+ : {}),
68
+ };
69
+ }
70
+ return {
71
+ db,
72
+ insert(table, data) {
73
+ const tableName = getTableName(table);
74
+ 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
+ });
83
+ },
84
+ update(table, where, data) {
85
+ const tableName = getTableName(table);
86
+ 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
+ });
109
+ },
110
+ delete(table, where) {
111
+ const tableName = getTableName(table);
112
+ 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
+ });
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./postgres/index.js";
2
+ export * from "./d1/index.js";
3
+ export * from "./d1-runtime/index.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,3 @@
1
+ export * from "./postgres/index.js";
2
+ export * from "./d1/index.js";
3
+ export * from "./d1-runtime/index.js";
@@ -0,0 +1,140 @@
1
+ export type PgAuditLogTableOptions = {
2
+ /** When set (e.g. "workspace_id"), the table definition includes this optional column to match the install. */
3
+ workspaceIdColumn?: string;
4
+ };
5
+ export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): import("drizzle-orm/pg-core").PgTableWithColumns<{
6
+ name: "audit_logs";
7
+ schema: undefined;
8
+ columns: {
9
+ old_data: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").PgJsonbBuilder, {
10
+ name: string;
11
+ tableName: "audit_logs";
12
+ dataType: "object json";
13
+ data: unknown;
14
+ driverParam: unknown;
15
+ notNull: false;
16
+ hasDefault: false;
17
+ isPrimaryKey: false;
18
+ isAutoincrement: false;
19
+ hasRuntimeDefault: false;
20
+ enumValues: undefined;
21
+ identity: undefined;
22
+ generated: undefined;
23
+ insertType: unknown;
24
+ }>;
25
+ new_data: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").PgJsonbBuilder, {
26
+ name: string;
27
+ tableName: "audit_logs";
28
+ dataType: "object json";
29
+ data: unknown;
30
+ driverParam: unknown;
31
+ notNull: false;
32
+ hasDefault: false;
33
+ isPrimaryKey: false;
34
+ isAutoincrement: false;
35
+ hasRuntimeDefault: false;
36
+ enumValues: undefined;
37
+ identity: undefined;
38
+ generated: undefined;
39
+ insertType: unknown;
40
+ }>;
41
+ 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
+ name: string;
43
+ tableName: "audit_logs";
44
+ dataType: "object date";
45
+ data: Date;
46
+ driverParam: string;
47
+ notNull: true;
48
+ hasDefault: true;
49
+ isPrimaryKey: false;
50
+ isAutoincrement: false;
51
+ hasRuntimeDefault: false;
52
+ enumValues: undefined;
53
+ identity: undefined;
54
+ generated: undefined;
55
+ insertType: Date | undefined;
56
+ }>;
57
+ id: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").SetIsPrimaryKey<import("drizzle-orm/pg-core").PgBigSerial53Builder>, {
58
+ name: string;
59
+ tableName: "audit_logs";
60
+ dataType: "number int53";
61
+ data: number;
62
+ driverParam: number;
63
+ notNull: true;
64
+ hasDefault: true;
65
+ isPrimaryKey: false;
66
+ isAutoincrement: false;
67
+ hasRuntimeDefault: false;
68
+ enumValues: undefined;
69
+ identity: undefined;
70
+ generated: undefined;
71
+ insertType: number | undefined;
72
+ }>;
73
+ table_name: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<undefined>>, {
74
+ name: string;
75
+ tableName: "audit_logs";
76
+ dataType: "string";
77
+ data: string;
78
+ driverParam: string;
79
+ notNull: true;
80
+ hasDefault: false;
81
+ isPrimaryKey: false;
82
+ isAutoincrement: false;
83
+ hasRuntimeDefault: false;
84
+ enumValues: undefined;
85
+ identity: undefined;
86
+ generated: undefined;
87
+ insertType: string;
88
+ }>;
89
+ operation: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<undefined>>, {
90
+ name: string;
91
+ tableName: "audit_logs";
92
+ dataType: "string";
93
+ data: string;
94
+ driverParam: string;
95
+ notNull: true;
96
+ hasDefault: false;
97
+ isPrimaryKey: false;
98
+ isAutoincrement: false;
99
+ hasRuntimeDefault: false;
100
+ enumValues: undefined;
101
+ identity: undefined;
102
+ generated: undefined;
103
+ insertType: string;
104
+ }>;
105
+ row_id: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").PgTextBuilder<undefined>, {
106
+ name: string;
107
+ tableName: "audit_logs";
108
+ dataType: "string";
109
+ data: string;
110
+ driverParam: string;
111
+ notNull: false;
112
+ hasDefault: false;
113
+ isPrimaryKey: false;
114
+ isAutoincrement: false;
115
+ hasRuntimeDefault: false;
116
+ enumValues: undefined;
117
+ identity: undefined;
118
+ generated: undefined;
119
+ insertType: string | null | undefined;
120
+ }>;
121
+ user_id: import("drizzle-orm/pg-core").PgBuildColumn<"audit_logs", import("drizzle-orm/pg-core").PgTextBuilder<undefined>, {
122
+ name: string;
123
+ tableName: "audit_logs";
124
+ dataType: "string";
125
+ data: string;
126
+ driverParam: string;
127
+ notNull: false;
128
+ hasDefault: false;
129
+ isPrimaryKey: false;
130
+ isAutoincrement: false;
131
+ hasRuntimeDefault: false;
132
+ enumValues: undefined;
133
+ identity: undefined;
134
+ generated: undefined;
135
+ insertType: string | null | undefined;
136
+ }>;
137
+ };
138
+ dialect: "pg";
139
+ }>;
140
+ //# sourceMappingURL=audit-log-schema.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,25 @@
1
+ import { bigserial, index, jsonb, pgTable, text, timestamp, } from "drizzle-orm/pg-core";
2
+ export function pgAuditLogTable(options) {
3
+ const workspaceIdColumn = options?.workspaceIdColumn?.trim();
4
+ const columns = {
5
+ id: bigserial("id", { mode: "number" }).primaryKey(),
6
+ table_name: text("table_name").notNull(),
7
+ operation: text("operation").notNull(),
8
+ row_id: text("row_id"),
9
+ user_id: text("user_id"),
10
+ ...(workspaceIdColumn
11
+ ? { [workspaceIdColumn]: text(workspaceIdColumn) }
12
+ : {}),
13
+ old_data: jsonb("old_data"),
14
+ new_data: jsonb("new_data"),
15
+ created_at: timestamp("created_at", { withTimezone: true })
16
+ .defaultNow()
17
+ .notNull(),
18
+ };
19
+ return pgTable("audit_logs", columns, (table) => [
20
+ index("audit_logs_table_name_idx").on(table.table_name),
21
+ index("audit_logs_row_id_idx").on(table.row_id),
22
+ index("audit_logs_user_id_idx").on(table.user_id),
23
+ index("audit_logs_created_at_idx").on(table.created_at),
24
+ ]);
25
+ }
@@ -0,0 +1,6 @@
1
+ export { pgAuditLogTable } from "./audit-log-schema.js";
2
+ export type { PgAuditLogTableOptions } from "./audit-log-schema.js";
3
+ export { createAttachAuditTriggerSql, createAttachAuditTriggersSql, createAuditAddWorkspaceColumnSql, createAuditInstallSql, } from "./sql.js";
4
+ export { setAuditContext, withAuditedTransaction } from "./runtime.js";
5
+ export type { AuditInstallOptions, AuditSqlExecutor, AuditTransactionCapable, AuditTriggerTarget, } from "./types.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/postgres/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,EACL,2BAA2B,EAC3B,4BAA4B,EAC5B,gCAAgC,EAChC,qBAAqB,GACtB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAEtE,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,YAAY,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { pgAuditLogTable } from "./audit-log-schema.js";
2
+ export { createAttachAuditTriggerSql, createAttachAuditTriggersSql, createAuditAddWorkspaceColumnSql, createAuditInstallSql, } from "./sql.js";
3
+ export { setAuditContext, withAuditedTransaction } from "./runtime.js";
@@ -0,0 +1,10 @@
1
+ import type { AuditSqlExecutor, AuditTransactionCapable } from "./types.js";
2
+ export declare function setAuditContext(db: AuditSqlExecutor, actorId: string, contextKey?: string, options?: {
3
+ workspaceId?: string;
4
+ workspaceContextKey?: string;
5
+ }): Promise<void>;
6
+ export declare function withAuditedTransaction<TTransaction extends AuditSqlExecutor, TResult>(db: AuditTransactionCapable<TTransaction>, actorId: string, callback: (tx: TTransaction) => Promise<TResult> | TResult, contextKey?: string, options?: {
7
+ workspaceId?: string;
8
+ workspaceContextKey?: string;
9
+ }): Promise<TResult>;
10
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/postgres/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACxB,MAAM,YAAY,CAAA;AAQnB,wBAAsB,eAAe,CACnC,EAAE,EAAE,gBAAgB,EACpB,OAAO,EAAE,MAAM,EACf,UAAU,SAAgB,EAC1B,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAAE,iBAajE;AAED,wBAAsB,sBAAsB,CAC1C,YAAY,SAAS,gBAAgB,EACrC,OAAO,EAEP,EAAE,EAAE,uBAAuB,CAAC,YAAY,CAAC,EACzC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,EAC1D,UAAU,SAAgB,EAC1B,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAAE,oBAQjE"}
@@ -0,0 +1,21 @@
1
+ import { sql } from "drizzle-orm";
2
+ function assertActorId(actorId) {
3
+ if (actorId.trim().length === 0) {
4
+ throw new Error("actorId must not be empty");
5
+ }
6
+ }
7
+ export async function setAuditContext(db, actorId, contextKey = "app.user_id", options) {
8
+ assertActorId(actorId);
9
+ await db.execute(sql `select set_config(${contextKey}, ${actorId}, true) as audit_context`);
10
+ if (options?.workspaceId !== undefined && options.workspaceId !== "") {
11
+ const wsKey = options.workspaceContextKey ?? "app.workspace_id";
12
+ await db.execute(sql `select set_config(${wsKey}, ${options.workspaceId}, true) as workspace_context`);
13
+ }
14
+ }
15
+ export async function withAuditedTransaction(db, actorId, callback, contextKey = "app.user_id", options) {
16
+ assertActorId(actorId);
17
+ return db.transaction(async (tx) => {
18
+ await setAuditContext(tx, actorId, contextKey, options);
19
+ return await callback(tx);
20
+ });
21
+ }
@@ -0,0 +1,14 @@
1
+ import type { AuditInstallOptions, AuditTriggerTarget } from "./types.js";
2
+ export declare function createAuditInstallSql(options?: AuditInstallOptions): string;
3
+ export declare function createAttachAuditTriggerSql(target: AuditTriggerTarget, options?: AuditInstallOptions): string;
4
+ export declare function createAttachAuditTriggersSql(targets: AuditTriggerTarget[], options?: AuditInstallOptions): string;
5
+ /**
6
+ * Generates SQL to add the workspace column and replace the trigger function on an
7
+ * existing audit_logs table. Use in a new migration when adding workspace_id after
8
+ * the initial install. Options must match your install (auditSchema, auditTable,
9
+ * triggerFunctionName, contextKey, workspaceIdColumn).
10
+ */
11
+ export declare function createAuditAddWorkspaceColumnSql(options: AuditInstallOptions & {
12
+ workspaceIdColumn: string;
13
+ }): string;
14
+ //# sourceMappingURL=sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../../src/postgres/sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAqHzE,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,mBAAwB,UA6DtE;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,kBAAkB,EAC1B,OAAO,GAAE,mBAAwB,UAiClC;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,GAAE,mBAAwB,UASlC;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,mBAAmB,GAAG;IAAE,iBAAiB,EAAE,MAAM,CAAA;CAAE,UAkC7D"}
@@ -0,0 +1,190 @@
1
+ const DEFAULT_AUDIT_SCHEMA = "public";
2
+ const DEFAULT_AUDIT_TABLE = "audit_logs";
3
+ const DEFAULT_CONTEXT_KEY = "app.user_id";
4
+ const DEFAULT_TRIGGER_FUNCTION = "drizzle_audit_trigger";
5
+ const DEFAULT_ROW_ID_COLUMN = "id";
6
+ function quoteIdent(value) {
7
+ return `"${value.replaceAll('"', '""')}"`;
8
+ }
9
+ function quoteLiteral(value) {
10
+ return `'${value.replaceAll("'", "''")}'`;
11
+ }
12
+ function qualifyName(name, schema) {
13
+ if (!schema) {
14
+ return quoteIdent(name);
15
+ }
16
+ return `${quoteIdent(schema)}.${quoteIdent(name)}`;
17
+ }
18
+ function assertNonEmpty(value, label) {
19
+ if (value.trim().length === 0) {
20
+ throw new Error(`${label} must not be empty`);
21
+ }
22
+ return value;
23
+ }
24
+ function buildTriggerFunctionSql(qualifiedAuditTable, qualifiedTriggerFunction, contextLiteral, options) {
25
+ const workspaceIdColumn = options.workspaceIdColumn?.trim();
26
+ const hasWorkspace = Boolean(workspaceIdColumn);
27
+ const workspaceContextLiteral = hasWorkspace
28
+ ? quoteLiteral("app." + workspaceIdColumn)
29
+ : "";
30
+ const workspaceCol = hasWorkspace ? quoteIdent(workspaceIdColumn) : "";
31
+ const declWorkspace = hasWorkspace ? "\n audit_workspace TEXT;" : "";
32
+ const readWorkspace = hasWorkspace
33
+ ? `\n audit_workspace := NULLIF(current_setting(${workspaceContextLiteral}, true), '');`
34
+ : "";
35
+ const insertColsBase = "table_name, operation, row_id, user_id";
36
+ const insertColsInsert = hasWorkspace
37
+ ? `${insertColsBase}, ${workspaceCol}, new_data`
38
+ : `${insertColsBase}, new_data`;
39
+ const insertColsUpdate = hasWorkspace
40
+ ? `${insertColsBase}, ${workspaceCol}, old_data, new_data`
41
+ : `${insertColsBase}, old_data, new_data`;
42
+ const insertColsDelete = hasWorkspace
43
+ ? `${insertColsBase}, ${workspaceCol}, old_data`
44
+ : `${insertColsBase}, old_data`;
45
+ const valuesInsert = hasWorkspace
46
+ ? `TG_TABLE_NAME, TG_OP, current_row_id, audit_user, audit_workspace, to_jsonb(NEW)`
47
+ : `TG_TABLE_NAME, TG_OP, current_row_id, audit_user, to_jsonb(NEW)`;
48
+ const valuesUpdate = hasWorkspace
49
+ ? `TG_TABLE_NAME, TG_OP, current_row_id, audit_user, audit_workspace, to_jsonb(OLD), to_jsonb(NEW)`
50
+ : `TG_TABLE_NAME, TG_OP, current_row_id, audit_user, to_jsonb(OLD), to_jsonb(NEW)`;
51
+ const valuesDelete = hasWorkspace
52
+ ? `TG_TABLE_NAME, TG_OP, current_row_id, audit_user, audit_workspace, to_jsonb(OLD)`
53
+ : `TG_TABLE_NAME, TG_OP, current_row_id, audit_user, to_jsonb(OLD)`;
54
+ return `
55
+ CREATE OR REPLACE FUNCTION ${qualifiedTriggerFunction}()
56
+ RETURNS TRIGGER AS $$
57
+ DECLARE
58
+ audit_user TEXT;${declWorkspace}
59
+ row_id_column TEXT;
60
+ current_row JSONB;
61
+ current_row_id TEXT;
62
+ BEGIN
63
+ audit_user := NULLIF(current_setting(${contextLiteral}, true), '');${readWorkspace}
64
+
65
+ row_id_column := COALESCE(NULLIF(TG_ARGV[0], ''), ${quoteLiteral(DEFAULT_ROW_ID_COLUMN)});
66
+
67
+ IF TG_OP = 'DELETE' THEN
68
+ current_row := to_jsonb(OLD);
69
+ ELSE
70
+ current_row := to_jsonb(NEW);
71
+ END IF;
72
+
73
+ IF NOT (current_row ? row_id_column) THEN
74
+ RAISE EXCEPTION 'Missing row id column "%" on audited table %.%.',
75
+ row_id_column,
76
+ TG_TABLE_SCHEMA,
77
+ TG_TABLE_NAME;
78
+ END IF;
79
+
80
+ current_row_id := current_row ->> row_id_column;
81
+
82
+ IF TG_OP = 'INSERT' THEN
83
+ INSERT INTO ${qualifiedAuditTable} (${insertColsInsert})
84
+ VALUES (${valuesInsert});
85
+ RETURN NEW;
86
+ END IF;
87
+
88
+ IF TG_OP = 'UPDATE' THEN
89
+ INSERT INTO ${qualifiedAuditTable} (${insertColsUpdate})
90
+ VALUES (${valuesUpdate});
91
+ RETURN NEW;
92
+ END IF;
93
+
94
+ INSERT INTO ${qualifiedAuditTable} (${insertColsDelete})
95
+ VALUES (${valuesDelete});
96
+ RETURN OLD;
97
+ END;
98
+ $$ LANGUAGE plpgsql;`.trim();
99
+ }
100
+ export function createAuditInstallSql(options = {}) {
101
+ const auditSchema = assertNonEmpty(options.auditSchema ?? DEFAULT_AUDIT_SCHEMA, "auditSchema");
102
+ const auditTable = assertNonEmpty(options.auditTable ?? DEFAULT_AUDIT_TABLE, "auditTable");
103
+ const contextKey = assertNonEmpty(options.contextKey ?? DEFAULT_CONTEXT_KEY, "contextKey");
104
+ const triggerFunctionName = assertNonEmpty(options.triggerFunctionName ?? DEFAULT_TRIGGER_FUNCTION, "triggerFunctionName");
105
+ const workspaceIdColumn = options.workspaceIdColumn?.trim();
106
+ const qualifiedAuditTable = qualifyName(auditTable, auditSchema);
107
+ const qualifiedTriggerFunction = qualifyName(triggerFunctionName, auditSchema);
108
+ const contextLiteral = quoteLiteral(contextKey);
109
+ const tableColumns = [
110
+ "id BIGSERIAL PRIMARY KEY",
111
+ "table_name TEXT NOT NULL",
112
+ "operation TEXT NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE'))",
113
+ "row_id TEXT",
114
+ "user_id TEXT",
115
+ ...(workspaceIdColumn ? [quoteIdent(workspaceIdColumn) + " TEXT"] : []),
116
+ "old_data JSONB",
117
+ "new_data JSONB",
118
+ "created_at TIMESTAMPTZ NOT NULL DEFAULT now()",
119
+ ];
120
+ const indexStatements = [
121
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_table_name_idx`)} ON ${qualifiedAuditTable} (table_name);`,
122
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_row_id_idx`)} ON ${qualifiedAuditTable} (row_id);`,
123
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_user_id_idx`)} ON ${qualifiedAuditTable} (user_id);`,
124
+ ...(workspaceIdColumn
125
+ ? [
126
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_${workspaceIdColumn}_idx`)} ON ${qualifiedAuditTable} (${quoteIdent(workspaceIdColumn)});`,
127
+ ]
128
+ : []),
129
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_created_at_idx`)} ON ${qualifiedAuditTable} (created_at DESC);`,
130
+ ];
131
+ return [
132
+ `CREATE SCHEMA IF NOT EXISTS ${quoteIdent(auditSchema)};`,
133
+ `
134
+ CREATE TABLE IF NOT EXISTS ${qualifiedAuditTable} (
135
+ ${tableColumns.join(",\n ")}
136
+ );`.trim(),
137
+ ...indexStatements,
138
+ buildTriggerFunctionSql(qualifiedAuditTable, qualifiedTriggerFunction, contextLiteral, { workspaceIdColumn: workspaceIdColumn ?? undefined }),
139
+ ].join("\n\n");
140
+ }
141
+ export function createAttachAuditTriggerSql(target, options = {}) {
142
+ const table = assertNonEmpty(target.table, "table");
143
+ const schema = target.schema?.trim() || undefined;
144
+ const rowIdColumn = assertNonEmpty(target.rowIdColumn ?? DEFAULT_ROW_ID_COLUMN, "rowIdColumn");
145
+ const triggerName = assertNonEmpty(target.triggerName ?? `${table}_audit`, "triggerName");
146
+ const auditSchema = assertNonEmpty(options.auditSchema ?? DEFAULT_AUDIT_SCHEMA, "auditSchema");
147
+ const triggerFunctionName = assertNonEmpty(options.triggerFunctionName ?? DEFAULT_TRIGGER_FUNCTION, "triggerFunctionName");
148
+ const qualifiedTable = qualifyName(table, schema);
149
+ const qualifiedTriggerFunction = qualifyName(triggerFunctionName, auditSchema);
150
+ return `
151
+ DROP TRIGGER IF EXISTS ${quoteIdent(triggerName)} ON ${qualifiedTable};
152
+
153
+ CREATE TRIGGER ${quoteIdent(triggerName)}
154
+ AFTER INSERT OR UPDATE OR DELETE
155
+ ON ${qualifiedTable}
156
+ FOR EACH ROW
157
+ EXECUTE FUNCTION ${qualifiedTriggerFunction}(${quoteLiteral(rowIdColumn)});
158
+ `.trim();
159
+ }
160
+ export function createAttachAuditTriggersSql(targets, options = {}) {
161
+ if (targets.length === 0) {
162
+ throw new Error("targets must contain at least one audited table");
163
+ }
164
+ return targets
165
+ .map((target) => createAttachAuditTriggerSql(target, options))
166
+ .join("\n\n");
167
+ }
168
+ /**
169
+ * Generates SQL to add the workspace column and replace the trigger function on an
170
+ * existing audit_logs table. Use in a new migration when adding workspace_id after
171
+ * the initial install. Options must match your install (auditSchema, auditTable,
172
+ * triggerFunctionName, contextKey, workspaceIdColumn).
173
+ */
174
+ export function createAuditAddWorkspaceColumnSql(options) {
175
+ const workspaceIdColumn = assertNonEmpty(options.workspaceIdColumn.trim(), "workspaceIdColumn");
176
+ const auditSchema = assertNonEmpty(options.auditSchema ?? DEFAULT_AUDIT_SCHEMA, "auditSchema");
177
+ const auditTable = assertNonEmpty(options.auditTable ?? DEFAULT_AUDIT_TABLE, "auditTable");
178
+ const contextKey = assertNonEmpty(options.contextKey ?? DEFAULT_CONTEXT_KEY, "contextKey");
179
+ const triggerFunctionName = assertNonEmpty(options.triggerFunctionName ?? DEFAULT_TRIGGER_FUNCTION, "triggerFunctionName");
180
+ const qualifiedAuditTable = qualifyName(auditTable, auditSchema);
181
+ const qualifiedTriggerFunction = qualifyName(triggerFunctionName, auditSchema);
182
+ const contextLiteral = quoteLiteral(contextKey);
183
+ return [
184
+ `ALTER TABLE ${qualifiedAuditTable} ADD COLUMN IF NOT EXISTS ${quoteIdent(workspaceIdColumn)} TEXT;`,
185
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_${workspaceIdColumn}_idx`)} ON ${qualifiedAuditTable} (${quoteIdent(workspaceIdColumn)});`,
186
+ buildTriggerFunctionSql(qualifiedAuditTable, qualifiedTriggerFunction, contextLiteral, {
187
+ workspaceIdColumn,
188
+ }),
189
+ ].join("\n\n");
190
+ }
@@ -0,0 +1,22 @@
1
+ import type { SQL } from "drizzle-orm";
2
+ export type AuditSqlExecutor = {
3
+ execute: (query: SQL) => Promise<unknown>;
4
+ };
5
+ export type AuditTransactionCapable<TTransaction extends AuditSqlExecutor> = {
6
+ transaction: <TResult>(callback: (tx: TTransaction) => Promise<TResult>) => Promise<TResult>;
7
+ };
8
+ export type AuditInstallOptions = {
9
+ auditSchema?: string;
10
+ auditTable?: string;
11
+ contextKey?: string;
12
+ triggerFunctionName?: string;
13
+ /** When set (e.g. "workspace_id"), the audit table and trigger include this column; trigger reads from session variable app.${workspaceIdColumn}. */
14
+ workspaceIdColumn?: string;
15
+ };
16
+ export type AuditTriggerTarget = {
17
+ table: string;
18
+ schema?: string;
19
+ rowIdColumn?: string;
20
+ triggerName?: string;
21
+ };
22
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/postgres/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAEtC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC1C,CAAA;AAED,MAAM,MAAM,uBAAuB,CAAC,YAAY,SAAS,gBAAgB,IAAI;IAC3E,WAAW,EAAE,CAAC,OAAO,EACnB,QAAQ,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,KAC7C,OAAO,CAAC,OAAO,CAAC,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,qJAAqJ;IACrJ,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=d1-runtime.integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-runtime.integration.test.d.ts","sourceRoot":"","sources":["../../test/d1-runtime.integration.test.ts"],"names":[],"mappings":""}