@bobbykim/manguito-cms-api 0.4.3 → 0.6.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-4Y4G564P.mjs → chunk-JVP33FI2.mjs} +99 -58
- package/dist/chunk-JVP33FI2.mjs.map +1 -0
- package/dist/{chunk-HEYT5CH5.mjs → chunk-XNXM4W4K.mjs} +147 -40
- package/dist/chunk-XNXM4W4K.mjs.map +1 -0
- package/dist/graphql/index.d.mts +7 -2
- package/dist/graphql/index.d.ts +7 -2
- package/dist/graphql/index.js +145 -38
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-XZVDLKZ2.mjs +8 -0
- package/dist/index.d.mts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +557 -171
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +303 -76
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
- package/dist/chunk-4Y4G564P.mjs.map +0 -1
- package/dist/chunk-HEYT5CH5.mjs.map +0 -1
- package/dist/handler-45O5OX2C.mjs +0 -8
- /package/dist/{handler-45O5OX2C.mjs.map → handler-XZVDLKZ2.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -30,6 +30,105 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/field-keys.ts
|
|
34
|
+
function isColumnBacked(field) {
|
|
35
|
+
const col = field.db_column;
|
|
36
|
+
if (col === null) return false;
|
|
37
|
+
if (col.column_name === "") return false;
|
|
38
|
+
if (col.junction) return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
|
|
42
|
+
const labelToColumn = /* @__PURE__ */ new Map();
|
|
43
|
+
const columnToLabel = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const { label, column } of pairs) {
|
|
45
|
+
labelToColumn.set(label, column);
|
|
46
|
+
columnToLabel.set(column, label);
|
|
47
|
+
}
|
|
48
|
+
for (const { label, ownColumn } of allLabels) {
|
|
49
|
+
if (ownColumn === label) continue;
|
|
50
|
+
const columnOwner = columnToLabel.get(label);
|
|
51
|
+
if (columnOwner !== void 0 && columnOwner !== label) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Fatal: field key map failed to build \u2014 field label "${label}" collides with the storage column of field "${columnOwner}". A field label may not reuse another field's column name. Rename the field, then run \`manguito validate\` to check your schema.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const label of labelToColumn.keys()) {
|
|
58
|
+
if (droppedKeys.has(label)) labelToColumn.delete(label);
|
|
59
|
+
}
|
|
60
|
+
for (const column of columnToLabel.keys()) {
|
|
61
|
+
if (droppedKeys.has(column)) columnToLabel.delete(column);
|
|
62
|
+
}
|
|
63
|
+
const diverges = [...labelToColumn].some(([label, column]) => label !== column);
|
|
64
|
+
function remap(input, lookup) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const key of Object.keys(input)) {
|
|
67
|
+
if (droppedKeys.has(key)) continue;
|
|
68
|
+
out[lookup.get(key) ?? key] = input[key];
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
toStorage: (input) => remap(input, labelToColumn),
|
|
74
|
+
toLabels: (row) => remap(row, columnToLabel),
|
|
75
|
+
columnFor: (label) => labelToColumn.get(label),
|
|
76
|
+
labelFor: (column) => columnToLabel.get(column),
|
|
77
|
+
labels: [...labelToColumn.keys()],
|
|
78
|
+
diverges
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function createFieldKeyMap(fields) {
|
|
82
|
+
const droppedKeys = /* @__PURE__ */ new Set();
|
|
83
|
+
const pairs = [];
|
|
84
|
+
for (const f of fields) {
|
|
85
|
+
if (!isColumnBacked(f)) continue;
|
|
86
|
+
pairs.push({ label: f.name, column: f.db_column.column_name });
|
|
87
|
+
if (f.removed === true) {
|
|
88
|
+
droppedKeys.add(f.name);
|
|
89
|
+
droppedKeys.add(f.db_column.column_name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return buildFieldKeyMap(
|
|
93
|
+
pairs,
|
|
94
|
+
fields.map((f) => ({
|
|
95
|
+
label: f.name,
|
|
96
|
+
// Conditional spread, not a plain assignment — exactOptionalPropertyTypes
|
|
97
|
+
// forbids present-and-undefined.
|
|
98
|
+
...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
|
|
99
|
+
})),
|
|
100
|
+
droppedKeys
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
function createFieldKeyMapFromProjection(projectionType, allFields) {
|
|
104
|
+
const pairs = projectionType.fields.map((f) => ({ label: f.exposed_as, column: f.column_name }));
|
|
105
|
+
const projected = new Set(pairs.map((p) => p.label));
|
|
106
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
107
|
+
for (const f of allFields) {
|
|
108
|
+
if (!isColumnBacked(f)) continue;
|
|
109
|
+
dropped.add(f.name);
|
|
110
|
+
dropped.add(f.db_column.column_name);
|
|
111
|
+
}
|
|
112
|
+
for (const p of pairs) dropped.delete(p.column);
|
|
113
|
+
for (const label of projected) dropped.delete(label);
|
|
114
|
+
return buildFieldKeyMap(
|
|
115
|
+
pairs,
|
|
116
|
+
allFields.map((f) => ({
|
|
117
|
+
label: f.name,
|
|
118
|
+
// See buildFieldKeyMap's collision check: this is what lets it tell a
|
|
119
|
+
// version exposing a field's own column under a new label (fine) from a
|
|
120
|
+
// label that reuses another field's column (not fine).
|
|
121
|
+
...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
|
|
122
|
+
})),
|
|
123
|
+
dropped
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
var init_field_keys = __esm({
|
|
127
|
+
"src/field-keys.ts"() {
|
|
128
|
+
"use strict";
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
33
132
|
// src/relations.ts
|
|
34
133
|
function quoteIdent(name) {
|
|
35
134
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
@@ -333,65 +432,6 @@ var init_relations = __esm({
|
|
|
333
432
|
}
|
|
334
433
|
});
|
|
335
434
|
|
|
336
|
-
// src/field-keys.ts
|
|
337
|
-
function isColumnBacked(field) {
|
|
338
|
-
const col = field.db_column;
|
|
339
|
-
if (col === null) return false;
|
|
340
|
-
if (col.column_name === "") return false;
|
|
341
|
-
if (col.junction) return false;
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
function createFieldKeyMap(fields) {
|
|
345
|
-
const labelToColumn = /* @__PURE__ */ new Map();
|
|
346
|
-
const columnToLabel = /* @__PURE__ */ new Map();
|
|
347
|
-
const droppedKeys = /* @__PURE__ */ new Set();
|
|
348
|
-
for (const f of fields) {
|
|
349
|
-
if (!isColumnBacked(f)) continue;
|
|
350
|
-
columnToLabel.set(f.db_column.column_name, f.name);
|
|
351
|
-
if (f.removed === true) {
|
|
352
|
-
droppedKeys.add(f.name);
|
|
353
|
-
droppedKeys.add(f.db_column.column_name);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
labelToColumn.set(f.name, f.db_column.column_name);
|
|
357
|
-
}
|
|
358
|
-
for (const f of fields) {
|
|
359
|
-
const columnOwner = columnToLabel.get(f.name);
|
|
360
|
-
if (columnOwner !== void 0 && columnOwner !== f.name) {
|
|
361
|
-
throw new Error(
|
|
362
|
-
`Fatal: field key map failed to build \u2014 field label "${f.name}" collides with the storage column of field "${columnOwner}". A field label may not reuse another field's column name. Rename the field, then run \`manguito validate\` to check your schema.`
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
for (const f of fields) {
|
|
367
|
-
if (isColumnBacked(f) && f.removed === true) {
|
|
368
|
-
columnToLabel.delete(f.db_column.column_name);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
const diverges = [...labelToColumn].some(([label, column]) => label !== column);
|
|
372
|
-
function remap(input, lookup) {
|
|
373
|
-
const out = {};
|
|
374
|
-
for (const key of Object.keys(input)) {
|
|
375
|
-
if (droppedKeys.has(key)) continue;
|
|
376
|
-
out[lookup.get(key) ?? key] = input[key];
|
|
377
|
-
}
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
380
|
-
return {
|
|
381
|
-
toStorage: (input) => remap(input, labelToColumn),
|
|
382
|
-
toLabels: (row) => remap(row, columnToLabel),
|
|
383
|
-
columnFor: (label) => labelToColumn.get(label),
|
|
384
|
-
labelFor: (column) => columnToLabel.get(column),
|
|
385
|
-
labels: [...labelToColumn.keys()],
|
|
386
|
-
diverges
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
var init_field_keys = __esm({
|
|
390
|
-
"src/field-keys.ts"() {
|
|
391
|
-
"use strict";
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
|
|
395
435
|
// src/graphql/scalars.ts
|
|
396
436
|
var import_graphql, DateTimeScalar, JSONScalar;
|
|
397
437
|
var init_scalars = __esm({
|
|
@@ -509,10 +549,13 @@ var init_naming = __esm({
|
|
|
509
549
|
});
|
|
510
550
|
|
|
511
551
|
// src/graphql/filters.ts
|
|
512
|
-
function buildSortFieldEnum(typeName) {
|
|
552
|
+
function buildSortFieldEnum(typeName, fieldKeys) {
|
|
553
|
+
const resolvable = SORTABLE.filter(
|
|
554
|
+
(s) => s.gql === "createdAt" || s.gql === "updatedAt" || fieldKeys === void 0 || fieldKeys.columnFor(s.value) !== void 0
|
|
555
|
+
);
|
|
513
556
|
return new import_graphql3.GraphQLEnumType({
|
|
514
557
|
name: `${typeName}SortField`,
|
|
515
|
-
values: Object.fromEntries(
|
|
558
|
+
values: Object.fromEntries(resolvable.map((s) => [s.gql, { value: s.value }]))
|
|
516
559
|
});
|
|
517
560
|
}
|
|
518
561
|
function comparableFilterInput(name, scalar) {
|
|
@@ -547,18 +590,19 @@ function filterInputForField(fieldType) {
|
|
|
547
590
|
if (scalar === null) return null;
|
|
548
591
|
return StringFilter;
|
|
549
592
|
}
|
|
550
|
-
function buildFilterInputType(type) {
|
|
593
|
+
function buildFilterInputType(type, viewFields) {
|
|
551
594
|
const fields = {};
|
|
552
595
|
fields["id"] = { type: IDFilter };
|
|
553
596
|
fields["published"] = { type: BooleanFilter };
|
|
554
597
|
fields["createdAt"] = { type: DateTimeFilter };
|
|
555
598
|
fields["updatedAt"] = { type: DateTimeFilter };
|
|
556
599
|
if (type.schema_type === "content-type") fields["slug"] = { type: StringFilter };
|
|
557
|
-
|
|
600
|
+
const entries = viewFields !== void 0 ? viewFields.map((v) => ({ field: v.field, exposedAs: v.exposedAs })) : type.fields.filter((f) => f.removed !== true).map((f) => ({ field: f, exposedAs: f.name }));
|
|
601
|
+
for (const { field: f, exposedAs } of entries) {
|
|
558
602
|
if (f.field_type === "programmatic" || f.field_type === "paragraph") continue;
|
|
559
603
|
if (f.field_type === "image" || f.field_type === "video" || f.field_type === "file") continue;
|
|
560
604
|
const input = filterInputForField(f.field_type);
|
|
561
|
-
if (input) fields[buildFieldNameMap([
|
|
605
|
+
if (input) fields[buildFieldNameMap([exposedAs]).toGraphql(exposedAs)] = { type: input };
|
|
562
606
|
}
|
|
563
607
|
if (Object.keys(fields).length === 0) return null;
|
|
564
608
|
return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
|
|
@@ -621,32 +665,41 @@ function codeError2(code, message) {
|
|
|
621
665
|
function isPublished2(item) {
|
|
622
666
|
return !!item && item["published"] === true;
|
|
623
667
|
}
|
|
624
|
-
function resolveFieldValue(field, row) {
|
|
668
|
+
function resolveFieldValue(field, row, fallback) {
|
|
625
669
|
const key = isColumnBacked(field) ? field.db_column.column_name : field.name;
|
|
626
|
-
|
|
670
|
+
const value = row[key];
|
|
671
|
+
if ((value === null || value === void 0) && fallback !== void 0) return fallback;
|
|
672
|
+
return value ?? null;
|
|
627
673
|
}
|
|
628
|
-
function scalarFieldResolver(field) {
|
|
629
|
-
return (parent) => resolveFieldValue(field, parent);
|
|
674
|
+
function scalarFieldResolver(field, fallback) {
|
|
675
|
+
return (parent) => resolveFieldValue(field, parent, fallback);
|
|
630
676
|
}
|
|
631
677
|
function relationFieldResolver(typeName, schemaFieldName) {
|
|
632
678
|
return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
|
|
633
679
|
}
|
|
634
|
-
function programmaticFieldResolver(typeName, schemaFieldName,
|
|
680
|
+
function programmaticFieldResolver(typeName, schemaFieldName, mediaFields = [], fieldKeys) {
|
|
635
681
|
return async (parent, _args, ctx) => {
|
|
636
682
|
let p = ctx.programmaticMemo.get(parent);
|
|
637
683
|
if (!p) {
|
|
638
|
-
p = resolveProgrammaticRow(typeName, parent, ctx,
|
|
684
|
+
p = resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys);
|
|
639
685
|
ctx.programmaticMemo.set(parent, p);
|
|
640
686
|
}
|
|
641
687
|
return (await p)[schemaFieldName];
|
|
642
688
|
};
|
|
643
689
|
}
|
|
644
|
-
async function resolveProgrammaticRow(typeName, parent, ctx,
|
|
690
|
+
async function resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys) {
|
|
645
691
|
const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
|
|
646
|
-
if (
|
|
692
|
+
if (mediaFields.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
|
|
647
693
|
const enriched = { ...parent };
|
|
648
|
-
await Promise.all(
|
|
649
|
-
return ctx.resolver.resolveItem(typeName, toLabels(enriched));
|
|
694
|
+
await Promise.all(mediaFields.map((m) => ctx.loaders.load(typeName, m.name, enriched)));
|
|
695
|
+
return ctx.resolver.resolveItem(typeName, relabelMedia(toLabels(enriched), enriched, mediaFields));
|
|
696
|
+
}
|
|
697
|
+
function relabelMedia(record, enriched, mediaFields) {
|
|
698
|
+
for (const { name, exposedAs } of mediaFields) {
|
|
699
|
+
if (!(name in enriched)) continue;
|
|
700
|
+
record[exposedAs] = enriched[name];
|
|
701
|
+
}
|
|
702
|
+
return record;
|
|
650
703
|
}
|
|
651
704
|
function collectionResolver(typeName, nameMap, fieldKeys) {
|
|
652
705
|
return async (_root, args, ctx) => {
|
|
@@ -704,7 +757,7 @@ var init_resolvers = __esm({
|
|
|
704
757
|
});
|
|
705
758
|
|
|
706
759
|
// src/graphql/schema.ts
|
|
707
|
-
function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
|
|
760
|
+
function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
|
|
708
761
|
const objectTypes = /* @__PURE__ */ new Map();
|
|
709
762
|
const enumTypes = /* @__PURE__ */ new Map();
|
|
710
763
|
for (const [name, enumType] of Object.entries(registry.enum_types)) {
|
|
@@ -723,14 +776,14 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
|
|
|
723
776
|
);
|
|
724
777
|
}
|
|
725
778
|
}
|
|
726
|
-
function outputTypeForField(field) {
|
|
779
|
+
function outputTypeForField(field, required) {
|
|
727
780
|
const scalar = scalarOutputType(field.field_type);
|
|
728
|
-
if (scalar) return
|
|
781
|
+
if (scalar) return required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
|
|
729
782
|
if (field.field_type === "enum") {
|
|
730
783
|
const ref = field.ui_component.component === "select" ? field.ui_component.enum_ref : void 0;
|
|
731
784
|
const et = ref ? enumTypes.get(ref) : void 0;
|
|
732
785
|
const t = et ?? import_graphql5.GraphQLString;
|
|
733
|
-
return
|
|
786
|
+
return required ? new import_graphql5.GraphQLNonNull(t) : t;
|
|
734
787
|
}
|
|
735
788
|
if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
736
789
|
return MEDIA;
|
|
@@ -760,8 +813,17 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
|
|
|
760
813
|
}
|
|
761
814
|
return import_graphql5.GraphQLString;
|
|
762
815
|
}
|
|
816
|
+
function exposedFields(type) {
|
|
817
|
+
return type.fields.filter((f) => f.removed !== true);
|
|
818
|
+
}
|
|
819
|
+
function viewFieldsFor(machineName, type) {
|
|
820
|
+
return view?.[machineName]?.fields ?? exposedFields(type).map((f) => ({ field: f, exposedAs: f.name, required: f.required }));
|
|
821
|
+
}
|
|
763
822
|
function buildObjectType(machineName, type) {
|
|
764
|
-
const
|
|
823
|
+
const visible = viewFieldsFor(machineName, type);
|
|
824
|
+
const mediaFields = visible.filter(
|
|
825
|
+
(v) => v.field.field_type === "image" || v.field.field_type === "video" || v.field.field_type === "file"
|
|
826
|
+
).map((v) => ({ name: v.field.name, exposedAs: v.exposedAs }));
|
|
765
827
|
return new import_graphql5.GraphQLObjectType({
|
|
766
828
|
name: graphqlTypeName(machineName),
|
|
767
829
|
fields: () => {
|
|
@@ -775,23 +837,25 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
|
|
|
775
837
|
}
|
|
776
838
|
fields["createdAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["created_at"] };
|
|
777
839
|
fields["updatedAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["updated_at"] };
|
|
778
|
-
for (const
|
|
779
|
-
const
|
|
780
|
-
const
|
|
840
|
+
for (const vf of visible) {
|
|
841
|
+
const field = vf.field;
|
|
842
|
+
const gqlName = toCamelCase(vf.exposedAs);
|
|
843
|
+
const outType = outputTypeForField(field, vf.required);
|
|
781
844
|
let resolve;
|
|
782
845
|
if (field.field_type === "programmatic") {
|
|
783
|
-
resolve = programmaticFieldResolver(
|
|
784
|
-
machineName,
|
|
785
|
-
field.name,
|
|
786
|
-
mediaFieldNames,
|
|
787
|
-
fieldKeyMaps[machineName]
|
|
788
|
-
);
|
|
846
|
+
resolve = programmaticFieldResolver(machineName, field.name, mediaFields, fieldKeyMaps[machineName]);
|
|
789
847
|
} else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
790
848
|
resolve = relationFieldResolver(machineName, field.name);
|
|
791
849
|
} else {
|
|
792
|
-
resolve = scalarFieldResolver(field);
|
|
850
|
+
resolve = scalarFieldResolver(field, vf.fallback);
|
|
793
851
|
}
|
|
794
|
-
fields[gqlName] = {
|
|
852
|
+
fields[gqlName] = {
|
|
853
|
+
type: outType,
|
|
854
|
+
resolve,
|
|
855
|
+
// Conditional spread, not `deprecationReason: vf.deprecationReason`
|
|
856
|
+
// — exactOptionalPropertyTypes forbids present-and-undefined.
|
|
857
|
+
...vf.deprecationReason !== void 0 && { deprecationReason: vf.deprecationReason }
|
|
858
|
+
};
|
|
795
859
|
}
|
|
796
860
|
return fields;
|
|
797
861
|
}
|
|
@@ -823,7 +887,8 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
|
|
|
823
887
|
const queryFields = {};
|
|
824
888
|
for (const [name, ct] of Object.entries(registry.content_types)) {
|
|
825
889
|
const objType = objectTypes.get(name);
|
|
826
|
-
const
|
|
890
|
+
const visible = viewFieldsFor(name, ct);
|
|
891
|
+
const nameMap = buildFieldNameMap(visible.map((v) => v.exposedAs));
|
|
827
892
|
if (ct.only_one) {
|
|
828
893
|
queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
|
|
829
894
|
continue;
|
|
@@ -835,13 +900,13 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
|
|
|
835
900
|
meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
|
|
836
901
|
}
|
|
837
902
|
});
|
|
838
|
-
const filterType = buildFilterInputType(ct);
|
|
903
|
+
const filterType = buildFilterInputType(ct, visible);
|
|
839
904
|
queryFields[collectionQueryName(name)] = {
|
|
840
905
|
type: new import_graphql5.GraphQLNonNull(listType),
|
|
841
906
|
args: {
|
|
842
907
|
page: { type: import_graphql5.GraphQLInt },
|
|
843
908
|
perPage: { type: import_graphql5.GraphQLInt },
|
|
844
|
-
sortBy: { type: buildSortFieldEnum(graphqlTypeName(name)) },
|
|
909
|
+
sortBy: { type: buildSortFieldEnum(graphqlTypeName(name), fieldKeyMaps[name]) },
|
|
845
910
|
sortOrder: { type: SortOrderEnum },
|
|
846
911
|
...filterType ? { filter: { type: filterType } } : {}
|
|
847
912
|
},
|
|
@@ -865,7 +930,11 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
|
|
|
865
930
|
queryFields[collectionQueryName(name)] = {
|
|
866
931
|
type: new import_graphql5.GraphQLNonNull(listType),
|
|
867
932
|
args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
|
|
868
|
-
resolve: collectionResolver(
|
|
933
|
+
resolve: collectionResolver(
|
|
934
|
+
name,
|
|
935
|
+
buildFieldNameMap(viewFieldsFor(name, tt).map((v) => v.exposedAs)),
|
|
936
|
+
fieldKeyMaps[name]
|
|
937
|
+
)
|
|
869
938
|
};
|
|
870
939
|
queryFields[singleQueryName(name)] = {
|
|
871
940
|
type: objType,
|
|
@@ -915,6 +984,76 @@ var init_schema = __esm({
|
|
|
915
984
|
}
|
|
916
985
|
});
|
|
917
986
|
|
|
987
|
+
// src/graphql/version-view.ts
|
|
988
|
+
function byColumn(fields) {
|
|
989
|
+
const out = /* @__PURE__ */ new Map();
|
|
990
|
+
for (const f of fields) {
|
|
991
|
+
if (!isColumnBacked(f)) continue;
|
|
992
|
+
out.set(f.db_column.column_name, f);
|
|
993
|
+
}
|
|
994
|
+
return out;
|
|
995
|
+
}
|
|
996
|
+
function deprecationFor(field, currentLabels, currentVersion) {
|
|
997
|
+
const currentLabel = currentLabels.get(field.column_name);
|
|
998
|
+
if (currentLabel === void 0) {
|
|
999
|
+
return `Removed in ${currentVersion}; column retained while this version is live.`;
|
|
1000
|
+
}
|
|
1001
|
+
if (currentLabel !== field.exposed_as) {
|
|
1002
|
+
return `Renamed to '${currentLabel}' in ${currentVersion}.`;
|
|
1003
|
+
}
|
|
1004
|
+
return void 0;
|
|
1005
|
+
}
|
|
1006
|
+
function buildVersionView(input) {
|
|
1007
|
+
const { registry, projection, currentProjection, currentVersion } = input;
|
|
1008
|
+
const out = {};
|
|
1009
|
+
const sources = [
|
|
1010
|
+
registry.content_types,
|
|
1011
|
+
registry.taxonomy_types,
|
|
1012
|
+
registry.paragraph_types
|
|
1013
|
+
];
|
|
1014
|
+
for (const source of sources) {
|
|
1015
|
+
for (const [typeName, type] of Object.entries(source)) {
|
|
1016
|
+
const projectionType = projection?.types[typeName];
|
|
1017
|
+
if (projectionType === void 0) {
|
|
1018
|
+
out[typeName] = {
|
|
1019
|
+
fields: type.fields.filter((f) => f.removed !== true).map((f) => ({ field: f, exposedAs: f.name, required: f.required }))
|
|
1020
|
+
};
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
const columns = byColumn(type.fields);
|
|
1024
|
+
const currentLabels = /* @__PURE__ */ new Map();
|
|
1025
|
+
for (const f of currentProjection?.types[typeName]?.fields ?? []) {
|
|
1026
|
+
currentLabels.set(f.column_name, f.exposed_as);
|
|
1027
|
+
}
|
|
1028
|
+
const fields = [];
|
|
1029
|
+
for (const pf of projectionType.fields) {
|
|
1030
|
+
const field = columns.get(pf.column_name);
|
|
1031
|
+
if (field === void 0) continue;
|
|
1032
|
+
const reason = deprecationFor(pf, currentLabels, currentVersion);
|
|
1033
|
+
fields.push({
|
|
1034
|
+
field,
|
|
1035
|
+
exposedAs: pf.exposed_as,
|
|
1036
|
+
required: pf.required,
|
|
1037
|
+
...pf.fallback !== void 0 && { fallback: pf.fallback },
|
|
1038
|
+
...reason !== void 0 && { deprecationReason: reason }
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
for (const f of type.fields) {
|
|
1042
|
+
if (isColumnBacked(f) || f.removed === true) continue;
|
|
1043
|
+
fields.push({ field: f, exposedAs: f.name, required: f.required });
|
|
1044
|
+
}
|
|
1045
|
+
out[typeName] = { fields };
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return out;
|
|
1049
|
+
}
|
|
1050
|
+
var init_version_view = __esm({
|
|
1051
|
+
"src/graphql/version-view.ts"() {
|
|
1052
|
+
"use strict";
|
|
1053
|
+
init_field_keys();
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
918
1057
|
// src/graphql/dataloaders.ts
|
|
919
1058
|
function createRelationLoaders(db, registry) {
|
|
920
1059
|
const relMaps = /* @__PURE__ */ new Map();
|
|
@@ -1007,15 +1146,29 @@ function isClientSafeGraphQLError(error) {
|
|
|
1007
1146
|
}
|
|
1008
1147
|
return false;
|
|
1009
1148
|
}
|
|
1010
|
-
function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options) {
|
|
1011
|
-
|
|
1149
|
+
function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options, versioning = {}) {
|
|
1150
|
+
let view;
|
|
1151
|
+
if (versioning.projection !== void 0) {
|
|
1152
|
+
if (versioning.currentVersion === void 0) {
|
|
1153
|
+
throw new Error(
|
|
1154
|
+
'createGraphQLHandler: currentVersion is required when a projection is supplied \u2014 deprecation reasons name it, and an empty one ships "Removed in ." into published SDL.'
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
view = buildVersionView({
|
|
1158
|
+
registry,
|
|
1159
|
+
projection: versioning.projection,
|
|
1160
|
+
currentProjection: versioning.currentProjection,
|
|
1161
|
+
currentVersion: versioning.currentVersion
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
const schema = buildGraphQLSchema(registry, fieldKeyMaps, view);
|
|
1012
1165
|
const { plugins } = buildArmorPlugin({
|
|
1013
1166
|
maxDepth: options.maxDepth,
|
|
1014
1167
|
maxComplexity: options.maxComplexity
|
|
1015
1168
|
});
|
|
1016
1169
|
const yoga = (0, import_graphql_yoga.createYoga)({
|
|
1017
1170
|
schema,
|
|
1018
|
-
graphqlEndpoint: "/graphql",
|
|
1171
|
+
graphqlEndpoint: versioning.endpoint ?? "/graphql",
|
|
1019
1172
|
graphiql: options.graphiql,
|
|
1020
1173
|
landingPage: false,
|
|
1021
1174
|
maskedErrors: {
|
|
@@ -1043,6 +1196,7 @@ var init_handler = __esm({
|
|
|
1043
1196
|
"use strict";
|
|
1044
1197
|
import_graphql_yoga = require("graphql-yoga");
|
|
1045
1198
|
init_schema();
|
|
1199
|
+
init_version_view();
|
|
1046
1200
|
init_dataloaders();
|
|
1047
1201
|
init_security();
|
|
1048
1202
|
}
|
|
@@ -1069,15 +1223,20 @@ function normalizePrefix(prefix) {
|
|
|
1069
1223
|
}
|
|
1070
1224
|
function createPublicPaths(prefix) {
|
|
1071
1225
|
return {
|
|
1072
|
-
collection: (basePath) => `${prefix}/${basePath}`,
|
|
1073
|
-
item: (basePath) => `${prefix}/${basePath}/:slug`,
|
|
1074
|
-
taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
|
|
1075
|
-
taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
|
|
1076
1226
|
mediaCollection: () => `${prefix}/media`,
|
|
1077
1227
|
mediaItem: () => `${prefix}/media/:id`,
|
|
1078
1228
|
openapi: () => `${prefix}/openapi.json`
|
|
1079
1229
|
};
|
|
1080
1230
|
}
|
|
1231
|
+
function createVersionedPaths(prefix, version) {
|
|
1232
|
+
const root = version === null ? prefix : `${prefix}/${version}`;
|
|
1233
|
+
return {
|
|
1234
|
+
collection: (basePath) => `${root}/${basePath}`,
|
|
1235
|
+
item: (basePath) => `${root}/${basePath}/:slug`,
|
|
1236
|
+
taxonomyCollection: (typeName) => `${root}/taxonomy/${typeName}`,
|
|
1237
|
+
taxonomyItem: (typeName) => `${root}/taxonomy/${typeName}/:id`
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1081
1240
|
|
|
1082
1241
|
// src/app.ts
|
|
1083
1242
|
var import_hono = require("hono");
|
|
@@ -1147,7 +1306,7 @@ function createSecurityHeadersMiddleware(options = {}) {
|
|
|
1147
1306
|
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
1148
1307
|
c.res.headers.set("X-Frame-Options", "DENY");
|
|
1149
1308
|
c.res.headers.set("Referrer-Policy", "no-referrer");
|
|
1150
|
-
const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
|
|
1309
|
+
const isGraphiql = graphiqlPath !== void 0 && (c.req.path === graphiqlPath || c.req.path.startsWith(`${graphiqlPath}/`));
|
|
1151
1310
|
c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
|
|
1152
1311
|
};
|
|
1153
1312
|
}
|
|
@@ -1673,7 +1832,7 @@ function nestedTargets(fields) {
|
|
|
1673
1832
|
}
|
|
1674
1833
|
return out;
|
|
1675
1834
|
}
|
|
1676
|
-
function buildProjectors(registry, fieldKeyMaps) {
|
|
1835
|
+
function buildProjectors(registry, fieldKeyMaps, fallbacks) {
|
|
1677
1836
|
const projectors = {};
|
|
1678
1837
|
const sources = [
|
|
1679
1838
|
registry.content_types,
|
|
@@ -1684,7 +1843,14 @@ function buildProjectors(registry, fieldKeyMaps) {
|
|
|
1684
1843
|
for (const [typeName, type] of Object.entries(source)) {
|
|
1685
1844
|
const map = fieldKeyMaps[typeName];
|
|
1686
1845
|
if (!map) continue;
|
|
1687
|
-
|
|
1846
|
+
const typeFallbacks = fallbacks?.[typeName];
|
|
1847
|
+
projectors[typeName] = {
|
|
1848
|
+
map,
|
|
1849
|
+
nested: nestedTargets(type.fields),
|
|
1850
|
+
// Spread rather than assign: with exactOptionalPropertyTypes, an optional
|
|
1851
|
+
// property may be ABSENT but not present-and-undefined.
|
|
1852
|
+
...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
|
|
1853
|
+
};
|
|
1688
1854
|
}
|
|
1689
1855
|
}
|
|
1690
1856
|
return projectors;
|
|
@@ -1693,6 +1859,11 @@ function projectRow(row, typeName, projectors) {
|
|
|
1693
1859
|
const p = projectors[typeName];
|
|
1694
1860
|
if (!p) return row;
|
|
1695
1861
|
const out = p.map.toLabels(row);
|
|
1862
|
+
if (p.fallbacks !== void 0) {
|
|
1863
|
+
for (const [label, value] of Object.entries(p.fallbacks)) {
|
|
1864
|
+
if (out[label] === null || out[label] === void 0) out[label] = value;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1696
1867
|
for (const { label, target } of p.nested) {
|
|
1697
1868
|
const v = out[label];
|
|
1698
1869
|
if (v === null || v === void 0) continue;
|
|
@@ -1705,6 +1876,9 @@ function projectRow(row, typeName, projectors) {
|
|
|
1705
1876
|
return out;
|
|
1706
1877
|
}
|
|
1707
1878
|
|
|
1879
|
+
// src/routes/content.ts
|
|
1880
|
+
init_field_keys();
|
|
1881
|
+
|
|
1708
1882
|
// src/routes/query-params.ts
|
|
1709
1883
|
var SORTABLE_FIELDS = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
1710
1884
|
var RELATION_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -1786,13 +1960,24 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
1786
1960
|
const basePath = contentType.default_base_path;
|
|
1787
1961
|
const repo = repos[typeName];
|
|
1788
1962
|
if (!repo) continue;
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
)
|
|
1963
|
+
const fieldKeys = projectors[typeName].map;
|
|
1964
|
+
const relationFieldNames = /* @__PURE__ */ new Set();
|
|
1965
|
+
for (const f of contentType.fields) {
|
|
1966
|
+
if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
|
|
1967
|
+
if (isColumnBacked(f)) {
|
|
1968
|
+
const label = fieldKeys.labelFor(f.db_column.column_name);
|
|
1969
|
+
if (label !== void 0) relationFieldNames.add(label);
|
|
1970
|
+
} else {
|
|
1971
|
+
relationFieldNames.add(f.name);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1792
1974
|
const filterableFieldNames = /* @__PURE__ */ new Set([
|
|
1793
|
-
...
|
|
1975
|
+
...fieldKeys.labels,
|
|
1794
1976
|
...contentType.system_fields.map((f) => f.name)
|
|
1795
1977
|
]);
|
|
1978
|
+
const sortableFieldNames = new Set(
|
|
1979
|
+
[...SORTABLE_FIELDS].filter((f) => filterableFieldNames.has(f))
|
|
1980
|
+
);
|
|
1796
1981
|
if (contentType.only_one) {
|
|
1797
1982
|
app.get(paths.collection(basePath), async (c) => {
|
|
1798
1983
|
const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
|
|
@@ -1808,8 +1993,6 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
1808
1993
|
});
|
|
1809
1994
|
} else {
|
|
1810
1995
|
registerListRoute(paths.collection(basePath), async (c) => {
|
|
1811
|
-
const projector = projectors[typeName];
|
|
1812
|
-
const fieldKeys = projector.map;
|
|
1813
1996
|
const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
|
|
1814
1997
|
if (!pagination.ok) {
|
|
1815
1998
|
return c.json(
|
|
@@ -1824,13 +2007,13 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
1824
2007
|
);
|
|
1825
2008
|
}
|
|
1826
2009
|
const sortBy = c.req.query("sort_by") ?? "created_at";
|
|
1827
|
-
if (!
|
|
2010
|
+
if (!sortableFieldNames.has(sortBy)) {
|
|
1828
2011
|
return c.json(
|
|
1829
2012
|
{
|
|
1830
2013
|
ok: false,
|
|
1831
2014
|
error: {
|
|
1832
2015
|
code: "INVALID_SORT_FIELD",
|
|
1833
|
-
message: `'${sortBy}' is not sortable. Allowed:
|
|
2016
|
+
message: `'${sortBy}' is not sortable. Allowed: ${[...sortableFieldNames].join(", ")}`
|
|
1834
2017
|
}
|
|
1835
2018
|
},
|
|
1836
2019
|
400
|
|
@@ -1877,7 +2060,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
1877
2060
|
);
|
|
1878
2061
|
}
|
|
1879
2062
|
}
|
|
1880
|
-
const sortColumn = fieldKeys.columnFor(sortBy)
|
|
2063
|
+
const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
|
|
1881
2064
|
const result = await repo.findMany({
|
|
1882
2065
|
published_only: true,
|
|
1883
2066
|
page: pagination.page,
|
|
@@ -3759,7 +3942,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3759
3942
|
var import_drizzle_orm6 = require("drizzle-orm");
|
|
3760
3943
|
var API_VERSION = (() => {
|
|
3761
3944
|
try {
|
|
3762
|
-
return "0.
|
|
3945
|
+
return "0.6.0";
|
|
3763
3946
|
} catch {
|
|
3764
3947
|
return "0.0.0";
|
|
3765
3948
|
}
|
|
@@ -4166,6 +4349,95 @@ function createMediaRepository(db) {
|
|
|
4166
4349
|
|
|
4167
4350
|
// src/app.ts
|
|
4168
4351
|
init_field_keys();
|
|
4352
|
+
|
|
4353
|
+
// src/versions.ts
|
|
4354
|
+
init_field_keys();
|
|
4355
|
+
var VERSION_SEGMENT = /^v\d+$/;
|
|
4356
|
+
function versionNumber(version) {
|
|
4357
|
+
return Number.parseInt(version.slice(1), 10);
|
|
4358
|
+
}
|
|
4359
|
+
function classifyVersion(segment, model) {
|
|
4360
|
+
if (!VERSION_SEGMENT.test(segment)) return "not-a-version";
|
|
4361
|
+
if (model.live.includes(segment)) return "live";
|
|
4362
|
+
return versionNumber(segment) < versionNumber(model.current) ? "retired" : "unknown";
|
|
4363
|
+
}
|
|
4364
|
+
function fallbacksFor(projection) {
|
|
4365
|
+
const out = {};
|
|
4366
|
+
for (const [typeName, type] of Object.entries(projection.types)) {
|
|
4367
|
+
const perLabel = {};
|
|
4368
|
+
for (const f of type.fields) {
|
|
4369
|
+
if (f.fallback !== void 0) perLabel[f.exposed_as] = f.fallback;
|
|
4370
|
+
}
|
|
4371
|
+
if (Object.keys(perLabel).length > 0) out[typeName] = perLabel;
|
|
4372
|
+
}
|
|
4373
|
+
return out;
|
|
4374
|
+
}
|
|
4375
|
+
function buildVersionSurface(input) {
|
|
4376
|
+
const { version, projectionVersion, prefix, registry, model, makeRepo } = input;
|
|
4377
|
+
const paths = createVersionedPaths(prefix, version);
|
|
4378
|
+
const projection = model.projections[projectionVersion];
|
|
4379
|
+
const fieldKeyMaps = {};
|
|
4380
|
+
const sources = [
|
|
4381
|
+
registry.content_types,
|
|
4382
|
+
registry.taxonomy_types,
|
|
4383
|
+
registry.paragraph_types
|
|
4384
|
+
];
|
|
4385
|
+
for (const source of sources) {
|
|
4386
|
+
for (const [typeName, type] of Object.entries(source)) {
|
|
4387
|
+
const projectionType = projection?.types[typeName];
|
|
4388
|
+
fieldKeyMaps[typeName] = projectionType ? createFieldKeyMapFromProjection(projectionType, type.fields) : createFieldKeyMap(type.fields);
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
const fallbacks = projection ? fallbacksFor(projection) : void 0;
|
|
4392
|
+
const projectors = buildProjectors(registry, fieldKeyMaps, fallbacks);
|
|
4393
|
+
const sortableColumnsFor = (typeName) => {
|
|
4394
|
+
const map = fieldKeyMaps[typeName];
|
|
4395
|
+
const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName];
|
|
4396
|
+
const systemFieldNames = new Set((type?.system_fields ?? []).map((f) => f.name));
|
|
4397
|
+
const out = /* @__PURE__ */ new Set();
|
|
4398
|
+
for (const label of SORTABLE_FIELDS) {
|
|
4399
|
+
const column = map.columnFor(label);
|
|
4400
|
+
if (column !== void 0) out.add(column);
|
|
4401
|
+
else if (systemFieldNames.has(label)) out.add(label);
|
|
4402
|
+
}
|
|
4403
|
+
return out;
|
|
4404
|
+
};
|
|
4405
|
+
const repos = {};
|
|
4406
|
+
for (const [typeName, ct] of Object.entries(registry.content_types)) {
|
|
4407
|
+
repos[typeName] = makeRepo(typeName, ct.db.table_name, sortableColumnsFor(typeName));
|
|
4408
|
+
}
|
|
4409
|
+
for (const [typeName, tt] of Object.entries(registry.taxonomy_types)) {
|
|
4410
|
+
repos[typeName] = makeRepo(typeName, tt.db.table_name, sortableColumnsFor(typeName));
|
|
4411
|
+
}
|
|
4412
|
+
return { paths, fieldKeyMaps, projectors, repos };
|
|
4413
|
+
}
|
|
4414
|
+
function deprecationHeaders(input) {
|
|
4415
|
+
const { requested, model, successor } = input;
|
|
4416
|
+
const link = `<${successor}>; rel="successor-version"`;
|
|
4417
|
+
if (requested === null) {
|
|
4418
|
+
if (model.live.length <= 1) return null;
|
|
4419
|
+
return {
|
|
4420
|
+
Deprecation: "true",
|
|
4421
|
+
Link: link,
|
|
4422
|
+
Warning: `299 - "Unversioned path resolves to the latest version (${model.current}) and will change when a new version is cut. Pin a version."`
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
if (requested === model.current) return null;
|
|
4426
|
+
return { Deprecation: "true", Link: link };
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
// src/app.ts
|
|
4430
|
+
function versionRoutePaths(registry, paths) {
|
|
4431
|
+
const out = [paths.collection("content"), paths.collection("taxonomy")];
|
|
4432
|
+
for (const ct of Object.values(registry.content_types)) {
|
|
4433
|
+
out.push(paths.collection(ct.default_base_path));
|
|
4434
|
+
if (!ct.only_one) out.push(paths.item(ct.default_base_path));
|
|
4435
|
+
}
|
|
4436
|
+
for (const tt of Object.values(registry.taxonomy_types)) {
|
|
4437
|
+
out.push(paths.taxonomyCollection(tt.name), paths.taxonomyItem(tt.name));
|
|
4438
|
+
}
|
|
4439
|
+
return out;
|
|
4440
|
+
}
|
|
4169
4441
|
var MISSING_STORAGE_ERROR = `\u2717 api.storage is required but not configured.
|
|
4170
4442
|
Add a storage adapter to your manguito.config.ts:
|
|
4171
4443
|
|
|
@@ -4181,6 +4453,7 @@ function createCmsApp(options) {
|
|
|
4181
4453
|
}
|
|
4182
4454
|
const prefix = normalizePrefix(options.prefix);
|
|
4183
4455
|
const publicPaths = createPublicPaths(prefix);
|
|
4456
|
+
const versionedPaths = createVersionedPaths(prefix, null);
|
|
4184
4457
|
const { storage, registry, db, rateLimit, media, cors } = options;
|
|
4185
4458
|
const cmsName = options.name ?? "Manguito CMS";
|
|
4186
4459
|
const maxFileSize = media?.max_file_size;
|
|
@@ -4229,9 +4502,18 @@ function createCmsApp(options) {
|
|
|
4229
4502
|
])
|
|
4230
4503
|
]);
|
|
4231
4504
|
const projectors = buildProjectors(registry, fieldKeyMaps);
|
|
4232
|
-
const sortableColumnsFor = (typeName) =>
|
|
4233
|
-
|
|
4234
|
-
|
|
4505
|
+
const sortableColumnsFor = (typeName) => {
|
|
4506
|
+
const map = fieldKeyMaps[typeName];
|
|
4507
|
+
const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName];
|
|
4508
|
+
const systemFieldNames = new Set((type?.system_fields ?? []).map((f) => f.name));
|
|
4509
|
+
const out = /* @__PURE__ */ new Set();
|
|
4510
|
+
for (const label of SORTABLE_FIELDS) {
|
|
4511
|
+
const column = map.columnFor(label);
|
|
4512
|
+
if (column !== void 0) out.add(column);
|
|
4513
|
+
else if (systemFieldNames.has(label)) out.add(label);
|
|
4514
|
+
}
|
|
4515
|
+
return out;
|
|
4516
|
+
};
|
|
4235
4517
|
const contentRepos = Object.fromEntries(
|
|
4236
4518
|
Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
4237
4519
|
typeName,
|
|
@@ -4250,41 +4532,14 @@ function createCmsApp(options) {
|
|
|
4250
4532
|
);
|
|
4251
4533
|
const repos = { ...contentRepos, ...taxonomyRepos };
|
|
4252
4534
|
const mediaRepo = createMediaRepository(db);
|
|
4253
|
-
const
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
])
|
|
4262
|
-
);
|
|
4263
|
-
const publicTaxonomyRepos = Object.fromEntries(
|
|
4264
|
-
Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
4265
|
-
typeName,
|
|
4266
|
-
createDrizzleContentRepository(db, tt.db.table_name, {
|
|
4267
|
-
relations: buildRelationsMap(tt.fields, registry),
|
|
4268
|
-
publishedRelations: true,
|
|
4269
|
-
sortableColumns: sortableColumnsFor(typeName)
|
|
4270
|
-
})
|
|
4271
|
-
])
|
|
4272
|
-
);
|
|
4273
|
-
const publicRepos = { ...publicContentRepos, ...publicTaxonomyRepos };
|
|
4274
|
-
const graphqlRepos = Object.fromEntries([
|
|
4275
|
-
...Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
4276
|
-
typeName,
|
|
4277
|
-
createDrizzleContentRepository(db, ct.db.table_name, {
|
|
4278
|
-
sortableColumns: sortableColumnsFor(typeName)
|
|
4279
|
-
})
|
|
4280
|
-
]),
|
|
4281
|
-
...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
4282
|
-
typeName,
|
|
4283
|
-
createDrizzleContentRepository(db, tt.db.table_name, {
|
|
4284
|
-
sortableColumns: sortableColumnsFor(typeName)
|
|
4285
|
-
})
|
|
4286
|
-
])
|
|
4287
|
-
]);
|
|
4535
|
+
const makeRepo = (typeName, tableName, sortableColumns) => {
|
|
4536
|
+
const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName];
|
|
4537
|
+
return createDrizzleContentRepository(db, tableName, {
|
|
4538
|
+
relations: buildRelationsMap(type.fields, registry),
|
|
4539
|
+
publishedRelations: true,
|
|
4540
|
+
sortableColumns
|
|
4541
|
+
});
|
|
4542
|
+
};
|
|
4288
4543
|
registerAuthRoutes(app, db);
|
|
4289
4544
|
app.use("/admin/api/*", authMiddleware);
|
|
4290
4545
|
app.use("/admin/api/*", mustChangePasswordCheck);
|
|
@@ -4293,17 +4548,17 @@ function createCmsApp(options) {
|
|
|
4293
4548
|
for (const ct of Object.values(registry.content_types)) {
|
|
4294
4549
|
const base = ct.default_base_path;
|
|
4295
4550
|
if (ct.only_one) {
|
|
4296
|
-
paths[
|
|
4551
|
+
paths[versionedPaths.collection(base)] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
|
|
4297
4552
|
} else {
|
|
4298
|
-
paths[
|
|
4299
|
-
paths[
|
|
4553
|
+
paths[versionedPaths.collection(base)] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
|
|
4554
|
+
paths[versionedPaths.item(base).replace(":slug", "{slug}")] = {
|
|
4300
4555
|
get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] }
|
|
4301
4556
|
};
|
|
4302
4557
|
}
|
|
4303
4558
|
}
|
|
4304
4559
|
for (const tt of Object.values(registry.taxonomy_types)) {
|
|
4305
|
-
paths[
|
|
4306
|
-
paths[
|
|
4560
|
+
paths[versionedPaths.taxonomyCollection(tt.name)] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
|
|
4561
|
+
paths[versionedPaths.taxonomyItem(tt.name).replace(":id", "{id}")] = {
|
|
4307
4562
|
get: { summary: `Get ${tt.label} by id`, tags: [tt.label] }
|
|
4308
4563
|
};
|
|
4309
4564
|
}
|
|
@@ -4323,35 +4578,166 @@ function createCmsApp(options) {
|
|
|
4323
4578
|
paths: {}
|
|
4324
4579
|
})
|
|
4325
4580
|
);
|
|
4326
|
-
|
|
4581
|
+
const model = options.versions ?? {
|
|
4582
|
+
current: "v1",
|
|
4583
|
+
live: ["v1"],
|
|
4584
|
+
projections: {}
|
|
4585
|
+
};
|
|
4586
|
+
const passes = [
|
|
4587
|
+
...options.versions !== void 0 ? model.live.map((v) => ({ version: v, projectionVersion: v })) : [],
|
|
4588
|
+
{ version: null, projectionVersion: model.current }
|
|
4589
|
+
];
|
|
4590
|
+
for (const pass of passes) {
|
|
4591
|
+
const surface = buildVersionSurface({ ...pass, prefix, registry, model, makeRepo });
|
|
4592
|
+
const headers = deprecationHeaders({
|
|
4593
|
+
requested: pass.version,
|
|
4594
|
+
model,
|
|
4595
|
+
// The version root, with no trailing slash — never
|
|
4596
|
+
// createVersionedPaths(prefix, model.current).collection(''), which
|
|
4597
|
+
// would yield `${prefix}/${model.current}/` in every Link header.
|
|
4598
|
+
successor: `${prefix}/${model.current}`
|
|
4599
|
+
});
|
|
4600
|
+
if (headers !== null) {
|
|
4601
|
+
const attachDeprecationHeaders = async (c, next) => {
|
|
4602
|
+
await next();
|
|
4603
|
+
for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
|
|
4604
|
+
};
|
|
4605
|
+
for (const path of versionRoutePaths(registry, surface.paths)) {
|
|
4606
|
+
app.use(path, attachDeprecationHeaders);
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
|
|
4610
|
+
}
|
|
4611
|
+
if (options.versions !== void 0) {
|
|
4612
|
+
app.all(`${prefix}/:version/*`, async (c, next) => {
|
|
4613
|
+
const kind = classifyVersion(c.req.param("version"), model);
|
|
4614
|
+
if (kind === "not-a-version" || kind === "live") return next();
|
|
4615
|
+
const live = model.live.join(", ");
|
|
4616
|
+
return kind === "retired" ? c.json(
|
|
4617
|
+
{
|
|
4618
|
+
ok: false,
|
|
4619
|
+
error: {
|
|
4620
|
+
code: "VERSION_RETIRED",
|
|
4621
|
+
message: `Version ${c.req.param("version")} is no longer served. Live versions: ${live}.`
|
|
4622
|
+
}
|
|
4623
|
+
},
|
|
4624
|
+
410
|
|
4625
|
+
) : c.json(
|
|
4626
|
+
{
|
|
4627
|
+
ok: false,
|
|
4628
|
+
error: {
|
|
4629
|
+
code: "VERSION_NOT_FOUND",
|
|
4630
|
+
message: `No version ${c.req.param("version")} is served. Live versions: ${live}.`
|
|
4631
|
+
}
|
|
4632
|
+
},
|
|
4633
|
+
404
|
|
4634
|
+
);
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4327
4637
|
registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
|
|
4328
4638
|
if (options.graphql?.enabled) {
|
|
4329
4639
|
const gqlOptions = options.graphql;
|
|
4330
|
-
|
|
4331
|
-
|
|
4640
|
+
const makeGraphqlRepo = (_typeName, tableName, sortableColumns) => createDrizzleContentRepository(db, tableName, { sortableColumns });
|
|
4641
|
+
const gqlPasses = [
|
|
4642
|
+
...options.versions !== void 0 ? model.live.map((v) => ({ version: v, projectionVersion: v })) : [],
|
|
4643
|
+
{ version: null, projectionVersion: model.current }
|
|
4644
|
+
];
|
|
4645
|
+
const mounts = gqlPasses.map((pass) => ({
|
|
4646
|
+
path: pass.version === null ? "/graphql" : `/graphql/${pass.version}`,
|
|
4647
|
+
version: pass.version,
|
|
4648
|
+
handler: null,
|
|
4649
|
+
error: null
|
|
4650
|
+
}));
|
|
4332
4651
|
const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
|
|
4333
|
-
|
|
4652
|
+
gqlPasses.forEach((pass, i) => {
|
|
4653
|
+
const mount = mounts[i];
|
|
4654
|
+
try {
|
|
4655
|
+
const surface = buildVersionSurface({
|
|
4656
|
+
...pass,
|
|
4657
|
+
prefix,
|
|
4658
|
+
registry,
|
|
4659
|
+
model,
|
|
4660
|
+
makeRepo: makeGraphqlRepo
|
|
4661
|
+
});
|
|
4662
|
+
const projection = model.projections[pass.projectionVersion];
|
|
4663
|
+
const currentProjection = model.projections[model.current];
|
|
4664
|
+
mount.handler = createGraphQLHandler2(
|
|
4665
|
+
registry,
|
|
4666
|
+
surface.repos,
|
|
4667
|
+
surface.fieldKeyMaps,
|
|
4668
|
+
programmaticResolver,
|
|
4669
|
+
db,
|
|
4670
|
+
gqlOptions,
|
|
4671
|
+
{
|
|
4672
|
+
endpoint: mount.path,
|
|
4673
|
+
currentVersion: model.current,
|
|
4674
|
+
...projection !== void 0 && { projection },
|
|
4675
|
+
...currentProjection !== void 0 && { currentProjection }
|
|
4676
|
+
}
|
|
4677
|
+
);
|
|
4678
|
+
} catch (err) {
|
|
4679
|
+
mount.error = err;
|
|
4680
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4681
|
+
process.stderr.write(
|
|
4682
|
+
`\u2717 GraphQL schema failed to initialize for ${mount.path}; it will return 500. ${message}
|
|
4683
|
+
`
|
|
4684
|
+
);
|
|
4685
|
+
}
|
|
4686
|
+
});
|
|
4334
4687
|
}).catch((err) => {
|
|
4335
|
-
|
|
4688
|
+
for (const m of mounts) m.error = err;
|
|
4336
4689
|
const message = err instanceof Error ? err.message : String(err);
|
|
4337
|
-
|
|
4690
|
+
const paths = mounts.map((m) => m.path).join(", ");
|
|
4691
|
+
process.stderr.write(`\u2717 GraphQL module failed to load; ${paths} will return 500. ${message}
|
|
4338
4692
|
`);
|
|
4339
4693
|
});
|
|
4340
|
-
const
|
|
4341
|
-
if (!
|
|
4342
|
-
if (!
|
|
4694
|
+
const invokeFor = (mount) => async (c) => {
|
|
4695
|
+
if (!mount.handler && !mount.error) await ready;
|
|
4696
|
+
if (!mount.handler) {
|
|
4343
4697
|
return c.json(
|
|
4344
4698
|
{ ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
|
|
4345
4699
|
500
|
|
4346
4700
|
);
|
|
4347
4701
|
}
|
|
4348
|
-
return
|
|
4702
|
+
return mount.handler(c, async () => {
|
|
4349
4703
|
});
|
|
4350
4704
|
};
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4705
|
+
for (const mount of mounts) {
|
|
4706
|
+
const headers = deprecationHeaders({
|
|
4707
|
+
requested: mount.version,
|
|
4708
|
+
model,
|
|
4709
|
+
successor: `/graphql/${model.current}`
|
|
4710
|
+
});
|
|
4711
|
+
if (headers === null) continue;
|
|
4712
|
+
const attach = async (c, next) => {
|
|
4713
|
+
await next();
|
|
4714
|
+
for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
|
|
4715
|
+
};
|
|
4716
|
+
app.use(mount.path, attach);
|
|
4717
|
+
}
|
|
4718
|
+
for (const mount of mounts) {
|
|
4719
|
+
if (listRateLimit) app.all(mount.path, listRateLimit, invokeFor(mount));
|
|
4720
|
+
else app.all(mount.path, invokeFor(mount));
|
|
4721
|
+
}
|
|
4722
|
+
if (options.versions !== void 0) {
|
|
4723
|
+
app.all("/graphql/:version", async (c, next) => {
|
|
4724
|
+
const segment = c.req.param("version");
|
|
4725
|
+
const kind = classifyVersion(segment, model);
|
|
4726
|
+
if (kind === "not-a-version" || kind === "live") return next();
|
|
4727
|
+
const live = model.live.join(", ");
|
|
4728
|
+
return c.json({
|
|
4729
|
+
errors: [
|
|
4730
|
+
{
|
|
4731
|
+
message: kind === "retired" ? `Version ${segment} is no longer served. Live versions: ${live}.` : `Version ${segment} does not exist. Live versions: ${live}.`,
|
|
4732
|
+
extensions: {
|
|
4733
|
+
code: kind === "retired" ? "VERSION_RETIRED" : "VERSION_UNKNOWN",
|
|
4734
|
+
current: model.current,
|
|
4735
|
+
live: model.live
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
]
|
|
4739
|
+
});
|
|
4740
|
+
});
|
|
4355
4741
|
}
|
|
4356
4742
|
}
|
|
4357
4743
|
registerConfigRoute(
|