@bobbykim/manguito-cms-api 0.6.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-JVP33FI2.mjs → chunk-IHEIX6P4.mjs} +67 -48
- package/dist/chunk-IHEIX6P4.mjs.map +1 -0
- package/dist/{chunk-XNXM4W4K.mjs → chunk-Y5SWT7PO.mjs} +24 -26
- 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.d.mts +1 -0
- package/dist/graphql/index.d.ts +1 -0
- package/dist/graphql/index.js +40 -45
- 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 +454 -265
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +323 -150
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-JVP33FI2.mjs.map +0 -1
- package/dist/chunk-XNXM4W4K.mjs.map +0 -1
- package/dist/handler-XZVDLKZ2.mjs +0 -8
- /package/dist/{handler-XZVDLKZ2.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
|
|
@@ -617,14 +623,16 @@ function isPlainRow(v) {
|
|
|
617
623
|
return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
|
|
618
624
|
}
|
|
619
625
|
var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
|
|
620
|
-
function nestedTargets(fields) {
|
|
626
|
+
function nestedTargets(fields, map) {
|
|
621
627
|
const out = [];
|
|
622
628
|
for (const f of fields) {
|
|
623
629
|
if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
|
|
624
630
|
if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
|
|
625
631
|
const ref = f.ui_component.ref;
|
|
626
632
|
if (!ref) continue;
|
|
627
|
-
|
|
633
|
+
const label = isColumnBacked(f) ? map.labelFor(f.db_column.column_name) : f.name;
|
|
634
|
+
if (label === void 0) continue;
|
|
635
|
+
out.push({ label, target: ref });
|
|
628
636
|
}
|
|
629
637
|
return out;
|
|
630
638
|
}
|
|
@@ -642,7 +650,7 @@ function buildProjectors(registry, fieldKeyMaps, fallbacks) {
|
|
|
642
650
|
const typeFallbacks = fallbacks?.[typeName];
|
|
643
651
|
projectors[typeName] = {
|
|
644
652
|
map,
|
|
645
|
-
nested: nestedTargets(type.fields),
|
|
653
|
+
nested: nestedTargets(type.fields, map),
|
|
646
654
|
// Spread rather than assign: with exactOptionalPropertyTypes, an optional
|
|
647
655
|
// property may be ABSENT but not present-and-undefined.
|
|
648
656
|
...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
|
|
@@ -726,7 +734,28 @@ function parseFilters(url, validFields, columnFor) {
|
|
|
726
734
|
function isPublished(item) {
|
|
727
735
|
return item["published"] === true;
|
|
728
736
|
}
|
|
729
|
-
function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver) {
|
|
737
|
+
function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
|
|
738
|
+
const forResolver = resolverProjectors ?? projectors;
|
|
739
|
+
function pickProgrammatic(resolved, names) {
|
|
740
|
+
const out = {};
|
|
741
|
+
for (const name of names) if (name in resolved) out[name] = resolved[name];
|
|
742
|
+
return out;
|
|
743
|
+
}
|
|
744
|
+
async function respondItem(row, typeName, programmatic) {
|
|
745
|
+
const data = projectRow(row, typeName, projectors);
|
|
746
|
+
if (!resolver?.hasSchema(typeName)) return data;
|
|
747
|
+
const resolved = await resolver.resolveItem(typeName, projectRow(row, typeName, forResolver));
|
|
748
|
+
return { ...data, ...pickProgrammatic(resolved, programmatic) };
|
|
749
|
+
}
|
|
750
|
+
async function respondList(rows, typeName, programmatic) {
|
|
751
|
+
const data = rows.map((row) => projectRow(row, typeName, projectors));
|
|
752
|
+
if (!resolver?.hasSchema(typeName)) return data;
|
|
753
|
+
const resolved = await resolver.resolveList(
|
|
754
|
+
typeName,
|
|
755
|
+
rows.map((row) => projectRow(row, typeName, forResolver))
|
|
756
|
+
);
|
|
757
|
+
return data.map((d, i) => ({ ...d, ...pickProgrammatic(resolved[i], programmatic) }));
|
|
758
|
+
}
|
|
730
759
|
function registerListRoute(path, handler) {
|
|
731
760
|
if (listRateLimit) {
|
|
732
761
|
app.get(path, listRateLimit, handler);
|
|
@@ -750,18 +779,29 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
750
779
|
return c.json({ ok: true, data });
|
|
751
780
|
});
|
|
752
781
|
for (const [typeName, contentType] of Object.entries(registry.content_types)) {
|
|
782
|
+
let translateInclude2 = function(requested) {
|
|
783
|
+
const include = [];
|
|
784
|
+
for (const field of requested) {
|
|
785
|
+
const registryName = includeNames.get(field);
|
|
786
|
+
if (registryName === void 0) return { ok: false, field };
|
|
787
|
+
include.push(registryName);
|
|
788
|
+
}
|
|
789
|
+
return { ok: true, include };
|
|
790
|
+
};
|
|
791
|
+
var translateInclude = translateInclude2;
|
|
753
792
|
const basePath = contentType.default_base_path;
|
|
754
793
|
const repo = repos[typeName];
|
|
755
794
|
if (!repo) continue;
|
|
756
795
|
const fieldKeys = projectors[typeName].map;
|
|
757
|
-
const
|
|
796
|
+
const programmatic = contentType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
|
|
797
|
+
const includeNames = /* @__PURE__ */ new Map();
|
|
758
798
|
for (const f of contentType.fields) {
|
|
759
799
|
if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
|
|
760
800
|
if (isColumnBacked(f)) {
|
|
761
801
|
const label = fieldKeys.labelFor(f.db_column.column_name);
|
|
762
|
-
if (label !== void 0)
|
|
802
|
+
if (label !== void 0) includeNames.set(label, f.name);
|
|
763
803
|
} else {
|
|
764
|
-
|
|
804
|
+
includeNames.set(f.name, f.name);
|
|
765
805
|
}
|
|
766
806
|
}
|
|
767
807
|
const filterableFieldNames = /* @__PURE__ */ new Set([
|
|
@@ -780,8 +820,11 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
780
820
|
404
|
|
781
821
|
);
|
|
782
822
|
}
|
|
783
|
-
|
|
784
|
-
|
|
823
|
+
const data = await respondItem(
|
|
824
|
+
result.data[0],
|
|
825
|
+
typeName,
|
|
826
|
+
programmatic
|
|
827
|
+
);
|
|
785
828
|
return c.json({ ok: true, data });
|
|
786
829
|
});
|
|
787
830
|
} else {
|
|
@@ -838,21 +881,20 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
838
881
|
400
|
|
839
882
|
);
|
|
840
883
|
}
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
);
|
|
854
|
-
}
|
|
884
|
+
const translated = translateInclude2(parseInclude(c.req.query("include")));
|
|
885
|
+
if (!translated.ok) {
|
|
886
|
+
return c.json(
|
|
887
|
+
{
|
|
888
|
+
ok: false,
|
|
889
|
+
error: {
|
|
890
|
+
code: "INVALID_INCLUDE_FIELD",
|
|
891
|
+
message: `'${translated.field}' is not a valid relation field`
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
400
|
|
895
|
+
);
|
|
855
896
|
}
|
|
897
|
+
const include = translated.include;
|
|
856
898
|
const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
|
|
857
899
|
const result = await repo.findMany({
|
|
858
900
|
published_only: true,
|
|
@@ -863,29 +905,25 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
863
905
|
filters: filtersResult.filters,
|
|
864
906
|
include
|
|
865
907
|
});
|
|
866
|
-
const
|
|
867
|
-
(row) => projectRow(row, typeName, projectors)
|
|
868
|
-
);
|
|
869
|
-
const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
|
|
908
|
+
const data = await respondList(result.data, typeName, programmatic);
|
|
870
909
|
return c.json({ ...result, data });
|
|
871
910
|
});
|
|
872
911
|
app.get(paths.item(basePath), async (c) => {
|
|
873
912
|
const slug = c.req.param("slug");
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
);
|
|
887
|
-
}
|
|
913
|
+
const translated = translateInclude2(parseInclude(c.req.query("include")));
|
|
914
|
+
if (!translated.ok) {
|
|
915
|
+
return c.json(
|
|
916
|
+
{
|
|
917
|
+
ok: false,
|
|
918
|
+
error: {
|
|
919
|
+
code: "INVALID_INCLUDE_FIELD",
|
|
920
|
+
message: `'${translated.field}' is not a valid relation field`
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
400
|
|
924
|
+
);
|
|
888
925
|
}
|
|
926
|
+
const include = translated.include;
|
|
889
927
|
const item = await repo.findBySlug(slug, include);
|
|
890
928
|
if (!item || !isPublished(item)) {
|
|
891
929
|
return c.json(
|
|
@@ -896,15 +934,15 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
896
934
|
404
|
|
897
935
|
);
|
|
898
936
|
}
|
|
899
|
-
|
|
900
|
-
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
937
|
+
const data = await respondItem(item, typeName, programmatic);
|
|
901
938
|
return c.json({ ok: true, data });
|
|
902
939
|
});
|
|
903
940
|
}
|
|
904
941
|
}
|
|
905
|
-
for (const [typeName] of Object.entries(registry.taxonomy_types)) {
|
|
942
|
+
for (const [typeName, taxonomyType] of Object.entries(registry.taxonomy_types)) {
|
|
906
943
|
const repo = repos[typeName];
|
|
907
944
|
if (!repo) continue;
|
|
945
|
+
const programmatic = taxonomyType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
|
|
908
946
|
registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
|
|
909
947
|
const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
|
|
910
948
|
if (!pagination.ok) {
|
|
@@ -924,10 +962,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
924
962
|
page: pagination.page,
|
|
925
963
|
per_page: pagination.per_page
|
|
926
964
|
});
|
|
927
|
-
const
|
|
928
|
-
(row) => projectRow(row, typeName, projectors)
|
|
929
|
-
);
|
|
930
|
-
const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
|
|
965
|
+
const data = await respondList(result.data, typeName, programmatic);
|
|
931
966
|
return c.json({ ...result, data });
|
|
932
967
|
});
|
|
933
968
|
app.get(paths.taxonomyItem(typeName), async (c) => {
|
|
@@ -942,8 +977,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
942
977
|
404
|
|
943
978
|
);
|
|
944
979
|
}
|
|
945
|
-
|
|
946
|
-
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
980
|
+
const data = await respondItem(item, typeName, programmatic);
|
|
947
981
|
return c.json({ ok: true, data });
|
|
948
982
|
});
|
|
949
983
|
}
|
|
@@ -1015,7 +1049,55 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
|
|
|
1015
1049
|
}
|
|
1016
1050
|
|
|
1017
1051
|
// src/routes/admin/content.ts
|
|
1052
|
+
import { sql as sql3 } from "drizzle-orm";
|
|
1053
|
+
|
|
1054
|
+
// src/references-in-use.ts
|
|
1018
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";
|
|
1019
1101
|
|
|
1020
1102
|
// src/media-references.ts
|
|
1021
1103
|
function extractMediaId(value) {
|
|
@@ -1051,14 +1133,60 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
1051
1133
|
if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
|
|
1052
1134
|
}
|
|
1053
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
|
+
|
|
1054
1182
|
// src/routes/admin/content.ts
|
|
1055
|
-
function
|
|
1183
|
+
function quoteIdent2(name) {
|
|
1056
1184
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1057
1185
|
return `"${name}"`;
|
|
1058
1186
|
}
|
|
1059
1187
|
async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
1060
1188
|
const result = await db.execute(
|
|
1061
|
-
|
|
1189
|
+
sql3`SELECT * FROM ${sql3.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
1062
1190
|
);
|
|
1063
1191
|
const rows = result.rows;
|
|
1064
1192
|
for (const nf of pType.fields) {
|
|
@@ -1068,14 +1196,17 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
|
1068
1196
|
const nType = registry.paragraph_types[comp.ref];
|
|
1069
1197
|
if (!nType) continue;
|
|
1070
1198
|
for (const row of rows) {
|
|
1071
|
-
row[nf.name] =
|
|
1199
|
+
row[nf.name] = oneOrMany(
|
|
1200
|
+
relationCardinality3(nf) ?? "many",
|
|
1201
|
+
await loadParagraphRows(db, registry, nType, row["id"], nf.name)
|
|
1202
|
+
);
|
|
1072
1203
|
}
|
|
1073
1204
|
}
|
|
1074
1205
|
return rows;
|
|
1075
1206
|
}
|
|
1076
1207
|
async function lookupBasePathId(db, pathOrName) {
|
|
1077
1208
|
const r = await db.execute(
|
|
1078
|
-
|
|
1209
|
+
sql3`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
1079
1210
|
);
|
|
1080
1211
|
return r.rows[0]?.id ?? null;
|
|
1081
1212
|
}
|
|
@@ -1089,6 +1220,14 @@ function isEmpty(value) {
|
|
|
1089
1220
|
function checkRequiredFields(fields, data) {
|
|
1090
1221
|
return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
|
|
1091
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
|
+
}
|
|
1092
1231
|
function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
|
|
1093
1232
|
const searchParams = new URL(url).searchParams;
|
|
1094
1233
|
const pagination = parsePagination(
|
|
@@ -1230,11 +1369,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1230
1369
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1231
1370
|
const pType = registry.paragraph_types[comp.ref];
|
|
1232
1371
|
if (!pType) continue;
|
|
1233
|
-
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));
|
|
1234
1373
|
} else if (f.db_column.junction) {
|
|
1235
1374
|
const j = f.db_column.junction;
|
|
1236
1375
|
const r = await db.execute(
|
|
1237
|
-
|
|
1376
|
+
sql3`SELECT ${sql3.raw(quoteIdent2(j.right_column))} FROM ${sql3.raw(quoteIdent2(j.table_name))} WHERE ${sql3.raw(quoteIdent2(j.left_column))} = ${id}`
|
|
1238
1377
|
);
|
|
1239
1378
|
row[f.name] = r.rows.map(
|
|
1240
1379
|
(row2) => row2[j.right_column]
|
|
@@ -1310,6 +1449,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1310
1449
|
}
|
|
1311
1450
|
);
|
|
1312
1451
|
const writeNewItem = async (c, body) => {
|
|
1452
|
+
const shapeError = relationShapeError(c, contentType.fields, body, registry);
|
|
1453
|
+
if (shapeError) return shapeError;
|
|
1313
1454
|
const storageBody = fieldKeys.toStorage(body);
|
|
1314
1455
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1315
1456
|
if (fieldErrors.length > 0) {
|
|
@@ -1373,7 +1514,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1373
1514
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1374
1515
|
const pType = registry.paragraph_types[comp.ref];
|
|
1375
1516
|
if (!pType) continue;
|
|
1376
|
-
const items =
|
|
1517
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1377
1518
|
mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
|
|
1378
1519
|
}
|
|
1379
1520
|
for (const f of junctionFields) {
|
|
@@ -1444,6 +1585,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1444
1585
|
}
|
|
1445
1586
|
);
|
|
1446
1587
|
const writeExistingItem = async (c, id, existing, body) => {
|
|
1588
|
+
const shapeError = relationShapeError(c, contentType.fields, body, registry);
|
|
1589
|
+
if (shapeError) return shapeError;
|
|
1447
1590
|
const storageBody = fieldKeys.toStorage(body);
|
|
1448
1591
|
if (body["published"] === true) {
|
|
1449
1592
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
@@ -1490,14 +1633,16 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1490
1633
|
];
|
|
1491
1634
|
if (db) {
|
|
1492
1635
|
for (const f of patchParagraphFields) {
|
|
1636
|
+
if (!(f.name in body)) continue;
|
|
1493
1637
|
const comp = f.ui_component;
|
|
1494
1638
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1495
1639
|
const pType = registry.paragraph_types[comp.ref];
|
|
1496
1640
|
if (!pType) continue;
|
|
1497
|
-
const items =
|
|
1641
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1498
1642
|
mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
|
|
1499
1643
|
}
|
|
1500
1644
|
for (const f of patchJunctionFields) {
|
|
1645
|
+
if (!(f.name in body)) continue;
|
|
1501
1646
|
const junction = f.db_column.junction;
|
|
1502
1647
|
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
1503
1648
|
await persistJunctionField(db, id, junction, relatedIds);
|
|
@@ -1537,6 +1682,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1537
1682
|
404
|
|
1538
1683
|
);
|
|
1539
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
|
+
}
|
|
1540
1697
|
const mediaDeltas = [
|
|
1541
1698
|
topLevelMediaDelta(mediaFields, item, null)
|
|
1542
1699
|
];
|
|
@@ -1550,7 +1707,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1550
1707
|
}
|
|
1551
1708
|
}
|
|
1552
1709
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1553
|
-
await repo.delete(id);
|
|
1554
1710
|
return c.json({ ok: true });
|
|
1555
1711
|
}
|
|
1556
1712
|
);
|
|
@@ -1628,6 +1784,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1628
1784
|
requirePermission("content:create"),
|
|
1629
1785
|
async (c) => {
|
|
1630
1786
|
const body = await c.req.json();
|
|
1787
|
+
const shapeError = relationShapeError(c, taxonomyType.fields, body, registry);
|
|
1788
|
+
if (shapeError) return shapeError;
|
|
1631
1789
|
const storageBody = fieldKeys.toStorage(body);
|
|
1632
1790
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1633
1791
|
if (fieldErrors.length > 0) {
|
|
@@ -1667,7 +1825,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1667
1825
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1668
1826
|
const pType = registry.paragraph_types[comp.ref];
|
|
1669
1827
|
if (!pType) continue;
|
|
1670
|
-
const items =
|
|
1828
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1671
1829
|
mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1672
1830
|
}
|
|
1673
1831
|
}
|
|
@@ -1690,6 +1848,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1690
1848
|
404
|
|
1691
1849
|
);
|
|
1692
1850
|
}
|
|
1851
|
+
const shapeError = relationShapeError(c, taxonomyType.fields, body, registry);
|
|
1852
|
+
if (shapeError) return shapeError;
|
|
1693
1853
|
if (body["published"] === true) {
|
|
1694
1854
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1695
1855
|
});
|
|
@@ -1731,11 +1891,12 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1731
1891
|
];
|
|
1732
1892
|
if (db) {
|
|
1733
1893
|
for (const f of taxPatchParagraphFields) {
|
|
1894
|
+
if (!(f.name in body)) continue;
|
|
1734
1895
|
const comp = f.ui_component;
|
|
1735
1896
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1736
1897
|
const pType = registry.paragraph_types[comp.ref];
|
|
1737
1898
|
if (!pType) continue;
|
|
1738
|
-
const items =
|
|
1899
|
+
const items = paragraphItems(f, body[f.name]);
|
|
1739
1900
|
mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1740
1901
|
}
|
|
1741
1902
|
}
|
|
@@ -1756,6 +1917,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1756
1917
|
404
|
|
1757
1918
|
);
|
|
1758
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
|
+
}
|
|
1759
1932
|
const mediaDeltas = [
|
|
1760
1933
|
topLevelMediaDelta(mediaFields, item, null)
|
|
1761
1934
|
];
|
|
@@ -1769,7 +1942,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1769
1942
|
}
|
|
1770
1943
|
}
|
|
1771
1944
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1772
|
-
await repo.delete(id);
|
|
1773
1945
|
return c.json({ ok: true });
|
|
1774
1946
|
}
|
|
1775
1947
|
);
|
|
@@ -2231,7 +2403,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
|
|
|
2231
2403
|
|
|
2232
2404
|
// src/routes/admin/auth.ts
|
|
2233
2405
|
import { getCookie as getCookie2 } from "hono/cookie";
|
|
2234
|
-
import { sql as
|
|
2406
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
2235
2407
|
|
|
2236
2408
|
// src/auth/password.ts
|
|
2237
2409
|
import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
|
|
@@ -2297,7 +2469,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2297
2469
|
);
|
|
2298
2470
|
}
|
|
2299
2471
|
const userResult = await db.execute(
|
|
2300
|
-
|
|
2472
|
+
sql4`SELECT id, email, password_hash, role_id, token_version
|
|
2301
2473
|
FROM users WHERE email = ${email} LIMIT 1`
|
|
2302
2474
|
);
|
|
2303
2475
|
const user = userResult.rows[0];
|
|
@@ -2310,7 +2482,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2310
2482
|
return c.json(invalidCredentials, 401);
|
|
2311
2483
|
}
|
|
2312
2484
|
const roleResult = await db.execute(
|
|
2313
|
-
|
|
2485
|
+
sql4`SELECT r.name AS name, u.must_change_password
|
|
2314
2486
|
FROM roles r
|
|
2315
2487
|
JOIN users u ON u.role_id = r.id
|
|
2316
2488
|
WHERE r.id = ${user.role_id} AND u.id = ${user.id}
|
|
@@ -2370,7 +2542,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2370
2542
|
);
|
|
2371
2543
|
}
|
|
2372
2544
|
const rowResult = await db.execute(
|
|
2373
|
-
|
|
2545
|
+
sql4`SELECT u.token_version, r.name AS role
|
|
2374
2546
|
FROM users u
|
|
2375
2547
|
JOIN roles r ON r.id = u.role_id
|
|
2376
2548
|
WHERE u.id = ${payload.user_id}
|
|
@@ -2394,7 +2566,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2394
2566
|
app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
|
|
2395
2567
|
const user = c.get("user");
|
|
2396
2568
|
await db.execute(
|
|
2397
|
-
|
|
2569
|
+
sql4`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
|
|
2398
2570
|
);
|
|
2399
2571
|
clearAuthCookie(c, "auth_token");
|
|
2400
2572
|
clearAuthCookie(c, "refresh_token", "/admin/api/auth");
|
|
@@ -2403,7 +2575,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2403
2575
|
}
|
|
2404
2576
|
|
|
2405
2577
|
// src/routes/admin/users.ts
|
|
2406
|
-
import { sql as
|
|
2578
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2407
2579
|
import { randomBytes } from "crypto";
|
|
2408
2580
|
function getActingUser(c) {
|
|
2409
2581
|
return c.get("user");
|
|
@@ -2438,7 +2610,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2438
2610
|
);
|
|
2439
2611
|
}
|
|
2440
2612
|
const hashResult = await db.execute(
|
|
2441
|
-
|
|
2613
|
+
sql5`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2442
2614
|
);
|
|
2443
2615
|
const hashRow = hashResult.rows[0];
|
|
2444
2616
|
if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
|
|
@@ -2449,7 +2621,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2449
2621
|
}
|
|
2450
2622
|
const newHash = await hashPassword(newPassword);
|
|
2451
2623
|
await db.execute(
|
|
2452
|
-
|
|
2624
|
+
sql5`UPDATE users
|
|
2453
2625
|
SET password_hash = ${newHash},
|
|
2454
2626
|
must_change_password = false,
|
|
2455
2627
|
token_version = token_version + 1,
|
|
@@ -2460,7 +2632,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2460
2632
|
});
|
|
2461
2633
|
app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
|
|
2462
2634
|
const result = await db.execute(
|
|
2463
|
-
|
|
2635
|
+
sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2464
2636
|
u.created_at, u.updated_at
|
|
2465
2637
|
FROM users u
|
|
2466
2638
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2471,7 +2643,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2471
2643
|
app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
|
|
2472
2644
|
const id = c.req.param("id");
|
|
2473
2645
|
const result = await db.execute(
|
|
2474
|
-
|
|
2646
|
+
sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2475
2647
|
u.created_at, u.updated_at
|
|
2476
2648
|
FROM users u
|
|
2477
2649
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2516,7 +2688,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2516
2688
|
);
|
|
2517
2689
|
}
|
|
2518
2690
|
const roleResult = await db.execute(
|
|
2519
|
-
|
|
2691
|
+
sql5`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
|
|
2520
2692
|
);
|
|
2521
2693
|
const roleRow = roleResult.rows[0];
|
|
2522
2694
|
if (!roleRow) {
|
|
@@ -2531,7 +2703,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2531
2703
|
const tempPassword = generateTempPassword();
|
|
2532
2704
|
const passwordHash = await hashPassword(tempPassword);
|
|
2533
2705
|
const insertResult = await db.execute(
|
|
2534
|
-
|
|
2706
|
+
sql5`INSERT INTO users
|
|
2535
2707
|
(id, email, password_hash, role_id, must_change_password, token_version,
|
|
2536
2708
|
created_at, updated_at)
|
|
2537
2709
|
VALUES
|
|
@@ -2586,7 +2758,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2586
2758
|
);
|
|
2587
2759
|
}
|
|
2588
2760
|
const existingResult = await db.execute(
|
|
2589
|
-
|
|
2761
|
+
sql5`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
|
|
2590
2762
|
);
|
|
2591
2763
|
const existing = existingResult.rows[0];
|
|
2592
2764
|
if (!existing) {
|
|
@@ -2600,7 +2772,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2600
2772
|
let changeRole = false;
|
|
2601
2773
|
if (typeof body.role === "string" && body.role) {
|
|
2602
2774
|
const roleResult = await db.execute(
|
|
2603
|
-
|
|
2775
|
+
sql5`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
|
|
2604
2776
|
);
|
|
2605
2777
|
const roleRow = roleResult.rows[0];
|
|
2606
2778
|
if (!roleRow) {
|
|
@@ -2620,7 +2792,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2620
2792
|
}
|
|
2621
2793
|
if (changeRole) {
|
|
2622
2794
|
await db.execute(
|
|
2623
|
-
|
|
2795
|
+
sql5`UPDATE users
|
|
2624
2796
|
SET email = ${newEmail},
|
|
2625
2797
|
role_id = ${newRoleId},
|
|
2626
2798
|
token_version = token_version + 1,
|
|
@@ -2629,7 +2801,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2629
2801
|
);
|
|
2630
2802
|
} else {
|
|
2631
2803
|
await db.execute(
|
|
2632
|
-
|
|
2804
|
+
sql5`UPDATE users
|
|
2633
2805
|
SET email = ${newEmail},
|
|
2634
2806
|
role_id = ${newRoleId},
|
|
2635
2807
|
updated_at = NOW()
|
|
@@ -2637,7 +2809,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2637
2809
|
);
|
|
2638
2810
|
}
|
|
2639
2811
|
const updatedResult = await db.execute(
|
|
2640
|
-
|
|
2812
|
+
sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2641
2813
|
u.created_at, u.updated_at
|
|
2642
2814
|
FROM users u
|
|
2643
2815
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2674,7 +2846,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2674
2846
|
);
|
|
2675
2847
|
}
|
|
2676
2848
|
const result = await db.execute(
|
|
2677
|
-
|
|
2849
|
+
sql5`DELETE FROM users WHERE id = ${id} RETURNING id`
|
|
2678
2850
|
);
|
|
2679
2851
|
if (result.rows.length === 0) {
|
|
2680
2852
|
return c.json(
|
|
@@ -2705,7 +2877,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2705
2877
|
);
|
|
2706
2878
|
}
|
|
2707
2879
|
const existingResult = await db.execute(
|
|
2708
|
-
|
|
2880
|
+
sql5`SELECT id FROM users WHERE id = ${id} LIMIT 1`
|
|
2709
2881
|
);
|
|
2710
2882
|
const existing = existingResult.rows[0];
|
|
2711
2883
|
if (!existing) {
|
|
@@ -2717,7 +2889,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2717
2889
|
const tempPassword = generateTempPassword();
|
|
2718
2890
|
const passwordHash = await hashPassword(tempPassword);
|
|
2719
2891
|
await db.execute(
|
|
2720
|
-
|
|
2892
|
+
sql5`UPDATE users
|
|
2721
2893
|
SET password_hash = ${passwordHash},
|
|
2722
2894
|
must_change_password = true,
|
|
2723
2895
|
token_version = token_version + 1,
|
|
@@ -2730,10 +2902,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2730
2902
|
}
|
|
2731
2903
|
|
|
2732
2904
|
// src/routes/admin/config.ts
|
|
2733
|
-
import { sql as
|
|
2905
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
2734
2906
|
var API_VERSION = (() => {
|
|
2735
2907
|
try {
|
|
2736
|
-
return "0.
|
|
2908
|
+
return "0.8.0";
|
|
2737
2909
|
} catch {
|
|
2738
2910
|
return "0.0.0";
|
|
2739
2911
|
}
|
|
@@ -2752,7 +2924,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2752
2924
|
permissions: r.permissions
|
|
2753
2925
|
}));
|
|
2754
2926
|
const userResult = await db.execute(
|
|
2755
|
-
|
|
2927
|
+
sql6`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2756
2928
|
);
|
|
2757
2929
|
const userRow = userResult.rows[0];
|
|
2758
2930
|
return c.json({
|
|
@@ -2778,7 +2950,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2778
2950
|
}
|
|
2779
2951
|
|
|
2780
2952
|
// src/routes/admin/schema.ts
|
|
2781
|
-
import { sql as
|
|
2953
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
2782
2954
|
function liveFields(fields) {
|
|
2783
2955
|
return fields.filter((f) => f.removed !== true);
|
|
2784
2956
|
}
|
|
@@ -2818,7 +2990,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2818
2990
|
const data = await Promise.all(
|
|
2819
2991
|
Object.values(registry.content_types).map(async (ct) => {
|
|
2820
2992
|
const result = await db.execute(
|
|
2821
|
-
|
|
2993
|
+
sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${ct.db.table_name}"`)}`
|
|
2822
2994
|
);
|
|
2823
2995
|
const count = Number(result.rows[0].count);
|
|
2824
2996
|
return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
|
|
@@ -2830,7 +3002,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2830
3002
|
const data = await Promise.all(
|
|
2831
3003
|
Object.values(registry.taxonomy_types).map(async (tt) => {
|
|
2832
3004
|
const result = await db.execute(
|
|
2833
|
-
|
|
3005
|
+
sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${tt.db.table_name}"`)}`
|
|
2834
3006
|
);
|
|
2835
3007
|
const count = Number(result.rows[0].count);
|
|
2836
3008
|
return { name: tt.name, label: tt.label, count };
|
|
@@ -2842,14 +3014,14 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2842
3014
|
|
|
2843
3015
|
// src/repositories/content.ts
|
|
2844
3016
|
import crypto from "crypto";
|
|
2845
|
-
import { sql as
|
|
3017
|
+
import { sql as sql8 } from "drizzle-orm";
|
|
2846
3018
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
2847
3019
|
function codeError(code, message) {
|
|
2848
3020
|
const err = new Error(message);
|
|
2849
3021
|
err.code = code;
|
|
2850
3022
|
return err;
|
|
2851
3023
|
}
|
|
2852
|
-
function
|
|
3024
|
+
function quoteIdent3(name) {
|
|
2853
3025
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
2854
3026
|
throw new Error(`Unsafe SQL identifier: ${name}`);
|
|
2855
3027
|
}
|
|
@@ -2864,43 +3036,43 @@ function quoteField(name) {
|
|
|
2864
3036
|
function buildConditions(published_only, filters, search) {
|
|
2865
3037
|
const conds = [];
|
|
2866
3038
|
if (published_only) {
|
|
2867
|
-
conds.push(
|
|
3039
|
+
conds.push(sql8`published = true`);
|
|
2868
3040
|
}
|
|
2869
3041
|
if (search && search.term.trim() !== "" && search.columns.length > 0) {
|
|
2870
3042
|
const pattern = `%${search.term.trim()}%`;
|
|
2871
|
-
const orConds = search.columns.map((col) =>
|
|
2872
|
-
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 `)})`);
|
|
2873
3045
|
}
|
|
2874
3046
|
if (filters) {
|
|
2875
3047
|
for (const [field, value] of Object.entries(filters)) {
|
|
2876
|
-
const col =
|
|
3048
|
+
const col = sql8.raw(quoteField(field));
|
|
2877
3049
|
if (Array.isArray(value)) {
|
|
2878
3050
|
if (value.length === 0) continue;
|
|
2879
|
-
const inList =
|
|
2880
|
-
value.map((v) =>
|
|
2881
|
-
|
|
3051
|
+
const inList = sql8.join(
|
|
3052
|
+
value.map((v) => sql8`${v}`),
|
|
3053
|
+
sql8`, `
|
|
2882
3054
|
);
|
|
2883
|
-
conds.push(
|
|
3055
|
+
conds.push(sql8`${col} IN (${inList})`);
|
|
2884
3056
|
} else if (typeof value === "object" && value !== null) {
|
|
2885
3057
|
const op = value;
|
|
2886
|
-
if (op.gt !== void 0) conds.push(
|
|
2887
|
-
if (op.gte !== void 0) conds.push(
|
|
2888
|
-
if (op.lt !== void 0) conds.push(
|
|
2889
|
-
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}`);
|
|
2890
3062
|
} else {
|
|
2891
|
-
conds.push(
|
|
3063
|
+
conds.push(sql8`${col} = ${value}`);
|
|
2892
3064
|
}
|
|
2893
3065
|
}
|
|
2894
3066
|
}
|
|
2895
3067
|
return conds;
|
|
2896
3068
|
}
|
|
2897
3069
|
function whereFragment(conds) {
|
|
2898
|
-
return conds.length > 0 ?
|
|
3070
|
+
return conds.length > 0 ? sql8` WHERE ${sql8.join(conds, sql8` AND `)}` : sql8``;
|
|
2899
3071
|
}
|
|
2900
3072
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
2901
3073
|
const { relations = {}, publishedRelations = false, sortableColumns } = options;
|
|
2902
3074
|
function tableRaw() {
|
|
2903
|
-
return
|
|
3075
|
+
return sql8.raw(quoteIdent3(tableName));
|
|
2904
3076
|
}
|
|
2905
3077
|
async function resolveRows(rows, include, cache) {
|
|
2906
3078
|
if (rows.length === 0) return rows;
|
|
@@ -2943,14 +3115,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2943
3115
|
const offset = (page - 1) * per_page;
|
|
2944
3116
|
const conds = buildConditions(published_only, filters, search);
|
|
2945
3117
|
const where = whereFragment(conds);
|
|
2946
|
-
const sortCol =
|
|
2947
|
-
const sortDir =
|
|
3118
|
+
const sortCol = sql8.raw(quoteIdent3(sort_by));
|
|
3119
|
+
const sortDir = sql8.raw(sort_order === "desc" ? "DESC" : "ASC");
|
|
2948
3120
|
const countResult = await db.execute(
|
|
2949
|
-
|
|
3121
|
+
sql8`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
|
|
2950
3122
|
);
|
|
2951
3123
|
const total = Number(countResult.rows[0]?.["total"] ?? 0);
|
|
2952
3124
|
const dataResult = await db.execute(
|
|
2953
|
-
|
|
3125
|
+
sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
|
|
2954
3126
|
);
|
|
2955
3127
|
const rows = dataResult.rows;
|
|
2956
3128
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -2971,7 +3143,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2971
3143
|
},
|
|
2972
3144
|
async findOne(id, include = []) {
|
|
2973
3145
|
const result = await db.execute(
|
|
2974
|
-
|
|
3146
|
+
sql8`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
|
|
2975
3147
|
);
|
|
2976
3148
|
if (result.rows.length === 0) return null;
|
|
2977
3149
|
const rows = result.rows;
|
|
@@ -2981,7 +3153,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2981
3153
|
},
|
|
2982
3154
|
async findBySlug(slug, include = []) {
|
|
2983
3155
|
const result = await db.execute(
|
|
2984
|
-
|
|
3156
|
+
sql8`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
|
|
2985
3157
|
);
|
|
2986
3158
|
if (result.rows.length === 0) return null;
|
|
2987
3159
|
const rows = result.rows;
|
|
@@ -2997,10 +3169,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2997
3169
|
updated_at: /* @__PURE__ */ new Date()
|
|
2998
3170
|
};
|
|
2999
3171
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3000
|
-
const colsSql =
|
|
3001
|
-
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`, `);
|
|
3002
3174
|
const result = await db.execute(
|
|
3003
|
-
|
|
3175
|
+
sql8`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3004
3176
|
);
|
|
3005
3177
|
return result.rows[0];
|
|
3006
3178
|
},
|
|
@@ -3013,25 +3185,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3013
3185
|
if (entries.length === 0) {
|
|
3014
3186
|
return this.findOne(id);
|
|
3015
3187
|
}
|
|
3016
|
-
const setClauses =
|
|
3017
|
-
entries.map(([k, v]) =>
|
|
3018
|
-
|
|
3188
|
+
const setClauses = sql8.join(
|
|
3189
|
+
entries.map(([k, v]) => sql8`${sql8.raw(quoteIdent3(k))} = ${v}`),
|
|
3190
|
+
sql8`, `
|
|
3019
3191
|
);
|
|
3020
3192
|
const result = await db.execute(
|
|
3021
|
-
|
|
3193
|
+
sql8`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3022
3194
|
);
|
|
3023
3195
|
if (result.rows.length === 0) return null;
|
|
3024
3196
|
return result.rows[0];
|
|
3025
3197
|
},
|
|
3026
3198
|
async delete(id) {
|
|
3027
|
-
await db.execute(
|
|
3199
|
+
await db.execute(sql8`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
|
|
3028
3200
|
},
|
|
3029
3201
|
async findAll(opts = {}) {
|
|
3030
3202
|
const { include = [], published_only } = opts;
|
|
3031
3203
|
const conds = buildConditions(published_only, void 0, void 0);
|
|
3032
3204
|
const where = whereFragment(conds);
|
|
3033
3205
|
const result = await db.execute(
|
|
3034
|
-
|
|
3206
|
+
sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
|
|
3035
3207
|
);
|
|
3036
3208
|
const rows = result.rows;
|
|
3037
3209
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3043,23 +3215,23 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3043
3215
|
|
|
3044
3216
|
// src/repositories/media.ts
|
|
3045
3217
|
import crypto2 from "crypto";
|
|
3046
|
-
import { sql as
|
|
3218
|
+
import { sql as sql9 } from "drizzle-orm";
|
|
3047
3219
|
function createMediaRepository(db) {
|
|
3048
|
-
const TABLE =
|
|
3220
|
+
const TABLE = sql9.raw('"media"');
|
|
3049
3221
|
return {
|
|
3050
3222
|
async findMany(opts = {}) {
|
|
3051
3223
|
const { page = 1, per_page = 10, type, orphaned } = opts;
|
|
3052
3224
|
const offset = (page - 1) * per_page;
|
|
3053
3225
|
const conds = [];
|
|
3054
|
-
if (type) conds.push(
|
|
3055
|
-
if (orphaned) conds.push(
|
|
3056
|
-
const where = conds.length > 0 ?
|
|
3057
|
-
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}`);
|
|
3058
3230
|
const total = Number(
|
|
3059
3231
|
countResult.rows[0]?.["total"] ?? 0
|
|
3060
3232
|
);
|
|
3061
3233
|
const dataResult = await db.execute(
|
|
3062
|
-
|
|
3234
|
+
sql9`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
|
|
3063
3235
|
);
|
|
3064
3236
|
const total_pages = Math.ceil(total / per_page);
|
|
3065
3237
|
return {
|
|
@@ -3077,7 +3249,7 @@ function createMediaRepository(db) {
|
|
|
3077
3249
|
},
|
|
3078
3250
|
async findOne(id) {
|
|
3079
3251
|
const result = await db.execute(
|
|
3080
|
-
|
|
3252
|
+
sql9`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
|
|
3081
3253
|
);
|
|
3082
3254
|
if (result.rows.length === 0) return null;
|
|
3083
3255
|
return result.rows[0];
|
|
@@ -3091,10 +3263,10 @@ function createMediaRepository(db) {
|
|
|
3091
3263
|
updated_at: /* @__PURE__ */ new Date()
|
|
3092
3264
|
};
|
|
3093
3265
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3094
|
-
const colsSql =
|
|
3095
|
-
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`, `);
|
|
3096
3268
|
const result = await db.execute(
|
|
3097
|
-
|
|
3269
|
+
sql9`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3098
3270
|
);
|
|
3099
3271
|
return result.rows[0];
|
|
3100
3272
|
},
|
|
@@ -3104,31 +3276,31 @@ function createMediaRepository(db) {
|
|
|
3104
3276
|
([k, v]) => k !== "id" && v !== void 0
|
|
3105
3277
|
);
|
|
3106
3278
|
if (entries.length === 0) return this.findOne(id);
|
|
3107
|
-
const setClauses =
|
|
3108
|
-
entries.map(([k, v]) =>
|
|
3109
|
-
|
|
3279
|
+
const setClauses = sql9.join(
|
|
3280
|
+
entries.map(([k, v]) => sql9`${sql9.raw(`"${k}"`)} = ${v}`),
|
|
3281
|
+
sql9`, `
|
|
3110
3282
|
);
|
|
3111
3283
|
const result = await db.execute(
|
|
3112
|
-
|
|
3284
|
+
sql9`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3113
3285
|
);
|
|
3114
3286
|
if (result.rows.length === 0) return null;
|
|
3115
3287
|
return result.rows[0];
|
|
3116
3288
|
},
|
|
3117
3289
|
async delete(id) {
|
|
3118
|
-
await db.execute(
|
|
3290
|
+
await db.execute(sql9`DELETE FROM ${TABLE} WHERE id = ${id}`);
|
|
3119
3291
|
},
|
|
3120
3292
|
async incrementReferenceCount(ids) {
|
|
3121
3293
|
if (ids.length === 0) return;
|
|
3122
|
-
const inList =
|
|
3294
|
+
const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
|
|
3123
3295
|
await db.execute(
|
|
3124
|
-
|
|
3296
|
+
sql9`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
|
|
3125
3297
|
);
|
|
3126
3298
|
},
|
|
3127
3299
|
async decrementReferenceCount(ids) {
|
|
3128
3300
|
if (ids.length === 0) return;
|
|
3129
|
-
const inList =
|
|
3301
|
+
const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
|
|
3130
3302
|
await db.execute(
|
|
3131
|
-
|
|
3303
|
+
sql9`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
|
|
3132
3304
|
);
|
|
3133
3305
|
}
|
|
3134
3306
|
};
|
|
@@ -3261,7 +3433,7 @@ function createCmsApp(options) {
|
|
|
3261
3433
|
const requirePermission = createPermissionMiddleware(rolesRegistry);
|
|
3262
3434
|
const getUserRole = async (userId) => {
|
|
3263
3435
|
const result = await db.execute(
|
|
3264
|
-
|
|
3436
|
+
sql10`SELECT r.name AS role
|
|
3265
3437
|
FROM users u
|
|
3266
3438
|
JOIN roles r ON r.id = u.role_id
|
|
3267
3439
|
WHERE u.id = ${userId}
|
|
@@ -3389,7 +3561,7 @@ function createCmsApp(options) {
|
|
|
3389
3561
|
app.use(path, attachDeprecationHeaders);
|
|
3390
3562
|
}
|
|
3391
3563
|
}
|
|
3392
|
-
registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
|
|
3564
|
+
registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver, projectors);
|
|
3393
3565
|
}
|
|
3394
3566
|
if (options.versions !== void 0) {
|
|
3395
3567
|
app.all(`${prefix}/:version/*`, async (c, next) => {
|
|
@@ -3431,7 +3603,7 @@ function createCmsApp(options) {
|
|
|
3431
3603
|
handler: null,
|
|
3432
3604
|
error: null
|
|
3433
3605
|
}));
|
|
3434
|
-
const ready = import("./handler-
|
|
3606
|
+
const ready = import("./handler-VEOWJRAL.mjs").then(({ createGraphQLHandler }) => {
|
|
3435
3607
|
gqlPasses.forEach((pass, i) => {
|
|
3436
3608
|
const mount = mounts[i];
|
|
3437
3609
|
try {
|
|
@@ -3455,7 +3627,8 @@ function createCmsApp(options) {
|
|
|
3455
3627
|
endpoint: mount.path,
|
|
3456
3628
|
currentVersion: model.current,
|
|
3457
3629
|
...projection !== void 0 && { projection },
|
|
3458
|
-
...currentProjection !== void 0 && { currentProjection }
|
|
3630
|
+
...currentProjection !== void 0 && { currentProjection },
|
|
3631
|
+
currentFieldKeyMaps: fieldKeyMaps
|
|
3459
3632
|
}
|
|
3460
3633
|
);
|
|
3461
3634
|
} catch (err) {
|