@bobbykim/manguito-cms-api 0.3.0 → 0.3.3

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
@@ -5,7 +5,7 @@ import {
5
5
  persistParagraphField,
6
6
  resolveRelationBareIds,
7
7
  resolveRelationField
8
- } from "./chunk-FBS2RQI3.mjs";
8
+ } from "./chunk-TK4DNRFF.mjs";
9
9
 
10
10
  // src/app.ts
11
11
  import { Hono } from "hono";
@@ -952,6 +952,23 @@ function quoteIdent(name) {
952
952
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
953
953
  return `"${name}"`;
954
954
  }
955
+ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
956
+ const result = await db.execute(
957
+ sql2`SELECT * FROM ${sql2.raw(quoteIdent(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
958
+ );
959
+ const rows = result.rows;
960
+ for (const nf of pType.fields) {
961
+ if (nf.db_column !== null) continue;
962
+ const comp = nf.ui_component;
963
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
964
+ const nType = registry.paragraph_types[comp.ref];
965
+ if (!nType) continue;
966
+ for (const row of rows) {
967
+ row[nf.name] = await loadParagraphRows(db, registry, nType, row["id"], nf.name);
968
+ }
969
+ }
970
+ return rows;
971
+ }
955
972
  async function lookupBasePathId(db, pathOrName) {
956
973
  const r = await db.execute(
957
974
  sql2`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
@@ -1103,10 +1120,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1103
1120
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1104
1121
  const pType = registry.paragraph_types[comp.ref];
1105
1122
  if (!pType) continue;
1106
- const r = await db.execute(
1107
- sql2`SELECT * FROM ${sql2.raw(quoteIdent(pType.db.table_name))} WHERE parent_id = ${id} AND parent_field = ${f.name} ORDER BY "order" ASC`
1108
- );
1109
- row[f.name] = r.rows;
1123
+ row[f.name] = await loadParagraphRows(db, registry, pType, id, f.name);
1110
1124
  } else if (f.db_column.junction) {
1111
1125
  const j = f.db_column.junction;
1112
1126
  const r = await db.execute(
@@ -1181,82 +1195,85 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1181
1195
  );
1182
1196
  }
1183
1197
  }
