@@ -70,11 +70,21 @@ function validateField(
7070 }
7171
7272 // ── Pure field validation (type, required, min/max, pattern, select) ──
73- const fieldErrors = validateFieldValue ( value , def )
74- if ( fieldErrors . length > 0 ) {
75- errors . push ( ...fieldErrors . map ( e => ( { ...e , ...errCtx } ) ) )
76- // If there are errors from pure validation, skip stateful checks
77- if ( fieldErrors . some ( e => e . severity === 'error' ) ) return errors
73+ // Skip types' validateFieldValue for relation/relations — Studio handles these with richer checks below
74+ const skipTypesValidation = def . type === 'relation' || def . type === 'relations'
75+ if ( ! skipTypesValidation ) {
76+ const fieldErrors = validateFieldValue ( value , def )
77+ if ( fieldErrors . length > 0 ) {
78+ errors . push ( ...fieldErrors . map ( ( e : ValidationError ) => ( { ...e , ...errCtx } ) ) )
79+ if ( fieldErrors . some ( ( e : ValidationError ) => e . severity === 'error' ) ) return errors
80+ }
81+ }
82+ else {
83+ // For relations, only check required ourselves
84+ if ( def . required && ( value === null || value === undefined || value === '' ) ) {
85+ errors . push ( { severity : 'error' , ...errCtx , message : `${ fieldId } is required` } )
86+ return errors
87+ }
7888 }
7989
8090 // Skip further checks if value is absent
@@ -91,6 +101,60 @@ function validateField(
91101 }
92102 }
93103
104+ // ── Supplementary checks not covered by validateFieldValue ──
105+
106+ // Email/URL heuristic warnings
107+ if ( def . type === 'email' && typeof value === 'string' && ! / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / . test ( value ) ) {
108+ errors . push ( { severity : 'warning' , ...errCtx , message : `${ fieldId } may not be a valid email` } )
109+ }
110+ if ( def . type === 'url' && typeof value === 'string' && ! / ^ h t t p s ? : \/ \/ .+ / . test ( value ) && ! value . startsWith ( '/' ) ) {
111+ errors . push ( { severity : 'warning' , ...errCtx , message : `${ fieldId } may not be a valid URL` } )
112+ }
113+
114+ // Relation: polymorphic model validation + scalar type check
115+ if ( def . type === 'relation' && def . model ) {
116+ const targets = Array . isArray ( def . model ) ? def . model : [ def . model ]
117+ if ( targets . length > 1 ) {
118+ if ( typeof value !== 'object' || value === null || ! ( 'model' in value ) || ! ( 'ref' in value ) ) {
119+ errors . push ( { severity : 'error' , ...errCtx , message : `${ fieldId } must be { model, ref } for polymorphic relation` } )
120+ }
121+ else {
122+ const polyVal = value as { model : string , ref : string }
123+ if ( ! targets . includes ( polyVal . model ) ) {
124+ errors . push ( { severity : 'error' , ...errCtx , message : `${ fieldId } target model "${ polyVal . model } " must be one of: ${ targets . join ( ', ' ) } ` } )
125+ }
126+ }
127+ }
128+ else if ( typeof value !== 'string' ) {
129+ errors . push ( { severity : 'error' , ...errCtx , message : `${ fieldId } must be a string (entry ID or slug)` } )
130+ }
131+ }
132+
133+ // Relations: array of string IDs + min/max
134+ if ( def . type === 'relations' ) {
135+ if ( ! Array . isArray ( value ) ) {
136+ errors . push ( { severity : 'error' , ...errCtx , message : `${ fieldId } must be an array` } )
137+ }
138+ else {
139+ if ( def . min !== undefined && value . length < def . min )
140+ errors . push ( { severity : 'error' , ...errCtx , message : `${ fieldId } must have at least ${ def . min } items` } )
141+ if ( def . max !== undefined && value . length > def . max )
142+ errors . push ( { severity : 'error' , ...errCtx , message : `${ fieldId } must have at most ${ def . max } items` } )
143+ for ( let i = 0 ; i < value . length ; i ++ ) {
144+ if ( typeof value [ i ] !== 'string' )
145+ errors . push ( { severity : 'error' , ...errCtx , message : `${ fieldId } [${ i } ] must be a string (entry ID or slug)` } )
146+ }
147+ }
148+ }
149+
150+ // Array: simple item type validation
151+ if ( def . type === 'array' && Array . isArray ( value ) && def . items && typeof def . items === 'string' ) {
152+ for ( let i = 0 ; i < value . length ; i ++ ) {
153+ const itemErrors = validateArrayItemType ( value [ i ] , def . items , errCtx , `${ fieldId } [${ i } ]` )
154+ errors . push ( ...itemErrors )
155+ }
156+ }
157+
94158 // ── Nested object validation ──
95159 if ( def . type === 'object' && def . fields && typeof value === 'object' && value !== null && ! Array . isArray ( value ) ) {
96160 const nested = validateContent ( value as Record < string , unknown > , def . fields , modelId , locale , entryId )
@@ -110,6 +174,29 @@ function validateField(
110174 return errors
111175}
112176
177+ /** Validate simple array item types (not covered by types' validateFieldValue) */
178+ function validateArrayItemType (
179+ value : unknown ,
180+ itemType : string ,
181+ errCtx : { model : string , locale : string , entry : string | undefined , field : string } ,
182+ fieldPath : string ,
183+ ) : ValidationError [ ] {
184+ const ctx = { ...errCtx , field : fieldPath }
185+ switch ( itemType ) {
186+ case 'string' : case 'email' : case 'url' : case 'slug' : case 'image' : case 'video' : case 'file' :
187+ if ( typeof value !== 'string' ) return [ { severity : 'error' , ...ctx , message : `${ fieldPath } must be a string` } ]
188+ break
189+ case 'number' : case 'integer' : case 'decimal' :
190+ if ( typeof value !== 'number' ) return [ { severity : 'error' , ...ctx , message : `${ fieldPath } must be a number` } ]
191+ if ( itemType === 'integer' && ! Number . isInteger ( value ) ) return [ { severity : 'error' , ...ctx , message : `${ fieldPath } must be an integer` } ]
192+ break
193+ case 'boolean' :
194+ if ( typeof value !== 'boolean' ) return [ { severity : 'error' , ...ctx , message : `${ fieldPath } must be a boolean` } ]
195+ break
196+ }
197+ return [ ]
198+ }
199+
113200/**
114201 * Check relation referential integrity.
115202 * Verifies that relation field values point to existing entries in target models.
0 commit comments