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