@@ -3,17 +3,13 @@ import { errors } from '@strapi/utils';
33const validationMiddleware = async ( context , next ) => {
44 const { uid, action, params } = context ;
55 // Run this middleware only for the publisher action.
6- if ( uid !== 'plugin::publisher.action' ) {
7- return next ( ) ;
8- }
6+ if ( uid !== 'plugin::publisher.action' ) return next ( ) ;
97
108 // Run it only for the create and update actions.
11- if ( action !== 'create' && action !== 'update' ) {
12- return next ( ) ;
13- }
9+ if ( action !== 'create' && action !== 'update' ) return next ( ) ;
1410
1511 // The create action will have the data directly.
16- let publisherAction = params . data ;
12+ let publisherAction = params ? .data ;
1713
1814 // The update action might have incomplete data, so we need to fetch it.
1915 if ( action === 'update' ) {
@@ -25,13 +21,11 @@ const validationMiddleware = async (context, next) => {
2521 // The complete, and possibly updated, publisher action.
2622 const { entityId, entitySlug, mode, locale : actionLocale } = {
2723 ...publisherAction ,
28- ...params . data ,
24+ ...params ? .data ,
2925 } ;
3026
3127 // Run it only for the publish mode.
32- if ( mode !== 'publish' ) {
33- return next ( ) ;
34- }
28+ if ( mode !== 'publish' ) return next ( ) ;
3529
3630 const populateBuilderService = strapi . plugin ( 'content-manager' ) . service ( 'populate-builder' ) ;
3731 const populate = await populateBuilderService ( entitySlug ) . populateDeep ( Infinity ) . build ( ) ;
@@ -46,28 +40,136 @@ const validationMiddleware = async (context, next) => {
4640
4741 if ( ! draft ) {
4842 throw new errors . NotFoundError (
49- `No draft found for ${ entitySlug } with documentId "${ entityId } "${ actionLocale ? ` and locale "${ actionLocale } ".` : '.' } `
43+ `No draft found for ${ entitySlug } with documentId "${ entityId } "${
44+ actionLocale ? ` and locale "${ actionLocale } ".` : '.'
45+ } `
5046 ) ;
5147 }
5248
53- // If no locale was provided in params.data, fill it in from the draft
49+ // If no locale was provided in params.data, fill it in from the draft.
5450 const locale = actionLocale || draft . locale ;
5551
56- // Fetch the published entity in this same locale
52+ // Fetch the published entity in this same locale.
5753 const published = await strapi . documents ( entitySlug ) . findOne ( {
5854 documentId : entityId ,
5955 status : 'published' ,
6056 locale,
6157 populate,
6258 } ) ;
6359
64- // Validate the draft before scheduling the publication.
65- await strapi . entityValidator . validateEntityCreation (
66- strapi . contentType ( entitySlug ) ,
67- draft ,
68- { isDraft : false , locale } ,
69- published
70- ) ;
60+ const model = strapi . contentType ( entitySlug ) ;
61+
62+ // ---------- helpers ----------
63+ const isEmptyValue = ( value , { multiple, repeatable } ) => {
64+ if ( multiple || repeatable ) return ! Array . isArray ( value ) || value . length === 0 ;
65+ return value === null || value === undefined ;
66+ } ;
67+
68+ // Minimal custom check: only required media/relations + nested structure inside components/DZ.
69+ const collectRequiredMissing = ( schema , dataNode , pathArr = [ ] ) => {
70+ const errs = [ ] ;
71+ const attrs = schema ?. attributes || { } ;
72+
73+ for ( const [ name , attr ] of Object . entries ( attrs ) ) {
74+ const nextPath = [ ...pathArr , name ] ;
75+ const value = dataNode ? dataNode [ name ] : undefined ;
76+
77+ // Media fields
78+ if ( attr . type === 'media' ) {
79+ if ( attr . required && isEmptyValue ( value , { multiple : ! ! attr . multiple } ) ) {
80+ errs . push ( { path : nextPath , message : 'This field is required' } ) ;
81+ }
82+ continue ;
83+ }
84+
85+ // Relations
86+ if ( attr . type === 'relation' ) {
87+ const many =
88+ [ 'oneToMany' , 'manyToMany' , 'morphToMany' ] . includes ( attr . relation ) ||
89+ ( typeof attr . relation === 'string' && attr . relation . toLowerCase ( ) . includes ( 'many' ) ) ;
90+ if ( attr . required && isEmptyValue ( value , { multiple : many } ) ) {
91+ errs . push ( { path : nextPath , message : 'This field is required' } ) ;
92+ }
93+ continue ;
94+ }
95+
96+ // Components (repeatable or single)
97+ if ( attr . type === 'component' ) {
98+ if ( attr . required && isEmptyValue ( value , { repeatable : ! ! attr . repeatable } ) ) {
99+ errs . push ( { path : nextPath , message : 'This field is required' } ) ;
100+ continue ;
101+ }
102+ const compSchema = strapi . components [ attr . component ] ;
103+ if ( attr . repeatable && Array . isArray ( value ) ) {
104+ value . forEach ( ( item , idx ) => {
105+ errs . push ( ...collectRequiredMissing ( compSchema , item , [ ...nextPath , idx ] ) ) ;
106+ } ) ;
107+ } else if ( value ) {
108+ errs . push ( ...collectRequiredMissing ( compSchema , value , nextPath ) ) ;
109+ }
110+ continue ;
111+ }
112+
113+ // Dynamic zones
114+ if ( attr . type === 'dynamiczone' ) {
115+ if ( attr . required && ( ! Array . isArray ( value ) || value . length === 0 ) ) {
116+ errs . push ( { path : nextPath , message : 'This field is required' } ) ;
117+ continue ;
118+ }
119+ if ( Array . isArray ( value ) ) {
120+ value . forEach ( ( dzItem , idx ) => {
121+ const compUid = dzItem ?. __component ;
122+ if ( ! compUid ) return ;
123+ const compSchema = strapi . components [ compUid ] ;
124+ errs . push ( ...collectRequiredMissing ( compSchema , dzItem , [ ...nextPath , idx ] ) ) ;
125+ } ) ;
126+ }
127+ continue ;
128+ }
129+ }
130+
131+ return errs ;
132+ } ;
133+
134+ // ---------- run core validator, normalize, and (optionally) add extras ----------
135+ try {
136+ await strapi . entityValidator . validateEntityCreation (
137+ model ,
138+ draft ,
139+ { isDraft : false , locale } ,
140+ published
141+ ) ;
142+ } catch ( e ) {
143+ const name = e ?. name || e ?. constructor ?. name ;
144+ const isValidationLike =
145+ Array . isArray ( e ?. details ?. errors ) || / V a l i d a t i o n E r r o r / i. test ( name || '' ) ;
146+
147+ if ( isValidationLike ) {
148+ // Use core errors and supplement with missing media/relations if needed.
149+ const core = ( e . details ?. errors || [ ] ) . map ( ( er ) => ( {
150+ path : er . path || er . name || '' ,
151+ message : er . message || 'This field is required' ,
152+ } ) ) ;
153+ const extras = collectRequiredMissing ( model , draft ) ;
154+ const merged = [ ...core , ...extras ] ;
155+
156+ throw new errors . ValidationError (
157+ 'There are validation errors in your document. Please fix them so you can publish.' ,
158+ { errors : merged }
159+ ) ;
160+ }
161+
162+ throw e ;
163+ }
164+
165+ // Enforce required media/relations even if core validator passed
166+ const extrasAfterPass = collectRequiredMissing ( model , draft ) ;
167+ if ( extrasAfterPass . length > 0 ) {
168+ throw new errors . ValidationError (
169+ 'There are validation errors in your document. Please fix them so you can publish.' ,
170+ { errors : extrasAfterPass }
171+ ) ;
172+ }
71173
72174 return next ( ) ;
73175} ;
0 commit comments