@pikku/kysely 0.13.18 → 0.13.20

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 (66) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/src/index.d.ts +1 -1
  3. package/dist/src/index.js +1 -1
  4. package/dist/src/kysely-agent-run-state-service.js +2 -2
  5. package/dist/src/kysely-agent-storage-service.js +2 -2
  6. package/dist/src/kysely-audit-service.js +2 -2
  7. package/dist/src/kysely-channel-store.js +2 -2
  8. package/dist/src/kysely-credential-service.js +2 -2
  9. package/dist/src/kysely-deployment-service.js +2 -2
  10. package/dist/src/kysely-scope-service.js +2 -2
  11. package/dist/src/kysely-secret-service.js +2 -2
  12. package/dist/src/kysely-session-store.js +2 -2
  13. package/dist/src/kysely-tables.d.ts +28 -0
  14. package/dist/src/kysely-virtual-user-run-store.js +2 -2
  15. package/dist/src/kysely-webhook-service.js +2 -2
  16. package/dist/src/kysely-workflow-mirror.d.ts +1 -1
  17. package/dist/src/kysely-workflow-mirror.js +3 -3
  18. package/dist/src/kysely-workflow-service.d.ts +39 -8
  19. package/dist/src/kysely-workflow-service.js +77 -11
  20. package/dist/src/schema/agent.schema.js +1 -0
  21. package/dist/src/schema/audit.schema.d.ts +5 -4
  22. package/dist/src/schema/audit.schema.js +6 -4
  23. package/dist/src/schema/channel.schema.js +1 -0
  24. package/dist/src/schema/credential.schema.js +1 -0
  25. package/dist/src/schema/index.d.ts +47 -26
  26. package/dist/src/schema/index.js +56 -46
  27. package/dist/src/schema/pikku-schema.types.d.ts +20 -0
  28. package/dist/src/schema/scope.schema.js +1 -0
  29. package/dist/src/schema/virtual-user.schema.js +2 -1
  30. package/dist/src/schema/webhook.schema.js +1 -0
  31. package/dist/src/schema/workflow.schema.js +1 -0
  32. package/package.json +2 -2
  33. package/src/index.ts +12 -2
  34. package/src/kysely-agent-run-state-service.ts +2 -2
  35. package/src/kysely-agent-storage-service.ts +2 -2
  36. package/src/kysely-audit-service.test.ts +2 -10
  37. package/src/kysely-audit-service.ts +2 -2
  38. package/src/kysely-channel-store.ts +2 -2
  39. package/src/kysely-credential-service.ts +2 -2
  40. package/src/kysely-deployment-service.ts +2 -2
  41. package/src/kysely-scope-service.test.ts +2 -0
  42. package/src/kysely-scope-service.ts +2 -2
  43. package/src/kysely-secret-service.ts +2 -2
  44. package/src/kysely-services.test.ts +26 -0
  45. package/src/kysely-session-store.ts +2 -2
  46. package/src/kysely-tables.test.ts +1 -12
  47. package/src/kysely-tables.ts +29 -0
  48. package/src/kysely-virtual-user-run-store.test.ts +22 -6
  49. package/src/kysely-virtual-user-run-store.ts +2 -2
  50. package/src/kysely-webhook-service.test.ts +4 -0
  51. package/src/kysely-webhook-service.ts +2 -2
  52. package/src/kysely-workflow-mirror.test.ts +2 -0
  53. package/src/kysely-workflow-mirror.ts +3 -3
  54. package/src/kysely-workflow-service.test.ts +114 -0
  55. package/src/kysely-workflow-service.ts +99 -11
  56. package/src/schema/agent.schema.ts +1 -0
  57. package/src/schema/audit.schema.ts +6 -4
  58. package/src/schema/channel.schema.ts +1 -0
  59. package/src/schema/credential.schema.ts +1 -0
  60. package/src/schema/index.ts +67 -52
  61. package/src/schema/pikku-schema.types.ts +20 -0
  62. package/src/schema/schema.test.ts +86 -29
  63. package/src/schema/scope.schema.ts +1 -0
  64. package/src/schema/virtual-user.schema.ts +2 -1
  65. package/src/schema/webhook.schema.ts +1 -0
  66. package/src/schema/workflow.schema.ts +1 -0
@@ -3,19 +3,42 @@ import { type PikkuSchema, type RequiredTypes, type SchemaRequirement } from './
3
3
  export type { PikkuSchema, RequiredTypes, SchemaContext, SchemaRequirement, SchemaStatement, SchemaStatementFactory, } from './pikku-schema.types.js';
