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