@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/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
  };
@@ -207,51 +224,117 @@ function dbLayoutField(fieldName = "dbLayout", tabLabel = "DB Layout") {
207
224
  function indent(n) {
208
225
  return " ".repeat(n);
209
226
  }
210
- function escStr(s) {
211
- return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
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;
212
263
  }
213
264
  function fieldToCode(field, depth = 1) {
214
265
  const pad = indent(depth);
215
266
  const innerPad = indent(depth + 1);
216
267
  const lines = [];
217
- lines.push(`${pad}name: '${escStr(field.name)}'`);
218
- lines.push(`${pad}type: '${field.type}'`);
219
- if (field.label) lines.push(`${pad}label: '${escStr(field.label)}'`);
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
+ }
220
275
  if (field.required) lines.push(`${pad}required: true`);
221
276
  if (field.unique) lines.push(`${pad}unique: true`);
222
- if (field.localized) lines.push(`${pad}localized: true`);
277
+ if (field.localized && !NO_LOCALIZED_TYPES.has(field.type)) {
278
+ lines.push(`${pad}localized: true`);
279
+ }
223
280
  if (field.defaultValue !== void 0) {
224
- const val = typeof field.defaultValue === "string" ? `'${escStr(String(field.defaultValue))}'` : field.defaultValue;
281
+ const val = typeof field.defaultValue === "string" ? safeStr(String(field.defaultValue)) : field.defaultValue;
225
282
  lines.push(`${pad}defaultValue: ${val}`);
226
283
  }
227
- if (field.type === "richText") {
284
+ if (field.type === "richtext") {
228
285
  lines.push(`${pad}editor: lexicalEditor({})`);
229
286
  }
230
287
  if (field.options && field.options.length > 0) {
231
- const opts = field.options.map((o) => `{ label: '${escStr(o.label)}', value: '${escStr(o.value)}' }`).join(`, `);
288
+ const opts = field.options.map((o) => `{ label: ${safeStr(o.label)}, value: ${safeStr(o.value)} }`).join(`, `);
232
289
  lines.push(`${pad}options: [${opts}]`);
233
290
  }
234
- if (field.relationTo) {
235
- lines.push(`${pad}relationTo: '${escStr(field.relationTo)}'`);
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)}`);
236
297
  }
237
- if (field.hasMany !== void 0) {
298
+ if (field.type === "multiselect") {
299
+ lines.push(`${pad}hasMany: true`);
300
+ } else if (field.hasMany !== void 0) {
238
301
  lines.push(`${pad}hasMany: ${field.hasMany}`);
239
302
  }
240
303
  if (field.minRows !== void 0) lines.push(`${pad}minRows: ${field.minRows}`);
241
304
  if (field.maxRows !== void 0) lines.push(`${pad}maxRows: ${field.maxRows}`);
242
- if (field.fields && field.fields.length > 0) {
243
- const nested = field.fields.map((f) => `${innerPad}{
244
- ${fieldToCode(f, depth + 2)}
245
- ${innerPad}}`).join(",\n");
246
- lines.push(`${pad}fields: [
247
- ${nested}
248
- ${innerPad}]`);
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
+ );
249
332
  }
250
333
  const adminParts = [];
251
- if (field.admin?.description)
252
- adminParts.push(`description: '${escStr(field.admin.description)}'`);
334
+ if (field.admin?.description && !NO_ADMIN_DESCRIPTION_TYPES.has(field.type))
335
+ adminParts.push(`description: ${safeStr(field.admin.description)}`);
253
336
  if (field.admin?.placeholder)
254
- adminParts.push(`placeholder: '${escStr(field.admin.placeholder)}'`);
337
+ adminParts.push(`placeholder: ${safeStr(field.admin.placeholder)}`);
255
338
  if (field.admin?.readOnly) adminParts.push(`readOnly: true`);
256
339
  if (field.admin?.hidden) adminParts.push(`hidden: true`);
257
340
  if (adminParts.length > 0) {
@@ -265,22 +348,22 @@ function generateBlockCode(block) {
265
348
  if (hasRichText) {
266
349
  imports.push(`import { lexicalEditor } from '@payloadcms/richtext-lexical'`);
267
350
  }
268
- const fieldsCode = block.fields.map((f) => ` {
269
- ${fieldToCode(f, 2)}
270
- }`).join(",\n");
351
+ const fieldsCode = block.fields.map((f) => ` {
352
+ ${fieldToCode(f, 3)}
353
+ }`).join(",\n");
271
354
  const labelsCode = block.labels ? `
272
355
  labels: {
273
- singular: '${escStr(block.labels.singular ?? block.slug)}',
274
- plural: '${escStr(block.labels.plural ?? block.slug + "s")}',
356
+ singular: ${safeStr(block.labels.singular ?? block.slug)},
357
+ plural: ${safeStr(block.labels.plural ?? block.slug + "s")},
275
358
  },` : "";
276
359
  const interfaceLine = block.interfaceName ? `
277
- interfaceName: '${escStr(block.interfaceName)}',` : "";
360
+ interfaceName: ${safeStr(block.interfaceName)},` : "";
278
361
  const exportName = block.interfaceName ?? toCamelCase(block.slug);
279
362
  return [
280
363
  imports.join("\n"),
281
364
  "",
282
365
  `export const ${exportName}: Block = {`,
283
- ` slug: '${escStr(block.slug)}',${interfaceLine}${labelsCode}`,
366
+ ` slug: ${safeStr(block.slug)},${interfaceLine}${labelsCode}`,
284
367
  ` fields: [`,
285
368
  fieldsCode,
286
369
  ` ],`,
@@ -289,15 +372,102 @@ ${fieldToCode(f, 2)}
289
372
  ].join("\n");
290
373
  }