4
4
  export { rawStatement, requiredType, schemaContext, unqualifiedContext, } from './pikku-schema.types.js';
5
5
  /**
6
- * Opt-in, so it is named here rather than folded into `pikkuSchemas` — the
7
- * runtime works without an audit table, and `KyselyAuditService.init()` applies
8
- * this for the projects that wire the sink.
9
- */
10
- export { auditSchema } from './audit.schema.js';
11
- /**
12
- * Every table the pikku runtime needs, in dependency order.
6
+ * Every table any part of the pikku runtime can need, in dependency order.
13
7
  *
14
8
  * Order is load-bearing, not cosmetic: `scope` has foreign keys onto Better
15
9
  * Auth's `user`, and the tables within each schema reference each other. Sort
16
10
  * this list alphabetically and it stops applying.
11
+ *
12
+ * `audit` and `virtualUser` are in the list now. They used to be left out and
13
+ * created by their own services at boot, which was the only way they ever came
14
+ * into existence — so with the runtime no longer creating anything, leaving
15
+ * them out would mean a wired audit sink starting up against a table that
16
+ * nothing writes a migration for. Both are gated by `ownedBy`, so a project
17
+ * that wires neither still carries neither.
17
18
  */
18
19
  export declare const pikkuSchemas: PikkuSchema[];
