@drhomet/mithaq 0.1.1 → 0.2.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/LICENSES/openstatus-time-picker.txt +21 -0
- package/README.md +161 -0
- package/dist/asas/build.d.ts +10 -2
- package/dist/asas/build.js +69 -22
- package/dist/asas/cli/mithaq.js +35 -3
- package/dist/asas/cli/upgrade.d.ts +22 -9
- package/dist/asas/cli/upgrade.js +187 -78
- package/dist/asas/http/fetch.d.ts +21 -3
- package/dist/asas/http/fetch.js +123 -60
- package/dist/asas/http/orpc.d.ts +5 -3
- package/dist/asas/http/orpc.js +126 -108
- package/dist/asas/runner/bulk.d.ts +1 -1
- package/dist/asas/runner/bulk.js +16 -11
- package/dist/asas/runner/errors.d.ts +5 -2
- package/dist/asas/runner/errors.js +18 -2
- package/dist/asas/runner/read.d.ts +6 -2
- package/dist/asas/runner/read.js +28 -6
- package/dist/asas/runner/steps.d.ts +22 -4
- package/dist/asas/runner/steps.js +15 -9
- package/dist/asas/runner/store.d.ts +4 -0
- package/dist/asas/runner/store.js +21 -0
- package/dist/asas/runner/tenant.d.ts +2 -2
- package/dist/asas/runner/tenant.js +78 -16
- package/dist/asas/runner/write.d.ts +173 -11
- package/dist/asas/runner/write.js +274 -46
- package/dist/auth/better-auth/access.d.ts +14 -0
- package/dist/auth/better-auth/access.js +32 -0
- package/dist/auth/better-auth/generate.d.ts +3 -1
- package/dist/auth/better-auth/generate.js +26 -8
- package/dist/auth/better-auth/index.d.ts +2 -0
- package/dist/auth/better-auth/tenant.d.ts +11 -3
- package/dist/auth/better-auth/tenant.js +10 -2
- package/dist/auth/local/index.js +5 -3
- package/dist/base/fields.d.ts +3 -2
- package/dist/base/fields.js +3 -2
- package/dist/base/index.d.ts +2 -1
- package/dist/base/json-schema.d.ts +7 -0
- package/dist/base/json-schema.js +35 -0
- package/dist/base/kinds.d.ts +21 -0
- package/dist/base/kinds.js +46 -1
- package/dist/base/rules.d.ts +4 -4
- package/dist/base/rules.js +4 -3
- package/dist/drizzle/index.d.ts +9 -139
- package/dist/drizzle/index.js +143 -24
- package/dist/drizzle/kinds.d.ts +1 -0
- package/dist/drizzle/kinds.js +3 -1
- package/dist/drizzle/migrate.d.ts +26 -0
- package/dist/drizzle/migrate.js +69 -26
- package/dist/drizzle/rules.d.ts +1 -1
- package/dist/index.d.ts +11 -21
- package/dist/index.js +8 -18
- package/dist/mithaq/define.d.ts +62 -15
- package/dist/mithaq/define.js +23 -9
- package/dist/mithaq/generate-messages.js +5 -4
- package/dist/mithaq/language.d.ts +43 -4
- package/dist/mithaq/message-file.d.ts +13 -0
- package/dist/mithaq/message-file.js +29 -0
- package/dist/mithaq/messages.js +5 -0
- package/dist/mithaq/resolve.js +70 -10
- package/dist/mithaq/types.d.ts +29 -1
- package/dist/rbac/index.d.ts +2 -2
- package/dist/rbac/index.js +1 -1
- package/dist/rich-text/index.d.ts +1 -0
- package/dist/rich-text/index.js +1 -0
- package/dist/rich-text/rich-text.d.ts +2 -55
- package/dist/rich-text/rich-text.js +2 -125
- package/dist/rich-text/value.d.ts +55 -0
- package/dist/rich-text/value.js +126 -0
- package/dist/screen/client.d.ts +5 -2
- package/dist/screen/client.js +5 -2
- package/dist/screen/generate.js +48 -6
- package/dist/screen/index.d.ts +18 -1
- package/dist/screen/index.js +24 -6
- package/dist/screen/react.d.ts +2 -0
- package/dist/storage/index.d.ts +2 -3
- package/dist/storage/index.js +1 -2
- package/dist/storage/storage.d.ts +2 -0
- package/dist/storage/storage.js +9 -2
- package/dist/targets.d.ts +14 -0
- package/dist/targets.js +16 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +81 -0
- package/dist/zod/index.d.ts +12 -3
- package/dist/zod/index.js +6 -5
- package/dist/zod/kinds.d.ts +3 -1
- package/dist/zod/kinds.js +11 -2
- package/dist/zod/rules.d.ts +1 -1
- package/docs/packages/mithaq/README.md +962 -0
- package/docs/packages/mithaq/src/asas/cli/README.md +252 -0
- package/docs/packages/mithaq/src/asas/runner/README.md +472 -0
- package/docs/packages/mithaq/src/drizzle/README.md +39 -0
- package/docs/packages/mithaq-admin/README.md +312 -0
- package/docs/templates/asas/README.md +1056 -0
- package/package.json +37 -8
- package/skills/add-field/SKILL.md +27 -0
- package/skills/add-kayan/SKILL.md +26 -0
- package/skills/eject-admin-piece/SKILL.md +28 -0
- package/skills/upgrade-mithaq/SKILL.md +35 -0
- package/r/mithaq-cells.json +0 -20
- package/r/mithaq-controls.json +0 -37
- package/r/mithaq-directional-icon.json +0 -15
- package/r/mithaq-expected-updated-at.json +0 -15
- package/r/mithaq-field-shell.json +0 -18
- package/r/mithaq-filter-operators.json +0 -15
- package/r/mithaq-form.json +0 -33
- package/r/mithaq-list-columns.json +0 -23
- package/r/mithaq-list-filters.json +0 -24
- package/r/mithaq-list-pagination.json +0 -24
- package/r/mithaq-list-query.json +0 -18
- package/r/mithaq-list-selection.json +0 -19
- package/r/mithaq-list.json +0 -33
- package/r/mithaq-nav.json +0 -24
- package/r/mithaq-nodes.json +0 -34
- package/r/mithaq-relationship-cell.json +0 -20
- package/r/mithaq-relationship-control.json +0 -20
- package/r/mithaq-rich-text.json +0 -40
- package/r/mithaq-screens-host.json +0 -96
- package/r/mithaq-storage.json +0 -44
- package/r/mithaq-upload.json +0 -29
- package/r/registry.json +0 -550
package/dist/screen/generate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { join, relative } from 'node:path';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { isIdentifierName } from '@babel/helper-validator-identifier';
|
|
@@ -63,7 +63,47 @@ export type ScreenClient = Client<Pages>
|
|
|
63
63
|
};
|
|
64
64
|
// One literal, lazy `import.meta.glob` per package over exactly the files the
|
|
65
65
|
// references name: Vite resolves a glob at build and takes no variable.
|
|
66
|
-
const globOf = ({ screen, client, root, app, appName,
|
|
66
|
+
const globOf = ({ screen, client, root, app, appName, from }) => {
|
|
67
|
+
// Each base is written from the screen's own folder into the project, the
|
|
68
|
+
// folder holding both the language and the app: the workspace root of a
|
|
69
|
+
// monorepo, the package itself otherwise. `files.ts` is committed, so a base
|
|
70
|
+
// never names a folder outside it, which differs from machine to machine.
|
|
71
|
+
const realApp = realpathSync(fileURLToPath(app));
|
|
72
|
+
const here = join(realApp, relative(fileURLToPath(app), fileURLToPath(from)));
|
|
73
|
+
const ours = realpathSync(fileURLToPath(root)).split(sep);
|
|
74
|
+
const theirs = realApp.split(sep);
|
|
75
|
+
let shared = 0;
|
|
76
|
+
while (shared < ours.length && ours[shared] === theirs[shared])
|
|
77
|
+
shared++;
|
|
78
|
+
const project = ours.slice(0, shared).join(sep) || sep;
|
|
79
|
+
const inProject = (folder) => !isAbsolute(relative(project, folder)) && relative(project, folder).split(sep)[0] !== '..';
|
|
80
|
+
const baseOf = (folder) => {
|
|
81
|
+
if (!inProject(folder))
|
|
82
|
+
throw new Refusal(`The ${screen} screen's files are read from ${folder}, outside the project at ${project}.`);
|
|
83
|
+
return `${relative(here, folder).replaceAll('\\', '/')}/`;
|
|
84
|
+
};
|
|
85
|
+
const linkFrom = ({ start, name }) => {
|
|
86
|
+
for (let folder = start;; folder = dirname(folder)) {
|
|
87
|
+
const link = join(folder, 'node_modules', name);
|
|
88
|
+
if (existsSync(link))
|
|
89
|
+
return link;
|
|
90
|
+
if (folder === project || dirname(folder) === folder)
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
// A workspace package is read where it is, which npm, pnpm, yarn and Bun
|
|
95
|
+
// agree on. Any other is read through its `node_modules` link, because the
|
|
96
|
+
// folder behind it names a version and a hash, or lies outside the project.
|
|
97
|
+
// An app that depends on the asas alone links no feature package under an
|
|
98
|
+
// isolated install (pnpm, Bun), so the asas, which depends on each, is asked next.
|
|
99
|
+
const installed = (name) => {
|
|
100
|
+
const link = linkFrom({ start: realApp, name }) ?? linkFrom({ start: ours.join(sep), name });
|
|
101
|
+
if (!link)
|
|
102
|
+
throw new Refusal(`The ${screen} screen names files of ${name}, which no node_modules in the project holds. Install it in the app.`);
|
|
103
|
+
const real = realpathSync(link);
|
|
104
|
+
return baseOf(inProject(real) && !relative(project, real).split(sep).includes('node_modules') ? real : link);
|
|
105
|
+
};
|
|
106
|
+
const linked = app.href === root.href ? '/' : baseOf(ours.join(sep));
|
|
67
107
|
const byPackage = new Map();
|
|
68
108
|
for (const key of new Set(referencesIn(client).map((reference) => reference.slice(0, reference.indexOf('#'))))) {
|
|
69
109
|
const name = key.startsWith('./') ? '' : key.slice(0, key.indexOf(':'));
|
|
@@ -92,7 +132,7 @@ export const files = ${own}
|
|
|
92
132
|
// Every file the \`${screen}\` screen's references name, each loaded when a page draws it, keyed as the reference prints it.
|
|
93
133
|
const named = (name: string, files: Record<string, () => Promise<unknown>>) => Object.fromEntries(Object.entries(files).map(([file, load]) => [\`\${name}:\${file}\`, load]))
|
|
94
134
|
export const files = {
|
|
95
|
-
${bare.length ? ` ...${own},\n` : ''}${named.map((name) => ` ...named(${sourceString(name)}, ${glob(byPackage.get(name), name === appName ? '/' :
|
|
135
|
+
${bare.length ? ` ...${own},\n` : ''}${named.map((name) => ` ...named(${sourceString(name)}, ${glob(byPackage.get(name), name === appName ? '/' : installed(name))}),\n`).join('')}}
|
|
96
136
|
`;
|
|
97
137
|
};
|
|
98
138
|
const routesOf = (screen, client, prefix) => {
|
|
@@ -153,14 +193,13 @@ export const generateScreens = async ({ root, screens, into, locales = [], check
|
|
|
153
193
|
const prefix = decodeURIComponent(into.pathname.slice(routesAt + '/routes'.length));
|
|
154
194
|
// The app is the package holding `into`, and Vite's root is its folder.
|
|
155
195
|
const app = packageOf(into) ?? root;
|
|
156
|
-
const linked = app.href === root.href ? '/' : `/node_modules/${JSON.parse(await readFile(new URL('package.json', root), 'utf8')).name}/`;
|
|
157
196
|
const appName = JSON.parse(await readFile(new URL('package.json', app), 'utf8').catch(() => '{}')).name;
|
|
158
197
|
const wanted = Object.entries(screens).flatMap(([key, client]) => {
|
|
159
198
|
const from = new URL(`${key}/`, into);
|
|
160
199
|
return [
|
|
161
200
|
[new URL('screen.json', from), `${JSON.stringify(client, null, 2)}\n`],
|
|
162
201
|
[new URL('types.ts', from), typesOf(client, locales)],
|
|
163
|
-
[new URL('files.ts', from), globOf({ screen: key, client, root, app, appName,
|
|
202
|
+
[new URL('files.ts', from), globOf({ screen: key, client, root, app, appName, from })],
|
|
164
203
|
...routesOf(key, client, prefix).map(([file, source]) => [new URL(file, into), source]),
|
|
165
204
|
];
|
|
166
205
|
});
|
|
@@ -209,6 +248,9 @@ export const run = async (project, check) => {
|
|
|
209
248
|
if (folder(at))
|
|
210
249
|
byFolder.set(at, {});
|
|
211
250
|
for (const [name, client] of Object.entries(screens)) {
|
|
251
|
+
// `false` is a project saying no app of its own serves this screen, so nothing is written for it.
|
|
252
|
+
if (!folder(into) && into[name] === false)
|
|
253
|
+
continue;
|
|
212
254
|
const at = folder(into) ?? folder(into[name]);
|
|
213
255
|
if (!at)
|
|
214
256
|
throw new Refusal(`generate.screens names no folder for the screen ${name}. Name one: \`screens: { ${name}: 'src/routes/(screens)/' }\`.`);
|
package/dist/screen/index.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export declare const screenKinds: {
|
|
|
53
53
|
cell: "relationship";
|
|
54
54
|
control: "relationship";
|
|
55
55
|
};
|
|
56
|
+
readonly json: {
|
|
57
|
+
cell: "json";
|
|
58
|
+
control: "json";
|
|
59
|
+
};
|
|
56
60
|
readonly join: {
|
|
57
61
|
cell: "join";
|
|
58
62
|
control: "join";
|
|
@@ -216,6 +220,12 @@ export type ScreenList<Locale extends string = string, Ref extends string | Ui =
|
|
|
216
220
|
readonly sort?: FieldName | `-${FieldName}`;
|
|
217
221
|
/** The page size a list opens with, which the URL's own `limit` overrides. */
|
|
218
222
|
readonly limit?: number;
|
|
223
|
+
/** The list's tabs, each a `where` it holds every page to; the first is the one it opens on. */
|
|
224
|
+
readonly tabs?: readonly {
|
|
225
|
+
readonly tab: string;
|
|
226
|
+
readonly label: Localized<Locale>;
|
|
227
|
+
readonly where?: Where;
|
|
228
|
+
}[];
|
|
219
229
|
};
|
|
220
230
|
/** A button that runs one of the kayan's operations, wherever it sits: `button` names the operation. */
|
|
221
231
|
export type ScreenButton<Locale extends string = string, Ref extends string | Ui = string | Ui> = {
|
|
@@ -235,7 +245,7 @@ export type ScreenButton<Locale extends string = string, Ref extends string | Ui
|
|
|
235
245
|
export type ScreenPage<Locale extends string = string, Ref extends string | Ui = string | Ui, FieldName extends string = string> = {
|
|
236
246
|
readonly path: string;
|
|
237
247
|
readonly page: ScreenFrame<Locale, Ref, FieldName>;
|
|
238
|
-
/** `create` and `edit` only: the record opens over the list, in
|
|
248
|
+
/** `create` and `edit` only: the record opens over the list, in shadcn's dialog or sheet, or a full-page drawer as Payload's document drawer. */
|
|
239
249
|
readonly mode?: 'dialog' | 'drawer' | 'sheet';
|
|
240
250
|
readonly component?: never;
|
|
241
251
|
} | {
|
|
@@ -274,6 +284,8 @@ type FieldData = {
|
|
|
274
284
|
on?: string;
|
|
275
285
|
/** Relationship only: whether this field holds a set of rows rather than one. */
|
|
276
286
|
hasMany?: true;
|
|
287
|
+
/** Relationship only: whether its picker reads `for` this field, so the server holds the rows to its `filterOptions`. */
|
|
288
|
+
filterOptions?: true;
|
|
277
289
|
};
|
|
278
290
|
/** One field as a screen draws it: the data a browser needs, and this screen's drawing facts decided. */
|
|
279
291
|
export type ClientField = FieldData & {
|
|
@@ -337,6 +349,11 @@ export type ListClient = {
|
|
|
337
349
|
row?: ButtonClient[];
|
|
338
350
|
sort?: string;
|
|
339
351
|
limit?: number;
|
|
352
|
+
tabs?: {
|
|
353
|
+
tab: string;
|
|
354
|
+
label: Localized;
|
|
355
|
+
where?: Where;
|
|
356
|
+
}[];
|
|
340
357
|
/** A list of another kayan: its name, and its relationship holding this page's row. */
|
|
341
358
|
of?: string;
|
|
342
359
|
where?: string;
|
package/dist/screen/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { builtInScreenKinds, choiceControls, packageName, registryCells, registryControls } from './client.js';
|
|
3
3
|
import { unsortable } from '../mithaq/types.js';
|
|
4
4
|
import { defaultKinds } from '../base/kinds.js';
|
|
5
|
-
import { assertFieldWhere } from '../asas/runner/write.js';
|
|
5
|
+
import { assertFieldWhere, assertWhere } from '../asas/runner/write.js';
|
|
6
6
|
import { bulkSeals, readSeals, sealOf, storeSeals } from '../asas/runner/steps.js';
|
|
7
7
|
import { kindOf } from './studio.js';
|
|
8
8
|
import { implementationOf } from '../mithaq/rukn.js';
|
|
@@ -40,13 +40,14 @@ export const clientFieldOf = (name, field) => ({
|
|
|
40
40
|
? { relationTo: field.relationTo().name }
|
|
41
41
|
: {}),
|
|
42
42
|
...(field.hasMany === true && { hasMany: true }),
|
|
43
|
+
...(typeof field.filterOptions === 'function' && { filterOptions: true }),
|
|
43
44
|
...(typeof field.on === 'string' && { on: field.on }),
|
|
44
45
|
});
|
|
45
46
|
const word = /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u;
|
|
46
47
|
const targetKeys = ['capability', 'type', 'path', 'kayan', 'controls', 'providers', 'pages', 'layouts', 'components'];
|
|
47
48
|
const leafKeys = ['field', 'component', 'cell', 'placeholder', 'description', 'readOnly', 'width', 'when'];
|
|
48
49
|
const buttonKeys = ['button', 'label', 'fields', 'reload', 'component', 'confirm', 'when'];
|
|
49
|
-
const listKeys = ['columns', 'filters', 'search', 'empty', 'menu', 'toolbar', 'bulk', 'row', 'sort', 'limit', 'of', 'where'];
|
|
50
|
+
const listKeys = ['columns', 'filters', 'search', 'empty', 'menu', 'toolbar', 'bulk', 'row', 'sort', 'limit', 'tabs', 'of', 'where'];
|
|
50
51
|
const modes = ['dialog', 'sheet', 'drawer'];
|
|
51
52
|
/** Every key of `value` is one `known` names, so nothing reaches `screen.json` unnamed. */
|
|
52
53
|
const named = (at, value, known, what) => {
|
|
@@ -177,6 +178,11 @@ export const screensOf = (language, resolved) => {
|
|
|
177
178
|
const map = place === 'layout' ? 'layouts' : 'components';
|
|
178
179
|
if (Object.hasOwn(maps[map], value))
|
|
179
180
|
return maps[map][value];
|
|
181
|
+
// A kind no registry control or cell draws names itself, and the project's components map draws it under that name.
|
|
182
|
+
if (place === 'control' && value === kind && !Object.hasOwn(shippedControls, value) && !registryControls.includes(value))
|
|
183
|
+
return value;
|
|
184
|
+
if (place === 'cell' && value === kind && !shippedCells.includes(value) && !registryCells.includes(value))
|
|
185
|
+
return value;
|
|
180
186
|
const shipped = place === 'control' ? Object.keys(shippedControls) : place === 'cell' ? shippedCells : [];
|
|
181
187
|
if (!shipped.includes(value)) {
|
|
182
188
|
throw new Error(`${at} names "${value}", which is no name in this screen's ${map} and no ${place} the registry ships.`);
|
|
@@ -199,7 +205,7 @@ export const screensOf = (language, resolved) => {
|
|
|
199
205
|
throw new Error(`${screen} controls has no row for kind "${kind}", and every kind of the language needs one.`);
|
|
200
206
|
controls[kind] = {
|
|
201
207
|
control: ref(`${screen} controls "${kind}" control`, 'control', row.control, kind),
|
|
202
|
-
cell: ref(`${screen} controls "${kind}" cell`, 'cell', row.cell),
|
|
208
|
+
cell: ref(`${screen} controls "${kind}" cell`, 'cell', row.cell, kind),
|
|
203
209
|
};
|
|
204
210
|
}
|
|
205
211
|
for (const name of target.kayan) {
|
|
@@ -224,7 +230,7 @@ export const screensOf = (language, resolved) => {
|
|
|
224
230
|
...(typeof field.suffix === 'object' && { suffix: field.suffix }),
|
|
225
231
|
...(typeof field.description === 'object' && { description: field.description }),
|
|
226
232
|
component,
|
|
227
|
-
cell: own.cell ? ref(`${where} targets.${key}.cell`, 'cell', own.cell) : controls[field.kind].cell,
|
|
233
|
+
cell: own.cell ? ref(`${where} targets.${key}.cell`, 'cell', own.cell, field.kind) : controls[field.kind].cell,
|
|
228
234
|
...(typeof own.width === 'string' && { width: own.width }),
|
|
229
235
|
...(own.filter === true && { filter: true }),
|
|
230
236
|
...(own.searchable === true && { searchable: true }),
|
|
@@ -370,7 +376,7 @@ export const screensOf = (language, resolved) => {
|
|
|
370
376
|
...(typeof readOnly === 'boolean' && { readOnly }),
|
|
371
377
|
...(typeof width === 'string' && { width }),
|
|
372
378
|
...(place === 'control' && { component: control }),
|
|
373
|
-
...((place === 'cell' || cell) && { cell: cell ? ref(`${node} "${name}" cell`, 'cell', cell) : field.cell }),
|
|
379
|
+
...((place === 'cell' || cell) && { cell: cell ? ref(`${node} "${name}" cell`, 'cell', cell, field.kind) : field.cell }),
|
|
374
380
|
...(placeholder !== undefined && { placeholder: copy(`${node} "${name}" placeholder`, placeholder) }),
|
|
375
381
|
...(description !== undefined && { description: copy(`${node} "${name}" description`, description) }),
|
|
376
382
|
decided,
|
|
@@ -443,7 +449,7 @@ export const screensOf = (language, resolved) => {
|
|
|
443
449
|
return { ...drawn, list: { of, where: where, ...drawn.list } };
|
|
444
450
|
}
|
|
445
451
|
held.list++;
|
|
446
|
-
const { columns, filters, search, empty, menu, sort, limit } = one.list;
|
|
452
|
+
const { columns, filters, search, empty, menu, sort, limit, tabs } = one.list;
|
|
447
453
|
// Where a button sits decides which operations it may run: bulk over ticked rows,
|
|
448
454
|
// row over its row, toolbar over none.
|
|
449
455
|
const buttons = (region) => {
|
|
@@ -480,6 +486,17 @@ export const screensOf = (language, resolved) => {
|
|
|
480
486
|
if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1 && limit <= maxLimit)) {
|
|
481
487
|
throw new Error(`${node} list limit is ${JSON.stringify(limit)}, and a limit is a whole number from 1 to ${maxLimit}.`);
|
|
482
488
|
}
|
|
489
|
+
if (tabs !== undefined && (!Array.isArray(tabs) || !tabs.length))
|
|
490
|
+
throw new Error(`${node} list tabs is no list of tabs.`);
|
|
491
|
+
const tabbed = tabs?.map((tab, at) => {
|
|
492
|
+
const said = `${node} list tab ${at}`;
|
|
493
|
+
named(said, tab, ['tab', 'label', 'where'], 'tab');
|
|
494
|
+
// Written into the URL as itself, so a word, once per list.
|
|
495
|
+
if (typeof tab.tab !== 'string' || !/^[A-Za-z][A-Za-z0-9]*$/.test(tab.tab) || tabs.findIndex((other) => other.tab === tab.tab) !== at) {
|
|
496
|
+
throw new Error(`${said} is named ${JSON.stringify(tab.tab)}, and a tab is named once in a list, in letters and digits.`);
|
|
497
|
+
}
|
|
498
|
+
return { tab: tab.tab, label: copy(`${said} label`, tab.label), ...(tab.where !== undefined && { where: assertWhere(kayan, tab.where) }) };
|
|
499
|
+
});
|
|
483
500
|
return {
|
|
484
501
|
list: {
|
|
485
502
|
columns: columns.map((column) => leafOf(`${node} list column`, column, 'cell')),
|
|
@@ -492,6 +509,7 @@ export const screensOf = (language, resolved) => {
|
|
|
492
509
|
...(row && { row }),
|
|
493
510
|
...(sort !== undefined && { sort }),
|
|
494
511
|
...(limit !== undefined && { limit }),
|
|
512
|
+
...(tabbed && { tabs: tabbed }),
|
|
495
513
|
},
|
|
496
514
|
...drawnBy(node, one),
|
|
497
515
|
};
|
package/dist/screen/react.d.ts
CHANGED
|
@@ -18,10 +18,12 @@ export type ScreenState = LocaleState & {
|
|
|
18
18
|
name: string;
|
|
19
19
|
path: string;
|
|
20
20
|
}[];
|
|
21
|
+
/** `names` is each locale's name, the JSON the project's auth setup writes; `name` is the default locale's. */
|
|
21
22
|
organizations?: readonly {
|
|
22
23
|
id: string;
|
|
23
24
|
name: string;
|
|
24
25
|
slug?: string;
|
|
26
|
+
names?: string;
|
|
25
27
|
}[];
|
|
26
28
|
activeOrganizationId?: string | null;
|
|
27
29
|
};
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '../mithaq/standard-schema.ts';
|
|
2
2
|
import type { ResolvedField } from '../mithaq/types.ts';
|
|
3
|
-
import type { Tables } from '../drizzle/index.ts';
|
|
4
3
|
import { type StorageConfig } from './storage.ts';
|
|
5
4
|
/** A relationship to a kayan that holds files, drawn as a file picker. */
|
|
6
5
|
export declare const upload: import("../base/kinds.ts").Kind<string | number, "defaultValue" | "index" | "onDelete" | "providedBy" | "required" | "unique"> & {
|
|
7
6
|
targets: {
|
|
8
|
-
readonly drizzle:
|
|
7
|
+
readonly drizzle: import("../targets.ts").ColumnBuilder;
|
|
9
8
|
readonly zod: (field: ResolvedField) => import("zod").ZodArray<import("zod").ZodGUID | import("zod").ZodInt32 | import("zod").ZodString> | import("zod").ZodGUID | import("zod").ZodInt32 | import("zod").ZodString;
|
|
10
9
|
readonly standardSchema: (field: ResolvedField) => StandardSchemaV1<unknown, number | string>;
|
|
11
10
|
readonly screen: {
|
|
@@ -25,7 +24,7 @@ export declare const storage: (config: StorageConfig) => {
|
|
|
25
24
|
readonly kinds: {
|
|
26
25
|
readonly upload: import("../base/kinds.ts").Kind<string | number, "defaultValue" | "index" | "onDelete" | "providedBy" | "required" | "unique"> & {
|
|
27
26
|
targets: {
|
|
28
|
-
readonly drizzle:
|
|
27
|
+
readonly drizzle: import("../targets.ts").ColumnBuilder;
|
|
29
28
|
readonly zod: (field: ResolvedField) => import("zod").ZodArray<import("zod").ZodGUID | import("zod").ZodInt32 | import("zod").ZodString> | import("zod").ZodGUID | import("zod").ZodInt32 | import("zod").ZodString;
|
|
30
29
|
readonly standardSchema: (field: ResolvedField) => StandardSchemaV1<unknown, number | string>;
|
|
31
30
|
readonly screen: {
|
package/dist/storage/index.js
CHANGED
|
@@ -21,8 +21,7 @@ export const upload = kind((value) => typeof value === 'number' || typeof value
|
|
|
21
21
|
// A file a row still names is never deleted from under it, whether or not the field is required.
|
|
22
22
|
onDelete: 'restrict',
|
|
23
23
|
targets: {
|
|
24
|
-
|
|
25
|
-
drizzle: (field, tables) => drizzleKinds.relationship(field.hasMany ? { ...field, rules: { ...field.rules, onDelete: 'restrict' } } : field, tables),
|
|
24
|
+
drizzle: drizzleKinds.relationship,
|
|
26
25
|
zod: zodKinds.relationship,
|
|
27
26
|
standardSchema: (field) => ({
|
|
28
27
|
'~standard': {
|
|
@@ -226,6 +226,8 @@ export type StorageConfig = {
|
|
|
226
226
|
adapter: string;
|
|
227
227
|
/** The largest file in bytes a kayan holds when its `targets.storage` names no `fileSize`. */
|
|
228
228
|
fileSize: number;
|
|
229
|
+
/** The largest request body `uploadInstructions` reads, answered 413 past it: `defaultMaxBodyBytes` or the project's own. */
|
|
230
|
+
maxBodyBytes: number;
|
|
229
231
|
/** The object key, decided before the upload. */
|
|
230
232
|
key: (args: {
|
|
231
233
|
prefix: string;
|
package/dist/storage/storage.js
CHANGED
|
@@ -4,8 +4,9 @@ import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
|
4
4
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
5
5
|
import { isStorage } from '../mithaq/language.js';
|
|
6
6
|
import { providerOf } from '../base/rules.js';
|
|
7
|
-
import { AsasError, codes, denied, invalid, missing } from '../asas/runner/errors.js';
|
|
7
|
+
import { AsasError, retryConflict, codes, denied, invalid, missing } from '../asas/runner/errors.js';
|
|
8
8
|
import { sealOf } from '../asas/runner/steps.js';
|
|
9
|
+
import { assertMaxBodyBytes, boundedBody } from '../asas/http/fetch.js';
|
|
9
10
|
import { allowedType } from './allowed-type.js';
|
|
10
11
|
/** Written by the runner when the row is made, from the signed upload, and by nobody after. */
|
|
11
12
|
const fileColumn = { create: 'application', update: 'never' };
|
|
@@ -181,6 +182,7 @@ export const resolveStorage = ({ language, resolved, storeOf, active }) => {
|
|
|
181
182
|
if (!Number.isSafeInteger(given.fileSize) || given.fileSize < 1) {
|
|
182
183
|
throw new Error(`storage.fileSize is ${JSON.stringify(given.fileSize)}, and it is a whole number of bytes above 0.`);
|
|
183
184
|
}
|
|
185
|
+
assertMaxBodyBytes('storage', given.maxBodyBytes);
|
|
184
186
|
if (typeof given.onError !== 'function')
|
|
185
187
|
throw new Error('storage names no "onError" function, which a file route hands what it threw before it answers 500. Name the project\'s own, such as console.error.');
|
|
186
188
|
const adapters = Object.entries(given.adapters ?? {});
|
|
@@ -323,9 +325,13 @@ export const storageRoutes = ({ kayan, config, read, authorize, admin, }) => {
|
|
|
323
325
|
return null;
|
|
324
326
|
try {
|
|
325
327
|
if (verb === 'uploadInstructions' && id === undefined && request.method === 'POST') {
|
|
328
|
+
// Bounded before the actor is asked, so no caller holds the server to a body of any size.
|
|
329
|
+
const read = await boundedBody(request, config.maxBodyBytes);
|
|
330
|
+
if (read instanceof Response)
|
|
331
|
+
return read;
|
|
326
332
|
let input;
|
|
327
333
|
try {
|
|
328
|
-
input = await
|
|
334
|
+
input = JSON.parse(await read.text());
|
|
329
335
|
}
|
|
330
336
|
catch {
|
|
331
337
|
input = undefined;
|
|
@@ -347,6 +353,7 @@ export const storageRoutes = ({ kayan, config, read, authorize, admin, }) => {
|
|
|
347
353
|
return new Response(served.body, { status: served.status, statusText: served.statusText, headers });
|
|
348
354
|
}
|
|
349
355
|
catch (thrown) {
|
|
356
|
+
thrown = retryConflict(thrown);
|
|
350
357
|
if (thrown instanceof AsasError)
|
|
351
358
|
return refusal(thrown);
|
|
352
359
|
config.onError(thrown);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { drizzleAdapter, type Adapter, type ColumnBuilder, type ColumnRule } from './drizzle/index.ts';
|
|
2
|
+
export { kinds as drizzleKinds, idKinds as drizzleIdKinds } from './drizzle/kinds.ts';
|
|
3
|
+
export { rules as drizzleRules } from './drizzle/rules.ts';
|
|
4
|
+
export { zodTarget, type ZodRule, type ZodShape } from './zod/index.ts';
|
|
5
|
+
export { kinds as zodKinds, idKinds as zodIdKinds } from './zod/kinds.ts';
|
|
6
|
+
export { rules as zodRules } from './zod/rules.ts';
|
|
7
|
+
export { defaultBulkLimit, defaultDepth, defaultJoinLimit, defaultLimit, defaultMaxLimit, serviceTarget, type ServiceTarget } from './service/index.ts';
|
|
8
|
+
export { defaultAuthorize } from './asas/runner/write.ts';
|
|
9
|
+
export { rbacTarget, type AccessOptions, type AccessTarget, type SystemRole, type SystemRoles } from './rbac/index.ts';
|
|
10
|
+
export { betterAuthTarget, memberRoleTarget, type BetterAuthAccessOptions } from './auth/better-auth/access.ts';
|
|
11
|
+
export { betterAuthTenant } from './auth/better-auth/tenant.ts';
|
|
12
|
+
export { screenKinds, screenTarget, type ScreenTarget } from './screen/index.ts';
|
|
13
|
+
export { defaultFileSize, storageTarget, type StorageConfig, type StorageTarget } from './storage/storage.ts';
|
|
14
|
+
export { versionsTarget, type KayanVersions, type VersionsTarget } from './versions/index.ts';
|
package/dist/targets.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Every target factory mithaq ships and the named defaults its options take, so a project's
|
|
2
|
+
// `targets` block reads from one place: `import { drizzleAdapter, serviceTarget } from 'mithaq/targets'`.
|
|
3
|
+
export { drizzleAdapter } from './drizzle/index.js';
|
|
4
|
+
export { kinds as drizzleKinds, idKinds as drizzleIdKinds } from './drizzle/kinds.js';
|
|
5
|
+
export { rules as drizzleRules } from './drizzle/rules.js';
|
|
6
|
+
export { zodTarget } from './zod/index.js';
|
|
7
|
+
export { kinds as zodKinds, idKinds as zodIdKinds } from './zod/kinds.js';
|
|
8
|
+
export { rules as zodRules } from './zod/rules.js';
|
|
9
|
+
export { defaultBulkLimit, defaultDepth, defaultJoinLimit, defaultLimit, defaultMaxLimit, serviceTarget } from './service/index.js';
|
|
10
|
+
export { defaultAuthorize } from './asas/runner/write.js';
|
|
11
|
+
export { rbacTarget } from './rbac/index.js';
|
|
12
|
+
export { betterAuthTarget, memberRoleTarget } from './auth/better-auth/access.js';
|
|
13
|
+
export { betterAuthTenant } from './auth/better-auth/tenant.js';
|
|
14
|
+
export { screenKinds, screenTarget } from './screen/index.js';
|
|
15
|
+
export { defaultFileSize, storageTarget } from './storage/storage.js';
|
|
16
|
+
export { versionsTarget } from './versions/index.js';
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Plugin } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* Serves the admin's pieces from the installed `mithaq-admin`, and a file the project ejected into `ejected`
|
|
4
|
+
* in place of the package's file of the same name, for every importer: the app, the package's own pieces and
|
|
5
|
+
* other ejected files. Deleting the ejected file returns the project to the package's.
|
|
6
|
+
*/
|
|
7
|
+
export declare const mithaqAdmin: (ejected: string) => Plugin;
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { normalizePath } from 'vite';
|
|
5
|
+
/**
|
|
6
|
+
* Serves the admin's pieces from the installed `mithaq-admin`, and a file the project ejected into `ejected`
|
|
7
|
+
* in place of the package's file of the same name, for every importer: the app, the package's own pieces and
|
|
8
|
+
* other ejected files. Deleting the ejected file returns the project to the package's.
|
|
9
|
+
*/
|
|
10
|
+
export const mithaqAdmin = (ejected) => {
|
|
11
|
+
// Resolved from the project, so a store layout finds the copy the project installed.
|
|
12
|
+
const registry = join(dirname(createRequire(join(ejected, 'x')).resolve('mithaq-admin/package.json')), 'registry/');
|
|
13
|
+
return {
|
|
14
|
+
name: 'mithaq-admin',
|
|
15
|
+
enforce: 'pre',
|
|
16
|
+
// Pre-bundled, a piece the app imports and the same piece a sibling imports are two modules in dev.
|
|
17
|
+
// No `server.warmup` of the package: measured cold, it served the first page 0.7 s later and loaded the
|
|
18
|
+
// client 1 s sooner, and it set the optimizer discovering dependencies no page asked for yet.
|
|
19
|
+
config: () => ({ optimizeDeps: { exclude: ['mithaq-admin'] } }),
|
|
20
|
+
// The dependency scan stops at the excluded package, so what only the pieces import (Lexical, TanStack Form, and
|
|
21
|
+
// through the app's own primitives, Base UI) would be found by the first page that needs it, re-optimized mid-page
|
|
22
|
+
// into a second React, and the page reloaded. Named up front instead, but only what the project installed from a
|
|
23
|
+
// registry: an optional peer it left out (Lexical without rich text) fails the whole optimization, and a linked
|
|
24
|
+
// workspace package is served as source, where an include would stop the scan finding what it imports. And only
|
|
25
|
+
// what nothing excludes, since an include wins over an exclude: last, after every plugin has named its own.
|
|
26
|
+
configEnvironment: { order: 'post', handler(name, { optimizeDeps, resolve }) {
|
|
27
|
+
if (name !== 'client')
|
|
28
|
+
return;
|
|
29
|
+
// ponytail: an exclude a plugin adds later than configEnvironment (in configResolved) is not seen here and loses
|
|
30
|
+
// to the include; read the resolved config there if a plugin ever does.
|
|
31
|
+
const excluded = optimizeDeps?.exclude ?? [];
|
|
32
|
+
const folders = createRequire(join(ejected, 'x')).resolve.paths('x') ?? [];
|
|
33
|
+
const alias = resolve?.alias ?? [];
|
|
34
|
+
const aliases = Array.isArray(alias) ? alias : Object.entries(alias).map(([find, replacement]) => ({ find, replacement }));
|
|
35
|
+
/** The file of the app's a relative or aliased specifier names, '' where none is, or nothing for a package. */
|
|
36
|
+
const local = (specifier, from) => {
|
|
37
|
+
const entry = aliases.find(({ find }) => (typeof find === 'string' ? specifier === find || specifier.startsWith(`${find}/`) : find.test(specifier)));
|
|
38
|
+
const path = specifier.startsWith('.') ? join(dirname(from), specifier) : entry && specifier.replace(entry.find, entry.replacement);
|
|
39
|
+
return path === undefined ? undefined : (['', '.tsx', '.ts'].map((extension) => path + extension).find((file) => statSync(file, { throwIfNoEntry: false })?.isFile()) ?? '');
|
|
40
|
+
};
|
|
41
|
+
// The pieces, and the app's own files they reach, such as the primitives the published package leaves out.
|
|
42
|
+
// What a project ejects into its language package (mail, steps, storage) runs on the server alone.
|
|
43
|
+
const files = new Set(readdirSync(registry, { recursive: true, encoding: 'utf8' }).filter((file) => /\.tsx?$/.test(file) && !/^(mail|steps|storage)\//.test(normalizePath(file))).map((file) => join(registry, file)));
|
|
44
|
+
const imports = new Set();
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
// ponytail: a regex over the source, which a ` from '...'` inside a string or a comment fools into one more
|
|
47
|
+
// include; parse the files if a piece ever reads that way.
|
|
48
|
+
for (const [, type, specifier] of readFileSync(file, 'utf8').matchAll(/^(?:import|export)\s+(type\s)?(?:[^'";]*?\sfrom\s*)?['"]([^'"]+)['"]/gm)) {
|
|
49
|
+
if (type)
|
|
50
|
+
continue;
|
|
51
|
+
const found = local(specifier, file);
|
|
52
|
+
if (found)
|
|
53
|
+
files.add(found);
|
|
54
|
+
else if (found === undefined)
|
|
55
|
+
imports.add(specifier);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const include = [...imports].filter((specifier) => {
|
|
59
|
+
// `mithaq` too, which the app's own code imports, so the scan finds it: named here, its rich text would be
|
|
60
|
+
// bundled where no Lexical is installed.
|
|
61
|
+
if (/^(\/|node:|mithaq(\/|$))/.test(specifier))
|
|
62
|
+
return false;
|
|
63
|
+
const pkg = specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
|
|
64
|
+
const installed = folders.map((folder) => join(folder, pkg)).find((folder) => existsSync(join(folder, 'package.json')));
|
|
65
|
+
return !excluded.includes(pkg) && !excluded.includes(specifier) && !!installed && normalizePath(realpathSync(installed)).includes('/node_modules/');
|
|
66
|
+
});
|
|
67
|
+
return { optimizeDeps: { include: include.sort() } };
|
|
68
|
+
} },
|
|
69
|
+
// By the specifier, not by resolving it: published as `@drhomet/mithaq-admin`, a piece in a store's folder cannot
|
|
70
|
+
// reach `mithaq-admin/<file>` by walking up, so the plugin answers for every importer.
|
|
71
|
+
resolveId(source) {
|
|
72
|
+
const [, file] = /^mithaq-admin\/(.+\.tsx?)$/.exec(source) ?? [];
|
|
73
|
+
// A piece names a sibling by its file alone, so a climb out of the package is no import of ours.
|
|
74
|
+
if (!file || file.split('/').includes('..'))
|
|
75
|
+
return;
|
|
76
|
+
const own = join(ejected, file);
|
|
77
|
+
// The real path, as Vite names every other module, so a symlinked folder is never a second module.
|
|
78
|
+
return normalizePath(existsSync(own) ? realpathSync(own) : join(registry, file));
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
package/dist/zod/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ type ZodShapes<F extends Record<string, RuntimeField>> = {
|
|
|
32
32
|
declare const listShape: z.ZodObject<{
|
|
33
33
|
data: z.ZodOptional<z.ZodUnknown>;
|
|
34
34
|
depth: z.ZodOptional<z.ZodUnknown>;
|
|
35
|
+
for: z.ZodOptional<z.ZodUnknown>;
|
|
35
36
|
limit: z.ZodOptional<z.ZodUnknown>;
|
|
36
37
|
page: z.ZodOptional<z.ZodUnknown>;
|
|
37
38
|
sort: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -58,8 +59,9 @@ declare const target: {
|
|
|
58
59
|
textArray: (field?: ResolvedField) => z.ZodArray<z.ZodEnum<{
|
|
59
60
|
[x: string]: string;
|
|
60
61
|
}> | z.ZodString>;
|
|
61
|
-
uuid: (field?: ResolvedField) => z.ZodGUID;
|
|
62
|
+
uuid: (field?: ResolvedField) => z.ZodGUID | z.ZodInt32 | z.ZodString;
|
|
62
63
|
relationship: (field: ResolvedField) => z.ZodArray<z.ZodGUID | z.ZodInt32 | z.ZodString> | z.ZodGUID | z.ZodInt32 | z.ZodString;
|
|
64
|
+
json: (field?: ResolvedField) => z.ZodCustom<import("../base/kinds.ts").Json, import("../base/kinds.ts").Json>;
|
|
63
65
|
join: (field?: ResolvedField) => z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
64
66
|
};
|
|
65
67
|
readonly rules: {
|
|
@@ -158,7 +160,7 @@ declare const target: {
|
|
|
158
160
|
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
159
161
|
defaultValue: ({ shape }: {
|
|
160
162
|
shape: z.ZodType;
|
|
161
|
-
value:
|
|
163
|
+
value: Date | readonly unknown[] | import("../index.ts").Ref | import("../base/kinds.ts").Json;
|
|
162
164
|
field: ResolvedField;
|
|
163
165
|
}) => z.ZodOptional<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
164
166
|
defaultNow: ({ shape, value: on }: {
|
|
@@ -178,7 +180,14 @@ declare const target: {
|
|
|
178
180
|
}, fields: F) => ZodShapes<F>;
|
|
179
181
|
};
|
|
180
182
|
/** The zod schema target. */
|
|
181
|
-
export declare const zodTarget:
|
|
183
|
+
export declare const zodTarget: <const Kinds extends Record<string, ZodShape>, const Rules extends Record<string, ZodRule>, const IdKinds extends readonly string[]>({ kinds, rules, idKinds }: {
|
|
184
|
+
kinds: Kinds;
|
|
185
|
+
rules: Rules;
|
|
186
|
+
idKinds: IdKinds;
|
|
187
|
+
}) => Omit<typeof target, 'kinds' | 'rules' | 'idKinds'> & {
|
|
188
|
+
kinds: Kinds;
|
|
189
|
+
rules: Rules;
|
|
190
|
+
idKinds: IdKinds;
|
|
182
191
|
readonly derives?: ZodDerives;
|
|
183
192
|
};
|
|
184
193
|
export {};
|
package/dist/zod/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { storedRichText } from '../rich-text/
|
|
2
|
+
import { storedRichText } from '../rich-text/value.js';
|
|
3
3
|
import { providerOf } from '../base/rules.js';
|
|
4
4
|
import { idKinds as zodIdKinds, kinds as zodKinds } from './kinds.js';
|
|
5
5
|
import { implementationOf } from '../mithaq/rukn.js';
|
|
@@ -74,6 +74,7 @@ const rowSchema = (schema, fields) => z.object(Object.fromEntries(Object.entries
|
|
|
74
74
|
const listShape = z.strictObject({
|
|
75
75
|
data: z.unknown().optional(),
|
|
76
76
|
depth: z.unknown().optional(),
|
|
77
|
+
for: z.unknown().optional(),
|
|
77
78
|
limit: z.unknown().optional(),
|
|
78
79
|
page: z.unknown().optional(),
|
|
79
80
|
sort: z.unknown().optional(),
|
|
@@ -88,11 +89,11 @@ const target = {
|
|
|
88
89
|
rules: zodRules,
|
|
89
90
|
derive(fields) {
|
|
90
91
|
const built = fieldShapes(fields, this.kinds, this.rules);
|
|
91
|
-
// A stored row is read against its kinds alone, with no rule
|
|
92
|
-
// so
|
|
92
|
+
// A stored row is read against its kinds alone, with no rule, `options`, `jsonSchema` or `maxBytes`,
|
|
93
|
+
// so one added after rows were stored refuses their next save, never their read.
|
|
93
94
|
const kinds = Object.fromEntries(Object.entries(fields).map(([name, field]) => [
|
|
94
95
|
name,
|
|
95
|
-
{ ...field, options: undefined, ...(field.kind === 'richText' && { richText: storedRichText }) },
|
|
96
|
+
{ ...field, options: undefined, jsonSchema: undefined, maxBytes: undefined, ...(field.kind === 'richText' && { richText: storedRichText }) },
|
|
96
97
|
]));
|
|
97
98
|
const stored = fieldShapes(kinds, this.kinds, { localized: this.rules.localized });
|
|
98
99
|
const drafted = fieldShapes(Object.fromEntries(Object.entries(kinds).map(([name, field]) => [name, field.kind === 'richText' ? { ...field, richText: fields[name].richText, rules: { ...field.rules, required: false } } : field])), this.kinds, { localized: this.rules.localized });
|
|
@@ -118,4 +119,4 @@ const target = {
|
|
|
118
119
|
},
|
|
119
120
|
};
|
|
120
121
|
/** The zod schema target. */
|
|
121
|
-
export const zodTarget = target;
|
|
122
|
+
export const zodTarget = ({ kinds, rules, idKinds }) => ({ ...target, kinds, rules, idKinds });
|
package/dist/zod/kinds.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ResolvedField } from '../mithaq/types.ts';
|
|
3
|
+
import { type Json } from '../base/kinds.ts';
|
|
3
4
|
export declare const errorOf: (field?: ResolvedField, element?: boolean) => ((issue: {
|
|
4
5
|
input: unknown;
|
|
5
6
|
}) => string) | undefined;
|
|
@@ -27,8 +28,9 @@ export declare const kinds: {
|
|
|
27
28
|
textArray: (field?: ResolvedField) => z.ZodArray<z.ZodEnum<{
|
|
28
29
|
[x: string]: string;
|
|
29
30
|
}> | z.ZodString>;
|
|
30
|
-
uuid: (field?: ResolvedField) => z.ZodGUID;
|
|
31
|
+
uuid: (field?: ResolvedField) => z.ZodGUID | z.ZodInt32 | z.ZodString;
|
|
31
32
|
relationship: (field: ResolvedField) => z.ZodArray<z.ZodGUID | z.ZodInt32 | z.ZodString> | z.ZodGUID | z.ZodInt32 | z.ZodString;
|
|
33
|
+
json: (field?: ResolvedField) => z.ZodCustom<Json, Json>;
|
|
32
34
|
join: (field?: ResolvedField) => z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
33
35
|
};
|
|
34
36
|
export {};
|
package/dist/zod/kinds.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { isJson } from '../base/kinds.js';
|
|
2
3
|
import { isDecimal } from '../base/decimal.js';
|
|
4
|
+
import { jsonSchemaCheck } from '../base/json-schema.js';
|
|
3
5
|
// A missing key, or a `null` on a required field, is the `required` rule refusing;
|
|
4
6
|
// anything else is the kind refusing the value. A field with no kayan keeps zod's text.
|
|
5
7
|
export const errorOf = (field, element = false) => field?.kayan
|
|
@@ -12,7 +14,8 @@ const idShapes = {
|
|
|
12
14
|
number: (error) => z.int32({ error }),
|
|
13
15
|
text: (error) => z.string({ error }),
|
|
14
16
|
// `guid`, not `uuid`: `z.uuid()` also demands an RFC version and variant, which Postgres does not.
|
|
15
|
-
|
|
17
|
+
// Lower-cased, the spelling Postgres answers, so a step comparing ids as text sees one id.
|
|
18
|
+
uuid: (error) => z.guid({ error }).toLowerCase(),
|
|
16
19
|
};
|
|
17
20
|
/** The kinds this target shapes an id of, the same three the store's columns are. */
|
|
18
21
|
export const idKinds = Object.keys(idShapes);
|
|
@@ -40,10 +43,16 @@ export const kinds = {
|
|
|
40
43
|
date: (field) => z.iso.date({ error: errorOf(field) }),
|
|
41
44
|
// The same error on each element, so a bad element reads as the field's own sentence.
|
|
42
45
|
textArray: (field) => z.array(textShape(field, true), { error: errorOf(field) }),
|
|
43
|
-
uuid: (field) =>
|
|
46
|
+
uuid: (field) => idShape('uuid', errorOf(field)),
|
|
44
47
|
relationship: (field) => field.hasMany
|
|
45
48
|
? z.array(idShape(field.target.fields.id.kind, errorOf(field, true)), { error: errorOf(field) })
|
|
46
49
|
: idShape(field.target.fields.id.kind, errorOf(field)),
|
|
50
|
+
// Only what JSON can say, so a value reads back as it was written; `z.json()` lets a cycle through.
|
|
51
|
+
// A `jsonSchema` only judges: what is stored is the value as sent.
|
|
52
|
+
json: (field) => {
|
|
53
|
+
const shape = z.custom((value) => isJson(value) && !(field?.maxBytes && new TextEncoder().encode(JSON.stringify(value)).length > field.maxBytes), { error: errorOf(field) });
|
|
54
|
+
return field?.jsonSchema ? shape.refine(jsonSchemaCheck(field), { error: errorOf(field) }) : shape;
|
|
55
|
+
},
|
|
47
56
|
// Left loose: the child's row shape may not be derived yet, and the child's own read already parsed the rows.
|
|
48
57
|
join: (field) => z.array(z.record(z.string(), z.unknown()), { error: errorOf(field) }),
|
|
49
58
|
};
|
package/dist/zod/rules.d.ts
CHANGED
|
@@ -99,7 +99,7 @@ export declare const rules: {
|
|
|
99
99
|
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
100
100
|
defaultValue: ({ shape }: {
|
|
101
101
|
shape: z.ZodType;
|
|
102
|
-
value:
|
|
102
|
+
value: Date | readonly unknown[] | import("../index.ts").Ref | import("../index.ts").Json;
|
|
103
103
|
field: ResolvedField;
|
|
104
104
|
}) => z.ZodOptional<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
105
105
|
/** The moment the defaults step writes, which leaves the key optional as a literal does. */
|