@pikku/kysely 0.13.23 → 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 (80) hide show
  1. package/CHANGELOG.md +248 -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/kysely-webhook-service.d.ts +1 -1
  10. package/dist/src/kysely-webhook-service.js +1 -1
  11. package/dist/src/schema/analytics.schema.d.ts +17 -0
  12. package/dist/src/schema/analytics.schema.js +48 -0
  13. package/dist/src/schema/flag.schema.d.ts +16 -0
  14. package/dist/src/schema/flag.schema.js +62 -0
  15. package/dist/src/schema/index.d.ts +2 -0
  16. package/dist/src/schema/index.js +6 -0
  17. package/package.json +11 -6
  18. package/run-tests.sh +9 -1
  19. package/bin/pikku-kysely-pure.ts +0 -59
  20. package/dist/src/kysely-lock-vault.d.ts +0 -34
  21. package/dist/src/kysely-lock-vault.js +0 -77
  22. package/dist/src/schema/data-lock.schema.d.ts +0 -15
  23. package/dist/src/schema/data-lock.schema.js +0 -27
  24. package/dist/tsconfig.tsbuildinfo +0 -1
  25. package/src/classification-crypto.test.ts +0 -143
  26. package/src/classification-crypto.ts +0 -163
  27. package/src/classification-plugin.pglite.test.ts +0 -226
  28. package/src/classification-plugin.test.ts +0 -265
  29. package/src/classification-plugin.ts +0 -249
  30. package/src/coercion-plugin.test.ts +0 -160
  31. package/src/coercion-plugin.ts +0 -187
  32. package/src/create-audited-kysely.test.ts +0 -560
  33. package/src/create-audited-kysely.ts +0 -254
  34. package/src/index.ts +0 -81
  35. package/src/kysely-agent-run-scores.ts +0 -47
  36. package/src/kysely-agent-run-service.ts +0 -244
  37. package/src/kysely-agent-run-state-service.ts +0 -200
  38. package/src/kysely-agent-storage-service.ts +0 -632
  39. package/src/kysely-audit-service.test.ts +0 -352
  40. package/src/kysely-audit-service.ts +0 -237
  41. package/src/kysely-channel-store.ts +0 -107
  42. package/src/kysely-credential-service.ts +0 -286
  43. package/src/kysely-deployment-service.ts +0 -163
  44. package/src/kysely-eventhub-store.ts +0 -53
  45. package/src/kysely-json.ts +0 -5
  46. package/src/kysely-scope-service.test.ts +0 -418
  47. package/src/kysely-scope-service.ts +0 -425
  48. package/src/kysely-secret-service.ts +0 -242
  49. package/src/kysely-services.test.ts +0 -405
  50. package/src/kysely-session-store.ts +0 -57
  51. package/src/kysely-tables.test.ts +0 -110
  52. package/src/kysely-tables.ts +0 -434
  53. package/src/kysely-virtual-user-run-store.test.ts +0 -369
  54. package/src/kysely-virtual-user-run-store.ts +0 -235
  55. package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
  56. package/src/kysely-virtual-user-schedule-store.ts +0 -187
  57. package/src/kysely-webhook-service.test.ts +0 -124
  58. package/src/kysely-webhook-service.ts +0 -137
  59. package/src/kysely-workflow-mirror.test.ts +0 -259
  60. package/src/kysely-workflow-mirror.ts +0 -374
  61. package/src/kysely-workflow-run-service.ts +0 -305
  62. package/src/kysely-workflow-service.test.ts +0 -544
  63. package/src/kysely-workflow-service.ts +0 -820
  64. package/src/schema/agent.schema.ts +0 -162
  65. package/src/schema/audit.schema.ts +0 -57
  66. package/src/schema/channel.schema.ts +0 -38
  67. package/src/schema/credential.schema.ts +0 -58
  68. package/src/schema/deployment.schema.ts +0 -47
  69. package/src/schema/index.ts +0 -441
  70. package/src/schema/pikku-schema.types.ts +0 -169
  71. package/src/schema/schema.test.ts +0 -591
  72. package/src/schema/scope.schema.ts +0 -110
  73. package/src/schema/secret.schema.ts +0 -49
  74. package/src/schema/session.schema.ts +0 -20
  75. package/src/schema/virtual-user-schedule.schema.ts +0 -48
  76. package/src/schema/virtual-user.schema.ts +0 -106
  77. package/src/schema/webhook.schema.ts +0 -58
  78. package/src/schema/workflow.schema.ts +0 -150
  79. package/src/serialize-plugin.ts +0 -233
  80. package/tsconfig.json +0 -17
