@strapi/strapi 4.11.2 → 4.11.4

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.
@@ -28,14 +28,19 @@ const writePackageJSON = async (path, file, spacing) => {
28
28
 
29
29
  const sendEvent = async (uuid) => {
30
30
  try {
31
+ const event = 'didOptOutTelemetry';
32
+
31
33
  await fetch('https://analytics.strapi.io/api/v2/track', {
32
34
  method: 'POST',
33
35
  body: JSON.stringify({
34
- event: 'didOptOutTelemetry',
36
+ event,
35
37
  deviceId: machineID(),
36
38
  groupProperties: { projectId: uuid },
37
39
  }),
38
- headers: { 'Content-Type': 'application/json' },
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ 'X-Strapi-Event': event,
43
+ },
39
44
  });
40
45
  } catch (e) {
41
46
  // ...
@@ -53,14 +53,19 @@ const generateNewPackageJSON = (packageObj) => {
53
53
 
54
54
  const sendEvent = async (uuid) => {
55
55
  try {
56
+ const event = 'didOptInTelemetry';
57
+
56
58
  await fetch('https://analytics.strapi.io/api/v2/track', {
57
59
  method: 'POST',
58
60
  body: JSON.stringify({
59
- event: 'didOptInTelemetry',
61
+ event,
60
62
  deviceId: machineID(),
61
63
  groupProperties: { projectId: uuid },
62
64
  }),
63
- headers: { 'Content-Type': 'application/json' },
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'X-Strapi-Event': event,
68
+ },
64
69
  });
65
70
  } catch (e) {
66
71
  // ...
@@ -322,6 +322,95 @@ const deleteComponents = async (uid, entityToDelete, { loadComponents = true } =
322
322
  }
323
323
  };
324
324
 
325
+ const cloneComponents = async (uid, entityToClone, data) => {
326
+ const { attributes = {} } = strapi.getModel(uid);
327
+
328
+ const componentBody = {};
329
+ const componentData = await getComponents(uid, entityToClone);
330
+
331
+ for (const attributeName of Object.keys(attributes)) {
332
+ const attribute = attributes[attributeName];
333
+
334
+ // If the attribute is not set or on the component to clone, skip it
335
+ if (!has(attributeName, data) && !has(attributeName, componentData)) {
336
+ continue;
337
+ }
338
+
339
+ if (attribute.type === 'component') {
340
+ const { component: componentUID, repeatable = false } = attribute;
341
+
342
+ const componentValue = has(attributeName, data)
343
+ ? data[attributeName]
344
+ : componentData[attributeName];
345
+
346
+ if (componentValue === null) {
347
+ continue;
348
+ }
349
+
350
+ if (repeatable === true) {
351
+ if (!Array.isArray(componentValue)) {
352
+ throw new Error('Expected an array to create repeatable component');
353
+ }
354
+
355
+ // MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
356
+ const components = await mapAsync(
357
+ componentValue,
358
+ (value) => cloneComponent(componentUID, value),
359
+ { concurrency: isDialectMySQL() ? 1 : Infinity }
360
+ );
361
+
362
+ componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }) => {
363
+ return {
364
+ id,
365
+ __pivot: {
366
+ field: attributeName,
367
+ component_type: componentUID,
368
+ },
369
+ };
370
+ });
371
+ } else {
372
+ const component = await cloneComponent(componentUID, componentValue);
373
+ componentBody[attributeName] = component && {
374
+ id: component.id,
375
+ __pivot: {
376
+ field: attributeName,
377
+ component_type: componentUID,
378
+ },
379
+ };
380
+ }
381
+
382
+ continue;
383
+ }
384
+
385
+ if (attribute.type === 'dynamiczone') {
386
+ const dynamiczoneValues = has(attributeName, data)
387
+ ? data[attributeName]
388
+ : componentData[attributeName];
389
+
390
+ if (!Array.isArray(dynamiczoneValues)) {
391
+ throw new Error('Expected an array to create repeatable component');
392
+ }
393
+ // MySQL/MariaDB can cause deadlocks here if concurrency higher than 1
394
+ componentBody[attributeName] = await mapAsync(
395
+ dynamiczoneValues,
396
+ async (value) => {
397
+ const { id } = await cloneComponent(value.__component, value);
398
+ return {
399
+ id,
400
+ __component: value.__component,
401
+ __pivot: {
402
+ field: attributeName,
403
+ },
404
+ };
405
+ },
406
+ { concurrency: isDialectMySQL() ? 1 : Infinity }
407
+ );
408
+ continue;
409
+ }
410
+ }
411
+
412
+ return componentBody;
413
+ };
325
414
  /** *************************
326
415
  Component queries
327
416
  ************************** */
