@pikku/kysely 0.13.2 → 0.13.4

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 (71) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/src/index.d.ts +1 -0
  3. package/dist/src/index.js +1 -0
  4. package/dist/src/kysely-ai-run-state-service.js +3 -18
  5. package/dist/src/kysely-ai-storage-service.d.ts +0 -1
  6. package/dist/src/kysely-ai-storage-service.js +4 -101
  7. package/dist/src/kysely-channel-store.js +4 -24
  8. package/dist/src/kysely-credential-service.js +3 -24
  9. package/dist/src/kysely-deployment-service.d.ts +0 -1
  10. package/dist/src/kysely-deployment-service.js +4 -48
  11. package/dist/src/kysely-scope-service.js +4 -54
  12. package/dist/src/kysely-secret-service.js +3 -21
  13. package/dist/src/kysely-session-store.js +4 -11
  14. package/dist/src/kysely-tables.d.ts +2 -0
  15. package/dist/src/kysely-webhook-service.d.ts +0 -1
  16. package/dist/src/kysely-webhook-service.js +4 -51
  17. package/dist/src/kysely-workflow-mirror.d.ts +7 -3
  18. package/dist/src/kysely-workflow-mirror.js +10 -87
  19. package/dist/src/kysely-workflow-service.d.ts +2 -2
  20. package/dist/src/kysely-workflow-service.js +11 -95
  21. package/dist/src/schema/ai.schema.d.ts +12 -0
  22. package/dist/src/schema/ai.schema.js +89 -0
  23. package/dist/src/schema/channel.schema.d.ts +3 -0
  24. package/dist/src/schema/channel.schema.js +24 -0
  25. package/dist/src/schema/credential.schema.d.ts +11 -0
  26. package/dist/src/schema/credential.schema.js +33 -0
  27. package/dist/src/schema/deployment.schema.d.ts +3 -0
  28. package/dist/src/schema/deployment.schema.js +32 -0
  29. package/dist/src/schema/index.d.ts +82 -0
  30. package/dist/src/schema/index.js +187 -0
  31. package/dist/src/schema/pikku-schema.types.d.ts +75 -0
  32. package/dist/src/schema/pikku-schema.types.js +21 -0
  33. package/dist/src/schema/scope.schema.d.ts +18 -0
  34. package/dist/src/schema/scope.schema.js +61 -0
  35. package/dist/src/schema/secret.schema.d.ts +11 -0
  36. package/dist/src/schema/secret.schema.js +29 -0
  37. package/dist/src/schema/session.schema.d.ts +3 -0
  38. package/dist/src/schema/session.schema.js +13 -0
  39. package/dist/src/schema/webhook.schema.d.ts +3 -0
  40. package/dist/src/schema/webhook.schema.js +38 -0
  41. package/dist/src/schema/workflow.schema.d.ts +15 -0
  42. package/dist/src/schema/workflow.schema.js +86 -0
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +2 -2
  45. package/src/index.ts +18 -0
  46. package/src/kysely-ai-run-state-service.ts +7 -28
  47. package/src/kysely-ai-storage-service.ts +4 -152
  48. package/src/kysely-channel-store.ts +4 -34
  49. package/src/kysely-credential-service.ts +3 -36
  50. package/src/kysely-deployment-service.ts +4 -63
  51. package/src/kysely-scope-service.ts +4 -81
  52. package/src/kysely-secret-service.ts +6 -33
  53. package/src/kysely-session-store.ts +4 -18
  54. package/src/kysely-tables.test.ts +110 -0
  55. package/src/kysely-tables.ts +2 -0
  56. package/src/kysely-webhook-service.ts +4 -67
  57. package/src/kysely-workflow-mirror.test.ts +7 -7
  58. package/src/kysely-workflow-mirror.ts +10 -109
  59. package/src/kysely-workflow-service.ts +18 -125
  60. package/src/schema/ai.schema.ts +140 -0
  61. package/src/schema/channel.schema.ts +37 -0
  62. package/src/schema/credential.schema.ts +46 -0
  63. package/src/schema/deployment.schema.ts +47 -0
  64. package/src/schema/index.ts +249 -0
  65. package/src/schema/pikku-schema.types.ts +94 -0
  66. package/src/schema/schema.test.ts +273 -0
  67. package/src/schema/scope.schema.ts +95 -0
  68. package/src/schema/secret.schema.ts +40 -0
  69. package/src/schema/session.schema.ts +20 -0
  70. package/src/schema/webhook.schema.ts +57 -0
  71. package/src/schema/workflow.schema.ts +113 -0
