@willyim/drizzle-audit 0.4.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.
Files changed (37) hide show
  1. package/README.md +52 -16
  2. package/dist/src/d1/audit-log-schema.d.ts +3 -2
  3. package/dist/src/d1/audit-log-schema.d.ts.map +1 -1
  4. package/dist/src/d1/audit-log-schema.js +14 -4
  5. package/dist/src/d1/index.d.ts +2 -1
  6. package/dist/src/d1/index.d.ts.map +1 -1
  7. package/dist/src/d1/runtime.d.ts +11 -11
  8. package/dist/src/d1/runtime.d.ts.map +1 -1
  9. package/dist/src/d1/runtime.js +26 -9
  10. package/dist/src/d1/sql.d.ts +2 -2
  11. package/dist/src/d1/sql.d.ts.map +1 -1
  12. package/dist/src/d1/sql.js +61 -29
  13. package/dist/src/d1/types.d.ts +10 -2
  14. package/dist/src/d1/types.d.ts.map +1 -1
  15. package/dist/src/d1-runtime/with-audit.d.ts +3 -2
  16. package/dist/src/d1-runtime/with-audit.d.ts.map +1 -1
  17. package/dist/src/d1-runtime/with-audit.js +12 -7
  18. package/dist/src/index.d.ts +2 -1
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/index.js +1 -1
  21. package/dist/src/postgres/audit-log-schema.d.ts +3 -2
  22. package/dist/src/postgres/audit-log-schema.d.ts.map +1 -1
  23. package/dist/src/postgres/audit-log-schema.js +14 -4
  24. package/dist/src/postgres/index.d.ts +3 -2
  25. package/dist/src/postgres/index.d.ts.map +1 -1
  26. package/dist/src/postgres/index.js +1 -1
  27. package/dist/src/postgres/runtime.d.ts +6 -8
  28. package/dist/src/postgres/runtime.d.ts.map +1 -1
  29. package/dist/src/postgres/runtime.js +15 -3
  30. package/dist/src/postgres/sql.d.ts +10 -7
  31. package/dist/src/postgres/sql.d.ts.map +1 -1
  32. package/dist/src/postgres/sql.js +72 -50
  33. package/dist/src/postgres/types.d.ts +10 -2
  34. package/dist/src/postgres/types.d.ts.map +1 -1
  35. package/dist/test/d1.integration.test.js +71 -4
  36. package/dist/test/sqlite.integration.test.js +65 -2
  37. package/package.json +1 -1
