@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.js CHANGED
@@ -57,16 +57,46 @@ async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
57
57
  }
58
58
  return ids;
59
59
  }
60
- async function persistParagraphField(db, itemId, parentType, fieldName, pType, items) {
60
+ function nestedParagraphFields(pType, registry) {
61
+ if (!registry) return [];
62
+ const out = [];
63
+ for (const f of pType.fields) {
64
+ if (f.db_column !== null) continue;
65
+ const comp = f.ui_component;
66
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
67
+ const nType = registry.paragraph_types[comp.ref];
68
+ if (nType) out.push({ fieldName: f.name, nType });
69
+ }
70
+ return out;
71
+ }
72
+ async function fetchParagraphRowIds(db, pType, itemId, fieldName) {
73
+ const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
74
+ const result = await db.execute(
75
+ import_drizzle_orm2.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
76
+ );
77
+ return result.rows.map((r) => r["id"]).filter((id) => typeof id === "string");
78
+ }
79
+ async function persistParagraphField(db, itemId, parentType, fieldName, pType, items, registry) {
61
80
  const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
81
+ const nested = nestedParagraphFields(pType, registry);
82
+ if (nested.length > 0) {
83
+ const staleIds = await fetchParagraphRowIds(db, pType, itemId, fieldName);
84
+ for (const rowId of staleIds) {
85
+ for (const n of nested) {
86
+ const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry);
87
+ removed.push(...d.removed);
88
+ }
89
+ }
90
+ }
62
91
  const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
63
92
  await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
64
93
  const mediaFieldNames = paragraphMediaFieldNames(pType);
65
94
  const added = [];
66
95
  for (let i = 0; i < items.length; i++) {
67
96
  const pItem = items[i];
97
+ const rowId = import_node_crypto.default.randomUUID();
68
98
  const data = {
69
- id: import_node_crypto.default.randomUUID(),
99
+ id: rowId,
70
100
  parent_id: itemId,
71
101
  parent_type: parentType,
72
102
  parent_field: fieldName,
@@ -85,11 +115,35 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
85
115
  const val = pItem[name];
86
116
  if (typeof val === "string" && val !== "") added.push(val);
87
117
  }
118
+ for (const n of nested) {
119
+ const nestedItems = Array.isArray(pItem[n.fieldName]) ? pItem[n.fieldName] : [];
120
+ const d = await persistParagraphField(
121
+ db,
122
+ rowId,
123
+ pType.db.table_name,
124
+ n.fieldName,
125
+ n.nType,
126
+ nestedItems,
127
+ registry
128
+ );
129
+ added.push(...d.added);
130
+ removed.push(...d.removed);
131
+ }
88
132
  }
89
133
  return { added, removed };
90
134
  }