@@ -1,4 +1,5 @@
1
- import { sql } from 'kysely';
1
+ import { ensurePikkuSchema } from './schema/index.js';
2
+ import { workflowSchema } from './schema/workflow.schema.js';
2
3
  /**
3
4
  * Kysely-backed `WorkflowRunMirror`.
4
5
  *
@@ -18,96 +19,18 @@ export class KyselyWorkflowMirror {
18
19
  this.db = db;
19
20
  }
20
21
  /**
21
- * Create the underlying tables if they don't exist. Safe to call from
22
- * either the mirror or `KyselyWorkflowService.init()` — schema is the
23
- * same and uses `ifNotExists`.
22
+ * Create the underlying tables if none of them exist yet.
23
+ *
24
+ * Safe to call from either the mirror or `KyselyWorkflowService.init()` —
25
+ * both apply the same `workflowSchema`, and `ensurePikkuSchema` is a no-op
26
+ * once the tables are there. A database holding only *part* of the schema
27
+ * throws rather than filling in the rest: something else owns those tables,
28
+ * and boot is not where that gets reconciled.
24
29
  */
25
30
  async init() {
26
31
  if (this.initialized)
27
32
  return;
28
- await this.db.schema
29
- .createTable('workflow_runs')
30
- .ifNotExists()
31
- .addColumn('workflow_run_id', 'text', (col) => col.primaryKey())
32
- .addColumn('workflow', 'text', (col) => col.notNull())
33
- .addColumn('status', 'text', (col) => col.notNull())
34
- .addColumn('input', 'text', (col) => col.notNull())
35
- .addColumn('output', 'text')
36
- .addColumn('error', 'text')
37
- .addColumn('state', 'text', (col) => col.defaultTo('{}'))
38
- .addColumn('inline', 'boolean', (col) => col.defaultTo(false))
39
- .addColumn('graph_hash', 'text')
40
- .addColumn('deterministic', 'boolean', (col) => col.defaultTo(false))
41
- .addColumn('planned_steps', 'text')
42
- .addColumn('wire', 'text')
43
- .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
44
- .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
45
- .execute();
46
- await this.db.schema
47
- .createTable('workflow_step')
48
- .ifNotExists()
49
- .addColumn('workflow_step_id', 'text', (col) => col.primaryKey())
50
- .addColumn('workflow_run_id', 'text', (col) => col
51
- .notNull()
52
- .references('workflow_runs.workflow_run_id')
53
- .onDelete('cascade'))
54
- .addColumn('step_name', 'text', (col) => col.notNull())
55
- .addColumn('rpc_name', 'text')
56
- .addColumn('data', 'text')
57
- .addColumn('status', 'text', (col) => col.notNull().defaultTo('pending'))
58
- .addColumn('result', 'text')
59
- .addColumn('error', 'text')
60
- .addColumn('child_run_id', 'text')
61
- .addColumn('branch_taken', 'text')
62
- .addColumn('retries', 'integer')
63
- .addColumn('retry_delay', 'text')
64
- .addColumn('from_step_name', 'text')
65
- .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
66
- .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
67
- .addUniqueConstraint('workflow_step_run_name_unique', [
68
- 'workflow_run_id',
69
- 'step_name',
70
- ])
71
- .execute();
72
- // Additive: backfill the column on mirror tables created before from_step_name.
73
- await this.db.schema
74
- .alterTable('workflow_step')
75
- .addColumn('from_step_name', 'text')
76
- .execute()
77
- .catch(() => {
78
- // Column already exists (fresh tables include it above) — nothing to do.
79
- });
80
- await this.db.schema
81
- .createTable('workflow_step_history')
82
- .ifNotExists()
83
- .addColumn('history_id', 'text', (col) => col.primaryKey())
84
- .addColumn('workflow_step_id', 'text', (col) => col
85
- .notNull()
86
- .references('workflow_step.workflow_step_id')
87
- .onDelete('cascade'))
88
- .addColumn('status', 'text', (col) => col.notNull())
89
- .addColumn('result', 'text')
90
- .addColumn('error', 'text')
91
- .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
92
- .addColumn('running_at', 'timestamp')
93
- .addColumn('scheduled_at', 'timestamp')
94
- .addColumn('succeeded_at', 'timestamp')
95
- .addColumn('failed_at', 'timestamp')
96
- .execute();
97
- await this.db.schema
98
- .createTable('workflow_versions')
99
- .ifNotExists()
100
- .addColumn('workflow_name', 'text', (col) => col.notNull())
101
- .addColumn('graph_hash', 'text', (col) => col.notNull())
102
- .addColumn('graph', 'text', (col) => col.notNull())
103
- .addColumn('source', 'text', (col) => col.notNull())
104
- .addColumn('status', 'text', (col) => col.notNull().defaultTo('active'))
105
- .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
106
- .addPrimaryKeyConstraint('workflow_versions_pk', [
107
- 'workflow_name',
108
- 'graph_hash',
109
- ])
110
- .execute();
33
+ await ensurePikkuSchema(this.db, workflowSchema);
111
34
  this.initialized = true;
112
35
  }
