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