1184
- const fieldErrors = checkRequiredFields(requiredFields, body);
1185
- if (fieldErrors.length > 0) {
1198
+ return writeNewItem(c, body);
1199
+ }
1200
+ );
1201
+ const writeNewItem = async (c, body) => {
1202
+ const fieldErrors = checkRequiredFields(requiredFields, body);
1203
+ if (fieldErrors.length > 0) {
1204
+ return c.json(
1205
+ {
1206
+ ok: false,
1207
+ error: {
1208
+ code: "VALIDATION_ERROR",
1209
+ message: "Required fields are missing",
1210
+ details: fieldErrors
1211
+ }
1212
+ },
1213
+ 422
1214
+ );
1215
+ }
1216
+ if (body["published"] === true) {
1217
+ const publishDeny = await requirePermission("content:edit")(c, async () => {
1218
+ });
1219
+ if (publishDeny) return publishDeny;
1220
+ }
1221
+ const paragraphFields = contentType.fields.filter((f) => f.db_column === null);
1222
+ const junctionFields = contentType.fields.filter(
1223
+ (f) => f.db_column !== null && f.db_column.junction !== void 0
1224
+ );
1225
+ const columnFields = contentType.fields.filter(
1226
+ (f) => f.db_column !== null && f.db_column.junction === void 0
1227
+ );
1228
+ const columnData = {
1229
+ published: body["published"] ?? false
1230
+ };
1231
+ if (contentType.only_one) {
1232
+ columnData["slug"] = typeName;
1233
+ } else {
1234
+ columnData["slug"] = body["slug"];
1235
+ }
1236
+ for (const f of columnFields) {
1237
+ const val = body[f.name];
1238
+ columnData[f.db_column.column_name] = val !== void 0 ? val : null;
1239
+ }
1240
+ if (db) {
1241
+ const basePathId = await lookupBasePathId(db, contentType.default_base_path);
1242
+ if (!basePathId) {
1186
1243
  return c.json(
1187
1244
  {
1188
1245
  ok: false,
1189
1246
  error: {
1190
- code: "VALIDATION_ERROR",
1191
- message: "Required fields are missing",
1192
- details: fieldErrors
1247
+ code: "BASE_PATH_NOT_FOUND",
1248
+ message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
1193
1249
  }
1194
1250
  },
1195
- 422
1251
+ 500
1196
1252
  );
1197
1253
  }
1198
- if (body["published"] === true) {
1199
- const publishDeny = await requirePermission("content:edit")(c, async () => {
1200
- });
1201
- if (publishDeny) return publishDeny;
1202
- }
1203
- const paragraphFields = contentType.fields.filter((f) => f.db_column === null);
1204
- const junctionFields = contentType.fields.filter(
1205
- (f) => f.db_column !== null && f.db_column.junction !== void 0
1206
- );
1207
- const columnFields = contentType.fields.filter(
1208
- (f) => f.db_column !== null && f.db_column.junction === void 0
1209
- );
1210
- const columnData = {
1211
- published: body["published"] ?? false
1212
- };
1213
- if (contentType.only_one) {
1214
- columnData["slug"] = typeName;
1215
- } else {
1216
- columnData["slug"] = body["slug"];
1217
- }
1218
- for (const f of columnFields) {
1219
- const val = body[f.name];
1220
- columnData[f.db_column.column_name] = val !== void 0 ? val : null;
1254
+ columnData["base_path_id"] = basePathId;
1255
+ }
1256
+ const item = await repo.create(columnData);
1257
+ const itemId = item["id"];
1258
+ const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
1259
+ if (db) {
1260
+ for (const f of paragraphFields) {
1261
+ const comp = f.ui_component;
1262
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1263
+ const pType = registry.paragraph_types[comp.ref];
1264
+ if (!pType) continue;
1265
+ const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1266
+ mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
1221
1267
  }
1222
- if (db) {
1223
- const basePathId = await lookupBasePathId(db, contentType.default_base_path);
1224
- if (!basePathId) {
1225
- return c.json(
1226
- {
1227
- ok: false,
1228
- error: {
1229
- code: "BASE_PATH_NOT_FOUND",
1230
- message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
1231
- }
1232
- },
1233
- 500
1234
- );
1235
- }
1236
- columnData["base_path_id"] = basePathId;
1268
+ for (const f of junctionFields) {
1269
+ const junction = f.db_column.junction;
1270
+ const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
1271
+ await persistJunctionField(db, itemId, junction, relatedIds);
1237
1272
  }
1238
- const item = await repo.create(columnData);
1239
- const itemId = item["id"];
1240
- const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
1241
- if (db) {
1242
- for (const f of paragraphFields) {
1243
- const comp = f.ui_component;
1244
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1245
- const pType = registry.paragraph_types[comp.ref];
1246
- if (!pType) continue;
1247
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1248
- mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items));
1249
- }
1250
- for (const f of junctionFields) {
1251
- const junction = f.db_column.junction;
1252
- const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
1253
- await persistJunctionField(db, itemId, junction, relatedIds);
1254
- }
1255
- }
1256
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1257
- return c.json({ ok: true, data: item }, 201);
1258
1273
  }
1259
- );
1274
+ await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1275
+ return c.json({ ok: true, data: item }, 201);
1276
+ };
1260
1277
  app.patch(
1261
1278
  `/admin/api/${basePath}/:id`,
1262
1279
  requirePermission("content:edit"),
@@ -1311,71 +1328,92 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1311
1328
  );
1312
1329
  }
1313
1330
  }
