@bobbykim/manguito-cms-api 0.8.4 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -144,7 +144,7 @@ var init_field_keys = __esm({
144
144
  });
145
145
 
146
146
  // src/relations.ts
147
- function quoteIdent2(name) {
147
+ function quoteIdent3(name) {
148
148
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
149
149
  return `"${name}"`;
150
150
  }
@@ -156,10 +156,10 @@ function paragraphMediaFieldNames(pType) {
156
156
  async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
157
157
  const mediaColumns = pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.db_column.column_name);
158
158
  if (mediaColumns.length === 0) return [];
159
- const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
160
- const cols = import_drizzle_orm3.sql.join(mediaColumns.map((c) => import_drizzle_orm3.sql.raw(quoteIdent2(c))), import_drizzle_orm3.sql`, `);
159
+ const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(pType.db.table_name));
160
+ const cols = import_drizzle_orm4.sql.join(mediaColumns.map((c) => import_drizzle_orm4.sql.raw(quoteIdent3(c))), import_drizzle_orm4.sql`, `);
161
161
  const result = await db.execute(
162
- import_drizzle_orm3.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
162
+ import_drizzle_orm4.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
163
163
  );
164
164
  const ids = [];
165
165
  for (const row of result.rows) {
@@ -183,9 +183,9 @@ function nestedParagraphFields(pType, registry) {
183
183
  return out;
184
184
  }
185
185
  async function fetchParagraphRowIds(db, pType, itemId, fieldName) {
186
- const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
186
+ const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(pType.db.table_name));
187
187
  const result = await db.execute(
188
- import_drizzle_orm3.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
188
+ import_drizzle_orm4.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
189
189
  );
190
190
  return result.rows.map((r) => r["id"]).filter((id) => typeof id === "string");
191
191
  }
@@ -201,8 +201,8 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
201
201
  }
202
202
  }
203
203
  }
