canopycms 0.4.0 → 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/CHANGELOG.md +78 -42
- package/dist/ai/generate.d.ts +3 -0
- package/dist/ai/generate.js +12 -29
- package/dist/ai/handler.js +1 -0
- package/dist/ai/types.d.ts +3 -9
- package/dist/api/reference-options.d.ts +2 -5
- package/dist/api/reference-options.js +21 -2
- package/dist/api/schema.d.ts +4 -4
- package/dist/build/generate-ai-content.js +1 -0
- package/dist/cli/cli.js +136 -39
- package/dist/cli/generate-ai-content.js +75 -32
- package/dist/cli/init.js +34 -5
- package/dist/cli/template-files/cms-stack.ts.template +3 -0
- package/dist/cli/templates.js +5 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +74 -0
- package/dist/config/schemas/config.js +21 -0
- package/dist/config/schemas/field.d.ts +30 -0
- package/dist/config/schemas/field.js +22 -0
- package/dist/config/types.d.ts +39 -0
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +33 -0
- package/dist/editor/FormRenderer.js +15 -13
- package/dist/editor/copy.d.ts +5 -0
- package/dist/editor/copy.js +5 -0
- package/dist/editor/fields/BlockField.d.ts +2 -0
- package/dist/editor/fields/BlockField.js +15 -8
- package/dist/editor/fields/ItemList.d.ts +21 -3
- package/dist/editor/fields/ItemList.js +16 -8
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +6 -2
- package/dist/editor/fields/ObjectListField.js +9 -3
- package/dist/editor/fields/ReferenceField.d.ts +6 -7
- package/dist/editor/fields/ReferenceField.js +28 -122
- package/dist/editor/fields/ReferenceListField.d.ts +41 -0
- package/dist/editor/fields/ReferenceListField.js +139 -0
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +6 -3
- package/dist/editor/fields/StringListField.d.ts +10 -1
- package/dist/editor/fields/StringListField.js +18 -8
- package/dist/editor/fields/item-expansion.d.ts +7 -3
- package/dist/editor/fields/item-expansion.js +6 -4
- package/dist/editor/fields/list-item-keys.d.ts +6 -5
- package/dist/editor/fields/list-item-keys.js +45 -13
- package/dist/editor/fields/reference-noindex.d.ts +37 -0
- package/dist/editor/fields/reference-noindex.js +48 -0
- package/dist/editor/fields/use-reference-options.d.ts +36 -0
- package/dist/editor/fields/use-reference-options.js +121 -0
- package/dist/editor/form-outline.d.ts +2 -2
- package/dist/editor/form-outline.js +4 -2
- package/dist/editor/theme.js +5 -0
- package/dist/entry-schema-registry.d.ts +1 -1
- package/dist/entry-schema-registry.js +3 -2
- package/dist/entry-schema.d.ts +5 -3
- package/dist/entry-schema.js +2 -1
- package/dist/reference-resolver.d.ts +18 -1
- package/dist/reference-resolver.js +175 -8
- package/dist/schema/schema-store.d.ts +2 -2
- package/dist/static/index.d.ts +8 -4
- package/dist/static/index.js +38 -13
- package/dist/static/seo.d.ts +7 -5
- package/dist/static/seo.js +13 -6
- package/dist/validation/entry-validator.d.ts +5 -0
- package/dist/validation/entry-validator.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/github-gateway-http.js +2 -2
- package/dist/worker/github-gateway.js +2 -2
- package/package.json +1 -1
|
@@ -599,7 +599,7 @@ var init_mdx_allowlist = __esm({
|
|
|
599
599
|
|
|
600
600
|
// dist/config/schemas/field.js
|
|
601
601
|
import { z as z2 } from "zod";
|
|
602
|
-
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
602
|
+
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
603
603
|
var init_field = __esm({
|
|
604
604
|
"dist/config/schemas/field.js"() {
|
|
605
605
|
"use strict";
|
|
@@ -612,7 +612,10 @@ var init_field = __esm({
|
|
|
612
612
|
required: z2.boolean().optional(),
|
|
613
613
|
list: z2.boolean().optional(),
|
|
614
614
|
isTitle: z2.boolean().optional(),
|
|
615
|
-
isBody: z2.boolean().optional()
|
|
615
|
+
isBody: z2.boolean().optional(),
|
|
616
|
+
// Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
|
|
617
|
+
// `ensureMaxItemsPlacement`'s call (config/validation.ts).
|
|
618
|
+
maxItems: z2.number().int().positive().optional()
|
|
616
619
|
});
|
|
617
620
|
selectOptionSchema = z2.union([
|
|
618
621
|
z2.string(),
|
|
@@ -670,6 +673,7 @@ var init_field = __esm({
|
|
|
670
673
|
type: z2.literal("select"),
|
|
671
674
|
options: z2.array(selectOptionSchema).min(1)
|
|
672
675
|
});
|
|
676
|
+
referenceSortFieldSchema = z2.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
|
|
673
677
|
referenceFieldSchema = fieldBaseSchema.extend({
|
|
674
678
|
type: z2.literal("reference"),
|
|
675
679
|
collections: z2.array(z2.string().min(1)).min(1).optional(),
|
|
@@ -680,7 +684,14 @@ var init_field = __esm({
|
|
|
680
684
|
// default, so a consumer that adopts this schema's parse output would silently delete a
|
|
681
685
|
// runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
|
|
682
686
|
// absent on purpose — it is type-inference-only and stripping it changes no behavior.)
|
|
683
|
-
includeBody: z2.boolean().optional()
|
|
687
|
+
includeBody: z2.boolean().optional(),
|
|
688
|
+
optionSort: z2.union([
|
|
689
|
+
z2.enum(["label", "collection"]),
|
|
690
|
+
z2.object({
|
|
691
|
+
field: referenceSortFieldSchema,
|
|
692
|
+
direction: z2.enum(["asc", "desc"]).optional()
|
|
693
|
+
}).strict()
|
|
694
|
+
]).optional()
|
|
684
695
|
});
|
|
685
696
|
fieldHolder = [z2.never()];
|
|
686
697
|
blockSchema = z2.object({
|
|
@@ -828,7 +839,7 @@ var init_media = __esm({
|
|
|
828
839
|
|
|
829
840
|
// dist/config/schemas/config.js
|
|
830
841
|
import { z as z6 } from "zod";
|
|
831
|
-
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
842
|
+
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, seoFieldNameSchema, seoSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
832
843
|
var init_config = __esm({
|
|
833
844
|
"dist/config/schemas/config.js"() {
|
|
834
845
|
"use strict";
|
|
@@ -875,6 +886,19 @@ var init_config = __esm({
|
|
|
875
886
|
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
876
887
|
SignInComponent: z6.custom().optional()
|
|
877
888
|
});
|
|
889
|
+
seoFieldNameSchema = z6.string().min(1).optional();
|
|
890
|
+
seoSchema = z6.object({
|
|
891
|
+
group: z6.string().min(1).optional(),
|
|
892
|
+
fields: z6.object({
|
|
893
|
+
title: seoFieldNameSchema,
|
|
894
|
+
description: seoFieldNameSchema,
|
|
895
|
+
ogImage: seoFieldNameSchema,
|
|
896
|
+
ogType: seoFieldNameSchema,
|
|
897
|
+
canonical: seoFieldNameSchema,
|
|
898
|
+
noindex: seoFieldNameSchema,
|
|
899
|
+
twitterCard: seoFieldNameSchema
|
|
900
|
+
}).strict().optional()
|
|
901
|
+
}).strict();
|
|
878
902
|
CanopyConfigSchema = z6.object({
|
|
879
903
|
media: mediaSchema.optional(),
|
|
880
904
|
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
@@ -924,6 +948,7 @@ var init_config = __esm({
|
|
|
924
948
|
entryLinkUrl: z6.custom().optional(),
|
|
925
949
|
validateEntry: z6.custom().optional(),
|
|
926
950
|
mdxAllow: mdxAllowlistSchema.optional(),
|
|
951
|
+
seo: seoSchema.optional(),
|
|
927
952
|
dev: devConfigSchema.optional()
|
|
928
953
|
}).strict();
|
|
929
954
|
DEFAULT_PROD_WORKSPACE = "/mnt/efs";
|
|
@@ -2980,7 +3005,11 @@ var asTwitterCard = (value) => {
|
|
|
2980
3005
|
return text === "summary" || text === "summary_large_image" ? text : void 0;
|
|
2981
3006
|
};
|
|
2982
3007
|
function extractSeoFields(entryData, opts = {}) {
|
|
2983
|
-
const names = { ...DEFAULT_SEO_FIELD_NAMES
|
|
3008
|
+
const names = { ...DEFAULT_SEO_FIELD_NAMES };
|
|
3009
|
+
for (const [key, name] of Object.entries(opts.fields ?? {})) {
|
|
3010
|
+
if (typeof name === "string" && name !== "")
|
|
3011
|
+
names[key] = name;
|
|
3012
|
+
}
|
|
2984
3013
|
const entry = asRecord2(entryData);
|
|
2985
3014
|
const source = opts.group ? asRecord2(entry?.[opts.group]) : entry;
|
|
2986
3015
|
return {
|
|
@@ -6723,7 +6752,7 @@ function createReferenceRendering(idIndex, flatSchema, entryLinkUrl, markdownCop
|
|
|
6723
6752
|
|
|
6724
6753
|
// dist/ai/generate.js
|
|
6725
6754
|
async function generateAIContent(options) {
|
|
6726
|
-
const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
|
|
6755
|
+
const { store, flatSchema, contentRoot, config, entryLinkUrl, seo, generatedAt, buildId } = options;
|
|
6727
6756
|
assertValidNoindexConfig(config);
|
|
6728
6757
|
assertValidReferenceLink(config);
|
|
6729
6758
|
const files = /* @__PURE__ */ new Map();
|
|
@@ -6734,6 +6763,7 @@ async function generateAIContent(options) {
|
|
|
6734
6763
|
config,
|
|
6735
6764
|
idIndex,
|
|
6736
6765
|
entryLinkUrl,
|
|
6766
|
+
seo,
|
|
6737
6767
|
resolveTarget: createReferenceTargetResolver(store)
|
|
6738
6768
|
};
|
|
6739
6769
|
const collections = flatSchema.filter((item) => item.type === "collection");
|
|
@@ -6885,7 +6915,7 @@ async function collectCollection(read, collection, flatSchema) {
|
|
|
6885
6915
|
return { collection, cleanPath, entries, subcollections };
|
|
6886
6916
|
}
|
|
6887
6917
|
async function collectEntries(read, collection, cleanPath) {
|
|
6888
|
-
const { store, contentRoot, config, idIndex, entryLinkUrl, resolveTarget } = read;
|
|
6918
|
+
const { store, contentRoot, config, idIndex, entryLinkUrl, seo, resolveTarget } = read;
|
|
6889
6919
|
const skipNoindex = config?.exclude?.noindex !== false;
|
|
6890
6920
|
const pending = [];
|
|
6891
6921
|
const listed = await store.getCollectionEntryPaths(collection.logicalPath);
|
|
@@ -6903,7 +6933,7 @@ async function collectEntries(read, collection, cleanPath) {
|
|
|
6903
6933
|
const doc = await store.read(listEntry.collection, listEntry.slug, {
|
|
6904
6934
|
resolveReferences: false
|
|
6905
6935
|
});
|
|
6906
|
-
if (skipNoindex && isNoindexEntry(doc.data,
|
|
6936
|
+
if (skipNoindex && isNoindexEntry(doc.data, seo))
|
|
6907
6937
|
continue;
|
|
6908
6938
|
const storedData = doc.data;
|
|
6909
6939
|
doc.data = await resolveReferenceFields(doc.data, entryTypeConfig.schema, resolveTarget);
|
|
@@ -6970,24 +7000,8 @@ function assertValidNoindexConfig(config) {
|
|
|
6970
7000
|
if (noindex !== void 0 && typeof noindex !== "boolean") {
|
|
6971
7001
|
throw new Error(`AI content config: exclude.noindex must be a boolean, got ${typeof noindex}`);
|
|
6972
7002
|
}
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
return;
|
|
6976
|
-
if (typeof seo !== "object" || seo === null || Array.isArray(seo)) {
|
|
6977
|
-
throw new Error("AI content config: seo must be an object of { group?, fields? }");
|
|
6978
|
-
}
|
|
6979
|
-
const { group, fields } = seo;
|
|
6980
|
-
if (group !== void 0 && typeof group !== "string") {
|
|
6981
|
-
throw new Error("AI content config: seo.group must be a string");
|
|
6982
|
-
}
|
|
6983
|
-
if (fields === void 0)
|
|
6984
|
-
return;
|
|
6985
|
-
if (typeof fields !== "object" || fields === null || Array.isArray(fields) || Object.values(fields).some((name) => typeof name !== "string")) {
|
|
6986
|
-
throw new Error("AI content config: seo.fields must map SEO fields to field-name strings");
|
|
6987
|
-
}
|
|
6988
|
-
const unknown = Object.keys(fields).filter((key) => !(key in DEFAULT_SEO_FIELD_NAMES));
|
|
6989
|
-
if (unknown.length > 0) {
|
|
6990
|
-
throw new Error(`AI content config: unknown seo.fields key(s): ${unknown.join(", ")}`);
|
|
7003
|
+
if (config && "seo" in config) {
|
|
7004
|
+
throw new Error("AI content config: seo is not an AI content option. Set it once as seo in defineCanopyConfig, where the sitemap, page metadata and AI content all read it.");
|
|
6991
7005
|
}
|
|
6992
7006
|
}
|
|
6993
7007
|
function stripContentRoot(logicalPath, contentRoot) {
|
|
@@ -10138,6 +10152,14 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
|
|
|
10138
10152
|
}
|
|
10139
10153
|
if (value === void 0 || value === null)
|
|
10140
10154
|
continue;
|
|
10155
|
+
if (Array.isArray(value) && "maxItems" in field && typeof field.maxItems === "number" && value.length > field.maxItems) {
|
|
10156
|
+
const cap = field.maxItems;
|
|
10157
|
+
errors.push({
|
|
10158
|
+
fieldPath: path27,
|
|
10159
|
+
message: `At most ${cap} ${cap === 1 ? "item" : "items"}; this list has ${value.length}`,
|
|
10160
|
+
maxItems: { count: value.length, cap }
|
|
10161
|
+
});
|
|
10162
|
+
}
|
|
10141
10163
|
if (field.type === "block") {
|
|
10142
10164
|
const blockField = field;
|
|
10143
10165
|
if (!Array.isArray(value)) {
|
|
@@ -10238,14 +10260,34 @@ function assertBuildEntriesValid(items, phaseLabel) {
|
|
|
10238
10260
|
const invalid = findInvalidEntries(items);
|
|
10239
10261
|
if (invalid.length === 0)
|
|
10240
10262
|
return;
|
|
10241
|
-
const lines =
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10263
|
+
const lines = [];
|
|
10264
|
+
let hasOverCap = false;
|
|
10265
|
+
let hasOrdinary = false;
|
|
10266
|
+
for (const { entryPath, errors } of invalid) {
|
|
10267
|
+
const ordinary = errors.filter((e) => e.maxItems === void 0);
|
|
10268
|
+
if (ordinary.length > 0) {
|
|
10269
|
+
hasOrdinary = true;
|
|
10270
|
+
const [first, ...rest] = ordinary;
|
|
10271
|
+
const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
|
|
10272
|
+
lines.push(` - ${entryPath} \u2014 ${summary}`);
|
|
10273
|
+
}
|
|
10274
|
+
for (const { fieldPath, maxItems } of errors) {
|
|
10275
|
+
if (maxItems === void 0)
|
|
10276
|
+
continue;
|
|
10277
|
+
hasOverCap = true;
|
|
10278
|
+
lines.push(` - ${entryPath} \u2014 ${fieldPath}: ${maxItems.count} items, but maxItems is ${maxItems.cap}`);
|
|
10279
|
+
}
|
|
10280
|
+
}
|
|
10281
|
+
const advice = [];
|
|
10282
|
+
if (hasOverCap) {
|
|
10283
|
+
advice.push(`A list over its maxItems: open the entry in the editor and remove a pick from that list (or remove items) until it holds at most maxItems, then rebuild. This usually happens when two branches each add one item to the same list and are merged one after the other.`);
|
|
10284
|
+
}
|
|
10285
|
+
if (hasOrdinary) {
|
|
10286
|
+
advice.push(`${hasOverCap ? "The other errors are" : "These are"} likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
|
|
10287
|
+
}
|
|
10246
10288
|
throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? "entry" : "entries"} during ${phaseLabel}:
|
|
10247
10289
|
${lines.join("\n")}
|
|
10248
|
-
|
|
10290
|
+
` + advice.join("\n"));
|
|
10249
10291
|
}
|
|
10250
10292
|
|
|
10251
10293
|
// dist/build/generate-ai-content.js
|
|
@@ -10342,6 +10384,7 @@ ${cached.issues.map((issue) => ` - ${issue.message}`).join("\n")}`);
|
|
|
10342
10384
|
contentRoot: contentRootName,
|
|
10343
10385
|
config: aiConfig,
|
|
10344
10386
|
entryLinkUrl: config.entryLinkUrl,
|
|
10387
|
+
seo: config.seo,
|
|
10345
10388
|
...resolveBuildStamp()
|
|
10346
10389
|
});
|
|
10347
10390
|
const absoluteOutputDir = path25.resolve(outputDir) + path25.sep;
|
package/dist/cli/init.js
CHANGED
|
@@ -160,7 +160,7 @@ var init_mdx_allowlist = __esm({
|
|
|
160
160
|
|
|
161
161
|
// dist/config/schemas/field.js
|
|
162
162
|
import { z } from "zod";
|
|
163
|
-
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
163
|
+
var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, mdxPropAllowSchema, mdxAllowlistSchema, markdownFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceSortFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
|
|
164
164
|
var init_field = __esm({
|
|
165
165
|
"dist/config/schemas/field.js"() {
|
|
166
166
|
"use strict";
|
|
@@ -173,7 +173,10 @@ var init_field = __esm({
|
|
|
173
173
|
required: z.boolean().optional(),
|
|
174
174
|
list: z.boolean().optional(),
|
|
175
175
|
isTitle: z.boolean().optional(),
|
|
176
|
-
isBody: z.boolean().optional()
|
|
176
|
+
isBody: z.boolean().optional(),
|
|
177
|
+
// Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
|
|
178
|
+
// `ensureMaxItemsPlacement`'s call (config/validation.ts).
|
|
179
|
+
maxItems: z.number().int().positive().optional()
|
|
177
180
|
});
|
|
178
181
|
selectOptionSchema = z.union([
|
|
179
182
|
z.string(),
|
|
@@ -231,6 +234,7 @@ var init_field = __esm({
|
|
|
231
234
|
type: z.literal("select"),
|
|
232
235
|
options: z.array(selectOptionSchema).min(1)
|
|
233
236
|
});
|
|
237
|
+
referenceSortFieldSchema = z.string().regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
|
|
234
238
|
referenceFieldSchema = fieldBaseSchema.extend({
|
|
235
239
|
type: z.literal("reference"),
|
|
236
240
|
collections: z.array(z.string().min(1)).min(1).optional(),
|
|
@@ -241,7 +245,14 @@ var init_field = __esm({
|
|
|
241
245
|
// default, so a consumer that adopts this schema's parse output would silently delete a
|
|
242
246
|
// runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
|
|
243
247
|
// absent on purpose — it is type-inference-only and stripping it changes no behavior.)
|
|
244
|
-
includeBody: z.boolean().optional()
|
|
248
|
+
includeBody: z.boolean().optional(),
|
|
249
|
+
optionSort: z.union([
|
|
250
|
+
z.enum(["label", "collection"]),
|
|
251
|
+
z.object({
|
|
252
|
+
field: referenceSortFieldSchema,
|
|
253
|
+
direction: z.enum(["asc", "desc"]).optional()
|
|
254
|
+
}).strict()
|
|
255
|
+
]).optional()
|
|
245
256
|
});
|
|
246
257
|
fieldHolder = [z.never()];
|
|
247
258
|
blockSchema = z.object({
|
|
@@ -439,7 +450,7 @@ var init_media = __esm({
|
|
|
439
450
|
|
|
440
451
|
// dist/config/schemas/config.js
|
|
441
452
|
import { z as z5 } from "zod";
|
|
442
|
-
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
453
|
+
var defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, basePathSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, seoFieldNameSchema, seoSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
|
|
443
454
|
var init_config = __esm({
|
|
444
455
|
"dist/config/schemas/config.js"() {
|
|
445
456
|
"use strict";
|
|
@@ -486,6 +497,19 @@ var init_config = __esm({
|
|
|
486
497
|
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
487
498
|
SignInComponent: z5.custom().optional()
|
|
488
499
|
});
|
|
500
|
+
seoFieldNameSchema = z5.string().min(1).optional();
|
|
501
|
+
seoSchema = z5.object({
|
|
502
|
+
group: z5.string().min(1).optional(),
|
|
503
|
+
fields: z5.object({
|
|
504
|
+
title: seoFieldNameSchema,
|
|
505
|
+
description: seoFieldNameSchema,
|
|
506
|
+
ogImage: seoFieldNameSchema,
|
|
507
|
+
ogType: seoFieldNameSchema,
|
|
508
|
+
canonical: seoFieldNameSchema,
|
|
509
|
+
noindex: seoFieldNameSchema,
|
|
510
|
+
twitterCard: seoFieldNameSchema
|
|
511
|
+
}).strict().optional()
|
|
512
|
+
}).strict();
|
|
489
513
|
CanopyConfigSchema = z5.object({
|
|
490
514
|
media: mediaSchema.optional(),
|
|
491
515
|
// No outer .optional(): both schemas already .default('deny'), so the field is optional on
|
|
@@ -535,6 +559,7 @@ var init_config = __esm({
|
|
|
535
559
|
entryLinkUrl: z5.custom().optional(),
|
|
536
560
|
validateEntry: z5.custom().optional(),
|
|
537
561
|
mdxAllow: mdxAllowlistSchema.optional(),
|
|
562
|
+
seo: seoSchema.optional(),
|
|
538
563
|
dev: devConfigSchema.optional()
|
|
539
564
|
}).strict();
|
|
540
565
|
DEFAULT_PROD_WORKSPACE = "/mnt/efs";
|
|
@@ -632,9 +657,13 @@ async function canopyContext(options) {
|
|
|
632
657
|
"// authenticated request instead of silently falling back to unauthenticated dev auth.",
|
|
633
658
|
"// CLERK_SECRET_KEY is read only where Clerk's backend API is called: the auth-cache",
|
|
634
659
|
"// refresh (the worker in prod, lazily on the dev server in dev).",
|
|
660
|
+
"// useOrganizationsAsGroups needs Organizations enabled on the Clerk instance and offers",
|
|
661
|
+
"// every organization in the instance as a group. In an AWS deployment the worker reads",
|
|
662
|
+
"// the setting from `clerkOrganizationsAsGroups` in the CDK stack, which governs there;",
|
|
663
|
+
"// keep the two equal.",
|
|
635
664
|
"const authPlugin =",
|
|
636
665
|
" config.server.mode === 'prod' || process.env.CANOPY_AUTH_MODE === 'clerk'",
|
|
637
|
-
" ? createClerkAuthPlugin({ useOrganizationsAsGroups:
|
|
666
|
+
" ? createClerkAuthPlugin({ useOrganizationsAsGroups: false })",
|
|
638
667
|
" : createDevAuthPlugin()"
|
|
639
668
|
].join("\n") : "const authPlugin = createDevAuthPlugin()";
|
|
640
669
|
return template.replace("{{AUTH_IMPORTS}}", authImports).replace("{{AUTH_PLUGIN_SETUP}}", authPluginSetup).replace("{{CONFIG_IMPORT}}", options.configImport);
|
|
@@ -172,6 +172,9 @@ export class CmsStack extends Stack {
|
|
|
172
172
|
].filter((arn): arn is string => arn !== undefined),
|
|
173
173
|
githubTokenSecretArn: githubToken?.secretArn,
|
|
174
174
|
clerkSecretKeySecretArn: clerkSecretKey.secretArn,
|
|
175
|
+
// Whether the worker maps Clerk Organizations onto Canopy groups. It needs Organizations
|
|
176
|
+
// enabled on the Clerk instance; keep it equal to useOrganizationsAsGroups in canopy.ts.
|
|
177
|
+
clerkOrganizationsAsGroups: false,
|
|
175
178
|
// GitHub App auth, when configured instead of the token above. Undefined
|
|
176
179
|
// unless the adopter set all three; CanopyCmsService refuses a partial
|
|
177
180
|
// set, and refuses an App alongside githubTokenSecretArn.
|
package/dist/cli/templates.js
CHANGED
|
@@ -41,9 +41,13 @@ export async function canopyContext(options) {
|
|
|
41
41
|
'// authenticated request instead of silently falling back to unauthenticated dev auth.',
|
|
42
42
|
"// CLERK_SECRET_KEY is read only where Clerk's backend API is called: the auth-cache",
|
|
43
43
|
'// refresh (the worker in prod, lazily on the dev server in dev).',
|
|
44
|
+
'// useOrganizationsAsGroups needs Organizations enabled on the Clerk instance and offers',
|
|
45
|
+
'// every organization in the instance as a group. In an AWS deployment the worker reads',
|
|
46
|
+
'// the setting from `clerkOrganizationsAsGroups` in the CDK stack, which governs there;',
|
|
47
|
+
'// keep the two equal.',
|
|
44
48
|
'const authPlugin =',
|
|
45
49
|
" config.server.mode === 'prod' || process.env.CANOPY_AUTH_MODE === 'clerk'",
|
|
46
|
-
' ? createClerkAuthPlugin({ useOrganizationsAsGroups:
|
|
50
|
+
' ? createClerkAuthPlugin({ useOrganizationsAsGroups: false })',
|
|
47
51
|
' : createDevAuthPlugin()',
|
|
48
52
|
].join('\n')
|
|
49
53
|
: 'const authPlugin = createDevAuthPlugin()';
|
package/dist/config/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import { defineCanopyConfig, type FieldConfig } from 'canopycms'
|
|
9
9
|
* ```
|
|
10
10
|
*/
|
|
11
|
-
export type { PrimitiveFieldType, FieldType, ContentFormat, MediaAdapterKind, PermissionLevel, PermissionTarget, PathPermission, SelectOption, ReferenceOption, FieldConfig, BlockConfig, BlockFieldConfig, SelectFieldConfig, ReferenceFieldConfig, ImageFieldConfig, MarkdownFieldConfig, MdxAllowlist, MdxComponentAllow, MdxPropAllow, ImageFieldValue, ObjectFieldConfig, InlineGroupFieldConfig, CustomFieldConfig, EntrySchema, BranchSchema, EntryTypeConfig, EntryTypeUnavailable, CollectionConfig, RootCollectionConfig, MediaConfig, CanopyEditorConfig, DefaultBranchAccess, DefaultPathAccess, DefaultPathAccessLevel, DefaultPathAccessLevels, DefaultBaseBranch, DefaultRemoteName, DefaultRemoteUrl, GitBotAuthorName, GitBotAuthorEmail, GithubTokenEnvVar, CanopyOperatingMode, ContentRoot, SourceRoot, CanopyConfig, CanopyConfigInput, CanopyConfigFragment, FlatSchemaItem, CanopyClientConfig, ClientOnlyFields, EditorSignInProps, EntryValidationIssue, ValidateEntryInput, ValidateEntryHook, } from './types.js';
|
|
11
|
+
export type { PrimitiveFieldType, FieldType, ContentFormat, MediaAdapterKind, PermissionLevel, PermissionTarget, PathPermission, SelectOption, ReferenceOption, FieldConfig, BlockConfig, BlockFieldConfig, SelectFieldConfig, ReferenceFieldConfig, ReferenceOptionSort, ImageFieldConfig, MarkdownFieldConfig, MdxAllowlist, MdxComponentAllow, MdxPropAllow, ImageFieldValue, ObjectFieldConfig, InlineGroupFieldConfig, CustomFieldConfig, EntrySchema, BranchSchema, EntryTypeConfig, EntryTypeUnavailable, CollectionConfig, RootCollectionConfig, MediaConfig, CanopyEditorConfig, DefaultBranchAccess, DefaultPathAccess, DefaultPathAccessLevel, DefaultPathAccessLevels, DefaultBaseBranch, DefaultRemoteName, DefaultRemoteUrl, GitBotAuthorName, GitBotAuthorEmail, GithubTokenEnvVar, CanopyOperatingMode, ContentRoot, SourceRoot, CanopyConfig, CanopyConfigInput, CanopyConfigFragment, FlatSchemaItem, CanopyClientConfig, ClientOnlyFields, EditorSignInProps, EntryValidationIssue, ValidateEntryInput, ValidateEntryHook, } from './types.js';
|
|
12
12
|
export { primitiveFieldTypes, fieldTypes } from './types.js';
|
|
13
13
|
export { CanopyConfigSchema, getConfigDefaults, DEFAULT_PROD_WORKSPACE } from './schemas/config.js';
|
|
14
14
|
export { fieldSchema, blockSchema, selectOptionSchema, referenceOptionSchema, imageFieldSchema, } from './schemas/field.js';
|
|
@@ -105,6 +105,56 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
105
105
|
entryLinkUrl: z.ZodOptional<z.ZodType<EntryLinkUrlResolver, z.ZodTypeDef, EntryLinkUrlResolver>>;
|
|
106
106
|
validateEntry: z.ZodOptional<z.ZodType<ValidateEntryHook, z.ZodTypeDef, ValidateEntryHook>>;
|
|
107
107
|
mdxAllow: z.ZodOptional<z.ZodType<import("../index.js").MdxAllowlist, z.ZodTypeDef, import("../index.js").MdxAllowlist>>;
|
|
108
|
+
seo: z.ZodOptional<z.ZodObject<{
|
|
109
|
+
group: z.ZodOptional<z.ZodString>;
|
|
110
|
+
fields: z.ZodOptional<z.ZodObject<{
|
|
111
|
+
title: z.ZodOptional<z.ZodString>;
|
|
112
|
+
description: z.ZodOptional<z.ZodString>;
|
|
113
|
+
ogImage: z.ZodOptional<z.ZodString>;
|
|
114
|
+
ogType: z.ZodOptional<z.ZodString>;
|
|
115
|
+
canonical: z.ZodOptional<z.ZodString>;
|
|
116
|
+
noindex: z.ZodOptional<z.ZodString>;
|
|
117
|
+
twitterCard: z.ZodOptional<z.ZodString>;
|
|
118
|
+
}, "strict", z.ZodTypeAny, {
|
|
119
|
+
description?: string | undefined;
|
|
120
|
+
title?: string | undefined;
|
|
121
|
+
ogImage?: string | undefined;
|
|
122
|
+
ogType?: string | undefined;
|
|
123
|
+
canonical?: string | undefined;
|
|
124
|
+
noindex?: string | undefined;
|
|
125
|
+
twitterCard?: string | undefined;
|
|
126
|
+
}, {
|
|
127
|
+
description?: string | undefined;
|
|
128
|
+
title?: string | undefined;
|
|
129
|
+
ogImage?: string | undefined;
|
|
130
|
+
ogType?: string | undefined;
|
|
131
|
+
canonical?: string | undefined;
|
|
132
|
+
noindex?: string | undefined;
|
|
133
|
+
twitterCard?: string | undefined;
|
|
134
|
+
}>>;
|
|
135
|
+
}, "strict", z.ZodTypeAny, {
|
|
136
|
+
fields?: {
|
|
137
|
+
description?: string | undefined;
|
|
138
|
+
title?: string | undefined;
|
|
139
|
+
ogImage?: string | undefined;
|
|
140
|
+
ogType?: string | undefined;
|
|
141
|
+
canonical?: string | undefined;
|
|
142
|
+
noindex?: string | undefined;
|
|
143
|
+
twitterCard?: string | undefined;
|
|
144
|
+
} | undefined;
|
|
145
|
+
group?: string | undefined;
|
|
146
|
+
}, {
|
|
147
|
+
fields?: {
|
|
148
|
+
description?: string | undefined;
|
|
149
|
+
title?: string | undefined;
|
|
150
|
+
ogImage?: string | undefined;
|
|
151
|
+
ogType?: string | undefined;
|
|
152
|
+
canonical?: string | undefined;
|
|
153
|
+
noindex?: string | undefined;
|
|
154
|
+
twitterCard?: string | undefined;
|
|
155
|
+
} | undefined;
|
|
156
|
+
group?: string | undefined;
|
|
157
|
+
}>>;
|
|
108
158
|
dev: z.ZodOptional<z.ZodObject<{
|
|
109
159
|
contentSync: z.ZodOptional<z.ZodDefault<z.ZodEnum<["off", "warn"]>>>;
|
|
110
160
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -167,6 +217,18 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
167
217
|
entryLinkUrl?: EntryLinkUrlResolver | undefined;
|
|
168
218
|
validateEntry?: ValidateEntryHook | undefined;
|
|
169
219
|
mdxAllow?: import("../index.js").MdxAllowlist | undefined;
|
|
220
|
+
seo?: {
|
|
221
|
+
fields?: {
|
|
222
|
+
description?: string | undefined;
|
|
223
|
+
title?: string | undefined;
|
|
224
|
+
ogImage?: string | undefined;
|
|
225
|
+
ogType?: string | undefined;
|
|
226
|
+
canonical?: string | undefined;
|
|
227
|
+
noindex?: string | undefined;
|
|
228
|
+
twitterCard?: string | undefined;
|
|
229
|
+
} | undefined;
|
|
230
|
+
group?: string | undefined;
|
|
231
|
+
} | undefined;
|
|
170
232
|
}, {
|
|
171
233
|
gitBotAuthorName: string;
|
|
172
234
|
gitBotAuthorEmail: string;
|
|
@@ -222,6 +284,18 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
222
284
|
entryLinkUrl?: EntryLinkUrlResolver | undefined;
|
|
223
285
|
validateEntry?: ValidateEntryHook | undefined;
|
|
224
286
|
mdxAllow?: import("../index.js").MdxAllowlist | undefined;
|
|
287
|
+
seo?: {
|
|
288
|
+
fields?: {
|
|
289
|
+
description?: string | undefined;
|
|
290
|
+
title?: string | undefined;
|
|
291
|
+
ogImage?: string | undefined;
|
|
292
|
+
ogType?: string | undefined;
|
|
293
|
+
canonical?: string | undefined;
|
|
294
|
+
noindex?: string | undefined;
|
|
295
|
+
twitterCard?: string | undefined;
|
|
296
|
+
} | undefined;
|
|
297
|
+
group?: string | undefined;
|
|
298
|
+
} | undefined;
|
|
225
299
|
}>;
|
|
226
300
|
/**
|
|
227
301
|
* Default workspace path for prod mode (used when CANOPYCMS_WORKSPACE_ROOT is not set): where
|
|
@@ -61,6 +61,26 @@ const editorConfigSchema = z.object({
|
|
|
61
61
|
// Optional: provider sign-in UI for signed-out users (see EditorSignInProps)
|
|
62
62
|
SignInComponent: z.custom().optional(),
|
|
63
63
|
});
|
|
64
|
+
const seoFieldNameSchema = z.string().min(1).optional();
|
|
65
|
+
// Strict, so a misspelled key (`noIndex`) fails the parse instead of leaving every surface
|
|
66
|
+
// reading the default field name.
|
|
67
|
+
const seoSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
group: z.string().min(1).optional(),
|
|
70
|
+
fields: z
|
|
71
|
+
.object({
|
|
72
|
+
title: seoFieldNameSchema,
|
|
73
|
+
description: seoFieldNameSchema,
|
|
74
|
+
ogImage: seoFieldNameSchema,
|
|
75
|
+
ogType: seoFieldNameSchema,
|
|
76
|
+
canonical: seoFieldNameSchema,
|
|
77
|
+
noindex: seoFieldNameSchema,
|
|
78
|
+
twitterCard: seoFieldNameSchema,
|
|
79
|
+
})
|
|
80
|
+
.strict()
|
|
81
|
+
.optional(),
|
|
82
|
+
})
|
|
83
|
+
.strict();
|
|
64
84
|
export const CanopyConfigSchema = z
|
|
65
85
|
.object({
|
|
66
86
|
media: mediaSchema.optional(),
|
|
@@ -110,6 +130,7 @@ export const CanopyConfigSchema = z
|
|
|
110
130
|
entryLinkUrl: z.custom().optional(),
|
|
111
131
|
validateEntry: z.custom().optional(),
|
|
112
132
|
mdxAllow: mdxAllowlistSchema.optional(),
|
|
133
|
+
seo: seoSchema.optional(),
|
|
113
134
|
dev: devConfigSchema.optional(),
|
|
114
135
|
})
|
|
115
136
|
.strict();
|
|
@@ -33,6 +33,7 @@ export declare const imageFieldSchema: z.ZodObject<{
|
|
|
33
33
|
list: z.ZodOptional<z.ZodBoolean>;
|
|
34
34
|
isTitle: z.ZodOptional<z.ZodBoolean>;
|
|
35
35
|
isBody: z.ZodOptional<z.ZodBoolean>;
|
|
36
|
+
maxItems: z.ZodOptional<z.ZodNumber>;
|
|
36
37
|
} & {
|
|
37
38
|
type: z.ZodLiteral<"image">;
|
|
38
39
|
aspect: z.ZodOptional<z.ZodString>;
|
|
@@ -46,6 +47,7 @@ export declare const imageFieldSchema: z.ZodObject<{
|
|
|
46
47
|
list?: boolean | undefined;
|
|
47
48
|
isTitle?: boolean | undefined;
|
|
48
49
|
isBody?: boolean | undefined;
|
|
50
|
+
maxItems?: number | undefined;
|
|
49
51
|
aspect?: string | undefined;
|
|
50
52
|
altOptional?: boolean | undefined;
|
|
51
53
|
}, {
|
|
@@ -57,9 +59,16 @@ export declare const imageFieldSchema: z.ZodObject<{
|
|
|
57
59
|
list?: boolean | undefined;
|
|
58
60
|
isTitle?: boolean | undefined;
|
|
59
61
|
isBody?: boolean | undefined;
|
|
62
|
+
maxItems?: number | undefined;
|
|
60
63
|
aspect?: string | undefined;
|
|
61
64
|
altOptional?: boolean | undefined;
|
|
62
65
|
}>;
|
|
66
|
+
/**
|
|
67
|
+
* A dotted field path for `optionSort: { field }`: non-empty segments, so `'a..b'` and `'.a'`
|
|
68
|
+
* fail at config time rather than sorting on a value no entry has.
|
|
69
|
+
* @internal Exported for the reference-options endpoint, which validates `sortField` the same way.
|
|
70
|
+
*/
|
|
71
|
+
export declare const referenceSortFieldSchema: z.ZodString;
|
|
63
72
|
/** @internal Exported for tests. */
|
|
64
73
|
export declare const referenceFieldSchema: z.ZodObject<{
|
|
65
74
|
name: z.ZodString;
|
|
@@ -69,6 +78,7 @@ export declare const referenceFieldSchema: z.ZodObject<{
|
|
|
69
78
|
list: z.ZodOptional<z.ZodBoolean>;
|
|
70
79
|
isTitle: z.ZodOptional<z.ZodBoolean>;
|
|
71
80
|
isBody: z.ZodOptional<z.ZodBoolean>;
|
|
81
|
+
maxItems: z.ZodOptional<z.ZodNumber>;
|
|
72
82
|
} & {
|
|
73
83
|
type: z.ZodLiteral<"reference">;
|
|
74
84
|
collections: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -85,6 +95,16 @@ export declare const referenceFieldSchema: z.ZodObject<{
|
|
|
85
95
|
label: string;
|
|
86
96
|
}>]>, "many">>;
|
|
87
97
|
includeBody: z.ZodOptional<z.ZodBoolean>;
|
|
98
|
+
optionSort: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["label", "collection"]>, z.ZodObject<{
|
|
99
|
+
field: z.ZodString;
|
|
100
|
+
direction: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
|
|
101
|
+
}, "strict", z.ZodTypeAny, {
|
|
102
|
+
field: string;
|
|
103
|
+
direction?: "asc" | "desc" | undefined;
|
|
104
|
+
}, {
|
|
105
|
+
field: string;
|
|
106
|
+
direction?: "asc" | "desc" | undefined;
|
|
107
|
+
}>]>>;
|
|
88
108
|
}, "strip", z.ZodTypeAny, {
|
|
89
109
|
name: string;
|
|
90
110
|
type: "reference";
|
|
@@ -98,10 +118,15 @@ export declare const referenceFieldSchema: z.ZodObject<{
|
|
|
98
118
|
list?: boolean | undefined;
|
|
99
119
|
isTitle?: boolean | undefined;
|
|
100
120
|
isBody?: boolean | undefined;
|
|
121
|
+
maxItems?: number | undefined;
|
|
101
122
|
collections?: string[] | undefined;
|
|
102
123
|
entryTypes?: string[] | undefined;
|
|
103
124
|
displayField?: string | undefined;
|
|
104
125
|
includeBody?: boolean | undefined;
|
|
126
|
+
optionSort?: "collection" | "label" | {
|
|
127
|
+
field: string;
|
|
128
|
+
direction?: "asc" | "desc" | undefined;
|
|
129
|
+
} | undefined;
|
|
105
130
|
}, {
|
|
106
131
|
name: string;
|
|
107
132
|
type: "reference";
|
|
@@ -115,10 +140,15 @@ export declare const referenceFieldSchema: z.ZodObject<{
|
|
|
115
140
|
list?: boolean | undefined;
|
|
116
141
|
isTitle?: boolean | undefined;
|
|
117
142
|
isBody?: boolean | undefined;
|
|
143
|
+
maxItems?: number | undefined;
|
|
118
144
|
collections?: string[] | undefined;
|
|
119
145
|
entryTypes?: string[] | undefined;
|
|
120
146
|
displayField?: string | undefined;
|
|
121
147
|
includeBody?: boolean | undefined;
|
|
148
|
+
optionSort?: "collection" | "label" | {
|
|
149
|
+
field: string;
|
|
150
|
+
direction?: "asc" | "desc" | undefined;
|
|
151
|
+
} | undefined;
|
|
122
152
|
}>;
|
|
123
153
|
export declare const blockSchema: z.ZodObject<{
|
|
124
154
|
name: z.ZodString;
|
|
@@ -12,6 +12,9 @@ const fieldBaseSchema = z.object({
|
|
|
12
12
|
list: z.boolean().optional(),
|
|
13
13
|
isTitle: z.boolean().optional(),
|
|
14
14
|
isBody: z.boolean().optional(),
|
|
15
|
+
// Listed so a parse keeps it (zod strips unknown keys); which fields may carry it is
|
|
16
|
+
// `ensureMaxItemsPlacement`'s call (config/validation.ts).
|
|
17
|
+
maxItems: z.number().int().positive().optional(),
|
|
15
18
|
});
|
|
16
19
|
export const selectOptionSchema = z.union([
|
|
17
20
|
z.string(),
|
|
@@ -94,6 +97,14 @@ const selectFieldSchema = fieldBaseSchema.extend({
|
|
|
94
97
|
type: z.literal('select'),
|
|
95
98
|
options: z.array(selectOptionSchema).min(1),
|
|
96
99
|
});
|
|
100
|
+
/**
|
|
101
|
+
* A dotted field path for `optionSort: { field }`: non-empty segments, so `'a..b'` and `'.a'`
|
|
102
|
+
* fail at config time rather than sorting on a value no entry has.
|
|
103
|
+
* @internal Exported for the reference-options endpoint, which validates `sortField` the same way.
|
|
104
|
+
*/
|
|
105
|
+
export const referenceSortFieldSchema = z
|
|
106
|
+
.string()
|
|
107
|
+
.regex(/^[^.]+(\.[^.]+)*$/, 'must be a field name or a dotted path such as "meta.publishedAt"');
|
|
97
108
|
// At least one of `collections` or `entryTypes` must be specified (enforced by config validation).
|
|
98
109
|
/** @internal Exported for tests. */
|
|
99
110
|
export const referenceFieldSchema = fieldBaseSchema.extend({
|
|
@@ -107,6 +118,17 @@ export const referenceFieldSchema = fieldBaseSchema.extend({
|
|
|
107
118
|
// runtime-consumed flag and the feature would no-op with no error. (`resolvedSchema` is
|
|
108
119
|
// absent on purpose — it is type-inference-only and stripping it changes no behavior.)
|
|
109
120
|
includeBody: z.boolean().optional(),
|
|
121
|
+
optionSort: z
|
|
122
|
+
.union([
|
|
123
|
+
z.enum(['label', 'collection']),
|
|
124
|
+
z
|
|
125
|
+
.object({
|
|
126
|
+
field: referenceSortFieldSchema,
|
|
127
|
+
direction: z.enum(['asc', 'desc']).optional(),
|
|
128
|
+
})
|
|
129
|
+
.strict(),
|
|
130
|
+
])
|
|
131
|
+
.optional(),
|
|
110
132
|
});
|
|
111
133
|
// Use a mutable holder to enable forward references in recursive z.lazy() closures.
|
|
112
134
|
// blockSchema/objectFieldSchema reference fieldHolder[0] via z.lazy, which is resolved
|