@humaan/payload-plugin-ai-assistant 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 (190) hide show
  1. package/README.md +277 -2
  2. package/dist/admin/AiAssistantControl.d.ts +11 -0
  3. package/dist/admin/AiAssistantControl.js +31 -0
  4. package/dist/admin/AiAssistantControl.js.map +1 -0
  5. package/dist/admin/AiAssistantPanel.d.ts +10 -0
  6. package/dist/admin/AiAssistantPanel.js +208 -0
  7. package/dist/admin/AiAssistantPanel.js.map +1 -0
  8. package/dist/admin/AiAssistantPanel.module.scss +689 -0
  9. package/dist/admin/AiSourceSwitch.d.ts +13 -0
  10. package/dist/admin/AiSourceSwitch.js +14 -0
  11. package/dist/admin/AiSourceSwitch.js.map +1 -0
  12. package/dist/admin/BudgetField.d.ts +8 -0
  13. package/dist/admin/BudgetField.js +145 -0
  14. package/dist/admin/BudgetField.js.map +1 -0
  15. package/dist/admin/BudgetField.module.scss +47 -0
  16. package/dist/admin/Conversation.d.ts +12 -0
  17. package/dist/admin/Conversation.js +469 -0
  18. package/dist/admin/Conversation.js.map +1 -0
  19. package/dist/admin/Markdown.d.ts +9 -0
  20. package/dist/admin/Markdown.js +63 -0
  21. package/dist/admin/Markdown.js.map +1 -0
  22. package/dist/admin/PageContextBridge.d.ts +5 -0
  23. package/dist/admin/PageContextBridge.js +72 -0
  24. package/dist/admin/PageContextBridge.js.map +1 -0
  25. package/dist/admin/Pill.d.ts +11 -0
  26. package/dist/admin/Pill.js +27 -0
  27. package/dist/admin/Pill.js.map +1 -0
  28. package/dist/admin/PlanCard.d.ts +14 -0
  29. package/dist/admin/PlanCard.js +465 -0
  30. package/dist/admin/PlanCard.js.map +1 -0
  31. package/dist/admin/SettingsAlerts.d.ts +7 -0
  32. package/dist/admin/SettingsAlerts.js +37 -0
  33. package/dist/admin/SettingsAlerts.js.map +1 -0
  34. package/dist/admin/UsageSummary.d.ts +11 -0
  35. package/dist/admin/UsageSummary.js +129 -0
  36. package/dist/admin/UsageSummary.js.map +1 -0
  37. package/dist/admin/attachmentUpload.d.ts +25 -0
  38. package/dist/admin/attachmentUpload.js +76 -0
  39. package/dist/admin/attachmentUpload.js.map +1 -0
  40. package/dist/admin/openDocument.d.ts +13 -0
  41. package/dist/admin/openDocument.js +34 -0
  42. package/dist/admin/openDocument.js.map +1 -0
  43. package/dist/admin/percent.d.ts +10 -0
  44. package/dist/admin/percent.js +21 -0
  45. package/dist/admin/percent.js.map +1 -0
  46. package/dist/admin/threads.d.ts +21 -0
  47. package/dist/admin/threads.js +92 -0
  48. package/dist/admin/threads.js.map +1 -0
  49. package/dist/admin/toolSteps.d.ts +12 -0
  50. package/dist/admin/toolSteps.js +57 -0
  51. package/dist/admin/toolSteps.js.map +1 -0
  52. package/dist/admin/transport.d.ts +24 -0
  53. package/dist/admin/transport.js +75 -0
  54. package/dist/admin/transport.js.map +1 -0
  55. package/dist/admin/useAttachments.d.ts +32 -0
  56. package/dist/admin/useAttachments.js +91 -0
  57. package/dist/admin/useAttachments.js.map +1 -0
  58. package/dist/admin/useEntityLabel.d.ts +3 -0
  59. package/dist/admin/useEntityLabel.js +23 -0
  60. package/dist/admin/useEntityLabel.js.map +1 -0
  61. package/dist/attachments/collection.d.ts +15 -0
  62. package/dist/attachments/collection.js +90 -0
  63. package/dist/attachments/collection.js.map +1 -0
  64. package/dist/attachments/conversation.d.ts +25 -0
  65. package/dist/attachments/conversation.js +105 -0
  66. package/dist/attachments/conversation.js.map +1 -0
  67. package/dist/attachments/rules.d.ts +9 -0
  68. package/dist/attachments/rules.js +17 -0
  69. package/dist/attachments/rules.js.map +1 -0
  70. package/dist/attachments/store.d.ts +27 -0
  71. package/dist/attachments/store.js +187 -0
  72. package/dist/attachments/store.js.map +1 -0
  73. package/dist/billing.d.ts +60 -0
  74. package/dist/billing.js +116 -0
  75. package/dist/billing.js.map +1 -0
  76. package/dist/chat/history.d.ts +37 -0
  77. package/dist/chat/history.js +165 -0
  78. package/dist/chat/history.js.map +1 -0
  79. package/dist/chat/messages.d.ts +5 -0
  80. package/dist/chat/messages.js +3 -0
  81. package/dist/chat/messages.js.map +1 -0
  82. package/dist/chat/threads.d.ts +33 -0
  83. package/dist/chat/threads.js +77 -0
  84. package/dist/chat/threads.js.map +1 -0
  85. package/dist/chat/tools.d.ts +76 -0
  86. package/dist/chat/tools.js +596 -0
  87. package/dist/chat/tools.js.map +1 -0
  88. package/dist/chat/usage.d.ts +66 -0
  89. package/dist/chat/usage.js +219 -0
  90. package/dist/chat/usage.js.map +1 -0
  91. package/dist/collectionAccess.d.ts +6 -0
  92. package/dist/collectionAccess.js +32 -0
  93. package/dist/collectionAccess.js.map +1 -0
  94. package/dist/constants.d.ts +64 -0
  95. package/dist/constants.js +51 -0
  96. package/dist/constants.js.map +1 -0
  97. package/dist/endpoints/access.d.ts +4 -0
  98. package/dist/endpoints/access.js +28 -0
  99. package/dist/endpoints/access.js.map +1 -0
  100. package/dist/endpoints/allowance.d.ts +22 -0
  101. package/dist/endpoints/allowance.js +37 -0
  102. package/dist/endpoints/allowance.js.map +1 -0
  103. package/dist/endpoints/applyEndpoint.d.ts +12 -0
  104. package/dist/endpoints/applyEndpoint.js +170 -0
  105. package/dist/endpoints/applyEndpoint.js.map +1 -0
  106. package/dist/endpoints/budget.d.ts +30 -0
  107. package/dist/endpoints/budget.js +153 -0
  108. package/dist/endpoints/budget.js.map +1 -0
  109. package/dist/endpoints/chatEndpoint.d.ts +12 -0
  110. package/dist/endpoints/chatEndpoint.js +295 -0
  111. package/dist/endpoints/chatEndpoint.js.map +1 -0
  112. package/dist/endpoints/form.d.ts +23 -0
  113. package/dist/endpoints/form.js +95 -0
  114. package/dist/endpoints/form.js.map +1 -0
  115. package/dist/endpoints/usageEndpoint.d.ts +13 -0
  116. package/dist/endpoints/usageEndpoint.js +70 -0
  117. package/dist/endpoints/usageEndpoint.js.map +1 -0
  118. package/dist/index.d.ts +4 -0
  119. package/dist/index.js +5 -0
  120. package/dist/index.js.map +1 -0
  121. package/dist/lexical/editorConfig.d.ts +4 -0
  122. package/dist/lexical/editorConfig.js +19 -0
  123. package/dist/lexical/editorConfig.js.map +1 -0
  124. package/dist/lexical/lexicalToMarkdown.d.ts +10 -0
  125. package/dist/lexical/lexicalToMarkdown.js +10 -0
  126. package/dist/lexical/lexicalToMarkdown.js.map +1 -0
  127. package/dist/lexical/markdownToLexical.d.ts +7 -0
  128. package/dist/lexical/markdownToLexical.js +10 -0
  129. package/dist/lexical/markdownToLexical.js.map +1 -0
  130. package/dist/plan/allowed.d.ts +7 -0
  131. package/dist/plan/allowed.js +19 -0
  132. package/dist/plan/allowed.js.map +1 -0
  133. package/dist/plan/applyPlan.d.ts +16 -0
  134. package/dist/plan/applyPlan.js +353 -0
  135. package/dist/plan/applyPlan.js.map +1 -0
  136. package/dist/plan/catalogue.d.ts +37 -0
  137. package/dist/plan/catalogue.js +157 -0
  138. package/dist/plan/catalogue.js.map +1 -0
  139. package/dist/plan/diff.d.ts +13 -0
  140. package/dist/plan/diff.js +87 -0
  141. package/dist/plan/diff.js.map +1 -0
  142. package/dist/plan/parsePlan.d.ts +27 -0
  143. package/dist/plan/parsePlan.js +322 -0
  144. package/dist/plan/parsePlan.js.map +1 -0
  145. package/dist/plan/prompt.d.ts +1 -0
  146. package/dist/plan/prompt.js +45 -0
  147. package/dist/plan/prompt.js.map +1 -0
  148. package/dist/plan/stored.d.ts +53 -0
  149. package/dist/plan/stored.js +3 -0
  150. package/dist/plan/stored.js.map +1 -0
  151. package/dist/plan/tokens.d.ts +8 -0
  152. package/dist/plan/tokens.js +79 -0
  153. package/dist/plan/tokens.js.map +1 -0
  154. package/dist/plan/view.d.ts +4 -0
  155. package/dist/plan/view.js +43 -0
  156. package/dist/plan/view.js.map +1 -0
  157. package/dist/plugin.d.ts +3 -0
  158. package/dist/plugin.js +464 -0
  159. package/dist/plugin.js.map +1 -0
  160. package/dist/read/outline.d.ts +7 -0
  161. package/dist/read/outline.js +70 -0
  162. package/dist/read/outline.js.map +1 -0
  163. package/dist/read/readTarget.d.ts +4 -0
  164. package/dist/read/readTarget.js +13 -0
  165. package/dist/read/readTarget.js.map +1 -0
  166. package/dist/read/readable.d.ts +8 -0
  167. package/dist/read/readable.js +131 -0
  168. package/dist/read/readable.js.map +1 -0
  169. package/dist/runtime.d.ts +15 -0
  170. package/dist/runtime.js +14 -0
  171. package/dist/runtime.js.map +1 -0
  172. package/dist/schema/fields.d.ts +113 -0
  173. package/dist/schema/fields.js +1036 -0
  174. package/dist/schema/fields.js.map +1 -0
  175. package/dist/schema/listingFields.d.ts +9 -0
  176. package/dist/schema/listingFields.js +95 -0
  177. package/dist/schema/listingFields.js.map +1 -0
  178. package/dist/schema/slugify.d.ts +2 -0
  179. package/dist/schema/slugify.js +5 -0
  180. package/dist/schema/slugify.js.map +1 -0
  181. package/dist/settings.d.ts +10 -0
  182. package/dist/settings.js +54 -0
  183. package/dist/settings.js.map +1 -0
  184. package/dist/types.d.ts +198 -0
  185. package/dist/types.js +6 -0
  186. package/dist/types.js.map +1 -0
  187. package/dist/uploads/routeAsset.d.ts +27 -0
  188. package/dist/uploads/routeAsset.js +127 -0
  189. package/dist/uploads/routeAsset.js.map +1 -0
  190. package/package.json +122 -3