@@ -1,591 +0,0 @@
1
- import { test, describe } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import {
4
- Kysely,
5
- PostgresAdapter,
6
- PostgresIntrospector,
7
- PostgresQueryCompiler,
8
- SqliteAdapter,
9
- SqliteIntrospector,
10
- SqliteQueryCompiler,
11
- SqliteDialect,
12
- DummyDriver,
13
- } from 'kysely'
14
- import Database from 'better-sqlite3'
15
- import {
16
- pikkuSchemas,
17
- requiredPikkuSchemas,
18
- compilePikkuSchemas,
19
- applyPikkuSchemas,
20
- requirePikkuSchema,
21
- declaredTables,
22
- unsatisfiedRequirements,
23
- type PikkuSchema,
24
- } from './index.js'
25
- import { workflowSchema } from './workflow.schema.js'
26
-
27
- const dialect = (kind: 'postgres' | 'sqlite') =>
28
- kind === 'postgres'
29
- ? {
30
- createAdapter: () => new PostgresAdapter(),
31
- createDriver: () => new DummyDriver(),
32
- createIntrospector: (db: Kysely<any>) => new PostgresIntrospector(db),
33
- createQueryCompiler: () => new PostgresQueryCompiler(),
34
- }
35
- : {
36
- createAdapter: () => new SqliteAdapter(),
37
- createDriver: () => new DummyDriver(),
38
- createIntrospector: (db: Kysely<any>) => new SqliteIntrospector(db),
39
- createQueryCompiler: () => new SqliteQueryCompiler(),
40
- }
41
-
42
- /** A driver whose every query fails with `error` — for testing the failure path. */
43
- class ThrowingDriver extends DummyDriver {
44
- constructor(private readonly error: Error) {
45
- super()
46
- }
47
- override async acquireConnection() {
48
- return {
49
- executeQuery: async (): Promise<never> => {
50
- throw this.error
51
- },
52
- streamQuery: async function* () {},
53
- }
54
- }
55
- }
56
-
57
- const compileFor = (kind: 'postgres' | 'sqlite') =>
58
- compilePikkuSchemas(new Kysely<any>({ dialect: dialect(kind) }))
59
-
60
- describe('pikku runtime schema', () => {
61
- test('declares logical names and compiles them to physical ones', () => {
62
- const sql = compileFor('postgres')
63
-
64
- // The declaration says `agentThreads.resourceId`, matching how every query in
65
- // the package addresses it; the plugin is what makes the table `agent_threads`.
66
- assert.match(sql, /create table "agent_threads"/)
67
- assert.match(sql, /"resource_id" varchar\(255\) not null/)
68
- assert.doesNotMatch(
69
- sql,
70
- /"agentThreads"/,
71
- 'a camelCase table name means the plugin was not applied'
72
- )
73
- })
74
-
75
- test('rewrites foreign key targets too', () => {
76
- assert.match(
77
- compileFor('postgres'),
78
- /references "agent_threads" \("id"\)/,
79
- 'an unrewritten reference target points at a table that does not exist'
80
- )
81
- })
82
-
83
- test('compiles for every dialect from the one declaration', () => {
84
- for (const kind of ['postgres', 'sqlite'] as const) {
85
- const sql = compileFor(kind)
86
- assert.match(sql, /create table "workflow_runs"/, `${kind} workflow_runs`)
87
- assert.match(sql, /create table "pikku_scopes"/, `${kind} pikku_scopes`)
88
- }
89
- })
90
-
91
- test('carries the agent_run column the two old declarations disagreed on', () => {
92
- assert.match(compileFor('postgres'), /"pending_approvals" text/)
93
- })
94
-
95
- test('creates tables outright rather than if-not-exists', () => {
96
- // A migration runs once. `if not exists` would let a table that already
97
- // exists in a different shape pass silently, which is the drift the
98
- // migration is supposed to surface.
99
- assert.doesNotMatch(compileFor('postgres'), /create table if not exists/)
100
- })
101
-
102
- test('drops the frozen uuid default from workflow primary keys', () => {
103
- // It was `sql.raw("'" + crypto.randomUUID() + "'")`, evaluated once when the
104
- // statement was built, so every row shared one default.
105
- assert.match(
106
- compileFor('postgres'),
107
- /"workflow_run_id" text primary key,/,
108
- 'the primary key should carry no default at all'
109
- )
110
- assert.match(compileFor('postgres'), /"workflow_step_id" text primary key,/)
111
- assert.match(compileFor('postgres'), /"history_id" text primary key,/)
112
- })
113
-
114
- test('every schema is named, for the migration it will be written into', () => {
115
- const names = pikkuSchemas.map((s) => s.name)
116
- assert.equal(new Set(names).size, names.length, 'names must be unique')
117
- assert.ok(names.every((n) => /^[a-z][a-z-]*$/.test(n)))
118
- })
119
- })
120
-
121
- describe('pikku runtime schema — prerequisites', () => {
122
- const memoryDb = () =>
123
- new Kysely<any>({
124
- dialect: new SqliteDialect({ database: new Database(':memory:') }),
125
- })
126
-
127
- test('names the missing table and who owns it, before creating anything', async () => {
128
- const db = memoryDb()
129
- try {
130
- await assert.rejects(
131
- applyPikkuSchemas(db),
132
- /The 'scope' schema requires 'user\.id', which nothing has created\. Better Auth owns it/
133
- )
134
-
135
- const created = await db.introspection.getTables()
136
- assert.deepEqual(
137
- created.map((t) => t.name),
138
- [],
139
- 'a failed prerequisite must not leave a half-applied database'
140
- )
141
- } finally {
142
- await db.destroy()
143
- }
144
- })
145
-
146
- test('applies in full once the prerequisite is there', async () => {
147
- const db = memoryDb()
148
- try {
149
- await db.schema
150
- .createTable('user')
151
- .addColumn('id', 'text', (col) => col.primaryKey())
152
- .execute()
153
-
154
- assert.deepEqual(await unsatisfiedRequirements(db), [])
155
- await applyPikkuSchemas(db)
156
-
157
- const created = new Set(
158
- (await db.introspection.getTables()).map((t) => t.name)
159
- )
160
- assert.ok(created.has('pikku_user_role'))
161
- assert.ok(created.has('workflow_runs'))
162
- } finally {
163
- await db.destroy()
164
- }
165
- })
166
-
167
- test('takes the grant column type from the user id it references', () => {
168
- // The hand-written DDL said `text`. Better Auth generates a `uuid` primary
169
- // key on postgres, and postgres rejects a text column referencing it —
170
- // which is why projects ended up writing these tables by hand.
171
- const sql = compilePikkuSchemas(
172
- new Kysely<any>({ dialect: dialect('postgres') }),
173
- pikkuSchemas,
174
- { 'user.id': 'uuid' }
175
- )
176
- assert.match(
177
- sql,
178
- /create table "pikku_user_role" \("user_id" uuid not null references "user" \("id"\)/
179
- )
180
- assert.match(
181
- sql,
182
- /create table "pikku_user_scope" \("user_id" uuid not null references "user" \("id"\)/
183
- )
184
- })
185
-
186
- test('reports the gap as data, so a caller can describe it instead of dying', async () => {
187
- const db = memoryDb()
188
- try {
189
- const unmet = await unsatisfiedRequirements(db)
190
- assert.deepEqual(
191
- unmet.map(({ schema, requirement }) => ({
192
- schema: schema.name,
193
- ...requirement,
194
- })),
195
- [{ schema: 'scope', table: 'user', column: 'id', owner: 'Better Auth' }]
196
- )
197
-
198
- // The rest of the declaration does not depend on auth, so it still applies.
199
- const satisfiable = pikkuSchemas.filter((s) => s.name !== 'scope')
200
- await applyPikkuSchemas(db, satisfiable)
201
- const created = new Set(
202
- (await db.introspection.getTables()).map((t) => t.name)
203
- )
204
- assert.ok(created.has('workflow_runs'))
205
- assert.equal(created.has('pikku_user_role'), false)
206
- } finally {
207
- await db.destroy()
208
- }
209
- })
210
-
211
- test('a statement failing part way takes the ones before it with it', async () => {
212
- const db = memoryDb()
213
- try {
214
- await db.schema.createTable('taken').addColumn('id', 'text').execute()
215
-
216
- // The prerequisite check cannot see this coming: nothing is missing, the
217
- // second statement simply collides with a table the database already has.
218
- const collides: PikkuSchema = {
219
- name: 'collides',
220
- statements: [
221
- (db) => db.schema.createTable('first').addColumn('id', 'text'),
222
- (db) => db.schema.createTable('taken').addColumn('id', 'text'),
223
- ],
224
- }
225
-
226
- await assert.rejects(applyPikkuSchemas(db, [collides]))
227
-
228
- const created = new Set(
229
- (await db.introspection.getTables()).map((t) => t.name)
230
- )
231
- assert.equal(
232
- created.has('first'),
233
- false,
234
- 'the statement that succeeded must roll back with the one that did not'
235
- )
236
- } finally {
237
- await db.destroy()
238
- }
239
- })
240
- })
241
-
242
- describe('requirePikkuSchema — what a service does at boot', () => {
243
- const memoryDb = () =>
244
- new Kysely<any>({
245
- dialect: new SqliteDialect({ database: new Database(':memory:') }),
246
- })
247
-
248
- test('refuses a database the migration never reached, naming the command', async () => {
249
- const db = memoryDb()
250
- try {
251
- await assert.rejects(
252
- requirePikkuSchema(db, workflowSchema),
253
- /The 'workflow' schema is not in this database.*pikku db generate/s
254
- )
255
- } finally {
256
- await db.destroy()
257
- }
258
- })
259
-
260
- test('creates nothing on the way to that refusal', async () => {
261
- // The whole point: a service that finds its tables missing stops, and the
262
- // database it stopped against is exactly as it was.
263
- const db = memoryDb()
264
- try {
265
- await assert.rejects(requirePikkuSchema(db, workflowSchema))
266
- const tables = await db.introspection.getTables()
267
- assert.deepEqual(tables, [])
268
- } finally {
269
- await db.destroy()
270
- }
271
- })
272
-
273
- test('two services declaring the same schema do not fight over it', async () => {
274
- // The workflow service and the workflow mirror both own these tables, and
275
- // both call init(). Neither creates anything, so both find the migrated
276
- // tables and return.
277
- const db = memoryDb()
278
- try {
279
- await applyPikkuSchemas(db, [workflowSchema])
280
- await assert.doesNotReject(requirePikkuSchema(db, workflowSchema))
281
- await assert.doesNotReject(requirePikkuSchema(db, workflowSchema))
282
- } finally {
283
- await db.destroy()
284
- }
285
- })
286
-
287
- test('a half-applied schema is refused the same way a missing one is', async () => {
288
- // Something else already owns part of it — a migration, an older release,
289
- // a hand-run script. Half-present is not a distinct case: the remedy is the
290
- // same migration either way, so the sentence names both halves and stops.
291
- const db = memoryDb()
292
- try {
293
- await db.schema
294
- .createTable('workflow_runs')
295
- .addColumn('workflow_run_id', 'text', (col) => col.primaryKey())
296
- .execute()
297
-
298
- await assert.rejects(
299
- requirePikkuSchema(db, workflowSchema),
300
- /workflow_step.*missing.*workflow_runs already there/s
301
- )
302
- } finally {
303
- await db.destroy()
304
- }
305
- })
306
- })
307
-
308
- describe('requirePikkuSchema — a connection bound to a schema', () => {
309
- const WORKFLOW_TABLES = [
310
- 'workflow_runs',
311
- 'workflow_step',
312
- 'workflow_step_history',
313
- 'workflow_versions',
314
- ]
315
-
316
- /**
317
- * A postgres connection whose introspection answers with `tables`.
318
- *
319
- * Stubbed rather than run against a real database because sqlite has one
320
- * schema and nothing else here speaks postgres — and introspection is the
321
- * only thing `requirePikkuSchema` reads before it decides. DummyDriver takes
322
- * whatever DDL follows.
323
- */
324
- const introspecting = (
325
- tables: Array<{ schema: string | undefined; name: string }>
326
- ) =>
327
- new Kysely<any>({
328
- dialect: {
329
- ...dialect('postgres'),
330
- createIntrospector: () => ({
331
- getSchemas: async () => [],
332
- getMetadata: async () => ({ tables: [] }),
333
- getTables: async () =>
334
- tables.map(({ schema, name }) => ({
335
- schema,
336
- name,
337
- isView: false,
338
- columns: [],
339
- })),
340
- }),
341
- },
342
- })
343
-
344
- test('reads the schema out of its own DDL, not the table name', () => {
345
- const db = new Kysely<any>({ dialect: dialect('postgres') })
346
-
347
- assert.deepEqual(declaredTables(workflowSchema, db.withSchema('app')), [
348
- { schema: 'app', name: 'workflow_runs' },
349
- { schema: 'app', name: 'workflow_step' },
350
- { schema: 'app', name: 'workflow_step_history' },
351
- { schema: 'app', name: 'workflow_versions' },
352
- ])
353
-
354
- // Unqualified DDL resolves against the connection's search_path, which is
355
- // not knowable from here — so there is no schema to report.
356
- assert.deepEqual(
357
- declaredTables(workflowSchema, db),
358
- WORKFLOW_TABLES.map((name) => ({ schema: undefined, name }))
359
- )
360
- })
361
-
362
- test('finds tables that already exist under that schema', async () => {
363
- // The regression this exists for: a `withSchema('app')` connection reading
364
- // its own `create table "app"."workflow_runs"` as a table called `app`,
365
- // concluding every table was missing, and issuing a bare CREATE that the
366
- // database rejects with `relation "workflow_runs" already exists` — on
367
- // every boot, forever.
368
- const db = introspecting(
369
- WORKFLOW_TABLES.map((name) => ({ schema: 'app', name }))
370
- )
371
-
372
- await assert.doesNotReject(
373
- requirePikkuSchema(db.withSchema('app'), workflowSchema)
374
- )
375
- })
376
-
377
- test('does not accept the same names in a different schema', async () => {
378
- // `withSchema('app')` reads and writes `app.workflow_runs`. A
379
- // `public.workflow_runs` is a different table and answers nothing.
380
- const db = introspecting(
381
- WORKFLOW_TABLES.map((name) => ({ schema: 'public', name }))
382
- )
383
-
384
- await assert.rejects(
385
- requirePikkuSchema(db.withSchema('app'), workflowSchema),
386
- /is not in this database/
387
- )
388
- })
389
-
390
- test('still matches on the bare name when the connection is unbound', async () => {
391
- const db = introspecting(
392
- WORKFLOW_TABLES.map((name) => ({ schema: 'public', name }))
393
- )
394
-
395
- await assert.doesNotReject(requirePikkuSchema(db, workflowSchema))
396
- })
397
-
398
- test('matches on the bare name when the engine reports no schemas', async () => {
399
- // sqlite and mysql leave `TableMetadata.schema` unset. Holding out for the
400
- // qualified pair there would read every existing table as missing — the
401
- // same false negative this suite exists for, arrived at from the other side.
402
- const db = introspecting(
403
- WORKFLOW_TABLES.map((name) => ({ schema: undefined, name }))
404
- )
405
-
406
- await assert.doesNotReject(
407
- requirePikkuSchema(db.withSchema('main'), workflowSchema)
408
- )
409
- })
410
-
411
- test('leaves a syntax error that is not sqlite refusing a qualifier alone', async () => {
412
- // Postgres compiles the same qualified reference legitimately, and can
413
- // raise a syntax error of its own on this DDL — a column type resolved from
414
- // `requires` goes in verbatim. Answering for it in sqlite's name would be
415
- // the misleading-error bug, reintroduced.
416
- const pgError = Object.assign(new Error('syntax error at or near "not"'), {
417
- code: '42601',
418
- })
419
- const db = new Kysely<any>({
420
- dialect: {
421
- ...dialect('postgres'),
422
- createDriver: () => new ThrowingDriver(pgError),
423
- createIntrospector: () => ({
424
- getSchemas: async () => [],
425
- getMetadata: async () => ({ tables: [] }),
426
- getTables: async () => [],
427
- }),
428
- },
429
- })
430
-
431
- await assert.rejects(
432
- applyPikkuSchemas(db.withSchema('app'), [workflowSchema]),
433
- (error: Error) => {
434
- assert.equal(error, pgError, 'the engine error must pass through as-is')
435
- return true
436
- }
437
- )
438
- })
439
-
440
- test('explains the qualified foreign key sqlite cannot express', async () => {
441
- // `withSchema` qualifies foreign key targets too. Postgres needs that;
442
- // sqlite refuses it, and says only `near ".": syntax error` — which names
443
- // neither the schema nor the connection that produced it.
444
- const db = new Kysely<any>({
445
- dialect: new SqliteDialect({ database: new Database(':memory:') }),
446
- })
447
- try {
448
- await assert.rejects(
449
- applyPikkuSchemas(db.withSchema('main'), [workflowSchema]),
450
- (error: Error) => {
451
- assert.match(error.message, /bound to a schema/)
452
- assert.match(error.message, /takes a bare table name/i)
453
- assert.ok(error.cause, 'the engine error must survive as the cause')
454
- return true
455
- }
456
- )
457
- } finally {
458
- await db.destroy()
459
- }
460
- })
461
-
462
- test('names the schema when it reports a half-applied one', async () => {
463
- const db = introspecting([{ schema: 'app', name: 'workflow_runs' }])
464
-
465
- await assert.rejects(
466
- requirePikkuSchema(db.withSchema('app'), workflowSchema),
467
- /app\.workflow_step.*missing.*app\.workflow_runs already there/s
468
- )
469
- })
470
- })
471
-
472
- describe('compiling into a schema', () => {
473
- const compilePg = (schemaName?: string) =>
474
- compilePikkuSchemas(
475
- new Kysely<any>({ dialect: dialect('postgres') }),
476
- pikkuSchemas,
477
- {},
478
- schemaName
479
- )
480
-
481
- test('qualifies every table it creates', () => {
482
- const sql = compilePg('app')
483
- const creates = sql.match(/create table[^(]*/gi) ?? []
484
-
485
- assert.ok(creates.length > 0, 'nothing was compiled')
486
- for (const create of creates) {
487
- assert.match(
488
- create,
489
- /"app"\./,
490
- `unqualified create table in a schema-bound compile: ${create.trim()}`
491
- )
492
- }
493
- })
494
-
495
- test('qualifies foreign key targets, so they do not resolve by search path', () => {
496
- const sql = compilePg('app')
497
-
498
- assert.match(sql, /references "app"\./)
499
- assert.doesNotMatch(
500
- sql,
501
- /references "(?!app")[^"]+" \(/,
502
- 'a foreign key onto an unqualified table would find whatever search_path points at'
503
- )
504
- })
505
-
506
- /**
507
- * The regression this option exists for.
508
- *
509
- * Raw SQL is passed through verbatim, so the expression index on `credentials`
510
- * is the one statement `withSchema` cannot reach. Left unqualified it creates
511
- * the uniqueness rule on a different table than the `create table` above it.
512
- */
513
- test('qualifies the raw expression index too', () => {
514
- assert.match(
515
- compilePg('app'),
516
- /CREATE UNIQUE INDEX credentials_name_user_id_unique ON "app"\."credentials"/
517
- )
518
- })
519
-
520
- test('leaves the index name itself unqualified', () => {
521
- // An index belongs to the schema of the table it is on; postgres rejects a
522
- // CREATE INDEX that says otherwise.
523
- assert.doesNotMatch(compilePg('app'), /INDEX "?app"?\."?credentials_name/i)
524
- })
525
-
526
- test('compiles unqualified when no schema is asked for', () => {
527
- const sql = compilePg()
528
-
529
- assert.doesNotMatch(sql, /"app"\./)
530
- assert.match(sql, /create table "credentials"/i)
531
- assert.match(
532
- sql,
533
- /CREATE UNIQUE INDEX credentials_name_user_id_unique ON "credentials"/
534
- )
535
- })
536
-
537
- test('does not qualify the connection it was handed', () => {
538
- // The caller's connection is a scratch database the declaration was just
539
- // applied to. Binding it would create tables in a schema that database has
540
- // never heard of — so the schema must reach the SQL and nothing else.
541
- const db = new Kysely<any>({ dialect: dialect('postgres') })
542
- compilePikkuSchemas(db, pikkuSchemas, {}, 'app')
543
-
544
- assert.match(
545
- db.schema.createTable('after').addColumn('id', 'text').compile().sql,
546
- /create table "after"/
547
- )
548
- })
549
- })
550
-
551
- describe('requiredPikkuSchemas', () => {
552
- test('a project that reaches no service still gets the ungated schemas', () => {
553
- const names = requiredPikkuSchemas(new Set()).map((s) => s.name)
554
- assert.deepEqual(names, ['session', 'secret', 'deployment'])
555
- })
556
-
557
- test('a service brings in its own schema, in the declared order', () => {
558
- const names = requiredPikkuSchemas(
559
- new Set(['channelStore', 'workflowService'])
560
- ).map((s) => s.name)
561
- assert.deepEqual(names, [
562
- 'channel',
563
- 'session',
564
- 'secret',
565
- 'deployment',
566
- 'workflow',
567
- ])
568
- })
569
-
570
- test('any one of a schema s owners is enough to ask for it', () => {
571
- // A schema several services share is needed as soon as one of them is.
572
- assert.ok(
573
- requiredPikkuSchemas(new Set(['workflowRunService'])).some(
574
- (s) => s.name === 'workflow'
575
- )
576
- )
577
- assert.ok(
578
- requiredPikkuSchemas(new Set(['eventHub'])).some(
579
- (s) => s.name === 'channel'
580
- )
581
- )
582
- })
583
-
584
- test('naming every owner asks for everything', () => {
585
- const owners = pikkuSchemas.flatMap((s) => s.ownedBy ?? [])
586
- assert.equal(
587
- requiredPikkuSchemas(new Set(owners)).length,
588
- pikkuSchemas.length
589
- )
590
- })
591
- })
@@ -1,110 +0,0 @@
1
- import { sql } from 'kysely'
2
- import { requiredType, type PikkuSchema } from './pikku-schema.types.js'
3
-
4
- /**
5
- * Scopes, roles and the grants that bind them to users.
6
- *
7
- * `pikkuUserRole` and `pikkuUserScope` reference `user.id`, which Better Auth
8
- * owns. Auth is a prerequisite rather than an optional companion: a grant is
9
- * made to a user, so without the table the user lives in there is nothing to
10
- * grant to, and the cascade that revokes grants when a user is deleted has
11
- * nothing to hang off.
12
- *
13
- * `userId` takes its type from `user.id` rather than declaring one, because
14
- * that type is Better Auth's to decide: `text` by default, `uuid` under
15
- * `generateId: 'uuid'`, an identity `integer` under `'serial'`. The old
16
- * hand-written DDL said `text`, and postgres rejects a `text` column
17
- * referencing either of the others — so the whole statement failed and projects
18
- * wrote these tables by hand instead.
19
- */
20
- export const scopeSchema: PikkuSchema = {
21
- name: 'scope',
22
- ownedBy: ['scopeService'],
23
- requires: [{ table: 'user', column: 'id', owner: 'Better Auth' }],
24
- statements: [
25
- (db) =>
26
- db.schema
27
- .createTable('pikkuScopes')
28
- .addColumn('name', 'text', (col) => col.primaryKey())
29
- .addColumn('description', 'text')
30
- .addColumn('declared', 'boolean', (col) =>
31
- col.defaultTo(true).notNull()
32
- ),
33
-
34
- (db) =>
35
- db.schema
36
- .createTable('pikkuRoles')
37
- .addColumn('name', 'text', (col) => col.primaryKey())
38
- .addColumn('description', 'text')
39
- // Declared in code with `defineSystemRole` rather than composed by an
40
- // admin. What it buys is the refusals: a system role cannot be renamed,
41
- // re-scoped or deleted from the console, and a console role cannot be
42
- // created with its name — two rows answering to one name would make
43
- // "does Susan hold `buyer`?" depend on which one the store returned.
44
- .addColumn('system', 'boolean', (col) => col.defaultTo(false).notNull())
45
- // A system role whose declaration has gone: still held by whoever holds
46
- // it, no longer offered for new grants, awaiting `pikku roles prune`.
47
- // The same additive contract as `pikkuScopes.declared`, and for the same
48
- // reason — a mid-deploy revocation is not something a code edit should
49
- // be able to cause.
50
- .addColumn('declared', 'boolean', (col) =>
51
- col.defaultTo(true).notNull()
52
- )
53
- .addColumn('createdAt', 'timestamp', (col) =>
54
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
55
- ),
56
-
57
- (db) =>
58
- db.schema
59
- .createTable('pikkuRoleScopes')
60
- .addColumn('role', 'text', (col) =>
61
- col.notNull().references('pikkuRoles.name').onDelete('cascade')
62
- )
63
- .addColumn('scope', 'text', (col) =>
64
- col.notNull().references('pikkuScopes.name').onDelete('cascade')
65
- )
66
- .addPrimaryKeyConstraint('pikku_role_scopes_pk', ['role', 'scope']),
67
-
68
- (db, types) =>
69
- db.schema
70
- .createTable('pikkuUserRole')
71
- .addColumn('userId', requiredType(types, 'user', 'id'), (col) =>
72
- col.notNull().references('user.id').onDelete('cascade')
73
- )
74
- .addColumn('role', 'text', (col) =>
75
- col.notNull().references('pikkuRoles.name').onDelete('cascade')
76
- )
77
- .addColumn('grantedBy', 'text')
78
- .addColumn('grantedAt', 'timestamp', (col) =>
79
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
80
- )
81
- .addPrimaryKeyConstraint('pikku_user_role_pk', ['userId', 'role']),
82
-
83
- (db, types) =>
84
- db.schema
85
- .createTable('pikkuUserScope')
86
- .addColumn('userId', requiredType(types, 'user', 'id'), (col) =>
87
- col.notNull().references('user.id').onDelete('cascade')
88
- )
89
- .addColumn('scope', 'text', (col) =>
90
- col.notNull().references('pikkuScopes.name').onDelete('cascade')
91
- )
92
- .addColumn('grantedBy', 'text')
93
- .addColumn('grantedAt', 'timestamp', (col) =>
94
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
95
- )
96
- .addPrimaryKeyConstraint('pikku_user_scope_pk', ['userId', 'scope']),
97
-
98
- (db) =>
99
- db.schema
100
- .createIndex('pikku_role_scopes_scope_idx')
101
- .on('pikkuRoleScopes')
102
- .column('scope'),
103
-
104
- (db) =>
105
- db.schema
106
- .createIndex('pikku_user_role_role_idx')
107
- .on('pikkuUserRole')
108
- .column('role'),
109
- ],
110
- }