@nextbridgehq/payload-block-builder 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  };
@@ -164,13 +182,26 @@ function dbLayoutField(fieldName = "dbLayout", tabLabel = "DB Layout") {
164
182
  type: "relationship",
165
183
  relationTo: "block-definitions",
166
184
  required: true,
167
- admin: { description: "Which block type to use.", width: "50%" }
185
+ admin: {
186
+ description: "Which block type to use.",
187
+ width: "50%",
188
+ components: {
189
+ afterInput: ["@nextbridgehq/payload-block-builder/client#BlockVersionSync"]
190
+ }
191
+ }
168
192
  },
169
193
  {
170
194
  name: "blockVersion",
171
195
  type: "relationship",
172
196
  relationTo: "block-definition-versions",
173
197
  required: true,
198
+ filterOptions: ({ siblingData }) => {
199
+ const def = siblingData?.blockDefinition;
200
+ if (!def) return false;
201
+ const defId = def && typeof def === "object" ? def.id : def;
202
+ if (!defId) return false;
203
+ return { blockDefinition: { equals: defId } };
204
+ },
174
205
  admin: { description: "Which schema version to use.", width: "50%" }
175
206
  }
176
207
  ]
@@ -222,51 +253,117 @@ function dbLayoutField(fieldName = "dbLayout", tabLabel = "DB Layout") {
222
253
  function indent(n) {
223
254
  return " ".repeat(n);
224
255
  }
225
- function escStr(s) {
226
- return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
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;
227
292
  }
228
293
  function fieldToCode(field, depth = 1) {
229
294
  const pad = indent(depth);
230
295
  const innerPad = indent(depth + 1);
231
296
  const lines = [];
232
- lines.push(`${pad}name: '${escStr(field.name)}'`);
233
- lines.push(`${pad}type: '${field.type}'`);
234
- if (field.label) lines.push(`${pad}label: '${escStr(field.label)}'`);
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
+ }
235
304
  if (field.required) lines.push(`${pad}required: true`);
236
305
  if (field.unique) lines.push(`${pad}unique: true`);
237
- if (field.localized) lines.push(`${pad}localized: true`);
306
+ if (field.localized && !NO_LOCALIZED_TYPES.has(field.type)) {
307
+ lines.push(`${pad}localized: true`);
308
+ }
238
309
  if (field.defaultValue !== void 0) {
239
- const val = typeof field.defaultValue === "string" ? `'${escStr(String(field.defaultValue))}'` : field.defaultValue;
310
+ const val = typeof field.defaultValue === "string" ? safeStr(String(field.defaultValue)) : field.defaultValue;
240
311
  lines.push(`${pad}defaultValue: ${val}`);
241
312
  }
242
- if (field.type === "richText") {
313
+ if (field.type === "richtext") {
243
314
  lines.push(`${pad}editor: lexicalEditor({})`);
244
315
  }
245
316
  if (field.options && field.options.length > 0) {
246
- const opts = field.options.map((o) => `{ label: '${escStr(o.label)}', value: '${escStr(o.value)}' }`).join(`, `);
317
+ const opts = field.options.map((o) => `{ label: ${safeStr(o.label)}, value: ${safeStr(o.value)} }`).join(`, `);
247
318
  lines.push(`${pad}options: [${opts}]`);
248
319
  }
249
- if (field.relationTo) {
250
- lines.push(`${pad}relationTo: '${escStr(field.relationTo)}'`);
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)}`);
251
326
  }
252
- if (field.hasMany !== void 0) {
327
+ if (field.type === "multiselect") {
328
+ lines.push(`${pad}hasMany: true`);
329
+ } else if (field.hasMany !== void 0) {
253
330
  lines.push(`${pad}hasMany: ${field.hasMany}`);
254
331
  }
255
332
  if (field.minRows !== void 0) lines.push(`${pad}minRows: ${field.minRows}`);
256
333
  if (field.maxRows !== void 0) lines.push(`${pad}maxRows: ${field.maxRows}`);
257
- if (field.fields && field.fields.length > 0) {
258
- const nested = field.fields.map((f) => `${innerPad}{
259
- ${fieldToCode(f, depth + 2)}
260
- ${innerPad}}`).join(",\n");
261
- lines.push(`${pad}fields: [
262
- ${nested}
263
- ${innerPad}]`);
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
+ );
264
361
  }
265
362
  const adminParts = [];
266
- if (field.admin?.description)
267
- adminParts.push(`description: '${escStr(field.admin.description)}'`);
363
+ if (field.admin?.description && !NO_ADMIN_DESCRIPTION_TYPES.has(field.type))
364
+ adminParts.push(`description: ${safeStr(field.admin.description)}`);
268
365
  if (field.admin?.placeholder)
269
- adminParts.push(`placeholder: '${escStr(field.admin.placeholder)}'`);
366
+ adminParts.push(`placeholder: ${safeStr(field.admin.placeholder)}`);
270
367
  if (field.admin?.readOnly) adminParts.push(`readOnly: true`);
271
368
  if (field.admin?.hidden) adminParts.push(`hidden: true`);
272
369
  if (adminParts.length > 0) {
@@ -280,22 +377,22 @@ function generateBlockCode(block) {
280
377
  if (hasRichText) {
281
378
  imports.push(`import { lexicalEditor } from '@payloadcms/richtext-lexical'`);
282
379
  }
283
- const fieldsCode = block.fields.map((f) => ` {
284
- ${fieldToCode(f, 2)}
285
- }`).join(",\n");
380
+ const fieldsCode = block.fields.map((f) => ` {
381
+ ${fieldToCode(f, 3)}
382
+ }`).join(",\n");
286
383
  const labelsCode = block.labels ? `
287
384
  labels: {
288
- singular: '${escStr(block.labels.singular ?? block.slug)}',
289
- plural: '${escStr(block.labels.plural ?? block.slug + "s")}',
385
+ singular: ${safeStr(block.labels.singular ?? block.slug)},
386
+ plural: ${safeStr(block.labels.plural ?? block.slug + "s")},
290
387
  },` : "";
291
388
  const interfaceLine = block.interfaceName ? `
292
- interfaceName: '${escStr(block.interfaceName)}',` : "";
389
+ interfaceName: ${safeStr(block.interfaceName)},` : "";
293
390
  const exportName = block.interfaceName ?? toCamelCase(block.slug);
294
391
  return [
295
392
  imports.join("\n"),
296
393
  "",
297
394
  `export const ${exportName}: Block = {`,
298
- ` slug: '${escStr(block.slug)}',${interfaceLine}${labelsCode}`,
395
+ ` slug: ${safeStr(block.slug)},${interfaceLine}${labelsCode}`,
299
396
  ` fields: [`,
300
397
  fieldsCode,
301
398
  ` ],`,
@@ -304,15 +401,102 @@ ${fieldToCode(f, 2)}
304
401
  ].join("\n");
305
402
  }
