@pikku/kysely 0.13.24 → 0.13.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +271 -0
  2. package/dist/src/index.d.ts +4 -1
  3. package/dist/src/index.js +3 -1
  4. package/dist/src/kysely-analytics-service.d.ts +30 -0
  5. package/dist/src/kysely-analytics-service.js +80 -0
  6. package/dist/src/kysely-feature-flag-store.d.ts +37 -0
  7. package/dist/src/kysely-feature-flag-store.js +239 -0
  8. package/dist/src/kysely-scope-service.d.ts +1 -0
  9. package/dist/src/kysely-scope-service.js +29 -0
  10. package/dist/src/kysely-tables.d.ts +45 -0
  11. package/dist/src/schema/analytics.schema.d.ts +17 -0
  12. package/dist/src/schema/analytics.schema.js +48 -0
  13. package/dist/src/schema/flag.schema.d.ts +16 -0
  14. package/dist/src/schema/flag.schema.js +62 -0
  15. package/dist/src/schema/index.d.ts +2 -0
  16. package/dist/src/schema/index.js +6 -0
  17. package/package.json +11 -6
  18. package/run-tests.sh +9 -1
  19. package/bin/pikku-kysely-pure.ts +0 -59
  20. package/dist/src/kysely-lock-vault.d.ts +0 -34
  21. package/dist/src/kysely-lock-vault.js +0 -77
  22. package/dist/src/schema/data-lock.schema.d.ts +0 -15
  23. package/dist/src/schema/data-lock.schema.js +0 -27
  24. package/dist/tsconfig.tsbuildinfo +0 -1
  25. package/src/classification-crypto.test.ts +0 -143
  26. package/src/classification-crypto.ts +0 -163
  27. package/src/classification-plugin.pglite.test.ts +0 -226
  28. package/src/classification-plugin.test.ts +0 -265
  29. package/src/classification-plugin.ts +0 -249
  30. package/src/coercion-plugin.test.ts +0 -160
  31. package/src/coercion-plugin.ts +0 -187
  32. package/src/create-audited-kysely.test.ts +0 -560
  33. package/src/create-audited-kysely.ts +0 -254
  34. package/src/index.ts +0 -81
  35. package/src/kysely-agent-run-scores.ts +0 -47
  36. package/src/kysely-agent-run-service.ts +0 -244
  37. package/src/kysely-agent-run-state-service.ts +0 -200
  38. package/src/kysely-agent-storage-service.ts +0 -632
  39. package/src/kysely-audit-service.test.ts +0 -352
  40. package/src/kysely-audit-service.ts +0 -237
  41. package/src/kysely-channel-store.ts +0 -107
  42. package/src/kysely-credential-service.ts +0 -286
  43. package/src/kysely-deployment-service.ts +0 -163
  44. package/src/kysely-eventhub-store.ts +0 -53
  45. package/src/kysely-json.ts +0 -5
  46. package/src/kysely-scope-service.test.ts +0 -418
  47. package/src/kysely-scope-service.ts +0 -425
  48. package/src/kysely-secret-service.ts +0 -242
  49. package/src/kysely-services.test.ts +0 -405
  50. package/src/kysely-session-store.ts +0 -57
  51. package/src/kysely-tables.test.ts +0 -110
  52. package/src/kysely-tables.ts +0 -434
  53. package/src/kysely-virtual-user-run-store.test.ts +0 -369
  54. package/src/kysely-virtual-user-run-store.ts +0 -235
  55. package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
  56. package/src/kysely-virtual-user-schedule-store.ts +0 -187
  57. package/src/kysely-webhook-service.test.ts +0 -153
  58. package/src/kysely-webhook-service.ts +0 -139
  59. package/src/kysely-workflow-mirror.test.ts +0 -259
  60. package/src/kysely-workflow-mirror.ts +0 -374
  61. package/src/kysely-workflow-run-service.ts +0 -305
  62. package/src/kysely-workflow-service.test.ts +0 -544
  63. package/src/kysely-workflow-service.ts +0 -820
  64. package/src/schema/agent.schema.ts +0 -162
  65. package/src/schema/audit.schema.ts +0 -57
  66. package/src/schema/channel.schema.ts +0 -38
  67. package/src/schema/credential.schema.ts +0 -58
  68. package/src/schema/deployment.schema.ts +0 -47
  69. package/src/schema/index.ts +0 -441
  70. package/src/schema/pikku-schema.types.ts +0 -169
  71. package/src/schema/schema.test.ts +0 -591
  72. package/src/schema/scope.schema.ts +0 -110
  73. package/src/schema/secret.schema.ts +0 -49
  74. package/src/schema/session.schema.ts +0 -20
  75. package/src/schema/virtual-user-schedule.schema.ts +0 -48
  76. package/src/schema/virtual-user.schema.ts +0 -106
  77. package/src/schema/webhook.schema.ts +0 -58
  78. package/src/schema/workflow.schema.ts +0 -150
  79. package/src/serialize-plugin.ts +0 -233
  80. package/tsconfig.json +0 -17
@@ -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
- }