@pikku/kysely 0.13.3 → 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 (70) hide show
  1. package/CHANGELOG.md +27 -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.js +9 -93
  20. package/dist/src/schema/ai.schema.d.ts +12 -0
  21. package/dist/src/schema/ai.schema.js +89 -0
  22. package/dist/src/schema/channel.schema.d.ts +3 -0
  23. package/dist/src/schema/channel.schema.js +24 -0
  24. package/dist/src/schema/credential.schema.d.ts +11 -0
  25. package/dist/src/schema/credential.schema.js +33 -0
  26. package/dist/src/schema/deployment.schema.d.ts +3 -0
  27. package/dist/src/schema/deployment.schema.js +32 -0
  28. package/dist/src/schema/index.d.ts +82 -0
  29. package/dist/src/schema/index.js +187 -0
  30. package/dist/src/schema/pikku-schema.types.d.ts +75 -0
  31. package/dist/src/schema/pikku-schema.types.js +21 -0
  32. package/dist/src/schema/scope.schema.d.ts +18 -0
  33. package/dist/src/schema/scope.schema.js +61 -0
  34. package/dist/src/schema/secret.schema.d.ts +11 -0
  35. package/dist/src/schema/secret.schema.js +29 -0
  36. package/dist/src/schema/session.schema.d.ts +3 -0
  37. package/dist/src/schema/session.schema.js +13 -0
  38. package/dist/src/schema/webhook.schema.d.ts +3 -0
  39. package/dist/src/schema/webhook.schema.js +38 -0
  40. package/dist/src/schema/workflow.schema.d.ts +15 -0
  41. package/dist/src/schema/workflow.schema.js +86 -0
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/src/index.ts +18 -0
  45. package/src/kysely-ai-run-state-service.ts +7 -28
  46. package/src/kysely-ai-storage-service.ts +4 -152
  47. package/src/kysely-channel-store.ts +4 -34
  48. package/src/kysely-credential-service.ts +3 -36
  49. package/src/kysely-deployment-service.ts +4 -63
  50. package/src/kysely-scope-service.ts +4 -81
  51. package/src/kysely-secret-service.ts +6 -33
  52. package/src/kysely-session-store.ts +4 -18
  53. package/src/kysely-tables.test.ts +110 -0
  54. package/src/kysely-tables.ts +2 -0
  55. package/src/kysely-webhook-service.ts +4 -67
  56. package/src/kysely-workflow-mirror.test.ts +7 -7
  57. package/src/kysely-workflow-mirror.ts +10 -109
  58. package/src/kysely-workflow-service.ts +12 -123
  59. package/src/schema/ai.schema.ts +140 -0
  60. package/src/schema/channel.schema.ts +37 -0
  61. package/src/schema/credential.schema.ts +46 -0
  62. package/src/schema/deployment.schema.ts +47 -0
  63. package/src/schema/index.ts +249 -0
  64. package/src/schema/pikku-schema.types.ts +94 -0
  65. package/src/schema/schema.test.ts +273 -0
  66. package/src/schema/scope.schema.ts +95 -0
  67. package/src/schema/secret.schema.ts +40 -0
  68. package/src/schema/session.schema.ts +20 -0
  69. package/src/schema/webhook.schema.ts +57 -0
  70. package/src/schema/workflow.schema.ts +113 -0
@@ -3,6 +3,8 @@ import type { Kysely } from 'kysely'
3
3
  import type { KyselyPikkuDB } from './kysely-tables.js'
4
4
  import type { AIRunStateService, CreateRunInput } from '@pikku/core/services'
5
5
  import type { AgentRunState, PendingApproval } from '@pikku/core/ai-agent'
6
+ import { ensurePikkuSchema } from './schema/index.js'
7
+ import { aiSchema } from './schema/ai.schema.js'
6
8
 
