@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.
Files changed (152) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/openstatus-time-picker.txt +21 -0
  3. package/LICENSES/payload.txt +22 -0
  4. package/LICENSES/ponytail.txt +21 -0
  5. package/README.md +160 -2
  6. package/dist/asas/actor.d.ts +96 -0
  7. package/dist/asas/actor.js +114 -0
  8. package/dist/asas/build.d.ts +239 -0
  9. package/dist/asas/build.js +834 -0
  10. package/dist/asas/cli/mithaq.d.ts +2 -0
  11. package/dist/asas/cli/mithaq.js +177 -0
  12. package/dist/asas/cli/upgrade.d.ts +33 -0
  13. package/dist/asas/cli/upgrade.js +256 -0
  14. package/dist/asas/http/fetch.d.ts +41 -0
  15. package/dist/asas/http/fetch.js +178 -0
  16. package/dist/asas/http/orpc.d.ts +45 -0
  17. package/dist/asas/http/orpc.js +153 -0
  18. package/dist/asas/runner/bulk.d.ts +20 -0
  19. package/dist/asas/runner/bulk.js +196 -0
  20. package/dist/asas/runner/errors.d.ts +63 -0
  21. package/dist/asas/runner/errors.js +75 -0
  22. package/dist/asas/runner/read.d.ts +72 -0
  23. package/dist/asas/runner/read.js +601 -0
  24. package/dist/asas/runner/runtime.d.ts +21 -0
  25. package/dist/asas/runner/runtime.js +8 -0
  26. package/dist/asas/runner/steps.d.ts +418 -0
  27. package/dist/asas/runner/steps.js +277 -0
  28. package/dist/asas/runner/store.d.ts +4 -0
  29. package/dist/asas/runner/store.js +21 -0
  30. package/dist/asas/runner/tenant.d.ts +54 -0
  31. package/dist/asas/runner/tenant.js +275 -0
  32. package/dist/asas/runner/write.d.ts +606 -0
  33. package/dist/asas/runner/write.js +1019 -0
  34. package/dist/auth/better-auth/access.d.ts +29 -0
  35. package/dist/auth/better-auth/access.js +103 -0
  36. package/dist/auth/better-auth/generate.d.ts +31 -0
  37. package/dist/auth/better-auth/generate.js +328 -0
  38. package/dist/auth/better-auth/index.d.ts +94 -0
  39. package/dist/auth/better-auth/index.js +81 -0
  40. package/dist/auth/better-auth/tenant.d.ts +26 -0
  41. package/dist/auth/better-auth/tenant.js +34 -0
  42. package/dist/auth/local/index.d.ts +285 -0
  43. package/dist/auth/local/index.js +738 -0
  44. package/dist/base/decimal.d.ts +9 -0
  45. package/dist/base/decimal.js +16 -0
  46. package/dist/base/fields.d.ts +124 -0
  47. package/dist/base/fields.js +52 -0
  48. package/dist/base/index.d.ts +47 -0
  49. package/dist/base/index.js +5 -0
  50. package/dist/base/json-schema.d.ts +7 -0
  51. package/dist/base/json-schema.js +35 -0
  52. package/dist/base/kinds.d.ts +75 -0
  53. package/dist/base/kinds.js +123 -0
  54. package/dist/base/rules.d.ts +73 -0
  55. package/dist/base/rules.js +81 -0
  56. package/dist/drizzle/index.d.ts +308 -0
  57. package/dist/drizzle/index.js +951 -0
  58. package/dist/drizzle/kinds.d.ts +28 -0
  59. package/dist/drizzle/kinds.js +43 -0
  60. package/dist/drizzle/migrate.d.ts +159 -0
  61. package/dist/drizzle/migrate.js +1328 -0
  62. package/dist/drizzle/proxy/index.d.ts +1 -0
  63. package/dist/drizzle/proxy/index.js +1 -0
  64. package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
  65. package/dist/drizzle/proxy/node-postgres.js +1 -0
  66. package/dist/drizzle/proxy/pg-core.d.ts +1 -0
  67. package/dist/drizzle/proxy/pg-core.js +1 -0
  68. package/dist/drizzle/rules.d.ts +336 -0
  69. package/dist/drizzle/rules.js +44 -0
  70. package/dist/index.d.ts +35 -0
  71. package/dist/index.js +26 -0
  72. package/dist/mithaq/define.d.ts +335 -0
  73. package/dist/mithaq/define.js +57 -0
  74. package/dist/mithaq/generate-messages.d.ts +38 -0
  75. package/dist/mithaq/generate-messages.js +235 -0
  76. package/dist/mithaq/language.d.ts +374 -0
  77. package/dist/mithaq/language.js +76 -0
  78. package/dist/mithaq/lock.d.ts +71 -0
  79. package/dist/mithaq/lock.js +329 -0
  80. package/dist/mithaq/message-file.d.ts +13 -0
  81. package/dist/mithaq/message-file.js +29 -0
  82. package/dist/mithaq/messages.d.ts +55 -0
  83. package/dist/mithaq/messages.js +111 -0
  84. package/dist/mithaq/refusal.d.ts +6 -0
  85. package/dist/mithaq/refusal.js +6 -0
  86. package/dist/mithaq/resolve.d.ts +10 -0
  87. package/dist/mithaq/resolve.js +1014 -0
  88. package/dist/mithaq/rows.d.ts +87 -0
  89. package/dist/mithaq/rows.js +174 -0
  90. package/dist/mithaq/rukn.d.ts +72 -0
  91. package/dist/mithaq/rukn.js +15 -0
  92. package/dist/mithaq/standard-schema.d.ts +38 -0
  93. package/dist/mithaq/standard-schema.js +2 -0
  94. package/dist/mithaq/types.d.ts +386 -0
  95. package/dist/mithaq/types.js +15 -0
  96. package/dist/mithaq/user.d.ts +19 -0
  97. package/dist/mithaq/user.js +16 -0
  98. package/dist/mithaq/where.d.ts +32 -0
  99. package/dist/mithaq/where.js +87 -0
  100. package/dist/rbac/index.d.ts +105 -0
  101. package/dist/rbac/index.js +182 -0
  102. package/dist/rbac/steps.d.ts +22 -0
  103. package/dist/rbac/steps.js +161 -0
  104. package/dist/rich-text/index.d.ts +28 -0
  105. package/dist/rich-text/index.js +59 -0
  106. package/dist/rich-text/rich-text.d.ts +106 -0
  107. package/dist/rich-text/rich-text.js +249 -0
  108. package/dist/rich-text/value.d.ts +55 -0
  109. package/dist/rich-text/value.js +126 -0
  110. package/dist/screen/client.d.ts +156 -0
  111. package/dist/screen/client.js +71 -0
  112. package/dist/screen/generate.d.ts +14 -0
  113. package/dist/screen/generate.js +268 -0
  114. package/dist/screen/index.d.ts +496 -0
  115. package/dist/screen/index.js +663 -0
  116. package/dist/screen/react.d.ts +97 -0
  117. package/dist/screen/react.js +58 -0
  118. package/dist/screen/studio.d.ts +24 -0
  119. package/dist/screen/studio.js +68 -0
  120. package/dist/service/index.d.ts +63 -0
  121. package/dist/service/index.js +15 -0
  122. package/dist/storage/allowed-type.d.ts +5 -0
  123. package/dist/storage/allowed-type.js +7 -0
  124. package/dist/storage/index.d.ts +42 -0
  125. package/dist/storage/index.js +43 -0
  126. package/dist/storage/storage.d.ts +386 -0
  127. package/dist/storage/storage.js +412 -0
  128. package/dist/targets.d.ts +14 -0
  129. package/dist/targets.js +16 -0
  130. package/dist/versions/index.d.ts +37 -0
  131. package/dist/versions/index.js +8 -0
  132. package/dist/versions/versions.d.ts +104 -0
  133. package/dist/versions/versions.js +160 -0
  134. package/dist/vite.d.ts +7 -0
  135. package/dist/vite.js +81 -0
  136. package/dist/zod/index.d.ts +193 -0
  137. package/dist/zod/index.js +122 -0
  138. package/dist/zod/kinds.d.ts +36 -0
  139. package/dist/zod/kinds.js +58 -0
  140. package/dist/zod/rules.d.ts +117 -0
  141. package/dist/zod/rules.js +67 -0
  142. package/docs/packages/mithaq/README.md +962 -0
  143. package/docs/packages/mithaq/src/asas/cli/README.md +252 -0
  144. package/docs/packages/mithaq/src/asas/runner/README.md +472 -0
  145. package/docs/packages/mithaq/src/drizzle/README.md +39 -0
  146. package/docs/packages/mithaq-admin/README.md +312 -0
  147. package/docs/templates/asas/README.md +1056 -0
  148. package/package.json +92 -4
  149. package/skills/add-field/SKILL.md +27 -0
  150. package/skills/add-kayan/SKILL.md +26 -0
  151. package/skills/eject-admin-piece/SKILL.md +28 -0
  152. package/skills/upgrade-mithaq/SKILL.md +35 -0
