@nextbridgehq/payload-block-builder 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +280 -76
- package/dist/bin/init.js +0 -0
- package/dist/client.cjs +1487 -603
- package/dist/client.d.cts +6 -2
- package/dist/client.d.ts +6 -2
- package/dist/client.js +1438 -530
- package/dist/index.cjs +887 -520
- package/dist/index.d.cts +52 -8
- package/dist/index.d.ts +52 -8
- package/dist/index.js +884 -518
- package/package.json +29 -10
- package/src/block-builder/builder.css +391 -36
- package/src/components/BlockDataField/BlockDataField.css +652 -393
- package/src/components/SchemaBuilderField/SchemaBuilderField.css +361 -361
package/dist/index.js
CHANGED
|
@@ -74,6 +74,17 @@ var BlockDefinitionVersions = {
|
|
|
74
74
|
update: () => false,
|
|
75
75
|
delete: authenticated
|
|
76
76
|
},
|
|
77
|
+
hooks: {
|
|
78
|
+
beforeValidate: [
|
|
79
|
+
({ data }) => {
|
|
80
|
+
if (data?.blockDefinition && data?.versionNumber) {
|
|
81
|
+
const bd = typeof data.blockDefinition === "object" && data.blockDefinition !== null ? data.blockDefinition.id : data.blockDefinition;
|
|
82
|
+
data.versionIdString = `${bd}_${data.versionNumber}`;
|
|
83
|
+
}
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
},
|
|
77
88
|
fields: [
|
|
78
89
|
{
|
|
79
90
|
name: "blockDefinition",
|
|
@@ -110,6 +121,12 @@ var BlockDefinitionVersions = {
|
|
|
110
121
|
type: "textarea",
|
|
111
122
|
required: false,
|
|
112
123
|
admin: { description: "Notes on what changed in this version." }
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "versionIdString",
|
|
127
|
+
type: "text",
|
|
128
|
+
unique: true,
|
|
129
|
+
admin: { hidden: true }
|
|
113
130
|
}
|
|
114
131
|
]
|
|
115
132
|
};
|
|
@@ -136,13 +153,26 @@ function dbLayoutField(fieldName = "dbLayout", tabLabel = "DB Layout") {
|
|
|
136
153
|
type: "relationship",
|
|
137
154
|
relationTo: "block-definitions",
|
|
138
155
|
required: true,
|
|
139
|
-
admin: {
|
|
156
|
+
admin: {
|
|
157
|
+
description: "Which block type to use.",
|
|
158
|
+
width: "50%",
|
|
159
|
+
components: {
|
|
160
|
+
afterInput: ["@nextbridgehq/payload-block-builder/client#BlockVersionSync"]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
140
163
|
},
|
|
141
164
|
{
|
|
142
165
|
name: "blockVersion",
|
|
143
166
|
type: "relationship",
|
|
144
167
|
relationTo: "block-definition-versions",
|
|
145
168
|
required: true,
|
|
169
|
+
filterOptions: ({ siblingData }) => {
|
|
170
|
+
const def = siblingData?.blockDefinition;
|
|
171
|
+
if (!def) return false;
|
|
172
|
+
const defId = def && typeof def === "object" ? def.id : def;
|
|
173
|
+
if (!defId) return false;
|
|
174
|
+
return { blockDefinition: { equals: defId } };
|
|
175
|
+
},
|
|
146
176
|
admin: { description: "Which schema version to use.", width: "50%" }
|
|
147
177
|
}
|
|
148
178
|
]
|
|
@@ -194,51 +224,117 @@ function dbLayoutField(fieldName = "dbLayout", tabLabel = "DB Layout") {
|
|
|
194
224
|
function indent(n) {
|
|
195
225
|
return " ".repeat(n);
|
|
196
226
|
}
|
|
197
|
-
function
|
|
198
|
-
return
|
|
227
|
+
function safeStr(s) {
|
|
228
|
+
return JSON.stringify(s);
|
|
229
|
+
}
|
|
230
|
+
function listToCode(items, body, depth) {
|
|
231
|
+
if (items.length === 0) return "[]";
|
|
232
|
+
const pad = indent(depth);
|
|
233
|
+
const innerPad = indent(depth + 1);
|
|
234
|
+
const entries = items.map((item) => `${innerPad}{
|
|
235
|
+
${body(item)}
|
|
236
|
+
${innerPad}}`).join(",\n");
|
|
237
|
+
return `[
|
|
238
|
+
${entries}
|
|
239
|
+
${pad}]`;
|
|
240
|
+
}
|
|
241
|
+
var UNNAMED_TYPES = /* @__PURE__ */ new Set(["row", "tabs", "collapsible"]);
|
|
242
|
+
var NO_LABEL_TYPES = /* @__PURE__ */ new Set(["row"]);
|
|
243
|
+
var NO_LOCALIZED_TYPES = /* @__PURE__ */ new Set(["row", "tabs", "collapsible"]);
|
|
244
|
+
var NO_ADMIN_DESCRIPTION_TYPES = /* @__PURE__ */ new Set(["row", "tabs"]);
|
|
245
|
+
var FIELDS_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
246
|
+
"array",
|
|
247
|
+
"group",
|
|
248
|
+
"row",
|
|
249
|
+
"collapsible"
|
|
250
|
+
]);
|
|
251
|
+
var PAYLOAD_TYPE = {
|
|
252
|
+
richtext: "richText",
|
|
253
|
+
image: "upload",
|
|
254
|
+
file: "upload",
|
|
255
|
+
multiselect: "select",
|
|
256
|
+
url: "text",
|
|
257
|
+
color: "text"
|
|
258
|
+
};
|
|
259
|
+
var UPLOAD_TYPES = /* @__PURE__ */ new Set(["image", "file"]);
|
|
260
|
+
var DEFAULT_UPLOAD_COLLECTION = "media";
|
|
261
|
+
function payloadType(type) {
|
|
262
|
+
return PAYLOAD_TYPE[type] ?? type;
|
|
199
263
|
}
|
|
200
264
|
function fieldToCode(field, depth = 1) {
|
|
201
265
|
const pad = indent(depth);
|
|
202
266
|
const innerPad = indent(depth + 1);
|
|
203
267
|
const lines = [];
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
268
|
+
if (!UNNAMED_TYPES.has(field.type)) {
|
|
269
|
+
lines.push(`${pad}name: ${safeStr(field.name)}`);
|
|
270
|
+
}
|
|
271
|
+
lines.push(`${pad}type: '${payloadType(field.type)}'`);
|
|
272
|
+
if (field.label && !NO_LABEL_TYPES.has(field.type)) {
|
|
273
|
+
lines.push(`${pad}label: ${safeStr(field.label)}`);
|
|
274
|
+
}
|
|
207
275
|
if (field.required) lines.push(`${pad}required: true`);
|
|
208
276
|
if (field.unique) lines.push(`${pad}unique: true`);
|
|
209
|
-
if (field.localized
|
|
277
|
+
if (field.localized && !NO_LOCALIZED_TYPES.has(field.type)) {
|
|
278
|
+
lines.push(`${pad}localized: true`);
|
|
279
|
+
}
|
|
210
280
|
if (field.defaultValue !== void 0) {
|
|
211
|
-
const val = typeof field.defaultValue === "string" ?
|
|
281
|
+
const val = typeof field.defaultValue === "string" ? safeStr(String(field.defaultValue)) : field.defaultValue;
|
|
212
282
|
lines.push(`${pad}defaultValue: ${val}`);
|
|
213
283
|
}
|
|
214
|
-
if (field.type === "
|
|
284
|
+
if (field.type === "richtext") {
|
|
215
285
|
lines.push(`${pad}editor: lexicalEditor({})`);
|
|
216
286
|
}
|
|
217
287
|
if (field.options && field.options.length > 0) {
|
|
218
|
-
const opts = field.options.map((o) => `{ label:
|
|
288
|
+
const opts = field.options.map((o) => `{ label: ${safeStr(o.label)}, value: ${safeStr(o.value)} }`).join(`, `);
|
|
219
289
|
lines.push(`${pad}options: [${opts}]`);
|
|
220
290
|
}
|
|
221
|
-
if (field.
|
|
222
|
-
lines.push(
|
|
291
|
+
if (UPLOAD_TYPES.has(field.type)) {
|
|
292
|
+
lines.push(
|
|
293
|
+
`${pad}relationTo: ${safeStr(field.collection || DEFAULT_UPLOAD_COLLECTION)}`
|
|
294
|
+
);
|
|
295
|
+
} else if (field.collection) {
|
|
296
|
+
lines.push(`${pad}relationTo: ${safeStr(field.collection)}`);
|
|
223
297
|
}
|
|
224
|
-
if (field.
|
|
298
|
+
if (field.type === "multiselect") {
|
|
299
|
+
lines.push(`${pad}hasMany: true`);
|
|
300
|
+
} else if (field.hasMany !== void 0) {
|
|
225
301
|
lines.push(`${pad}hasMany: ${field.hasMany}`);
|
|
226
302
|
}
|
|
227
303
|
if (field.minRows !== void 0) lines.push(`${pad}minRows: ${field.minRows}`);
|
|
228
304
|
if (field.maxRows !== void 0) lines.push(`${pad}maxRows: ${field.maxRows}`);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
${
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
305
|
+
const children = field.fields ?? [];
|
|
306
|
+
if (children.length > 0 || FIELDS_CONTAINER_TYPES.has(field.type)) {
|
|
307
|
+
lines.push(
|
|
308
|
+
`${pad}fields: ${listToCode(children, (f) => fieldToCode(f, depth + 2), depth)}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (field.type === "blocks") {
|
|
312
|
+
lines.push(`${pad}blocks: []`);
|
|
313
|
+
}
|
|
314
|
+
if (field.type === "tabs") {
|
|
315
|
+
const tabsCode = (field.tabs ?? []).map((tab) => {
|
|
316
|
+
const tabPad = indent(depth + 2);
|
|
317
|
+
const tabLines = [];
|
|
318
|
+
if (tab.name) tabLines.push(`${tabPad}name: ${safeStr(tab.name)}`);
|
|
319
|
+
tabLines.push(`${tabPad}label: ${safeStr(tab.label)}`);
|
|
320
|
+
tabLines.push(
|
|
321
|
+
`${tabPad}fields: ${listToCode(tab.fields ?? [], (f) => fieldToCode(f, depth + 4), depth + 2)}`
|
|
322
|
+
);
|
|
323
|
+
return `${innerPad}{
|
|
324
|
+
${tabLines.join(",\n")}
|
|
325
|
+
${innerPad}}`;
|
|
326
|
+
}).join(",\n");
|
|
327
|
+
lines.push(
|
|
328
|
+
`${pad}tabs: ${(field.tabs ?? []).length > 0 ? `[
|
|
329
|
+
${tabsCode}
|
|
330
|
+
${pad}]` : "[]"}`
|
|
331
|
+
);
|
|
236
332
|
}
|
|
237
333
|
const adminParts = [];
|
|
238
|
-
if (field.admin?.description)
|
|
239
|
-
adminParts.push(`description:
|
|
334
|
+
if (field.admin?.description && !NO_ADMIN_DESCRIPTION_TYPES.has(field.type))
|
|
335
|
+
adminParts.push(`description: ${safeStr(field.admin.description)}`);
|
|
240
336
|
if (field.admin?.placeholder)
|
|
241
|
-
adminParts.push(`placeholder:
|
|
337
|
+
adminParts.push(`placeholder: ${safeStr(field.admin.placeholder)}`);
|
|
242
338
|
if (field.admin?.readOnly) adminParts.push(`readOnly: true`);
|
|
243
339
|
if (field.admin?.hidden) adminParts.push(`hidden: true`);
|
|
244
340
|
if (adminParts.length > 0) {
|
|
@@ -252,22 +348,22 @@ function generateBlockCode(block) {
|
|
|
252
348
|
if (hasRichText) {
|
|
253
349
|
imports.push(`import { lexicalEditor } from '@payloadcms/richtext-lexical'`);
|
|
254
350
|
}
|
|
255
|
-
const fieldsCode = block.fields.map((f) => `
|
|
256
|
-
${fieldToCode(f,
|
|
257
|
-
|
|
351
|
+
const fieldsCode = block.fields.map((f) => ` {
|
|
352
|
+
${fieldToCode(f, 3)}
|
|
353
|
+
}`).join(",\n");
|
|
258
354
|
const labelsCode = block.labels ? `
|
|
259
355
|
labels: {
|
|
260
|
-
singular:
|
|
261
|
-
plural:
|
|
356
|
+
singular: ${safeStr(block.labels.singular ?? block.slug)},
|
|
357
|
+
plural: ${safeStr(block.labels.plural ?? block.slug + "s")},
|
|
262
358
|
},` : "";
|
|
263
359
|
const interfaceLine = block.interfaceName ? `
|
|
264
|
-
interfaceName:
|
|
360
|
+
interfaceName: ${safeStr(block.interfaceName)},` : "";
|
|
265
361
|
const exportName = block.interfaceName ?? toCamelCase(block.slug);
|
|
266
362
|
return [
|
|
267
363
|
imports.join("\n"),
|
|
268
364
|
"",
|
|
269
365
|
`export const ${exportName}: Block = {`,
|
|
270
|
-
` slug:
|
|
366
|
+
` slug: ${safeStr(block.slug)},${interfaceLine}${labelsCode}`,
|
|
271
367
|
` fields: [`,
|
|
272
368
|
fieldsCode,
|
|
273
369
|
` ],`,
|
|
@@ -276,15 +372,102 @@ ${fieldToCode(f, 2)}
|
|
|
276
372
|
].join("\n");
|
|
277
373
|
}
|
|
278
374
|
function containsRichText(fields) {
|
|
279
|
-
return fields.some(
|
|
280
|
-
(f
|
|
281
|
-
|
|
375
|
+
return fields.some((f) => {
|
|
376
|
+
if (f.type === "richtext") return true;
|
|
377
|
+
if (f.fields && containsRichText(f.fields)) return true;
|
|
378
|
+
if (f.tabs?.some((tab) => containsRichText(tab.fields ?? []))) return true;
|
|
379
|
+
return false;
|
|
380
|
+
});
|
|
282
381
|
}
|
|
283
382
|
function toCamelCase(slug) {
|
|
284
383
|
return slug.split(/[-_]/).map(
|
|
285
384
|
(part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
|
|
286
385
|
).join("");
|
|
287
386
|
}
|
|
387
|
+
function toPascalCase(slug) {
|
|
388
|
+
return slug.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
389
|
+
}
|
|
390
|
+
function getTsType(field) {
|
|
391
|
+
switch (field.type) {
|
|
392
|
+
case "number":
|
|
393
|
+
return "number";
|
|
394
|
+
case "checkbox":
|
|
395
|
+
return "boolean";
|
|
396
|
+
case "text":
|
|
397
|
+
case "textarea":
|
|
398
|
+
case "email":
|
|
399
|
+
case "url":
|
|
400
|
+
case "color":
|
|
401
|
+
case "select":
|
|
402
|
+
case "date":
|
|
403
|
+
return "string";
|
|
404
|
+
case "multiselect":
|
|
405
|
+
return "string[]";
|
|
406
|
+
case "group":
|
|
407
|
+
return field.fields ? objectType(flattenProps(field.fields)) : "any";
|
|
408
|
+
case "array":
|
|
409
|
+
if (field.fields) {
|
|
410
|
+
const inner = flattenProps(field.fields).map((p) => `${p.name}: ${p.type}`).join("; ");
|
|
411
|
+
return `Array<{ id: string; ${inner} }>`;
|
|
412
|
+
}
|
|
413
|
+
return "any[]";
|
|
414
|
+
default:
|
|
415
|
+
return "any";
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function objectType(props) {
|
|
419
|
+
return props.length > 0 ? `{ ${props.map((p) => `${p.name}: ${p.type}`).join("; ")} }` : "Record<string, unknown>";
|
|
420
|
+
}
|
|
421
|
+
function flattenProps(fields) {
|
|
422
|
+
const out = [];
|
|
423
|
+
for (const f of fields) {
|
|
424
|
+
if (f.type === "row" || f.type === "collapsible") {
|
|
425
|
+
out.push(...flattenProps(f.fields ?? []));
|
|
426
|
+
} else if (f.type === "tabs") {
|
|
427
|
+
for (const tab of f.tabs ?? []) {
|
|
428
|
+
if (tab.name) {
|
|
429
|
+
out.push({ name: tab.name, type: objectType(flattenProps(tab.fields ?? [])) });
|
|
430
|
+
} else {
|
|
431
|
+
out.push(...flattenProps(tab.fields ?? []));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
out.push({ name: f.name, type: getTsType(f) });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
function generateReactComponent(block) {
|
|
441
|
+
const componentName = block.interfaceName ?? toPascalCase(block.slug);
|
|
442
|
+
const propsName = `${componentName}Props`;
|
|
443
|
+
const propList = flattenProps(block.fields);
|
|
444
|
+
const propsCode = propList.map((p) => ` ${p.name}: ${p.type}`).join("\n");
|
|
445
|
+
const fieldsJsx = propList.map((p) => ` <div className="field-${p.name}">
|
|
446
|
+
{/* ${p.name} */}
|
|
447
|
+
{String(props.${p.name})}
|
|
448
|
+
</div>`).join("\n");
|
|
449
|
+
const code = [
|
|
450
|
+
`import React from 'react'`,
|
|
451
|
+
``,
|
|
452
|
+
`export type ${propsName} = {`,
|
|
453
|
+
propsCode,
|
|
454
|
+
`}`,
|
|
455
|
+
``,
|
|
456
|
+
`export function ${componentName}(props: ${propsName}) {`,
|
|
457
|
+
` return (`,
|
|
458
|
+
` <div className="${block.slug}">`,
|
|
459
|
+
fieldsJsx,
|
|
460
|
+
` </div>`,
|
|
461
|
+
` )`,
|
|
462
|
+
`}`,
|
|
463
|
+
``
|
|
464
|
+
].join("\n");
|
|
465
|
+
return {
|
|
466
|
+
filename: `${componentName}.tsx`,
|
|
467
|
+
code,
|
|
468
|
+
language: "typescript"
|
|
469
|
+
};
|
|
470
|
+
}
|
|
288
471
|
function generateBlockOutput(block) {
|
|
289
472
|
return {
|
|
290
473
|
filename: `${block.slug}.ts`,
|
|
@@ -292,8 +475,10 @@ function generateBlockOutput(block) {
|
|
|
292
475
|
language: "typescript"
|
|
293
476
|
};
|
|
294
477
|
}
|
|
295
|
-
function generateAllBlocks(blocks) {
|
|
296
|
-
return blocks.
|
|
478
|
+
function generateAllBlocks(blocks, options = {}) {
|
|
479
|
+
return blocks.flatMap(
|
|
480
|
+
(block) => options.react ? [generateBlockOutput(block), generateReactComponent(block)] : [generateBlockOutput(block)]
|
|
481
|
+
);
|
|
297
482
|
}
|
|
298
483
|
function generateIndexFile(blocks) {
|
|
299
484
|
const exportName = (b) => b.interfaceName ?? toCamelCase(b.slug);
|
|
@@ -310,185 +495,29 @@ function generateIndexFile(blocks) {
|
|
|
310
495
|
return { filename: "index.ts", code, language: "typescript" };
|
|
311
496
|
}
|
|
312
497
|
|
|
313
|
-
// src/endpoints/
|
|
314
|
-
var
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
fileMap[indexOutput.filename] = indexOutput.code;
|
|
333
|
-
return Response.json({ files: fileMap });
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
// src/block-builder/lib/schemaToBuilderBlock.ts
|
|
337
|
-
import { v4 as uuidv4 } from "uuid";
|
|
338
|
-
var REVERSE_TYPE_MAP = {
|
|
339
|
-
richtext: "richText",
|
|
340
|
-
image: "upload",
|
|
341
|
-
file: "upload",
|
|
342
|
-
// file and image both map to upload in the builder
|
|
343
|
-
multiselect: "select",
|
|
344
|
-
// builder has no multiselect — nearest equivalent
|
|
345
|
-
url: "text",
|
|
346
|
-
// builder has no url field — falls back to text
|
|
347
|
-
color: "text"
|
|
348
|
-
// builder has no color field — falls back to text
|
|
349
|
-
};
|
|
350
|
-
var VALID_BUILDER_TYPES = /* @__PURE__ */ new Set([
|
|
351
|
-
"text",
|
|
352
|
-
"textarea",
|
|
353
|
-
"number",
|
|
354
|
-
"email",
|
|
355
|
-
"checkbox",
|
|
356
|
-
"select",
|
|
357
|
-
"radio",
|
|
358
|
-
"date",
|
|
359
|
-
"richText",
|
|
360
|
-
"upload",
|
|
361
|
-
"relationship",
|
|
362
|
-
"array",
|
|
363
|
-
"group",
|
|
364
|
-
"tabs",
|
|
365
|
-
"row",
|
|
366
|
-
"collapsible",
|
|
367
|
-
"json",
|
|
368
|
-
"code",
|
|
369
|
-
"point",
|
|
370
|
-
"ui"
|
|
371
|
-
]);
|
|
372
|
-
function fieldToBuilderField(raw) {
|
|
373
|
-
const rawType = String(raw.type ?? "text");
|
|
374
|
-
const mappedType = REVERSE_TYPE_MAP[rawType] ?? rawType;
|
|
375
|
-
const isKnown = VALID_BUILDER_TYPES.has(mappedType);
|
|
376
|
-
if (!isKnown) {
|
|
377
|
-
console.warn(`[block-builder] Unknown field type "${rawType}" \u2014 rendering as "text". Add a mapping in REVERSE_TYPE_MAP.`);
|
|
378
|
-
}
|
|
379
|
-
const fieldType = isKnown ? mappedType : "text";
|
|
380
|
-
const field = {
|
|
381
|
-
id: uuidv4(),
|
|
382
|
-
type: fieldType,
|
|
383
|
-
name: String(raw.name ?? "field"),
|
|
384
|
-
label: raw.label ? String(raw.label) : void 0,
|
|
385
|
-
required: Boolean(raw.required)
|
|
386
|
-
};
|
|
387
|
-
if (raw.options && Array.isArray(raw.options)) {
|
|
388
|
-
field.options = raw.options.map(
|
|
389
|
-
(o) => typeof o === "string" ? { label: o, value: o } : { label: String(o.label), value: String(o.value) }
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
if (raw.hasMany !== void 0) field.hasMany = Boolean(raw.hasMany);
|
|
393
|
-
if (raw.collection) field.relationTo = String(raw.collection);
|
|
394
|
-
if (raw.relationTo) {
|
|
395
|
-
field.relationTo = String(raw.relationTo);
|
|
396
|
-
}
|
|
397
|
-
if (raw.minRows !== void 0) field.minRows = Number(raw.minRows);
|
|
398
|
-
if (raw.maxRows !== void 0) field.maxRows = Number(raw.maxRows);
|
|
399
|
-
if (raw.fields && Array.isArray(raw.fields)) {
|
|
400
|
-
field.fields = raw.fields.map(fieldToBuilderField);
|
|
401
|
-
}
|
|
402
|
-
if (raw.admin && typeof raw.admin === "object") {
|
|
403
|
-
const a = raw.admin;
|
|
404
|
-
field.admin = {
|
|
405
|
-
description: a.description ? String(a.description) : void 0,
|
|
406
|
-
placeholder: a.placeholder ? String(a.placeholder) : void 0,
|
|
407
|
-
readOnly: Boolean(a.readOnly),
|
|
408
|
-
hidden: Boolean(a.hidden)
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
return field;
|
|
412
|
-
}
|
|
413
|
-
function slugToInterfaceName(slug) {
|
|
414
|
-
return slug.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
415
|
-
}
|
|
416
|
-
function schemaToBuilderBlock(slug, _name, labels, schemaFields) {
|
|
417
|
-
return {
|
|
418
|
-
id: uuidv4(),
|
|
419
|
-
slug,
|
|
420
|
-
interfaceName: slugToInterfaceName(slug),
|
|
421
|
-
labels,
|
|
422
|
-
fields: schemaFields.map(fieldToBuilderField)
|
|
498
|
+
// src/endpoints/guard.ts
|
|
499
|
+
var BUILDER_HEADER = "X-Block-Builder";
|
|
500
|
+
var BUILDER_HEADER_LOWER = BUILDER_HEADER.toLowerCase();
|
|
501
|
+
var BUILDER_HEADER_VALUE = "1";
|
|
502
|
+
function withBuilderGuard(handler) {
|
|
503
|
+
return async (req) => {
|
|
504
|
+
if (!req.user) {
|
|
505
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
506
|
+
}
|
|
507
|
+
if (req.headers.get(BUILDER_HEADER_LOWER) !== BUILDER_HEADER_VALUE) {
|
|
508
|
+
return Response.json(
|
|
509
|
+
{
|
|
510
|
+
error: `Missing or invalid "${BUILDER_HEADER}" header. Internal Block Builder endpoints require "${BUILDER_HEADER}: ${BUILDER_HEADER_VALUE}".`
|
|
511
|
+
},
|
|
512
|
+
{ status: 403 }
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return handler(req);
|
|
423
516
|
};
|
|
424
517
|
}
|
|
425
518
|
|
|
426
|
-
// src/
|
|
427
|
-
var
|
|
428
|
-
if (!req.user) {
|
|
429
|
-
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
430
|
-
}
|
|
431
|
-
const slug = req.routeParams?.slug;
|
|
432
|
-
if (!slug) {
|
|
433
|
-
return Response.json({ error: "Slug is required" }, { status: 400 });
|
|
434
|
-
}
|
|
435
|
-
const requestedVersionId = req.url ? new URL(req.url).searchParams.get("versionId") : null;
|
|
436
|
-
const result = await req.payload.find({
|
|
437
|
-
collection: "block-definitions",
|
|
438
|
-
where: { slug: { equals: slug } },
|
|
439
|
-
depth: 2,
|
|
440
|
-
limit: 1
|
|
441
|
-
});
|
|
442
|
-
const def = result.docs[0];
|
|
443
|
-
if (!def) {
|
|
444
|
-
return Response.json({ error: `Block definition "${slug}" not found` }, { status: 404 });
|
|
445
|
-
}
|
|
446
|
-
const name = def.name ?? slug;
|
|
447
|
-
const currentVersionId = def.currentVersion && typeof def.currentVersion === "object" ? String(def.currentVersion.id) : typeof def.currentVersion === "string" || typeof def.currentVersion === "number" ? String(def.currentVersion) : null;
|
|
448
|
-
let version = null;
|
|
449
|
-
if (requestedVersionId) {
|
|
450
|
-
try {
|
|
451
|
-
const v = await req.payload.findByID({
|
|
452
|
-
collection: "block-definition-versions",
|
|
453
|
-
id: requestedVersionId,
|
|
454
|
-
depth: 0
|
|
455
|
-
});
|
|
456
|
-
version = v;
|
|
457
|
-
} catch {
|
|
458
|
-
return Response.json({ error: `Version "${requestedVersionId}" not found` }, { status: 404 });
|
|
459
|
-
}
|
|
460
|
-
} else if (def.currentVersion && typeof def.currentVersion === "object") {
|
|
461
|
-
version = def.currentVersion;
|
|
462
|
-
} else {
|
|
463
|
-
const latestResult = await req.payload.find({
|
|
464
|
-
collection: "block-definition-versions",
|
|
465
|
-
where: { blockDefinition: { equals: def.id } },
|
|
466
|
-
sort: "-versionNumber",
|
|
467
|
-
depth: 0,
|
|
468
|
-
limit: 1
|
|
469
|
-
});
|
|
470
|
-
version = latestResult.docs[0] ?? null;
|
|
471
|
-
}
|
|
472
|
-
if (!version) {
|
|
473
|
-
const block2 = schemaToBuilderBlock(slug, name, {}, []);
|
|
474
|
-
return Response.json({ block: block2, versionId: null, versionNumber: null, isCurrent: true });
|
|
475
|
-
}
|
|
476
|
-
const versionId = String(version.id);
|
|
477
|
-
const schema = version.schema;
|
|
478
|
-
const schemaFields = schema ? Array.isArray(schema) ? schema : schema.fields ?? [] : [];
|
|
479
|
-
const labels = version.labels ?? {};
|
|
480
|
-
const versionNumber = version.versionNumber;
|
|
481
|
-
const block = schemaToBuilderBlock(slug, name, labels, schemaFields);
|
|
482
|
-
return Response.json({
|
|
483
|
-
block,
|
|
484
|
-
versionId,
|
|
485
|
-
versionNumber: versionNumber ?? null,
|
|
486
|
-
isCurrent: versionId === currentVersionId
|
|
487
|
-
});
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
// src/builder/normalizer.ts
|
|
491
|
-
var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
519
|
+
// src/validation/schemaValidator.ts
|
|
520
|
+
var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
492
521
|
"text",
|
|
493
522
|
"textarea",
|
|
494
523
|
"richtext",
|
|
@@ -506,230 +535,64 @@ var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
|
506
535
|
"group",
|
|
507
536
|
"relationship",
|
|
508
537
|
"json",
|
|
509
|
-
"blocks"
|
|
538
|
+
"blocks",
|
|
539
|
+
"row",
|
|
540
|
+
"tabs",
|
|
541
|
+
"collapsible"
|
|
510
542
|
]);
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
543
|
+
var CONTAINER_TYPES = /* @__PURE__ */ new Set(["array", "group", "row", "collapsible"]);
|
|
544
|
+
var UNNAMED_TYPES2 = /* @__PURE__ */ new Set(["row", "tabs", "collapsible"]);
|
|
545
|
+
var LEAF_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
546
|
+
var VALID_CONDITION_OPERATORS = /* @__PURE__ */ new Set([
|
|
547
|
+
"equals",
|
|
548
|
+
"not_equals",
|
|
549
|
+
"contains",
|
|
550
|
+
"not_contains",
|
|
551
|
+
"greater_than",
|
|
552
|
+
"less_than",
|
|
553
|
+
"in",
|
|
554
|
+
"not_in",
|
|
555
|
+
"exists",
|
|
556
|
+
"empty"
|
|
557
|
+
]);
|
|
558
|
+
function validateConditions(conditions, path, errors) {
|
|
559
|
+
if (!Array.isArray(conditions)) {
|
|
560
|
+
errors.push(`${path}: "conditions" must be an array.`);
|
|
561
|
+
return;
|
|
520
562
|
}
|
|
521
|
-
|
|
563
|
+
;
|
|
564
|
+
conditions.forEach((cond, i) => {
|
|
565
|
+
const cp = `${path}[${i}]`;
|
|
566
|
+
if (!cond || typeof cond !== "object") {
|
|
567
|
+
errors.push(`${cp}: condition must be an object.`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const c = cond;
|
|
571
|
+
if (typeof c.field !== "string" || !c.field.trim()) {
|
|
572
|
+
errors.push(`${cp}: "field" must be a non-empty string.`);
|
|
573
|
+
}
|
|
574
|
+
if (typeof c.operator !== "string" || !VALID_CONDITION_OPERATORS.has(c.operator)) {
|
|
575
|
+
errors.push(
|
|
576
|
+
`${cp}: "operator" must be one of: ${[...VALID_CONDITION_OPERATORS].join(", ")}.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
522
580
|
}
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const v = raw;
|
|
539
|
-
const out = {};
|
|
540
|
-
if (typeof v.required === "boolean") out.required = v.required;
|
|
541
|
-
if (typeof v.minLength === "number") out.minLength = v.minLength;
|
|
542
|
-
if (typeof v.maxLength === "number") out.maxLength = v.maxLength;
|
|
543
|
-
if (typeof v.regex === "string") out.regex = v.regex;
|
|
544
|
-
if (typeof v.min === "number") out.min = v.min;
|
|
545
|
-
if (typeof v.max === "number") out.max = v.max;
|
|
546
|
-
if (typeof v.step === "number") out.step = v.step;
|
|
547
|
-
if (typeof v.integerOnly === "boolean") out.integerOnly = v.integerOnly;
|
|
548
|
-
if (typeof v.minRows === "number") out.minRows = v.minRows;
|
|
549
|
-
if (typeof v.maxRows === "number") out.maxRows = v.maxRows;
|
|
550
|
-
if (typeof v.uniqueItems === "boolean") out.uniqueItems = v.uniqueItems;
|
|
551
|
-
if (Array.isArray(v.allowedMimeTypes)) out.allowedMimeTypes = v.allowedMimeTypes;
|
|
552
|
-
if (typeof v.maxFileSize === "number") out.maxFileSize = v.maxFileSize;
|
|
553
|
-
if (typeof v.maxSelections === "number") out.maxSelections = v.maxSelections;
|
|
554
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
555
|
-
}
|
|
556
|
-
function normaliseUI(raw) {
|
|
557
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
558
|
-
const u = raw;
|
|
559
|
-
const out = {};
|
|
560
|
-
if (typeof u.tab === "string") out.tab = u.tab;
|
|
561
|
-
if (typeof u.section === "string") out.section = u.section;
|
|
562
|
-
if (["full", "half", "third", "quarter"].includes(u.width)) {
|
|
563
|
-
out.width = u.width;
|
|
564
|
-
}
|
|
565
|
-
if (typeof u.collapsed === "boolean") out.collapsed = u.collapsed;
|
|
566
|
-
if (typeof u.order === "number") out.order = u.order;
|
|
567
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
568
|
-
}
|
|
569
|
-
function normaliseField(raw) {
|
|
570
|
-
const type = raw.type ?? "text";
|
|
571
|
-
const resolvedType = KNOWN_TYPES.has(type) ? type : "text";
|
|
572
|
-
const base = {
|
|
573
|
-
name: String(raw.name ?? "").trim(),
|
|
574
|
-
type: resolvedType
|
|
575
|
-
};
|
|
576
|
-
if (raw.label) base.label = String(raw.label);
|
|
577
|
-
if (raw.required !== void 0) base.required = Boolean(raw.required);
|
|
578
|
-
if (raw.admin && typeof raw.admin === "object") base.admin = raw.admin;
|
|
579
|
-
const conditions = normaliseConditions(raw.conditions);
|
|
580
|
-
if (conditions) base.conditions = conditions;
|
|
581
|
-
if (raw.conditionMode === "AND" || raw.conditionMode === "OR") {
|
|
582
|
-
base.conditionMode = raw.conditionMode;
|
|
583
|
-
}
|
|
584
|
-
const validation = normaliseValidation(raw.validation);
|
|
585
|
-
if (validation) base.validation = validation;
|
|
586
|
-
const ui = normaliseUI(raw.ui);
|
|
587
|
-
if (ui) base.ui = ui;
|
|
588
|
-
switch (resolvedType) {
|
|
589
|
-
case "text":
|
|
590
|
-
case "textarea": {
|
|
591
|
-
if (raw.minLength !== void 0) base.minLength = Number(raw.minLength);
|
|
592
|
-
if (raw.maxLength !== void 0) base.maxLength = Number(raw.maxLength);
|
|
593
|
-
if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
596
|
-
case "number": {
|
|
597
|
-
if (raw.min !== void 0) base.min = Number(raw.min);
|
|
598
|
-
if (raw.max !== void 0) base.max = Number(raw.max);
|
|
599
|
-
if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
|
|
600
|
-
break;
|
|
601
|
-
}
|
|
602
|
-
case "checkbox": {
|
|
603
|
-
if (raw.defaultValue !== void 0) base.defaultValue = Boolean(raw.defaultValue);
|
|
604
|
-
break;
|
|
605
|
-
}
|
|
606
|
-
case "select":
|
|
607
|
-
case "multiselect": {
|
|
608
|
-
const rawOpts = Array.isArray(raw.options) ? raw.options : [];
|
|
609
|
-
base.options = rawOpts.map(normaliseOption);
|
|
610
|
-
if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
|
|
611
|
-
break;
|
|
612
|
-
}
|
|
613
|
-
case "date": {
|
|
614
|
-
if (raw.timeFormat !== void 0) base.timeFormat = Boolean(raw.timeFormat);
|
|
615
|
-
break;
|
|
616
|
-
}
|
|
617
|
-
case "array": {
|
|
618
|
-
const subFields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
|
|
619
|
-
base.fields = subFields;
|
|
620
|
-
if (raw.minRows !== void 0) base.minRows = Number(raw.minRows);
|
|
621
|
-
if (raw.maxRows !== void 0) base.maxRows = Number(raw.maxRows);
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
case "group": {
|
|
625
|
-
const subFields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
|
|
626
|
-
base.fields = subFields;
|
|
627
|
-
break;
|
|
628
|
-
}
|
|
629
|
-
case "relationship": {
|
|
630
|
-
if (raw.collection) base.collection = String(raw.collection);
|
|
631
|
-
if (raw.hasMany !== void 0) base.hasMany = Boolean(raw.hasMany);
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
case "file": {
|
|
635
|
-
if (Array.isArray(raw.allowedMimeTypes)) base.allowedMimeTypes = raw.allowedMimeTypes;
|
|
636
|
-
break;
|
|
637
|
-
}
|
|
638
|
-
case "blocks": {
|
|
639
|
-
if (Array.isArray(raw.allowedBlocks)) {
|
|
640
|
-
base.allowedBlocks = raw.allowedBlocks.map(String).filter(Boolean);
|
|
641
|
-
}
|
|
642
|
-
if (raw.minBlocks !== void 0) base.minBlocks = Number(raw.minBlocks);
|
|
643
|
-
if (raw.maxBlocks !== void 0) base.maxBlocks = Number(raw.maxBlocks);
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
return base;
|
|
648
|
-
}
|
|
649
|
-
function normaliseSchema(raw) {
|
|
650
|
-
const fields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
|
|
651
|
-
const schema = { fields };
|
|
652
|
-
if (raw.layout === "sidebar" || raw.layout === "tabs") {
|
|
653
|
-
schema.layout = raw.layout;
|
|
654
|
-
} else {
|
|
655
|
-
schema.layout = "default";
|
|
656
|
-
}
|
|
657
|
-
return schema;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// src/validation/schemaValidator.ts
|
|
661
|
-
var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
662
|
-
"text",
|
|
663
|
-
"textarea",
|
|
664
|
-
"richtext",
|
|
665
|
-
"number",
|
|
666
|
-
"checkbox",
|
|
667
|
-
"select",
|
|
668
|
-
"multiselect",
|
|
669
|
-
"date",
|
|
670
|
-
"image",
|
|
671
|
-
"file",
|
|
672
|
-
"url",
|
|
673
|
-
"email",
|
|
674
|
-
"color",
|
|
675
|
-
"array",
|
|
676
|
-
"group",
|
|
677
|
-
"relationship",
|
|
678
|
-
"json",
|
|
679
|
-
"blocks"
|
|
680
|
-
]);
|
|
681
|
-
var CONTAINER_TYPES = /* @__PURE__ */ new Set(["array", "group"]);
|
|
682
|
-
var LEAF_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
683
|
-
var VALID_CONDITION_OPERATORS = /* @__PURE__ */ new Set([
|
|
684
|
-
"equals",
|
|
685
|
-
"not_equals",
|
|
686
|
-
"contains",
|
|
687
|
-
"not_contains",
|
|
688
|
-
"greater_than",
|
|
689
|
-
"less_than",
|
|
690
|
-
"in",
|
|
691
|
-
"not_in",
|
|
692
|
-
"exists",
|
|
693
|
-
"empty"
|
|
694
|
-
]);
|
|
695
|
-
function validateConditions(conditions, path, errors) {
|
|
696
|
-
if (!Array.isArray(conditions)) {
|
|
697
|
-
errors.push(`${path}: "conditions" must be an array.`);
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
;
|
|
701
|
-
conditions.forEach((cond, i) => {
|
|
702
|
-
const cp = `${path}[${i}]`;
|
|
703
|
-
if (!cond || typeof cond !== "object") {
|
|
704
|
-
errors.push(`${cp}: condition must be an object.`);
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
const c = cond;
|
|
708
|
-
if (typeof c.field !== "string" || !c.field.trim()) {
|
|
709
|
-
errors.push(`${cp}: "field" must be a non-empty string.`);
|
|
710
|
-
}
|
|
711
|
-
if (typeof c.operator !== "string" || !VALID_CONDITION_OPERATORS.has(c.operator)) {
|
|
712
|
-
errors.push(
|
|
713
|
-
`${cp}: "operator" must be one of: ${[...VALID_CONDITION_OPERATORS].join(", ")}.`
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
function validateValidationRules(v, path, errors) {
|
|
719
|
-
const numericProps = [
|
|
720
|
-
"minLength",
|
|
721
|
-
"maxLength",
|
|
722
|
-
"min",
|
|
723
|
-
"max",
|
|
724
|
-
"step",
|
|
725
|
-
"minRows",
|
|
726
|
-
"maxRows",
|
|
727
|
-
"maxFileSize",
|
|
728
|
-
"maxSelections"
|
|
729
|
-
];
|
|
730
|
-
for (const prop of numericProps) {
|
|
731
|
-
if (v[prop] !== void 0 && typeof v[prop] !== "number") {
|
|
732
|
-
errors.push(`${path}.${prop}: must be a number.`);
|
|
581
|
+
function validateValidationRules(v, path, errors) {
|
|
582
|
+
const numericProps = [
|
|
583
|
+
"minLength",
|
|
584
|
+
"maxLength",
|
|
585
|
+
"min",
|
|
586
|
+
"max",
|
|
587
|
+
"step",
|
|
588
|
+
"minRows",
|
|
589
|
+
"maxRows",
|
|
590
|
+
"maxFileSize",
|
|
591
|
+
"maxSelections"
|
|
592
|
+
];
|
|
593
|
+
for (const prop of numericProps) {
|
|
594
|
+
if (v[prop] !== void 0 && typeof v[prop] !== "number") {
|
|
595
|
+
errors.push(`${path}.${prop}: must be a number.`);
|
|
733
596
|
}
|
|
734
597
|
}
|
|
735
598
|
if (typeof v.minLength === "number" && typeof v.maxLength === "number" && v.minLength > v.maxLength) {
|
|
@@ -762,7 +625,7 @@ function validateValidationRules(v, path, errors) {
|
|
|
762
625
|
errors.push(`${path}.allowedMimeTypes: must be an array of strings.`);
|
|
763
626
|
}
|
|
764
627
|
}
|
|
765
|
-
function validateField(field, path, errors, warnings) {
|
|
628
|
+
function validateField(field, path, errors, warnings, reserved = [], names = /* @__PURE__ */ new Set()) {
|
|
766
629
|
if (!field || typeof field !== "object" || Array.isArray(field)) {
|
|
767
630
|
errors.push(`${path}: must be a non-array object.`);
|
|
768
631
|
return;
|
|
@@ -873,7 +736,16 @@ function validateField(field, path, errors, warnings) {
|
|
|
873
736
|
if (!Array.isArray(f.fields) || f.fields.length === 0) {
|
|
874
737
|
errors.push(`${path}: "${type}" fields must have a non-empty "fields" array.`);
|
|
875
738
|
} else {
|
|
876
|
-
validateFields(
|
|
739
|
+
validateFields(
|
|
740
|
+
f.fields,
|
|
741
|
+
`${path}.fields`,
|
|
742
|
+
errors,
|
|
743
|
+
warnings,
|
|
744
|
+
childReservedNames(type, reserved),
|
|
745
|
+
// A `row` or `collapsible` stores its children in the surrounding
|
|
746
|
+
// object, so they share its namespace; `array` and `group` open one.
|
|
747
|
+
FLATTENING_TYPES.has(type) ? names : /* @__PURE__ */ new Set()
|
|
748
|
+
);
|
|
877
749
|
}
|
|
878
750
|
if (type === "array") {
|
|
879
751
|
if (f.minRows !== void 0 && typeof f.minRows !== "number") {
|
|
@@ -907,18 +779,63 @@ function validateField(field, path, errors, warnings) {
|
|
|
907
779
|
errors.push(`${path}: "minBlocks" (${f.minBlocks}) must be <= "maxBlocks" (${f.maxBlocks}).`);
|
|
908
780
|
}
|
|
909
781
|
}
|
|
782
|
+
if (type === "tabs") {
|
|
783
|
+
if (!Array.isArray(f.tabs) || f.tabs.length === 0) {
|
|
784
|
+
errors.push(`${path}: "tabs" fields must have a non-empty "tabs" array.`);
|
|
785
|
+
} else {
|
|
786
|
+
;
|
|
787
|
+
f.tabs.forEach((tab, i) => {
|
|
788
|
+
const tabPath = `${path}.tabs[${i}]`;
|
|
789
|
+
if (!tab || typeof tab !== "object") {
|
|
790
|
+
errors.push(`${tabPath}: must be an object.`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const t = tab;
|
|
794
|
+
if (typeof t.label !== "string" || !t.label.trim()) {
|
|
795
|
+
errors.push(`${tabPath}: "label" is required.`);
|
|
796
|
+
}
|
|
797
|
+
if (!Array.isArray(t.fields) || t.fields.length === 0) {
|
|
798
|
+
errors.push(`${tabPath}: "fields" must be a non-empty array.`);
|
|
799
|
+
} else {
|
|
800
|
+
const named = typeof t.name === "string" && t.name.trim().length > 0;
|
|
801
|
+
if (named) claimName(String(t.name).trim(), tabPath, errors, reserved, names);
|
|
802
|
+
validateFields(
|
|
803
|
+
t.fields,
|
|
804
|
+
`${tabPath}.fields`,
|
|
805
|
+
errors,
|
|
806
|
+
warnings,
|
|
807
|
+
named ? [] : reserved,
|
|
808
|
+
named ? /* @__PURE__ */ new Set() : names
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
var BLOCK_RESERVED_NAMES = ["id", "createdAt", "updatedAt", "blockType", "blockName"];
|
|
816
|
+
var ARRAY_ROW_RESERVED_NAMES = ["id"];
|
|
817
|
+
function childReservedNames(type, reserved) {
|
|
818
|
+
if (FLATTENING_TYPES.has(type)) return reserved;
|
|
819
|
+
if (type === "array") return ARRAY_ROW_RESERVED_NAMES;
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
var FLATTENING_TYPES = /* @__PURE__ */ new Set(["row", "collapsible"]);
|
|
823
|
+
function claimName(name, path, errors, reserved, names) {
|
|
824
|
+
if (names.has(name)) {
|
|
825
|
+
errors.push(`${path}: duplicate field name "${name}".`);
|
|
826
|
+
}
|
|
827
|
+
if (reserved.includes(name)) {
|
|
828
|
+
errors.push(`${path}: field name "${name}" is a reserved word in Payload CMS.`);
|
|
829
|
+
}
|
|
830
|
+
names.add(name);
|
|
910
831
|
}
|
|
911
|
-
function validateFields(fields, path, errors, warnings) {
|
|
912
|
-
const names = /* @__PURE__ */ new Set();
|
|
832
|
+
function validateFields(fields, path, errors, warnings, reserved = [], names = /* @__PURE__ */ new Set()) {
|
|
913
833
|
fields.forEach((field, index) => {
|
|
914
834
|
const fieldPath = `${path}[${index}]`;
|
|
915
|
-
validateField(field, fieldPath, errors, warnings);
|
|
835
|
+
validateField(field, fieldPath, errors, warnings, reserved, names);
|
|
916
836
|
const f = field;
|
|
917
|
-
if (typeof f.name === "string" && f.name) {
|
|
918
|
-
|
|
919
|
-
errors.push(`${path}: duplicate field name "${f.name}".`);
|
|
920
|
-
}
|
|
921
|
-
names.add(f.name);
|
|
837
|
+
if (typeof f.name === "string" && f.name && !UNNAMED_TYPES2.has(f.type)) {
|
|
838
|
+
claimName(f.name, path, errors, reserved, names);
|
|
922
839
|
}
|
|
923
840
|
});
|
|
924
841
|
}
|
|
@@ -938,7 +855,7 @@ function validateBlockSchema(schema) {
|
|
|
938
855
|
} else if (s.fields.length === 0) {
|
|
939
856
|
warnings.push("Schema has no fields defined.");
|
|
940
857
|
} else {
|
|
941
|
-
validateFields(s.fields, "schema.fields", errors, warnings);
|
|
858
|
+
validateFields(s.fields, "schema.fields", errors, warnings, BLOCK_RESERVED_NAMES);
|
|
942
859
|
}
|
|
943
860
|
if (s.layout !== void 0 && !["default", "sidebar", "tabs"].includes(s.layout)) {
|
|
944
861
|
errors.push(`schema.layout must be one of: "default", "sidebar", "tabs".`);
|
|
@@ -946,49 +863,467 @@ function validateBlockSchema(schema) {
|
|
|
946
863
|
return { valid: errors.length === 0, errors, warnings };
|
|
947
864
|
}
|
|
948
865
|
|
|
949
|
-
// src/
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
866
|
+
// src/endpoints/generate.ts
|
|
867
|
+
var SLUG_RE = /^[A-Za-z][A-Za-z0-9]*(?:[-_][A-Za-z0-9]+)*$/;
|
|
868
|
+
var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
869
|
+
var generateEndpoint = withBuilderGuard(async (req) => {
|
|
870
|
+
let blocks;
|
|
871
|
+
let react = false;
|
|
872
|
+
try {
|
|
873
|
+
if (!req.json) return Response.json({ error: "No JSON parser available" }, { status: 500 });
|
|
874
|
+
const body = await req.json();
|
|
875
|
+
const raw = body.blocks ?? [];
|
|
876
|
+
if (!Array.isArray(raw)) {
|
|
877
|
+
return Response.json({ error: '"blocks" must be an array.' }, { status: 400 });
|
|
878
|
+
}
|
|
879
|
+
blocks = raw;
|
|
880
|
+
react = body.react === true;
|
|
881
|
+
} catch {
|
|
882
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
962
883
|
}
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
884
|
+
const errors = [];
|
|
885
|
+
blocks.forEach((block, i) => {
|
|
886
|
+
const label = block?.slug ? `"${block.slug}"` : `at index ${i}`;
|
|
887
|
+
if (typeof block?.slug !== "string" || !SLUG_RE.test(block.slug)) {
|
|
888
|
+
errors.push(
|
|
889
|
+
`Block ${label}: "slug" must start with a letter and contain only letters, digits, and single "-" or "_" separators.`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (block?.interfaceName !== void 0 && (typeof block.interfaceName !== "string" || !IDENTIFIER_RE.test(block.interfaceName))) {
|
|
893
|
+
errors.push(`Block ${label}: "interfaceName" must be a valid TypeScript identifier.`);
|
|
894
|
+
}
|
|
895
|
+
const result = validateBlockSchema({ fields: block?.fields ?? [] });
|
|
896
|
+
result.errors.forEach((e) => errors.push(`Block ${label}: ${e}`));
|
|
897
|
+
});
|
|
898
|
+
if (errors.length > 0) {
|
|
899
|
+
return Response.json({ error: "Invalid block schema", errors }, { status: 400 });
|
|
973
900
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
901
|
+
const blockOutputs = generateAllBlocks(blocks, { react });
|
|
902
|
+
const indexOutput = generateIndexFile(blocks);
|
|
903
|
+
const fileMap = {};
|
|
904
|
+
for (const out of blockOutputs) {
|
|
905
|
+
fileMap[out.filename] = out.code;
|
|
906
|
+
}
|
|
907
|
+
fileMap[indexOutput.filename] = indexOutput.code;
|
|
908
|
+
return Response.json({ files: fileMap });
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// src/utils/uuid.ts
|
|
912
|
+
function uuidv4() {
|
|
913
|
+
const c = globalThis.crypto;
|
|
914
|
+
if (typeof c?.randomUUID === "function") {
|
|
915
|
+
return c.randomUUID();
|
|
916
|
+
}
|
|
917
|
+
if (typeof c?.getRandomValues === "function") {
|
|
918
|
+
const bytes = c.getRandomValues(new Uint8Array(16));
|
|
919
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
920
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
921
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
922
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
923
|
+
}
|
|
924
|
+
throw new Error(
|
|
925
|
+
"No cryptographic random source available. `crypto.randomUUID` requires a secure context (HTTPS or localhost)."
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/utils/resolveId.ts
|
|
930
|
+
function resolveId(value) {
|
|
931
|
+
if (value === null || value === void 0) return null;
|
|
932
|
+
if (typeof value === "string") return value;
|
|
933
|
+
if (typeof value === "number") return String(value);
|
|
934
|
+
if (typeof value === "object" && "id" in value) {
|
|
935
|
+
const id = value.id;
|
|
936
|
+
if (typeof id === "string") return id;
|
|
937
|
+
if (typeof id === "number") return String(id);
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/endpoints/load.ts
|
|
943
|
+
function assignFieldIds(fields) {
|
|
944
|
+
if (!Array.isArray(fields)) return [];
|
|
945
|
+
return fields.map((field) => {
|
|
946
|
+
const f = { ...field, id: uuidv4() };
|
|
947
|
+
if (Array.isArray(f.fields)) f.fields = assignFieldIds(f.fields);
|
|
948
|
+
if (Array.isArray(f.tabs)) {
|
|
949
|
+
f.tabs = f.tabs.map((tab) => ({ ...tab, fields: assignFieldIds(tab.fields) }));
|
|
950
|
+
}
|
|
951
|
+
return f;
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
var loadEndpoint = withBuilderGuard(async (req) => {
|
|
955
|
+
const slug = req.routeParams?.slug;
|
|
956
|
+
if (!slug) {
|
|
957
|
+
return Response.json({ error: "Slug is required" }, { status: 400 });
|
|
958
|
+
}
|
|
959
|
+
const requestedVersionId = req.url ? new URL(req.url).searchParams.get("versionId") : null;
|
|
960
|
+
const result = await req.payload.find({
|
|
961
|
+
collection: "block-definitions",
|
|
962
|
+
where: { slug: { equals: slug } },
|
|
963
|
+
depth: 2,
|
|
964
|
+
limit: 1
|
|
965
|
+
});
|
|
966
|
+
const def = result.docs[0];
|
|
967
|
+
if (!def) {
|
|
968
|
+
return Response.json({ error: `Block definition "${slug}" not found` }, { status: 404 });
|
|
969
|
+
}
|
|
970
|
+
const currentVersionId = resolveId(def.currentVersion);
|
|
971
|
+
let version = null;
|
|
972
|
+
if (requestedVersionId) {
|
|
973
|
+
try {
|
|
974
|
+
const v = await req.payload.findByID({
|
|
975
|
+
collection: "block-definition-versions",
|
|
976
|
+
id: requestedVersionId,
|
|
977
|
+
depth: 0
|
|
978
|
+
});
|
|
979
|
+
version = v;
|
|
980
|
+
} catch (err) {
|
|
981
|
+
if (err instanceof Error && err.name === "NotFound") {
|
|
982
|
+
return Response.json({ error: `Version "${requestedVersionId}" not found` }, { status: 404 });
|
|
983
|
+
}
|
|
984
|
+
return Response.json({ error: `Failed to load version "${requestedVersionId}"` }, { status: 500 });
|
|
985
|
+
}
|
|
986
|
+
} else if (def.currentVersion && typeof def.currentVersion === "object") {
|
|
987
|
+
version = def.currentVersion;
|
|
988
|
+
} else {
|
|
989
|
+
const latestResult = await req.payload.find({
|
|
990
|
+
collection: "block-definition-versions",
|
|
991
|
+
where: { blockDefinition: { equals: def.id } },
|
|
992
|
+
sort: "-versionNumber",
|
|
993
|
+
depth: 0,
|
|
994
|
+
limit: 1
|
|
995
|
+
});
|
|
996
|
+
version = latestResult.docs[0] ?? null;
|
|
997
|
+
}
|
|
998
|
+
function slugToInterfaceName(s) {
|
|
999
|
+
return s.split(/[-_]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
1000
|
+
}
|
|
1001
|
+
if (!version) {
|
|
1002
|
+
const block2 = { id: uuidv4(), slug, interfaceName: slugToInterfaceName(slug), labels: {}, fields: [] };
|
|
1003
|
+
return Response.json({ block: block2, versionId: null, versionNumber: null, isCurrent: true });
|
|
1004
|
+
}
|
|
1005
|
+
const versionId = String(version.id);
|
|
1006
|
+
const schema = version.schema;
|
|
1007
|
+
const schemaFields = schema ? Array.isArray(schema) ? schema : schema.fields ?? [] : [];
|
|
1008
|
+
const labels = version.labels ?? {};
|
|
1009
|
+
const versionNumber = version.versionNumber;
|
|
1010
|
+
const block = {
|
|
1011
|
+
id: uuidv4(),
|
|
1012
|
+
slug,
|
|
1013
|
+
interfaceName: slugToInterfaceName(slug),
|
|
1014
|
+
labels,
|
|
1015
|
+
fields: assignFieldIds(schemaFields)
|
|
1016
|
+
};
|
|
1017
|
+
return Response.json({
|
|
1018
|
+
block,
|
|
1019
|
+
versionId,
|
|
1020
|
+
versionNumber: versionNumber ?? null,
|
|
1021
|
+
isCurrent: versionId === currentVersionId
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// src/builder/normalizer.ts
|
|
1026
|
+
var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
1027
|
+
"text",
|
|
1028
|
+
"textarea",
|
|
1029
|
+
"richtext",
|
|
1030
|
+
"number",
|
|
1031
|
+
"checkbox",
|
|
1032
|
+
"select",
|
|
1033
|
+
"multiselect",
|
|
1034
|
+
"date",
|
|
1035
|
+
"image",
|
|
1036
|
+
"file",
|
|
1037
|
+
"url",
|
|
1038
|
+
"email",
|
|
1039
|
+
"color",
|
|
1040
|
+
"array",
|
|
1041
|
+
"group",
|
|
1042
|
+
"relationship",
|
|
1043
|
+
"json",
|
|
1044
|
+
"blocks",
|
|
1045
|
+
"row",
|
|
1046
|
+
"tabs",
|
|
1047
|
+
"collapsible"
|
|
1048
|
+
]);
|
|
1049
|
+
function normaliseOption(opt) {
|
|
1050
|
+
if (typeof opt === "string") {
|
|
1051
|
+
return { label: opt, value: opt.toLowerCase().replace(/\s+/g, "-") };
|
|
1052
|
+
}
|
|
1053
|
+
if (opt && typeof opt === "object") {
|
|
1054
|
+
const o = opt;
|
|
1055
|
+
const value = String(o.value ?? o.label ?? "").toLowerCase().replace(/\s+/g, "-");
|
|
1056
|
+
const label = String(o.label ?? o.value ?? value);
|
|
1057
|
+
return { label, value };
|
|
1058
|
+
}
|
|
1059
|
+
return { label: String(opt), value: String(opt) };
|
|
1060
|
+
}
|
|
1061
|
+
function normaliseConditions(raw) {
|
|
1062
|
+
if (!Array.isArray(raw)) return void 0;
|
|
1063
|
+
const result = [];
|
|
1064
|
+
for (const item of raw) {
|
|
1065
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
1066
|
+
const c = item;
|
|
1067
|
+
if (typeof c.field === "string" && typeof c.operator === "string") {
|
|
1068
|
+
result.push({ field: c.field, operator: c.operator, value: c.value });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return result.length > 0 ? result : void 0;
|
|
1073
|
+
}
|
|
1074
|
+
function normaliseValidation(raw) {
|
|
1075
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
1076
|
+
const v = raw;
|
|
1077
|
+
const out = {};
|
|
1078
|
+
if (typeof v.required === "boolean") out.required = v.required;
|
|
1079
|
+
if (typeof v.minLength === "number") out.minLength = v.minLength;
|
|
1080
|
+
if (typeof v.maxLength === "number") out.maxLength = v.maxLength;
|
|
1081
|
+
if (typeof v.regex === "string") out.regex = v.regex;
|
|
1082
|
+
if (typeof v.min === "number") out.min = v.min;
|
|
1083
|
+
if (typeof v.max === "number") out.max = v.max;
|
|
1084
|
+
if (typeof v.step === "number") out.step = v.step;
|
|
1085
|
+
if (typeof v.integerOnly === "boolean") out.integerOnly = v.integerOnly;
|
|
1086
|
+
if (typeof v.minRows === "number") out.minRows = v.minRows;
|
|
1087
|
+
if (typeof v.maxRows === "number") out.maxRows = v.maxRows;
|
|
1088
|
+
if (typeof v.uniqueItems === "boolean") out.uniqueItems = v.uniqueItems;
|
|
1089
|
+
if (Array.isArray(v.allowedMimeTypes)) out.allowedMimeTypes = v.allowedMimeTypes;
|
|
1090
|
+
if (typeof v.maxFileSize === "number") out.maxFileSize = v.maxFileSize;
|
|
1091
|
+
if (typeof v.maxSelections === "number") out.maxSelections = v.maxSelections;
|
|
1092
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1093
|
+
}
|
|
1094
|
+
function normaliseUI(raw) {
|
|
1095
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
1096
|
+
const u = raw;
|
|
1097
|
+
const out = {};
|
|
1098
|
+
if (typeof u.tab === "string") out.tab = u.tab;
|
|
1099
|
+
if (typeof u.section === "string") out.section = u.section;
|
|
1100
|
+
if (["full", "half", "third", "quarter"].includes(u.width)) {
|
|
1101
|
+
out.width = u.width;
|
|
1102
|
+
}
|
|
1103
|
+
if (typeof u.collapsed === "boolean") out.collapsed = u.collapsed;
|
|
1104
|
+
if (typeof u.order === "number") out.order = u.order;
|
|
1105
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1106
|
+
}
|
|
1107
|
+
function normaliseField(raw) {
|
|
1108
|
+
const type = raw.type ?? "text";
|
|
1109
|
+
const resolvedType = KNOWN_TYPES.has(type) ? type : "text";
|
|
1110
|
+
const base = {
|
|
1111
|
+
name: String(raw.name ?? "").trim(),
|
|
1112
|
+
type: resolvedType
|
|
1113
|
+
};
|
|
1114
|
+
if (raw.label) base.label = String(raw.label);
|
|
1115
|
+
if (raw.required !== void 0) base.required = Boolean(raw.required);
|
|
1116
|
+
if (raw.unique !== void 0) base.unique = Boolean(raw.unique);
|
|
1117
|
+
if (raw.localized !== void 0) base.localized = Boolean(raw.localized);
|
|
1118
|
+
if (raw.admin && typeof raw.admin === "object") base.admin = raw.admin;
|
|
1119
|
+
const conditions = normaliseConditions(raw.conditions);
|
|
1120
|
+
if (conditions) base.conditions = conditions;
|
|
1121
|
+
if (raw.conditionMode === "AND" || raw.conditionMode === "OR") {
|
|
1122
|
+
base.conditionMode = raw.conditionMode;
|
|
1123
|
+
}
|
|
1124
|
+
const validation = normaliseValidation(raw.validation);
|
|
1125
|
+
if (validation) base.validation = validation;
|
|
1126
|
+
const ui = normaliseUI(raw.ui);
|
|
1127
|
+
if (ui) base.ui = ui;
|
|
1128
|
+
switch (resolvedType) {
|
|
1129
|
+
case "text":
|
|
1130
|
+
case "textarea": {
|
|
1131
|
+
if (raw.minLength !== void 0) base.minLength = Number(raw.minLength);
|
|
1132
|
+
if (raw.maxLength !== void 0) base.maxLength = Number(raw.maxLength);
|
|
1133
|
+
if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
case "number": {
|
|
1137
|
+
if (raw.min !== void 0) base.min = Number(raw.min);
|
|
1138
|
+
if (raw.max !== void 0) base.max = Number(raw.max);
|
|
1139
|
+
if (raw.defaultValue !== void 0 && raw.defaultValue !== "") {
|
|
1140
|
+
const n = Number(raw.defaultValue);
|
|
1141
|
+
if (!Number.isNaN(n)) base.defaultValue = n;
|
|
1142
|
+
}
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
case "checkbox": {
|
|
1146
|
+
if (raw.defaultValue !== void 0) {
|
|
1147
|
+
base.defaultValue = typeof raw.defaultValue === "string" ? raw.defaultValue.toLowerCase() === "true" : Boolean(raw.defaultValue);
|
|
1148
|
+
}
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
case "select":
|
|
1152
|
+
case "multiselect": {
|
|
1153
|
+
const rawOpts = Array.isArray(raw.options) ? raw.options : [];
|
|
1154
|
+
base.options = rawOpts.map(normaliseOption);
|
|
1155
|
+
if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
case "date": {
|
|
1159
|
+
if (raw.timeFormat !== void 0) base.timeFormat = Boolean(raw.timeFormat);
|
|
1160
|
+
if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
// The config panel offers a Default Value input for all of these, and the
|
|
1164
|
+
// emitter writes one out, so the normaliser has to carry it through.
|
|
1165
|
+
case "richtext":
|
|
1166
|
+
case "email":
|
|
1167
|
+
case "url":
|
|
1168
|
+
case "color": {
|
|
1169
|
+
if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
|
|
1170
|
+
break;
|
|
1171
|
+
}
|
|
1172
|
+
case "array": {
|
|
1173
|
+
const subFields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
|
|
1174
|
+
base.fields = subFields;
|
|
1175
|
+
if (raw.minRows !== void 0) base.minRows = Number(raw.minRows);
|
|
1176
|
+
if (raw.maxRows !== void 0) base.maxRows = Number(raw.maxRows);
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
case "group": {
|
|
1180
|
+
const subFields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
|
|
1181
|
+
base.fields = subFields;
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
case "relationship": {
|
|
1185
|
+
if (raw.collection) base.collection = String(raw.collection);
|
|
1186
|
+
if (raw.hasMany !== void 0) base.hasMany = Boolean(raw.hasMany);
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
// Both upload types carry the target collection through to `relationTo`.
|
|
1190
|
+
case "image": {
|
|
1191
|
+
if (raw.collection) base.collection = String(raw.collection);
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
case "file": {
|
|
1195
|
+
if (Array.isArray(raw.allowedMimeTypes)) base.allowedMimeTypes = raw.allowedMimeTypes;
|
|
1196
|
+
if (raw.collection) base.collection = String(raw.collection);
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
case "blocks": {
|
|
1200
|
+
if (Array.isArray(raw.allowedBlocks)) {
|
|
1201
|
+
base.allowedBlocks = raw.allowedBlocks.map(String).filter(Boolean);
|
|
1202
|
+
}
|
|
1203
|
+
if (raw.minBlocks !== void 0) base.minBlocks = Number(raw.minBlocks);
|
|
1204
|
+
if (raw.maxBlocks !== void 0) base.maxBlocks = Number(raw.maxBlocks);
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
case "row":
|
|
1208
|
+
case "collapsible": {
|
|
1209
|
+
const subFields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
|
|
1210
|
+
base.fields = subFields;
|
|
1211
|
+
if (resolvedType === "collapsible") {
|
|
1212
|
+
base.label = String(raw.label ?? "Collapsible Section");
|
|
1213
|
+
}
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
case "tabs": {
|
|
1217
|
+
const rawTabs = Array.isArray(raw.tabs) ? raw.tabs : [];
|
|
1218
|
+
base.tabs = rawTabs.map((t) => {
|
|
1219
|
+
const tab = t;
|
|
1220
|
+
return {
|
|
1221
|
+
id: tab.id ? String(tab.id) : void 0,
|
|
1222
|
+
name: tab.name ? String(tab.name) : void 0,
|
|
1223
|
+
label: String(tab.label ?? "Tab"),
|
|
1224
|
+
description: tab.description ? String(tab.description) : void 0,
|
|
1225
|
+
fields: Array.isArray(tab.fields) ? tab.fields.map(normaliseField) : []
|
|
1226
|
+
};
|
|
1227
|
+
});
|
|
1228
|
+
break;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return base;
|
|
1232
|
+
}
|
|
1233
|
+
function normaliseSchema(raw) {
|
|
1234
|
+
const fields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
|
|
1235
|
+
const schema = { fields };
|
|
1236
|
+
if (raw.layout === "sidebar" || raw.layout === "tabs") {
|
|
1237
|
+
schema.layout = raw.layout;
|
|
1238
|
+
} else {
|
|
1239
|
+
schema.layout = "default";
|
|
1240
|
+
}
|
|
1241
|
+
return schema;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/utils/isUniqueConstraintViolation.ts
|
|
1245
|
+
function isUniqueConstraintViolation(err) {
|
|
1246
|
+
for (const candidate of unwrap(err)) {
|
|
1247
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
1248
|
+
const e = candidate;
|
|
1249
|
+
const code = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : "";
|
|
1250
|
+
if (code === "23505") return true;
|
|
1251
|
+
if (code === "11000" || code === "11001") return true;
|
|
1252
|
+
if (code === "ER_DUP_ENTRY") return true;
|
|
1253
|
+
if (code.startsWith("SQLITE_CONSTRAINT")) return true;
|
|
1254
|
+
if (e.errno === 1062) return true;
|
|
1255
|
+
if (e.name === "MongoServerError" && code === "11000") return true;
|
|
1256
|
+
const message = typeof e.message === "string" ? e.message.toLowerCase() : "";
|
|
1257
|
+
if (message.includes("duplicate key") || message.includes("unique constraint") || message.includes("duplicate entry")) {
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
function unwrap(err) {
|
|
1264
|
+
const seen = [];
|
|
1265
|
+
let current = err;
|
|
1266
|
+
for (let depth = 0; depth < 5 && current; depth++) {
|
|
1267
|
+
if (seen.includes(current)) break;
|
|
1268
|
+
seen.push(current);
|
|
1269
|
+
const next = current;
|
|
1270
|
+
current = next.cause ?? next.originalError;
|
|
1271
|
+
}
|
|
1272
|
+
return seen;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/builder/saveSchema.ts
|
|
1276
|
+
async function saveSchemaLocally(payload, request) {
|
|
1277
|
+
const { blockSlug, name, description, category, schema: rawSchema, changelog } = request;
|
|
1278
|
+
if (!/^[a-z0-9-]+$/.test(blockSlug)) {
|
|
1279
|
+
return {
|
|
1280
|
+
success: false,
|
|
1281
|
+
definitionId: "",
|
|
1282
|
+
versionId: "",
|
|
1283
|
+
versionNumber: 0,
|
|
1284
|
+
errors: [`Invalid block slug "${blockSlug}". Use only lowercase letters, numbers, and hyphens.`],
|
|
1285
|
+
warnings: []
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
const schema = normaliseSchema(rawSchema);
|
|
1289
|
+
if (!schema.fields?.length) {
|
|
1290
|
+
return {
|
|
1291
|
+
success: false,
|
|
1292
|
+
definitionId: "",
|
|
1293
|
+
versionId: "",
|
|
1294
|
+
versionNumber: 0,
|
|
1295
|
+
errors: [`Block "${blockSlug}" schema has no fields. Schemas must be imported from a server-safe module (not a "use client" file).`],
|
|
1296
|
+
warnings: []
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
const validation = validateBlockSchema(schema);
|
|
1300
|
+
if (!validation.valid) {
|
|
1301
|
+
return {
|
|
1302
|
+
success: false,
|
|
1303
|
+
definitionId: "",
|
|
1304
|
+
versionId: "",
|
|
1305
|
+
versionNumber: 0,
|
|
1306
|
+
errors: validation.errors,
|
|
1307
|
+
warnings: validation.warnings
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
let definitionId;
|
|
1312
|
+
const existing = await payload.find({
|
|
1313
|
+
collection: "block-definitions",
|
|
1314
|
+
where: { slug: { equals: blockSlug } },
|
|
1315
|
+
limit: 1
|
|
1316
|
+
});
|
|
1317
|
+
if (existing.docs.length > 0) {
|
|
1318
|
+
definitionId = existing.docs[0].id;
|
|
1319
|
+
const updates = {};
|
|
1320
|
+
if (description !== void 0) updates.description = description;
|
|
1321
|
+
if (category !== void 0) updates.category = category;
|
|
1322
|
+
if (name !== void 0) updates.name = name;
|
|
1323
|
+
if (Object.keys(updates).length > 0) {
|
|
1324
|
+
await payload.update({
|
|
1325
|
+
collection: "block-definitions",
|
|
1326
|
+
id: definitionId,
|
|
992
1327
|
data: updates
|
|
993
1328
|
});
|
|
994
1329
|
}
|
|
@@ -1003,34 +1338,71 @@ async function saveSchemaLocally(payload, request) {
|
|
|
1003
1338
|
warnings: validation.warnings
|
|
1004
1339
|
};
|
|
1005
1340
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1341
|
+
try {
|
|
1342
|
+
const created = await payload.create({
|
|
1343
|
+
collection: "block-definitions",
|
|
1344
|
+
data: {
|
|
1345
|
+
slug: blockSlug,
|
|
1346
|
+
name,
|
|
1347
|
+
description
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
definitionId = created.id;
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
if (!isUniqueConstraintViolation(err)) throw err;
|
|
1353
|
+
const raced = await payload.find({
|
|
1354
|
+
collection: "block-definitions",
|
|
1355
|
+
where: { slug: { equals: blockSlug } },
|
|
1356
|
+
limit: 1
|
|
1357
|
+
});
|
|
1358
|
+
if (raced.docs.length === 0) throw err;
|
|
1359
|
+
definitionId = raced.docs[0].id;
|
|
1360
|
+
}
|
|
1016
1361
|
}
|
|
1017
|
-
const
|
|
1362
|
+
const latestVersionRes = await payload.find({
|
|
1018
1363
|
collection: "block-definition-versions",
|
|
1019
1364
|
where: { blockDefinition: { equals: definitionId } },
|
|
1020
|
-
limit:
|
|
1365
|
+
limit: 1,
|
|
1366
|
+
sort: "-versionNumber"
|
|
1021
1367
|
});
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1368
|
+
let nextVersionNumber = latestVersionRes.docs.length > 0 ? latestVersionRes.docs[0].versionNumber + 1 : 1;
|
|
1369
|
+
let version = null;
|
|
1370
|
+
let attempts = 0;
|
|
1371
|
+
const MAX_ATTEMPTS = 5;
|
|
1372
|
+
while (attempts < MAX_ATTEMPTS) {
|
|
1373
|
+
attempts++;
|
|
1374
|
+
try {
|
|
1375
|
+
version = await payload.create({
|
|
1376
|
+
collection: "block-definition-versions",
|
|
1377
|
+
data: {
|
|
1378
|
+
blockDefinition: definitionId,
|
|
1379
|
+
versionNumber: nextVersionNumber,
|
|
1380
|
+
label: `v${nextVersionNumber}`,
|
|
1381
|
+
schema,
|
|
1382
|
+
changelog: changelog ?? "Created via Block Builder"
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
break;
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
if (!isUniqueConstraintViolation(err)) throw err;
|
|
1388
|
+
if (attempts >= MAX_ATTEMPTS) {
|
|
1389
|
+
throw new Error(
|
|
1390
|
+
`Failed to create version after ${MAX_ATTEMPTS} attempts due to concurrency conflicts.`
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
const latest = await payload.find({
|
|
1394
|
+
collection: "block-definition-versions",
|
|
1395
|
+
where: { blockDefinition: { equals: definitionId } },
|
|
1396
|
+
limit: 1,
|
|
1397
|
+
sort: "-versionNumber"
|
|
1398
|
+
});
|
|
1399
|
+
const observed = latest.docs.length > 0 ? latest.docs[0].versionNumber : nextVersionNumber;
|
|
1400
|
+
nextVersionNumber = Math.max(observed + 1, nextVersionNumber + 1);
|
|
1032
1401
|
}
|
|
1033
|
-
}
|
|
1402
|
+
}
|
|
1403
|
+
if (!version) {
|
|
1404
|
+
throw new Error("Failed to create version.");
|
|
1405
|
+
}
|
|
1034
1406
|
await payload.update({
|
|
1035
1407
|
collection: "block-definitions",
|
|
1036
1408
|
id: definitionId,
|
|
@@ -1057,10 +1429,7 @@ async function saveSchemaLocally(payload, request) {
|
|
|
1057
1429
|
}
|
|
1058
1430
|
|
|
1059
1431
|
// src/endpoints/save.ts
|
|
1060
|
-
var saveEndpoint = async (req) => {
|
|
1061
|
-
if (!req.user) {
|
|
1062
|
-
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
1063
|
-
}
|
|
1432
|
+
var saveEndpoint = withBuilderGuard(async (req) => {
|
|
1064
1433
|
let body;
|
|
1065
1434
|
try {
|
|
1066
1435
|
if (!req.json) return Response.json({ error: "No JSON parser available" }, { status: 500 });
|
|
@@ -1073,13 +1442,10 @@ var saveEndpoint = async (req) => {
|
|
|
1073
1442
|
}
|
|
1074
1443
|
const result = await saveSchemaLocally(req.payload, body);
|
|
1075
1444
|
return Response.json(result, { status: result.success ? 200 : 422 });
|
|
1076
|
-
};
|
|
1445
|
+
});
|
|
1077
1446
|
|
|
1078
1447
|
// src/endpoints/versions.ts
|
|
1079
|
-
var versionsEndpoint = async (req) => {
|
|
1080
|
-
if (!req.user) {
|
|
1081
|
-
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
1082
|
-
}
|
|
1448
|
+
var versionsEndpoint = withBuilderGuard(async (req) => {
|
|
1083
1449
|
const slug = req.routeParams?.slug;
|
|
1084
1450
|
if (!slug) {
|
|
1085
1451
|
return Response.json({ error: "Slug is required" }, { status: 400 });
|
|
@@ -1094,7 +1460,7 @@ var versionsEndpoint = async (req) => {
|
|
|
1094
1460
|
if (!def) {
|
|
1095
1461
|
return Response.json({ error: `Block definition "${slug}" not found` }, { status: 404 });
|
|
1096
1462
|
}
|
|
1097
|
-
const currentVersionId =
|
|
1463
|
+
const currentVersionId = resolveId(def.currentVersion);
|
|
1098
1464
|
const versionsResult = await req.payload.find({
|
|
1099
1465
|
collection: "block-definition-versions",
|
|
1100
1466
|
where: { blockDefinition: { equals: def.id } },
|
|
@@ -1114,7 +1480,7 @@ var versionsEndpoint = async (req) => {
|
|
|
1114
1480
|
};
|
|
1115
1481
|
});
|
|
1116
1482
|
return Response.json({ versions });
|
|
1117
|
-
};
|
|
1483
|
+
});
|
|
1118
1484
|
|
|
1119
1485
|
// src/plugin.ts
|
|
1120
1486
|
var dynamicBlocksPlugin = (options = {}) => {
|