91
- async function deleteParagraphField(db, itemId, fieldName, pType) {
135
+ async function deleteParagraphField(db, itemId, fieldName, pType, registry) {
92
136
  const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
137
+ const nested = nestedParagraphFields(pType, registry);
138
+ if (nested.length > 0) {
139
+ const rowIds = await fetchParagraphRowIds(db, pType, itemId, fieldName);
140
+ for (const rowId of rowIds) {
141
+ for (const n of nested) {
142
+ const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry);
143
+ removed.push(...d.removed);
144
+ }
145
+ }
146
+ }
93
147
  const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
94
148
  await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
95
149
  return { added: [], removed };
@@ -152,7 +206,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
152
206
  const parentIds = rows.map((r) => r["id"]);
153
207
  const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
154
208
  const result = await db.execute(
155
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
209
+ import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
156
210
  );
157
211
  const byParent = groupBy(result.rows, "parent_id");
158
212
  for (const row of rows) {
@@ -232,7 +286,7 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
232
286
  const parentIds = rows.map((r) => r["id"]);
233
287
  const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
234
288
  const result = await db.execute(
235
- import_drizzle_orm2.sql`SELECT id, parent_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
289
+ import_drizzle_orm2.sql`SELECT id, parent_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
236
290
  );
237
291
  const byParent = groupBy(result.rows, "parent_id");
238
292
  for (const row of rows) {
@@ -493,16 +547,22 @@ function scalarFieldResolver(schemaName) {
493
547
  function relationFieldResolver(typeName, schemaFieldName) {
494
548
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
495
549
  }
496
- function programmaticFieldResolver(typeName, schemaFieldName) {
550
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = []) {
497
551
  return async (parent, _args, ctx) => {
498
552
  let p = ctx.programmaticMemo.get(parent);
499
553
  if (!p) {
500
- p = ctx.resolver.resolveItem(typeName, parent);
554
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames);
501
555
  ctx.programmaticMemo.set(parent, p);
502
556
  }
503
557
  return (await p)[schemaFieldName];
504
558
  };
505
559
  }
560
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames) {
561
+ if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, parent);
562
+ const enriched = { ...parent };
563
+ await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
564
+ return ctx.resolver.resolveItem(typeName, enriched);
565
+ }
506
566
  function collectionResolver(typeName, nameMap) {
507
567
  return async (_root, args, ctx) => {
508
568
  const page = args.page ?? 1;
@@ -613,6 +673,7 @@ function buildGraphQLSchema(registry) {
613
673
  return import_graphql5.GraphQLString;
614
674
  }
615
675
  function buildObjectType(machineName, type) {
676
+ const mediaFieldNames = type.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name);
616
677
  return new import_graphql5.GraphQLObjectType({
617
678
  name: graphqlTypeName(machineName),
618
679
  fields: () => {
@@ -631,7 +692,7 @@ function buildGraphQLSchema(registry) {
631
692
  const outType = outputTypeForField(field);
632
693
  let resolve;
633
694
  if (field.field_type === "programmatic") {
634
- resolve = programmaticFieldResolver(machineName, field.name);
695
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames);
635
696
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
636
697
  resolve = relationFieldResolver(machineName, field.name);
637
698
  } else {
@@ -845,6 +906,14 @@ var handler_exports = {};
845
906
  __export(handler_exports, {
846
907
  createGraphQLHandler: () => createGraphQLHandler
847
908
  });
909
+ function isClientSafeGraphQLError(error) {
910
+ let current = error;
911
+ while (current != null && typeof current === "object" && current.name === "GraphQLError") {
912
+ if (current.originalError == null) return true;
913
+ current = current.originalError;
914
+ }
915
+ return false;
916
+ }
848
917
  function createGraphQLHandler(registry, repos, resolver, db, options) {
849
918
  const schema = buildGraphQLSchema(registry);
850
919
  const { plugins } = buildArmorPlugin({
@@ -856,6 +925,9 @@ function createGraphQLHandler(registry, repos, resolver, db, options) {
856
925
  graphqlEndpoint: "/graphql",
857
926
  graphiql: options.graphiql,
858
927
  landingPage: false,
928
+ maskedErrors: {
929
+ maskError: (error, message, isDev) => isClientSafeGraphQLError(error) ? error : (0, import_graphql_yoga.maskError)(error, message, isDev)
930
+ },
859
931
  // buildArmorPlugin's return type is `unknown[]` (see security.ts) since Armor's
860
932
  // plugin shapes aren't part of its public typings; Yoga's `plugins` option only
861
933
  // accepts its own `Plugin` type, so the cast bridges the two.
@@ -1841,6 +1913,23 @@ function quoteIdent2(name) {
1841
1913
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1842
1914
  return `"${name}"`;
1843
1915
  }
1916
+ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
1917
+ const result = await db.execute(
1918
+ import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
1919
+ );
1920
+ const rows = result.rows;
1921
+ for (const nf of pType.fields) {
1922
+ if (nf.db_column !== null) continue;
1923
+ const comp = nf.ui_component;
1924
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1925
+ const nType = registry.paragraph_types[comp.ref];
1926
+ if (!nType) continue;
1927
+ for (const row of rows) {
1928
+ row[nf.name] = await loadParagraphRows(db, registry, nType, row["id"], nf.name);
1929
+ }
1930
+ }
1931
+ return rows;
1932
+ }
1844
1933
  async function lookupBasePathId(db, pathOrName) {
1845
1934
  const r = await db.execute(
1846
1935
  import_drizzle_orm3.sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
@@ -1992,10 +2081,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1992
2081
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1993
2082
  const pType = registry.paragraph_types[comp.ref];
1994
2083
  if (!pType) continue;
1995
- const r = await db.execute(
1996
- import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${id} AND parent_field = ${f.name} ORDER BY "order" ASC`
1997
- );
1998
- row[f.name] = r.rows;
2084
+ row[f.name] = await loadParagraphRows(db, registry, pType, id, f.name);
1999
2085
  } else if (f.db_column.junction) {
2000
2086
  const j = f.db_column.junction;
2001
2087
  const r = await db.execute(
@@ -2070,82 +2156,85 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2070
2156
  );
2071
2157
  }
2072
2158
  }
2073
- const fieldErrors = checkRequiredFields(requiredFields, body);
2074
- if (fieldErrors.length > 0) {
2159
+ return writeNewItem(c, body);
2160
+ }
2161
+ );
2162
+ const writeNewItem = async (c, body) => {
2163
+ const fieldErrors = checkRequiredFields(requiredFields, body);
2164
+ if (fieldErrors.length > 0) {
2165
+ return c.json(
2166
+ {
2167
+ ok: false,
2168
+ error: {
2169
+ code: "VALIDATION_ERROR",
2170
+ message: "Required fields are missing",
2171
+ details: fieldErrors
2172
+ }
2173
+ },
2174
+ 422
2175
+ );
2176
+ }
2177
+ if (body["published"] === true) {
2178
+ const publishDeny = await requirePermission("content:edit")(c, async () => {
2179
+ });
2180
+ if (publishDeny) return publishDeny;
2181
+ }
2182
+ const paragraphFields = contentType.fields.filter((f) => f.db_column === null);
2183
+ const junctionFields = contentType.fields.filter(
2184
+ (f) => f.db_column !== null && f.db_column.junction !== void 0
2185
+ );
2186
+ const columnFields = contentType.fields.filter(
2187
+ (f) => f.db_column !== null && f.db_column.junction === void 0
2188
+ );
2189
+ const columnData = {
2190
+ published: body["published"] ?? false
2191
+ };
2192
+ if (contentType.only_one) {
2193
+ columnData["slug"] = typeName;
2194
+ } else {
2195
+ columnData["slug"] = body["slug"];
2196
+ }
2197
+ for (const f of columnFields) {
2198
+ const val = body[f.name];
2199
+ columnData[f.db_column.column_name] = val !== void 0 ? val : null;
2200
+ }
2201
+ if (db) {
2202
+ const basePathId = await lookupBasePathId(db, contentType.default_base_path);
2203
+ if (!basePathId) {
2075
2204
  return c.json(
2076
2205
  {
2077
2206
  ok: false,
2078
2207
  error: {
2079
- code: "VALIDATION_ERROR",
2080
- message: "Required fields are missing",
2081
- details: fieldErrors
2208
+ code: "BASE_PATH_NOT_FOUND",
2209
+ message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
2082
2210
  }
2083
2211
  },
2084
- 422
2212
+ 500
2085
2213
  );
2086
2214
  }
2087
- if (body["published"] === true) {
2088
- const publishDeny = await requirePermission("content:edit")(c, async () => {
2089
- });
2090
- if (publishDeny) return publishDeny;
2091
- }
2092
- const paragraphFields = contentType.fields.filter((f) => f.db_column === null);
2093
- const junctionFields = contentType.fields.filter(
2094
- (f) => f.db_column !== null && f.db_column.junction !== void 0
2095
- );
2096
- const columnFields = contentType.fields.filter(
2097
- (f) => f.db_column !== null && f.db_column.junction === void 0
2098
- );
2099
- const columnData = {
2100
- published: body["published"] ?? false
2101
- };
2102
- if (contentType.only_one) {
2103
- columnData["slug"] = typeName;
2104
- } else {
2105
- columnData["slug"] = body["slug"];
2106
- }
2107
- for (const f of columnFields) {
2108
- const val = body[f.name];
2109
- columnData[f.db_column.column_name] = val !== void 0 ? val : null;
2215
+ columnData["base_path_id"] = basePathId;
2216
+ }
2217
+ const item = await repo.create(columnData);
2218
+ const itemId = item["id"];
2219
+ const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
2220
+ if (db) {
2221
+ for (const f of paragraphFields) {
2222
+ const comp = f.ui_component;
2223
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2224
+ const pType = registry.paragraph_types[comp.ref];
2225
+ if (!pType) continue;
2226
+ const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2227
+ mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
2110
2228
  }
2111
- if (db) {
2112
- const basePathId = await lookupBasePathId(db, contentType.default_base_path);
2113
- if (!basePathId) {
2114
- return c.json(
2115
- {
2116
- ok: false,
2117
- error: {
2118
- code: "BASE_PATH_NOT_FOUND",
2119
- message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
2120
- }
2121
- },
2122
- 500
2123
- );
2124
- }
2125
- columnData["base_path_id"] = basePathId;
2229
+ for (const f of junctionFields) {
2230
+ const junction = f.db_column.junction;
2231
+ const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
2232
+ await persistJunctionField(db, itemId, junction, relatedIds);
2126
2233
  }
2127
- const item = await repo.create(columnData);
2128
- const itemId = item["id"];
2129
- const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
2130
- if (db) {
2131
- for (const f of paragraphFields) {
2132
- const comp = f.ui_component;
2133
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2134
- const pType = registry.paragraph_types[comp.ref];
2135
- if (!pType) continue;
2136
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2137
- mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items));
2138
- }
2139
- for (const f of junctionFields) {
2140
- const junction = f.db_column.junction;
2141
- const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
2142
- await persistJunctionField(db, itemId, junction, relatedIds);
2143
- }
2144
- }
2145
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2146
- return c.json({ ok: true, data: item }, 201);
2147
2234
  }
2148
- );
2235
+ await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2236
+ return c.json({ ok: true, data: item }, 201);
2237
+ };
2149
2238
  app.patch(
2150
2239
  `/admin/api/${basePath}/:id`,
2151
2240
  requirePermission("content:edit"),
@@ -2200,71 +2289,92 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2200
2289
  );
2201
2290
  }
2202
2291
  }
2203
- if (body["published"] === true) {
2204
- const publishDeny = await requirePermission("content:edit")(c, async () => {
2205
- });
2206
- if (publishDeny) return publishDeny;
2207
- const merged = { ...existing, ...body };
2208
- const fieldErrors = checkRequiredFields(requiredFields, merged);
2209
- if (fieldErrors.length > 0) {
2210
- return c.json(
2211
- {
2212
- ok: false,
2213
- error: {
2214
- code: "PUBLISH_VALIDATION_ERROR",
2215
- message: "Cannot publish \u2014 required fields are missing",
2216
- details: fieldErrors
2217
- }
2218
- },
2219
- 422
2220
- );
2221
- }
2292
+ return writeExistingItem(c, id, existing, body);
2293
+ }
2294
+ );
2295
+ const writeExistingItem = async (c, id, existing, body) => {
2296
+ if (body["published"] === true) {
2297
+ const publishDeny = await requirePermission("content:edit")(c, async () => {
2298
+ });
2299
+ if (publishDeny) return publishDeny;
2300
+ const merged = { ...existing, ...body };
2301
+ const fieldErrors = checkRequiredFields(requiredFields, merged);
2302
+ if (fieldErrors.length > 0) {
2303
+ return c.json(
2304
+ {
2305
+ ok: false,
2306
+ error: {
2307
+ code: "PUBLISH_VALIDATION_ERROR",
2308
+ message: "Cannot publish \u2014 required fields are missing",
2309
+ details: fieldErrors
2310
+ }
2311
+ },
2312
+ 422
2313
+ );
2222
2314
  }
2223
- const patchParagraphFields = contentType.fields.filter((f) => f.db_column === null);
2224
- const patchJunctionFields = contentType.fields.filter(
2225
- (f) => f.db_column !== null && f.db_column.junction !== void 0
2226
- );
2227
- const patchColumnFields = contentType.fields.filter(
2228
- (f) => f.db_column !== null && f.db_column.junction === void 0
2315
+ }
2316
+ const patchParagraphFields = contentType.fields.filter((f) => f.db_column === null);
2317
+ const patchJunctionFields = contentType.fields.filter(
2318
+ (f) => f.db_column !== null && f.db_column.junction !== void 0
2319
+ );
2320
+ const patchColumnFields = contentType.fields.filter(
2321
+ (f) => f.db_column !== null && f.db_column.junction === void 0
2322
+ );
2323
+ const patchData = {};
2324
+ if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
2325
+ if ("published" in body) patchData["published"] = body["published"];
2326
+ for (const f of patchColumnFields) {
2327
+ if (f.name in body) {
2328
+ const val = body[f.name];
2329
+ patchData[f.db_column.column_name] = val !== void 0 ? val : null;
2330
+ }
2331
+ }
2332
+ const updated = await repo.update(id, patchData);
2333
+ if (!updated) {
2334
+ return c.json(
2335
+ { ok: false, error: { code: "NOT_FOUND", message: "Not found" } },
2336
+ 404
2229
2337
  );
2230
- const patchData = {};
2231
- if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
2232
- if ("published" in body) patchData["published"] = body["published"];
2233
- for (const f of patchColumnFields) {
2234
- if (f.name in body) {
2235
- const val = body[f.name];
2236
- patchData[f.db_column.column_name] = val !== void 0 ? val : null;
2237
- }
2338
+ }
2339
+ const mediaDeltas = [
2340
+ topLevelMediaDelta(mediaFields, existing, body)
2341
+ ];
2342
+ if (db) {
2343
+ for (const f of patchParagraphFields) {
2344
+ const comp = f.ui_component;
2345
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2346
+ const pType = registry.paragraph_types[comp.ref];
2347
+ if (!pType) continue;
2348
+ const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2349
+ mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
2238
2350
  }
2239
- const updated = await repo.update(id, patchData);
2240
- if (!updated) {
2241
- return c.json(
2242
- { ok: false, error: { code: "NOT_FOUND", message: "Not found" } },
2243
- 404
2244
- );
2351
+ for (const f of patchJunctionFields) {
2352
+ const junction = f.db_column.junction;
2353
+ const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
2354
+ await persistJunctionField(db, id, junction, relatedIds);
2245
2355
  }
2246
- const mediaDeltas = [
2247
- topLevelMediaDelta(mediaFields, existing, body)
2248
- ];
2249
- if (db) {
2250
- for (const f of patchParagraphFields) {
2251
- const comp = f.ui_component;
2252
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2253
- const pType = registry.paragraph_types[comp.ref];
2254
- if (!pType) continue;
2255
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2256
- mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items));
2257
- }
2258
- for (const f of patchJunctionFields) {
2259
- const junction = f.db_column.junction;
2260
- const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
2261
- await persistJunctionField(db, id, junction, relatedIds);
2356
+ }
2357
+ await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2358
+ return c.json({ ok: true, data: updated });
2359
+ };
2360
+ if (contentType.only_one) {
2361
+ app.put(
2362
+ `/admin/api/${basePath}`,
2363
+ requirePermission("content:edit"),
2364
+ async (c) => {
2365
+ const body = await c.req.json();
2366
+ const existingResult = await repo.findMany({ page: 1, per_page: 1 });
2367
+ const existing = existingResult.data[0];
2368
+ if (!existing) {
2369
+ const createDeny = await requirePermission("content:create")(c, async () => {
2370
+ });
2371
+ if (createDeny) return createDeny;
2372
+ return writeNewItem(c, body);
2262
2373
  }
2374
+ return writeExistingItem(c, existing["id"], existing, body);
2263
2375
  }
2264
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2265
- return c.json({ ok: true, data: updated });
2266
- }
2267
- );
2376
+ );
2377
+ }
2268
2378
  app.delete(
2269
2379
  `/admin/api/${basePath}/:id`,
2270
2380
  requirePermission("content:delete"),
@@ -2286,7 +2396,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2286
2396
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2287
2397
  const pType = registry.paragraph_types[comp.ref];
2288
2398
  if (!pType) continue;
2289
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
2399
+ mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
2290
2400
  }
2291
2401
  }
2292
2402
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -2401,7 +2511,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2401
2511
  const pType = registry.paragraph_types[comp.ref];
2402
2512
  if (!pType) continue;
2403
2513
  const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2404
- mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items));
2514
+ mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
2405
2515
  }
2406
2516
  }
2407
2517
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -2470,7 +2580,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2470
2580
  const pType = registry.paragraph_types[comp.ref];
2471
2581
  if (!pType) continue;
2472
2582
  const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2473
- mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items));
2583
+ mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
2474
2584
  }
2475
2585
  }
2476
2586
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -2498,7 +2608,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
2498
2608
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2499
2609
  const pType = registry.paragraph_types[comp.ref];
2500
2610
  if (!pType) continue;
2501
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
2611
+ mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
2502
2612
  }
2503
2613
  }
2504
2614
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -3466,7 +3576,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3466
3576
  var import_drizzle_orm6 = require("drizzle-orm");
3467
3577
  var API_VERSION = (() => {
3468
3578
  try {
3469
- return "0.3.0";
3579
+ return "0.3.3";
3470
3580
  } catch {
3471
3581
  return "0.0.0";
3472
3582
  }
@@ -3948,6 +4058,16 @@ function createCmsApp(options) {
3948
4058
  ])
3949
4059
  );
3950
4060
  const publicRepos = { ...publicContentRepos, ...publicTaxonomyRepos };
4061
+ const graphqlRepos = Object.fromEntries([
4062
+ ...Object.entries(registry.content_types).map(([typeName, ct]) => [
4063
+ typeName,
4064
+ createDrizzleContentRepository(db, ct.db.table_name)
4065
+ ]),
4066
+ ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
4067
+ typeName,
4068
+ createDrizzleContentRepository(db, tt.db.table_name)
4069
+ ])
4070
+ ]);
3951
4071
  registerAuthRoutes(app, db);
3952
4072
  app.use("/admin/api/*", authMiddleware);
3953
4073
  app.use("/admin/api/*", mustChangePasswordCheck);
@@ -3989,7 +4109,7 @@ function createCmsApp(options) {
3989
4109
  let gqlHandler = null;
3990
4110
  let initError = null;
3991
4111
  const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
3992
- gqlHandler = createGraphQLHandler2(registry, publicRepos, programmaticResolver, db, gqlOptions);
4112
+ gqlHandler = createGraphQLHandler2(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
3993
4113
  }).catch((err) => {
3994
4114
  initError = err;
3995
4115
  const message = err instanceof Error ? err.message : String(err);