@bobbykim/manguito-cms-api 0.8.3 → 0.9.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.mjs CHANGED
@@ -4,13 +4,14 @@ import {
4
4
  createFieldKeyMapFromProjection,
5
5
  deleteParagraphField,
6
6
  isColumnBacked,
7
+ junctionOrderBy,
7
8
  oneOrMany,
8
9
  paragraphItems,
9
10
  persistJunctionField,
10
11
  persistParagraphField,
11
12
  resolveRelationBareIds,
12
13
  resolveRelationField
13
- } from "./chunk-IHEIX6P4.mjs";
14
+ } from "./chunk-GUZZQZSN.mjs";
14
15
 
15
16
  // src/paths.ts
16
17
  function normalizePrefix(prefix) {
@@ -1169,8 +1170,7 @@ function checkRelationInput(fields, body, registry, path = "") {
1169
1170
  });
1170
1171
  continue;
1171
1172
  }
1172
- const comp = field.ui_component;
1173
- const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
1173
+ const pType = paragraphTypeOf(field, registry);
1174
1174
  if (!pType) continue;
1175
1175
  items.forEach((item, i) => {
1176
1176
  const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
@@ -1179,12 +1179,15 @@ function checkRelationInput(fields, body, registry, path = "") {
1179
1179
  }
1180
1180
  return errors;
1181
1181
  }
1182
+ function paragraphTypeOf(field, registry) {
1183
+ const comp = field.ui_component;
1184
+ return comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
1185
+ }
1182
1186
  function paragraphItemsIn(fields, body, registry, path) {
1183
1187
  const out = [];
1184
1188
  for (const field of fields) {
1185
1189
  if (field.field_type !== "paragraph" || !(field.name in body)) continue;
1186
- const comp = field.ui_component;
1187
- const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
1190
+ const pType = paragraphTypeOf(field, registry);
1188
1191
  if (!pType) continue;
1189
1192
  const value = body[field.name];
1190
1193
  const name = `${path}${field.name}`;
@@ -1256,6 +1259,133 @@ async function findMissingReferences(db, fields, body, registry) {
1256
1259
  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.` }));
1257
1260
  }
1258
1261
 
1262
+ // src/field-values.ts
1263
+ var VARCHAR_DEFAULT_LIMIT = 255;
1264
+ var INTEGER_MIN = -2147483648;
1265
+ var INTEGER_MAX = 2147483647;
1266
+ var DECIMAL_LIMIT = 1e6;
1267
+ var ISO_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
1268
+ var DECIMAL_STRING = /^-?\d+(?:\.\d+)?$/;
1269
+ var EMPTY_MEANS_NULL = /* @__PURE__ */ new Set(["date", "integer", "float", "enum"]);
1270
+ function emptyValuesToNull(fields, body, registry) {
1271
+ const out = { ...body };
1272
+ for (const f of fields) {
1273
+ if (!(f.name in out)) continue;
1274
+ const value = out[f.name];
1275
+ if (value === "" && EMPTY_MEANS_NULL.has(f.field_type)) {
1276
+ out[f.name] = null;
1277
+ continue;
1278
+ }
1279
+ if (f.field_type !== "paragraph") continue;
1280
+ const pType = paragraphTypeOf(f, registry);
1281
+ if (!pType) continue;
1282
+ if (isPlainObject(value)) out[f.name] = emptyValuesToNull(pType.fields, value, registry);
1283
+ else if (Array.isArray(value)) {
1284
+ out[f.name] = value.map((item) => isPlainObject(item) ? emptyValuesToNull(pType.fields, item, registry) : item);
1285
+ }
1286
+ }
1287
+ return out;
1288
+ }
1289
+ var compiled = /* @__PURE__ */ new Map();
1290
+ function patternOf(source) {
1291
+ if (!compiled.has(source)) {
1292
+ let re;
1293
+ try {
1294
+ re = new RegExp(source);
1295
+ } catch {
1296
+ re = null;
1297
+ }
1298
+ compiled.set(source, re);
1299
+ }
1300
+ return compiled.get(source) ?? null;
1301
+ }
1302
+ function isRealDate(value) {
1303
+ const m = ISO_TIMESTAMP.exec(value);
1304
+ if (!m) return false;
1305
+ const [y, mo, d, h = 0, mi = 0, sec = 0] = m.slice(1).map((part) => part === void 0 ? void 0 : Number(part));
1306
+ if (y === void 0 || mo === void 0 || d === void 0 || y < 1) return false;
1307
+ const date = new Date(Date.UTC(y, mo - 1, d));
1308
+ date.setUTCFullYear(y);
1309
+ return date.getUTCMonth() === mo - 1 && date.getUTCDate() === d && h <= 23 && mi <= 59 && sec <= 59;
1310
+ }
1311
+ function valueError(f, value, name) {
1312
+ const v = f.validation;
1313
+ if (Array.isArray(value) && (f.field_type === "paragraph" || f.field_type === "reference")) {
1314
+ if (v.max_items !== void 0 && value.length > v.max_items) {
1315
+ return `${name} can hold at most ${v.max_items} items.`;
1316
+ }
1317
+ return null;
1318
+ }
1319
+ const col = f.db_column;
1320
+ if (!col || col.junction || col.column_type === "uuid") return null;
1321
+ if (value === null || value === void 0) {
1322
+ if (col.nullable) return null;
1323
+ return f.required ? `${name} is required.` : `${name} cannot be empty.`;
1324
+ }
1325
+ switch (f.field_type) {
1326
+ case "text/plain": {
1327
+ if (typeof value !== "string") return `${name} must be text.`;
1328
+ if (value.includes("\0")) return `${name} contains a character that cannot be stored (U+0000).`;
1329
+ const limit = v.limit ?? VARCHAR_DEFAULT_LIMIT;
1330
+ if ([...value].length > limit) return `${name} can be at most ${limit} characters.`;
1331
+ const pattern = v.pattern === void 0 ? null : patternOf(v.pattern);
1332
+ if (value !== "" && pattern && !pattern.test(value)) return `${name} is not in the correct format.`;
1333
+ return null;
1334
+ }
1335
+ case "text/rich":
1336
+ if (typeof value !== "string") return `${name} must be text.`;
1337
+ return value.includes("\0") ? `${name} contains a character that cannot be stored (U+0000).` : null;
1338
+ case "integer":
1339
+ if (typeof value !== "number" || !Number.isInteger(value)) return `${name} must be a whole number.`;
1340
+ if (value < INTEGER_MIN || value > INTEGER_MAX) {
1341
+ return `${name} must be between ${INTEGER_MIN} and ${INTEGER_MAX}.`;
1342
+ }
1343
+ return rangeError(name, value, v.min, v.max);
1344
+ case "float": {
1345
+ const n = typeof value === "number" ? value : typeof value === "string" && DECIMAL_STRING.test(value) ? Number(value) : NaN;
1346
+ if (!Number.isFinite(n)) return `${name} must be a number.`;
1347
+ if (roundedSize(typeof value === "string" ? value : String(n)) >= DECIMAL_LIMIT) {
1348
+ return `${name} must be less than ${DECIMAL_LIMIT} in size.`;
1349
+ }
1350
+ return rangeError(name, n, v.min, v.max);
1351
+ }
1352
+ case "boolean":
1353
+ return typeof value === "boolean" ? null : `${name} must be true or false.`;
1354
+ case "date":
1355
+ return typeof value === "string" && isRealDate(value) ? null : `${name} must be a date, such as 2026-10-11 or 2026-10-11T09:30:00Z.`;
1356
+ case "enum": {
1357
+ const allowed = v.allowed_values ?? [];
1358
+ return typeof value === "string" && allowed.includes(value) ? null : `${name} must be one of: ${allowed.join(", ")}.`;
1359
+ }
1360
+ default:
1361
+ return null;
1362
+ }
1363
+ }
1364
+ function roundedSize(text) {
1365
+ if (text.includes("e")) return Math.abs(Number(text));
1366
+ const [int = "0", frac = ""] = text.replace("-", "").split(".");
1367
+ const kept = Number(`${int}.${frac.slice(0, 4) || "0"}`);
1368
+ return frac.length > 4 && frac.charAt(4) >= "5" ? kept + 1e-4 : kept;
1369
+ }
1370
+ function rangeError(name, value, min, max) {
1371
+ if (min !== void 0 && value < min) return `${name} must be at least ${min}.`;
1372
+ if (max !== void 0 && value > max) return `${name} must be at most ${max}.`;
1373
+ return null;
1374
+ }
1375
+ function checkFieldValues(fields, body, registry, path = "") {
1376
+ const errors = [];
1377
+ for (const f of fields) {
1378
+ if (!(f.name in body) || f.removed === true) continue;
1379
+ const name = `${path}${f.name}`;
1380
+ const message = valueError(f, body[f.name], name);
1381
+ if (message) errors.push({ field: name, message });
1382
+ }
1383
+ for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
1384
+ errors.push(...checkFieldValues(itemFields, item, registry, prefix));
1385
+ }
1386
+ return errors;
1387
+ }
1388
+
1259
1389
  // src/routes/admin/content.ts
1260
1390
  function quoteIdent3(name) {
1261
1391
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
@@ -1292,7 +1422,7 @@ async function loadRelationsForEdit(db, registry, fields, id, row) {
1292
1422
  } else if (f.db_column.junction) {
1293
1423
  const j = f.db_column.junction;
1294
1424
  const r = await db.execute(
1295
- sql4`SELECT ${sql4.raw(quoteIdent3(j.right_column))} FROM ${sql4.raw(quoteIdent3(j.table_name))} WHERE ${sql4.raw(quoteIdent3(j.left_column))} = ${id}`
1425
+ sql4`SELECT ${sql4.raw(quoteIdent3(j.right_column))} FROM ${sql4.raw(quoteIdent3(j.table_name))} WHERE ${sql4.raw(quoteIdent3(j.left_column))} = ${id}${junctionOrderBy(j)}`
1296
1426
  );
1297
1427
  row[f.name] = r.rows.map((linked) => linked[j.right_column]);
1298
1428
  }
@@ -1335,6 +1465,8 @@ async function relationInputError(c, fields, body, registry, db) {
1335
1465
  const refuse = (message, details) => c.json({ ok: false, error: { code: "VALIDATION_ERROR", message, details } }, 422);
1336
1466
  const shape = checkRelationInput(fields, body, registry);
1337
1467
  if (shape.length > 0) return refuse("Relation fields have the wrong shape", shape);
1468
+ const values = checkFieldValues(fields, body, registry);
1469
+ if (values.length > 0) return refuse("Some values are not valid", values);
1338
1470
  const required = checkRequiredInParagraphItems(fields, body, registry);
1339
1471
  if (required.length > 0) return refuse("Required fields are missing", required);
1340
1472
  if (db) {
@@ -1485,7 +1617,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1485
1617
  `/admin/api/${basePath}`,
1486
1618
  requirePermission("content:create"),
1487
1619
  async (c) => {
1488
- const body = await c.req.json();
1620
+ const body = emptyValuesToNull(contentType.fields, await c.req.json(), registry);
1489
1621
  if (contentType.only_one) {
1490
1622
  const existing = await repo.findMany({ page: 1, per_page: 1 });
1491
1623
  if (existing.meta.total > 0) {
@@ -1612,7 +1744,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1612
1744
  requirePermission("content:edit"),
1613
1745
  async (c) => {
1614
1746
  const id = c.req.param("id");
1615
- const body = await c.req.json();
1747
+ const body = emptyValuesToNull(contentType.fields, await c.req.json(), registry);
1616
1748
  const existing = await repo.findOne(id);
1617
1749
  if (!existing) {
1618
1750
  return c.json(
@@ -1719,7 +1851,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1719
1851
  `/admin/api/${basePath}`,
1720
1852
  requirePermission("content:edit"),
1721
1853
  async (c) => {
1722
- const body = await c.req.json();
1854
+ const body = emptyValuesToNull(contentType.fields, await c.req.json(), registry);
1723
1855
  const existingResult = await repo.findMany({ page: 1, per_page: 1 });
1724
1856
  const existing = existingResult.data[0];
1725
1857
  if (!existing) {
@@ -1846,7 +1978,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1846
1978
  `/admin/api/taxonomy/${typeName}`,
1847
1979
  requirePermission("content:create"),
1848
1980
  async (c) => {
1849
- const body = await c.req.json();
1981
+ const body = emptyValuesToNull(taxonomyType.fields, await c.req.json(), registry);
1850
1982
  const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
1851
1983
  if (inputError) return inputError;
1852
1984
  const storageBody = fieldKeys.toStorage(body);
@@ -1894,7 +2026,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
1894
2026
  requirePermission("content:edit"),
1895
2027
  async (c) => {
1896
2028
  const id = c.req.param("id");
1897
- const body = await c.req.json();
2029
+ const body = emptyValuesToNull(taxonomyType.fields, await c.req.json(), registry);
1898
2030
  const storageBody = fieldKeys.toStorage(body);
1899
2031
  const existing = await repo.findOne(id);
1900
2032
  if (!existing) {
@@ -2951,7 +3083,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2951
3083
  import { sql as sql7 } from "drizzle-orm";
2952
3084
  var API_VERSION = (() => {
2953
3085
  try {
2954
- return "0.8.3";
3086
+ return "0.9.0";
2955
3087
  } catch {
2956
3088
  return "0.0.0";
2957
3089
  }
@@ -3649,7 +3781,7 @@ function createCmsApp(options) {
3649
3781
  handler: null,
3650
3782
  error: null
3651
3783
  }));
3652
- const ready = import("./handler-VEOWJRAL.mjs").then(({ createGraphQLHandler }) => {
3784
+ const ready = import("./handler-MFF32JB4.mjs").then(({ createGraphQLHandler }) => {
3653
3785
  gqlPasses.forEach((pass, i) => {
3654
3786
  const mount = mounts[i];
3655
3787
  try {