@bobbykim/manguito-cms-api 0.7.0 → 0.8.1
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-FXPKGPYF.mjs → chunk-IHEIX6P4.mjs} +22 -6
- package/dist/chunk-IHEIX6P4.mjs.map +1 -0
- package/dist/{chunk-4LLELFAZ.mjs → chunk-Y5SWT7PO.mjs} +6 -5
- package/dist/chunk-Y5SWT7PO.mjs.map +1 -0
- package/dist/codegen/index.js +3 -2
- package/dist/codegen/index.js.map +1 -1
- package/dist/codegen/index.mjs +3 -2
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/graphql/index.js +14 -5
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-VEOWJRAL.mjs +8 -0
- package/dist/index.js +391 -157
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +324 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-4LLELFAZ.mjs.map +0 -1
- package/dist/chunk-FXPKGPYF.mjs.map +0 -1
- package/dist/handler-GED3I7Z2.mjs +0 -8
- /package/dist/{handler-GED3I7Z2.mjs.map → handler-VEOWJRAL.mjs.map} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
createFieldKeyMapFromProjection,
|
|
5
5
|
deleteParagraphField,
|
|
6
6
|
isColumnBacked,
|
|
7
|
+
oneOrMany,
|
|
8
|
+
paragraphItems,
|
|
7
9
|
persistJunctionField,
|
|
8
10
|
persistParagraphField,
|
|
9
11
|
resolveRelationBareIds,
|
|
10
12
|
resolveRelationField
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-IHEIX6P4.mjs";
|
|
12
14
|
|
|
13
15
|
// src/paths.ts
|
|
14
16
|
function normalizePrefix(prefix) {
|
|
@@ -36,7 +38,7 @@ function createVersionedPaths(prefix, version) {
|
|
|
36
38
|
|
|
37
39
|
// src/app.ts
|
|
38
40
|
import { Hono } from "hono";
|
|
39
|
-
import { sql as
|
|
41
|
+
import { sql as sql11 } from "drizzle-orm";
|
|
40
42
|
|
|
41
43
|
// src/middleware/cors.ts
|
|
42
44
|
function createCorsMiddleware(corsConfig) {
|
|
@@ -130,16 +132,20 @@ var ERROR_STATUS_MAP = {
|
|
|
130
132
|
UNSUPPORTED_MIME_TYPE: 415,
|
|
131
133
|
STORAGE_ERROR: 502,
|
|
132
134
|
MEDIA_IN_USE: 409,
|
|
135
|
+
ITEM_IN_USE: 409,
|
|
133
136
|
PRESIGNED_URL_EXPIRED: 410,
|
|
134
137
|
RATE_LIMITED: 429,
|
|
135
138
|
INTERNAL_ERROR: 500
|
|
136
139
|
};
|
|
137
140
|
var errorHandler = (err, c) => {
|
|
138
141
|
console.error(err.stack ?? err.message);
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
const raw = err.code;
|
|
143
|
+
const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && raw in ERROR_STATUS_MAP;
|
|
144
|
+
if (!deliberate) {
|
|
145
|
+
return c.json({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
|
|
146
|
+
}
|
|
147
|
+
const status = ERROR_STATUS_MAP[raw] ?? 500;
|
|
148
|
+
return c.json({ ok: false, error: { code: raw, message: err.message } }, status);
|
|
143
149
|
};
|
|
144
150
|
|
|
145
151
|
// src/middleware/auth.ts
|
|
@@ -1043,7 +1049,55 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
|
|
|
1043
1049
|
}
|
|
1044
1050
|
|
|
1045
1051
|
// src/routes/admin/content.ts
|
|
1052
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
1053
|
+
|
|
1054
|
+
// src/references-in-use.ts
|
|
1046
1055
|
import { sql as sql2 } from "drizzle-orm";
|
|
1056
|
+
import { relationCardinality } from "@bobbykim/manguito-cms-core";
|
|
1057
|
+
function quoteIdent(name) {
|
|
1058
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1059
|
+
return `"${name}"`;
|
|
1060
|
+
}
|
|
1061
|
+
function isRequiredSingleReferenceTo(f, targetType) {
|
|
1062
|
+
return f.field_type === "reference" && f.required && relationCardinality(f) === "one" && f.db_column?.foreign_key !== void 0 && f.ui_component.component === "typeahead-select" && f.ui_component.ref === targetType;
|
|
1063
|
+
}
|
|
1064
|
+
async function findRequiredReferrers(db, registry, targetType, id) {
|
|
1065
|
+
const owners = [
|
|
1066
|
+
...Object.values(registry.content_types),
|
|
1067
|
+
...Object.values(registry.taxonomy_types),
|
|
1068
|
+
...Object.values(registry.paragraph_types)
|
|
1069
|
+
];
|
|
1070
|
+
const out = [];
|
|
1071
|
+
for (const owner of owners) {
|
|
1072
|
+
for (const f of owner.fields) {
|
|
1073
|
+
if (!isRequiredSingleReferenceTo(f, targetType)) continue;
|
|
1074
|
+
const notSelf = owner.name === targetType ? sql2` AND id <> ${id}` : sql2``;
|
|
1075
|
+
const r = await db.execute(
|
|
1076
|
+
sql2`SELECT count(*)::int AS n FROM ${sql2.raw(quoteIdent(owner.db.table_name))} WHERE ${sql2.raw(quoteIdent(f.db_column.column_name))} = ${id}${notSelf}`
|
|
1077
|
+
);
|
|
1078
|
+
const n = r.rows[0].n;
|
|
1079
|
+
if (n > 0) out.push({ type_label: owner.label, field_label: f.label, count: n });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return out;
|
|
1083
|
+
}
|
|
1084
|
+
function inUseMessage(referrers) {
|
|
1085
|
+
const total = referrers.reduce((sum, r) => sum + r.count, 0);
|
|
1086
|
+
const where = referrers.map((r) => `${r.count} in ${r.type_label} (${r.field_label})`).join(", ");
|
|
1087
|
+
return `This item is still used by ${total} item${total === 1 ? "" : "s"}: ${where}. Remove it from those first.`;
|
|
1088
|
+
}
|
|
1089
|
+
var IN_USE_RACE_MESSAGE = "This item is still in use. Remove it from the items that use it first.";
|
|
1090
|
+
function isForeignKeyViolation(err) {
|
|
1091
|
+
let e = err;
|
|
1092
|
+
for (let depth = 0; depth < 3 && typeof e === "object" && e !== null; depth++) {
|
|
1093
|
+
if (e.code === "23503") return true;
|
|
1094
|
+
e = e.cause;
|
|
1095
|
+
}
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/routes/admin/content.ts
|
|
1100
|
+
import { relationCardinality as relationCardinality3 } from "@bobbykim/manguito-cms-core";
|
|
1047
1101
|
|
|
1048
1102
|
// src/media-references.ts
|
|
1049
1103
|
function extractMediaId(value) {
|
|
@@ -1079,14 +1133,137 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
1079
1133
|
if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
|
|
1080
1134
|
}
|
|
1081
1135
|
|
|
1136
|
+
// src/relation-input.ts
|
|
1137
|
+
import { sql as sql3 } from "drizzle-orm";
|
|
1138
|
+
import { relationCardinality as relationCardinality2 } from "@bobbykim/manguito-cms-core";
|
|
1139
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1140
|
+
function isUuid(v) {
|
|
1141
|
+
return typeof v === "string" && UUID_RE.test(v);
|
|
1142
|
+
}
|
|
1143
|
+
function isPlainObject(v) {
|
|
1144
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1145
|
+
}
|
|
1146
|
+
function checkRelationInput(fields, body, registry, path = "") {
|
|
1147
|
+
const errors = [];
|
|
1148
|
+
for (const field of fields) {
|
|
1149
|
+
if (!(field.name in body)) continue;
|
|
1150
|
+
const cardinality = relationCardinality2(field);
|
|
1151
|
+
if (cardinality === null) continue;
|
|
1152
|
+
const value = body[field.name];
|
|
1153
|
+
const name = `${path}${field.name}`;
|
|
1154
|
+
if (field.field_type === "reference") {
|
|
1155
|
+
const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
|
|
1156
|
+
if (!ok) {
|
|
1157
|
+
errors.push({
|
|
1158
|
+
field: name,
|
|
1159
|
+
message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
|
|
1165
|
+
if (items === void 0) {
|
|
1166
|
+
errors.push({
|
|
1167
|
+
field: name,
|
|
1168
|
+
message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
|
|
1169
|
+
});
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
const comp = field.ui_component;
|
|
1173
|
+
const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
|
|
1174
|
+
if (!pType) continue;
|
|
1175
|
+
items.forEach((item, i) => {
|
|
1176
|
+
const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
|
|
1177
|
+
errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
return errors;
|
|
1181
|
+
}
|
|
1182
|
+
function paragraphItemsIn(fields, body, registry, path) {
|
|
1183
|
+
const out = [];
|
|
1184
|
+
for (const field of fields) {
|
|
1185
|
+
if (field.field_type !== "paragraph" || !(field.name in body)) continue;
|
|
1186
|
+
const comp = field.ui_component;
|
|
1187
|
+
const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
|
|
1188
|
+
if (!pType) continue;
|
|
1189
|
+
const value = body[field.name];
|
|
1190
|
+
const name = `${path}${field.name}`;
|
|
1191
|
+
if (relationCardinality2(field) === "one") {
|
|
1192
|
+
if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
|
|
1193
|
+
} else if (Array.isArray(value)) {
|
|
1194
|
+
value.forEach((item, i) => {
|
|
1195
|
+
if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
function isMissing(value) {
|
|
1202
|
+
if (value === null || value === void 0) return true;
|
|
1203
|
+
if (typeof value === "string" && value.trim() === "") return true;
|
|
1204
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
function checkRequiredInParagraphItems(fields, body, registry, path = "") {
|
|
1208
|
+
const errors = [];
|
|
1209
|
+
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
1210
|
+
for (const f of itemFields) {
|
|
1211
|
+
if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
|
|
1212
|
+
if (isMissing(item[f.name])) {
|
|
1213
|
+
errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
|
|
1217
|
+
}
|
|
1218
|
+
return errors;
|
|
1219
|
+
}
|
|
1220
|
+
function quoteIdent2(name) {
|
|
1221
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1222
|
+
return `"${name}"`;
|
|
1223
|
+
}
|
|
1224
|
+
function collectReferences(fields, body, registry, path, out) {
|
|
1225
|
+
for (const f of fields) {
|
|
1226
|
+
if (!(f.name in body) || !f.db_column) continue;
|
|
1227
|
+
const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
|
|
1228
|
+
if (!table) continue;
|
|
1229
|
+
const value = body[f.name];
|
|
1230
|
+
const name = `${path}${f.name}`;
|
|
1231
|
+
if (typeof value === "string") {
|
|
1232
|
+
out.push({ table, id: value, field: name });
|
|
1233
|
+
} else if (Array.isArray(value)) {
|
|
1234
|
+
value.forEach((v, i) => {
|
|
1235
|
+
if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
1240
|
+
collectReferences(itemFields, item, registry, prefix, out);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
async function findMissingReferences(db, fields, body, registry) {
|
|
1244
|
+
const uses = [];
|
|
1245
|
+
collectReferences(fields, body, registry, "", uses);
|
|
1246
|
+
if (uses.length === 0) return [];
|
|
1247
|
+
const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
|
|
1248
|
+
const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
|
|
1249
|
+
if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
|
|
1250
|
+
const result = await db.execute(
|
|
1251
|
+
sql3`SELECT id FROM ${sql3.raw(quoteIdent2(table))} WHERE id IN (${sql3.join(ids.map((id) => sql3`${id}`), sql3`, `)})`
|
|
1252
|
+
);
|
|
1253
|
+
return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
|
|
1254
|
+
});
|
|
1255
|
+
const found = new Map(await Promise.all(lookups));
|
|
1256
|
+
return uses.filter((u) => !found.get(u.table).has(u.id.toLowerCase())).map((u) => ({ field: u.field, message: `${u.field} refers to an item that does not exist.` }));
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1082
1259
|
// src/routes/admin/content.ts
|
|
1083
|
-
function
|
|
1260
|
+
function quoteIdent3(name) {
|
|
1084
1261
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1085
1262
|
return `"${name}"`;
|
|
1086
1263
|
}
|
|
1087
1264
|
async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
1088
1265
|
const result = await db.execute(
|
|
1089
|
-
|
|
1266
|
+
sql4`SELECT * FROM ${sql4.raw(quoteIdent3(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
1090
1267
|
);
|
|
1091
1268
|
const rows = result.rows;
|
|
1092
1269
|
for (const nf of pType.fields) {
|
|
@@ -1096,26 +1273,35 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
|
1096
1273
|
const nType = registry.paragraph_types[comp.ref];
|
|
1097
1274
|
if (!nType) continue;
|
|
1098
1275
|
for (const row of rows) {
|
|
1099
|
-
row[nf.name] =
|
|
1276
|
+
row[nf.name] = oneOrMany(
|
|
1277
|
+
relationCardinality3(nf) ?? "many",
|
|
1278
|
+
await loadParagraphRows(db, registry, nType, row["id"], nf.name)
|
|
1279
|
+
);
|
|
1100
1280
|
}
|
|
1101
1281
|
}
|
|
1102
1282
|
return rows;
|
|
1103
1283
|
}
|
|
1104
1284
|
async function lookupBasePathId(db, pathOrName) {
|
|
1105
1285
|
const r = await db.execute(
|
|
1106
|
-
|
|
1286
|
+
sql4`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
1107
1287
|
);
|
|
1108
1288
|
return r.rows[0]?.id ?? null;
|
|
1109
1289
|
}
|
|
1110
1290
|
var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1111
|
-
function isEmpty(value) {
|
|
1112
|
-
if (value === null || value === void 0) return true;
|
|
1113
|
-
if (typeof value === "string" && value.trim() === "") return true;
|
|
1114
|
-
if (Array.isArray(value) && value.length === 0) return true;
|
|
1115
|
-
return false;
|
|
1116
|
-
}
|
|
1117
1291
|
function checkRequiredFields(fields, data) {
|
|
1118
|
-
return fields.filter((f) => f.required &&
|
|
1292
|
+
return fields.filter((f) => f.required && isMissing(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
|
|
1293
|
+
}
|
|
1294
|
+
async function relationInputError(c, fields, body, registry, db) {
|
|
1295
|
+
const refuse = (message, details) => c.json({ ok: false, error: { code: "VALIDATION_ERROR", message, details } }, 422);
|
|
1296
|
+
const shape = checkRelationInput(fields, body, registry);
|
|
1297
|
+
if (shape.length > 0) return refuse("Relation fields have the wrong shape", shape);
|
|
1298
|
+
const required = checkRequiredInParagraphItems(fields, body, registry);
|
|
1299
|
+
if (required.length > 0) return refuse("Required fields are missing", required);
|
|
1300
|
+
if (db) {
|
|
1301
|
+
const missing = await findMissingReferences(db, fields, body, registry);
|
|
1302
|
+
if (missing.length > 0) return refuse("Relation fields refer to items that do not exist", missing);
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1119
1305
|
}
|
|
1120
1306
|
function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
|
|
1121
1307
|
const searchParams = new URL(url).searchParams;
|
|
@@ -1258,11 +1444,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1258
1444
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1259
1445
|
const pType = registry.paragraph_types[comp.ref];
|
|
1260
1446
|
if (!pType) continue;
|
|
1261
|
-
row[f.name] = await loadParagraphRows(db, registry, pType, id, f.name);
|
|
1447
|
+
row[f.name] = oneOrMany(relationCardinality3(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name));
|
|
1262
1448
|
} else if (f.db_column.junction) {
|
|
1263
1449
|
const j = f.db_column.junction;
|
|
1264
1450
|
const r = await db.execute(
|
|
1265
|
-
|
|
1451
|
+
sql4`SELECT ${sql4.raw(quoteIdent3(j.right_column))} FROM ${sql4.raw(quoteIdent3(j.table_name))} WHERE ${sql4.raw(quoteIdent3(j.left_column))} = ${id}`
|
|
1266
1452
|
);
|
|
1267
1453
|
row[f.name] = r.rows.map(
|
|
1268
1454
|
(row2) => row2[j.right_column]
|
|
@@ -1338,6 +1524,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1338
1524
|
}
|
|
1339
1525
|
);
|
|
1340
1526
|
const writeNewItem = async (c, body) => {
|
|
1527
|
+
const inputError = await relationInputError(c, contentType.fields, body, registry, db);
|
|
1528
|
+
if (inputError) return inputError;
|
|
1341
1529
|
const storageBody = fieldKeys.toStorage(body);
|
|
1342
1530
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1343
1531
|
if (fieldErrors.length > 0) {
|
|
@@ -1401,7 +1589,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1401
1589
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1402
1590
|
const pType = registry.paragraph_types[comp.ref];
|
|
1403
1591
|
if (!pType) continue;
|
|
1404
|
-
const items =
|
|
1592
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1405
1593
|
mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
|
|
1406
1594
|
}
|
|
1407
1595
|
for (const f of junctionFields) {
|
|
@@ -1472,6 +1660,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1472
1660
|
}
|
|
1473
1661
|
);
|
|
1474
1662
|
const writeExistingItem = async (c, id, existing, body) => {
|
|
1663
|
+
const inputError = await relationInputError(c, contentType.fields, body, registry, db);
|
|
1664
|
+
if (inputError) return inputError;
|
|
1475
1665
|
const storageBody = fieldKeys.toStorage(body);
|
|
1476
1666
|
if (body["published"] === true) {
|
|
1477
1667
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
@@ -1518,14 +1708,16 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1518
1708
|
];
|
|
1519
1709
|
if (db) {
|
|
1520
1710
|
for (const f of patchParagraphFields) {
|
|
1711
|
+
if (!(f.name in body)) continue;
|
|
1521
1712
|
const comp = f.ui_component;
|
|
1522
1713
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1523
1714
|
const pType = registry.paragraph_types[comp.ref];
|
|
1524
1715
|
if (!pType) continue;
|
|
1525
|
-
const items =
|
|
1716
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1526
1717
|
mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
|
|
1527
1718
|
}
|
|
1528
1719
|
for (const f of patchJunctionFields) {
|
|
1720
|
+
if (!(f.name in body)) continue;
|
|
1529
1721
|
const junction = f.db_column.junction;
|
|
1530
1722
|
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
1531
1723
|
await persistJunctionField(db, id, junction, relatedIds);
|
|
@@ -1565,6 +1757,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1565
1757
|
404
|
|
1566
1758
|
);
|
|
1567
1759
|
}
|
|
1760
|
+
const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
|
|
1761
|
+
if (referrers.length > 0) {
|
|
1762
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
|
|
1763
|
+
}
|
|
1764
|
+
try {
|
|
1765
|
+
await repo.delete(id);
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
if (isForeignKeyViolation(err)) {
|
|
1768
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
|
|
1769
|
+
}
|
|
1770
|
+
throw err;
|
|
1771
|
+
}
|
|
1568
1772
|
const mediaDeltas = [
|
|
1569
1773
|
topLevelMediaDelta(mediaFields, item, null)
|
|
1570
1774
|
];
|
|
@@ -1578,7 +1782,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1578
1782
|
}
|
|
1579
1783
|
}
|
|
1580
1784
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1581
|
-
await repo.delete(id);
|
|
1582
1785
|
return c.json({ ok: true });
|
|
1583
1786
|
}
|
|
1584
1787
|
);
|
|
@@ -1656,6 +1859,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1656
1859
|
requirePermission("content:create"),
|
|
1657
1860
|
async (c) => {
|
|
1658
1861
|
const body = await c.req.json();
|
|
1862
|
+
const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
|
|
1863
|
+
if (inputError) return inputError;
|
|
1659
1864
|
const storageBody = fieldKeys.toStorage(body);
|
|
1660
1865
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1661
1866
|
if (fieldErrors.length > 0) {
|
|
@@ -1695,7 +1900,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1695
1900
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1696
1901
|
const pType = registry.paragraph_types[comp.ref];
|
|
1697
1902
|
if (!pType) continue;
|
|
1698
|
-
const items =
|
|
1903
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1699
1904
|
mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1700
1905
|
}
|
|
1701
1906
|
}
|
|
@@ -1718,6 +1923,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1718
1923
|
404
|
|
1719
1924
|
);
|
|
1720
1925
|
}
|
|
1926
|
+
const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
|
|
1927
|
+
if (inputError) return inputError;
|
|
1721
1928
|
if (body["published"] === true) {
|
|
1722
1929
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1723
1930
|
});
|
|
@@ -1759,11 +1966,12 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1759
1966
|
];
|
|
1760
1967
|
if (db) {
|
|
1761
1968
|
for (const f of taxPatchParagraphFields) {
|
|
1969
|
+
if (!(f.name in body)) continue;
|
|
1762
1970
|
const comp = f.ui_component;
|
|
1763
1971
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1764
1972
|
const pType = registry.paragraph_types[comp.ref];
|
|
1765
1973
|
if (!pType) continue;
|
|
1766
|
-
const items =
|
|
1974
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1767
1975
|
mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1768
1976
|
}
|
|
1769
1977
|
}
|
|
@@ -1784,6 +1992,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1784
1992
|
404
|
|
1785
1993
|
);
|
|
1786
1994
|
}
|
|
1995
|
+
const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
|
|
1996
|
+
if (referrers.length > 0) {
|
|
1997
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
|
|
1998
|
+
}
|
|
1999
|
+
try {
|
|
2000
|
+
await repo.delete(id);
|
|
2001
|
+
} catch (err) {
|
|
2002
|
+
if (isForeignKeyViolation(err)) {
|
|
2003
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
|
|
2004
|
+
}
|
|
2005
|
+
throw err;
|
|
2006
|
+
}
|
|
1787
2007
|
const mediaDeltas = [
|
|
1788
2008
|
topLevelMediaDelta(mediaFields, item, null)
|
|
1789
2009
|
];
|
|
@@ -1797,7 +2017,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1797
2017
|
}
|
|
1798
2018
|
}
|
|
1799
2019
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1800
|
-
await repo.delete(id);
|
|
1801
2020
|
return c.json({ ok: true });
|
|
1802
2021
|
}
|
|
1803
2022
|
);
|
|
@@ -2259,7 +2478,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
|
|
|
2259
2478
|
|
|
2260
2479
|
// src/routes/admin/auth.ts
|
|
2261
2480
|
import { getCookie as getCookie2 } from "hono/cookie";
|
|
2262
|
-
import { sql as
|
|
2481
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2263
2482
|
|
|
2264
2483
|
// src/auth/password.ts
|
|
2265
2484
|
import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
|
|
@@ -2325,7 +2544,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2325
2544
|
);
|
|
2326
2545
|
}
|
|
2327
2546
|
const userResult = await db.execute(
|
|
2328
|
-
|
|
2547
|
+
sql5`SELECT id, email, password_hash, role_id, token_version
|
|
2329
2548
|
FROM users WHERE email = ${email} LIMIT 1`
|
|
2330
2549
|
);
|
|
2331
2550
|
const user = userResult.rows[0];
|
|
@@ -2338,7 +2557,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2338
2557
|
return c.json(invalidCredentials, 401);
|
|
2339
2558
|
}
|
|
2340
2559
|
const roleResult = await db.execute(
|
|
2341
|
-
|
|
2560
|
+
sql5`SELECT r.name AS name, u.must_change_password
|
|
2342
2561
|
FROM roles r
|
|
2343
2562
|
JOIN users u ON u.role_id = r.id
|
|
2344
2563
|
WHERE r.id = ${user.role_id} AND u.id = ${user.id}
|
|
@@ -2398,7 +2617,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2398
2617
|
);
|
|
2399
2618
|
}
|
|
2400
2619
|
const rowResult = await db.execute(
|
|
2401
|
-
|
|
2620
|
+
sql5`SELECT u.token_version, r.name AS role
|
|
2402
2621
|
FROM users u
|
|
2403
2622
|
JOIN roles r ON r.id = u.role_id
|
|
2404
2623
|
WHERE u.id = ${payload.user_id}
|
|
@@ -2422,7 +2641,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2422
2641
|
app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
|
|
2423
2642
|
const user = c.get("user");
|
|
2424
2643
|
await db.execute(
|
|
2425
|
-
|
|
2644
|
+
sql5`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
|
|
2426
2645
|
);
|
|
2427
2646
|
clearAuthCookie(c, "auth_token");
|
|
2428
2647
|
clearAuthCookie(c, "refresh_token", "/admin/api/auth");
|
|
@@ -2431,7 +2650,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2431
2650
|
}
|
|
2432
2651
|
|
|
2433
2652
|
// src/routes/admin/users.ts
|
|
2434
|
-
import { sql as
|
|
2653
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
2435
2654
|
import { randomBytes } from "crypto";
|
|
2436
2655
|
function getActingUser(c) {
|
|
2437
2656
|
return c.get("user");
|
|
@@ -2466,7 +2685,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2466
2685
|
);
|
|
2467
2686
|
}
|
|
2468
2687
|
const hashResult = await db.execute(
|
|
2469
|
-
|
|
2688
|
+
sql6`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2470
2689
|
);
|
|
2471
2690
|
const hashRow = hashResult.rows[0];
|
|
2472
2691
|
if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
|
|
@@ -2477,7 +2696,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2477
2696
|
}
|
|
2478
2697
|
const newHash = await hashPassword(newPassword);
|
|
2479
2698
|
await db.execute(
|
|
2480
|
-
|
|
2699
|
+
sql6`UPDATE users
|
|
2481
2700
|
SET password_hash = ${newHash},
|
|
2482
2701
|
must_change_password = false,
|
|
2483
2702
|
token_version = token_version + 1,
|
|
@@ -2488,7 +2707,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2488
2707
|
});
|
|
2489
2708
|
app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
|
|
2490
2709
|
const result = await db.execute(
|
|
2491
|
-
|
|
2710
|
+
sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2492
2711
|
u.created_at, u.updated_at
|
|
2493
2712
|
FROM users u
|
|
2494
2713
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2499,7 +2718,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2499
2718
|
app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
|
|
2500
2719
|
const id = c.req.param("id");
|
|
2501
2720
|
const result = await db.execute(
|
|
2502
|
-
|
|
2721
|
+
sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2503
2722
|
u.created_at, u.updated_at
|
|
2504
2723
|
FROM users u
|
|
2505
2724
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2544,7 +2763,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2544
2763
|
);
|
|
2545
2764
|
}
|
|
2546
2765
|
const roleResult = await db.execute(
|
|
2547
|
-
|
|
2766
|
+
sql6`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
|
|
2548
2767
|
);
|
|
2549
2768
|
const roleRow = roleResult.rows[0];
|
|
2550
2769
|
if (!roleRow) {
|
|
@@ -2559,7 +2778,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2559
2778
|
const tempPassword = generateTempPassword();
|
|
2560
2779
|
const passwordHash = await hashPassword(tempPassword);
|
|
2561
2780
|
const insertResult = await db.execute(
|
|
2562
|
-
|
|
2781
|
+
sql6`INSERT INTO users
|
|
2563
2782
|
(id, email, password_hash, role_id, must_change_password, token_version,
|
|
2564
2783
|
created_at, updated_at)
|
|
2565
2784
|
VALUES
|
|
@@ -2614,7 +2833,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2614
2833
|
);
|
|
2615
2834
|
}
|
|
2616
2835
|
const existingResult = await db.execute(
|
|
2617
|
-
|
|
2836
|
+
sql6`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
|
|
2618
2837
|
);
|
|
2619
2838
|
const existing = existingResult.rows[0];
|
|
2620
2839
|
if (!existing) {
|
|
@@ -2628,7 +2847,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2628
2847
|
let changeRole = false;
|
|
2629
2848
|
if (typeof body.role === "string" && body.role) {
|
|
2630
2849
|
const roleResult = await db.execute(
|
|
2631
|
-
|
|
2850
|
+
sql6`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
|
|
2632
2851
|
);
|
|
2633
2852
|
const roleRow = roleResult.rows[0];
|
|
2634
2853
|
if (!roleRow) {
|
|
@@ -2648,7 +2867,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2648
2867
|
}
|
|
2649
2868
|
if (changeRole) {
|
|
2650
2869
|
await db.execute(
|
|
2651
|
-
|
|
2870
|
+
sql6`UPDATE users
|
|
2652
2871
|
SET email = ${newEmail},
|
|
2653
2872
|
role_id = ${newRoleId},
|
|
2654
2873
|
token_version = token_version + 1,
|
|
@@ -2657,7 +2876,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2657
2876
|
);
|
|
2658
2877
|
} else {
|
|
2659
2878
|
await db.execute(
|
|
2660
|
-
|
|
2879
|
+
sql6`UPDATE users
|
|
2661
2880
|
SET email = ${newEmail},
|
|
2662
2881
|
role_id = ${newRoleId},
|
|
2663
2882
|
updated_at = NOW()
|
|
@@ -2665,7 +2884,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2665
2884
|
);
|
|
2666
2885
|
}
|
|
2667
2886
|
const updatedResult = await db.execute(
|
|
2668
|
-
|
|
2887
|
+
sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2669
2888
|
u.created_at, u.updated_at
|
|
2670
2889
|
FROM users u
|
|
2671
2890
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2702,7 +2921,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2702
2921
|
);
|
|
2703
2922
|
}
|
|
2704
2923
|
const result = await db.execute(
|
|
2705
|
-
|
|
2924
|
+
sql6`DELETE FROM users WHERE id = ${id} RETURNING id`
|
|
2706
2925
|
);
|
|
2707
2926
|
if (result.rows.length === 0) {
|
|
2708
2927
|
return c.json(
|
|
@@ -2733,7 +2952,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2733
2952
|
);
|
|
2734
2953
|
}
|
|
2735
2954
|
const existingResult = await db.execute(
|
|
2736
|
-
|
|
2955
|
+
sql6`SELECT id FROM users WHERE id = ${id} LIMIT 1`
|
|
2737
2956
|
);
|
|
2738
2957
|
const existing = existingResult.rows[0];
|
|
2739
2958
|
if (!existing) {
|
|
@@ -2745,7 +2964,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2745
2964
|
const tempPassword = generateTempPassword();
|
|
2746
2965
|
const passwordHash = await hashPassword(tempPassword);
|
|
2747
2966
|
await db.execute(
|
|
2748
|
-
|
|
2967
|
+
sql6`UPDATE users
|
|
2749
2968
|
SET password_hash = ${passwordHash},
|
|
2750
2969
|
must_change_password = true,
|
|
2751
2970
|
token_version = token_version + 1,
|
|
@@ -2758,10 +2977,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2758
2977
|
}
|
|
2759
2978
|
|
|
2760
2979
|
// src/routes/admin/config.ts
|
|
2761
|
-
import { sql as
|
|
2980
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
2762
2981
|
var API_VERSION = (() => {
|
|
2763
2982
|
try {
|
|
2764
|
-
return "0.
|
|
2983
|
+
return "0.8.1";
|
|
2765
2984
|
} catch {
|
|
2766
2985
|
return "0.0.0";
|
|
2767
2986
|
}
|
|
@@ -2780,7 +2999,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2780
2999
|
permissions: r.permissions
|
|
2781
3000
|
}));
|
|
2782
3001
|
const userResult = await db.execute(
|
|
2783
|
-
|
|
3002
|
+
sql7`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2784
3003
|
);
|
|
2785
3004
|
const userRow = userResult.rows[0];
|
|
2786
3005
|
return c.json({
|
|
@@ -2806,7 +3025,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2806
3025
|
}
|
|
2807
3026
|
|
|
2808
3027
|
// src/routes/admin/schema.ts
|
|
2809
|
-
import { sql as
|
|
3028
|
+
import { sql as sql8 } from "drizzle-orm";
|
|
2810
3029
|
function liveFields(fields) {
|
|
2811
3030
|
return fields.filter((f) => f.removed !== true);
|
|
2812
3031
|
}
|
|
@@ -2846,7 +3065,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2846
3065
|
const data = await Promise.all(
|
|
2847
3066
|
Object.values(registry.content_types).map(async (ct) => {
|
|
2848
3067
|
const result = await db.execute(
|
|
2849
|
-
|
|
3068
|
+
sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${ct.db.table_name}"`)}`
|
|
2850
3069
|
);
|
|
2851
3070
|
const count = Number(result.rows[0].count);
|
|
2852
3071
|
return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
|
|
@@ -2858,7 +3077,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2858
3077
|
const data = await Promise.all(
|
|
2859
3078
|
Object.values(registry.taxonomy_types).map(async (tt) => {
|
|
2860
3079
|
const result = await db.execute(
|
|
2861
|
-
|
|
3080
|
+
sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${tt.db.table_name}"`)}`
|
|
2862
3081
|
);
|
|
2863
3082
|
const count = Number(result.rows[0].count);
|
|
2864
3083
|
return { name: tt.name, label: tt.label, count };
|
|
@@ -2870,14 +3089,14 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2870
3089
|
|
|
2871
3090
|
// src/repositories/content.ts
|
|
2872
3091
|
import crypto from "crypto";
|
|
2873
|
-
import { sql as
|
|
3092
|
+
import { sql as sql9 } from "drizzle-orm";
|
|
2874
3093
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
2875
3094
|
function codeError(code, message) {
|
|
2876
3095
|
const err = new Error(message);
|
|
2877
3096
|
err.code = code;
|
|
2878
3097
|
return err;
|
|
2879
3098
|
}
|
|
2880
|
-
function
|
|
3099
|
+
function quoteIdent4(name) {
|
|
2881
3100
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
2882
3101
|
throw new Error(`Unsafe SQL identifier: ${name}`);
|
|
2883
3102
|
}
|
|
@@ -2892,43 +3111,43 @@ function quoteField(name) {
|
|
|
2892
3111
|
function buildConditions(published_only, filters, search) {
|
|
2893
3112
|
const conds = [];
|
|
2894
3113
|
if (published_only) {
|
|
2895
|
-
conds.push(
|
|
3114
|
+
conds.push(sql9`published = true`);
|
|
2896
3115
|
}
|
|
2897
3116
|
if (search && search.term.trim() !== "" && search.columns.length > 0) {
|
|
2898
3117
|
const pattern = `%${search.term.trim()}%`;
|
|
2899
|
-
const orConds = search.columns.map((col) =>
|
|
2900
|
-
conds.push(
|
|
3118
|
+
const orConds = search.columns.map((col) => sql9`${sql9.raw(quoteField(col))} ILIKE ${pattern}`);
|
|
3119
|
+
conds.push(sql9`(${sql9.join(orConds, sql9` OR `)})`);
|
|
2901
3120
|
}
|
|
2902
3121
|
if (filters) {
|
|
2903
3122
|
for (const [field, value] of Object.entries(filters)) {
|
|
2904
|
-
const col =
|
|
3123
|
+
const col = sql9.raw(quoteField(field));
|
|
2905
3124
|
if (Array.isArray(value)) {
|
|
2906
3125
|
if (value.length === 0) continue;
|
|
2907
|
-
const inList =
|
|
2908
|
-
value.map((v) =>
|
|
2909
|
-
|
|
3126
|
+
const inList = sql9.join(
|
|
3127
|
+
value.map((v) => sql9`${v}`),
|
|
3128
|
+
sql9`, `
|
|
2910
3129
|
);
|
|
2911
|
-
conds.push(
|
|
3130
|
+
conds.push(sql9`${col} IN (${inList})`);
|
|
2912
3131
|
} else if (typeof value === "object" && value !== null) {
|
|
2913
3132
|
const op = value;
|
|
2914
|
-
if (op.gt !== void 0) conds.push(
|
|
2915
|
-
if (op.gte !== void 0) conds.push(
|
|
2916
|
-
if (op.lt !== void 0) conds.push(
|
|
2917
|
-
if (op.lte !== void 0) conds.push(
|
|
3133
|
+
if (op.gt !== void 0) conds.push(sql9`${col} > ${op.gt}`);
|
|
3134
|
+
if (op.gte !== void 0) conds.push(sql9`${col} >= ${op.gte}`);
|
|
3135
|
+
if (op.lt !== void 0) conds.push(sql9`${col} < ${op.lt}`);
|
|
3136
|
+
if (op.lte !== void 0) conds.push(sql9`${col} <= ${op.lte}`);
|
|
2918
3137
|
} else {
|
|
2919
|
-
conds.push(
|
|
3138
|
+
conds.push(sql9`${col} = ${value}`);
|
|
2920
3139
|
}
|
|
2921
3140
|
}
|
|
2922
3141
|
}
|
|
2923
3142
|
return conds;
|
|
2924
3143
|
}
|
|
2925
3144
|
function whereFragment(conds) {
|
|
2926
|
-
return conds.length > 0 ?
|
|
3145
|
+
return conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
|
|
2927
3146
|
}
|
|
2928
3147
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
2929
3148
|
const { relations = {}, publishedRelations = false, sortableColumns } = options;
|
|
2930
3149
|
function tableRaw() {
|
|
2931
|
-
return
|
|
3150
|
+
return sql9.raw(quoteIdent4(tableName));
|
|
2932
3151
|
}
|
|
2933
3152
|
async function resolveRows(rows, include, cache) {
|
|
2934
3153
|
if (rows.length === 0) return rows;
|
|
@@ -2971,14 +3190,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2971
3190
|
const offset = (page - 1) * per_page;
|
|
2972
3191
|
const conds = buildConditions(published_only, filters, search);
|
|
2973
3192
|
const where = whereFragment(conds);
|
|
2974
|
-
const sortCol =
|
|
2975
|
-
const sortDir =
|
|
3193
|
+
const sortCol = sql9.raw(quoteIdent4(sort_by));
|
|
3194
|
+
const sortDir = sql9.raw(sort_order === "desc" ? "DESC" : "ASC");
|
|
2976
3195
|
const countResult = await db.execute(
|
|
2977
|
-
|
|
3196
|
+
sql9`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
|
|
2978
3197
|
);
|
|
2979
3198
|
const total = Number(countResult.rows[0]?.["total"] ?? 0);
|
|
2980
3199
|
const dataResult = await db.execute(
|
|
2981
|
-
|
|
3200
|
+
sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
|
|
2982
3201
|
);
|
|
2983
3202
|
const rows = dataResult.rows;
|
|
2984
3203
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2999,7 +3218,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2999
3218
|
},
|
|
3000
3219
|
async findOne(id, include = []) {
|
|
3001
3220
|
const result = await db.execute(
|
|
3002
|
-
|
|
3221
|
+
sql9`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
|
|
3003
3222
|
);
|
|
3004
3223
|
if (result.rows.length === 0) return null;
|
|
3005
3224
|
const rows = result.rows;
|
|
@@ -3009,7 +3228,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3009
3228
|
},
|
|
3010
3229
|
async findBySlug(slug, include = []) {
|
|
3011
3230
|
const result = await db.execute(
|
|
3012
|
-
|
|
3231
|
+
sql9`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
|
|
3013
3232
|
);
|
|
3014
3233
|
if (result.rows.length === 0) return null;
|
|
3015
3234
|
const rows = result.rows;
|
|
@@ -3025,10 +3244,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3025
3244
|
updated_at: /* @__PURE__ */ new Date()
|
|
3026
3245
|
};
|
|
3027
3246
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3028
|
-
const colsSql =
|
|
3029
|
-
const valsSql =
|
|
3247
|
+
const colsSql = sql9.join(entries.map(([k]) => sql9.raw(quoteIdent4(k))), sql9`, `);
|
|
3248
|
+
const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
|
|
3030
3249
|
const result = await db.execute(
|
|
3031
|
-
|
|
3250
|
+
sql9`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3032
3251
|
);
|
|
3033
3252
|
return result.rows[0];
|
|
3034
3253
|
},
|
|
@@ -3041,25 +3260,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3041
3260
|
if (entries.length === 0) {
|
|
3042
3261
|
return this.findOne(id);
|
|
3043
3262
|
}
|
|
3044
|
-
const setClauses =
|
|
3045
|
-
entries.map(([k, v]) =>
|
|
3046
|
-
|
|
3263
|
+
const setClauses = sql9.join(
|
|
3264
|
+
entries.map(([k, v]) => sql9`${sql9.raw(quoteIdent4(k))} = ${v}`),
|
|
3265
|
+
sql9`, `
|
|
3047
3266
|
);
|
|
3048
3267
|
const result = await db.execute(
|
|
3049
|
-
|
|
3268
|
+
sql9`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3050
3269
|
);
|
|
3051
3270
|
if (result.rows.length === 0) return null;
|
|
3052
3271
|
return result.rows[0];
|
|
3053
3272
|
},
|
|
3054
3273
|
async delete(id) {
|
|
3055
|
-
await db.execute(
|
|
3274
|
+
await db.execute(sql9`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
|
|
3056
3275
|
},
|
|
3057
3276
|
async findAll(opts = {}) {
|
|
3058
3277
|
const { include = [], published_only } = opts;
|
|
3059
3278
|
const conds = buildConditions(published_only, void 0, void 0);
|
|
3060
3279
|
const where = whereFragment(conds);
|
|
3061
3280
|
const result = await db.execute(
|
|
3062
|
-
|
|
3281
|
+
sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
|
|
3063
3282
|
);
|
|
3064
3283
|
const rows = result.rows;
|
|
3065
3284
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3071,23 +3290,23 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3071
3290
|
|
|
3072
3291
|
// src/repositories/media.ts
|
|
3073
3292
|
import crypto2 from "crypto";
|
|
3074
|
-
import { sql as
|
|
3293
|
+
import { sql as sql10 } from "drizzle-orm";
|
|
3075
3294
|
function createMediaRepository(db) {
|
|
3076
|
-
const TABLE =
|
|
3295
|
+
const TABLE = sql10.raw('"media"');
|
|
3077
3296
|
return {
|
|
3078
3297
|
async findMany(opts = {}) {
|
|
3079
3298
|
const { page = 1, per_page = 10, type, orphaned } = opts;
|
|
3080
3299
|
const offset = (page - 1) * per_page;
|
|
3081
3300
|
const conds = [];
|
|
3082
|
-
if (type) conds.push(
|
|
3083
|
-
if (orphaned) conds.push(
|
|
3084
|
-
const where = conds.length > 0 ?
|
|
3085
|
-
const countResult = await db.execute(
|
|
3301
|
+
if (type) conds.push(sql10`type = ${type}`);
|
|
3302
|
+
if (orphaned) conds.push(sql10`reference_count = 0`);
|
|
3303
|
+
const where = conds.length > 0 ? sql10` WHERE ${sql10.join(conds, sql10` AND `)}` : sql10``;
|
|
3304
|
+
const countResult = await db.execute(sql10`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
|
|
3086
3305
|
const total = Number(
|
|
3087
3306
|
countResult.rows[0]?.["total"] ?? 0
|
|
3088
3307
|
);
|
|
3089
3308
|
const dataResult = await db.execute(
|
|
3090
|
-
|
|
3309
|
+
sql10`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
|
|
3091
3310
|
);
|
|
3092
3311
|
const total_pages = Math.ceil(total / per_page);
|
|
3093
3312
|
return {
|
|
@@ -3105,7 +3324,7 @@ function createMediaRepository(db) {
|
|
|
3105
3324
|
},
|
|
3106
3325
|
async findOne(id) {
|
|
3107
3326
|
const result = await db.execute(
|
|
3108
|
-
|
|
3327
|
+
sql10`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
|
|
3109
3328
|
);
|
|
3110
3329
|
if (result.rows.length === 0) return null;
|
|
3111
3330
|
return result.rows[0];
|
|
@@ -3119,10 +3338,10 @@ function createMediaRepository(db) {
|
|
|
3119
3338
|
updated_at: /* @__PURE__ */ new Date()
|
|
3120
3339
|
};
|
|
3121
3340
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3122
|
-
const colsSql =
|
|
3123
|
-
const valsSql =
|
|
3341
|
+
const colsSql = sql10.join(entries.map(([k]) => sql10.raw(`"${k}"`)), sql10`, `);
|
|
3342
|
+
const valsSql = sql10.join(entries.map(([, v]) => sql10`${v}`), sql10`, `);
|
|
3124
3343
|
const result = await db.execute(
|
|
3125
|
-
|
|
3344
|
+
sql10`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3126
3345
|
);
|
|
3127
3346
|
return result.rows[0];
|
|
3128
3347
|
},
|
|
@@ -3132,31 +3351,31 @@ function createMediaRepository(db) {
|
|
|
3132
3351
|
([k, v]) => k !== "id" && v !== void 0
|
|
3133
3352
|
);
|
|
3134
3353
|
if (entries.length === 0) return this.findOne(id);
|
|
3135
|
-
const setClauses =
|
|
3136
|
-
entries.map(([k, v]) =>
|
|
3137
|
-
|
|
3354
|
+
const setClauses = sql10.join(
|
|
3355
|
+
entries.map(([k, v]) => sql10`${sql10.raw(`"${k}"`)} = ${v}`),
|
|
3356
|
+
sql10`, `
|
|
3138
3357
|
);
|
|
3139
3358
|
const result = await db.execute(
|
|
3140
|
-
|
|
3359
|
+
sql10`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3141
3360
|
);
|
|
3142
3361
|
if (result.rows.length === 0) return null;
|
|
3143
3362
|
return result.rows[0];
|
|
3144
3363
|
},
|
|
3145
3364
|
async delete(id) {
|
|
3146
|
-
await db.execute(
|
|
3365
|
+
await db.execute(sql10`DELETE FROM ${TABLE} WHERE id = ${id}`);
|
|
3147
3366
|
},
|
|
3148
3367
|
async incrementReferenceCount(ids) {
|
|
3149
3368
|
if (ids.length === 0) return;
|
|
3150
|
-
const inList =
|
|
3369
|
+
const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
|
|
3151
3370
|
await db.execute(
|
|
3152
|
-
|
|
3371
|
+
sql10`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
|
|
3153
3372
|
);
|
|
3154
3373
|
},
|
|
3155
3374
|
async decrementReferenceCount(ids) {
|
|
3156
3375
|
if (ids.length === 0) return;
|
|
3157
|
-
const inList =
|
|
3376
|
+
const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
|
|
3158
3377
|
await db.execute(
|
|
3159
|
-
|
|
3378
|
+
sql10`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
|
|
3160
3379
|
);
|
|
3161
3380
|
}
|
|
3162
3381
|
};
|
|
@@ -3289,7 +3508,7 @@ function createCmsApp(options) {
|
|
|
3289
3508
|
const requirePermission = createPermissionMiddleware(rolesRegistry);
|
|
3290
3509
|
const getUserRole = async (userId) => {
|
|
3291
3510
|
const result = await db.execute(
|
|
3292
|
-
|
|
3511
|
+
sql11`SELECT r.name AS role
|
|
3293
3512
|
FROM users u
|
|
3294
3513
|
JOIN roles r ON r.id = u.role_id
|
|
3295
3514
|
WHERE u.id = ${userId}
|
|
@@ -3459,7 +3678,7 @@ function createCmsApp(options) {
|
|
|
3459
3678
|
handler: null,
|
|
3460
3679
|
error: null
|
|
3461
3680
|
}));
|
|
3462
|
-
const ready = import("./handler-
|
|
3681
|
+
const ready = import("./handler-VEOWJRAL.mjs").then(({ createGraphQLHandler }) => {
|
|
3463
3682
|
gqlPasses.forEach((pass, i) => {
|
|
3464
3683
|
const mount = mounts[i];
|
|
3465
3684
|
try {
|