1314
- if (body["published"] === true) {
1315
- const publishDeny = await requirePermission("content:edit")(c, async () => {
1316
- });
1317
- if (publishDeny) return publishDeny;
1318
- const merged = { ...existing, ...body };
1319
- const fieldErrors = checkRequiredFields(requiredFields, merged);
1320
- if (fieldErrors.length > 0) {
1321
- return c.json(
1322
- {
1323
- ok: false,
1324
- error: {
1325
- code: "PUBLISH_VALIDATION_ERROR",
1326
- message: "Cannot publish \u2014 required fields are missing",
1327
- details: fieldErrors
1328
- }
1329
- },
1330
- 422
1331
- );
1332
- }
1331
+ return writeExistingItem(c, id, existing, body);
1332
+ }
1333
+ );
1334
+ const writeExistingItem = async (c, id, existing, body) => {
1335
+ if (body["published"] === true) {
1336
+ const publishDeny = await requirePermission("content:edit")(c, async () => {
1337
+ });
1338
+ if (publishDeny) return publishDeny;
1339
+ const merged = { ...existing, ...body };
1340
+ const fieldErrors = checkRequiredFields(requiredFields, merged);
1341
+ if (fieldErrors.length > 0) {
1342
+ return c.json(
1343
+ {
1344
+ ok: false,
1345
+ error: {
1346
+ code: "PUBLISH_VALIDATION_ERROR",
1347
+ message: "Cannot publish \u2014 required fields are missing",
1348
+ details: fieldErrors
1349
+ }
1350
+ },
1351
+ 422
1352
+ );
1333
1353
  }
1334
- const patchParagraphFields = contentType.fields.filter((f) => f.db_column === null);
1335
- const patchJunctionFields = contentType.fields.filter(
1336
- (f) => f.db_column !== null && f.db_column.junction !== void 0
1337
- );
1338
- const patchColumnFields = contentType.fields.filter(
1339
- (f) => f.db_column !== null && f.db_column.junction === void 0
1354
+ }
1355
+ const patchParagraphFields = contentType.fields.filter((f) => f.db_column === null);
1356
+ const patchJunctionFields = contentType.fields.filter(
1357
+ (f) => f.db_column !== null && f.db_column.junction !== void 0
1358
+ );
1359
+ const patchColumnFields = contentType.fields.filter(
1360
+ (f) => f.db_column !== null && f.db_column.junction === void 0
1361
+ );
1362
+ const patchData = {};
1363
+ if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
1364
+ if ("published" in body) patchData["published"] = body["published"];
1365
+ for (const f of patchColumnFields) {
1366
+ if (f.name in body) {
1367
+ const val = body[f.name];
1368
+ patchData[f.db_column.column_name] = val !== void 0 ? val : null;
1369
+ }
1370
+ }
1371
+ const updated = await repo.update(id, patchData);
1372
+ if (!updated) {
1373
+ return c.json(
1374
+ { ok: false, error: { code: "NOT_FOUND", message: "Not found" } },
1375
+ 404
1340
1376
  );
1341
- const patchData = {};
1342
- if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
1343
- if ("published" in body) patchData["published"] = body["published"];
1344
- for (const f of patchColumnFields) {
1345
- if (f.name in body) {
1346
- const val = body[f.name];
1347
- patchData[f.db_column.column_name] = val !== void 0 ? val : null;
1348
- }
1377
+ }
1378
+ const mediaDeltas = [
1379
+ topLevelMediaDelta(mediaFields, existing, body)
1380
+ ];
1381
+ if (db) {
1382
+ for (const f of patchParagraphFields) {
1383
+ const comp = f.ui_component;
1384
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1385
+ const pType = registry.paragraph_types[comp.ref];
1386
+ if (!pType) continue;
1387
+ const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1388
+ mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
1349
1389
  }
1350
- const updated = await repo.update(id, patchData);
1351
- if (!updated) {
1352
- return c.json(
1353
- { ok: false, error: { code: "NOT_FOUND", message: "Not found" } },
1354
- 404
1355
- );
1390
+ for (const f of patchJunctionFields) {
1391
+ const junction = f.db_column.junction;
1392
+ const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
1393
+ await persistJunctionField(db, id, junction, relatedIds);
1356
1394
  }
1357
- const mediaDeltas = [
1358
- topLevelMediaDelta(mediaFields, existing, body)
1359
- ];
1360
- if (db) {
1361
- for (const f of patchParagraphFields) {
1362
- const comp = f.ui_component;
1363
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1364
- const pType = registry.paragraph_types[comp.ref];
1365
- if (!pType) continue;
1366
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1367
- mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items));
1368
- }
1369
- for (const f of patchJunctionFields) {
1370
- const junction = f.db_column.junction;
1371
- const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
1372
- await persistJunctionField(db, id, junction, relatedIds);
1395
+ }
1396
+ await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1397
+ return c.json({ ok: true, data: updated });
1398
+ };
1399
+ if (contentType.only_one) {
1400
+ app.put(
1401
+ `/admin/api/${basePath}`,
1402
+ requirePermission("content:edit"),
1403
+ async (c) => {
1404
+ const body = await c.req.json();
1405
+ const existingResult = await repo.findMany({ page: 1, per_page: 1 });
1406
+ const existing = existingResult.data[0];
1407
+ if (!existing) {
1408
+ const createDeny = await requirePermission("content:create")(c, async () => {
1409
+ });
1410
+ if (createDeny) return createDeny;
1411
+ return writeNewItem(c, body);
1373
1412
  }
1413
+ return writeExistingItem(c, existing["id"], existing, body);
1374
1414
  }
1375
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1376
- return c.json({ ok: true, data: updated });
1377
- }
1378
- );
1415
+ );
1416
+ }
1379
1417
  app.delete(
1380
1418
  `/admin/api/${basePath}/:id`,
1381
1419
  requirePermission("content:delete"),
@@ -1397,7 +1435,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1397
1435
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1398
1436
  const pType = registry.paragraph_types[comp.ref];
1399
1437
  if (!pType) continue;
1400
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
1438
+ mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
1401
1439
  }
1402
1440
  }
