evikit 2.5.2 → 2.6.0

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