7
9
  export class KyselyAIRunStateService implements AIRunStateService {
8
10
  private initialized = false
@@ -11,30 +13,7 @@ export class KyselyAIRunStateService implements AIRunStateService {
11
13
 
12
14
  async init(): Promise<void> {
13
15
  if (this.initialized) return
14
-
15
- await this.db.schema
16
- .createTable('aiRun')
17
- .ifNotExists()
18
- .addColumn('runId', 'text', (col) => col.primaryKey())
19
- .addColumn('agentName', 'text', (col) => col.notNull())
20
- .addColumn('threadId', 'text', (col) => col.notNull())
21
- .addColumn('resourceId', 'text', (col) => col.notNull())
22
- .addColumn('status', 'text', (col) => col.defaultTo('running').notNull())
23
- .addColumn('errorMessage', 'text')
24
- .addColumn('suspendReason', 'text')
25
- .addColumn('missingRpcs', 'text')
26
- .addColumn('pendingApprovals', 'text')
27
- .addColumn('usageInputTokens', 'integer', (col) => col.defaultTo(0))
28
- .addColumn('usageOutputTokens', 'integer', (col) => col.defaultTo(0))
29
- .addColumn('usageModel', 'text', (col) => col.defaultTo(''))
30
- .addColumn('createdAt', 'timestamp', (col) =>
31
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
32
- )
33
- .addColumn('updatedAt', 'timestamp', (col) =>
34
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
35
- )
36
- .execute()
37
-
16
+ await ensurePikkuSchema(this.db, aiSchema)
38
17
  this.initialized = true
39
18
  }
40
19
 
@@ -113,7 +92,7 @@ export class KyselyAIRunStateService implements AIRunStateService {
113
92
  ): Promise<void> {
114
93
  const rows = await this.db
115
94
  .selectFrom('aiRun')
116
- .select(['runId', 'pendingApprovals' as any])
95
+ .select(['runId', 'pendingApprovals'])
117
96
  .where('status', '=', 'suspended')
118
97
  .execute()
119
98
 
@@ -121,7 +100,7 @@ export class KyselyAIRunStateService implements AIRunStateService {
121
100
  let approvals: PendingApproval[] = []
122
101
  if (row.pendingApprovals) {
123
102
  try {
124
- approvals = JSON.parse(row.pendingApprovals as string)
103
+ approvals = JSON.parse(row.pendingApprovals)
125
104
  } catch {
126
105
  console.warn(
127
106
  `Failed to parse pendingApprovals for run ${row.runId}, treating as empty`
@@ -159,9 +138,9 @@ export class KyselyAIRunStateService implements AIRunStateService {
159
138
 
160
139
  for (const row of rows) {
161
140
  let approvals: PendingApproval[] = []
162
- if ((row as any).pendingApprovals) {
141
+ if (row.pendingApprovals) {
163
142
  try {
164
- approvals = JSON.parse((row as any).pendingApprovals)
143
+ approvals = JSON.parse(row.pendingApprovals)
165
144
  } catch {
166
145
  console.warn(
167
146
  `Failed to parse pendingApprovals for run ${row.runId}, treating as empty`
@@ -5,9 +5,10 @@ import type {
5
5
  } from '@pikku/core/services'
6
6
  import type { AIThread, AIMessage, AgentRunState } from '@pikku/core/ai-agent'
7
7
  import type { Kysely } from 'kysely'
8
- import { sql } from 'kysely'
9
8
  import type { KyselyPikkuDB } from './kysely-tables.js'
10
9
  import { parseJson } from './kysely-json.js'
10
+ import { ensurePikkuSchema } from './schema/index.js'
11
+ import { aiSchema } from './schema/ai.schema.js'
11
12
 
12
13
  export class KyselyAIStorageService
13
14
  implements AIStorageService, AIRunStateService
@@ -16,158 +17,9 @@ export class KyselyAIStorageService
16
17
 
17
18
  constructor(private db: Kysely<KyselyPikkuDB>) {}
18
19
 
19
- private async createIndexSafe(builder: {
20
- execute(): Promise<void>
21
- }): Promise<void> {
22
- try {
23
- await builder.execute()
24
- } catch (e: any) {
25
- // Ignore "index already exists" errors across databases
26
- // MySQL: ER_DUP_KEYNAME, Postgres: 42P07, SQLite: "already exists"
27
- if (e?.code === 'ER_DUP_KEYNAME' || e?.errno === 1061) return
28
- if (e?.code === '42P07') return
29
- if (e?.message?.includes('already exists')) return
30
- throw e
31
- }
32
- }
33
-
34
20
  public async init(): Promise<void> {
35
- if (this.initialized) {
36
- return
37
- }
38
-
39
- await this.db.schema
40
- .createTable('ai_threads')
41
- .ifNotExists()
42
- .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
43
- .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
44
- .addColumn('title', 'text')
45
- .addColumn('metadata', 'text')
46
- .addColumn('created_at', 'timestamp', (col) =>
47
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
48
- )
49
- .addColumn('updated_at', 'timestamp', (col) =>
50
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
51
- )
52
- .execute()
53
-
54
- await this.createIndexSafe(
55
- this.db.schema
56
- .createIndex('idx_ai_threads_resource')
57
- .on('ai_threads')
58
- .column('resource_id')
59
- )
60
-
61
- await this.db.schema
62
- .createTable('ai_message')
63
- .ifNotExists()
64
- .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
65
- .addColumn('thread_id', 'varchar(36)', (col) =>
66
- col.notNull().references('ai_threads.id').onDelete('cascade')
67
- )
68
- .addColumn('role', 'varchar(50)', (col) => col.notNull())
69
- .addColumn('content', 'text')
70
- .addColumn('created_at', 'timestamp', (col) =>
71
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
72
- )
73
- .execute()
74
-
75
- await this.createIndexSafe(
76
- this.db.schema
77
- .createIndex('idx_ai_message_thread')
78
- .on('ai_message')
79
- .columns(['thread_id', 'created_at'])
80
- )
81
-
82
- await this.db.schema
83
- .createTable('ai_tool_call')
84
- .ifNotExists()
85
- .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
86
- .addColumn('thread_id', 'varchar(36)', (col) =>
87
- col.notNull().references('ai_threads.id').onDelete('cascade')
88
- )
89
- .addColumn('message_id', 'varchar(36)', (col) =>
90
- col.notNull().references('ai_message.id').onDelete('cascade')
91
- )
92
- .addColumn('run_id', 'varchar(36)')
93
- .addColumn('tool_name', 'varchar(255)', (col) => col.notNull())
94
- .addColumn('args', 'text', (col) => col.notNull())
95
- .addColumn('result', 'text')
96
- .addColumn('approval_status', 'varchar(50)')
97
- .addColumn('approval_type', 'varchar(50)')
98
- .addColumn('agent_run_id', 'varchar(36)')
99
- .addColumn('display_tool_name', 'varchar(255)')
100
- .addColumn('display_args', 'text')
101
- .addColumn('created_at', 'timestamp', (col) =>
102
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
103
- )
104
- .execute()
105
-
106
- await this.createIndexSafe(
107
- this.db.schema
108
- .createIndex('idx_ai_tool_call_thread')
109
- .on('ai_tool_call')
110
- .column('thread_id')
111
- )
112
-
113
- await this.createIndexSafe(
114
- this.db.schema
115
- .createIndex('idx_ai_tool_call_message')
116
- .on('ai_tool_call')
117
- .column('message_id')
118
- )
119
-
120
- await this.db.schema
121
- .createTable('ai_working_memory')
122
- .ifNotExists()
123
- .addColumn('id', 'varchar(255)', (col) => col.notNull())
124
- .addColumn('scope', 'varchar(50)', (col) => col.notNull())
125
- .addColumn('data', 'text', (col) => col.notNull())
126
- .addColumn('updated_at', 'timestamp', (col) =>
127
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
128
- )
129
- .addPrimaryKeyConstraint('ai_working_memory_pk', ['id', 'scope'])
130
- .execute()
131
-
132
- await this.db.schema
133
- .createTable('ai_run')
134
- .ifNotExists()
135
- .addColumn('run_id', 'varchar(36)', (col) => col.primaryKey())
136
- .addColumn('agent_name', 'varchar(255)', (col) => col.notNull())
137
- .addColumn('thread_id', 'varchar(36)', (col) =>
138
- col.notNull().references('ai_threads.id').onDelete('cascade')
139
- )
140
- .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
141
- .addColumn('status', 'varchar(50)', (col) =>
142
- col.notNull().defaultTo('running')
143
- )
144
- .addColumn('error_message', 'text')
145
- .addColumn('suspend_reason', 'text')
146
- .addColumn('missing_rpcs', 'text')
147
- .addColumn('usage_input_tokens', 'integer', (col) =>
148
- col.notNull().defaultTo(0)
149
- )
150
- .addColumn('usage_output_tokens', 'integer', (col) =>
151
- col.notNull().defaultTo(0)
152
- )
153
- .addColumn('usage_model', 'varchar(255)', (col) =>
154
- col.notNull().defaultTo('')
155
- )
156
- .addColumn('created_at', 'timestamp', (col) =>
157
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
158
- )
159
- .addColumn('updated_at', 'timestamp', (col) =>
160
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
161
- )
162
- .execute()
163
-
164
- await this.createIndexSafe(
165
- this.db.schema
166
- .createIndex('idx_ai_run_thread')
167
- .on('ai_run')
168
- .columns(['thread_id', 'created_at'])
169
- )
170
-
21
+ if (this.initialized) return
22
+ await ensurePikkuSchema(this.db, aiSchema)
171
23
  this.initialized = true
172
24
  }
173
25
 
@@ -1,9 +1,10 @@
1
1
  import type { Channel } from '@pikku/core/channel'
2
2
  import { ChannelStore } from '@pikku/core/channel'
3
3
  import type { Kysely } from 'kysely'
4
- import { sql } from 'kysely'
5
4
  import type { KyselyPikkuDB } from './kysely-tables.js'
6
5
  import { parseJson } from './kysely-json.js'
6
+ import { ensurePikkuSchema } from './schema/index.js'
7
+ import { channelSchema } from './schema/channel.schema.js'
7
8
 
8
9
  export class KyselyChannelStore extends ChannelStore {
9
10
  private initialized = false
@@ -13,39 +14,8 @@ export class KyselyChannelStore extends ChannelStore {
13
14
  }
14
15
 
15
16
  public async init(): Promise<void> {
16
- if (this.initialized) {
17
- return
18
- }
19
-
20
- await this.db.schema
21
- .createTable('channels')
22
- .ifNotExists()
23
- .addColumn('channel_id', 'text', (col) => col.primaryKey())
24
- .addColumn('channel_name', 'text', (col) => col.notNull())
25
- .addColumn('created_at', 'timestamp', (col) =>
26
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
27
- )
28
- .addColumn('opening_data', 'text', (col) => col.notNull().defaultTo('{}'))
29
- .addColumn('pikku_user_id', 'text')
30
- .addColumn('state', 'text')
31
- .addColumn('last_wire', 'timestamp', (col) =>
32
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
33
- )
34
- .execute()
35
-
36
- await this.db.schema
37
- .createTable('channel_subscriptions')
38
- .ifNotExists()
39
- .addColumn('channel_id', 'text', (col) =>
40
- col.notNull().references('channels.channel_id').onDelete('cascade')
41
- )
42
- .addColumn('topic', 'text', (col) => col.notNull())
43
- .addPrimaryKeyConstraint('channel_subscriptions_pk', [
44
- 'channel_id',
45
- 'topic',
46
- ])
47
- .execute()
48
-
17
+ if (this.initialized) return
18
+ await ensurePikkuSchema(this.db, channelSchema)
49
19
  this.initialized = true
50
20
  }
51
21
 
@@ -1,12 +1,13 @@
1
1
  import type { CredentialService } from '@pikku/core/services'
2
2
  import type { Kysely } from 'kysely'
3
- import { sql } from 'kysely'
4
3
  import type { KyselyPikkuDB } from './kysely-tables.js'
5
4
  import {
6
5
  envelopeEncrypt,
7
6
  envelopeDecrypt,
8
7
  envelopeRewrap,
9
8
  } from '@pikku/core/crypto-utils'
9
+ import { ensurePikkuSchema } from './schema/index.js'
10
+ import { credentialSchema } from './schema/credential.schema.js'
10
11
 
11
12
  export interface KyselyCredentialServiceConfig {
12
13
  key: string
@@ -37,41 +38,7 @@ export class KyselyCredentialService implements CredentialService {
37
38
 
38
39
  public async init(): Promise<void> {
39
40
  if (this.initialized) return
40
-
41
- await this.db.schema
42
- .createTable('credentials')
43
- .ifNotExists()
44
- .addColumn('name', 'varchar(255)', (col) => col.notNull())
45
- .addColumn('user_id', 'varchar(255)')
46
- .addColumn('ciphertext', 'text', (col) => col.notNull())
47
- .addColumn('wrapped_dek', 'text', (col) => col.notNull())
48
- .addColumn('key_version', 'integer', (col) => col.notNull())
49
- .addColumn('created_at', 'timestamp', (col) =>
50
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
51
- )
52
- .addColumn('updated_at', 'timestamp', (col) =>
53
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
54
- )
55
- .execute()
56
-
57
- await sql`CREATE UNIQUE INDEX IF NOT EXISTS credentials_name_user_id_unique ON credentials (name, COALESCE(user_id, ''))`.execute(
58
- this.db
59
- )
60
-
61
- if (this.audit) {
62
- await this.db.schema
63
- .createTable('credentials_audit')
64
- .ifNotExists()
65
- .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
66
- .addColumn('credential_name', 'varchar(255)', (col) => col.notNull())
67
- .addColumn('user_id', 'varchar(255)')
68
- .addColumn('action', 'varchar(20)', (col) => col.notNull())
69
- .addColumn('performed_at', 'timestamp', (col) =>
70
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
71
- )
72
- .execute()
73
- }
74
-
41
+ await ensurePikkuSchema(this.db, credentialSchema)
75
42
  this.initialized = true
76
43
  }
77
44
 
@@ -7,8 +7,9 @@ import type {
7
7
  import type { JWTService, SecretService } from '@pikku/core/services'
8
8
  import { getAllFunctionNames } from '@pikku/core/function'
9
9
  import type { Kysely } from 'kysely'
10
- import { sql } from 'kysely'
11
10
  import type { KyselyPikkuDB } from './kysely-tables.js'
11
+ import { ensurePikkuSchema } from './schema/index.js'
12
+ import { deploymentSchema } from './schema/deployment.schema.js'
12
13
 
13
14
  export class KyselyDeploymentService implements DeploymentService {
14
15
  private initialized = false
@@ -28,55 +29,8 @@ export class KyselyDeploymentService implements DeploymentService {
28
29
  }
29
30
 
30
31
  public async init(): Promise<void> {
31
- if (this.initialized) {
32
- return
33
- }
34
-
35
- await this.db.schema
36
- .createTable('pikku_deployments')
37
- .ifNotExists()
38
- .addColumn('deployment_id', 'text', (col) => col.primaryKey())
39
- .addColumn('endpoint', 'text', (col) => col.notNull())
40
- .addColumn('last_heartbeat', 'timestamp', (col) =>
41
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
42
- )
43
- .addColumn('created_at', 'timestamp', (col) =>
44
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
45
- )
46
- .execute()
47
-
48
- await this.db.schema
49
- .createTable('pikku_deployment_functions')
50
- .ifNotExists()
51
- .addColumn('deployment_id', 'text', (col) =>
52
- col
53
- .notNull()
54
- .references('pikku_deployments.deployment_id')
55
- .onDelete('cascade')
56
- )
57
- .addColumn('function_name', 'text', (col) => col.notNull())
58
- .addPrimaryKeyConstraint('pikku_deployment_functions_pk', [
59
- 'deployment_id',
60
- 'function_name',
61
- ])
62
- .execute()
63
-
64
- await this.createIndexSafe(
65
- this.db.schema
66
- .createIndex('idx_pikku_deployments_heartbeat')
67
- .ifNotExists()
68
- .on('pikku_deployments')
69
- .column('last_heartbeat')
70
- )
71
-
72
- await this.createIndexSafe(
73
- this.db.schema
74
- .createIndex('idx_pikku_deployment_functions_name')
75
- .ifNotExists()
76
- .on('pikku_deployment_functions')
77
- .column('function_name')
78
- )
79
-
32
+ if (this.initialized) return
33
+ await ensurePikkuSchema(this.db, deploymentSchema)
80
34
  this.initialized = true
81
35
  }
82
36
 
@@ -192,19 +146,6 @@ export class KyselyDeploymentService implements DeploymentService {
192
146
  return response.json()
193
147
  }
194
148
 
195
- private async createIndexSafe(builder: {
196
- execute(): Promise<void>
197
- }): Promise<void> {
198
- try {
199
- await builder.execute()
200
- } catch (e: any) {
201
- if (e?.code === 'ER_DUP_KEYNAME' || e?.errno === 1061) return
202
- if (e?.code === '42P07') return
203
- if (e?.message?.includes('already exists')) return
204
- throw e
205
- }
206
- }
207
-
208
149
  private async sendHeartbeat(): Promise<void> {
209
150
  if (!this.deploymentConfig) return
210
151
 
@@ -1,8 +1,9 @@
1
1
  import type { Role, ScopeService } from '@pikku/core/services'
2
2
  import type { FlatScope } from '@pikku/core/scope'
3
3
  import type { Kysely } from 'kysely'
4
- import { sql } from 'kysely'
5
4
  import type { KyselyPikkuDB } from './kysely-tables.js'
5
+ import { ensurePikkuSchema } from './schema/index.js'
6
+ import { scopeSchema } from './schema/scope.schema.js'
6
7
 
7
8
  /**
8
9
  * Resolves and administers user scopes against four self-created tables.
@@ -22,86 +23,8 @@ export class KyselyScopeService implements ScopeService {
22
23
  constructor(private db: Kysely<KyselyPikkuDB>) {}
23
24
 
24
25
  public async init(): Promise<void> {
25
- if (this.initialized) {
26
- return
27
- }
28
-
29
- await this.db.schema
30
- .createTable('pikku_scopes')
31
- .ifNotExists()
32
- .addColumn('name', 'text', (col) => col.primaryKey())
33
- .addColumn('description', 'text')
34
- .addColumn('declared', 'boolean', (col) => col.defaultTo(true).notNull())
35
- .execute()
36
-
37
- await this.db.schema
38
- .createTable('pikku_roles')
39
- .ifNotExists()
40
- .addColumn('name', 'text', (col) => col.primaryKey())
41
- .addColumn('description', 'text')
42
- .addColumn('created_at', 'timestamp', (col) =>
43
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
44
- )
45
- .execute()
46
-
47
- await this.db.schema
48
- .createTable('pikku_role_scopes')
49
- .ifNotExists()
50
- .addColumn('role', 'text', (col) =>
51
- col.notNull().references('pikku_roles.name').onDelete('cascade')
52
- )
53
- .addColumn('scope', 'text', (col) =>
54
- col.notNull().references('pikku_scopes.name').onDelete('cascade')
55
- )
56
- .addPrimaryKeyConstraint('pikku_role_scopes_pk', ['role', 'scope'])
57
- .execute()
58
-
59
- await this.db.schema
60
- .createTable('pikku_user_role')
61
- .ifNotExists()
62
- .addColumn('user_id', 'text', (col) =>
63
- col.notNull().references('user.id').onDelete('cascade')
64
- )
65
- .addColumn('role', 'text', (col) =>
66
- col.notNull().references('pikku_roles.name').onDelete('cascade')
67
- )
68
- .addColumn('granted_by', 'text')
69
- .addColumn('granted_at', 'timestamp', (col) =>
70
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
71
- )
72
- .addPrimaryKeyConstraint('pikku_user_role_pk', ['user_id', 'role'])
73
- .execute()
74
-
75
- await this.db.schema
76
- .createTable('pikku_user_scope')
77
- .ifNotExists()
78
- .addColumn('user_id', 'text', (col) =>
79
- col.notNull().references('user.id').onDelete('cascade')
80
- )
81
- .addColumn('scope', 'text', (col) =>
82
- col.notNull().references('pikku_scopes.name').onDelete('cascade')
83
- )
84
- .addColumn('granted_by', 'text')
85
- .addColumn('granted_at', 'timestamp', (col) =>
86
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
87
- )
88
- .addPrimaryKeyConstraint('pikku_user_scope_pk', ['user_id', 'scope'])
89
- .execute()
90
-
91
- await this.db.schema
92
- .createIndex('pikku_role_scopes_scope_idx')
93
- .ifNotExists()
94
- .on('pikku_role_scopes')
95
- .column('scope')
96
- .execute()
97
-
98
- await this.db.schema
99
- .createIndex('pikku_user_role_role_idx')
100
- .ifNotExists()
101
- .on('pikku_user_role')
102
- .column('role')
103
- .execute()
104
-
26
+ if (this.initialized) return
27
+ await ensurePikkuSchema(this.db, scopeSchema)
105
28
  this.initialized = true
106
29
  }
107
30
 
@@ -1,12 +1,13 @@
1
1
  import type { SecretService } from '@pikku/core/services'
2
2
  import type { Kysely } from 'kysely'
3
- import { sql } from 'kysely'
4
3
  import type { KyselyPikkuDB } from './kysely-tables.js'
5
4
  import {
6
5
  envelopeEncrypt,
7
6
  envelopeDecrypt,
8
7
  envelopeRewrap,
9
8
  } from '@pikku/core/crypto-utils'
9
+ import { ensurePikkuSchema } from './schema/index.js'
10
+ import { secretSchema } from './schema/secret.schema.js'
10
11
 
11
12
  export interface KyselySecretServiceConfig {
12
13
  key: string
@@ -37,35 +38,7 @@ export class KyselySecretService implements SecretService {
37
38
 
38
39
  public async init(): Promise<void> {
39
40
  if (this.initialized) return
40
-
41
- await this.db.schema
42
- .createTable('secrets')
43
- .ifNotExists()
44
- .addColumn('key', 'varchar(255)', (col) => col.primaryKey())
45
- .addColumn('ciphertext', 'text', (col) => col.notNull())
46
- .addColumn('wrapped_dek', 'text', (col) => col.notNull())
47
- .addColumn('key_version', 'integer', (col) => col.notNull())
48
- .addColumn('created_at', 'timestamp', (col) =>
49
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
50
- )
51
- .addColumn('updated_at', 'timestamp', (col) =>
52
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
53
- )
54
- .execute()
55
-
56
- if (this.audit) {
57
- await this.db.schema
58
- .createTable('secrets_audit')
59
- .ifNotExists()
60
- .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
61
- .addColumn('secret_key', 'varchar(255)', (col) => col.notNull())
62
- .addColumn('action', 'varchar(20)', (col) => col.notNull())
63
- .addColumn('performed_at', 'timestamp', (col) =>
64
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
65
- )
66
- .execute()
67
- }
68
-
41
+ await ensurePikkuSchema(this.db, secretSchema)
69
42
  this.initialized = true
70
43
  }
71
44
 
@@ -149,9 +122,9 @@ export class KyselySecretService implements SecretService {
149
122
  await this.logAudit(key, 'delete')
150
123
  }
151
124
 
152
- async getSecrets<
153
- T extends Record<string, unknown> = Record<string, unknown>,
154
- >(keys: (keyof T & string)[]): Promise<T> {
125
+ async getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(
126
+ keys: (keyof T & string)[]
127
+ ): Promise<T> {
155
128
  const rows = await this.db
156
129
  .selectFrom('secrets')
157
130
  .select(['key', 'ciphertext', 'wrappedDek', 'keyVersion'])
@@ -1,9 +1,10 @@
1
1
  import type { CoreUserSession } from '@pikku/core'
2
2
  import type { SessionStore } from '@pikku/core/services'
3
3
  import type { Kysely } from 'kysely'
4
- import { sql } from 'kysely'
5
4
  import type { KyselyPikkuDB } from './kysely-tables.js'
6
5
  import { parseJson } from './kysely-json.js'
6
+ import { ensurePikkuSchema } from './schema/index.js'
7
+ import { sessionSchema } from './schema/session.schema.js'
7
8
 
8
9
  export class KyselySessionStore implements SessionStore {
9
10
  private initialized = false
@@ -11,23 +12,8 @@ export class KyselySessionStore implements SessionStore {
11
12
  constructor(private db: Kysely<KyselyPikkuDB>) {}
12
13
 
13
14
  public async init(): Promise<void> {
14
- if (this.initialized) {
15
- return
16
- }
17
-
18
- await this.db.schema
19
- .createTable('pikku_user_sessions')
20
- .ifNotExists()
21
- .addColumn('pikku_user_id', 'text', (col) => col.primaryKey())
22
- .addColumn('session', 'text', (col) => col.notNull())
23
- .addColumn('created_at', 'timestamp', (col) =>
24
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
25
- )
26
- .addColumn('updated_at', 'timestamp', (col) =>
27
- col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
28
- )
29
- .execute()
30
-
15
+ if (this.initialized) return
16
+ await ensurePikkuSchema(this.db, sessionSchema)
31
17
  this.initialized = true
32
18
  }
33
19