20
+ export { agentSchema } from './agent.schema.js';
21
+ export { auditSchema } from './audit.schema.js';
22
+ export { channelSchema } from './channel.schema.js';
23
+ export { credentialSchema } from './credential.schema.js';
24
+ export { deploymentSchema } from './deployment.schema.js';
25
+ export { scopeSchema } from './scope.schema.js';
26
+ export { secretSchema } from './secret.schema.js';
27
+ export { sessionSchema } from './session.schema.js';
28
+ export { virtualUserSchema } from './virtual-user.schema.js';
29
+ export { webhookSchema } from './webhook.schema.js';
30
+ export { workflowSchema } from './workflow.schema.js';
31
+ /**
32
+ * The schemas a project needs, given the services its code actually reaches.
33
+ *
34
+ * `services` is the inspector's `requiredServices` — the same set that drives
35
+ * service tree-shaking, so a schema is written for exactly the projects whose
36
+ * functions, middleware or wirings reach the service that owns its tables.
37
+ *
38
+ * The gate is one-sided: a schema declaring no `ownedBy` is always in, because
39
+ * nothing a project writes implies it and leaving it out would be a guess.
40
+ */
41
+ export declare const requiredPikkuSchemas: (services: ReadonlySet<string>) => PikkuSchema[];
19
42
  export interface UnmetRequirement {
20
43
  schema: PikkuSchema;
21
44
  requirement: SchemaRequirement;
@@ -74,27 +97,25 @@ export interface DeclaredTable {
74
97
  * schema the DDL will actually target — the same binding the lookup has to use.
75
98
  */
76
99
  export declare const declaredTables: (schema: PikkuSchema, db: Kysely<any>) => DeclaredTable[];
77
- /** What `ensurePikkuSchema` found when it looked. */
78
- export type EnsureOutcome = 'present' | 'created';
79
100
  /**
80
- * Make sure one schema's tables are there, creating them only if none are.
81
- *
82
- * What a service calls at boot. It looks before it creates, which is the whole
83
- * difference from the `.ifNotExists()` DDL this replaces: that spelling turned
84
- * every failure into a silent no-op, and hid a foreign key onto `user.id` that
85
- * postgres had been rejecting since the day it was written.
86
- *
87
- * A partially-present schema is refused rather than completed. Half a schema
88
- * means something else already applied part of it — a migration, an older
89
- * version, a hand-run script — and creating the remainder at boot would leave
90
- * two authorities over one set of tables, which is the condition all of this
91
- * exists to end.
92
- *
93
- * Creating at boot is the fallback, not the intent. `pikku db generate` writes
94
- * the declaration down as a migration; a project that has done so takes the
95
- * `present` path and this never issues DDL at all.
101
+ * Refuse to start unless one schema's tables are already there.
102
+ *
103
+ * What a service calls at boot, and it never issues DDL. The runtime is not an
104
+ * author of the schema: `pikku db generate` writes the declaration down as a
105
+ * migration and `pikku db migrate` applies it, and those two are the only way
106
+ * these tables come into existence. A service that finds them missing says so
107
+ * and stops.
108
+ *
109
+ * This is the whole point of gating what `db generate` writes. While boot could
110
+ * still create, a schema that generation left out was created silently at
111
+ * startup instead — two authorities over one set of tables, which is the
112
+ * condition all of this exists to end. With creation gone, a schema generation
113
+ * missed is a sentence at startup naming the command that fixes it.
114
+ *
115
+ * Half-present is not a distinct case any more. Every missing table is reported
116
+ * the same way, because the remedy is the same one either way.
96
117
  */
97
- export declare const ensurePikkuSchema: (db: Kysely<any>, schema: PikkuSchema) => Promise<EnsureOutcome>;
118
+ export declare const requirePikkuSchema: (db: Kysely<any>, schema: PikkuSchema) => Promise<void>;
98
119
  /**
99
120
  * Render the declaration as SQL for the dialect `db` was built with.
100
121
  *
@@ -1,6 +1,7 @@
1
1
  import { CamelCasePlugin } from 'kysely';
2
2
  import { schemaContext, } from './pikku-schema.types.js';
3
3
  import { agentSchema } from './agent.schema.js';
4
+ import { auditSchema } from './audit.schema.js';
4
5
  import { channelSchema } from './channel.schema.js';
5
6
  import { credentialSchema } from './credential.schema.js';
6
7
  import { deploymentSchema } from './deployment.schema.js';
@@ -9,19 +10,21 @@ import { secretSchema } from './secret.schema.js';
9
10
  import { sessionSchema } from './session.schema.js';
10
11
  import { webhookSchema } from './webhook.schema.js';
11
12
  import { workflowSchema } from './workflow.schema.js';
13
+ import { virtualUserSchema } from './virtual-user.schema.js';
12
14
  export { rawStatement, requiredType, schemaContext, unqualifiedContext, } from './pikku-schema.types.js';
13
15
  /**
14
- * Opt-in, so it is named here rather than folded into `pikkuSchemas` — the
15
- * runtime works without an audit table, and `KyselyAuditService.init()` applies
16
- * this for the projects that wire the sink.
17
- */
18
- export { auditSchema } from './audit.schema.js';
19
- /**
20
- * Every table the pikku runtime needs, in dependency order.
16
+ * Every table any part of the pikku runtime can need, in dependency order.
21
17
  *
22
18
  * Order is load-bearing, not cosmetic: `scope` has foreign keys onto Better
23
19
  * Auth's `user`, and the tables within each schema reference each other. Sort
24
20
  * this list alphabetically and it stops applying.
21
+ *
22
+ * `audit` and `virtualUser` are in the list now. They used to be left out and
23
+ * created by their own services at boot, which was the only way they ever came
24
+ * into existence — so with the runtime no longer creating anything, leaving
25
+ * them out would mean a wired audit sink starting up against a table that
26
+ * nothing writes a migration for. Both are gated by `ownedBy`, so a project
27
+ * that wires neither still carries neither.
25
28
  */
26
29
  export const pikkuSchemas = [
27
30
  channelSchema,
@@ -33,7 +36,31 @@ export const pikkuSchemas = [
33
36
  workflowSchema,
34
37
  agentSchema,
35
38
  scopeSchema,
39
+ auditSchema,
40
+ virtualUserSchema,
36
41
  ];
42
+ export { agentSchema } from './agent.schema.js';
43
+ export { auditSchema } from './audit.schema.js';
44
+ export { channelSchema } from './channel.schema.js';
45
+ export { credentialSchema } from './credential.schema.js';
46
+ export { deploymentSchema } from './deployment.schema.js';
47
+ export { scopeSchema } from './scope.schema.js';
48
+ export { secretSchema } from './secret.schema.js';
49
+ export { sessionSchema } from './session.schema.js';
50
+ export { virtualUserSchema } from './virtual-user.schema.js';
51
+ export { webhookSchema } from './webhook.schema.js';
52
+ export { workflowSchema } from './workflow.schema.js';
53
+ /**
54
+ * The schemas a project needs, given the services its code actually reaches.
55
+ *
56
+ * `services` is the inspector's `requiredServices` — the same set that drives
57
+ * service tree-shaking, so a schema is written for exactly the projects whose
58
+ * functions, middleware or wirings reach the service that owns its tables.
59
+ *
60
+ * The gate is one-sided: a schema declaring no `ownedBy` is always in, because
61
+ * nothing a project writes implies it and leaving it out would be a guess.
62
+ */
63
+ export const requiredPikkuSchemas = (services) => pikkuSchemas.filter((schema) => !schema.ownedBy || schema.ownedBy.some((name) => services.has(name)));
37
64
  /**
38
65
  * Bind a declaration to a database.
39
66
  *
@@ -237,53 +264,36 @@ const isPresent = (existing, table) => table.schema && existing.schemaAware
237
264
  ? existing.qualified.has(`${table.schema}.${table.name}`)
238
265
  : existing.bare.has(table.name);
239
266
  /**
240
- * Make sure one schema's tables are there, creating them only if none are.
267
+ * Refuse to start unless one schema's tables are already there.
241
268
  *
242
- * What a service calls at boot. It looks before it creates, which is the whole
243
- * difference from the `.ifNotExists()` DDL this replaces: that spelling turned
244
- * every failure into a silent no-op, and hid a foreign key onto `user.id` that
245
- * postgres had been rejecting since the day it was written.
269
+ * What a service calls at boot, and it never issues DDL. The runtime is not an
270
+ * author of the schema: `pikku db generate` writes the declaration down as a
271
+ * migration and `pikku db migrate` applies it, and those two are the only way
272
+ * these tables come into existence. A service that finds them missing says so
273
+ * and stops.
246
274
  *
247
- * A partially-present schema is refused rather than completed. Half a schema
248
- * means something else already applied part of it — a migration, an older
249
- * version, a hand-run script — and creating the remainder at boot would leave
250
- * two authorities over one set of tables, which is the condition all of this
251
- * exists to end.
275
+ * This is the whole point of gating what `db generate` writes. While boot could
276
+ * still create, a schema that generation left out was created silently at
277
+ * startup instead — two authorities over one set of tables, which is the
278
+ * condition all of this exists to end. With creation gone, a schema generation
279
+ * missed is a sentence at startup naming the command that fixes it.
252
280
  *
253
- * Creating at boot is the fallback, not the intent. `pikku db generate` writes
254
- * the declaration down as a migration; a project that has done so takes the
255
- * `present` path and this never issues DDL at all.
281
+ * Half-present is not a distinct case any more. Every missing table is reported
282
+ * the same way, because the remedy is the same one either way.
256
283
  */
257
- export const ensurePikkuSchema = async (db, schema) => {
284
+ export const requirePikkuSchema = async (db, schema) => {
258
285
  const existing = await tablesOf(db);
259
286
  const declared = declaredTables(schema, db);
260
287
  const missing = declared.filter((table) => !isPresent(existing, table));
261
288
  if (missing.length === 0)
262
- return 'present';
263
- if (missing.length < declared.length) {
264
- throw new Error(`The '${schema.name}' schema is half applied: ${missing.map(displayName).join(', ')} missing, ` +
265
- `${declared
266
- .filter((table) => isPresent(existing, table))
267
- .map(displayName)
268
- .join(', ')} already there. ` +
269
- 'Something else owns part of it. Reconcile it with a migration — ' +
270
- '`pikku db generate` writes the declaration down — rather than creating the rest at boot.');
271
- }
272
- try {
273
- await applyPikkuSchemas(db, [schema]);
274
- return 'created';
275
- }
276
- catch (error) {
277
- // Two instances booting cold against one database can both see it empty
278
- // before either has finished creating. If the other one got there, say so;
279
- // anything else is a real failure and still throws. This is a courtesy, not
280
- // a guarantee — a boot race is only fully answered by having run the
281
- // migration, which is what `pikku db generate` is for.
282
- const after = await tablesOf(db);
283
- if (declared.every((table) => isPresent(after, table)))
284
- return 'present';
285
- throw error;
286
- }
289
+ return;
290
+ const present = declared.filter((table) => isPresent(existing, table));
291
+ throw new Error(`The '${schema.name}' schema is not in this database: ${missing.map(displayName).join(', ')} missing` +
292
+ (present.length > 0
293
+ ? `, ${present.map(displayName).join(', ')} already there`
294
+ : '') +
295
+ '. The runtime does not create these — run `pikku db generate` to write the ' +
296
+ 'migration, then `pikku db migrate` to apply it.');
287
297
  };
288
298
  /**
289
299
  * Render the declaration as SQL for the dialect `db` was built with.
@@ -80,6 +80,26 @@ export interface PikkuSchema {
80
80
  * database — or, worse, a stub table conjured up to make the error go away.
81
81
  */
82
82
  requires?: SchemaRequirement[];
83
+ /**
84
+ * The singleton services whose `init()` requires these tables, or absent for
85
+ * a schema every project needs.
86
+ *
87
+ * Named as services rather than as wirings because a service is what the
88
+ * tables actually belong to: one `requirePikkuSchema` call site per service,
89
+ * and `pikku db generate` writes the schema for a project whose functions
90
+ * reach any of them. A wiring is a poor proxy — a project can construct
91
+ * `KyselyWorkflowService` and wire no workflows at all.
92
+ *
93
+ * Absent means unconditional. `secrets` is on every project and the session
94
+ * and deployment stores are reached by generated code rather than by
95
+ * functions, so nothing a project writes implies them and their absence
96
+ * cannot be inferred.
97
+ *
98
+ * Under-naming is safe now that the runtime creates nothing: a schema left
99
+ * out of the migration is a sentence at startup, not a table conjured behind
100
+ * your back. Over-naming costs an unused table. Prefer over-naming.
101
+ */
102
+ ownedBy?: string[];
83
103
  }
84
104
  /**
85
105
  * A column another source owns, which one of these schemas depends on.
@@ -18,6 +18,7 @@ import { requiredType } from './pikku-schema.types.js';
18
18
  */
19
19
  export const scopeSchema = {
20
20
  name: 'scope',
21
+ ownedBy: ['scopeService'],
21
22
  requires: [{ table: 'user', column: 'id', owner: 'Better Auth' }],
22
23
  statements: [
23
24
  (db) => db.schema
@@ -17,7 +17,8 @@ import { sql } from 'kysely';
17
17
  * week's does not depend on which database they were found in.
18
18
  */
19
19
  export const virtualUserSchema = {
20
- name: 'virtualUser',
20
+ name: 'virtual-user',
21
+ ownedBy: ['virtualUserRunStore'],
21
22
  statements: [
22
23
  (db) => db.schema
23
24
  .createTable('virtualUserRun')
@@ -2,6 +2,7 @@ import { sql } from 'kysely';
2
2
  /** Webhook deliveries and one row per delivery attempt. */
3
3
  export const webhookSchema = {
4
4
  name: 'webhook',
5
+ ownedBy: ['webhookService'],
5
6
  statements: [
6
7
  (db) => db.schema
7
8
  .createTable('webhookDelivery')
@@ -16,6 +16,7 @@ import { sql } from 'kysely';
16
16
  */
17
17
  export const workflowSchema = {
18
18
  name: 'workflow',
19
+ ownedBy: ['workflowService', 'workflowRunService'],
19
20
  statements: [
20
21
  (db) => db.schema
21
22
  .createTable('workflowRuns')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/kysely",
3
- "version": "0.13.18",
3
+ "version": "0.13.20",
4
4
  "description": "Kysely-backed Pikku services — workflow state, secrets, sessions, scopes and audit",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "prepublishOnly": "yarn build"
22
22
  },
23
23
  "peerDependencies": {
24
- "@pikku/core": "^0.12.86"
24
+ "@pikku/core": "^0.12.89"
25
25
  },
26
26
  "dependencies": {
27
27
  "kysely": "^0.29.5"
package/src/index.ts CHANGED
@@ -32,12 +32,22 @@ export {
32
32
  type CreateCoercionPluginOptions,
33
33
  } from './coercion-plugin.js'
34
34
  export {
35
+ agentSchema,
35
36
  auditSchema,
37
+ channelSchema,
38
+ credentialSchema,
39
+ deploymentSchema,
40
+ scopeSchema,
41
+ secretSchema,
42
+ sessionSchema,
43
+ virtualUserSchema,
44
+ webhookSchema,
45
+ workflowSchema,
36
46
  pikkuSchemas,
47
+ requiredPikkuSchemas,
37
48
  applyPikkuSchemas,
38
49
  compilePikkuSchemas,
39
- ensurePikkuSchema,
40
- type EnsureOutcome,
50
+ requirePikkuSchema,
41
51
  resolveRequirements,
42
52
  type PikkuSchema,
43
53
  type RequiredTypes,
@@ -9,7 +9,7 @@ import type {
9
9
  SaveScoreInput,
10
10
  } from '@pikku/core/services'
11
11
  import type { PendingApproval } from '@pikku/core/agent'
12
- import { ensurePikkuSchema } from './schema/index.js'
12
+ import { requirePikkuSchema } from './schema/index.js'
13
13
  import { agentSchema } from './schema/agent.schema.js'
14
14
  import { getRunScores, saveRunScore } from './kysely-agent-run-scores.js'
15
15
 
@@ -20,7 +20,7 @@ export class KyselyAgentRunStateService implements AgentRunStateService {
20
20
 
21
21
  async init(): Promise<void> {
22
22
  if (this.initialized) return
23
- await ensurePikkuSchema(this.db, agentSchema)
23
+ await requirePikkuSchema(this.db, agentSchema)
24
24
  this.initialized = true
25
25
  }
26
26
 
@@ -14,7 +14,7 @@ import type { Kysely } from 'kysely'
14
14
  import type { KyselyPikkuDB } from './kysely-tables.js'
15
15
  import { parseJson } from './kysely-json.js'
16
16
  import { getRunScores, saveRunScore } from './kysely-agent-run-scores.js'
17
- import { ensurePikkuSchema } from './schema/index.js'
17
+ import { requirePikkuSchema } from './schema/index.js'
18
18
  import { agentSchema } from './schema/agent.schema.js'
19
19
 
20
20
  export class KyselyAgentStorageService
@@ -26,7 +26,7 @@ export class KyselyAgentStorageService
26
26
 
27
27
  public async init(): Promise<void> {
28
28
  if (this.initialized) return
29
- await ensurePikkuSchema(this.db, agentSchema)
29
+ await requirePikkuSchema(this.db, agentSchema)
30
30
  this.initialized = true
31
31
  }
32
32
 
@@ -309,21 +309,13 @@ describe('KyselyAuditService against a CamelCasePlugin connection', () => {
309
309
  })
310
310
 
311
311
  describe('KyselyAuditService.init', () => {
312
- test('creates the audit table on a database that has none', async () => {
312
+ test('refuses to boot against a database that has none', async () => {
313
313
  const fresh = new Kysely<any>({
314
314
  dialect: new SqliteDialect({ database: new Database(':memory:') }),
315
315
  plugins: [new CamelCasePlugin()],
316
316
  })
317
317
  const freshService = new KyselyAuditService(fresh)
318
- await freshService.init()
319
- await freshService.write([
320
- event({ eventId: 'e1', type: 't', occurredAt: '2026-01-01' }),
321
- ])
322
- const { events } = await freshService.query()
323
- assert.deepEqual(
324
- events.map((e) => e.eventId),
325
- ['e1']
326
- )
318
+ await assert.rejects(freshService.init(), /pikku db generate/)
327
319
  })
328
320
 
329
321
  // Two boots against one database, and the second must not wipe the first.
@@ -6,7 +6,7 @@ import type {
6
6
  AuditQueryResult,
7
7
  } from '@pikku/core/services'
8
8
  import type { Kysely } from 'kysely'
9
- import { ensurePikkuSchema } from './schema/index.js'
9
+ import { requirePikkuSchema } from './schema/index.js'
10
10
  import { auditSchema } from './schema/audit.schema.js'
11
11
 
12
12
  const jsonOrNull = (v: unknown): string | null =>
@@ -103,7 +103,7 @@ export class KyselyAuditService implements AuditService {
103
103
  */
104
104
  public async init(): Promise<void> {
105
105
  if (this.initialized) return
106
- await ensurePikkuSchema(this.db, auditSchema)
106
+ await requirePikkuSchema(this.db, auditSchema)
107
107
  this.initialized = true
108
108
  }
109
109
 
@@ -3,7 +3,7 @@ import type { Channel } from '@pikku/core/channel'
3
3
  import type { Kysely } from 'kysely'
4
4
  import type { KyselyPikkuDB } from './kysely-tables.js'
5
5
  import { parseJson } from './kysely-json.js'
6
- import { ensurePikkuSchema } from './schema/index.js'
6
+ import { requirePikkuSchema } from './schema/index.js'
7
7
  import { channelSchema } from './schema/channel.schema.js'
8
8
 
9
9
  export class KyselyChannelStore extends ChannelStore {
@@ -15,7 +15,7 @@ export class KyselyChannelStore extends ChannelStore {
15
15
 
16
16
  public async init(): Promise<void> {
17
17
  if (this.initialized) return
18
- await ensurePikkuSchema(this.db, channelSchema)
18
+ await requirePikkuSchema(this.db, channelSchema)
19
19
  this.initialized = true
20
20
  }
21
21
 
@@ -8,7 +8,7 @@ import {
8
8
  import type { CredentialService } from '@pikku/core/services'
9
9
  import type { Kysely } from 'kysely'
10
10
  import type { KyselyPikkuDB } from './kysely-tables.js'
11
- import { ensurePikkuSchema } from './schema/index.js'
11
+ import { requirePikkuSchema } from './schema/index.js'
12
12
  import { credentialSchema } from './schema/credential.schema.js'
13
13
 
14
14
  export interface KyselyCredentialServiceConfig {
@@ -42,7 +42,7 @@ export class KyselyCredentialService implements CredentialService {
42
42
 
43
43
  public async init(): Promise<void> {
44
44
  if (this.initialized) return
45
- await ensurePikkuSchema(this.db, credentialSchema)
45
+ await requirePikkuSchema(this.db, credentialSchema)
46
46
  this.initialized = true
47
47
  }
48
48
 
@@ -9,7 +9,7 @@ import type { SecretService } from '@pikku/core/services'
9
9
  import { getAllFunctionNames } from '@pikku/core/function'
10
10
  import type { Kysely } from 'kysely'
11
11
  import type { KyselyPikkuDB } from './kysely-tables.js'
12
- import { ensurePikkuSchema } from './schema/index.js'
12
+ import { requirePikkuSchema } from './schema/index.js'
13
13
  import { deploymentSchema } from './schema/deployment.schema.js'
14
14
 
15
15
  export class KyselyDeploymentService implements DeploymentService {
@@ -31,7 +31,7 @@ export class KyselyDeploymentService implements DeploymentService {
31
31
 
32
32
  public async init(): Promise<void> {
33
33
  if (this.initialized) return
34
- await ensurePikkuSchema(this.db, deploymentSchema)
34
+ await requirePikkuSchema(this.db, deploymentSchema)
35
35
  this.initialized = true
36
36
  }
37
37
 
@@ -5,6 +5,7 @@ import Database from 'better-sqlite3'
5
5
  import { SerializePlugin } from './serialize-plugin.js'
6
6
  import type { KyselyPikkuDB } from './kysely-tables.js'
7
7
  import { KyselyScopeService } from './kysely-scope-service.js'
8
+ import { applyPikkuSchemas, scopeSchema } from './schema/index.js'
8
9
 
9
10
  const SCOPES = [
10
11
  { id: 'admin' },
@@ -41,6 +42,7 @@ beforeEach(async () => {
41
42
  plugins: [new CamelCasePlugin(), new SerializePlugin()],
42
43
  })
43
44
  await createUserTable(db)
45
+ await applyPikkuSchemas(db, [scopeSchema])
44
46
  service = new KyselyScopeService(db)
45
47
  await service.init()
46
48
  await service.syncScopes(SCOPES)
@@ -8,7 +8,7 @@ import {
8
8
  } from '@pikku/core/errors'
9
9
  import type { Kysely } from 'kysely'
10
10
  import type { KyselyPikkuDB } from './kysely-tables.js'
11
- import { ensurePikkuSchema } from './schema/index.js'
11
+ import { requirePikkuSchema } from './schema/index.js'
12
12
  import { scopeSchema } from './schema/scope.schema.js'
13
13
 
14
14
  /**
@@ -30,7 +30,7 @@ export class KyselyScopeService implements ScopeService {
30
30
 
31
31
  public async init(): Promise<void> {
32
32
  if (this.initialized) return
33
- await ensurePikkuSchema(this.db, scopeSchema)
33
+ await requirePikkuSchema(this.db, scopeSchema)
34
34
  this.initialized = true
35
35
  }
36
36
 
@@ -10,7 +10,7 @@ import type { SecretService, SecretValues } from '@pikku/core/services'
10
10
  import type { SecretValue } from '@pikku/core/classification'
11
11
  import type { Kysely } from 'kysely'
12
12
  import type { KyselyPikkuDB } from './kysely-tables.js'
13
- import { ensurePikkuSchema } from './schema/index.js'
13
+ import { requirePikkuSchema } from './schema/index.js'
14
14
  import { secretSchema } from './schema/secret.schema.js'
15
15
 
16
16
  export interface KyselySecretServiceConfig {
@@ -44,7 +44,7 @@ export class KyselySecretService implements SecretService {
44
44
 
45
45
  public async init(): Promise<void> {
46
46
  if (this.initialized) return
47
- await ensurePikkuSchema(this.db, secretSchema)
47
+ await requirePikkuSchema(this.db, secretSchema)
48
48
  this.initialized = true
49
49
  }
50
50
 
@@ -18,6 +18,30 @@ import { KyselyAgentRunService } from './kysely-agent-run-service.js'
18
18
  import { KyselySecretService } from './kysely-secret-service.js'
19
19
  import { KyselyCredentialService } from './kysely-credential-service.js'
20
20
  import { KyselySessionStore } from './kysely-session-store.js'
21
+ import {
22
+ agentSchema,
23
+ applyPikkuSchemas,
24
+ channelSchema,
25
+ credentialSchema,
26
+ deploymentSchema,
27
+ secretSchema,
28
+ sessionSchema,
29
+ workflowSchema,
30
+ } from './schema/index.js'
31
+
32
+ /**
33
+ * Every schema the services under test boot against. They no longer create
34
+ * their own tables, so the harness stands in for `pikku db migrate`.
35
+ */
36
+ const SERVICE_SCHEMAS = [
37
+ channelSchema,
38
+ workflowSchema,
39
+ deploymentSchema,
40
+ agentSchema,
41
+ secretSchema,
42
+ credentialSchema,
43
+ sessionSchema,
44
+ ]
21
45
 
22
46
  function createSqliteDb(): Kysely<KyselyPikkuDB> {
23
47
  return new Kysely<KyselyPikkuDB>({
@@ -345,6 +369,7 @@ describe('Kysely Services - SQLite', () => {
345
369
 
346
370
  before(async () => {
347
371
  db = createSqliteDb()
372
+ await applyPikkuSchemas(db, SERVICE_SCHEMAS)
348
373
  })
349
374
 
350
375
  after(async () => {
@@ -365,6 +390,7 @@ describe(
365
390
  before(async () => {
366
391
  db = createPostgresDb()!
367
392
  await dropAllTables(db)
393
+ await applyPikkuSchemas(db, SERVICE_SCHEMAS)
368
394
  })
369
395
 
370
396
  after(async () => {
@@ -3,7 +3,7 @@ import type { CoreUserSession } from '@pikku/core/types'
3
3
  import type { Kysely } from 'kysely'
4
4
  import type { KyselyPikkuDB } from './kysely-tables.js'
5
5
  import { parseJson } from './kysely-json.js'
6
- import { ensurePikkuSchema } from './schema/index.js'
6
+ import { requirePikkuSchema } from './schema/index.js'
7
7
  import { sessionSchema } from './schema/session.schema.js'
8
8
 
9
9
  export class KyselySessionStore implements SessionStore {
@@ -13,7 +13,7 @@ export class KyselySessionStore implements SessionStore {
13
13
 
14
14
  public async init(): Promise<void> {
15
15
  if (this.initialized) return
16
- await ensurePikkuSchema(this.db, sessionSchema)
16
+ await requirePikkuSchema(this.db, sessionSchema)
17
17
  this.initialized = true
18
18
  }
19
19
 
@@ -5,15 +5,7 @@ import { join } from 'node:path'
5
5
  import { Kysely, SqliteDialect } from 'kysely'
6
6
  import Database from 'better-sqlite3'
7
7
 
8
- import { applyPikkuSchemas, ensurePikkuSchema } from './schema/index.js'
9
- import { virtualUserSchema } from './schema/virtual-user.schema.js'
10
-
11
- /**
12
- * Opt-in schemas that still have an interface in `KyselyPikkuDB`, so the drift
13
- * check covers them too. A table being opt-in says when it is created, not
14
- * whether its type is allowed to disagree with its DDL.
15
- */
16
- const OPTIONAL_SCHEMAS = [virtualUserSchema]
8
+ import { applyPikkuSchemas } from './schema/index.js'
17
9
 
18
10
  /**
19
11
  * `KyselyPikkuDB` and the schema declaration describe the same tables, and
@@ -73,9 +65,6 @@ const declaredSchema = async (): Promise<Map<string, Set<string>>> => {
73
65
  .addColumn('id', 'text', (col) => col.primaryKey())
74
66
  .execute()
75
67
  await applyPikkuSchemas(db)
76
- for (const schema of OPTIONAL_SCHEMAS) {
77
- await ensurePikkuSchema(db, schema)
78
- }
79
68
 
80
69
  const tables = await db.introspection.getTables()
81
70
  return new Map(