@drhomet/mithaq 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/payload.txt +22 -0
  3. package/LICENSES/ponytail.txt +21 -0
  4. package/dist/asas/actor.d.ts +96 -0
  5. package/dist/asas/actor.js +114 -0
  6. package/dist/asas/build.d.ts +231 -0
  7. package/dist/asas/build.js +787 -0
  8. package/dist/asas/cli/mithaq.d.ts +2 -0
  9. package/dist/asas/cli/mithaq.js +145 -0
  10. package/dist/asas/cli/upgrade.d.ts +20 -0
  11. package/dist/asas/cli/upgrade.js +147 -0
  12. package/dist/asas/http/fetch.d.ts +23 -0
  13. package/dist/asas/http/fetch.js +115 -0
  14. package/dist/asas/http/orpc.d.ts +43 -0
  15. package/dist/asas/http/orpc.js +135 -0
  16. package/dist/asas/runner/bulk.d.ts +20 -0
  17. package/dist/asas/runner/bulk.js +191 -0
  18. package/dist/asas/runner/errors.d.ts +60 -0
  19. package/dist/asas/runner/errors.js +59 -0
  20. package/dist/asas/runner/read.d.ts +68 -0
  21. package/dist/asas/runner/read.js +579 -0
  22. package/dist/asas/runner/runtime.d.ts +21 -0
  23. package/dist/asas/runner/runtime.js +8 -0
  24. package/dist/asas/runner/steps.d.ts +400 -0
  25. package/dist/asas/runner/steps.js +271 -0
  26. package/dist/asas/runner/tenant.d.ts +54 -0
  27. package/dist/asas/runner/tenant.js +213 -0
  28. package/dist/asas/runner/write.d.ts +444 -0
  29. package/dist/asas/runner/write.js +791 -0
  30. package/dist/auth/better-auth/access.d.ts +15 -0
  31. package/dist/auth/better-auth/access.js +71 -0
  32. package/dist/auth/better-auth/generate.d.ts +29 -0
  33. package/dist/auth/better-auth/generate.js +310 -0
  34. package/dist/auth/better-auth/index.d.ts +92 -0
  35. package/dist/auth/better-auth/index.js +81 -0
  36. package/dist/auth/better-auth/tenant.d.ts +18 -0
  37. package/dist/auth/better-auth/tenant.js +26 -0
  38. package/dist/auth/local/index.d.ts +285 -0
  39. package/dist/auth/local/index.js +736 -0
  40. package/dist/base/decimal.d.ts +9 -0
  41. package/dist/base/decimal.js +16 -0
  42. package/dist/base/fields.d.ts +123 -0
  43. package/dist/base/fields.js +51 -0
  44. package/dist/base/index.d.ts +46 -0
  45. package/dist/base/index.js +5 -0
  46. package/dist/base/kinds.d.ts +54 -0
  47. package/dist/base/kinds.js +78 -0
  48. package/dist/base/rules.d.ts +73 -0
  49. package/dist/base/rules.js +80 -0
  50. package/dist/drizzle/index.d.ts +438 -0
  51. package/dist/drizzle/index.js +832 -0
  52. package/dist/drizzle/kinds.d.ts +27 -0
  53. package/dist/drizzle/kinds.js +41 -0
  54. package/dist/drizzle/migrate.d.ts +133 -0
  55. package/dist/drizzle/migrate.js +1285 -0
  56. package/dist/drizzle/proxy/index.d.ts +1 -0
  57. package/dist/drizzle/proxy/index.js +1 -0
  58. package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
  59. package/dist/drizzle/proxy/node-postgres.js +1 -0
  60. package/dist/drizzle/proxy/pg-core.d.ts +1 -0
  61. package/dist/drizzle/proxy/pg-core.js +1 -0
  62. package/dist/drizzle/rules.d.ts +336 -0
  63. package/dist/drizzle/rules.js +44 -0
  64. package/dist/index.d.ts +45 -0
  65. package/dist/index.js +36 -0
  66. package/dist/mithaq/define.d.ts +288 -0
  67. package/dist/mithaq/define.js +43 -0
  68. package/dist/mithaq/generate-messages.d.ts +38 -0
  69. package/dist/mithaq/generate-messages.js +234 -0
  70. package/dist/mithaq/language.d.ts +335 -0
  71. package/dist/mithaq/language.js +76 -0
  72. package/dist/mithaq/lock.d.ts +71 -0
  73. package/dist/mithaq/lock.js +329 -0
  74. package/dist/mithaq/messages.d.ts +55 -0
  75. package/dist/mithaq/messages.js +106 -0
  76. package/dist/mithaq/refusal.d.ts +6 -0
  77. package/dist/mithaq/refusal.js +6 -0
  78. package/dist/mithaq/resolve.d.ts +10 -0
  79. package/dist/mithaq/resolve.js +954 -0
  80. package/dist/mithaq/rows.d.ts +87 -0
  81. package/dist/mithaq/rows.js +174 -0
  82. package/dist/mithaq/rukn.d.ts +72 -0
  83. package/dist/mithaq/rukn.js +15 -0
  84. package/dist/mithaq/standard-schema.d.ts +38 -0
  85. package/dist/mithaq/standard-schema.js +2 -0
  86. package/dist/mithaq/types.d.ts +358 -0
  87. package/dist/mithaq/types.js +15 -0
  88. package/dist/mithaq/user.d.ts +19 -0
  89. package/dist/mithaq/user.js +16 -0
  90. package/dist/mithaq/where.d.ts +32 -0
  91. package/dist/mithaq/where.js +87 -0
  92. package/dist/rbac/index.d.ts +105 -0
  93. package/dist/rbac/index.js +182 -0
  94. package/dist/rbac/steps.d.ts +22 -0
  95. package/dist/rbac/steps.js +161 -0
  96. package/dist/rich-text/index.d.ts +27 -0
  97. package/dist/rich-text/index.js +58 -0
  98. package/dist/rich-text/rich-text.d.ts +159 -0
  99. package/dist/rich-text/rich-text.js +372 -0
  100. package/dist/screen/client.d.ts +153 -0
  101. package/dist/screen/client.js +68 -0
  102. package/dist/screen/generate.d.ts +14 -0
  103. package/dist/screen/generate.js +226 -0
  104. package/dist/screen/index.d.ts +479 -0
  105. package/dist/screen/index.js +645 -0
  106. package/dist/screen/react.d.ts +95 -0
  107. package/dist/screen/react.js +58 -0
  108. package/dist/screen/studio.d.ts +24 -0
  109. package/dist/screen/studio.js +68 -0
  110. package/dist/service/index.d.ts +63 -0
  111. package/dist/service/index.js +15 -0
  112. package/dist/storage/allowed-type.d.ts +5 -0
  113. package/dist/storage/allowed-type.js +7 -0
  114. package/dist/storage/index.d.ts +43 -0
  115. package/dist/storage/index.js +44 -0
  116. package/dist/storage/storage.d.ts +384 -0
  117. package/dist/storage/storage.js +405 -0
  118. package/dist/versions/index.d.ts +37 -0
  119. package/dist/versions/index.js +8 -0
  120. package/dist/versions/versions.d.ts +104 -0
  121. package/dist/versions/versions.js +160 -0
  122. package/dist/zod/index.d.ts +184 -0
  123. package/dist/zod/index.js +121 -0
  124. package/dist/zod/kinds.d.ts +34 -0
  125. package/dist/zod/kinds.js +49 -0
  126. package/dist/zod/rules.d.ts +117 -0
  127. package/dist/zod/rules.js +67 -0
  128. package/package.json +63 -4
  129. package/r/mithaq-cells.json +20 -0
  130. package/r/mithaq-controls.json +37 -0
  131. package/r/mithaq-directional-icon.json +15 -0
  132. package/r/mithaq-expected-updated-at.json +15 -0
  133. package/r/mithaq-field-shell.json +18 -0
  134. package/r/mithaq-filter-operators.json +15 -0
  135. package/r/mithaq-form.json +33 -0
  136. package/r/mithaq-list-columns.json +23 -0
  137. package/r/mithaq-list-filters.json +24 -0
  138. package/r/mithaq-list-pagination.json +24 -0
  139. package/r/mithaq-list-query.json +18 -0
  140. package/r/mithaq-list-selection.json +19 -0
  141. package/r/mithaq-list.json +33 -0
  142. package/r/mithaq-nav.json +24 -0
  143. package/r/mithaq-nodes.json +34 -0
  144. package/r/mithaq-relationship-cell.json +20 -0
  145. package/r/mithaq-relationship-control.json +20 -0
  146. package/r/mithaq-rich-text.json +40 -0
  147. package/r/mithaq-screens-host.json +96 -0
  148. package/r/mithaq-storage.json +44 -0
  149. package/r/mithaq-upload.json +29 -0
  150. package/r/registry.json +550 -0
  151. package/README.md +0 -3