204
- const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
205
- await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
204
+ const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(pType.db.table_name));
205
+ await db.execute(import_drizzle_orm4.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
206
206
  const mediaFieldNames = paragraphMediaFieldNames(pType);
207
207
  const added = [];
208
208
  for (let i = 0; i < items.length; i++) {
@@ -221,9 +221,9 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
221
221
  if (!pf.db_column || pf.db_column.junction) continue;
222
222
  data[pf.db_column.column_name] = pItem[pf.name] ?? null;
223
223
  }
224
- const cols = import_drizzle_orm3.sql.join(Object.keys(data).map((k) => import_drizzle_orm3.sql.raw(quoteIdent2(k))), import_drizzle_orm3.sql`, `);
225
- const vals = import_drizzle_orm3.sql.join(Object.values(data).map((v) => import_drizzle_orm3.sql`${v}`), import_drizzle_orm3.sql`, `);
226
- await db.execute(import_drizzle_orm3.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
224
+ const cols = import_drizzle_orm4.sql.join(Object.keys(data).map((k) => import_drizzle_orm4.sql.raw(quoteIdent3(k))), import_drizzle_orm4.sql`, `);
225
+ const vals = import_drizzle_orm4.sql.join(Object.values(data).map((v) => import_drizzle_orm4.sql`${v}`), import_drizzle_orm4.sql`, `);
226
+ await db.execute(import_drizzle_orm4.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
227
227
  for (const name of mediaFieldNames) {
228
228
  const val = pItem[name];
229
229
  if (typeof val === "string" && val !== "") added.push(val);
@@ -257,24 +257,42 @@ async function deleteParagraphField(db, itemId, fieldName, pType, registry) {
257
257
  }
258
258
  }
259
259
  }
260
- const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
261
- await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
260
+ const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(pType.db.table_name));
261
+ await db.execute(import_drizzle_orm4.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
262
262
  return { added: [], removed };
263
263
  }
264
264
  async function persistJunctionField(db, itemId, junction, relatedIds) {
265
- const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(junction.table_name));
266
- const left = import_drizzle_orm3.sql.raw(quoteIdent2(junction.left_column));
267
- const right = import_drizzle_orm3.sql.raw(quoteIdent2(junction.right_column));
268
- await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
269
- for (const rid of relatedIds) {
270
- await db.execute(import_drizzle_orm3.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
271
- }
265
+ const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(junction.table_name));
266
+ const left = import_drizzle_orm4.sql.raw(quoteIdent3(junction.left_column));
267
+ const right = import_drizzle_orm4.sql.raw(quoteIdent3(junction.right_column));
268
+ const seen = /* @__PURE__ */ new Set();
269
+ const ids = relatedIds.filter((id) => {
270
+ const key = id.toLowerCase();
271
+ if (seen.has(key)) return false;
272
+ seen.add(key);
273
+ return true;
274
+ });
275
+ await db.execute(import_drizzle_orm4.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
276
+ if (ids.length === 0) return;
277
+ const rows = import_drizzle_orm4.sql.join(
278
+ ids.map(
279
+ (rid, position) => junction.order_column ? import_drizzle_orm4.sql`(${itemId}, ${rid}, ${position})` : import_drizzle_orm4.sql`(${itemId}, ${rid})`
280
+ ),
281
+ import_drizzle_orm4.sql`, `
282
+ );
283
+ await db.execute(
284
+ junction.order_column ? import_drizzle_orm4.sql`INSERT INTO ${tbl} (${left}, ${right}, "order") VALUES ${rows}` : import_drizzle_orm4.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES ${rows}`
285
+ );
286
+ }
287
+ function junctionOrderBy(junction) {
288
+ const right = import_drizzle_orm4.sql.raw(quoteIdent3(junction.right_column));
289
+ return junction.order_column ? import_drizzle_orm4.sql` ORDER BY "order" ASC, ${right} ASC` : import_drizzle_orm4.sql` ORDER BY ${right} ASC`;
272
290
  }
273
291
  function oneOrMany(cardinality, list) {
274
292
  return cardinality === "one" ? list[0] ?? null : list;
275
293
  }
276
294
  function paragraphItems(field, value) {
277
- if ((0, import_manguito_cms_core2.relationCardinality)(field) === "one") {
295
+ if ((0, import_manguito_cms_core3.relationCardinality)(field) === "one") {
278
296
  return typeof value === "object" && value !== null && !Array.isArray(value) ? [value] : [];
279
297
  }
280
298
  return Array.isArray(value) ? value : [];
@@ -292,7 +310,7 @@ function buildRelationsMap(fields, registry) {
292
310
  relations[field.name] = {
293
311
  type: "paragraph",
294
312
  table: pType.db.table_name,
295
- cardinality: (0, import_manguito_cms_core2.relationCardinality)(field) ?? "many"
313
+ cardinality: (0, import_manguito_cms_core3.relationCardinality)(field) ?? "many"
296
314
  };
297
315
  } else if (field.field_type === "reference") {
298
316
  if (!field.db_column) continue;
@@ -335,9 +353,9 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
335
353
  const batch = [...new Set(rows)];
336
354
  if (rel.type === "paragraph") {
337
355
  const parentIds = batch.map((r) => r["id"]);
338
- const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
356
+ const inList = import_drizzle_orm4.sql.join(parentIds.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
339
357
  const result = await db.execute(
340
- import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
358
+ import_drizzle_orm4.sql`SELECT * FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
341
359
  );
342
360
  const byParent = groupBy(result.rows, "parent_id");
343
361
  for (const row of batch) {
@@ -354,10 +372,10 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
354
372
  const unique = [...new Set(fkValues)];
355
373
  const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
356
374
  if (uncached.length > 0) {
357
- const inList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
358
- const publishedCond = publishedOnly ? import_drizzle_orm3.sql` AND published = true` : import_drizzle_orm3.sql``;
375
+ const inList = import_drizzle_orm4.sql.join(uncached.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
376
+ const publishedCond = publishedOnly ? import_drizzle_orm4.sql` AND published = true` : import_drizzle_orm4.sql``;
359
377
  const result = await db.execute(
360
- import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE id IN (${inList})${publishedCond}`
378
+ import_drizzle_orm4.sql`SELECT * FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.table))} WHERE id IN (${inList})${publishedCond}`
361
379
  );
362
380
  for (const item of result.rows) {
363
381
  cache.set(`${rel.table}:${item["id"]}`, item);
@@ -369,19 +387,18 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
369
387
  }
370
388
  } else if (rel.type === "junction") {
371
389
  const parentIds = batch.map((r) => r["id"]);
372
- const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
373
- const orderBy = rel.order_column ? import_drizzle_orm3.sql` ORDER BY "order" ASC` : import_drizzle_orm3.sql``;
390
+ const inList = import_drizzle_orm4.sql.join(parentIds.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
374
391
  const junctionResult = await db.execute(
375
- import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.junction_table))} WHERE ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.left_column))} IN (${inList})${orderBy}`
392
+ import_drizzle_orm4.sql`SELECT * FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.junction_table))} WHERE ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
376
393
  );
377
394
  const jRows = junctionResult.rows;
378
395
  const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
379
396
  const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
380
397
  if (uncached.length > 0) {
381
- const rightInList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
382
- const publishedCond = publishedOnly ? import_drizzle_orm3.sql` AND published = true` : import_drizzle_orm3.sql``;
398
+ const rightInList = import_drizzle_orm4.sql.join(uncached.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
399
+ const publishedCond = publishedOnly ? import_drizzle_orm4.sql` AND published = true` : import_drizzle_orm4.sql``;
383
400
  const entitiesResult = await db.execute(
384
- import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
401
+ import_drizzle_orm4.sql`SELECT * FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
385
402
  );
386
403
  for (const item of entitiesResult.rows) {
387
404
  cache.set(`${rel.table}:${item["id"]}`, item);
@@ -403,8 +420,8 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
403
420
  const unique = [...new Set(fkValues)];
404
421
  const uncached = unique.filter((id) => !cache.has(`media:${id}`));
405
422
  if (uncached.length > 0) {
406
- const inList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
407
- const result = await db.execute(import_drizzle_orm3.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
423
+ const inList = import_drizzle_orm4.sql.join(uncached.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
424
+ const result = await db.execute(import_drizzle_orm4.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
408
425
  for (const item of result.rows) {
409
426
  cache.set(`media:${item["id"]}`, item);
410
427
  }
@@ -419,9 +436,9 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
419
436
  if (rows.length === 0) return;
420
437
  if (rel.type === "paragraph") {
421
438
  const parentIds = rows.map((r) => r["id"]);
422
- const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
439
+ const inList = import_drizzle_orm4.sql.join(parentIds.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
423
440
  const result = await db.execute(
424
- import_drizzle_orm3.sql`SELECT id, parent_id FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
441
+ import_drizzle_orm4.sql`SELECT id, parent_id FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
425
442
  );
426
443
  const byParent = groupBy(result.rows, "parent_id");
427
444
  for (const row of rows) {
@@ -429,9 +446,9 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
429
446
  }
430
447
  } else if (rel.type === "junction") {
431
448
  const parentIds = rows.map((r) => r["id"]);
432
- const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
449
+ const inList = import_drizzle_orm4.sql.join(parentIds.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
433
450
  const result = await db.execute(
434
- import_drizzle_orm3.sql`SELECT ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.left_column))} AS left_id, ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.right_column))} AS right_id FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.junction_table))} WHERE ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.left_column))} IN (${inList})`
451
+ import_drizzle_orm4.sql`SELECT ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.left_column))} AS left_id, ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.right_column))} AS right_id FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.junction_table))} WHERE ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
435
452
  );
436
453
  const byLeft = groupBy(result.rows, "left_id");
437
454
  for (const row of rows) {
@@ -439,13 +456,13 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
439
456
  }
440
457
  }
441
458
  }
442
- var import_node_crypto, import_drizzle_orm3, import_manguito_cms_core2;
459
+ var import_node_crypto, import_drizzle_orm4, import_manguito_cms_core3;
443
460
  var init_relations = __esm({
444
461
  "src/relations.ts"() {
445
462
  "use strict";
446
463
  import_node_crypto = __toESM(require("crypto"));
447
- import_drizzle_orm3 = require("drizzle-orm");
448
- import_manguito_cms_core2 = require("@bobbykim/manguito-cms-core");
464
+ import_drizzle_orm4 = require("drizzle-orm");
465
+ import_manguito_cms_core3 = require("@bobbykim/manguito-cms-core");
449
466
  }
450
467
  });
451
468
 
@@ -2354,12 +2371,9 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
2354
2371
  if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
2355
2372
  }
2356
2373
 
2357
- // src/routes/admin/content.ts
2358
- init_relations();
2359
-
2360
2374
  // src/relation-input.ts
2361
- var import_drizzle_orm4 = require("drizzle-orm");
2362
- var import_manguito_cms_core3 = require("@bobbykim/manguito-cms-core");
2375
+ var import_drizzle_orm3 = require("drizzle-orm");
2376
+ var import_manguito_cms_core2 = require("@bobbykim/manguito-cms-core");
2363
2377
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2364
2378
  function isUuid(v) {
2365
2379
  return typeof v === "string" && UUID_RE.test(v);
@@ -2371,7 +2385,7 @@ function checkRelationInput(fields, body, registry, path = "") {
2371
2385
  const errors = [];
2372
2386
  for (const field of fields) {
2373
2387
  if (!(field.name in body)) continue;
2374
- const cardinality = (0, import_manguito_cms_core3.relationCardinality)(field);
2388
+ const cardinality = (0, import_manguito_cms_core2.relationCardinality)(field);
2375
2389
  if (cardinality === null) continue;
2376
2390
  const value = body[field.name];
2377
2391
  const name = `${path}${field.name}`;
@@ -2393,8 +2407,7 @@ function checkRelationInput(fields, body, registry, path = "") {
2393
2407
  });
2394
2408
  continue;
2395
2409
  }
2396
- const comp = field.ui_component;
2397
- const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
2410
+ const pType = paragraphTypeOf(field, registry);
2398
2411
  if (!pType) continue;
2399
2412
  items.forEach((item, i) => {
2400
2413
  const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
@@ -2403,16 +2416,19 @@ function checkRelationInput(fields, body, registry, path = "") {
2403
2416
  }
2404
2417
  return errors;
2405
2418
  }
2419
+ function paragraphTypeOf(field, registry) {
2420
+ const comp = field.ui_component;
2421
+ return comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
2422
+ }
2406
2423
  function paragraphItemsIn(fields, body, registry, path) {
2407
2424
  const out = [];
2408
2425
  for (const field of fields) {
2409
2426
  if (field.field_type !== "paragraph" || !(field.name in body)) continue;
2410
- const comp = field.ui_component;
2411
- const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
2427
+ const pType = paragraphTypeOf(field, registry);
2412
2428
  if (!pType) continue;
2413
2429
  const value = body[field.name];
2414
2430
  const name = `${path}${field.name}`;
2415
- if ((0, import_manguito_cms_core3.relationCardinality)(field) === "one") {
2431
+ if ((0, import_manguito_cms_core2.relationCardinality)(field) === "one") {
2416
2432
  if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
2417
2433
  } else if (Array.isArray(value)) {
2418
2434
  value.forEach((item, i) => {
@@ -2441,7 +2457,7 @@ function checkRequiredInParagraphItems(fields, body, registry, path = "") {
2441
2457
  }
2442
2458
  return errors;
2443
2459
  }
2444
- function quoteIdent3(name) {
2460
+ function quoteIdent2(name) {
2445
2461
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
2446
2462
  return `"${name}"`;
2447
2463
  }
@@ -2472,7 +2488,7 @@ async function findMissingReferences(db, fields, body, registry) {
2472
2488
  const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
2473
2489
  if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
2474
2490
  const result = await db.execute(
2475
- import_drizzle_orm4.sql`SELECT id FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(table))} WHERE id IN (${import_drizzle_orm4.sql.join(ids.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `)})`
2491
+ import_drizzle_orm3.sql`SELECT id FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(table))} WHERE id IN (${import_drizzle_orm3.sql.join(ids.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `)})`
2476
2492
  );
2477
2493
  return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
2478
2494
  });
@@ -2480,7 +2496,135 @@ async function findMissingReferences(db, fields, body, registry) {
2480
2496
  return uses.filter((u) => !found.get(u.table).has(u.id.toLowerCase())).map((u) => ({ field: u.field, message: `${u.field} refers to an item that does not exist.` }));
2481
2497
  }
2482
2498
 
2499
+ // src/field-values.ts
2500
+ var VARCHAR_DEFAULT_LIMIT = 255;
2501
+ var INTEGER_MIN = -2147483648;
2502
+ var INTEGER_MAX = 2147483647;
2503
+ var DECIMAL_LIMIT = 1e6;
2504
+ var ISO_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
2505
+ var DECIMAL_STRING = /^-?\d+(?:\.\d+)?$/;
2506
+ var EMPTY_MEANS_NULL = /* @__PURE__ */ new Set(["date", "integer", "float", "enum"]);
2507
+ function emptyValuesToNull(fields, body, registry) {
2508
+ const out = { ...body };
2509
+ for (const f of fields) {
2510
+ if (!(f.name in out)) continue;
2511
+ const value = out[f.name];
2512
+ if (value === "" && EMPTY_MEANS_NULL.has(f.field_type)) {
2513
+ out[f.name] = null;
2514
+ continue;
2515
+ }
2516
+ if (f.field_type !== "paragraph") continue;
2517
+ const pType = paragraphTypeOf(f, registry);
2518
+ if (!pType) continue;
2519
+ if (isPlainObject(value)) out[f.name] = emptyValuesToNull(pType.fields, value, registry);
2520
+ else if (Array.isArray(value)) {
2521
+ out[f.name] = value.map((item) => isPlainObject(item) ? emptyValuesToNull(pType.fields, item, registry) : item);
2522
+ }
2523
+ }
2524
+ return out;
2525
+ }
2526
+ var compiled = /* @__PURE__ */ new Map();
2527
+ function patternOf(source) {
2528
+ if (!compiled.has(source)) {
2529
+ let re;
2530
+ try {
2531
+ re = new RegExp(source);
2532
+ } catch {
2533
+ re = null;
2534
+ }
2535
+ compiled.set(source, re);
2536
+ }
2537
+ return compiled.get(source) ?? null;
2538
+ }
2539
+ function isRealDate(value) {
2540
+ const m = ISO_TIMESTAMP.exec(value);
2541
+ if (!m) return false;
2542
+ const [y, mo, d, h = 0, mi = 0, sec = 0] = m.slice(1).map((part) => part === void 0 ? void 0 : Number(part));
2543
+ if (y === void 0 || mo === void 0 || d === void 0 || y < 1) return false;
2544
+ const date = new Date(Date.UTC(y, mo - 1, d));
2545
+ date.setUTCFullYear(y);
2546
+ return date.getUTCMonth() === mo - 1 && date.getUTCDate() === d && h <= 23 && mi <= 59 && sec <= 59;
2547
+ }
2548
+ function valueError(f, value, name) {
2549
+ const v = f.validation;
2550
+ if (Array.isArray(value) && (f.field_type === "paragraph" || f.field_type === "reference")) {
2551
+ if (v.max_items !== void 0 && value.length > v.max_items) {
2552
+ return `${name} can hold at most ${v.max_items} items.`;
2553
+ }
2554
+ return null;
2555
+ }
2556
+ const col = f.db_column;
2557
+ if (!col || col.junction || col.column_type === "uuid") return null;
2558
+ if (value === null || value === void 0) {
2559
+ if (col.nullable) return null;
2560
+ return f.required ? `${name} is required.` : `${name} cannot be empty.`;
2561
+ }
2562
+ switch (f.field_type) {
2563
+ case "text/plain": {
2564
+ if (typeof value !== "string") return `${name} must be text.`;
2565
+ if (value.includes("\0")) return `${name} contains a character that cannot be stored (U+0000).`;
2566
+ const limit = v.limit ?? VARCHAR_DEFAULT_LIMIT;
2567
+ if ([...value].length > limit) return `${name} can be at most ${limit} characters.`;
2568
+ const pattern = v.pattern === void 0 ? null : patternOf(v.pattern);
2569
+ if (value !== "" && pattern && !pattern.test(value)) return `${name} is not in the correct format.`;
2570
+ return null;
2571
+ }
2572
+ case "text/rich":
2573
+ if (typeof value !== "string") return `${name} must be text.`;
2574
+ return value.includes("\0") ? `${name} contains a character that cannot be stored (U+0000).` : null;
2575
+ case "integer":
2576
+ if (typeof value !== "number" || !Number.isInteger(value)) return `${name} must be a whole number.`;
2577
+ if (value < INTEGER_MIN || value > INTEGER_MAX) {
2578
+ return `${name} must be between ${INTEGER_MIN} and ${INTEGER_MAX}.`;
2579
+ }
2580
+ return rangeError(name, value, v.min, v.max);
2581
+ case "float": {
2582
+ const n = typeof value === "number" ? value : typeof value === "string" && DECIMAL_STRING.test(value) ? Number(value) : NaN;
2583
+ if (!Number.isFinite(n)) return `${name} must be a number.`;
2584
+ if (roundedSize(typeof value === "string" ? value : String(n)) >= DECIMAL_LIMIT) {
2585
+ return `${name} must be less than ${DECIMAL_LIMIT} in size.`;
2586
+ }
2587
+ return rangeError(name, n, v.min, v.max);
2588
+ }
2589
+ case "boolean":
2590
+ return typeof value === "boolean" ? null : `${name} must be true or false.`;
2591
+ case "date":
2592
+ return typeof value === "string" && isRealDate(value) ? null : `${name} must be a date, such as 2026-10-11 or 2026-10-11T09:30:00Z.`;
2593
+ case "enum": {
2594
+ const allowed = v.allowed_values ?? [];
2595
+ return typeof value === "string" && allowed.includes(value) ? null : `${name} must be one of: ${allowed.join(", ")}.`;
2596
+ }
2597
+ default:
2598
+ return null;
2599
+ }
2600
+ }
2601
+ function roundedSize(text) {
2602
+ if (text.includes("e")) return Math.abs(Number(text));
2603
+ const [int = "0", frac = ""] = text.replace("-", "").split(".");
2604
+ const kept = Number(`${int}.${frac.slice(0, 4) || "0"}`);
2605
+ return frac.length > 4 && frac.charAt(4) >= "5" ? kept + 1e-4 : kept;
2606
+ }
2607
+ function rangeError(name, value, min, max) {
2608
+ if (min !== void 0 && value < min) return `${name} must be at least ${min}.`;
2609
+ if (max !== void 0 && value > max) return `${name} must be at most ${max}.`;
2610
+ return null;
2611
+ }
2612
+ function checkFieldValues(fields, body, registry, path = "") {
2613
+ const errors = [];
2614
+ for (const f of fields) {
2615
+ if (!(f.name in body) || f.removed === true) continue;
2616
+ const name = `${path}${f.name}`;
2617
+ const message = valueError(f, body[f.name], name);
2618
+ if (message) errors.push({ field: name, message });
2619
+ }
2620
+ for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
2621
+ errors.push(...checkFieldValues(itemFields, item, registry, prefix));
2622
+ }
2623
+ return errors;
2624
+ }
2625
+
2483
2626
  // src/routes/admin/content.ts
2627
+ init_relations();
2484
2628
  init_field_keys();
2485
2629
  function quoteIdent4(name) {
2486
2630
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
@@ -2517,7 +2661,7 @@ async function loadRelationsForEdit(db, registry, fields, id, row) {
2517
2661
  } else if (f.db_column.junction) {
2518
2662
  const j = f.db_column.junction;
2519
2663
  const r = await db.execute(
2520
- import_drizzle_orm5.sql`SELECT ${import_drizzle_orm5.sql.raw(quoteIdent4(j.right_column))} FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(j.table_name))} WHERE ${import_drizzle_orm5.sql.raw(quoteIdent4(j.left_column))} = ${id}`
2664
+ import_drizzle_orm5.sql`SELECT ${import_drizzle_orm5.sql.raw(quoteIdent4(j.right_column))} FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(j.table_name))} WHERE ${import_drizzle_orm5.sql.raw(quoteIdent4(j.left_column))} = ${id}${junctionOrderBy(j)}`
2521
2665
  );
2522
2666
  row[f.name] = r.rows.map((linked) => linked[j.right_column]);
2523
2667
  }
@@ -2560,6 +2704,8 @@ async function relationInputError(c, fields, body, registry, db) {
2560
2704
  const refuse = (message, details) => c.json({ ok: false, error: { code: "VALIDATION_ERROR", message, details } }, 422);
2561
2705
  const shape = checkRelationInput(fields, body, registry);
2562
2706
  if (shape.length > 0) return refuse("Relation fields have the wrong shape", shape);
2707
+ const values = checkFieldValues(fields, body, registry);
2708
+ if (values.length > 0) return refuse("Some values are not valid", values);
2563
2709
  const required = checkRequiredInParagraphItems(fields, body, registry);
2564
2710
  if (required.length > 0) return refuse("Required fields are missing", required);
2565
2711
  if (db) {
@@ -2710,7 +2856,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2710
2856
  `/admin/api/${basePath}`,
2711
2857
  requirePermission("content:create"),
2712
2858
  async (c) => {
2713
- const body = await c.req.json();
2859
+ const body = emptyValuesToNull(contentType.fields, await c.req.json(), registry);
2714
2860
  if (contentType.only_one) {
2715
2861
  const existing = await repo.findMany({ page: 1, per_page: 1 });
2716
2862
  if (existing.meta.total > 0) {
@@ -2837,7 +2983,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2837
2983
  requirePermission("content:edit"),
2838
2984
  async (c) => {
2839
2985
  const id = c.req.param("id");
2840
- const body = await c.req.json();
2986
+ const body = emptyValuesToNull(contentType.fields, await c.req.json(), registry);
2841
2987
  const existing = await repo.findOne(id);
2842
2988
  if (!existing) {
2843
2989
  return c.json(
@@ -2944,7 +3090,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2944
3090
  `/admin/api/${basePath}`,
2945
3091
  requirePermission("content:edit"),
2946
3092
  async (c) => {
2947
- const body = await c.req.json();
3093
+ const body = emptyValuesToNull(contentType.fields, await c.req.json(), registry);
2948
3094
  const existingResult = await repo.findMany({ page: 1, per_page: 1 });
2949
3095
  const existing = existingResult.data[0];
2950
3096
  if (!existing) {
@@ -3071,7 +3217,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
3071
3217
  `/admin/api/taxonomy/${typeName}`,
3072
3218
  requirePermission("content:create"),
3073
3219
  async (c) => {
3074
- const body = await c.req.json();
3220
+ const body = emptyValuesToNull(taxonomyType.fields, await c.req.json(), registry);
3075
3221
  const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
3076
3222
  if (inputError) return inputError;
3077
3223
  const storageBody = fieldKeys.toStorage(body);
@@ -3119,7 +3265,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
3119
3265
  requirePermission("content:edit"),
3120
3266
  async (c) => {
3121
3267
  const id = c.req.param("id");
3122
- const body = await c.req.json();
3268
+ const body = emptyValuesToNull(taxonomyType.fields, await c.req.json(), registry);
3123
3269
  const storageBody = fieldKeys.toStorage(body);
3124
3270
  const existing = await repo.findOne(id);
3125
3271
  if (!existing) {
@@ -4176,7 +4322,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
4176
4322
  var import_drizzle_orm8 = require("drizzle-orm");
4177
4323
  var API_VERSION = (() => {
4178
4324
  try {
4179
- return "0.8.4";
4325
+ return "0.9.1";
4180
4326
  } catch {
4181
4327
  return "0.0.0";
4182
4328
  }