@@ -21,7 +21,7 @@ function getRowId(row, pk) {
21
21
  *
22
22
  * @param db - A Drizzle SQLite database instance (D1, better-sqlite3, libsql)
23
23
  * @param auditTable - The Drizzle table definition for audit_logs
24
- * @param context - The audit context (userId, optional workspaceId)
24
+ * @param context - The audit context (userId, optional context columns)
25
25
  *
26
26
  * @example
27
27
  * import { withAudit } from "drizzle-audit/d1-runtime"
@@ -43,7 +43,7 @@ function getRowId(row, pk) {
43
43
  * audited.db.select().from(users).all()
44
44
  */
45
45
  export function withAudit(db, auditTable, context) {
46
- const workspaceIdColumn = (() => {
46
+ const extraColumns = (() => {
47
47
  const cols = getTableColumns(auditTable);
48
48
  const known = new Set([
49
49
  "id",
@@ -55,10 +55,17 @@ export function withAudit(db, auditTable, context) {
55
55
  "new_data",
56
56
  "created_at",
57
57
  ]);
58
- const extra = Object.keys(cols).find((k) => !known.has(k));
59
- return extra ?? null;
58
+ return Object.keys(cols).filter((k) => !known.has(k));
60
59
  })();
60
+ const contextValues = { ...(context.context ?? {}) };
61
61
  function buildAuditRow(tableName, operation, rowId, oldData, newData) {
62
+ const extra = {};
63
+ for (const column of extraColumns) {
64
+ const value = contextValues[column];
65
+ if (value !== undefined && value !== "") {
66
+ extra[column] = value;
67
+ }
68
+ }
62
69
  return {
63
70
  table_name: tableName,
64
71
  operation,
@@ -66,9 +73,7 @@ export function withAudit(db, auditTable, context) {
66
73
  user_id: context.userId,
67
74
  old_data: oldData ? JSON.stringify(oldData) : null,
68
75
  new_data: newData ? JSON.stringify(newData) : null,
69
- ...(workspaceIdColumn && context.workspaceId
70
- ? { [workspaceIdColumn]: context.workspaceId }
71
- : {}),
76
+ ...extra,
72
77
  };
73
78
  }
74
79
  return {
@@ -1,5 +1,6 @@
1
1
  export * from "./postgres/index.js";
2
- export * from "./d1/index.js";
2
+ export { clearD1AuditContext, createAttachD1AuditTriggerSql, createAttachD1AuditTriggersSql, createAttachD1AuditTriggerSqlWithColumns, createAttachD1AuditTriggersSqlWithColumns, createD1AuditInstallSql, d1AuditContextTable, d1AuditLogTable, setD1AuditContext, withD1AuditedTransaction, } from "./d1/index.js";
3
+ export type { D1AuditContextOptions, D1AuditInstallOptions, D1AuditLogTableOptions, D1AuditSqlExecutor, D1AuditTriggerTarget, D1AuditTriggerTargetWithColumns, } from "./d1/index.js";
3
4
  export * from "./d1-runtime/index.js";
4
5
  export * from "./compute-diff.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,8BAA8B,EAC9B,wCAAwC,EACxC,yCAAyC,EACzC,uBAAuB,EACvB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EAClB,oBAAoB,EACpB,+BAA+B,GAChC,MAAM,eAAe,CAAA;AACtB,cAAc,uBAAuB,CAAA;AACrC,cAAc,mBAAmB,CAAA"}
package/dist/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from "./postgres/index.js";
2
- export * from "./d1/index.js";
2
+ export { clearD1AuditContext, createAttachD1AuditTriggerSql, createAttachD1AuditTriggersSql, createAttachD1AuditTriggerSqlWithColumns, createAttachD1AuditTriggersSqlWithColumns, createD1AuditInstallSql, d1AuditContextTable, d1AuditLogTable, setD1AuditContext, withD1AuditedTransaction, } from "./d1/index.js";
3
3
  export * from "./d1-runtime/index.js";
4
4
  export * from "./compute-diff.js";
@@ -1,6 +1,7 @@
1
+ import type { AuditContextColumn } from "./types.js";
1
2
  export type PgAuditLogTableOptions = {
2
- /** When set (e.g. "workspace_id"), the table definition includes this optional column to match the install. */
3
- workspaceIdColumn?: string;
3
+ /** Extra context columns to include in the table definition, matching the install. */
4
+ contextColumns?: AuditContextColumn[];
4
5
  };
5
6
  export declare function pgAuditLogTable(options?: PgAuditLogTableOptions): import("drizzle-orm/pg-core").PgTableWithColumns<{
6
7
  name: "audit_logs";
@@ -1 +1 @@
1
- {"version":3,"file":"audit-log-schema.d.ts","sourceRoot":"","sources":["../../../src/postgres/audit-log-schema.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,sBAAsB,GAAG;IACnC,+GAA+G;IAC/G,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2B/D"}
1
+ {"version":3,"file":"audit-log-schema.d.ts","sourceRoot":"","sources":["../../../src/postgres/audit-log-schema.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAEpD,MAAM,MAAM,sBAAsB,GAAG;IACnC,sFAAsF;IACtF,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAA;CACtC,CAAA;AAiBD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyB/D"}
@@ -1,15 +1,25 @@
1
1
  import { bigserial, index, jsonb, pgTable, text, timestamp, } from "drizzle-orm/pg-core";
2
+ function resolveColumns(options) {
3
+ const columns = [];
4
+ const seen = new Set();
5
+ for (const entry of options?.contextColumns ?? []) {
6
+ const column = entry.column?.trim();
7
+ if (column && !seen.has(column)) {
8
+ seen.add(column);
9
+ columns.push(column);
10
+ }
11
+ }
12
+ return columns;
13
+ }
2
14
  export function pgAuditLogTable(options) {
3
- const workspaceIdColumn = options?.workspaceIdColumn?.trim();
15
+ const contextColumns = resolveColumns(options);
4
16
  const columns = {
5
17
  id: bigserial("id", { mode: "number" }).primaryKey(),
6
18
  table_name: text("table_name").notNull(),
7
19
  operation: text("operation").notNull(),
8
20
  row_id: text("row_id"),
9
21
  user_id: text("user_id"),
10
- ...(workspaceIdColumn
11
- ? { [workspaceIdColumn]: text(workspaceIdColumn) }
12
- : {}),
22
+ ...Object.fromEntries(contextColumns.map((c) => [c, text(c)])),
13
23
  old_data: jsonb("old_data"),
14
24
  new_data: jsonb("new_data"),
15
25
  created_at: timestamp("created_at", { withTimezone: true })
@@ -1,6 +1,7 @@
1
1
  export { pgAuditLogTable } from "./audit-log-schema.js";
2
2
  export type { PgAuditLogTableOptions } from "./audit-log-schema.js";
3
- export { createAttachAuditTriggerSql, createAttachAuditTriggersSql, createAuditAddWorkspaceColumnSql, createAuditInstallSql, } from "./sql.js";
3
+ export { createAttachAuditTriggerSql, createAttachAuditTriggersSql, createAuditAddContextColumnsSql, createAuditInstallSql, } from "./sql.js";
4
4
  export { setAuditContext, withAuditedTransaction } from "./runtime.js";
5
- export type { AuditInstallOptions, AuditSqlExecutor, AuditTransactionCapable, AuditTriggerTarget, } from "./types.js";
5
+ export type { AuditContextOptions } from "./runtime.js";
6
+ export type { AuditContextColumn, AuditInstallOptions, AuditSqlExecutor, AuditTransactionCapable, AuditTriggerTarget, } from "./types.js";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
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,+BAA+B,EAC/B,qBAAqB,GACtB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AACtE,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAEvD,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,YAAY,CAAA"}
@@ -1,3 +1,3 @@
1
1
  export { pgAuditLogTable } from "./audit-log-schema.js";
2
- export { createAttachAuditTriggerSql, createAttachAuditTriggersSql, createAuditAddWorkspaceColumnSql, createAuditInstallSql, } from "./sql.js";
2
+ export { createAttachAuditTriggerSql, createAttachAuditTriggersSql, createAuditAddContextColumnsSql, createAuditInstallSql, } from "./sql.js";
3
3
  export { setAuditContext, withAuditedTransaction } from "./runtime.js";
@@ -1,10 +1,8 @@
1
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>;
2
+ export type AuditContextOptions = {
3
+ /** Map of session GUC name → value to set for the transaction. */
4
+ context?: Record<string, string>;
5
+ };
6
+ export declare function setAuditContext(db: AuditSqlExecutor, actorId: string, contextKey?: string, options?: AuditContextOptions): Promise<void>;
7
+ export declare function withAuditedTransaction<TTransaction extends AuditSqlExecutor, TResult>(db: AuditTransactionCapable<TTransaction>, actorId: string, callback: (tx: TTransaction) => Promise<TResult> | TResult, contextKey?: string, options?: AuditContextOptions): Promise<TResult>;
10
8
  //# sourceMappingURL=runtime.d.ts.map
@@ -1 +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"}
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;AAwBnB,MAAM,MAAM,mBAAmB,GAAG;IAChC,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC,CAAA;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,gBAAgB,EACpB,OAAO,EAAE,MAAM,EACf,UAAU,SAAgB,EAC1B,OAAO,CAAC,EAAE,mBAAmB,iBAa9B;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,mBAAmB,oBAQ9B"}
@@ -4,12 +4,24 @@ function assertActorId(actorId) {
4
4
  throw new Error("actorId must not be empty");
5
5
  }
6
6
  }
7
+ /**
8
+ * Builds the `context` GUC map from a set of options. Empty/undefined values
9
+ * are dropped so the trigger's NULLIF yields NULL.
10
+ */
11
+ function resolveContext(options) {
12
+ const context = {};
13
+ for (const [key, value] of Object.entries(options?.context ?? {})) {
14
+ if (value !== undefined && value !== "") {
15
+ context[key] = value;
16
+ }
17
+ }
18
+ return context;
19
+ }
7
20
  export async function setAuditContext(db, actorId, contextKey = "app.user_id", options) {
8
21
  assertActorId(actorId);
9
22
  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`);
23
+ for (const [key, value] of Object.entries(resolveContext(options))) {
24
+ await db.execute(sql `select set_config(${key}, ${value}, true) as audit_context`);
13
25
  }
14
26
  }
15
27
  export async function withAuditedTransaction(db, actorId, callback, contextKey = "app.user_id", options) {
@@ -3,12 +3,15 @@ export declare function createAuditInstallSql(options?: AuditInstallOptions): st
3
3
  export declare function createAttachAuditTriggerSql(target: AuditTriggerTarget, options?: AuditInstallOptions): string;
4
4
  export declare function createAttachAuditTriggersSql(targets: AuditTriggerTarget[], options?: AuditInstallOptions): string;
5
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).
6
+ * Generates SQL to add context columns and regenerate the trigger function on an
7
+ * existing audit_logs table. Use in a new migration when adding context columns
8
+ * after the initial install.
9
+ *
10
+ * Pass the FULL set of context columns the audit table should have (the trigger is
11
+ * a single CREATE OR REPLACE, so it must reference every column). Columns are added
12
+ * with `ADD COLUMN IF NOT EXISTS`, so already-present columns are left untouched.
13
+ * Options must match your install (auditSchema, auditTable, triggerFunctionName,
14
+ * contextKey).
10
15
  */
11
- export declare function createAuditAddWorkspaceColumnSql(options: AuditInstallOptions & {
12
- workspaceIdColumn: string;
13
- }): string;
16
+ export declare function createAuditAddContextColumnsSql(options?: AuditInstallOptions): string;
14
17
  //# sourceMappingURL=sql.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../../src/postgres/sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,YAAY,CAAA;AAkJnB,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,mBAAwB,UA8DtE;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;;;;;;;;;;GAUG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,GAAE,mBAAwB,UA2ChF"}
@@ -21,46 +21,58 @@ function assertNonEmpty(value, label) {
21
21
  }
22
22
  return value;
23
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
- : "";
24
+ /**
25
+ * Normalizes the context columns from the install options, de-duplicating by
26
+ * column name and applying defaults for `sessionKey` / `index`.
27
+ */
28
+ function normalizeContextColumns(options) {
29
+ const resolved = [];
30
+ const seen = new Set();
31
+ for (const entry of options.contextColumns ?? []) {
32
+ const column = entry.column?.trim();
33
+ if (!column) {
34
+ throw new Error("contextColumns[].column must not be empty");
35
+ }
36
+ if (seen.has(column)) {
37
+ continue;
38
+ }
39
+ seen.add(column);
40
+ resolved.push({
41
+ column,
42
+ sessionKey: entry.sessionKey?.trim() || `app.${column}`,
43
+ index: entry.index ?? true,
44
+ });
45
+ }
46
+ return resolved;
47
+ }
48
+ function buildTriggerFunctionSql(qualifiedAuditTable, qualifiedTriggerFunction, contextLiteral, contextColumns) {
49
+ const declContext = contextColumns
50
+ .map((_, i) => `\n audit_ctx_${i} TEXT;`)
51
+ .join("");
52
+ const readContext = contextColumns
53
+ .map((col, i) => `\n audit_ctx_${i} := NULLIF(current_setting(${quoteLiteral(col.sessionKey)}, true), '');`)
54
+ .join("");
55
+ const ctxColIdents = contextColumns.map((col) => quoteIdent(col.column));
56
+ const ctxColPrefix = ctxColIdents.length ? `, ${ctxColIdents.join(", ")}` : "";
57
+ const ctxValExprs = contextColumns.map((_, i) => `audit_ctx_${i}`);
58
+ const ctxValPrefix = ctxValExprs.length ? `, ${ctxValExprs.join(", ")}` : "";
35
59
  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)`;
60
+ const insertColsInsert = `${insertColsBase}${ctxColPrefix}, new_data`;
61
+ const insertColsUpdate = `${insertColsBase}${ctxColPrefix}, old_data, new_data`;
62
+ const insertColsDelete = `${insertColsBase}${ctxColPrefix}, old_data`;
63
+ const valuesInsert = `TG_TABLE_NAME, TG_OP, current_row_id, audit_user${ctxValPrefix}, to_jsonb(NEW)`;
64
+ const valuesUpdate = `TG_TABLE_NAME, TG_OP, current_row_id, audit_user${ctxValPrefix}, to_jsonb(OLD), to_jsonb(NEW)`;
65
+ const valuesDelete = `TG_TABLE_NAME, TG_OP, current_row_id, audit_user${ctxValPrefix}, to_jsonb(OLD)`;
54
66
  return `
55
67
  CREATE OR REPLACE FUNCTION ${qualifiedTriggerFunction}()
56
68
  RETURNS TRIGGER AS $$
57
69
  DECLARE
58
- audit_user TEXT;${declWorkspace}
70
+ audit_user TEXT;${declContext}
59
71
  row_id_column TEXT;
60
72
  current_row JSONB;
61
73
  current_row_id TEXT;
62
74
  BEGIN
63
- audit_user := NULLIF(current_setting(${contextLiteral}, true), '');${readWorkspace}
75
+ audit_user := NULLIF(current_setting(${contextLiteral}, true), '');${readContext}
64
76
 
65
77
  row_id_column := COALESCE(NULLIF(TG_ARGV[0], ''), ${quoteLiteral(DEFAULT_ROW_ID_COLUMN)});
66
78
 
@@ -102,7 +114,7 @@ export function createAuditInstallSql(options = {}) {
102
114
  const auditTable = assertNonEmpty(options.auditTable ?? DEFAULT_AUDIT_TABLE, "auditTable");
103
115
  const contextKey = assertNonEmpty(options.contextKey ?? DEFAULT_CONTEXT_KEY, "contextKey");
104
116
  const triggerFunctionName = assertNonEmpty(options.triggerFunctionName ?? DEFAULT_TRIGGER_FUNCTION, "triggerFunctionName");
105
- const workspaceIdColumn = options.workspaceIdColumn?.trim();
117
+ const contextColumns = normalizeContextColumns(options);
106
118
  const qualifiedAuditTable = qualifyName(auditTable, auditSchema);
107
119
  const qualifiedTriggerFunction = qualifyName(triggerFunctionName, auditSchema);
108
120
  const contextLiteral = quoteLiteral(contextKey);
@@ -112,7 +124,7 @@ export function createAuditInstallSql(options = {}) {
112
124
  "operation TEXT NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE'))",
113
125
  "row_id TEXT",
114
126
  "user_id TEXT",
115
- ...(workspaceIdColumn ? [quoteIdent(workspaceIdColumn) + " TEXT"] : []),
127
+ ...contextColumns.map((col) => quoteIdent(col.column) + " TEXT"),
116
128
  "old_data JSONB",
117
129
  "new_data JSONB",
118
130
  "created_at TIMESTAMPTZ NOT NULL DEFAULT now()",
@@ -121,11 +133,9 @@ export function createAuditInstallSql(options = {}) {
121
133
  `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_table_name_idx`)} ON ${qualifiedAuditTable} (table_name);`,
122
134
  `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_row_id_idx`)} ON ${qualifiedAuditTable} (row_id);`,
123
135
  `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
- : []),
136
+ ...contextColumns
137
+ .filter((col) => col.index)
138
+ .map((col) => `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_${col.column}_idx`)} ON ${qualifiedAuditTable} (${quoteIdent(col.column)});`),
129
139
  `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_created_at_idx`)} ON ${qualifiedAuditTable} (created_at DESC);`,
130
140
  ];
131
141
  return [
@@ -135,7 +145,7 @@ CREATE TABLE IF NOT EXISTS ${qualifiedAuditTable} (
135
145
  ${tableColumns.join(",\n ")}
136
146
  );`.trim(),
137
147
  ...indexStatements,
138
- buildTriggerFunctionSql(qualifiedAuditTable, qualifiedTriggerFunction, contextLiteral, { workspaceIdColumn: workspaceIdColumn ?? undefined }),
148
+ buildTriggerFunctionSql(qualifiedAuditTable, qualifiedTriggerFunction, contextLiteral, contextColumns),
139
149
  ].join("\n\n");
140
150
  }
141
151
  export function createAttachAuditTriggerSql(target, options = {}) {
@@ -166,25 +176,37 @@ export function createAttachAuditTriggersSql(targets, options = {}) {
166
176
  .join("\n\n");
167
177
  }
168
178
  /**
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).
179
+ * Generates SQL to add context columns and regenerate the trigger function on an
180
+ * existing audit_logs table. Use in a new migration when adding context columns
181
+ * after the initial install.
182
+ *
183
+ * Pass the FULL set of context columns the audit table should have (the trigger is
184
+ * a single CREATE OR REPLACE, so it must reference every column). Columns are added
185
+ * with `ADD COLUMN IF NOT EXISTS`, so already-present columns are left untouched.
186
+ * Options must match your install (auditSchema, auditTable, triggerFunctionName,
187
+ * contextKey).
173
188
  */
174
- export function createAuditAddWorkspaceColumnSql(options) {
175
- const workspaceIdColumn = assertNonEmpty(options.workspaceIdColumn.trim(), "workspaceIdColumn");
189
+ export function createAuditAddContextColumnsSql(options = {}) {
176
190
  const auditSchema = assertNonEmpty(options.auditSchema ?? DEFAULT_AUDIT_SCHEMA, "auditSchema");
177
191
  const auditTable = assertNonEmpty(options.auditTable ?? DEFAULT_AUDIT_TABLE, "auditTable");
178
192
  const contextKey = assertNonEmpty(options.contextKey ?? DEFAULT_CONTEXT_KEY, "contextKey");
179
193
  const triggerFunctionName = assertNonEmpty(options.triggerFunctionName ?? DEFAULT_TRIGGER_FUNCTION, "triggerFunctionName");
194
+ const contextColumns = normalizeContextColumns(options);
195
+ if (contextColumns.length === 0) {
196
+ throw new Error("createAuditAddContextColumnsSql requires at least one context column");
197
+ }
180
198
  const qualifiedAuditTable = qualifyName(auditTable, auditSchema);
181
199
  const qualifiedTriggerFunction = qualifyName(triggerFunctionName, auditSchema);
182
200
  const contextLiteral = quoteLiteral(contextKey);
183
201
  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
- }),
202
+ ...contextColumns.flatMap((col) => [
203
+ `ALTER TABLE ${qualifiedAuditTable} ADD COLUMN IF NOT EXISTS ${quoteIdent(col.column)} TEXT;`,
204
+ ...(col.index
205
+ ? [
206
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(`${auditTable}_${col.column}_idx`)} ON ${qualifiedAuditTable} (${quoteIdent(col.column)});`,
207
+ ]
208
+ : []),
209
+ ]),
210
+ buildTriggerFunctionSql(qualifiedAuditTable, qualifiedTriggerFunction, contextLiteral, contextColumns),
189
211
  ].join("\n\n");
190
212
  }
@@ -5,13 +5,21 @@ export type AuditSqlExecutor = {
5
5
  export type AuditTransactionCapable<TTransaction extends AuditSqlExecutor> = {
6
6
  transaction: <TResult>(callback: (tx: TTransaction) => Promise<TResult>) => Promise<TResult>;
7
7
  };
8
+ export type AuditContextColumn = {
9
+ /** Column added to the audit table (TEXT, nullable). */
10
+ column: string;
11
+ /** Session GUC the trigger reads. Default `app.${column}`. */
12
+ sessionKey?: string;
13
+ /** Create an index on the column. Default true. */
14
+ index?: boolean;
15
+ };
8
16
  export type AuditInstallOptions = {
9
17
  auditSchema?: string;
10
18
  auditTable?: string;
11
19
  contextKey?: string;
12
20
  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;
21
+ /** Extra context columns added to the audit table and populated by the trigger from session GUCs. */
22
+ contextColumns?: AuditContextColumn[];
15
23
  };
16
24
  export type AuditTriggerTarget = {
17
25
  table: string;
@@ -1 +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"}
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,kBAAkB,GAAG;IAC/B,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,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,qGAAqG;IACrG,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAA;CACtC,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"}
@@ -122,20 +122,22 @@ test("d1 column-aware triggers capture full row data", () => {
122
122
  });
123
123
  test("d1 workspace_id column and context are stored when enabled", () => {
124
124
  const sqlite = new Database(":memory:");
125
- const auditLogsWithWorkspace = d1AuditLogTable({ workspaceIdColumn: "workspace_id" });
125
+ const auditLogsWithWorkspace = d1AuditLogTable({
126
+ contextColumns: [{ column: "workspace_id" }],
127
+ });
126
128
  const db = drizzle({ client: sqlite, schema: { auditLogs: auditLogsWithWorkspace, auditContext, users } });
127
129
  try {
128
- sqlite.exec(createD1AuditInstallSql({ workspaceIdColumn: "workspace_id" }));
130
+ sqlite.exec(createD1AuditInstallSql({ contextColumns: [{ column: "workspace_id" }] }));
129
131
  sqlite.exec(`
130
132
  CREATE TABLE users (
131
133
  id TEXT PRIMARY KEY,
132
134
  name TEXT NOT NULL
133
135
  );
134
136
  `);
135
- sqlite.exec(createAttachD1AuditTriggersSql([{ table: "users" }], { workspaceIdColumn: "workspace_id" }));
137
+ sqlite.exec(createAttachD1AuditTriggersSql([{ table: "users" }], { contextColumns: [{ column: "workspace_id" }] }));
136
138
  withD1AuditedTransaction(db, "user_1", (tx) => {
137
139
  tx.insert(users).values({ id: "u1", name: "Alice" }).run();
138
- }, { workspaceId: "ws_1" });
140
+ }, { context: { workspace_id: "ws_1" } });
139
141
  const logs = db.select().from(auditLogsWithWorkspace).all();
140
142
  assert.equal(logs.length, 1);
141
143
  assert.equal(logs[0]?.user_id, "user_1");
@@ -157,6 +159,71 @@ test("d1 workspace_id column and context are stored when enabled", () => {
157
159
  sqlite.close();
158
160
  }
159
161
  });
162
+ test("d1 generic contextColumns populate and stay NULL without context", () => {
163
+ const sqlite = new Database(":memory:");
164
+ const contextColumns = [
165
+ { column: "workspace_id" },
166
+ { column: "tenant_id" },
167
+ { column: "request_id" },
168
+ ];
169
+ const auditLogsWithCtx = d1AuditLogTable({ contextColumns });
170
+ const db = drizzle({
171
+ client: sqlite,
172
+ schema: { auditLogs: auditLogsWithCtx, auditContext, users },
173
+ });
174
+ try {
175
+ sqlite.exec(createD1AuditInstallSql({ contextColumns }));
176
+ sqlite.exec(`
177
+ CREATE TABLE users (
178
+ id TEXT PRIMARY KEY,
179
+ name TEXT NOT NULL
180
+ );
181
+ `);
182
+ sqlite.exec(createAttachD1AuditTriggersSql([{ table: "users" }], { contextColumns }));
183
+ withD1AuditedTransaction(db, "user_1", (tx) => {
184
+ tx.insert(users).values({ id: "u1", name: "Alice" }).run();
185
+ tx.update(users).set({ name: "Alice Updated" }).where(eq(users.id, "u1")).run();
186
+ tx.delete(users).where(eq(users.id, "u1")).run();
187
+ }, {
188
+ context: {
189
+ workspace_id: "ws_1",
190
+ tenant_id: "tenant_1",
191
+ request_id: "req_1",
192
+ },
193
+ });
194
+ const logs = db
195
+ .select()
196
+ .from(auditLogsWithCtx)
197
+ .orderBy(asc(auditLogsWithCtx.id))
198
+ .all();
199
+ assert.equal(logs.length, 3);
200
+ for (const log of logs) {
201
+ const row = log;
202
+ assert.equal(row.user_id, "user_1");
203
+ assert.equal(row.workspace_id, "ws_1");
204
+ assert.equal(row.tenant_id, "tenant_1");
205
+ assert.equal(row.request_id, "req_1");
206
+ }
207
+ assert.equal(logs[0]?.operation, "INSERT");
208
+ assert.equal(logs[1]?.operation, "UPDATE");
209
+ assert.equal(logs[2]?.operation, "DELETE");
210
+ // No context: all context columns NULL.
211
+ db.insert(users).values({ id: "u2", name: "No Context" }).run();
212
+ const all = db
213
+ .select()
214
+ .from(auditLogsWithCtx)
215
+ .orderBy(asc(auditLogsWithCtx.id))
216
+ .all();
217
+ const last = all[all.length - 1];
218
+ assert.equal(last.user_id, null);
219
+ assert.equal(last.workspace_id, null);
220
+ assert.equal(last.tenant_id, null);
221
+ assert.equal(last.request_id, null);
222
+ }
223
+ finally {
224
+ sqlite.close();
225
+ }
226
+ });
160
227
  test("d1 writes without audit context produce rows with user_id = NULL", () => {
161
228
  const { db, sqlite } = setupDb();
162
229
  try {
@@ -165,7 +165,9 @@ test("withAudit handles multi-row update", async () => {
165
165
  });
166
166
  test("withAudit with workspace_id", async () => {
167
167
  const sqlite = new Database(":memory:");
168
- const auditLogsWithWs = d1AuditLogTable({ workspaceIdColumn: "workspace_id" });
168
+ const auditLogsWithWs = d1AuditLogTable({
169
+ contextColumns: [{ column: "workspace_id" }],
170
+ });
169
171
  const db = drizzle({ client: sqlite, schema: { auditLogs: auditLogsWithWs, users } });
170
172
  try {
171
173
  sqlite.exec(`
@@ -188,7 +190,7 @@ test("withAudit with workspace_id", async () => {
188
190
  `);
189
191
  const audited = withAudit(db, auditLogsWithWs, {
190
192
  userId: "user_1",
191
- workspaceId: "ws_1",
193
+ context: { workspace_id: "ws_1" },
192
194
  });
193
195
  await audited.insert(users, { id: "u1", name: "Ada" });
194
196
  const logs = db.select().from(auditLogsWithWs).all();
@@ -200,6 +202,67 @@ test("withAudit with workspace_id", async () => {
200
202
  sqlite.close();
201
203
  }
202
204
  });
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
+ });
203
266
  test("withAudit.db gives access to raw db for non-audited ops", async () => {
204
267
  const { db, sqlite } = setupDb();
205
268
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willyim/drizzle-audit",
3
- "version": "0.4.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",