113
36
  async createRun(runId, workflowName, input, inline, graphHash, wire, options) {
@@ -1,12 +1,12 @@
1
1
  import type { SerializedError } from '@pikku/core';
2
- import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowRun, type WorkflowRunWire, type StepState, type StepStatus, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
2
+ import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowQueueOptions, type WorkflowRun, type WorkflowRunWire, type StepState, type StepStatus, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
3
3
  import type { Kysely } from 'kysely';
4
4
  import type { KyselyPikkuDB } from './kysely-tables.js';
5
5
  export declare class KyselyWorkflowService extends PikkuWorkflowService {
6
6
  protected db: Kysely<KyselyPikkuDB>;
7
7
  private initialized;
8
8
  private runService;
9
- constructor(db: Kysely<KyselyPikkuDB>);
9
+ constructor(db: Kysely<KyselyPikkuDB>, options?: WorkflowQueueOptions);
10
10
  init(): Promise<void>;
11
11
  protected createRunImpl(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
12
12
  deterministic?: boolean;
@@ -1,109 +1,21 @@
1
1
  import { PikkuWorkflowService, } from '@pikku/core/workflow';
2
- import { sql } from 'kysely';
3
2
  import { KyselyWorkflowRunService } from './kysely-workflow-run-service.js';
4
3
  import { parseJson } from './kysely-json.js';
4
+ import { ensurePikkuSchema } from './schema/index.js';
5
+ import { workflowSchema } from './schema/workflow.schema.js';
5
6
  export class KyselyWorkflowService extends PikkuWorkflowService {
6
7
  db;
7
8
  initialized = false;
8
9
  runService;
9
- constructor(db) {
10
- super();
10
+ constructor(db, options = {}) {
11
+ super(options);
11
12
  this.db = db;
12
13
  this.runService = new KyselyWorkflowRunService(db);
13
14
  }
14
15
  async init() {
15
- if (this.initialized) {
16
+ if (this.initialized)
16
17
  return;
17
- }
18
- await this.db.schema
19
- .createTable('workflow_runs')
20
- .ifNotExists()
21
- .addColumn('workflow_run_id', 'text', (col) => col
22
- .primaryKey()
23
- .defaultTo(sql `${sql.raw("'" + crypto.randomUUID() + "'")}`))
24
- .addColumn('workflow', 'text', (col) => col.notNull())
25
- .addColumn('status', 'text', (col) => col.notNull())
26
- .addColumn('input', 'text', (col) => col.notNull())
27
- .addColumn('output', 'text')
28
- .addColumn('error', 'text')
29
- .addColumn('state', 'text', (col) => col.defaultTo('{}'))
30
- .addColumn('inline', 'boolean', (col) => col.defaultTo(false))
31
- .addColumn('graph_hash', 'text')
32
- .addColumn('deterministic', 'boolean', (col) => col.defaultTo(false))
33
- .addColumn('planned_steps', 'text')
34
- .addColumn('wire', 'text')
35
- .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
36
- .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
37
- .execute();
38
- await this.db.schema
39
- .createTable('workflow_step')
40
- .ifNotExists()
41
- .addColumn('workflow_step_id', 'text', (col) => col
42
- .primaryKey()
43
- .defaultTo(sql `${sql.raw("'" + crypto.randomUUID() + "'")}`))
44
- .addColumn('workflow_run_id', 'text', (col) => col
45
- .notNull()
46
- .references('workflow_runs.workflow_run_id')
47
- .onDelete('cascade'))
48
- .addColumn('step_name', 'text', (col) => col.notNull())
49
- .addColumn('rpc_name', 'text')
50
- .addColumn('data', 'text')
51
- .addColumn('status', 'text', (col) => col.notNull().defaultTo('pending'))
52
- .addColumn('result', 'text')
53
- .addColumn('error', 'text')
54
- .addColumn('child_run_id', 'text')
55
- .addColumn('branch_taken', 'text')
56
- .addColumn('retries', 'integer')
57
- .addColumn('retry_delay', 'text')
58
- .addColumn('from_step_name', 'text')
59
- .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
60
- .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
61
- .addUniqueConstraint('workflow_step_run_name_unique', [
62
- 'workflow_run_id',
63
- 'step_name',
64
- ])
65
- .execute();
66
- // Additive: backfill the column on tables created before from_step_name.
67
- await this.db.schema
68
- .alterTable('workflow_step')
69
- .addColumn('from_step_name', 'text')
70
- .execute()
71
- .catch(() => {
72
- // Column already exists (fresh tables include it above) — nothing to do.
73
- });
74
- await this.db.schema
75
- .createTable('workflow_step_history')
76
- .ifNotExists()
77
- .addColumn('history_id', 'text', (col) => col
78
- .primaryKey()
79
- .defaultTo(sql `${sql.raw("'" + crypto.randomUUID() + "'")}`))
80
- .addColumn('workflow_step_id', 'text', (col) => col
81
- .notNull()
82
- .references('workflow_step.workflow_step_id')
83
- .onDelete('cascade'))
84
- .addColumn('status', 'text', (col) => col.notNull())
85
- .addColumn('result', 'text')
86
- .addColumn('error', 'text')
87
- .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
88
- .addColumn('running_at', 'timestamp')
89
- .addColumn('scheduled_at', 'timestamp')
90
- .addColumn('succeeded_at', 'timestamp')
91
- .addColumn('failed_at', 'timestamp')
92
- .execute();
93
- await this.db.schema
94
- .createTable('workflow_versions')
95
- .ifNotExists()
96
- .addColumn('workflow_name', 'text', (col) => col.notNull())
97
- .addColumn('graph_hash', 'text', (col) => col.notNull())
98
- .addColumn('graph', 'text', (col) => col.notNull())
99
- .addColumn('source', 'text', (col) => col.notNull())
100
- .addColumn('status', 'text', (col) => col.notNull().defaultTo('active'))
101
- .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
102
- .addPrimaryKeyConstraint('workflow_versions_pk', [
103
- 'workflow_name',
104
- 'graph_hash',
105
- ])
106
- .execute();
18
+ await ensurePikkuSchema(this.db, workflowSchema);
107
19
  this.initialized = true;
108
20
  }
109
21
  async createRunImpl(workflowName, input, inline, graphHash, wire, options) {
@@ -308,7 +220,11 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
308
220
  if (latestHistory) {
309
221
  await this.db
310
222
  .updateTable('workflowStepHistory')
311
- .set({ status: 'succeeded', result: resultJson, succeededAt: new Date() })
223
+ .set({
224
+ status: 'succeeded',
225
+ result: resultJson,
226
+ succeededAt: new Date(),
227
+ })
312
228
  .where('historyId', '=', latestHistory.historyId)
313
229
  .execute();
314
230
  }
@@ -0,0 +1,12 @@
1
+ import type { PikkuSchema } from './pikku-schema.types.js';
2
+ /**
3
+ * Threads, messages, tool calls, working memory and agent runs.
4
+ *
5
+ * `aiRun` carries `pendingApprovals` even though the two services that used to
6
+ * create this table disagreed about it: `KyselyAIStorageService` omitted the
7
+ * column while `KyselyAIRunStateService` declared it and read it back through
8
+ * casts. Whichever service happened to run first decided the shape, and on a
9
+ * database where the storage service won, resolving an approval failed. One
10
+ * declaration ends that — the column is here because live code reads it.
11
+ */
12
+ export declare const aiSchema: PikkuSchema;
@@ -0,0 +1,89 @@
1
+ import { sql } from 'kysely';
2
+ /**
3
+ * Threads, messages, tool calls, working memory and agent runs.
4
+ *
5
+ * `aiRun` carries `pendingApprovals` even though the two services that used to
6
+ * create this table disagreed about it: `KyselyAIStorageService` omitted the
7
+ * column while `KyselyAIRunStateService` declared it and read it back through
8
+ * casts. Whichever service happened to run first decided the shape, and on a
9
+ * database where the storage service won, resolving an approval failed. One
10
+ * declaration ends that — the column is here because live code reads it.
11
+ */
12
+ export const aiSchema = {
13
+ name: 'ai',
14
+ statements: [
15
+ (db) => db.schema
16
+ .createTable('aiThreads')
17
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
18
+ .addColumn('resourceId', 'varchar(255)', (col) => col.notNull())
19
+ .addColumn('title', 'text')
20
+ .addColumn('metadata', 'text')
21
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
22
+ .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
23
+ (db) => db.schema
24
+ .createIndex('idx_ai_threads_resource')
25
+ .on('aiThreads')
26
+ .column('resourceId'),
27
+ (db) => db.schema
28
+ .createTable('aiMessage')
29
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
30
+ .addColumn('threadId', 'varchar(36)', (col) => col.notNull().references('aiThreads.id').onDelete('cascade'))
31
+ .addColumn('role', 'varchar(50)', (col) => col.notNull())
32
+ .addColumn('content', 'text')
33
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
34
+ (db) => db.schema
35
+ .createIndex('idx_ai_message_thread')
36
+ .on('aiMessage')
37
+ .columns(['threadId', 'createdAt']),
38
+ (db) => db.schema
39
+ .createTable('aiToolCall')
40
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
41
+ .addColumn('threadId', 'varchar(36)', (col) => col.notNull().references('aiThreads.id').onDelete('cascade'))
42
+ .addColumn('messageId', 'varchar(36)', (col) => col.notNull().references('aiMessage.id').onDelete('cascade'))
43
+ .addColumn('runId', 'varchar(36)')
44
+ .addColumn('toolName', 'varchar(255)', (col) => col.notNull())
45
+ .addColumn('args', 'text', (col) => col.notNull())
46
+ .addColumn('result', 'text')
47
+ .addColumn('approvalStatus', 'varchar(50)')
48
+ .addColumn('approvalType', 'varchar(50)')
49
+ .addColumn('agentRunId', 'varchar(36)')
50
+ .addColumn('displayToolName', 'varchar(255)')
51
+ .addColumn('displayArgs', 'text')
52
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
53
+ (db) => db.schema
54
+ .createIndex('idx_ai_tool_call_thread')
55
+ .on('aiToolCall')
56
+ .column('threadId'),
57
+ (db) => db.schema
58
+ .createIndex('idx_ai_tool_call_message')
59
+ .on('aiToolCall')
60
+ .column('messageId'),
61
+ (db) => db.schema
62
+ .createTable('aiWorkingMemory')
63
+ .addColumn('id', 'varchar(255)', (col) => col.notNull())
64
+ .addColumn('scope', 'varchar(50)', (col) => col.notNull())
65
+ .addColumn('data', 'text', (col) => col.notNull())
66
+ .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
67
+ .addPrimaryKeyConstraint('ai_working_memory_pk', ['id', 'scope']),
68
+ (db) => db.schema
69
+ .createTable('aiRun')
70
+ .addColumn('runId', 'varchar(36)', (col) => col.primaryKey())
71
+ .addColumn('agentName', 'varchar(255)', (col) => col.notNull())
72
+ .addColumn('threadId', 'varchar(36)', (col) => col.notNull().references('aiThreads.id').onDelete('cascade'))
73
+ .addColumn('resourceId', 'varchar(255)', (col) => col.notNull())
74
+ .addColumn('status', 'varchar(50)', (col) => col.notNull().defaultTo('running'))
75
+ .addColumn('errorMessage', 'text')
76
+ .addColumn('suspendReason', 'text')
77
+ .addColumn('missingRpcs', 'text')
78
+ .addColumn('pendingApprovals', 'text')
79
+ .addColumn('usageInputTokens', 'integer', (col) => col.notNull().defaultTo(0))
80
+ .addColumn('usageOutputTokens', 'integer', (col) => col.notNull().defaultTo(0))
81
+ .addColumn('usageModel', 'varchar(255)', (col) => col.notNull().defaultTo(''))
82
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
83
+ .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
84
+ (db) => db.schema
85
+ .createIndex('idx_ai_run_thread')
86
+ .on('aiRun')
87
+ .columns(['threadId', 'createdAt']),
88
+ ],
89
+ };
@@ -0,0 +1,3 @@
1
+ import type { PikkuSchema } from './pikku-schema.types.js';
2
+ /** Channels and their topic subscriptions, also read by the event-hub store. */
3
+ export declare const channelSchema: PikkuSchema;
@@ -0,0 +1,24 @@
1
+ import { sql } from 'kysely';
2
+ /** Channels and their topic subscriptions, also read by the event-hub store. */
3
+ export const channelSchema = {
4
+ name: 'channel',
5
+ statements: [
6
+ (db) => db.schema
7
+ .createTable('channels')
8
+ .addColumn('channelId', 'text', (col) => col.primaryKey())
9
+ .addColumn('channelName', 'text', (col) => col.notNull())
10
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
11
+ .addColumn('openingData', 'text', (col) => col.notNull().defaultTo('{}'))
12
+ .addColumn('pikkuUserId', 'text')
13
+ .addColumn('state', 'text')
14
+ .addColumn('lastWire', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
15
+ (db) => db.schema
16
+ .createTable('channelSubscriptions')
17
+ .addColumn('channelId', 'text', (col) => col.notNull().references('channels.channelId').onDelete('cascade'))
18
+ .addColumn('topic', 'text', (col) => col.notNull())
19
+ .addPrimaryKeyConstraint('channel_subscriptions_pk', [
20
+ 'channelId',
21
+ 'topic',
22
+ ]),
23
+ ],
24
+ };
@@ -0,0 +1,11 @@
1
+ import { type PikkuSchema } from './pikku-schema.types.js';
2
+ /**
3
+ * Encrypted per-user credentials and their audit trail.
4
+ *
5
+ * The uniqueness rule is an expression index — `user_id` is nullable and two
6
+ * rows with a null user must still collide on name — which the schema builder
7
+ * cannot express, so it stays raw. Raw SQL is not rewritten by `withSchema`,
8
+ * which is why the table is named explicitly rather than left to the search
9
+ * path.
10
+ */
11
+ export declare const credentialSchema: PikkuSchema;
@@ -0,0 +1,33 @@
1
+ import { sql } from 'kysely';
2
+ import { rawStatement } from './pikku-schema.types.js';
3
+ /**
4
+ * Encrypted per-user credentials and their audit trail.
5
+ *
6
+ * The uniqueness rule is an expression index — `user_id` is nullable and two
7
+ * rows with a null user must still collide on name — which the schema builder
8
+ * cannot express, so it stays raw. Raw SQL is not rewritten by `withSchema`,
9
+ * which is why the table is named explicitly rather than left to the search
10
+ * path.
11
+ */
12
+ export const credentialSchema = {
13
+ name: 'credential',
14
+ statements: [
15
+ (db) => db.schema
16
+ .createTable('credentials')
17
+ .addColumn('name', 'varchar(255)', (col) => col.notNull())
18
+ .addColumn('userId', 'varchar(255)')
19
+ .addColumn('ciphertext', 'text', (col) => col.notNull())
20
+ .addColumn('wrappedDek', 'text', (col) => col.notNull())
21
+ .addColumn('keyVersion', 'integer', (col) => col.notNull())
22
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
23
+ .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
24
+ rawStatement(sql `CREATE UNIQUE INDEX credentials_name_user_id_unique ON credentials (name, COALESCE(user_id, ''))`),
25
+ (db) => db.schema
26
+ .createTable('credentialsAudit')
27
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
28
+ .addColumn('credentialName', 'varchar(255)', (col) => col.notNull())
29
+ .addColumn('userId', 'varchar(255)')
30
+ .addColumn('action', 'varchar(20)', (col) => col.notNull())
31
+ .addColumn('performedAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
32
+ ],
33
+ };
@@ -0,0 +1,3 @@
1
+ import type { PikkuSchema } from './pikku-schema.types.js';
2
+ /** Registered deployments and the functions each one serves. */
3
+ export declare const deploymentSchema: PikkuSchema;
@@ -0,0 +1,32 @@
1
+ import { sql } from 'kysely';
2
+ /** Registered deployments and the functions each one serves. */
3
+ export const deploymentSchema = {
4
+ name: 'deployment',
5
+ statements: [
6
+ (db) => db.schema
7
+ .createTable('pikkuDeployments')
8
+ .addColumn('deploymentId', 'text', (col) => col.primaryKey())
9
+ .addColumn('endpoint', 'text', (col) => col.notNull())
10
+ .addColumn('lastHeartbeat', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
11
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull()),
12
+ (db) => db.schema
13
+ .createTable('pikkuDeploymentFunctions')
14
+ .addColumn('deploymentId', 'text', (col) => col
15
+ .notNull()
16
+ .references('pikkuDeployments.deploymentId')
17
+ .onDelete('cascade'))
18
+ .addColumn('functionName', 'text', (col) => col.notNull())
19
+ .addPrimaryKeyConstraint('pikku_deployment_functions_pk', [
20
+ 'deploymentId',
21
+ 'functionName',
22
+ ]),
23
+ (db) => db.schema
24
+ .createIndex('idx_pikku_deployments_heartbeat')
25
+ .on('pikkuDeployments')
26
+ .column('lastHeartbeat'),
27
+ (db) => db.schema
28
+ .createIndex('idx_pikku_deployment_functions_name')
29
+ .on('pikkuDeploymentFunctions')
30
+ .column('functionName'),
31
+ ],
32
+ };
@@ -0,0 +1,82 @@
1
+ import { type Kysely } from 'kysely';
2
+ import type { PikkuSchema, RequiredTypes, SchemaRequirement } from './pikku-schema.types.js';
3
+ export type { PikkuSchema, RequiredTypes, SchemaRequirement, SchemaStatement, SchemaStatementFactory, } from './pikku-schema.types.js';
4
+ export { rawStatement, requiredType } from './pikku-schema.types.js';
5
+ /**
6
+ * Every table the pikku runtime needs, in dependency order.
7
+ *
8
+ * Order is load-bearing, not cosmetic: `scope` has foreign keys onto Better
9
+ * Auth's `user`, and the tables within each schema reference each other. Sort
10
+ * this list alphabetically and it stops applying.
11
+ */
12
+ export declare const pikkuSchemas: PikkuSchema[];
13
+ export interface UnmetRequirement {
14
+ schema: PikkuSchema;
15
+ requirement: SchemaRequirement;
16
+ }
17
+ /**
18
+ * Look up every prerequisite in `db`, reporting the gaps and the types found.
19
+ *
20
+ * Both halves come from one introspection because they are one question. The
21
+ * gaps say whether a schema can be applied at all; the types say how — a
22
+ * foreign key onto a `uuid` primary key has to be declared `uuid`, and guessing
23
+ * `text` produces DDL the database refuses.
24
+ *
25
+ * Introspection returns physical names, so `requires` is written in physical
26
+ * names too — this is the one place in the declaration that is not camelCase.
27
+ */
28
+ export declare const resolveRequirements: (db: Kysely<any>, schemas?: PikkuSchema[]) => Promise<{
29
+ types: RequiredTypes;
30
+ unmet: UnmetRequirement[];
31
+ }>;
32
+ /**
33
+ * The prerequisites `db` does not satisfy, as a list rather than a throw.
34
+ *
35
+ * Separate from `applyPikkuSchemas` because the two callers want opposite
36
+ * things from the same answer: a service booting cannot proceed without its
37
+ * tables, while a tool asking what a project's schema *would* be needs to
38
+ * describe the gap rather than die on it.
39
+ */
40
+ export declare const unsatisfiedRequirements: (db: Kysely<any>, schemas?: PikkuSchema[]) => Promise<UnmetRequirement[]>;
41
+ /**
42
+ * Create the tables, in order, on a database that has whatever they require.
43
+ *
44
+ * Prerequisites are checked before anything is created, so a project missing
45
+ * one is told which schema wanted what — rather than being handed a foreign key
46
+ * error, or a half-applied database to clean up.
47
+ *
48
+ * The statements then run in one transaction, so the same promise holds for a
49
+ * failure the check cannot foresee: a statement that dies mid-list takes the
50
+ * ones before it with it. On an engine without transactional DDL the rollback
51
+ * is the engine's to give, not ours.
52
+ */
53
+ export declare const applyPikkuSchemas: (db: Kysely<any>, schemas?: PikkuSchema[]) => Promise<void>;
54
+ /** What `ensurePikkuSchema` found when it looked. */
55
+ export type EnsureOutcome = 'present' | 'created';
56
+ /**
57
+ * Make sure one schema's tables are there, creating them only if none are.
58
+ *
59
+ * What a service calls at boot. It looks before it creates, which is the whole
60
+ * difference from the `.ifNotExists()` DDL this replaces: that spelling turned
61
+ * every failure into a silent no-op, and hid a foreign key onto `user.id` that
62
+ * postgres had been rejecting since the day it was written.
63
+ *
64
+ * A partially-present schema is refused rather than completed. Half a schema
65
+ * means something else already applied part of it — a migration, an older
66
+ * version, a hand-run script — and creating the remainder at boot would leave
67
+ * two authorities over one set of tables, which is the condition all of this
68
+ * exists to end.
69
+ *
70
+ * Creating at boot is the fallback, not the intent. `pikku db generate` writes
71
+ * the declaration down as a migration; a project that has done so takes the
72
+ * `present` path and this never issues DDL at all.
73
+ */
74
+ export declare const ensurePikkuSchema: (db: Kysely<any>, schema: PikkuSchema) => Promise<EnsureOutcome>;
75
+ /**
76
+ * Render the declaration as SQL for the dialect `db` was built with.
77
+ *
78
+ * `types` comes from `resolveRequirements` against the database the migration
79
+ * is being written for. Omit it and foreign keys onto another source's columns
80
+ * compile as `text`, which is a shape, not a migration.
81
+ */
82
+ export declare const compilePikkuSchemas: (db: Kysely<any>, schemas?: PikkuSchema[], types?: RequiredTypes) => string;