@@ -0,0 +1,372 @@
1
+ // No React and no DOM: the server checks a write with the same node list the browser draws with.
2
+ import { HorizontalRuleNode } from '@lexical/extension';
3
+ import { LinkNode } from '@lexical/link';
4
+ import { ListItemNode, ListNode } from '@lexical/list';
5
+ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
6
+ import { $getDocument, $getRoot, $isDecoratorNode, $isElementNode, $getState, $setState, createEditor, createState, DecoratorNode, LineBreakNode, ParagraphNode, RootNode, TabNode, TextNode, } from 'lexical';
7
+ /**
8
+ * How deep a rich text value may nest, how large it may be and how many nodes it may hold,
9
+ * all checked before any Lexical code reads it.
10
+ */
11
+ export const richTextBounds = { depth: 64, bytes: 512 * 1024, nodes: 10_000 };
12
+ // Depth is checked before `JSON.stringify` walks a hostile value. An object met
13
+ // twice is refused, so one reachable many ways is not walked once per way.
14
+ const within = (value, depth, seen = new WeakSet()) => {
15
+ if (value === null || typeof value !== 'object')
16
+ return undefined;
17
+ if (depth <= 0)
18
+ return `The value nests deeper than ${richTextBounds.depth} levels.`;
19
+ if (seen.has(value))
20
+ return 'The value holds one object in two places.';
21
+ seen.add(value);
22
+ for (const one of Object.values(value)) {
23
+ const refused = within(one, depth - 1, seen);
24
+ if (refused)
25
+ return refused;
26
+ }
27
+ return undefined;
28
+ };
29
+ /** Whether a link may point here: `http`, `https`, `mailto`, `tel`, a fragment or a path. */
30
+ export const safeUrl = (url) => {
31
+ if (typeof url !== 'string')
32
+ return false;
33
+ // eslint-disable-next-line no-control-regex
34
+ const cleaned = url.trim().replace(/[\x00-\x1f\x7f]/g, '');
35
+ if (cleaned.startsWith('#') || cleaned.startsWith('/') || cleaned.startsWith('./') || cleaned.startsWith('../'))
36
+ return true;
37
+ const protocol = cleaned.match(/^([a-z][a-z0-9+\-.]*):(?=.)/i)?.[1]?.toLowerCase();
38
+ // A web address must name a host: refuses `https://`, `http:///path` (read as
39
+ // a host named `path`), `https://@` and the like.
40
+ if (protocol === 'http' || protocol === 'https') {
41
+ try {
42
+ return /^https?:\/\/[^/?#\\\s]/i.test(cleaned) && /[\p{L}\p{N}]/u.test(new URL(cleaned).hostname);
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ return protocol === 'mailto' || protocol === 'tel';
49
+ };
50
+ const held = (node, key) => node[key];
51
+ /**
52
+ * Why a value is no Lexical editor state a `richText` column may hold, or nothing. Runs before
53
+ * any Lexical code reads the value; a `style` is refused, since no editor of the project writes one.
54
+ */
55
+ export const richTextGuard = (value) => {
56
+ if (!value || typeof value !== 'object' || Array.isArray(value))
57
+ return 'The value is no object.';
58
+ const nested = within(value, richTextBounds.depth);
59
+ if (nested)
60
+ return nested;
61
+ if (new TextEncoder().encode(JSON.stringify(value)).length > richTextBounds.bytes)
62
+ return `The value is larger than ${richTextBounds.bytes} bytes.`;
63
+ const root = held(value, 'root');
64
+ if (!root || typeof root !== 'object' || Array.isArray(root) || held(root, 'type') !== 'root')
65
+ return 'The value has no root of type "root".';
66
+ if (!Array.isArray(held(root, 'children')))
67
+ return 'The root holds no list of children.';
68
+ let count = 0;
69
+ const walk = (node) => {
70
+ if (!node || typeof node !== 'object' || Array.isArray(node) || typeof held(node, 'type') !== 'string')
71
+ return 'A node is no object with a string type.';
72
+ if (++count > richTextBounds.nodes)
73
+ return `The value holds more than ${richTextBounds.nodes} nodes.`;
74
+ const type = held(node, 'type');
75
+ if (Object.hasOwn(node, 'text') && typeof held(node, 'text') !== 'string')
76
+ return `A node of type "${type}" holds text that is no string.`;
77
+ if (typeof held(node, 'text') === 'string') {
78
+ for (const key of ['format', 'detail']) {
79
+ if (Object.hasOwn(node, key) && !Number.isInteger(held(node, key)))
80
+ return `A node of type "${type}" holds a ${key} that is no whole number.`;
81
+ }
82
+ }
83
+ for (const key of ['style', 'textStyle']) {
84
+ if (Object.hasOwn(node, key) && held(node, key) !== '')
85
+ return `A node of type "${type}" holds a ${key}, and this editor writes none.`;
86
+ }
87
+ if ((type === 'link' || type === 'autolink') && !safeUrl(held(node, 'url')))
88
+ return `A link points at an address that is not http, https, mailto, tel or a path.`;
89
+ if (!Object.hasOwn(node, 'children'))
90
+ return undefined;
91
+ const children = held(node, 'children');
92
+ if (!Array.isArray(children))
93
+ return `A node of type "${type}" holds children that are no list.`;
94
+ for (const child of children) {
95
+ const refused = walk(child);
96
+ if (refused)
97
+ return refused;
98
+ }
99
+ return undefined;
100
+ };
101
+ return walk(root);
102
+ };
103
+ /** Whether a value passes `richTextGuard`, which is all the kind itself asks of one. */
104
+ export const isRichText = (value) => richTextGuard(value) === undefined;
105
+ /** The node classes the registry's editor draws. A project's list spreads it: `[...defaultNodes, CalloutNode]`. */
106
+ export const defaultNodes = [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, HorizontalRuleNode];
107
+ /** The nodes every Lexical editor registers whatever it is given. */
108
+ const coreNodes = [RootNode, ParagraphNode, TextNode, LineBreakNode, TabNode];
109
+ /** Every node type a list of classes can read, the core ones included. */
110
+ export const typesOf = (nodes) => new Set([...coreNodes, ...nodes].map((node) => node.getType()));
111
+ /**
112
+ * The first node type under a value's `root` that `types` does not hold, or nothing, and `''`
113
+ * past the depth bound. Read off the JSON, since a production Lexical minifies its errors.
114
+ */
115
+ export const unknownType = ({ node, types, depth = richTextBounds.depth }) => {
116
+ if (!node || typeof node !== 'object')
117
+ return undefined;
118
+ if (depth <= 0)
119
+ return '';
120
+ const held = node;
121
+ if (typeof held.type === 'string' && !types.has(held.type))
122
+ return held.type;
123
+ for (const child of Array.isArray(held.children) ? held.children : []) {
124
+ const type = unknownType({ node: child, types, depth: depth - 1 });
125
+ if (type !== undefined)
126
+ return type;
127
+ }
128
+ return undefined;
129
+ };
130
+ // Set by `richTextReader` around its own synchronous parse alone, so a value is
131
+ // never checked against another build's list.
132
+ let holdingFiles;
133
+ const formats = ['', 'left', 'start', 'center', 'right', 'end', 'justify'];
134
+ const formatState = createState('format', { parse: (value) => (formats.includes(value) ? value : '') });
135
+ // The node's own id, not the file's: an ObjectID's hex.
136
+ const uploadIdState = createState('id', { parse: (value) => (typeof value === 'string' ? value : '') });
137
+ const relationToState = createState('relationTo', {
138
+ parse: (value) => {
139
+ if (value === undefined)
140
+ return '';
141
+ if (typeof value !== 'string' || !value)
142
+ throw new Error('An upload names the kayan its file is a row of, as a string under relationTo.');
143
+ if (holdingFiles && !holdingFiles.has(value))
144
+ throw new Error(`An upload's relationTo "${value}" is no kayan of this build that holds files.`);
145
+ return value;
146
+ },
147
+ });
148
+ const valueState = createState('value', {
149
+ parse: (value) => {
150
+ if (value === undefined)
151
+ return '';
152
+ if (typeof value !== 'string' || !value || value.length > 255)
153
+ throw new Error("An upload's value is the id of its file's row, a string.");
154
+ return value;
155
+ },
156
+ });
157
+ /** The extra fields Payload's editor asks of an upload, such as a caption, kept as they were written and drawn by nothing here. */
158
+ const fieldsState = createState('fields', { parse: (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : null) });
159
+ /**
160
+ * A file inside rich text, in the JSON Payload's editor writes (`type: 'upload'`, `version: 3`).
161
+ * DOM-free, so the server checks a write with it. A project opts in: `[...defaultNodes, UploadNode]`.
162
+ */
163
+ export class UploadNode extends DecoratorNode {
164
+ $config() {
165
+ return this.config('upload', {
166
+ extends: DecoratorNode,
167
+ stateConfigs: [
168
+ { stateConfig: formatState, flat: true },
169
+ { stateConfig: uploadIdState, flat: true },
170
+ { stateConfig: relationToState, flat: true },
171
+ { stateConfig: valueState, flat: true },
172
+ { stateConfig: fieldsState, flat: true },
173
+ ],
174
+ });
175
+ }
176
+ /** Refused where either half of what the node points at is missing, which an absent key leaves at its default. */
177
+ updateFromJSON(json) {
178
+ const node = super.updateFromJSON(json);
179
+ if (!$getState(node, relationToState) || !$getState(node, valueState))
180
+ throw new Error('An upload names its relationTo and its value.');
181
+ return node;
182
+ }
183
+ /** Every key, defaults included, as Payload's editor writes it. */
184
+ exportJSON() {
185
+ return {
186
+ ...super.exportJSON(),
187
+ type: 'upload',
188
+ version: 3,
189
+ format: $getState(this, formatState),
190
+ id: $getState(this, uploadIdState),
191
+ relationTo: $getState(this, relationToState),
192
+ value: $getState(this, valueState),
193
+ fields: $getState(this, fieldsState),
194
+ };
195
+ }
196
+ getRelationTo() {
197
+ return $getState(this, relationToState);
198
+ }
199
+ getValue() {
200
+ return $getState(this, valueState);
201
+ }
202
+ getFormat() {
203
+ return $getState(this, formatState);
204
+ }
205
+ /** Points the node at one file, with a fresh node id: an ObjectID's twelve bytes in hex. */
206
+ setUpload(relationTo, value) {
207
+ const id = Array.from(crypto.getRandomValues(new Uint8Array(12)), (byte) => byte.toString(16).padStart(2, '0')).join('');
208
+ return $setState($setState($setState(this, uploadIdState, id), relationToState, relationTo), valueState, value);
209
+ }
210
+ createDOM() {
211
+ return $getDocument().createElement('div');
212
+ }
213
+ updateDOM() {
214
+ return false;
215
+ }
216
+ isInline() {
217
+ return false;
218
+ }
219
+ decorate() {
220
+ return null;
221
+ }
222
+ }
223
+ /** Whether a node is a decorator or holds one at any depth. */
224
+ const $holdsDecorator = (node) => $isDecoratorNode(node) || ($isElementNode(node) && node.getChildren().some($holdsDecorator));
225
+ /**
226
+ * A reader of `richText` values over a node list: the guard walk, then a headless parse.
227
+ * `uploads` names the kayan an `UploadNode` may point at.
228
+ */
229
+ export const richTextReader = (nodes, uploads) => {
230
+ const types = typesOf(nodes);
231
+ const holding = uploads && new Set(uploads);
232
+ // Made with the reader at `buildAsas`, so a list Lexical cannot register fails
233
+ // the build rather than the first write.
234
+ const editor = createEditor({ nodes: [...nodes], onError: (error) => { throw error; } });
235
+ return (value) => {
236
+ const refused = richTextGuard(value);
237
+ if (refused)
238
+ return { refused };
239
+ const type = unknownType({ node: value.root, types });
240
+ if (type) {
241
+ return {
242
+ refused: `No node of this editor is of type "${type}". Add its class to the nodes passed to richText({ nodes }), or convert or remove the node before writing.`,
243
+ type,
244
+ };
245
+ }
246
+ // Restored rather than cleared, so a nested parse never leaves the outer one without its list.
247
+ const before = holdingFiles;
248
+ try {
249
+ holdingFiles = holding;
250
+ return editor.parseEditorState(value).read(() => ({ text: $getRoot().getTextContent(), holdsDecorator: $holdsDecorator($getRoot()) }));
251
+ }
252
+ catch (error) {
253
+ return { refused: `The editor could not read the value: ${error instanceof Error ? error.message : String(error)}` };
254
+ }
255
+ finally {
256
+ holdingFiles = before;
257
+ }
258
+ };
259
+ };
260
+ /**
261
+ * How a stored `richText` value is read back: a root with children and nothing more,
262
+ * so a value holding a node the project has since dropped still reads.
263
+ */
264
+ export const storedRichText = (value) => {
265
+ const root = value && typeof value === 'object' && !Array.isArray(value) ? value.root : undefined;
266
+ return root && typeof root === 'object' && root.type === 'root' && Array.isArray(root.children) ? { text: '' } : { refused: 'The stored value is no editor state.' };
267
+ };
268
+ // Canonical through Lexical's own `toJSON`, so converting again changes nothing,
269
+ // and read once more, since the canonical value is larger.
270
+ const settle = (convert, nodes) => {
271
+ if (!Array.isArray(nodes))
272
+ return { refused: 'The converters were handed no "nodes", and pick none of their own. Pass the project\'s list: nodes: [...defaultNodes, ...].' };
273
+ try {
274
+ const { value, dropped } = convert();
275
+ const reader = richTextReader(nodes);
276
+ const before = reader(value);
277
+ if ('refused' in before)
278
+ return { refused: before.refused };
279
+ const editor = createEditor({ nodes: [...nodes], onError: (error) => { throw error; } });
280
+ // Lexical writes every default key, so a value inside the bounds can grow past them.
281
+ const canonical = JSON.parse(JSON.stringify(editor.parseEditorState(value).toJSON()));
282
+ const after = reader(canonical);
283
+ return 'refused' in after ? { refused: after.refused } : { value: canonical, dropped };
284
+ }
285
+ catch (error) {
286
+ return { refused: `The value could not be converted: ${error instanceof Error ? error.message : String(error)}` };
287
+ }
288
+ };
289
+ /** Payload's own node types, which no class of `defaultNodes` reads: kept where the project's list registers one, dropped where it does not. */
290
+ const payloadTypes = new Set(['upload', 'relationship', 'block', 'inlineBlock']);
291
+ /**
292
+ * A value Payload's lexical editor stored, as one this editor reads, with what could not come across.
293
+ * A link the editor would not write is replaced by its text; a `style` is cleared.
294
+ */
295
+ export const convertPayloadToLexical = ({ data, nodes }) => settle(() => {
296
+ const types = typesOf(nodes);
297
+ const dropped = [];
298
+ // Bounded like `unknownType`; a node met twice is refused, so one reachable many
299
+ // ways is never walked once for each way.
300
+ const seen = new WeakSet();
301
+ const walk = (node, path, depth) => {
302
+ if (!node || typeof node !== 'object' || Array.isArray(node) || depth <= 0)
303
+ return [node];
304
+ if (seen.has(node))
305
+ throw new Error(`The value holds the node at ${path} in two places.`);
306
+ seen.add(node);
307
+ const next = { ...node };
308
+ const type = String(next.type);
309
+ const drop = (reason) => dropped.push({ path, type, reason });
310
+ // Only a string is quoted in a reason: anything else may be too large or deep to write out.
311
+ const quote = (value) => (typeof value === 'string' ? JSON.stringify(value) : 'a value that is not a string');
312
+ if (payloadTypes.has(type) && !types.has(type)) {
313
+ drop(`No node of this editor is of type "${type}", so it was left out.`);
314
+ return [];
315
+ }
316
+ for (const key of ['style', 'textStyle']) {
317
+ if (Object.hasOwn(next, key) && next[key] !== '') {
318
+ drop(`Its ${key}, ${quote(next[key])}, was cleared, since this editor writes none.`);
319
+ next[key] = '';
320
+ }
321
+ }
322
+ if (Array.isArray(next.children))
323
+ next.children = next.children.flatMap((child, index) => walk(child, `${path}.children[${index}]`, depth - 1));
324
+ // Payload's postgres adapter writes a numeric id where a collection's ids are serial.
325
+ if (type === 'upload' && typeof next.value === 'number' && Number.isFinite(next.value))
326
+ next.value = String(next.value);
327
+ if (type !== 'link' && type !== 'autolink')
328
+ return [next];
329
+ const { fields, id: _id, ...link } = next;
330
+ const held = fields && typeof fields === 'object' ? fields : undefined;
331
+ let url = held ? held.url : link.url;
332
+ // Lexical's own autolink marks one its writer unlinked.
333
+ if (next.isUnlinked === true) {
334
+ drop('It was an autolink its writer had unlinked, so only its text was kept.');
335
+ return next.children ?? [];
336
+ }
337
+ if (held?.linkType === 'internal' || (held?.doc && !url)) {
338
+ drop('It linked to a Payload document, which this editor has no link for, so only its text was kept.');
339
+ return next.children ?? [];
340
+ }
341
+ if (typeof url === 'string' && url.startsWith('www.'))
342
+ url = `https://${url}`;
343
+ if (!safeUrl(url)) {
344
+ drop(`Its address is ${quote(url)}, not http, https, mailto, tel or a path, so only its text was kept.`);
345
+ return next.children ?? [];
346
+ }
347
+ // `noopener`, so the page a new tab opens cannot reach this one.
348
+ const blank = held?.newTab === true;
349
+ const tab = held ? { target: blank ? '_blank' : null, rel: blank ? 'noopener' : null } : {};
350
+ return [{ ...link, type: 'link', url, ...tab }];
351
+ };
352
+ return { value: { root: walk(data?.root, 'root', richTextBounds.depth)[0] }, dropped };
353
+ }, nodes);
354
+ /** Plain text as a value: a blank line starts a paragraph, a newline is a `linebreak` and a tab a `tab`. */
355
+ export const convertPlaintextToLexical = ({ text, nodes }) => settle(() => ({
356
+ value: {
357
+ root: {
358
+ type: 'root',
359
+ children: text
360
+ .replace(/\r\n?/g, '\n')
361
+ .split('\n\n')
362
+ .map((paragraph) => ({
363
+ type: 'paragraph',
364
+ children: paragraph
365
+ .split(/(\n|\t)/)
366
+ .filter(Boolean)
367
+ .map((part) => (part === '\n' ? { type: 'linebreak' } : part === '\t' ? { type: 'tab', text: '\t' } : { type: 'text', text: part })),
368
+ })),
369
+ },
370
+ },
371
+ dropped: [],
372
+ }), nodes);
@@ -0,0 +1,153 @@
1
+ export { afterCommitHeader } from '../asas/runner/errors.ts';
2
+ export { allowedType } from '../storage/allowed-type.ts';
3
+ export { compareDecimals, decimalDigits, isDecimal } from '../base/decimal.ts';
4
+ export type { ButtonClient, ClientField, ClientKayan, FrameClient, LeafClient, NavEntry, NodeClient, PageClient, RegionsClient, ScreenClient, ScreenPageClient, } from './index.ts';
5
+ import type { ClientField, NavEntry, PageClient, ScreenClient } from './index.ts';
6
+ export { compareValues, matchesWhere, same } from '../mithaq/where.ts';
7
+ /** How copy is resolved: a key, and what to show when nothing answers it. */
8
+ export type Translate = (key: string, fallback?: string) => string | undefined;
9
+ /** What `GET /api/access` answers for the actor. */
10
+ export type AccessReport = {
11
+ /** The screens this actor may enter, by name, and no other. */
12
+ readonly screens?: readonly string[];
13
+ /** Every catalog key the actor holds, `'*'` for all, absent where no access target grades. */
14
+ readonly permissions?: '*' | readonly string[];
15
+ readonly collections?: Readonly<Record<string, {
16
+ readonly read?: boolean;
17
+ readonly create?: boolean;
18
+ readonly update?: boolean;
19
+ readonly delete?: boolean;
20
+ readonly operations?: Readonly<Record<string, boolean>>;
21
+ readonly fields?: Readonly<Record<string, {
22
+ readonly create?: boolean;
23
+ readonly read?: boolean;
24
+ readonly update?: boolean;
25
+ }>>;
26
+ } | undefined>>;
27
+ };
28
+ /**
29
+ * A router no type was given: every kayan and every operation a name held in a
30
+ * string, which is what registry code that learns its kayan at runtime holds.
31
+ */
32
+ export type AnyRouter = Readonly<Record<string, Readonly<Record<string, unknown>>>>;
33
+ type SchemaOf<Procedure, Side extends 'inputSchema' | 'outputSchema', Otherwise> = Procedure extends {
34
+ readonly '~orpc': Readonly<Partial<Record<Side, infer Schema>>>;
35
+ } ? Schema extends {
36
+ readonly '~standard': {
37
+ readonly types?: infer Types;
38
+ };
39
+ } ? Types extends Record<Side extends 'inputSchema' ? 'input' : 'output', infer Value> ? Value : Otherwise : Otherwise : Otherwise;
40
+ /**
41
+ * Posts one operation of one kayan to `POST /api/<kayan>/<operation>`, typed by the built router:
42
+ * `FetchClient<typeof asas.router>`. A `Date` in the answer arrives as its ISO string.
43
+ */
44
+ export type FetchClient<R = AnyRouter> = <K extends keyof R & string, O extends keyof R[K] & string>(call: {
45
+ kayan: K;
46
+ operation: O;
47
+ input: SchemaOf<R[K][O], 'inputSchema', Record<string, unknown>>;
48
+ draft?: boolean;
49
+ autosave?: boolean;
50
+ }) => Promise<SchemaOf<R[K][O], 'outputSchema', unknown>>;
51
+ /** One stored row, as a read answers it. */
52
+ export type Row = Record<string, unknown>;
53
+ /** Whether the access report lets its actor hold one catalog key: a screen's, an operation's, or any other key. */
54
+ export declare const holds: (report: AccessReport, key: string) => boolean;
55
+ export declare const packageName: RegExp;
56
+ /** Every `ui()` reference a projection prints, `./src/file.tsx#Export` or `<package>:./src/file.tsx#Export`. */
57
+ export declare const referencesIn: (value: unknown) => string[];
58
+ /** The screen's nav as one actor sees it: every entry whose key the actor holds, and every entry that names none. */
59
+ export declare const navOf: (screen: Pick<ScreenClient, 'nav'>, report: AccessReport) => NavEntry[];
60
+ export type ProviderProps<Node = unknown> = {
61
+ children: Node;
62
+ };
63
+ /** A shell, at the root of a page's tree: its regions drawn. The nav is `mithaq/admin/react` `useNav()`. */
64
+ export type ShellProps<Regions extends readonly string[] = readonly string[], Node = unknown> = {
65
+ regions: Record<Regions[number], Node>;
66
+ };
67
+ /** A page of the screen's own, drawn whole: what its file's `loader` answered, if it exports one. */
68
+ export type PageProps<Data = unknown> = {
69
+ loaderData?: Data;
70
+ };
71
+ /**
72
+ * What a page's `loader` reads with, as the actor of the request: `client`, `request` and the
73
+ * access report. It reads the same rows on the server and in the browser.
74
+ */
75
+ export type LoaderTools = {
76
+ client: FetchClient;
77
+ request: (path: string, init?: RequestInit) => Promise<Response>;
78
+ access: AccessReport;
79
+ maxLimit: number;
80
+ };
81
+ /** A layout nested in a region: its regions drawn, and the page it frames. */
82
+ export type LayoutProps<Regions extends readonly string[] = readonly string[], Node = unknown> = {
83
+ regions: Record<Regions[number], Node>;
84
+ page: PageClient;
85
+ };
86
+ /** A component node, and a kayan page drawn whole: the page it sits on. */
87
+ export type ComponentProps = {
88
+ page: PageClient;
89
+ };
90
+ /** A field's control: the field, its value and the one way to change it. */
91
+ export type ControlProps = {
92
+ field: ClientField;
93
+ value: unknown;
94
+ onChange: (value: unknown) => void;
95
+ readOnly: boolean;
96
+ issue: string | null;
97
+ };
98
+ /** A list's cell: the field, its value, and the row it sits in. */
99
+ export type CellProps = {
100
+ field: ClientField;
101
+ value: unknown;
102
+ row: Row;
103
+ };
104
+ /** What an empty list draws: the page the list sits on. */
105
+ export type EmptyProps = {
106
+ page: PageClient;
107
+ };
108
+ /** What a list's `menu` draws inside one row's menu: the row, and `open`, which draws a node beside the list until `null`. */
109
+ export type MenuProps<Node = unknown> = {
110
+ row: Row;
111
+ open: (node: Node | null) => void;
112
+ };
113
+ /** A button that names a component: its operation, the run that asks the confirm first, and whether the form is busy. */
114
+ export type ButtonProps = {
115
+ operation: string;
116
+ run: (input: Row) => Promise<void>;
117
+ busy: boolean;
118
+ };
119
+ export { regionsIn, studioText, studioTree, type StudioNode } from './studio.ts';
120
+ export { kindOf, nodeKinds, type NodeKind, type NodeOf } from './studio.ts';
121
+ /** The controls the copied registry supplies, each with the kinds whose value it reports. */
122
+ export declare const builtInScreenKinds: {
123
+ readonly text: readonly ["text", "email", "date", "datetime", "decimal"];
124
+ readonly email: readonly ["email"];
125
+ readonly number: readonly ["number", "integer"];
126
+ readonly integer: readonly ["integer"];
127
+ readonly decimal: readonly ["decimal"];
128
+ readonly checkbox: readonly ["checkbox"];
129
+ readonly datetime: readonly ["datetime"];
130
+ readonly date: readonly ["date"];
131
+ readonly textArray: readonly ["textArray"];
132
+ readonly uuid: readonly ["uuid"];
133
+ readonly relationship: readonly ["relationship", "upload"];
134
+ readonly join: readonly ["join"];
135
+ readonly switch: readonly ["checkbox"];
136
+ readonly radio: readonly ["text"];
137
+ readonly toggleGroup: readonly ["text"];
138
+ readonly select: readonly ["text"];
139
+ readonly nativeSelect: readonly ["text"];
140
+ readonly slider: readonly ["number"];
141
+ readonly otp: readonly ["text"];
142
+ readonly combobox: readonly ["text"];
143
+ readonly textarea: readonly ["text"];
144
+ readonly toggle: readonly ["checkbox"];
145
+ readonly buttonGroup: readonly ["text"];
146
+ readonly checkboxGroup: readonly ["textArray"];
147
+ readonly calendar: readonly ["date"];
148
+ };
149
+ /** Every control and cell the copied registry ships, by the name it looks each up by. */
150
+ export declare const registryControls: readonly [...("buttonGroup" | "calendar" | "checkbox" | "checkboxGroup" | "combobox" | "date" | "datetime" | "decimal" | "email" | "integer" | "nativeSelect" | "number" | "otp" | "radio" | "relationship" | "select" | "slider" | "switch" | "text" | "textArray" | "textarea" | "toggle" | "toggleGroup" | "uuid")[], "richText", "upload"];
151
+ export declare const registryCells: readonly ['text', 'email', 'number', 'integer', 'decimal', 'checkbox', 'datetime', 'date', 'textArray', 'uuid', 'relationship', 'join', 'richText', 'upload'];
152
+ /** The built-in controls that draw a field's `options`; the build refuses one over a field that declares none. */
153
+ export declare const choiceControls: string[];
@@ -0,0 +1,68 @@
1
+ // Every runtime import of this entry imports nothing but types, so a browser
2
+ // receives no server code and no framework.
3
+ export { afterCommitHeader } from '../asas/runner/errors.js';
4
+ export { allowedType } from '../storage/allowed-type.js';
5
+ export { compareDecimals, decimalDigits, isDecimal } from '../base/decimal.js';
6
+ export { compareValues, matchesWhere, same } from '../mithaq/where.js';
7
+ /** Whether the access report lets its actor hold one catalog key: a screen's, an operation's, or any other key. */
8
+ export const holds = (report, key) => {
9
+ const dot = key.indexOf('.');
10
+ const [resource, action] = [key.slice(0, dot), key.slice(dot + 1)];
11
+ if (resource === 'screen')
12
+ return report.screens?.includes(action) === true;
13
+ const operations = report.collections?.[resource]?.operations;
14
+ if (operations && Object.hasOwn(operations, action))
15
+ return operations[action] === true;
16
+ return report.permissions === '*' || report.permissions?.includes(key) === true;
17
+ };
18
+ // A reference prints the package name into a glob's base, `/node_modules/<name>/`,
19
+ // so no path up, absolute path or space may cross.
20
+ const npmName = '(?:@[a-z0-9~-][a-z0-9._~-]*/)?[a-z0-9~-][a-z0-9._~-]*';
21
+ export const packageName = new RegExp(`^${npmName}$`, 'i');
22
+ const reference = new RegExp(`^(?:${npmName}:)?\\./[^#]+#[\\p{ID_Start}$_][\\p{ID_Continue}$]*$`, 'iu');
23
+ /** Every `ui()` reference a projection prints, `./src/file.tsx#Export` or `<package>:./src/file.tsx#Export`. */
24
+ export const referencesIn = (value) => typeof value === 'string'
25
+ ? reference.test(value) ? [value] : []
26
+ : value && typeof value === 'object' ? Object.values(value).flatMap(referencesIn) : [];
27
+ /** The screen's nav as one actor sees it: every entry whose key the actor holds, and every entry that names none. */
28
+ export const navOf = (screen, report) => screen.nav.filter((entry) => entry.permission === undefined || holds(report, entry.permission));
29
+ export { regionsIn, studioText, studioTree } from './studio.js';
30
+ export { kindOf, nodeKinds } from './studio.js';
31
+ /** The controls the copied registry supplies, each with the kinds whose value it reports. */
32
+ export const builtInScreenKinds = {
33
+ text: ['text', 'email', 'date', 'datetime', 'decimal'],
34
+ email: ['email'],
35
+ number: ['number', 'integer'],
36
+ integer: ['integer'],
37
+ decimal: ['decimal'],
38
+ checkbox: ['checkbox'],
39
+ datetime: ['datetime'],
40
+ date: ['date'],
41
+ textArray: ['textArray'],
42
+ uuid: ['uuid'],
43
+ // A file is a row of a storage kayan, so the picker of rows draws it too.
44
+ relationship: ['relationship', 'upload'],
45
+ join: ['join'],
46
+ switch: ['checkbox'],
47
+ radio: ['text'],
48
+ toggleGroup: ['text'],
49
+ select: ['text'],
50
+ nativeSelect: ['text'],
51
+ slider: ['number'],
52
+ otp: ['text'],
53
+ combobox: ['text'],
54
+ textarea: ['text'],
55
+ toggle: ['checkbox'],
56
+ buttonGroup: ['text'],
57
+ checkboxGroup: ['textArray'],
58
+ calendar: ['date'],
59
+ };
60
+ /** Every control and cell the copied registry ships, by the name it looks each up by. */
61
+ export const registryControls = [
62
+ ...Object.keys(builtInScreenKinds).filter((name) => name !== 'join'),
63
+ 'richText',
64
+ 'upload',
65
+ ];
66
+ export const registryCells = ['text', 'email', 'number', 'integer', 'decimal', 'checkbox', 'datetime', 'date', 'textArray', 'uuid', 'relationship', 'join', 'richText', 'upload'];
67
+ /** The built-in controls that draw a field's `options`; the build refuses one over a field that declares none. */
68
+ export const choiceControls = ['radio', 'toggleGroup', 'select', 'nativeSelect', 'combobox', 'buttonGroup', 'checkboxGroup'];
@@ -0,0 +1,14 @@
1
+ import { type ScreenClient } from './index.ts';
2
+ /**
3
+ * Writes each screen's `screen.json`, `types.ts`, `files.ts` and route files under `into`, and answers the
4
+ * stale files. With `check` it writes nothing. `into` must sit under a TanStack Router `routes` folder.
5
+ */
6
+ export declare const generateScreens: ({ root, screens, into, locales, check, }: {
7
+ root: URL;
8
+ screens: Readonly<Record<string, ScreenClient>>;
9
+ into: URL;
10
+ locales?: readonly string[];
11
+ check?: boolean;
12
+ }) => Promise<string[]>;
13
+ /** The command, given the project's own root, answering the code it exits with. */
14
+ export declare const run: (project: URL, check: boolean) => Promise<number>;