@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.
- package/README.md +52 -16
- package/dist/src/d1/audit-log-schema.d.ts +3 -2
- package/dist/src/d1/audit-log-schema.d.ts.map +1 -1
- package/dist/src/d1/audit-log-schema.js +14 -4
- package/dist/src/d1/index.d.ts +2 -1
- package/dist/src/d1/index.d.ts.map +1 -1
- package/dist/src/d1/runtime.d.ts +11 -11
- package/dist/src/d1/runtime.d.ts.map +1 -1
- package/dist/src/d1/runtime.js +26 -9
- package/dist/src/d1/sql.d.ts +2 -2
- package/dist/src/d1/sql.d.ts.map +1 -1
- package/dist/src/d1/sql.js +61 -29
- package/dist/src/d1/types.d.ts +10 -2
- package/dist/src/d1/types.d.ts.map +1 -1
- package/dist/src/d1-runtime/with-audit.d.ts +3 -2
- package/dist/src/d1-runtime/with-audit.d.ts.map +1 -1
- package/dist/src/d1-runtime/with-audit.js +12 -7
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/postgres/audit-log-schema.d.ts +3 -2
- package/dist/src/postgres/audit-log-schema.d.ts.map +1 -1
- package/dist/src/postgres/audit-log-schema.js +14 -4
- package/dist/src/postgres/index.d.ts +3 -2
- package/dist/src/postgres/index.d.ts.map +1 -1
- package/dist/src/postgres/index.js +1 -1
- package/dist/src/postgres/runtime.d.ts +6 -8
- package/dist/src/postgres/runtime.d.ts.map +1 -1
- package/dist/src/postgres/runtime.js +15 -3
- package/dist/src/postgres/sql.d.ts +10 -7
- package/dist/src/postgres/sql.d.ts.map +1 -1
- package/dist/src/postgres/sql.js +72 -50
- package/dist/src/postgres/types.d.ts +10 -2
- package/dist/src/postgres/types.d.ts.map +1 -1
- package/dist/test/d1.integration.test.js +71 -4
- package/dist/test/sqlite.integration.test.js +65 -2
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
...
|
|
70
|
-
? { [workspaceIdColumn]: context.workspaceId }
|
|
71
|
-
: {}),
|
|
76
|
+
...extra,
|
|
72
77
|
};
|
|
73
78
|
}
|
|
74
79
|
return {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./postgres/index.js";
|
|
2
|
-
export
|
|
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
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,
|
|
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
|
|
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
|
-
/**
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
-
...(
|
|
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,
|
|
3
|
+
export { createAttachAuditTriggerSql, createAttachAuditTriggersSql, createAuditAddContextColumnsSql, createAuditInstallSql, } from "./sql.js";
|
|
4
4
|
export { setAuditContext, withAuditedTransaction } from "./runtime.js";
|
|
5
|
-
export type {
|
|
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,
|
|
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,
|
|
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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
export declare function
|
|
7
|
-
|
|
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;
|
|
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
|
-
|
|
11
|
-
|
|
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
|
|
7
|
-
* existing audit_logs table. Use in a new migration when adding
|
|
8
|
-
* the initial install.
|
|
9
|
-
*
|
|
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
|
|
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,
|
|
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"}
|
package/dist/src/postgres/sql.js
CHANGED
|
@@ -21,46 +21,58 @@ function assertNonEmpty(value, label) {
|
|
|
21
21
|
}
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
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;${
|
|
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), '');${
|
|
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
|
|
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
|
-
...(
|
|
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
|
-
...
|
|
125
|
-
|
|
126
|
-
|
|
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,
|
|
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
|
|
170
|
-
* existing audit_logs table. Use in a new migration when adding
|
|
171
|
-
* the initial install.
|
|
172
|
-
*
|
|
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
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
/**
|
|
14
|
-
|
|
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,
|
|
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({
|
|
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({
|
|
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" }], {
|
|
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
|
-
}, {
|
|
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({
|
|
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
|
-
|
|
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