@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.
- package/CHANGELOG.md +56 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/kysely-ai-run-state-service.js +3 -18
- package/dist/src/kysely-ai-storage-service.d.ts +0 -1
- package/dist/src/kysely-ai-storage-service.js +4 -101
- package/dist/src/kysely-channel-store.js +4 -24
- package/dist/src/kysely-credential-service.js +3 -24
- package/dist/src/kysely-deployment-service.d.ts +0 -1
- package/dist/src/kysely-deployment-service.js +4 -48
- package/dist/src/kysely-scope-service.js +4 -54
- package/dist/src/kysely-secret-service.js +3 -21
- package/dist/src/kysely-session-store.js +4 -11
- package/dist/src/kysely-tables.d.ts +2 -0
- package/dist/src/kysely-webhook-service.d.ts +0 -1
- package/dist/src/kysely-webhook-service.js +4 -51
- package/dist/src/kysely-workflow-mirror.d.ts +7 -3
- package/dist/src/kysely-workflow-mirror.js +10 -87
- package/dist/src/kysely-workflow-service.d.ts +2 -2
- package/dist/src/kysely-workflow-service.js +11 -95
- package/dist/src/schema/ai.schema.d.ts +12 -0
- package/dist/src/schema/ai.schema.js +89 -0
- package/dist/src/schema/channel.schema.d.ts +3 -0
- package/dist/src/schema/channel.schema.js +24 -0
- package/dist/src/schema/credential.schema.d.ts +11 -0
- package/dist/src/schema/credential.schema.js +33 -0
- package/dist/src/schema/deployment.schema.d.ts +3 -0
- package/dist/src/schema/deployment.schema.js +32 -0
- package/dist/src/schema/index.d.ts +82 -0
- package/dist/src/schema/index.js +187 -0
- package/dist/src/schema/pikku-schema.types.d.ts +75 -0
- package/dist/src/schema/pikku-schema.types.js +21 -0
- package/dist/src/schema/scope.schema.d.ts +18 -0
- package/dist/src/schema/scope.schema.js +61 -0
- package/dist/src/schema/secret.schema.d.ts +11 -0
- package/dist/src/schema/secret.schema.js +29 -0
- package/dist/src/schema/session.schema.d.ts +3 -0
- package/dist/src/schema/session.schema.js +13 -0
- package/dist/src/schema/webhook.schema.d.ts +3 -0
- package/dist/src/schema/webhook.schema.js +38 -0
- package/dist/src/schema/workflow.schema.d.ts +15 -0
- package/dist/src/schema/workflow.schema.js +86 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/index.ts +18 -0
- package/src/kysely-ai-run-state-service.ts +7 -28
- package/src/kysely-ai-storage-service.ts +4 -152
- package/src/kysely-channel-store.ts +4 -34
- package/src/kysely-credential-service.ts +3 -36
- package/src/kysely-deployment-service.ts +4 -63
- package/src/kysely-scope-service.ts +4 -81
- package/src/kysely-secret-service.ts +6 -33
- package/src/kysely-session-store.ts +4 -18
- package/src/kysely-tables.test.ts +110 -0
- package/src/kysely-tables.ts +2 -0
- package/src/kysely-webhook-service.ts +4 -67
- package/src/kysely-workflow-mirror.test.ts +7 -7
- package/src/kysely-workflow-mirror.ts +10 -109
- package/src/kysely-workflow-service.ts +18 -125
- package/src/schema/ai.schema.ts +140 -0
- package/src/schema/channel.schema.ts +37 -0
- package/src/schema/credential.schema.ts +46 -0
- package/src/schema/deployment.schema.ts +47 -0
- package/src/schema/index.ts +249 -0
- package/src/schema/pikku-schema.types.ts +94 -0
- package/src/schema/schema.test.ts +273 -0
- package/src/schema/scope.schema.ts +95 -0
- package/src/schema/secret.schema.ts +40 -0
- package/src/schema/session.schema.ts +20 -0
- package/src/schema/webhook.schema.ts +57 -0
- package/src/schema/workflow.schema.ts +113 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { test, describe } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import {
|
|
4
|
+
Kysely,
|
|
5
|
+
PostgresAdapter,
|
|
6
|
+
PostgresIntrospector,
|
|
7
|
+
PostgresQueryCompiler,
|
|
8
|
+
SqliteAdapter,
|
|
9
|
+
SqliteIntrospector,
|
|
10
|
+
SqliteQueryCompiler,
|
|
11
|
+
SqliteDialect,
|
|
12
|
+
DummyDriver,
|
|
13
|
+
} from 'kysely'
|
|
14
|
+
import Database from 'better-sqlite3'
|
|
15
|
+
import {
|
|
16
|
+
pikkuSchemas,
|
|
17
|
+
compilePikkuSchemas,
|
|
18
|
+
applyPikkuSchemas,
|
|
19
|
+
ensurePikkuSchema,
|
|
20
|
+
unsatisfiedRequirements,
|
|
21
|
+
type PikkuSchema,
|
|
22
|
+
} from './index.js'
|
|
23
|
+
import { workflowSchema } from './workflow.schema.js'
|
|
24
|
+
|
|
25
|
+
const dialect = (kind: 'postgres' | 'sqlite') =>
|
|
26
|
+
kind === 'postgres'
|
|
27
|
+
? {
|
|
28
|
+
createAdapter: () => new PostgresAdapter(),
|
|
29
|
+
createDriver: () => new DummyDriver(),
|
|
30
|
+
createIntrospector: (db: Kysely<any>) => new PostgresIntrospector(db),
|
|
31
|
+
createQueryCompiler: () => new PostgresQueryCompiler(),
|
|
32
|
+
}
|
|
33
|
+
: {
|
|
34
|
+
createAdapter: () => new SqliteAdapter(),
|
|
35
|
+
createDriver: () => new DummyDriver(),
|
|
36
|
+
createIntrospector: (db: Kysely<any>) => new SqliteIntrospector(db),
|
|
37
|
+
createQueryCompiler: () => new SqliteQueryCompiler(),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const compileFor = (kind: 'postgres' | 'sqlite') =>
|
|
41
|
+
compilePikkuSchemas(new Kysely<any>({ dialect: dialect(kind) }))
|
|
42
|
+
|
|
43
|
+
describe('pikku runtime schema', () => {
|
|
44
|
+
test('declares logical names and compiles them to physical ones', () => {
|
|
45
|
+
const sql = compileFor('postgres')
|
|
46
|
+
|
|
47
|
+
// The declaration says `aiThreads.resourceId`, matching how every query in
|
|
48
|
+
// the package addresses it; the plugin is what makes the table `ai_threads`.
|
|
49
|
+
assert.match(sql, /create table "ai_threads"/)
|
|
50
|
+
assert.match(sql, /"resource_id" varchar\(255\) not null/)
|
|
51
|
+
assert.doesNotMatch(
|
|
52
|
+
sql,
|
|
53
|
+
/"aiThreads"/,
|
|
54
|
+
'a camelCase table name means the plugin was not applied'
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('rewrites foreign key targets too', () => {
|
|
59
|
+
assert.match(
|
|
60
|
+
compileFor('postgres'),
|
|
61
|
+
/references "ai_threads" \("id"\)/,
|
|
62
|
+
'an unrewritten reference target points at a table that does not exist'
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('compiles for every dialect from the one declaration', () => {
|
|
67
|
+
for (const kind of ['postgres', 'sqlite'] as const) {
|
|
68
|
+
const sql = compileFor(kind)
|
|
69
|
+
assert.match(sql, /create table "workflow_runs"/, `${kind} workflow_runs`)
|
|
70
|
+
assert.match(sql, /create table "pikku_scopes"/, `${kind} pikku_scopes`)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('carries the ai_run column the two old declarations disagreed on', () => {
|
|
75
|
+
assert.match(compileFor('postgres'), /"pending_approvals" text/)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('creates tables outright rather than if-not-exists', () => {
|
|
79
|
+
// A migration runs once. `if not exists` would let a table that already
|
|
80
|
+
// exists in a different shape pass silently, which is the drift the
|
|
81
|
+
// migration is supposed to surface.
|
|
82
|
+
assert.doesNotMatch(compileFor('postgres'), /create table if not exists/)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('drops the frozen uuid default from workflow primary keys', () => {
|
|
86
|
+
// It was `sql.raw("'" + crypto.randomUUID() + "'")`, evaluated once when the
|
|
87
|
+
// statement was built, so every row shared one default.
|
|
88
|
+
assert.match(
|
|
89
|
+
compileFor('postgres'),
|
|
90
|
+
/"workflow_run_id" text primary key,/,
|
|
91
|
+
'the primary key should carry no default at all'
|
|
92
|
+
)
|
|
93
|
+
assert.match(compileFor('postgres'), /"workflow_step_id" text primary key,/)
|
|
94
|
+
assert.match(compileFor('postgres'), /"history_id" text primary key,/)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('every schema is named, for the migration it will be written into', () => {
|
|
98
|
+
const names = pikkuSchemas.map((s) => s.name)
|
|
99
|
+
assert.equal(new Set(names).size, names.length, 'names must be unique')
|
|
100
|
+
assert.ok(names.every((n) => /^[a-z][a-z-]*$/.test(n)))
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('pikku runtime schema — prerequisites', () => {
|
|
105
|
+
const memoryDb = () =>
|
|
106
|
+
new Kysely<any>({
|
|
107
|
+
dialect: new SqliteDialect({ database: new Database(':memory:') }),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('names the missing table and who owns it, before creating anything', async () => {
|
|
111
|
+
const db = memoryDb()
|
|
112
|
+
try {
|
|
113
|
+
await assert.rejects(
|
|
114
|
+
applyPikkuSchemas(db),
|
|
115
|
+
/The 'scope' schema requires 'user\.id', which nothing has created\. Better Auth owns it/
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const created = await db.introspection.getTables()
|
|
119
|
+
assert.deepEqual(
|
|
120
|
+
created.map((t) => t.name),
|
|
121
|
+
[],
|
|
122
|
+
'a failed prerequisite must not leave a half-applied database'
|
|
123
|
+
)
|
|
124
|
+
} finally {
|
|
125
|
+
await db.destroy()
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('applies in full once the prerequisite is there', async () => {
|
|
130
|
+
const db = memoryDb()
|
|
131
|
+
try {
|
|
132
|
+
await db.schema
|
|
133
|
+
.createTable('user')
|
|
134
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
135
|
+
.execute()
|
|
136
|
+
|
|
137
|
+
assert.deepEqual(await unsatisfiedRequirements(db), [])
|
|
138
|
+
await applyPikkuSchemas(db)
|
|
139
|
+
|
|
140
|
+
const created = new Set(
|
|
141
|
+
(await db.introspection.getTables()).map((t) => t.name)
|
|
142
|
+
)
|
|
143
|
+
assert.ok(created.has('pikku_user_role'))
|
|
144
|
+
assert.ok(created.has('workflow_runs'))
|
|
145
|
+
} finally {
|
|
146
|
+
await db.destroy()
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('takes the grant column type from the user id it references', () => {
|
|
151
|
+
// The hand-written DDL said `text`. Better Auth generates a `uuid` primary
|
|
152
|
+
// key on postgres, and postgres rejects a text column referencing it —
|
|
153
|
+
// which is why projects ended up writing these tables by hand.
|
|
154
|
+
const sql = compilePikkuSchemas(
|
|
155
|
+
new Kysely<any>({ dialect: dialect('postgres') }),
|
|
156
|
+
pikkuSchemas,
|
|
157
|
+
{ 'user.id': 'uuid' }
|
|
158
|
+
)
|
|
159
|
+
assert.match(
|
|
160
|
+
sql,
|
|
161
|
+
/create table "pikku_user_role" \("user_id" uuid not null references "user" \("id"\)/
|
|
162
|
+
)
|
|
163
|
+
assert.match(
|
|
164
|
+
sql,
|
|
165
|
+
/create table "pikku_user_scope" \("user_id" uuid not null references "user" \("id"\)/
|
|
166
|
+
)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('reports the gap as data, so a caller can describe it instead of dying', async () => {
|
|
170
|
+
const db = memoryDb()
|
|
171
|
+
try {
|
|
172
|
+
const unmet = await unsatisfiedRequirements(db)
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
unmet.map(({ schema, requirement }) => ({
|
|
175
|
+
schema: schema.name,
|
|
176
|
+
...requirement,
|
|
177
|
+
})),
|
|
178
|
+
[{ schema: 'scope', table: 'user', column: 'id', owner: 'Better Auth' }]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// The rest of the declaration does not depend on auth, so it still applies.
|
|
182
|
+
const satisfiable = pikkuSchemas.filter((s) => s.name !== 'scope')
|
|
183
|
+
await applyPikkuSchemas(db, satisfiable)
|
|
184
|
+
const created = new Set(
|
|
185
|
+
(await db.introspection.getTables()).map((t) => t.name)
|
|
186
|
+
)
|
|
187
|
+
assert.ok(created.has('workflow_runs'))
|
|
188
|
+
assert.equal(created.has('pikku_user_role'), false)
|
|
189
|
+
} finally {
|
|
190
|
+
await db.destroy()
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('a statement failing part way takes the ones before it with it', async () => {
|
|
195
|
+
const db = memoryDb()
|
|
196
|
+
try {
|
|
197
|
+
await db.schema.createTable('taken').addColumn('id', 'text').execute()
|
|
198
|
+
|
|
199
|
+
// The prerequisite check cannot see this coming: nothing is missing, the
|
|
200
|
+
// second statement simply collides with a table the database already has.
|
|
201
|
+
const collides: PikkuSchema = {
|
|
202
|
+
name: 'collides',
|
|
203
|
+
statements: [
|
|
204
|
+
(db) => db.schema.createTable('first').addColumn('id', 'text'),
|
|
205
|
+
(db) => db.schema.createTable('taken').addColumn('id', 'text'),
|
|
206
|
+
],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await assert.rejects(applyPikkuSchemas(db, [collides]))
|
|
210
|
+
|
|
211
|
+
const created = new Set(
|
|
212
|
+
(await db.introspection.getTables()).map((t) => t.name)
|
|
213
|
+
)
|
|
214
|
+
assert.equal(
|
|
215
|
+
created.has('first'),
|
|
216
|
+
false,
|
|
217
|
+
'the statement that succeeded must roll back with the one that did not'
|
|
218
|
+
)
|
|
219
|
+
} finally {
|
|
220
|
+
await db.destroy()
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('ensurePikkuSchema — what a service does at boot', () => {
|
|
226
|
+
const memoryDb = () =>
|
|
227
|
+
new Kysely<any>({
|
|
228
|
+
dialect: new SqliteDialect({ database: new Database(':memory:') }),
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('creates the tables the first time and finds them the second', async () => {
|
|
232
|
+
const db = memoryDb()
|
|
233
|
+
try {
|
|
234
|
+
assert.equal(await ensurePikkuSchema(db, workflowSchema), 'created')
|
|
235
|
+
assert.equal(await ensurePikkuSchema(db, workflowSchema), 'present')
|
|
236
|
+
} finally {
|
|
237
|
+
await db.destroy()
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('two services declaring the same schema do not fight over it', async () => {
|
|
242
|
+
// The workflow service and the workflow mirror both own these tables, and
|
|
243
|
+
// both call init(). Whoever runs second must find them and stop, rather
|
|
244
|
+
// than issuing DDL the database will reject.
|
|
245
|
+
const db = memoryDb()
|
|
246
|
+
try {
|
|
247
|
+
assert.equal(await ensurePikkuSchema(db, workflowSchema), 'created')
|
|
248
|
+
assert.equal(await ensurePikkuSchema(db, workflowSchema), 'present')
|
|
249
|
+
} finally {
|
|
250
|
+
await db.destroy()
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('refuses a half-applied schema rather than filling in the rest', async () => {
|
|
255
|
+
// Something else already owns part of it — a migration, an older release,
|
|
256
|
+
// a hand-run script. Creating the remainder at boot leaves two authorities
|
|
257
|
+
// over one set of tables, which is the condition all of this exists to end.
|
|
258
|
+
const db = memoryDb()
|
|
259
|
+
try {
|
|
260
|
+
await db.schema
|
|
261
|
+
.createTable('workflow_runs')
|
|
262
|
+
.addColumn('workflow_run_id', 'text', (col) => col.primaryKey())
|
|
263
|
+
.execute()
|
|
264
|
+
|
|
265
|
+
await assert.rejects(
|
|
266
|
+
ensurePikkuSchema(db, workflowSchema),
|
|
267
|
+
/The 'workflow' schema is half applied/
|
|
268
|
+
)
|
|
269
|
+
} finally {
|
|
270
|
+
await db.destroy()
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { sql } from 'kysely'
|
|
2
|
+
import { requiredType, type PikkuSchema } from './pikku-schema.types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scopes, roles and the grants that bind them to users.
|
|
6
|
+
*
|
|
7
|
+
* `pikkuUserRole` and `pikkuUserScope` reference `user.id`, which Better Auth
|
|
8
|
+
* owns. Auth is a prerequisite rather than an optional companion: a grant is
|
|
9
|
+
* made to a user, so without the table the user lives in there is nothing to
|
|
10
|
+
* grant to, and the cascade that revokes grants when a user is deleted has
|
|
11
|
+
* nothing to hang off.
|
|
12
|
+
*
|
|
13
|
+
* `userId` takes its type from `user.id` rather than declaring one, because
|
|
14
|
+
* that type is Better Auth's to decide: `text` by default, `uuid` under
|
|
15
|
+
* `generateId: 'uuid'`, an identity `integer` under `'serial'`. The old
|
|
16
|
+
* hand-written DDL said `text`, and postgres rejects a `text` column
|
|
17
|
+
* referencing either of the others — so the whole statement failed and projects
|
|
18
|
+
* wrote these tables by hand instead.
|
|
19
|
+
*/
|
|
20
|
+
export const scopeSchema: PikkuSchema = {
|
|
21
|
+
name: 'scope',
|
|
22
|
+
requires: [{ table: 'user', column: 'id', owner: 'Better Auth' }],
|
|
23
|
+
statements: [
|
|
24
|
+
(db) =>
|
|
25
|
+
db.schema
|
|
26
|
+
.createTable('pikkuScopes')
|
|
27
|
+
.addColumn('name', 'text', (col) => col.primaryKey())
|
|
28
|
+
.addColumn('description', 'text')
|
|
29
|
+
.addColumn('declared', 'boolean', (col) =>
|
|
30
|
+
col.defaultTo(true).notNull()
|
|
31
|
+
),
|
|
32
|
+
|
|
33
|
+
(db) =>
|
|
34
|
+
db.schema
|
|
35
|
+
.createTable('pikkuRoles')
|
|
36
|
+
.addColumn('name', 'text', (col) => col.primaryKey())
|
|
37
|
+
.addColumn('description', 'text')
|
|
38
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
39
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
40
|
+
),
|
|
41
|
+
|
|
42
|
+
(db) =>
|
|
43
|
+
db.schema
|
|
44
|
+
.createTable('pikkuRoleScopes')
|
|
45
|
+
.addColumn('role', 'text', (col) =>
|
|
46
|
+
col.notNull().references('pikkuRoles.name').onDelete('cascade')
|
|
47
|
+
)
|
|
48
|
+
.addColumn('scope', 'text', (col) =>
|
|
49
|
+
col.notNull().references('pikkuScopes.name').onDelete('cascade')
|
|
50
|
+
)
|
|
51
|
+
.addPrimaryKeyConstraint('pikku_role_scopes_pk', ['role', 'scope']),
|
|
52
|
+
|
|
53
|
+
(db, types) =>
|
|
54
|
+
db.schema
|
|
55
|
+
.createTable('pikkuUserRole')
|
|
56
|
+
.addColumn('userId', requiredType(types, 'user', 'id'), (col) =>
|
|
57
|
+
col.notNull().references('user.id').onDelete('cascade')
|
|
58
|
+
)
|
|
59
|
+
.addColumn('role', 'text', (col) =>
|
|
60
|
+
col.notNull().references('pikkuRoles.name').onDelete('cascade')
|
|
61
|
+
)
|
|
62
|
+
.addColumn('grantedBy', 'text')
|
|
63
|
+
.addColumn('grantedAt', 'timestamp', (col) =>
|
|
64
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
65
|
+
)
|
|
66
|
+
.addPrimaryKeyConstraint('pikku_user_role_pk', ['userId', 'role']),
|
|
67
|
+
|
|
68
|
+
(db, types) =>
|
|
69
|
+
db.schema
|
|
70
|
+
.createTable('pikkuUserScope')
|
|
71
|
+
.addColumn('userId', requiredType(types, 'user', 'id'), (col) =>
|
|
72
|
+
col.notNull().references('user.id').onDelete('cascade')
|
|
73
|
+
)
|
|
74
|
+
.addColumn('scope', 'text', (col) =>
|
|
75
|
+
col.notNull().references('pikkuScopes.name').onDelete('cascade')
|
|
76
|
+
)
|
|
77
|
+
.addColumn('grantedBy', 'text')
|
|
78
|
+
.addColumn('grantedAt', 'timestamp', (col) =>
|
|
79
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
80
|
+
)
|
|
81
|
+
.addPrimaryKeyConstraint('pikku_user_scope_pk', ['userId', 'scope']),
|
|
82
|
+
|
|
83
|
+
(db) =>
|
|
84
|
+
db.schema
|
|
85
|
+
.createIndex('pikku_role_scopes_scope_idx')
|
|
86
|
+
.on('pikkuRoleScopes')
|
|
87
|
+
.column('scope'),
|
|
88
|
+
|
|
89
|
+
(db) =>
|
|
90
|
+
db.schema
|
|
91
|
+
.createIndex('pikku_user_role_role_idx')
|
|
92
|
+
.on('pikkuUserRole')
|
|
93
|
+
.column('role'),
|
|
94
|
+
],
|
|
95
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { sql } from 'kysely'
|
|
2
|
+
import type { PikkuSchema } from './pikku-schema.types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Encrypted secrets and their audit trail.
|
|
6
|
+
*
|
|
7
|
+
* The audit table is declared unconditionally, where `init()` created it only
|
|
8
|
+
* when the service was constructed with auditing on. A migration cannot depend
|
|
9
|
+
* on runtime configuration, and the asymmetry favours declaring it: an unused
|
|
10
|
+
* table costs nothing, while turning auditing on against a database that never
|
|
11
|
+
* got the table fails on the first write.
|
|
12
|
+
*/
|
|
13
|
+
export const secretSchema: PikkuSchema = {
|
|
14
|
+
name: 'secret',
|
|
15
|
+
statements: [
|
|
16
|
+
(db) =>
|
|
17
|
+
db.schema
|
|
18
|
+
.createTable('secrets')
|
|
19
|
+
.addColumn('key', 'varchar(255)', (col) => col.primaryKey())
|
|
20
|
+
.addColumn('ciphertext', 'text', (col) => col.notNull())
|
|
21
|
+
.addColumn('wrappedDek', 'text', (col) => col.notNull())
|
|
22
|
+
.addColumn('keyVersion', 'integer', (col) => col.notNull())
|
|
23
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
24
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
25
|
+
)
|
|
26
|
+
.addColumn('updatedAt', 'timestamp', (col) =>
|
|
27
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
28
|
+
),
|
|
29
|
+
|
|
30
|
+
(db) =>
|
|
31
|
+
db.schema
|
|
32
|
+
.createTable('secretsAudit')
|
|
33
|
+
.addColumn('id', 'varchar(36)', (col) => col.primaryKey())
|
|
34
|
+
.addColumn('secretKey', 'varchar(255)', (col) => col.notNull())
|
|
35
|
+
.addColumn('action', 'varchar(20)', (col) => col.notNull())
|
|
36
|
+
.addColumn('performedAt', 'timestamp', (col) =>
|
|
37
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { sql } from 'kysely'
|
|
2
|
+
import type { PikkuSchema } from './pikku-schema.types.js'
|
|
3
|
+
|
|
4
|
+
/** Server-side session storage. */
|
|
5
|
+
export const sessionSchema: PikkuSchema = {
|
|
6
|
+
name: 'session',
|
|
7
|
+
statements: [
|
|
8
|
+
(db) =>
|
|
9
|
+
db.schema
|
|
10
|
+
.createTable('pikkuUserSessions')
|
|
11
|
+
.addColumn('pikkuUserId', 'text', (col) => col.primaryKey())
|
|
12
|
+
.addColumn('session', 'text', (col) => col.notNull())
|
|
13
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
14
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
15
|
+
)
|
|
16
|
+
.addColumn('updatedAt', 'timestamp', (col) =>
|
|
17
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { sql } from 'kysely'
|
|
2
|
+
import type { PikkuSchema } from './pikku-schema.types.js'
|
|
3
|
+
|
|
4
|
+
/** Webhook deliveries and one row per delivery attempt. */
|
|
5
|
+
export const webhookSchema: PikkuSchema = {
|
|
6
|
+
name: 'webhook',
|
|
7
|
+
statements: [
|
|
8
|
+
(db) =>
|
|
9
|
+
db.schema
|
|
10
|
+
.createTable('webhookDelivery')
|
|
11
|
+
.addColumn('deliveryId', 'text', (col) => col.primaryKey())
|
|
12
|
+
.addColumn('organizationId', 'text')
|
|
13
|
+
.addColumn('url', 'text', (col) => col.notNull())
|
|
14
|
+
.addColumn('event', 'text')
|
|
15
|
+
.addColumn('status', 'text', (col) =>
|
|
16
|
+
col.defaultTo('pending').notNull()
|
|
17
|
+
)
|
|
18
|
+
.addColumn('attempts', 'integer', (col) => col.defaultTo(0).notNull())
|
|
19
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
20
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
21
|
+
)
|
|
22
|
+
.addColumn('updatedAt', 'timestamp', (col) =>
|
|
23
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
24
|
+
)
|
|
25
|
+
.addColumn('deliveredAt', 'timestamp'),
|
|
26
|
+
|
|
27
|
+
(db) =>
|
|
28
|
+
db.schema
|
|
29
|
+
.createTable('webhookDeliveryAttempt')
|
|
30
|
+
.addColumn('attemptId', 'text', (col) => col.primaryKey())
|
|
31
|
+
.addColumn('deliveryId', 'text', (col) =>
|
|
32
|
+
col
|
|
33
|
+
.notNull()
|
|
34
|
+
.references('webhookDelivery.deliveryId')
|
|
35
|
+
.onDelete('cascade')
|
|
36
|
+
)
|
|
37
|
+
.addColumn('attemptNumber', 'integer', (col) => col.notNull())
|
|
38
|
+
.addColumn('statusCode', 'integer')
|
|
39
|
+
.addColumn('responseBody', 'text')
|
|
40
|
+
.addColumn('error', 'text')
|
|
41
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
42
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
43
|
+
),
|
|
44
|
+
|
|
45
|
+
(db) =>
|
|
46
|
+
db.schema
|
|
47
|
+
.createIndex('idx_webhook_delivery_org')
|
|
48
|
+
.on('webhookDelivery')
|
|
49
|
+
.column('organizationId'),
|
|
50
|
+
|
|
51
|
+
(db) =>
|
|
52
|
+
db.schema
|
|
53
|
+
.createIndex('idx_webhook_delivery_attempt_delivery')
|
|
54
|
+
.on('webhookDeliveryAttempt')
|
|
55
|
+
.column('deliveryId'),
|
|
56
|
+
],
|
|
57
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { sql } from 'kysely'
|
|
2
|
+
import type { PikkuSchema } from './pikku-schema.types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Workflow runs, their steps, per-step history and the versioned graphs.
|
|
6
|
+
*
|
|
7
|
+
* Two things from the boot-time DDL are deliberately not carried over. The
|
|
8
|
+
* primary keys had `defaultTo(sql.raw("'" + crypto.randomUUID() + "'"))`, which
|
|
9
|
+
* evaluates once while the statement is built — every row would have taken the
|
|
10
|
+
* same default, so it was never a generator; the services supply the id. And
|
|
11
|
+
* `workflowStep.fromStepName` was backfilled by an `alterTable(...).catch(() =>
|
|
12
|
+
* {})` bolted on after the fact, which is the job a migration does.
|
|
13
|
+
*
|
|
14
|
+
* `KyselyWorkflowMirror` created these same four tables separately. There is one
|
|
15
|
+
* declaration now, and both services read it.
|
|
16
|
+
*/
|
|
17
|
+
export const workflowSchema: PikkuSchema = {
|
|
18
|
+
name: 'workflow',
|
|
19
|
+
statements: [
|
|
20
|
+
(db) =>
|
|
21
|
+
db.schema
|
|
22
|
+
.createTable('workflowRuns')
|
|
23
|
+
.addColumn('workflowRunId', 'text', (col) => col.primaryKey())
|
|
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('graphHash', 'text')
|
|
32
|
+
.addColumn('deterministic', 'boolean', (col) => col.defaultTo(false))
|
|
33
|
+
.addColumn('plannedSteps', 'text')
|
|
34
|
+
.addColumn('wire', 'text')
|
|
35
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
36
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
37
|
+
)
|
|
38
|
+
.addColumn('updatedAt', 'timestamp', (col) =>
|
|
39
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
40
|
+
),
|
|
41
|
+
|
|
42
|
+
(db) =>
|
|
43
|
+
db.schema
|
|
44
|
+
.createTable('workflowStep')
|
|
45
|
+
.addColumn('workflowStepId', 'text', (col) => col.primaryKey())
|
|
46
|
+
.addColumn('workflowRunId', 'text', (col) =>
|
|
47
|
+
col
|
|
48
|
+
.notNull()
|
|
49
|
+
.references('workflowRuns.workflowRunId')
|
|
50
|
+
.onDelete('cascade')
|
|
51
|
+
)
|
|
52
|
+
.addColumn('stepName', 'text', (col) => col.notNull())
|
|
53
|
+
.addColumn('rpcName', 'text')
|
|
54
|
+
.addColumn('data', 'text')
|
|
55
|
+
.addColumn('status', 'text', (col) =>
|
|
56
|
+
col.notNull().defaultTo('pending')
|
|
57
|
+
)
|
|
58
|
+
.addColumn('result', 'text')
|
|
59
|
+
.addColumn('error', 'text')
|
|
60
|
+
.addColumn('childRunId', 'text')
|
|
61
|
+
.addColumn('branchTaken', 'text')
|
|
62
|
+
.addColumn('retries', 'integer')
|
|
63
|
+
.addColumn('retryDelay', 'text')
|
|
64
|
+
.addColumn('fromStepName', 'text')
|
|
65
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
66
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
67
|
+
)
|
|
68
|
+
.addColumn('updatedAt', 'timestamp', (col) =>
|
|
69
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
70
|
+
)
|
|
71
|
+
.addUniqueConstraint('workflow_step_run_name_unique', [
|
|
72
|
+
'workflowRunId',
|
|
73
|
+
'stepName',
|
|
74
|
+
]),
|
|
75
|
+
|
|
76
|
+
(db) =>
|
|
77
|
+
db.schema
|
|
78
|
+
.createTable('workflowStepHistory')
|
|
79
|
+
.addColumn('historyId', 'text', (col) => col.primaryKey())
|
|
80
|
+
.addColumn('workflowStepId', 'text', (col) =>
|
|
81
|
+
col
|
|
82
|
+
.notNull()
|
|
83
|
+
.references('workflowStep.workflowStepId')
|
|
84
|
+
.onDelete('cascade')
|
|
85
|
+
)
|
|
86
|
+
.addColumn('status', 'text', (col) => col.notNull())
|
|
87
|
+
.addColumn('result', 'text')
|
|
88
|
+
.addColumn('error', 'text')
|
|
89
|
+
.addColumn('createdAt', 'timestamp', (col) =>
|
|
90
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
91
|
+
)
|
|
92
|
+
.addColumn('runningAt', 'timestamp')
|
|
93
|
+
.addColumn('scheduledAt', 'timestamp')
|
|
94
|
+
.addColumn('succeededAt', 'timestamp')
|
|
95
|
+
.addColumn('failedAt', 'timestamp'),
|
|
96
|
+
|
|
97
|
+
(db) =>
|
|
98
|
+
db.schema
|
|
99
|
+
.createTable('workflowVersions')
|
|
100
|
+
.addColumn('workflowName', 'text', (col) => col.notNull())
|
|
101
|
+
.addColumn('graphHash', '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('createdAt', 'timestamp', (col) =>
|
|
106
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
107
|
+
)
|
|
108
|
+
.addPrimaryKeyConstraint('workflow_versions_pk', [
|
|
109
|
+
'workflowName',
|
|
110
|
+
'graphHash',
|
|
111
|
+
]),
|
|
112
|
+
],
|
|
113
|
+
}
|