@drhomet/mithaq 0.0.0-stage → 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/LICENSE +22 -0
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/LICENSES/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/README.md +160 -2
- package/dist/asas/actor.d.ts +96 -0
- package/dist/asas/actor.js +114 -0
- package/dist/asas/build.d.ts +239 -0
- package/dist/asas/build.js +834 -0
- package/dist/asas/cli/mithaq.d.ts +2 -0
- package/dist/asas/cli/mithaq.js +177 -0
- package/dist/asas/cli/upgrade.d.ts +33 -0
- package/dist/asas/cli/upgrade.js +256 -0
- package/dist/asas/http/fetch.d.ts +41 -0
- package/dist/asas/http/fetch.js +178 -0
- package/dist/asas/http/orpc.d.ts +45 -0
- package/dist/asas/http/orpc.js +153 -0
- package/dist/asas/runner/bulk.d.ts +20 -0
- package/dist/asas/runner/bulk.js +196 -0
- package/dist/asas/runner/errors.d.ts +63 -0
- package/dist/asas/runner/errors.js +75 -0
- package/dist/asas/runner/read.d.ts +72 -0
- package/dist/asas/runner/read.js +601 -0
- package/dist/asas/runner/runtime.d.ts +21 -0
- package/dist/asas/runner/runtime.js +8 -0
- package/dist/asas/runner/steps.d.ts +418 -0
- package/dist/asas/runner/steps.js +277 -0
- package/dist/asas/runner/store.d.ts +4 -0
- package/dist/asas/runner/store.js +21 -0
- package/dist/asas/runner/tenant.d.ts +54 -0
- package/dist/asas/runner/tenant.js +275 -0
- package/dist/asas/runner/write.d.ts +606 -0
- package/dist/asas/runner/write.js +1019 -0
- package/dist/auth/better-auth/access.d.ts +29 -0
- package/dist/auth/better-auth/access.js +103 -0
- package/dist/auth/better-auth/generate.d.ts +31 -0
- package/dist/auth/better-auth/generate.js +328 -0
- package/dist/auth/better-auth/index.d.ts +94 -0
- package/dist/auth/better-auth/index.js +81 -0
- package/dist/auth/better-auth/tenant.d.ts +26 -0
- package/dist/auth/better-auth/tenant.js +34 -0
- package/dist/auth/local/index.d.ts +285 -0
- package/dist/auth/local/index.js +738 -0
- package/dist/base/decimal.d.ts +9 -0
- package/dist/base/decimal.js +16 -0
- package/dist/base/fields.d.ts +124 -0
- package/dist/base/fields.js +52 -0
- package/dist/base/index.d.ts +47 -0
- package/dist/base/index.js +5 -0
- package/dist/base/json-schema.d.ts +7 -0
- package/dist/base/json-schema.js +35 -0
- package/dist/base/kinds.d.ts +75 -0
- package/dist/base/kinds.js +123 -0
- package/dist/base/rules.d.ts +73 -0
- package/dist/base/rules.js +81 -0
- package/dist/drizzle/index.d.ts +308 -0
- package/dist/drizzle/index.js +951 -0
- package/dist/drizzle/kinds.d.ts +28 -0
- package/dist/drizzle/kinds.js +43 -0
- package/dist/drizzle/migrate.d.ts +159 -0
- package/dist/drizzle/migrate.js +1328 -0
- package/dist/drizzle/proxy/index.d.ts +1 -0
- package/dist/drizzle/proxy/index.js +1 -0
- package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
- package/dist/drizzle/proxy/node-postgres.js +1 -0
- package/dist/drizzle/proxy/pg-core.d.ts +1 -0
- package/dist/drizzle/proxy/pg-core.js +1 -0
- package/dist/drizzle/rules.d.ts +336 -0
- package/dist/drizzle/rules.js +44 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +26 -0
- package/dist/mithaq/define.d.ts +335 -0
- package/dist/mithaq/define.js +57 -0
- package/dist/mithaq/generate-messages.d.ts +38 -0
- package/dist/mithaq/generate-messages.js +235 -0
- package/dist/mithaq/language.d.ts +374 -0
- package/dist/mithaq/language.js +76 -0
- package/dist/mithaq/lock.d.ts +71 -0
- package/dist/mithaq/lock.js +329 -0
- package/dist/mithaq/message-file.d.ts +13 -0
- package/dist/mithaq/message-file.js +29 -0
- package/dist/mithaq/messages.d.ts +55 -0
- package/dist/mithaq/messages.js +111 -0
- package/dist/mithaq/refusal.d.ts +6 -0
- package/dist/mithaq/refusal.js +6 -0
- package/dist/mithaq/resolve.d.ts +10 -0
- package/dist/mithaq/resolve.js +1014 -0
- package/dist/mithaq/rows.d.ts +87 -0
- package/dist/mithaq/rows.js +174 -0
- package/dist/mithaq/rukn.d.ts +72 -0
- package/dist/mithaq/rukn.js +15 -0
- package/dist/mithaq/standard-schema.d.ts +38 -0
- package/dist/mithaq/standard-schema.js +2 -0
- package/dist/mithaq/types.d.ts +386 -0
- package/dist/mithaq/types.js +15 -0
- package/dist/mithaq/user.d.ts +19 -0
- package/dist/mithaq/user.js +16 -0
- package/dist/mithaq/where.d.ts +32 -0
- package/dist/mithaq/where.js +87 -0
- package/dist/rbac/index.d.ts +105 -0
- package/dist/rbac/index.js +182 -0
- package/dist/rbac/steps.d.ts +22 -0
- package/dist/rbac/steps.js +161 -0
- package/dist/rich-text/index.d.ts +28 -0
- package/dist/rich-text/index.js +59 -0
- package/dist/rich-text/rich-text.d.ts +106 -0
- package/dist/rich-text/rich-text.js +249 -0
- package/dist/rich-text/value.d.ts +55 -0
- package/dist/rich-text/value.js +126 -0
- package/dist/screen/client.d.ts +156 -0
- package/dist/screen/client.js +71 -0
- package/dist/screen/generate.d.ts +14 -0
- package/dist/screen/generate.js +268 -0
- package/dist/screen/index.d.ts +496 -0
- package/dist/screen/index.js +663 -0
- package/dist/screen/react.d.ts +97 -0
- package/dist/screen/react.js +58 -0
- package/dist/screen/studio.d.ts +24 -0
- package/dist/screen/studio.js +68 -0
- package/dist/service/index.d.ts +63 -0
- package/dist/service/index.js +15 -0
- package/dist/storage/allowed-type.d.ts +5 -0
- package/dist/storage/allowed-type.js +7 -0
- package/dist/storage/index.d.ts +42 -0
- package/dist/storage/index.js +43 -0
- package/dist/storage/storage.d.ts +386 -0
- package/dist/storage/storage.js +412 -0
- package/dist/targets.d.ts +14 -0
- package/dist/targets.js +16 -0
- package/dist/versions/index.d.ts +37 -0
- package/dist/versions/index.js +8 -0
- package/dist/versions/versions.d.ts +104 -0
- package/dist/versions/versions.js +160 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +81 -0
- package/dist/zod/index.d.ts +193 -0
- package/dist/zod/index.js +122 -0
- package/dist/zod/kinds.d.ts +36 -0
- package/dist/zod/kinds.js +58 -0
- package/dist/zod/rules.d.ts +117 -0
- package/dist/zod/rules.js +67 -0
- 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 +92 -4
- 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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { same } from '../mithaq/where.js';
|
|
2
|
+
import { snakeCase, versionsTableOf } from '../mithaq/resolve.js';
|
|
3
|
+
import { providerOf } from '../base/rules.js';
|
|
4
|
+
import { missing } from '../asas/runner/errors.js';
|
|
5
|
+
import { findVersionByID, findVersions, issueRefusal, restoreVersion } from '../asas/runner/steps.js';
|
|
6
|
+
/**
|
|
7
|
+
* The `_status` field a kayan that keeps drafts spreads into its own `fields`.
|
|
8
|
+
* A plain read leaves out a row that is not `published`.
|
|
9
|
+
*/
|
|
10
|
+
export const draftFields = {
|
|
11
|
+
_status: {
|
|
12
|
+
kind: 'text',
|
|
13
|
+
label: { en: 'Status', ar: 'الحالة' },
|
|
14
|
+
options: [
|
|
15
|
+
{ value: 'draft', label: { en: 'Draft', ar: 'مسودة' } },
|
|
16
|
+
{ value: 'published', label: { en: 'Published', ar: 'منشور' } },
|
|
17
|
+
],
|
|
18
|
+
rules: { defaultValue: 'draft', index: true },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The three operations a kayan that keeps versions spreads beside `defaultOperations`:
|
|
23
|
+
* `findVersions`, `findVersionByID` and `restoreVersion`.
|
|
24
|
+
*/
|
|
25
|
+
export const versionOperations = {
|
|
26
|
+
findVersions: { steps: [findVersions] },
|
|
27
|
+
findVersionByID: { steps: [findVersionByID] },
|
|
28
|
+
restoreVersion: { steps: [restoreVersion] },
|
|
29
|
+
};
|
|
30
|
+
export const versionsOf = ({ kayan, kept, name, window, }) => {
|
|
31
|
+
const where = `Kayan "${kayan.name}" writes "targets.${name}"`;
|
|
32
|
+
if (kayan.single)
|
|
33
|
+
throw new Error(`${where}, and a global keeps no versions: versions are a kayan's alone.`);
|
|
34
|
+
// A version is addressed by its own uuid, which the kayan's id shape takes only where its id is a uuid.
|
|
35
|
+
if (kayan.fields.id?.kind !== 'uuid') {
|
|
36
|
+
throw new Error(`${where}, and its id is of kind "${String(kayan.fields.id?.kind)}", while a version is addressed by a uuid. Keep the uuid id defaultFields spreads.`);
|
|
37
|
+
}
|
|
38
|
+
if (kept.drafts && !kayan.fields._status) {
|
|
39
|
+
throw new Error(`${where} with drafts, and declares no field "_status", which is where a row says whether it is a draft or published. Spread draftFields into its fields.`);
|
|
40
|
+
}
|
|
41
|
+
const max = kept.max ?? window;
|
|
42
|
+
if (!Number.isInteger(max) || max < 0)
|
|
43
|
+
throw new Error(`${where} with max ${String(max)}, and max is a whole number of versions, 0 keeping every one.`);
|
|
44
|
+
const interval = typeof kept.drafts === 'object' && typeof kept.drafts.autosave === 'object' ? kept.drafts.autosave.interval : 2000;
|
|
45
|
+
if (!Number.isInteger(interval) || interval <= 0)
|
|
46
|
+
throw new Error(`${where} with an autosave interval of ${String(interval)}, and an interval is a whole number of milliseconds above 0.`);
|
|
47
|
+
const table = versionsTableOf(kayan);
|
|
48
|
+
const column = (field, kind, named = snakeCase(field)) => ({ name: field, kind, column: named, kayan: table });
|
|
49
|
+
return {
|
|
50
|
+
table,
|
|
51
|
+
drafts: !!kept.drafts,
|
|
52
|
+
autosave: typeof kept.drafts === 'object' && kept.drafts.autosave ? { interval } : false,
|
|
53
|
+
max,
|
|
54
|
+
kayan: {
|
|
55
|
+
name: table,
|
|
56
|
+
table,
|
|
57
|
+
...(kayan.schemaName !== undefined && { schemaName: kayan.schemaName }),
|
|
58
|
+
store: kayan.store,
|
|
59
|
+
schema: kayan.schema,
|
|
60
|
+
operations: {},
|
|
61
|
+
indexes: [],
|
|
62
|
+
// No prototype, so a query naming `toString` finds no field rather than a function.
|
|
63
|
+
fields: Object.assign(Object.create(null), {
|
|
64
|
+
id: column('id', 'uuid'),
|
|
65
|
+
parent: { ...column('parent', kayan.fields.id.kind, 'parent_id'), target: kayan },
|
|
66
|
+
latest: column('latest', 'checkbox'),
|
|
67
|
+
autosave: column('autosave', 'checkbox'),
|
|
68
|
+
createdAt: column('createdAt', 'datetime'),
|
|
69
|
+
updatedAt: column('updatedAt', 'datetime'),
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const newest = { field: 'updatedAt', direction: 'desc' };
|
|
75
|
+
// jsonb holds a moment as its ISO string, so each datetime field becomes a `Date` again.
|
|
76
|
+
export const revived = (kayan, snapshot) => {
|
|
77
|
+
const row = { ...snapshot };
|
|
78
|
+
const moment = (held) => (typeof held === 'string' ? new Date(held) : held);
|
|
79
|
+
for (const field of Object.values(kayan.fields)) {
|
|
80
|
+
const held = row[field.name];
|
|
81
|
+
if (field.kind !== 'datetime' || held === null || held === undefined)
|
|
82
|
+
continue;
|
|
83
|
+
row[field.name] = typeof held === 'object' && !(held instanceof Date)
|
|
84
|
+
? Object.fromEntries(Object.entries(held).map(([locale, one]) => [locale, moment(one)]))
|
|
85
|
+
: moment(held);
|
|
86
|
+
}
|
|
87
|
+
return row;
|
|
88
|
+
};
|
|
89
|
+
export const latestVersion = async ({ kayan, store, parent, tx, }) => (await store.find({ kayan: kayan.versions.table, args: {
|
|
90
|
+
limit: 1,
|
|
91
|
+
page: 1,
|
|
92
|
+
sort: newest,
|
|
93
|
+
where: { and: [{ parent: { equals: parent } }, { latest: { equals: true } }] },
|
|
94
|
+
}, tx })).docs[0];
|
|
95
|
+
// In the write's own transaction, so the audit step after it covers it. An
|
|
96
|
+
// autosave overwrites the latest version only where that one is an autosave.
|
|
97
|
+
export const saveVersion = async ({ kayan, store, writes, tx, row, autosave, }) => {
|
|
98
|
+
const { table, max } = kayan.versions;
|
|
99
|
+
const latest = await latestVersion({ kayan, store, parent: row.id, tx });
|
|
100
|
+
// A row's version moments only rise: two saves in one millisecond would let the
|
|
101
|
+
// previous-version read skip one and leave the order to the random ids.
|
|
102
|
+
const now = new Date(Math.max(Date.now(), latest ? new Date(latest.updatedAt).getTime() + 1 : 0));
|
|
103
|
+
if (autosave && latest?.autosave === true) {
|
|
104
|
+
await writes.update({ tx, kayan: table, id: latest.id, data: { version: row, updatedAt: now } });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (latest)
|
|
108
|
+
await writes.update({ tx, kayan: table, id: latest.id, data: { latest: false } });
|
|
109
|
+
await writes.insert({ tx, kayan: table, data: { parent: row.id, version: row, latest: true, autosave, createdAt: now, updatedAt: now } });
|
|
110
|
+
if (max < 1)
|
|
111
|
+
return;
|
|
112
|
+
// Never the one just inserted: two saves in one write can share a millisecond.
|
|
113
|
+
const older = { and: [{ parent: { equals: row.id } }, { latest: { equals: false } }] };
|
|
114
|
+
const past = await store.find({ kayan: table, args: { ...(max > 1 ? { limit: max - 1, page: 2 } : { limit: 0, page: 1 }), sort: newest, where: older }, tx });
|
|
115
|
+
for (const old of past.docs)
|
|
116
|
+
await writes.remove({ tx, kayan: table, id: old.id });
|
|
117
|
+
};
|
|
118
|
+
// `_status` is left out because the write decides it: the row always holds the
|
|
119
|
+
// last published copy, so a restore that writes the row publishes it.
|
|
120
|
+
export const changedColumns = ({ kayan, snapshot, row, whole = false, }) => Object.fromEntries(Object.values(kayan.fields)
|
|
121
|
+
.filter((field) => field.name !== 'id' && field.name !== '_status' && field.kind !== 'join' &&
|
|
122
|
+
providerOf(field, 'update') === 'client' && field.name in snapshot &&
|
|
123
|
+
((whole && snapshot[field.name] != null && !same(snapshot[field.name], [])) || !same(snapshot[field.name], row[field.name])))
|
|
124
|
+
.map((field) => [field.name, snapshot[field.name] ?? null]));
|
|
125
|
+
// The row's read access comes first, so nothing about a row the actor cannot
|
|
126
|
+
// read is told: such a version is refused in the words an absent one is.
|
|
127
|
+
export const restoreInput = async ({ kayan, store, data, actor, authorize, admin, alive, seesDrafts, admits, }) => {
|
|
128
|
+
const absent = () => missing({
|
|
129
|
+
message: `No "${kayan.name}" version with id ${String(data.id)} to restore.`,
|
|
130
|
+
key: 'error.not_found.row',
|
|
131
|
+
params: { kayan: kayan.name, id: data.id, operation: 'restoreVersion' },
|
|
132
|
+
});
|
|
133
|
+
// Nothing has parsed a version id yet, and it is the versions table's own uuid whatever the row's id is.
|
|
134
|
+
if (typeof data.id !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(data.id)) {
|
|
135
|
+
throw issueRefusal([{ message: 'Invalid UUID', path: ['id'] }], 'mithaq');
|
|
136
|
+
}
|
|
137
|
+
// A restore may publish a draft nothing else shows this actor, so it asks the
|
|
138
|
+
// key a draft read asks; without it the id answers as absent.
|
|
139
|
+
if (!seesDrafts)
|
|
140
|
+
throw absent();
|
|
141
|
+
const [version] = (await store.find({ kayan: kayan.versions.table, args: { limit: 1, page: 1, sort: newest, where: { id: { equals: data.id } } } })).docs;
|
|
142
|
+
// No version has that id, so it may name a published row whose pending draft is
|
|
143
|
+
// being discarded: restoring the row onto itself publishes the last published copy.
|
|
144
|
+
const parent = version ? version.parent : data.id;
|
|
145
|
+
// Asked with no operation, so no key but the restore's own is asked for.
|
|
146
|
+
const rowOf = async (id, admitted) => {
|
|
147
|
+
const readable = await authorize({ admin, actor, data: { id }, effect: 'read', kayan });
|
|
148
|
+
const scoped = [{ id: { equals: id } }, readable, alive, admitted].filter(Boolean);
|
|
149
|
+
return (await store.find({ kayan: kayan.name, args: { limit: 1, page: 1, sort: { field: 'id', direction: 'asc' }, where: { and: scoped } } })).docs[0];
|
|
150
|
+
};
|
|
151
|
+
// Version and row ids share one uuid space, and a caller may choose a row's id,
|
|
152
|
+
// so an id that also names a row the actor sees is the row's. Looked for in the
|
|
153
|
+
// actor's scope only, or another tenant could block restores with a learned id.
|
|
154
|
+
if (version && await rowOf(data.id))
|
|
155
|
+
throw absent();
|
|
156
|
+
const row = await rowOf(parent, admits);
|
|
157
|
+
if (!row || (!version && row._status !== 'published'))
|
|
158
|
+
throw absent();
|
|
159
|
+
return { ...(version && changedColumns({ kayan, snapshot: revived(kayan, version.version), row, whole: kayan.versions.drafts && row._status !== 'published' })), id: parent };
|
|
160
|
+
};
|
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
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ResolvedField, RuntimeField } from '../mithaq/types.ts';
|
|
3
|
+
import type { Derives, FieldsOf } from '../asas/runner/steps.ts';
|
|
4
|
+
import type { CreateOf, IdOf, RowOf, UpdateOf } from '../asas/runner/write.ts';
|
|
5
|
+
export type ZodShape = (field: ResolvedField) => z.ZodType;
|
|
6
|
+
export type ZodRule<Value = never> = (args: {
|
|
7
|
+
shape: z.ZodType;
|
|
8
|
+
value: Value;
|
|
9
|
+
field: ResolvedField;
|
|
10
|
+
}) => z.ZodType;
|
|
11
|
+
type ZodTyped<Value> = z.ZodType<Value, Value>;
|
|
12
|
+
type ZodColumn<Value> = undefined extends Value ? z.ZodOptional<ZodTyped<Exclude<Value, undefined>>> : ZodTyped<Value>;
|
|
13
|
+
type ZodDerived<Shape, Config extends z.core.$ZodObjectConfig> = z.ZodObject<{
|
|
14
|
+
[Key in keyof Shape]-?: ZodColumn<Shape[Key]>;
|
|
15
|
+
}, Config>;
|
|
16
|
+
type ZodShapes<F extends Record<string, RuntimeField>> = {
|
|
17
|
+
create: ZodDerived<CreateOf<F, 'zod'>, z.core.$strict>;
|
|
18
|
+
update: ZodDerived<UpdateOf<F, 'zod'>, z.core.$strict>;
|
|
19
|
+
id: ZodDerived<IdOf<F, 'zod'>, z.core.$strict>;
|
|
20
|
+
byID: z.ZodObject<ZodDerived<IdOf<F, 'zod'>, z.core.$strict>['shape'] & {
|
|
21
|
+
depth: z.ZodOptional<z.ZodUnknown>;
|
|
22
|
+
}, z.core.$strict>;
|
|
23
|
+
row: ZodDerived<RowOf<F, 'zod'>, z.core.$strip>;
|
|
24
|
+
list: typeof listShape;
|
|
25
|
+
draft: z.ZodObject<{
|
|
26
|
+
[Key in keyof RowOf<F, 'zod'>]-?: z.ZodOptional<z.ZodNullable<ZodTyped<RowOf<F, 'zod'>[Key]>>>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
draftWrite: z.ZodObject<{
|
|
29
|
+
[Key in keyof RowOf<F, 'zod'>]-?: z.ZodOptional<z.ZodNullable<ZodTyped<RowOf<F, 'zod'>[Key]>>>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
};
|
|
32
|
+
declare const listShape: z.ZodObject<{
|
|
33
|
+
data: z.ZodOptional<z.ZodUnknown>;
|
|
34
|
+
depth: z.ZodOptional<z.ZodUnknown>;
|
|
35
|
+
for: z.ZodOptional<z.ZodUnknown>;
|
|
36
|
+
limit: z.ZodOptional<z.ZodUnknown>;
|
|
37
|
+
page: z.ZodOptional<z.ZodUnknown>;
|
|
38
|
+
sort: z.ZodOptional<z.ZodUnknown>;
|
|
39
|
+
where: z.ZodOptional<z.ZodUnknown>;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
interface ZodDerives extends Derives {
|
|
42
|
+
readonly shapes: ZodShapes<FieldsOf<this>>;
|
|
43
|
+
}
|
|
44
|
+
declare const target: {
|
|
45
|
+
readonly capability: 'schema';
|
|
46
|
+
readonly type: 'zod';
|
|
47
|
+
readonly idKinds: ("number" | "text" | "uuid")[];
|
|
48
|
+
readonly kinds: {
|
|
49
|
+
text: (field?: ResolvedField, element?: boolean) => z.ZodEnum<{
|
|
50
|
+
[x: string]: string;
|
|
51
|
+
}> | z.ZodString;
|
|
52
|
+
email: (field?: ResolvedField) => z.ZodEmail;
|
|
53
|
+
number: (field?: ResolvedField) => z.ZodGUID | z.ZodNumber | z.ZodString;
|
|
54
|
+
integer: (field?: ResolvedField) => z.ZodInt32;
|
|
55
|
+
decimal: (field?: ResolvedField) => z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
56
|
+
checkbox: (field?: ResolvedField) => z.ZodBoolean;
|
|
57
|
+
datetime: (field?: ResolvedField) => z.ZodDate;
|
|
58
|
+
date: (field?: ResolvedField) => z.ZodISODate;
|
|
59
|
+
textArray: (field?: ResolvedField) => z.ZodArray<z.ZodEnum<{
|
|
60
|
+
[x: string]: string;
|
|
61
|
+
}> | z.ZodString>;
|
|
62
|
+
uuid: (field?: ResolvedField) => z.ZodGUID | z.ZodInt32 | z.ZodString;
|
|
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>;
|
|
65
|
+
join: (field?: ResolvedField) => z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
66
|
+
};
|
|
67
|
+
readonly rules: {
|
|
68
|
+
unique: ({ shape }: {
|
|
69
|
+
shape: z.ZodType;
|
|
70
|
+
value: boolean;
|
|
71
|
+
field: ResolvedField;
|
|
72
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
73
|
+
index: ({ shape }: {
|
|
74
|
+
shape: z.ZodType;
|
|
75
|
+
value: boolean;
|
|
76
|
+
field: ResolvedField;
|
|
77
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
78
|
+
onDelete: ({ shape }: {
|
|
79
|
+
shape: z.ZodType;
|
|
80
|
+
value: import("../base/kinds.ts").OnDelete;
|
|
81
|
+
field: ResolvedField;
|
|
82
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
83
|
+
providedBy: ({ shape }: {
|
|
84
|
+
shape: z.ZodType;
|
|
85
|
+
value: Readonly<{
|
|
86
|
+
create?: import("../base/rules.ts").Provider;
|
|
87
|
+
update?: import("../base/rules.ts").Provider;
|
|
88
|
+
}>;
|
|
89
|
+
field: ResolvedField;
|
|
90
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
91
|
+
minLength: ({ shape, value: length, field }: {
|
|
92
|
+
shape: z.ZodType;
|
|
93
|
+
value: number;
|
|
94
|
+
field: ResolvedField;
|
|
95
|
+
}) => z.ZodString;
|
|
96
|
+
maxLength: ({ shape, value: length, field }: {
|
|
97
|
+
shape: z.ZodType;
|
|
98
|
+
value: number;
|
|
99
|
+
field: ResolvedField;
|
|
100
|
+
}) => z.ZodString;
|
|
101
|
+
pattern: ({ shape, value: expression, field }: {
|
|
102
|
+
shape: z.ZodType;
|
|
103
|
+
value: RegExp;
|
|
104
|
+
field: ResolvedField;
|
|
105
|
+
}) => z.ZodString;
|
|
106
|
+
precision: ({ shape, value: precision, field }: {
|
|
107
|
+
shape: z.ZodType;
|
|
108
|
+
value: number;
|
|
109
|
+
field: ResolvedField;
|
|
110
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
111
|
+
scale: ({ shape, value: scale, field }: {
|
|
112
|
+
shape: z.ZodType;
|
|
113
|
+
value: number;
|
|
114
|
+
field: ResolvedField;
|
|
115
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
116
|
+
min: ({ shape, value, field }: {
|
|
117
|
+
shape: z.ZodType;
|
|
118
|
+
value: string | number;
|
|
119
|
+
field: ResolvedField;
|
|
120
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
121
|
+
max: ({ shape, value, field }: {
|
|
122
|
+
shape: z.ZodType;
|
|
123
|
+
value: string | number;
|
|
124
|
+
field: ResolvedField;
|
|
125
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
126
|
+
minRows: ({ shape, value: rows, field }: {
|
|
127
|
+
shape: z.ZodType;
|
|
128
|
+
value: number;
|
|
129
|
+
field: ResolvedField;
|
|
130
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
131
|
+
maxRows: ({ shape, value: rows, field }: {
|
|
132
|
+
shape: z.ZodType;
|
|
133
|
+
value: number;
|
|
134
|
+
field: ResolvedField;
|
|
135
|
+
}) => z.ZodArray<z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
136
|
+
trim: ({ shape, value: on, field }: {
|
|
137
|
+
shape: z.ZodType;
|
|
138
|
+
value: boolean;
|
|
139
|
+
field: ResolvedField;
|
|
140
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
141
|
+
uppercase: ({ shape, value: on, field }: {
|
|
142
|
+
shape: z.ZodType;
|
|
143
|
+
value: boolean;
|
|
144
|
+
field: ResolvedField;
|
|
145
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
146
|
+
lowercase: ({ shape, value: on, field }: {
|
|
147
|
+
shape: z.ZodType;
|
|
148
|
+
value: boolean;
|
|
149
|
+
field: ResolvedField;
|
|
150
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
151
|
+
localized: ({ shape, value: on, field }: {
|
|
152
|
+
shape: z.ZodType;
|
|
153
|
+
value: boolean;
|
|
154
|
+
field: ResolvedField;
|
|
155
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
156
|
+
required: ({ shape, value: on, field }: {
|
|
157
|
+
shape: z.ZodType;
|
|
158
|
+
value: boolean;
|
|
159
|
+
field: ResolvedField;
|
|
160
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
161
|
+
defaultValue: ({ shape }: {
|
|
162
|
+
shape: z.ZodType;
|
|
163
|
+
value: Date | readonly unknown[] | import("../index.ts").Ref | import("../base/kinds.ts").Json;
|
|
164
|
+
field: ResolvedField;
|
|
165
|
+
}) => z.ZodOptional<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
166
|
+
defaultNow: ({ shape, value: on }: {
|
|
167
|
+
shape: z.ZodType;
|
|
168
|
+
value: boolean;
|
|
169
|
+
field: ResolvedField;
|
|
170
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
171
|
+
nowOnUpdate: ({ shape }: {
|
|
172
|
+
shape: z.ZodType;
|
|
173
|
+
value: boolean;
|
|
174
|
+
field: ResolvedField;
|
|
175
|
+
}) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
176
|
+
};
|
|
177
|
+
readonly derive: <F extends Record<string, ResolvedField>>(this: {
|
|
178
|
+
kinds: Record<string, ZodShape>;
|
|
179
|
+
rules: Record<string, ZodRule>;
|
|
180
|
+
}, fields: F) => ZodShapes<F>;
|
|
181
|
+
};
|
|
182
|
+
/** The zod schema target. */
|
|
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;
|
|
191
|
+
readonly derives?: ZodDerives;
|
|
192
|
+
};
|
|
193
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { storedRichText } from '../rich-text/value.js';
|
|
3
|
+
import { providerOf } from '../base/rules.js';
|
|
4
|
+
import { idKinds as zodIdKinds, kinds as zodKinds } from './kinds.js';
|
|
5
|
+
import { implementationOf } from '../mithaq/rukn.js';
|
|
6
|
+
import { rules as zodRules } from './rules.js';
|
|
7
|
+
// Every rule is applied here and nowhere else. A field's `targets.zod` is its
|
|
8
|
+
// whole shape, rules skipped.
|
|
9
|
+
const fieldShapes = (fields, shapes, rules) => Object.fromEntries(Object.values(fields).map((field) => {
|
|
10
|
+
const override = (field.schemaTarget && field.targets?.[field.schemaTarget]);
|
|
11
|
+
if (override)
|
|
12
|
+
return [field.name, override];
|
|
13
|
+
// The project's own entry, then the kind's own, then the shipped table.
|
|
14
|
+
let shape = implementationOf({ table: shapes, shipped: zodKinds, name: field.kind, own: field.kindTargets, type: 'zod' })(field);
|
|
15
|
+
const fieldRules = field.rules ?? {};
|
|
16
|
+
for (const rule of Object.keys(rules)) {
|
|
17
|
+
if (rule in fieldRules) {
|
|
18
|
+
shape = rules[rule]({ shape, value: fieldRules[rule], field });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// An id is filled by the database or by whoever owns the table, so a write never has to carry one.
|
|
22
|
+
return [field.name, field.name === 'id' ? shape.optional() : shape];
|
|
23
|
+
}));
|
|
24
|
+
// Strict, so an unknown key is refused at the boundary rather than stripped.
|
|
25
|
+
const kayanSchema = (built) => z.strictObject(built);
|
|
26
|
+
// A field the client does not supply is absent, not optional, so the strict
|
|
27
|
+
// object refuses a caller that sends it rather than discarding the value.
|
|
28
|
+
const supplied = (built, fields, on) => Object.fromEntries(Object.values(fields)
|
|
29
|
+
.filter((field) => field.name === 'id' || providerOf(field, on) === 'client')
|
|
30
|
+
.map((field) => [field.name, built[field.name]]));
|
|
31
|
+
const outputShape = (shape) => {
|
|
32
|
+
const hasTransform = (value, seen = new WeakSet()) => {
|
|
33
|
+
if (!value || typeof value !== 'object' || seen.has(value))
|
|
34
|
+
return false;
|
|
35
|
+
seen.add(value);
|
|
36
|
+
if (value instanceof z.ZodType) {
|
|
37
|
+
const definition = value._def;
|
|
38
|
+
if (definition?.type === 'transform')
|
|
39
|
+
return true;
|
|
40
|
+
return hasTransform(definition, seen);
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(value))
|
|
43
|
+
return value.some((one) => hasTransform(one, seen));
|
|
44
|
+
return Object.values(value).some((one) => hasTransform(one, seen));
|
|
45
|
+
};
|
|
46
|
+
const definition = shape._def;
|
|
47
|
+
if (definition?.type === 'optional' && definition.innerType)
|
|
48
|
+
return outputShape(definition.innerType);
|
|
49
|
+
if (definition?.type === 'nullable' && definition.innerType) {
|
|
50
|
+
return outputShape(definition.innerType).nullable();
|
|
51
|
+
}
|
|
52
|
+
if (['catch', 'default'].includes(String(definition?.type)) && definition?.innerType) {
|
|
53
|
+
return outputShape(definition.innerType);
|
|
54
|
+
}
|
|
55
|
+
if (definition?.type === 'pipe' && definition.out)
|
|
56
|
+
return outputShape(definition.out);
|
|
57
|
+
if (hasTransform(shape)) {
|
|
58
|
+
throw new Error('A zod transform override needs an output schema: use `.pipe(schema)`.');
|
|
59
|
+
}
|
|
60
|
+
return shape;
|
|
61
|
+
};
|
|
62
|
+
// A `join` is optional rather than nullable: a read with no depth hands back no
|
|
63
|
+
// value for it, and a nullable key would demand an explicit `null`.
|
|
64
|
+
const rowSchema = (schema, fields) => z.object(Object.fromEntries(Object.entries(schema.shape).map(([name, shape]) => [
|
|
65
|
+
name,
|
|
66
|
+
(() => {
|
|
67
|
+
const required = outputShape(shape);
|
|
68
|
+
if (fields[name]?.kind === 'join')
|
|
69
|
+
return required.optional();
|
|
70
|
+
return name === 'id' || fields[name]?.rules?.required ? required : required.nullable();
|
|
71
|
+
})(),
|
|
72
|
+
])));
|
|
73
|
+
// Strict: a misspelled `wher` stripped would answer every row unfiltered.
|
|
74
|
+
const listShape = z.strictObject({
|
|
75
|
+
data: z.unknown().optional(),
|
|
76
|
+
depth: z.unknown().optional(),
|
|
77
|
+
for: z.unknown().optional(),
|
|
78
|
+
limit: z.unknown().optional(),
|
|
79
|
+
page: z.unknown().optional(),
|
|
80
|
+
sort: z.unknown().optional(),
|
|
81
|
+
where: z.unknown().optional(),
|
|
82
|
+
});
|
|
83
|
+
// The cast puts the mapped return type on: nothing inside can prove what `ZodDerives` states.
|
|
84
|
+
const target = {
|
|
85
|
+
capability: 'schema',
|
|
86
|
+
type: 'zod',
|
|
87
|
+
idKinds: zodIdKinds,
|
|
88
|
+
kinds: zodKinds,
|
|
89
|
+
rules: zodRules,
|
|
90
|
+
derive(fields) {
|
|
91
|
+
const built = fieldShapes(fields, this.kinds, this.rules);
|
|
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.
|
|
94
|
+
const kinds = Object.fromEntries(Object.entries(fields).map(([name, field]) => [
|
|
95
|
+
name,
|
|
96
|
+
{ ...field, options: undefined, jsonSchema: undefined, maxBytes: undefined, ...(field.kind === 'richText' && { richText: storedRichText }) },
|
|
97
|
+
]));
|
|
98
|
+
const stored = fieldShapes(kinds, this.kinds, { localized: this.rules.localized });
|
|
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 });
|
|
100
|
+
const create = kayanSchema(supplied(built, fields, 'create'));
|
|
101
|
+
const changeable = kayanSchema(supplied(built, fields, 'update'));
|
|
102
|
+
const update = 'id' in fields ? changeable.partial().required({ id: true }) : changeable.partial();
|
|
103
|
+
// `pick` keeps the strict object strict, so every other key is refused.
|
|
104
|
+
const id = 'id' in fields ? update.pick({ id: true }) : z.strictObject({});
|
|
105
|
+
return {
|
|
106
|
+
create,
|
|
107
|
+
update,
|
|
108
|
+
id,
|
|
109
|
+
byID: id.extend({ depth: z.unknown().optional() }),
|
|
110
|
+
// Built from every field: a row is what the store holds, not what a caller may send.
|
|
111
|
+
row: rowSchema(kayanSchema(stored), fields),
|
|
112
|
+
list: listShape,
|
|
113
|
+
// Stripped rather than strict: the runner has already refused unknown keys, and
|
|
114
|
+
// a version stored before a field was dropped still reads.
|
|
115
|
+
draft: z.object(Object.fromEntries(Object.entries(stored).map(([name, shape]) => [name, shape.nullable().optional()]))),
|
|
116
|
+
// A draft may leave out what `required` asks for, but its rich text is still checked.
|
|
117
|
+
draftWrite: z.object(Object.fromEntries(Object.entries(drafted).map(([name, shape]) => [name, shape.nullable().optional()]))),
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
/** The zod schema target. */
|
|
122
|
+
export const zodTarget = ({ kinds, rules, idKinds }) => ({ ...target, kinds, rules, idKinds });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ResolvedField } from '../mithaq/types.ts';
|
|
3
|
+
import { type Json } from '../base/kinds.ts';
|
|
4
|
+
export declare const errorOf: (field?: ResolvedField, element?: boolean) => ((issue: {
|
|
5
|
+
input: unknown;
|
|
6
|
+
}) => string) | undefined;
|
|
7
|
+
type KindError = ReturnType<typeof errorOf>;
|
|
8
|
+
declare const idShapes: {
|
|
9
|
+
number: (error?: KindError) => z.ZodInt32;
|
|
10
|
+
text: (error?: KindError) => z.ZodString;
|
|
11
|
+
uuid: (error?: KindError) => z.ZodGUID;
|
|
12
|
+
};
|
|
13
|
+
/** The kinds this target shapes an id of, the same three the store's columns are. */
|
|
14
|
+
export declare const idKinds: (keyof typeof idShapes)[];
|
|
15
|
+
declare const textShape: (field?: ResolvedField, element?: boolean) => z.ZodEnum<{
|
|
16
|
+
[x: string]: string;
|
|
17
|
+
}> | z.ZodString;
|
|
18
|
+
/** One shape per kind, the same record shape as the adapter's kinds. */
|
|
19
|
+
export declare const kinds: {
|
|
20
|
+
text: typeof textShape;
|
|
21
|
+
email: (field?: ResolvedField) => z.ZodEmail;
|
|
22
|
+
number: (field?: ResolvedField) => z.ZodGUID | z.ZodNumber | z.ZodString;
|
|
23
|
+
integer: (field?: ResolvedField) => z.ZodInt32;
|
|
24
|
+
decimal: (field?: ResolvedField) => z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>;
|
|
25
|
+
checkbox: (field?: ResolvedField) => z.ZodBoolean;
|
|
26
|
+
datetime: (field?: ResolvedField) => z.ZodDate;
|
|
27
|
+
date: (field?: ResolvedField) => z.ZodISODate;
|
|
28
|
+
textArray: (field?: ResolvedField) => z.ZodArray<z.ZodEnum<{
|
|
29
|
+
[x: string]: string;
|
|
30
|
+
}> | z.ZodString>;
|
|
31
|
+
uuid: (field?: ResolvedField) => z.ZodGUID | z.ZodInt32 | z.ZodString;
|
|
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>;
|
|
34
|
+
join: (field?: ResolvedField) => z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
35
|
+
};
|
|
36
|
+
export {};
|