@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
package/src/schema/index.ts
DELETED
|
@@ -1,441 +0,0 @@
|
|
|
1
|
-
import { CamelCasePlugin, type Kysely } from 'kysely'
|
|
2
|
-
import {
|
|
3
|
-
schemaContext,
|
|
4
|
-
type PikkuSchema,
|
|
5
|
-
type RequiredTypes,
|
|
6
|
-
type SchemaContext,
|
|
7
|
-
type SchemaRequirement,
|
|
8
|
-
} from './pikku-schema.types.js'
|
|
9
|
-
import { agentSchema } from './agent.schema.js'
|
|
10
|
-
import { auditSchema } from './audit.schema.js'
|
|
11
|
-
import { channelSchema } from './channel.schema.js'
|
|
12
|
-
import { credentialSchema } from './credential.schema.js'
|
|
13
|
-
import { deploymentSchema } from './deployment.schema.js'
|
|
14
|
-
import { scopeSchema } from './scope.schema.js'
|
|
15
|
-
import { secretSchema } from './secret.schema.js'
|
|
16
|
-
import { sessionSchema } from './session.schema.js'
|
|
17
|
-
import { webhookSchema } from './webhook.schema.js'
|
|
18
|
-
import { workflowSchema } from './workflow.schema.js'
|
|
19
|
-
import { virtualUserSchema } from './virtual-user.schema.js'
|
|
20
|
-
import { virtualUserScheduleSchema } from './virtual-user-schedule.schema.js'
|
|
21
|
-
|
|
22
|
-
export type {
|
|
23
|
-
PikkuSchema,
|
|
24
|
-
RequiredTypes,
|
|
25
|
-
SchemaContext,
|
|
26
|
-
SchemaRequirement,
|
|
27
|
-
SchemaStatement,
|
|
28
|
-
SchemaStatementFactory,
|
|
29
|
-
} from './pikku-schema.types.js'
|
|
30
|
-
export {
|
|
31
|
-
rawStatement,
|
|
32
|
-
requiredType,
|
|
33
|
-
schemaContext,
|
|
34
|
-
unqualifiedContext,
|
|
35
|
-
} from './pikku-schema.types.js'
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Every table any part of the pikku runtime can need, in dependency order.
|
|
39
|
-
*
|
|
40
|
-
* Order is load-bearing, not cosmetic: `scope` has foreign keys onto Better
|
|
41
|
-
* Auth's `user`, and the tables within each schema reference each other. Sort
|
|
42
|
-
* this list alphabetically and it stops applying.
|
|
43
|
-
*
|
|
44
|
-
* `audit` and `virtualUser` are in the list now. They used to be left out and
|
|
45
|
-
* created by their own services at boot, which was the only way they ever came
|
|
46
|
-
* into existence — so with the runtime no longer creating anything, leaving
|
|
47
|
-
* them out would mean a wired audit sink starting up against a table that
|
|
48
|
-
* nothing writes a migration for. Both are gated by `ownedBy`, so a project
|
|
49
|
-
* that wires neither still carries neither.
|
|
50
|
-
*/
|
|
51
|
-
export const pikkuSchemas: PikkuSchema[] = [
|
|
52
|
-
channelSchema,
|
|
53
|
-
sessionSchema,
|
|
54
|
-
secretSchema,
|
|
55
|
-
credentialSchema,
|
|
56
|
-
deploymentSchema,
|
|
57
|
-
webhookSchema,
|
|
58
|
-
workflowSchema,
|
|
59
|
-
agentSchema,
|
|
60
|
-
scopeSchema,
|
|
61
|
-
auditSchema,
|
|
62
|
-
virtualUserSchema,
|
|
63
|
-
virtualUserScheduleSchema,
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
export { agentSchema } from './agent.schema.js'
|
|
67
|
-
export { auditSchema } from './audit.schema.js'
|
|
68
|
-
export { channelSchema } from './channel.schema.js'
|
|
69
|
-
export { credentialSchema } from './credential.schema.js'
|
|
70
|
-
export { deploymentSchema } from './deployment.schema.js'
|
|
71
|
-
export { scopeSchema } from './scope.schema.js'
|
|
72
|
-
export { secretSchema } from './secret.schema.js'
|
|
73
|
-
export { sessionSchema } from './session.schema.js'
|
|
74
|
-
export { virtualUserSchema } from './virtual-user.schema.js'
|
|
75
|
-
export { virtualUserScheduleSchema } from './virtual-user-schedule.schema.js'
|
|
76
|
-
export { webhookSchema } from './webhook.schema.js'
|
|
77
|
-
export { workflowSchema } from './workflow.schema.js'
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* The schemas a project needs, given the services its code actually reaches.
|
|
81
|
-
*
|
|
82
|
-
* `services` is the inspector's `requiredServices` — the same set that drives
|
|
83
|
-
* service tree-shaking, so a schema is written for exactly the projects whose
|
|
84
|
-
* functions, middleware or wirings reach the service that owns its tables.
|
|
85
|
-
*
|
|
86
|
-
* The gate is one-sided: a schema declaring no `ownedBy` is always in, because
|
|
87
|
-
* nothing a project writes implies it and leaving it out would be a guess.
|
|
88
|
-
*/
|
|
89
|
-
export const requiredPikkuSchemas = (
|
|
90
|
-
services: ReadonlySet<string>
|
|
91
|
-
): PikkuSchema[] =>
|
|
92
|
-
pikkuSchemas.filter(
|
|
93
|
-
(schema) =>
|
|
94
|
-
!schema.ownedBy || schema.ownedBy.some((name) => services.has(name))
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Bind a declaration to a database.
|
|
99
|
-
*
|
|
100
|
-
* The plugin is applied here rather than trusted to the caller. Declarations
|
|
101
|
-
* name tables and columns the way the rest of the codebase queries them
|
|
102
|
-
* (`agentThreads.resourceId`), and it is `CamelCasePlugin` that turns those into
|
|
103
|
-
* the physical `agent_threads.resource_id`. Without it the same declaration
|
|
104
|
-
* silently creates quoted camelCase tables that nothing can read.
|
|
105
|
-
*/
|
|
106
|
-
const bind = (db: Kysely<any>) => db.withPlugin(new CamelCasePlugin())
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* The schema a connection is bound to, or `undefined` for one that is not.
|
|
110
|
-
*
|
|
111
|
-
* Read back out of compiled DDL rather than off the connection, because kysely
|
|
112
|
-
* keeps the `withSchema(...)` setting inside its executor and exposes no way to
|
|
113
|
-
* ask. Compiling a throwaway `create table` is the same trick `declaredTables`
|
|
114
|
-
* uses on the real statements, against the same regex — nothing is executed and
|
|
115
|
-
* the probe never leaves this function.
|
|
116
|
-
*
|
|
117
|
-
* Derived rather than passed in so that raw statements follow whatever
|
|
118
|
-
* connection they are handed. A caller that had to remember to say the schema
|
|
119
|
-
* twice would eventually say it once.
|
|
120
|
-
*/
|
|
121
|
-
const boundSchema = (db: Kysely<any>): string | undefined =>
|
|
122
|
-
CREATE_TABLE.exec(
|
|
123
|
-
db.schema.createTable('pikkuSchemaProbe').addColumn('id', 'text').compile()
|
|
124
|
-
.sql
|
|
125
|
-
)?.[1]
|
|
126
|
-
|
|
127
|
-
/** The context for statements compiled against `db`, matching its binding. */
|
|
128
|
-
const contextFor = (db: Kysely<any>): SchemaContext =>
|
|
129
|
-
schemaContext(boundSchema(db))
|
|
130
|
-
|
|
131
|
-
export interface UnmetRequirement {
|
|
132
|
-
schema: PikkuSchema
|
|
133
|
-
requirement: SchemaRequirement
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Look up every prerequisite in `db`, reporting the gaps and the types found.
|
|
138
|
-
*
|
|
139
|
-
* Both halves come from one introspection because they are one question. The
|
|
140
|
-
* gaps say whether a schema can be applied at all; the types say how — a
|
|
141
|
-
* foreign key onto a `uuid` primary key has to be declared `uuid`, and guessing
|
|
142
|
-
* `text` produces DDL the database refuses.
|
|
143
|
-
*
|
|
144
|
-
* Introspection returns physical names, so `requires` is written in physical
|
|
145
|
-
* names too — this is the one place in the declaration that is not camelCase.
|
|
146
|
-
*/
|
|
147
|
-
export const resolveRequirements = async (
|
|
148
|
-
db: Kysely<any>,
|
|
149
|
-
schemas: PikkuSchema[] = pikkuSchemas
|
|
150
|
-
): Promise<{ types: RequiredTypes; unmet: UnmetRequirement[] }> => {
|
|
151
|
-
const tables = new Map(
|
|
152
|
-
(await bind(db).introspection.getTables()).map((table) => [
|
|
153
|
-
table.name,
|
|
154
|
-
table,
|
|
155
|
-
])
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
const types: RequiredTypes = {}
|
|
159
|
-
const unmet: UnmetRequirement[] = []
|
|
160
|
-
for (const schema of schemas) {
|
|
161
|
-
for (const requirement of schema.requires ?? []) {
|
|
162
|
-
const column = tables
|
|
163
|
-
.get(requirement.table)
|
|
164
|
-
?.columns.find((c) => c.name === requirement.column)
|
|
165
|
-
if (!column) {
|
|
166
|
-
unmet.push({ schema, requirement })
|
|
167
|
-
} else {
|
|
168
|
-
types[`${requirement.table}.${requirement.column}`] = column.dataType
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return { types, unmet }
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* The prerequisites `db` does not satisfy, as a list rather than a throw.
|
|
177
|
-
*
|
|
178
|
-
* Separate from `applyPikkuSchemas` because the two callers want opposite
|
|
179
|
-
* things from the same answer: a service booting cannot proceed without its
|
|
180
|
-
* tables, while a tool asking what a project's schema *would* be needs to
|
|
181
|
-
* describe the gap rather than die on it.
|
|
182
|
-
*/
|
|
183
|
-
export const unsatisfiedRequirements = async (
|
|
184
|
-
db: Kysely<any>,
|
|
185
|
-
schemas: PikkuSchema[] = pikkuSchemas
|
|
186
|
-
): Promise<UnmetRequirement[]> => (await resolveRequirements(db, schemas)).unmet
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Create the tables, in order, on a database that has whatever they require.
|
|
190
|
-
*
|
|
191
|
-
* Prerequisites are checked before anything is created, so a project missing
|
|
192
|
-
* one is told which schema wanted what — rather than being handed a foreign key
|
|
193
|
-
* error, or a half-applied database to clean up.
|
|
194
|
-
*
|
|
195
|
-
* The statements then run in one transaction, so the same promise holds for a
|
|
196
|
-
* failure the check cannot foresee: a statement that dies mid-list takes the
|
|
197
|
-
* ones before it with it. On an engine without transactional DDL the rollback
|
|
198
|
-
* is the engine's to give, not ours.
|
|
199
|
-
*/
|
|
200
|
-
export const applyPikkuSchemas = async (
|
|
201
|
-
db: Kysely<any>,
|
|
202
|
-
schemas: PikkuSchema[] = pikkuSchemas
|
|
203
|
-
): Promise<void> => {
|
|
204
|
-
const { types, unmet } = await resolveRequirements(db, schemas)
|
|
205
|
-
if (unmet.length > 0) {
|
|
206
|
-
throw new Error(
|
|
207
|
-
unmet
|
|
208
|
-
.map(
|
|
209
|
-
({ schema, requirement }) =>
|
|
210
|
-
`The '${schema.name}' schema requires '${requirement.table}.${requirement.column}', which nothing has created. ` +
|
|
211
|
-
`${requirement.owner} owns it — apply its schema first.`
|
|
212
|
-
)
|
|
213
|
-
.join('\n')
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
await db.transaction().execute(async (trx) => {
|
|
218
|
-
const bound = bind(trx)
|
|
219
|
-
const ctx = contextFor(bound)
|
|
220
|
-
for (const schema of schemas) {
|
|
221
|
-
for (const statement of schema.statements) {
|
|
222
|
-
const query = statement(bound, types, ctx)
|
|
223
|
-
try {
|
|
224
|
-
await query.execute()
|
|
225
|
-
} catch (error) {
|
|
226
|
-
throw explain(error, query.compile().sql, schema)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** `references "app"."workflow_runs"` — a foreign key onto a qualified table. */
|
|
234
|
-
const QUALIFIED_REFERENCE = /references "[^"]+"\."[^"]+"/i
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* sqlite's own words for "you put a dot where a table name goes".
|
|
238
|
-
*
|
|
239
|
-
* Matched rather than a driver's `code`, because every sqlite binding
|
|
240
|
-
* (better-sqlite3, node:sqlite, bun:sqlite, libsql) passes the engine's message
|
|
241
|
-
* through verbatim while disagreeing about how to label it. Postgres phrases the
|
|
242
|
-
* same class of error the other way round — `syntax error at or near "."` — so
|
|
243
|
-
* this cannot fire on it.
|
|
244
|
-
*/
|
|
245
|
-
const SQLITE_REJECTED_QUALIFIER = /near "\.":\s*syntax error/i
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Say what a schema-bound connection did to the DDL, when that is what failed.
|
|
249
|
-
*
|
|
250
|
-
* `withSchema(...)` qualifies foreign key targets along with everything else.
|
|
251
|
-
* Postgres needs that — an unqualified reference resolves against `search_path`,
|
|
252
|
-
* which a schema-bound connection does not control — and sqlite refuses it: a
|
|
253
|
-
* `REFERENCES` clause there takes a bare table name, because a foreign key can
|
|
254
|
-
* only point inside the same database, so there is no qualifier to give. One
|
|
255
|
-
* declaration cannot spell both, and what the engine says about it is
|
|
256
|
-
* `near ".": syntax error`, which names neither the schema nor the cause.
|
|
257
|
-
*
|
|
258
|
-
* Gated on sqlite's exact phrasing as well as the SQL. Postgres compiles the
|
|
259
|
-
* same qualified reference legitimately, and can raise a syntax error of its own
|
|
260
|
-
* on this DDL — a column type resolved from `requires` goes in verbatim — so a
|
|
261
|
-
* looser `/syntax error/` would answer for postgres in sqlite's name.
|
|
262
|
-
*/
|
|
263
|
-
const explain = (error: unknown, sql: string, schema: PikkuSchema): unknown => {
|
|
264
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
265
|
-
if (
|
|
266
|
-
!QUALIFIED_REFERENCE.test(sql) ||
|
|
267
|
-
!SQLITE_REJECTED_QUALIFIER.test(message)
|
|
268
|
-
) {
|
|
269
|
-
return error
|
|
270
|
-
}
|
|
271
|
-
return new Error(
|
|
272
|
-
`The '${schema.name}' schema has a foreign key, and this connection is bound to a schema, ` +
|
|
273
|
-
`so it compiled to a qualified 'references "schema"."table"' that the database rejected. ` +
|
|
274
|
-
'sqlite is the engine that does this: its REFERENCES clause takes a bare table name. ' +
|
|
275
|
-
'Use a connection without withSchema(...) there — sqlite has one schema to be in — ' +
|
|
276
|
-
'or write the schema down as a migration with `pikku db generate` and apply it yourself. ' +
|
|
277
|
-
`The database said: ${message}`,
|
|
278
|
-
{ cause: error }
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** A table a schema creates, as the DDL addresses it. */
|
|
283
|
-
export interface DeclaredTable {
|
|
284
|
-
/** Present when the connection is `withSchema(...)`-bound. */
|
|
285
|
-
schema?: string
|
|
286
|
-
name: string
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** How a table reads in an error message: qualified only when it is. */
|
|
290
|
-
const displayName = (table: DeclaredTable): string =>
|
|
291
|
-
table.schema ? `${table.schema}.${table.name}` : table.name
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* `create table "workflow_runs"` and `create table "app"."workflow_runs"`.
|
|
295
|
-
*
|
|
296
|
-
* The schema half is optional and the table is always the LAST quoted
|
|
297
|
-
* identifier — matching the first one instead reads a `withSchema('app')`
|
|
298
|
-
* connection's DDL as a table called `app`, which is no table at all.
|
|
299
|
-
*/
|
|
300
|
-
const CREATE_TABLE =
|
|
301
|
-
/^\s*create table (?:if not exists\s+)?(?:"([^"]+)"\.)?"([^"]+)"/i
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* The tables a schema creates, read back out of its own compiled SQL.
|
|
305
|
-
*
|
|
306
|
-
* Derived rather than declared alongside the statements, because a hand-kept
|
|
307
|
-
* list of names is a second source of truth: rename a table in a statement and
|
|
308
|
-
* the list keeps answering for the old one.
|
|
309
|
-
*
|
|
310
|
-
* Compiled against `db` rather than in the abstract, so the answer carries the
|
|
311
|
-
* schema the DDL will actually target — the same binding the lookup has to use.
|
|
312
|
-
*/
|
|
313
|
-
export const declaredTables = (
|
|
314
|
-
schema: PikkuSchema,
|
|
315
|
-
db: Kysely<any>
|
|
316
|
-
): DeclaredTable[] => {
|
|
317
|
-
const bound = bind(db)
|
|
318
|
-
const ctx = contextFor(bound)
|
|
319
|
-
const tables: DeclaredTable[] = []
|
|
320
|
-
for (const statement of schema.statements) {
|
|
321
|
-
const match = CREATE_TABLE.exec(statement(bound, {}, ctx).compile().sql)
|
|
322
|
-
if (match) tables.push({ schema: match[1], name: match[2]! })
|
|
323
|
-
}
|
|
324
|
-
return tables
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* What `db` already has, indexed both ways.
|
|
329
|
-
*
|
|
330
|
-
* Introspection reports every schema it can see, so the bare name alone cannot
|
|
331
|
-
* answer the question: a `workflow_runs` in some other schema is not the one a
|
|
332
|
-
* `withSchema('app')` connection reads or writes. Qualified declarations are
|
|
333
|
-
* matched on the pair. Unqualified DDL resolves against the connection's
|
|
334
|
-
* search_path, which is not knowable from here, so it falls back to the name.
|
|
335
|
-
*/
|
|
336
|
-
interface ExistingTables {
|
|
337
|
-
qualified: Set<string>
|
|
338
|
-
bare: Set<string>
|
|
339
|
-
/** Whether this engine reports schemas at all. */
|
|
340
|
-
schemaAware: boolean
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const tablesOf = async (db: Kysely<any>): Promise<ExistingTables> => {
|
|
344
|
-
const tables = await bind(db).introspection.getTables()
|
|
345
|
-
return {
|
|
346
|
-
qualified: new Set(
|
|
347
|
-
tables
|
|
348
|
-
.filter((table) => table.schema)
|
|
349
|
-
.map((table) => `${table.schema}.${table.name}`)
|
|
350
|
-
),
|
|
351
|
-
bare: new Set(tables.map((table) => table.name)),
|
|
352
|
-
schemaAware: tables.some((table) => table.schema !== undefined),
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Qualified declarations match on the pair — but only where that pair means
|
|
358
|
-
* something.
|
|
359
|
-
*
|
|
360
|
-
* `TableMetadata.schema` is optional, and sqlite and mysql leave it unset:
|
|
361
|
-
* sqlite has one schema to be in, and mysql calls the thing a database. A
|
|
362
|
-
* `withSchema('main')` connection there declares `main.workflow_runs` while
|
|
363
|
-
* introspection can only offer `workflow_runs`, and holding out for the pair
|
|
364
|
-
* would read every existing table as missing — the same false negative this
|
|
365
|
-
* whole function exists to fix, arrived at from the other side. So when the
|
|
366
|
-
* engine reports no schemas, the bare name is the only name there is.
|
|
367
|
-
*/
|
|
368
|
-
const isPresent = (existing: ExistingTables, table: DeclaredTable): boolean =>
|
|
369
|
-
table.schema && existing.schemaAware
|
|
370
|
-
? existing.qualified.has(`${table.schema}.${table.name}`)
|
|
371
|
-
: existing.bare.has(table.name)
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Refuse to start unless one schema's tables are already there.
|
|
375
|
-
*
|
|
376
|
-
* What a service calls at boot, and it never issues DDL. The runtime is not an
|
|
377
|
-
* author of the schema: `pikku db generate` writes the declaration down as a
|
|
378
|
-
* migration and `pikku db migrate` applies it, and those two are the only way
|
|
379
|
-
* these tables come into existence. A service that finds them missing says so
|
|
380
|
-
* and stops.
|
|
381
|
-
*
|
|
382
|
-
* This is the whole point of gating what `db generate` writes. While boot could
|
|
383
|
-
* still create, a schema that generation left out was created silently at
|
|
384
|
-
* startup instead — two authorities over one set of tables, which is the
|
|
385
|
-
* condition all of this exists to end. With creation gone, a schema generation
|
|
386
|
-
* missed is a sentence at startup naming the command that fixes it.
|
|
387
|
-
*
|
|
388
|
-
* Half-present is not a distinct case any more. Every missing table is reported
|
|
389
|
-
* the same way, because the remedy is the same one either way.
|
|
390
|
-
*/
|
|
391
|
-
export const requirePikkuSchema = async (
|
|
392
|
-
db: Kysely<any>,
|
|
393
|
-
schema: PikkuSchema
|
|
394
|
-
): Promise<void> => {
|
|
395
|
-
const existing = await tablesOf(db)
|
|
396
|
-
const declared = declaredTables(schema, db)
|
|
397
|
-
const missing = declared.filter((table) => !isPresent(existing, table))
|
|
398
|
-
|
|
399
|
-
if (missing.length === 0) return
|
|
400
|
-
|
|
401
|
-
const present = declared.filter((table) => isPresent(existing, table))
|
|
402
|
-
throw new Error(
|
|
403
|
-
`The '${schema.name}' schema is not in this database: ${missing.map(displayName).join(', ')} missing` +
|
|
404
|
-
(present.length > 0
|
|
405
|
-
? `, ${present.map(displayName).join(', ')} already there`
|
|
406
|
-
: '') +
|
|
407
|
-
'. The runtime does not create these — run `pikku db generate` to write the ' +
|
|
408
|
-
'migration, then `pikku db migrate` to apply it.'
|
|
409
|
-
)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Render the declaration as SQL for the dialect `db` was built with.
|
|
414
|
-
*
|
|
415
|
-
* `types` comes from `resolveRequirements` against the database the migration
|
|
416
|
-
* is being written for. Omit it and foreign keys onto another source's columns
|
|
417
|
-
* compile as `text`, which is a shape, not a migration.
|
|
418
|
-
*
|
|
419
|
-
* `schema` qualifies every table the migration creates, for a project that
|
|
420
|
-
* keeps the runtime tables somewhere other than the default search path. It is
|
|
421
|
-
* applied here and not to `db` itself because the caller's connection is a
|
|
422
|
-
* throwaway the declaration was just *applied* to, in whatever schema that
|
|
423
|
-
* database actually has — qualifying that would create tables in a schema the
|
|
424
|
-
* scratch database has never heard of. Only the rendered SQL is bound.
|
|
425
|
-
*/
|
|
426
|
-
export const compilePikkuSchemas = (
|
|
427
|
-
db: Kysely<any>,
|
|
428
|
-
schemas: PikkuSchema[] = pikkuSchemas,
|
|
429
|
-
types: RequiredTypes = {},
|
|
430
|
-
schemaName?: string
|
|
431
|
-
): string => {
|
|
432
|
-
const bound = schemaName ? bind(db).withSchema(schemaName) : bind(db)
|
|
433
|
-
const ctx = contextFor(bound)
|
|
434
|
-
return schemas
|
|
435
|
-
.flatMap((schema) =>
|
|
436
|
-
schema.statements.map(
|
|
437
|
-
(statement) => `${statement(bound, types, ctx).compile().sql};`
|
|
438
|
-
)
|
|
439
|
-
)
|
|
440
|
-
.join('\n\n')
|
|
441
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { sql, type CompiledQuery, type Kysely, type RawBuilder } from 'kysely'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* One DDL statement, bound to a database.
|
|
5
|
-
*
|
|
6
|
-
* Bound rather than free-standing because the same declaration serves two
|
|
7
|
-
* purposes that need the same object: applied to a throwaway database it yields
|
|
8
|
-
* the schema to introspect, and compiled against a dialect it yields the SQL a
|
|
9
|
-
* migration is written from.
|
|
10
|
-
*/
|
|
11
|
-
export interface SchemaStatement {
|
|
12
|
-
execute(): Promise<unknown>
|
|
13
|
-
compile(): CompiledQuery
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type SchemaStatementFactory = (
|
|
17
|
-
db: Kysely<any>,
|
|
18
|
-
types: RequiredTypes,
|
|
19
|
-
ctx: SchemaContext
|
|
20
|
-
) => SchemaStatement
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* What a statement needs to know about the schema its DDL lands in.
|
|
24
|
-
*
|
|
25
|
-
* Only raw SQL needs this. The schema builder learns the schema from the
|
|
26
|
-
* connection — `withSchema('app')` qualifies every table it names, foreign key
|
|
27
|
-
* targets included — but a raw fragment is passed through verbatim, so a
|
|
28
|
-
* statement that spells its own SQL has to spell the qualifier too.
|
|
29
|
-
*/
|
|
30
|
-
export interface SchemaContext {
|
|
31
|
-
/**
|
|
32
|
-
* A table as the surrounding DDL addresses it: `"app"."credentials"` on a
|
|
33
|
-
* schema-bound connection, `"credentials"` otherwise.
|
|
34
|
-
*
|
|
35
|
-
* Takes the physical name, not the camelCase one the schema builder accepts.
|
|
36
|
-
* Raw SQL does not go through `CamelCasePlugin`, so it is already written in
|
|
37
|
-
* physical names throughout and this is no exception.
|
|
38
|
-
*/
|
|
39
|
-
table: (name: string) => RawBuilder<unknown>
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The `SchemaContext` for DDL that names no schema.
|
|
44
|
-
*
|
|
45
|
-
* The default everywhere a schema has not been asked for, which is every engine
|
|
46
|
-
* but postgres and most postgres projects too.
|
|
47
|
-
*/
|
|
48
|
-
export const unqualifiedContext: SchemaContext = {
|
|
49
|
-
table: (name) => sql.raw(`"${name}"`),
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* A `SchemaContext` qualifying every table into `schema`.
|
|
54
|
-
*
|
|
55
|
-
* Identifiers are quoted rather than interpolated bare so a schema named after
|
|
56
|
-
* a reserved word still compiles.
|
|
57
|
-
*/
|
|
58
|
-
export const schemaContext = (schema: string | undefined): SchemaContext =>
|
|
59
|
-
schema
|
|
60
|
-
? { table: (name) => sql.raw(`"${schema}"."${name}"`) }
|
|
61
|
-
: unqualifiedContext
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The physical types of the columns named in `requires`, keyed `table.column`.
|
|
65
|
-
*
|
|
66
|
-
* Introspected from the database the schemas are being applied to, because a
|
|
67
|
-
* foreign key column has to be declared with the type of the column it points
|
|
68
|
-
* at and that column belongs to somebody else.
|
|
69
|
-
*/
|
|
70
|
-
export type RequiredTypes = Record<string, string>
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* The declared type for a foreign key onto a column another source owns.
|
|
74
|
-
*
|
|
75
|
-
* Falls back to `text` when nothing has been introspected — the case when the
|
|
76
|
-
* declaration is compiled without a database, where the real type is unknowable
|
|
77
|
-
* and only the shape of the statement is being inspected.
|
|
78
|
-
*/
|
|
79
|
-
export const requiredType = (
|
|
80
|
-
types: RequiredTypes,
|
|
81
|
-
table: string,
|
|
82
|
-
column: string
|
|
83
|
-
): RawBuilder<unknown> => sql.raw(types[`${table}.${column}`] ?? 'text')
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* The tables one part of the pikku runtime needs, declared rather than created.
|
|
87
|
-
*
|
|
88
|
-
* `name` labels the generated migration and the drift report, so it is part of
|
|
89
|
-
* the contract — changing it orphans the migration that was written under the
|
|
90
|
-
* old one.
|
|
91
|
-
*/
|
|
92
|
-
export interface PikkuSchema {
|
|
93
|
-
name: string
|
|
94
|
-
statements: SchemaStatementFactory[]
|
|
95
|
-
/**
|
|
96
|
-
* Tables this schema references but does not create, which something else
|
|
97
|
-
* must have created first.
|
|
98
|
-
*
|
|
99
|
-
* Declared rather than discovered so the failure arrives as a sentence naming
|
|
100
|
-
* what is missing and who owns it, instead of a foreign key error from the
|
|
101
|
-
* database — or, worse, a stub table conjured up to make the error go away.
|
|
102
|
-
*/
|
|
103
|
-
requires?: SchemaRequirement[]
|
|
104
|
-
/**
|
|
105
|
-
* The singleton services whose `init()` requires these tables, or absent for
|
|
106
|
-
* a schema every project needs.
|
|
107
|
-
*
|
|
108
|
-
* Named as services rather than as wirings because a service is what the
|
|
109
|
-
* tables actually belong to: one `requirePikkuSchema` call site per service,
|
|
110
|
-
* and `pikku db generate` writes the schema for a project whose functions
|
|
111
|
-
* reach any of them. A wiring is a poor proxy — a project can construct
|
|
112
|
-
* `KyselyWorkflowService` and wire no workflows at all.
|
|
113
|
-
*
|
|
114
|
-
* Absent means unconditional. `secrets` is on every project and the session
|
|
115
|
-
* and deployment stores are reached by generated code rather than by
|
|
116
|
-
* functions, so nothing a project writes implies them and their absence
|
|
117
|
-
* cannot be inferred.
|
|
118
|
-
*
|
|
119
|
-
* Under-naming is safe now that the runtime creates nothing: a schema left
|
|
120
|
-
* out of the migration is a sentence at startup, not a table conjured behind
|
|
121
|
-
* your back. Over-naming costs an unused table. Prefer over-naming.
|
|
122
|
-
*/
|
|
123
|
-
ownedBy?: string[]
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* A column another source owns, which one of these schemas depends on.
|
|
128
|
-
*
|
|
129
|
-
* Named down to the column, not just the table, because that is the granularity
|
|
130
|
-
* the dependency actually has: a foreign key needs the referenced column to
|
|
131
|
-
* exist *and* needs its type in order to declare a matching one.
|
|
132
|
-
*
|
|
133
|
-
* `owner` is carried in the declaration rather than composed into the error at
|
|
134
|
-
* the point it is thrown, so adding a second prerequisite cannot leave behind a
|
|
135
|
-
* message that still names the first one's owner.
|
|
136
|
-
*/
|
|
137
|
-
export interface SchemaRequirement {
|
|
138
|
-
table: string
|
|
139
|
-
column: string
|
|
140
|
-
owner: string
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Wrap a raw SQL fragment as a statement.
|
|
145
|
-
*
|
|
146
|
-
* For the DDL kysely's schema builder cannot express — a unique index over an
|
|
147
|
-
* expression, say. Raw SQL is *not* rewritten by `withSchema`, so a fragment
|
|
148
|
-
* naming a table unqualified resolves against the connection's search path
|
|
149
|
-
* rather than the schema the rest of the declaration lands in. Take the
|
|
150
|
-
* function form and name tables through `ctx.table` to stay with them; the
|
|
151
|
-
* bare-fragment form is for SQL that names no table at all.
|
|
152
|
-
*/
|
|
153
|
-
export function rawStatement(
|
|
154
|
-
fragment: RawBuilder<unknown>
|
|
155
|
-
): SchemaStatementFactory
|
|
156
|
-
export function rawStatement(
|
|
157
|
-
build: (ctx: SchemaContext) => RawBuilder<unknown>
|
|
158
|
-
): SchemaStatementFactory
|
|
159
|
-
export function rawStatement(
|
|
160
|
-
source: RawBuilder<unknown> | ((ctx: SchemaContext) => RawBuilder<unknown>)
|
|
161
|
-
): SchemaStatementFactory {
|
|
162
|
-
return (db, _types, ctx = unqualifiedContext) => {
|
|
163
|
-
const fragment = typeof source === 'function' ? source(ctx) : source
|
|
164
|
-
return {
|
|
165
|
-
execute: () => fragment.execute(db),
|
|
166
|
-
compile: () => fragment.compile(db),
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|