evikit 1.0.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/sqlite.js ADDED
@@ -0,0 +1,475 @@
1
+ import { fail, ok } from "node:assert/strict";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import { getMetadata, parse } from "valibot";
4
+
5
+ import { flatten, relations } from "./db.js";
6
+
7
+ /** @type {typeof import("./sqlite.d.ts").sqlite} */
8
+ export function sqlite(path, { schema }) {
9
+ const database = new DatabaseSync(path, { timeout: 1000 });
10
+ const flat = flatten(relations(schema));
11
+
12
+ const haveTables = /** @type {{ name: string }[]} */ (
13
+ database
14
+ .prepare(
15
+ "select * from sqlite_schema where type='table' and name not like 'sqlite_%'",
16
+ )
17
+ .all()
18
+ ).map((table) => {
19
+ const columns =
20
+ /** @type {{ name: string, notnull: 0 | 1, pk: 0 | 1, type: "INTEGER" | "TEXT", }[]} */ (
21
+ database.prepare(`pragma table_info(${table.name})`).all()
22
+ );
23
+ const foreignKeys =
24
+ /** @type {{ from: string, on_delete: "CASCADE" | "SET NULL", on_update: "CASCADE" | "SET NULL", table: string, to: string, }[]} */ (
25
+ database.prepare(`pragma foreign_key_list(${table.name})`).all()
26
+ );
27
+ return {
28
+ columns,
29
+ foreignKeys,
30
+ indices: /** @type {{ name: string }[]} */ (
31
+ database.prepare(`pragma index_list(${table.name})`).all()
32
+ )
33
+ .filter(({ name }) => !/^sqlite_/.test(name))
34
+ .map((index) => ({
35
+ ...index,
36
+ info: /** @type {{ name: string; }} */ (
37
+ database.prepare(`pragma index_info(${index.name})`).get()
38
+ ),
39
+ })),
40
+ table,
41
+ };
42
+ });
43
+
44
+ const wantTables = Object.entries(schema).map(([name, { entries }]) => ({
45
+ columns: Object.entries(entries).map(([name, maybeEntry]) => {
46
+ const entry =
47
+ /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
48
+ maybeEntry
49
+ );
50
+ const pk = /** @type {{ primary?: true }} */ (getMetadata(entry)).primary
51
+ ? 1
52
+ : 0;
53
+ return {
54
+ name,
55
+ notnull: !pk && entry.expects.includes("undefined") ? 0 : 1,
56
+ pk,
57
+ type:
58
+ entry.expects == "string" || entry.expects == "(string | undefined)"
59
+ ? "TEXT"
60
+ : entry.expects == "number" ||
61
+ entry.expects == "(number | undefined)"
62
+ ? "INTEGER"
63
+ : fail(name),
64
+ };
65
+ }),
66
+ foreignKeys: Object.entries(entries)
67
+ .map(([name, maybeEntry]) => {
68
+ const entry =
69
+ /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
70
+ maybeEntry
71
+ );
72
+
73
+ const metadata =
74
+ /** @type {{ key?: string, references?: any } | undefined} */ (
75
+ getMetadata(entry)
76
+ );
77
+ const table = metadata
78
+ ? Object.entries(schema)
79
+ .filter(([, value]) => value == metadata.references)
80
+ .find(Boolean)
81
+ : undefined;
82
+ if (!metadata?.references) {
83
+ return;
84
+ }
85
+ ok(table);
86
+ const to = Object.entries(table[1].entries).find(
87
+ ([, value]) => value == metadata.key,
88
+ );
89
+ ok(to);
90
+ return {
91
+ from: name,
92
+ on_delete: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
93
+ on_update: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
94
+ table: table[0],
95
+ to: to[0],
96
+ };
97
+ })
98
+ .filter(Boolean)
99
+ .map((x) => {
100
+ ok(x);
101
+ return x;
102
+ }),
103
+ table: { name },
104
+ }));
105
+
106
+ const migration = [];
107
+
108
+ for (const wantTable of wantTables) {
109
+ const haveTable = haveTables.find(
110
+ ({ table }) => table.name == wantTable.table.name,
111
+ );
112
+ if (haveTable) {
113
+ for (const { info, name } of haveTable.indices) {
114
+ if (!wantTable.foreignKeys.find(({ from }) => from == info.name)) {
115
+ migration.push(`drop index ${name}`);
116
+ }
117
+ }
118
+ const haveColumns = haveTable.columns;
119
+ const columnsNowMissing = haveColumns.filter(
120
+ (haveColumn) =>
121
+ !wantTable.columns.find(({ name }) => name == haveColumn.name),
122
+ );
123
+ for (const { name, pk } of columnsNowMissing) {
124
+ if (pk) {
125
+ fail(`${haveTable.table.name}.${name}: Can't drop primary key`);
126
+ }
127
+ }
128
+ for (const wantColumn of wantTable.columns) {
129
+ const haveColumn = haveColumns.find(
130
+ ({ name }) => name == wantColumn.name,
131
+ );
132
+
133
+ /** @param {typeof haveColumn} column */
134
+ function checkConstraint(column) {
135
+ ok(column);
136
+ ok(haveTable);
137
+ if (column.pk != wantColumn.pk) {
138
+ fail(
139
+ `${haveTable.table.name}.${column.name}: Can't toggle primary key`,
140
+ );
141
+ }
142
+ if (
143
+ !wantColumn.pk && // Without this condition I get an error with real database created by drizzle-kit.
144
+ column.notnull != wantColumn.notnull
145
+ ) {
146
+ fail(
147
+ `${haveTable.table.name}.${column.name}: Can't toggle optional`,
148
+ );
149
+ }
150
+ }
151
+
152
+ if (haveColumn) {
153
+ checkConstraint(haveColumn);
154
+ } else {
155
+ const renamedColumn = columnsNowMissing.find(
156
+ ({ notnull, type }) =>
157
+ wantColumn.type == type && wantColumn.notnull == notnull,
158
+ );
159
+ if (renamedColumn) {
160
+ checkConstraint(renamedColumn);
161
+ columnsNowMissing.splice(
162
+ columnsNowMissing.indexOf(renamedColumn),
163
+ 1,
164
+ );
165
+ migration.push(
166
+ `alter table ${wantTable.table.name} rename column ${renamedColumn.name} to ${wantColumn.name}`,
167
+ );
168
+ } else {
169
+ if (wantColumn.pk) {
170
+ fail(
171
+ `${wantTable.table.name}.${wantColumn.name}: Can't add primary key`,
172
+ );
173
+ }
174
+ migration.push(
175
+ [
176
+ `alter table ${wantTable.table.name} add column`,
177
+ wantColumn.name,
178
+ wantColumn.type,
179
+ wantColumn.notnull ? "not null" : "",
180
+ ...wantTable.foreignKeys
181
+ .map(({ from, on_delete, on_update, table, to }) =>
182
+ from == wantColumn.name
183
+ ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
184
+ : "",
185
+ )
186
+ .filter(Boolean),
187
+ ]
188
+ .filter(Boolean)
189
+ .join(" "),
190
+ );
191
+ }
192
+ }
193
+
194
+ for (const { from } of wantTable.foreignKeys) {
195
+ if (
196
+ from == wantColumn.name &&
197
+ !haveTable.indices.find(({ info }) => info.name == from)
198
+ ) {
199
+ migration.push(
200
+ `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
201
+ );
202
+ }
203
+ }
204
+ }
205
+ for (const missingColumn of columnsNowMissing) {
206
+ migration.push(
207
+ `alter table ${wantTable.table.name} drop column ${missingColumn.name}`,
208
+ );
209
+ }
210
+ } else {
211
+ if (!wantTable.columns.some(({ pk }) => pk)) {
212
+ fail(`${wantTable.table.name} has no primary key`);
213
+ }
214
+ migration.push(
215
+ `create table ${wantTable.table.name} (\n` +
216
+ wantTable.columns
217
+ .map(({ name, notnull, pk, type }) =>
218
+ [
219
+ name,
220
+ type,
221
+ pk ? "primary key" : "",
222
+ !pk && notnull ? "not null" : "",
223
+ ...wantTable.foreignKeys
224
+ .map(({ from, on_delete, on_update, table, to }) =>
225
+ from == name
226
+ ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
227
+ : "",
228
+ )
229
+ .filter(Boolean),
230
+ ]
231
+ .filter(Boolean)
232
+ .join(" "),
233
+ )
234
+ .join(",\n") +
235
+ `)`,
236
+ );
237
+ for (const { from } of wantTable.foreignKeys) {
238
+ migration.push(
239
+ `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
240
+ );
241
+ }
242
+ }
243
+ }
244
+
245
+ for (const haveTable of haveTables) {
246
+ const wantTable = wantTables.find(
247
+ ({ table }) => table.name == haveTable.table.name,
248
+ );
249
+ if (!wantTable) {
250
+ migration.push(`drop table ${haveTable.table.name}`);
251
+ }
252
+ }
253
+
254
+ for (const sql of migration) {
255
+ database.exec(sql);
256
+ }
257
+
258
+ return /** @type {any} */ (
259
+ Object.fromEntries(
260
+ Object.entries(schema).map(([maybeTable]) => {
261
+ const table = /** @type {keyof typeof schema & string} */ (maybeTable);
262
+
263
+ /** @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 */
264
+ function ensureArrayRecord(where) {
265
+ for (const key of Object.keys(where)) {
266
+ if (!Array.isArray(where[key])) {
267
+ Object.assign(where, { [key]: [where[key]] });
268
+ }
269
+ }
270
+ }
271
+
272
+ return [
273
+ table,
274
+ {
275
+ /** @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 */
276
+ delete(where) {
277
+ ensureArrayRecord(where);
278
+ database
279
+ .prepare(
280
+ `delete from ${table} where true\n` +
281
+ Object.entries(where)
282
+ .map(
283
+ ([name, values]) =>
284
+ `and ${name} in (${values.map(() => "?").join(", ")})`,
285
+ )
286
+ .join("\n"),
287
+ )
288
+ .run(
289
+ ...Object.values(where)
290
+ .flatMap((values) => values)
291
+ .map((value) => value ?? null),
292
+ );
293
+ },
294
+
295
+ /** @param {any} join */
296
+ select(join) {
297
+ return select({ database, flat, join, schema, table });
298
+ },
299
+
300
+ /** @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 */
301
+ update({ set, where }) {
302
+ ensureArrayRecord(where);
303
+ database
304
+ .prepare(
305
+ `update ${table} set\n` +
306
+ Object.keys(set)
307
+ .map((name) => `${name} = ?`)
308
+ .join(",\n") +
309
+ `\nwhere true\n` +
310
+ Object.entries(where)
311
+ .map(
312
+ ([name, values]) =>
313
+ `and ${name} in (${values.map(() => "?").join(", ")})`,
314
+ )
315
+ .join("\n"),
316
+ )
317
+ .run(
318
+ ...[
319
+ ...Object.values(set).map((value) => value ?? null),
320
+ ...Object.values(where)
321
+ .flatMap((values) => values)
322
+ .map((value) => value ?? null),
323
+ ],
324
+ );
325
+ },
326
+
327
+ /** @param {any[]} values */
328
+ upsert(...values) {
329
+ ok(schema[table]);
330
+ const columns = Object.entries(schema[table].entries);
331
+ const primaryKey = columns.find(
332
+ ([, column]) =>
333
+ /** @type {{ primary?: true }} */ (getMetadata(column))
334
+ .primary,
335
+ );
336
+ ok(primaryKey);
337
+ const query = database.prepare(
338
+ `insert into ${table} (${columns.map(([name]) => name).join(", ")}) values\n` +
339
+ `(${columns.map(() => "?").join(", ")})\n` +
340
+ `on conflict (${primaryKey[0]}) do update set\n` +
341
+ columns.map(([name]) => `${name} = ?`).join(",\n"),
342
+ );
343
+ const outputs = [];
344
+ for (const row of values) {
345
+ const output = parse(schema[table], row);
346
+ const values = columns.map(([name]) => output[name] ?? null);
347
+ query.run(...values, ...values);
348
+ outputs.push(output);
349
+ }
350
+ return outputs;
351
+ },
352
+ },
353
+ ];
354
+ }),
355
+ )
356
+ );
357
+ }
358
+
359
+ /** @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[]} */
360
+ function select(props) {
361
+ ok(typeof props.table == "string");
362
+ let alias = `${props.table}_0`;
363
+ let sql = `from ${props.table} ${alias}`;
364
+
365
+ let columns = Object.keys(
366
+ /** @type {any} */ (props.schema)[props.table].entries,
367
+ ).map((name) => `${alias}.${name} ${alias}_${name}`);
368
+
369
+ /** @type {Record<string, { entities: any[]; table: string }>} */
370
+ const aliases = {};
371
+ /**
372
+ * @param {number} unique
373
+ * @param {string} table
374
+ * @param {string} alias
375
+ * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
376
+ */
377
+ function join(unique, table, alias, ownJoin) {
378
+ if (!aliases[alias]) {
379
+ aliases[alias] = { entities: [], table };
380
+ }
381
+ for (const [name, itsJoin] of ownJoin ? Object.entries(ownJoin.with) : []) {
382
+ const otherFlat = props.flat[table];
383
+ ok(otherFlat);
384
+ const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
385
+ ok(relation);
386
+
387
+ const { Its, Other, Own } =
388
+ /** @type {{ Its: string, Other: string, Own: string }} */ (
389
+ /** @type {any} */ (relation)
390
+ );
391
+ const otherAlias = `${name}_${unique + 1}`;
392
+ if (!aliases[otherAlias]) {
393
+ aliases[otherAlias] = { entities: [], table: Other };
394
+ }
395
+ columns = [
396
+ ...columns,
397
+
398
+ ...Object.keys(/** @type {any} */ (props.schema)[Other].entries).map(
399
+ (name) => `${otherAlias}.${name} ${otherAlias}_${name}`,
400
+ ),
401
+ ];
402
+ sql += `\n left join ${Other} ${otherAlias} on ${otherAlias}.${Its} = ${alias}.${Own}`;
403
+ if (typeof itsJoin == "object") {
404
+ ok(itsJoin);
405
+ join(unique + 1, Other, otherAlias, itsJoin);
406
+ }
407
+ }
408
+ }
409
+
410
+ join(0, props.table, alias, /** @type {any} */ (props.join));
411
+ sql = `select ${columns.join(",\n")} ${sql}`;
412
+
413
+ const rows = props.database.prepare(sql).all();
414
+ for (const [alias, state] of Object.entries(aliases)) {
415
+ /** @type {Record<string, (typeof state.entities)[0]>} */
416
+ const unique = {};
417
+ for (const row of rows) {
418
+ /** @type {Record<string, any>} */
419
+ const unwrapped = {};
420
+
421
+ let empty = true;
422
+ for (const name of Object.keys(
423
+ /** @type {any} */ (props.schema)[state.table].entries,
424
+ )) {
425
+ unwrapped[name] = row[`${alias}_${name}`] ?? undefined;
426
+ if (unwrapped[name] !== undefined) {
427
+ empty = false;
428
+ }
429
+ }
430
+ if (!empty) {
431
+ unique[JSON.stringify(unwrapped)] = unwrapped;
432
+ }
433
+ }
434
+ state.entities = Object.values(unique);
435
+ }
436
+
437
+ alias = `${props.table}_0`;
438
+ /**
439
+ * @param {number} unique
440
+ * @param {string} table
441
+ * @param {string} alias
442
+ * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
443
+ */
444
+ function merge(unique, table, alias, ownJoin) {
445
+ ok(aliases[alias]);
446
+ for (const [name, itsJoin] of ownJoin ? Object.entries(ownJoin.with) : []) {
447
+ const otherFlat = props.flat[table];
448
+ ok(otherFlat);
449
+ const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
450
+ ok(relation);
451
+
452
+ const { Its, Other, Own, type } =
453
+ /** @type {{ Its: string, Other: string, Own: string, type: "referencedBy" | "references" }} */ (
454
+ /** @type {any} */ (relation)
455
+ );
456
+ const otherAlias = `${name}_${unique + 1}`;
457
+ ok(aliases[otherAlias]?.entities);
458
+ for (const self of aliases[alias].entities) {
459
+ const matchingOthers = aliases[otherAlias].entities.filter(
460
+ (other) => other[Its] == self[Own],
461
+ );
462
+ self[name] = type == "references" ? matchingOthers[0] : matchingOthers;
463
+ }
464
+ if (typeof itsJoin == "object") {
465
+ ok(itsJoin);
466
+ merge(unique + 1, Other, otherAlias, itsJoin);
467
+ }
468
+ }
469
+ }
470
+
471
+ merge(0, props.table, alias, /** @type {any} */ (props.join));
472
+ const state = aliases[alias];
473
+ ok(state);
474
+ return state.entities;
475
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "dist",
4
+ "module": "nodenext",
5
+ "target": "esnext",
6
+ "types": [],
7
+ "sourceMap": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "noUncheckedIndexedAccess": true,
11
+ "exactOptionalPropertyTypes": true,
12
+ "noImplicitReturns": true,
13
+ "noImplicitOverride": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noFallthroughCasesInSwitch": true,
17
+ "noPropertyAccessFromIndexSignature": true,
18
+ "strict": true,
19
+ "jsx": "react-jsx",
20
+ "verbatimModuleSyntax": true,
21
+ "isolatedModules": true,
22
+ "noUncheckedSideEffectImports": true,
23
+ "moduleDetection": "force",
24
+ "skipLibCheck": true,
25
+ "allowJs": true,
26
+ "checkJs": true,
27
+ "noEmit": true,
28
+ "esModuleInterop": true
29
+ }
30
+ }
package/use-api.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { type InferOutput, type ObjectSchema } from "valibot";
2
+
3
+ export function queryApi<T, U extends ObjectSchema<T, undefined>>(props: {
4
+ lang?: string;
5
+ origin: string | undefined;
6
+ path: string;
7
+ schema?: U;
8
+ }): { queryFn: () => Promise<InferOutput<U>>; queryKey: string[] };
9
+
10
+ export function useApi<T, U extends ObjectSchema<T, undefined>>(routeType: {
11
+ outputs: { 200: U };
12
+ }): { data?: InferOutput<U>; error?: unknown };
package/use-api.js ADDED
@@ -0,0 +1,46 @@
1
+ import { useRoute } from "preact-iso";
2
+ import { parse } from "valibot";
3
+
4
+ import { jsdomCookie } from "./jsdom-cookie.js";
5
+ import { useQuery } from "./query.js";
6
+
7
+ /** @param {{ lang?: string, origin: string | undefined, path: string, schema?: any }} props */
8
+ export function queryApi({ lang, origin, path, schema }) {
9
+ const queryKey =
10
+ path == "/" ? ["home"] : path.split("/").slice(1).map(decodeURIComponent);
11
+
12
+ return {
13
+ queryFn: async () => {
14
+ const response = await fetch(
15
+ (origin || "") +
16
+ path.replace(/^\//, "/api/").replace(/^\/api\/$/, "/api"),
17
+ {
18
+ headers: {
19
+ ...(lang ? { "Accept-Language": lang } : {}),
20
+ Cookie: jsdomCookie.setCookie.join("; "),
21
+ },
22
+ },
23
+ );
24
+ if (response.status >= 400) {
25
+ const error = new Error(await response.text());
26
+ error.name = response.status.toString();
27
+ throw Error;
28
+ }
29
+ const json = await response.json();
30
+ return schema ? parse(schema, json) : json;
31
+ },
32
+ queryKey,
33
+ };
34
+ }
35
+
36
+ /** @param {{ outputs: { 200: any } }} routeType */
37
+ export function useApi(routeType) {
38
+ const { path } = useRoute();
39
+ const { queryFn, queryKey } = queryApi({
40
+ origin: location.origin,
41
+ path,
42
+ schema: routeType.outputs[200],
43
+ });
44
+ const { data, error } = useQuery({ queryFn, queryKey });
45
+ return { data, error };
46
+ }
@@ -0,0 +1,19 @@
1
+ import {
2
+ type InferOutput,
3
+ type ObjectSchema,
4
+ type StringSchema,
5
+ } from "valibot";
6
+
7
+ export function useEnhance<
8
+ T,
9
+ U extends ObjectSchema<T, undefined> | StringSchema<undefined>,
10
+ >(
11
+ schema: { outputs: Record<number, U> },
12
+ onSuccess?: (output: InferOutput<U>) => void,
13
+ ): {
14
+ busy: boolean;
15
+ onSubmit: (event: {
16
+ preventDefault: () => void;
17
+ target: EventTarget | null;
18
+ }) => void;
19
+ };
package/use-enhance.js ADDED
@@ -0,0 +1,99 @@
1
+ import { useState } from "preact/hooks";
2
+ import { parse } from "valibot";
3
+
4
+ import { jsdomCookie } from "./jsdom-cookie.js";
5
+ import { useQueryClient } from "./query.js";
6
+
7
+ /**
8
+ * @param {{ outputs: Record<number, import("valibot").ObjectSchema<any, undefined> | import("valibot").StringSchema<undefined>> }} schema
9
+ * @param {(output: any) => any} [onSuccess]
10
+ */
11
+ export function useEnhance(schema, onSuccess) {
12
+ const [busy, setBusy] = useState(false);
13
+ const queryClient = useQueryClient();
14
+
15
+ /** @param {{ preventDefault: () => void, target: EventTarget | null }} event */
16
+ async function onSubmit(event) {
17
+ event.preventDefault();
18
+ if (busy) {
19
+ return;
20
+ }
21
+ setBusy(true);
22
+ const form = /** @type {HTMLFormElement} */ (event.target);
23
+ let output;
24
+ try {
25
+ const body = new URLSearchParams();
26
+ for (const input of [...form.querySelectorAll("input")]) {
27
+ if (
28
+ !input.disabled &&
29
+ ((input.type != "checkbox" && input.type != "radio") || input.checked)
30
+ ) {
31
+ body.append(input.name, input.value);
32
+ }
33
+ }
34
+ for (const select of [...form.querySelectorAll("select")]) {
35
+ if (!select.disabled) {
36
+ body.append(select.name, select.value);
37
+ }
38
+ }
39
+ for (const textarea of [...form.querySelectorAll("textarea")]) {
40
+ if (!textarea.disabled) {
41
+ body.append(textarea.name, textarea.value);
42
+ }
43
+ }
44
+ const response = await fetch(form.action, {
45
+ body: body.toString(),
46
+ headers: {
47
+ Accept: "application/json",
48
+ "Content-Type": "application/x-www-form-urlencoded",
49
+ Cookie: jsdomCookie.setCookie.join("; "),
50
+ },
51
+ method: form.method,
52
+ redirect: "manual", // Do not follow, just run callback.
53
+ });
54
+ const setCookie = response.headers.getSetCookie();
55
+ if (setCookie.length) {
56
+ jsdomCookie.setCookie = [...jsdomCookie.setCookie, ...setCookie];
57
+ }
58
+ if (response.status >= 400) {
59
+ throw new Error(await response.text());
60
+ }
61
+ const json = await response.json();
62
+ if (json.opaqueRedirectWorkaround) {
63
+ const error = new Error(json.opaqueRedirectWorkaround.location);
64
+ error.name = json.opaqueRedirectWorkaround.status;
65
+ throw error;
66
+ }
67
+ const outputSchema =
68
+ schema.outputs[
69
+ json.opaqueRedirectWorkaround?.status || response.status
70
+ ];
71
+ if (!outputSchema) {
72
+ const error = new Error(response.status.toString());
73
+ error.name = response.status.toString();
74
+ throw error;
75
+ }
76
+ output = parse(outputSchema, json);
77
+ } catch (e) {
78
+ const error = /** @type {Error} */ (e);
79
+ if (error.name == "302") {
80
+ if (new URL(location.href).pathname != error.message) {
81
+ location.href = error.message;
82
+ return;
83
+ }
84
+ } else {
85
+ console.error(error);
86
+ alert(error.message);
87
+ return;
88
+ }
89
+ } finally {
90
+ setTimeout(() => {
91
+ setBusy(false);
92
+ }, 1e3);
93
+ }
94
+ onSuccess?.(output);
95
+ queryClient.invalidateQueries();
96
+ }
97
+
98
+ return { busy, onSubmit };
99
+ }