strapi-plugin-publish-media-validation 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +106 -10
- package/dist/server/index.js.map +1 -1
- package/package.json +4 -4
- package/src/server/__tests__/index.test.ts +276 -10
- package/src/server/index.ts +123 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAO,MAAM,gBAAgB,CAAC;;2BAoHlB;QAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAA;KAAE;uBAiC3B;QAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAA;KAAE;qBACzB;QAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAA;KAAE;;AAE/C,wBAAgD"}
|
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,106 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const
|
|
6
|
+
const path_1 = __importDefault(require("path"));
|
|
7
|
+
function resolveValidationError() {
|
|
8
|
+
var _a, _b, _c;
|
|
9
|
+
const sep = path_1.default.sep;
|
|
10
|
+
for (const [filename, mod] of Object.entries((_a = require.cache) !== null && _a !== void 0 ? _a : {})) {
|
|
11
|
+
if (filename.includes(`@strapi${sep}utils`) &&
|
|
12
|
+
filename.endsWith('index.js') &&
|
|
13
|
+
((_c = (_b = mod === null || mod === void 0 ? void 0 : mod.exports) === null || _b === void 0 ? void 0 : _b.errors) === null || _c === void 0 ? void 0 : _c.ValidationError)) {
|
|
14
|
+
return mod.exports.errors.ValidationError;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return class extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'ValidationError';
|
|
21
|
+
this.status = 400;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Recursively build a Strapi populate object that covers every media field
|
|
27
|
+
* (at any depth) inside the given model, including inside components and
|
|
28
|
+
* dynamic zones. The visited set prevents infinite loops from circular
|
|
29
|
+
* component references.
|
|
30
|
+
*/
|
|
31
|
+
function buildPopulate(modelUID, strapi, visited = new Set()) {
|
|
32
|
+
var _a;
|
|
33
|
+
if (visited.has(modelUID))
|
|
34
|
+
return {};
|
|
35
|
+
const seen = new Set(visited);
|
|
36
|
+
seen.add(modelUID);
|
|
37
|
+
const model = strapi.getModel(modelUID);
|
|
38
|
+
if (!(model === null || model === void 0 ? void 0 : model.attributes))
|
|
39
|
+
return {};
|
|
40
|
+
const populate = {};
|
|
41
|
+
for (const [key, attr] of Object.entries(model.attributes)) {
|
|
42
|
+
const a = attr;
|
|
43
|
+
if (a.type === 'media') {
|
|
44
|
+
populate[key] = true;
|
|
45
|
+
}
|
|
46
|
+
else if (a.type === 'component') {
|
|
47
|
+
const nested = buildPopulate(a.component, strapi, seen);
|
|
48
|
+
// Always include the component so we can inspect its fields; fall back
|
|
49
|
+
// to populate:'*' when there are no explicitly known nested keys.
|
|
50
|
+
populate[key] = { populate: Object.keys(nested).length ? nested : '*' };
|
|
51
|
+
}
|
|
52
|
+
else if (a.type === 'dynamiczone') {
|
|
53
|
+
// Merge field names from every possible component type so that
|
|
54
|
+
// Strapi populates whichever fields exist on each concrete item.
|
|
55
|
+
const merged = {};
|
|
56
|
+
for (const compUID of ((_a = a.components) !== null && _a !== void 0 ? _a : [])) {
|
|
57
|
+
const nested = buildPopulate(compUID, strapi, seen);
|
|
58
|
+
Object.assign(merged, nested);
|
|
59
|
+
}
|
|
60
|
+
populate[key] = { populate: Object.keys(merged).length ? merged : '*' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return populate;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Walk the fetched document data and collect human-readable paths for every
|
|
67
|
+
* required media field that is empty. Handles nested components and dynamic
|
|
68
|
+
* zones recursively.
|
|
69
|
+
*/
|
|
70
|
+
function collectMissingMedia(data, modelUID, strapi) {
|
|
71
|
+
if (!data)
|
|
72
|
+
return [];
|
|
73
|
+
const model = strapi.getModel(modelUID);
|
|
74
|
+
if (!(model === null || model === void 0 ? void 0 : model.attributes))
|
|
75
|
+
return [];
|
|
76
|
+
const missing = [];
|
|
77
|
+
for (const [key, attr] of Object.entries(model.attributes)) {
|
|
78
|
+
const a = attr;
|
|
79
|
+
if (a.type === 'media' && a.required === true) {
|
|
80
|
+
if (!data[key]) {
|
|
81
|
+
missing.push(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (a.type === 'component' && data[key] != null) {
|
|
85
|
+
const items = Array.isArray(data[key]) ? data[key] : [data[key]];
|
|
86
|
+
items.forEach((item, i) => {
|
|
87
|
+
const prefix = Array.isArray(data[key]) ? `${key}[${i + 1}]` : key;
|
|
88
|
+
collectMissingMedia(item, a.component, strapi).forEach((f) => missing.push(`${prefix} > ${f}`));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else if (a.type === 'dynamiczone' && Array.isArray(data[key])) {
|
|
92
|
+
data[key].forEach((item, i) => {
|
|
93
|
+
const compUID = item === null || item === void 0 ? void 0 : item.__component;
|
|
94
|
+
if (compUID) {
|
|
95
|
+
collectMissingMedia(item, compUID, strapi).forEach((f) => missing.push(`${key}[${i + 1}] > ${f}`));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return missing;
|
|
101
|
+
}
|
|
4
102
|
const register = ({ strapi }) => {
|
|
103
|
+
const ValidationError = resolveValidationError();
|
|
5
104
|
strapi.documents.use(async (ctx, next) => {
|
|
6
105
|
var _a;
|
|
7
106
|
if (ctx.action !== 'publish')
|
|
@@ -9,25 +108,22 @@ const register = ({ strapi }) => {
|
|
|
9
108
|
const model = strapi.getModel(ctx.uid);
|
|
10
109
|
if (!(model === null || model === void 0 ? void 0 : model.attributes))
|
|
11
110
|
return next();
|
|
12
|
-
const requiredMediaFields = Object.entries(model.attributes)
|
|
13
|
-
.filter(([, attr]) => attr.type === 'media' && attr.required === true)
|
|
14
|
-
.map(([key]) => key);
|
|
15
|
-
if (requiredMediaFields.length === 0)
|
|
16
|
-
return next();
|
|
17
111
|
const documentId = (_a = ctx.params) === null || _a === void 0 ? void 0 : _a.documentId;
|
|
18
112
|
if (!documentId)
|
|
19
113
|
return next();
|
|
20
|
-
const populate =
|
|
114
|
+
const populate = buildPopulate(ctx.uid, strapi);
|
|
115
|
+
if (Object.keys(populate).length === 0)
|
|
116
|
+
return next();
|
|
21
117
|
const doc = await strapi.documents(ctx.uid).findOne({
|
|
22
118
|
documentId,
|
|
23
119
|
populate,
|
|
24
120
|
});
|
|
25
|
-
const missing =
|
|
121
|
+
const missing = collectMissingMedia(doc, ctx.uid, strapi);
|
|
26
122
|
if (missing.length > 0) {
|
|
27
123
|
const labels = missing
|
|
28
|
-
.map((f) => f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1'))
|
|
124
|
+
.map((f) => f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1').replace(/_/g, ' '))
|
|
29
125
|
.join(', ');
|
|
30
|
-
throw new
|
|
126
|
+
throw new ValidationError(`The following required media fields are empty: ${labels}`);
|
|
31
127
|
}
|
|
32
128
|
return next();
|
|
33
129
|
});
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-publish-media-validation",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Strapi v5 plugin that enforces required media fields at publish time — works around the known limitation where required: true on media fields is not validated on publish.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
],
|
|
14
14
|
"main": "./dist/server/index.js",
|
|
15
15
|
"exports": {
|
|
16
|
-
"./strapi-server": "./dist/server/index.js"
|
|
16
|
+
"./strapi-server": "./dist/server/index.js",
|
|
17
|
+
"./package.json": "./package.json"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"dist",
|
|
@@ -40,8 +41,7 @@
|
|
|
40
41
|
}
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
|
-
"@strapi/strapi": ">=5.0.0"
|
|
44
|
-
"@strapi/utils": ">=5.0.0"
|
|
44
|
+
"@strapi/strapi": ">=5.0.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@strapi/strapi": "5.44.0",
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { errors } from '@strapi/utils';
|
|
2
2
|
import plugin from '../index';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const baseCtx = {
|
|
5
|
+
action: 'publish',
|
|
6
|
+
uid: 'api::article.article',
|
|
7
|
+
params: { documentId: 'abc123' },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const ARTICLE_UID = 'api::article.article';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Simple helper: getModel always returns the same model regardless of UID.
|
|
18
|
+
* Use for flat content-type tests with no components or dynamic zones.
|
|
19
|
+
*/
|
|
5
20
|
function buildStrapi(
|
|
6
21
|
modelAttributes: Record<string, unknown> | null,
|
|
7
22
|
docFields: Record<string, unknown> | null = {}
|
|
@@ -10,7 +25,7 @@ function buildStrapi(
|
|
|
10
25
|
|
|
11
26
|
const strapi = {
|
|
12
27
|
documents: Object.assign(
|
|
13
|
-
(
|
|
28
|
+
(_uid: string) => ({
|
|
14
29
|
findOne: jest.fn().mockResolvedValue(docFields),
|
|
15
30
|
}),
|
|
16
31
|
{
|
|
@@ -28,7 +43,7 @@ function buildStrapi(
|
|
|
28
43
|
|
|
29
44
|
return {
|
|
30
45
|
strapi,
|
|
31
|
-
runMiddleware: (ctx: Record<string, unknown>) => {
|
|
46
|
+
runMiddleware: (ctx: Record<string, unknown> = baseCtx) => {
|
|
32
47
|
if (!capturedMiddleware) throw new Error('middleware not registered');
|
|
33
48
|
const next = jest.fn().mockResolvedValue(undefined);
|
|
34
49
|
return { result: capturedMiddleware(ctx, next), next };
|
|
@@ -36,11 +51,48 @@ function buildStrapi(
|
|
|
36
51
|
};
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Multi-model helper: getModel returns different attributes per UID.
|
|
56
|
+
* Required for tests involving components or dynamic zones.
|
|
57
|
+
*/
|
|
58
|
+
function buildStrapiFromModels(
|
|
59
|
+
modelsByUid: Record<string, Record<string, unknown>>,
|
|
60
|
+
docFields: Record<string, unknown> | null = {}
|
|
61
|
+
) {
|
|
62
|
+
let capturedMiddleware: Function | null = null;
|
|
63
|
+
|
|
64
|
+
const strapi = {
|
|
65
|
+
documents: Object.assign(
|
|
66
|
+
(_uid: string) => ({
|
|
67
|
+
findOne: jest.fn().mockResolvedValue(docFields),
|
|
68
|
+
}),
|
|
69
|
+
{
|
|
70
|
+
use: jest.fn((fn: Function) => {
|
|
71
|
+
capturedMiddleware = fn;
|
|
72
|
+
}),
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
getModel: jest.fn((uid: string) => {
|
|
76
|
+
const attrs = modelsByUid[uid];
|
|
77
|
+
return attrs !== undefined ? { attributes: attrs } : null;
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
plugin.register({ strapi: strapi as any });
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
strapi,
|
|
85
|
+
runMiddleware: (ctx: Record<string, unknown> = baseCtx) => {
|
|
86
|
+
if (!capturedMiddleware) throw new Error('middleware not registered');
|
|
87
|
+
const next = jest.fn().mockResolvedValue(undefined);
|
|
88
|
+
return { result: capturedMiddleware(ctx, next), next };
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Registration
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
44
96
|
|
|
45
97
|
describe('register', () => {
|
|
46
98
|
it('calls strapi.documents.use once', () => {
|
|
@@ -49,7 +101,11 @@ describe('register', () => {
|
|
|
49
101
|
});
|
|
50
102
|
});
|
|
51
103
|
|
|
52
|
-
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Flat (top-level) media fields
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
describe('middleware — flat media fields', () => {
|
|
53
109
|
it('passes through when action is not publish', async () => {
|
|
54
110
|
const { runMiddleware } = buildStrapi({});
|
|
55
111
|
const { next } = runMiddleware({ ...baseCtx, action: 'create' });
|
|
@@ -64,6 +120,15 @@ describe('middleware', () => {
|
|
|
64
120
|
expect(next).toHaveBeenCalled();
|
|
65
121
|
});
|
|
66
122
|
|
|
123
|
+
it('passes through when model has no media fields at all', async () => {
|
|
124
|
+
const { runMiddleware } = buildStrapi({
|
|
125
|
+
title: { type: 'string', required: true },
|
|
126
|
+
});
|
|
127
|
+
const { next } = runMiddleware(baseCtx);
|
|
128
|
+
await next;
|
|
129
|
+
expect(next).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
67
132
|
it('passes through when model has no required media fields', async () => {
|
|
68
133
|
const { runMiddleware } = buildStrapi({
|
|
69
134
|
title: { type: 'string', required: true },
|
|
@@ -129,7 +194,7 @@ describe('middleware', () => {
|
|
|
129
194
|
expect(next).toHaveBeenCalled();
|
|
130
195
|
});
|
|
131
196
|
|
|
132
|
-
it('
|
|
197
|
+
it('builds populate that includes all media fields when fetching the document', async () => {
|
|
133
198
|
const findOne = jest.fn().mockResolvedValue({ cover: { id: 1 } });
|
|
134
199
|
const strapi = {
|
|
135
200
|
documents: Object.assign(() => ({ findOne }), {
|
|
@@ -151,3 +216,204 @@ describe('middleware', () => {
|
|
|
151
216
|
});
|
|
152
217
|
});
|
|
153
218
|
});
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Component fields
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
describe('middleware — component fields', () => {
|
|
225
|
+
const HERO_UID = 'sections.hero';
|
|
226
|
+
|
|
227
|
+
it('passes through when component required media is populated', async () => {
|
|
228
|
+
const { runMiddleware } = buildStrapiFromModels(
|
|
229
|
+
{
|
|
230
|
+
[ARTICLE_UID]: { hero: { type: 'component', component: HERO_UID } },
|
|
231
|
+
[HERO_UID]: { photo: { type: 'media', required: true } },
|
|
232
|
+
},
|
|
233
|
+
{ hero: { photo: { id: 1 } } }
|
|
234
|
+
);
|
|
235
|
+
const { result, next } = runMiddleware();
|
|
236
|
+
await result;
|
|
237
|
+
expect(next).toHaveBeenCalled();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('throws when component required media is empty', async () => {
|
|
241
|
+
const { runMiddleware } = buildStrapiFromModels(
|
|
242
|
+
{
|
|
243
|
+
[ARTICLE_UID]: { hero: { type: 'component', component: HERO_UID } },
|
|
244
|
+
[HERO_UID]: { photo: { type: 'media', required: true } },
|
|
245
|
+
},
|
|
246
|
+
{ hero: { photo: null } }
|
|
247
|
+
);
|
|
248
|
+
const { result } = runMiddleware();
|
|
249
|
+
await expect(result).rejects.toThrow(errors.ValidationError);
|
|
250
|
+
await expect(result).rejects.toThrow('Hero > photo');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('passes through when all repeatable component items have required media', async () => {
|
|
254
|
+
const { runMiddleware } = buildStrapiFromModels(
|
|
255
|
+
{
|
|
256
|
+
[ARTICLE_UID]: { cards: { type: 'component', component: HERO_UID, repeatable: true } },
|
|
257
|
+
[HERO_UID]: { photo: { type: 'media', required: true } },
|
|
258
|
+
},
|
|
259
|
+
{ cards: [{ photo: { id: 1 } }, { photo: { id: 2 } }] }
|
|
260
|
+
);
|
|
261
|
+
const { result, next } = runMiddleware();
|
|
262
|
+
await result;
|
|
263
|
+
expect(next).toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('reports the correct index for a missing repeatable component item', async () => {
|
|
267
|
+
const { runMiddleware } = buildStrapiFromModels(
|
|
268
|
+
{
|
|
269
|
+
[ARTICLE_UID]: { cards: { type: 'component', component: HERO_UID, repeatable: true } },
|
|
270
|
+
[HERO_UID]: { photo: { type: 'media', required: true } },
|
|
271
|
+
},
|
|
272
|
+
{ cards: [{ photo: { id: 1 } }, { photo: null }] }
|
|
273
|
+
);
|
|
274
|
+
const { result } = runMiddleware();
|
|
275
|
+
await expect(result).rejects.toThrow('Cards[2] > photo');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('builds populate that includes the component media field', async () => {
|
|
279
|
+
const findOne = jest.fn().mockResolvedValue({ hero: { photo: { id: 1 } } });
|
|
280
|
+
let capturedMiddleware: Function | null = null;
|
|
281
|
+
const strapi = {
|
|
282
|
+
documents: Object.assign(() => ({ findOne }), {
|
|
283
|
+
use: jest.fn((fn: Function) => {
|
|
284
|
+
capturedMiddleware = fn;
|
|
285
|
+
}),
|
|
286
|
+
}),
|
|
287
|
+
getModel: jest.fn((uid: string) => {
|
|
288
|
+
if (uid === ARTICLE_UID) return { attributes: { hero: { type: 'component', component: HERO_UID } } };
|
|
289
|
+
if (uid === HERO_UID) return { attributes: { photo: { type: 'media', required: true } } };
|
|
290
|
+
return null;
|
|
291
|
+
}),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
plugin.register({ strapi: strapi as any });
|
|
295
|
+
await capturedMiddleware!(baseCtx, jest.fn().mockResolvedValue(undefined));
|
|
296
|
+
|
|
297
|
+
expect(findOne).toHaveBeenCalledWith({
|
|
298
|
+
documentId: 'abc123',
|
|
299
|
+
populate: { hero: { populate: { photo: true } } },
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Dynamic zone fields
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
describe('middleware — dynamiczone fields', () => {
|
|
309
|
+
const COPY_PHOTO_UID = 'sections.copy-photo';
|
|
310
|
+
const TEXT_UID = 'sections.text';
|
|
311
|
+
|
|
312
|
+
it('passes through when all blocks have required media populated', async () => {
|
|
313
|
+
const { runMiddleware } = buildStrapiFromModels(
|
|
314
|
+
{
|
|
315
|
+
[ARTICLE_UID]: { blocks: { type: 'dynamiczone', components: [COPY_PHOTO_UID] } },
|
|
316
|
+
[COPY_PHOTO_UID]: { photo: { type: 'media', required: true } },
|
|
317
|
+
},
|
|
318
|
+
{ blocks: [{ __component: COPY_PHOTO_UID, photo: { id: 1 } }] }
|
|
319
|
+
);
|
|
320
|
+
const { result, next } = runMiddleware();
|
|
321
|
+
await result;
|
|
322
|
+
expect(next).toHaveBeenCalled();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('throws when a block is missing required media', async () => {
|
|
326
|
+
const { runMiddleware } = buildStrapiFromModels(
|
|
327
|
+
{
|
|
328
|
+
[ARTICLE_UID]: { blocks: { type: 'dynamiczone', components: [COPY_PHOTO_UID] } },
|
|
329
|
+
[COPY_PHOTO_UID]: { photo: { type: 'media', required: true } },
|
|
330
|
+
},
|
|
331
|
+
{ blocks: [{ __component: COPY_PHOTO_UID, photo: null }] }
|
|
332
|
+
);
|
|
333
|
+
const { result } = runMiddleware();
|
|
334
|
+
await expect(result).rejects.toThrow(errors.ValidationError);
|
|
335
|
+
await expect(result).rejects.toThrow('Blocks[1] > photo');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('reports the correct block index in a multi-block dynamic zone', async () => {
|
|
339
|
+
const { runMiddleware } = buildStrapiFromModels(
|
|
340
|
+
{
|
|
341
|
+
[ARTICLE_UID]: { blocks: { type: 'dynamiczone', components: [COPY_PHOTO_UID] } },
|
|
342
|
+
[COPY_PHOTO_UID]: { photo: { type: 'media', required: true } },
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
blocks: [
|
|
346
|
+
{ __component: COPY_PHOTO_UID, photo: { id: 1 } },
|
|
347
|
+
{ __component: COPY_PHOTO_UID, photo: null },
|
|
348
|
+
],
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
const { result } = runMiddleware();
|
|
352
|
+
await expect(result).rejects.toThrow('Blocks[2] > photo');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('skips blocks whose component type has no required media', async () => {
|
|
356
|
+
const { runMiddleware } = buildStrapiFromModels(
|
|
357
|
+
{
|
|
358
|
+
[ARTICLE_UID]: { blocks: { type: 'dynamiczone', components: [COPY_PHOTO_UID, TEXT_UID] } },
|
|
359
|
+
[COPY_PHOTO_UID]: { photo: { type: 'media', required: true } },
|
|
360
|
+
[TEXT_UID]: { content: { type: 'richtext' } },
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
blocks: [
|
|
364
|
+
{ __component: TEXT_UID, content: 'hello' },
|
|
365
|
+
{ __component: COPY_PHOTO_UID, photo: { id: 1 } },
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
const { result, next } = runMiddleware();
|
|
370
|
+
await result;
|
|
371
|
+
expect(next).toHaveBeenCalled();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('throws only for the failing block in a mixed dynamic zone', async () => {
|
|
375
|
+
const { runMiddleware } = buildStrapiFromModels(
|
|
376
|
+
{
|
|
377
|
+
[ARTICLE_UID]: { blocks: { type: 'dynamiczone', components: [COPY_PHOTO_UID, TEXT_UID] } },
|
|
378
|
+
[COPY_PHOTO_UID]: { photo: { type: 'media', required: true } },
|
|
379
|
+
[TEXT_UID]: { content: { type: 'richtext' } },
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
blocks: [
|
|
383
|
+
{ __component: TEXT_UID, content: 'hello' },
|
|
384
|
+
{ __component: COPY_PHOTO_UID, photo: null },
|
|
385
|
+
],
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
const { result } = runMiddleware();
|
|
389
|
+
await expect(result).rejects.toThrow('Blocks[2] > photo');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('builds populate that merges media fields from all component types', async () => {
|
|
393
|
+
const findOne = jest.fn().mockResolvedValue({
|
|
394
|
+
blocks: [{ __component: COPY_PHOTO_UID, photo: { id: 1 } }],
|
|
395
|
+
});
|
|
396
|
+
let capturedMiddleware: Function | null = null;
|
|
397
|
+
const strapi = {
|
|
398
|
+
documents: Object.assign(() => ({ findOne }), {
|
|
399
|
+
use: jest.fn((fn: Function) => {
|
|
400
|
+
capturedMiddleware = fn;
|
|
401
|
+
}),
|
|
402
|
+
}),
|
|
403
|
+
getModel: jest.fn((uid: string) => {
|
|
404
|
+
if (uid === ARTICLE_UID) return { attributes: { blocks: { type: 'dynamiczone', components: [COPY_PHOTO_UID, TEXT_UID] } } };
|
|
405
|
+
if (uid === COPY_PHOTO_UID) return { attributes: { photo: { type: 'media', required: true } } };
|
|
406
|
+
if (uid === TEXT_UID) return { attributes: { content: { type: 'richtext' } } };
|
|
407
|
+
return null;
|
|
408
|
+
}),
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
plugin.register({ strapi: strapi as any });
|
|
412
|
+
await capturedMiddleware!(baseCtx, jest.fn().mockResolvedValue(undefined));
|
|
413
|
+
|
|
414
|
+
expect(findOne).toHaveBeenCalledWith({
|
|
415
|
+
documentId: 'abc123',
|
|
416
|
+
populate: { blocks: { populate: { photo: true } } },
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
package/src/server/index.ts
CHANGED
|
@@ -1,37 +1,147 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import type { Core, UID } from '@strapi/strapi';
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
type ValidationErrorCtor = new (message: string) => Error;
|
|
5
|
+
|
|
6
|
+
function resolveValidationError(): ValidationErrorCtor {
|
|
7
|
+
const sep = path.sep;
|
|
8
|
+
for (const [filename, mod] of Object.entries(require.cache ?? {})) {
|
|
9
|
+
if (
|
|
10
|
+
filename.includes(`@strapi${sep}utils`) &&
|
|
11
|
+
filename.endsWith('index.js') &&
|
|
12
|
+
(mod as any)?.exports?.errors?.ValidationError
|
|
13
|
+
) {
|
|
14
|
+
return (mod as any).exports.errors.ValidationError;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return class extends Error {
|
|
18
|
+
constructor(message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'ValidationError';
|
|
21
|
+
(this as any).status = 400;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Recursively build a Strapi populate object that covers every media field
|
|
28
|
+
* (at any depth) inside the given model, including inside components and
|
|
29
|
+
* dynamic zones. The visited set prevents infinite loops from circular
|
|
30
|
+
* component references.
|
|
31
|
+
*/
|
|
32
|
+
function buildPopulate(
|
|
33
|
+
modelUID: string,
|
|
34
|
+
strapi: Core.Strapi,
|
|
35
|
+
visited = new Set<string>()
|
|
36
|
+
): Record<string, any> {
|
|
37
|
+
if (visited.has(modelUID)) return {};
|
|
38
|
+
const seen = new Set(visited);
|
|
39
|
+
seen.add(modelUID);
|
|
40
|
+
|
|
41
|
+
const model = strapi.getModel(modelUID as UID.Schema);
|
|
42
|
+
if (!model?.attributes) return {};
|
|
43
|
+
|
|
44
|
+
const populate: Record<string, any> = {};
|
|
45
|
+
|
|
46
|
+
for (const [key, attr] of Object.entries(model.attributes)) {
|
|
47
|
+
const a = attr as any;
|
|
48
|
+
|
|
49
|
+
if (a.type === 'media') {
|
|
50
|
+
populate[key] = true;
|
|
51
|
+
} else if (a.type === 'component') {
|
|
52
|
+
const nested = buildPopulate(a.component, strapi, seen);
|
|
53
|
+
// Always include the component so we can inspect its fields; fall back
|
|
54
|
+
// to populate:'*' when there are no explicitly known nested keys.
|
|
55
|
+
populate[key] = { populate: Object.keys(nested).length ? nested : '*' };
|
|
56
|
+
} else if (a.type === 'dynamiczone') {
|
|
57
|
+
// Merge field names from every possible component type so that
|
|
58
|
+
// Strapi populates whichever fields exist on each concrete item.
|
|
59
|
+
const merged: Record<string, any> = {};
|
|
60
|
+
for (const compUID of (a.components ?? [])) {
|
|
61
|
+
const nested = buildPopulate(compUID, strapi, seen);
|
|
62
|
+
Object.assign(merged, nested);
|
|
63
|
+
}
|
|
64
|
+
populate[key] = { populate: Object.keys(merged).length ? merged : '*' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return populate;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Walk the fetched document data and collect human-readable paths for every
|
|
73
|
+
* required media field that is empty. Handles nested components and dynamic
|
|
74
|
+
* zones recursively.
|
|
75
|
+
*/
|
|
76
|
+
function collectMissingMedia(
|
|
77
|
+
data: any,
|
|
78
|
+
modelUID: string,
|
|
79
|
+
strapi: Core.Strapi
|
|
80
|
+
): string[] {
|
|
81
|
+
if (!data) return [];
|
|
82
|
+
|
|
83
|
+
const model = strapi.getModel(modelUID as UID.Schema);
|
|
84
|
+
if (!model?.attributes) return [];
|
|
85
|
+
|
|
86
|
+
const missing: string[] = [];
|
|
87
|
+
|
|
88
|
+
for (const [key, attr] of Object.entries(model.attributes)) {
|
|
89
|
+
const a = attr as any;
|
|
90
|
+
|
|
91
|
+
if (a.type === 'media' && a.required === true) {
|
|
92
|
+
if (!data[key]) {
|
|
93
|
+
missing.push(key);
|
|
94
|
+
}
|
|
95
|
+
} else if (a.type === 'component' && data[key] != null) {
|
|
96
|
+
const items: any[] = Array.isArray(data[key]) ? data[key] : [data[key]];
|
|
97
|
+
items.forEach((item, i) => {
|
|
98
|
+
const prefix = Array.isArray(data[key]) ? `${key}[${i + 1}]` : key;
|
|
99
|
+
collectMissingMedia(item, a.component, strapi).forEach((f) =>
|
|
100
|
+
missing.push(`${prefix} > ${f}`)
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
} else if (a.type === 'dynamiczone' && Array.isArray(data[key])) {
|
|
104
|
+
data[key].forEach((item: any, i: number) => {
|
|
105
|
+
const compUID: string | undefined = item?.__component;
|
|
106
|
+
if (compUID) {
|
|
107
|
+
collectMissingMedia(item, compUID, strapi).forEach((f) =>
|
|
108
|
+
missing.push(`${key}[${i + 1}] > ${f}`)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return missing;
|
|
116
|
+
}
|
|
3
117
|
|
|
4
118
|
const register = ({ strapi }: { strapi: Core.Strapi }) => {
|
|
119
|
+
const ValidationError = resolveValidationError();
|
|
120
|
+
|
|
5
121
|
strapi.documents.use(async (ctx, next) => {
|
|
6
122
|
if (ctx.action !== 'publish') return next();
|
|
7
123
|
|
|
8
124
|
const model = strapi.getModel(ctx.uid as UID.ContentType);
|
|
9
125
|
if (!model?.attributes) return next();
|
|
10
126
|
|
|
11
|
-
const requiredMediaFields = Object.entries(model.attributes)
|
|
12
|
-
.filter(([, attr]) => (attr as any).type === 'media' && (attr as any).required === true)
|
|
13
|
-
.map(([key]) => key);
|
|
14
|
-
|
|
15
|
-
if (requiredMediaFields.length === 0) return next();
|
|
16
|
-
|
|
17
127
|
const documentId = (ctx.params as { documentId?: string })?.documentId;
|
|
18
128
|
if (!documentId) return next();
|
|
19
129
|
|
|
20
|
-
const populate =
|
|
130
|
+
const populate = buildPopulate(ctx.uid as string, strapi);
|
|
131
|
+
if (Object.keys(populate).length === 0) return next();
|
|
132
|
+
|
|
21
133
|
const doc = await strapi.documents(ctx.uid as UID.ContentType).findOne({
|
|
22
134
|
documentId,
|
|
23
135
|
populate,
|
|
24
136
|
});
|
|
25
137
|
|
|
26
|
-
const missing =
|
|
138
|
+
const missing = collectMissingMedia(doc, ctx.uid as string, strapi);
|
|
27
139
|
|
|
28
140
|
if (missing.length > 0) {
|
|
29
141
|
const labels = missing
|
|
30
|
-
.map((f) => f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1'))
|
|
142
|
+
.map((f) => f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1').replace(/_/g, ' '))
|
|
31
143
|
.join(', ');
|
|
32
|
-
throw new
|
|
33
|
-
`The following required media fields are empty: ${labels}`
|
|
34
|
-
);
|
|
144
|
+
throw new ValidationError(`The following required media fields are empty: ${labels}`);
|
|
35
145
|
}
|
|
36
146
|
|
|
37
147
|
return next();
|