291
374
  function containsRichText(fields) {
292
- return fields.some(
293
- (f) => f.type === "richText" || (f.fields ? containsRichText(f.fields) : false)
294
- );
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
+ });
295
381
  }
296
382
  function toCamelCase(slug) {
297
383
  return slug.split(/[-_]/).map(
298
384
  (part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
299
385
  ).join("");
300
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
+ }
301
471
  function generateBlockOutput(block) {
302
472
  return {
303
473
  filename: `${block.slug}.ts`,
@@ -305,8 +475,10 @@ function generateBlockOutput(block) {
305
475
  language: "typescript"
306
476
  };
307
477
  }
308
- function generateAllBlocks(blocks) {
309
- return blocks.map(generateBlockOutput);
478
+ function generateAllBlocks(blocks, options = {}) {
479
+ return blocks.flatMap(
480
+ (block) => options.react ? [generateBlockOutput(block), generateReactComponent(block)] : [generateBlockOutput(block)]
481
+ );
310
482
  }
311
483
  function generateIndexFile(blocks) {
312
484
  const exportName = (b) => b.interfaceName ?? toCamelCase(b.slug);
@@ -323,185 +495,29 @@ function generateIndexFile(blocks) {
323
495
  return { filename: "index.ts", code, language: "typescript" };
324
496
  }
325
497
 
326
- // src/endpoints/generate.ts
327
- var generateEndpoint = async (req) => {
328
- if (!req.user) {
329
- return Response.json({ error: "Unauthorized" }, { status: 401 });
330
- }
331
- let blocks;
332
- try {
333
- if (!req.json) return Response.json({ error: "No JSON parser available" }, { status: 500 });
334
- const body = await req.json();
335
- blocks = body.blocks ?? [];
336
- } catch {
337
- return Response.json({ error: "Invalid JSON body" }, { status: 400 });
338
- }
339
- const blockOutputs = generateAllBlocks(blocks);
340
- const indexOutput = generateIndexFile(blocks);
341
- const fileMap = {};
342
- for (const out of blockOutputs) {
343
- fileMap[out.filename] = out.code;
344
- }
345
- fileMap[indexOutput.filename] = indexOutput.code;
346
- return Response.json({ files: fileMap });
347
- };
348
-
349
- // src/block-builder/lib/schemaToBuilderBlock.ts
350
- import { v4 as uuidv4 } from "uuid";
351
- var REVERSE_TYPE_MAP = {
352
- richtext: "richText",
353
- image: "upload",
354
- file: "upload",
355
- // file and image both map to upload in the builder
356
- multiselect: "select",
357
- // builder has no multiselect — nearest equivalent
358
- url: "text",
359
- // builder has no url field — falls back to text
360
- color: "text"
361
- // builder has no color field — falls back to text
362
- };
363
- var VALID_BUILDER_TYPES = /* @__PURE__ */ new Set([
364
- "text",
365
- "textarea",
366
- "number",
367
- "email",
368
- "checkbox",
369
- "select",
370
- "radio",
371
- "date",
372
- "richText",
373
- "upload",
374
- "relationship",
375
- "array",
376
- "group",
377
- "tabs",
378
- "row",
379
- "collapsible",
380
- "json",
381
- "code",
382
- "point",
383
- "ui"
384
- ]);
385
- function fieldToBuilderField(raw) {
386
- const rawType = String(raw.type ?? "text");
387
- const mappedType = REVERSE_TYPE_MAP[rawType] ?? rawType;
388
- const isKnown = VALID_BUILDER_TYPES.has(mappedType);
389
- if (!isKnown) {
390
- console.warn(`[block-builder] Unknown field type "${rawType}" \u2014 rendering as "text". Add a mapping in REVERSE_TYPE_MAP.`);
391
- }
392
- const fieldType = isKnown ? mappedType : "text";
393
- const field = {
394
- id: uuidv4(),
395
- type: fieldType,
396
- name: String(raw.name ?? "field"),
397
- label: raw.label ? String(raw.label) : void 0,
398
- required: Boolean(raw.required)
399
- };
400
- if (raw.options && Array.isArray(raw.options)) {
401
- field.options = raw.options.map(
402
- (o) => typeof o === "string" ? { label: o, value: o } : { label: String(o.label), value: String(o.value) }
403
- );
404
- }
405
- if (raw.hasMany !== void 0) field.hasMany = Boolean(raw.hasMany);
406
- if (raw.collection) field.relationTo = String(raw.collection);
407
- if (raw.relationTo) {
408
- field.relationTo = String(raw.relationTo);
409
- }
410
- if (raw.minRows !== void 0) field.minRows = Number(raw.minRows);
411
- if (raw.maxRows !== void 0) field.maxRows = Number(raw.maxRows);
412
- if (raw.fields && Array.isArray(raw.fields)) {
413
- field.fields = raw.fields.map(fieldToBuilderField);
414
- }
415
- if (raw.admin && typeof raw.admin === "object") {
416
- const a = raw.admin;
417
- field.admin = {
418
- description: a.description ? String(a.description) : void 0,
419
- placeholder: a.placeholder ? String(a.placeholder) : void 0,
420
- readOnly: Boolean(a.readOnly),
421
- hidden: Boolean(a.hidden)
422
- };
423
- }
424
- return field;
425
- }
426
- function slugToInterfaceName(slug) {
427
- return slug.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
428
- }
429
- function schemaToBuilderBlock(slug, _name, labels, schemaFields) {
430
- return {
431
- id: uuidv4(),
432
- slug,
433
- interfaceName: slugToInterfaceName(slug),
434
- labels,
435
- 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);
436
516
  };
437
517
  }
438
518
 
439
- // src/endpoints/load.ts
440
- var loadEndpoint = async (req) => {
441
- if (!req.user) {
442
- return Response.json({ error: "Unauthorized" }, { status: 401 });
443
- }
444
- const slug = req.routeParams?.slug;
445
- if (!slug) {
446
- return Response.json({ error: "Slug is required" }, { status: 400 });
447
- }
448
- const requestedVersionId = req.url ? new URL(req.url).searchParams.get("versionId") : null;
449
- const result = await req.payload.find({
450
- collection: "block-definitions",
451
- where: { slug: { equals: slug } },
452
- depth: 2,
453
- limit: 1
454
- });
455
- const def = result.docs[0];
456
- if (!def) {
457
- return Response.json({ error: `Block definition "${slug}" not found` }, { status: 404 });
458
- }
459
- const name = def.name ?? slug;
460
- const currentVersionId = def.currentVersion && typeof def.currentVersion === "object" ? String(def.currentVersion.id) : typeof def.currentVersion === "string" || typeof def.currentVersion === "number" ? String(def.currentVersion) : null;
461
- let version = null;
462
- if (requestedVersionId) {
463
- try {
464
- const v = await req.payload.findByID({
465
- collection: "block-definition-versions",
466
- id: requestedVersionId,
467
- depth: 0
468
- });
469
- version = v;
470
- } catch {
471
- return Response.json({ error: `Version "${requestedVersionId}" not found` }, { status: 404 });
472
- }
473
- } else if (def.currentVersion && typeof def.currentVersion === "object") {
474
- version = def.currentVersion;
475
- } else {
476
- const latestResult = await req.payload.find({
477
- collection: "block-definition-versions",
478
- where: { blockDefinition: { equals: def.id } },
479
- sort: "-versionNumber",
480
- depth: 0,
481
- limit: 1
482
- });
483
- version = latestResult.docs[0] ?? null;
484
- }
485
- if (!version) {
486
- const block2 = schemaToBuilderBlock(slug, name, {}, []);
487
- return Response.json({ block: block2, versionId: null, versionNumber: null, isCurrent: true });
488
- }
489
- const versionId = String(version.id);
490
- const schema = version.schema;
491
- const schemaFields = schema ? Array.isArray(schema) ? schema : schema.fields ?? [] : [];
492
- const labels = version.labels ?? {};
493
- const versionNumber = version.versionNumber;
494
- const block = schemaToBuilderBlock(slug, name, labels, schemaFields);
495
- return Response.json({
496
- block,
497
- versionId,
498
- versionNumber: versionNumber ?? null,
499
- isCurrent: versionId === currentVersionId
500
- });
501
- };
502
-
503
- // src/builder/normalizer.ts
504
- var KNOWN_TYPES = /* @__PURE__ */ new Set([
519
+ // src/validation/schemaValidator.ts
520
+ var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
505
521
  "text",
506
522
  "textarea",
507
523
  "richtext",
@@ -519,230 +535,64 @@ var KNOWN_TYPES = /* @__PURE__ */ new Set([
519
535
  "group",
520
536
  "relationship",
521
537
  "json",
522
- "blocks"
538
+ "blocks",
539
+ "row",
540
+ "tabs",
541
+ "collapsible"
523
542
  ]);
524
- function normaliseOption(opt) {
525
- if (typeof opt === "string") {
526
- return { label: opt, value: opt.toLowerCase().replace(/\s+/g, "-") };
527
- }
528
- if (opt && typeof opt === "object") {
529
- const o = opt;
530
- const value = String(o.value ?? o.label ?? "").toLowerCase().replace(/\s+/g, "-");
531
- const label = String(o.label ?? o.value ?? value);
532
- return { label, value };
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;
533
562
  }
534
- return { label: String(opt), value: String(opt) };
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
+ });
535
580
  }
536
- function normaliseConditions(raw) {
537
- if (!Array.isArray(raw)) return void 0;
538
- const result = [];
539
- for (const item of raw) {
540
- if (item && typeof item === "object" && !Array.isArray(item)) {
541
- const c = item;
542
- if (typeof c.field === "string" && typeof c.operator === "string") {
543
- result.push({ field: c.field, operator: c.operator, value: c.value });
544
- }
545
- }
546
- }
547
- return result.length > 0 ? result : void 0;
548
- }
549
- function normaliseValidation(raw) {
550
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
551
- const v = raw;
552
- const out = {};
553
- if (typeof v.required === "boolean") out.required = v.required;
554
- if (typeof v.minLength === "number") out.minLength = v.minLength;
555
- if (typeof v.maxLength === "number") out.maxLength = v.maxLength;
556
- if (typeof v.regex === "string") out.regex = v.regex;
557
- if (typeof v.min === "number") out.min = v.min;
558
- if (typeof v.max === "number") out.max = v.max;
559
- if (typeof v.step === "number") out.step = v.step;
560
- if (typeof v.integerOnly === "boolean") out.integerOnly = v.integerOnly;
561
- if (typeof v.minRows === "number") out.minRows = v.minRows;
562
- if (typeof v.maxRows === "number") out.maxRows = v.maxRows;
563
- if (typeof v.uniqueItems === "boolean") out.uniqueItems = v.uniqueItems;
564
- if (Array.isArray(v.allowedMimeTypes)) out.allowedMimeTypes = v.allowedMimeTypes;
565
- if (typeof v.maxFileSize === "number") out.maxFileSize = v.maxFileSize;
566
- if (typeof v.maxSelections === "number") out.maxSelections = v.maxSelections;
567
- return Object.keys(out).length > 0 ? out : void 0;
568
- }
569
- function normaliseUI(raw) {
570
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
571
- const u = raw;
572
- const out = {};
573
- if (typeof u.tab === "string") out.tab = u.tab;
574
- if (typeof u.section === "string") out.section = u.section;
575
- if (["full", "half", "third", "quarter"].includes(u.width)) {
576
- out.width = u.width;
577
- }
578
- if (typeof u.collapsed === "boolean") out.collapsed = u.collapsed;
579
- if (typeof u.order === "number") out.order = u.order;
580
- return Object.keys(out).length > 0 ? out : void 0;
581
- }
582
- function normaliseField(raw) {
583
- const type = raw.type ?? "text";
584
- const resolvedType = KNOWN_TYPES.has(type) ? type : "text";
585
- const base = {
586
- name: String(raw.name ?? "").trim(),
587
- type: resolvedType
588
- };
589
- if (raw.label) base.label = String(raw.label);
590
- if (raw.required !== void 0) base.required = Boolean(raw.required);
591
- if (raw.admin && typeof raw.admin === "object") base.admin = raw.admin;
592
- const conditions = normaliseConditions(raw.conditions);
593
- if (conditions) base.conditions = conditions;
594
- if (raw.conditionMode === "AND" || raw.conditionMode === "OR") {
595
- base.conditionMode = raw.conditionMode;
596
- }
597
- const validation = normaliseValidation(raw.validation);
598
- if (validation) base.validation = validation;
599
- const ui = normaliseUI(raw.ui);
600
- if (ui) base.ui = ui;
601
- switch (resolvedType) {
602
- case "text":
603
- case "textarea": {
604
- if (raw.minLength !== void 0) base.minLength = Number(raw.minLength);
605
- if (raw.maxLength !== void 0) base.maxLength = Number(raw.maxLength);
606
- if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
607
- break;
608
- }
609
- case "number": {
610
- if (raw.min !== void 0) base.min = Number(raw.min);
611
- if (raw.max !== void 0) base.max = Number(raw.max);
612
- if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
613
- break;
614
- }
615
- case "checkbox": {
616
- if (raw.defaultValue !== void 0) base.defaultValue = Boolean(raw.defaultValue);
617
- break;
618
- }
619
- case "select":
620
- case "multiselect": {
621
- const rawOpts = Array.isArray(raw.options) ? raw.options : [];
622
- base.options = rawOpts.map(normaliseOption);
623
- if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
624
- break;
625
- }
626
- case "date": {
627
- if (raw.timeFormat !== void 0) base.timeFormat = Boolean(raw.timeFormat);
628
- break;
629
- }
630
- case "array": {
631
- const subFields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
632
- base.fields = subFields;
633
- if (raw.minRows !== void 0) base.minRows = Number(raw.minRows);
634
- if (raw.maxRows !== void 0) base.maxRows = Number(raw.maxRows);
635
- break;
636
- }
637
- case "group": {
638
- const subFields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
639
- base.fields = subFields;
640
- break;
641
- }
642
- case "relationship": {
643
- if (raw.collection) base.collection = String(raw.collection);
644
- if (raw.hasMany !== void 0) base.hasMany = Boolean(raw.hasMany);
645
- break;
646
- }
647
- case "file": {
648
- if (Array.isArray(raw.allowedMimeTypes)) base.allowedMimeTypes = raw.allowedMimeTypes;
649
- break;
650
- }
651
- case "blocks": {
652
- if (Array.isArray(raw.allowedBlocks)) {
653
- base.allowedBlocks = raw.allowedBlocks.map(String).filter(Boolean);
654
- }
655
- if (raw.minBlocks !== void 0) base.minBlocks = Number(raw.minBlocks);
656
- if (raw.maxBlocks !== void 0) base.maxBlocks = Number(raw.maxBlocks);
657
- break;
658
- }
659
- }
660
- return base;
661
- }
662
- function normaliseSchema(raw) {
663
- const fields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
664
- const schema = { fields };
665
- if (raw.layout === "sidebar" || raw.layout === "tabs") {
666
- schema.layout = raw.layout;
667
- } else {
668
- schema.layout = "default";
669
- }
670
- return schema;
671
- }
672
-
673
- // src/validation/schemaValidator.ts
674
- var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
675
- "text",
676
- "textarea",
677
- "richtext",
678
- "number",
679
- "checkbox",
680
- "select",
681
- "multiselect",
682
- "date",
683
- "image",
684
- "file",
685
- "url",
686
- "email",
687
- "color",
688
- "array",
689
- "group",
690
- "relationship",
691
- "json",
692
- "blocks"
693
- ]);
694
- var CONTAINER_TYPES = /* @__PURE__ */ new Set(["array", "group"]);
695
- var LEAF_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
696
- var VALID_CONDITION_OPERATORS = /* @__PURE__ */ new Set([
697
- "equals",
698
- "not_equals",
699
- "contains",
700
- "not_contains",
701
- "greater_than",
702
- "less_than",
703
- "in",
704
- "not_in",
705
- "exists",
706
- "empty"
707
- ]);
708
- function validateConditions(conditions, path, errors) {
709
- if (!Array.isArray(conditions)) {
710
- errors.push(`${path}: "conditions" must be an array.`);
711
- return;
712
- }
713
- ;
714
- conditions.forEach((cond, i) => {
715
- const cp = `${path}[${i}]`;
716
- if (!cond || typeof cond !== "object") {
717
- errors.push(`${cp}: condition must be an object.`);
718
- return;
719
- }
720
- const c = cond;
721
- if (typeof c.field !== "string" || !c.field.trim()) {
722
- errors.push(`${cp}: "field" must be a non-empty string.`);
723
- }
724
- if (typeof c.operator !== "string" || !VALID_CONDITION_OPERATORS.has(c.operator)) {
725
- errors.push(
726
- `${cp}: "operator" must be one of: ${[...VALID_CONDITION_OPERATORS].join(", ")}.`
727
- );
728
- }
729
- });
730
- }
731
- function validateValidationRules(v, path, errors) {
732
- const numericProps = [
733
- "minLength",
734
- "maxLength",
735
- "min",
736
- "max",
737
- "step",
738
- "minRows",
739
- "maxRows",
740
- "maxFileSize",
741
- "maxSelections"
742
- ];
743
- for (const prop of numericProps) {
744
- if (v[prop] !== void 0 && typeof v[prop] !== "number") {
745
- 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.`);
746
596
  }
747
597
  }
748
598
  if (typeof v.minLength === "number" && typeof v.maxLength === "number" && v.minLength > v.maxLength) {
@@ -775,7 +625,7 @@ function validateValidationRules(v, path, errors) {
775
625
  errors.push(`${path}.allowedMimeTypes: must be an array of strings.`);
776
626
  }
777
627
  }
778
- function validateField(field, path, errors, warnings) {
628
+ function validateField(field, path, errors, warnings, reserved = [], names = /* @__PURE__ */ new Set()) {
779
629
  if (!field || typeof field !== "object" || Array.isArray(field)) {
780
630
  errors.push(`${path}: must be a non-array object.`);
781
631
  return;
@@ -886,7 +736,16 @@ function validateField(field, path, errors, warnings) {
886
736
  if (!Array.isArray(f.fields) || f.fields.length === 0) {
887
737
  errors.push(`${path}: "${type}" fields must have a non-empty "fields" array.`);
888
738
  } else {
889
- validateFields(f.fields, `${path}.fields`, errors, warnings);
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
+ );
890
749
  }
891
750
  if (type === "array") {
892
751
  if (f.minRows !== void 0 && typeof f.minRows !== "number") {
@@ -920,18 +779,63 @@ function validateField(field, path, errors, warnings) {
920
779
  errors.push(`${path}: "minBlocks" (${f.minBlocks}) must be <= "maxBlocks" (${f.maxBlocks}).`);
921
780
  }
922
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);
923
831
  }
924
- function validateFields(fields, path, errors, warnings) {
925
- const names = /* @__PURE__ */ new Set();
832
+ function validateFields(fields, path, errors, warnings, reserved = [], names = /* @__PURE__ */ new Set()) {
926
833
  fields.forEach((field, index) => {
927
834
  const fieldPath = `${path}[${index}]`;
928
- validateField(field, fieldPath, errors, warnings);
835
+ validateField(field, fieldPath, errors, warnings, reserved, names);
929
836
  const f = field;
930
- if (typeof f.name === "string" && f.name) {
931
- if (names.has(f.name)) {
932
- errors.push(`${path}: duplicate field name "${f.name}".`);
933
- }
934
- 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);
935
839
  }
936
840
  });
937
841
  }
@@ -951,7 +855,7 @@ function validateBlockSchema(schema) {
951
855
  } else if (s.fields.length === 0) {
952
856
  warnings.push("Schema has no fields defined.");
953
857
  } else {
954
- validateFields(s.fields, "schema.fields", errors, warnings);
858
+ validateFields(s.fields, "schema.fields", errors, warnings, BLOCK_RESERVED_NAMES);
955
859
  }
956
860
  if (s.layout !== void 0 && !["default", "sidebar", "tabs"].includes(s.layout)) {
957
861
  errors.push(`schema.layout must be one of: "default", "sidebar", "tabs".`);
@@ -959,50 +863,468 @@ function validateBlockSchema(schema) {
959
863
  return { valid: errors.length === 0, errors, warnings };
960
864
  }
961
865
 
962
- // src/builder/saveSchema.ts
963
- async function saveSchemaLocally(payload, request) {
964
- const { blockSlug, name, description, category, schema: rawSchema, changelog } = request;
965
- const schema = normaliseSchema(rawSchema);
966
- if (!schema.fields?.length) {
967
- return {
968
- success: false,
969
- definitionId: "",
970
- versionId: "",
971
- versionNumber: 0,
972
- errors: [`Block "${blockSlug}" schema has no fields. Schemas must be imported from a server-safe module (not a "use client" file).`],
973
- warnings: []
974
- };
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 });
975
883
  }
976
- const validation = validateBlockSchema(schema);
977
- if (!validation.valid) {
978
- return {
979
- success: false,
980
- definitionId: "",
981
- versionId: "",
982
- versionNumber: 0,
983
- errors: validation.errors,
984
- warnings: validation.warnings
985
- };
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 });
986
900
  }
987
- try {
988
- let definitionId;
989
- const existing = await payload.find({
990
- collection: "block-definitions",
991
- where: { slug: { equals: blockSlug } },
992
- limit: 1
993
- });
994
- if (existing.docs.length > 0) {
995
- definitionId = existing.docs[0].id;
996
- const updates = {};
997
- if (description !== void 0) updates.description = description;
998
- if (category !== void 0) updates.category = category;
999
- if (name !== void 0) updates.name = name;
1000
- if (Object.keys(updates).length > 0) {
1001
- await payload.update({
1002
- collection: "block-definitions",
1003
- id: definitionId,
1004
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1005
- data: updates
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,
1327
+ data: updates
1006
1328
  });
1007
1329
  }
1008
1330
  } else {
@@ -1016,34 +1338,71 @@ async function saveSchemaLocally(payload, request) {
1016
1338
  warnings: validation.warnings
1017
1339
  };
1018
1340
  }
1019
- const created = await payload.create({
1020
- collection: "block-definitions",
1021
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1022
- data: {
1023
- slug: blockSlug,
1024
- name,
1025
- description
1026
- }
1027
- });
1028
- definitionId = created.id;
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
+ }
1029
1361
  }
1030
- const existingVersions = await payload.find({
1362
+ const latestVersionRes = await payload.find({
1031
1363
  collection: "block-definition-versions",
1032
1364
  where: { blockDefinition: { equals: definitionId } },
1033
- limit: 0
1365
+ limit: 1,
1366
+ sort: "-versionNumber"
1034
1367
  });
1035
- const versionNumber = existingVersions.totalDocs + 1;
1036
- const version = await payload.create({
1037
- collection: "block-definition-versions",
1038
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1039
- data: {
1040
- blockDefinition: definitionId,
1041
- versionNumber,
1042
- label: `v${versionNumber}`,
1043
- schema,
1044
- changelog: changelog ?? "Created via Block Builder"
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);
1045
1401
  }
1046
- });
1402
+ }
1403
+ if (!version) {
1404
+ throw new Error("Failed to create version.");
1405
+ }
1047
1406
  await payload.update({
1048
1407
  collection: "block-definitions",
1049
1408
  id: definitionId,
@@ -1070,10 +1429,7 @@ async function saveSchemaLocally(payload, request) {
1070
1429
  }
1071
1430
 
1072
1431
  // src/endpoints/save.ts
1073
- var saveEndpoint = async (req) => {
1074
- if (!req.user) {
1075
- return Response.json({ error: "Unauthorized" }, { status: 401 });
1076
- }
1432
+ var saveEndpoint = withBuilderGuard(async (req) => {
1077
1433
  let body;
1078
1434
  try {
1079
1435
  if (!req.json) return Response.json({ error: "No JSON parser available" }, { status: 500 });
@@ -1086,13 +1442,10 @@ var saveEndpoint = async (req) => {
1086
1442
  }
1087
1443
  const result = await saveSchemaLocally(req.payload, body);
1088
1444
  return Response.json(result, { status: result.success ? 200 : 422 });
1089
- };
1445
+ });
1090
1446
 
1091
1447
  // src/endpoints/versions.ts
1092
- var versionsEndpoint = async (req) => {
1093
- if (!req.user) {
1094
- return Response.json({ error: "Unauthorized" }, { status: 401 });
1095
- }
1448
+ var versionsEndpoint = withBuilderGuard(async (req) => {
1096
1449
  const slug = req.routeParams?.slug;
1097
1450
  if (!slug) {
1098
1451
  return Response.json({ error: "Slug is required" }, { status: 400 });
@@ -1107,7 +1460,7 @@ var versionsEndpoint = async (req) => {
1107
1460
  if (!def) {
1108
1461
  return Response.json({ error: `Block definition "${slug}" not found` }, { status: 404 });
1109
1462
  }
1110
- const currentVersionId = def.currentVersion && typeof def.currentVersion === "object" ? String(def.currentVersion.id) : typeof def.currentVersion === "string" || typeof def.currentVersion === "number" ? String(def.currentVersion) : null;
1463
+ const currentVersionId = resolveId(def.currentVersion);
1111
1464
  const versionsResult = await req.payload.find({
1112
1465
  collection: "block-definition-versions",
1113
1466
  where: { blockDefinition: { equals: def.id } },
@@ -1127,7 +1480,7 @@ var versionsEndpoint = async (req) => {
1127
1480
  };
1128
1481
  });
1129
1482
  return Response.json({ versions });
1130
- };
1483
+ });
1131
1484
 
1132
1485
  // src/plugin.ts
1133
1486
  var dynamicBlocksPlugin = (options = {}) => {