306
403
  function containsRichText(fields) {
307
- return fields.some(
308
- (f) => f.type === "richText" || (f.fields ? containsRichText(f.fields) : false)
309
- );
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
+ });
310
410
  }
311
411
  function toCamelCase(slug) {
312
412
  return slug.split(/[-_]/).map(
313
413
  (part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
314
414
  ).join("");
315
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
+ }
316
500
  function generateBlockOutput(block) {
317
501
  return {
318
502
  filename: `${block.slug}.ts`,
@@ -320,8 +504,10 @@ function generateBlockOutput(block) {
320
504
  language: "typescript"
321
505
  };
322
506
  }
323
- function generateAllBlocks(blocks) {
324
- return blocks.map(generateBlockOutput);
507
+ function generateAllBlocks(blocks, options = {}) {
508
+ return blocks.flatMap(
509
+ (block) => options.react ? [generateBlockOutput(block), generateReactComponent(block)] : [generateBlockOutput(block)]
510
+ );
325
511
  }
326
512
  function generateIndexFile(blocks) {
327
513
  const exportName = (b) => b.interfaceName ?? toCamelCase(b.slug);
@@ -338,185 +524,29 @@ function generateIndexFile(blocks) {
338
524
  return { filename: "index.ts", code, language: "typescript" };
339
525
  }
340
526
 
341
- // src/endpoints/generate.ts
342
- var generateEndpoint = async (req) => {
343
- if (!req.user) {
344
- return Response.json({ error: "Unauthorized" }, { status: 401 });
345
- }
346
- let blocks;
347
- try {
348
- if (!req.json) return Response.json({ error: "No JSON parser available" }, { status: 500 });
349
- const body = await req.json();
350
- blocks = body.blocks ?? [];
351
- } catch {
352
- return Response.json({ error: "Invalid JSON body" }, { status: 400 });
353
- }
354
- const blockOutputs = generateAllBlocks(blocks);
355
- const indexOutput = generateIndexFile(blocks);
356
- const fileMap = {};
357
- for (const out of blockOutputs) {
358
- fileMap[out.filename] = out.code;
359
- }
360
- fileMap[indexOutput.filename] = indexOutput.code;
361
- return Response.json({ files: fileMap });
362
- };
363
-
364
- // src/block-builder/lib/schemaToBuilderBlock.ts
365
- var import_uuid = require("uuid");
366
- var REVERSE_TYPE_MAP = {
367
- richtext: "richText",
368
- image: "upload",
369
- file: "upload",
370
- // file and image both map to upload in the builder
371
- multiselect: "select",
372
- // builder has no multiselect — nearest equivalent
373
- url: "text",
374
- // builder has no url field — falls back to text
375
- color: "text"
376
- // builder has no color field — falls back to text
377
- };
378
- var VALID_BUILDER_TYPES = /* @__PURE__ */ new Set([
379
- "text",
380
- "textarea",
381
- "number",
382
- "email",
383
- "checkbox",
384
- "select",
385
- "radio",
386
- "date",
387
- "richText",
388
- "upload",
389
- "relationship",
390
- "array",
391
- "group",
392
- "tabs",
393
- "row",
394
- "collapsible",
395
- "json",
396
- "code",
397
- "point",
398
- "ui"
399
- ]);
400
- function fieldToBuilderField(raw) {
401
- const rawType = String(raw.type ?? "text");
402
- const mappedType = REVERSE_TYPE_MAP[rawType] ?? rawType;
403
- const isKnown = VALID_BUILDER_TYPES.has(mappedType);
404
- if (!isKnown) {
405
- console.warn(`[block-builder] Unknown field type "${rawType}" \u2014 rendering as "text". Add a mapping in REVERSE_TYPE_MAP.`);
406
- }
407
- const fieldType = isKnown ? mappedType : "text";
408
- const field = {
409
- id: (0, import_uuid.v4)(),
410
- type: fieldType,
411
- name: String(raw.name ?? "field"),
412
- label: raw.label ? String(raw.label) : void 0,
413
- required: Boolean(raw.required)
414
- };
415
- if (raw.options && Array.isArray(raw.options)) {
416
- field.options = raw.options.map(
417
- (o) => typeof o === "string" ? { label: o, value: o } : { label: String(o.label), value: String(o.value) }
418
- );
419
- }
420
- if (raw.hasMany !== void 0) field.hasMany = Boolean(raw.hasMany);
421
- if (raw.collection) field.relationTo = String(raw.collection);
422
- if (raw.relationTo) {
423
- field.relationTo = String(raw.relationTo);
424
- }
425
- if (raw.minRows !== void 0) field.minRows = Number(raw.minRows);
426
- if (raw.maxRows !== void 0) field.maxRows = Number(raw.maxRows);
427
- if (raw.fields && Array.isArray(raw.fields)) {
428
- field.fields = raw.fields.map(fieldToBuilderField);
429
- }
430
- if (raw.admin && typeof raw.admin === "object") {
431
- const a = raw.admin;
432
- field.admin = {
433
- description: a.description ? String(a.description) : void 0,
434
- placeholder: a.placeholder ? String(a.placeholder) : void 0,
435
- readOnly: Boolean(a.readOnly),
436
- hidden: Boolean(a.hidden)
437
- };
438
- }
439
- return field;
440
- }
441
- function slugToInterfaceName(slug) {
442
- return slug.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
443
- }
444
- function schemaToBuilderBlock(slug, _name, labels, schemaFields) {
445
- return {
446
- id: (0, import_uuid.v4)(),
447
- slug,
448
- interfaceName: slugToInterfaceName(slug),
449
- labels,
450
- 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);
451
545
  };
452
546
  }
453
547
 
454
- // src/endpoints/load.ts
455
- var loadEndpoint = async (req) => {
456
- if (!req.user) {
457
- return Response.json({ error: "Unauthorized" }, { status: 401 });
458
- }
459
- const slug = req.routeParams?.slug;
460
- if (!slug) {
461
- return Response.json({ error: "Slug is required" }, { status: 400 });
462
- }
463
- const requestedVersionId = req.url ? new URL(req.url).searchParams.get("versionId") : null;
464
- const result = await req.payload.find({
465
- collection: "block-definitions",
466
- where: { slug: { equals: slug } },
467
- depth: 2,
468
- limit: 1
469
- });
470
- const def = result.docs[0];
471
- if (!def) {
472
- return Response.json({ error: `Block definition "${slug}" not found` }, { status: 404 });
473
- }
474
- const name = def.name ?? slug;
475
- const currentVersionId = def.currentVersion && typeof def.currentVersion === "object" ? String(def.currentVersion.id) : typeof def.currentVersion === "string" || typeof def.currentVersion === "number" ? String(def.currentVersion) : null;
476
- let version = null;
477
- if (requestedVersionId) {
478
- try {
479
- const v = await req.payload.findByID({
480
- collection: "block-definition-versions",
481
- id: requestedVersionId,
482
- depth: 0
483
- });
484
- version = v;
485
- } catch {
486
- return Response.json({ error: `Version "${requestedVersionId}" not found` }, { status: 404 });
487
- }
488
- } else if (def.currentVersion && typeof def.currentVersion === "object") {
489
- version = def.currentVersion;
490
- } else {
491
- const latestResult = await req.payload.find({
492
- collection: "block-definition-versions",
493
- where: { blockDefinition: { equals: def.id } },
494
- sort: "-versionNumber",
495
- depth: 0,
496
- limit: 1
497
- });
498
- version = latestResult.docs[0] ?? null;
499
- }
500
- if (!version) {
501
- const block2 = schemaToBuilderBlock(slug, name, {}, []);
502
- return Response.json({ block: block2, versionId: null, versionNumber: null, isCurrent: true });
503
- }
504
- const versionId = String(version.id);
505
- const schema = version.schema;
506
- const schemaFields = schema ? Array.isArray(schema) ? schema : schema.fields ?? [] : [];
507
- const labels = version.labels ?? {};
508
- const versionNumber = version.versionNumber;
509
- const block = schemaToBuilderBlock(slug, name, labels, schemaFields);
510
- return Response.json({
511
- block,
512
- versionId,
513
- versionNumber: versionNumber ?? null,
514
- isCurrent: versionId === currentVersionId
515
- });
516
- };
517
-
518
- // src/builder/normalizer.ts
519
- var KNOWN_TYPES = /* @__PURE__ */ new Set([
548
+ // src/validation/schemaValidator.ts
549
+ var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
520
550
  "text",
521
551
  "textarea",
522
552
  "richtext",
@@ -534,230 +564,64 @@ var KNOWN_TYPES = /* @__PURE__ */ new Set([
534
564
  "group",
535
565
  "relationship",
536
566
  "json",
537
- "blocks"
567
+ "blocks",
568
+ "row",
569
+ "tabs",
570
+ "collapsible"
538
571
  ]);
539
- function normaliseOption(opt) {
540
- if (typeof opt === "string") {
541
- return { label: opt, value: opt.toLowerCase().replace(/\s+/g, "-") };
542
- }
543
- if (opt && typeof opt === "object") {
544
- const o = opt;
545
- const value = String(o.value ?? o.label ?? "").toLowerCase().replace(/\s+/g, "-");
546
- const label = String(o.label ?? o.value ?? value);
547
- return { label, value };
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;
548
591
  }
549
- return { label: String(opt), value: String(opt) };
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
+ });
550
609
  }
551
- function normaliseConditions(raw) {
552
- if (!Array.isArray(raw)) return void 0;
553
- const result = [];
554
- for (const item of raw) {
555
- if (item && typeof item === "object" && !Array.isArray(item)) {
556
- const c = item;
557
- if (typeof c.field === "string" && typeof c.operator === "string") {
558
- result.push({ field: c.field, operator: c.operator, value: c.value });
559
- }
560
- }
561
- }
562
- return result.length > 0 ? result : void 0;
563
- }
564
- function normaliseValidation(raw) {
565
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
566
- const v = raw;
567
- const out = {};
568
- if (typeof v.required === "boolean") out.required = v.required;
569
- if (typeof v.minLength === "number") out.minLength = v.minLength;
570
- if (typeof v.maxLength === "number") out.maxLength = v.maxLength;
571
- if (typeof v.regex === "string") out.regex = v.regex;
572
- if (typeof v.min === "number") out.min = v.min;
573
- if (typeof v.max === "number") out.max = v.max;
574
- if (typeof v.step === "number") out.step = v.step;
575
- if (typeof v.integerOnly === "boolean") out.integerOnly = v.integerOnly;
576
- if (typeof v.minRows === "number") out.minRows = v.minRows;
577
- if (typeof v.maxRows === "number") out.maxRows = v.maxRows;
578
- if (typeof v.uniqueItems === "boolean") out.uniqueItems = v.uniqueItems;
579
- if (Array.isArray(v.allowedMimeTypes)) out.allowedMimeTypes = v.allowedMimeTypes;
580
- if (typeof v.maxFileSize === "number") out.maxFileSize = v.maxFileSize;
581
- if (typeof v.maxSelections === "number") out.maxSelections = v.maxSelections;
582
- return Object.keys(out).length > 0 ? out : void 0;
583
- }
584
- function normaliseUI(raw) {
585
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
586
- const u = raw;
587
- const out = {};
588
- if (typeof u.tab === "string") out.tab = u.tab;
589
- if (typeof u.section === "string") out.section = u.section;
590
- if (["full", "half", "third", "quarter"].includes(u.width)) {
591
- out.width = u.width;
592
- }
593
- if (typeof u.collapsed === "boolean") out.collapsed = u.collapsed;
594
- if (typeof u.order === "number") out.order = u.order;
595
- return Object.keys(out).length > 0 ? out : void 0;
596
- }
597
- function normaliseField(raw) {
598
- const type = raw.type ?? "text";
599
- const resolvedType = KNOWN_TYPES.has(type) ? type : "text";
600
- const base = {
601
- name: String(raw.name ?? "").trim(),
602
- type: resolvedType
603
- };
604
- if (raw.label) base.label = String(raw.label);
605
- if (raw.required !== void 0) base.required = Boolean(raw.required);
606
- if (raw.admin && typeof raw.admin === "object") base.admin = raw.admin;
607
- const conditions = normaliseConditions(raw.conditions);
608
- if (conditions) base.conditions = conditions;
609
- if (raw.conditionMode === "AND" || raw.conditionMode === "OR") {
610
- base.conditionMode = raw.conditionMode;
611
- }
612
- const validation = normaliseValidation(raw.validation);
613
- if (validation) base.validation = validation;
614
- const ui = normaliseUI(raw.ui);
615
- if (ui) base.ui = ui;
616
- switch (resolvedType) {
617
- case "text":
618
- case "textarea": {
619
- if (raw.minLength !== void 0) base.minLength = Number(raw.minLength);
620
- if (raw.maxLength !== void 0) base.maxLength = Number(raw.maxLength);
621
- if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
622
- break;
623
- }
624
- case "number": {
625
- if (raw.min !== void 0) base.min = Number(raw.min);
626
- if (raw.max !== void 0) base.max = Number(raw.max);
627
- if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
628
- break;
629
- }
630
- case "checkbox": {
631
- if (raw.defaultValue !== void 0) base.defaultValue = Boolean(raw.defaultValue);
632
- break;
633
- }
634
- case "select":
635
- case "multiselect": {
636
- const rawOpts = Array.isArray(raw.options) ? raw.options : [];
637
- base.options = rawOpts.map(normaliseOption);
638
- if (raw.defaultValue !== void 0) base.defaultValue = raw.defaultValue;
639
- break;
640
- }
641
- case "date": {
642
- if (raw.timeFormat !== void 0) base.timeFormat = Boolean(raw.timeFormat);
643
- break;
644
- }
645
- case "array": {
646
- const subFields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
647
- base.fields = subFields;
648
- if (raw.minRows !== void 0) base.minRows = Number(raw.minRows);
649
- if (raw.maxRows !== void 0) base.maxRows = Number(raw.maxRows);
650
- break;
651
- }
652
- case "group": {
653
- const subFields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
654
- base.fields = subFields;
655
- break;
656
- }
657
- case "relationship": {
658
- if (raw.collection) base.collection = String(raw.collection);
659
- if (raw.hasMany !== void 0) base.hasMany = Boolean(raw.hasMany);
660
- break;
661
- }
662
- case "file": {
663
- if (Array.isArray(raw.allowedMimeTypes)) base.allowedMimeTypes = raw.allowedMimeTypes;
664
- break;
665
- }
666
- case "blocks": {
667
- if (Array.isArray(raw.allowedBlocks)) {
668
- base.allowedBlocks = raw.allowedBlocks.map(String).filter(Boolean);
669
- }
670
- if (raw.minBlocks !== void 0) base.minBlocks = Number(raw.minBlocks);
671
- if (raw.maxBlocks !== void 0) base.maxBlocks = Number(raw.maxBlocks);
672
- break;
673
- }
674
- }
675
- return base;
676
- }
677
- function normaliseSchema(raw) {
678
- const fields = Array.isArray(raw.fields) ? raw.fields.map(normaliseField) : [];
679
- const schema = { fields };
680
- if (raw.layout === "sidebar" || raw.layout === "tabs") {
681
- schema.layout = raw.layout;
682
- } else {
683
- schema.layout = "default";
684
- }
685
- return schema;
686
- }
687
-
688
- // src/validation/schemaValidator.ts
689
- var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
690
- "text",
691
- "textarea",
692
- "richtext",
693
- "number",
694
- "checkbox",
695
- "select",
696
- "multiselect",
697
- "date",
698
- "image",
699
- "file",
700
- "url",
701
- "email",
702
- "color",
703
- "array",
704
- "group",
705
- "relationship",
706
- "json",
707
- "blocks"
708
- ]);
709
- var CONTAINER_TYPES = /* @__PURE__ */ new Set(["array", "group"]);
710
- var LEAF_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
711
- var VALID_CONDITION_OPERATORS = /* @__PURE__ */ new Set([
712
- "equals",
713
- "not_equals",
714
- "contains",
715
- "not_contains",
716
- "greater_than",
717
- "less_than",
718
- "in",
719
- "not_in",
720
- "exists",
721
- "empty"
722
- ]);
723
- function validateConditions(conditions, path, errors) {
724
- if (!Array.isArray(conditions)) {
725
- errors.push(`${path}: "conditions" must be an array.`);
726
- return;
727
- }
728
- ;
729
- conditions.forEach((cond, i) => {
730
- const cp = `${path}[${i}]`;
731
- if (!cond || typeof cond !== "object") {
732
- errors.push(`${cp}: condition must be an object.`);
733
- return;
734
- }
735
- const c = cond;
736
- if (typeof c.field !== "string" || !c.field.trim()) {
737
- errors.push(`${cp}: "field" must be a non-empty string.`);
738
- }
739
- if (typeof c.operator !== "string" || !VALID_CONDITION_OPERATORS.has(c.operator)) {
740
- errors.push(
741
- `${cp}: "operator" must be one of: ${[...VALID_CONDITION_OPERATORS].join(", ")}.`
742
- );
743
- }
744
- });
745
- }
746
- function validateValidationRules(v, path, errors) {
747
- const numericProps = [
748
- "minLength",
749
- "maxLength",
750
- "min",
751
- "max",
752
- "step",
753
- "minRows",
754
- "maxRows",
755
- "maxFileSize",
756
- "maxSelections"
757
- ];
758
- for (const prop of numericProps) {
759
- if (v[prop] !== void 0 && typeof v[prop] !== "number") {
760
- 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.`);
761
625
  }
762
626
  }
763
627
  if (typeof v.minLength === "number" && typeof v.maxLength === "number" && v.minLength > v.maxLength) {
@@ -790,7 +654,7 @@ function validateValidationRules(v, path, errors) {
790
654
  errors.push(`${path}.allowedMimeTypes: must be an array of strings.`);
791
655
  }
792
656
  }
793
- function validateField(field, path, errors, warnings) {
657
+ function validateField(field, path, errors, warnings, reserved = [], names = /* @__PURE__ */ new Set()) {
794
658
  if (!field || typeof field !== "object" || Array.isArray(field)) {
795
659
  errors.push(`${path}: must be a non-array object.`);
796
660
  return;
@@ -901,7 +765,16 @@ function validateField(field, path, errors, warnings) {
901
765
  if (!Array.isArray(f.fields) || f.fields.length === 0) {
902
766
  errors.push(`${path}: "${type}" fields must have a non-empty "fields" array.`);
903
767
  } else {
904
- validateFields(f.fields, `${path}.fields`, errors, warnings);
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
+ );
905
778
  }
906
779
  if (type === "array") {
907
780
  if (f.minRows !== void 0 && typeof f.minRows !== "number") {
@@ -935,18 +808,63 @@ function validateField(field, path, errors, warnings) {
935
808
  errors.push(`${path}: "minBlocks" (${f.minBlocks}) must be <= "maxBlocks" (${f.maxBlocks}).`);
936
809
  }
937
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);
938
860
  }
939
- function validateFields(fields, path, errors, warnings) {
940
- const names = /* @__PURE__ */ new Set();
861
+ function validateFields(fields, path, errors, warnings, reserved = [], names = /* @__PURE__ */ new Set()) {
941
862
  fields.forEach((field, index) => {
942
863
  const fieldPath = `${path}[${index}]`;
943
- validateField(field, fieldPath, errors, warnings);
864
+ validateField(field, fieldPath, errors, warnings, reserved, names);
944
865
  const f = field;
945
- if (typeof f.name === "string" && f.name) {
946
- if (names.has(f.name)) {
947
- errors.push(`${path}: duplicate field name "${f.name}".`);
948
- }
949
- 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);
950
868
  }
951
869
  });
952
870
  }
@@ -966,7 +884,7 @@ function validateBlockSchema(schema) {
966
884
  } else if (s.fields.length === 0) {
967
885
  warnings.push("Schema has no fields defined.");
968
886
  } else {
969
- validateFields(s.fields, "schema.fields", errors, warnings);
887
+ validateFields(s.fields, "schema.fields", errors, warnings, BLOCK_RESERVED_NAMES);
970
888
  }
971
889
  if (s.layout !== void 0 && !["default", "sidebar", "tabs"].includes(s.layout)) {
972
890
  errors.push(`schema.layout must be one of: "default", "sidebar", "tabs".`);
@@ -974,51 +892,469 @@ function validateBlockSchema(schema) {
974
892
  return { valid: errors.length === 0, errors, warnings };
975
893
  }
976
894
 
977
- // src/builder/saveSchema.ts
978
- async function saveSchemaLocally(payload, request) {
979
- const { blockSlug, name, description, category, schema: rawSchema, changelog } = request;
980
- const schema = normaliseSchema(rawSchema);
981
- if (!schema.fields?.length) {
982
- return {
983
- success: false,
984
- definitionId: "",
985
- versionId: "",
986
- versionNumber: 0,
987
- errors: [`Block "${blockSlug}" schema has no fields. Schemas must be imported from a server-safe module (not a "use client" file).`],
988
- warnings: []
989
- };
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 });
990
912
  }
991
- const validation = validateBlockSchema(schema);
992
- if (!validation.valid) {
993
- return {
994
- success: false,
995
- definitionId: "",
996
- versionId: "",
997
- versionNumber: 0,
998
- errors: validation.errors,
999
- warnings: validation.warnings
1000
- };
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 });
1001
929
  }
1002
- try {
1003
- let definitionId;
1004
- const existing = await payload.find({
1005
- collection: "block-definitions",
1006
- where: { slug: { equals: blockSlug } },
1007
- limit: 1
1008
- });
1009
- if (existing.docs.length > 0) {
1010
- definitionId = existing.docs[0].id;
1011
- const updates = {};
1012
- if (description !== void 0) updates.description = description;
1013
- if (category !== void 0) updates.category = category;
1014
- if (name !== void 0) updates.name = name;
1015
- if (Object.keys(updates).length > 0) {
1016
- await payload.update({
1017
- collection: "block-definitions",
1018
- id: definitionId,
1019
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1020
- data: updates
1021
- });
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
+ });
1022
1358
  }
1023
1359
  } else {
1024
1360
  if (!name) {
@@ -1031,34 +1367,71 @@ async function saveSchemaLocally(payload, request) {
1031
1367
  warnings: validation.warnings
1032
1368
  };
1033
1369
  }
1034
- const created = await payload.create({
1035
- collection: "block-definitions",
1036
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1037
- data: {
1038
- slug: blockSlug,
1039
- name,
1040
- description
1041
- }
1042
- });
1043
- definitionId = created.id;
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
+ }
1044
1390
  }
1045
- const existingVersions = await payload.find({
1391
+ const latestVersionRes = await payload.find({
1046
1392
  collection: "block-definition-versions",
1047
1393
  where: { blockDefinition: { equals: definitionId } },
1048
- limit: 0
1394
+ limit: 1,
1395
+ sort: "-versionNumber"
1049
1396
  });
1050
- const versionNumber = existingVersions.totalDocs + 1;
1051
- const version = await payload.create({
1052
- collection: "block-definition-versions",
1053
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1054
- data: {
1055
- blockDefinition: definitionId,
1056
- versionNumber,
1057
- label: `v${versionNumber}`,
1058
- schema,
1059
- changelog: changelog ?? "Created via Block Builder"
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);
1060
1430
  }
1061
- });
1431
+ }
1432
+ if (!version) {
1433
+ throw new Error("Failed to create version.");
1434
+ }
1062
1435
  await payload.update({
1063
1436
  collection: "block-definitions",
1064
1437
  id: definitionId,
@@ -1085,10 +1458,7 @@ async function saveSchemaLocally(payload, request) {
1085
1458
  }
1086
1459
 
1087
1460
  // src/endpoints/save.ts
1088
- var saveEndpoint = async (req) => {
1089
- if (!req.user) {
1090
- return Response.json({ error: "Unauthorized" }, { status: 401 });
1091
- }
1461
+ var saveEndpoint = withBuilderGuard(async (req) => {
1092
1462
  let body;
1093
1463
  try {
1094
1464
  if (!req.json) return Response.json({ error: "No JSON parser available" }, { status: 500 });
@@ -1101,13 +1471,10 @@ var saveEndpoint = async (req) => {
1101
1471
  }
1102
1472
  const result = await saveSchemaLocally(req.payload, body);
1103
1473
  return Response.json(result, { status: result.success ? 200 : 422 });
1104
- };
1474
+ });
1105
1475
 
1106
1476
  // src/endpoints/versions.ts
1107
- var versionsEndpoint = async (req) => {
1108
- if (!req.user) {
1109
- return Response.json({ error: "Unauthorized" }, { status: 401 });
1110
- }
1477
+ var versionsEndpoint = withBuilderGuard(async (req) => {
1111
1478
  const slug = req.routeParams?.slug;
1112
1479
  if (!slug) {
1113
1480
  return Response.json({ error: "Slug is required" }, { status: 400 });
@@ -1122,7 +1489,7 @@ var versionsEndpoint = async (req) => {
1122
1489
  if (!def) {
1123
1490
  return Response.json({ error: `Block definition "${slug}" not found` }, { status: 404 });
1124
1491
  }
1125
- const currentVersionId = def.currentVersion && typeof def.currentVersion === "object" ? String(def.currentVersion.id) : typeof def.currentVersion === "string" || typeof def.currentVersion === "number" ? String(def.currentVersion) : null;
1492
+ const currentVersionId = resolveId(def.currentVersion);
1126
1493
  const versionsResult = await req.payload.find({
1127
1494
  collection: "block-definition-versions",
1128
1495
  where: { blockDefinition: { equals: def.id } },
@@ -1142,7 +1509,7 @@ var versionsEndpoint = async (req) => {
1142
1509
  };
1143
1510
  });
1144
1511
  return Response.json({ versions });
1145
- };
1512
+ });
1146
1513
 
1147
1514
  // src/plugin.ts
1148
1515
  var dynamicBlocksPlugin = (options = {}) => {