@@ -377,6 +466,26 @@ const deleteComponent = async (uid, componentToDelete) => {
377
466
  await strapi.query(uid).delete({ where: { id: componentToDelete.id } });
378
467
  };
379
468
 
469
+ const cloneComponent = async (uid, data) => {
470
+ const model = strapi.getModel(uid);
471
+
472
+ if (!has('id', data)) {
473
+ return createComponent(uid, data);
474
+ }
475
+
476
+ const componentData = await cloneComponents(uid, { id: data.id }, data);
477
+ const transform = pipe(
478
+ // Make sure we don't save the component with a pre-defined ID
479
+ omit('id'),
480
+ // Remove the component data from the original data object ...
481
+ (payload) => omitComponentData(model, payload),
482
+ // ... and assign the newly created component instead
483
+ assign(componentData)
484
+ );
485
+
486
+ return strapi.query(uid).clone(data.id, { data: transform(data) });
487
+ };
488
+
380
489
  module.exports = {
381
490
  omitComponentData,
382
491
  getComponents,
@@ -384,4 +493,5 @@ module.exports = {
384
493
  updateComponents,
385
494
  deleteComponents,
386
495
  deleteComponent,
496
+ cloneComponents,
387
497
  };
@@ -84,6 +84,11 @@ export interface EntityService {
84
84
  entityId: ID,
85
85
  params: Params<T>
86
86
  ): Promise<any>;
87
+ clone<K extends keyof AllTypes, T extends AllTypes[K]>(
88
+ uid: K,
89
+ cloneId: ID,
90
+ params: Params<T>
91
+ ): Promise<any>;
87
92
  }
88
93
 
89
94
  export default function (opts: {
@@ -16,6 +16,7 @@ const {
16
16
  createComponents,
17
17
  updateComponents,
18
18
  deleteComponents,
19
+ cloneComponents,
19
20
  } = require('./components');
20
21
  const { pickSelectionParams } = require('./params');
21
22
  const { applyTransforms } = require('./attributes');
@@ -267,6 +268,55 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
267
268
  return entityToDelete;
268
269
  },
269
270
 
271
+ async clone(uid, cloneId, opts) {
272
+ const wrappedParams = await this.wrapParams(opts, { uid, action: 'clone' });
273
+ const { data, files } = wrappedParams;
274
+
275
+ const model = strapi.getModel(uid);
276
+
277
+ const entityToClone = await db.query(uid).findOne({ where: { id: cloneId } });
278
+
279
+ if (!entityToClone) {
280
+ return null;
281
+ }
282
+ const isDraft = contentTypesUtils.isDraft(entityToClone, model);
283
+
284
+ const validData = await entityValidator.validateEntityUpdate(
285
+ model,
286
+ data,
287
+ {
288
+ isDraft,
289
+ },
290
+ entityToClone
291
+ );
292
+ const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams));
293
+
294
+ // TODO: wrap into transaction
295
+ const componentData = await cloneComponents(uid, entityToClone, validData);
296
+
297
+ const entityData = creationPipeline(
298
+ Object.assign(omitComponentData(model, validData), componentData),
299
+ {
300
+ contentType: model,
301
+ }
302
+ );
303
+
304
+ let entity = await db.query(uid).clone(cloneId, {
305
+ ...query,
306
+ data: entityData,
307
+ });
308
+
309
+ // TODO: upload the files then set the links in the entity like with compo to avoid making too many queries
310
+ if (files && Object.keys(files).length > 0) {
311
+ await this.uploadFiles(uid, Object.assign(entityData, entity), files);
312
+ entity = await this.findOne(uid, entity.id, wrappedParams);
313
+ }
314
+
315
+ const { ENTRY_CREATE } = ALLOWED_WEBHOOK_EVENTS;
316
+ await this.emitEvent(uid, ENTRY_CREATE, entity);
317
+
318
+ return entity;
319
+ },
270
320
  // FIXME: used only for the CM to be removed
