evikit 2.5.1 → 2.5.2

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 (4) hide show
  1. package/index.js +13 -4
  2. package/package.json +1 -1
  3. package/server.js +5 -1
  4. package/sqlite.js +489 -489
package/index.js CHANGED
@@ -116,13 +116,22 @@ export async function ssr(App, data) {
116
116
  const { html, links } = await prerender(h(/** @type {any} */ (App), {}), {
117
117
  props: { dehydratedState, i18n },
118
118
  });
119
- const { metas, title } = toStatic();
119
+ const { links: hoofdLinks, metas, title } = toStatic();
120
120
 
121
121
  return {
122
122
  data: { ...data, dehydratedState },
123
123
  head: {
124
- elements: new Set(
125
- metas.map((props) => ({
124
+ elements: new Set([
125
+ ...hoofdLinks.map((props) => ({
126
+ props: Object.fromEntries(
127
+ Object.entries(props).map(([name, content]) => [
128
+ name,
129
+ String(content),
130
+ ]),
131
+ ),
132
+ type: "link",
133
+ })),
134
+ ...metas.map((props) => ({
126
135
  props: Object.fromEntries(
127
136
  Object.entries(props).map(([name, content]) => [
128
137
  name,
@@ -131,7 +140,7 @@ export async function ssr(App, data) {
131
140
  ),
132
141
  type: "meta",
133
142
  })),
134
- ),
143
+ ]),
135
144
  lang: i18n.getLocale(),
136
145
  title,
137
146
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "evikit",
3
3
  "description": "Preact/SQLite SSR framework for small web apps",
4
- "version": "2.5.1",
4
+ "version": "2.5.2",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./index.js",
package/server.js CHANGED
@@ -260,7 +260,11 @@ export function production({ html, prerender }) {
260
260
  renderToString(h("title", null, rendered.head.title)),
261
261
  )
262
262
  .replace(/<!--app-head-->[\s\S]+?<body/, (match) =>
263
- match.replace(/<meta[^>]+>/g, ""),
263
+ match
264
+ .replace(/<link[^>]+>/g, (link) =>
265
+ /data-hoofd/.test(link) ? "" : link,
266
+ )
267
+ .replace(/<meta[^>]+>/g, ""),
264
268
  )
265
269
  .replace(
266
270
  `<!--app-head-->`,
package/sqlite.js CHANGED
@@ -1,489 +1,489 @@
1
- import envPaths from "env-paths";
2
- import { fail, ok } from "node:assert/strict";
3
- import { mkdirSync } from "node:fs";
4
- import { DatabaseSync } from "node:sqlite";
5
- import { getMetadata, parse } from "valibot";
6
-
7
- import { flatten, relations } from "./db.js";
8
-
9
- /** @type {typeof import("./sqlite.d.ts").sqlite} */
10
- export function sqlite({ pkg, schema }) {
11
- const data = pkg
12
- ? (() => {
13
- const { data } = envPaths(pkg.name);
14
- mkdirSync(data, { recursive: true });
15
- return data;
16
- })()
17
- : undefined;
18
- const database = new DatabaseSync(data ? `${data}/db.sqlite3` : ":memory:", {
19
- timeout: 1000,
20
- });
21
- const flat = flatten(relations(schema));
22
-
23
- const haveTables = /** @type {{ name: string }[]} */ (
24
- database
25
- .prepare(
26
- "select * from sqlite_schema where type='table' and name not like 'sqlite_%'",
27
- )
28
- .all()
29
- ).map((table) => {
30
- const columns =
31
- /** @type {{ name: string, notnull: 0 | 1, pk: 0 | 1, type: "INTEGER" | "TEXT", }[]} */ (
32
- database.prepare(`pragma table_info(${table.name})`).all()
33
- );
34
- const foreignKeys =
35
- /** @type {{ from: string, on_delete: "CASCADE" | "SET NULL", on_update: "CASCADE" | "SET NULL", table: string, to: string, }[]} */ (
36
- database.prepare(`pragma foreign_key_list(${table.name})`).all()
37
- );
38
- return {
39
- columns,
40
- foreignKeys,
41
- indices: /** @type {{ name: string }[]} */ (
42
- database.prepare(`pragma index_list(${table.name})`).all()
43
- )
44
- .filter(({ name }) => !/^sqlite_/.test(name))
45
- .map((index) => ({
46
- ...index,
47
- info: /** @type {{ name: string; }} */ (
48
- database.prepare(`pragma index_info(${index.name})`).get()
49
- ),
50
- })),
51
- table,
52
- };
53
- });
54
-
55
- const wantTables = Object.entries(schema).map(([name, { entries }]) => ({
56
- columns: Object.entries(entries).map(([name, maybeEntry]) => {
57
- const entry =
58
- /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
59
- maybeEntry
60
- );
61
- const pk = /** @type {{ primary?: true }} */ (getMetadata(entry)).primary
62
- ? 1
63
- : 0;
64
- return {
65
- name,
66
- notnull: !pk && entry.expects.includes("undefined") ? 0 : 1,
67
- pk,
68
- type:
69
- entry.expects == "string" || entry.expects == "(string | undefined)"
70
- ? "TEXT"
71
- : entry.expects == "number" ||
72
- entry.expects == "(number | undefined)"
73
- ? "INTEGER"
74
- : entry.expects == "Uint8Array" ||
75
- entry.expects == "(Uint8Array | undefined)"
76
- ? "BLOB"
77
- : fail(name),
78
- };
79
- }),
80
- foreignKeys: Object.entries(entries)
81
- .map(([name, maybeEntry]) => {
82
- const entry =
83
- /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
84
- maybeEntry
85
- );
86
-
87
- const metadata =
88
- /** @type {{ key?: string, references?: any } | undefined} */ (
89
- getMetadata(entry)
90
- );
91
- const table = metadata
92
- ? Object.entries(schema)
93
- .filter(([, value]) => value == metadata.references)
94
- .find(Boolean)
95
- : undefined;
96
- if (!metadata?.references) {
97
- return;
98
- }
99
- ok(table);
100
- const to = Object.entries(table[1].entries).find(
101
- ([, value]) => value == metadata.key,
102
- );
103
- ok(to);
104
- return {
105
- from: name,
106
- on_delete: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
107
- on_update: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
108
- table: table[0],
109
- to: to[0],
110
- };
111
- })
112
- .filter(Boolean)
113
- .map((x) => {
114
- ok(x);
115
- return x;
116
- }),
117
- table: { name },
118
- }));
119
-
120
- const migration = [];
121
-
122
- for (const wantTable of wantTables) {
123
- const haveTable = haveTables.find(
124
- ({ table }) => table.name == wantTable.table.name,
125
- );
126
- if (haveTable) {
127
- for (const { info, name } of haveTable.indices) {
128
- if (!wantTable.foreignKeys.find(({ from }) => from == info.name)) {
129
- migration.push(`drop index ${name}`);
130
- }
131
- }
132
- const haveColumns = haveTable.columns;
133
- const columnsNowMissing = haveColumns.filter(
134
- (haveColumn) =>
135
- !wantTable.columns.find(({ name }) => name == haveColumn.name),
136
- );
137
- for (const { name, pk } of columnsNowMissing) {
138
- if (pk) {
139
- fail(`${haveTable.table.name}.${name}: Can't drop primary key`);
140
- }
141
- }
142
- for (const wantColumn of wantTable.columns) {
143
- const haveColumn = haveColumns.find(
144
- ({ name }) => name == wantColumn.name,
145
- );
146
-
147
- /** @param {typeof haveColumn} column */
148
- function checkConstraint(column) {
149
- ok(column);
150
- ok(haveTable);
151
- if (column.pk != wantColumn.pk) {
152
- fail(
153
- `${haveTable.table.name}.${column.name}: Can't toggle primary key`,
154
- );
155
- }
156
- if (
157
- !wantColumn.pk && // Without this condition I get an error with real database created by drizzle-kit.
158
- column.notnull != wantColumn.notnull
159
- ) {
160
- fail(
161
- `${haveTable.table.name}.${column.name}: Can't toggle optional`,
162
- );
163
- }
164
- }
165
-
166
- if (haveColumn) {
167
- checkConstraint(haveColumn);
168
- } else {
169
- const renamedColumn = columnsNowMissing.find(
170
- ({ notnull, type }) =>
171
- wantColumn.type == type && wantColumn.notnull == notnull,
172
- );
173
- if (renamedColumn) {
174
- checkConstraint(renamedColumn);
175
- columnsNowMissing.splice(
176
- columnsNowMissing.indexOf(renamedColumn),
177
- 1,
178
- );
179
- migration.push(
180
- `alter table ${wantTable.table.name} rename column ${renamedColumn.name} to ${wantColumn.name}`,
181
- );
182
- } else {
183
- if (wantColumn.pk) {
184
- fail(
185
- `${wantTable.table.name}.${wantColumn.name}: Can't add primary key`,
186
- );
187
- }
188
- migration.push(
189
- [
190
- `alter table ${wantTable.table.name} add column`,
191
- wantColumn.name,
192
- wantColumn.type,
193
- wantColumn.notnull ? "not null" : "",
194
- ...wantTable.foreignKeys
195
- .map(({ from, on_delete, on_update, table, to }) =>
196
- from == wantColumn.name
197
- ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
198
- : "",
199
- )
200
- .filter(Boolean),
201
- ]
202
- .filter(Boolean)
203
- .join(" "),
204
- );
205
- }
206
- }
207
-
208
- for (const { from } of wantTable.foreignKeys) {
209
- if (
210
- from == wantColumn.name &&
211
- !haveTable.indices.find(({ info }) => info.name == from)
212
- ) {
213
- migration.push(
214
- `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
215
- );
216
- }
217
- }
218
- }
219
- for (const missingColumn of columnsNowMissing) {
220
- migration.push(
221
- `alter table ${wantTable.table.name} drop column ${missingColumn.name}`,
222
- );
223
- }
224
- } else {
225
- if (!wantTable.columns.some(({ pk }) => pk)) {
226
- fail(`${wantTable.table.name} has no primary key`);
227
- }
228
- migration.push(
229
- `create table ${wantTable.table.name} (\n` +
230
- wantTable.columns
231
- .map(({ name, notnull, pk, type }) =>
232
- [
233
- name,
234
- type,
235
- pk ? "primary key" : "",
236
- !pk && notnull ? "not null" : "",
237
- ...wantTable.foreignKeys
238
- .map(({ from, on_delete, on_update, table, to }) =>
239
- from == name
240
- ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
241
- : "",
242
- )
243
- .filter(Boolean),
244
- ]
245
- .filter(Boolean)
246
- .join(" "),
247
- )
248
- .join(",\n") +
249
- `)`,
250
- );
251
- for (const { from } of wantTable.foreignKeys) {
252
- migration.push(
253
- `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
254
- );
255
- }
256
- }
257
- }
258
-
259
- for (const haveTable of haveTables) {
260
- const wantTable = wantTables.find(
261
- ({ table }) => table.name == haveTable.table.name,
262
- );
263
- if (!wantTable) {
264
- migration.push(`drop table ${haveTable.table.name}`);
265
- }
266
- }
267
-
268
- for (const sql of migration) {
269
- database.exec(sql);
270
- }
271
-
272
- return /** @type {any} */ (
273
- Object.fromEntries(
274
- Object.entries(schema).map(([maybeTable]) => {
275
- const table = /** @type {keyof typeof schema & string} */ (maybeTable);
276
-
277
- /** @param {{ [K in keyof (typeof schema)[typeof table]["entries"]]?: import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]> | import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]>[]; }} where */
278
- function ensureArrayRecord(where) {
279
- for (const key of Object.keys(where)) {
280
- if (!Array.isArray(where[key])) {
281
- Object.assign(where, { [key]: [where[key]] });
282
- }
283
- }
284
- }
285
-
286
- return [
287
- table,
288
- {
289
- /** @param {{ [K in keyof (typeof schema)[typeof table]["entries"]]?: import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]> | import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]>[]; }} where */
290
- delete(where) {
291
- ensureArrayRecord(where);
292
- database
293
- .prepare(
294
- `delete from ${table} where true\n` +
295
- Object.entries(where)
296
- .map(
297
- ([name, values]) =>
298
- `and ${name} in (${values.map(() => "?").join(", ")})`,
299
- )
300
- .join("\n"),
301
- )
302
- .run(
303
- ...Object.values(where)
304
- .flatMap((values) => values)
305
- .map((value) => value ?? null),
306
- );
307
- },
308
-
309
- /** @param {any} join */
310
- select(join) {
311
- return select({ database, flat, join, schema, table });
312
- },
313
-
314
- /** @param {{ set: Partial<import("valibot").InferOutput<(typeof schema)[typeof table]>>, where: { [K in keyof (typeof schema)[typeof table]["entries"]]?: import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]> | import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]>[]; } }} props */
315
- update({ set, where }) {
316
- ensureArrayRecord(where);
317
- database
318
- .prepare(
319
- `update ${table} set\n` +
320
- Object.keys(set)
321
- .map((name) => `${name} = ?`)
322
- .join(",\n") +
323
- `\nwhere true\n` +
324
- Object.entries(where)
325
- .map(
326
- ([name, values]) =>
327
- `and ${name} in (${values.map(() => "?").join(", ")})`,
328
- )
329
- .join("\n"),
330
- )
331
- .run(
332
- ...[
333
- ...Object.values(set).map((value) => value ?? null),
334
- ...Object.values(where)
335
- .flatMap((values) => values)
336
- .map((value) => value ?? null),
337
- ],
338
- );
339
- },
340
-
341
- /** @param {any[]} values */
342
- upsert(...values) {
343
- ok(schema[table]);
344
- const columns = Object.entries(schema[table].entries);
345
- const primaryKey = columns.find(
346
- ([, column]) =>
347
- /** @type {{ primary?: true }} */ (getMetadata(column))
348
- .primary,
349
- );
350
- ok(primaryKey);
351
- const query = database.prepare(
352
- `insert into ${table} (${columns.map(([name]) => name).join(", ")}) values\n` +
353
- `(${columns.map(() => "?").join(", ")})\n` +
354
- `on conflict (${primaryKey[0]}) do update set\n` +
355
- columns.map(([name]) => `${name} = ?`).join(",\n"),
356
- );
357
- const outputs = [];
358
- for (const row of values) {
359
- const output = parse(schema[table], row);
360
- const values = columns.map(([name]) => output[name] ?? null);
361
- query.run(...values, ...values);
362
- outputs.push(output);
363
- }
364
- return outputs;
365
- },
366
- },
367
- ];
368
- }),
369
- )
370
- );
371
- }
372
-
373
- /** @type {<Schema extends Record<string, import("valibot").ObjectSchema<any, any>>, R extends import("./db.d.ts").Relations<Schema> = import("./db.d.ts").Relations<Schema>, F extends import("./db.d.ts").Flatten<Schema> = import("./db.d.ts").Flatten<Schema>, Table extends keyof F & keyof Schema = keyof F & keyof Schema >(props: { database: DatabaseSync; flat: F; join?: import("./db.d.ts").Join<R, Table>; schema: Schema; table: Table; }) => any[]} */
374
- function select(props) {
375
- ok(typeof props.table == "string");
376
- let alias = `${props.table}_0`;
377
- let sql = `from ${props.table} ${alias}`;
378
-
379
- let columns = Object.keys(
380
- /** @type {any} */ (props.schema)[props.table].entries,
381
- ).map((name) => `${alias}.${name} ${alias}_${name}`);
382
-
383
- /** @type {Record<string, { entities: any[]; table: string }>} */
384
- const aliases = {};
385
- /**
386
- * @param {number} unique
387
- * @param {string} table
388
- * @param {string} alias
389
- * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
390
- */
391
- function join(unique, table, alias, ownJoin) {
392
- if (!aliases[alias]) {
393
- aliases[alias] = { entities: [], table };
394
- }
395
- for (const [name, itsJoin] of ownJoin ? Object.entries(ownJoin.with) : []) {
396
- const otherFlat = props.flat[table];
397
- ok(otherFlat);
398
- const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
399
- ok(relation);
400
-
401
- const { Its, Other, Own } =
402
- /** @type {{ Its: string, Other: string, Own: string }} */ (
403
- /** @type {any} */ (relation)
404
- );
405
- const otherAlias = `${name}_${unique + 1}`;
406
- if (!aliases[otherAlias]) {
407
- aliases[otherAlias] = { entities: [], table: Other };
408
- }
409
- columns = [
410
- ...columns,
411
-
412
- ...Object.keys(/** @type {any} */ (props.schema)[Other].entries).map(
413
- (name) => `${otherAlias}.${name} ${otherAlias}_${name}`,
414
- ),
415
- ];
416
- sql += `\n left join ${Other} ${otherAlias} on ${otherAlias}.${Its} = ${alias}.${Own}`;
417
- if (typeof itsJoin == "object") {
418
- ok(itsJoin);
419
- join(unique + 1, Other, otherAlias, itsJoin);
420
- }
421
- }
422
- }
423
-
424
- join(0, props.table, alias, /** @type {any} */ (props.join));
425
- sql = `select ${columns.join(",\n")} ${sql}`;
426
-
427
- const rows = props.database.prepare(sql).all();
428
- for (const [alias, state] of Object.entries(aliases)) {
429
- /** @type {Record<string, (typeof state.entities)[0]>} */
430
- const unique = {};
431
- for (const row of rows) {
432
- /** @type {Record<string, any>} */
433
- const unwrapped = {};
434
-
435
- let empty = true;
436
- for (const name of Object.keys(
437
- /** @type {any} */ (props.schema)[state.table].entries,
438
- )) {
439
- unwrapped[name] = row[`${alias}_${name}`] ?? undefined;
440
- if (unwrapped[name] !== undefined) {
441
- empty = false;
442
- }
443
- }
444
- if (!empty) {
445
- unique[JSON.stringify(unwrapped)] = unwrapped;
446
- }
447
- }
448
- state.entities = Object.values(unique);
449
- }
450
-
451
- alias = `${props.table}_0`;
452
- /**
453
- * @param {number} unique
454
- * @param {string} table
455
- * @param {string} alias
456
- * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
457
- */
458
- function merge(unique, table, alias, ownJoin) {
459
- ok(aliases[alias]);
460
- for (const [name, itsJoin] of ownJoin ? Object.entries(ownJoin.with) : []) {
461
- const otherFlat = props.flat[table];
462
- ok(otherFlat);
463
- const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
464
- ok(relation);
465
-
466
- const { Its, Other, Own, type } =
467
- /** @type {{ Its: string, Other: string, Own: string, type: "referencedBy" | "references" }} */ (
468
- /** @type {any} */ (relation)
469
- );
470
- const otherAlias = `${name}_${unique + 1}`;
471
- ok(aliases[otherAlias]?.entities);
472
- for (const self of aliases[alias].entities) {
473
- const matchingOthers = aliases[otherAlias].entities.filter(
474
- (other) => other[Its] == self[Own],
475
- );
476
- self[name] = type == "references" ? matchingOthers[0] : matchingOthers;
477
- }
478
- if (typeof itsJoin == "object") {
479
- ok(itsJoin);
480
- merge(unique + 1, Other, otherAlias, itsJoin);
481
- }
482
- }
483
- }
484
-
485
- merge(0, props.table, alias, /** @type {any} */ (props.join));
486
- const state = aliases[alias];
487
- ok(state);
488
- return state.entities;
489
- }
1
+ import envPaths from "env-paths";
2
+ import { fail, ok } from "node:assert/strict";
3
+ import { mkdirSync } from "node:fs";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { getMetadata, parse } from "valibot";
6
+
7
+ import { flatten, relations } from "./db.js";
8
+
9
+ /** @type {typeof import("./sqlite.d.ts").sqlite} */
10
+ export function sqlite({ pkg, schema }) {
11
+ const data = pkg
12
+ ? (() => {
13
+ const { data } = envPaths(pkg.name);
14
+ mkdirSync(data, { recursive: true });
15
+ return data;
16
+ })()
17
+ : undefined;
18
+ const database = new DatabaseSync(data ? `${data}/db.sqlite3` : ":memory:", {
19
+ timeout: 1000,
20
+ });
21
+ const flat = flatten(relations(schema));
22
+
23
+ const haveTables = /** @type {{ name: string }[]} */ (
24
+ database
25
+ .prepare(
26
+ "select * from sqlite_schema where type='table' and name not like 'sqlite_%'",
27
+ )
28
+ .all()
29
+ ).map((table) => {
30
+ const columns =
31
+ /** @type {{ name: string, notnull: 0 | 1, pk: 0 | 1, type: "INTEGER" | "TEXT", }[]} */ (
32
+ database.prepare(`pragma table_info(${table.name})`).all()
33
+ );
34
+ const foreignKeys =
35
+ /** @type {{ from: string, on_delete: "CASCADE" | "SET NULL", on_update: "CASCADE" | "SET NULL", table: string, to: string, }[]} */ (
36
+ database.prepare(`pragma foreign_key_list(${table.name})`).all()
37
+ );
38
+ return {
39
+ columns,
40
+ foreignKeys,
41
+ indices: /** @type {{ name: string }[]} */ (
42
+ database.prepare(`pragma index_list(${table.name})`).all()
43
+ )
44
+ .filter(({ name }) => !/^sqlite_/.test(name))
45
+ .map((index) => ({
46
+ ...index,
47
+ info: /** @type {{ name: string; }} */ (
48
+ database.prepare(`pragma index_info(${index.name})`).get()
49
+ ),
50
+ })),
51
+ table,
52
+ };
53
+ });
54
+
55
+ const wantTables = Object.entries(schema).map(([name, { entries }]) => ({
56
+ columns: Object.entries(entries).map(([name, maybeEntry]) => {
57
+ const entry =
58
+ /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
59
+ maybeEntry
60
+ );
61
+ const pk = /** @type {{ primary?: true }} */ (getMetadata(entry)).primary
62
+ ? 1
63
+ : 0;
64
+ return {
65
+ name,
66
+ notnull: !pk && entry.expects.includes("undefined") ? 0 : 1,
67
+ pk,
68
+ type:
69
+ entry.expects == "string" || entry.expects == "(string | undefined)"
70
+ ? "TEXT"
71
+ : entry.expects == "number" ||
72
+ entry.expects == "(number | undefined)"
73
+ ? "INTEGER"
74
+ : entry.expects == "Uint8Array" ||
75
+ entry.expects == "(Uint8Array | undefined)"
76
+ ? "BLOB"
77
+ : fail(name),
78
+ };
79
+ }),
80
+ foreignKeys: Object.entries(entries)
81
+ .map(([name, maybeEntry]) => {
82
+ const entry =
83
+ /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
84
+ maybeEntry
85
+ );
86
+
87
+ const metadata =
88
+ /** @type {{ key?: string, references?: any } | undefined} */ (
89
+ getMetadata(entry)
90
+ );
91
+ const table = metadata
92
+ ? Object.entries(schema)
93
+ .filter(([, value]) => value == metadata.references)
94
+ .find(Boolean)
95
+ : undefined;
96
+ if (!metadata?.references) {
97
+ return;
98
+ }
99
+ ok(table);
100
+ const to = Object.entries(table[1].entries).find(
101
+ ([, value]) => value == metadata.key,
102
+ );
103
+ ok(to);
104
+ return {
105
+ from: name,
106
+ on_delete: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
107
+ on_update: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
108
+ table: table[0],
109
+ to: to[0],
110
+ };
111
+ })
112
+ .filter(Boolean)
113
+ .map((x) => {
114
+ ok(x);
115
+ return x;
116
+ }),
117
+ table: { name },
118
+ }));
119
+
120
+ const migration = [];
121
+
122
+ for (const wantTable of wantTables) {
123
+ const haveTable = haveTables.find(
124
+ ({ table }) => table.name == wantTable.table.name,
125
+ );
126
+ if (haveTable) {
127
+ for (const { info, name } of haveTable.indices) {
128
+ if (!wantTable.foreignKeys.find(({ from }) => from == info.name)) {
129
+ migration.push(`drop index ${name}`);
130
+ }
131
+ }
132
+ const haveColumns = haveTable.columns;
133
+ const columnsNowMissing = haveColumns.filter(
134
+ (haveColumn) =>
135
+ !wantTable.columns.find(({ name }) => name == haveColumn.name),
136
+ );
137
+ for (const { name, pk } of columnsNowMissing) {
138
+ if (pk) {
139
+ fail(`${haveTable.table.name}.${name}: Can't drop primary key`);
140
+ }
141
+ }
142
+ for (const wantColumn of wantTable.columns) {
143
+ const haveColumn = haveColumns.find(
144
+ ({ name }) => name == wantColumn.name,
145
+ );
146
+
147
+ /** @param {typeof haveColumn} column */
148
+ function checkConstraint(column) {
149
+ ok(column);
150
+ ok(haveTable);
151
+ if (column.pk != wantColumn.pk) {
152
+ fail(
153
+ `${haveTable.table.name}.${column.name}: Can't toggle primary key`,
154
+ );
155
+ }
156
+ if (
157
+ !wantColumn.pk && // Without this condition I get an error with real database created by drizzle-kit.
158
+ column.notnull != wantColumn.notnull
159
+ ) {
160
+ fail(
161
+ `${haveTable.table.name}.${column.name}: Can't toggle optional`,
162
+ );
163
+ }
164
+ }
165
+
166
+ if (haveColumn) {
167
+ checkConstraint(haveColumn);
168
+ } else {
169
+ const renamedColumn = columnsNowMissing.find(
170
+ ({ notnull, type }) =>
171
+ wantColumn.type == type && wantColumn.notnull == notnull,
172
+ );
173
+ if (renamedColumn) {
174
+ checkConstraint(renamedColumn);
175
+ columnsNowMissing.splice(
176
+ columnsNowMissing.indexOf(renamedColumn),
177
+ 1,
178
+ );
179
+ migration.push(
180
+ `alter table ${wantTable.table.name} rename column ${renamedColumn.name} to ${wantColumn.name}`,
181
+ );
182
+ } else {
183
+ if (wantColumn.pk) {
184
+ fail(
185
+ `${wantTable.table.name}.${wantColumn.name}: Can't add primary key`,
186
+ );
187
+ }
188
+ migration.push(
189
+ [
190
+ `alter table ${wantTable.table.name} add column`,
191
+ wantColumn.name,
192
+ wantColumn.type,
193
+ wantColumn.notnull ? "not null" : "",
194
+ ...wantTable.foreignKeys
195
+ .map(({ from, on_delete, on_update, table, to }) =>
196
+ from == wantColumn.name
197
+ ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
198
+ : "",
199
+ )
200
+ .filter(Boolean),
201
+ ]
202
+ .filter(Boolean)
203
+ .join(" "),
204
+ );
205
+ }
206
+ }
207
+
208
+ for (const { from } of wantTable.foreignKeys) {
209
+ if (
210
+ from == wantColumn.name &&
211
+ !haveTable.indices.find(({ info }) => info.name == from)
212
+ ) {
213
+ migration.push(
214
+ `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
215
+ );
216
+ }
217
+ }
218
+ }
219
+ for (const missingColumn of columnsNowMissing) {
220
+ migration.push(
221
+ `alter table ${wantTable.table.name} drop column ${missingColumn.name}`,
222
+ );
223
+ }
224
+ } else {
225
+ if (!wantTable.columns.some(({ pk }) => pk)) {
226
+ fail(`${wantTable.table.name} has no primary key`);
227
+ }
228
+ migration.push(
229
+ `create table ${wantTable.table.name} (\n` +
230
+ wantTable.columns
231
+ .map(({ name, notnull, pk, type }) =>
232
+ [
233
+ name,
234
+ type,
235
+ pk ? "primary key" : "",
236
+ !pk && notnull ? "not null" : "",
237
+ ...wantTable.foreignKeys
238
+ .map(({ from, on_delete, on_update, table, to }) =>
239
+ from == name
240
+ ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
241
+ : "",
242
+ )
243
+ .filter(Boolean),
244
+ ]
245
+ .filter(Boolean)
246
+ .join(" "),
247
+ )
248
+ .join(",\n") +
249
+ `)`,
250
+ );
251
+ for (const { from } of wantTable.foreignKeys) {
252
+ migration.push(
253
+ `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
254
+ );
255
+ }
256
+ }
257
+ }
258
+
259
+ for (const haveTable of haveTables) {
260
+ const wantTable = wantTables.find(
261
+ ({ table }) => table.name == haveTable.table.name,
262
+ );
263
+ if (!wantTable) {
264
+ migration.push(`drop table ${haveTable.table.name}`);
265
+ }
266
+ }
267
+
268
+ for (const sql of migration) {
269
+ database.exec(sql);
270
+ }
271
+
272
+ return /** @type {any} */ (
273
+ Object.fromEntries(
274
+ Object.entries(schema).map(([maybeTable]) => {
275
+ const table = /** @type {keyof typeof schema & string} */ (maybeTable);
276
+
277
+ /** @param {{ [K in keyof (typeof schema)[typeof table]["entries"]]?: import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]> | import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]>[]; }} where */
278
+ function ensureArrayRecord(where) {
279
+ for (const key of Object.keys(where)) {
280
+ if (!Array.isArray(where[key])) {
281
+ Object.assign(where, { [key]: [where[key]] });
282
+ }
283
+ }
284
+ }
285
+
286
+ return [
287
+ table,
288
+ {
289
+ /** @param {{ [K in keyof (typeof schema)[typeof table]["entries"]]?: import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]> | import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]>[]; }} where */
290
+ delete(where) {
291
+ ensureArrayRecord(where);
292
+ database
293
+ .prepare(
294
+ `delete from ${table} where true\n` +
295
+ Object.entries(where)
296
+ .map(
297
+ ([name, values]) =>
298
+ `and ${name} in (${values.map(() => "?").join(", ")})`,
299
+ )
300
+ .join("\n"),
301
+ )
302
+ .run(
303
+ ...Object.values(where)
304
+ .flatMap((values) => values)
305
+ .map((value) => value ?? null),
306
+ );
307
+ },
308
+
309
+ /** @param {any} join */
310
+ select(join) {
311
+ return select({ database, flat, join, schema, table });
312
+ },
313
+
314
+ /** @param {{ set: Partial<import("valibot").InferOutput<(typeof schema)[typeof table]>>, where: { [K in keyof (typeof schema)[typeof table]["entries"]]?: import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]> | import("valibot").InferOutput<(typeof schema)[typeof table]["entries"][K]>[]; } }} props */
315
+ update({ set, where }) {
316
+ ensureArrayRecord(where);
317
+ database
318
+ .prepare(
319
+ `update ${table} set\n` +
320
+ Object.keys(set)
321
+ .map((name) => `${name} = ?`)
322
+ .join(",\n") +
323
+ `\nwhere true\n` +
324
+ Object.entries(where)
325
+ .map(
326
+ ([name, values]) =>
327
+ `and ${name} in (${values.map(() => "?").join(", ")})`,
328
+ )
329
+ .join("\n"),
330
+ )
331
+ .run(
332
+ ...[
333
+ ...Object.values(set).map((value) => value ?? null),
334
+ ...Object.values(where)
335
+ .flatMap((values) => values)
336
+ .map((value) => value ?? null),
337
+ ],
338
+ );
339
+ },
340
+
341
+ /** @param {any[]} values */
342
+ upsert(...values) {
343
+ ok(schema[table]);
344
+ const columns = Object.entries(schema[table].entries);
345
+ const primaryKey = columns.find(
346
+ ([, column]) =>
347
+ /** @type {{ primary?: true }} */ (getMetadata(column))
348
+ .primary,
349
+ );
350
+ ok(primaryKey);
351
+ const query = database.prepare(
352
+ `insert into ${table} (${columns.map(([name]) => name).join(", ")}) values\n` +
353
+ `(${columns.map(() => "?").join(", ")})\n` +
354
+ `on conflict (${primaryKey[0]}) do update set\n` +
355
+ columns.map(([name]) => `${name} = ?`).join(",\n"),
356
+ );
357
+ const outputs = [];
358
+ for (const row of values) {
359
+ const output = parse(schema[table], row);
360
+ const values = columns.map(([name]) => output[name] ?? null);
361
+ query.run(...values, ...values);
362
+ outputs.push(output);
363
+ }
364
+ return outputs;
365
+ },
366
+ },
367
+ ];
368
+ }),
369
+ )
370
+ );
371
+ }
372
+
373
+ /** @type {<Schema extends Record<string, import("valibot").ObjectSchema<any, any>>, R extends import("./db.d.ts").Relations<Schema> = import("./db.d.ts").Relations<Schema>, F extends import("./db.d.ts").Flatten<Schema> = import("./db.d.ts").Flatten<Schema>, Table extends keyof F & keyof Schema = keyof F & keyof Schema >(props: { database: DatabaseSync; flat: F; join?: import("./db.d.ts").Join<R, Table>; schema: Schema; table: Table; }) => any[]} */
374
+ function select(props) {
375
+ ok(typeof props.table == "string");
376
+ let alias = `${props.table}_0`;
377
+ let sql = `from ${props.table} ${alias}`;
378
+
379
+ let columns = Object.keys(
380
+ /** @type {any} */ (props.schema)[props.table].entries,
381
+ ).map((name) => `${alias}.${name} ${alias}_${name}`);
382
+
383
+ /** @type {Record<string, { entities: any[]; table: string }>} */
384
+ const aliases = {};
385
+ /**
386
+ * @param {number} unique
387
+ * @param {string} table
388
+ * @param {string} alias
389
+ * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
390
+ */
391
+ function join(unique, table, alias, ownJoin) {
392
+ if (!aliases[alias]) {
393
+ aliases[alias] = { entities: [], table };
394
+ }
395
+ for (const [name, itsJoin] of ownJoin ? Object.entries(ownJoin.with) : []) {
396
+ const otherFlat = props.flat[table];
397
+ ok(otherFlat);
398
+ const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
399
+ ok(relation);
400
+
401
+ const { Its, Other, Own } =
402
+ /** @type {{ Its: string, Other: string, Own: string }} */ (
403
+ /** @type {any} */ (relation)
404
+ );
405
+ const otherAlias = `${name}_${unique + 1}`;
406
+ if (!aliases[otherAlias]) {
407
+ aliases[otherAlias] = { entities: [], table: Other };
408
+ }
409
+ columns = [
410
+ ...columns,
411
+
412
+ ...Object.keys(/** @type {any} */ (props.schema)[Other].entries).map(
413
+ (name) => `${otherAlias}.${name} ${otherAlias}_${name}`,
414
+ ),
415
+ ];
416
+ sql += `\n left join ${Other} ${otherAlias} on ${otherAlias}.${Its} = ${alias}.${Own}`;
417
+ if (typeof itsJoin == "object") {
418
+ ok(itsJoin);
419
+ join(unique + 1, Other, otherAlias, itsJoin);
420
+ }
421
+ }
422
+ }
423
+
424
+ join(0, props.table, alias, /** @type {any} */ (props.join));
425
+ sql = `select ${columns.join(",\n")} ${sql}`;
426
+
427
+ const rows = props.database.prepare(sql).all();
428
+ for (const [alias, state] of Object.entries(aliases)) {
429
+ /** @type {Record<string, (typeof state.entities)[0]>} */
430
+ const unique = {};
431
+ for (const row of rows) {
432
+ /** @type {Record<string, any>} */
433
+ const unwrapped = {};
434
+
435
+ let empty = true;
436
+ for (const name of Object.keys(
437
+ /** @type {any} */ (props.schema)[state.table].entries,
438
+ )) {
439
+ unwrapped[name] = row[`${alias}_${name}`] ?? undefined;
440
+ if (unwrapped[name] !== undefined) {
441
+ empty = false;
442
+ }
443
+ }
444
+ if (!empty) {
445
+ unique[JSON.stringify(unwrapped)] = unwrapped;
446
+ }
447
+ }
448
+ state.entities = Object.values(unique);
449
+ }
450
+
451
+ alias = `${props.table}_0`;
452
+ /**
453
+ * @param {number} unique
454
+ * @param {string} table
455
+ * @param {string} alias
456
+ * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
457
+ */
458
+ function merge(unique, table, alias, ownJoin) {
459
+ ok(aliases[alias]);
460
+ for (const [name, itsJoin] of ownJoin ? Object.entries(ownJoin.with) : []) {
461
+ const otherFlat = props.flat[table];
462
+ ok(otherFlat);
463
+ const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
464
+ ok(relation);
465
+
466
+ const { Its, Other, Own, type } =
467
+ /** @type {{ Its: string, Other: string, Own: string, type: "referencedBy" | "references" }} */ (
468
+ /** @type {any} */ (relation)
469
+ );
470
+ const otherAlias = `${name}_${unique + 1}`;
471
+ ok(aliases[otherAlias]?.entities);
472
+ for (const self of aliases[alias].entities) {
473
+ const matchingOthers = aliases[otherAlias].entities.filter(
474
+ (other) => other[Its] == self[Own],
475
+ );
476
+ self[name] = type == "references" ? matchingOthers[0] : matchingOthers;
477
+ }
478
+ if (typeof itsJoin == "object") {
479
+ ok(itsJoin);
480
+ merge(unique + 1, Other, otherAlias, itsJoin);
481
+ }
482
+ }
483
+ }
484
+
485
+ merge(0, props.table, alias, /** @type {any} */ (props.join));
486
+ const state = aliases[alias];
487
+ ok(state);
488
+ return state.entities;
489
+ }