@pikku/kysely 0.12.11 → 0.12.12
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/CHANGELOG.md +9 -0
- package/dist/src/create-audited-kysely.d.ts +10 -0
- package/dist/src/create-audited-kysely.js +167 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/kysely-ai-agent-run-service.d.ts +1 -0
- package/dist/src/kysely-ai-agent-run-service.js +4 -1
- package/dist/src/kysely-channel-store.d.ts +3 -0
- package/dist/src/kysely-channel-store.js +25 -0
- package/dist/src/kysely-tables.d.ts +2 -0
- package/dist/src/kysely-workflow-mirror.d.ts +60 -0
- package/dist/src/kysely-workflow-mirror.js +329 -0
- package/dist/src/kysely-workflow-service.d.ts +13 -13
- package/dist/src/kysely-workflow-service.js +13 -13
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/create-audited-kysely.test.ts +560 -0
- package/src/create-audited-kysely.ts +254 -0
- package/src/index.ts +5 -0
- package/src/kysely-ai-agent-run-service.ts +6 -1
- package/src/kysely-ai-run-state-service.ts +6 -2
- package/src/kysely-channel-store.ts +27 -0
- package/src/kysely-session-store.ts +1 -3
- package/src/kysely-tables.ts +2 -0
- package/src/kysely-workflow-mirror.test.ts +246 -0
- package/src/kysely-workflow-mirror.ts +438 -0
- package/src/kysely-workflow-service.ts +25 -13
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { afterEach, describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import Database from 'better-sqlite3'
|
|
4
|
+
import { Kysely, SqliteDialect, type Generated } from 'kysely'
|
|
5
|
+
import {
|
|
6
|
+
createInvocationAudit,
|
|
7
|
+
NoopAuditService,
|
|
8
|
+
type AuditEvent,
|
|
9
|
+
type AuditService,
|
|
10
|
+
} from '../../../core/src/index.js'
|
|
11
|
+
|
|
12
|
+
import { createAuditedKysely } from './create-audited-kysely.js'
|
|
13
|
+
|
|
14
|
+
interface TodoTable {
|
|
15
|
+
id: Generated<number>
|
|
16
|
+
title: string
|
|
17
|
+
done: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TestDB {
|
|
21
|
+
todos: TodoTable
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface AuditEventTable {
|
|
25
|
+
eventId: string
|
|
26
|
+
occurredAt: string
|
|
27
|
+
type: string
|
|
28
|
+
source: string
|
|
29
|
+
outcome: string | null
|
|
30
|
+
functionId: string | null
|
|
31
|
+
wireType: string | null
|
|
32
|
+
wireId: string | null
|
|
33
|
+
traceId: string | null
|
|
34
|
+
transactionId: string | null
|
|
35
|
+
queryId: string | null
|
|
36
|
+
actorUserId: string | null
|
|
37
|
+
actorOrgId: string | null
|
|
38
|
+
actorPikkuUserId: string | null
|
|
39
|
+
metadataJson: string | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface AuditVerifierDB extends TestDB {
|
|
43
|
+
audit_events: AuditEventTable
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createDb(): Kysely<TestDB> {
|
|
47
|
+
return new Kysely<TestDB>({
|
|
48
|
+
dialect: new SqliteDialect({
|
|
49
|
+
database: new Database(':memory:'),
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function createTodosTable(db: Kysely<TestDB>): Promise<void> {
|
|
55
|
+
await db.schema
|
|
56
|
+
.createTable('todos')
|
|
57
|
+
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
|
|
58
|
+
.addColumn('title', 'text', (col) => col.notNull())
|
|
59
|
+
.addColumn('done', 'integer', (col) => col.notNull().defaultTo(0))
|
|
60
|
+
.execute()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function createAuditEventsTable(
|
|
64
|
+
db: Kysely<AuditVerifierDB>
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
await db.schema
|
|
67
|
+
.createTable('audit_events')
|
|
68
|
+
.addColumn('eventId', 'text', (col) => col.primaryKey())
|
|
69
|
+
.addColumn('occurredAt', 'text', (col) => col.notNull())
|
|
70
|
+
.addColumn('type', 'text', (col) => col.notNull())
|
|
71
|
+
.addColumn('source', 'text', (col) => col.notNull())
|
|
72
|
+
.addColumn('outcome', 'text')
|
|
73
|
+
.addColumn('functionId', 'text')
|
|
74
|
+
.addColumn('wireType', 'text')
|
|
75
|
+
.addColumn('wireId', 'text')
|
|
76
|
+
.addColumn('traceId', 'text')
|
|
77
|
+
.addColumn('transactionId', 'text')
|
|
78
|
+
.addColumn('queryId', 'text')
|
|
79
|
+
.addColumn('actorUserId', 'text')
|
|
80
|
+
.addColumn('actorOrgId', 'text')
|
|
81
|
+
.addColumn('actorPikkuUserId', 'text')
|
|
82
|
+
.addColumn('metadataJson', 'text')
|
|
83
|
+
.execute()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class ImmediateAuditService implements AuditService {
|
|
87
|
+
constructor(private readonly db: Kysely<AuditVerifierDB>) {}
|
|
88
|
+
|
|
89
|
+
async audit(event: AuditEvent): Promise<void> {
|
|
90
|
+
await this.insertEvent(event)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async write(batch: AuditEvent[]): Promise<void> {
|
|
94
|
+
for (const event of batch) {
|
|
95
|
+
await this.insertEvent(event)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async insertEvent(event: AuditEvent): Promise<void> {
|
|
100
|
+
await this.db
|
|
101
|
+
.insertInto('audit_events')
|
|
102
|
+
.values({
|
|
103
|
+
eventId:
|
|
104
|
+
event.eventId ??
|
|
105
|
+
`${event.traceId ?? 'trace'}:${event.queryId ?? 'single'}:${event.occurredAt}`,
|
|
106
|
+
occurredAt: event.occurredAt,
|
|
107
|
+
type: event.type,
|
|
108
|
+
source: event.source,
|
|
109
|
+
outcome: event.outcome ?? null,
|
|
110
|
+
functionId: event.functionId ?? null,
|
|
111
|
+
wireType: event.wireType ?? null,
|
|
112
|
+
wireId: event.wireId ?? null,
|
|
113
|
+
traceId: event.traceId ?? null,
|
|
114
|
+
transactionId: event.transactionId ?? null,
|
|
115
|
+
queryId: event.queryId ?? null,
|
|
116
|
+
actorUserId: event.actor?.userId ?? null,
|
|
117
|
+
actorOrgId: event.actor?.orgId ?? null,
|
|
118
|
+
actorPikkuUserId: event.actor?.pikkuUserId ?? null,
|
|
119
|
+
metadataJson: event.metadata ? JSON.stringify(event.metadata) : null,
|
|
120
|
+
})
|
|
121
|
+
.execute()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function countPersistedEvents(
|
|
126
|
+
db: Kysely<AuditVerifierDB>
|
|
127
|
+
): Promise<number> {
|
|
128
|
+
const row = await db
|
|
129
|
+
.selectFrom('audit_events')
|
|
130
|
+
.select((eb) => eb.fn.countAll<number>().as('count'))
|
|
131
|
+
.executeTakeFirstOrThrow()
|
|
132
|
+
|
|
133
|
+
return Number(row.count)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function listPersistedEvents(
|
|
137
|
+
db: Kysely<AuditVerifierDB>
|
|
138
|
+
): Promise<
|
|
139
|
+
Array<AuditEventTable & { metadata: Record<string, unknown> | null }>
|
|
140
|
+
> {
|
|
141
|
+
const rows = await db
|
|
142
|
+
.selectFrom('audit_events')
|
|
143
|
+
.selectAll()
|
|
144
|
+
.orderBy('occurredAt', 'asc')
|
|
145
|
+
.execute()
|
|
146
|
+
|
|
147
|
+
return rows.map((row) => ({
|
|
148
|
+
...row,
|
|
149
|
+
metadata: row.metadataJson
|
|
150
|
+
? (JSON.parse(row.metadataJson) as Record<string, unknown>)
|
|
151
|
+
: null,
|
|
152
|
+
}))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
describe('createAuditedKysely', () => {
|
|
156
|
+
const dbs: Kysely<TestDB>[] = []
|
|
157
|
+
|
|
158
|
+
afterEach(async () => {
|
|
159
|
+
while (dbs.length > 0) {
|
|
160
|
+
const db = dbs.pop()!
|
|
161
|
+
await db.destroy()
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('captures insert queries through the invocation audit wrapper', async () => {
|
|
166
|
+
const events: AuditEvent[] = []
|
|
167
|
+
const db = createDb()
|
|
168
|
+
dbs.push(db)
|
|
169
|
+
await createTodosTable(db)
|
|
170
|
+
|
|
171
|
+
const auditService: AuditService = {
|
|
172
|
+
audit: async (event) => {
|
|
173
|
+
events.push(event)
|
|
174
|
+
},
|
|
175
|
+
write: async (batch) => {
|
|
176
|
+
events.push(...batch)
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
const wire: any = {
|
|
180
|
+
wireType: 'rpc',
|
|
181
|
+
wireId: 'wire-1',
|
|
182
|
+
traceId: 'trace-1',
|
|
183
|
+
functionId: 'todos:create',
|
|
184
|
+
session: { userId: 'user-1', orgId: 'org-1' },
|
|
185
|
+
pikkuUserId: 'user-1',
|
|
186
|
+
audit: { durability: 'best-effort' },
|
|
187
|
+
logger: { warn: () => {} },
|
|
188
|
+
}
|
|
189
|
+
const audit = createInvocationAudit(auditService, wire)
|
|
190
|
+
const auditedDb = createAuditedKysely(db, { audit })
|
|
191
|
+
|
|
192
|
+
await auditedDb
|
|
193
|
+
.insertInto('todos')
|
|
194
|
+
.values({ title: 'write tests', done: 0 })
|
|
195
|
+
.execute()
|
|
196
|
+
await audit.close()
|
|
197
|
+
|
|
198
|
+
assert.equal(events.length, 1)
|
|
199
|
+
assert.equal(events[0]?.type, 'db.query')
|
|
200
|
+
assert.equal(events[0]?.traceId, 'trace-1')
|
|
201
|
+
assert.equal(events[0]?.functionId, 'todos:create')
|
|
202
|
+
assert.equal(events[0]?.queryId, null)
|
|
203
|
+
assert.deepEqual(events[0]?.metadata, {
|
|
204
|
+
queryKind: 'insert',
|
|
205
|
+
tables: ['todos'],
|
|
206
|
+
changedColumns: ['title', 'done'],
|
|
207
|
+
changes: { title: 'write tests', done: 0 },
|
|
208
|
+
rowCount: 1,
|
|
209
|
+
})
|
|
210
|
+
assert.deepEqual(events[0]?.actor, {
|
|
211
|
+
userId: 'user-1',
|
|
212
|
+
orgId: 'org-1',
|
|
213
|
+
pikkuUserId: 'user-1',
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('captures update changes from the query tree', async () => {
|
|
218
|
+
const events: AuditEvent[] = []
|
|
219
|
+
const db = createDb()
|
|
220
|
+
dbs.push(db)
|
|
221
|
+
await createTodosTable(db)
|
|
222
|
+
await db.insertInto('todos').values({ title: 'before', done: 0 }).execute()
|
|
223
|
+
|
|
224
|
+
const audit = createInvocationAudit(
|
|
225
|
+
{
|
|
226
|
+
audit: async (event) => {
|
|
227
|
+
events.push(event)
|
|
228
|
+
},
|
|
229
|
+
write: async (batch) => {
|
|
230
|
+
events.push(...batch)
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
wireType: 'rpc',
|
|
235
|
+
wireId: 'wire-2',
|
|
236
|
+
traceId: 'trace-2',
|
|
237
|
+
functionId: 'todos:update',
|
|
238
|
+
session: { userId: 'user-2' },
|
|
239
|
+
pikkuUserId: 'user-2',
|
|
240
|
+
audit: { durability: 'best-effort' },
|
|
241
|
+
logger: { warn: () => {} },
|
|
242
|
+
} as any
|
|
243
|
+
)
|
|
244
|
+
const auditedDb = createAuditedKysely(db, { audit })
|
|
245
|
+
|
|
246
|
+
await auditedDb
|
|
247
|
+
.updateTable('todos')
|
|
248
|
+
.set({ title: 'after', done: 1 })
|
|
249
|
+
.where('id', '=', 1)
|
|
250
|
+
.execute()
|
|
251
|
+
await audit.close()
|
|
252
|
+
|
|
253
|
+
assert.equal(events.length, 1)
|
|
254
|
+
assert.equal(events[0]?.metadata?.queryKind, 'update')
|
|
255
|
+
assert.deepEqual(events[0]?.metadata?.changedColumns, ['title', 'done'])
|
|
256
|
+
assert.deepEqual(events[0]?.metadata?.changes, {
|
|
257
|
+
title: 'after',
|
|
258
|
+
done: 1,
|
|
259
|
+
})
|
|
260
|
+
assert.deepEqual(events[0]?.actor, {
|
|
261
|
+
userId: 'user-2',
|
|
262
|
+
orgId: undefined,
|
|
263
|
+
pikkuUserId: 'user-2',
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('keeps query ids when multiple queries are captured in one invocation', async () => {
|
|
268
|
+
const events: AuditEvent[] = []
|
|
269
|
+
const db = createDb()
|
|
270
|
+
dbs.push(db)
|
|
271
|
+
await createTodosTable(db)
|
|
272
|
+
|
|
273
|
+
const audit = createInvocationAudit(
|
|
274
|
+
{
|
|
275
|
+
audit: async (event) => {
|
|
276
|
+
events.push(event)
|
|
277
|
+
},
|
|
278
|
+
write: async (batch) => {
|
|
279
|
+
events.push(...batch)
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
wireType: 'rpc',
|
|
284
|
+
wireId: 'wire-3',
|
|
285
|
+
traceId: 'trace-3',
|
|
286
|
+
functionId: 'todos:multi',
|
|
287
|
+
session: { userId: 'user-3' },
|
|
288
|
+
pikkuUserId: 'user-3',
|
|
289
|
+
audit: { durability: 'best-effort' },
|
|
290
|
+
logger: { warn: () => {} },
|
|
291
|
+
} as any
|
|
292
|
+
)
|
|
293
|
+
const auditedDb = createAuditedKysely(db, {
|
|
294
|
+
audit,
|
|
295
|
+
transactionId: 'tx-1',
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
await auditedDb
|
|
299
|
+
.insertInto('todos')
|
|
300
|
+
.values({ title: 'first', done: 0 })
|
|
301
|
+
.execute()
|
|
302
|
+
await auditedDb
|
|
303
|
+
.insertInto('todos')
|
|
304
|
+
.values({ title: 'second', done: 1 })
|
|
305
|
+
.execute()
|
|
306
|
+
await audit.close()
|
|
307
|
+
|
|
308
|
+
assert.equal(events.length, 2)
|
|
309
|
+
assert.equal(events[0]?.transactionId, 'tx-1')
|
|
310
|
+
assert.equal(events[1]?.transactionId, 'tx-1')
|
|
311
|
+
assert.equal(events[0]?.queryId, 'q-1')
|
|
312
|
+
assert.equal(events[1]?.queryId, 'q-2')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('captures select queries by default', async () => {
|
|
316
|
+
const events: AuditEvent[] = []
|
|
317
|
+
const db = createDb()
|
|
318
|
+
dbs.push(db)
|
|
319
|
+
await createTodosTable(db)
|
|
320
|
+
await db.insertInto('todos').values({ title: 'read me', done: 0 }).execute()
|
|
321
|
+
|
|
322
|
+
const audit = createInvocationAudit(
|
|
323
|
+
{
|
|
324
|
+
audit: async (event) => {
|
|
325
|
+
events.push(event)
|
|
326
|
+
},
|
|
327
|
+
write: async (batch) => {
|
|
328
|
+
events.push(...batch)
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
wireType: 'rpc',
|
|
333
|
+
wireId: 'wire-4',
|
|
334
|
+
audit: { durability: 'best-effort' },
|
|
335
|
+
logger: { warn: () => {} },
|
|
336
|
+
} as any
|
|
337
|
+
)
|
|
338
|
+
const auditedDb = createAuditedKysely(db, { audit })
|
|
339
|
+
|
|
340
|
+
await auditedDb.selectFrom('todos').selectAll().execute()
|
|
341
|
+
await audit.close()
|
|
342
|
+
|
|
343
|
+
assert.equal(events.length, 1)
|
|
344
|
+
assert.equal(events[0]?.metadata?.queryKind, 'select')
|
|
345
|
+
assert.deepEqual(events[0]?.metadata?.tables, ['todos'])
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test('can skip select auditing when auditReads is false', async () => {
|
|
349
|
+
const events: AuditEvent[] = []
|
|
350
|
+
const db = createDb()
|
|
351
|
+
dbs.push(db)
|
|
352
|
+
await createTodosTable(db)
|
|
353
|
+
await db
|
|
354
|
+
.insertInto('todos')
|
|
355
|
+
.values({ title: 'skip read', done: 0 })
|
|
356
|
+
.execute()
|
|
357
|
+
|
|
358
|
+
const audit = createInvocationAudit(
|
|
359
|
+
{
|
|
360
|
+
audit: async (event) => {
|
|
361
|
+
events.push(event)
|
|
362
|
+
},
|
|
363
|
+
write: async (batch) => {
|
|
364
|
+
events.push(...batch)
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
wireType: 'rpc',
|
|
369
|
+
wireId: 'wire-5',
|
|
370
|
+
audit: { durability: 'best-effort' },
|
|
371
|
+
logger: { warn: () => {} },
|
|
372
|
+
} as any
|
|
373
|
+
)
|
|
374
|
+
const auditedDb = createAuditedKysely(db, {
|
|
375
|
+
audit,
|
|
376
|
+
auditReads: false,
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
await auditedDb.selectFrom('todos').selectAll().execute()
|
|
380
|
+
await audit.close()
|
|
381
|
+
|
|
382
|
+
assert.equal(events.length, 0)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test('noops when audit is not enabled on the wire', async () => {
|
|
386
|
+
const db = createDb()
|
|
387
|
+
dbs.push(db)
|
|
388
|
+
await createTodosTable(db)
|
|
389
|
+
|
|
390
|
+
const audit = createInvocationAudit(new NoopAuditService(), {
|
|
391
|
+
wireType: 'rpc',
|
|
392
|
+
wireId: 'wire-6',
|
|
393
|
+
logger: { warn: () => {} },
|
|
394
|
+
} as any)
|
|
395
|
+
const auditedDb = createAuditedKysely(db, { audit })
|
|
396
|
+
|
|
397
|
+
await auditedDb
|
|
398
|
+
.insertInto('todos')
|
|
399
|
+
.values({ title: 'noop', done: 0 })
|
|
400
|
+
.execute()
|
|
401
|
+
await audit.close()
|
|
402
|
+
|
|
403
|
+
assert.ok(true)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test('persists read and update queries after best-effort flush', async () => {
|
|
407
|
+
const db = createDb() as Kysely<AuditVerifierDB>
|
|
408
|
+
dbs.push(db as unknown as Kysely<TestDB>)
|
|
409
|
+
await createTodosTable(db)
|
|
410
|
+
await createAuditEventsTable(db)
|
|
411
|
+
await db.insertInto('todos').values({ title: 'seed', done: 0 }).execute()
|
|
412
|
+
|
|
413
|
+
const audit = createInvocationAudit(new ImmediateAuditService(db), {
|
|
414
|
+
wireType: 'rpc',
|
|
415
|
+
wireId: 'wire-best-effort',
|
|
416
|
+
traceId: 'trace-best-effort',
|
|
417
|
+
functionId: 'todos:best-effort',
|
|
418
|
+
session: { userId: 'user-best-effort' },
|
|
419
|
+
pikkuUserId: 'user-best-effort',
|
|
420
|
+
audit: { durability: 'best-effort' },
|
|
421
|
+
logger: { warn: () => {} },
|
|
422
|
+
} as any)
|
|
423
|
+
const auditedDb = createAuditedKysely(db, { audit })
|
|
424
|
+
|
|
425
|
+
const beforeCount = await countPersistedEvents(db)
|
|
426
|
+
await auditedDb
|
|
427
|
+
.selectFrom('todos')
|
|
428
|
+
.selectAll()
|
|
429
|
+
.where('id', '=', 1)
|
|
430
|
+
.executeTakeFirstOrThrow()
|
|
431
|
+
await auditedDb
|
|
432
|
+
.updateTable('todos')
|
|
433
|
+
.set({ done: 1 })
|
|
434
|
+
.where('id', '=', 1)
|
|
435
|
+
.execute()
|
|
436
|
+
const insideCount = await countPersistedEvents(db)
|
|
437
|
+
|
|
438
|
+
assert.equal(beforeCount, 0)
|
|
439
|
+
assert.equal(insideCount, 0)
|
|
440
|
+
|
|
441
|
+
await audit.close()
|
|
442
|
+
|
|
443
|
+
const persisted = await listPersistedEvents(db)
|
|
444
|
+
assert.equal(persisted.length, 2)
|
|
445
|
+
assert.deepEqual(
|
|
446
|
+
persisted.map((event) => event.metadata?.queryKind),
|
|
447
|
+
['select', 'update']
|
|
448
|
+
)
|
|
449
|
+
assert.deepEqual(
|
|
450
|
+
persisted.map((event) => event.traceId),
|
|
451
|
+
['trace-best-effort', 'trace-best-effort']
|
|
452
|
+
)
|
|
453
|
+
assert.deepEqual(
|
|
454
|
+
persisted.map((event) => event.functionId),
|
|
455
|
+
['todos:best-effort', 'todos:best-effort']
|
|
456
|
+
)
|
|
457
|
+
assert.deepEqual(
|
|
458
|
+
persisted.map((event) => event.actorUserId),
|
|
459
|
+
['user-best-effort', 'user-best-effort']
|
|
460
|
+
)
|
|
461
|
+
assert.equal(persisted[0]?.queryId, 'q-1')
|
|
462
|
+
assert.equal(persisted[1]?.queryId, 'q-2')
|
|
463
|
+
assert.deepEqual(persisted[1]?.metadata?.changedColumns, ['done'])
|
|
464
|
+
assert.deepEqual(persisted[1]?.metadata?.changes, { done: 1 })
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
test('writes read and update audit events immediately in transactional mode', async () => {
|
|
468
|
+
const db = createDb()
|
|
469
|
+
dbs.push(db)
|
|
470
|
+
await createTodosTable(db)
|
|
471
|
+
await db.insertInto('todos').values({ title: 'seed', done: 0 }).execute()
|
|
472
|
+
|
|
473
|
+
const events: AuditEvent[] = []
|
|
474
|
+
const audit = createInvocationAudit(
|
|
475
|
+
{
|
|
476
|
+
audit: async (event) => {
|
|
477
|
+
events.push(event)
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
wireType: 'rpc',
|
|
482
|
+
wireId: 'wire-transactional',
|
|
483
|
+
traceId: 'trace-transactional',
|
|
484
|
+
functionId: 'todos:transactional',
|
|
485
|
+
session: { userId: 'user-transactional' },
|
|
486
|
+
pikkuUserId: 'user-transactional',
|
|
487
|
+
audit: { durability: 'transactional' },
|
|
488
|
+
logger: { warn: () => {} },
|
|
489
|
+
} as any
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
const counts: number[] = []
|
|
493
|
+
|
|
494
|
+
await db.transaction().execute(async (trx) => {
|
|
495
|
+
const auditedTrx = createAuditedKysely(trx as Kysely<TestDB>, {
|
|
496
|
+
audit,
|
|
497
|
+
transactionId: 'tx-verifier-1',
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
await auditedTrx
|
|
501
|
+
.selectFrom('todos')
|
|
502
|
+
.selectAll()
|
|
503
|
+
.where('id', '=', 1)
|
|
504
|
+
.executeTakeFirstOrThrow()
|
|
505
|
+
counts.push(events.length)
|
|
506
|
+
|
|
507
|
+
await auditedTrx
|
|
508
|
+
.updateTable('todos')
|
|
509
|
+
.set({ done: 1 })
|
|
510
|
+
.where('id', '=', 1)
|
|
511
|
+
.execute()
|
|
512
|
+
counts.push(events.length)
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
assert.deepEqual(counts, [1, 2])
|
|
516
|
+
assert.equal(events.length, 2)
|
|
517
|
+
assert.deepEqual(
|
|
518
|
+
events.map((event) => event.transactionId),
|
|
519
|
+
['tx-verifier-1', 'tx-verifier-1']
|
|
520
|
+
)
|
|
521
|
+
assert.deepEqual(
|
|
522
|
+
events.map((event) => event.metadata?.queryKind),
|
|
523
|
+
['select', 'update']
|
|
524
|
+
)
|
|
525
|
+
assert.deepEqual(events[1]?.metadata?.changes, { done: 1 })
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
test('does not persist audit rows when the invocation is not audited', async () => {
|
|
529
|
+
const db = createDb() as Kysely<AuditVerifierDB>
|
|
530
|
+
dbs.push(db as unknown as Kysely<TestDB>)
|
|
531
|
+
await createTodosTable(db)
|
|
532
|
+
await createAuditEventsTable(db)
|
|
533
|
+
await db.insertInto('todos').values({ title: 'seed', done: 0 }).execute()
|
|
534
|
+
|
|
535
|
+
const audit = createInvocationAudit(new ImmediateAuditService(db), {
|
|
536
|
+
wireType: 'rpc',
|
|
537
|
+
wireId: 'wire-plain',
|
|
538
|
+
traceId: 'trace-plain',
|
|
539
|
+
functionId: 'todos:plain',
|
|
540
|
+
session: { userId: 'user-plain' },
|
|
541
|
+
pikkuUserId: 'user-plain',
|
|
542
|
+
logger: { warn: () => {} },
|
|
543
|
+
} as any)
|
|
544
|
+
const auditedDb = createAuditedKysely(db, { audit })
|
|
545
|
+
|
|
546
|
+
await auditedDb
|
|
547
|
+
.selectFrom('todos')
|
|
548
|
+
.selectAll()
|
|
549
|
+
.where('id', '=', 1)
|
|
550
|
+
.executeTakeFirstOrThrow()
|
|
551
|
+
await auditedDb
|
|
552
|
+
.updateTable('todos')
|
|
553
|
+
.set({ done: 1 })
|
|
554
|
+
.where('id', '=', 1)
|
|
555
|
+
.execute()
|
|
556
|
+
await audit.close()
|
|
557
|
+
|
|
558
|
+
assert.equal(await countPersistedEvents(db), 0)
|
|
559
|
+
})
|
|
560
|
+
})
|