@bobbykim/manguito-cms-api 0.2.0 → 0.3.2
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-GM55HGWA.mjs +607 -0
- package/dist/chunk-GM55HGWA.mjs.map +1 -0
- package/dist/chunk-TK4DNRFF.mjs +286 -0
- package/dist/chunk-TK4DNRFF.mjs.map +1 -0
- package/dist/graphql/index.d.mts +15 -0
- package/dist/graphql/index.d.ts +15 -0
- package/dist/graphql/index.js +747 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/graphql/index.mjs +8 -0
- package/dist/graphql/index.mjs.map +1 -0
- package/dist/handler-CGHZG3J5.mjs +8 -0
- package/dist/handler-CGHZG3J5.mjs.map +1 -0
- package/dist/index.d.mts +7 -14
- package/dist/index.d.ts +7 -14
- package/dist/index.js +1265 -439
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +357 -455
- package/dist/index.mjs.map +1 -1
- package/dist/resolve-Dtsk27aI.d.mts +15 -0
- package/dist/resolve-Dtsk27aI.d.ts +15 -0
- package/package.json +12 -3
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildRelationsMap,
|
|
3
|
+
deleteParagraphField,
|
|
4
|
+
persistJunctionField,
|
|
5
|
+
persistParagraphField,
|
|
6
|
+
resolveRelationBareIds,
|
|
7
|
+
resolveRelationField
|
|
8
|
+
} from "./chunk-TK4DNRFF.mjs";
|
|
9
|
+
|
|
1
10
|
// src/app.ts
|
|
2
11
|
import { Hono } from "hono";
|
|
3
|
-
import { sql as
|
|
12
|
+
import { sql as sql9 } from "drizzle-orm";
|
|
4
13
|
|
|
5
14
|
// src/middleware/cors.ts
|
|
6
15
|
function createCorsMiddleware(corsConfig) {
|
|
@@ -31,26 +40,43 @@ function createCorsMiddleware(corsConfig) {
|
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
// src/middleware/security-headers.ts
|
|
43
|
+
var GRAPHIQL_CDN = "https://unpkg.com";
|
|
34
44
|
function createSecurityHeadersMiddleware(options = {}) {
|
|
35
45
|
const connectSrc = ["'self'", ...options.connectSrc ?? []].join(" ");
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
function buildCsp(parts) {
|
|
47
|
+
return [
|
|
48
|
+
"default-src 'self'",
|
|
49
|
+
"img-src 'self' data: https:",
|
|
50
|
+
"media-src 'self' https:",
|
|
51
|
+
`style-src ${parts.styleSrc}`,
|
|
52
|
+
`script-src ${parts.scriptSrc}`,
|
|
53
|
+
"font-src 'self' data:",
|
|
54
|
+
`connect-src ${parts.connectSrc}`,
|
|
55
|
+
...parts.workerSrc ? [`worker-src ${parts.workerSrc}`] : [],
|
|
56
|
+
"frame-ancestors 'none'",
|
|
57
|
+
"base-uri 'self'",
|
|
58
|
+
"form-action 'self'"
|
|
59
|
+
].join("; ");
|
|
60
|
+
}
|
|
61
|
+
const csp = buildCsp({
|
|
62
|
+
scriptSrc: "'self'",
|
|
63
|
+
styleSrc: "'self' 'unsafe-inline'",
|
|
64
|
+
connectSrc
|
|
65
|
+
});
|
|
66
|
+
const graphiqlCsp = buildCsp({
|
|
67
|
+
scriptSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
|
|
68
|
+
styleSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
|
|
69
|
+
connectSrc: `${connectSrc} ${GRAPHIQL_CDN}`,
|
|
70
|
+
workerSrc: "'self' blob:"
|
|
71
|
+
});
|
|
72
|
+
const graphiqlPath = options.graphiqlPath;
|
|
48
73
|
return async function securityHeaders(c, next) {
|
|
49
74
|
await next();
|
|
50
75
|
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
51
76
|
c.res.headers.set("X-Frame-Options", "DENY");
|
|
52
77
|
c.res.headers.set("Referrer-Policy", "no-referrer");
|
|
53
|
-
c.
|
|
78
|
+
const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
|
|
79
|
+
c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
|
|
54
80
|
};
|
|
55
81
|
}
|
|
56
82
|
|
|
@@ -887,7 +913,7 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
|
|
|
887
913
|
}
|
|
888
914
|
|
|
889
915
|
// src/routes/admin/content.ts
|
|
890
|
-
import { sql as
|
|
916
|
+
import { sql as sql2 } from "drizzle-orm";
|
|
891
917
|
|
|
892
918
|
// src/media-references.ts
|
|
893
919
|
function extractMediaId(value) {
|
|
@@ -921,235 +947,31 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
921
947
|
if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
|
|
922
948
|
}
|
|
923
949
|
|
|
924
|
-
// src/
|
|
925
|
-
import crypto from "crypto";
|
|
926
|
-
import { sql as sql2 } from "drizzle-orm";
|
|
950
|
+
// src/routes/admin/content.ts
|
|
927
951
|
function quoteIdent(name) {
|
|
928
952
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
929
953
|
return `"${name}"`;
|
|
930
954
|
}
|
|
931
|
-
function
|
|
932
|
-
return new Set(
|
|
933
|
-
pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name)
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
|
|
937
|
-
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);
|
|
938
|
-
if (mediaColumns.length === 0) return [];
|
|
939
|
-
const tbl = sql2.raw(quoteIdent(pType.db.table_name));
|
|
940
|
-
const cols = sql2.join(mediaColumns.map((c) => sql2.raw(quoteIdent(c))), sql2`, `);
|
|
955
|
+
async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
941
956
|
const result = await db.execute(
|
|
942
|
-
sql2`SELECT
|
|
957
|
+
sql2`SELECT * FROM ${sql2.raw(quoteIdent(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
943
958
|
);
|
|
944
|
-
const
|
|
945
|
-
for (const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
return ids;
|
|
952
|
-
}
|
|
953
|
-
async function persistParagraphField(db, itemId, parentType, fieldName, pType, items) {
|
|
954
|
-
const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
|
|
955
|
-
const tbl = sql2.raw(quoteIdent(pType.db.table_name));
|
|
956
|
-
await db.execute(sql2`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
957
|
-
const mediaFieldNames = paragraphMediaFieldNames(pType);
|
|
958
|
-
const added = [];
|
|
959
|
-
for (let i = 0; i < items.length; i++) {
|
|
960
|
-
const pItem = items[i];
|
|
961
|
-
const data = {
|
|
962
|
-
id: crypto.randomUUID(),
|
|
963
|
-
parent_id: itemId,
|
|
964
|
-
parent_type: parentType,
|
|
965
|
-
parent_field: fieldName,
|
|
966
|
-
order: i,
|
|
967
|
-
created_at: /* @__PURE__ */ new Date(),
|
|
968
|
-
updated_at: /* @__PURE__ */ new Date()
|
|
969
|
-
};
|
|
970
|
-
for (const pf of pType.fields) {
|
|
971
|
-
if (!pf.db_column || pf.db_column.junction) continue;
|
|
972
|
-
data[pf.db_column.column_name] = pItem[pf.name] ?? null;
|
|
973
|
-
}
|
|
974
|
-
const cols = sql2.join(Object.keys(data).map((k) => sql2.raw(quoteIdent(k))), sql2`, `);
|
|
975
|
-
const vals = sql2.join(Object.values(data).map((v) => sql2`${v}`), sql2`, `);
|
|
976
|
-
await db.execute(sql2`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
|
|
977
|
-
for (const name of mediaFieldNames) {
|
|
978
|
-
const val = pItem[name];
|
|
979
|
-
if (typeof val === "string" && val !== "") added.push(val);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
return { added, removed };
|
|
983
|
-
}
|
|
984
|
-
async function deleteParagraphField(db, itemId, fieldName, pType) {
|
|
985
|
-
const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
|
|
986
|
-
const tbl = sql2.raw(quoteIdent(pType.db.table_name));
|
|
987
|
-
await db.execute(sql2`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
988
|
-
return { added: [], removed };
|
|
989
|
-
}
|
|
990
|
-
async function persistJunctionField(db, itemId, junction, relatedIds) {
|
|
991
|
-
const tbl = sql2.raw(quoteIdent(junction.table_name));
|
|
992
|
-
const left = sql2.raw(quoteIdent(junction.left_column));
|
|
993
|
-
const right = sql2.raw(quoteIdent(junction.right_column));
|
|
994
|
-
await db.execute(sql2`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
|
|
995
|
-
for (const rid of relatedIds) {
|
|
996
|
-
await db.execute(sql2`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
function buildRelationsMap(fields, registry) {
|
|
1000
|
-
const relations = {};
|
|
1001
|
-
for (const field of fields) {
|
|
1002
|
-
if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
1003
|
-
if (!field.db_column) continue;
|
|
1004
|
-
relations[field.name] = { type: "media", fk_column: field.db_column.column_name };
|
|
1005
|
-
} else if (field.field_type === "paragraph") {
|
|
1006
|
-
const ref = field.ui_component.ref;
|
|
1007
|
-
const pType = ref ? registry.paragraph_types[ref] : void 0;
|
|
1008
|
-
if (!pType) continue;
|
|
1009
|
-
relations[field.name] = { type: "paragraph", table: pType.db.table_name };
|
|
1010
|
-
} else if (field.field_type === "reference") {
|
|
1011
|
-
if (!field.db_column) continue;
|
|
1012
|
-
if (field.db_column.junction) {
|
|
1013
|
-
const j = field.db_column.junction;
|
|
1014
|
-
relations[field.name] = {
|
|
1015
|
-
type: "junction",
|
|
1016
|
-
table: j.right_table,
|
|
1017
|
-
junction_table: j.table_name,
|
|
1018
|
-
left_column: j.left_column,
|
|
1019
|
-
right_column: j.right_column,
|
|
1020
|
-
order_column: j.order_column
|
|
1021
|
-
};
|
|
1022
|
-
} else if (field.db_column.foreign_key) {
|
|
1023
|
-
relations[field.name] = {
|
|
1024
|
-
type: "reference",
|
|
1025
|
-
table: field.db_column.foreign_key.table,
|
|
1026
|
-
fk_column: field.db_column.column_name
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
return relations;
|
|
1032
|
-
}
|
|
1033
|
-
function groupBy(items, key) {
|
|
1034
|
-
const result = {};
|
|
1035
|
-
for (const item of items) {
|
|
1036
|
-
const k = String(item[key]);
|
|
1037
|
-
if (!result[k]) result[k] = [];
|
|
1038
|
-
result[k].push(item);
|
|
1039
|
-
}
|
|
1040
|
-
return result;
|
|
1041
|
-
}
|
|
1042
|
-
async function resolveRelationField(db, rows, fieldName, rel, cache) {
|
|
1043
|
-
if (rows.length === 0) return;
|
|
1044
|
-
if (rel.type === "paragraph") {
|
|
1045
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1046
|
-
const inList = sql2.join(parentIds.map((id) => sql2`${id}`), sql2`, `);
|
|
1047
|
-
const result = await db.execute(
|
|
1048
|
-
sql2`SELECT * FROM ${sql2.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
|
|
1049
|
-
);
|
|
1050
|
-
const byParent = groupBy(result.rows, "parent_id");
|
|
1051
|
-
for (const row of rows) {
|
|
1052
|
-
row[fieldName] = byParent[row["id"]] ?? [];
|
|
1053
|
-
}
|
|
1054
|
-
} else if (rel.type === "reference") {
|
|
1055
|
-
const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
1056
|
-
if (fkValues.length === 0) {
|
|
1057
|
-
for (const row of rows) row[fieldName] = null;
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
const unique = [...new Set(fkValues)];
|
|
1061
|
-
const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
1062
|
-
if (uncached.length > 0) {
|
|
1063
|
-
const inList = sql2.join(uncached.map((id) => sql2`${id}`), sql2`, `);
|
|
1064
|
-
const result = await db.execute(
|
|
1065
|
-
sql2`SELECT * FROM ${sql2.raw(quoteIdent(rel.table))} WHERE id IN (${inList})`
|
|
1066
|
-
);
|
|
1067
|
-
for (const item of result.rows) {
|
|
1068
|
-
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
959
|
+
const rows = result.rows;
|
|
960
|
+
for (const nf of pType.fields) {
|
|
961
|
+
if (nf.db_column !== null) continue;
|
|
962
|
+
const comp = nf.ui_component;
|
|
963
|
+
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
964
|
+
const nType = registry.paragraph_types[comp.ref];
|
|
965
|
+
if (!nType) continue;
|
|
1071
966
|
for (const row of rows) {
|
|
1072
|
-
|
|
1073
|
-
row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
|
|
1074
|
-
}
|
|
1075
|
-
} else if (rel.type === "junction") {
|
|
1076
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1077
|
-
const inList = sql2.join(parentIds.map((id) => sql2`${id}`), sql2`, `);
|
|
1078
|
-
const orderBy = rel.order_column ? sql2` ORDER BY "order" ASC` : sql2``;
|
|
1079
|
-
const junctionResult = await db.execute(
|
|
1080
|
-
sql2`SELECT * FROM ${sql2.raw(quoteIdent(rel.junction_table))} WHERE ${sql2.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`
|
|
1081
|
-
);
|
|
1082
|
-
const jRows = junctionResult.rows;
|
|
1083
|
-
const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
|
|
1084
|
-
const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
1085
|
-
if (uncached.length > 0) {
|
|
1086
|
-
const rightInList = sql2.join(uncached.map((id) => sql2`${id}`), sql2`, `);
|
|
1087
|
-
const entitiesResult = await db.execute(
|
|
1088
|
-
sql2`SELECT * FROM ${sql2.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})`
|
|
1089
|
-
);
|
|
1090
|
-
for (const item of entitiesResult.rows) {
|
|
1091
|
-
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
const byLeft = groupBy(jRows, rel.left_column);
|
|
1095
|
-
for (const row of rows) {
|
|
1096
|
-
const jEntries = byLeft[row["id"]] ?? [];
|
|
1097
|
-
row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
|
|
1098
|
-
}
|
|
1099
|
-
} else if (rel.type === "media") {
|
|
1100
|
-
const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
1101
|
-
if (fkValues.length === 0) {
|
|
1102
|
-
for (const row of rows) row[fieldName] = null;
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
const unique = [...new Set(fkValues)];
|
|
1106
|
-
const uncached = unique.filter((id) => !cache.has(`media:${id}`));
|
|
1107
|
-
if (uncached.length > 0) {
|
|
1108
|
-
const inList = sql2.join(uncached.map((id) => sql2`${id}`), sql2`, `);
|
|
1109
|
-
const result = await db.execute(sql2`SELECT * FROM "media" WHERE id IN (${inList})`);
|
|
1110
|
-
for (const item of result.rows) {
|
|
1111
|
-
cache.set(`media:${item["id"]}`, item);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
for (const row of rows) {
|
|
1115
|
-
const fkVal = row[rel.fk_column];
|
|
1116
|
-
row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
|
|
967
|
+
row[nf.name] = await loadParagraphRows(db, registry, nType, row["id"], nf.name);
|
|
1117
968
|
}
|
|
1118
969
|
}
|
|
1119
|
-
|
|
1120
|
-
async function resolveRelationBareIds(db, rows, fieldName, rel) {
|
|
1121
|
-
if (rows.length === 0) return;
|
|
1122
|
-
if (rel.type === "paragraph") {
|
|
1123
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1124
|
-
const inList = sql2.join(parentIds.map((id) => sql2`${id}`), sql2`, `);
|
|
1125
|
-
const result = await db.execute(
|
|
1126
|
-
sql2`SELECT id, parent_id FROM ${sql2.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
|
|
1127
|
-
);
|
|
1128
|
-
const byParent = groupBy(result.rows, "parent_id");
|
|
1129
|
-
for (const row of rows) {
|
|
1130
|
-
row[fieldName] = (byParent[row["id"]] ?? []).map((r) => r["id"]);
|
|
1131
|
-
}
|
|
1132
|
-
} else if (rel.type === "junction") {
|
|
1133
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1134
|
-
const inList = sql2.join(parentIds.map((id) => sql2`${id}`), sql2`, `);
|
|
1135
|
-
const result = await db.execute(
|
|
1136
|
-
sql2`SELECT ${sql2.raw(quoteIdent(rel.left_column))} AS left_id, ${sql2.raw(quoteIdent(rel.right_column))} AS right_id FROM ${sql2.raw(quoteIdent(rel.junction_table))} WHERE ${sql2.raw(quoteIdent(rel.left_column))} IN (${inList})`
|
|
1137
|
-
);
|
|
1138
|
-
const byLeft = groupBy(result.rows, "left_id");
|
|
1139
|
-
for (const row of rows) {
|
|
1140
|
-
row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
// src/routes/admin/content.ts
|
|
1146
|
-
function quoteIdent2(name) {
|
|
1147
|
-
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1148
|
-
return `"${name}"`;
|
|
970
|
+
return rows;
|
|
1149
971
|
}
|
|
1150
972
|
async function lookupBasePathId(db, pathOrName) {
|
|
1151
973
|
const r = await db.execute(
|
|
1152
|
-
|
|
974
|
+
sql2`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
1153
975
|
);
|
|
1154
976
|
return r.rows[0]?.id ?? null;
|
|
1155
977
|
}
|
|
@@ -1298,14 +1120,11 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1298
1120
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1299
1121
|
const pType = registry.paragraph_types[comp.ref];
|
|
1300
1122
|
if (!pType) continue;
|
|
1301
|
-
|
|
1302
|
-
sql3`SELECT * FROM ${sql3.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${id} AND parent_field = ${f.name} ORDER BY "order" ASC`
|
|
1303
|
-
);
|
|
1304
|
-
row[f.name] = r.rows;
|
|
1123
|
+
row[f.name] = await loadParagraphRows(db, registry, pType, id, f.name);
|
|
1305
1124
|
} else if (f.db_column.junction) {
|
|
1306
1125
|
const j = f.db_column.junction;
|
|
1307
1126
|
const r = await db.execute(
|
|
1308
|
-
|
|
1127
|
+
sql2`SELECT ${sql2.raw(quoteIdent(j.right_column))} FROM ${sql2.raw(quoteIdent(j.table_name))} WHERE ${sql2.raw(quoteIdent(j.left_column))} = ${id}`
|
|
1309
1128
|
);
|
|
1310
1129
|
row[f.name] = r.rows.map(
|
|
1311
1130
|
(row2) => row2[j.right_column]
|
|
@@ -1376,82 +1195,85 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1376
1195
|
);
|
|
1377
1196
|
}
|
|
1378
1197
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1198
|
+
return writeNewItem(c, body);
|
|
1199
|
+
}
|
|
1200
|
+
);
|
|
1201
|
+
const writeNewItem = async (c, body) => {
|
|
1202
|
+
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1203
|
+
if (fieldErrors.length > 0) {
|
|
1204
|
+
return c.json(
|
|
1205
|
+
{
|
|
1206
|
+
ok: false,
|
|
1207
|
+
error: {
|
|
1208
|
+
code: "VALIDATION_ERROR",
|
|
1209
|
+
message: "Required fields are missing",
|
|
1210
|
+
details: fieldErrors
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
422
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
if (body["published"] === true) {
|
|
1217
|
+
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1218
|
+
});
|
|
1219
|
+
if (publishDeny) return publishDeny;
|
|
1220
|
+
}
|
|
1221
|
+
const paragraphFields = contentType.fields.filter((f) => f.db_column === null);
|
|
1222
|
+
const junctionFields = contentType.fields.filter(
|
|
1223
|
+
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
1224
|
+
);
|
|
1225
|
+
const columnFields = contentType.fields.filter(
|
|
1226
|
+
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
1227
|
+
);
|
|
1228
|
+
const columnData = {
|
|
1229
|
+
published: body["published"] ?? false
|
|
1230
|
+
};
|
|
1231
|
+
if (contentType.only_one) {
|
|
1232
|
+
columnData["slug"] = typeName;
|
|
1233
|
+
} else {
|
|
1234
|
+
columnData["slug"] = body["slug"];
|
|
1235
|
+
}
|
|
1236
|
+
for (const f of columnFields) {
|
|
1237
|
+
const val = body[f.name];
|
|
1238
|
+
columnData[f.db_column.column_name] = val !== void 0 ? val : null;
|
|
1239
|
+
}
|
|
1240
|
+
if (db) {
|
|
1241
|
+
const basePathId = await lookupBasePathId(db, contentType.default_base_path);
|
|
1242
|
+
if (!basePathId) {
|
|
1381
1243
|
return c.json(
|
|
1382
1244
|
{
|
|
1383
1245
|
ok: false,
|
|
1384
1246
|
error: {
|
|
1385
|
-
code: "
|
|
1386
|
-
message:
|
|
1387
|
-
details: fieldErrors
|
|
1247
|
+
code: "BASE_PATH_NOT_FOUND",
|
|
1248
|
+
message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
|
|
1388
1249
|
}
|
|
1389
1250
|
},
|
|
1390
|
-
|
|
1251
|
+
500
|
|
1391
1252
|
);
|
|
1392
1253
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
(
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
published: body["published"] ?? false
|
|
1407
|
-
};
|
|
1408
|
-
if (contentType.only_one) {
|
|
1409
|
-
columnData["slug"] = typeName;
|
|
1410
|
-
} else {
|
|
1411
|
-
columnData["slug"] = body["slug"];
|
|
1254
|
+
columnData["base_path_id"] = basePathId;
|
|
1255
|
+
}
|
|
1256
|
+
const item = await repo.create(columnData);
|
|
1257
|
+
const itemId = item["id"];
|
|
1258
|
+
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
|
|
1259
|
+
if (db) {
|
|
1260
|
+
for (const f of paragraphFields) {
|
|
1261
|
+
const comp = f.ui_component;
|
|
1262
|
+
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1263
|
+
const pType = registry.paragraph_types[comp.ref];
|
|
1264
|
+
if (!pType) continue;
|
|
1265
|
+
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
1266
|
+
mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
|
|
1412
1267
|
}
|
|
1413
|
-
for (const f of
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1268
|
+
for (const f of junctionFields) {
|
|
1269
|
+
const junction = f.db_column.junction;
|
|
1270
|
+
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
1271
|
+
await persistJunctionField(db, itemId, junction, relatedIds);
|
|
1416
1272
|
}
|
|
1417
|
-
if (db) {
|
|
1418
|
-
const basePathId = await lookupBasePathId(db, contentType.default_base_path);
|
|
1419
|
-
if (!basePathId) {
|
|
1420
|
-
return c.json(
|
|
1421
|
-
{
|
|
1422
|
-
ok: false,
|
|
1423
|
-
error: {
|
|
1424
|
-
code: "BASE_PATH_NOT_FOUND",
|
|
1425
|
-
message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
|
|
1426
|
-
}
|
|
1427
|
-
},
|
|
1428
|
-
500
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
columnData["base_path_id"] = basePathId;
|
|
1432
|
-
}
|
|
1433
|
-
const item = await repo.create(columnData);
|
|
1434
|
-
const itemId = item["id"];
|
|
1435
|
-
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
|
|
1436
|
-
if (db) {
|
|
1437
|
-
for (const f of paragraphFields) {
|
|
1438
|
-
const comp = f.ui_component;
|
|
1439
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1440
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
1441
|
-
if (!pType) continue;
|
|
1442
|
-
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
1443
|
-
mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items));
|
|
1444
|
-
}
|
|
1445
|
-
for (const f of junctionFields) {
|
|
1446
|
-
const junction = f.db_column.junction;
|
|
1447
|
-
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
1448
|
-
await persistJunctionField(db, itemId, junction, relatedIds);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1452
|
-
return c.json({ ok: true, data: item }, 201);
|
|
1453
1273
|
}
|
|
1454
|
-
|
|
1274
|
+
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1275
|
+
return c.json({ ok: true, data: item }, 201);
|
|
1276
|
+
};
|
|
1455
1277
|
app.patch(
|
|
1456
1278
|
`/admin/api/${basePath}/:id`,
|
|
1457
1279
|
requirePermission("content:edit"),
|
|
@@ -1506,71 +1328,92 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1506
1328
|
);
|
|
1507
1329
|
}
|
|
1508
1330
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1331
|
+
return writeExistingItem(c, id, existing, body);
|
|
1332
|
+
}
|
|
1333
|
+
);
|
|
1334
|
+
const writeExistingItem = async (c, id, existing, body) => {
|
|
1335
|
+
if (body["published"] === true) {
|
|
1336
|
+
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1337
|
+
});
|
|
1338
|
+
if (publishDeny) return publishDeny;
|
|
1339
|
+
const merged = { ...existing, ...body };
|
|
1340
|
+
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
1341
|
+
if (fieldErrors.length > 0) {
|
|
1342
|
+
return c.json(
|
|
1343
|
+
{
|
|
1344
|
+
ok: false,
|
|
1345
|
+
error: {
|
|
1346
|
+
code: "PUBLISH_VALIDATION_ERROR",
|
|
1347
|
+
message: "Cannot publish \u2014 required fields are missing",
|
|
1348
|
+
details: fieldErrors
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
422
|
|
1352
|
+
);
|
|
1528
1353
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
)
|
|
1533
|
-
|
|
1534
|
-
|
|
1354
|
+
}
|
|
1355
|
+
const patchParagraphFields = contentType.fields.filter((f) => f.db_column === null);
|
|
1356
|
+
const patchJunctionFields = contentType.fields.filter(
|
|
1357
|
+
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
1358
|
+
);
|
|
1359
|
+
const patchColumnFields = contentType.fields.filter(
|
|
1360
|
+
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
1361
|
+
);
|
|
1362
|
+
const patchData = {};
|
|
1363
|
+
if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
|
|
1364
|
+
if ("published" in body) patchData["published"] = body["published"];
|
|
1365
|
+
for (const f of patchColumnFields) {
|
|
1366
|
+
if (f.name in body) {
|
|
1367
|
+
const val = body[f.name];
|
|
1368
|
+
patchData[f.db_column.column_name] = val !== void 0 ? val : null;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
const updated = await repo.update(id, patchData);
|
|
1372
|
+
if (!updated) {
|
|
1373
|
+
return c.json(
|
|
1374
|
+
{ ok: false, error: { code: "NOT_FOUND", message: "Not found" } },
|
|
1375
|
+
404
|
|
1535
1376
|
);
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1377
|
+
}
|
|
1378
|
+
const mediaDeltas = [
|
|
1379
|
+
topLevelMediaDelta(mediaFields, existing, body)
|
|
1380
|
+
];
|
|
1381
|
+
if (db) {
|
|
1382
|
+
for (const f of patchParagraphFields) {
|
|
1383
|
+
const comp = f.ui_component;
|
|
1384
|
+
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1385
|
+
const pType = registry.paragraph_types[comp.ref];
|
|
1386
|
+
if (!pType) continue;
|
|
1387
|
+
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
1388
|
+
mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
|
|
1544
1389
|
}
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
404
|
|
1550
|
-
);
|
|
1390
|
+
for (const f of patchJunctionFields) {
|
|
1391
|
+
const junction = f.db_column.junction;
|
|
1392
|
+
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
1393
|
+
await persistJunctionField(db, id, junction, relatedIds);
|
|
1551
1394
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1395
|
+
}
|
|
1396
|
+
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1397
|
+
return c.json({ ok: true, data: updated });
|
|
1398
|
+
};
|
|
1399
|
+
if (contentType.only_one) {
|
|
1400
|
+
app.put(
|
|
1401
|
+
`/admin/api/${basePath}`,
|
|
1402
|
+
requirePermission("content:edit"),
|
|
1403
|
+
async (c) => {
|
|
1404
|
+
const body = await c.req.json();
|
|
1405
|
+
const existingResult = await repo.findMany({ page: 1, per_page: 1 });
|
|
1406
|
+
const existing = existingResult.data[0];
|
|
1407
|
+
if (!existing) {
|
|
1408
|
+
const createDeny = await requirePermission("content:create")(c, async () => {
|
|
1409
|
+
});
|
|
1410
|
+
if (createDeny) return createDeny;
|
|
1411
|
+
return writeNewItem(c, body);
|
|
1568
1412
|
}
|
|
1413
|
+
return writeExistingItem(c, existing["id"], existing, body);
|
|
1569
1414
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
}
|
|
1573
|
-
);
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1574
1417
|
app.delete(
|
|
1575
1418
|
`/admin/api/${basePath}/:id`,
|
|
1576
1419
|
requirePermission("content:delete"),
|
|
@@ -1592,7 +1435,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1592
1435
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1593
1436
|
const pType = registry.paragraph_types[comp.ref];
|
|
1594
1437
|
if (!pType) continue;
|
|
1595
|
-
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
|
|
1438
|
+
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
|
|
1596
1439
|
}
|
|
1597
1440
|
}
|
|
1598
1441
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
@@ -1707,7 +1550,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1707
1550
|
const pType = registry.paragraph_types[comp.ref];
|
|
1708
1551
|
if (!pType) continue;
|
|
1709
1552
|
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
1710
|
-
mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items));
|
|
1553
|
+
mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1711
1554
|
}
|
|
1712
1555
|
}
|
|
1713
1556
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
@@ -1776,7 +1619,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1776
1619
|
const pType = registry.paragraph_types[comp.ref];
|
|
1777
1620
|
if (!pType) continue;
|
|
1778
1621
|
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
1779
|
-
mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items));
|
|
1622
|
+
mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1780
1623
|
}
|
|
1781
1624
|
}
|
|
1782
1625
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
@@ -1804,7 +1647,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1804
1647
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1805
1648
|
const pType = registry.paragraph_types[comp.ref];
|
|
1806
1649
|
if (!pType) continue;
|
|
1807
|
-
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
|
|
1650
|
+
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
|
|
1808
1651
|
}
|
|
1809
1652
|
}
|
|
1810
1653
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
@@ -2270,7 +2113,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
|
|
|
2270
2113
|
|
|
2271
2114
|
// src/routes/admin/auth.ts
|
|
2272
2115
|
import { getCookie as getCookie2 } from "hono/cookie";
|
|
2273
|
-
import { sql as
|
|
2116
|
+
import { sql as sql3 } from "drizzle-orm";
|
|
2274
2117
|
|
|
2275
2118
|
// src/auth/password.ts
|
|
2276
2119
|
import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
|
|
@@ -2336,7 +2179,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2336
2179
|
);
|
|
2337
2180
|
}
|
|
2338
2181
|
const userResult = await db.execute(
|
|
2339
|
-
|
|
2182
|
+
sql3`SELECT id, email, password_hash, role_id, token_version
|
|
2340
2183
|
FROM users WHERE email = ${email} LIMIT 1`
|
|
2341
2184
|
);
|
|
2342
2185
|
const user = userResult.rows[0];
|
|
@@ -2349,7 +2192,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2349
2192
|
return c.json(invalidCredentials, 401);
|
|
2350
2193
|
}
|
|
2351
2194
|
const roleResult = await db.execute(
|
|
2352
|
-
|
|
2195
|
+
sql3`SELECT r.name AS name, u.must_change_password
|
|
2353
2196
|
FROM roles r
|
|
2354
2197
|
JOIN users u ON u.role_id = r.id
|
|
2355
2198
|
WHERE r.id = ${user.role_id} AND u.id = ${user.id}
|
|
@@ -2409,7 +2252,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2409
2252
|
);
|
|
2410
2253
|
}
|
|
2411
2254
|
const rowResult = await db.execute(
|
|
2412
|
-
|
|
2255
|
+
sql3`SELECT u.token_version, r.name AS role
|
|
2413
2256
|
FROM users u
|
|
2414
2257
|
JOIN roles r ON r.id = u.role_id
|
|
2415
2258
|
WHERE u.id = ${payload.user_id}
|
|
@@ -2433,7 +2276,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2433
2276
|
app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
|
|
2434
2277
|
const user = c.get("user");
|
|
2435
2278
|
await db.execute(
|
|
2436
|
-
|
|
2279
|
+
sql3`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
|
|
2437
2280
|
);
|
|
2438
2281
|
clearAuthCookie(c, "auth_token");
|
|
2439
2282
|
clearAuthCookie(c, "refresh_token", "/admin/api/auth");
|
|
@@ -2442,7 +2285,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2442
2285
|
}
|
|
2443
2286
|
|
|
2444
2287
|
// src/routes/admin/users.ts
|
|
2445
|
-
import { sql as
|
|
2288
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
2446
2289
|
import { randomBytes } from "crypto";
|
|
2447
2290
|
function getActingUser(c) {
|
|
2448
2291
|
return c.get("user");
|
|
@@ -2477,7 +2320,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2477
2320
|
);
|
|
2478
2321
|
}
|
|
2479
2322
|
const hashResult = await db.execute(
|
|
2480
|
-
|
|
2323
|
+
sql4`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2481
2324
|
);
|
|
2482
2325
|
const hashRow = hashResult.rows[0];
|
|
2483
2326
|
if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
|
|
@@ -2488,7 +2331,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2488
2331
|
}
|
|
2489
2332
|
const newHash = await hashPassword(newPassword);
|
|
2490
2333
|
await db.execute(
|
|
2491
|
-
|
|
2334
|
+
sql4`UPDATE users
|
|
2492
2335
|
SET password_hash = ${newHash},
|
|
2493
2336
|
must_change_password = false,
|
|
2494
2337
|
token_version = token_version + 1,
|
|
@@ -2499,7 +2342,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2499
2342
|
});
|
|
2500
2343
|
app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
|
|
2501
2344
|
const result = await db.execute(
|
|
2502
|
-
|
|
2345
|
+
sql4`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2503
2346
|
u.created_at, u.updated_at
|
|
2504
2347
|
FROM users u
|
|
2505
2348
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2510,7 +2353,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2510
2353
|
app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
|
|
2511
2354
|
const id = c.req.param("id");
|
|
2512
2355
|
const result = await db.execute(
|
|
2513
|
-
|
|
2356
|
+
sql4`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2514
2357
|
u.created_at, u.updated_at
|
|
2515
2358
|
FROM users u
|
|
2516
2359
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2555,7 +2398,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2555
2398
|
);
|
|
2556
2399
|
}
|
|
2557
2400
|
const roleResult = await db.execute(
|
|
2558
|
-
|
|
2401
|
+
sql4`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
|
|
2559
2402
|
);
|
|
2560
2403
|
const roleRow = roleResult.rows[0];
|
|
2561
2404
|
if (!roleRow) {
|
|
@@ -2570,7 +2413,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2570
2413
|
const tempPassword = generateTempPassword();
|
|
2571
2414
|
const passwordHash = await hashPassword(tempPassword);
|
|
2572
2415
|
const insertResult = await db.execute(
|
|
2573
|
-
|
|
2416
|
+
sql4`INSERT INTO users
|
|
2574
2417
|
(id, email, password_hash, role_id, must_change_password, token_version,
|
|
2575
2418
|
created_at, updated_at)
|
|
2576
2419
|
VALUES
|
|
@@ -2625,7 +2468,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2625
2468
|
);
|
|
2626
2469
|
}
|
|
2627
2470
|
const existingResult = await db.execute(
|
|
2628
|
-
|
|
2471
|
+
sql4`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
|
|
2629
2472
|
);
|
|
2630
2473
|
const existing = existingResult.rows[0];
|
|
2631
2474
|
if (!existing) {
|
|
@@ -2639,7 +2482,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2639
2482
|
let changeRole = false;
|
|
2640
2483
|
if (typeof body.role === "string" && body.role) {
|
|
2641
2484
|
const roleResult = await db.execute(
|
|
2642
|
-
|
|
2485
|
+
sql4`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
|
|
2643
2486
|
);
|
|
2644
2487
|
const roleRow = roleResult.rows[0];
|
|
2645
2488
|
if (!roleRow) {
|
|
@@ -2659,7 +2502,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2659
2502
|
}
|
|
2660
2503
|
if (changeRole) {
|
|
2661
2504
|
await db.execute(
|
|
2662
|
-
|
|
2505
|
+
sql4`UPDATE users
|
|
2663
2506
|
SET email = ${newEmail},
|
|
2664
2507
|
role_id = ${newRoleId},
|
|
2665
2508
|
token_version = token_version + 1,
|
|
@@ -2668,7 +2511,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2668
2511
|
);
|
|
2669
2512
|
} else {
|
|
2670
2513
|
await db.execute(
|
|
2671
|
-
|
|
2514
|
+
sql4`UPDATE users
|
|
2672
2515
|
SET email = ${newEmail},
|
|
2673
2516
|
role_id = ${newRoleId},
|
|
2674
2517
|
updated_at = NOW()
|
|
@@ -2676,7 +2519,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2676
2519
|
);
|
|
2677
2520
|
}
|
|
2678
2521
|
const updatedResult = await db.execute(
|
|
2679
|
-
|
|
2522
|
+
sql4`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2680
2523
|
u.created_at, u.updated_at
|
|
2681
2524
|
FROM users u
|
|
2682
2525
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2713,7 +2556,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2713
2556
|
);
|
|
2714
2557
|
}
|
|
2715
2558
|
const result = await db.execute(
|
|
2716
|
-
|
|
2559
|
+
sql4`DELETE FROM users WHERE id = ${id} RETURNING id`
|
|
2717
2560
|
);
|
|
2718
2561
|
if (result.rows.length === 0) {
|
|
2719
2562
|
return c.json(
|
|
@@ -2744,7 +2587,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2744
2587
|
);
|
|
2745
2588
|
}
|
|
2746
2589
|
const existingResult = await db.execute(
|
|
2747
|
-
|
|
2590
|
+
sql4`SELECT id FROM users WHERE id = ${id} LIMIT 1`
|
|
2748
2591
|
);
|
|
2749
2592
|
const existing = existingResult.rows[0];
|
|
2750
2593
|
if (!existing) {
|
|
@@ -2756,7 +2599,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2756
2599
|
const tempPassword = generateTempPassword();
|
|
2757
2600
|
const passwordHash = await hashPassword(tempPassword);
|
|
2758
2601
|
await db.execute(
|
|
2759
|
-
|
|
2602
|
+
sql4`UPDATE users
|
|
2760
2603
|
SET password_hash = ${passwordHash},
|
|
2761
2604
|
must_change_password = true,
|
|
2762
2605
|
token_version = token_version + 1,
|
|
@@ -2769,10 +2612,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2769
2612
|
}
|
|
2770
2613
|
|
|
2771
2614
|
// src/routes/admin/config.ts
|
|
2772
|
-
import { sql as
|
|
2615
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2773
2616
|
var API_VERSION = (() => {
|
|
2774
2617
|
try {
|
|
2775
|
-
return "0.2
|
|
2618
|
+
return "0.3.2";
|
|
2776
2619
|
} catch {
|
|
2777
2620
|
return "0.0.0";
|
|
2778
2621
|
}
|
|
@@ -2791,7 +2634,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2791
2634
|
permissions: r.permissions
|
|
2792
2635
|
}));
|
|
2793
2636
|
const userResult = await db.execute(
|
|
2794
|
-
|
|
2637
|
+
sql5`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2795
2638
|
);
|
|
2796
2639
|
const userRow = userResult.rows[0];
|
|
2797
2640
|
return c.json({
|
|
@@ -2817,7 +2660,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2817
2660
|
}
|
|
2818
2661
|
|
|
2819
2662
|
// src/routes/admin/schema.ts
|
|
2820
|
-
import { sql as
|
|
2663
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
2821
2664
|
function registerSchemaRoute(app, registry, db) {
|
|
2822
2665
|
app.get("/admin/api/schema", (c) => {
|
|
2823
2666
|
const content_types = Object.values(registry.content_types).map((ct) => ({
|
|
@@ -2854,7 +2697,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2854
2697
|
const data = await Promise.all(
|
|
2855
2698
|
Object.values(registry.content_types).map(async (ct) => {
|
|
2856
2699
|
const result = await db.execute(
|
|
2857
|
-
|
|
2700
|
+
sql6`SELECT COUNT(*)::int AS count FROM ${sql6.raw(`"${ct.db.table_name}"`)}`
|
|
2858
2701
|
);
|
|
2859
2702
|
const count = Number(result.rows[0].count);
|
|
2860
2703
|
return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
|
|
@@ -2866,7 +2709,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2866
2709
|
const data = await Promise.all(
|
|
2867
2710
|
Object.values(registry.taxonomy_types).map(async (tt) => {
|
|
2868
2711
|
const result = await db.execute(
|
|
2869
|
-
|
|
2712
|
+
sql6`SELECT COUNT(*)::int AS count FROM ${sql6.raw(`"${tt.db.table_name}"`)}`
|
|
2870
2713
|
);
|
|
2871
2714
|
const count = Number(result.rows[0].count);
|
|
2872
2715
|
return { name: tt.name, label: tt.label, count };
|
|
@@ -2877,15 +2720,15 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2877
2720
|
}
|
|
2878
2721
|
|
|
2879
2722
|
// src/repositories/content.ts
|
|
2880
|
-
import
|
|
2881
|
-
import { sql as
|
|
2723
|
+
import crypto from "crypto";
|
|
2724
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
2882
2725
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
2883
2726
|
function codeError(code, message) {
|
|
2884
2727
|
const err = new Error(message);
|
|
2885
2728
|
err.code = code;
|
|
2886
2729
|
return err;
|
|
2887
2730
|
}
|
|
2888
|
-
function
|
|
2731
|
+
function quoteIdent2(name) {
|
|
2889
2732
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
2890
2733
|
throw new Error(`Unsafe SQL identifier: ${name}`);
|
|
2891
2734
|
}
|
|
@@ -2900,43 +2743,43 @@ function quoteField(name) {
|
|
|
2900
2743
|
function buildConditions(published_only, filters, search) {
|
|
2901
2744
|
const conds = [];
|
|
2902
2745
|
if (published_only) {
|
|
2903
|
-
conds.push(
|
|
2746
|
+
conds.push(sql7`published = true`);
|
|
2904
2747
|
}
|
|
2905
2748
|
if (search && search.term.trim() !== "" && search.columns.length > 0) {
|
|
2906
2749
|
const pattern = `%${search.term.trim()}%`;
|
|
2907
|
-
const orConds = search.columns.map((col) =>
|
|
2908
|
-
conds.push(
|
|
2750
|
+
const orConds = search.columns.map((col) => sql7`${sql7.raw(quoteField(col))} ILIKE ${pattern}`);
|
|
2751
|
+
conds.push(sql7`(${sql7.join(orConds, sql7` OR `)})`);
|
|
2909
2752
|
}
|
|
2910
2753
|
if (filters) {
|
|
2911
2754
|
for (const [field, value] of Object.entries(filters)) {
|
|
2912
|
-
const col =
|
|
2755
|
+
const col = sql7.raw(quoteField(field));
|
|
2913
2756
|
if (Array.isArray(value)) {
|
|
2914
2757
|
if (value.length === 0) continue;
|
|
2915
|
-
const inList =
|
|
2916
|
-
value.map((v) =>
|
|
2917
|
-
|
|
2758
|
+
const inList = sql7.join(
|
|
2759
|
+
value.map((v) => sql7`${v}`),
|
|
2760
|
+
sql7`, `
|
|
2918
2761
|
);
|
|
2919
|
-
conds.push(
|
|
2762
|
+
conds.push(sql7`${col} IN (${inList})`);
|
|
2920
2763
|
} else if (typeof value === "object" && value !== null) {
|
|
2921
2764
|
const op = value;
|
|
2922
|
-
if (op.gt !== void 0) conds.push(
|
|
2923
|
-
if (op.gte !== void 0) conds.push(
|
|
2924
|
-
if (op.lt !== void 0) conds.push(
|
|
2925
|
-
if (op.lte !== void 0) conds.push(
|
|
2765
|
+
if (op.gt !== void 0) conds.push(sql7`${col} > ${op.gt}`);
|
|
2766
|
+
if (op.gte !== void 0) conds.push(sql7`${col} >= ${op.gte}`);
|
|
2767
|
+
if (op.lt !== void 0) conds.push(sql7`${col} < ${op.lt}`);
|
|
2768
|
+
if (op.lte !== void 0) conds.push(sql7`${col} <= ${op.lte}`);
|
|
2926
2769
|
} else {
|
|
2927
|
-
conds.push(
|
|
2770
|
+
conds.push(sql7`${col} = ${value}`);
|
|
2928
2771
|
}
|
|
2929
2772
|
}
|
|
2930
2773
|
}
|
|
2931
2774
|
return conds;
|
|
2932
2775
|
}
|
|
2933
2776
|
function whereFragment(conds) {
|
|
2934
|
-
return conds.length > 0 ?
|
|
2777
|
+
return conds.length > 0 ? sql7` WHERE ${sql7.join(conds, sql7` AND `)}` : sql7``;
|
|
2935
2778
|
}
|
|
2936
2779
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
2937
|
-
const { relations = {} } = options;
|
|
2780
|
+
const { relations = {}, publishedRelations = false } = options;
|
|
2938
2781
|
function tableRaw() {
|
|
2939
|
-
return
|
|
2782
|
+
return sql7.raw(quoteIdent2(tableName));
|
|
2940
2783
|
}
|
|
2941
2784
|
async function resolveRows(rows, include, cache) {
|
|
2942
2785
|
if (rows.length === 0) return rows;
|
|
@@ -2947,7 +2790,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2947
2790
|
}
|
|
2948
2791
|
for (const [fieldName, rel] of Object.entries(relations)) {
|
|
2949
2792
|
if (rel.type === "media") {
|
|
2950
|
-
await resolveRelationField(db, rows, fieldName, rel, cache);
|
|
2793
|
+
await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
|
|
2951
2794
|
} else if (!include.includes(fieldName)) {
|
|
2952
2795
|
await resolveRelationBareIds(db, rows, fieldName, rel);
|
|
2953
2796
|
}
|
|
@@ -2955,7 +2798,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2955
2798
|
for (const fieldName of include) {
|
|
2956
2799
|
const rel = relations[fieldName];
|
|
2957
2800
|
if (rel.type !== "media") {
|
|
2958
|
-
await resolveRelationField(db, rows, fieldName, rel, cache);
|
|
2801
|
+
await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
|
|
2959
2802
|
}
|
|
2960
2803
|
}
|
|
2961
2804
|
return rows;
|
|
@@ -2978,14 +2821,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2978
2821
|
const offset = (page - 1) * per_page;
|
|
2979
2822
|
const conds = buildConditions(published_only, filters, search);
|
|
2980
2823
|
const where = whereFragment(conds);
|
|
2981
|
-
const sortCol =
|
|
2982
|
-
const sortDir =
|
|
2824
|
+
const sortCol = sql7.raw(quoteIdent2(sort_by));
|
|
2825
|
+
const sortDir = sql7.raw(sort_order === "desc" ? "DESC" : "ASC");
|
|
2983
2826
|
const countResult = await db.execute(
|
|
2984
|
-
|
|
2827
|
+
sql7`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
|
|
2985
2828
|
);
|
|
2986
2829
|
const total = Number(countResult.rows[0]?.["total"] ?? 0);
|
|
2987
2830
|
const dataResult = await db.execute(
|
|
2988
|
-
|
|
2831
|
+
sql7`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
|
|
2989
2832
|
);
|
|
2990
2833
|
const rows = dataResult.rows;
|
|
2991
2834
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3006,7 +2849,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3006
2849
|
},
|
|
3007
2850
|
async findOne(id, include = []) {
|
|
3008
2851
|
const result = await db.execute(
|
|
3009
|
-
|
|
2852
|
+
sql7`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
|
|
3010
2853
|
);
|
|
3011
2854
|
if (result.rows.length === 0) return null;
|
|
3012
2855
|
const rows = result.rows;
|
|
@@ -3016,7 +2859,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3016
2859
|
},
|
|
3017
2860
|
async findBySlug(slug, include = []) {
|
|
3018
2861
|
const result = await db.execute(
|
|
3019
|
-
|
|
2862
|
+
sql7`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
|
|
3020
2863
|
);
|
|
3021
2864
|
if (result.rows.length === 0) return null;
|
|
3022
2865
|
const rows = result.rows;
|
|
@@ -3027,15 +2870,15 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3027
2870
|
async create(data) {
|
|
3028
2871
|
const record = {
|
|
3029
2872
|
...data,
|
|
3030
|
-
id:
|
|
2873
|
+
id: crypto.randomUUID(),
|
|
3031
2874
|
created_at: /* @__PURE__ */ new Date(),
|
|
3032
2875
|
updated_at: /* @__PURE__ */ new Date()
|
|
3033
2876
|
};
|
|
3034
2877
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3035
|
-
const colsSql =
|
|
3036
|
-
const valsSql =
|
|
2878
|
+
const colsSql = sql7.join(entries.map(([k]) => sql7.raw(quoteIdent2(k))), sql7`, `);
|
|
2879
|
+
const valsSql = sql7.join(entries.map(([, v]) => sql7`${v}`), sql7`, `);
|
|
3037
2880
|
const result = await db.execute(
|
|
3038
|
-
|
|
2881
|
+
sql7`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3039
2882
|
);
|
|
3040
2883
|
return result.rows[0];
|
|
3041
2884
|
},
|
|
@@ -3048,25 +2891,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3048
2891
|
if (entries.length === 0) {
|
|
3049
2892
|
return this.findOne(id);
|
|
3050
2893
|
}
|
|
3051
|
-
const setClauses =
|
|
3052
|
-
entries.map(([k, v]) =>
|
|
3053
|
-
|
|
2894
|
+
const setClauses = sql7.join(
|
|
2895
|
+
entries.map(([k, v]) => sql7`${sql7.raw(quoteIdent2(k))} = ${v}`),
|
|
2896
|
+
sql7`, `
|
|
3054
2897
|
);
|
|
3055
2898
|
const result = await db.execute(
|
|
3056
|
-
|
|
2899
|
+
sql7`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3057
2900
|
);
|
|
3058
2901
|
if (result.rows.length === 0) return null;
|
|
3059
2902
|
return result.rows[0];
|
|
3060
2903
|
},
|
|
3061
2904
|
async delete(id) {
|
|
3062
|
-
await db.execute(
|
|
2905
|
+
await db.execute(sql7`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
|
|
3063
2906
|
},
|
|
3064
2907
|
async findAll(opts = {}) {
|
|
3065
2908
|
const { include = [], published_only } = opts;
|
|
3066
2909
|
const conds = buildConditions(published_only, void 0, void 0);
|
|
3067
2910
|
const where = whereFragment(conds);
|
|
3068
2911
|
const result = await db.execute(
|
|
3069
|
-
|
|
2912
|
+
sql7`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
|
|
3070
2913
|
);
|
|
3071
2914
|
const rows = result.rows;
|
|
3072
2915
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3077,24 +2920,24 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3077
2920
|
}
|
|
3078
2921
|
|
|
3079
2922
|
// src/repositories/media.ts
|
|
3080
|
-
import
|
|
3081
|
-
import { sql as
|
|
2923
|
+
import crypto2 from "crypto";
|
|
2924
|
+
import { sql as sql8 } from "drizzle-orm";
|
|
3082
2925
|
function createMediaRepository(db) {
|
|
3083
|
-
const TABLE =
|
|
2926
|
+
const TABLE = sql8.raw('"media"');
|
|
3084
2927
|
return {
|
|
3085
2928
|
async findMany(opts = {}) {
|
|
3086
2929
|
const { page = 1, per_page = 10, type, orphaned } = opts;
|
|
3087
2930
|
const offset = (page - 1) * per_page;
|
|
3088
2931
|
const conds = [];
|
|
3089
|
-
if (type) conds.push(
|
|
3090
|
-
if (orphaned) conds.push(
|
|
3091
|
-
const where = conds.length > 0 ?
|
|
3092
|
-
const countResult = await db.execute(
|
|
2932
|
+
if (type) conds.push(sql8`type = ${type}`);
|
|
2933
|
+
if (orphaned) conds.push(sql8`reference_count = 0`);
|
|
2934
|
+
const where = conds.length > 0 ? sql8` WHERE ${sql8.join(conds, sql8` AND `)}` : sql8``;
|
|
2935
|
+
const countResult = await db.execute(sql8`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
|
|
3093
2936
|
const total = Number(
|
|
3094
2937
|
countResult.rows[0]?.["total"] ?? 0
|
|
3095
2938
|
);
|
|
3096
2939
|
const dataResult = await db.execute(
|
|
3097
|
-
|
|
2940
|
+
sql8`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
|
|
3098
2941
|
);
|
|
3099
2942
|
const total_pages = Math.ceil(total / per_page);
|
|
3100
2943
|
return {
|
|
@@ -3112,7 +2955,7 @@ function createMediaRepository(db) {
|
|
|
3112
2955
|
},
|
|
3113
2956
|
async findOne(id) {
|
|
3114
2957
|
const result = await db.execute(
|
|
3115
|
-
|
|
2958
|
+
sql8`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
|
|
3116
2959
|
);
|
|
3117
2960
|
if (result.rows.length === 0) return null;
|
|
3118
2961
|
return result.rows[0];
|
|
@@ -3120,16 +2963,16 @@ function createMediaRepository(db) {
|
|
|
3120
2963
|
async create(data) {
|
|
3121
2964
|
const record = {
|
|
3122
2965
|
...data,
|
|
3123
|
-
id:
|
|
2966
|
+
id: crypto2.randomUUID(),
|
|
3124
2967
|
reference_count: 0,
|
|
3125
2968
|
created_at: /* @__PURE__ */ new Date(),
|
|
3126
2969
|
updated_at: /* @__PURE__ */ new Date()
|
|
3127
2970
|
};
|
|
3128
2971
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3129
|
-
const colsSql =
|
|
3130
|
-
const valsSql =
|
|
2972
|
+
const colsSql = sql8.join(entries.map(([k]) => sql8.raw(`"${k}"`)), sql8`, `);
|
|
2973
|
+
const valsSql = sql8.join(entries.map(([, v]) => sql8`${v}`), sql8`, `);
|
|
3131
2974
|
const result = await db.execute(
|
|
3132
|
-
|
|
2975
|
+
sql8`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3133
2976
|
);
|
|
3134
2977
|
return result.rows[0];
|
|
3135
2978
|
},
|
|
@@ -3139,31 +2982,31 @@ function createMediaRepository(db) {
|
|
|
3139
2982
|
([k, v]) => k !== "id" && v !== void 0
|
|
3140
2983
|
);
|
|
3141
2984
|
if (entries.length === 0) return this.findOne(id);
|
|
3142
|
-
const setClauses =
|
|
3143
|
-
entries.map(([k, v]) =>
|
|
3144
|
-
|
|
2985
|
+
const setClauses = sql8.join(
|
|
2986
|
+
entries.map(([k, v]) => sql8`${sql8.raw(`"${k}"`)} = ${v}`),
|
|
2987
|
+
sql8`, `
|
|
3145
2988
|
);
|
|
3146
2989
|
const result = await db.execute(
|
|
3147
|
-
|
|
2990
|
+
sql8`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3148
2991
|
);
|
|
3149
2992
|
if (result.rows.length === 0) return null;
|
|
3150
2993
|
return result.rows[0];
|
|
3151
2994
|
},
|
|
3152
2995
|
async delete(id) {
|
|
3153
|
-
await db.execute(
|
|
2996
|
+
await db.execute(sql8`DELETE FROM ${TABLE} WHERE id = ${id}`);
|
|
3154
2997
|
},
|
|
3155
2998
|
async incrementReferenceCount(ids) {
|
|
3156
2999
|
if (ids.length === 0) return;
|
|
3157
|
-
const inList =
|
|
3000
|
+
const inList = sql8.join(ids.map((id) => sql8`${id}`), sql8`, `);
|
|
3158
3001
|
await db.execute(
|
|
3159
|
-
|
|
3002
|
+
sql8`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
|
|
3160
3003
|
);
|
|
3161
3004
|
},
|
|
3162
3005
|
async decrementReferenceCount(ids) {
|
|
3163
3006
|
if (ids.length === 0) return;
|
|
3164
|
-
const inList =
|
|
3007
|
+
const inList = sql8.join(ids.map((id) => sql8`${id}`), sql8`, `);
|
|
3165
3008
|
await db.execute(
|
|
3166
|
-
|
|
3009
|
+
sql8`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
|
|
3167
3010
|
);
|
|
3168
3011
|
}
|
|
3169
3012
|
};
|
|
@@ -3193,7 +3036,14 @@ function createCmsApp(options) {
|
|
|
3193
3036
|
const programmaticResolver = createProgrammaticResolver(resolverMap);
|
|
3194
3037
|
const app = new Hono();
|
|
3195
3038
|
const uploadOrigins = storage.getUploadOrigins?.() ?? [];
|
|
3196
|
-
|
|
3039
|
+
const graphiqlEnabled = options.graphql?.enabled === true && options.graphql.graphiql === true;
|
|
3040
|
+
app.use(
|
|
3041
|
+
"*",
|
|
3042
|
+
createSecurityHeadersMiddleware({
|
|
3043
|
+
connectSrc: uploadOrigins,
|
|
3044
|
+
...graphiqlEnabled && { graphiqlPath: "/graphql" }
|
|
3045
|
+
})
|
|
3046
|
+
);
|
|
3197
3047
|
app.use("*", createCorsMiddleware(cors ?? { origin: "*", enabled: true }));
|
|
3198
3048
|
app.onError(errorHandler);
|
|
3199
3049
|
const listRateLimit = resolveListRateLimit(rateLimit);
|
|
@@ -3201,7 +3051,7 @@ function createCmsApp(options) {
|
|
|
3201
3051
|
const requirePermission = createPermissionMiddleware(rolesRegistry);
|
|
3202
3052
|
const getUserRole = async (userId) => {
|
|
3203
3053
|
const result = await db.execute(
|
|
3204
|
-
|
|
3054
|
+
sql9`SELECT r.name AS role
|
|
3205
3055
|
FROM users u
|
|
3206
3056
|
JOIN roles r ON r.id = u.role_id
|
|
3207
3057
|
WHERE u.id = ${userId}
|
|
@@ -3228,7 +3078,8 @@ function createCmsApp(options) {
|
|
|
3228
3078
|
Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
3229
3079
|
typeName,
|
|
3230
3080
|
createDrizzleContentRepository(db, ct.db.table_name, {
|
|
3231
|
-
relations: buildRelationsMap(ct.fields, registry)
|
|
3081
|
+
relations: buildRelationsMap(ct.fields, registry),
|
|
3082
|
+
publishedRelations: true
|
|
3232
3083
|
})
|
|
3233
3084
|
])
|
|
3234
3085
|
);
|
|
@@ -3236,11 +3087,22 @@ function createCmsApp(options) {
|
|
|
3236
3087
|
Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
3237
3088
|
typeName,
|
|
3238
3089
|
createDrizzleContentRepository(db, tt.db.table_name, {
|
|
3239
|
-
relations: buildRelationsMap(tt.fields, registry)
|
|
3090
|
+
relations: buildRelationsMap(tt.fields, registry),
|
|
3091
|
+
publishedRelations: true
|
|
3240
3092
|
})
|
|
3241
3093
|
])
|
|
3242
3094
|
);
|
|
3243
3095
|
const publicRepos = { ...publicContentRepos, ...publicTaxonomyRepos };
|
|
3096
|
+
const graphqlRepos = Object.fromEntries([
|
|
3097
|
+
...Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
3098
|
+
typeName,
|
|
3099
|
+
createDrizzleContentRepository(db, ct.db.table_name)
|
|
3100
|
+
]),
|
|
3101
|
+
...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
3102
|
+
typeName,
|
|
3103
|
+
createDrizzleContentRepository(db, tt.db.table_name)
|
|
3104
|
+
])
|
|
3105
|
+
]);
|
|
3244
3106
|
registerAuthRoutes(app, db);
|
|
3245
3107
|
app.use("/admin/api/*", authMiddleware);
|
|
3246
3108
|
app.use("/admin/api/*", mustChangePasswordCheck);
|
|
@@ -3277,6 +3139,35 @@ function createCmsApp(options) {
|
|
|
3277
3139
|
);
|
|
3278
3140
|
registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
|
|
3279
3141
|
registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
|
|
3142
|
+
if (options.graphql?.enabled) {
|
|
3143
|
+
const gqlOptions = options.graphql;
|
|
3144
|
+
let gqlHandler = null;
|
|
3145
|
+
let initError = null;
|
|
3146
|
+
const ready = import("./handler-CGHZG3J5.mjs").then(({ createGraphQLHandler }) => {
|
|
3147
|
+
gqlHandler = createGraphQLHandler(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
|
|
3148
|
+
}).catch((err) => {
|
|
3149
|
+
initError = err;
|
|
3150
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3151
|
+
process.stderr.write(`\u2717 GraphQL schema failed to initialize; /graphql will return 500. ${message}
|
|
3152
|
+
`);
|
|
3153
|
+
});
|
|
3154
|
+
const invokeGraphQL = async (c) => {
|
|
3155
|
+
if (!gqlHandler && !initError) await ready;
|
|
3156
|
+
if (!gqlHandler) {
|
|
3157
|
+
return c.json(
|
|
3158
|
+
{ ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
|
|
3159
|
+
500
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
return gqlHandler(c, async () => {
|
|
3163
|
+
});
|
|
3164
|
+
};
|
|
3165
|
+
if (listRateLimit) {
|
|
3166
|
+
app.all("/graphql", listRateLimit, invokeGraphQL);
|
|
3167
|
+
} else {
|
|
3168
|
+
app.all("/graphql", invokeGraphQL);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3280
3171
|
registerConfigRoute(
|
|
3281
3172
|
app,
|
|
3282
3173
|
{
|
|
@@ -3323,13 +3214,24 @@ function createServer(options = {}) {
|
|
|
3323
3214
|
}
|
|
3324
3215
|
|
|
3325
3216
|
// src/index.ts
|
|
3217
|
+
function resolveGraphQL(cfg) {
|
|
3218
|
+
const devDefault = process.env["NODE_ENV"] !== "production";
|
|
3219
|
+
return {
|
|
3220
|
+
enabled: cfg.enabled ?? false,
|
|
3221
|
+
maxDepth: cfg.maxDepth ?? 8,
|
|
3222
|
+
maxComplexity: cfg.maxComplexity ?? 1e3,
|
|
3223
|
+
graphiql: cfg.graphiql ?? devDefault,
|
|
3224
|
+
introspection: cfg.introspection ?? cfg.graphiql ?? devDefault
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3326
3227
|
function createAPIAdapter(options = {}) {
|
|
3327
3228
|
const prefix = options.prefix ?? "/api";
|
|
3328
3229
|
const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
|
|
3329
3230
|
return {
|
|
3330
3231
|
prefix,
|
|
3331
3232
|
...media !== void 0 && { media },
|
|
3332
|
-
...options.rateLimit !== void 0 && { rateLimit: options.rateLimit }
|
|
3233
|
+
...options.rateLimit !== void 0 && { rateLimit: options.rateLimit },
|
|
3234
|
+
...options.graphql !== void 0 && { graphql: resolveGraphQL(options.graphql) }
|
|
3333
3235
|
};
|
|
3334
3236
|
}
|
|
3335
3237
|
export {
|