1403
1441
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -1512,7 +1550,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1512
1550
  const pType = registry.paragraph_types[comp.ref];
1513
1551
  if (!pType) continue;
1514
1552
  const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1515
- mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items));
1553
+ mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
1516
1554
  }
1517
1555
  }
1518
1556
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -1581,7 +1619,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1581
1619
  const pType = registry.paragraph_types[comp.ref];
1582
1620
  if (!pType) continue;
1583
1621
  const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1584
- mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items));
1622
+ mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
1585
1623
  }
1586
1624
  }
1587
1625
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -1609,7 +1647,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1609
1647
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1610
1648
  const pType = registry.paragraph_types[comp.ref];
1611
1649
  if (!pType) continue;
1612
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
1650
+ mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
1613
1651
  }
1614
1652
  }
1615
1653
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -2577,7 +2615,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2577
2615
  import { sql as sql5 } from "drizzle-orm";
2578
2616
  var API_VERSION = (() => {
2579
2617
  try {
2580
- return "0.3.0";
2618
+ return "0.3.3";
2581
2619
  } catch {
2582
2620
  return "0.0.0";
2583
2621
  }
@@ -3055,6 +3093,16 @@ function createCmsApp(options) {
3055
3093
  ])
3056
3094
  );
3057
3095
  const publicRepos = { ...publicContentRepos, ...publicTaxonomyRepos };
3096
+ const graphqlRepos = Object.fromEntries([
3097
+ ...Object.entries(registry.content_types).map(([typeName, ct]) => [
3098
+ typeName,
3099
+ createDrizzleContentRepository(db, ct.db.table_name)
3100
+ ]),
3101
+ ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3102
+ typeName,
3103
+ createDrizzleContentRepository(db, tt.db.table_name)
3104
+ ])
3105
+ ]);
3058
3106
  registerAuthRoutes(app, db);
3059
3107
  app.use("/admin/api/*", authMiddleware);
3060
3108
  app.use("/admin/api/*", mustChangePasswordCheck);
@@ -3095,8 +3143,8 @@ function createCmsApp(options) {
3095
3143
  const gqlOptions = options.graphql;
3096
3144
  let gqlHandler = null;
3097
3145
  let initError = null;
3098
- const ready = import("./handler-GMCFYZY5.mjs").then(({ createGraphQLHandler }) => {
3099
- gqlHandler = createGraphQLHandler(registry, publicRepos, programmaticResolver, db, gqlOptions);
3146
+ const ready = import("./handler-CGHZG3J5.mjs").then(({ createGraphQLHandler }) => {
3147
+ gqlHandler = createGraphQLHandler(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
3100
3148
  }).catch((err) => {
3101
3149
  initError = err;
3102
3150
  const message = err instanceof Error ? err.message : String(err);