convex-cms 0.0.2 → 0.0.3
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/admin-dist/nitro.json +15 -0
- package/admin-dist/public/assets/CmsEmptyState-CRswfTzk.js +5 -0
- package/admin-dist/public/assets/CmsPageHeader-CirpXndm.js +1 -0
- package/admin-dist/public/assets/CmsStatusBadge-CbEUpQu-.js +1 -0
- package/admin-dist/public/assets/CmsToolbar-BI2nZOXp.js +1 -0
- package/admin-dist/public/assets/ContentEntryEditor-CBeCyK_m.js +4 -0
- package/admin-dist/public/assets/ErrorState-BIVaWmom.js +1 -0
- package/admin-dist/public/assets/TaxonomyFilter-ChaY6Y_x.js +1 -0
- package/admin-dist/public/assets/_contentTypeId-DQ8k_Rvw.js +1 -0
- package/admin-dist/public/assets/_entryId-CKU_glsK.js +1 -0
- package/admin-dist/public/assets/alert-BXjTqrwQ.js +1 -0
- package/admin-dist/public/assets/badge-hvUOzpVZ.js +1 -0
- package/admin-dist/public/assets/circle-check-big-CF_pR17r.js +1 -0
- package/admin-dist/public/assets/command-DU82cJlt.js +1 -0
- package/admin-dist/public/assets/content-_LXl3pp7.js +1 -0
- package/admin-dist/public/assets/content-types-KjxaXGxY.js +2 -0
- package/admin-dist/public/assets/globals-CS6BZ0zp.css +1 -0
- package/admin-dist/public/assets/index-DNGIZHL-.js +1 -0
- package/admin-dist/public/assets/label-KNtpL71g.js +1 -0
- package/admin-dist/public/assets/link-2-Bw2aI4V4.js +1 -0
- package/admin-dist/public/assets/list-sYepHjt_.js +1 -0
- package/admin-dist/public/assets/main-CKj5yfEi.js +97 -0
- package/admin-dist/public/assets/media-Bkrkffm7.js +1 -0
- package/admin-dist/public/assets/new._contentTypeId-C3LstjNs.js +1 -0
- package/admin-dist/public/assets/plus-DUn8v_Xf.js +1 -0
- package/admin-dist/public/assets/rotate-ccw-DJEoHcRI.js +1 -0
- package/admin-dist/public/assets/scroll-area-DfIlT0in.js +1 -0
- package/admin-dist/public/assets/search-MuAUDJKR.js +1 -0
- package/admin-dist/public/assets/select-BD29IXCI.js +1 -0
- package/admin-dist/public/assets/settings-DmMyn_6A.js +1 -0
- package/admin-dist/public/assets/switch-h3Rrnl5i.js +1 -0
- package/admin-dist/public/assets/tabs-imc8h-Dp.js +1 -0
- package/admin-dist/public/assets/taxonomies-dAsrT65H.js +1 -0
- package/admin-dist/public/assets/textarea-BTy7nwzR.js +1 -0
- package/admin-dist/public/assets/trash-SAWKZZHv.js +1 -0
- package/admin-dist/public/assets/triangle-alert-E52Vfeuh.js +1 -0
- package/admin-dist/public/assets/useBreadcrumbLabel-BECBMCzM.js +1 -0
- package/admin-dist/public/assets/usePermissions-Basjs9BT.js +1 -0
- package/admin-dist/public/favicon.ico +0 -0
- package/admin-dist/server/_chunks/_libs/@date-fns/tz.mjs +217 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/core.mjs +719 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/dom.mjs +622 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/react-dom.mjs +292 -0
- package/admin-dist/server/_chunks/_libs/@floating-ui/utils.mjs +320 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/number.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/primitive.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-arrow.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-avatar.mjs +119 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-checkbox.mjs +270 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-collection.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-compose-refs.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-context.mjs +137 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dialog.mjs +325 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-direction.mjs +9 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dismissable-layer.mjs +210 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-dropdown-menu.mjs +253 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-guards.mjs +29 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-focus-scope.mjs +206 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-id.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-label.mjs +23 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-menu.mjs +812 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popover.mjs +300 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-popper.mjs +286 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-portal.mjs +16 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-presence.mjs +128 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-primitive.mjs +141 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-roving-focus.mjs +224 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-scroll-area.mjs +721 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-select.mjs +1163 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-separator.mjs +28 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-slot.mjs +601 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-switch.mjs +152 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-tabs.mjs +189 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-callback-ref.mjs +11 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-controllable-state.mjs +69 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-effect-event.mjs +1 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-escape-keydown.mjs +17 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-is-hydrated.mjs +15 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-layout-effect.mjs +6 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-previous.mjs +14 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-use-size.mjs +39 -0
- package/admin-dist/server/_chunks/_libs/@radix-ui/react-visually-hidden.mjs +33 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/history.mjs +409 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-router.mjs +1711 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/react-store.mjs +56 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/router-core.mjs +4829 -0
- package/admin-dist/server/_chunks/_libs/@tanstack/store.mjs +134 -0
- package/admin-dist/server/_chunks/_libs/react-dom.mjs +10781 -0
- package/admin-dist/server/_chunks/_libs/react.mjs +513 -0
- package/admin-dist/server/_libs/aria-hidden.mjs +122 -0
- package/admin-dist/server/_libs/class-variance-authority.mjs +44 -0
- package/admin-dist/server/_libs/clsx.mjs +16 -0
- package/admin-dist/server/_libs/cmdk.mjs +315 -0
- package/admin-dist/server/_libs/convex.mjs +4841 -0
- package/admin-dist/server/_libs/cookie-es.mjs +58 -0
- package/admin-dist/server/_libs/croner.mjs +1 -0
- package/admin-dist/server/_libs/crossws.mjs +1 -0
- package/admin-dist/server/_libs/date-fns.mjs +1716 -0
- package/admin-dist/server/_libs/detect-node-es.mjs +1 -0
- package/admin-dist/server/_libs/get-nonce.mjs +9 -0
- package/admin-dist/server/_libs/h3-v2.mjs +277 -0
- package/admin-dist/server/_libs/h3.mjs +401 -0
- package/admin-dist/server/_libs/hookable.mjs +1 -0
- package/admin-dist/server/_libs/isbot.mjs +20 -0
- package/admin-dist/server/_libs/lucide-react.mjs +850 -0
- package/admin-dist/server/_libs/ohash.mjs +1 -0
- package/admin-dist/server/_libs/react-day-picker.mjs +2201 -0
- package/admin-dist/server/_libs/react-remove-scroll-bar.mjs +82 -0
- package/admin-dist/server/_libs/react-remove-scroll.mjs +328 -0
- package/admin-dist/server/_libs/react-style-singleton.mjs +69 -0
- package/admin-dist/server/_libs/rou3.mjs +8 -0
- package/admin-dist/server/_libs/seroval-plugins.mjs +58 -0
- package/admin-dist/server/_libs/seroval.mjs +1765 -0
- package/admin-dist/server/_libs/srvx.mjs +719 -0
- package/admin-dist/server/_libs/tailwind-merge.mjs +3010 -0
- package/admin-dist/server/_libs/tiny-invariant.mjs +12 -0
- package/admin-dist/server/_libs/tiny-warning.mjs +5 -0
- package/admin-dist/server/_libs/tslib.mjs +39 -0
- package/admin-dist/server/_libs/ufo.mjs +54 -0
- package/admin-dist/server/_libs/unctx.mjs +1 -0
- package/admin-dist/server/_libs/unstorage.mjs +1 -0
- package/admin-dist/server/_libs/use-callback-ref.mjs +66 -0
- package/admin-dist/server/_libs/use-sidecar.mjs +106 -0
- package/admin-dist/server/_libs/use-sync-external-store.mjs +139 -0
- package/admin-dist/server/_libs/zod.mjs +4223 -0
- package/admin-dist/server/_ssr/CmsEmptyState-DU7-7-mV.mjs +290 -0
- package/admin-dist/server/_ssr/CmsPageHeader-CseW0AHm.mjs +24 -0
- package/admin-dist/server/_ssr/CmsStatusBadge-B_pi4KCp.mjs +127 -0
- package/admin-dist/server/_ssr/CmsToolbar-X75ex6ek.mjs +49 -0
- package/admin-dist/server/_ssr/ContentEntryEditor-CepusRsA.mjs +3720 -0
- package/admin-dist/server/_ssr/ErrorState-cI-bKLez.mjs +89 -0
- package/admin-dist/server/_ssr/TaxonomyFilter-Bwrq0-cz.mjs +188 -0
- package/admin-dist/server/_ssr/_contentTypeId-BqYKEcLr.mjs +379 -0
- package/admin-dist/server/_ssr/_entryId-CRfnqeDf.mjs +161 -0
- package/admin-dist/server/_ssr/_tanstack-start-manifest_v-BwDlABVk.mjs +4 -0
- package/admin-dist/server/_ssr/alert-CVt45UUP.mjs +92 -0
- package/admin-dist/server/_ssr/badge-6BsP37vG.mjs +125 -0
- package/admin-dist/server/_ssr/command-fy8epIKf.mjs +128 -0
- package/admin-dist/server/_ssr/config.server-D7JHDcDv.mjs +117 -0
- package/admin-dist/server/_ssr/content-B5RhL7uW.mjs +532 -0
- package/admin-dist/server/_ssr/content-types-BIOqCQYN.mjs +1166 -0
- package/admin-dist/server/_ssr/index-DHSHDPt1.mjs +193 -0
- package/admin-dist/server/_ssr/index.mjs +1275 -0
- package/admin-dist/server/_ssr/label-C8Dko1j7.mjs +22 -0
- package/admin-dist/server/_ssr/media-CSx3XttC.mjs +1832 -0
- package/admin-dist/server/_ssr/new._contentTypeId-DzanEZQM.mjs +144 -0
- package/admin-dist/server/_ssr/router-DDWcF-kt.mjs +1556 -0
- package/admin-dist/server/_ssr/scroll-area-bjPYwhXN.mjs +59 -0
- package/admin-dist/server/_ssr/select-BUhDDf4T.mjs +142 -0
- package/admin-dist/server/_ssr/settings-DAsxnw2q.mjs +348 -0
- package/admin-dist/server/_ssr/start-HYkvq4Ni.mjs +4 -0
- package/admin-dist/server/_ssr/switch-BgyRtQ1Z.mjs +31 -0
- package/admin-dist/server/_ssr/tabs-DzMdRB1A.mjs +628 -0
- package/admin-dist/server/_ssr/taxonomies-C8j8g5Q5.mjs +915 -0
- package/admin-dist/server/_ssr/textarea-9jNeYJSc.mjs +18 -0
- package/admin-dist/server/_ssr/trash-DYMxwhZB.mjs +291 -0
- package/admin-dist/server/_ssr/useBreadcrumbLabel-FNSAr2Ha.mjs +16 -0
- package/admin-dist/server/_ssr/usePermissions-BJGGahrJ.mjs +68 -0
- package/admin-dist/server/favicon.ico +0 -0
- package/admin-dist/server/index.mjs +627 -0
- package/dist/cli/index.js +0 -0
- package/dist/client/admin-config.d.ts +0 -1
- package/dist/client/admin-config.d.ts.map +1 -1
- package/dist/client/admin-config.js +0 -1
- package/dist/client/admin-config.js.map +1 -1
- package/dist/client/adminApi.d.ts.map +1 -1
- package/dist/client/agentTools.d.ts +1237 -135
- package/dist/client/agentTools.d.ts.map +1 -1
- package/dist/client/agentTools.js +33 -9
- package/dist/client/agentTools.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +9 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/mediaAssets.d.ts +35 -0
- package/dist/component/mediaAssets.d.ts.map +1 -1
- package/dist/component/mediaAssets.js +81 -0
- package/dist/component/mediaAssets.js.map +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +2 -1
- package/dist/test.js.map +1 -1
- package/package.json +9 -5
- package/dist/component/auditLog.d.ts +0 -410
- package/dist/component/auditLog.d.ts.map +0 -1
- package/dist/component/auditLog.js +0 -607
- package/dist/component/auditLog.js.map +0 -1
- package/dist/component/types.d.ts +0 -4
- package/dist/component/types.d.ts.map +0 -1
- package/dist/component/types.js +0 -2
- package/dist/component/types.js.map +0 -1
- package/src/cli/commands/admin.ts +0 -104
- package/src/cli/index.ts +0 -21
- package/src/cli/utils/detectConvexUrl.ts +0 -54
- package/src/cli/utils/openBrowser.ts +0 -16
- package/src/client/admin-config.ts +0 -138
- package/src/client/adminApi.ts +0 -942
- package/src/client/agentTools.ts +0 -1311
- package/src/client/argTypes.ts +0 -316
- package/src/client/field-types.ts +0 -187
- package/src/client/index.ts +0 -1301
- package/src/client/queryBuilder.ts +0 -1100
- package/src/client/schema/codegen.ts +0 -500
- package/src/client/schema/defineContentType.ts +0 -501
- package/src/client/schema/index.ts +0 -169
- package/src/client/schema/schemaDrift.ts +0 -574
- package/src/client/schema/typedClient.ts +0 -688
- package/src/client/schema/types.ts +0 -666
- package/src/client/types.ts +0 -723
- package/src/client/workflows.ts +0 -141
- package/src/client/wrapper.ts +0 -4304
- package/src/component/_generated/api.ts +0 -140
- package/src/component/_generated/component.ts +0 -5029
- package/src/component/_generated/dataModel.ts +0 -60
- package/src/component/_generated/server.ts +0 -156
- package/src/component/authorization.ts +0 -647
- package/src/component/authorizationHooks.ts +0 -668
- package/src/component/bulkOperations.ts +0 -687
- package/src/component/contentEntries.ts +0 -1976
- package/src/component/contentEntryMutations.ts +0 -1223
- package/src/component/contentEntryValidation.ts +0 -707
- package/src/component/contentLock.ts +0 -550
- package/src/component/contentTypeMigration.ts +0 -1064
- package/src/component/contentTypeMutations.ts +0 -969
- package/src/component/contentTypes.ts +0 -346
- package/src/component/convex.config.ts +0 -44
- package/src/component/documentTypes.ts +0 -240
- package/src/component/eventEmitter.ts +0 -485
- package/src/component/exportImport.ts +0 -1169
- package/src/component/index.ts +0 -491
- package/src/component/lib/deepReferenceResolver.ts +0 -999
- package/src/component/lib/errors.ts +0 -816
- package/src/component/lib/index.ts +0 -145
- package/src/component/lib/mediaReferenceResolver.ts +0 -495
- package/src/component/lib/metadataExtractor.ts +0 -792
- package/src/component/lib/mutationAuth.ts +0 -199
- package/src/component/lib/queries.ts +0 -79
- package/src/component/lib/ragContentChunker.ts +0 -1371
- package/src/component/lib/referenceResolver.ts +0 -430
- package/src/component/lib/slugGenerator.ts +0 -262
- package/src/component/lib/slugUniqueness.ts +0 -333
- package/src/component/lib/softDelete.ts +0 -44
- package/src/component/localeFallbackChain.ts +0 -673
- package/src/component/localeFields.ts +0 -896
- package/src/component/mediaAssetMutations.ts +0 -725
- package/src/component/mediaAssets.ts +0 -932
- package/src/component/mediaFolderMutations.ts +0 -1046
- package/src/component/mediaUploadMutations.ts +0 -224
- package/src/component/mediaVariantMutations.ts +0 -900
- package/src/component/mediaVariants.ts +0 -793
- package/src/component/ragContentIndexer.ts +0 -1067
- package/src/component/rateLimitHooks.ts +0 -572
- package/src/component/roles.ts +0 -1360
- package/src/component/scheduledPublish.ts +0 -358
- package/src/component/schema.ts +0 -617
- package/src/component/taxonomies.ts +0 -949
- package/src/component/taxonomyMutations.ts +0 -1210
- package/src/component/trash.ts +0 -724
- package/src/component/userContext.ts +0 -898
- package/src/component/validation.ts +0 -1388
- package/src/component/validators.ts +0 -949
- package/src/component/versionMutations.ts +0 -392
- package/src/component/webhookTrigger.ts +0 -1922
- package/src/react/index.ts +0 -898
- package/src/test.ts +0 -1580
|
@@ -1,1388 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content Validation Functions
|
|
3
|
-
*
|
|
4
|
-
* Runtime validation helpers that check content data against field configurations.
|
|
5
|
-
* These complement the Convex validators by providing detailed validation logic
|
|
6
|
-
* and human-readable error messages.
|
|
7
|
-
*
|
|
8
|
-
* Supports both plain field values and localized field values (LocalizedFieldValue<T>)
|
|
9
|
-
* for fields marked as `localized: true` in their field definition.
|
|
10
|
-
*/
|
|
11
|
-
import { FieldType } from "./validators.js";
|
|
12
|
-
import {
|
|
13
|
-
isLocalizedFieldValue,
|
|
14
|
-
type LocalizedFieldValue,
|
|
15
|
-
} from "./localeFields.js";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Field options structure (matches schema.ts fieldOptionsValidator)
|
|
19
|
-
*/
|
|
20
|
-
export interface FieldOptions {
|
|
21
|
-
// Text fields
|
|
22
|
-
minLength?: number;
|
|
23
|
-
maxLength?: number;
|
|
24
|
-
pattern?: string;
|
|
25
|
-
|
|
26
|
-
// Number fields
|
|
27
|
-
min?: number;
|
|
28
|
-
max?: number;
|
|
29
|
-
step?: number;
|
|
30
|
-
precision?: number;
|
|
31
|
-
|
|
32
|
-
// Reference fields
|
|
33
|
-
allowedContentTypes?: string[];
|
|
34
|
-
multiple?: boolean;
|
|
35
|
-
/** Minimum number of references required (only applies when multiple is true) */
|
|
36
|
-
minItems?: number;
|
|
37
|
-
|
|
38
|
-
// Media fields
|
|
39
|
-
allowedMimeTypes?: string[];
|
|
40
|
-
maxFileSize?: number;
|
|
41
|
-
|
|
42
|
-
// Select fields
|
|
43
|
-
options?: Array<{ value: string; label: string }>;
|
|
44
|
-
|
|
45
|
-
// Rich text fields
|
|
46
|
-
allowedBlocks?: string[];
|
|
47
|
-
allowedMarks?: string[];
|
|
48
|
-
|
|
49
|
-
// Tag fields
|
|
50
|
-
taxonomyId?: string;
|
|
51
|
-
allowCreate?: boolean;
|
|
52
|
-
maxTags?: number;
|
|
53
|
-
minTags?: number;
|
|
54
|
-
|
|
55
|
-
// Category fields
|
|
56
|
-
allowMultiple?: boolean;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Field definition structure (matches schema.ts fieldDefinitionValidator)
|
|
61
|
-
*/
|
|
62
|
-
export interface FieldDefinition {
|
|
63
|
-
name: string;
|
|
64
|
-
label: string;
|
|
65
|
-
type: FieldType;
|
|
66
|
-
required: boolean;
|
|
67
|
-
searchable?: boolean;
|
|
68
|
-
localized?: boolean;
|
|
69
|
-
description?: string;
|
|
70
|
-
defaultValue?: unknown;
|
|
71
|
-
options?: FieldOptions;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Content type schema structure
|
|
76
|
-
*/
|
|
77
|
-
export interface ContentTypeSchema {
|
|
78
|
-
name: string;
|
|
79
|
-
displayName: string;
|
|
80
|
-
description?: string;
|
|
81
|
-
fields: FieldDefinition[];
|
|
82
|
-
titleField?: string;
|
|
83
|
-
slugField?: string;
|
|
84
|
-
singleton?: boolean;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Content data is a record of field names to their values
|
|
89
|
-
*/
|
|
90
|
-
export type ContentData = Record<string, unknown>;
|
|
91
|
-
|
|
92
|
-
// =============================================================================
|
|
93
|
-
// Validation Result Types
|
|
94
|
-
// =============================================================================
|
|
95
|
-
|
|
96
|
-
export type ValidationError = {
|
|
97
|
-
field: string;
|
|
98
|
-
message: string;
|
|
99
|
-
code: ValidationErrorCode;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
export type ValidationErrorCode =
|
|
103
|
-
| "REQUIRED"
|
|
104
|
-
| "MIN_LENGTH"
|
|
105
|
-
| "MAX_LENGTH"
|
|
106
|
-
| "PATTERN_MISMATCH"
|
|
107
|
-
| "MIN_VALUE"
|
|
108
|
-
| "MAX_VALUE"
|
|
109
|
-
| "NOT_INTEGER"
|
|
110
|
-
| "MIN_DATE"
|
|
111
|
-
| "MAX_DATE"
|
|
112
|
-
| "INVALID_TYPE"
|
|
113
|
-
| "MIN_ITEMS"
|
|
114
|
-
| "MAX_ITEMS"
|
|
115
|
-
| "INVALID_CONTENT_TYPE"
|
|
116
|
-
| "UNKNOWN_FIELD"
|
|
117
|
-
| "INVALID_MIME_TYPE"
|
|
118
|
-
| "FILE_TOO_LARGE"
|
|
119
|
-
| "INVALID_LOCALIZED_STRUCTURE"
|
|
120
|
-
| "MISSING_LOCALE";
|
|
121
|
-
|
|
122
|
-
export type ValidationResult =
|
|
123
|
-
| { valid: true; errors: [] }
|
|
124
|
-
| { valid: false; errors: ValidationError[] };
|
|
125
|
-
|
|
126
|
-
// =============================================================================
|
|
127
|
-
// Field Value Validators
|
|
128
|
-
// =============================================================================
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Validate a text field value against its configuration
|
|
132
|
-
*/
|
|
133
|
-
export function validateTextField(
|
|
134
|
-
value: unknown,
|
|
135
|
-
fieldDef: FieldDefinition
|
|
136
|
-
): ValidationError[] {
|
|
137
|
-
const errors: ValidationError[] = [];
|
|
138
|
-
const { name, required, options } = fieldDef;
|
|
139
|
-
|
|
140
|
-
// Check required
|
|
141
|
-
if (required && (value === null || value === undefined || value === "")) {
|
|
142
|
-
errors.push({
|
|
143
|
-
field: name,
|
|
144
|
-
message: `${name} is required`,
|
|
145
|
-
code: "REQUIRED",
|
|
146
|
-
});
|
|
147
|
-
return errors;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Skip further validation if value is empty and not required
|
|
151
|
-
if (value === null || value === undefined || value === "") {
|
|
152
|
-
return errors;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Type check
|
|
156
|
-
if (typeof value !== "string") {
|
|
157
|
-
errors.push({
|
|
158
|
-
field: name,
|
|
159
|
-
message: `${name} must be a string`,
|
|
160
|
-
code: "INVALID_TYPE",
|
|
161
|
-
});
|
|
162
|
-
return errors;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Min length
|
|
166
|
-
if (options?.minLength !== undefined && value.length < options.minLength) {
|
|
167
|
-
errors.push({
|
|
168
|
-
field: name,
|
|
169
|
-
message: `${name} must be at least ${options.minLength} characters`,
|
|
170
|
-
code: "MIN_LENGTH",
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Max length
|
|
175
|
-
if (options?.maxLength !== undefined && value.length > options.maxLength) {
|
|
176
|
-
errors.push({
|
|
177
|
-
field: name,
|
|
178
|
-
message: `${name} must be at most ${options.maxLength} characters`,
|
|
179
|
-
code: "MAX_LENGTH",
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Pattern
|
|
184
|
-
if (options?.pattern !== undefined) {
|
|
185
|
-
const regex = new RegExp(options.pattern);
|
|
186
|
-
if (!regex.test(value)) {
|
|
187
|
-
errors.push({
|
|
188
|
-
field: name,
|
|
189
|
-
message: `${name} does not match the required pattern`,
|
|
190
|
-
code: "PATTERN_MISMATCH",
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return errors;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Validate a rich text field value against its configuration
|
|
200
|
-
*/
|
|
201
|
-
export function validateRichTextField(
|
|
202
|
-
value: unknown,
|
|
203
|
-
fieldDef: FieldDefinition
|
|
204
|
-
): ValidationError[] {
|
|
205
|
-
const errors: ValidationError[] = [];
|
|
206
|
-
const { name, required, options } = fieldDef;
|
|
207
|
-
|
|
208
|
-
// Check required
|
|
209
|
-
if (required && (value === null || value === undefined || value === "")) {
|
|
210
|
-
errors.push({
|
|
211
|
-
field: name,
|
|
212
|
-
message: `${name} is required`,
|
|
213
|
-
code: "REQUIRED",
|
|
214
|
-
});
|
|
215
|
-
return errors;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Skip further validation if value is empty and not required
|
|
219
|
-
if (value === null || value === undefined || value === "") {
|
|
220
|
-
return errors;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Type check
|
|
224
|
-
if (typeof value !== "string") {
|
|
225
|
-
errors.push({
|
|
226
|
-
field: name,
|
|
227
|
-
message: `${name} must be a string`,
|
|
228
|
-
code: "INVALID_TYPE",
|
|
229
|
-
});
|
|
230
|
-
return errors;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Max length (strip HTML tags before counting)
|
|
234
|
-
if (options?.maxLength !== undefined) {
|
|
235
|
-
const plainText = value.replace(/<[^>]*>/g, "");
|
|
236
|
-
if (plainText.length > options.maxLength) {
|
|
237
|
-
errors.push({
|
|
238
|
-
field: name,
|
|
239
|
-
message: `${name} content must be at most ${options.maxLength} characters`,
|
|
240
|
-
code: "MAX_LENGTH",
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return errors;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Validate a number field value against its configuration
|
|
250
|
-
*/
|
|
251
|
-
export function validateNumberField(
|
|
252
|
-
value: unknown,
|
|
253
|
-
fieldDef: FieldDefinition
|
|
254
|
-
): ValidationError[] {
|
|
255
|
-
const errors: ValidationError[] = [];
|
|
256
|
-
const { name, required, options } = fieldDef;
|
|
257
|
-
|
|
258
|
-
// Check required
|
|
259
|
-
if (required && (value === null || value === undefined)) {
|
|
260
|
-
errors.push({
|
|
261
|
-
field: name,
|
|
262
|
-
message: `${name} is required`,
|
|
263
|
-
code: "REQUIRED",
|
|
264
|
-
});
|
|
265
|
-
return errors;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Skip further validation if value is empty and not required
|
|
269
|
-
if (value === null || value === undefined) {
|
|
270
|
-
return errors;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Type check
|
|
274
|
-
if (typeof value !== "number" || isNaN(value)) {
|
|
275
|
-
errors.push({
|
|
276
|
-
field: name,
|
|
277
|
-
message: `${name} must be a number`,
|
|
278
|
-
code: "INVALID_TYPE",
|
|
279
|
-
});
|
|
280
|
-
return errors;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Precision check (step = 1 means integer)
|
|
284
|
-
if (options?.precision === 0 && !Number.isInteger(value)) {
|
|
285
|
-
errors.push({
|
|
286
|
-
field: name,
|
|
287
|
-
message: `${name} must be a whole number`,
|
|
288
|
-
code: "NOT_INTEGER",
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Min value
|
|
293
|
-
if (options?.min !== undefined && value < options.min) {
|
|
294
|
-
errors.push({
|
|
295
|
-
field: name,
|
|
296
|
-
message: `${name} must be at least ${options.min}`,
|
|
297
|
-
code: "MIN_VALUE",
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Max value
|
|
302
|
-
if (options?.max !== undefined && value > options.max) {
|
|
303
|
-
errors.push({
|
|
304
|
-
field: name,
|
|
305
|
-
message: `${name} must be at most ${options.max}`,
|
|
306
|
-
code: "MAX_VALUE",
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return errors;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Validate a boolean field value against its configuration
|
|
315
|
-
*/
|
|
316
|
-
export function validateBooleanField(
|
|
317
|
-
value: unknown,
|
|
318
|
-
fieldDef: FieldDefinition
|
|
319
|
-
): ValidationError[] {
|
|
320
|
-
const errors: ValidationError[] = [];
|
|
321
|
-
const { name, required } = fieldDef;
|
|
322
|
-
|
|
323
|
-
// Check required
|
|
324
|
-
if (required && (value === null || value === undefined)) {
|
|
325
|
-
errors.push({
|
|
326
|
-
field: name,
|
|
327
|
-
message: `${name} is required`,
|
|
328
|
-
code: "REQUIRED",
|
|
329
|
-
});
|
|
330
|
-
return errors;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Skip further validation if value is empty and not required
|
|
334
|
-
if (value === null || value === undefined) {
|
|
335
|
-
return errors;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Type check
|
|
339
|
-
if (typeof value !== "boolean") {
|
|
340
|
-
errors.push({
|
|
341
|
-
field: name,
|
|
342
|
-
message: `${name} must be a boolean`,
|
|
343
|
-
code: "INVALID_TYPE",
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return errors;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Validate a date or datetime field value against its configuration
|
|
352
|
-
*/
|
|
353
|
-
export function validateDateField(
|
|
354
|
-
value: unknown,
|
|
355
|
-
fieldDef: FieldDefinition
|
|
356
|
-
): ValidationError[] {
|
|
357
|
-
const errors: ValidationError[] = [];
|
|
358
|
-
const { name, required, options } = fieldDef;
|
|
359
|
-
|
|
360
|
-
// Check required
|
|
361
|
-
if (required && (value === null || value === undefined)) {
|
|
362
|
-
errors.push({
|
|
363
|
-
field: name,
|
|
364
|
-
message: `${name} is required`,
|
|
365
|
-
code: "REQUIRED",
|
|
366
|
-
});
|
|
367
|
-
return errors;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Skip further validation if value is empty and not required
|
|
371
|
-
if (value === null || value === undefined) {
|
|
372
|
-
return errors;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Type check (must be a valid timestamp)
|
|
376
|
-
if (typeof value !== "number" || isNaN(value)) {
|
|
377
|
-
errors.push({
|
|
378
|
-
field: name,
|
|
379
|
-
message: `${name} must be a valid timestamp`,
|
|
380
|
-
code: "INVALID_TYPE",
|
|
381
|
-
});
|
|
382
|
-
return errors;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Min date (using min from options)
|
|
386
|
-
if (options?.min !== undefined && value < options.min) {
|
|
387
|
-
errors.push({
|
|
388
|
-
field: name,
|
|
389
|
-
message: `${name} must be on or after the minimum date`,
|
|
390
|
-
code: "MIN_DATE",
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Max date (using max from options)
|
|
395
|
-
if (options?.max !== undefined && value > options.max) {
|
|
396
|
-
errors.push({
|
|
397
|
-
field: name,
|
|
398
|
-
message: `${name} must be on or before the maximum date`,
|
|
399
|
-
code: "MAX_DATE",
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return errors;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Validate a reference field value against its configuration.
|
|
408
|
-
*
|
|
409
|
-
* Reference fields store IDs to other content entries. They support:
|
|
410
|
-
* - Single reference: `string` (one entry ID)
|
|
411
|
-
* - Multiple references: `string[]` (array of entry IDs) when `multiple: true`
|
|
412
|
-
*
|
|
413
|
-
* Configuration options:
|
|
414
|
-
* - `allowedContentTypes`: Array of content type names that can be referenced
|
|
415
|
-
* - `multiple`: If true, accepts an array of references
|
|
416
|
-
* - `minItems`: Minimum number of references required (only when `multiple: true`)
|
|
417
|
-
* - `max`: Maximum number of references allowed (only when `multiple: true`)
|
|
418
|
-
*
|
|
419
|
-
* @example
|
|
420
|
-
* ```typescript
|
|
421
|
-
* // Single reference to an author
|
|
422
|
-
* const authorField: FieldDefinition = {
|
|
423
|
-
* name: "author",
|
|
424
|
-
* label: "Author",
|
|
425
|
-
* type: "reference",
|
|
426
|
-
* required: true,
|
|
427
|
-
* options: {
|
|
428
|
-
* allowedContentTypes: ["user"],
|
|
429
|
-
* },
|
|
430
|
-
* };
|
|
431
|
-
*
|
|
432
|
-
* // Multiple references to related posts (1-5 required)
|
|
433
|
-
* const relatedPostsField: FieldDefinition = {
|
|
434
|
-
* name: "relatedPosts",
|
|
435
|
-
* label: "Related Posts",
|
|
436
|
-
* type: "reference",
|
|
437
|
-
* required: true,
|
|
438
|
-
* options: {
|
|
439
|
-
* allowedContentTypes: ["blog_post"],
|
|
440
|
-
* multiple: true,
|
|
441
|
-
* minItems: 1,
|
|
442
|
-
* max: 5,
|
|
443
|
-
* },
|
|
444
|
-
* };
|
|
445
|
-
* ```
|
|
446
|
-
*/
|
|
447
|
-
export function validateReferenceField(
|
|
448
|
-
value: unknown,
|
|
449
|
-
fieldDef: FieldDefinition
|
|
450
|
-
): ValidationError[] {
|
|
451
|
-
const errors: ValidationError[] = [];
|
|
452
|
-
const { name, required, options } = fieldDef;
|
|
453
|
-
const multiple = options?.multiple ?? false;
|
|
454
|
-
|
|
455
|
-
// Check required
|
|
456
|
-
if (required && (value === null || value === undefined)) {
|
|
457
|
-
errors.push({
|
|
458
|
-
field: name,
|
|
459
|
-
message: `${name} is required`,
|
|
460
|
-
code: "REQUIRED",
|
|
461
|
-
});
|
|
462
|
-
return errors;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Skip further validation if value is empty and not required
|
|
466
|
-
if (value === null || value === undefined) {
|
|
467
|
-
return errors;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Type check based on multiple setting
|
|
471
|
-
if (multiple) {
|
|
472
|
-
if (!Array.isArray(value)) {
|
|
473
|
-
errors.push({
|
|
474
|
-
field: name,
|
|
475
|
-
message: `${name} must be an array of references`,
|
|
476
|
-
code: "INVALID_TYPE",
|
|
477
|
-
});
|
|
478
|
-
return errors;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Check if required and empty array
|
|
482
|
-
if (required && value.length === 0) {
|
|
483
|
-
errors.push({
|
|
484
|
-
field: name,
|
|
485
|
-
message: `${name} requires at least one reference`,
|
|
486
|
-
code: "REQUIRED",
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Check each item is a string (valid ID format)
|
|
491
|
-
for (const item of value) {
|
|
492
|
-
if (typeof item !== "string") {
|
|
493
|
-
errors.push({
|
|
494
|
-
field: name,
|
|
495
|
-
message: `${name} contains invalid reference IDs`,
|
|
496
|
-
code: "INVALID_TYPE",
|
|
497
|
-
});
|
|
498
|
-
break;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Min items validation (only for multiple references)
|
|
503
|
-
if (options?.minItems !== undefined && value.length < options.minItems) {
|
|
504
|
-
errors.push({
|
|
505
|
-
field: name,
|
|
506
|
-
message: `${name} requires at least ${options.minItems} reference${options.minItems === 1 ? "" : "s"}`,
|
|
507
|
-
code: "MIN_ITEMS",
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Max items (using max from options)
|
|
512
|
-
if (options?.max !== undefined && value.length > options.max) {
|
|
513
|
-
errors.push({
|
|
514
|
-
field: name,
|
|
515
|
-
message: `${name} can have at most ${options.max} reference${options.max === 1 ? "" : "s"}`,
|
|
516
|
-
code: "MAX_ITEMS",
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
} else {
|
|
520
|
-
if (typeof value !== "string") {
|
|
521
|
-
errors.push({
|
|
522
|
-
field: name,
|
|
523
|
-
message: `${name} must be a reference ID`,
|
|
524
|
-
code: "INVALID_TYPE",
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return errors;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Check if a reference value is valid for a given content type constraint.
|
|
534
|
-
*
|
|
535
|
-
* This is a helper function that can be used in mutation handlers to validate
|
|
536
|
-
* that referenced entries exist and belong to allowed content types.
|
|
537
|
-
*
|
|
538
|
-
* @param referenceId - The content entry ID to validate
|
|
539
|
-
* @param allowedContentTypes - Array of allowed content type names (optional)
|
|
540
|
-
* @param contentTypeLookup - Function to get content type name by entry ID
|
|
541
|
-
* @returns Object with `valid` boolean and optional `error` message
|
|
542
|
-
*/
|
|
543
|
-
export async function validateReferenceContentType(
|
|
544
|
-
referenceId: string,
|
|
545
|
-
allowedContentTypes: string[] | undefined,
|
|
546
|
-
contentTypeLookup: (entryId: string) => Promise<string | null>
|
|
547
|
-
): Promise<{ valid: boolean; error?: string }> {
|
|
548
|
-
// If no content type constraints, the reference is valid
|
|
549
|
-
if (!allowedContentTypes || allowedContentTypes.length === 0) {
|
|
550
|
-
return { valid: true };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Look up the content type of the referenced entry
|
|
554
|
-
const contentTypeName = await contentTypeLookup(referenceId);
|
|
555
|
-
|
|
556
|
-
// If the entry doesn't exist, it's invalid
|
|
557
|
-
if (contentTypeName === null) {
|
|
558
|
-
return {
|
|
559
|
-
valid: false,
|
|
560
|
-
error: `Referenced entry not found: ${referenceId}`,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Check if the content type is in the allowed list
|
|
565
|
-
if (!allowedContentTypes.includes(contentTypeName)) {
|
|
566
|
-
return {
|
|
567
|
-
valid: false,
|
|
568
|
-
error: `Reference must be of type: ${allowedContentTypes.join(", ")}. Got: ${contentTypeName}`,
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return { valid: true };
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Validate a media field value against its configuration.
|
|
577
|
-
*
|
|
578
|
-
* Media fields store IDs to media assets. They support:
|
|
579
|
-
* - Single reference: `string` (one media asset ID)
|
|
580
|
-
* - Multiple references (gallery): `string[]` (array of media asset IDs) when `multiple: true`
|
|
581
|
-
*
|
|
582
|
-
* Configuration options:
|
|
583
|
-
* - `allowedMimeTypes`: Array of allowed MIME types (supports wildcards like "image/*")
|
|
584
|
-
* - `multiple`: If true, accepts an array of references (gallery mode)
|
|
585
|
-
* - `minItems`: Minimum number of media assets required (only when `multiple: true`)
|
|
586
|
-
* - `max`: Maximum number of media assets allowed (only when `multiple: true`)
|
|
587
|
-
* - `maxFileSize`: Maximum file size in bytes (validated at upload time, not here)
|
|
588
|
-
*
|
|
589
|
-
* Note: MIME type validation requires database lookups and is performed by
|
|
590
|
-
* `validateAllMediaReferences` in the mediaReferenceResolver module.
|
|
591
|
-
*
|
|
592
|
-
* @example
|
|
593
|
-
* ```typescript
|
|
594
|
-
* // Single featured image (images only)
|
|
595
|
-
* const featuredImageField: FieldDefinition = {
|
|
596
|
-
* name: "featuredImage",
|
|
597
|
-
* label: "Featured Image",
|
|
598
|
-
* type: "media",
|
|
599
|
-
* required: true,
|
|
600
|
-
* options: {
|
|
601
|
-
* allowedMimeTypes: ["image/*"],
|
|
602
|
-
* },
|
|
603
|
-
* };
|
|
604
|
-
*
|
|
605
|
-
* // Gallery with 2-10 images
|
|
606
|
-
* const galleryField: FieldDefinition = {
|
|
607
|
-
* name: "gallery",
|
|
608
|
-
* label: "Photo Gallery",
|
|
609
|
-
* type: "media",
|
|
610
|
-
* required: true,
|
|
611
|
-
* options: {
|
|
612
|
-
* allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
|
|
613
|
-
* multiple: true,
|
|
614
|
-
* minItems: 2,
|
|
615
|
-
* max: 10,
|
|
616
|
-
* },
|
|
617
|
-
* };
|
|
618
|
-
* ```
|
|
619
|
-
*/
|
|
620
|
-
export function validateMediaField(
|
|
621
|
-
value: unknown,
|
|
622
|
-
fieldDef: FieldDefinition
|
|
623
|
-
): ValidationError[] {
|
|
624
|
-
const errors: ValidationError[] = [];
|
|
625
|
-
const { name, required, options } = fieldDef;
|
|
626
|
-
const multiple = options?.multiple ?? false;
|
|
627
|
-
|
|
628
|
-
// Check required
|
|
629
|
-
if (required && (value === null || value === undefined)) {
|
|
630
|
-
errors.push({
|
|
631
|
-
field: name,
|
|
632
|
-
message: `${name} is required`,
|
|
633
|
-
code: "REQUIRED",
|
|
634
|
-
});
|
|
635
|
-
return errors;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Skip further validation if value is empty and not required
|
|
639
|
-
if (value === null || value === undefined) {
|
|
640
|
-
return errors;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Type check based on multiple setting
|
|
644
|
-
if (multiple) {
|
|
645
|
-
if (!Array.isArray(value)) {
|
|
646
|
-
errors.push({
|
|
647
|
-
field: name,
|
|
648
|
-
message: `${name} must be an array of media asset IDs`,
|
|
649
|
-
code: "INVALID_TYPE",
|
|
650
|
-
});
|
|
651
|
-
return errors;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Check if required and empty array
|
|
655
|
-
if (required && value.length === 0) {
|
|
656
|
-
errors.push({
|
|
657
|
-
field: name,
|
|
658
|
-
message: `${name} requires at least one media asset`,
|
|
659
|
-
code: "REQUIRED",
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Check each item is a string (valid ID format)
|
|
664
|
-
for (const item of value) {
|
|
665
|
-
if (typeof item !== "string") {
|
|
666
|
-
errors.push({
|
|
667
|
-
field: name,
|
|
668
|
-
message: `${name} contains invalid media asset IDs`,
|
|
669
|
-
code: "INVALID_TYPE",
|
|
670
|
-
});
|
|
671
|
-
break;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Min items validation (only for multiple/gallery media fields)
|
|
676
|
-
if (options?.minItems !== undefined && value.length < options.minItems) {
|
|
677
|
-
errors.push({
|
|
678
|
-
field: name,
|
|
679
|
-
message: `${name} requires at least ${options.minItems} media asset${options.minItems === 1 ? "" : "s"}`,
|
|
680
|
-
code: "MIN_ITEMS",
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Max items (using max from options)
|
|
685
|
-
if (options?.max !== undefined && value.length > options.max) {
|
|
686
|
-
errors.push({
|
|
687
|
-
field: name,
|
|
688
|
-
message: `${name} can have at most ${options.max} media asset${options.max === 1 ? "" : "s"}`,
|
|
689
|
-
code: "MAX_ITEMS",
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
} else {
|
|
693
|
-
if (typeof value !== "string") {
|
|
694
|
-
errors.push({
|
|
695
|
-
field: name,
|
|
696
|
-
message: `${name} must be a media asset ID`,
|
|
697
|
-
code: "INVALID_TYPE",
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
return errors;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Validate a select field value against its configuration
|
|
707
|
-
*/
|
|
708
|
-
export function validateSelectField(
|
|
709
|
-
value: unknown,
|
|
710
|
-
fieldDef: FieldDefinition
|
|
711
|
-
): ValidationError[] {
|
|
712
|
-
const errors: ValidationError[] = [];
|
|
713
|
-
const { name, required, options } = fieldDef;
|
|
714
|
-
|
|
715
|
-
// Check required
|
|
716
|
-
if (required && (value === null || value === undefined || value === "")) {
|
|
717
|
-
errors.push({
|
|
718
|
-
field: name,
|
|
719
|
-
message: `${name} is required`,
|
|
720
|
-
code: "REQUIRED",
|
|
721
|
-
});
|
|
722
|
-
return errors;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Skip further validation if value is empty and not required
|
|
726
|
-
if (value === null || value === undefined || value === "") {
|
|
727
|
-
return errors;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Type check
|
|
731
|
-
if (typeof value !== "string") {
|
|
732
|
-
errors.push({
|
|
733
|
-
field: name,
|
|
734
|
-
message: `${name} must be a string`,
|
|
735
|
-
code: "INVALID_TYPE",
|
|
736
|
-
});
|
|
737
|
-
return errors;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Validate against allowed options
|
|
741
|
-
if (options?.options) {
|
|
742
|
-
const allowedValues = options.options.map((opt) => opt.value);
|
|
743
|
-
if (!allowedValues.includes(value)) {
|
|
744
|
-
errors.push({
|
|
745
|
-
field: name,
|
|
746
|
-
message: `${name} has an invalid value`,
|
|
747
|
-
code: "INVALID_TYPE",
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
return errors;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
/**
|
|
756
|
-
* Validate a multi-select field value against its configuration
|
|
757
|
-
*/
|
|
758
|
-
export function validateMultiSelectField(
|
|
759
|
-
value: unknown,
|
|
760
|
-
fieldDef: FieldDefinition
|
|
761
|
-
): ValidationError[] {
|
|
762
|
-
const errors: ValidationError[] = [];
|
|
763
|
-
const { name, required, options } = fieldDef;
|
|
764
|
-
|
|
765
|
-
// Check required
|
|
766
|
-
if (required && (value === null || value === undefined)) {
|
|
767
|
-
errors.push({
|
|
768
|
-
field: name,
|
|
769
|
-
message: `${name} is required`,
|
|
770
|
-
code: "REQUIRED",
|
|
771
|
-
});
|
|
772
|
-
return errors;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Skip further validation if value is empty and not required
|
|
776
|
-
if (value === null || value === undefined) {
|
|
777
|
-
return errors;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Type check - must be array
|
|
781
|
-
if (!Array.isArray(value)) {
|
|
782
|
-
errors.push({
|
|
783
|
-
field: name,
|
|
784
|
-
message: `${name} must be an array`,
|
|
785
|
-
code: "INVALID_TYPE",
|
|
786
|
-
});
|
|
787
|
-
return errors;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Check if required and empty array
|
|
791
|
-
if (required && value.length === 0) {
|
|
792
|
-
errors.push({
|
|
793
|
-
field: name,
|
|
794
|
-
message: `${name} requires at least one selection`,
|
|
795
|
-
code: "REQUIRED",
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// Validate each item is a string and in allowed options
|
|
800
|
-
const allowedValues = options?.options?.map((opt) => opt.value) ?? [];
|
|
801
|
-
for (const item of value) {
|
|
802
|
-
if (typeof item !== "string") {
|
|
803
|
-
errors.push({
|
|
804
|
-
field: name,
|
|
805
|
-
message: `${name} contains invalid values`,
|
|
806
|
-
code: "INVALID_TYPE",
|
|
807
|
-
});
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
if (allowedValues.length > 0 && !allowedValues.includes(item)) {
|
|
811
|
-
errors.push({
|
|
812
|
-
field: name,
|
|
813
|
-
message: `${name} contains an invalid option`,
|
|
814
|
-
code: "INVALID_TYPE",
|
|
815
|
-
});
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
return errors;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
/**
|
|
824
|
-
* Validate a JSON field value
|
|
825
|
-
*/
|
|
826
|
-
export function validateJsonField(
|
|
827
|
-
value: unknown,
|
|
828
|
-
fieldDef: FieldDefinition
|
|
829
|
-
): ValidationError[] {
|
|
830
|
-
const errors: ValidationError[] = [];
|
|
831
|
-
const { name, required } = fieldDef;
|
|
832
|
-
|
|
833
|
-
// Check required
|
|
834
|
-
if (required && (value === null || value === undefined)) {
|
|
835
|
-
errors.push({
|
|
836
|
-
field: name,
|
|
837
|
-
message: `${name} is required`,
|
|
838
|
-
code: "REQUIRED",
|
|
839
|
-
});
|
|
840
|
-
return errors;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// JSON fields can be any valid JSON value, so minimal type checking
|
|
844
|
-
// The value has already been parsed if it was a string
|
|
845
|
-
return errors;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Validate a tags field value against its configuration.
|
|
850
|
-
*
|
|
851
|
-
* Tags fields store arrays of taxonomy term IDs for flexible content categorization.
|
|
852
|
-
* They support:
|
|
853
|
-
* - Multiple term selection
|
|
854
|
-
* - Optional inline term creation (when allowCreate is true)
|
|
855
|
-
* - Min/max limits on number of tags
|
|
856
|
-
*
|
|
857
|
-
* Configuration options:
|
|
858
|
-
* - `taxonomyId`: The taxonomy these tags belong to (required at content type level)
|
|
859
|
-
* - `allowCreate`: If true, users can create new tags inline
|
|
860
|
-
* - `minTags`: Minimum number of tags required
|
|
861
|
-
* - `maxTags`: Maximum number of tags allowed
|
|
862
|
-
*
|
|
863
|
-
* @example
|
|
864
|
-
* ```typescript
|
|
865
|
-
* const tagsField: FieldDefinition = {
|
|
866
|
-
* name: "tags",
|
|
867
|
-
* label: "Tags",
|
|
868
|
-
* type: "tags",
|
|
869
|
-
* required: true,
|
|
870
|
-
* options: {
|
|
871
|
-
* taxonomyId: "tags_taxonomy_id",
|
|
872
|
-
* allowCreate: true,
|
|
873
|
-
* minTags: 1,
|
|
874
|
-
* maxTags: 10,
|
|
875
|
-
* },
|
|
876
|
-
* };
|
|
877
|
-
* ```
|
|
878
|
-
*/
|
|
879
|
-
export function validateTagsField(
|
|
880
|
-
value: unknown,
|
|
881
|
-
fieldDef: FieldDefinition
|
|
882
|
-
): ValidationError[] {
|
|
883
|
-
const errors: ValidationError[] = [];
|
|
884
|
-
const { name, required, options } = fieldDef;
|
|
885
|
-
|
|
886
|
-
// Check required
|
|
887
|
-
if (required && (value === null || value === undefined)) {
|
|
888
|
-
errors.push({
|
|
889
|
-
field: name,
|
|
890
|
-
message: `${name} is required`,
|
|
891
|
-
code: "REQUIRED",
|
|
892
|
-
});
|
|
893
|
-
return errors;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Skip further validation if value is empty and not required
|
|
897
|
-
if (value === null || value === undefined) {
|
|
898
|
-
return errors;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// Type check - must be array of strings (term IDs)
|
|
902
|
-
if (!Array.isArray(value)) {
|
|
903
|
-
errors.push({
|
|
904
|
-
field: name,
|
|
905
|
-
message: `${name} must be an array of tag IDs`,
|
|
906
|
-
code: "INVALID_TYPE",
|
|
907
|
-
});
|
|
908
|
-
return errors;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// Check if required and empty array
|
|
912
|
-
if (required && value.length === 0) {
|
|
913
|
-
errors.push({
|
|
914
|
-
field: name,
|
|
915
|
-
message: `${name} requires at least one tag`,
|
|
916
|
-
code: "REQUIRED",
|
|
917
|
-
});
|
|
918
|
-
return errors;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// Validate each item is a string
|
|
922
|
-
for (const item of value) {
|
|
923
|
-
if (typeof item !== "string") {
|
|
924
|
-
errors.push({
|
|
925
|
-
field: name,
|
|
926
|
-
message: `${name} contains invalid tag IDs`,
|
|
927
|
-
code: "INVALID_TYPE",
|
|
928
|
-
});
|
|
929
|
-
break;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// Min tags validation
|
|
934
|
-
const minTags = options?.minTags;
|
|
935
|
-
if (minTags !== undefined && value.length < minTags) {
|
|
936
|
-
errors.push({
|
|
937
|
-
field: name,
|
|
938
|
-
message: `${name} requires at least ${minTags} tag${minTags === 1 ? "" : "s"}`,
|
|
939
|
-
code: "MIN_ITEMS",
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// Max tags validation
|
|
944
|
-
const maxTags = options?.maxTags;
|
|
945
|
-
if (maxTags !== undefined && value.length > maxTags) {
|
|
946
|
-
errors.push({
|
|
947
|
-
field: name,
|
|
948
|
-
message: `${name} can have at most ${maxTags} tag${maxTags === 1 ? "" : "s"}`,
|
|
949
|
-
code: "MAX_ITEMS",
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
return errors;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Validate a category field value against its configuration.
|
|
958
|
-
*
|
|
959
|
-
* Category fields store taxonomy term IDs for hierarchical content organization.
|
|
960
|
-
* They support:
|
|
961
|
-
* - Single category selection (default)
|
|
962
|
-
* - Multiple category selection (when allowMultiple is true)
|
|
963
|
-
*
|
|
964
|
-
* Configuration options:
|
|
965
|
-
* - `taxonomyId`: The taxonomy these categories belong to (required at content type level)
|
|
966
|
-
* - `allowMultiple`: If true, accepts an array of category IDs
|
|
967
|
-
*
|
|
968
|
-
* @example
|
|
969
|
-
* ```typescript
|
|
970
|
-
* // Single category selection
|
|
971
|
-
* const categoryField: FieldDefinition = {
|
|
972
|
-
* name: "category",
|
|
973
|
-
* label: "Category",
|
|
974
|
-
* type: "category",
|
|
975
|
-
* required: true,
|
|
976
|
-
* options: {
|
|
977
|
-
* taxonomyId: "categories_taxonomy_id",
|
|
978
|
-
* },
|
|
979
|
-
* };
|
|
980
|
-
*
|
|
981
|
-
* // Multiple category selection
|
|
982
|
-
* const categoriesField: FieldDefinition = {
|
|
983
|
-
* name: "categories",
|
|
984
|
-
* label: "Categories",
|
|
985
|
-
* type: "category",
|
|
986
|
-
* required: true,
|
|
987
|
-
* options: {
|
|
988
|
-
* taxonomyId: "categories_taxonomy_id",
|
|
989
|
-
* allowMultiple: true,
|
|
990
|
-
* },
|
|
991
|
-
* };
|
|
992
|
-
* ```
|
|
993
|
-
*/
|
|
994
|
-
export function validateCategoryField(
|
|
995
|
-
value: unknown,
|
|
996
|
-
fieldDef: FieldDefinition
|
|
997
|
-
): ValidationError[] {
|
|
998
|
-
const errors: ValidationError[] = [];
|
|
999
|
-
const { name, required, options } = fieldDef;
|
|
1000
|
-
const allowMultiple = options?.allowMultiple ?? false;
|
|
1001
|
-
|
|
1002
|
-
// Check required
|
|
1003
|
-
if (required && (value === null || value === undefined)) {
|
|
1004
|
-
errors.push({
|
|
1005
|
-
field: name,
|
|
1006
|
-
message: `${name} is required`,
|
|
1007
|
-
code: "REQUIRED",
|
|
1008
|
-
});
|
|
1009
|
-
return errors;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// Skip further validation if value is empty and not required
|
|
1013
|
-
if (value === null || value === undefined) {
|
|
1014
|
-
return errors;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// Type check based on allowMultiple setting
|
|
1018
|
-
if (allowMultiple) {
|
|
1019
|
-
if (!Array.isArray(value)) {
|
|
1020
|
-
errors.push({
|
|
1021
|
-
field: name,
|
|
1022
|
-
message: `${name} must be an array of category IDs`,
|
|
1023
|
-
code: "INVALID_TYPE",
|
|
1024
|
-
});
|
|
1025
|
-
return errors;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// Check if required and empty array
|
|
1029
|
-
if (required && value.length === 0) {
|
|
1030
|
-
errors.push({
|
|
1031
|
-
field: name,
|
|
1032
|
-
message: `${name} requires at least one category`,
|
|
1033
|
-
code: "REQUIRED",
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Check each item is a string (valid ID format)
|
|
1038
|
-
for (const item of value) {
|
|
1039
|
-
if (typeof item !== "string") {
|
|
1040
|
-
errors.push({
|
|
1041
|
-
field: name,
|
|
1042
|
-
message: `${name} contains invalid category IDs`,
|
|
1043
|
-
code: "INVALID_TYPE",
|
|
1044
|
-
});
|
|
1045
|
-
break;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
} else {
|
|
1049
|
-
// Single category selection
|
|
1050
|
-
if (typeof value !== "string") {
|
|
1051
|
-
errors.push({
|
|
1052
|
-
field: name,
|
|
1053
|
-
message: `${name} must be a category ID`,
|
|
1054
|
-
code: "INVALID_TYPE",
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
return errors;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// =============================================================================
|
|
1063
|
-
// Main Validation Function
|
|
1064
|
-
// =============================================================================
|
|
1065
|
-
|
|
1066
|
-
/**
|
|
1067
|
-
* Options for validating localized fields.
|
|
1068
|
-
*/
|
|
1069
|
-
export interface LocalizedValidationOptions {
|
|
1070
|
-
/**
|
|
1071
|
-
* The locale to validate. If provided, only that locale's value is validated
|
|
1072
|
-
* for localized fields. If not provided, all locale values are validated.
|
|
1073
|
-
*/
|
|
1074
|
-
locale?: string;
|
|
1075
|
-
|
|
1076
|
-
/**
|
|
1077
|
-
* Locales that must have values for required localized fields.
|
|
1078
|
-
* If not provided, only checks if at least one locale has a value for required fields.
|
|
1079
|
-
*/
|
|
1080
|
-
requiredLocales?: string[];
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/**
|
|
1084
|
-
* Validate a single field value (non-localized) based on its type.
|
|
1085
|
-
* This is the core validation logic that handles the actual value checking.
|
|
1086
|
-
*/
|
|
1087
|
-
function validateSingleValue(
|
|
1088
|
-
value: unknown,
|
|
1089
|
-
fieldDef: FieldDefinition
|
|
1090
|
-
): ValidationError[] {
|
|
1091
|
-
const { name, type } = fieldDef;
|
|
1092
|
-
|
|
1093
|
-
switch (type) {
|
|
1094
|
-
case "text":
|
|
1095
|
-
return validateTextField(value, fieldDef);
|
|
1096
|
-
case "richText":
|
|
1097
|
-
return validateRichTextField(value, fieldDef);
|
|
1098
|
-
case "number":
|
|
1099
|
-
return validateNumberField(value, fieldDef);
|
|
1100
|
-
case "boolean":
|
|
1101
|
-
return validateBooleanField(value, fieldDef);
|
|
1102
|
-
case "date":
|
|
1103
|
-
case "datetime":
|
|
1104
|
-
return validateDateField(value, fieldDef);
|
|
1105
|
-
case "reference":
|
|
1106
|
-
return validateReferenceField(value, fieldDef);
|
|
1107
|
-
case "media":
|
|
1108
|
-
return validateMediaField(value, fieldDef);
|
|
1109
|
-
case "select":
|
|
1110
|
-
return validateSelectField(value, fieldDef);
|
|
1111
|
-
case "multiSelect":
|
|
1112
|
-
return validateMultiSelectField(value, fieldDef);
|
|
1113
|
-
case "json":
|
|
1114
|
-
return validateJsonField(value, fieldDef);
|
|
1115
|
-
case "tags":
|
|
1116
|
-
return validateTagsField(value, fieldDef);
|
|
1117
|
-
case "category":
|
|
1118
|
-
return validateCategoryField(value, fieldDef);
|
|
1119
|
-
default: {
|
|
1120
|
-
// Unknown field type
|
|
1121
|
-
return [
|
|
1122
|
-
{
|
|
1123
|
-
field: name,
|
|
1124
|
-
message: `Unknown field type: ${type}`,
|
|
1125
|
-
code: "INVALID_TYPE",
|
|
1126
|
-
},
|
|
1127
|
-
];
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
/**
|
|
1133
|
-
* Validate a localized field value.
|
|
1134
|
-
*
|
|
1135
|
-
* For localized fields, the value should be a LocalizedFieldValue structure:
|
|
1136
|
-
* `{ "en-US": "Hello", "es-ES": "Hola" }`
|
|
1137
|
-
*
|
|
1138
|
-
* This function validates:
|
|
1139
|
-
* 1. The structure is a valid LocalizedFieldValue
|
|
1140
|
-
* 2. Each locale's value passes the field type validation
|
|
1141
|
-
* 3. Required locales have values (if specified)
|
|
1142
|
-
*
|
|
1143
|
-
* @param value - The localized field value to validate
|
|
1144
|
-
* @param fieldDef - The field definition
|
|
1145
|
-
* @param options - Validation options for localized fields
|
|
1146
|
-
* @returns Array of validation errors
|
|
1147
|
-
*/
|
|
1148
|
-
export function validateLocalizedFieldValue(
|
|
1149
|
-
value: unknown,
|
|
1150
|
-
fieldDef: FieldDefinition,
|
|
1151
|
-
options: LocalizedValidationOptions = {}
|
|
1152
|
-
): ValidationError[] {
|
|
1153
|
-
const errors: ValidationError[] = [];
|
|
1154
|
-
const { name, required } = fieldDef;
|
|
1155
|
-
const { locale, requiredLocales } = options;
|
|
1156
|
-
|
|
1157
|
-
// Handle null/undefined for required fields
|
|
1158
|
-
if (value === null || value === undefined) {
|
|
1159
|
-
if (required) {
|
|
1160
|
-
errors.push({
|
|
1161
|
-
field: name,
|
|
1162
|
-
message: `${name} is required`,
|
|
1163
|
-
code: "REQUIRED",
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
return errors;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// Check if the value is a valid LocalizedFieldValue structure
|
|
1170
|
-
if (!isLocalizedFieldValue(value)) {
|
|
1171
|
-
errors.push({
|
|
1172
|
-
field: name,
|
|
1173
|
-
message: `${name} must be a localized field structure (object with locale codes as keys)`,
|
|
1174
|
-
code: "INVALID_LOCALIZED_STRUCTURE",
|
|
1175
|
-
});
|
|
1176
|
-
return errors;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
const localizedValue = value as LocalizedFieldValue;
|
|
1180
|
-
const locales = Object.keys(localizedValue);
|
|
1181
|
-
|
|
1182
|
-
// Check if required and empty
|
|
1183
|
-
if (required && locales.length === 0) {
|
|
1184
|
-
errors.push({
|
|
1185
|
-
field: name,
|
|
1186
|
-
message: `${name} requires at least one locale value`,
|
|
1187
|
-
code: "REQUIRED",
|
|
1188
|
-
});
|
|
1189
|
-
return errors;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// Check required locales
|
|
1193
|
-
if (requiredLocales && requiredLocales.length > 0) {
|
|
1194
|
-
for (const requiredLocale of requiredLocales) {
|
|
1195
|
-
if (!(requiredLocale in localizedValue)) {
|
|
1196
|
-
errors.push({
|
|
1197
|
-
field: name,
|
|
1198
|
-
message: `${name} is missing required translation for locale: ${requiredLocale}`,
|
|
1199
|
-
code: "MISSING_LOCALE",
|
|
1200
|
-
});
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// If a specific locale is specified, validate only that locale
|
|
1206
|
-
if (locale) {
|
|
1207
|
-
if (locale in localizedValue) {
|
|
1208
|
-
// Create a non-localized field definition for single value validation
|
|
1209
|
-
const nonLocalizedFieldDef = { ...fieldDef, localized: false };
|
|
1210
|
-
const localeErrors = validateSingleValue(
|
|
1211
|
-
localizedValue[locale],
|
|
1212
|
-
nonLocalizedFieldDef
|
|
1213
|
-
);
|
|
1214
|
-
// Prefix errors with locale info
|
|
1215
|
-
for (const error of localeErrors) {
|
|
1216
|
-
errors.push({
|
|
1217
|
-
...error,
|
|
1218
|
-
field: `${name}[${locale}]`,
|
|
1219
|
-
message: `${name} (${locale}): ${error.message.replace(`${name} `, "")}`,
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
} else {
|
|
1224
|
-
// Validate all locale values
|
|
1225
|
-
for (const [localeCode, localeValue] of Object.entries(localizedValue)) {
|
|
1226
|
-
// Create a non-localized field definition for single value validation
|
|
1227
|
-
const nonLocalizedFieldDef = { ...fieldDef, localized: false, required: false };
|
|
1228
|
-
const localeErrors = validateSingleValue(localeValue, nonLocalizedFieldDef);
|
|
1229
|
-
// Prefix errors with locale info
|
|
1230
|
-
for (const error of localeErrors) {
|
|
1231
|
-
errors.push({
|
|
1232
|
-
...error,
|
|
1233
|
-
field: `${name}[${localeCode}]`,
|
|
1234
|
-
message: `${name} (${localeCode}): ${error.message.replace(`${name} `, "")}`,
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
return errors;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
/**
|
|
1244
|
-
* Validate a single field value based on its definition.
|
|
1245
|
-
*
|
|
1246
|
-
* Handles both localized and non-localized fields:
|
|
1247
|
-
* - Non-localized fields: Validates the value directly
|
|
1248
|
-
* - Localized fields: Validates the LocalizedFieldValue structure and each locale's value
|
|
1249
|
-
*
|
|
1250
|
-
* @param value - The field value to validate (plain value or LocalizedFieldValue)
|
|
1251
|
-
* @param fieldDef - The field definition
|
|
1252
|
-
* @param options - Optional validation options for localized fields
|
|
1253
|
-
* @returns Array of validation errors
|
|
1254
|
-
*/
|
|
1255
|
-
export function validateFieldValue(
|
|
1256
|
-
value: unknown,
|
|
1257
|
-
fieldDef: FieldDefinition,
|
|
1258
|
-
options?: LocalizedValidationOptions
|
|
1259
|
-
): ValidationError[] {
|
|
1260
|
-
// Check if this is a localized field
|
|
1261
|
-
if (fieldDef.localized) {
|
|
1262
|
-
return validateLocalizedFieldValue(value, fieldDef, options);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// Non-localized field - use standard validation
|
|
1266
|
-
return validateSingleValue(value, fieldDef);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
/**
|
|
1270
|
-
* Options for validating content data.
|
|
1271
|
-
*/
|
|
1272
|
-
export interface ContentValidationOptions {
|
|
1273
|
-
/**
|
|
1274
|
-
* If true, reports unknown fields as errors.
|
|
1275
|
-
* If false (default), unknown fields are silently ignored.
|
|
1276
|
-
*/
|
|
1277
|
-
strictFields?: boolean;
|
|
1278
|
-
|
|
1279
|
-
/**
|
|
1280
|
-
* Locale to validate for localized fields.
|
|
1281
|
-
* If provided, only that locale's values are validated.
|
|
1282
|
-
*/
|
|
1283
|
-
locale?: string;
|
|
1284
|
-
|
|
1285
|
-
/**
|
|
1286
|
-
* Locales that must have values for required localized fields.
|
|
1287
|
-
*/
|
|
1288
|
-
requiredLocales?: string[];
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
/**
|
|
1292
|
-
* Validate content data against a content type schema
|
|
1293
|
-
*
|
|
1294
|
-
* @param data - The content data to validate
|
|
1295
|
-
* @param schema - The content type schema defining expected fields
|
|
1296
|
-
* @param options - Validation options
|
|
1297
|
-
* @returns ValidationResult with any errors found
|
|
1298
|
-
*
|
|
1299
|
-
* @example
|
|
1300
|
-
* ```typescript
|
|
1301
|
-
* // Basic validation
|
|
1302
|
-
* const result = validateContentData(data, schema);
|
|
1303
|
-
*
|
|
1304
|
-
* // Validate with localized field support
|
|
1305
|
-
* const result = validateContentData(data, schema, {
|
|
1306
|
-
* locale: "en-US",
|
|
1307
|
-
* requiredLocales: ["en-US", "es-ES"],
|
|
1308
|
-
* });
|
|
1309
|
-
* ```
|
|
1310
|
-
*/
|
|
1311
|
-
export function validateContentData(
|
|
1312
|
-
data: ContentData,
|
|
1313
|
-
schema: ContentTypeSchema,
|
|
1314
|
-
options: ContentValidationOptions = {}
|
|
1315
|
-
): ValidationResult {
|
|
1316
|
-
const errors: ValidationError[] = [];
|
|
1317
|
-
const fieldMap = new Map(schema.fields.map((f) => [f.name, f]));
|
|
1318
|
-
const { strictFields, locale, requiredLocales } = options;
|
|
1319
|
-
|
|
1320
|
-
// Create localized validation options
|
|
1321
|
-
const localizedOptions: LocalizedValidationOptions = {
|
|
1322
|
-
locale,
|
|
1323
|
-
requiredLocales,
|
|
1324
|
-
};
|
|
1325
|
-
|
|
1326
|
-
// Validate each defined field
|
|
1327
|
-
for (const fieldDef of schema.fields) {
|
|
1328
|
-
const value = data[fieldDef.name];
|
|
1329
|
-
const fieldErrors = validateFieldValue(value, fieldDef, localizedOptions);
|
|
1330
|
-
errors.push(...fieldErrors);
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
// Check for unknown fields if strict mode
|
|
1334
|
-
if (strictFields) {
|
|
1335
|
-
for (const key of Object.keys(data)) {
|
|
1336
|
-
if (!fieldMap.has(key)) {
|
|
1337
|
-
errors.push({
|
|
1338
|
-
field: key,
|
|
1339
|
-
message: `Unknown field: ${key}`,
|
|
1340
|
-
code: "UNKNOWN_FIELD",
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
if (errors.length === 0) {
|
|
1347
|
-
return { valid: true, errors: [] };
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
return { valid: false, errors };
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
/**
|
|
1354
|
-
* Apply default values to content data based on field definitions
|
|
1355
|
-
*/
|
|
1356
|
-
export function applyFieldDefaults(
|
|
1357
|
-
data: ContentData,
|
|
1358
|
-
schema: ContentTypeSchema
|
|
1359
|
-
): ContentData {
|
|
1360
|
-
const result = { ...data };
|
|
1361
|
-
|
|
1362
|
-
for (const fieldDef of schema.fields) {
|
|
1363
|
-
const { name, defaultValue } = fieldDef;
|
|
1364
|
-
|
|
1365
|
-
// Only apply default if field is not already set
|
|
1366
|
-
if (result[name] === undefined || result[name] === null) {
|
|
1367
|
-
if (defaultValue !== undefined) {
|
|
1368
|
-
result[name] = defaultValue;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
return result;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
/**
|
|
1377
|
-
* Get the field type from a field definition
|
|
1378
|
-
*/
|
|
1379
|
-
export function getFieldType(fieldDef: FieldDefinition): FieldType {
|
|
1380
|
-
return fieldDef.type;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
/**
|
|
1384
|
-
* Check if a field is required based on its configuration
|
|
1385
|
-
*/
|
|
1386
|
-
export function isFieldRequired(fieldDef: FieldDefinition): boolean {
|
|
1387
|
-
return fieldDef.required === true;
|
|
1388
|
-
}
|