271
321
  async deleteMany(uid, opts) {
272
322
  const wrappedParams = await this.wrapParams(opts, { uid, action: 'delete' });
@@ -77,7 +77,7 @@ module.exports = (strapi) => {
77
77
  ...payload.groupProperties,
78
78
  },
79
79
  }),
80
- ..._.merge({}, defaultQueryOpts, opts),
80
+ ..._.merge({ headers: { 'X-Strapi-Event': event } }, defaultQueryOpts, opts),
81
81
  };
82
82
 
83
83
  try {
@@ -8,6 +8,6 @@ export type Date = Attribute.OfType<'date'> &
8
8
  Attribute.RequiredOption &
9
9
  Attribute.UniqueOption;
10
10
 
11
- export type DateValue = Date;
11
+ export type DateValue = globalThis.Date | string;
12
12
 
13
13
  export type GetDateValue<T extends Attribute.Attribute> = T extends Date ? DateValue : never;
@@ -17,13 +17,17 @@ try {
17
17
  process.env.npm_config_global === 'true' ||
18
18
  JSON.parse(process.env.npm_config_argv).original.includes('global')
19
19
  ) {
20
+ const event = 'didInstallStrapi';
20
21
  fetch('https://analytics.strapi.io/api/v2/track', {
21
22
  method: 'POST',
22
23
  body: JSON.stringify({
23
- event: 'didInstallStrapi',
24
+ event,
24
25
  deviceId: machineID(),
25
26
  }),
26
- headers: { 'Content-Type': 'application/json' },
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ 'X-Strapi-Event': event,
30
+ },
27
31
  }).catch(() => {});
28
32
  }
29
33
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/strapi",
3
- "version": "4.11.2",
3
+ "version": "4.11.4",
4
4
  "description": "An open source headless CMS solution to create and manage your own API. It provides a powerful dashboard and features to make your life easier. Databases supported: MySQL, MariaDB, PostgreSQL, SQLite",
5
5
  "keywords": [
6
6
  "strapi",
@@ -81,19 +81,19 @@
81
81
  "dependencies": {
82
82
  "@koa/cors": "3.4.3",
83
83
  "@koa/router": "10.1.1",
84
- "@strapi/admin": "4.11.2",
85
- "@strapi/data-transfer": "4.11.2",
86
- "@strapi/database": "4.11.2",
87
- "@strapi/generate-new": "4.11.2",
88
- "@strapi/generators": "4.11.2",
89
- "@strapi/logger": "4.11.2",
90
- "@strapi/permissions": "4.11.2",
91
- "@strapi/plugin-content-manager": "4.11.2",
92
- "@strapi/plugin-content-type-builder": "4.11.2",
93
- "@strapi/plugin-email": "4.11.2",
94
- "@strapi/plugin-upload": "4.11.2",
95
- "@strapi/typescript-utils": "4.11.2",
96
- "@strapi/utils": "4.11.2",
84
+ "@strapi/admin": "4.11.4",
85
+ "@strapi/data-transfer": "4.11.4",
86
+ "@strapi/database": "4.11.4",
87
+ "@strapi/generate-new": "4.11.4",
88
+ "@strapi/generators": "4.11.4",
89
+ "@strapi/logger": "4.11.4",
90
+ "@strapi/permissions": "4.11.4",
91
+ "@strapi/plugin-content-manager": "4.11.4",
92
+ "@strapi/plugin-content-type-builder": "4.11.4",
93
+ "@strapi/plugin-email": "4.11.4",
94
+ "@strapi/plugin-upload": "4.11.4",
95
+ "@strapi/typescript-utils": "4.11.4",
96
+ "@strapi/utils": "4.11.4",
97
97
  "bcryptjs": "2.4.3",
98
98
  "boxen": "5.1.2",
99
99
  "chalk": "4.1.2",
@@ -142,5 +142,5 @@
142
142
  "node": ">=14.19.1 <=18.x.x",
143
143
  "npm": ">=6.0.0"
144
144
  },
145
- "gitHead": "6f7c815c2bbe41dda7d77136eb8df736c028ff67"
145
+ "gitHead": "8d325a695386ab9b578b360bf768cfa25173050f"
146
146
  }