@strapi/content-releases 0.0.0-next.e1ede8c55a0e1e22ce20137bf238fc374bd5dd51 → 0.0.0-next.f4ff842a3cb7b83db540bee67554b704e042b042
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/_chunks/App-dLXY5ei3.js +1353 -0
- package/dist/_chunks/App-dLXY5ei3.js.map +1 -0
- package/dist/_chunks/App-jrh58sXY.mjs +1330 -0
- package/dist/_chunks/App-jrh58sXY.mjs.map +1 -0
- package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs +51 -0
- package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +1 -0
- package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js +51 -0
- package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +1 -0
- package/dist/_chunks/{en-haKSQIo8.js → en-HrREghh3.js} +31 -7
- package/dist/_chunks/en-HrREghh3.js.map +1 -0
- package/dist/_chunks/{en-ngTk74JV.mjs → en-ltT1TlKQ.mjs} +31 -7
- package/dist/_chunks/en-ltT1TlKQ.mjs.map +1 -0
- package/dist/_chunks/{index-EdBmRHRU.js → index-CVO0Rqdm.js} +551 -64
- package/dist/_chunks/index-CVO0Rqdm.js.map +1 -0
- package/dist/_chunks/{index-XAQOX_IB.mjs → index-PiOGBETy.mjs} +570 -83
- package/dist/_chunks/index-PiOGBETy.mjs.map +1 -0
- package/dist/admin/index.js +2 -1
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +3 -2
- package/dist/admin/index.mjs.map +1 -1
- package/dist/server/index.js +1192 -299
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1191 -300
- package/dist/server/index.mjs.map +1 -1
- package/package.json +16 -12
- package/dist/_chunks/App-g2P5kbSm.mjs +0 -945
- package/dist/_chunks/App-g2P5kbSm.mjs.map +0 -1
- package/dist/_chunks/App-o5_WfqR-.js +0 -967
- package/dist/_chunks/App-o5_WfqR-.js.map +0 -1
- package/dist/_chunks/en-haKSQIo8.js.map +0 -1
- package/dist/_chunks/en-ngTk74JV.mjs.map +0 -1
- package/dist/_chunks/index-EdBmRHRU.js.map +0 -1
- package/dist/_chunks/index-XAQOX_IB.mjs.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const utils = require("@strapi/utils");
|
|
3
|
+
const isEqual = require("lodash/isEqual");
|
|
4
|
+
const lodash = require("lodash");
|
|
3
5
|
const _ = require("lodash/fp");
|
|
6
|
+
const EE = require("@strapi/strapi/dist/utils/ee");
|
|
7
|
+
const nodeSchedule = require("node-schedule");
|
|
4
8
|
const yup = require("yup");
|
|
5
9
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
6
10
|
function _interopNamespace(e) {
|
|
@@ -21,7 +25,9 @@ function _interopNamespace(e) {
|
|
|
21
25
|
n.default = e;
|
|
22
26
|
return Object.freeze(n);
|
|
23
27
|
}
|
|
28
|
+
const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
|
|
24
29
|
const ___default = /* @__PURE__ */ _interopDefault(_);
|
|
30
|
+
const EE__default = /* @__PURE__ */ _interopDefault(EE);
|
|
25
31
|
const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
|
|
26
32
|
const RELEASE_MODEL_UID = "plugin::content-releases.release";
|
|
27
33
|
const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
|
|
@@ -69,10 +75,361 @@ const ACTIONS = [
|
|
|
69
75
|
pluginName: "content-releases"
|
|
70
76
|
}
|
|
71
77
|
];
|
|
72
|
-
const
|
|
78
|
+
const ALLOWED_WEBHOOK_EVENTS = {
|
|
79
|
+
RELEASES_PUBLISH: "releases.publish"
|
|
80
|
+
};
|
|
81
|
+
const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
|
|
82
|
+
return strapi2.plugin("content-releases").service(name);
|
|
83
|
+
};
|
|
84
|
+
const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
|
|
85
|
+
const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
|
|
86
|
+
const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
|
|
87
|
+
const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
|
|
88
|
+
return entry;
|
|
89
|
+
};
|
|
90
|
+
const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
|
|
91
|
+
try {
|
|
92
|
+
await strapi2.entityValidator.validateEntityCreation(
|
|
93
|
+
strapi2.getModel(contentTypeUid),
|
|
94
|
+
entry,
|
|
95
|
+
void 0,
|
|
96
|
+
// @ts-expect-error - FIXME: entity here is unnecessary
|
|
97
|
+
entry
|
|
98
|
+
);
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
async function deleteActionsOnDisableDraftAndPublish({
|
|
105
|
+
oldContentTypes,
|
|
106
|
+
contentTypes: contentTypes2
|
|
107
|
+
}) {
|
|
108
|
+
if (!oldContentTypes) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
for (const uid in contentTypes2) {
|
|
112
|
+
if (!oldContentTypes[uid]) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const oldContentType = oldContentTypes[uid];
|
|
116
|
+
const contentType = contentTypes2[uid];
|
|
117
|
+
if (utils.contentTypes.hasDraftAndPublish(oldContentType) && !utils.contentTypes.hasDraftAndPublish(contentType)) {
|
|
118
|
+
await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
|
|
123
|
+
const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
|
|
124
|
+
if (deletedContentTypes.length) {
|
|
125
|
+
await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
|
|
126
|
+
return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function migrateIsValidAndStatusReleases() {
|
|
131
|
+
const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
|
|
132
|
+
where: {
|
|
133
|
+
status: null,
|
|
134
|
+
releasedAt: null
|
|
135
|
+
},
|
|
136
|
+
populate: {
|
|
137
|
+
actions: {
|
|
138
|
+
populate: {
|
|
139
|
+
entry: true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
utils.mapAsync(releasesWithoutStatus, async (release2) => {
|
|
145
|
+
const actions = release2.actions;
|
|
146
|
+
const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
|
|
147
|
+
for (const action of notValidatedActions) {
|
|
148
|
+
if (action.entry) {
|
|
149
|
+
const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
|
|
150
|
+
strapi
|
|
151
|
+
});
|
|
152
|
+
if (populatedEntry) {
|
|
153
|
+
const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
|
|
154
|
+
await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
|
|
155
|
+
where: {
|
|
156
|
+
id: action.id
|
|
157
|
+
},
|
|
158
|
+
data: {
|
|
159
|
+
isEntryValid
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return getService("release", { strapi }).updateReleaseStatus(release2.id);
|
|
166
|
+
});
|
|
167
|
+
const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
|
|
168
|
+
where: {
|
|
169
|
+
status: null,
|
|
170
|
+
releasedAt: {
|
|
171
|
+
$notNull: true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
utils.mapAsync(publishedReleases, async (release2) => {
|
|
176
|
+
return strapi.db.query(RELEASE_MODEL_UID).update({
|
|
177
|
+
where: {
|
|
178
|
+
id: release2.id
|
|
179
|
+
},
|
|
180
|
+
data: {
|
|
181
|
+
status: "done"
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
|
|
187
|
+
if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
|
|
188
|
+
const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
|
|
189
|
+
(uid) => oldContentTypes[uid]?.options?.draftAndPublish
|
|
190
|
+
);
|
|
191
|
+
const releasesAffected = /* @__PURE__ */ new Set();
|
|
192
|
+
utils.mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
|
|
193
|
+
const oldContentType = oldContentTypes[contentTypeUID];
|
|
194
|
+
const contentType = contentTypes2[contentTypeUID];
|
|
195
|
+
if (!isEqual__default.default(oldContentType?.attributes, contentType?.attributes)) {
|
|
196
|
+
const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
|
|
197
|
+
where: {
|
|
198
|
+
contentType: contentTypeUID
|
|
199
|
+
},
|
|
200
|
+
populate: {
|
|
201
|
+
entry: true,
|
|
202
|
+
release: true
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
await utils.mapAsync(actions, async (action) => {
|
|
206
|
+
if (action.entry && action.release) {
|
|
207
|
+
const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
|
|
208
|
+
strapi
|
|
209
|
+
});
|
|
210
|
+
if (populatedEntry) {
|
|
211
|
+
const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
|
|
212
|
+
strapi
|
|
213
|
+
});
|
|
214
|
+
releasesAffected.add(action.release.id);
|
|
215
|
+
await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
|
|
216
|
+
where: {
|
|
217
|
+
id: action.id
|
|
218
|
+
},
|
|
219
|
+
data: {
|
|
220
|
+
isEntryValid
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}).then(() => {
|
|
228
|
+
utils.mapAsync(releasesAffected, async (releaseId) => {
|
|
229
|
+
return getService("release", { strapi }).updateReleaseStatus(releaseId);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
|
|
235
|
+
if (!oldContentTypes) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const i18nPlugin = strapi.plugin("i18n");
|
|
239
|
+
if (!i18nPlugin) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
for (const uid in contentTypes2) {
|
|
243
|
+
if (!oldContentTypes[uid]) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const oldContentType = oldContentTypes[uid];
|
|
247
|
+
const contentType = contentTypes2[uid];
|
|
248
|
+
const { isLocalizedContentType } = i18nPlugin.service("content-types");
|
|
249
|
+
if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
|
|
250
|
+
await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
|
|
251
|
+
locale: null
|
|
252
|
+
}).where({ contentType: uid }).execute();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
|
|
257
|
+
if (!oldContentTypes) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const i18nPlugin = strapi.plugin("i18n");
|
|
261
|
+
if (!i18nPlugin) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
for (const uid in contentTypes2) {
|
|
265
|
+
if (!oldContentTypes[uid]) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const oldContentType = oldContentTypes[uid];
|
|
269
|
+
const contentType = contentTypes2[uid];
|
|
270
|
+
const { isLocalizedContentType } = i18nPlugin.service("content-types");
|
|
271
|
+
const { getDefaultLocale } = i18nPlugin.service("locales");
|
|
272
|
+
if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
|
|
273
|
+
const defaultLocale = await getDefaultLocale();
|
|
274
|
+
await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
|
|
275
|
+
locale: defaultLocale
|
|
276
|
+
}).where({ contentType: uid }).execute();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
|
|
73
281
|
const register = async ({ strapi: strapi2 }) => {
|
|
74
|
-
if (features$
|
|
282
|
+
if (features$2.isEnabled("cms-content-releases")) {
|
|
75
283
|
await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
|
|
284
|
+
strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
|
|
285
|
+
strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
|
|
286
|
+
}
|
|
287
|
+
if (strapi2.plugin("graphql")) {
|
|
288
|
+
const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
|
|
289
|
+
graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
|
|
290
|
+
graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
|
|
294
|
+
const bootstrap = async ({ strapi: strapi2 }) => {
|
|
295
|
+
if (features$1.isEnabled("cms-content-releases")) {
|
|
296
|
+
const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
|
|
297
|
+
(uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
|
|
298
|
+
);
|
|
299
|
+
strapi2.db.lifecycles.subscribe({
|
|
300
|
+
models: contentTypesWithDraftAndPublish,
|
|
301
|
+
async afterDelete(event) {
|
|
302
|
+
try {
|
|
303
|
+
const { model, result } = event;
|
|
304
|
+
if (model.kind === "collectionType" && model.options?.draftAndPublish) {
|
|
305
|
+
const { id } = result;
|
|
306
|
+
const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
|
|
307
|
+
where: {
|
|
308
|
+
actions: {
|
|
309
|
+
target_type: model.uid,
|
|
310
|
+
target_id: id
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
|
|
315
|
+
where: {
|
|
316
|
+
target_type: model.uid,
|
|
317
|
+
target_id: id
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
for (const release2 of releases) {
|
|
321
|
+
getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
strapi2.log.error("Error while deleting release actions after entry delete", { error });
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
/**
|
|
329
|
+
* deleteMany hook doesn't return the deleted entries ids
|
|
330
|
+
* so we need to fetch them before deleting the entries to save the ids on our state
|
|
331
|
+
*/
|
|
332
|
+
async beforeDeleteMany(event) {
|
|
333
|
+
const { model, params } = event;
|
|
334
|
+
if (model.kind === "collectionType" && model.options?.draftAndPublish) {
|
|
335
|
+
const { where } = params;
|
|
336
|
+
const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
|
|
337
|
+
event.state.entriesToDelete = entriesToDelete;
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
/**
|
|
341
|
+
* We delete the release actions related to deleted entries
|
|
342
|
+
* We make this only after deleteMany is succesfully executed to avoid errors
|
|
343
|
+
*/
|
|
344
|
+
async afterDeleteMany(event) {
|
|
345
|
+
try {
|
|
346
|
+
const { model, state } = event;
|
|
347
|
+
const entriesToDelete = state.entriesToDelete;
|
|
348
|
+
if (entriesToDelete) {
|
|
349
|
+
const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
|
|
350
|
+
where: {
|
|
351
|
+
actions: {
|
|
352
|
+
target_type: model.uid,
|
|
353
|
+
target_id: {
|
|
354
|
+
$in: entriesToDelete.map(
|
|
355
|
+
(entry) => entry.id
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
|
|
362
|
+
where: {
|
|
363
|
+
target_type: model.uid,
|
|
364
|
+
target_id: {
|
|
365
|
+
$in: entriesToDelete.map((entry) => entry.id)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
for (const release2 of releases) {
|
|
370
|
+
getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch (error) {
|
|
374
|
+
strapi2.log.error("Error while deleting release actions after entry deleteMany", {
|
|
375
|
+
error
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
async afterUpdate(event) {
|
|
380
|
+
try {
|
|
381
|
+
const { model, result } = event;
|
|
382
|
+
if (model.kind === "collectionType" && model.options?.draftAndPublish) {
|
|
383
|
+
const isEntryValid = await getEntryValidStatus(
|
|
384
|
+
model.uid,
|
|
385
|
+
result,
|
|
386
|
+
{
|
|
387
|
+
strapi: strapi2
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
|
|
391
|
+
where: {
|
|
392
|
+
target_type: model.uid,
|
|
393
|
+
target_id: result.id
|
|
394
|
+
},
|
|
395
|
+
data: {
|
|
396
|
+
isEntryValid
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
|
|
400
|
+
where: {
|
|
401
|
+
actions: {
|
|
402
|
+
target_type: model.uid,
|
|
403
|
+
target_id: result.id
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
for (const release2 of releases) {
|
|
408
|
+
getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch (error) {
|
|
412
|
+
strapi2.log.error("Error while updating release actions after entry update", { error });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
|
|
417
|
+
strapi2.log.error(
|
|
418
|
+
"Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
|
|
419
|
+
);
|
|
420
|
+
throw err;
|
|
421
|
+
});
|
|
422
|
+
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
|
|
423
|
+
strapi2.webhookStore.addAllowedEvent(key, value);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const destroy = async ({ strapi: strapi2 }) => {
|
|
428
|
+
const scheduledJobs = getService("scheduling", {
|
|
429
|
+
strapi: strapi2
|
|
430
|
+
}).getAll();
|
|
431
|
+
for (const [, job] of scheduledJobs) {
|
|
432
|
+
job.cancel();
|
|
76
433
|
}
|
|
77
434
|
};
|
|
78
435
|
const schema$1 = {
|
|
@@ -101,6 +458,17 @@ const schema$1 = {
|
|
|
101
458
|
releasedAt: {
|
|
102
459
|
type: "datetime"
|
|
103
460
|
},
|
|
461
|
+
scheduledAt: {
|
|
462
|
+
type: "datetime"
|
|
463
|
+
},
|
|
464
|
+
timezone: {
|
|
465
|
+
type: "string"
|
|
466
|
+
},
|
|
467
|
+
status: {
|
|
468
|
+
type: "enumeration",
|
|
469
|
+
enum: ["ready", "blocked", "failed", "done", "empty"],
|
|
470
|
+
required: true
|
|
471
|
+
},
|
|
104
472
|
actions: {
|
|
105
473
|
type: "relation",
|
|
106
474
|
relation: "oneToMany",
|
|
@@ -153,6 +521,9 @@ const schema = {
|
|
|
153
521
|
relation: "manyToOne",
|
|
154
522
|
target: RELEASE_MODEL_UID,
|
|
155
523
|
inversedBy: "actions"
|
|
524
|
+
},
|
|
525
|
+
isEntryValid: {
|
|
526
|
+
type: "boolean"
|
|
156
527
|
}
|
|
157
528
|
}
|
|
158
529
|
};
|
|
@@ -163,327 +534,606 @@ const contentTypes = {
|
|
|
163
534
|
release: release$1,
|
|
164
535
|
"release-action": releaseAction$1
|
|
165
536
|
};
|
|
166
|
-
const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
|
|
167
|
-
return strapi2.plugin("content-releases").service(name);
|
|
168
|
-
};
|
|
169
537
|
const getGroupName = (queryValue) => {
|
|
170
538
|
switch (queryValue) {
|
|
171
539
|
case "contentType":
|
|
172
|
-
return "
|
|
540
|
+
return "contentType.displayName";
|
|
173
541
|
case "action":
|
|
174
542
|
return "type";
|
|
175
543
|
case "locale":
|
|
176
|
-
return ___default.default.getOr("No locale", "
|
|
544
|
+
return ___default.default.getOr("No locale", "locale.name");
|
|
177
545
|
default:
|
|
178
|
-
return "
|
|
546
|
+
return "contentType.displayName";
|
|
179
547
|
}
|
|
180
548
|
};
|
|
181
|
-
const createReleaseService = ({ strapi: strapi2 }) =>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
549
|
+
const createReleaseService = ({ strapi: strapi2 }) => {
|
|
550
|
+
const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
|
|
551
|
+
strapi2.eventHub.emit(event, {
|
|
552
|
+
isPublished,
|
|
553
|
+
error,
|
|
554
|
+
release: release2
|
|
186
555
|
});
|
|
187
|
-
}
|
|
188
|
-
async
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
actions: {
|
|
199
|
-
// @ts-expect-error Ignore missing properties
|
|
200
|
-
count: true
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
},
|
|
205
|
-
async findManyForContentTypeEntry(contentTypeUid, entryId, {
|
|
206
|
-
hasEntryAttached
|
|
207
|
-
} = {
|
|
208
|
-
hasEntryAttached: false
|
|
209
|
-
}) {
|
|
210
|
-
const whereActions = hasEntryAttached ? {
|
|
211
|
-
// Find all Releases where the content type entry is present
|
|
212
|
-
actions: {
|
|
213
|
-
target_type: contentTypeUid,
|
|
214
|
-
target_id: entryId
|
|
215
|
-
}
|
|
216
|
-
} : {
|
|
217
|
-
// Find all Releases where the content type entry is not present
|
|
218
|
-
$or: [
|
|
219
|
-
{
|
|
220
|
-
$not: {
|
|
221
|
-
actions: {
|
|
222
|
-
target_type: contentTypeUid,
|
|
223
|
-
target_id: entryId
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
actions: null
|
|
229
|
-
}
|
|
230
|
-
]
|
|
231
|
-
};
|
|
232
|
-
const populateAttachedAction = hasEntryAttached ? {
|
|
233
|
-
// Filter the action to get only the content type entry
|
|
234
|
-
actions: {
|
|
235
|
-
where: {
|
|
236
|
-
target_type: contentTypeUid,
|
|
237
|
-
target_id: entryId
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
} : {};
|
|
241
|
-
const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
|
|
242
|
-
where: {
|
|
243
|
-
...whereActions,
|
|
244
|
-
releasedAt: {
|
|
245
|
-
$null: true
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
populate: {
|
|
249
|
-
...populateAttachedAction
|
|
556
|
+
};
|
|
557
|
+
const publishSingleTypeAction = async (uid, actionType, entryId) => {
|
|
558
|
+
const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
|
|
559
|
+
const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
|
|
560
|
+
const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
|
|
561
|
+
const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
|
|
562
|
+
try {
|
|
563
|
+
if (actionType === "publish") {
|
|
564
|
+
await entityManagerService.publish(entry, uid);
|
|
565
|
+
} else {
|
|
566
|
+
await entityManagerService.unpublish(entry, uid);
|
|
250
567
|
}
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
...release2,
|
|
258
|
-
action: actionForEntry
|
|
259
|
-
};
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
|
|
570
|
+
;
|
|
571
|
+
else {
|
|
572
|
+
throw error;
|
|
260
573
|
}
|
|
261
|
-
return release2;
|
|
262
|
-
});
|
|
263
|
-
},
|
|
264
|
-
async update(id, releaseData, { user }) {
|
|
265
|
-
const updatedRelease = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
|
|
266
|
-
const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
|
|
267
|
-
/*
|
|
268
|
-
* The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
|
|
269
|
-
* is not compatible with the type we are passing here: UpdateRelease.Request['body']
|
|
270
|
-
*/
|
|
271
|
-
// @ts-expect-error see above
|
|
272
|
-
data: updatedRelease
|
|
273
|
-
});
|
|
274
|
-
if (!release2) {
|
|
275
|
-
throw new utils.errors.NotFoundError(`No release found for id ${id}`);
|
|
276
574
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const { entry, type } = action;
|
|
288
|
-
return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
|
|
289
|
-
data: {
|
|
290
|
-
type,
|
|
291
|
-
contentType: entry.contentType,
|
|
292
|
-
locale: entry.locale,
|
|
293
|
-
entry: {
|
|
294
|
-
id: entry.id,
|
|
295
|
-
__type: entry.contentType,
|
|
296
|
-
__pivot: { field: "entry" }
|
|
297
|
-
},
|
|
298
|
-
release: releaseId
|
|
575
|
+
};
|
|
576
|
+
const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
|
|
577
|
+
const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
|
|
578
|
+
const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
|
|
579
|
+
const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
|
|
580
|
+
const entriesToPublish = await strapi2.entityService.findMany(uid, {
|
|
581
|
+
filters: {
|
|
582
|
+
id: {
|
|
583
|
+
$in: entriesToPublishIds
|
|
584
|
+
}
|
|
299
585
|
},
|
|
300
|
-
populate
|
|
586
|
+
populate
|
|
301
587
|
});
|
|
302
|
-
|
|
303
|
-
async findActions(releaseId, query) {
|
|
304
|
-
const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
|
|
305
|
-
fields: ["id"]
|
|
306
|
-
});
|
|
307
|
-
if (!release2) {
|
|
308
|
-
throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
|
|
309
|
-
}
|
|
310
|
-
return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
|
|
311
|
-
...query,
|
|
312
|
-
populate: {
|
|
313
|
-
entry: true
|
|
314
|
-
},
|
|
588
|
+
const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
|
|
315
589
|
filters: {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
319
|
-
},
|
|
320
|
-
async countActions(query) {
|
|
321
|
-
return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
|
|
322
|
-
},
|
|
323
|
-
async groupActions(actions, groupBy) {
|
|
324
|
-
const contentTypeUids = actions.reduce((acc, action) => {
|
|
325
|
-
if (!acc.includes(action.contentType)) {
|
|
326
|
-
acc.push(action.contentType);
|
|
327
|
-
}
|
|
328
|
-
return acc;
|
|
329
|
-
}, []);
|
|
330
|
-
const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
|
|
331
|
-
contentTypeUids
|
|
332
|
-
);
|
|
333
|
-
const allLocales = await strapi2.plugin("i18n").service("locales").find();
|
|
334
|
-
const allLocalesDictionary = allLocales.reduce((acc, locale) => {
|
|
335
|
-
acc[locale.code] = { name: locale.name, code: locale.code };
|
|
336
|
-
return acc;
|
|
337
|
-
}, {});
|
|
338
|
-
const formattedData = actions.map((action) => {
|
|
339
|
-
const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
|
|
340
|
-
return {
|
|
341
|
-
...action,
|
|
342
|
-
entry: {
|
|
343
|
-
id: action.entry.id,
|
|
344
|
-
contentType: {
|
|
345
|
-
displayName,
|
|
346
|
-
mainFieldValue: action.entry[mainField]
|
|
347
|
-
},
|
|
348
|
-
locale: action.locale ? allLocalesDictionary[action.locale] : null,
|
|
349
|
-
status: action.entry.publishedAt ? "published" : "draft"
|
|
590
|
+
id: {
|
|
591
|
+
$in: entriestoUnpublishIds
|
|
350
592
|
}
|
|
351
|
-
}
|
|
593
|
+
},
|
|
594
|
+
populate
|
|
352
595
|
});
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
},
|
|
356
|
-
async getContentTypesDataForActions(contentTypesUids) {
|
|
357
|
-
const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
|
|
358
|
-
const contentTypesData = {};
|
|
359
|
-
for (const contentTypeUid of contentTypesUids) {
|
|
360
|
-
const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
|
|
361
|
-
uid: contentTypeUid
|
|
362
|
-
});
|
|
363
|
-
contentTypesData[contentTypeUid] = {
|
|
364
|
-
mainField: contentTypeConfig.settings.mainField,
|
|
365
|
-
displayName: strapi2.getModel(contentTypeUid).info.displayName
|
|
366
|
-
};
|
|
596
|
+
if (entriesToPublish.length > 0) {
|
|
597
|
+
await entityManagerService.publishMany(entriesToPublish, uid);
|
|
367
598
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
599
|
+
if (entriesToUnpublish.length > 0) {
|
|
600
|
+
await entityManagerService.unpublishMany(entriesToUnpublish, uid);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const getFormattedActions = async (releaseId) => {
|
|
604
|
+
const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
|
|
605
|
+
where: {
|
|
606
|
+
release: {
|
|
607
|
+
id: releaseId
|
|
608
|
+
}
|
|
609
|
+
},
|
|
372
610
|
populate: {
|
|
373
|
-
|
|
611
|
+
entry: {
|
|
374
612
|
fields: ["id"]
|
|
375
613
|
}
|
|
376
614
|
}
|
|
377
615
|
});
|
|
378
|
-
if (
|
|
379
|
-
throw new utils.errors.
|
|
616
|
+
if (actions.length === 0) {
|
|
617
|
+
throw new utils.errors.ValidationError("No entries to publish");
|
|
380
618
|
}
|
|
381
|
-
|
|
382
|
-
|
|
619
|
+
const collectionTypeActions = {};
|
|
620
|
+
const singleTypeActions = [];
|
|
621
|
+
for (const action of actions) {
|
|
622
|
+
const contentTypeUid = action.contentType;
|
|
623
|
+
if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
|
|
624
|
+
if (!collectionTypeActions[contentTypeUid]) {
|
|
625
|
+
collectionTypeActions[contentTypeUid] = {
|
|
626
|
+
entriesToPublishIds: [],
|
|
627
|
+
entriesToUnpublishIds: []
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
if (action.type === "publish") {
|
|
631
|
+
collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
|
|
632
|
+
} else {
|
|
633
|
+
collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
singleTypeActions.push({
|
|
637
|
+
uid: contentTypeUid,
|
|
638
|
+
action: action.type,
|
|
639
|
+
id: action.entry.id
|
|
640
|
+
});
|
|
641
|
+
}
|
|
383
642
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
643
|
+
return { collectionTypeActions, singleTypeActions };
|
|
644
|
+
};
|
|
645
|
+
return {
|
|
646
|
+
async create(releaseData, { user }) {
|
|
647
|
+
const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
|
|
648
|
+
const {
|
|
649
|
+
validatePendingReleasesLimit,
|
|
650
|
+
validateUniqueNameForPendingRelease,
|
|
651
|
+
validateScheduledAtIsLaterThanNow
|
|
652
|
+
} = getService("release-validation", { strapi: strapi2 });
|
|
653
|
+
await Promise.all([
|
|
654
|
+
validatePendingReleasesLimit(),
|
|
655
|
+
validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
|
|
656
|
+
validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
|
|
657
|
+
]);
|
|
658
|
+
const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
|
|
659
|
+
data: {
|
|
660
|
+
...releaseWithCreatorFields,
|
|
661
|
+
status: "empty"
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
if (releaseWithCreatorFields.scheduledAt) {
|
|
665
|
+
const schedulingService = getService("scheduling", { strapi: strapi2 });
|
|
666
|
+
await schedulingService.set(release2.id, release2.scheduledAt);
|
|
667
|
+
}
|
|
668
|
+
strapi2.telemetry.send("didCreateContentRelease");
|
|
669
|
+
return release2;
|
|
670
|
+
},
|
|
671
|
+
async findOne(id, query = {}) {
|
|
672
|
+
const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
|
|
673
|
+
...query
|
|
674
|
+
});
|
|
675
|
+
return release2;
|
|
676
|
+
},
|
|
677
|
+
findPage(query) {
|
|
678
|
+
return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
|
|
679
|
+
...query,
|
|
680
|
+
populate: {
|
|
681
|
+
actions: {
|
|
682
|
+
// @ts-expect-error Ignore missing properties
|
|
683
|
+
count: true
|
|
389
684
|
}
|
|
390
685
|
}
|
|
391
686
|
});
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
RELEASE_MODEL_UID
|
|
399
|
-
|
|
400
|
-
|
|
687
|
+
},
|
|
688
|
+
async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
|
|
689
|
+
let entries = entriesIds;
|
|
690
|
+
if (!Array.isArray(entriesIds)) {
|
|
691
|
+
entries = [entriesIds];
|
|
692
|
+
}
|
|
693
|
+
const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
|
|
694
|
+
where: {
|
|
695
|
+
actions: {
|
|
696
|
+
target_type: contentTypeUid,
|
|
697
|
+
target_id: {
|
|
698
|
+
$in: entries
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
releasedAt: {
|
|
702
|
+
$null: true
|
|
703
|
+
}
|
|
704
|
+
},
|
|
401
705
|
populate: {
|
|
706
|
+
// Filter the action to get only the content type entry
|
|
402
707
|
actions: {
|
|
708
|
+
where: {
|
|
709
|
+
target_type: contentTypeUid,
|
|
710
|
+
target_id: {
|
|
711
|
+
$in: entries
|
|
712
|
+
}
|
|
713
|
+
},
|
|
403
714
|
populate: {
|
|
404
|
-
entry:
|
|
715
|
+
entry: {
|
|
716
|
+
select: ["id"]
|
|
717
|
+
}
|
|
405
718
|
}
|
|
406
719
|
}
|
|
407
720
|
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
721
|
+
});
|
|
722
|
+
return releases.map((release2) => {
|
|
723
|
+
if (release2.actions?.length) {
|
|
724
|
+
const actionsForEntry = release2.actions;
|
|
725
|
+
delete release2.actions;
|
|
726
|
+
return {
|
|
727
|
+
...release2,
|
|
728
|
+
actions: actionsForEntry
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
return release2;
|
|
732
|
+
});
|
|
733
|
+
},
|
|
734
|
+
async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
|
|
735
|
+
const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
|
|
736
|
+
where: {
|
|
737
|
+
releasedAt: {
|
|
738
|
+
$null: true
|
|
739
|
+
},
|
|
740
|
+
actions: {
|
|
741
|
+
target_type: contentTypeUid,
|
|
742
|
+
target_id: entryId
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
|
|
747
|
+
where: {
|
|
748
|
+
$or: [
|
|
749
|
+
{
|
|
750
|
+
id: {
|
|
751
|
+
$notIn: releasesRelated.map((release2) => release2.id)
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
actions: null
|
|
756
|
+
}
|
|
757
|
+
],
|
|
758
|
+
releasedAt: {
|
|
759
|
+
$null: true
|
|
760
|
+
}
|
|
440
761
|
}
|
|
441
|
-
|
|
442
|
-
|
|
762
|
+
});
|
|
763
|
+
return releases.map((release2) => {
|
|
764
|
+
if (release2.actions?.length) {
|
|
765
|
+
const [actionForEntry] = release2.actions;
|
|
766
|
+
delete release2.actions;
|
|
767
|
+
return {
|
|
768
|
+
...release2,
|
|
769
|
+
action: actionForEntry
|
|
770
|
+
};
|
|
443
771
|
}
|
|
772
|
+
return release2;
|
|
773
|
+
});
|
|
774
|
+
},
|
|
775
|
+
async update(id, releaseData, { user }) {
|
|
776
|
+
const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(
|
|
777
|
+
releaseData
|
|
778
|
+
);
|
|
779
|
+
const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
|
|
780
|
+
"release-validation",
|
|
781
|
+
{ strapi: strapi2 }
|
|
782
|
+
);
|
|
783
|
+
await Promise.all([
|
|
784
|
+
validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
|
|
785
|
+
validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
|
|
786
|
+
]);
|
|
787
|
+
const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
|
|
788
|
+
if (!release2) {
|
|
789
|
+
throw new utils.errors.NotFoundError(`No release found for id ${id}`);
|
|
444
790
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
791
|
+
if (release2.releasedAt) {
|
|
792
|
+
throw new utils.errors.ValidationError("Release already published");
|
|
793
|
+
}
|
|
794
|
+
const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
|
|
448
795
|
/*
|
|
449
|
-
* The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
|
|
796
|
+
* The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
|
|
797
|
+
* is not compatible with the type we are passing here: UpdateRelease.Request['body']
|
|
450
798
|
*/
|
|
451
799
|
// @ts-expect-error see above
|
|
452
|
-
|
|
800
|
+
data: releaseWithCreatorFields
|
|
801
|
+
});
|
|
802
|
+
const schedulingService = getService("scheduling", { strapi: strapi2 });
|
|
803
|
+
if (releaseData.scheduledAt) {
|
|
804
|
+
await schedulingService.set(id, releaseData.scheduledAt);
|
|
805
|
+
} else if (release2.scheduledAt) {
|
|
806
|
+
schedulingService.cancel(id);
|
|
453
807
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
808
|
+
this.updateReleaseStatus(id);
|
|
809
|
+
strapi2.telemetry.send("didUpdateContentRelease");
|
|
810
|
+
return updatedRelease;
|
|
811
|
+
},
|
|
812
|
+
async createAction(releaseId, action) {
|
|
813
|
+
const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
|
|
814
|
+
strapi: strapi2
|
|
815
|
+
});
|
|
816
|
+
await Promise.all([
|
|
817
|
+
validateEntryContentType(action.entry.contentType),
|
|
818
|
+
validateUniqueEntry(releaseId, action)
|
|
819
|
+
]);
|
|
820
|
+
const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
|
|
821
|
+
if (!release2) {
|
|
822
|
+
throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
|
|
823
|
+
}
|
|
824
|
+
if (release2.releasedAt) {
|
|
825
|
+
throw new utils.errors.ValidationError("Release already published");
|
|
826
|
+
}
|
|
827
|
+
const { entry, type } = action;
|
|
828
|
+
const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
|
|
829
|
+
const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
|
|
830
|
+
const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
|
|
831
|
+
data: {
|
|
832
|
+
type,
|
|
833
|
+
contentType: entry.contentType,
|
|
834
|
+
locale: entry.locale,
|
|
835
|
+
isEntryValid,
|
|
836
|
+
entry: {
|
|
837
|
+
id: entry.id,
|
|
838
|
+
__type: entry.contentType,
|
|
839
|
+
__pivot: { field: "entry" }
|
|
840
|
+
},
|
|
841
|
+
release: releaseId
|
|
842
|
+
},
|
|
843
|
+
populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
|
|
844
|
+
});
|
|
845
|
+
this.updateReleaseStatus(releaseId);
|
|
846
|
+
return releaseAction2;
|
|
847
|
+
},
|
|
848
|
+
async findActions(releaseId, query) {
|
|
849
|
+
const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
|
|
850
|
+
fields: ["id"]
|
|
851
|
+
});
|
|
852
|
+
if (!release2) {
|
|
853
|
+
throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
|
|
854
|
+
}
|
|
855
|
+
return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
|
|
856
|
+
...query,
|
|
857
|
+
populate: {
|
|
858
|
+
entry: {
|
|
859
|
+
populate: "*"
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
filters: {
|
|
863
|
+
release: releaseId
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
},
|
|
867
|
+
async countActions(query) {
|
|
868
|
+
return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
|
|
869
|
+
},
|
|
870
|
+
async groupActions(actions, groupBy) {
|
|
871
|
+
const contentTypeUids = actions.reduce((acc, action) => {
|
|
872
|
+
if (!acc.includes(action.contentType)) {
|
|
873
|
+
acc.push(action.contentType);
|
|
874
|
+
}
|
|
875
|
+
return acc;
|
|
876
|
+
}, []);
|
|
877
|
+
const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
|
|
878
|
+
contentTypeUids
|
|
468
879
|
);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
880
|
+
const allLocalesDictionary = await this.getLocalesDataForActions();
|
|
881
|
+
const formattedData = actions.map((action) => {
|
|
882
|
+
const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
|
|
883
|
+
return {
|
|
884
|
+
...action,
|
|
885
|
+
locale: action.locale ? allLocalesDictionary[action.locale] : null,
|
|
886
|
+
contentType: {
|
|
887
|
+
displayName,
|
|
888
|
+
mainFieldValue: action.entry[mainField],
|
|
889
|
+
uid: action.contentType
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
});
|
|
893
|
+
const groupName = getGroupName(groupBy);
|
|
894
|
+
return ___default.default.groupBy(groupName)(formattedData);
|
|
895
|
+
},
|
|
896
|
+
async getLocalesDataForActions() {
|
|
897
|
+
if (!strapi2.plugin("i18n")) {
|
|
898
|
+
return {};
|
|
477
899
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
900
|
+
const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
|
|
901
|
+
return allLocales.reduce((acc, locale) => {
|
|
902
|
+
acc[locale.code] = { name: locale.name, code: locale.code };
|
|
903
|
+
return acc;
|
|
904
|
+
}, {});
|
|
905
|
+
},
|
|
906
|
+
async getContentTypesDataForActions(contentTypesUids) {
|
|
907
|
+
const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
|
|
908
|
+
const contentTypesData = {};
|
|
909
|
+
for (const contentTypeUid of contentTypesUids) {
|
|
910
|
+
const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
|
|
911
|
+
uid: contentTypeUid
|
|
912
|
+
});
|
|
913
|
+
contentTypesData[contentTypeUid] = {
|
|
914
|
+
mainField: contentTypeConfig.settings.mainField,
|
|
915
|
+
displayName: strapi2.getModel(contentTypeUid).info.displayName
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
return contentTypesData;
|
|
919
|
+
},
|
|
920
|
+
getContentTypeModelsFromActions(actions) {
|
|
921
|
+
const contentTypeUids = actions.reduce((acc, action) => {
|
|
922
|
+
if (!acc.includes(action.contentType)) {
|
|
923
|
+
acc.push(action.contentType);
|
|
924
|
+
}
|
|
925
|
+
return acc;
|
|
926
|
+
}, []);
|
|
927
|
+
const contentTypeModelsMap = contentTypeUids.reduce(
|
|
928
|
+
(acc, contentTypeUid) => {
|
|
929
|
+
acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
|
|
930
|
+
return acc;
|
|
931
|
+
},
|
|
932
|
+
{}
|
|
482
933
|
);
|
|
934
|
+
return contentTypeModelsMap;
|
|
935
|
+
},
|
|
936
|
+
async getAllComponents() {
|
|
937
|
+
const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
|
|
938
|
+
const components = await contentManagerComponentsService.findAllComponents();
|
|
939
|
+
const componentsMap = components.reduce(
|
|
940
|
+
(acc, component) => {
|
|
941
|
+
acc[component.uid] = component;
|
|
942
|
+
return acc;
|
|
943
|
+
},
|
|
944
|
+
{}
|
|
945
|
+
);
|
|
946
|
+
return componentsMap;
|
|
947
|
+
},
|
|
948
|
+
async delete(releaseId) {
|
|
949
|
+
const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
|
|
950
|
+
populate: {
|
|
951
|
+
actions: {
|
|
952
|
+
fields: ["id"]
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
if (!release2) {
|
|
957
|
+
throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
|
|
958
|
+
}
|
|
959
|
+
if (release2.releasedAt) {
|
|
960
|
+
throw new utils.errors.ValidationError("Release already published");
|
|
961
|
+
}
|
|
962
|
+
await strapi2.db.transaction(async () => {
|
|
963
|
+
await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
|
|
964
|
+
where: {
|
|
965
|
+
id: {
|
|
966
|
+
$in: release2.actions.map((action) => action.id)
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
|
|
971
|
+
});
|
|
972
|
+
if (release2.scheduledAt) {
|
|
973
|
+
const schedulingService = getService("scheduling", { strapi: strapi2 });
|
|
974
|
+
await schedulingService.cancel(release2.id);
|
|
975
|
+
}
|
|
976
|
+
strapi2.telemetry.send("didDeleteContentRelease");
|
|
977
|
+
return release2;
|
|
978
|
+
},
|
|
979
|
+
async publish(releaseId) {
|
|
980
|
+
const {
|
|
981
|
+
release: release2,
|
|
982
|
+
error
|
|
983
|
+
} = await strapi2.db.transaction(async ({ trx }) => {
|
|
984
|
+
const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
|
|
985
|
+
if (!lockedRelease) {
|
|
986
|
+
throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
|
|
987
|
+
}
|
|
988
|
+
if (lockedRelease.releasedAt) {
|
|
989
|
+
throw new utils.errors.ValidationError("Release already published");
|
|
990
|
+
}
|
|
991
|
+
if (lockedRelease.status === "failed") {
|
|
992
|
+
throw new utils.errors.ValidationError("Release failed to publish");
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
|
|
996
|
+
const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
|
|
997
|
+
releaseId
|
|
998
|
+
);
|
|
999
|
+
await strapi2.db.transaction(async () => {
|
|
1000
|
+
for (const { uid, action, id } of singleTypeActions) {
|
|
1001
|
+
await publishSingleTypeAction(uid, action, id);
|
|
1002
|
+
}
|
|
1003
|
+
for (const contentTypeUid of Object.keys(collectionTypeActions)) {
|
|
1004
|
+
const uid = contentTypeUid;
|
|
1005
|
+
await publishCollectionTypeAction(
|
|
1006
|
+
uid,
|
|
1007
|
+
collectionTypeActions[uid].entriesToPublishIds,
|
|
1008
|
+
collectionTypeActions[uid].entriesToUnpublishIds
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
|
|
1013
|
+
where: {
|
|
1014
|
+
id: releaseId
|
|
1015
|
+
},
|
|
1016
|
+
data: {
|
|
1017
|
+
status: "done",
|
|
1018
|
+
releasedAt: /* @__PURE__ */ new Date()
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
|
|
1022
|
+
isPublished: true,
|
|
1023
|
+
release: release22
|
|
1024
|
+
});
|
|
1025
|
+
strapi2.telemetry.send("didPublishContentRelease");
|
|
1026
|
+
return { release: release22, error: null };
|
|
1027
|
+
} catch (error2) {
|
|
1028
|
+
dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
|
|
1029
|
+
isPublished: false,
|
|
1030
|
+
error: error2
|
|
1031
|
+
});
|
|
1032
|
+
await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
|
|
1033
|
+
status: "failed"
|
|
1034
|
+
}).transacting(trx).execute();
|
|
1035
|
+
return {
|
|
1036
|
+
release: null,
|
|
1037
|
+
error: error2
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
if (error) {
|
|
1042
|
+
throw error;
|
|
1043
|
+
}
|
|
1044
|
+
return release2;
|
|
1045
|
+
},
|
|
1046
|
+
async updateAction(actionId, releaseId, update) {
|
|
1047
|
+
const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
|
|
1048
|
+
where: {
|
|
1049
|
+
id: actionId,
|
|
1050
|
+
release: {
|
|
1051
|
+
id: releaseId,
|
|
1052
|
+
releasedAt: {
|
|
1053
|
+
$null: true
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
data: update
|
|
1058
|
+
});
|
|
1059
|
+
if (!updatedAction) {
|
|
1060
|
+
throw new utils.errors.NotFoundError(
|
|
1061
|
+
`Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
return updatedAction;
|
|
1065
|
+
},
|
|
1066
|
+
async deleteAction(actionId, releaseId) {
|
|
1067
|
+
const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
|
|
1068
|
+
where: {
|
|
1069
|
+
id: actionId,
|
|
1070
|
+
release: {
|
|
1071
|
+
id: releaseId,
|
|
1072
|
+
releasedAt: {
|
|
1073
|
+
$null: true
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
if (!deletedAction) {
|
|
1079
|
+
throw new utils.errors.NotFoundError(
|
|
1080
|
+
`Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
this.updateReleaseStatus(releaseId);
|
|
1084
|
+
return deletedAction;
|
|
1085
|
+
},
|
|
1086
|
+
async updateReleaseStatus(releaseId) {
|
|
1087
|
+
const [totalActions, invalidActions] = await Promise.all([
|
|
1088
|
+
this.countActions({
|
|
1089
|
+
filters: {
|
|
1090
|
+
release: releaseId
|
|
1091
|
+
}
|
|
1092
|
+
}),
|
|
1093
|
+
this.countActions({
|
|
1094
|
+
filters: {
|
|
1095
|
+
release: releaseId,
|
|
1096
|
+
isEntryValid: false
|
|
1097
|
+
}
|
|
1098
|
+
})
|
|
1099
|
+
]);
|
|
1100
|
+
if (totalActions > 0) {
|
|
1101
|
+
if (invalidActions > 0) {
|
|
1102
|
+
return strapi2.db.query(RELEASE_MODEL_UID).update({
|
|
1103
|
+
where: {
|
|
1104
|
+
id: releaseId
|
|
1105
|
+
},
|
|
1106
|
+
data: {
|
|
1107
|
+
status: "blocked"
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
return strapi2.db.query(RELEASE_MODEL_UID).update({
|
|
1112
|
+
where: {
|
|
1113
|
+
id: releaseId
|
|
1114
|
+
},
|
|
1115
|
+
data: {
|
|
1116
|
+
status: "ready"
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
return strapi2.db.query(RELEASE_MODEL_UID).update({
|
|
1121
|
+
where: {
|
|
1122
|
+
id: releaseId
|
|
1123
|
+
},
|
|
1124
|
+
data: {
|
|
1125
|
+
status: "empty"
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
483
1128
|
}
|
|
484
|
-
|
|
1129
|
+
};
|
|
1130
|
+
};
|
|
1131
|
+
class AlreadyOnReleaseError extends utils.errors.ApplicationError {
|
|
1132
|
+
constructor(message) {
|
|
1133
|
+
super(message);
|
|
1134
|
+
this.name = "AlreadyOnReleaseError";
|
|
485
1135
|
}
|
|
486
|
-
}
|
|
1136
|
+
}
|
|
487
1137
|
const createReleaseValidationService = ({ strapi: strapi2 }) => ({
|
|
488
1138
|
async validateUniqueEntry(releaseId, releaseActionArgs) {
|
|
489
1139
|
const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
|
|
@@ -496,7 +1146,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
|
|
|
496
1146
|
(action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
|
|
497
1147
|
);
|
|
498
1148
|
if (isEntryInRelease) {
|
|
499
|
-
throw new
|
|
1149
|
+
throw new AlreadyOnReleaseError(
|
|
500
1150
|
`Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
|
|
501
1151
|
);
|
|
502
1152
|
}
|
|
@@ -511,11 +1161,120 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
|
|
|
511
1161
|
`Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
|
|
512
1162
|
);
|
|
513
1163
|
}
|
|
1164
|
+
},
|
|
1165
|
+
async validatePendingReleasesLimit() {
|
|
1166
|
+
const maximumPendingReleases = (
|
|
1167
|
+
// @ts-expect-error - options is not typed into features
|
|
1168
|
+
EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
|
|
1169
|
+
);
|
|
1170
|
+
const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
|
|
1171
|
+
filters: {
|
|
1172
|
+
releasedAt: {
|
|
1173
|
+
$null: true
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
if (pendingReleasesCount >= maximumPendingReleases) {
|
|
1178
|
+
throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
async validateUniqueNameForPendingRelease(name, id) {
|
|
1182
|
+
const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
|
|
1183
|
+
filters: {
|
|
1184
|
+
releasedAt: {
|
|
1185
|
+
$null: true
|
|
1186
|
+
},
|
|
1187
|
+
name,
|
|
1188
|
+
...id && { id: { $ne: id } }
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
const isNameUnique = pendingReleases.length === 0;
|
|
1192
|
+
if (!isNameUnique) {
|
|
1193
|
+
throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
|
|
1194
|
+
}
|
|
1195
|
+
},
|
|
1196
|
+
async validateScheduledAtIsLaterThanNow(scheduledAt) {
|
|
1197
|
+
if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
|
|
1198
|
+
throw new utils.errors.ValidationError("Scheduled at must be later than now");
|
|
1199
|
+
}
|
|
514
1200
|
}
|
|
515
1201
|
});
|
|
516
|
-
const
|
|
1202
|
+
const createSchedulingService = ({ strapi: strapi2 }) => {
|
|
1203
|
+
const scheduledJobs = /* @__PURE__ */ new Map();
|
|
1204
|
+
return {
|
|
1205
|
+
async set(releaseId, scheduleDate) {
|
|
1206
|
+
const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
|
|
1207
|
+
if (!release2) {
|
|
1208
|
+
throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
|
|
1209
|
+
}
|
|
1210
|
+
const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
|
|
1211
|
+
try {
|
|
1212
|
+
await getService("release").publish(releaseId);
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
}
|
|
1215
|
+
this.cancel(releaseId);
|
|
1216
|
+
});
|
|
1217
|
+
if (scheduledJobs.has(releaseId)) {
|
|
1218
|
+
this.cancel(releaseId);
|
|
1219
|
+
}
|
|
1220
|
+
scheduledJobs.set(releaseId, job);
|
|
1221
|
+
return scheduledJobs;
|
|
1222
|
+
},
|
|
1223
|
+
cancel(releaseId) {
|
|
1224
|
+
if (scheduledJobs.has(releaseId)) {
|
|
1225
|
+
scheduledJobs.get(releaseId).cancel();
|
|
1226
|
+
scheduledJobs.delete(releaseId);
|
|
1227
|
+
}
|
|
1228
|
+
return scheduledJobs;
|
|
1229
|
+
},
|
|
1230
|
+
getAll() {
|
|
1231
|
+
return scheduledJobs;
|
|
1232
|
+
},
|
|
1233
|
+
/**
|
|
1234
|
+
* On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
|
|
1235
|
+
* This is useful in case the server was restarted and the scheduled jobs were lost
|
|
1236
|
+
* This also could be used to sync different Strapi instances in case of a cluster
|
|
1237
|
+
*/
|
|
1238
|
+
async syncFromDatabase() {
|
|
1239
|
+
const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
|
|
1240
|
+
where: {
|
|
1241
|
+
scheduledAt: {
|
|
1242
|
+
$gte: /* @__PURE__ */ new Date()
|
|
1243
|
+
},
|
|
1244
|
+
releasedAt: null
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
for (const release2 of releases) {
|
|
1248
|
+
this.set(release2.id, release2.scheduledAt);
|
|
1249
|
+
}
|
|
1250
|
+
return scheduledJobs;
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
};
|
|
1254
|
+
const services = {
|
|
1255
|
+
release: createReleaseService,
|
|
1256
|
+
"release-validation": createReleaseValidationService,
|
|
1257
|
+
scheduling: createSchedulingService
|
|
1258
|
+
};
|
|
517
1259
|
const RELEASE_SCHEMA = yup__namespace.object().shape({
|
|
518
|
-
name: yup__namespace.string().trim().required()
|
|
1260
|
+
name: yup__namespace.string().trim().required(),
|
|
1261
|
+
scheduledAt: yup__namespace.string().nullable(),
|
|
1262
|
+
isScheduled: yup__namespace.boolean().optional(),
|
|
1263
|
+
time: yup__namespace.string().when("isScheduled", {
|
|
1264
|
+
is: true,
|
|
1265
|
+
then: yup__namespace.string().trim().required(),
|
|
1266
|
+
otherwise: yup__namespace.string().nullable()
|
|
1267
|
+
}),
|
|
1268
|
+
timezone: yup__namespace.string().when("isScheduled", {
|
|
1269
|
+
is: true,
|
|
1270
|
+
then: yup__namespace.string().required().nullable(),
|
|
1271
|
+
otherwise: yup__namespace.string().nullable()
|
|
1272
|
+
}),
|
|
1273
|
+
date: yup__namespace.string().when("isScheduled", {
|
|
1274
|
+
is: true,
|
|
1275
|
+
then: yup__namespace.string().required().nullable(),
|
|
1276
|
+
otherwise: yup__namespace.string().nullable()
|
|
1277
|
+
})
|
|
519
1278
|
}).required().noUnknown();
|
|
520
1279
|
const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
|
|
521
1280
|
const releaseController = {
|
|
@@ -532,9 +1291,7 @@ const releaseController = {
|
|
|
532
1291
|
const contentTypeUid = query.contentTypeUid;
|
|
533
1292
|
const entryId = query.entryId;
|
|
534
1293
|
const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
|
|
535
|
-
const data = await releaseService.
|
|
536
|
-
hasEntryAttached
|
|
537
|
-
});
|
|
1294
|
+
const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
|
|
538
1295
|
ctx.body = { data };
|
|
539
1296
|
} else {
|
|
540
1297
|
const query = await permissionsManager.sanitizeQuery(ctx.query);
|
|
@@ -550,26 +1307,30 @@ const releaseController = {
|
|
|
550
1307
|
}
|
|
551
1308
|
};
|
|
552
1309
|
});
|
|
553
|
-
|
|
1310
|
+
const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
|
|
1311
|
+
where: {
|
|
1312
|
+
releasedAt: null
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
ctx.body = { data, meta: { pagination, pendingReleasesCount } };
|
|
554
1316
|
}
|
|
555
1317
|
},
|
|
556
1318
|
async findOne(ctx) {
|
|
557
1319
|
const id = ctx.params.id;
|
|
558
1320
|
const releaseService = getService("release", { strapi });
|
|
559
1321
|
const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
});
|
|
564
|
-
const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
|
|
1322
|
+
if (!release2) {
|
|
1323
|
+
throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
|
|
1324
|
+
}
|
|
565
1325
|
const count = await releaseService.countActions({
|
|
566
1326
|
filters: {
|
|
567
1327
|
release: id
|
|
568
1328
|
}
|
|
569
1329
|
});
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1330
|
+
const sanitizedRelease = {
|
|
1331
|
+
...release2,
|
|
1332
|
+
createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
|
|
1333
|
+
};
|
|
573
1334
|
const data = {
|
|
574
1335
|
...sanitizedRelease,
|
|
575
1336
|
actions: {
|
|
@@ -580,6 +1341,33 @@ const releaseController = {
|
|
|
580
1341
|
};
|
|
581
1342
|
ctx.body = { data };
|
|
582
1343
|
},
|
|
1344
|
+
async mapEntriesToReleases(ctx) {
|
|
1345
|
+
const { contentTypeUid, entriesIds } = ctx.query;
|
|
1346
|
+
if (!contentTypeUid || !entriesIds) {
|
|
1347
|
+
throw new utils.errors.ValidationError("Missing required query parameters");
|
|
1348
|
+
}
|
|
1349
|
+
const releaseService = getService("release", { strapi });
|
|
1350
|
+
const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
|
|
1351
|
+
contentTypeUid,
|
|
1352
|
+
entriesIds
|
|
1353
|
+
);
|
|
1354
|
+
const mappedEntriesInReleases = releasesWithActions.reduce(
|
|
1355
|
+
(acc, release2) => {
|
|
1356
|
+
release2.actions.forEach((action) => {
|
|
1357
|
+
if (!acc[action.entry.id]) {
|
|
1358
|
+
acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
|
|
1359
|
+
} else {
|
|
1360
|
+
acc[action.entry.id].push({ id: release2.id, name: release2.name });
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
return acc;
|
|
1364
|
+
},
|
|
1365
|
+
{}
|
|
1366
|
+
);
|
|
1367
|
+
ctx.body = {
|
|
1368
|
+
data: mappedEntriesInReleases
|
|
1369
|
+
};
|
|
1370
|
+
},
|
|
583
1371
|
async create(ctx) {
|
|
584
1372
|
const user = ctx.state.user;
|
|
585
1373
|
const releaseArgs = ctx.request.body;
|
|
@@ -622,8 +1410,27 @@ const releaseController = {
|
|
|
622
1410
|
const id = ctx.params.id;
|
|
623
1411
|
const releaseService = getService("release", { strapi });
|
|
624
1412
|
const release2 = await releaseService.publish(id, { user });
|
|
1413
|
+
const [countPublishActions, countUnpublishActions] = await Promise.all([
|
|
1414
|
+
releaseService.countActions({
|
|
1415
|
+
filters: {
|
|
1416
|
+
release: id,
|
|
1417
|
+
type: "publish"
|
|
1418
|
+
}
|
|
1419
|
+
}),
|
|
1420
|
+
releaseService.countActions({
|
|
1421
|
+
filters: {
|
|
1422
|
+
release: id,
|
|
1423
|
+
type: "unpublish"
|
|
1424
|
+
}
|
|
1425
|
+
})
|
|
1426
|
+
]);
|
|
625
1427
|
ctx.body = {
|
|
626
|
-
data: release2
|
|
1428
|
+
data: release2,
|
|
1429
|
+
meta: {
|
|
1430
|
+
totalEntries: countPublishActions + countUnpublishActions,
|
|
1431
|
+
totalPublishedEntries: countPublishActions,
|
|
1432
|
+
totalUnpublishedEntries: countUnpublishActions
|
|
1433
|
+
}
|
|
627
1434
|
};
|
|
628
1435
|
}
|
|
629
1436
|
};
|
|
@@ -650,6 +1457,38 @@ const releaseActionController = {
|
|
|
650
1457
|
data: releaseAction2
|
|
651
1458
|
};
|
|
652
1459
|
},
|
|
1460
|
+
async createMany(ctx) {
|
|
1461
|
+
const releaseId = ctx.params.releaseId;
|
|
1462
|
+
const releaseActionsArgs = ctx.request.body;
|
|
1463
|
+
await Promise.all(
|
|
1464
|
+
releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
|
|
1465
|
+
);
|
|
1466
|
+
const releaseService = getService("release", { strapi });
|
|
1467
|
+
const releaseActions = await strapi.db.transaction(async () => {
|
|
1468
|
+
const releaseActions2 = await Promise.all(
|
|
1469
|
+
releaseActionsArgs.map(async (releaseActionArgs) => {
|
|
1470
|
+
try {
|
|
1471
|
+
const action = await releaseService.createAction(releaseId, releaseActionArgs);
|
|
1472
|
+
return action;
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
if (error instanceof AlreadyOnReleaseError) {
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
throw error;
|
|
1478
|
+
}
|
|
1479
|
+
})
|
|
1480
|
+
);
|
|
1481
|
+
return releaseActions2;
|
|
1482
|
+
});
|
|
1483
|
+
const newReleaseActions = releaseActions.filter((action) => action !== null);
|
|
1484
|
+
ctx.body = {
|
|
1485
|
+
data: newReleaseActions,
|
|
1486
|
+
meta: {
|
|
1487
|
+
entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
|
|
1488
|
+
totalEntries: releaseActions.length
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
},
|
|
653
1492
|
async findMany(ctx) {
|
|
654
1493
|
const releaseId = ctx.params.releaseId;
|
|
655
1494
|
const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
|
|
@@ -662,11 +1501,30 @@ const releaseActionController = {
|
|
|
662
1501
|
sort: query.groupBy === "action" ? "type" : query.groupBy,
|
|
663
1502
|
...query
|
|
664
1503
|
});
|
|
665
|
-
const
|
|
1504
|
+
const contentTypeOutputSanitizers = results.reduce((acc, action) => {
|
|
1505
|
+
if (acc[action.contentType]) {
|
|
1506
|
+
return acc;
|
|
1507
|
+
}
|
|
1508
|
+
const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
|
|
1509
|
+
ability: ctx.state.userAbility,
|
|
1510
|
+
model: action.contentType
|
|
1511
|
+
});
|
|
1512
|
+
acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
|
|
1513
|
+
return acc;
|
|
1514
|
+
}, {});
|
|
1515
|
+
const sanitizedResults = await utils.mapAsync(results, async (action) => ({
|
|
1516
|
+
...action,
|
|
1517
|
+
entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
|
|
1518
|
+
}));
|
|
1519
|
+
const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
|
|
1520
|
+
const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
|
|
1521
|
+
const components = await releaseService.getAllComponents();
|
|
666
1522
|
ctx.body = {
|
|
667
1523
|
data: groupedData,
|
|
668
1524
|
meta: {
|
|
669
|
-
pagination
|
|
1525
|
+
pagination,
|
|
1526
|
+
contentTypes: contentTypes2,
|
|
1527
|
+
components
|
|
670
1528
|
}
|
|
671
1529
|
};
|
|
672
1530
|
},
|
|
@@ -688,10 +1546,8 @@ const releaseActionController = {
|
|
|
688
1546
|
async delete(ctx) {
|
|
689
1547
|
const actionId = ctx.params.actionId;
|
|
690
1548
|
const releaseId = ctx.params.releaseId;
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
releaseId
|
|
694
|
-
);
|
|
1549
|
+
const releaseService = getService("release", { strapi });
|
|
1550
|
+
const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
|
|
695
1551
|
ctx.body = {
|
|
696
1552
|
data: deletedReleaseAction
|
|
697
1553
|
};
|
|
@@ -701,6 +1557,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
|
|
|
701
1557
|
const release = {
|
|
702
1558
|
type: "admin",
|
|
703
1559
|
routes: [
|
|
1560
|
+
{
|
|
1561
|
+
method: "GET",
|
|
1562
|
+
path: "/mapEntriesToReleases",
|
|
1563
|
+
handler: "release.mapEntriesToReleases",
|
|
1564
|
+
config: {
|
|
1565
|
+
policies: [
|
|
1566
|
+
"admin::isAuthenticatedAdmin",
|
|
1567
|
+
{
|
|
1568
|
+
name: "admin::hasPermissions",
|
|
1569
|
+
config: {
|
|
1570
|
+
actions: ["plugin::content-releases.read"]
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
]
|
|
1574
|
+
}
|
|
1575
|
+
},
|
|
704
1576
|
{
|
|
705
1577
|
method: "POST",
|
|
706
1578
|
path: "/",
|
|
@@ -818,6 +1690,22 @@ const releaseAction = {
|
|
|
818
1690
|
]
|
|
819
1691
|
}
|
|
820
1692
|
},
|
|
1693
|
+
{
|
|
1694
|
+
method: "POST",
|
|
1695
|
+
path: "/:releaseId/actions/bulk",
|
|
1696
|
+
handler: "release-action.createMany",
|
|
1697
|
+
config: {
|
|
1698
|
+
policies: [
|
|
1699
|
+
"admin::isAuthenticatedAdmin",
|
|
1700
|
+
{
|
|
1701
|
+
name: "admin::hasPermissions",
|
|
1702
|
+
config: {
|
|
1703
|
+
actions: ["plugin::content-releases.create-action"]
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
]
|
|
1707
|
+
}
|
|
1708
|
+
},
|
|
821
1709
|
{
|
|
822
1710
|
method: "GET",
|
|
823
1711
|
path: "/:releaseId/actions",
|
|
@@ -874,9 +1762,11 @@ const routes = {
|
|
|
874
1762
|
};
|
|
875
1763
|
const { features } = require("@strapi/strapi/dist/utils/ee");
|
|
876
1764
|
const getPlugin = () => {
|
|
877
|
-
if (features.isEnabled("cms-content-releases")
|
|
1765
|
+
if (features.isEnabled("cms-content-releases")) {
|
|
878
1766
|
return {
|
|
879
1767
|
register,
|
|
1768
|
+
bootstrap,
|
|
1769
|
+
destroy,
|
|
880
1770
|
contentTypes,
|
|
881
1771
|
services,
|
|
882
1772
|
controllers,
|
|
@@ -884,6 +1774,9 @@ const getPlugin = () => {
|
|
|
884
1774
|
};
|
|
885
1775
|
}
|
|
886
1776
|
return {
|
|
1777
|
+
// Always return register, it handles its own feature check
|
|
1778
|
+
register,
|
|
1779
|
+
// Always return contentTypes to avoid losing data when the feature is disabled
|
|
887
1780
|
contentTypes
|
|
888
1781
|
};
|
|
889
1782
|
};
|