@bobbykim/manguito-cms-api 0.9.2 → 0.9.4
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/{chunk-ITOSMGDT.mjs → chunk-DHOVO42E.mjs} +2 -2
- package/dist/{chunk-GUZZQZSN.mjs → chunk-EFTMAG7N.mjs} +207 -46
- package/dist/chunk-EFTMAG7N.mjs.map +1 -0
- package/dist/graphql/index.js +31 -16
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-5OTVPIAF.mjs +8 -0
- package/dist/index.js +349 -287
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +229 -320
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-GUZZQZSN.mjs.map +0 -1
- package/dist/handler-MFF32JB4.mjs +0 -8
- /package/dist/{chunk-ITOSMGDT.mjs.map → chunk-DHOVO42E.mjs.map} +0 -0
- /package/dist/{handler-MFF32JB4.mjs.map → handler-5OTVPIAF.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -143,8 +143,139 @@ var init_field_keys = __esm({
|
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
// src/relation-input.ts
|
|
147
|
+
function isUuid(v) {
|
|
148
|
+
return typeof v === "string" && UUID_RE.test(v);
|
|
149
|
+
}
|
|
150
|
+
function isPlainObject(v) {
|
|
151
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
152
|
+
}
|
|
153
|
+
function checkRelationInput(fields, body, registry, path = "") {
|
|
154
|
+
const errors = [];
|
|
155
|
+
for (const field of fields) {
|
|
156
|
+
if (!Object.hasOwn(body, field.name)) continue;
|
|
157
|
+
const cardinality = (0, import_manguito_cms_core.relationCardinality)(field);
|
|
158
|
+
if (cardinality === null) continue;
|
|
159
|
+
const value = body[field.name];
|
|
160
|
+
const name = `${path}${field.name}`;
|
|
161
|
+
if (field.field_type === "reference") {
|
|
162
|
+
const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
|
|
163
|
+
if (!ok) {
|
|
164
|
+
errors.push({
|
|
165
|
+
field: name,
|
|
166
|
+
message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
|
|
172
|
+
if (items === void 0) {
|
|
173
|
+
errors.push({
|
|
174
|
+
field: name,
|
|
175
|
+
message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const pType = paragraphTypeOf(field, registry);
|
|
180
|
+
if (!pType) continue;
|
|
181
|
+
items.forEach((item, i) => {
|
|
182
|
+
const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
|
|
183
|
+
errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return errors;
|
|
187
|
+
}
|
|
188
|
+
function paragraphTypeOf(field, registry) {
|
|
189
|
+
const comp = field.ui_component;
|
|
190
|
+
return comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
|
|
191
|
+
}
|
|
192
|
+
function paragraphItemsIn(fields, body, registry, path) {
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const field of fields) {
|
|
195
|
+
if (field.field_type !== "paragraph" || !Object.hasOwn(body, field.name)) continue;
|
|
196
|
+
const pType = paragraphTypeOf(field, registry);
|
|
197
|
+
if (!pType) continue;
|
|
198
|
+
const value = body[field.name];
|
|
199
|
+
const name = `${path}${field.name}`;
|
|
200
|
+
if ((0, import_manguito_cms_core.relationCardinality)(field) === "one") {
|
|
201
|
+
if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
|
|
202
|
+
} else if (Array.isArray(value)) {
|
|
203
|
+
value.forEach((item, i) => {
|
|
204
|
+
if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
function isMissing(value) {
|
|
211
|
+
if (value === null || value === void 0) return true;
|
|
212
|
+
if (typeof value === "string" && value.trim() === "") return true;
|
|
213
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
function checkRequiredInParagraphItems(fields, body, registry, path = "") {
|
|
217
|
+
const errors = [];
|
|
218
|
+
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
219
|
+
for (const f of itemFields) {
|
|
220
|
+
if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
|
|
221
|
+
if (isMissing(item[f.name])) {
|
|
222
|
+
errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
|
|
226
|
+
}
|
|
227
|
+
return errors;
|
|
228
|
+
}
|
|
229
|
+
function quoteIdent(name) {
|
|
230
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
231
|
+
return `"${name}"`;
|
|
232
|
+
}
|
|
233
|
+
function collectReferences(fields, body, registry, path, out) {
|
|
234
|
+
for (const f of fields) {
|
|
235
|
+
if (!Object.hasOwn(body, f.name) || !f.db_column) continue;
|
|
236
|
+
const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
|
|
237
|
+
if (!table) continue;
|
|
238
|
+
const value = body[f.name];
|
|
239
|
+
const name = `${path}${f.name}`;
|
|
240
|
+
if (typeof value === "string") {
|
|
241
|
+
out.push({ table, id: value, field: name });
|
|
242
|
+
} else if (Array.isArray(value)) {
|
|
243
|
+
value.forEach((v, i) => {
|
|
244
|
+
if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
249
|
+
collectReferences(itemFields, item, registry, prefix, out);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function findMissingReferences(db, fields, body, registry) {
|
|
253
|
+
const uses = [];
|
|
254
|
+
collectReferences(fields, body, registry, "", uses);
|
|
255
|
+
if (uses.length === 0) return [];
|
|
256
|
+
const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
|
|
257
|
+
const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
|
|
258
|
+
if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
|
|
259
|
+
const result = await db.execute(
|
|
260
|
+
import_drizzle_orm2.sql`SELECT id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(table))} WHERE id IN (${import_drizzle_orm2.sql.join(ids.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `)})`
|
|
261
|
+
);
|
|
262
|
+
return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
|
|
263
|
+
});
|
|
264
|
+
const found = new Map(await Promise.all(lookups));
|
|
265
|
+
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.` }));
|
|
266
|
+
}
|
|
267
|
+
var import_drizzle_orm2, import_manguito_cms_core, UUID_RE;
|
|
268
|
+
var init_relation_input = __esm({
|
|
269
|
+
"src/relation-input.ts"() {
|
|
270
|
+
"use strict";
|
|
271
|
+
import_drizzle_orm2 = require("drizzle-orm");
|
|
272
|
+
import_manguito_cms_core = require("@bobbykim/manguito-cms-core");
|
|
273
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
146
277
|
// src/relations.ts
|
|
147
|
-
function
|
|
278
|
+
function quoteIdent2(name) {
|
|
148
279
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
149
280
|
return `"${name}"`;
|
|
150
281
|
}
|
|
@@ -156,10 +287,10 @@ function paragraphMediaFieldNames(pType) {
|
|
|
156
287
|
async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
|
|
157
288
|
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
289
|
if (mediaColumns.length === 0) return [];
|
|
159
|
-
const tbl =
|
|
160
|
-
const cols =
|
|
290
|
+
const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
|
|
291
|
+
const cols = import_drizzle_orm3.sql.join(mediaColumns.map((c) => import_drizzle_orm3.sql.raw(quoteIdent2(c))), import_drizzle_orm3.sql`, `);
|
|
161
292
|
const result = await db.execute(
|
|
162
|
-
|
|
293
|
+
import_drizzle_orm3.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
|
|
163
294
|
);
|
|
164
295
|
const ids = [];
|
|
165
296
|
for (const row of result.rows) {
|
|
@@ -175,20 +306,37 @@ function nestedParagraphFields(pType, registry) {
|
|
|
175
306
|
const out = [];
|
|
176
307
|
for (const f of pType.fields) {
|
|
177
308
|
if (f.db_column !== null) continue;
|
|
178
|
-
const
|
|
179
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
180
|
-
const nType = registry.paragraph_types[comp.ref];
|
|
309
|
+
const nType = paragraphTypeOf(f, registry);
|
|
181
310
|
if (nType) out.push({ fieldName: f.name, nType, field: f });
|
|
182
311
|
}
|
|
183
312
|
return out;
|
|
184
313
|
}
|
|
185
314
|
async function fetchParagraphRowIds(db, pType, itemId, fieldName) {
|
|
186
|
-
const tbl =
|
|
315
|
+
const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
|
|
187
316
|
const result = await db.execute(
|
|
188
|
-
|
|
317
|
+
import_drizzle_orm3.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
|
|
189
318
|
);
|
|
190
319
|
return result.rows.map((r) => r["id"]).filter((id) => typeof id === "string");
|
|
191
320
|
}
|
|
321
|
+
async function ownParagraphRowIds(db, registry, fields, itemId) {
|
|
322
|
+
const ids = [];
|
|
323
|
+
for (const f of fields) {
|
|
324
|
+
const pType = paragraphTypeOf(f, registry);
|
|
325
|
+
if (!pType) continue;
|
|
326
|
+
const rowIds = await fetchParagraphRowIds(db, pType, itemId, f.name);
|
|
327
|
+
ids.push(...rowIds);
|
|
328
|
+
if (rowIds.length === 0) continue;
|
|
329
|
+
for (const n of nestedParagraphFields(pType, registry)) {
|
|
330
|
+
const result = await db.execute(
|
|
331
|
+
import_drizzle_orm3.sql`SELECT id FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(n.nType.db.table_name))} WHERE parent_id = ANY(${import_drizzle_orm3.sql.param(rowIds)}::uuid[]) AND parent_field = ${n.fieldName}`
|
|
332
|
+
);
|
|
333
|
+
for (const r of result.rows) {
|
|
334
|
+
if (typeof r["id"] === "string") ids.push(r["id"]);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return ids;
|
|
339
|
+
}
|
|
192
340
|
async function persistParagraphField(db, itemId, parentType, fieldName, pType, items, registry) {
|
|
193
341
|
const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
|
|
194
342
|
const nested = nestedParagraphFields(pType, registry);
|
|
@@ -201,8 +349,8 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
|
|
|
201
349
|
}
|
|
202
350
|
}
|
|
203
351
|
}
|
|
204
|
-
const tbl =
|
|
205
|
-
await db.execute(
|
|
352
|
+
const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
|
|
353
|
+
await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
206
354
|
const mediaFieldNames = paragraphMediaFieldNames(pType);
|
|
207
355
|
const added = [];
|
|
208
356
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -221,9 +369,9 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
|
|
|
221
369
|
if (!pf.db_column || pf.db_column.junction) continue;
|
|
222
370
|
data[pf.db_column.column_name] = pItem[pf.name] ?? null;
|
|
223
371
|
}
|
|
224
|
-
const cols =
|
|
225
|
-
const vals =
|
|
226
|
-
await db.execute(
|
|
372
|
+
const cols = import_drizzle_orm3.sql.join(Object.keys(data).map((k) => import_drizzle_orm3.sql.raw(quoteIdent2(k))), import_drizzle_orm3.sql`, `);
|
|
373
|
+
const vals = import_drizzle_orm3.sql.join(Object.values(data).map((v) => import_drizzle_orm3.sql`${v}`), import_drizzle_orm3.sql`, `);
|
|
374
|
+
await db.execute(import_drizzle_orm3.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
|
|
227
375
|
for (const name of mediaFieldNames) {
|
|
228
376
|
const val = pItem[name];
|
|
229
377
|
if (typeof val === "string" && val !== "") added.push(val);
|
|
@@ -257,14 +405,14 @@ async function deleteParagraphField(db, itemId, fieldName, pType, registry) {
|
|
|
257
405
|
}
|
|
258
406
|
}
|
|
259
407
|
}
|
|
260
|
-
const tbl =
|
|
261
|
-
await db.execute(
|
|
408
|
+
const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
|
|
409
|
+
await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
262
410
|
return { added: [], removed };
|
|
263
411
|
}
|
|
264
412
|
async function persistJunctionField(db, itemId, junction, relatedIds) {
|
|
265
|
-
const tbl =
|
|
266
|
-
const left =
|
|
267
|
-
const right =
|
|
413
|
+
const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(junction.table_name));
|
|
414
|
+
const left = import_drizzle_orm3.sql.raw(quoteIdent2(junction.left_column));
|
|
415
|
+
const right = import_drizzle_orm3.sql.raw(quoteIdent2(junction.right_column));
|
|
268
416
|
const seen = /* @__PURE__ */ new Set();
|
|
269
417
|
const ids = relatedIds.filter((id) => {
|
|
270
418
|
const key = id.toLowerCase();
|
|
@@ -272,27 +420,36 @@ async function persistJunctionField(db, itemId, junction, relatedIds) {
|
|
|
272
420
|
seen.add(key);
|
|
273
421
|
return true;
|
|
274
422
|
});
|
|
275
|
-
await db.execute(
|
|
423
|
+
await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
|
|
276
424
|
if (ids.length === 0) return;
|
|
277
|
-
const rows =
|
|
425
|
+
const rows = import_drizzle_orm3.sql.join(
|
|
278
426
|
ids.map(
|
|
279
|
-
(rid, position) => junction.order_column ?
|
|
427
|
+
(rid, position) => junction.order_column ? import_drizzle_orm3.sql`(${itemId}, ${rid}, ${position})` : import_drizzle_orm3.sql`(${itemId}, ${rid})`
|
|
280
428
|
),
|
|
281
|
-
|
|
429
|
+
import_drizzle_orm3.sql`, `
|
|
282
430
|
);
|
|
283
431
|
await db.execute(
|
|
284
|
-
junction.order_column ?
|
|
432
|
+
junction.order_column ? import_drizzle_orm3.sql`INSERT INTO ${tbl} (${left}, ${right}, "order") VALUES ${rows} ON CONFLICT DO NOTHING` : import_drizzle_orm3.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES ${rows} ON CONFLICT DO NOTHING`
|
|
285
433
|
);
|
|
286
434
|
}
|
|
287
435
|
function junctionOrderBy(junction) {
|
|
288
|
-
const right =
|
|
289
|
-
return junction.order_column ?
|
|
436
|
+
const right = import_drizzle_orm3.sql.raw(quoteIdent2(junction.right_column));
|
|
437
|
+
return junction.order_column ? import_drizzle_orm3.sql` ORDER BY "order" ASC, ${right} ASC` : import_drizzle_orm3.sql` ORDER BY ${right} ASC`;
|
|
438
|
+
}
|
|
439
|
+
function firstOfEach(items, key) {
|
|
440
|
+
const seen = /* @__PURE__ */ new Set();
|
|
441
|
+
return items.filter((item) => {
|
|
442
|
+
const k = key(item);
|
|
443
|
+
if (seen.has(k)) return false;
|
|
444
|
+
seen.add(k);
|
|
445
|
+
return true;
|
|
446
|
+
});
|
|
290
447
|
}
|
|
291
448
|
function oneOrMany(cardinality, list) {
|
|
292
449
|
return cardinality === "one" ? list[0] ?? null : list;
|
|
293
450
|
}
|
|
294
451
|
function paragraphItems(field, value) {
|
|
295
|
-
if ((0,
|
|
452
|
+
if ((0, import_manguito_cms_core2.relationCardinality)(field) === "one") {
|
|
296
453
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? [value] : [];
|
|
297
454
|
}
|
|
298
455
|
return Array.isArray(value) ? value : [];
|
|
@@ -310,7 +467,7 @@ function buildRelationsMap(fields, registry) {
|
|
|
310
467
|
relations[field.name] = {
|
|
311
468
|
type: "paragraph",
|
|
312
469
|
table: pType.db.table_name,
|
|
313
|
-
cardinality: (0,
|
|
470
|
+
cardinality: (0, import_manguito_cms_core2.relationCardinality)(field) ?? "many"
|
|
314
471
|
};
|
|
315
472
|
} else if (field.field_type === "reference") {
|
|
316
473
|
if (!field.db_column) continue;
|
|
@@ -353,9 +510,9 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
|
|
|
353
510
|
const batch = [...new Set(rows)];
|
|
354
511
|
if (rel.type === "paragraph") {
|
|
355
512
|
const parentIds = batch.map((r) => r["id"]);
|
|
356
|
-
const inList =
|
|
513
|
+
const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
|
|
357
514
|
const result = await db.execute(
|
|
358
|
-
|
|
515
|
+
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, id ASC`
|
|
359
516
|
);
|
|
360
517
|
const byParent = groupBy(result.rows, "parent_id");
|
|
361
518
|
for (const row of batch) {
|
|
@@ -372,10 +529,10 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
|
|
|
372
529
|
const unique = [...new Set(fkValues)];
|
|
373
530
|
const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
374
531
|
if (uncached.length > 0) {
|
|
375
|
-
const inList =
|
|
376
|
-
const publishedCond = publishedOnly ?
|
|
532
|
+
const inList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
|
|
533
|
+
const publishedCond = publishedOnly ? import_drizzle_orm3.sql` AND published = true` : import_drizzle_orm3.sql``;
|
|
377
534
|
const result = await db.execute(
|
|
378
|
-
|
|
535
|
+
import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE id IN (${inList})${publishedCond}`
|
|
379
536
|
);
|
|
380
537
|
for (const item of result.rows) {
|
|
381
538
|
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
@@ -387,18 +544,18 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
|
|
|
387
544
|
}
|
|
388
545
|
} else if (rel.type === "junction") {
|
|
389
546
|
const parentIds = batch.map((r) => r["id"]);
|
|
390
|
-
const inList =
|
|
547
|
+
const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
|
|
391
548
|
const junctionResult = await db.execute(
|
|
392
|
-
|
|
549
|
+
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})${junctionOrderBy(rel)}`
|
|
393
550
|
);
|
|
394
551
|
const jRows = junctionResult.rows;
|
|
395
552
|
const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
|
|
396
553
|
const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
397
554
|
if (uncached.length > 0) {
|
|
398
|
-
const rightInList =
|
|
399
|
-
const publishedCond = publishedOnly ?
|
|
555
|
+
const rightInList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
|
|
556
|
+
const publishedCond = publishedOnly ? import_drizzle_orm3.sql` AND published = true` : import_drizzle_orm3.sql``;
|
|
400
557
|
const entitiesResult = await db.execute(
|
|
401
|
-
|
|
558
|
+
import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
|
|
402
559
|
);
|
|
403
560
|
for (const item of entitiesResult.rows) {
|
|
404
561
|
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
@@ -406,7 +563,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
|
|
|
406
563
|
}
|
|
407
564
|
const byLeft = groupBy(jRows, rel.left_column);
|
|
408
565
|
for (const row of batch) {
|
|
409
|
-
const jEntries = byLeft[row["id"]] ?? [];
|
|
566
|
+
const jEntries = firstOfEach(byLeft[row["id"]] ?? [], (jr) => jr[rel.right_column]);
|
|
410
567
|
row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
|
|
411
568
|
}
|
|
412
569
|
} else if (rel.type === "media") {
|
|
@@ -420,8 +577,8 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
|
|
|
420
577
|
const unique = [...new Set(fkValues)];
|
|
421
578
|
const uncached = unique.filter((id) => !cache.has(`media:${id}`));
|
|
422
579
|
if (uncached.length > 0) {
|
|
423
|
-
const inList =
|
|
424
|
-
const result = await db.execute(
|
|
580
|
+
const inList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
|
|
581
|
+
const result = await db.execute(import_drizzle_orm3.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
|
|
425
582
|
for (const item of result.rows) {
|
|
426
583
|
cache.set(`media:${item["id"]}`, item);
|
|
427
584
|
}
|
|
@@ -436,9 +593,9 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
|
|
|
436
593
|
if (rows.length === 0) return;
|
|
437
594
|
if (rel.type === "paragraph") {
|
|
438
595
|
const parentIds = rows.map((r) => r["id"]);
|
|
439
|
-
const inList =
|
|
596
|
+
const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
|
|
440
597
|
const result = await db.execute(
|
|
441
|
-
|
|
598
|
+
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, id ASC`
|
|
442
599
|
);
|
|
443
600
|
const byParent = groupBy(result.rows, "parent_id");
|
|
444
601
|
for (const row of rows) {
|
|
@@ -446,23 +603,24 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
|
|
|
446
603
|
}
|
|
447
604
|
} else if (rel.type === "junction") {
|
|
448
605
|
const parentIds = rows.map((r) => r["id"]);
|
|
449
|
-
const inList =
|
|
606
|
+
const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
|
|
450
607
|
const result = await db.execute(
|
|
451
|
-
|
|
608
|
+
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})${junctionOrderBy(rel)}`
|
|
452
609
|
);
|
|
453
610
|
const byLeft = groupBy(result.rows, "left_id");
|
|
454
611
|
for (const row of rows) {
|
|
455
|
-
row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
|
|
612
|
+
row[fieldName] = firstOfEach(byLeft[row["id"]] ?? [], (r) => r["right_id"]).map((r) => r["right_id"]);
|
|
456
613
|
}
|
|
457
614
|
}
|
|
458
615
|
}
|
|
459
|
-
var import_node_crypto,
|
|
616
|
+
var import_node_crypto, import_drizzle_orm3, import_manguito_cms_core2;
|
|
460
617
|
var init_relations = __esm({
|
|
461
618
|
"src/relations.ts"() {
|
|
462
619
|
"use strict";
|
|
463
620
|
import_node_crypto = __toESM(require("crypto"));
|
|
464
|
-
|
|
465
|
-
|
|
621
|
+
import_drizzle_orm3 = require("drizzle-orm");
|
|
622
|
+
import_manguito_cms_core2 = require("@bobbykim/manguito-cms-core");
|
|
623
|
+
init_relation_input();
|
|
466
624
|
}
|
|
467
625
|
});
|
|
468
626
|
|
|
@@ -1374,7 +1532,7 @@ var ERROR_STATUS_MAP = {
|
|
|
1374
1532
|
var errorHandler = (err, c) => {
|
|
1375
1533
|
console.error(err.stack ?? err.message);
|
|
1376
1534
|
const raw = err.code;
|
|
1377
|
-
const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && raw
|
|
1535
|
+
const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && Object.hasOwn(ERROR_STATUS_MAP, raw);
|
|
1378
1536
|
if (!deliberate) {
|
|
1379
1537
|
return c.json({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
|
|
1380
1538
|
}
|
|
@@ -1969,16 +2127,16 @@ function parseFilters(url, validFields, columnFor) {
|
|
|
1969
2127
|
}
|
|
1970
2128
|
|
|
1971
2129
|
// src/routes/content.ts
|
|
2130
|
+
function pickProgrammatic(resolved, names) {
|
|
2131
|
+
const out = {};
|
|
2132
|
+
for (const name of names) if (Object.hasOwn(resolved, name)) out[name] = resolved[name];
|
|
2133
|
+
return out;
|
|
2134
|
+
}
|
|
1972
2135
|
function isPublished(item) {
|
|
1973
2136
|
return item["published"] === true;
|
|
1974
2137
|
}
|
|
1975
2138
|
function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
|
|
1976
2139
|
const forResolver = resolverProjectors ?? projectors;
|
|
1977
|
-
function pickProgrammatic(resolved, names) {
|
|
1978
|
-
const out = {};
|
|
1979
|
-
for (const name of names) if (name in resolved) out[name] = resolved[name];
|
|
1980
|
-
return out;
|
|
1981
|
-
}
|
|
1982
2140
|
async function respondItem(row, typeName, programmatic) {
|
|
1983
2141
|
const data = projectRow(row, typeName, projectors);
|
|
1984
2142
|
if (!resolver?.hasSchema(typeName)) return data;
|
|
@@ -2290,34 +2448,52 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
|
|
|
2290
2448
|
var import_drizzle_orm5 = require("drizzle-orm");
|
|
2291
2449
|
|
|
2292
2450
|
// src/references-in-use.ts
|
|
2293
|
-
var
|
|
2294
|
-
var
|
|
2295
|
-
|
|
2451
|
+
var import_drizzle_orm4 = require("drizzle-orm");
|
|
2452
|
+
var import_manguito_cms_core3 = require("@bobbykim/manguito-cms-core");
|
|
2453
|
+
init_relations();
|
|
2454
|
+
function quoteIdent3(name) {
|
|
2296
2455
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
2297
2456
|
return `"${name}"`;
|
|
2298
2457
|
}
|
|
2299
2458
|
function isRequiredSingleReferenceTo(f, targetType) {
|
|
2300
|
-
return f.field_type === "reference" && f.required && (0,
|
|
2459
|
+
return f.field_type === "reference" && f.required && (0, import_manguito_cms_core3.relationCardinality)(f) === "one" && f.db_column?.foreign_key !== void 0 && f.ui_component.component === "typeahead-select" && f.ui_component.ref === targetType;
|
|
2460
|
+
}
|
|
2461
|
+
function requiredReferencesTo(owners, targetType) {
|
|
2462
|
+
return owners.flatMap(
|
|
2463
|
+
(owner) => owner.fields.filter((f) => isRequiredSingleReferenceTo(f, targetType)).map((f) => ({ owner, f }))
|
|
2464
|
+
);
|
|
2301
2465
|
}
|
|
2302
2466
|
async function findRequiredReferrers(db, registry, targetType, id) {
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
];
|
|
2308
|
-
const
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2467
|
+
const targetFields = registry.content_types[targetType]?.fields ?? registry.taxonomy_types[targetType]?.fields ?? [];
|
|
2468
|
+
const itemOwners = [...Object.values(registry.content_types), ...Object.values(registry.taxonomy_types)];
|
|
2469
|
+
const itemFields = requiredReferencesTo(itemOwners, targetType);
|
|
2470
|
+
const paragraphFields = requiredReferencesTo(Object.values(registry.paragraph_types), targetType);
|
|
2471
|
+
const ownIds = paragraphFields.length > 0 ? await ownParagraphRowIds(db, registry, targetFields, id) : [];
|
|
2472
|
+
const [itemCounts, paragraphCounts] = await Promise.all([
|
|
2473
|
+
Promise.all(
|
|
2474
|
+
itemFields.map(async ({ owner, f }) => {
|
|
2475
|
+
const notSelf = owner.name === targetType ? import_drizzle_orm4.sql` AND id <> ${id}` : import_drizzle_orm4.sql``;
|
|
2476
|
+
const r = await db.execute(
|
|
2477
|
+
import_drizzle_orm4.sql`SELECT count(*)::int AS n FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(owner.db.table_name))} WHERE ${import_drizzle_orm4.sql.raw(quoteIdent3(f.db_column.column_name))} = ${id}${notSelf}`
|
|
2478
|
+
);
|
|
2479
|
+
return { owner, f, n: r.rows[0].n, own: 0 };
|
|
2480
|
+
})
|
|
2481
|
+
),
|
|
2482
|
+
Promise.all(
|
|
2483
|
+
paragraphFields.map(async ({ owner, f }) => {
|
|
2484
|
+
const r = await db.execute(
|
|
2485
|
+
import_drizzle_orm4.sql`SELECT count(*)::int AS n, count(*) FILTER (WHERE id = ANY(${import_drizzle_orm4.sql.param(ownIds)}::uuid[]))::int AS own FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(owner.db.table_name))} WHERE ${import_drizzle_orm4.sql.raw(quoteIdent3(f.db_column.column_name))} = ${id}`
|
|
2486
|
+
);
|
|
2487
|
+
const { n, own } = r.rows[0];
|
|
2488
|
+
return { owner, f, n, own };
|
|
2489
|
+
})
|
|
2490
|
+
)
|
|
2491
|
+
]);
|
|
2492
|
+
const others = [];
|
|
2493
|
+
for (const { owner, f, n, own } of [...itemCounts, ...paragraphCounts]) {
|
|
2494
|
+
if (n - own > 0) others.push({ type_label: owner.label, field_label: f.label, count: n - own });
|
|
2319
2495
|
}
|
|
2320
|
-
return
|
|
2496
|
+
return { others, fromOwnParagraphs: paragraphCounts.some((c) => c.own > 0) };
|
|
2321
2497
|
}
|
|
2322
2498
|
function inUseMessage(referrers) {
|
|
2323
2499
|
const total = referrers.reduce((sum, r) => sum + r.count, 0);
|
|
@@ -2347,7 +2523,7 @@ function topLevelMediaDelta(mediaFields, before, after) {
|
|
|
2347
2523
|
for (const f of mediaFields) {
|
|
2348
2524
|
const key = f.db_column?.column_name;
|
|
2349
2525
|
if (key === void 0 || key === "") continue;
|
|
2350
|
-
if (after !== null && !(key
|
|
2526
|
+
if (after !== null && !Object.hasOwn(after, key)) continue;
|
|
2351
2527
|
const oldId = before ? extractMediaId(before[key]) : null;
|
|
2352
2528
|
const newId = after ? extractMediaId(after[key]) : null;
|
|
2353
2529
|
if (newId === oldId) continue;
|
|
@@ -2371,132 +2547,8 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
2371
2547
|
if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
|
|
2372
2548
|
}
|
|
2373
2549
|
|
|
2374
|
-
// src/relation-input.ts
|
|
2375
|
-
var import_drizzle_orm3 = require("drizzle-orm");
|
|
2376
|
-
var import_manguito_cms_core2 = require("@bobbykim/manguito-cms-core");
|
|
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;
|
|
2378
|
-
function isUuid(v) {
|
|
2379
|
-
return typeof v === "string" && UUID_RE.test(v);
|
|
2380
|
-
}
|
|
2381
|
-
function isPlainObject(v) {
|
|
2382
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2383
|
-
}
|
|
2384
|
-
function checkRelationInput(fields, body, registry, path = "") {
|
|
2385
|
-
const errors = [];
|
|
2386
|
-
for (const field of fields) {
|
|
2387
|
-
if (!(field.name in body)) continue;
|
|
2388
|
-
const cardinality = (0, import_manguito_cms_core2.relationCardinality)(field);
|
|
2389
|
-
if (cardinality === null) continue;
|
|
2390
|
-
const value = body[field.name];
|
|
2391
|
-
const name = `${path}${field.name}`;
|
|
2392
|
-
if (field.field_type === "reference") {
|
|
2393
|
-
const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
|
|
2394
|
-
if (!ok) {
|
|
2395
|
-
errors.push({
|
|
2396
|
-
field: name,
|
|
2397
|
-
message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
|
|
2398
|
-
});
|
|
2399
|
-
}
|
|
2400
|
-
continue;
|
|
2401
|
-
}
|
|
2402
|
-
const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
|
|
2403
|
-
if (items === void 0) {
|
|
2404
|
-
errors.push({
|
|
2405
|
-
field: name,
|
|
2406
|
-
message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
|
|
2407
|
-
});
|
|
2408
|
-
continue;
|
|
2409
|
-
}
|
|
2410
|
-
const pType = paragraphTypeOf(field, registry);
|
|
2411
|
-
if (!pType) continue;
|
|
2412
|
-
items.forEach((item, i) => {
|
|
2413
|
-
const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
|
|
2414
|
-
errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
|
|
2415
|
-
});
|
|
2416
|
-
}
|
|
2417
|
-
return errors;
|
|
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
|
-
}
|
|
2423
|
-
function paragraphItemsIn(fields, body, registry, path) {
|
|
2424
|
-
const out = [];
|
|
2425
|
-
for (const field of fields) {
|
|
2426
|
-
if (field.field_type !== "paragraph" || !(field.name in body)) continue;
|
|
2427
|
-
const pType = paragraphTypeOf(field, registry);
|
|
2428
|
-
if (!pType) continue;
|
|
2429
|
-
const value = body[field.name];
|
|
2430
|
-
const name = `${path}${field.name}`;
|
|
2431
|
-
if ((0, import_manguito_cms_core2.relationCardinality)(field) === "one") {
|
|
2432
|
-
if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
|
|
2433
|
-
} else if (Array.isArray(value)) {
|
|
2434
|
-
value.forEach((item, i) => {
|
|
2435
|
-
if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
|
|
2436
|
-
});
|
|
2437
|
-
}
|
|
2438
|
-
}
|
|
2439
|
-
return out;
|
|
2440
|
-
}
|
|
2441
|
-
function isMissing(value) {
|
|
2442
|
-
if (value === null || value === void 0) return true;
|
|
2443
|
-
if (typeof value === "string" && value.trim() === "") return true;
|
|
2444
|
-
if (Array.isArray(value) && value.length === 0) return true;
|
|
2445
|
-
return false;
|
|
2446
|
-
}
|
|
2447
|
-
function checkRequiredInParagraphItems(fields, body, registry, path = "") {
|
|
2448
|
-
const errors = [];
|
|
2449
|
-
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
2450
|
-
for (const f of itemFields) {
|
|
2451
|
-
if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
|
|
2452
|
-
if (isMissing(item[f.name])) {
|
|
2453
|
-
errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
|
-
errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
|
|
2457
|
-
}
|
|
2458
|
-
return errors;
|
|
2459
|
-
}
|
|
2460
|
-
function quoteIdent2(name) {
|
|
2461
|
-
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
2462
|
-
return `"${name}"`;
|
|
2463
|
-
}
|
|
2464
|
-
function collectReferences(fields, body, registry, path, out) {
|
|
2465
|
-
for (const f of fields) {
|
|
2466
|
-
if (!(f.name in body) || !f.db_column) continue;
|
|
2467
|
-
const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
|
|
2468
|
-
if (!table) continue;
|
|
2469
|
-
const value = body[f.name];
|
|
2470
|
-
const name = `${path}${f.name}`;
|
|
2471
|
-
if (typeof value === "string") {
|
|
2472
|
-
out.push({ table, id: value, field: name });
|
|
2473
|
-
} else if (Array.isArray(value)) {
|
|
2474
|
-
value.forEach((v, i) => {
|
|
2475
|
-
if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
|
|
2476
|
-
});
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
2480
|
-
collectReferences(itemFields, item, registry, prefix, out);
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
async function findMissingReferences(db, fields, body, registry) {
|
|
2484
|
-
const uses = [];
|
|
2485
|
-
collectReferences(fields, body, registry, "", uses);
|
|
2486
|
-
if (uses.length === 0) return [];
|
|
2487
|
-
const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
|
|
2488
|
-
const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
|
|
2489
|
-
if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
|
|
2490
|
-
const result = await db.execute(
|
|
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`, `)})`
|
|
2492
|
-
);
|
|
2493
|
-
return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
|
|
2494
|
-
});
|
|
2495
|
-
const found = new Map(await Promise.all(lookups));
|
|
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.` }));
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
2550
|
// src/field-values.ts
|
|
2551
|
+
init_relation_input();
|
|
2500
2552
|
var VARCHAR_DEFAULT_LIMIT = 255;
|
|
2501
2553
|
var INTEGER_MIN = -2147483648;
|
|
2502
2554
|
var INTEGER_MAX = 2147483647;
|
|
@@ -2507,7 +2559,7 @@ var EMPTY_MEANS_NULL = /* @__PURE__ */ new Set(["date", "integer", "float", "enu
|
|
|
2507
2559
|
function emptyValuesToNull(fields, body, registry) {
|
|
2508
2560
|
const out = { ...body };
|
|
2509
2561
|
for (const f of fields) {
|
|
2510
|
-
if (!(f.name
|
|
2562
|
+
if (!Object.hasOwn(out, f.name)) continue;
|
|
2511
2563
|
const value = out[f.name];
|
|
2512
2564
|
if (value === "" && EMPTY_MEANS_NULL.has(f.field_type)) {
|
|
2513
2565
|
out[f.name] = null;
|
|
@@ -2612,7 +2664,7 @@ function rangeError(name, value, min, max) {
|
|
|
2612
2664
|
function checkFieldValues(fields, body, registry, path = "") {
|
|
2613
2665
|
const errors = [];
|
|
2614
2666
|
for (const f of fields) {
|
|
2615
|
-
if (!(f.name
|
|
2667
|
+
if (!Object.hasOwn(body, f.name) || f.removed === true) continue;
|
|
2616
2668
|
const name = `${path}${f.name}`;
|
|
2617
2669
|
const message = valueError(f, body[f.name], name);
|
|
2618
2670
|
if (message) errors.push({ field: name, message });
|
|
@@ -2625,6 +2677,7 @@ function checkFieldValues(fields, body, registry, path = "") {
|
|
|
2625
2677
|
|
|
2626
2678
|
// src/routes/admin/content.ts
|
|
2627
2679
|
init_relations();
|
|
2680
|
+
init_relation_input();
|
|
2628
2681
|
init_field_keys();
|
|
2629
2682
|
function quoteIdent4(name) {
|
|
2630
2683
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
@@ -2632,14 +2685,12 @@ function quoteIdent4(name) {
|
|
|
2632
2685
|
}
|
|
2633
2686
|
async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
2634
2687
|
const result = await db.execute(
|
|
2635
|
-
import_drizzle_orm5.sql`SELECT * FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
2688
|
+
import_drizzle_orm5.sql`SELECT * FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC, id ASC`
|
|
2636
2689
|
);
|
|
2637
2690
|
const rows = result.rows;
|
|
2638
2691
|
for (const nf of pType.fields) {
|
|
2639
2692
|
if (nf.db_column !== null) continue;
|
|
2640
|
-
const
|
|
2641
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
2642
|
-
const nType = registry.paragraph_types[comp.ref];
|
|
2693
|
+
const nType = paragraphTypeOf(nf, registry);
|
|
2643
2694
|
if (!nType) continue;
|
|
2644
2695
|
for (const row of rows) {
|
|
2645
2696
|
row[nf.name] = oneOrMany(
|
|
@@ -2651,35 +2702,36 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
|
2651
2702
|
return rows;
|
|
2652
2703
|
}
|
|
2653
2704
|
async function loadRelationsForEdit(db, registry, fields, id, row) {
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2705
|
+
const loaded = await Promise.all(
|
|
2706
|
+
fields.map(async (f) => {
|
|
2707
|
+
if (f.db_column === null) {
|
|
2708
|
+
const pType = paragraphTypeOf(f, registry);
|
|
2709
|
+
if (!pType) return null;
|
|
2710
|
+
return [f.name, oneOrMany((0, import_manguito_cms_core4.relationCardinality)(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name))];
|
|
2711
|
+
}
|
|
2712
|
+
if (f.db_column.junction) {
|
|
2713
|
+
const j = f.db_column.junction;
|
|
2714
|
+
const r = await db.execute(
|
|
2715
|
+
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)}`
|
|
2716
|
+
);
|
|
2717
|
+
return [f.name, [...new Set(r.rows.map((linked) => linked[j.right_column]))]];
|
|
2718
|
+
}
|
|
2719
|
+
return null;
|
|
2720
|
+
})
|
|
2721
|
+
);
|
|
2722
|
+
for (const entry of loaded) if (entry) row[entry[0]] = entry[1];
|
|
2669
2723
|
}
|
|
2670
2724
|
async function valuesForPublishCheck(db, registry, requiredFields, id, storedLabels, body) {
|
|
2671
2725
|
const stored = { ...storedLabels };
|
|
2672
|
-
if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !(f.name
|
|
2726
|
+
if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !Object.hasOwn(body, f.name)), id, stored);
|
|
2673
2727
|
return { ...stored, ...body };
|
|
2674
2728
|
}
|
|
2675
2729
|
async function persistRelationFields(db, registry, fields, body, itemId, ownerTable) {
|
|
2676
2730
|
const deltas = [];
|
|
2677
2731
|
for (const f of fields) {
|
|
2678
|
-
if (!(f.name
|
|
2732
|
+
if (!Object.hasOwn(body, f.name)) continue;
|
|
2679
2733
|
if (f.db_column === null) {
|
|
2680
|
-
const
|
|
2681
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
2682
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
2734
|
+
const pType = paragraphTypeOf(f, registry);
|
|
2683
2735
|
if (!pType) continue;
|
|
2684
2736
|
deltas.push(await persistParagraphField(db, itemId, ownerTable, f.name, pType, paragraphItems(f, body[f.name]), registry));
|
|
2685
2737
|
} else if (f.db_column.junction) {
|
|
@@ -2690,6 +2742,46 @@ async function persistRelationFields(db, registry, fields, body, itemId, ownerTa
|
|
|
2690
2742
|
}
|
|
2691
2743
|
return deltas;
|
|
2692
2744
|
}
|
|
2745
|
+
async function deleteOwnParagraphs(db, registry, fields, itemId) {
|
|
2746
|
+
const deltas = [];
|
|
2747
|
+
for (const f of fields) {
|
|
2748
|
+
const pType = paragraphTypeOf(f, registry);
|
|
2749
|
+
if (!pType) continue;
|
|
2750
|
+
deltas.push(await deleteParagraphField(db, itemId, f.name, pType, registry));
|
|
2751
|
+
}
|
|
2752
|
+
return deltas;
|
|
2753
|
+
}
|
|
2754
|
+
function releasedFromParagraphsOnly(paragraphDeltas, topLevel) {
|
|
2755
|
+
const stillHeld = new Set(topLevel.removed);
|
|
2756
|
+
return { added: [], removed: mergeMediaDeltas(...paragraphDeltas).removed.filter((mediaId) => !stillHeld.has(mediaId)) };
|
|
2757
|
+
}
|
|
2758
|
+
async function deleteItemCascade(db, registry, mediaRepo, target) {
|
|
2759
|
+
const { typeName, fields, mediaFields, repo, id, row } = target;
|
|
2760
|
+
const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : { others: [], fromOwnParagraphs: false };
|
|
2761
|
+
if (referrers.others.length > 0) return { ok: false, message: inUseMessage(referrers.others) };
|
|
2762
|
+
const topLevel = topLevelMediaDelta(mediaFields, row, null);
|
|
2763
|
+
if (!db || !referrers.fromOwnParagraphs) {
|
|
2764
|
+
try {
|
|
2765
|
+
await repo.delete(id);
|
|
2766
|
+
} catch (err) {
|
|
2767
|
+
if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
|
|
2768
|
+
throw err;
|
|
2769
|
+
}
|
|
2770
|
+
const paragraphDeltas2 = db ? await deleteOwnParagraphs(db, registry, fields, id) : [];
|
|
2771
|
+
await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas2), mediaRepo);
|
|
2772
|
+
return { ok: true };
|
|
2773
|
+
}
|
|
2774
|
+
const paragraphDeltas = await deleteOwnParagraphs(db, registry, fields, id);
|
|
2775
|
+
try {
|
|
2776
|
+
await repo.delete(id);
|
|
2777
|
+
} catch (err) {
|
|
2778
|
+
await applyMediaReferenceDelta(releasedFromParagraphsOnly(paragraphDeltas, topLevel), mediaRepo);
|
|
2779
|
+
if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
|
|
2780
|
+
throw err;
|
|
2781
|
+
}
|
|
2782
|
+
await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas), mediaRepo);
|
|
2783
|
+
return { ok: true };
|
|
2784
|
+
}
|
|
2693
2785
|
async function lookupBasePathId(db, pathOrName) {
|
|
2694
2786
|
const r = await db.execute(
|
|
2695
2787
|
import_drizzle_orm5.sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
@@ -2799,7 +2891,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
2799
2891
|
const mediaFields = contentType.fields.filter(
|
|
2800
2892
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
2801
2893
|
);
|
|
2802
|
-
const paragraphFieldDefs = contentType.fields.filter((f) => f.db_column === null);
|
|
2803
2894
|
const searchableColumns = [
|
|
2804
2895
|
...contentType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
|
|
2805
2896
|
...contentType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
|
|
@@ -2949,7 +3040,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
2949
3040
|
}
|
|
2950
3041
|
for (const f of columnBackedFields) {
|
|
2951
3042
|
const key = f.db_column.column_name;
|
|
2952
|
-
if (!(key
|
|
3043
|
+
if (!Object.hasOwn(storageBody, key)) continue;
|
|
2953
3044
|
columnData[key] = storageBody[key];
|
|
2954
3045
|
}
|
|
2955
3046
|
if (db) {
|
|
@@ -2991,7 +3082,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
2991
3082
|
404
|
|
2992
3083
|
);
|
|
2993
3084
|
}
|
|
2994
|
-
if (!contentType.only_one && "slug"
|
|
3085
|
+
if (!contentType.only_one && Object.hasOwn(body, "slug")) {
|
|
2995
3086
|
const slug = body["slug"];
|
|
2996
3087
|
if (typeof slug !== "string" || slug.trim() === "") {
|
|
2997
3088
|
return c.json(
|
|
@@ -3061,11 +3152,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
3061
3152
|
}
|
|
3062
3153
|
const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
3063
3154
|
const patchData = {};
|
|
3064
|
-
if (!contentType.only_one && "slug"
|
|
3065
|
-
if ("published"
|
|
3155
|
+
if (!contentType.only_one && Object.hasOwn(body, "slug")) patchData["slug"] = body["slug"];
|
|
3156
|
+
if (Object.hasOwn(body, "published")) patchData["published"] = body["published"];
|
|
3066
3157
|
for (const f of patchColumnBackedFields) {
|
|
3067
3158
|
const key = f.db_column.column_name;
|
|
3068
|
-
if (!(key
|
|
3159
|
+
if (!Object.hasOwn(storageBody, key)) continue;
|
|
3069
3160
|
patchData[key] = storageBody[key];
|
|
3070
3161
|
}
|
|
3071
3162
|
const updated = await repo.update(id, patchData);
|
|
@@ -3115,31 +3206,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
3115
3206
|
404
|
|
3116
3207
|
);
|
|
3117
3208
|
}
|
|
3118
|
-
const
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
}
|
|
3128
|
-
throw err;
|
|
3129
|
-
}
|
|
3130
|
-
const mediaDeltas = [
|
|
3131
|
-
topLevelMediaDelta(mediaFields, item, null)
|
|
3132
|
-
];
|
|
3133
|
-
if (db) {
|
|
3134
|
-
for (const f of paragraphFieldDefs) {
|
|
3135
|
-
const comp = f.ui_component;
|
|
3136
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
3137
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
3138
|
-
if (!pType) continue;
|
|
3139
|
-
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
|
|
3140
|
-
}
|
|
3209
|
+
const outcome = await deleteItemCascade(db, registry, mediaRepo, {
|
|
3210
|
+
typeName,
|
|
3211
|
+
fields: contentType.fields,
|
|
3212
|
+
mediaFields,
|
|
3213
|
+
repo,
|
|
3214
|
+
id,
|
|
3215
|
+
row: item
|
|
3216
|
+
});
|
|
3217
|
+
if (!outcome.ok) {
|
|
3218
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
|
|
3141
3219
|
}
|
|
3142
|
-
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
3143
3220
|
return c.json({ ok: true });
|
|
3144
3221
|
}
|
|
3145
3222
|
);
|
|
@@ -3160,7 +3237,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
3160
3237
|
const mediaFields = taxonomyType.fields.filter(
|
|
3161
3238
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
3162
3239
|
);
|
|
3163
|
-
const paragraphFieldDefs = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
3164
3240
|
const searchableColumns = [
|
|
3165
3241
|
...taxonomyType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
|
|
3166
3242
|
...taxonomyType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
|
|
@@ -3246,7 +3322,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
3246
3322
|
};
|
|
3247
3323
|
for (const f of taxColumnBackedFields) {
|
|
3248
3324
|
const key = f.db_column.column_name;
|
|
3249
|
-
if (!(key
|
|
3325
|
+
if (!Object.hasOwn(storageBody, key)) continue;
|
|
3250
3326
|
taxColumnData[key] = storageBody[key];
|
|
3251
3327
|
}
|
|
3252
3328
|
const item = await repo.create(taxColumnData);
|
|
@@ -3298,10 +3374,10 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
3298
3374
|
}
|
|
3299
3375
|
const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
3300
3376
|
const taxPatchData = {};
|
|
3301
|
-
if ("published"
|
|
3377
|
+
if (Object.hasOwn(body, "published")) taxPatchData["published"] = body["published"];
|
|
3302
3378
|
for (const f of taxPatchColumnBackedFields) {
|
|
3303
3379
|
const key = f.db_column.column_name;
|
|
3304
|
-
if (!(key
|
|
3380
|
+
if (!Object.hasOwn(storageBody, key)) continue;
|
|
3305
3381
|
taxPatchData[key] = storageBody[key];
|
|
3306
3382
|
}
|
|
3307
3383
|
const updated = await repo.update(id, taxPatchData);
|
|
@@ -3334,31 +3410,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
3334
3410
|
404
|
|
3335
3411
|
);
|
|
3336
3412
|
}
|
|
3337
|
-
const
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
}
|
|
3347
|
-
throw err;
|
|
3348
|
-
}
|
|
3349
|
-
const mediaDeltas = [
|
|
3350
|
-
topLevelMediaDelta(mediaFields, item, null)
|
|
3351
|
-
];
|
|
3352
|
-
if (db) {
|
|
3353
|
-
for (const f of paragraphFieldDefs) {
|
|
3354
|
-
const comp = f.ui_component;
|
|
3355
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
3356
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
3357
|
-
if (!pType) continue;
|
|
3358
|
-
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
|
|
3359
|
-
}
|
|
3413
|
+
const outcome = await deleteItemCascade(db, registry, mediaRepo, {
|
|
3414
|
+
typeName,
|
|
3415
|
+
fields: taxonomyType.fields,
|
|
3416
|
+
mediaFields,
|
|
3417
|
+
repo,
|
|
3418
|
+
id,
|
|
3419
|
+
row: item
|
|
3420
|
+
});
|
|
3421
|
+
if (!outcome.ok) {
|
|
3422
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
|
|
3360
3423
|
}
|
|
3361
|
-
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
3362
3424
|
return c.json({ ok: true });
|
|
3363
3425
|
}
|
|
3364
3426
|
);
|
|
@@ -4322,7 +4384,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
4322
4384
|
var import_drizzle_orm8 = require("drizzle-orm");
|
|
4323
4385
|
var API_VERSION = (() => {
|
|
4324
4386
|
try {
|
|
4325
|
-
return "0.9.
|
|
4387
|
+
return "0.9.4";
|
|
4326
4388
|
} catch {
|
|
4327
4389
|
return "0.0.0";
|
|
4328
4390
|
}
|