@@ -0,0 +1,1036 @@
1
+ import { slugify } from './slugify.js';
2
+ /** Set by the site or by the editor, never by the assistant. Publishing is always the editor's decision. */ const derivedNames = new Set([
3
+ '_status',
4
+ 'collectionSlug',
5
+ 'isNavigableCollection',
6
+ 'path',
7
+ 'slug',
8
+ 'slugToggle',
9
+ 'wordCount'
10
+ ]);
11
+ const unsupportedTypes = new Set([
12
+ 'code',
13
+ 'json',
14
+ 'point'
15
+ ]);
16
+ function isRecord(value) {
17
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
18
+ }
19
+ function fieldAdmin(field) {
20
+ if (!field.admin) {
21
+ return undefined;
22
+ }
23
+ return field.admin;
24
+ }
25
+ /**
26
+ * The field's `admin.condition`, run the way the admin runs it, against the values beside it.
27
+ * A condition that throws counts as shown, so a field is never wrongly treated as optional.
28
+ */ function conditionOf(field) {
29
+ const condition = fieldAdmin(field)?.condition;
30
+ if (typeof condition !== 'function') {
31
+ return undefined;
32
+ }
33
+ return (siblingData)=>{
34
+ try {
35
+ return Boolean(condition({}, siblingData, {
36
+ blockData: siblingData,
37
+ operation: 'create',
38
+ path: [],
39
+ user: null
40
+ }));
41
+ } catch {
42
+ return true;
43
+ }
44
+ };
45
+ }
46
+ function isSkipped(field) {
47
+ const admin = fieldAdmin(field);
48
+ if (admin?.hidden === true || admin?.readOnly === true) {
49
+ return true;
50
+ }
51
+ if ('virtual' in field && field.virtual === true) {
52
+ return true;
53
+ }
54
+ if ('name' in field && typeof field.name === 'string' && derivedNames.has(field.name)) {
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ function labelOf(field) {
60
+ if (!('label' in field) || typeof field.label !== 'string') {
61
+ return undefined;
62
+ }
63
+ return field.label;
64
+ }
65
+ function descriptionOf(field) {
66
+ const description = fieldAdmin(field)?.description;
67
+ if (typeof description !== 'string') {
68
+ return undefined;
69
+ }
70
+ const trimmed = description.replace(/\s+/g, ' ').trim();
71
+ return trimmed.length > 240 ? `${trimmed.slice(0, 237)}...` : trimmed;
72
+ }
73
+ /**
74
+ * Guidance for the assistant that editors do not need to see: a string at `custom.aiAssistant` on a field,
75
+ * block, collection, or global. Guidance that helps editors too belongs in `admin.description`, which the
76
+ * guide also includes.
77
+ */ export function assistantNote(config) {
78
+ const custom = config.custom;
79
+ if (!custom || typeof custom !== 'object' || !('aiAssistant' in custom)) {
80
+ return undefined;
81
+ }
82
+ const note = custom.aiAssistant;
83
+ return typeof note === 'string' && note.trim() ? note.replace(/\s+/g, ' ').trim() : undefined;
84
+ }
85
+ function requiredOf(field) {
86
+ return 'required' in field && field.required === true;
87
+ }
88
+ function hasManyOf(field) {
89
+ return 'hasMany' in field && field.hasMany === true;
90
+ }
91
+ function relationToOf(field) {
92
+ if (!('relationTo' in field)) {
93
+ return [];
94
+ }
95
+ const relationTo = field.relationTo;
96
+ if (typeof relationTo === 'string') {
97
+ return [
98
+ relationTo
99
+ ];
100
+ }
101
+ if (Array.isArray(relationTo)) {
102
+ return relationTo.map((item)=>String(item));
103
+ }
104
+ return [];
105
+ }
106
+ function optionValues(field) {
107
+ if (!('options' in field) || !Array.isArray(field.options)) {
108
+ return [];
109
+ }
110
+ return field.options.flatMap((option)=>{
111
+ if (typeof option === 'string') {
112
+ return [
113
+ option
114
+ ];
115
+ }
116
+ if (option && typeof option === 'object' && 'value' in option) {
117
+ return typeof option.value === 'string' ? [
118
+ option.value
119
+ ] : [];
120
+ }
121
+ return [];
122
+ });
123
+ }
124
+ function childFields(field) {
125
+ if ('fields' in field && Array.isArray(field.fields)) {
126
+ return field.fields;
127
+ }
128
+ return [];
129
+ }
130
+ function containsUnsupported(fields) {
131
+ return fields.some((field)=>{
132
+ if (isSkipped(field)) {
133
+ return false;
134
+ }
135
+ if (unsupportedTypes.has(field.type)) {
136
+ return true;
137
+ }
138
+ if (field.type === 'tabs') {
139
+ return field.tabs.some((tab)=>containsUnsupported(tabFields(tab)));
140
+ }
141
+ const nested = childFields(field);
142
+ return nested.length > 0 && containsUnsupported(nested);
143
+ });
144
+ }
145
+ function tabFields(tab) {
146
+ return 'fields' in tab && Array.isArray(tab.fields) ? tab.fields : [];
147
+ }
148
+ function named(field) {
149
+ return 'name' in field && typeof field.name === 'string' ? field.name : undefined;
150
+ }
151
+ function meta(field, name) {
152
+ return {
153
+ name,
154
+ label: labelOf(field),
155
+ description: descriptionOf(field),
156
+ note: assistantNote(field),
157
+ condition: conditionOf(field),
158
+ required: requiredOf(field)
159
+ };
160
+ }
161
+ /** Fields the model is allowed to fill. JSON, code, and point fields are left out. Every block in the schema is included. */ export function compileFields(fields) {
162
+ const compiled = [];
163
+ for (const field of fields){
164
+ const next = compileField(field);
165
+ if (Array.isArray(next)) {
166
+ compiled.push(...next);
167
+ } else if (next) {
168
+ compiled.push(next);
169
+ }
170
+ }
171
+ return compiled;
172
+ }
173
+ function blockLabelOf(block) {
174
+ const labels = block.labels;
175
+ if (!labels || typeof labels !== 'object' || !('singular' in labels)) {
176
+ return undefined;
177
+ }
178
+ return typeof labels.singular === 'string' ? labels.singular : undefined;
179
+ }
180
+ function compileBlocks(field) {
181
+ const name = named(field);
182
+ if (!name) {
183
+ return null;
184
+ }
185
+ const blocks = field.blocks.flatMap((block)=>{
186
+ const nested = compileFields(block.fields);
187
+ if (nested.length === 0) {
188
+ return [];
189
+ }
190
+ return [
191
+ {
192
+ slug: block.slug,
193
+ label: blockLabelOf(block),
194
+ note: assistantNote(block),
195
+ fields: nested
196
+ }
197
+ ];
198
+ });
199
+ if (blocks.length === 0) {
200
+ return null;
201
+ }
202
+ return {
203
+ ...meta(field, name),
204
+ kind: 'blocks',
205
+ blocks,
206
+ maxRows: typeof field.maxRows === 'number' ? field.maxRows : undefined,
207
+ minRows: typeof field.minRows === 'number' ? field.minRows : undefined
208
+ };
209
+ }
210
+ function compileField(field) {
211
+ if (isSkipped(field)) {
212
+ return null;
213
+ }
214
+ if (field.type === 'blocks') {
215
+ return compileBlocks(field);
216
+ }
217
+ if (unsupportedTypes.has(field.type)) {
218
+ return null;
219
+ }
220
+ switch(field.type){
221
+ case 'array':
222
+ {
223
+ if (containsUnsupported(childFields(field))) {
224
+ return null;
225
+ }
226
+ const name = named(field);
227
+ const nested = compileFields(childFields(field));
228
+ if (!name || nested.length === 0) {
229
+ return null;
230
+ }
231
+ return {
232
+ ...meta(field, name),
233
+ kind: 'array',
234
+ fields: nested,
235
+ maxRows: typeof field.maxRows === 'number' ? field.maxRows : undefined,
236
+ minRows: typeof field.minRows === 'number' ? field.minRows : undefined
237
+ };
238
+ }
239
+ case 'checkbox':
240
+ {
241
+ const name = named(field);
242
+ if (!name) {
243
+ return null;
244
+ }
245
+ return {
246
+ ...meta(field, name),
247
+ kind: 'checkbox'
248
+ };
249
+ }
250
+ case 'collapsible':
251
+ case 'row':
252
+ {
253
+ const nested = compileFields(childFields(field));
254
+ if (nested.length === 0) {
255
+ return null;
256
+ }
257
+ return {
258
+ kind: 'flatten',
259
+ fields: nested,
260
+ condition: conditionOf(field)
261
+ };
262
+ }
263
+ case 'date':
264
+ case 'email':
265
+ case 'text':
266
+ case 'textarea':
267
+ {
268
+ const name = named(field);
269
+ if (!name) {
270
+ return null;
271
+ }
272
+ return {
273
+ ...meta(field, name),
274
+ kind: field.type,
275
+ hasMany: hasManyOf(field)
276
+ };
277
+ }
278
+ case 'group':
279
+ {
280
+ const nested = compileFields(childFields(field));
281
+ if (nested.length === 0) {
282
+ return null;
283
+ }
284
+ const name = named(field);
285
+ if (!name) {
286
+ return {
287
+ kind: 'flatten',
288
+ fields: nested,
289
+ condition: conditionOf(field)
290
+ };
291
+ }
292
+ return {
293
+ ...meta(field, name),
294
+ kind: 'group',
295
+ fields: nested
296
+ };
297
+ }
298
+ case 'number':
299
+ {
300
+ const name = named(field);
301
+ if (!name) {
302
+ return null;
303
+ }
304
+ return {
305
+ ...meta(field, name),
306
+ kind: 'number',
307
+ hasMany: hasManyOf(field)
308
+ };
309
+ }
310
+ case 'radio':
311
+ case 'select':
312
+ {
313
+ const name = named(field);
314
+ if (!name) {
315
+ return null;
316
+ }
317
+ return {
318
+ ...meta(field, name),
319
+ kind: 'select',
320
+ options: optionValues(field),
321
+ hasMany: field.type === 'select' && hasManyOf(field)
322
+ };
323
+ }
324
+ case 'relationship':
325
+ case 'upload':
326
+ {
327
+ const name = named(field);
328
+ if (!name) {
329
+ return null;
330
+ }
331
+ return {
332
+ ...meta(field, name),
333
+ kind: field.type,
334
+ relationTo: relationToOf(field),
335
+ hasMany: hasManyOf(field)
336
+ };
337
+ }
338
+ case 'richText':
339
+ {
340
+ const name = named(field);
341
+ if (!name) {
342
+ return null;
343
+ }
344
+ return {
345
+ ...meta(field, name),
346
+ kind: 'richText',
347
+ field
348
+ };
349
+ }
350
+ case 'tabs':
351
+ {
352
+ const compiled = [];
353
+ for (const tab of field.tabs){
354
+ const nested = compileFields(tabFields(tab));
355
+ if (nested.length === 0) {
356
+ continue;
357
+ }
358
+ if ('name' in tab && typeof tab.name === 'string' && tab.name) {
359
+ compiled.push({
360
+ kind: 'group',
361
+ name: tab.name,
362
+ label: typeof tab.label === 'string' ? tab.label : undefined,
363
+ description: typeof tab.description === 'string' ? tab.description : undefined,
364
+ required: false,
365
+ fields: nested
366
+ });
367
+ } else {
368
+ compiled.push({
369
+ kind: 'flatten',
370
+ fields: nested,
371
+ condition: conditionOf(tab)
372
+ });
373
+ }
374
+ }
375
+ return compiled;
376
+ }
377
+ default:
378
+ return null;
379
+ }
380
+ }
381
+ function heading(field) {
382
+ if (field.kind === 'flatten') {
383
+ return '';
384
+ }
385
+ const bits = [
386
+ field.label ? `${field.name} (${field.label})` : field.name
387
+ ];
388
+ return bits[0];
389
+ }
390
+ function listingHint(field) {
391
+ if (field.kind === 'flatten') {
392
+ return '';
393
+ }
394
+ if (field.name === 'cardImage' || field.name === 'cardDescription') {
395
+ return '. Fill this when creating a document';
396
+ }
397
+ if (field.name === 'seo' && field.kind === 'group') {
398
+ return '. Fill title, description, and image when creating a document';
399
+ }
400
+ return '';
401
+ }
402
+ function kindLabel(field) {
403
+ switch(field.kind){
404
+ case 'array':
405
+ return `array${field.maxRows ? `, max ${field.maxRows}` : ''}${field.minRows ? `, min ${field.minRows}` : ''}`;
406
+ case 'blocks':
407
+ return `blocks${field.maxRows ? `, max ${field.maxRows}` : ''}${field.minRows ? `, min ${field.minRows}` : ''}. Each item is an object with blockType`;
408
+ case 'checkbox':
409
+ return 'boolean';
410
+ case 'flatten':
411
+ return '';
412
+ case 'group':
413
+ return 'group';
414
+ case 'relationship':
415
+ case 'upload':
416
+ {
417
+ const target = field.relationTo.join(' | ') || 'unknown';
418
+ const many = field.hasMany ? ', many' : '';
419
+ const shape = field.relationTo.length > 1 ? field.hasMany ? 'array of { relationTo, value }' : '{ relationTo, value }' : field.hasMany ? 'ids' : 'id';
420
+ return `${field.kind} to ${target}${many}; ${shape}`;
421
+ }
422
+ case 'richText':
423
+ return 'markdown';
424
+ case 'select':
425
+ return `one of: ${field.options.join(', ')}${field.hasMany ? ' (many)' : ''}`;
426
+ default:
427
+ return field.hasMany ? `${field.kind}, many` : field.kind;
428
+ }
429
+ }
430
+ /**
431
+ * Compact field guide the model fills. Names match the collection schema.
432
+ * Block definitions are most of a page's schema, so only the blocks asked for are expanded.
433
+ */ export function formatFieldGuide(fields, options = {}) {
434
+ return formatCompiled(compileFields(fields), 0, new Set(options.blocks ?? [])).trim();
435
+ }
436
+ /** The field a dotted path points at, through groups, tabs, and rows. Lists and blocks end the walk. */ export function compiledFieldAtPath(fields, path) {
437
+ let level = compileFields(fields);
438
+ let found;
439
+ for (const name of path.split('.')){
440
+ found = findCompiled(level, name);
441
+ if (!found) {
442
+ return undefined;
443
+ }
444
+ level = found.kind === 'group' ? found.fields : [];
445
+ }
446
+ return found;
447
+ }
448
+ function findCompiled(fields, name) {
449
+ for (const field of fields){
450
+ if (field.kind === 'flatten') {
451
+ const nested = findCompiled(field.fields, name);
452
+ if (nested) {
453
+ return nested;
454
+ }
455
+ } else if (field.name === name) {
456
+ return field;
457
+ }
458
+ }
459
+ return undefined;
460
+ }
461
+ /**
462
+ * Keys in a set that match no field. Writes drop unknown keys, so these must be rejected
463
+ * before the editor sees a plan that would quietly skip them.
464
+ */ export function unknownFieldPaths(fields, set) {
465
+ const walk = (level, values, prefix)=>Object.entries(values).flatMap(([key, item])=>{
466
+ const path = prefix ? `${prefix}.${key}` : key;
467
+ const field = findCompiled(level, key);
468
+ if (!field) {
469
+ return [
470
+ path
471
+ ];
472
+ }
473
+ if (field.kind === 'group' && isRecord(item)) {
474
+ return walk(field.fields, item, path);
475
+ }
476
+ return [];
477
+ });
478
+ return walk(compileFields(fields), set, '');
479
+ }
480
+ /** Every block slug in the guide, so the model can be told which ones it can ask about. */ export function blockSlugs(fields) {
481
+ const slugs = new Set();
482
+ const visit = (compiled)=>{
483
+ for (const field of compiled){
484
+ if (field.kind === 'flatten' || field.kind === 'group' || field.kind === 'array') {
485
+ visit(field.fields);
486
+ }
487
+ if (field.kind === 'blocks') {
488
+ for (const block of field.blocks){
489
+ slugs.add(block.slug);
490
+ visit(block.fields);
491
+ }
492
+ }
493
+ }
494
+ };
495
+ visit(compileFields(fields));
496
+ return [
497
+ ...slugs
498
+ ];
499
+ }
500
+ function formatCompiled(fields, depth, expand) {
501
+ const pad = ' '.repeat(depth);
502
+ return fields.map((field)=>{
503
+ if (field.kind === 'flatten') {
504
+ return formatCompiled(field.fields, depth, expand);
505
+ }
506
+ const required = 'required' in field && field.required ? field.condition ? ', required when shown' : ', required' : '';
507
+ const listing = listingHint(field);
508
+ const description = 'description' in field && field.description ? `. ${field.description}` : '';
509
+ const base = `${pad}- ${heading(field)}: ${kindLabel(field)}${required}${listing}${description}`;
510
+ const note = 'note' in field && field.note ? `${/[.!?]$/.test(base) ? '' : '.'} Note: ${field.note}` : '';
511
+ const line = `${base}${note}`;
512
+ if (field.kind === 'group' || field.kind === 'array') {
513
+ const nested = formatCompiled(field.fields, depth + 1, expand);
514
+ return nested ? `${line}\n${nested}` : line;
515
+ }
516
+ if (field.kind === 'blocks') {
517
+ const nested = field.blocks.map((block)=>{
518
+ const name = block.label ? `${block.slug} (${block.label})` : block.slug;
519
+ // The note shows even when the block is only listed, so it can help choose between blocks.
520
+ const title = block.note ? `${name}: ${block.note}` : name;
521
+ if (!expand.has(block.slug)) {
522
+ return `${pad} - ${title}`;
523
+ }
524
+ const body = formatCompiled(block.fields, depth + 2, expand);
525
+ return body ? `${pad} - ${title}\n${body}` : `${pad} - ${title}`;
526
+ }).join('\n');
527
+ return nested ? `${line}\n${nested}` : line;
528
+ }
529
+ return line;
530
+ }).filter(Boolean).join('\n');
531
+ }
532
+ export function exampleValues(fields) {
533
+ return exampleCompiled(compileFields(fields));
534
+ }
535
+ function exampleCompiled(fields) {
536
+ const example = {};
537
+ for (const field of fields){
538
+ if (field.kind === 'flatten') {
539
+ Object.assign(example, exampleCompiled(field.fields));
540
+ continue;
541
+ }
542
+ example[field.name] = exampleValue(field);
543
+ }
544
+ return example;
545
+ }
546
+ function exampleValue(field) {
547
+ switch(field.kind){
548
+ case 'array':
549
+ return [
550
+ exampleCompiled(field.fields)
551
+ ];
552
+ case 'blocks':
553
+ {
554
+ const block = field.blocks[0];
555
+ if (!block) {
556
+ return [];
557
+ }
558
+ return [
559
+ {
560
+ blockType: block.slug,
561
+ ...exampleCompiled(block.fields)
562
+ }
563
+ ];
564
+ }
565
+ case 'checkbox':
566
+ return false;
567
+ case 'flatten':
568
+ return undefined;
569
+ case 'group':
570
+ return exampleCompiled(field.fields);
571
+ case 'number':
572
+ return field.hasMany ? [
573
+ 0
574
+ ] : 0;
575
+ case 'relationship':
576
+ case 'upload':
577
+ if (field.relationTo.length > 1) {
578
+ const value = {
579
+ relationTo: field.relationTo[0] ?? '',
580
+ value: 'id'
581
+ };
582
+ return field.hasMany ? [
583
+ value
584
+ ] : value;
585
+ }
586
+ return field.hasMany ? [
587
+ 'id'
588
+ ] : 'id';
589
+ case 'richText':
590
+ return 'Markdown. Headings start at h2.';
591
+ case 'select':
592
+ return field.hasMany ? field.options.slice(0, 1) : field.options[0] ?? '';
593
+ default:
594
+ return field.hasMany ? [
595
+ ''
596
+ ] : '';
597
+ }
598
+ }
599
+ export async function applyFields(fields, values, context) {
600
+ if (!isRecord(values)) {
601
+ return {
602
+ ok: false,
603
+ errors: [
604
+ {
605
+ path: '',
606
+ message: 'Values must be an object.'
607
+ }
608
+ ]
609
+ };
610
+ }
611
+ const errors = [];
612
+ const data = await applyCompiled(compileFields(fields), values, '', context, errors);
613
+ if (errors.length > 0) {
614
+ return {
615
+ ok: false,
616
+ errors
617
+ };
618
+ }
619
+ return {
620
+ ok: true,
621
+ data
622
+ };
623
+ }
624
+ /**
625
+ * `hidden` is true inside a row or tab whose condition hides it. A hidden field is not required,
626
+ * as in the admin, but a value sent for it is still kept.
627
+ */ async function applyCompiled(fields, source, prefix, context, errors, hidden = false) {
628
+ const data = {};
629
+ for (const field of fields){
630
+ const fieldHidden = hidden || (field.condition ? !field.condition(source) : false);
631
+ if (field.kind === 'flatten') {
632
+ Object.assign(data, await applyCompiled(field.fields, source, prefix, context, errors, fieldHidden));
633
+ continue;
634
+ }
635
+ const path = prefix ? `${prefix}.${field.name}` : field.name;
636
+ const raw = source[field.name];
637
+ if (raw === undefined || raw === null) {
638
+ if (!context.partial && !fieldHidden && 'required' in field && field.required) {
639
+ errors.push({
640
+ path,
641
+ message: 'This field is required.'
642
+ });
643
+ }
644
+ continue;
645
+ }
646
+ const value = await readValue(field, raw, path, context, errors);
647
+ if (value !== undefined) {
648
+ data[field.name] = value;
649
+ }
650
+ }
651
+ return data;
652
+ }
653
+ async function readValue(field, raw, path, context, errors) {
654
+ switch(field.kind){
655
+ case 'array':
656
+ return readArray(field, raw, path, context, errors);
657
+ case 'blocks':
658
+ return readBlocks(field, raw, path, context, errors);
659
+ case 'checkbox':
660
+ if (typeof raw !== 'boolean') {
661
+ errors.push({
662
+ path,
663
+ message: 'Expected true or false.'
664
+ });
665
+ return undefined;
666
+ }
667
+ return raw;
668
+ case 'date':
669
+ case 'email':
670
+ case 'text':
671
+ case 'textarea':
672
+ return readStrings(raw, path, field.hasMany, field.required, errors);
673
+ case 'group':
674
+ {
675
+ if (!isRecord(raw)) {
676
+ errors.push({
677
+ path,
678
+ message: 'Expected an object.'
679
+ });
680
+ return undefined;
681
+ }
682
+ return applyCompiled(field.fields, raw, path, context, errors);
683
+ }
684
+ case 'number':
685
+ return readNumbers(raw, path, field.hasMany, field.required, errors);
686
+ case 'relationship':
687
+ case 'upload':
688
+ return readRelation(raw, path, field.relationTo, field.hasMany, field.required, errors);
689
+ case 'richText':
690
+ return readRichText(field.field, raw, path, field.required, context, errors);
691
+ case 'select':
692
+ return readSelect(raw, path, field.options, field.hasMany, field.required, errors);
693
+ default:
694
+ return undefined;
695
+ }
696
+ }
697
+ function readStrings(raw, path, hasMany, required, errors) {
698
+ if (hasMany) {
699
+ if (!Array.isArray(raw) || raw.some((item)=>typeof item !== 'string')) {
700
+ errors.push({
701
+ path,
702
+ message: 'Expected a list of strings.'
703
+ });
704
+ return undefined;
705
+ }
706
+ const values = raw.map((item)=>item.trim()).filter(Boolean);
707
+ if (required && values.length === 0) {
708
+ errors.push({
709
+ path,
710
+ message: 'This field is required.'
711
+ });
712
+ }
713
+ return values;
714
+ }
715
+ if (typeof raw !== 'string') {
716
+ errors.push({
717
+ path,
718
+ message: 'Expected a string.'
719
+ });
720
+ return undefined;
721
+ }
722
+ const value = raw.trim();
723
+ if (required && !value) {
724
+ errors.push({
725
+ path,
726
+ message: 'This field is required.'
727
+ });
728
+ }
729
+ return value;
730
+ }
731
+ function readNumbers(raw, path, hasMany, required, errors) {
732
+ const valid = (value)=>typeof value === 'number' && Number.isFinite(value);
733
+ if (hasMany) {
734
+ if (!Array.isArray(raw) || !raw.every(valid)) {
735
+ errors.push({
736
+ path,
737
+ message: 'Expected a list of numbers.'
738
+ });
739
+ return undefined;
740
+ }
741
+ if (required && raw.length === 0) {
742
+ errors.push({
743
+ path,
744
+ message: 'This field is required.'
745
+ });
746
+ }
747
+ return raw;
748
+ }
749
+ if (!valid(raw)) {
750
+ errors.push({
751
+ path,
752
+ message: 'Expected a number.'
753
+ });
754
+ return undefined;
755
+ }
756
+ return raw;
757
+ }
758
+ function readSelect(raw, path, options, hasMany, required, errors) {
759
+ const known = new Set(options);
760
+ const accept = (value)=>typeof value === 'string' && (known.size === 0 || known.has(value));
761
+ if (hasMany) {
762
+ if (!Array.isArray(raw) || !raw.every(accept)) {
763
+ errors.push({
764
+ path,
765
+ message: `Expected values from: ${options.join(', ')}.`
766
+ });
767
+ return undefined;
768
+ }
769
+ if (required && raw.length === 0) {
770
+ errors.push({
771
+ path,
772
+ message: 'This field is required.'
773
+ });
774
+ }
775
+ return raw;
776
+ }
777
+ if (!accept(raw)) {
778
+ errors.push({
779
+ path,
780
+ message: `Expected one of: ${options.join(', ')}.`
781
+ });
782
+ return undefined;
783
+ }
784
+ return raw;
785
+ }
786
+ async function readRichText(field, raw, path, required, context, errors) {
787
+ if (typeof raw !== 'string') {
788
+ errors.push({
789
+ path,
790
+ message: 'Expected markdown.'
791
+ });
792
+ return undefined;
793
+ }
794
+ const markdown = stripFence(raw);
795
+ if (!markdown) {
796
+ if (required) {
797
+ errors.push({
798
+ path,
799
+ message: 'This field is required.'
800
+ });
801
+ }
802
+ return undefined;
803
+ }
804
+ return context.toLexical(field, markdown);
805
+ }
806
+ function readRelation(raw, path, relationTo, hasMany, required, errors) {
807
+ const readOne = (value, itemPath)=>{
808
+ if (relationTo.length > 1) {
809
+ if (!isRecord(value) || typeof value.relationTo !== 'string' || !isId(value.value)) {
810
+ errors.push({
811
+ path: itemPath,
812
+ message: 'Expected { relationTo, value }.'
813
+ });
814
+ return undefined;
815
+ }
816
+ if (!relationTo.includes(value.relationTo)) {
817
+ errors.push({
818
+ path: itemPath,
819
+ message: `relationTo must be one of: ${relationTo.join(', ')}.`
820
+ });
821
+ return undefined;
822
+ }
823
+ return {
824
+ relationTo: value.relationTo,
825
+ value: normaliseId(value.value)
826
+ };
827
+ }
828
+ if (!isId(value)) {
829
+ errors.push({
830
+ path: itemPath,
831
+ message: 'Expected an id.'
832
+ });
833
+ return undefined;
834
+ }
835
+ return normaliseId(value);
836
+ };
837
+ if (hasMany) {
838
+ if (!Array.isArray(raw)) {
839
+ errors.push({
840
+ path,
841
+ message: 'Expected a list of ids.'
842
+ });
843
+ return undefined;
844
+ }
845
+ const values = raw.map((item, index)=>readOne(item, `${path}.${index}`)).filter((item)=>item !== undefined);
846
+ if (required && values.length === 0) {
847
+ errors.push({
848
+ path,
849
+ message: 'This field is required.'
850
+ });
851
+ }
852
+ return values;
853
+ }
854
+ return readOne(raw, path);
855
+ }
856
+ async function readArray(field, raw, path, context, errors) {
857
+ if (!Array.isArray(raw)) {
858
+ errors.push({
859
+ path,
860
+ message: 'Expected a list.'
861
+ });
862
+ return undefined;
863
+ }
864
+ if (field.maxRows !== undefined && raw.length > field.maxRows) {
865
+ errors.push({
866
+ path,
867
+ message: `Use at most ${field.maxRows}.`
868
+ });
869
+ return undefined;
870
+ }
871
+ if (field.minRows !== undefined && raw.length < field.minRows) {
872
+ errors.push({
873
+ path,
874
+ message: `Use at least ${field.minRows}.`
875
+ });
876
+ }
877
+ const rows = [];
878
+ for (const [index, row] of raw.entries()){
879
+ if (!isRecord(row)) {
880
+ errors.push({
881
+ path: `${path}.${index}`,
882
+ message: 'Expected an object.'
883
+ });
884
+ continue;
885
+ }
886
+ rows.push(await applyCompiled(field.fields, row, `${path}.${index}`, context, errors));
887
+ }
888
+ return rows;
889
+ }
890
+ async function readBlocks(field, raw, path, context, errors) {
891
+ if (!Array.isArray(raw)) {
892
+ errors.push({
893
+ path,
894
+ message: 'Expected a list of blocks.'
895
+ });
896
+ return undefined;
897
+ }
898
+ if (field.maxRows !== undefined && raw.length > field.maxRows) {
899
+ errors.push({
900
+ path,
901
+ message: `Use at most ${field.maxRows} blocks.`
902
+ });
903
+ return undefined;
904
+ }
905
+ if (field.minRows !== undefined && raw.length < field.minRows) {
906
+ errors.push({
907
+ path,
908
+ message: `Use at least ${field.minRows} blocks.`
909
+ });
910
+ }
911
+ const known = new Map(field.blocks.map((block)=>[
912
+ block.slug,
913
+ block
914
+ ]));
915
+ const rows = [];
916
+ for (const [index, row] of raw.entries()){
917
+ const rowPath = `${path}.${index}`;
918
+ if (!isRecord(row) || typeof row.blockType !== 'string') {
919
+ errors.push({
920
+ path: rowPath,
921
+ message: 'Expected an object with blockType.'
922
+ });
923
+ continue;
924
+ }
925
+ const block = known.get(row.blockType);
926
+ if (!block) {
927
+ errors.push({
928
+ path: rowPath,
929
+ message: `blockType must be one of: ${[
930
+ ...known.keys()
931
+ ].join(', ')}.`
932
+ });
933
+ continue;
934
+ }
935
+ const values = await applyCompiled(block.fields, row, rowPath, context, errors);
936
+ rows.push({
937
+ ...values,
938
+ blockType: block.slug
939
+ });
940
+ }
941
+ return rows;
942
+ }
943
+ function isId(value) {
944
+ if (typeof value === 'number') {
945
+ return Number.isFinite(value);
946
+ }
947
+ return typeof value === 'string' && value.trim().length > 0;
948
+ }
949
+ function normaliseId(value) {
950
+ return typeof value === 'string' ? value.trim() : value;
951
+ }
952
+ function stripFence(value) {
953
+ const trimmed = value.trim();
954
+ const fenced = trimmed.match(/^```(?:markdown|md)?([\s\S]*?)```$/i);
955
+ return (fenced?.[1] ?? trimmed).trim();
956
+ }
957
+ /** Fills slug, path, and hidden defaults the editor never types, so create validation can pass. */ /**
958
+ * The slug a renamed document gets when its "Use title" toggle is on, matching the slug field in the editor,
959
+ * which rebuilds the slug from the title as it changes. Undefined when the slug stays as it is.
960
+ * The path field's own hook then rebuilds the URL from the slug and adds a redirect if the page was published.
961
+ */ export function followedSlug(fields, current, nextTitle) {
962
+ if (!findNamed(fields, 'slug') || !findNamed(fields, 'slugToggle')) {
963
+ return undefined;
964
+ }
965
+ if (!isRecord(current) || current.slugToggle !== 'title') {
966
+ return undefined;
967
+ }
968
+ if (typeof nextTitle !== 'string' || nextTitle === current.title) {
969
+ return undefined;
970
+ }
971
+ const slug = slugify(nextTitle);
972
+ return slug && slug !== current.slug ? slug : undefined;
973
+ }
974
+ export function withDerivedValues(fields, data, options) {
975
+ const next = {
976
+ ...data
977
+ };
978
+ const slugField = findNamed(fields, 'slug');
979
+ if (slugField && (typeof next.slug !== 'string' || next.slug.trim() === '')) {
980
+ const title = typeof next.title === 'string' ? next.title : '';
981
+ const slug = slugify(title);
982
+ if (slug) {
983
+ next.slug = slug;
984
+ // Keep the generated slug. The "use title" toggle would rewrite it in the editor.
985
+ if (findNamed(fields, 'slugToggle')) {
986
+ next.slugToggle = 'manual';
987
+ }
988
+ }
989
+ }
990
+ const pathField = findNamed(fields, 'path');
991
+ if (pathField && (typeof next.path !== 'string' || next.path.trim() === '') && typeof next.slug === 'string') {
992
+ const path = documentPath(options?.prependPath, next.slug);
993
+ if (path) {
994
+ next.path = path;
995
+ }
996
+ }
997
+ const collectionSlug = findNamed(fields, 'collectionSlug');
998
+ if (collectionSlug && next.collectionSlug === undefined && 'defaultValue' in collectionSlug && typeof collectionSlug.defaultValue === 'string') {
999
+ next.collectionSlug = collectionSlug.defaultValue;
1000
+ }
1001
+ return next;
1002
+ }
1003
+ function documentPath(prepend, slug) {
1004
+ const trimmed = slug.trim();
1005
+ if (!trimmed) {
1006
+ return '';
1007
+ }
1008
+ const prefix = prepend?.trim() ?? '';
1009
+ const joined = `${prefix}/${trimmed}`.replace(/\/+/g, '/');
1010
+ return joined.length > 1 ? joined.replace(/\/+$/, '') : '/';
1011
+ }
1012
+ function findNamed(fields, name) {
1013
+ for (const field of fields){
1014
+ if ('name' in field && field.name === name) {
1015
+ return field;
1016
+ }
1017
+ if (field.type === 'tabs') {
1018
+ for (const tab of field.tabs){
1019
+ const found = findNamed(tabFields(tab), name);
1020
+ if (found) {
1021
+ return found;
1022
+ }
1023
+ }
1024
+ }
1025
+ const nested = childFields(field);
1026
+ if (nested.length > 0) {
1027
+ const found = findNamed(nested, name);
1028
+ if (found) {
1029
+ return found;
1030
+ }
1031
+ }
1032
+ }
1033
+ return undefined;
1034
+ }
1035
+
1036
+ //# sourceMappingURL=fields.js.map