@pikku/kysely 0.13.24 → 0.13.27

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 (78) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/dist/src/index.d.ts +4 -1
  3. package/dist/src/index.js +3 -1
  4. package/dist/src/kysely-analytics-service.d.ts +30 -0
  5. package/dist/src/kysely-analytics-service.js +80 -0
  6. package/dist/src/kysely-feature-flag-store.d.ts +37 -0
  7. package/dist/src/kysely-feature-flag-store.js +239 -0
  8. package/dist/src/kysely-tables.d.ts +45 -0
  9. package/dist/src/schema/analytics.schema.d.ts +17 -0
  10. package/dist/src/schema/analytics.schema.js +48 -0
  11. package/dist/src/schema/flag.schema.d.ts +16 -0
  12. package/dist/src/schema/flag.schema.js +62 -0
  13. package/dist/src/schema/index.d.ts +2 -0
  14. package/dist/src/schema/index.js +6 -0
  15. package/package.json +11 -6
  16. package/run-tests.sh +9 -1
  17. package/bin/pikku-kysely-pure.ts +0 -59
  18. package/dist/src/kysely-lock-vault.d.ts +0 -34
  19. package/dist/src/kysely-lock-vault.js +0 -77
  20. package/dist/src/schema/data-lock.schema.d.ts +0 -15
  21. package/dist/src/schema/data-lock.schema.js +0 -27
  22. package/dist/tsconfig.tsbuildinfo +0 -1
  23. package/src/classification-crypto.test.ts +0 -143
  24. package/src/classification-crypto.ts +0 -163
  25. package/src/classification-plugin.pglite.test.ts +0 -226
  26. package/src/classification-plugin.test.ts +0 -265
  27. package/src/classification-plugin.ts +0 -249
  28. package/src/coercion-plugin.test.ts +0 -160
  29. package/src/coercion-plugin.ts +0 -187
  30. package/src/create-audited-kysely.test.ts +0 -560
  31. package/src/create-audited-kysely.ts +0 -254
  32. package/src/index.ts +0 -81
  33. package/src/kysely-agent-run-scores.ts +0 -47
  34. package/src/kysely-agent-run-service.ts +0 -244
  35. package/src/kysely-agent-run-state-service.ts +0 -200
  36. package/src/kysely-agent-storage-service.ts +0 -632
  37. package/src/kysely-audit-service.test.ts +0 -352
  38. package/src/kysely-audit-service.ts +0 -237
  39. package/src/kysely-channel-store.ts +0 -107
  40. package/src/kysely-credential-service.ts +0 -286
  41. package/src/kysely-deployment-service.ts +0 -163
  42. package/src/kysely-eventhub-store.ts +0 -53
  43. package/src/kysely-json.ts +0 -5
  44. package/src/kysely-scope-service.test.ts +0 -418
  45. package/src/kysely-scope-service.ts +0 -425
  46. package/src/kysely-secret-service.ts +0 -242
  47. package/src/kysely-services.test.ts +0 -405
  48. package/src/kysely-session-store.ts +0 -57
  49. package/src/kysely-tables.test.ts +0 -110
  50. package/src/kysely-tables.ts +0 -434
  51. package/src/kysely-virtual-user-run-store.test.ts +0 -369
  52. package/src/kysely-virtual-user-run-store.ts +0 -235
  53. package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
  54. package/src/kysely-virtual-user-schedule-store.ts +0 -187
  55. package/src/kysely-webhook-service.test.ts +0 -153
  56. package/src/kysely-webhook-service.ts +0 -139
  57. package/src/kysely-workflow-mirror.test.ts +0 -259
  58. package/src/kysely-workflow-mirror.ts +0 -374
  59. package/src/kysely-workflow-run-service.ts +0 -305
  60. package/src/kysely-workflow-service.test.ts +0 -544
  61. package/src/kysely-workflow-service.ts +0 -820
  62. package/src/schema/agent.schema.ts +0 -162
  63. package/src/schema/audit.schema.ts +0 -57
  64. package/src/schema/channel.schema.ts +0 -38
  65. package/src/schema/credential.schema.ts +0 -58
  66. package/src/schema/deployment.schema.ts +0 -47
  67. package/src/schema/index.ts +0 -441
  68. package/src/schema/pikku-schema.types.ts +0 -169
  69. package/src/schema/schema.test.ts +0 -591
  70. package/src/schema/scope.schema.ts +0 -110
  71. package/src/schema/secret.schema.ts +0 -49
  72. package/src/schema/session.schema.ts +0 -20
  73. package/src/schema/virtual-user-schedule.schema.ts +0 -48
  74. package/src/schema/virtual-user.schema.ts +0 -106
  75. package/src/schema/webhook.schema.ts +0 -58
  76. package/src/schema/workflow.schema.ts +0 -150
  77. package/src/serialize-plugin.ts +0 -233
  78. package/tsconfig.json +0 -17
@@ -1,560 +0,0 @@
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 '@pikku/core/services'
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
- userId: string | null
37
- orgId: 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('userId', 'text')
80
- .addColumn('orgId', '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
- userId: event.userIdentity?.userId ?? null,
117
- orgId: event.userIdentity?.orgId ?? null,
118
- actorPikkuUserId: event.userIdentity?.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]?.userIdentity, {
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]?.userIdentity, {
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.userId),
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
- })