@pikku/kysely 0.13.23 → 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 +248 -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/kysely-webhook-service.d.ts +1 -1
- package/dist/src/kysely-webhook-service.js +1 -1
- 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 -124
- package/src/kysely-webhook-service.ts +0 -137
- 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,286 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
deriveKEK,
|
|
3
|
-
envelopeDecrypt,
|
|
4
|
-
envelopeEncrypt,
|
|
5
|
-
envelopeRewrap,
|
|
6
|
-
generateKEKSalt,
|
|
7
|
-
} from '@pikku/core/crypto-utils'
|
|
8
|
-
import type { CredentialService } from '@pikku/core/services'
|
|
9
|
-
import type { Kysely } from 'kysely'
|
|
10
|
-
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
11
|
-
import { requirePikkuSchema } from './schema/index.js'
|
|
12
|
-
import { credentialSchema } from './schema/credential.schema.js'
|
|
13
|
-
|
|
14
|
-
export interface KyselyCredentialServiceConfig {
|
|
15
|
-
key: string
|
|
16
|
-
keyVersion?: number
|
|
17
|
-
previousKey?: string
|
|
18
|
-
audit?: boolean
|
|
19
|
-
auditReads?: boolean
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class KyselyCredentialService implements CredentialService {
|
|
23
|
-
private initialized = false
|
|
24
|
-
private kekSalts = new Map<number, string>()
|
|
25
|
-
private keks = new Map<number, CryptoKey>()
|
|
26
|
-
private key: string
|
|
27
|
-
private keyVersion: number
|
|
28
|
-
private previousKey?: string
|
|
29
|
-
private audit: boolean
|
|
30
|
-
private auditReads: boolean
|
|
31
|
-
|
|
32
|
-
constructor(
|
|
33
|
-
private db: Kysely<KyselyPikkuDB>,
|
|
34
|
-
config: KyselyCredentialServiceConfig
|
|
35
|
-
) {
|
|
36
|
-
this.key = config.key
|
|
37
|
-
this.keyVersion = config.keyVersion ?? 1
|
|
38
|
-
this.previousKey = config.previousKey
|
|
39
|
-
this.audit = config.audit ?? false
|
|
40
|
-
this.auditReads = config.auditReads ?? false
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
public async init(): Promise<void> {
|
|
44
|
-
if (this.initialized) return
|
|
45
|
-
await requirePikkuSchema(this.db, credentialSchema)
|
|
46
|
-
this.initialized = true
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private async logAudit(
|
|
50
|
-
name: string,
|
|
51
|
-
userId: string | undefined,
|
|
52
|
-
action: 'read' | 'write' | 'delete' | 'rotate'
|
|
53
|
-
): Promise<void> {
|
|
54
|
-
if (!this.audit) return
|
|
55
|
-
if (action === 'read' && !this.auditReads) return
|
|
56
|
-
|
|
57
|
-
await this.db
|
|
58
|
-
.insertInto('credentialsAudit')
|
|
59
|
-
.values({
|
|
60
|
-
id: crypto.randomUUID(),
|
|
61
|
-
credentialName: name,
|
|
62
|
-
userId: userId ?? null,
|
|
63
|
-
action,
|
|
64
|
-
performedAt: new Date().toISOString() as unknown as Date,
|
|
65
|
-
})
|
|
66
|
-
.execute()
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private async getKEKSalt(version: number): Promise<string> {
|
|
70
|
-
const cached = this.kekSalts.get(version)
|
|
71
|
-
if (cached) return cached
|
|
72
|
-
|
|
73
|
-
const existing = await this.db
|
|
74
|
-
.selectFrom('credentialKekSalts')
|
|
75
|
-
.select('salt')
|
|
76
|
-
.where('keyVersion', '=', version)
|
|
77
|
-
.executeTakeFirst()
|
|
78
|
-
|
|
79
|
-
let salt = existing?.salt
|
|
80
|
-
if (!salt) {
|
|
81
|
-
await this.db
|
|
82
|
-
.insertInto('credentialKekSalts')
|
|
83
|
-
.values({
|
|
84
|
-
keyVersion: version,
|
|
85
|
-
salt: generateKEKSalt(),
|
|
86
|
-
createdAt: new Date().toISOString() as unknown as Date,
|
|
87
|
-
})
|
|
88
|
-
.onConflict((oc) => oc.column('keyVersion').doNothing())
|
|
89
|
-
.execute()
|
|
90
|
-
|
|
91
|
-
const row = await this.db
|
|
92
|
-
.selectFrom('credentialKekSalts')
|
|
93
|
-
.select('salt')
|
|
94
|
-
.where('keyVersion', '=', version)
|
|
95
|
-
.executeTakeFirstOrThrow()
|
|
96
|
-
salt = row.salt
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
this.kekSalts.set(version, salt)
|
|
100
|
-
return salt
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private async getKEK(version: number): Promise<CryptoKey> {
|
|
104
|
-
const cached = this.keks.get(version)
|
|
105
|
-
if (cached) return cached
|
|
106
|
-
|
|
107
|
-
const passphrase = version === this.keyVersion ? this.key : this.previousKey
|
|
108
|
-
if (!passphrase) {
|
|
109
|
-
throw new Error(`No KEK available for key_version ${version}`)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const kek = await deriveKEK(passphrase, await this.getKEKSalt(version))
|
|
113
|
-
this.keks.set(version, kek)
|
|
114
|
-
return kek
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private whereUserId(qb: any, userId?: string) {
|
|
118
|
-
return userId
|
|
119
|
-
? qb.where('userId', '=', userId)
|
|
120
|
-
: qb.where('userId', 'is', null)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async get<T = unknown>(name: string, userId?: string): Promise<T | null> {
|
|
124
|
-
let qb = this.db
|
|
125
|
-
.selectFrom('credentials')
|
|
126
|
-
.select(['ciphertext', 'wrappedDek', 'keyVersion'])
|
|
127
|
-
.where('name', '=', name)
|
|
128
|
-
qb = this.whereUserId(qb, userId)
|
|
129
|
-
|
|
130
|
-
const row = await qb.executeTakeFirst()
|
|
131
|
-
if (!row) return null
|
|
132
|
-
|
|
133
|
-
const kek = await this.getKEK(row.keyVersion)
|
|
134
|
-
const plaintext = await envelopeDecrypt<string>(
|
|
135
|
-
kek,
|
|
136
|
-
row.ciphertext,
|
|
137
|
-
row.wrappedDek
|
|
138
|
-
)
|
|
139
|
-
await this.logAudit(name, userId, 'read')
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
return JSON.parse(plaintext) as T
|
|
143
|
-
} catch {
|
|
144
|
-
throw new Error(`Credential '${name}' contains invalid data`)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async set(name: string, value: unknown, userId?: string): Promise<void> {
|
|
149
|
-
const plaintext = JSON.stringify(value)
|
|
150
|
-
const { ciphertext, wrappedDEK } = await envelopeEncrypt(
|
|
151
|
-
await this.getKEK(this.keyVersion),
|
|
152
|
-
plaintext
|
|
153
|
-
)
|
|
154
|
-
const now = new Date().toISOString()
|
|
155
|
-
const exists = await this.has(name, userId)
|
|
156
|
-
|
|
157
|
-
if (exists) {
|
|
158
|
-
let qb = this.db
|
|
159
|
-
.updateTable('credentials')
|
|
160
|
-
.set({
|
|
161
|
-
ciphertext,
|
|
162
|
-
wrappedDek: wrappedDEK,
|
|
163
|
-
keyVersion: this.keyVersion,
|
|
164
|
-
updatedAt: now as unknown as Date,
|
|
165
|
-
})
|
|
166
|
-
.where('name', '=', name)
|
|
167
|
-
qb = this.whereUserId(qb, userId)
|
|
168
|
-
await qb.execute()
|
|
169
|
-
} else {
|
|
170
|
-
await this.db
|
|
171
|
-
.insertInto('credentials')
|
|
172
|
-
.values({
|
|
173
|
-
name,
|
|
174
|
-
userId: userId ?? null,
|
|
175
|
-
ciphertext,
|
|
176
|
-
wrappedDek: wrappedDEK,
|
|
177
|
-
keyVersion: this.keyVersion,
|
|
178
|
-
createdAt: now as unknown as Date,
|
|
179
|
-
updatedAt: now as unknown as Date,
|
|
180
|
-
})
|
|
181
|
-
.execute()
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
await this.logAudit(name, userId, 'write')
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async delete(name: string, userId?: string): Promise<void> {
|
|
188
|
-
let qb = this.db.deleteFrom('credentials').where('name', '=', name)
|
|
189
|
-
qb = this.whereUserId(qb, userId)
|
|
190
|
-
await qb.execute()
|
|
191
|
-
|
|
192
|
-
await this.logAudit(name, userId, 'delete')
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async has(name: string, userId?: string): Promise<boolean> {
|
|
196
|
-
let qb = this.db
|
|
197
|
-
.selectFrom('credentials')
|
|
198
|
-
.select('name')
|
|
199
|
-
.where('name', '=', name)
|
|
200
|
-
qb = this.whereUserId(qb, userId)
|
|
201
|
-
|
|
202
|
-
const row = await qb.executeTakeFirst()
|
|
203
|
-
return !!row
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async getAll(userId: string): Promise<Record<string, unknown>> {
|
|
207
|
-
const rows = await this.db
|
|
208
|
-
.selectFrom('credentials')
|
|
209
|
-
.select(['name', 'ciphertext', 'wrappedDek', 'keyVersion'])
|
|
210
|
-
.where('userId', '=', userId)
|
|
211
|
-
.execute()
|
|
212
|
-
|
|
213
|
-
const result: Record<string, unknown> = {}
|
|
214
|
-
for (const row of rows) {
|
|
215
|
-
const kek = await this.getKEK(row.keyVersion)
|
|
216
|
-
const plaintext = await envelopeDecrypt<string>(
|
|
217
|
-
kek,
|
|
218
|
-
row.ciphertext,
|
|
219
|
-
row.wrappedDek
|
|
220
|
-
)
|
|
221
|
-
try {
|
|
222
|
-
result[row.name] = JSON.parse(plaintext)
|
|
223
|
-
} catch {
|
|
224
|
-
throw new Error(`Credential '${row.name}' contains invalid data`)
|
|
225
|
-
}
|
|
226
|
-
await this.logAudit(row.name, userId, 'read')
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return result
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async getUsersWithCredential(name: string): Promise<string[]> {
|
|
233
|
-
const rows = await this.db
|
|
234
|
-
.selectFrom('credentials')
|
|
235
|
-
.select('userId')
|
|
236
|
-
.where('name', '=', name)
|
|
237
|
-
.where('userId', 'is not', null)
|
|
238
|
-
.execute()
|
|
239
|
-
|
|
240
|
-
return rows.map((row) => row.userId!).filter(Boolean)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async getAllUsers(): Promise<string[]> {
|
|
244
|
-
const rows = await this.db
|
|
245
|
-
.selectFrom('credentials')
|
|
246
|
-
.select('userId')
|
|
247
|
-
.distinct()
|
|
248
|
-
.where('userId', 'is not', null)
|
|
249
|
-
.execute()
|
|
250
|
-
|
|
251
|
-
return rows.map((row) => row.userId!).filter(Boolean)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async rotateKEK(): Promise<number> {
|
|
255
|
-
if (!this.previousKey) {
|
|
256
|
-
throw new Error('No previousKey configured — nothing to rotate from')
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const rows = await this.db
|
|
260
|
-
.selectFrom('credentials')
|
|
261
|
-
.select(['name', 'userId', 'wrappedDek'])
|
|
262
|
-
.where('keyVersion', '<', this.keyVersion)
|
|
263
|
-
.execute()
|
|
264
|
-
|
|
265
|
-
const oldKEK = await this.getKEK(this.keyVersion - 1)
|
|
266
|
-
const newKEK = await this.getKEK(this.keyVersion)
|
|
267
|
-
|
|
268
|
-
for (const row of rows) {
|
|
269
|
-
const newWrappedDEK = await envelopeRewrap(oldKEK, newKEK, row.wrappedDek)
|
|
270
|
-
let qb = this.db
|
|
271
|
-
.updateTable('credentials')
|
|
272
|
-
.set({
|
|
273
|
-
wrappedDek: newWrappedDEK,
|
|
274
|
-
keyVersion: this.keyVersion,
|
|
275
|
-
updatedAt: new Date().toISOString() as unknown as Date,
|
|
276
|
-
})
|
|
277
|
-
.where('name', '=', row.name)
|
|
278
|
-
qb = this.whereUserId(qb, row.userId ?? undefined)
|
|
279
|
-
await qb.execute()
|
|
280
|
-
|
|
281
|
-
await this.logAudit(row.name, row.userId ?? undefined, 'rotate')
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return rows.length
|
|
285
|
-
}
|
|
286
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { buildRemoteHeaders } from '@pikku/core/remote'
|
|
2
|
-
import type {
|
|
3
|
-
DeploymentService,
|
|
4
|
-
DeploymentServiceConfig,
|
|
5
|
-
DeploymentConfig,
|
|
6
|
-
} from '@pikku/core/services'
|
|
7
|
-
import type { JWTService } from '@pikku/core/services'
|
|
8
|
-
import type { SecretService } from '@pikku/core/services'
|
|
9
|
-
import { getAllFunctionNames } from '@pikku/core/function'
|
|
10
|
-
import type { Kysely } from 'kysely'
|
|
11
|
-
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
12
|
-
import { requirePikkuSchema } from './schema/index.js'
|
|
13
|
-
import { deploymentSchema } from './schema/deployment.schema.js'
|
|
14
|
-
|
|
15
|
-
export class KyselyDeploymentService implements DeploymentService {
|
|
16
|
-
private initialized = false
|
|
17
|
-
private heartbeatTimer?: ReturnType<typeof setInterval>
|
|
18
|
-
private deploymentConfig?: DeploymentConfig
|
|
19
|
-
private heartbeatInterval: number
|
|
20
|
-
private heartbeatTtl: number
|
|
21
|
-
|
|
22
|
-
constructor(
|
|
23
|
-
config: DeploymentServiceConfig,
|
|
24
|
-
protected db: Kysely<KyselyPikkuDB>,
|
|
25
|
-
private jwt?: JWTService,
|
|
26
|
-
private secrets?: SecretService
|
|
27
|
-
) {
|
|
28
|
-
this.heartbeatInterval = config.heartbeatInterval ?? 10000
|
|
29
|
-
this.heartbeatTtl = config.heartbeatTtl ?? 30000
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public async init(): Promise<void> {
|
|
33
|
-
if (this.initialized) return
|
|
34
|
-
await requirePikkuSchema(this.db, deploymentSchema)
|
|
35
|
-
this.initialized = true
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async start(config: DeploymentConfig): Promise<void> {
|
|
39
|
-
const functions = config.functions ?? getAllFunctionNames()
|
|
40
|
-
this.deploymentConfig = { ...config, functions }
|
|
41
|
-
|
|
42
|
-
await this.db.transaction().execute(async (trx) => {
|
|
43
|
-
await trx
|
|
44
|
-
.insertInto('pikkuDeployments')
|
|
45
|
-
.values({
|
|
46
|
-
deploymentId: config.deploymentId,
|
|
47
|
-
endpoint: config.endpoint,
|
|
48
|
-
lastHeartbeat: new Date(),
|
|
49
|
-
})
|
|
50
|
-
.onConflict((oc) =>
|
|
51
|
-
oc.column('deploymentId').doUpdateSet({
|
|
52
|
-
endpoint: config.endpoint,
|
|
53
|
-
lastHeartbeat: new Date(),
|
|
54
|
-
})
|
|
55
|
-
)
|
|
56
|
-
.execute()
|
|
57
|
-
|
|
58
|
-
await trx
|
|
59
|
-
.deleteFrom('pikkuDeploymentFunctions')
|
|
60
|
-
.where('deploymentId', '=', config.deploymentId)
|
|
61
|
-
.execute()
|
|
62
|
-
|
|
63
|
-
if (functions.length > 0) {
|
|
64
|
-
await trx
|
|
65
|
-
.insertInto('pikkuDeploymentFunctions')
|
|
66
|
-
.values(
|
|
67
|
-
functions.map((fn) => ({
|
|
68
|
-
deploymentId: config.deploymentId,
|
|
69
|
-
functionName: fn,
|
|
70
|
-
}))
|
|
71
|
-
)
|
|
72
|
-
.execute()
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
this.heartbeatTimer = setInterval(
|
|
77
|
-
() => this.sendHeartbeat(),
|
|
78
|
-
this.heartbeatInterval
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async stop(): Promise<void> {
|
|
83
|
-
if (this.heartbeatTimer) {
|
|
84
|
-
clearInterval(this.heartbeatTimer)
|
|
85
|
-
this.heartbeatTimer = undefined
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (this.deploymentConfig) {
|
|
89
|
-
await this.db
|
|
90
|
-
.deleteFrom('pikkuDeployments')
|
|
91
|
-
.where('deploymentId', '=', this.deploymentConfig.deploymentId)
|
|
92
|
-
.execute()
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async invoke(
|
|
97
|
-
funcName: string,
|
|
98
|
-
data: unknown,
|
|
99
|
-
session?: unknown,
|
|
100
|
-
traceId?: string
|
|
101
|
-
): Promise<unknown> {
|
|
102
|
-
const headers = await buildRemoteHeaders(
|
|
103
|
-
this.jwt,
|
|
104
|
-
this.secrets,
|
|
105
|
-
funcName,
|
|
106
|
-
session,
|
|
107
|
-
traceId
|
|
108
|
-
)
|
|
109
|
-
const ttlMs = this.heartbeatTtl
|
|
110
|
-
const cutoff = new Date(Date.now() - ttlMs)
|
|
111
|
-
|
|
112
|
-
const result = await this.db
|
|
113
|
-
.selectFrom('pikkuDeployments as d')
|
|
114
|
-
.innerJoin(
|
|
115
|
-
'pikkuDeploymentFunctions as f',
|
|
116
|
-
'f.deploymentId',
|
|
117
|
-
'd.deploymentId'
|
|
118
|
-
)
|
|
119
|
-
.select(['d.deploymentId', 'd.endpoint'])
|
|
120
|
-
.where('f.functionName', '=', funcName)
|
|
121
|
-
.where('d.lastHeartbeat', '>', cutoff)
|
|
122
|
-
.orderBy('d.lastHeartbeat', 'desc')
|
|
123
|
-
.limit(1)
|
|
124
|
-
.execute()
|
|
125
|
-
|
|
126
|
-
if (result.length === 0) {
|
|
127
|
-
throw new Error(`No deployment found for function '${funcName}'`)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const endpoint = result[0].endpoint
|
|
131
|
-
const url = `${endpoint}/remote/rpc/${encodeURIComponent(funcName)}`
|
|
132
|
-
const response = await fetch(url, {
|
|
133
|
-
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
-
'content-type': 'application/json',
|
|
136
|
-
...headers,
|
|
137
|
-
},
|
|
138
|
-
body: JSON.stringify({ data }),
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
if (!response.ok) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`Remote RPC call to '${funcName}' failed: ${response.status}`
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return response.json()
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private async sendHeartbeat(): Promise<void> {
|
|
151
|
-
if (!this.deploymentConfig) return
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
await this.db
|
|
155
|
-
.updateTable('pikkuDeployments')
|
|
156
|
-
.set({ lastHeartbeat: new Date() })
|
|
157
|
-
.where('deploymentId', '=', this.deploymentConfig.deploymentId)
|
|
158
|
-
.execute()
|
|
159
|
-
} catch {
|
|
160
|
-
// Heartbeat failed, will retry on next interval
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { EventHubStore } from '@pikku/core/channel'
|
|
2
|
-
import type { Kysely } from 'kysely'
|
|
3
|
-
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
4
|
-
|
|
5
|
-
export class KyselyEventHubStore extends EventHubStore {
|
|
6
|
-
private initialized = false
|
|
7
|
-
|
|
8
|
-
constructor(private db: Kysely<KyselyPikkuDB>) {
|
|
9
|
-
super()
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
public async init(): Promise<void> {
|
|
13
|
-
if (this.initialized) {
|
|
14
|
-
return
|
|
15
|
-
}
|
|
16
|
-
this.initialized = true
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
public async getChannelIdsForTopic(topic: string): Promise<string[]> {
|
|
20
|
-
const result = await this.db
|
|
21
|
-
.selectFrom('channelSubscriptions')
|
|
22
|
-
.select('channelId')
|
|
23
|
-
.where('topic', '=', topic)
|
|
24
|
-
.execute()
|
|
25
|
-
|
|
26
|
-
return result.map((row) => row.channelId)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
public async subscribe(topic: string, channelId: string): Promise<boolean> {
|
|
30
|
-
try {
|
|
31
|
-
await this.db
|
|
32
|
-
.insertInto('channelSubscriptions')
|
|
33
|
-
.values({ channelId: channelId, topic: topic as string })
|
|
34
|
-
.onConflict((oc) => oc.columns(['channelId', 'topic']).doNothing())
|
|
35
|
-
.execute()
|
|
36
|
-
return true
|
|
37
|
-
} catch {
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public async unsubscribe(topic: string, channelId: string): Promise<boolean> {
|
|
43
|
-
const result = await this.db
|
|
44
|
-
.deleteFrom('channelSubscriptions')
|
|
45
|
-
.where('channelId', '=', channelId)
|
|
46
|
-
.where('topic', '=', topic as string)
|
|
47
|
-
.executeTakeFirst()
|
|
48
|
-
|
|
49
|
-
return BigInt(result.numDeletedRows) > 0n
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public async close(): Promise<void> {}
|
|
53
|
-
}
|