@@ -0,0 +1,663 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { builtInScreenKinds, choiceControls, packageName, registryCells, registryControls } from './client.js';
3
+ import { unsortable } from '../mithaq/types.js';
4
+ import { defaultKinds } from '../base/kinds.js';
5
+ import { assertFieldWhere, assertWhere } from '../asas/runner/write.js';
6
+ import { bulkSeals, readSeals, sealOf, storeSeals } from '../asas/runner/steps.js';
7
+ import { kindOf } from './studio.js';
8
+ import { implementationOf } from '../mithaq/rukn.js';
9
+ /** What the shipped registry draws each shipped kind with. A screen's `controls` spreads it and adds or rewrites rows. */
10
+ export const screenKinds = {
11
+ ...Object.fromEntries(Object.keys(defaultKinds).map((kind) => [kind, { cell: kind, control: kind }])),
12
+ };
13
+ /**
14
+ * The folder a project's screen files sit in, handed back as `ui`:
15
+ * `const ui = folderOf(new URL('./ui-dashboard/', import.meta.url))`. A missing file or export fails the build.
16
+ */
17
+ export const folderOf = (folder) => {
18
+ if (!(folder instanceof URL) || !folder.href.endsWith('/')) {
19
+ throw new Error(`folderOf takes a folder URL ending in /, and was handed ${String(folder)}. Write folderOf(new URL('./folder/', import.meta.url)).`);
20
+ }
21
+ return (path) => new URL(path, folder);
22
+ };
23
+ export const screenTarget = (target) => ({
24
+ capability: 'screen',
25
+ type: 'screen',
26
+ ...target,
27
+ });
28
+ // No server closures: this is what every screen hands a browser.
29
+ export const clientFieldOf = (name, field) => ({
30
+ name,
31
+ kind: field.kind,
32
+ ...(typeof field.label === 'object' && { label: field.label }),
33
+ rules: Object.fromEntries(Object.entries(field.rules ?? {}).filter(([rule, value]) => rule !== 'defaultValue' && typeof value !== 'function')),
34
+ // A declared operation input field is resolved nowhere, so its target is read off the declaration.
35
+ ...(field.target
36
+ ? { relationTo: field.target.name }
37
+ : typeof field.relationTo === 'string'
38
+ ? { relationTo: field.relationTo }
39
+ : typeof field.relationTo === 'function'
40
+ ? { relationTo: field.relationTo().name }
41
+ : {}),
42
+ ...(field.hasMany === true && { hasMany: true }),
43
+ ...(typeof field.filterOptions === 'function' && { filterOptions: true }),
44
+ ...(typeof field.on === 'string' && { on: field.on }),
45
+ });
46
+ const word = /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u;
47
+ const targetKeys = ['capability', 'type', 'path', 'kayan', 'controls', 'providers', 'pages', 'layouts', 'components'];
48
+ const leafKeys = ['field', 'component', 'cell', 'placeholder', 'description', 'readOnly', 'width', 'when'];
49
+ const buttonKeys = ['button', 'label', 'fields', 'reload', 'component', 'confirm', 'when'];
50
+ const listKeys = ['columns', 'filters', 'search', 'empty', 'menu', 'toolbar', 'bulk', 'row', 'sort', 'limit', 'tabs', 'of', 'where'];
51
+ const modes = ['dialog', 'sheet', 'drawer'];
52
+ /** Every key of `value` is one `known` names, so nothing reaches `screen.json` unnamed. */
53
+ const named = (at, value, known, what) => {
54
+ for (const key of Object.keys(value)) {
55
+ if (!known.includes(key))
56
+ throw new Error(`${at} carries "${key}", which a ${what} does not take. A ${what} takes ${known.join(', ')}.`);
57
+ }
58
+ };
59
+ // Every `export` line is cut into its identifiers, so no interpolated name reaches
60
+ // a regular expression.
61
+ const exports = (source, name) => (source.match(/^\s*export\b.*$/gm) ?? []).some((line) => line.split(/[^\p{ID_Continue}$]+/u).includes(name));
62
+ // Read as the one line `export const regions = ['main', 'side'] as const`.
63
+ const regionsOf = (source) => {
64
+ const list = /^\s*export\s+const\s+regions\s*=\s*\[([^\]]*)\]/m.exec(source)?.[1];
65
+ return list === undefined ? undefined : [...list.matchAll(/['"]([^'"]+)['"]/g)].map((match) => match[1]);
66
+ };
67
+ /** The folder holding the nearest `package.json` above a file; references print relative to it, so `screen.json` names no absolute path. */
68
+ export const packageOf = (file) => {
69
+ for (let folder = new URL('./', file);; folder = new URL('../', folder)) {
70
+ if (existsSync(new URL('package.json', folder)))
71
+ return folder;
72
+ // `file:///C:/` on Windows has a pathname that is never `/`.
73
+ if (new URL('../', folder).href === folder.href)
74
+ return undefined;
75
+ }
76
+ };
77
+ /** Every screen of a build, checked and projected in one walk. Each refusal names the screen, kayan, page and node. */
78
+ export const screensOf = (language, resolved) => {
79
+ const locales = language.locales ?? [];
80
+ const { maxLimit } = Object.values(language.targets).find((target) => target?.capability === 'service');
81
+ const byName = new Map(resolved.map((kayan) => [kayan.name, kayan]));
82
+ const screens = {};
83
+ const copy = (at, value) => {
84
+ if (!value || typeof value !== 'object')
85
+ throw new Error(`${at} is no copy: write one string per locale.`);
86
+ for (const locale of locales) {
87
+ if (typeof value[locale] !== 'string')
88
+ throw new Error(`${at} is short of locale "${locale}".`);
89
+ }
90
+ return value;
91
+ };
92
+ const sources = new Map();
93
+ const sourceOf = (file) => {
94
+ if (!sources.has(file.href)) {
95
+ let source;
96
+ try {
97
+ source = readFileSync(file, 'utf8');
98
+ }
99
+ catch {
100
+ source = undefined;
101
+ }
102
+ sources.set(file.href, source);
103
+ }
104
+ return sources.get(file.href);
105
+ };
106
+ // Files of the package drawing the root page print bare; any other package's file
107
+ // prints its name first, with a colon, which a package name never holds.
108
+ let home;
109
+ const nameOf = (root) => {
110
+ try {
111
+ const { name } = JSON.parse(sourceOf(new URL('package.json', root)) ?? '{}');
112
+ return typeof name === 'string' && packageName.test(name) ? name : undefined;
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ };
118
+ const uiOf = (at, url) => {
119
+ const name = decodeURIComponent(url.hash.slice(1));
120
+ const file = new URL(url);
121
+ file.hash = '';
122
+ const root = packageOf(file);
123
+ const shown = root ? `./${decodeURIComponent(file.pathname.slice(root.pathname.length))}` : file.pathname;
124
+ if (!word.test(name))
125
+ throw new Error(`${at} names ${shown}, and names no export: write ui('file.tsx#Export').`);
126
+ const source = sourceOf(file);
127
+ if (source === undefined)
128
+ throw new Error(`${at} names ${shown}#${name}, and ${shown} is no file.`);
129
+ if (!exports(source, name))
130
+ throw new Error(`${at} names ${shown}#${name}, and ${shown} does not export "${name}".`);
131
+ if (!root)
132
+ throw new Error(`${at} names ${shown}#${name}, which no package holds: no package.json sits above it.`);
133
+ if (!home || root.href === home.href)
134
+ return `${shown}#${name}`;
135
+ const named = nameOf(root);
136
+ if (!named)
137
+ throw new Error(`${at} names ${shown}#${name}, whose package, ${decodeURIComponent(new URL('package.json', root).pathname)}, names no npm package name, and a file outside the package drawing the screen's root page is printed by its package's name. Give that package.json one.`);
138
+ return `${named}:${shown}#${name}`;
139
+ };
140
+ // A registry name off these lists is shipped by no one, whoever names it.
141
+ const shippedControls = { ...builtInScreenKinds };
142
+ const shippedCells = Object.keys(defaultKinds);
143
+ const knownKinds = new Set(Object.keys(defaultKinds));
144
+ for (const [name, one] of Object.entries(language.kinds)) {
145
+ const own = one.targets?.screen;
146
+ if (typeof own?.control === 'string' && registryControls.includes(own.control)) {
147
+ if (!Object.hasOwn(builtInScreenKinds, own.control))
148
+ knownKinds.add(name);
149
+ shippedControls[own.control] = [...(shippedControls[own.control] ?? []), name];
150
+ }
151
+ if (typeof own?.cell === 'string' && registryCells.includes(own.cell))
152
+ shippedCells.push(own.cell);
153
+ }
154
+ for (const [key, target] of Object.entries(language.targets)) {
155
+ if (target?.capability !== 'screen')
156
+ continue;
157
+ const screen = `Screen "${key}"`;
158
+ // Refused by name rather than carried unread.
159
+ named(screen, target, targetKeys, 'screen');
160
+ const rootPage = Object.values(target.pages ?? {}).find((page) => page?.path === '/')?.component;
161
+ const rootUrl = rootPage instanceof URL ? rootPage : typeof rootPage === 'string' && Object.hasOwn(target.components ?? {}, rootPage) ? target.components?.[rootPage] : undefined;
162
+ home = rootUrl instanceof URL ? packageOf(rootUrl) : undefined;
163
+ const maps = { layouts: {}, components: {} };
164
+ for (const map of ['layouts', 'components']) {
165
+ for (const [name, value] of Object.entries(target[map] ?? {})) {
166
+ if (!(value instanceof URL))
167
+ throw new Error(`${screen} ${map} names "${name}" as ${JSON.stringify(value)}, which is no ui() reference. Write ui('file.tsx#Export') from folderOf.`);
168
+ maps[map][name] = uiOf(`${screen} ${map} "${name}"`, value);
169
+ }
170
+ }
171
+ // One function for every place a component is named, so the ways a reference can be wrong are said once.
172
+ const ref = (at, place, value, kind) => {
173
+ if (value instanceof URL)
174
+ return uiOf(at, value);
175
+ if (typeof value !== 'string' || !word.test(value)) {
176
+ throw new Error(`${at} names ${JSON.stringify(value)}, which is neither a name nor a ui() reference.`);
177
+ }
178
+ const map = place === 'layout' ? 'layouts' : 'components';
179
+ if (Object.hasOwn(maps[map], value))
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;
186
+ const shipped = place === 'control' ? Object.keys(shippedControls) : place === 'cell' ? shippedCells : [];
187
+ if (!shipped.includes(value)) {
188
+ throw new Error(`${at} names "${value}", which is no name in this screen's ${map} and no ${place} the registry ships.`);
189
+ }
190
+ const draws = place === 'control' ? shippedControls[value] : [];
191
+ if (place === 'control' && kind && knownKinds.has(kind) && !draws.includes(kind)) {
192
+ throw new Error(`${at} names built-in control "${value}", which draws ${draws.join(', ')} and not ${kind}.`);
193
+ }
194
+ return value;
195
+ };
196
+ const controls = {};
197
+ for (const kind of Object.keys(target.controls)) {
198
+ if (!Object.hasOwn(language.kinds, kind))
199
+ throw new Error(`${screen} controls names kind "${kind}", which is not a kind of this language.`);
200
+ }
201
+ for (const kind of Object.keys(language.kinds)) {
202
+ // The project's own row, then the kind's own, then the shipped table.
203
+ const row = implementationOf({ table: target.controls, shipped: screenKinds, name: kind, own: language.kinds[kind].targets, type: 'screen' });
204
+ if (!row)
205
+ throw new Error(`${screen} controls has no row for kind "${kind}", and every kind of the language needs one.`);
206
+ controls[kind] = {
207
+ control: ref(`${screen} controls "${kind}" control`, 'control', row.control, kind),
208
+ cell: ref(`${screen} controls "${kind}" cell`, 'cell', row.cell, kind),
209
+ };
210
+ }
211
+ for (const name of target.kayan) {
212
+ if (!byName.has(name))
213
+ throw new Error(`${screen} kayan lists "${name}", which is no kayan of this build.`);
214
+ }
215
+ for (const kayan of resolved) {
216
+ if (kayan.targets?.[key] !== undefined && !target.kayan.includes(kayan.name)) {
217
+ throw new Error(`Kayan "${kayan.name}" writes targets.${key}, and ${screen.toLowerCase()} does not list it in kayan.`);
218
+ }
219
+ }
220
+ const fieldOf = (where, name, field) => {
221
+ const data = clientFieldOf(name, field);
222
+ const own = (field.targets?.[key] ?? {});
223
+ const component = own.component ? ref(`${where} targets.${key}.component`, 'control', own.component, field.kind) : controls[field.kind].control;
224
+ needsOptions(where, component, field);
225
+ return {
226
+ ...data,
227
+ ...(Array.isArray(field.options) && { options: field.options }),
228
+ ...(typeof field.placeholder === 'object' && { placeholder: field.placeholder }),
229
+ ...(typeof field.prefix === 'object' && { prefix: field.prefix }),
230
+ ...(typeof field.suffix === 'object' && { suffix: field.suffix }),
231
+ ...(typeof field.description === 'object' && { description: field.description }),
232
+ component,
233
+ cell: own.cell ? ref(`${where} targets.${key}.cell`, 'cell', own.cell, field.kind) : controls[field.kind].cell,
234
+ ...(typeof own.width === 'string' && { width: own.width }),
235
+ ...(own.filter === true && { filter: true }),
236
+ ...(own.searchable === true && { searchable: true }),
237
+ // So no list offers a sort the server refuses.
238
+ ...((own.sortable === false || field.sortable === false) && { sortable: false }),
239
+ };
240
+ };
241
+ const needsOptions = (where, component, field) => {
242
+ if (choiceControls.includes(component) && !field.options?.length)
243
+ throw new Error(`${where} is drawn by "${component}", which needs options.`);
244
+ };
245
+ // So no two paths match one URL.
246
+ const paths = new Map();
247
+ const route = (at, path, owner) => {
248
+ if (typeof path !== 'string')
249
+ throw new Error(`${at} has no path, and every page names one.`);
250
+ const shape = path.replace(/:[^/]+/g, ':');
251
+ const taken = paths.get(shape);
252
+ if (taken)
253
+ throw new Error(`${at} path "${path}" is the path ${taken} already takes on this screen.`);
254
+ paths.set(shape, owner);
255
+ return path;
256
+ };
257
+ const pages = {};
258
+ for (const [name, declared] of Object.entries(target.pages ?? {})) {
259
+ const at = `${screen} page "${name}"`;
260
+ named(at, declared, ['path', 'component', 'nav', 'permission'], 'screen page');
261
+ const path = route(at, declared.path, `of page "${name}"`);
262
+ if (declared.nav !== undefined)
263
+ named(`${at} nav`, declared.nav, ['label'], 'nav');
264
+ if (declared.permission !== undefined && typeof declared.permission !== 'string')
265
+ throw new Error(`${at} permission is no catalog key.`);
266
+ pages[name] = {
267
+ path,
268
+ component: ref(`${at} component`, 'component', declared.component),
269
+ ...(declared.nav !== undefined && { nav: { label: copy(`${at} nav label`, declared.nav.label) } }),
270
+ ...(declared.permission !== undefined && { permission: declared.permission }),
271
+ };
272
+ }
273
+ if (!Object.values(pages).some((one) => one.path === '/'))
274
+ throw new Error(`${screen} has no page at /, and a screen draws one at its own path.`);
275
+ const client = {
276
+ path: target.path,
277
+ providers: target.providers.map((one, at) => ref(`${screen} providers[${at}]`, 'component', one)),
278
+ pages,
279
+ nav: [],
280
+ layouts: maps.layouts,
281
+ components: maps.components,
282
+ controls,
283
+ kayan: {},
284
+ maxLimit,
285
+ };
286
+ const scopeOf = (name) => {
287
+ const kayan = byName.get(name);
288
+ const where = `${screen} kayan "${name}"`;
289
+ const fields = Object.fromEntries(Object.values(kayan.fields).map((field) => [field.name, fieldOf(`${where} field "${field.name}"`, field.name, field)]));
290
+ const fieldNamed = (at, field) => {
291
+ if (typeof field !== 'string' || !Object.hasOwn(fields, field))
292
+ throw new Error(`${at} names "${String(field)}", which is not a field of this kayan.`);
293
+ return fields[field];
294
+ };
295
+ const searchedPath = (at, path) => {
296
+ if (typeof path !== 'string' || !path.includes('.'))
297
+ return fieldNamed(at, path).name;
298
+ const [own = '', far = '', ...rest] = path.split('.');
299
+ const near = Object.hasOwn(kayan.fields, own) ? kayan.fields[own] : undefined;
300
+ const target = near?.kind === 'relationship' && !near.hasMany ? near.target : undefined;
301
+ const field = target && Object.hasOwn(target.fields, far) ? target.fields[far] : undefined;
302
+ if (rest.length || !field || !['text', 'email'].includes(field.kind) || field.rules?.localized) {
303
+ throw new Error(`${at} names "${path}", and a search path crosses one relationship of this kayan to a text or email field of the kayan it points at.`);
304
+ }
305
+ return path;
306
+ };
307
+ // A related row's field is refused: the browser holds its id, not the row.
308
+ // A `stored` `where` runs over stored rows, so it names no `record.` or `$user.`.
309
+ const whenOf = (said, when, stored = false) => {
310
+ if (!when || typeof when !== 'object' || Array.isArray(when) || !Object.keys(when).length)
311
+ throw new Error(`${said} is no Where: write { field: { operator: value } }.`);
312
+ for (const [key, value] of Object.entries(when)) {
313
+ if (key === 'and' || key === 'or') {
314
+ if (!Array.isArray(value) || !value.length)
315
+ throw new Error(`${said} "${key}" constraint must be a non-empty array.`);
316
+ for (const one of value)
317
+ whenOf(said, one, stored);
318
+ continue;
319
+ }
320
+ const [head, ...rest] = key.split('.');
321
+ if (stored && (head === '$user' || head === 'record'))
322
+ throw new Error(`${said} names "${key}", and a filter of stored rows names this kayan's own fields alone.`);
323
+ let kind;
324
+ let operators;
325
+ if (head === '$user') {
326
+ const [collections, name, ...path] = rest;
327
+ // The host hands a page the report cut to the kayan its screen draws.
328
+ const other = collections === 'collections' && target.kayan.includes(name) ? byName.get(name) : undefined;
329
+ const ok = other && ((path.length === 1 && ['create', 'read', 'update', 'delete'].includes(path[0]))
330
+ || (path.length === 2 && path[0] === 'operations' && Object.hasOwn(other.operations, path[1]))
331
+ || (path.length === 3 && path[0] === 'fields' && Object.hasOwn(other.fields, path[1]) && ['create', 'read', 'update'].includes(path[2])));
332
+ if (!ok) {
333
+ throw new Error(`${said} names "${key}", and $user is the access report, which carries $user.collections.<kayan>.<create|read|update|delete>, .operations.<operation> and .fields.<field>.<create|read|update> of a kayan this screen draws.`);
334
+ }
335
+ kind = 'checkbox';
336
+ }
337
+ else {
338
+ const [name, locale, ...more] = head === 'record' ? rest : [head, ...rest];
339
+ const field = name !== undefined && Object.hasOwn(kayan.fields, name) ? kayan.fields[name] : undefined;
340
+ if (!field || more.length || (locale !== undefined && !field.locales?.includes(locale)))
341
+ throw new Error(`${said} names "${key}", which is no field of this kayan.`);
342
+ kind = field.kind;
343
+ operators = field.operators;
344
+ }
345
+ assertFieldWhere({ said, key, kind, operators, value });
346
+ }
347
+ return when;
348
+ };
349
+ return { kayan, fields, fieldNamed, searchedPath, whenOf };
350
+ };
351
+ const treeOf = ({ kayan, fieldNamed, searchedPath, whenOf }, page, drawsRow, held) => {
352
+ let inForm = 0;
353
+ const leafOf = (node, leaf, place) => {
354
+ const name = typeof leaf === 'string' ? leaf : leaf.field;
355
+ const field = fieldNamed(node, name);
356
+ // A join's rows are drawn as a list of that kayan scoped to this row, never as nested forms.
357
+ if (place === 'control' && field.kind === 'join') {
358
+ throw new Error(`${node} "${name}" names a join, and a page draws a join's rows as a list: write { list: { of: '${field.relationTo}', where: '${field.on}', columns: [...] } }.`);
359
+ }
360
+ const drawn = place === 'control' ? 'component' : 'cell';
361
+ const decided = typeof leaf !== 'string' && leaf[drawn] ? 'leaf'
362
+ : (kayan.fields[name].targets?.[key] ?? {})[drawn] ? 'field'
363
+ : 'table';
364
+ if (typeof leaf === 'string')
365
+ return place === 'control' ? { field: name, component: field.component, decided } : { field: name, cell: field.cell, decided };
366
+ named(`${node} "${name}"`, leaf, leafKeys, 'leaf');
367
+ // A column is no node of the tree, so nothing would ever read its `when`.
368
+ if (place === 'cell' && leaf.when !== undefined)
369
+ throw new Error(`${node} "${name}" carries a when, and a list column is drawn on every row: put the when on a node.`);
370
+ const { component, cell, placeholder, description, readOnly, width } = leaf;
371
+ const control = component ? ref(`${node} "${name}" component`, 'control', component, field.kind) : field.component;
372
+ if (place === 'control')
373
+ needsOptions(`${node} "${name}"`, control, kayan.fields[name]);
374
+ return {
375
+ field: name,
376
+ ...(typeof readOnly === 'boolean' && { readOnly }),
377
+ ...(typeof width === 'string' && { width }),
378
+ ...(place === 'control' && { component: control }),
379
+ ...((place === 'cell' || cell) && { cell: cell ? ref(`${node} "${name}" cell`, 'cell', cell, field.kind) : field.cell }),
380
+ ...(placeholder !== undefined && { placeholder: copy(`${node} "${name}" placeholder`, placeholder) }),
381
+ ...(description !== undefined && { description: copy(`${node} "${name}" description`, description) }),
382
+ decided,
383
+ };
384
+ };
385
+ const groupOf = (node, name, group) => ({
386
+ label: copy(`${node} "${name}" label`, group.label),
387
+ ...(group.description !== undefined && { description: copy(`${node} "${name}" description`, group.description) }),
388
+ fields: group.fields.map((one) => nodeOf(`${node} "${name}"`, one)),
389
+ });
390
+ // Each region it fills must be one its file exports in `regions`.
391
+ const frameOf = (node, frame, declared) => {
392
+ const reference = declared[frame];
393
+ const printed = ref(`${node} ${frame}`, 'layout', reference);
394
+ const url = reference instanceof URL ? reference : target.layouts[reference];
395
+ const file = new URL(url);
396
+ file.hash = '';
397
+ const exported = regionsOf(sourceOf(file) ?? '') ?? [];
398
+ const regions = {};
399
+ for (const [region, nodes] of Object.entries(declared)) {
400
+ if (region === frame || (frame === 'layout' && region === 'when'))
401
+ continue;
402
+ if (!exported.includes(region)) {
403
+ throw new Error(`${node} fills region "${region}", which ${printed} does not export in its regions: ${exported.join(', ') || 'none'}.`);
404
+ }
405
+ if (!Array.isArray(nodes))
406
+ throw new Error(`${node} region "${region}" is no list of nodes.`);
407
+ regions[region] = nodes.map((one) => nodeOf(`${node} region "${region}"`, one));
408
+ }
409
+ return { [frame]: printed, ...regions };
410
+ };
411
+ // One entry per kind, so a kind with no entry is a type error.
412
+ const drawnBy = (node, value) => value.component !== undefined && { component: ref(`${node} component`, 'component', value.component) };
413
+ const kinds = {
414
+ layout: (node, one) => frameOf(`${node} layout`, 'layout', one),
415
+ field: (node, one) => leafOf(`${node} leaf`, one, 'control'),
416
+ form: (node, one) => {
417
+ named(`${node} form`, one, ['form', 'component', 'when'], 'form node');
418
+ held.form++;
419
+ inForm++;
420
+ try {
421
+ return { form: one.form.map((each) => nodeOf(`${node} form`, each)), ...drawnBy(node, one) };
422
+ }
423
+ finally {
424
+ inForm--;
425
+ }
426
+ },
427
+ list: (node, one) => {
428
+ named(`${node} list`, one, ['list', 'component', 'when'], 'list node');
429
+ named(`${node} list`, one.list, listKeys, 'list');
430
+ const { of, where, ...list } = one.list;
431
+ if (of !== undefined || where !== undefined) {
432
+ if (of === undefined)
433
+ throw new Error(`${node} list names where and no of, and where scopes a list of another kayan to this row.`);
434
+ if (typeof of !== 'string' || !target.kayan.includes(of))
435
+ throw new Error(`${node} list of names "${String(of)}", which is no kayan of this screen.`);
436
+ if (where === undefined)
437
+ throw new Error(`${node} list names of and no where, and a list of another kayan names the relationship that scopes it to this row.`);
438
+ const other = byName.get(of);
439
+ const back = typeof where === 'string' && Object.hasOwn(other.fields, where) ? other.fields[where] : undefined;
440
+ if (!back || back.kind !== 'relationship' || back.hasMany || back.target?.name !== kayan.name) {
441
+ throw new Error(`${node} list where names "${String(where)}", which is no relationship of kayan "${of}" holding one row of kayan "${kayan.name}".`);
442
+ }
443
+ if (!drawsRow)
444
+ throw new Error(`${node} list lists kayan "${of}" by the row this page draws, and page "${page}" draws no row.`);
445
+ // A form inside the page's one `<form>` is no HTML.
446
+ if (inForm)
447
+ throw new Error(`${node} list lists kayan "${of}" inside a form node, and its rows save apart from this page's form: put it beside the form node.`);
448
+ const drawn = treeOf(scopeOf(of), page, drawsRow, { form: 0, list: 0 }).nodeOf(node, { list, ...(one.component !== undefined && { component: one.component }) });
449
+ return { ...drawn, list: { of, where: where, ...drawn.list } };
450
+ }
451
+ held.list++;
452
+ const { columns, filters, search, empty, menu, sort, limit, tabs } = one.list;
453
+ // Where a button sits decides which operations it may run: bulk over ticked rows,
454
+ // row over its row, toolbar over none.
455
+ const buttons = (region) => {
456
+ const declared = one.list[region];
457
+ if (declared === undefined)
458
+ return undefined;
459
+ if (!Array.isArray(declared))
460
+ throw new Error(`${node} list ${region} is no list of buttons.`);
461
+ return declared.map((button) => {
462
+ const at = `${node} list ${region}`;
463
+ if (kindOf(button) !== 'button')
464
+ throw new Error(`${at} holds ${JSON.stringify(Object.keys(button))}, which is no button.`);
465
+ const drawn = nodeOf(at, button);
466
+ const said = `${at} button "${drawn.button}"`;
467
+ const seal = kayan.operations[drawn.button].steps.map(sealOf).find((one) => one && [...bulkSeals, ...readSeals, ...storeSeals].includes(one));
468
+ const writes = seal && bulkSeals.includes(seal) ? 'many' : seal === 'update' || seal === 'remove' ? 'one' : seal === 'insert' ? 'new' : 'none';
469
+ const what = { many: 'writes many rows', one: 'writes one row', new: 'makes a new row', none: 'writes no row' }[writes];
470
+ const wants = { bulk: ['many', 'a bulk button runs an updateMany or a deleteMany over the ticked rows'], row: ['one', 'a row button runs an update or a remove over its own row'], toolbar: ['', 'a toolbar button runs over no row'] }[region];
471
+ if (region === 'toolbar' ? writes === 'many' || writes === 'one' : writes !== wants[0])
472
+ throw new Error(`${said} names an operation that ${what}, and ${wants[1]}.`);
473
+ if (region === 'bulk')
474
+ for (const field of drawn.fields ?? [])
475
+ fieldNamed(`${said} fields`, field);
476
+ return drawn;
477
+ });
478
+ };
479
+ const [toolbar, bulk, row] = [buttons('toolbar'), buttons('bulk'), buttons('row')];
480
+ if (sort !== undefined) {
481
+ const field = fieldNamed(`${node} list sort`, typeof sort === 'string' ? sort.replace(/^-/, '') : sort);
482
+ if (unsortable(kayan.fields[field.name]))
483
+ throw new Error(`${node} list sort names "${field.name}", which cannot order a page.`);
484
+ }
485
+ // The list's first page is a served read, so it is held to the project's maximum.
486
+ if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1 && limit <= maxLimit)) {
487
+ throw new Error(`${node} list limit is ${JSON.stringify(limit)}, and a limit is a whole number from 1 to ${maxLimit}.`);
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
+ });
500
+ return {
501
+ list: {
502
+ columns: columns.map((column) => leafOf(`${node} list column`, column, 'cell')),
503
+ ...(filters && { filters: filters.map((field) => fieldNamed(`${node} list filter`, field).name) }),
504
+ ...(search && { search: search.map((field) => searchedPath(`${node} list search`, field)) }),
505
+ ...(empty !== undefined && { empty: ref(`${node} list empty`, 'component', empty) }),
506
+ ...(menu !== undefined && { menu: ref(`${node} list menu`, 'component', menu) }),
507
+ ...(toolbar && { toolbar }),
508
+ ...(bulk && { bulk }),
509
+ ...(row && { row }),
510
+ ...(sort !== undefined && { sort }),
511
+ ...(limit !== undefined && { limit }),
512
+ ...(tabbed && { tabs: tabbed }),
513
+ },
514
+ ...drawnBy(node, one),
515
+ };
516
+ },
517
+ button: (node, one) => {
518
+ const at = `${node} button "${one.button}"`;
519
+ named(at, one, buttonKeys, 'button');
520
+ if (!Object.hasOwn(kayan.operations, one.button))
521
+ throw new Error(`${at} names operation "${one.button}", which this kayan does not declare.`);
522
+ if (one.fields !== undefined && !(Array.isArray(one.fields) && one.fields.every((field) => typeof field === 'string'))) {
523
+ throw new Error(`${at} fields is no list of names.`);
524
+ }
525
+ // A field name the operation never declared would ask for nothing and send the call short of it.
526
+ const writesMany = kayan.operations[one.button].steps.some((step) => bulkSeals.includes(sealOf(step)));
527
+ const asks = (kayan.operations[one.button].targets?.[key])?.fields ?? {};
528
+ for (const field of writesMany ? [] : (one.fields ?? [])) {
529
+ if (!Object.hasOwn(asks, field))
530
+ throw new Error(`${at} fields names "${field}", which operation "${one.button}" does not ask for on screen "${key}".`);
531
+ }
532
+ // An operation that asks for fields is drawn by a button that names them, or by a component that sends its own input.
533
+ if (!writesMany && !one.fields?.length && one.component === undefined && Object.keys(asks).length) {
534
+ throw new Error(`${at} names no fields, and operation "${one.button}" asks for ${Object.keys(asks).map((name) => `"${name}"`).join(', ')} on screen "${key}": name the fields its dialog asks for, or a component that sends them.`);
535
+ }
536
+ if (one.confirm !== undefined)
537
+ named(`${at} confirm`, one.confirm, ['title', 'body'], 'confirm');
538
+ return {
539
+ button: one.button,
540
+ label: copy(`${at} label`, one.label),
541
+ ...(one.fields !== undefined && { fields: [...one.fields] }),
542
+ ...(typeof one.reload === 'boolean' && { reload: one.reload }),
543
+ ...drawnBy(node, one),
544
+ ...(one.confirm !== undefined && { confirm: { title: copy(`${at} confirm title`, one.confirm.title), body: copy(`${at} confirm body`, one.confirm.body) } }),
545
+ };
546
+ },
547
+ title: (node, one) => {
548
+ named(`${node} title`, one, ['title', 'when'], 'title node');
549
+ return { title: {} };
550
+ },
551
+ nav: (node, one) => {
552
+ named(`${node} nav`, one, ['nav', 'when'], 'nav node');
553
+ return { nav: {} };
554
+ },
555
+ controls: (node, one) => {
556
+ named(`${node} controls`, one, ['controls', 'when'], 'controls node');
557
+ return { controls: {} };
558
+ },
559
+ component: (node, one) => {
560
+ named(`${node} component`, one, ['component', 'when'], 'component node');
561
+ return { component: ref(`${node} component`, 'component', one.component) };
562
+ },
563
+ row: (node, one) => ({ row: one.row.map((each) => nodeOf(`${node} row`, each)), ...(one.resizable === true && { resizable: true }) }),
564
+ separator: () => ({ separator: true }),
565
+ tabs: (node, one) => ({ tabs: one.tabs.map((tab) => ({ tab: tab.tab, ...groupOf(`${node} tab`, tab.tab, tab) })) }),
566
+ accordion: (node, one) => ({ accordion: one.accordion.map((item) => ({ item: item.item, ...groupOf(`${node} item`, item.item, item) })) }),
567
+ section: (node, one) => ({
568
+ section: one.section,
569
+ ...(typeof one.collapsed === 'boolean' && { collapsed: one.collapsed }),
570
+ ...(typeof one.width === 'number' && { width: one.width }),
571
+ ...(typeof one.scroll === 'string' && { scroll: one.scroll }),
572
+ ...groupOf(`${node} section`, one.section, one),
573
+ }),
574
+ };
575
+ const nodeOf = (node, one) => {
576
+ const kind = kindOf(one);
577
+ if (!kind)
578
+ throw new Error(`${node} holds ${JSON.stringify(one && typeof one === 'object' ? Object.keys(one) : one)}, which is no node a page takes.`);
579
+ const drawn = kinds[kind](node, one);
580
+ const when = typeof one === 'object' ? one.when : undefined;
581
+ const at = `${node} ${kind === 'field' ? `leaf "${one.field}"` : kind === 'button' ? `button "${one.button}"` : kind}`;
582
+ return when === undefined ? drawn : { ...drawn, when: whenOf(`${at} when`, when) };
583
+ };
584
+ return { frameOf, nodeOf };
585
+ };
586
+ for (const name of target.kayan) {
587
+ const scope = scopeOf(name);
588
+ const { kayan, fields, fieldNamed } = scope;
589
+ const block = (kayan.targets?.[key] ?? {});
590
+ const where = `${screen} kayan "${name}"`;
591
+ if (block.useAsTitle !== undefined)
592
+ fieldNamed(`${where} useAsTitle`, block.useAsTitle);
593
+ const baseFilter = block.baseFilter === undefined ? undefined : scope.whenOf(`${where} baseFilter`, block.baseFilter, true);
594
+ const kayanPages = {};
595
+ for (const [page, declared] of Object.entries(block.pages ?? {})) {
596
+ const at = `${where} page "${page}"`;
597
+ const whole = 'component' in declared;
598
+ if (whole && 'page' in declared) {
599
+ throw new Error(`${at} carries both page and component, and a page is either one tree or drawn whole by one component.`);
600
+ }
601
+ named(at, declared, whole ? ['path', 'component'] : ['path', 'page', 'mode'], whole ? 'page drawn whole' : 'page');
602
+ const path = route(at, declared.path, `of kayan "${name}" page "${page}"`);
603
+ const id = /(^|\/):id(\/|$)/.test(path);
604
+ if (page === 'edit' && !id)
605
+ throw new Error(`${at} path "${path}" carries no :id, and an edit page draws one row.`);
606
+ if (page === 'list' && id)
607
+ throw new Error(`${at} path "${path}" carries :id, and a list page draws every row.`);
608
+ if (whole) {
609
+ kayanPages[page] = { path, component: ref(`${at} component`, 'component', declared.component) };
610
+ continue;
611
+ }
612
+ if (declared.mode !== undefined) {
613
+ if (!modes.includes(declared.mode))
614
+ throw new Error(`${at} opens as "${declared.mode}", and a page opens as dialog, sheet or drawer.`);
615
+ if (page !== 'create' && page !== 'edit')
616
+ throw new Error(`${at} opens as "${declared.mode}", and only create and edit open over the list.`);
617
+ }
618
+ if (!declared.page || typeof declared.page !== 'object' || !('shell' in declared.page)) {
619
+ throw new Error(`${at} has no page with a shell at its root, and a page is one tree rooted at a shell or a component drawing it whole.`);
620
+ }
621
+ const held = { form: 0, list: 0 };
622
+ const { frameOf } = treeOf(scope, page, id || kayan.single === true, held);
623
+ const tree = frameOf(at, 'shell', declared.page);
624
+ const needs = page === 'list' ? 'list' : page === 'create' || page === 'edit' ? 'form' : undefined;
625
+ if (needs && (held[needs] !== 1 || held[needs === 'list' ? 'form' : 'list'] !== 0)) {
626
+ throw new Error(`${at} is a reserved page, and it holds exactly ${needs === 'list' ? 'one list node and no form' : 'one form node and no list'}.`);
627
+ }
628
+ kayanPages[page] = { path, page: tree, ...(declared.mode !== undefined && { mode: declared.mode }) };
629
+ }
630
+ client.kayan[name] = {
631
+ name,
632
+ single: kayan.single === true,
633
+ fields: Object.values(fields),
634
+ operations: Object.entries(kayan.operations).map(([operation, declared]) => {
635
+ const input = (declared.targets?.[key])?.fields;
636
+ return {
637
+ name: operation,
638
+ effect: declared.effect,
639
+ ...(input && { fields: Object.entries(input).map(([field, one]) => fieldOf(`${where} operation "${operation}" field "${field}"`, field, one)) }),
640
+ // A box left empty leaves each row's value as it is.
641
+ ...(declared.steps.some((step) => sealOf(step) === 'updateMany') && { updatesMany: true }),
642
+ };
643
+ }),
644
+ ...(block.useAsTitle !== undefined && { useAsTitle: block.useAsTitle }),
645
+ ...(block.description !== undefined && { description: copy(`${where} description`, block.description) }),
646
+ ...(baseFilter && { baseFilter }),
647
+ ...(kayan.versions?.drafts && { drafts: kayan.versions.autosave ? { autosave: { interval: kayan.versions.autosave.interval } } : {} }),
648
+ ...(kayan.storage && { storage: { mimeTypes: kayan.storage.mimeTypes, fileSize: kayan.storage.fileSize } }),
649
+ pages: kayanPages,
650
+ };
651
+ const listed = kayanPages.list ? ['list', kayanPages.list] : kayan.single === true ? Object.entries(kayanPages)[0] : undefined;
652
+ if (listed)
653
+ client.nav.push({ path: listed[1].path, page: listed[0], kayan: name, permission: `${name}.${kayan.single === true ? 'findOne' : 'find'}` });
654
+ }
655
+ for (const [name, one] of Object.entries(pages)) {
656
+ if (one.nav)
657
+ client.nav.push({ path: one.path, page: name, label: one.nav.label, ...(one.permission !== undefined && { permission: one.permission }) });
658
+ }
659
+ // Data a browser receives, so only what survives the clone.
660
+ screens[key] = structuredClone(client);
661
+ }
662
+ return screens;
663
+ };