@willyim/drizzle-audit 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -0
- package/dist/src/cli/generate-migration.d.ts +14 -0
- package/dist/src/cli/generate-migration.d.ts.map +1 -0
- package/dist/src/cli/generate-migration.js +118 -0
- package/dist/src/cli/runner.d.ts +2 -0
- package/dist/src/cli/runner.d.ts.map +1 -0
- package/dist/src/cli/runner.js +23 -0
- package/dist/src/d1/audit-log-schema.d.ts +181 -0
- package/dist/src/d1/audit-log-schema.d.ts.map +1 -0
- package/dist/src/d1/audit-log-schema.js +30 -0
- package/dist/src/d1/index.d.ts +7 -0
- package/dist/src/d1/index.d.ts.map +1 -0
- package/dist/src/d1/index.js +3 -0
- package/dist/src/d1/runtime.d.ts +44 -0
- package/dist/src/d1/runtime.d.ts.map +1 -0
- package/dist/src/d1/runtime.js +68 -0
- package/dist/src/d1/sql.d.ts +37 -0
- package/dist/src/d1/sql.d.ts.map +1 -0
- package/dist/src/d1/sql.js +261 -0
- package/dist/src/d1/types.d.ts +22 -0
- package/dist/src/d1/types.d.ts.map +1 -0
- package/dist/src/d1/types.js +1 -0
- package/dist/src/d1-runtime/index.d.ts +3 -0
- package/dist/src/d1-runtime/index.d.ts.map +1 -0
- package/dist/src/d1-runtime/index.js +1 -0
- package/dist/src/d1-runtime/with-audit.d.ts +77 -0
- package/dist/src/d1-runtime/with-audit.d.ts.map +1 -0
- package/dist/src/d1-runtime/with-audit.js +130 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/postgres/audit-log-schema.d.ts +140 -0
- package/dist/src/postgres/audit-log-schema.d.ts.map +1 -0
- package/dist/src/postgres/audit-log-schema.js +25 -0
- package/dist/src/postgres/index.d.ts +6 -0
- package/dist/src/postgres/index.d.ts.map +1 -0
- package/dist/src/postgres/index.js +3 -0
- package/dist/src/postgres/runtime.d.ts +10 -0
- package/dist/src/postgres/runtime.d.ts.map +1 -0
- package/dist/src/postgres/runtime.js +21 -0
- package/dist/src/postgres/sql.d.ts +14 -0
- package/dist/src/postgres/sql.d.ts.map +1 -0
- package/dist/src/postgres/sql.js +190 -0
- package/dist/src/postgres/types.d.ts +22 -0
- package/dist/src/postgres/types.d.ts.map +1 -0
- package/dist/src/postgres/types.js +1 -0
- package/dist/test/d1-runtime.integration.test.d.ts +2 -0
- package/dist/test/d1-runtime.integration.test.d.ts.map +1 -0
- package/dist/test/d1-runtime.integration.test.js +222 -0
- package/dist/test/d1.integration.test.d.ts +2 -0
- package/dist/test/d1.integration.test.d.ts.map +1 -0
- package/dist/test/d1.integration.test.js +223 -0
- package/dist/test/postgres.integration.test.d.ts +2 -0
- package/dist/test/postgres.integration.test.d.ts.map +1 -0
- package/dist/test/postgres.integration.test.js +286 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# drizzle-audit
|
|
2
|
+
|
|
3
|
+
Automatic audit logging for [Drizzle ORM](https://orm.drizzle.team). Supports Postgres (triggers) and D1/SQLite (triggers or app-level wrapper).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @willyim/drizzle-audit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `drizzle-orm`. Optional: `drizzle-kit` (for CLI), `tsx` (for TS config files).
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Postgres (triggers)
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
pgAuditLogTable,
|
|
20
|
+
createAuditInstallSql,
|
|
21
|
+
createAttachAuditTriggersSql,
|
|
22
|
+
withAuditedTransaction,
|
|
23
|
+
} from "@willyim/drizzle-audit/postgres"
|
|
24
|
+
|
|
25
|
+
// 1. Add to your Drizzle schema
|
|
26
|
+
export const auditLogs = pgAuditLogTable()
|
|
27
|
+
|
|
28
|
+
// 2. Generate migration SQL
|
|
29
|
+
const migrationSql = [
|
|
30
|
+
createAuditInstallSql(),
|
|
31
|
+
createAttachAuditTriggersSql([
|
|
32
|
+
{ table: "users" },
|
|
33
|
+
{ table: "invoices", rowIdColumn: "invoice_id" },
|
|
34
|
+
]),
|
|
35
|
+
].join("\n\n")
|
|
36
|
+
|
|
37
|
+
// 3. Use in your app
|
|
38
|
+
await withAuditedTransaction(db, currentUser.id, async (tx) => {
|
|
39
|
+
await tx.insert(users).values({ id: "u1", name: "Ada" })
|
|
40
|
+
await tx.update(users).set({ name: "Ada Lovelace" }).where(eq(users.id, "u1"))
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Postgres triggers capture full row snapshots (`old_data`/`new_data` as JSONB) automatically.
|
|
45
|
+
|
|
46
|
+
### D1/SQLite — App-Level Wrapper (recommended)
|
|
47
|
+
|
|
48
|
+
For D1 and SQLite, the `withAudit` wrapper is the simplest approach. No triggers, no context tables — it intercepts operations in your JS code where you already have the user session.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { d1AuditLogTable } from "@willyim/drizzle-audit/d1"
|
|
52
|
+
import { withAudit } from "@willyim/drizzle-audit/d1-runtime"
|
|
53
|
+
|
|
54
|
+
// 1. Add to your schema
|
|
55
|
+
export const auditLogs = d1AuditLogTable()
|
|
56
|
+
|
|
57
|
+
// 2. Create the audit_logs table (in a migration or setup script)
|
|
58
|
+
// CREATE TABLE audit_logs (
|
|
59
|
+
// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
// table_name TEXT NOT NULL,
|
|
61
|
+
// operation TEXT NOT NULL,
|
|
62
|
+
// row_id TEXT,
|
|
63
|
+
// user_id TEXT,
|
|
64
|
+
// old_data TEXT,
|
|
65
|
+
// new_data TEXT,
|
|
66
|
+
// created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
67
|
+
// );
|
|
68
|
+
|
|
69
|
+
// 3. Use in your app (e.g. React Router action)
|
|
70
|
+
const audit = withAudit(db, auditLogs, { userId: session.userId })
|
|
71
|
+
|
|
72
|
+
audit.insert(users, { id: "u1", name: "Ada" })
|
|
73
|
+
audit.update(users, eq(users.id, "u1"), { name: "Ada Lovelace" })
|
|
74
|
+
audit.delete(users, eq(users.id, "u1"))
|
|
75
|
+
|
|
76
|
+
// Non-audited access is still available
|
|
77
|
+
audit.db.select().from(users).all()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The wrapper auto-detects primary keys from your Drizzle table schema, captures old/new row data, and runs each operation + audit log insert in a transaction.
|
|
81
|
+
|
|
82
|
+
### D1/SQLite — Triggers
|
|
83
|
+
|
|
84
|
+
If you prefer trigger-based auditing on SQLite (works with D1, better-sqlite3, libsql):
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import {
|
|
88
|
+
createD1AuditInstallSql,
|
|
89
|
+
createAttachD1AuditTriggersSql,
|
|
90
|
+
d1AuditLogTable,
|
|
91
|
+
d1AuditContextTable,
|
|
92
|
+
withD1AuditedTransaction,
|
|
93
|
+
} from "@willyim/drizzle-audit/d1"
|
|
94
|
+
|
|
95
|
+
// 1. Add to schema
|
|
96
|
+
export const auditLogs = d1AuditLogTable()
|
|
97
|
+
export const auditContext = d1AuditContextTable()
|
|
98
|
+
|
|
99
|
+
// 2. Install (creates audit_logs + _audit_context tables + triggers)
|
|
100
|
+
sqlite.exec(createD1AuditInstallSql())
|
|
101
|
+
sqlite.exec(createAttachD1AuditTriggersSql([
|
|
102
|
+
{ table: "users" },
|
|
103
|
+
]))
|
|
104
|
+
|
|
105
|
+
// 3. Use — context is passed via _audit_context table within a transaction
|
|
106
|
+
withD1AuditedTransaction(db, "user_123", (tx) => {
|
|
107
|
+
tx.insert(users).values({ id: "u1", name: "Ada" }).run()
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Since SQLite has no session variables, context (user_id) is stored in a `_audit_context` table that triggers read from within the transaction.
|
|
112
|
+
|
|
113
|
+
For full row snapshots, use the column-aware variant (SQLite can't enumerate columns dynamically):
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
sqlite.exec(createAttachD1AuditTriggersSqlWithColumns([
|
|
117
|
+
{ table: "users", columns: ["id", "name", "email"] },
|
|
118
|
+
]))
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Workspace / Tenant Scoping
|
|
122
|
+
|
|
123
|
+
All three approaches support an optional workspace column for multi-tenant apps.
|
|
124
|
+
|
|
125
|
+
### Postgres
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// Install with workspace column
|
|
129
|
+
createAuditInstallSql({ workspaceIdColumn: "workspace_id" })
|
|
130
|
+
export const auditLogs = pgAuditLogTable({ workspaceIdColumn: "workspace_id" })
|
|
131
|
+
|
|
132
|
+
// Pass workspace at runtime
|
|
133
|
+
await withAuditedTransaction(
|
|
134
|
+
db, userId, async (tx) => { /* ... */ },
|
|
135
|
+
"app.user_id",
|
|
136
|
+
{ workspaceId: "ws_1" },
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
To add workspace to an existing install, use `createAuditAddWorkspaceColumnSql()`.
|
|
141
|
+
|
|
142
|
+
### D1 Runtime
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const audit = withAudit(db, auditLogs, {
|
|
146
|
+
userId: "user_1",
|
|
147
|
+
workspaceId: "ws_1",
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### D1 Triggers
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
createD1AuditInstallSql({ workspaceIdColumn: "workspace_id" })
|
|
155
|
+
createAttachD1AuditTriggersSql(
|
|
156
|
+
[{ table: "users" }],
|
|
157
|
+
{ workspaceIdColumn: "workspace_id" },
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
withD1AuditedTransaction(db, "user_1", (tx) => { /* ... */ }, {
|
|
161
|
+
workspaceId: "ws_1",
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## CLI
|
|
166
|
+
|
|
167
|
+
Generate a Drizzle migration with audit SQL appended:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npx drizzle-audit generate \
|
|
171
|
+
--config app/db/audit.ts \
|
|
172
|
+
--drizzle-config drizzle.config.ts \
|
|
173
|
+
--migrations-dir drizzle
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Your config file exports a `createAuditSql()` function:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// app/db/audit.ts
|
|
180
|
+
import { createAuditInstallSql, createAttachAuditTriggersSql } from "@willyim/drizzle-audit/postgres"
|
|
181
|
+
|
|
182
|
+
export function createAuditSql() {
|
|
183
|
+
return [
|
|
184
|
+
createAuditInstallSql(),
|
|
185
|
+
createAttachAuditTriggersSql([
|
|
186
|
+
{ table: "users" },
|
|
187
|
+
{ table: "workspaces" },
|
|
188
|
+
]),
|
|
189
|
+
].join("\n\n")
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## API Reference
|
|
194
|
+
|
|
195
|
+
### `@willyim/drizzle-audit/postgres`
|
|
196
|
+
|
|
197
|
+
| Export | Description |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `pgAuditLogTable(options?)` | Drizzle table definition for `audit_logs` |
|
|
200
|
+
| `createAuditInstallSql(options?)` | SQL to create the audit table, indexes, and trigger function |
|
|
201
|
+
| `createAttachAuditTriggerSql(target, options?)` | SQL to attach audit trigger to one table |
|
|
202
|
+
| `createAttachAuditTriggersSql(targets, options?)` | Same, for multiple tables |
|
|
203
|
+
| `createAuditAddWorkspaceColumnSql(options)` | SQL to add workspace column to existing install |
|
|
204
|
+
| `setAuditContext(db, actorId, contextKey?, options?)` | Set actor context in current transaction |
|
|
205
|
+
| `withAuditedTransaction(db, actorId, callback, contextKey?, options?)` | Transaction wrapper with actor context |
|
|
206
|
+
|
|
207
|
+
### `@willyim/drizzle-audit/d1`
|
|
208
|
+
|
|
209
|
+
| Export | Description |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `d1AuditLogTable(options?)` | Drizzle SQLite table definition for `audit_logs` |
|
|
212
|
+
| `d1AuditContextTable(options?)` | Drizzle SQLite table definition for `_audit_context` |
|
|
213
|
+
| `createD1AuditInstallSql(options?)` | SQL to create audit + context tables and indexes |
|
|
214
|
+
| `createAttachD1AuditTriggerSql(target, options?)` | SQL for audit triggers (one table) |
|
|
215
|
+
| `createAttachD1AuditTriggersSql(targets, options?)` | Same, for multiple tables |
|
|
216
|
+
| `createAttachD1AuditTriggerSqlWithColumns(target, options?)` | Column-aware triggers with full row snapshots |
|
|
217
|
+
| `createAttachD1AuditTriggersSqlWithColumns(targets, options?)` | Same, for multiple tables |
|
|
218
|
+
| `setD1AuditContext(db, actorId, options?)` | Set actor in `_audit_context` table |
|
|
219
|
+
| `clearD1AuditContext(db, options?)` | Clear actor from `_audit_context` table |
|
|
220
|
+
| `withD1AuditedTransaction(db, actorId, callback, options?)` | Transaction wrapper with context management |
|
|
221
|
+
|
|
222
|
+
### `@willyim/drizzle-audit/d1-runtime`
|
|
223
|
+
|
|
224
|
+
| Export | Description |
|
|
225
|
+
|---|---|
|
|
226
|
+
| `withAudit(db, auditTable, context)` | App-level audit wrapper (no triggers needed) |
|
|
227
|
+
|
|
228
|
+
`withAudit` returns an object with:
|
|
229
|
+
- `.insert(table, data)` — Insert + audit log
|
|
230
|
+
- `.update(table, where, data)` — Fetch old rows, update, audit log (per row)
|
|
231
|
+
- `.delete(table, where)` — Fetch old rows, delete, audit log (per row)
|
|
232
|
+
- `.db` — Raw Drizzle instance for non-audited operations
|
|
233
|
+
|
|
234
|
+
## Audit Log Schema
|
|
235
|
+
|
|
236
|
+
### Postgres
|
|
237
|
+
|
|
238
|
+
```sql
|
|
239
|
+
CREATE TABLE audit_logs (
|
|
240
|
+
id BIGSERIAL PRIMARY KEY,
|
|
241
|
+
table_name TEXT NOT NULL,
|
|
242
|
+
operation TEXT NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE')),
|
|
243
|
+
row_id TEXT,
|
|
244
|
+
user_id TEXT,
|
|
245
|
+
old_data JSONB,
|
|
246
|
+
new_data JSONB,
|
|
247
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
248
|
+
);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### D1/SQLite
|
|
252
|
+
|
|
253
|
+
```sql
|
|
254
|
+
CREATE TABLE audit_logs (
|
|
255
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
256
|
+
table_name TEXT NOT NULL,
|
|
257
|
+
operation TEXT NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE')),
|
|
258
|
+
row_id TEXT,
|
|
259
|
+
user_id TEXT,
|
|
260
|
+
old_data TEXT, -- JSON string
|
|
261
|
+
new_data TEXT, -- JSON string
|
|
262
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
263
|
+
);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Which Approach Should I Use?
|
|
267
|
+
|
|
268
|
+
| | Postgres Triggers | D1 Runtime (`withAudit`) | D1 Triggers |
|
|
269
|
+
|---|---|---|---|
|
|
270
|
+
| **Setup** | Migration SQL | Create table only | Migration SQL + context table |
|
|
271
|
+
| **Row snapshots** | Automatic (full JSONB) | Automatic (full JSON) | Requires listing columns |
|
|
272
|
+
| **User context** | Native session vars | Available in JS | `_audit_context` table |
|
|
273
|
+
| **Bypass risk** | Low (DB-level) | Medium (must use wrapper) | Low (DB-level) |
|
|
274
|
+
| **Best for** | Postgres apps | D1/Cloudflare Workers | SQLite apps needing DB-level guarantees |
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI: run drizzle-kit generate, then append audit SQL from the consumer's
|
|
4
|
+
* config to the newly created migration file.
|
|
5
|
+
*
|
|
6
|
+
* Usage: drizzle-audit generate [options]
|
|
7
|
+
* Options:
|
|
8
|
+
* --config <path> Path to audit config (TS or JS) exporting createAuditSql/createWebAuditSql
|
|
9
|
+
* --drizzle-config <path> Path to drizzle config for drizzle-kit (default: drizzle.config.ts)
|
|
10
|
+
* --migrations-dir <path> Dir for migrations relative to cwd (default: drizzle)
|
|
11
|
+
* --cwd <path> Working directory (default: process.cwd())
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=generate-migration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-migration.d.ts","sourceRoot":"","sources":["../../../src/cli/generate-migration.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI: run drizzle-kit generate, then append audit SQL from the consumer's
|
|
4
|
+
* config to the newly created migration file.
|
|
5
|
+
*
|
|
6
|
+
* Usage: drizzle-audit generate [options]
|
|
7
|
+
* Options:
|
|
8
|
+
* --config <path> Path to audit config (TS or JS) exporting createAuditSql/createWebAuditSql
|
|
9
|
+
* --drizzle-config <path> Path to drizzle config for drizzle-kit (default: drizzle.config.ts)
|
|
10
|
+
* --migrations-dir <path> Dir for migrations relative to cwd (default: drizzle)
|
|
11
|
+
* --cwd <path> Working directory (default: process.cwd())
|
|
12
|
+
*/
|
|
13
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
14
|
+
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
const DEFAULT_DRIZZLE_CONFIG = "drizzle.config.ts";
|
|
18
|
+
const DEFAULT_MIGRATIONS_DIR = "drizzle";
|
|
19
|
+
function parseArgs() {
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
if (args[0] !== "generate") {
|
|
22
|
+
console.error("Usage: drizzle-audit generate --config <path> [options]");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
let config = "";
|
|
26
|
+
let drizzleConfig = DEFAULT_DRIZZLE_CONFIG;
|
|
27
|
+
let migrationsDir = DEFAULT_MIGRATIONS_DIR;
|
|
28
|
+
let cwd = process.cwd();
|
|
29
|
+
for (let i = 1; i < args.length; i++) {
|
|
30
|
+
if (args[i] === "--config" && args[i + 1]) {
|
|
31
|
+
config = args[++i];
|
|
32
|
+
}
|
|
33
|
+
else if (args[i] === "--drizzle-config" && args[i + 1]) {
|
|
34
|
+
drizzleConfig = args[++i];
|
|
35
|
+
}
|
|
36
|
+
else if (args[i] === "--migrations-dir" && args[i + 1]) {
|
|
37
|
+
migrationsDir = args[++i];
|
|
38
|
+
}
|
|
39
|
+
else if (args[i] === "--cwd" && args[i + 1]) {
|
|
40
|
+
cwd = args[++i];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!config) {
|
|
44
|
+
console.error("Missing required --config <path>");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
return { config, drizzleConfig, migrationsDir, cwd };
|
|
48
|
+
}
|
|
49
|
+
function findNewestMigrationDir(migrationsDir) {
|
|
50
|
+
const abs = resolve(migrationsDir);
|
|
51
|
+
const entries = readdirSync(abs, { withFileTypes: true });
|
|
52
|
+
const dirs = entries
|
|
53
|
+
.filter((e) => e.isDirectory())
|
|
54
|
+
.map((e) => e.name)
|
|
55
|
+
.filter((n) => /^\d+_.+/.test(n))
|
|
56
|
+
.sort();
|
|
57
|
+
const newest = dirs[dirs.length - 1];
|
|
58
|
+
if (!newest) {
|
|
59
|
+
throw new Error(`No migration folders found in ${abs}`);
|
|
60
|
+
}
|
|
61
|
+
return resolve(abs, newest);
|
|
62
|
+
}
|
|
63
|
+
async function getAuditSql(configPath, cwd) {
|
|
64
|
+
const resolved = resolve(cwd, configPath);
|
|
65
|
+
const ext = resolved.endsWith(".ts")
|
|
66
|
+
? ".ts"
|
|
67
|
+
: resolved.endsWith(".mts")
|
|
68
|
+
? ".mts"
|
|
69
|
+
: "";
|
|
70
|
+
if (ext) {
|
|
71
|
+
try {
|
|
72
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
73
|
+
const runnerPath = resolve(__dirname, "runner.js");
|
|
74
|
+
const out = execSync(`node --experimental-strip-types "${runnerPath}" "${resolved}"`, { cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
|
|
75
|
+
return out.trim();
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
const err = e;
|
|
79
|
+
console.error("Failed to load .ts config. Requires Node >= 22.6.0.");
|
|
80
|
+
console.error(err.stderr ?? err.message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const url = pathToFileURL(resolved).href;
|
|
85
|
+
const mod = await import(url);
|
|
86
|
+
const fn = mod.createAuditSql ?? mod.createWebAuditSql;
|
|
87
|
+
if (typeof fn !== "function") {
|
|
88
|
+
console.error("Config module must export createAuditSql() or createWebAuditSql()");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
return String(fn()).trim();
|
|
92
|
+
}
|
|
93
|
+
async function main() {
|
|
94
|
+
const { config, drizzleConfig, migrationsDir, cwd } = parseArgs();
|
|
95
|
+
const drizzleConfigPath = resolve(cwd, drizzleConfig);
|
|
96
|
+
const migrationsAbs = resolve(cwd, migrationsDir);
|
|
97
|
+
console.log("Running drizzle-kit generate...");
|
|
98
|
+
const kit = spawnSync("npx", ["drizzle-kit", "generate", "--config", drizzleConfigPath], {
|
|
99
|
+
cwd,
|
|
100
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
101
|
+
input: "\n\n",
|
|
102
|
+
encoding: "utf-8",
|
|
103
|
+
});
|
|
104
|
+
if (kit.status !== 0) {
|
|
105
|
+
process.exit(kit.status ?? 1);
|
|
106
|
+
}
|
|
107
|
+
const migrationDir = findNewestMigrationDir(migrationsAbs);
|
|
108
|
+
const migrationFile = resolve(migrationDir, "migration.sql");
|
|
109
|
+
const existing = readFileSync(migrationFile, "utf-8");
|
|
110
|
+
const auditSql = await getAuditSql(config, cwd);
|
|
111
|
+
const separator = "\n\n";
|
|
112
|
+
writeFileSync(migrationFile, existing + separator + "-- drizzle-audit\n\n" + auditSql, "utf-8");
|
|
113
|
+
console.log("Appended audit SQL to", migrationFile);
|
|
114
|
+
}
|
|
115
|
+
main().catch((e) => {
|
|
116
|
+
console.error(e);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/cli/runner.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner script used to load a consumer's audit config (TS or JS) and print the
|
|
3
|
+
* generated SQL to stdout. Invoked with: node --experimental-strip-types runner.js <config-path>
|
|
4
|
+
* Config module must export createAuditSql() or createWebAuditSql().
|
|
5
|
+
*/
|
|
6
|
+
const configPath = process.argv[2];
|
|
7
|
+
if (!configPath) {
|
|
8
|
+
process.stderr.write("Usage: runner.js <config-path>\n");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const path = await import("node:path");
|
|
12
|
+
const { pathToFileURL } = await import("node:url");
|
|
13
|
+
const resolved = path.resolve(process.cwd(), configPath);
|
|
14
|
+
const url = pathToFileURL(resolved).href;
|
|
15
|
+
const mod = await import(url);
|
|
16
|
+
const fn = mod.createAuditSql ?? mod.createWebAuditSql;
|
|
17
|
+
if (typeof fn !== "function") {
|
|
18
|
+
process.stderr.write("Config module must export createAuditSql() or createWebAuditSql()\n");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const sql = String(fn());
|
|
22
|
+
process.stdout.write(sql);
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export type D1AuditLogTableOptions = {
|
|
2
|
+
/** When set (e.g. "workspace_id"), the table definition includes this optional column. */
|
|
3
|
+
workspaceIdColumn?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function d1AuditLogTable(options?: D1AuditLogTableOptions): import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
6
|
+
name: "audit_logs";
|
|
7
|
+
schema: undefined;
|
|
8
|
+
columns: {
|
|
9
|
+
old_data: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
10
|
+
name: string;
|
|
11
|
+
tableName: "audit_logs";
|
|
12
|
+
dataType: "string";
|
|
13
|
+
data: string;
|
|
14
|
+
driverParam: string;
|
|
15
|
+
notNull: false;
|
|
16
|
+
hasDefault: false;
|
|
17
|
+
isPrimaryKey: false;
|
|
18
|
+
isAutoincrement: false;
|
|
19
|
+
hasRuntimeDefault: false;
|
|
20
|
+
enumValues: [string, ...string[]];
|
|
21
|
+
baseColumn: never;
|
|
22
|
+
identity: undefined;
|
|
23
|
+
generated: undefined;
|
|
24
|
+
}, {}>;
|
|
25
|
+
new_data: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
26
|
+
name: string;
|
|
27
|
+
tableName: "audit_logs";
|
|
28
|
+
dataType: "string";
|
|
29
|
+
data: string;
|
|
30
|
+
driverParam: string;
|
|
31
|
+
notNull: false;
|
|
32
|
+
hasDefault: false;
|
|
33
|
+
isPrimaryKey: false;
|
|
34
|
+
isAutoincrement: false;
|
|
35
|
+
hasRuntimeDefault: false;
|
|
36
|
+
enumValues: [string, ...string[]];
|
|
37
|
+
baseColumn: never;
|
|
38
|
+
identity: undefined;
|
|
39
|
+
generated: undefined;
|
|
40
|
+
}, {}>;
|
|
41
|
+
created_at: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
42
|
+
name: string;
|
|
43
|
+
tableName: "audit_logs";
|
|
44
|
+
dataType: "string";
|
|
45
|
+
data: string;
|
|
46
|
+
driverParam: string;
|
|
47
|
+
notNull: true;
|
|
48
|
+
hasDefault: true;
|
|
49
|
+
isPrimaryKey: false;
|
|
50
|
+
isAutoincrement: false;
|
|
51
|
+
hasRuntimeDefault: false;
|
|
52
|
+
enumValues: [string, ...string[]];
|
|
53
|
+
baseColumn: never;
|
|
54
|
+
identity: undefined;
|
|
55
|
+
generated: undefined;
|
|
56
|
+
}, {}>;
|
|
57
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
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: true;
|
|
66
|
+
isAutoincrement: false;
|
|
67
|
+
hasRuntimeDefault: false;
|
|
68
|
+
enumValues: undefined;
|
|
69
|
+
baseColumn: never;
|
|
70
|
+
identity: undefined;
|
|
71
|
+
generated: undefined;
|
|
72
|
+
}, {}>;
|
|
73
|
+
table_name: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
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: [string, ...string[]];
|
|
85
|
+
baseColumn: never;
|
|
86
|
+
identity: undefined;
|
|
87
|
+
generated: undefined;
|
|
88
|
+
}, {}>;
|
|
89
|
+
operation: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
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: [string, ...string[]];
|
|
101
|
+
baseColumn: never;
|
|
102
|
+
identity: undefined;
|
|
103
|
+
generated: undefined;
|
|
104
|
+
}, {}>;
|
|
105
|
+
row_id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
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: [string, ...string[]];
|
|
117
|
+
baseColumn: never;
|
|
118
|
+
identity: undefined;
|
|
119
|
+
generated: undefined;
|
|
120
|
+
}, {}>;
|
|
121
|
+
user_id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
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: [string, ...string[]];
|
|
133
|
+
baseColumn: never;
|
|
134
|
+
identity: undefined;
|
|
135
|
+
generated: undefined;
|
|
136
|
+
}, {}>;
|
|
137
|
+
};
|
|
138
|
+
dialect: "sqlite";
|
|
139
|
+
}>;
|
|
140
|
+
export declare function d1AuditContextTable(options?: {
|
|
141
|
+
contextTable?: string;
|
|
142
|
+
}): import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
143
|
+
name: string;
|
|
144
|
+
schema: undefined;
|
|
145
|
+
columns: {
|
|
146
|
+
key: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
147
|
+
name: string;
|
|
148
|
+
tableName: string;
|
|
149
|
+
dataType: "string";
|
|
150
|
+
data: string;
|
|
151
|
+
driverParam: string;
|
|
152
|
+
notNull: true;
|
|
153
|
+
hasDefault: false;
|
|
154
|
+
isPrimaryKey: true;
|
|
155
|
+
isAutoincrement: false;
|
|
156
|
+
hasRuntimeDefault: false;
|
|
157
|
+
enumValues: [string, ...string[]];
|
|
158
|
+
baseColumn: never;
|
|
159
|
+
identity: undefined;
|
|
160
|
+
generated: undefined;
|
|
161
|
+
}, {}>;
|
|
162
|
+
value: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
163
|
+
name: string;
|
|
164
|
+
tableName: string;
|
|
165
|
+
dataType: "string";
|
|
166
|
+
data: string;
|
|
167
|
+
driverParam: string;
|
|
168
|
+
notNull: false;
|
|
169
|
+
hasDefault: false;
|
|
170
|
+
isPrimaryKey: false;
|
|
171
|
+
isAutoincrement: false;
|
|
172
|
+
hasRuntimeDefault: false;
|
|
173
|
+
enumValues: [string, ...string[]];
|
|
174
|
+
baseColumn: never;
|
|
175
|
+
identity: undefined;
|
|
176
|
+
generated: undefined;
|
|
177
|
+
}, {}>;
|
|
178
|
+
};
|
|
179
|
+
dialect: "sqlite";
|
|
180
|
+
}>;
|
|
181
|
+
//# sourceMappingURL=audit-log-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log-schema.d.ts","sourceRoot":"","sources":["../../../src/d1/audit-log-schema.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,sBAAsB,GAAG;IACnC,0FAA0F;IAC1F,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyB/D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAMtE"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { index, integer, sqliteTable, text, } from "drizzle-orm/sqlite-core";
|
|
2
|
+
export function d1AuditLogTable(options) {
|
|
3
|
+
const workspaceIdColumn = options?.workspaceIdColumn?.trim();
|
|
4
|
+
const columns = {
|
|
5
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
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: text("old_data"),
|
|
14
|
+
new_data: text("new_data"),
|
|
15
|
+
created_at: text("created_at").notNull().default("(datetime('now'))"),
|
|
16
|
+
};
|
|
17
|
+
return sqliteTable("audit_logs", columns, (table) => [
|
|
18
|
+
index("audit_logs_table_name_idx").on(table.table_name),
|
|
19
|
+
index("audit_logs_row_id_idx").on(table.row_id),
|
|
20
|
+
index("audit_logs_user_id_idx").on(table.user_id),
|
|
21
|
+
index("audit_logs_created_at_idx").on(table.created_at),
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
export function d1AuditContextTable(options) {
|
|
25
|
+
const tableName = options?.contextTable ?? "_audit_context";
|
|
26
|
+
return sqliteTable(tableName, {
|
|
27
|
+
key: text("key").primaryKey(),
|
|
28
|
+
value: text("value"),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { d1AuditLogTable, d1AuditContextTable } from "./audit-log-schema.js";
|
|
2
|
+
export type { D1AuditLogTableOptions } from "./audit-log-schema.js";
|
|
3
|
+
export { createAttachD1AuditTriggerSql, createAttachD1AuditTriggersSql, createAttachD1AuditTriggerSqlWithColumns, createAttachD1AuditTriggersSqlWithColumns, createD1AuditInstallSql, } from "./sql.js";
|
|
4
|
+
export type { D1AuditTriggerTargetWithColumns } from "./sql.js";
|
|
5
|
+
export { clearD1AuditContext, setD1AuditContext, withD1AuditedTransaction, } from "./runtime.js";
|
|
6
|
+
export type { D1AuditInstallOptions, D1AuditSqlExecutor, D1AuditTriggerTarget, } from "./types.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/d1/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC5E,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,EAC9B,wCAAwC,EACxC,yCAAyC,EACzC,uBAAuB,GACxB,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,+BAA+B,EAAE,MAAM,UAAU,CAAA;AAC/D,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { d1AuditLogTable, d1AuditContextTable } from "./audit-log-schema.js";
|
|
2
|
+
export { createAttachD1AuditTriggerSql, createAttachD1AuditTriggersSql, createAttachD1AuditTriggerSqlWithColumns, createAttachD1AuditTriggersSqlWithColumns, createD1AuditInstallSql, } from "./sql.js";
|
|
3
|
+
export { clearD1AuditContext, setD1AuditContext, withD1AuditedTransaction, } from "./runtime.js";
|