@bobbykim/manguito-cms-api 0.7.0 → 0.8.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-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 +309 -150
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +242 -98
- 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 sql10 } 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 sql3 } 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,60 @@ 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 { relationCardinality as relationCardinality2 } from "@bobbykim/manguito-cms-core";
|
|
1138
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1139
|
+
function isUuid(v) {
|
|
1140
|
+
return typeof v === "string" && UUID_RE.test(v);
|
|
1141
|
+
}
|
|
1142
|
+
function isPlainObject(v) {
|
|
1143
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1144
|
+
}
|
|
1145
|
+
function checkRelationInput(fields, body, registry, path = "") {
|
|
1146
|
+
const errors = [];
|
|
1147
|
+
for (const field of fields) {
|
|
1148
|
+
if (!(field.name in body)) continue;
|
|
1149
|
+
const cardinality = relationCardinality2(field);
|
|
1150
|
+
if (cardinality === null) continue;
|
|
1151
|
+
const value = body[field.name];
|
|
1152
|
+
const name = `${path}${field.name}`;
|
|
1153
|
+
if (field.field_type === "reference") {
|
|
1154
|
+
const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
|
|
1155
|
+
if (!ok) {
|
|
1156
|
+
errors.push({
|
|
1157
|
+
field: name,
|
|
1158
|
+
message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
|
|
1164
|
+
if (items === void 0) {
|
|
1165
|
+
errors.push({
|
|
1166
|
+
field: name,
|
|
1167
|
+
message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
|
|
1168
|
+
});
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
const comp = field.ui_component;
|
|
1172
|
+
const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
|
|
1173
|
+
if (!pType) continue;
|
|
1174
|
+
items.forEach((item, i) => {
|
|
1175
|
+
const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
|
|
1176
|
+
errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
return errors;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1082
1182
|
// src/routes/admin/content.ts
|
|
1083
|
-
function
|
|
1183
|
+
function quoteIdent2(name) {
|
|
1084
1184
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1085
1185
|
return `"${name}"`;
|
|
1086
1186
|
}
|
|
1087
1187
|
async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
1088
1188
|
const result = await db.execute(
|
|
1089
|
-
|
|
1189
|
+
sql3`SELECT * FROM ${sql3.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
1090
1190
|
);
|
|
1091
1191
|
const rows = result.rows;
|
|
1092
1192
|
for (const nf of pType.fields) {
|
|
@@ -1096,14 +1196,17 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
|
1096
1196
|
const nType = registry.paragraph_types[comp.ref];
|
|
1097
1197
|
if (!nType) continue;
|
|
1098
1198
|
for (const row of rows) {
|
|
1099
|
-
row[nf.name] =
|
|
1199
|
+
row[nf.name] = oneOrMany(
|
|
1200
|
+
relationCardinality3(nf) ?? "many",
|
|
1201
|
+
await loadParagraphRows(db, registry, nType, row["id"], nf.name)
|
|
1202
|
+
);
|
|
1100
1203
|
}
|
|
1101
1204
|
}
|
|
1102
1205
|
return rows;
|
|
1103
1206
|
}
|
|
1104
1207
|
async function lookupBasePathId(db, pathOrName) {
|
|
1105
1208
|
const r = await db.execute(
|
|
1106
|
-
|
|
1209
|
+
sql3`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
1107
1210
|
);
|
|
1108
1211
|
return r.rows[0]?.id ?? null;
|
|
1109
1212
|
}
|
|
@@ -1117,6 +1220,14 @@ function isEmpty(value) {
|
|
|
1117
1220
|
function checkRequiredFields(fields, data) {
|
|
1118
1221
|
return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
|
|
1119
1222
|
}
|
|
1223
|
+
function relationShapeError(c, fields, body, registry) {
|
|
1224
|
+
const details = checkRelationInput(fields, body, registry);
|
|
1225
|
+
if (details.length === 0) return null;
|
|
1226
|
+
return c.json(
|
|
1227
|
+
{ ok: false, error: { code: "VALIDATION_ERROR", message: "Relation fields have the wrong shape", details } },
|
|
1228
|
+
422
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1120
1231
|
function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
|
|
1121
1232
|
const searchParams = new URL(url).searchParams;
|
|
1122
1233
|
const pagination = parsePagination(
|
|
@@ -1258,11 +1369,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1258
1369
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1259
1370
|
const pType = registry.paragraph_types[comp.ref];
|
|
1260
1371
|
if (!pType) continue;
|
|
1261
|
-
row[f.name] = await loadParagraphRows(db, registry, pType, id, f.name);
|
|
1372
|
+
row[f.name] = oneOrMany(relationCardinality3(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name));
|
|
1262
1373
|
} else if (f.db_column.junction) {
|
|
1263
1374
|
const j = f.db_column.junction;
|
|
1264
1375
|
const r = await db.execute(
|
|
1265
|
-
|
|
1376
|
+
sql3`SELECT ${sql3.raw(quoteIdent2(j.right_column))} FROM ${sql3.raw(quoteIdent2(j.table_name))} WHERE ${sql3.raw(quoteIdent2(j.left_column))} = ${id}`
|
|
1266
1377
|
);
|
|
1267
1378
|
row[f.name] = r.rows.map(
|
|
1268
1379
|
(row2) => row2[j.right_column]
|
|
@@ -1338,6 +1449,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1338
1449
|
}
|
|
1339
1450
|
);
|
|
1340
1451
|
const writeNewItem = async (c, body) => {
|
|
1452
|
+
const shapeError = relationShapeError(c, contentType.fields, body, registry);
|
|
1453
|
+
if (shapeError) return shapeError;
|
|
1341
1454
|
const storageBody = fieldKeys.toStorage(body);
|
|
1342
1455
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1343
1456
|
if (fieldErrors.length > 0) {
|
|
@@ -1401,7 +1514,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1401
1514
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1402
1515
|
const pType = registry.paragraph_types[comp.ref];
|
|
1403
1516
|
if (!pType) continue;
|
|
1404
|
-
const items =
|
|
1517
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1405
1518
|
mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
|
|
1406
1519
|
}
|
|
1407
1520
|
for (const f of junctionFields) {
|
|
@@ -1472,6 +1585,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1472
1585
|
}
|
|
1473
1586
|
);
|
|
1474
1587
|
const writeExistingItem = async (c, id, existing, body) => {
|
|
1588
|
+
const shapeError = relationShapeError(c, contentType.fields, body, registry);
|
|
1589
|
+
if (shapeError) return shapeError;
|
|
1475
1590
|
const storageBody = fieldKeys.toStorage(body);
|
|
1476
1591
|
if (body["published"] === true) {
|
|
1477
1592
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
@@ -1518,14 +1633,16 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1518
1633
|
];
|
|
1519
1634
|
if (db) {
|
|
1520
1635
|
for (const f of patchParagraphFields) {
|
|
1636
|
+
if (!(f.name in body)) continue;
|
|
1521
1637
|
const comp = f.ui_component;
|
|
1522
1638
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1523
1639
|
const pType = registry.paragraph_types[comp.ref];
|
|
1524
1640
|
if (!pType) continue;
|
|
1525
|
-
const items =
|
|
1641
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1526
1642
|
mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
|
|
1527
1643
|
}
|
|
1528
1644
|
for (const f of patchJunctionFields) {
|
|
1645
|
+
if (!(f.name in body)) continue;
|
|
1529
1646
|
const junction = f.db_column.junction;
|
|
1530
1647
|
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
1531
1648
|
await persistJunctionField(db, id, junction, relatedIds);
|
|
@@ -1565,6 +1682,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1565
1682
|
404
|
|
1566
1683
|
);
|
|
1567
1684
|
}
|
|
1685
|
+
const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
|
|
1686
|
+
if (referrers.length > 0) {
|
|
1687
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
|
|
1688
|
+
}
|
|
1689
|
+
try {
|
|
1690
|
+
await repo.delete(id);
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
if (isForeignKeyViolation(err)) {
|
|
1693
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
|
|
1694
|
+
}
|
|
1695
|
+
throw err;
|
|
1696
|
+
}
|
|
1568
1697
|
const mediaDeltas = [
|
|
1569
1698
|
topLevelMediaDelta(mediaFields, item, null)
|
|
1570
1699
|
];
|
|
@@ -1578,7 +1707,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1578
1707
|
}
|
|
1579
1708
|
}
|
|
1580
1709
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1581
|
-
await repo.delete(id);
|
|
1582
1710
|
return c.json({ ok: true });
|
|
1583
1711
|
}
|
|
1584
1712
|
);
|
|
@@ -1656,6 +1784,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1656
1784
|
requirePermission("content:create"),
|
|
1657
1785
|
async (c) => {
|
|
1658
1786
|
const body = await c.req.json();
|
|
1787
|
+
const shapeError = relationShapeError(c, taxonomyType.fields, body, registry);
|
|
1788
|
+
if (shapeError) return shapeError;
|
|
1659
1789
|
const storageBody = fieldKeys.toStorage(body);
|
|
1660
1790
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1661
1791
|
if (fieldErrors.length > 0) {
|
|
@@ -1695,7 +1825,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1695
1825
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1696
1826
|
const pType = registry.paragraph_types[comp.ref];
|
|
1697
1827
|
if (!pType) continue;
|
|
1698
|
-
const items =
|
|
1828
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1699
1829
|
mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1700
1830
|
}
|
|
1701
1831
|
}
|
|
@@ -1718,6 +1848,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1718
1848
|
404
|
|
1719
1849
|
);
|
|
1720
1850
|
}
|
|
1851
|
+
const shapeError = relationShapeError(c, taxonomyType.fields, body, registry);
|
|
1852
|
+
if (shapeError) return shapeError;
|
|
1721
1853
|
if (body["published"] === true) {
|
|
1722
1854
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1723
1855
|
});
|
|
@@ -1759,11 +1891,12 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1759
1891
|
];
|
|
1760
1892
|
if (db) {
|
|
1761
1893
|
for (const f of taxPatchParagraphFields) {
|
|
1894
|
+
if (!(f.name in body)) continue;
|
|
1762
1895
|
const comp = f.ui_component;
|
|
1763
1896
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1764
1897
|
const pType = registry.paragraph_types[comp.ref];
|
|
1765
1898
|
if (!pType) continue;
|
|
1766
|
-
const items =
|
|
1899
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1767
1900
|
mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1768
1901
|
}
|
|
1769
1902
|
}
|
|
@@ -1784,6 +1917,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1784
1917
|
404
|
|
1785
1918
|
);
|
|
1786
1919
|
}
|
|
1920
|
+
const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
|
|
1921
|
+
if (referrers.length > 0) {
|
|
1922
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
|
|
1923
|
+
}
|
|
1924
|
+
try {
|
|
1925
|
+
await repo.delete(id);
|
|
1926
|
+
} catch (err) {
|
|
1927
|
+
if (isForeignKeyViolation(err)) {
|
|
1928
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
|
|
1929
|
+
}
|
|
1930
|
+
throw err;
|
|
1931
|
+
}
|
|
1787
1932
|
const mediaDeltas = [
|
|
1788
1933
|
topLevelMediaDelta(mediaFields, item, null)
|
|
1789
1934
|
];
|
|
@@ -1797,7 +1942,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1797
1942
|
}
|
|
1798
1943
|
}
|
|
1799
1944
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1800
|
-
await repo.delete(id);
|
|
1801
1945
|
return c.json({ ok: true });
|
|
1802
1946
|
}
|
|
1803
1947
|
);
|
|
@@ -2259,7 +2403,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
|
|
|
2259
2403
|
|
|
2260
2404
|
// src/routes/admin/auth.ts
|
|
2261
2405
|
import { getCookie as getCookie2 } from "hono/cookie";
|
|
2262
|
-
import { sql as
|
|
2406
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
2263
2407
|
|
|
2264
2408
|
// src/auth/password.ts
|
|
2265
2409
|
import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
|
|
@@ -2325,7 +2469,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2325
2469
|
);
|
|
2326
2470
|
}
|
|
2327
2471
|
const userResult = await db.execute(
|
|
2328
|
-
|
|
2472
|
+
sql4`SELECT id, email, password_hash, role_id, token_version
|
|
2329
2473
|
FROM users WHERE email = ${email} LIMIT 1`
|
|
2330
2474
|
);
|
|
2331
2475
|
const user = userResult.rows[0];
|
|
@@ -2338,7 +2482,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2338
2482
|
return c.json(invalidCredentials, 401);
|
|
2339
2483
|
}
|
|
2340
2484
|
const roleResult = await db.execute(
|
|
2341
|
-
|
|
2485
|
+
sql4`SELECT r.name AS name, u.must_change_password
|
|
2342
2486
|
FROM roles r
|
|
2343
2487
|
JOIN users u ON u.role_id = r.id
|
|
2344
2488
|
WHERE r.id = ${user.role_id} AND u.id = ${user.id}
|
|
@@ -2398,7 +2542,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2398
2542
|
);
|
|
2399
2543
|
}
|
|
2400
2544
|
const rowResult = await db.execute(
|
|
2401
|
-
|
|
2545
|
+
sql4`SELECT u.token_version, r.name AS role
|
|
2402
2546
|
FROM users u
|
|
2403
2547
|
JOIN roles r ON r.id = u.role_id
|
|
2404
2548
|
WHERE u.id = ${payload.user_id}
|
|
@@ -2422,7 +2566,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2422
2566
|
app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
|
|
2423
2567
|
const user = c.get("user");
|
|
2424
2568
|
await db.execute(
|
|
2425
|
-
|
|
2569
|
+
sql4`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
|
|
2426
2570
|
);
|
|
2427
2571
|
clearAuthCookie(c, "auth_token");
|
|
2428
2572
|
clearAuthCookie(c, "refresh_token", "/admin/api/auth");
|
|
@@ -2431,7 +2575,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2431
2575
|
}
|
|
2432
2576
|
|
|
2433
2577
|
// src/routes/admin/users.ts
|
|
2434
|
-
import { sql as
|
|
2578
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2435
2579
|
import { randomBytes } from "crypto";
|
|
2436
2580
|
function getActingUser(c) {
|
|
2437
2581
|
return c.get("user");
|
|
@@ -2466,7 +2610,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2466
2610
|
);
|
|
2467
2611
|
}
|
|
2468
2612
|
const hashResult = await db.execute(
|
|
2469
|
-
|
|
2613
|
+
sql5`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2470
2614
|
);
|
|
2471
2615
|
const hashRow = hashResult.rows[0];
|
|
2472
2616
|
if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
|
|
@@ -2477,7 +2621,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2477
2621
|
}
|
|
2478
2622
|
const newHash = await hashPassword(newPassword);
|
|
2479
2623
|
await db.execute(
|
|
2480
|
-
|
|
2624
|
+
sql5`UPDATE users
|
|
2481
2625
|
SET password_hash = ${newHash},
|
|
2482
2626
|
must_change_password = false,
|
|
2483
2627
|
token_version = token_version + 1,
|
|
@@ -2488,7 +2632,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2488
2632
|
});
|
|
2489
2633
|
app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
|
|
2490
2634
|
const result = await db.execute(
|
|
2491
|
-
|
|
2635
|
+
sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2492
2636
|
u.created_at, u.updated_at
|
|
2493
2637
|
FROM users u
|
|
2494
2638
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2499,7 +2643,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2499
2643
|
app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
|
|
2500
2644
|
const id = c.req.param("id");
|
|
2501
2645
|
const result = await db.execute(
|
|
2502
|
-
|
|
2646
|
+
sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2503
2647
|
u.created_at, u.updated_at
|
|
2504
2648
|
FROM users u
|
|
2505
2649
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2544,7 +2688,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2544
2688
|
);
|
|
2545
2689
|
}
|
|
2546
2690
|
const roleResult = await db.execute(
|
|
2547
|
-
|
|
2691
|
+
sql5`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
|
|
2548
2692
|
);
|
|
2549
2693
|
const roleRow = roleResult.rows[0];
|
|
2550
2694
|
if (!roleRow) {
|
|
@@ -2559,7 +2703,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2559
2703
|
const tempPassword = generateTempPassword();
|
|
2560
2704
|
const passwordHash = await hashPassword(tempPassword);
|
|
2561
2705
|
const insertResult = await db.execute(
|
|
2562
|
-
|
|
2706
|
+
sql5`INSERT INTO users
|
|
2563
2707
|
(id, email, password_hash, role_id, must_change_password, token_version,
|
|
2564
2708
|
created_at, updated_at)
|
|
2565
2709
|
VALUES
|
|
@@ -2614,7 +2758,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2614
2758
|
);
|
|
2615
2759
|
}
|
|
2616
2760
|
const existingResult = await db.execute(
|
|
2617
|
-
|
|
2761
|
+
sql5`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
|
|
2618
2762
|
);
|
|
2619
2763
|
const existing = existingResult.rows[0];
|
|
2620
2764
|
if (!existing) {
|
|
@@ -2628,7 +2772,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2628
2772
|
let changeRole = false;
|
|
2629
2773
|
if (typeof body.role === "string" && body.role) {
|
|
2630
2774
|
const roleResult = await db.execute(
|
|
2631
|
-
|
|
2775
|
+
sql5`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
|
|
2632
2776
|
);
|
|
2633
2777
|
const roleRow = roleResult.rows[0];
|
|
2634
2778
|
if (!roleRow) {
|
|
@@ -2648,7 +2792,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2648
2792
|
}
|
|
2649
2793
|
if (changeRole) {
|
|
2650
2794
|
await db.execute(
|
|
2651
|
-
|
|
2795
|
+
sql5`UPDATE users
|
|
2652
2796
|
SET email = ${newEmail},
|
|
2653
2797
|
role_id = ${newRoleId},
|
|
2654
2798
|
token_version = token_version + 1,
|
|
@@ -2657,7 +2801,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2657
2801
|
);
|
|
2658
2802
|
} else {
|
|
2659
2803
|
await db.execute(
|
|
2660
|
-
|
|
2804
|
+
sql5`UPDATE users
|
|
2661
2805
|
SET email = ${newEmail},
|
|
2662
2806
|
role_id = ${newRoleId},
|
|
2663
2807
|
updated_at = NOW()
|
|
@@ -2665,7 +2809,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2665
2809
|
);
|
|
2666
2810
|
}
|
|
2667
2811
|
const updatedResult = await db.execute(
|
|
2668
|
-
|
|
2812
|
+
sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2669
2813
|
u.created_at, u.updated_at
|
|
2670
2814
|
FROM users u
|
|
2671
2815
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2702,7 +2846,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2702
2846
|
);
|
|
2703
2847
|
}
|
|
2704
2848
|
const result = await db.execute(
|
|
2705
|
-
|
|
2849
|
+
sql5`DELETE FROM users WHERE id = ${id} RETURNING id`
|
|
2706
2850
|
);
|
|
2707
2851
|
if (result.rows.length === 0) {
|
|
2708
2852
|
return c.json(
|
|
@@ -2733,7 +2877,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2733
2877
|
);
|
|
2734
2878
|
}
|
|
2735
2879
|
const existingResult = await db.execute(
|
|
2736
|
-
|
|
2880
|
+
sql5`SELECT id FROM users WHERE id = ${id} LIMIT 1`
|
|
2737
2881
|
);
|
|
2738
2882
|
const existing = existingResult.rows[0];
|
|
2739
2883
|
if (!existing) {
|
|
@@ -2745,7 +2889,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2745
2889
|
const tempPassword = generateTempPassword();
|
|
2746
2890
|
const passwordHash = await hashPassword(tempPassword);
|
|
2747
2891
|
await db.execute(
|
|
2748
|
-
|
|
2892
|
+
sql5`UPDATE users
|
|
2749
2893
|
SET password_hash = ${passwordHash},
|
|
2750
2894
|
must_change_password = true,
|
|
2751
2895
|
token_version = token_version + 1,
|
|
@@ -2758,10 +2902,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2758
2902
|
}
|
|
2759
2903
|
|
|
2760
2904
|
// src/routes/admin/config.ts
|
|
2761
|
-
import { sql as
|
|
2905
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
2762
2906
|
var API_VERSION = (() => {
|
|
2763
2907
|
try {
|
|
2764
|
-
return "0.
|
|
2908
|
+
return "0.8.0";
|
|
2765
2909
|
} catch {
|
|
2766
2910
|
return "0.0.0";
|
|
2767
2911
|
}
|
|
@@ -2780,7 +2924,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2780
2924
|
permissions: r.permissions
|
|
2781
2925
|
}));
|
|
2782
2926
|
const userResult = await db.execute(
|
|
2783
|
-
|
|
2927
|
+
sql6`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2784
2928
|
);
|
|
2785
2929
|
const userRow = userResult.rows[0];
|
|
2786
2930
|
return c.json({
|
|
@@ -2806,7 +2950,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2806
2950
|
}
|
|
2807
2951
|
|
|
2808
2952
|
// src/routes/admin/schema.ts
|
|
2809
|
-
import { sql as
|
|
2953
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
2810
2954
|
function liveFields(fields) {
|
|
2811
2955
|
return fields.filter((f) => f.removed !== true);
|
|
2812
2956
|
}
|
|
@@ -2846,7 +2990,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2846
2990
|
const data = await Promise.all(
|
|
2847
2991
|
Object.values(registry.content_types).map(async (ct) => {
|
|
2848
2992
|
const result = await db.execute(
|
|
2849
|
-
|
|
2993
|
+
sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${ct.db.table_name}"`)}`
|
|
2850
2994
|
);
|
|
2851
2995
|
const count = Number(result.rows[0].count);
|
|
2852
2996
|
return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
|
|
@@ -2858,7 +3002,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2858
3002
|
const data = await Promise.all(
|
|
2859
3003
|
Object.values(registry.taxonomy_types).map(async (tt) => {
|
|
2860
3004
|
const result = await db.execute(
|
|
2861
|
-
|
|
3005
|
+
sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${tt.db.table_name}"`)}`
|
|
2862
3006
|
);
|
|
2863
3007
|
const count = Number(result.rows[0].count);
|
|
2864
3008
|
return { name: tt.name, label: tt.label, count };
|
|
@@ -2870,14 +3014,14 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2870
3014
|
|
|
2871
3015
|
// src/repositories/content.ts
|
|
2872
3016
|
import crypto from "crypto";
|
|
2873
|
-
import { sql as
|
|
3017
|
+
import { sql as sql8 } from "drizzle-orm";
|
|
2874
3018
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
2875
3019
|
function codeError(code, message) {
|
|
2876
3020
|
const err = new Error(message);
|
|
2877
3021
|
err.code = code;
|
|
2878
3022
|
return err;
|
|
2879
3023
|
}
|
|
2880
|
-
function
|
|
3024
|
+
function quoteIdent3(name) {
|
|
2881
3025
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
2882
3026
|
throw new Error(`Unsafe SQL identifier: ${name}`);
|
|
2883
3027
|
}
|
|
@@ -2892,43 +3036,43 @@ function quoteField(name) {
|
|
|
2892
3036
|
function buildConditions(published_only, filters, search) {
|
|
2893
3037
|
const conds = [];
|
|
2894
3038
|
if (published_only) {
|
|
2895
|
-
conds.push(
|
|
3039
|
+
conds.push(sql8`published = true`);
|
|
2896
3040
|
}
|
|
2897
3041
|
if (search && search.term.trim() !== "" && search.columns.length > 0) {
|
|
2898
3042
|
const pattern = `%${search.term.trim()}%`;
|
|
2899
|
-
const orConds = search.columns.map((col) =>
|
|
2900
|
-
conds.push(
|
|
3043
|
+
const orConds = search.columns.map((col) => sql8`${sql8.raw(quoteField(col))} ILIKE ${pattern}`);
|
|
3044
|
+
conds.push(sql8`(${sql8.join(orConds, sql8` OR `)})`);
|
|
2901
3045
|
}
|
|
2902
3046
|
if (filters) {
|
|
2903
3047
|
for (const [field, value] of Object.entries(filters)) {
|
|
2904
|
-
const col =
|
|
3048
|
+
const col = sql8.raw(quoteField(field));
|
|
2905
3049
|
if (Array.isArray(value)) {
|
|
2906
3050
|
if (value.length === 0) continue;
|
|
2907
|
-
const inList =
|
|
2908
|
-
value.map((v) =>
|
|
2909
|
-
|
|
3051
|
+
const inList = sql8.join(
|
|
3052
|
+
value.map((v) => sql8`${v}`),
|
|
3053
|
+
sql8`, `
|
|
2910
3054
|
);
|
|
2911
|
-
conds.push(
|
|
3055
|
+
conds.push(sql8`${col} IN (${inList})`);
|
|
2912
3056
|
} else if (typeof value === "object" && value !== null) {
|
|
2913
3057
|
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(
|
|
3058
|
+
if (op.gt !== void 0) conds.push(sql8`${col} > ${op.gt}`);
|
|
3059
|
+
if (op.gte !== void 0) conds.push(sql8`${col} >= ${op.gte}`);
|
|
3060
|
+
if (op.lt !== void 0) conds.push(sql8`${col} < ${op.lt}`);
|
|
3061
|
+
if (op.lte !== void 0) conds.push(sql8`${col} <= ${op.lte}`);
|
|
2918
3062
|
} else {
|
|
2919
|
-
conds.push(
|
|
3063
|
+
conds.push(sql8`${col} = ${value}`);
|
|
2920
3064
|
}
|
|
2921
3065
|
}
|
|
2922
3066
|
}
|
|
2923
3067
|
return conds;
|
|
2924
3068
|
}
|
|
2925
3069
|
function whereFragment(conds) {
|
|
2926
|
-
return conds.length > 0 ?
|
|
3070
|
+
return conds.length > 0 ? sql8` WHERE ${sql8.join(conds, sql8` AND `)}` : sql8``;
|
|
2927
3071
|
}
|
|
2928
3072
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
2929
3073
|
const { relations = {}, publishedRelations = false, sortableColumns } = options;
|
|
2930
3074
|
function tableRaw() {
|
|
2931
|
-
return
|
|
3075
|
+
return sql8.raw(quoteIdent3(tableName));
|
|
2932
3076
|
}
|
|
2933
3077
|
async function resolveRows(rows, include, cache) {
|
|
2934
3078
|
if (rows.length === 0) return rows;
|
|
@@ -2971,14 +3115,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2971
3115
|
const offset = (page - 1) * per_page;
|
|
2972
3116
|
const conds = buildConditions(published_only, filters, search);
|
|
2973
3117
|
const where = whereFragment(conds);
|
|
2974
|
-
const sortCol =
|
|
2975
|
-
const sortDir =
|
|
3118
|
+
const sortCol = sql8.raw(quoteIdent3(sort_by));
|
|
3119
|
+
const sortDir = sql8.raw(sort_order === "desc" ? "DESC" : "ASC");
|
|
2976
3120
|
const countResult = await db.execute(
|
|
2977
|
-
|
|
3121
|
+
sql8`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
|
|
2978
3122
|
);
|
|
2979
3123
|
const total = Number(countResult.rows[0]?.["total"] ?? 0);
|
|
2980
3124
|
const dataResult = await db.execute(
|
|
2981
|
-
|
|
3125
|
+
sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
|
|
2982
3126
|
);
|
|
2983
3127
|
const rows = dataResult.rows;
|
|
2984
3128
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2999,7 +3143,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2999
3143
|
},
|
|
3000
3144
|
async findOne(id, include = []) {
|
|
3001
3145
|
const result = await db.execute(
|
|
3002
|
-
|
|
3146
|
+
sql8`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
|
|
3003
3147
|
);
|
|
3004
3148
|
if (result.rows.length === 0) return null;
|
|
3005
3149
|
const rows = result.rows;
|
|
@@ -3009,7 +3153,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3009
3153
|
},
|
|
3010
3154
|
async findBySlug(slug, include = []) {
|
|
3011
3155
|
const result = await db.execute(
|
|
3012
|
-
|
|
3156
|
+
sql8`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
|
|
3013
3157
|
);
|
|
3014
3158
|
if (result.rows.length === 0) return null;
|
|
3015
3159
|
const rows = result.rows;
|
|
@@ -3025,10 +3169,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3025
3169
|
updated_at: /* @__PURE__ */ new Date()
|
|
3026
3170
|
};
|
|
3027
3171
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3028
|
-
const colsSql =
|
|
3029
|
-
const valsSql =
|
|
3172
|
+
const colsSql = sql8.join(entries.map(([k]) => sql8.raw(quoteIdent3(k))), sql8`, `);
|
|
3173
|
+
const valsSql = sql8.join(entries.map(([, v]) => sql8`${v}`), sql8`, `);
|
|
3030
3174
|
const result = await db.execute(
|
|
3031
|
-
|
|
3175
|
+
sql8`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3032
3176
|
);
|
|
3033
3177
|
return result.rows[0];
|
|
3034
3178
|
},
|
|
@@ -3041,25 +3185,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3041
3185
|
if (entries.length === 0) {
|
|
3042
3186
|
return this.findOne(id);
|
|
3043
3187
|
}
|
|
3044
|
-
const setClauses =
|
|
3045
|
-
entries.map(([k, v]) =>
|
|
3046
|
-
|
|
3188
|
+
const setClauses = sql8.join(
|
|
3189
|
+
entries.map(([k, v]) => sql8`${sql8.raw(quoteIdent3(k))} = ${v}`),
|
|
3190
|
+
sql8`, `
|
|
3047
3191
|
);
|
|
3048
3192
|
const result = await db.execute(
|
|
3049
|
-
|
|
3193
|
+
sql8`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3050
3194
|
);
|
|
3051
3195
|
if (result.rows.length === 0) return null;
|
|
3052
3196
|
return result.rows[0];
|
|
3053
3197
|
},
|
|
3054
3198
|
async delete(id) {
|
|
3055
|
-
await db.execute(
|
|
3199
|
+
await db.execute(sql8`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
|
|
3056
3200
|
},
|
|
3057
3201
|
async findAll(opts = {}) {
|
|
3058
3202
|
const { include = [], published_only } = opts;
|
|
3059
3203
|
const conds = buildConditions(published_only, void 0, void 0);
|
|
3060
3204
|
const where = whereFragment(conds);
|
|
3061
3205
|
const result = await db.execute(
|
|
3062
|
-
|
|
3206
|
+
sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
|
|
3063
3207
|
);
|
|
3064
3208
|
const rows = result.rows;
|
|
3065
3209
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3071,23 +3215,23 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3071
3215
|
|
|
3072
3216
|
// src/repositories/media.ts
|
|
3073
3217
|
import crypto2 from "crypto";
|
|
3074
|
-
import { sql as
|
|
3218
|
+
import { sql as sql9 } from "drizzle-orm";
|
|
3075
3219
|
function createMediaRepository(db) {
|
|
3076
|
-
const TABLE =
|
|
3220
|
+
const TABLE = sql9.raw('"media"');
|
|
3077
3221
|
return {
|
|
3078
3222
|
async findMany(opts = {}) {
|
|
3079
3223
|
const { page = 1, per_page = 10, type, orphaned } = opts;
|
|
3080
3224
|
const offset = (page - 1) * per_page;
|
|
3081
3225
|
const conds = [];
|
|
3082
|
-
if (type) conds.push(
|
|
3083
|
-
if (orphaned) conds.push(
|
|
3084
|
-
const where = conds.length > 0 ?
|
|
3085
|
-
const countResult = await db.execute(
|
|
3226
|
+
if (type) conds.push(sql9`type = ${type}`);
|
|
3227
|
+
if (orphaned) conds.push(sql9`reference_count = 0`);
|
|
3228
|
+
const where = conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
|
|
3229
|
+
const countResult = await db.execute(sql9`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
|
|
3086
3230
|
const total = Number(
|
|
3087
3231
|
countResult.rows[0]?.["total"] ?? 0
|
|
3088
3232
|
);
|
|
3089
3233
|
const dataResult = await db.execute(
|
|
3090
|
-
|
|
3234
|
+
sql9`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
|
|
3091
3235
|
);
|
|
3092
3236
|
const total_pages = Math.ceil(total / per_page);
|
|
3093
3237
|
return {
|
|
@@ -3105,7 +3249,7 @@ function createMediaRepository(db) {
|
|
|
3105
3249
|
},
|
|
3106
3250
|
async findOne(id) {
|
|
3107
3251
|
const result = await db.execute(
|
|
3108
|
-
|
|
3252
|
+
sql9`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
|
|
3109
3253
|
);
|
|
3110
3254
|
if (result.rows.length === 0) return null;
|
|
3111
3255
|
return result.rows[0];
|
|
@@ -3119,10 +3263,10 @@ function createMediaRepository(db) {
|
|
|
3119
3263
|
updated_at: /* @__PURE__ */ new Date()
|
|
3120
3264
|
};
|
|
3121
3265
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3122
|
-
const colsSql =
|
|
3123
|
-
const valsSql =
|
|
3266
|
+
const colsSql = sql9.join(entries.map(([k]) => sql9.raw(`"${k}"`)), sql9`, `);
|
|
3267
|
+
const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
|
|
3124
3268
|
const result = await db.execute(
|
|
3125
|
-
|
|
3269
|
+
sql9`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3126
3270
|
);
|
|
3127
3271
|
return result.rows[0];
|
|
3128
3272
|
},
|
|
@@ -3132,31 +3276,31 @@ function createMediaRepository(db) {
|
|
|
3132
3276
|
([k, v]) => k !== "id" && v !== void 0
|
|
3133
3277
|
);
|
|
3134
3278
|
if (entries.length === 0) return this.findOne(id);
|
|
3135
|
-
const setClauses =
|
|
3136
|
-
entries.map(([k, v]) =>
|
|
3137
|
-
|
|
3279
|
+
const setClauses = sql9.join(
|
|
3280
|
+
entries.map(([k, v]) => sql9`${sql9.raw(`"${k}"`)} = ${v}`),
|
|
3281
|
+
sql9`, `
|
|
3138
3282
|
);
|
|
3139
3283
|
const result = await db.execute(
|
|
3140
|
-
|
|
3284
|
+
sql9`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3141
3285
|
);
|
|
3142
3286
|
if (result.rows.length === 0) return null;
|
|
3143
3287
|
return result.rows[0];
|
|
3144
3288
|
},
|
|
3145
3289
|
async delete(id) {
|
|
3146
|
-
await db.execute(
|
|
3290
|
+
await db.execute(sql9`DELETE FROM ${TABLE} WHERE id = ${id}`);
|
|
3147
3291
|
},
|
|
3148
3292
|
async incrementReferenceCount(ids) {
|
|
3149
3293
|
if (ids.length === 0) return;
|
|
3150
|
-
const inList =
|
|
3294
|
+
const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
|
|
3151
3295
|
await db.execute(
|
|
3152
|
-
|
|
3296
|
+
sql9`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
|
|
3153
3297
|
);
|
|
3154
3298
|
},
|
|
3155
3299
|
async decrementReferenceCount(ids) {
|
|
3156
3300
|
if (ids.length === 0) return;
|
|
3157
|
-
const inList =
|
|
3301
|
+
const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
|
|
3158
3302
|
await db.execute(
|
|
3159
|
-
|
|
3303
|
+
sql9`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
|
|
3160
3304
|
);
|
|
3161
3305
|
}
|
|
3162
3306
|
};
|
|
@@ -3289,7 +3433,7 @@ function createCmsApp(options) {
|
|
|
3289
3433
|
const requirePermission = createPermissionMiddleware(rolesRegistry);
|
|
3290
3434
|
const getUserRole = async (userId) => {
|
|
3291
3435
|
const result = await db.execute(
|
|
3292
|
-
|
|
3436
|
+
sql10`SELECT r.name AS role
|
|
3293
3437
|
FROM users u
|
|
3294
3438
|
JOIN roles r ON r.id = u.role_id
|
|
3295
3439
|
WHERE u.id = ${userId}
|
|
@@ -3459,7 +3603,7 @@ function createCmsApp(options) {
|
|
|
3459
3603
|
handler: null,
|
|
3460
3604
|
error: null
|
|
3461
3605
|
}));
|
|
3462
|
-
const ready = import("./handler-
|
|
3606
|
+
const ready = import("./handler-VEOWJRAL.mjs").then(({ createGraphQLHandler }) => {
|
|
3463
3607
|
gqlPasses.forEach((pass, i) => {
|
|
3464
3608
|
const mount = mounts[i];
|
|
3465
3609
|
try {
|