@pikku/kysely 0.13.19 → 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.
- package/CHANGELOG.md +54 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/kysely-agent-run-state-service.js +2 -2
- package/dist/src/kysely-agent-storage-service.js +2 -2
- package/dist/src/kysely-audit-service.js +2 -2
- package/dist/src/kysely-channel-store.js +2 -2
- package/dist/src/kysely-credential-service.js +2 -2
- package/dist/src/kysely-deployment-service.js +2 -2
- package/dist/src/kysely-scope-service.js +2 -2
- package/dist/src/kysely-secret-service.js +2 -2
- package/dist/src/kysely-session-store.js +2 -2
- package/dist/src/kysely-tables.d.ts +28 -0
- package/dist/src/kysely-virtual-user-run-store.js +2 -2
- package/dist/src/kysely-webhook-service.js +2 -2
- package/dist/src/kysely-workflow-mirror.d.ts +1 -1
- package/dist/src/kysely-workflow-mirror.js +3 -3
- package/dist/src/kysely-workflow-service.js +2 -2
- package/dist/src/schema/agent.schema.js +1 -0
- package/dist/src/schema/audit.schema.d.ts +5 -4
- package/dist/src/schema/audit.schema.js +6 -4
- package/dist/src/schema/channel.schema.js +1 -0
- package/dist/src/schema/credential.schema.js +1 -0
- package/dist/src/schema/index.d.ts +47 -26
- package/dist/src/schema/index.js +56 -46
- package/dist/src/schema/pikku-schema.types.d.ts +20 -0
- package/dist/src/schema/scope.schema.js +1 -0
- package/dist/src/schema/virtual-user.schema.js +2 -1
- package/dist/src/schema/webhook.schema.js +1 -0
- package/dist/src/schema/workflow.schema.js +1 -0
- package/package.json +2 -2
- package/src/index.ts +12 -2
- package/src/kysely-agent-run-state-service.ts +2 -2
- package/src/kysely-agent-storage-service.ts +2 -2
- package/src/kysely-audit-service.test.ts +2 -10
- package/src/kysely-audit-service.ts +2 -2
- package/src/kysely-channel-store.ts +2 -2
- package/src/kysely-credential-service.ts +2 -2
- package/src/kysely-deployment-service.ts +2 -2
- package/src/kysely-scope-service.test.ts +2 -0
- package/src/kysely-scope-service.ts +2 -2
- package/src/kysely-secret-service.ts +2 -2
- package/src/kysely-services.test.ts +26 -0
- package/src/kysely-session-store.ts +2 -2
- package/src/kysely-tables.test.ts +1 -12
- package/src/kysely-tables.ts +29 -0
- package/src/kysely-virtual-user-run-store.test.ts +22 -6
- package/src/kysely-virtual-user-run-store.ts +2 -2
- package/src/kysely-webhook-service.test.ts +4 -0
- package/src/kysely-webhook-service.ts +2 -2
- package/src/kysely-workflow-mirror.test.ts +2 -0
- package/src/kysely-workflow-mirror.ts +3 -3
- package/src/kysely-workflow-service.test.ts +2 -0
- package/src/kysely-workflow-service.ts +2 -2
- package/src/schema/agent.schema.ts +1 -0
- package/src/schema/audit.schema.ts +6 -4
- package/src/schema/channel.schema.ts +1 -0
- package/src/schema/credential.schema.ts +1 -0
- package/src/schema/index.ts +67 -52
- package/src/schema/pikku-schema.types.ts +20 -0
- package/src/schema/schema.test.ts +86 -29
- package/src/schema/scope.schema.ts +1 -0
- package/src/schema/virtual-user.schema.ts +2 -1
- package/src/schema/webhook.schema.ts +1 -0
- package/src/schema/workflow.schema.ts +1 -0
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
} from '@pikku/core/virtual-user'
|
|
10
10
|
import type { KyselyPikkuDB, VirtualUserRunTable } from './kysely-tables.js'
|
|
11
11
|
import { parseJson } from './kysely-json.js'
|
|
12
|
-
import {
|
|
12
|
+
import { requirePikkuSchema } from './schema/index.js'
|
|
13
13
|
import { virtualUserSchema } from './schema/virtual-user.schema.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -43,7 +43,7 @@ export class KyselyVirtualUserRunStore implements VirtualUserRunStore {
|
|
|
43
43
|
*/
|
|
44
44
|
public async init(): Promise<void> {
|
|
45
45
|
if (this.initialized) return
|
|
46
|
-
await
|
|
46
|
+
await requirePikkuSchema(this.db, virtualUserSchema)
|
|
47
47
|
this.initialized = true
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -7,6 +7,7 @@ import { setSingletonServices } from '@pikku/core/state'
|
|
|
7
7
|
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
8
8
|
import { SerializePlugin } from './serialize-plugin.js'
|
|
9
9
|
import { KyselyWebhookService } from './kysely-webhook-service.js'
|
|
10
|
+
import { applyPikkuSchemas, webhookSchema } from './schema/index.js'
|
|
10
11
|
|
|
11
12
|
const noopLogger = { error() {}, info() {}, warn() {}, debug() {} }
|
|
12
13
|
|
|
@@ -50,6 +51,7 @@ describe('KyselyWebhookService', () => {
|
|
|
50
51
|
test('send() inserts a pending delivery and enqueues with deliveryId === jobId', async () => {
|
|
51
52
|
const db = createDb()
|
|
52
53
|
const { added, queue } = createQueue()
|
|
54
|
+
await applyPikkuSchemas(db, [webhookSchema])
|
|
53
55
|
const service = new KyselyWebhookService(queue as any, db)
|
|
54
56
|
await service.init()
|
|
55
57
|
|
|
@@ -76,6 +78,7 @@ describe('KyselyWebhookService', () => {
|
|
|
76
78
|
test('recordAttempt() appends attempts and rolls status forward', async () => {
|
|
77
79
|
const db = createDb()
|
|
78
80
|
const { queue } = createQueue()
|
|
81
|
+
await applyPikkuSchemas(db, [webhookSchema])
|
|
79
82
|
const service = new KyselyWebhookService(queue as any, db)
|
|
80
83
|
await service.init()
|
|
81
84
|
|
|
@@ -104,6 +107,7 @@ describe('KyselyWebhookService', () => {
|
|
|
104
107
|
test('listDeliveries() filters by organization', async () => {
|
|
105
108
|
const db = createDb()
|
|
106
109
|
const { queue } = createQueue()
|
|
110
|
+
await applyPikkuSchemas(db, [webhookSchema])
|
|
107
111
|
const service = new KyselyWebhookService(queue as any, db)
|
|
108
112
|
await service.init()
|
|
109
113
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from '@pikku/core/services'
|
|
11
11
|
import type { Kysely } from 'kysely'
|
|
12
12
|
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
13
|
-
import {
|
|
13
|
+
import { requirePikkuSchema } from './schema/index.js'
|
|
14
14
|
import { webhookSchema } from './schema/webhook.schema.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -32,7 +32,7 @@ export class KyselyWebhookService extends QueueWebhookService {
|
|
|
32
32
|
|
|
33
33
|
public async init(): Promise<void> {
|
|
34
34
|
if (this.initialized) return
|
|
35
|
-
await
|
|
35
|
+
await requirePikkuSchema(this.db, webhookSchema)
|
|
36
36
|
this.initialized = true
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -7,6 +7,7 @@ import Database from 'better-sqlite3'
|
|
|
7
7
|
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
8
8
|
import { KyselyWorkflowMirror } from './kysely-workflow-mirror.js'
|
|
9
9
|
import { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
|
|
10
|
+
import { applyPikkuSchemas, workflowSchema } from './schema/index.js'
|
|
10
11
|
|
|
11
12
|
let db: Kysely<KyselyPikkuDB>
|
|
12
13
|
let mirror: KyselyWorkflowMirror
|
|
@@ -20,6 +21,7 @@ beforeEach(async () => {
|
|
|
20
21
|
dialect: new SqliteDialect({ database: new Database(':memory:') }),
|
|
21
22
|
plugins: [new CamelCasePlugin(), new SerializePlugin()],
|
|
22
23
|
})
|
|
24
|
+
await applyPikkuSchemas(db, [workflowSchema])
|
|
23
25
|
mirror = new KyselyWorkflowMirror(db)
|
|
24
26
|
await mirror.init()
|
|
25
27
|
runService = new KyselyWorkflowRunService(db)
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
} from '@pikku/core/workflow'
|
|
10
10
|
import type { Kysely } from 'kysely'
|
|
11
11
|
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
12
|
-
import {
|
|
12
|
+
import { requirePikkuSchema } from './schema/index.js'
|
|
13
13
|
import { workflowSchema } from './schema/workflow.schema.js'
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -33,14 +33,14 @@ export class KyselyWorkflowMirror implements WorkflowRunMirror {
|
|
|
33
33
|
* Create the underlying tables if none of them exist yet.
|
|
34
34
|
*
|
|
35
35
|
* Safe to call from either the mirror or `KyselyWorkflowService.init()` —
|
|
36
|
-
* both apply the same `workflowSchema`, and `
|
|
36
|
+
* both apply the same `workflowSchema`, and `requirePikkuSchema` is a no-op
|
|
37
37
|
* once the tables are there. A database holding only *part* of the schema
|
|
38
38
|
* throws rather than filling in the rest: something else owns those tables,
|
|
39
39
|
* and boot is not where that gets reconciled.
|
|
40
40
|
*/
|
|
41
41
|
public async init(): Promise<void> {
|
|
42
42
|
if (this.initialized) return
|
|
43
|
-
await
|
|
43
|
+
await requirePikkuSchema(this.db, workflowSchema)
|
|
44
44
|
this.initialized = true
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -7,6 +7,7 @@ import type { StepState } from '@pikku/core/ecosystem/workflow'
|
|
|
7
7
|
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
8
8
|
import { SerializePlugin } from './serialize-plugin.js'
|
|
9
9
|
import { KyselyWorkflowService } from './kysely-workflow-service.js'
|
|
10
|
+
import { applyPikkuSchemas, workflowSchema } from './schema/index.js'
|
|
10
11
|
|
|
11
12
|
let db: Kysely<KyselyPikkuDB>
|
|
12
13
|
let service: KyselyWorkflowService
|
|
@@ -31,6 +32,7 @@ const createDb = () => {
|
|
|
31
32
|
|
|
32
33
|
beforeEach(async () => {
|
|
33
34
|
db = createDb()
|
|
35
|
+
await applyPikkuSchemas(db, [workflowSchema])
|
|
34
36
|
service = new KyselyWorkflowService(db, { wireQueues: false } as any)
|
|
35
37
|
await service.init()
|
|
36
38
|
})
|
|
@@ -17,7 +17,7 @@ import { sql, type Kysely } from 'kysely'
|
|
|
17
17
|
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
18
18
|
import { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
|
|
19
19
|
import { parseJson } from './kysely-json.js'
|
|
20
|
-
import {
|
|
20
|
+
import { requirePikkuSchema } from './schema/index.js'
|
|
21
21
|
import { workflowSchema } from './schema/workflow.schema.js'
|
|
22
22
|
|
|
23
23
|
export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
@@ -34,7 +34,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
34
34
|
|
|
35
35
|
public async init(): Promise<void> {
|
|
36
36
|
if (this.initialized) return
|
|
37
|
-
await
|
|
37
|
+
await requirePikkuSchema(this.db, workflowSchema)
|
|
38
38
|
this.initialized = true
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -3,10 +3,11 @@ import type { PikkuSchema } from './pikku-schema.types.js'
|
|
|
3
3
|
/**
|
|
4
4
|
* The `audit` table {@link KyselyAuditService} writes to and reads back.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Gated by `ownedBy` rather than written for everyone: only a project that has
|
|
7
|
+
* wired a durable audit sink needs this table, and generating it for the rest
|
|
8
|
+
* would put an empty table in every database. `KyselyAuditService.init()`
|
|
9
|
+
* requires it and never creates it, so it arrives with the migration `pikku db
|
|
10
|
+
* generate` writes for the sink that fills it.
|
|
10
11
|
*
|
|
11
12
|
* Every column is text on every engine, matching the platform audit-queue
|
|
12
13
|
* consumer's row shape — a locally-run project and a deployed stage write rows
|
|
@@ -16,6 +17,7 @@ import type { PikkuSchema } from './pikku-schema.types.js'
|
|
|
16
17
|
*/
|
|
17
18
|
export const auditSchema: PikkuSchema = {
|
|
18
19
|
name: 'audit',
|
|
20
|
+
ownedBy: ['audit', 'auditLog'],
|
|
19
21
|
statements: [
|
|
20
22
|
(db) =>
|
|
21
23
|
db.schema
|
|
@@ -4,6 +4,7 @@ import type { PikkuSchema } from './pikku-schema.types.js'
|
|
|
4
4
|
/** Channels and their topic subscriptions, also read by the event-hub store. */
|
|
5
5
|
export const channelSchema: PikkuSchema = {
|
|
6
6
|
name: 'channel',
|
|
7
|
+
ownedBy: ['channelStore', 'eventHub'],
|
|
7
8
|
statements: [
|
|
8
9
|
(db) =>
|
|
9
10
|
db.schema
|
package/src/schema/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type SchemaRequirement,
|
|
8
8
|
} from './pikku-schema.types.js'
|
|
9
9
|
import { agentSchema } from './agent.schema.js'
|
|
10
|
+
import { auditSchema } from './audit.schema.js'
|
|
10
11
|
import { channelSchema } from './channel.schema.js'
|
|
11
12
|
import { credentialSchema } from './credential.schema.js'
|
|
12
13
|
import { deploymentSchema } from './deployment.schema.js'
|
|
@@ -15,6 +16,7 @@ import { secretSchema } from './secret.schema.js'
|
|
|
15
16
|
import { sessionSchema } from './session.schema.js'
|
|
16
17
|
import { webhookSchema } from './webhook.schema.js'
|
|
17
18
|
import { workflowSchema } from './workflow.schema.js'
|
|
19
|
+
import { virtualUserSchema } from './virtual-user.schema.js'
|
|
18
20
|
|
|
19
21
|
export type {
|
|
20
22
|
PikkuSchema,
|
|
@@ -32,18 +34,18 @@ export {
|
|
|
32
34
|
} from './pikku-schema.types.js'
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
36
|
-
* runtime works without an audit table, and `KyselyAuditService.init()` applies
|
|
37
|
-
* this for the projects that wire the sink.
|
|
38
|
-
*/
|
|
39
|
-
export { auditSchema } from './audit.schema.js'
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Every table the pikku runtime needs, in dependency order.
|
|
37
|
+
* Every table any part of the pikku runtime can need, in dependency order.
|
|
43
38
|
*
|
|
44
39
|
* Order is load-bearing, not cosmetic: `scope` has foreign keys onto Better
|
|
45
40
|
* Auth's `user`, and the tables within each schema reference each other. Sort
|
|
46
41
|
* this list alphabetically and it stops applying.
|
|
42
|
+
*
|
|
43
|
+
* `audit` and `virtualUser` are in the list now. They used to be left out and
|
|
44
|
+
* created by their own services at boot, which was the only way they ever came
|
|
45
|
+
* into existence — so with the runtime no longer creating anything, leaving
|
|
46
|
+
* them out would mean a wired audit sink starting up against a table that
|
|
47
|
+
* nothing writes a migration for. Both are gated by `ownedBy`, so a project
|
|
48
|
+
* that wires neither still carries neither.
|
|
47
49
|
*/
|
|
48
50
|
export const pikkuSchemas: PikkuSchema[] = [
|
|
49
51
|
channelSchema,
|
|
@@ -55,8 +57,40 @@ export const pikkuSchemas: PikkuSchema[] = [
|
|
|
55
57
|
workflowSchema,
|
|
56
58
|
agentSchema,
|
|
57
59
|
scopeSchema,
|
|
60
|
+
auditSchema,
|
|
61
|
+
virtualUserSchema,
|
|
58
62
|
]
|
|
59
63
|
|
|
64
|
+
export { agentSchema } from './agent.schema.js'
|
|
65
|
+
export { auditSchema } from './audit.schema.js'
|
|
66
|
+
export { channelSchema } from './channel.schema.js'
|
|
67
|
+
export { credentialSchema } from './credential.schema.js'
|
|
68
|
+
export { deploymentSchema } from './deployment.schema.js'
|
|
69
|
+
export { scopeSchema } from './scope.schema.js'
|
|
70
|
+
export { secretSchema } from './secret.schema.js'
|
|
71
|
+
export { sessionSchema } from './session.schema.js'
|
|
72
|
+
export { virtualUserSchema } from './virtual-user.schema.js'
|
|
73
|
+
export { webhookSchema } from './webhook.schema.js'
|
|
74
|
+
export { workflowSchema } from './workflow.schema.js'
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The schemas a project needs, given the services its code actually reaches.
|
|
78
|
+
*
|
|
79
|
+
* `services` is the inspector's `requiredServices` — the same set that drives
|
|
80
|
+
* service tree-shaking, so a schema is written for exactly the projects whose
|
|
81
|
+
* functions, middleware or wirings reach the service that owns its tables.
|
|
82
|
+
*
|
|
83
|
+
* The gate is one-sided: a schema declaring no `ownedBy` is always in, because
|
|
84
|
+
* nothing a project writes implies it and leaving it out would be a guess.
|
|
85
|
+
*/
|
|
86
|
+
export const requiredPikkuSchemas = (
|
|
87
|
+
services: ReadonlySet<string>
|
|
88
|
+
): PikkuSchema[] =>
|
|
89
|
+
pikkuSchemas.filter(
|
|
90
|
+
(schema) =>
|
|
91
|
+
!schema.ownedBy || schema.ownedBy.some((name) => services.has(name))
|
|
92
|
+
)
|
|
93
|
+
|
|
60
94
|
/**
|
|
61
95
|
* Bind a declaration to a database.
|
|
62
96
|
*
|
|
@@ -287,9 +321,6 @@ export const declaredTables = (
|
|
|
287
321
|
return tables
|
|
288
322
|
}
|
|
289
323
|
|
|
290
|
-
/** What `ensurePikkuSchema` found when it looked. */
|
|
291
|
-
export type EnsureOutcome = 'present' | 'created'
|
|
292
|
-
|
|
293
324
|
/**
|
|
294
325
|
* What `db` already has, indexed both ways.
|
|
295
326
|
*
|
|
@@ -337,58 +368,42 @@ const isPresent = (existing: ExistingTables, table: DeclaredTable): boolean =>
|
|
|
337
368
|
: existing.bare.has(table.name)
|
|
338
369
|
|
|
339
370
|
/**
|
|
340
|
-
*
|
|
371
|
+
* Refuse to start unless one schema's tables are already there.
|
|
341
372
|
*
|
|
342
|
-
* What a service calls at boot
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
373
|
+
* What a service calls at boot, and it never issues DDL. The runtime is not an
|
|
374
|
+
* author of the schema: `pikku db generate` writes the declaration down as a
|
|
375
|
+
* migration and `pikku db migrate` applies it, and those two are the only way
|
|
376
|
+
* these tables come into existence. A service that finds them missing says so
|
|
377
|
+
* and stops.
|
|
346
378
|
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
379
|
+
* This is the whole point of gating what `db generate` writes. While boot could
|
|
380
|
+
* still create, a schema that generation left out was created silently at
|
|
381
|
+
* startup instead — two authorities over one set of tables, which is the
|
|
382
|
+
* condition all of this exists to end. With creation gone, a schema generation
|
|
383
|
+
* missed is a sentence at startup naming the command that fixes it.
|
|
352
384
|
*
|
|
353
|
-
*
|
|
354
|
-
* the
|
|
355
|
-
* `present` path and this never issues DDL at all.
|
|
385
|
+
* Half-present is not a distinct case any more. Every missing table is reported
|
|
386
|
+
* the same way, because the remedy is the same one either way.
|
|
356
387
|
*/
|
|
357
|
-
export const
|
|
388
|
+
export const requirePikkuSchema = async (
|
|
358
389
|
db: Kysely<any>,
|
|
359
390
|
schema: PikkuSchema
|
|
360
|
-
): Promise<
|
|
391
|
+
): Promise<void> => {
|
|
361
392
|
const existing = await tablesOf(db)
|
|
362
393
|
const declared = declaredTables(schema, db)
|
|
363
394
|
const missing = declared.filter((table) => !isPresent(existing, table))
|
|
364
395
|
|
|
365
|
-
if (missing.length === 0) return
|
|
366
|
-
|
|
367
|
-
if (missing.length < declared.length) {
|
|
368
|
-
throw new Error(
|
|
369
|
-
`The '${schema.name}' schema is half applied: ${missing.map(displayName).join(', ')} missing, ` +
|
|
370
|
-
`${declared
|
|
371
|
-
.filter((table) => isPresent(existing, table))
|
|
372
|
-
.map(displayName)
|
|
373
|
-
.join(', ')} already there. ` +
|
|
374
|
-
'Something else owns part of it. Reconcile it with a migration — ' +
|
|
375
|
-
'`pikku db generate` writes the declaration down — rather than creating the rest at boot.'
|
|
376
|
-
)
|
|
377
|
-
}
|
|
396
|
+
if (missing.length === 0) return
|
|
378
397
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const after = await tablesOf(db)
|
|
389
|
-
if (declared.every((table) => isPresent(after, table))) return 'present'
|
|
390
|
-
throw error
|
|
391
|
-
}
|
|
398
|
+
const present = declared.filter((table) => isPresent(existing, table))
|
|
399
|
+
throw new Error(
|
|
400
|
+
`The '${schema.name}' schema is not in this database: ${missing.map(displayName).join(', ')} missing` +
|
|
401
|
+
(present.length > 0
|
|
402
|
+
? `, ${present.map(displayName).join(', ')} already there`
|
|
403
|
+
: '') +
|
|
404
|
+
'. The runtime does not create these — run `pikku db generate` to write the ' +
|
|
405
|
+
'migration, then `pikku db migrate` to apply it.'
|
|
406
|
+
)
|
|
392
407
|
}
|
|
393
408
|
|
|
394
409
|
/**
|
|
@@ -101,6 +101,26 @@ export interface PikkuSchema {
|
|
|
101
101
|
* database — or, worse, a stub table conjured up to make the error go away.
|
|
102
102
|
*/
|
|
103
103
|
requires?: SchemaRequirement[]
|
|
104
|
+
/**
|
|
105
|
+
* The singleton services whose `init()` requires these tables, or absent for
|
|
106
|
+
* a schema every project needs.
|
|
107
|
+
*
|
|
108
|
+
* Named as services rather than as wirings because a service is what the
|
|
109
|
+
* tables actually belong to: one `requirePikkuSchema` call site per service,
|
|
110
|
+
* and `pikku db generate` writes the schema for a project whose functions
|
|
111
|
+
* reach any of them. A wiring is a poor proxy — a project can construct
|
|
112
|
+
* `KyselyWorkflowService` and wire no workflows at all.
|
|
113
|
+
*
|
|
114
|
+
* Absent means unconditional. `secrets` is on every project and the session
|
|
115
|
+
* and deployment stores are reached by generated code rather than by
|
|
116
|
+
* functions, so nothing a project writes implies them and their absence
|
|
117
|
+
* cannot be inferred.
|
|
118
|
+
*
|
|
119
|
+
* Under-naming is safe now that the runtime creates nothing: a schema left
|
|
120
|
+
* out of the migration is a sentence at startup, not a table conjured behind
|
|
121
|
+
* your back. Over-naming costs an unused table. Prefer over-naming.
|
|
122
|
+
*/
|
|
123
|
+
ownedBy?: string[]
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
/**
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
import Database from 'better-sqlite3'
|
|
15
15
|
import {
|
|
16
16
|
pikkuSchemas,
|
|
17
|
+
requiredPikkuSchemas,
|
|
17
18
|
compilePikkuSchemas,
|
|
18
19
|
applyPikkuSchemas,
|
|
19
|
-
|
|
20
|
+
requirePikkuSchema,
|
|
20
21
|
declaredTables,
|
|
21
22
|
unsatisfiedRequirements,
|
|
22
23
|
type PikkuSchema,
|
|
@@ -238,17 +239,32 @@ describe('pikku runtime schema — prerequisites', () => {
|
|
|
238
239
|
})
|
|
239
240
|
})
|
|
240
241
|
|
|
241
|
-
describe('
|
|
242
|
+
describe('requirePikkuSchema — what a service does at boot', () => {
|
|
242
243
|
const memoryDb = () =>
|
|
243
244
|
new Kysely<any>({
|
|
244
245
|
dialect: new SqliteDialect({ database: new Database(':memory:') }),
|
|
245
246
|
})
|
|
246
247
|
|
|
247
|
-
test('
|
|
248
|
+
test('refuses a database the migration never reached, naming the command', async () => {
|
|
248
249
|
const db = memoryDb()
|
|
249
250
|
try {
|
|
250
|
-
assert.
|
|
251
|
-
|
|
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, [])
|
|
252
268
|
} finally {
|
|
253
269
|
await db.destroy()
|
|
254
270
|
}
|
|
@@ -256,21 +272,22 @@ describe('ensurePikkuSchema — what a service does at boot', () => {
|
|
|
256
272
|
|
|
257
273
|
test('two services declaring the same schema do not fight over it', async () => {
|
|
258
274
|
// The workflow service and the workflow mirror both own these tables, and
|
|
259
|
-
// both call init().
|
|
260
|
-
//
|
|
275
|
+
// both call init(). Neither creates anything, so both find the migrated
|
|
276
|
+
// tables and return.
|
|
261
277
|
const db = memoryDb()
|
|
262
278
|
try {
|
|
263
|
-
|
|
264
|
-
assert.
|
|
279
|
+
await applyPikkuSchemas(db, [workflowSchema])
|
|
280
|
+
await assert.doesNotReject(requirePikkuSchema(db, workflowSchema))
|
|
281
|
+
await assert.doesNotReject(requirePikkuSchema(db, workflowSchema))
|
|
265
282
|
} finally {
|
|
266
283
|
await db.destroy()
|
|
267
284
|
}
|
|
268
285
|
})
|
|
269
286
|
|
|
270
|
-
test('
|
|
287
|
+
test('a half-applied schema is refused the same way a missing one is', async () => {
|
|
271
288
|
// Something else already owns part of it — a migration, an older release,
|
|
272
|
-
// a hand-run script.
|
|
273
|
-
//
|
|
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.
|
|
274
291
|
const db = memoryDb()
|
|
275
292
|
try {
|
|
276
293
|
await db.schema
|
|
@@ -279,8 +296,8 @@ describe('ensurePikkuSchema — what a service does at boot', () => {
|
|
|
279
296
|
.execute()
|
|
280
297
|
|
|
281
298
|
await assert.rejects(
|
|
282
|
-
|
|
283
|
-
/
|
|
299
|
+
requirePikkuSchema(db, workflowSchema),
|
|
300
|
+
/workflow_step.*missing.*workflow_runs already there/s
|
|
284
301
|
)
|
|
285
302
|
} finally {
|
|
286
303
|
await db.destroy()
|
|
@@ -288,7 +305,7 @@ describe('ensurePikkuSchema — what a service does at boot', () => {
|
|
|
288
305
|
})
|
|
289
306
|
})
|
|
290
307
|
|
|
291
|
-
describe('
|
|
308
|
+
describe('requirePikkuSchema — a connection bound to a schema', () => {
|
|
292
309
|
const WORKFLOW_TABLES = [
|
|
293
310
|
'workflow_runs',
|
|
294
311
|
'workflow_step',
|
|
@@ -301,7 +318,7 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
301
318
|
*
|
|
302
319
|
* Stubbed rather than run against a real database because sqlite has one
|
|
303
320
|
* schema and nothing else here speaks postgres — and introspection is the
|
|
304
|
-
* only thing `
|
|
321
|
+
* only thing `requirePikkuSchema` reads before it decides. DummyDriver takes
|
|
305
322
|
* whatever DDL follows.
|
|
306
323
|
*/
|
|
307
324
|
const introspecting = (
|
|
@@ -352,9 +369,8 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
352
369
|
WORKFLOW_TABLES.map((name) => ({ schema: 'app', name }))
|
|
353
370
|
)
|
|
354
371
|
|
|
355
|
-
assert.
|
|
356
|
-
|
|
357
|
-
'present'
|
|
372
|
+
await assert.doesNotReject(
|
|
373
|
+
requirePikkuSchema(db.withSchema('app'), workflowSchema)
|
|
358
374
|
)
|
|
359
375
|
})
|
|
360
376
|
|
|
@@ -365,9 +381,9 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
365
381
|
WORKFLOW_TABLES.map((name) => ({ schema: 'public', name }))
|
|
366
382
|
)
|
|
367
383
|
|
|
368
|
-
assert.
|
|
369
|
-
|
|
370
|
-
|
|
384
|
+
await assert.rejects(
|
|
385
|
+
requirePikkuSchema(db.withSchema('app'), workflowSchema),
|
|
386
|
+
/is not in this database/
|
|
371
387
|
)
|
|
372
388
|
})
|
|
373
389
|
|
|
@@ -376,7 +392,7 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
376
392
|
WORKFLOW_TABLES.map((name) => ({ schema: 'public', name }))
|
|
377
393
|
)
|
|
378
394
|
|
|
379
|
-
assert.
|
|
395
|
+
await assert.doesNotReject(requirePikkuSchema(db, workflowSchema))
|
|
380
396
|
})
|
|
381
397
|
|
|
382
398
|
test('matches on the bare name when the engine reports no schemas', async () => {
|
|
@@ -387,9 +403,8 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
387
403
|
WORKFLOW_TABLES.map((name) => ({ schema: undefined, name }))
|
|
388
404
|
)
|
|
389
405
|
|
|
390
|
-
assert.
|
|
391
|
-
|
|
392
|
-
'present'
|
|
406
|
+
await assert.doesNotReject(
|
|
407
|
+
requirePikkuSchema(db.withSchema('main'), workflowSchema)
|
|
393
408
|
)
|
|
394
409
|
})
|
|
395
410
|
|
|
@@ -414,7 +429,7 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
414
429
|
})
|
|
415
430
|
|
|
416
431
|
await assert.rejects(
|
|
417
|
-
|
|
432
|
+
applyPikkuSchemas(db.withSchema('app'), [workflowSchema]),
|
|
418
433
|
(error: Error) => {
|
|
419
434
|
assert.equal(error, pgError, 'the engine error must pass through as-is')
|
|
420
435
|
return true
|
|
@@ -431,7 +446,7 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
431
446
|
})
|
|
432
447
|
try {
|
|
433
448
|
await assert.rejects(
|
|
434
|
-
|
|
449
|
+
applyPikkuSchemas(db.withSchema('main'), [workflowSchema]),
|
|
435
450
|
(error: Error) => {
|
|
436
451
|
assert.match(error.message, /bound to a schema/)
|
|
437
452
|
assert.match(error.message, /takes a bare table name/i)
|
|
@@ -448,7 +463,7 @@ describe('ensurePikkuSchema — a connection bound to a schema', () => {
|
|
|
448
463
|
const db = introspecting([{ schema: 'app', name: 'workflow_runs' }])
|
|
449
464
|
|
|
450
465
|
await assert.rejects(
|
|
451
|
-
|
|
466
|
+
requirePikkuSchema(db.withSchema('app'), workflowSchema),
|
|
452
467
|
/app\.workflow_step.*missing.*app\.workflow_runs already there/s
|
|
453
468
|
)
|
|
454
469
|
})
|
|
@@ -532,3 +547,45 @@ describe('compiling into a schema', () => {
|
|
|
532
547
|
)
|
|
533
548
|
})
|
|
534
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
|
+
})
|
|
@@ -19,6 +19,7 @@ import { requiredType, type PikkuSchema } from './pikku-schema.types.js'
|
|
|
19
19
|
*/
|
|
20
20
|
export const scopeSchema: PikkuSchema = {
|
|
21
21
|
name: 'scope',
|
|
22
|
+
ownedBy: ['scopeService'],
|
|
22
23
|
requires: [{ table: 'user', column: 'id', owner: 'Better Auth' }],
|
|
23
24
|
statements: [
|
|
24
25
|
(db) =>
|
|
@@ -19,7 +19,8 @@ import type { PikkuSchema } from './pikku-schema.types.js'
|
|
|
19
19
|
* week's does not depend on which database they were found in.
|
|
20
20
|
*/
|
|
21
21
|
export const virtualUserSchema: PikkuSchema = {
|
|
22
|
-
name: '
|
|
22
|
+
name: 'virtual-user',
|
|
23
|
+
ownedBy: ['virtualUserRunStore'],
|
|
23
24
|
statements: [
|
|
24
25
|
(db) =>
|
|
25
26
|
db.schema
|