@pikku/kysely 0.13.24 → 0.13.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +235 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +3 -1
- package/dist/src/kysely-analytics-service.d.ts +30 -0
- package/dist/src/kysely-analytics-service.js +80 -0
- package/dist/src/kysely-feature-flag-store.d.ts +37 -0
- package/dist/src/kysely-feature-flag-store.js +239 -0
- package/dist/src/kysely-tables.d.ts +45 -0
- package/dist/src/schema/analytics.schema.d.ts +17 -0
- package/dist/src/schema/analytics.schema.js +48 -0
- package/dist/src/schema/flag.schema.d.ts +16 -0
- package/dist/src/schema/flag.schema.js +62 -0
- package/dist/src/schema/index.d.ts +2 -0
- package/dist/src/schema/index.js +6 -0
- package/package.json +11 -6
- package/run-tests.sh +9 -1
- package/bin/pikku-kysely-pure.ts +0 -59
- package/dist/src/kysely-lock-vault.d.ts +0 -34
- package/dist/src/kysely-lock-vault.js +0 -77
- package/dist/src/schema/data-lock.schema.d.ts +0 -15
- package/dist/src/schema/data-lock.schema.js +0 -27
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/classification-crypto.test.ts +0 -143
- package/src/classification-crypto.ts +0 -163
- package/src/classification-plugin.pglite.test.ts +0 -226
- package/src/classification-plugin.test.ts +0 -265
- package/src/classification-plugin.ts +0 -249
- package/src/coercion-plugin.test.ts +0 -160
- package/src/coercion-plugin.ts +0 -187
- package/src/create-audited-kysely.test.ts +0 -560
- package/src/create-audited-kysely.ts +0 -254
- package/src/index.ts +0 -81
- package/src/kysely-agent-run-scores.ts +0 -47
- package/src/kysely-agent-run-service.ts +0 -244
- package/src/kysely-agent-run-state-service.ts +0 -200
- package/src/kysely-agent-storage-service.ts +0 -632
- package/src/kysely-audit-service.test.ts +0 -352
- package/src/kysely-audit-service.ts +0 -237
- package/src/kysely-channel-store.ts +0 -107
- package/src/kysely-credential-service.ts +0 -286
- package/src/kysely-deployment-service.ts +0 -163
- package/src/kysely-eventhub-store.ts +0 -53
- package/src/kysely-json.ts +0 -5
- package/src/kysely-scope-service.test.ts +0 -418
- package/src/kysely-scope-service.ts +0 -425
- package/src/kysely-secret-service.ts +0 -242
- package/src/kysely-services.test.ts +0 -405
- package/src/kysely-session-store.ts +0 -57
- package/src/kysely-tables.test.ts +0 -110
- package/src/kysely-tables.ts +0 -434
- package/src/kysely-virtual-user-run-store.test.ts +0 -369
- package/src/kysely-virtual-user-run-store.ts +0 -235
- package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
- package/src/kysely-virtual-user-schedule-store.ts +0 -187
- package/src/kysely-webhook-service.test.ts +0 -153
- package/src/kysely-webhook-service.ts +0 -139
- package/src/kysely-workflow-mirror.test.ts +0 -259
- package/src/kysely-workflow-mirror.ts +0 -374
- package/src/kysely-workflow-run-service.ts +0 -305
- package/src/kysely-workflow-service.test.ts +0 -544
- package/src/kysely-workflow-service.ts +0 -820
- package/src/schema/agent.schema.ts +0 -162
- package/src/schema/audit.schema.ts +0 -57
- package/src/schema/channel.schema.ts +0 -38
- package/src/schema/credential.schema.ts +0 -58
- package/src/schema/deployment.schema.ts +0 -47
- package/src/schema/index.ts +0 -441
- package/src/schema/pikku-schema.types.ts +0 -169
- package/src/schema/schema.test.ts +0 -591
- package/src/schema/scope.schema.ts +0 -110
- package/src/schema/secret.schema.ts +0 -49
- package/src/schema/session.schema.ts +0 -20
- package/src/schema/virtual-user-schedule.schema.ts +0 -48
- package/src/schema/virtual-user.schema.ts +0 -106
- package/src/schema/webhook.schema.ts +0 -58
- package/src/schema/workflow.schema.ts +0 -150
- package/src/serialize-plugin.ts +0 -233
- package/tsconfig.json +0 -17
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
import { describe, test, before, beforeEach, after } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely'
|
|
4
|
-
import { SerializePlugin } from './serialize-plugin.js'
|
|
5
|
-
import Database from 'better-sqlite3'
|
|
6
|
-
import { PostgresJSDialect } from 'kysely-postgres-js'
|
|
7
|
-
import postgres from 'postgres'
|
|
8
|
-
import { defineServiceTests } from '@pikku/core/testing'
|
|
9
|
-
|
|
10
|
-
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
11
|
-
import { KyselyChannelStore } from './kysely-channel-store.js'
|
|
12
|
-
import { KyselyEventHubStore } from './kysely-eventhub-store.js'
|
|
13
|
-
import { KyselyWorkflowService } from './kysely-workflow-service.js'
|
|
14
|
-
import { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
|
|
15
|
-
import { KyselyDeploymentService } from './kysely-deployment-service.js'
|
|
16
|
-
import { KyselyAgentStorageService } from './kysely-agent-storage-service.js'
|
|
17
|
-
import { KyselyAgentRunService } from './kysely-agent-run-service.js'
|
|
18
|
-
import { KyselySecretService } from './kysely-secret-service.js'
|
|
19
|
-
import { KyselyCredentialService } from './kysely-credential-service.js'
|
|
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
|
-
]
|
|
45
|
-
|
|
46
|
-
function createSqliteDb(): Kysely<KyselyPikkuDB> {
|
|
47
|
-
return new Kysely<KyselyPikkuDB>({
|
|
48
|
-
dialect: new SqliteDialect({
|
|
49
|
-
database: new Database(':memory:'),
|
|
50
|
-
}),
|
|
51
|
-
plugins: [new CamelCasePlugin(), new SerializePlugin()],
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createPostgresDb(): Kysely<KyselyPikkuDB> | null {
|
|
56
|
-
const url = process.env.DATABASE_URL
|
|
57
|
-
if (!url) return null
|
|
58
|
-
|
|
59
|
-
return new Kysely<KyselyPikkuDB>({
|
|
60
|
-
dialect: new PostgresJSDialect({ postgres: postgres(url) }),
|
|
61
|
-
plugins: [new CamelCasePlugin()],
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function dropAllTables(db: Kysely<KyselyPikkuDB>): Promise<void> {
|
|
66
|
-
const tables = [
|
|
67
|
-
'pikku_deployment_functions',
|
|
68
|
-
'pikku_deployments',
|
|
69
|
-
'agent_tool_call',
|
|
70
|
-
'agent_message',
|
|
71
|
-
'agent_run_score',
|
|
72
|
-
'agent_run',
|
|
73
|
-
'agent_working_memory',
|
|
74
|
-
'agent_threads',
|
|
75
|
-
'channel_subscriptions',
|
|
76
|
-
'channels',
|
|
77
|
-
'workflow_step_history',
|
|
78
|
-
'workflow_step',
|
|
79
|
-
'workflow_runs',
|
|
80
|
-
'workflow_versions',
|
|
81
|
-
'secrets_audit',
|
|
82
|
-
'secret_kek_salts',
|
|
83
|
-
'secrets',
|
|
84
|
-
'credentials_audit',
|
|
85
|
-
'credential_kek_salts',
|
|
86
|
-
'credentials',
|
|
87
|
-
]
|
|
88
|
-
for (const table of tables) {
|
|
89
|
-
await db.schema.dropTable(table).ifExists().execute()
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function registerTests(
|
|
94
|
-
dialectName: string,
|
|
95
|
-
getDb: () => Kysely<KyselyPikkuDB>
|
|
96
|
-
) {
|
|
97
|
-
defineServiceTests({
|
|
98
|
-
name: dialectName,
|
|
99
|
-
services: {
|
|
100
|
-
channelStore: async () => {
|
|
101
|
-
const s = new KyselyChannelStore(getDb())
|
|
102
|
-
await s.init()
|
|
103
|
-
return s
|
|
104
|
-
},
|
|
105
|
-
eventHubStore: async () => {
|
|
106
|
-
const s = new KyselyEventHubStore(getDb())
|
|
107
|
-
await s.init()
|
|
108
|
-
return s
|
|
109
|
-
},
|
|
110
|
-
workflowService: async () => {
|
|
111
|
-
const s = new KyselyWorkflowService(getDb())
|
|
112
|
-
await s.init()
|
|
113
|
-
return s
|
|
114
|
-
},
|
|
115
|
-
workflowRunService: async () => new KyselyWorkflowRunService(getDb()),
|
|
116
|
-
deploymentService: async () => {
|
|
117
|
-
const s = new KyselyDeploymentService(
|
|
118
|
-
{ heartbeatInterval: 60000, heartbeatTtl: 120000 },
|
|
119
|
-
getDb()
|
|
120
|
-
)
|
|
121
|
-
await s.init()
|
|
122
|
-
return s
|
|
123
|
-
},
|
|
124
|
-
agentStorageService: async () => {
|
|
125
|
-
const s = new KyselyAgentStorageService(getDb())
|
|
126
|
-
await s.init()
|
|
127
|
-
return s
|
|
128
|
-
},
|
|
129
|
-
agentRunService: async () => new KyselyAgentRunService(getDb()),
|
|
130
|
-
secretService: async (config) => {
|
|
131
|
-
const s = new KyselySecretService(getDb(), config)
|
|
132
|
-
await s.init()
|
|
133
|
-
return s
|
|
134
|
-
},
|
|
135
|
-
credentialService: async (config) => {
|
|
136
|
-
const s = new KyselyCredentialService(getDb(), config)
|
|
137
|
-
await s.init()
|
|
138
|
-
return s
|
|
139
|
-
},
|
|
140
|
-
sessionStore: async () => {
|
|
141
|
-
const s = new KyselySessionStore(getDb())
|
|
142
|
-
await s.init()
|
|
143
|
-
return s
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
describe(`KyselySecretService audit [${dialectName}]`, () => {
|
|
149
|
-
const kek = 'test-key-encryption-key-32chars!'
|
|
150
|
-
|
|
151
|
-
test('audit logs writes, reads, and deletes', async () => {
|
|
152
|
-
const service = new KyselySecretService(getDb(), {
|
|
153
|
-
key: kek,
|
|
154
|
-
audit: true,
|
|
155
|
-
auditReads: true,
|
|
156
|
-
})
|
|
157
|
-
await service.init()
|
|
158
|
-
await service.setSecret('audit-test', 'value')
|
|
159
|
-
await service.getSecret('audit-test')
|
|
160
|
-
await service.deleteSecret('audit-test')
|
|
161
|
-
|
|
162
|
-
const logs = await getDb()
|
|
163
|
-
.selectFrom('secretsAudit')
|
|
164
|
-
.select(['secretKey', 'action'])
|
|
165
|
-
.where('secretKey', '=', 'audit-test')
|
|
166
|
-
.orderBy('performedAt', 'asc')
|
|
167
|
-
.execute()
|
|
168
|
-
|
|
169
|
-
assert.equal(logs.length, 3)
|
|
170
|
-
assert.equal(logs[0]!.action, 'write')
|
|
171
|
-
assert.equal(logs[1]!.action, 'read')
|
|
172
|
-
assert.equal(logs[2]!.action, 'delete')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
test('audit skips reads when auditReads is false', async () => {
|
|
176
|
-
const service = new KyselySecretService(getDb(), {
|
|
177
|
-
key: kek,
|
|
178
|
-
audit: true,
|
|
179
|
-
auditReads: false,
|
|
180
|
-
})
|
|
181
|
-
await service.init()
|
|
182
|
-
await service.setSecret('no-read-audit', 'value')
|
|
183
|
-
await service.getSecret('no-read-audit')
|
|
184
|
-
|
|
185
|
-
const logs = await getDb()
|
|
186
|
-
.selectFrom('secretsAudit')
|
|
187
|
-
.select(['action'])
|
|
188
|
-
.where('secretKey', '=', 'no-read-audit')
|
|
189
|
-
.execute()
|
|
190
|
-
|
|
191
|
-
assert.equal(logs.length, 1)
|
|
192
|
-
assert.equal(logs[0]!.action, 'write')
|
|
193
|
-
})
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
describe(`KyselySecretService KEK derivation [${dialectName}]`, () => {
|
|
197
|
-
const kek = 'test-key-encryption-key-32chars!'
|
|
198
|
-
|
|
199
|
-
beforeEach(async () => {
|
|
200
|
-
await getDb().deleteFrom('secrets').execute()
|
|
201
|
-
await getDb().deleteFrom('secretKekSalts').execute()
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test('a KEK salt row is created once per key version', async () => {
|
|
205
|
-
const service = new KyselySecretService(getDb(), { key: kek })
|
|
206
|
-
await service.init()
|
|
207
|
-
await service.setSecret('salt-a', 'one')
|
|
208
|
-
await service.setSecret('salt-b', 'two')
|
|
209
|
-
|
|
210
|
-
const rows = await getDb()
|
|
211
|
-
.selectFrom('secretKekSalts')
|
|
212
|
-
.select(['keyVersion', 'salt'])
|
|
213
|
-
.execute()
|
|
214
|
-
|
|
215
|
-
assert.equal(rows.length, 1)
|
|
216
|
-
assert.equal(rows[0]!.keyVersion, 1)
|
|
217
|
-
assert.ok(rows[0]!.salt.length > 0)
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
test('a second instance reuses the stored salt rather than minting one', async () => {
|
|
221
|
-
const first = new KyselySecretService(getDb(), { key: kek })
|
|
222
|
-
await first.init()
|
|
223
|
-
await first.setSecret('shared-salt', 'value')
|
|
224
|
-
|
|
225
|
-
const second = new KyselySecretService(getDb(), { key: kek })
|
|
226
|
-
await second.init()
|
|
227
|
-
assert.equal((await second.getSecret('shared-salt')).reveal(), 'value')
|
|
228
|
-
|
|
229
|
-
const rows = await getDb()
|
|
230
|
-
.selectFrom('secretKekSalts')
|
|
231
|
-
.selectAll()
|
|
232
|
-
.execute()
|
|
233
|
-
assert.equal(rows.length, 1)
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
test('getSecrets over 50 secrets costs one KEK derivation, not 50', async () => {
|
|
237
|
-
const service = new KyselySecretService(getDb(), { key: kek })
|
|
238
|
-
await service.init()
|
|
239
|
-
|
|
240
|
-
const keys: string[] = []
|
|
241
|
-
for (let i = 0; i < 50; i++) {
|
|
242
|
-
const key = `bulk-${i}`
|
|
243
|
-
keys.push(key)
|
|
244
|
-
await service.setSecret(key, `value-${i}`)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const reader = new KyselySecretService(getDb(), { key: kek })
|
|
248
|
-
await reader.init()
|
|
249
|
-
|
|
250
|
-
const start = performance.now()
|
|
251
|
-
const out = await reader.getSecrets(keys)
|
|
252
|
-
const elapsed = performance.now() - start
|
|
253
|
-
|
|
254
|
-
assert.equal(Object.keys(out).length, 50)
|
|
255
|
-
assert.equal(out['bulk-7']!.reveal(), 'value-7')
|
|
256
|
-
assert.ok(
|
|
257
|
-
elapsed < 400,
|
|
258
|
-
`getSecrets over 50 secrets took ${elapsed.toFixed(0)}ms; one derivation per secret would cost seconds`
|
|
259
|
-
)
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
test('rotateKEK over 50 secrets derives two KEKs, not 100', async () => {
|
|
263
|
-
const previousKey = 'previous-key-encryption-key-32c!'
|
|
264
|
-
const writer = new KyselySecretService(getDb(), {
|
|
265
|
-
key: previousKey,
|
|
266
|
-
keyVersion: 1,
|
|
267
|
-
})
|
|
268
|
-
await writer.init()
|
|
269
|
-
|
|
270
|
-
for (let i = 0; i < 50; i++) {
|
|
271
|
-
await writer.setSecret(`rotate-${i}`, `value-${i}`)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const rotator = new KyselySecretService(getDb(), {
|
|
275
|
-
key: kek,
|
|
276
|
-
keyVersion: 2,
|
|
277
|
-
previousKey,
|
|
278
|
-
})
|
|
279
|
-
await rotator.init()
|
|
280
|
-
|
|
281
|
-
const start = performance.now()
|
|
282
|
-
const rotated = await rotator.rotateKEK()
|
|
283
|
-
const elapsed = performance.now() - start
|
|
284
|
-
|
|
285
|
-
assert.equal(rotated, 50)
|
|
286
|
-
assert.equal((await rotator.getSecret('rotate-7')).reveal(), 'value-7')
|
|
287
|
-
assert.ok(
|
|
288
|
-
elapsed < 700,
|
|
289
|
-
`rotating 50 secrets took ${elapsed.toFixed(0)}ms; two derivations per secret would cost seconds`
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
const salts = await getDb()
|
|
293
|
-
.selectFrom('secretKekSalts')
|
|
294
|
-
.select('keyVersion')
|
|
295
|
-
.orderBy('keyVersion', 'asc')
|
|
296
|
-
.execute()
|
|
297
|
-
assert.deepEqual(
|
|
298
|
-
salts.map((r) => r.keyVersion),
|
|
299
|
-
[1, 2]
|
|
300
|
-
)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
test('a wrong KEK passphrase fails loudly rather than returning garbage', async () => {
|
|
304
|
-
const writer = new KyselySecretService(getDb(), { key: kek })
|
|
305
|
-
await writer.init()
|
|
306
|
-
await writer.setSecret('wrong-kek', 'value')
|
|
307
|
-
|
|
308
|
-
const reader = new KyselySecretService(getDb(), {
|
|
309
|
-
key: 'a-completely-different-kek-32ch!',
|
|
310
|
-
})
|
|
311
|
-
await reader.init()
|
|
312
|
-
await assert.rejects(() => reader.getSecret('wrong-kek'))
|
|
313
|
-
})
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
describe(`KyselyCredentialService audit [${dialectName}]`, () => {
|
|
317
|
-
const kek = 'test-key-encryption-key-32chars!'
|
|
318
|
-
|
|
319
|
-
test('audit logs writes, reads, and deletes', async () => {
|
|
320
|
-
const service = new KyselyCredentialService(getDb(), {
|
|
321
|
-
key: kek,
|
|
322
|
-
audit: true,
|
|
323
|
-
auditReads: true,
|
|
324
|
-
})
|
|
325
|
-
await service.init()
|
|
326
|
-
await service.set('audit-cred', { token: 'abc' }, 'user-1')
|
|
327
|
-
await service.get('audit-cred', 'user-1')
|
|
328
|
-
await service.delete('audit-cred', 'user-1')
|
|
329
|
-
|
|
330
|
-
const logs = await getDb()
|
|
331
|
-
.selectFrom('credentialsAudit')
|
|
332
|
-
.select(['credentialName', 'userId', 'action'])
|
|
333
|
-
.where('credentialName', '=', 'audit-cred')
|
|
334
|
-
.orderBy('performedAt', 'asc')
|
|
335
|
-
.execute()
|
|
336
|
-
|
|
337
|
-
assert.equal(logs.length, 3)
|
|
338
|
-
assert.equal(logs[0]!.action, 'write')
|
|
339
|
-
assert.equal(logs[0]!.userId, 'user-1')
|
|
340
|
-
assert.equal(logs[1]!.action, 'read')
|
|
341
|
-
assert.equal(logs[2]!.action, 'delete')
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
test('audit logs global credential with null user_id', async () => {
|
|
345
|
-
const service = new KyselyCredentialService(getDb(), {
|
|
346
|
-
key: kek,
|
|
347
|
-
audit: true,
|
|
348
|
-
auditReads: true,
|
|
349
|
-
})
|
|
350
|
-
await service.init()
|
|
351
|
-
await service.set('global-cred', { key: 'val' })
|
|
352
|
-
await service.get('global-cred')
|
|
353
|
-
|
|
354
|
-
const logs = await getDb()
|
|
355
|
-
.selectFrom('credentialsAudit')
|
|
356
|
-
.select(['credentialName', 'userId', 'action'])
|
|
357
|
-
.where('credentialName', '=', 'global-cred')
|
|
358
|
-
.execute()
|
|
359
|
-
|
|
360
|
-
assert.equal(logs.length, 2)
|
|
361
|
-
assert.equal(logs[0]!.userId, null)
|
|
362
|
-
assert.equal(logs[1]!.userId, null)
|
|
363
|
-
})
|
|
364
|
-
})
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
describe('Kysely Services - SQLite', () => {
|
|
368
|
-
let db: Kysely<KyselyPikkuDB>
|
|
369
|
-
|
|
370
|
-
before(async () => {
|
|
371
|
-
db = createSqliteDb()
|
|
372
|
-
await applyPikkuSchemas(db, SERVICE_SCHEMAS)
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
after(async () => {
|
|
376
|
-
await db.destroy()
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
registerTests('SQLite', () => db)
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
describe(
|
|
383
|
-
'Kysely Services - PostgreSQL',
|
|
384
|
-
{
|
|
385
|
-
skip: !process.env.DATABASE_URL ? 'DATABASE_URL not set' : undefined,
|
|
386
|
-
},
|
|
387
|
-
() => {
|
|
388
|
-
let db: Kysely<KyselyPikkuDB>
|
|
389
|
-
|
|
390
|
-
before(async () => {
|
|
391
|
-
db = createPostgresDb()!
|
|
392
|
-
await dropAllTables(db)
|
|
393
|
-
await applyPikkuSchemas(db, SERVICE_SCHEMAS)
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
after(async () => {
|
|
397
|
-
if (db) {
|
|
398
|
-
await dropAllTables(db)
|
|
399
|
-
await db.destroy()
|
|
400
|
-
}
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
registerTests('PostgreSQL', () => db)
|
|
404
|
-
}
|
|
405
|
-
)
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { SessionStore } from '@pikku/core/services'
|
|
2
|
-
import type { CoreUserSession } from '@pikku/core/types'
|
|
3
|
-
import type { Kysely } from 'kysely'
|
|
4
|
-
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
5
|
-
import { parseJson } from './kysely-json.js'
|
|
6
|
-
import { requirePikkuSchema } from './schema/index.js'
|
|
7
|
-
import { sessionSchema } from './schema/session.schema.js'
|
|
8
|
-
|
|
9
|
-
export class KyselySessionStore implements SessionStore {
|
|
10
|
-
private initialized = false
|
|
11
|
-
|
|
12
|
-
constructor(private db: Kysely<KyselyPikkuDB>) {}
|
|
13
|
-
|
|
14
|
-
public async init(): Promise<void> {
|
|
15
|
-
if (this.initialized) return
|
|
16
|
-
await requirePikkuSchema(this.db, sessionSchema)
|
|
17
|
-
this.initialized = true
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async get(pikkuUserId: string): Promise<CoreUserSession | undefined> {
|
|
21
|
-
const row = await this.db
|
|
22
|
-
.selectFrom('pikkuUserSessions')
|
|
23
|
-
.select(['session'])
|
|
24
|
-
.where('pikkuUserId', '=', pikkuUserId)
|
|
25
|
-
.executeTakeFirst()
|
|
26
|
-
|
|
27
|
-
if (!row) {
|
|
28
|
-
return undefined
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return (parseJson(row.session) ?? undefined) as CoreUserSession | undefined
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async set(pikkuUserId: string, session: CoreUserSession): Promise<void> {
|
|
35
|
-
await this.db
|
|
36
|
-
.insertInto('pikkuUserSessions')
|
|
37
|
-
.values({
|
|
38
|
-
pikkuUserId,
|
|
39
|
-
session: JSON.stringify(session),
|
|
40
|
-
updatedAt: new Date(),
|
|
41
|
-
})
|
|
42
|
-
.onConflict((oc) =>
|
|
43
|
-
oc.column('pikkuUserId').doUpdateSet({
|
|
44
|
-
session: JSON.stringify(session),
|
|
45
|
-
updatedAt: new Date(),
|
|
46
|
-
})
|
|
47
|
-
)
|
|
48
|
-
.execute()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async clear(pikkuUserId: string): Promise<void> {
|
|
52
|
-
await this.db
|
|
53
|
-
.deleteFrom('pikkuUserSessions')
|
|
54
|
-
.where('pikkuUserId', '=', pikkuUserId)
|
|
55
|
-
.execute()
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { test, describe } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { readFileSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { Kysely, SqliteDialect } from 'kysely'
|
|
6
|
-
import Database from 'better-sqlite3'
|
|
7
|
-
|
|
8
|
-
import { applyPikkuSchemas } from './schema/index.js'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* `KyselyPikkuDB` and the schema declaration describe the same tables, and
|
|
12
|
-
* nothing but this makes them agree.
|
|
13
|
-
*
|
|
14
|
-
* The types stay hand-written on purpose: they carry knowledge introspection
|
|
15
|
-
* cannot recover — `WorkflowStatus` rather than `string`, `Generated<Date>`
|
|
16
|
-
* rather than `Date`, and the comments explaining what a column holds.
|
|
17
|
-
* Generating them from the DDL would trade all of that for one fewer file.
|
|
18
|
-
*
|
|
19
|
-
* What generating would have bought is drift protection, and that is what these
|
|
20
|
-
* tests buy instead: add a column to a schema and forget the interface, or the
|
|
21
|
-
* reverse, and the names stop matching here.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
const SOURCE = readFileSync(
|
|
25
|
-
join(import.meta.dirname, 'kysely-tables.ts'),
|
|
26
|
-
'utf8'
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
/** The same transform `CamelCasePlugin` applies to every declared identifier. */
|
|
30
|
-
const physical = (name: string): string =>
|
|
31
|
-
name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()
|
|
32
|
-
|
|
33
|
-
const interfaceBody = (name: string): string => {
|
|
34
|
-
const start = SOURCE.indexOf(`export interface ${name} {`)
|
|
35
|
-
assert.notEqual(start, -1, `no interface ${name}`)
|
|
36
|
-
const end = SOURCE.indexOf('\n}', start)
|
|
37
|
-
return SOURCE.slice(SOURCE.indexOf('{', start) + 1, end)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const fieldsOf = (name: string): string[] =>
|
|
41
|
-
interfaceBody(name)
|
|
42
|
-
.split('\n')
|
|
43
|
-
.map((line) => /^ {2}(\w+)(\??):/.exec(line)?.[1])
|
|
44
|
-
.filter((field): field is string => field !== undefined)
|
|
45
|
-
|
|
46
|
-
/** `KyselyPikkuDB`'s own table-name → interface-name mapping. */
|
|
47
|
-
const tableInterfaces = new Map(
|
|
48
|
-
interfaceBody('KyselyPikkuDB')
|
|
49
|
-
.split('\n')
|
|
50
|
-
.map((line) => /^ {2}(\w+): (\w+)$/.exec(line))
|
|
51
|
-
.filter((match): match is RegExpExecArray => match !== null)
|
|
52
|
-
.map((match) => [match[1]!, match[2]!])
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
/** The schema as declared, materialized and read back. */
|
|
56
|
-
const declaredSchema = async (): Promise<Map<string, Set<string>>> => {
|
|
57
|
-
const db = new Kysely<any>({
|
|
58
|
-
dialect: new SqliteDialect({ database: new Database(':memory:') }),
|
|
59
|
-
})
|
|
60
|
-
try {
|
|
61
|
-
// The scope schema's grants reference it, so it has to be there first —
|
|
62
|
-
// the same prerequisite a real project satisfies with Better Auth.
|
|
63
|
-
await db.schema
|
|
64
|
-
.createTable('user')
|
|
65
|
-
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
66
|
-
.execute()
|
|
67
|
-
await applyPikkuSchemas(db)
|
|
68
|
-
|
|
69
|
-
const tables = await db.introspection.getTables()
|
|
70
|
-
return new Map(
|
|
71
|
-
tables
|
|
72
|
-
.filter((table) => table.name !== 'user')
|
|
73
|
-
.map((table) => [
|
|
74
|
-
table.name,
|
|
75
|
-
new Set(table.columns.map((column) => column.name)),
|
|
76
|
-
])
|
|
77
|
-
)
|
|
78
|
-
} finally {
|
|
79
|
-
await db.destroy()
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
describe('KyselyPikkuDB tracks the schema declaration', () => {
|
|
84
|
-
test('names every table the declaration creates, and no others', async () => {
|
|
85
|
-
const declared = await declaredSchema()
|
|
86
|
-
|
|
87
|
-
const typed = new Set([...tableInterfaces.keys()].map(physical))
|
|
88
|
-
assert.deepEqual(
|
|
89
|
-
[...typed].sort(),
|
|
90
|
-
[...declared.keys()].sort(),
|
|
91
|
-
'a table in one and not the other means the two have drifted apart'
|
|
92
|
-
)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('names every column the declaration creates, and no others', async () => {
|
|
96
|
-
const declared = await declaredSchema()
|
|
97
|
-
|
|
98
|
-
for (const [key, interfaceName] of tableInterfaces) {
|
|
99
|
-
const table = physical(key)
|
|
100
|
-
const columns = declared.get(table)
|
|
101
|
-
assert.ok(columns, `${table} is declared nowhere`)
|
|
102
|
-
|
|
103
|
-
assert.deepEqual(
|
|
104
|
-
fieldsOf(interfaceName).map(physical).sort(),
|
|
105
|
-
[...columns].sort(),
|
|
106
|
-
`${interfaceName} and the '${table}' declaration disagree`
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
})
|