@strapi/content-type-builder 5.9.0 → 5.10.1

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.
Files changed (210) hide show
  1. package/dist/admin/chunks/ListView-CDnrvVrV.mjs +1184 -0
  2. package/dist/admin/chunks/ListView-CDnrvVrV.mjs.map +1 -0
  3. package/dist/admin/chunks/ListView-CQwvSbZH.js +1186 -0
  4. package/dist/admin/chunks/ListView-CQwvSbZH.js.map +1 -0
  5. package/dist/admin/chunks/ar-Df0f0-PT.js +52 -0
  6. package/dist/admin/chunks/ar-Df0f0-PT.js.map +1 -0
  7. package/dist/admin/chunks/ar-sRW9VFC-.mjs +49 -0
  8. package/dist/admin/chunks/ar-sRW9VFC-.mjs.map +1 -0
  9. package/dist/admin/chunks/cs-BpQ26jiq.mjs +136 -0
  10. package/dist/{_chunks/cs-ChL4LaFY.mjs.map → admin/chunks/cs-BpQ26jiq.mjs.map} +1 -1
  11. package/dist/admin/chunks/cs-DeTwqc7p.js +140 -0
  12. package/dist/{_chunks/cs-Ci3js5EC.js.map → admin/chunks/cs-DeTwqc7p.js.map} +1 -1
  13. package/dist/admin/chunks/de-BJkS06jF.js +194 -0
  14. package/dist/{_chunks/de-DnlblIOh.js.map → admin/chunks/de-BJkS06jF.js.map} +1 -1
  15. package/dist/admin/chunks/de-DSxx5_x-.mjs +190 -0
  16. package/dist/{_chunks/de-DsHQNzp2.mjs.map → admin/chunks/de-DSxx5_x-.mjs.map} +1 -1
  17. package/dist/admin/chunks/dk-BnjVZ7A_.mjs +180 -0
  18. package/dist/{_chunks/es-BE_zx2_w.mjs.map → admin/chunks/dk-BnjVZ7A_.mjs.map} +1 -1
  19. package/dist/admin/chunks/dk-CGm-qVH7.js +184 -0
  20. package/dist/{_chunks/dk-D3XnOjYz.js.map → admin/chunks/dk-CGm-qVH7.js.map} +1 -1
  21. package/dist/admin/chunks/en-BJUu34b0.js +217 -0
  22. package/dist/{_chunks/en-CXG5y_vo.js.map → admin/chunks/en-BJUu34b0.js.map} +1 -1
  23. package/dist/admin/chunks/en-Bhut8Yay.mjs +213 -0
  24. package/dist/{_chunks/en-jBwb53yg.mjs.map → admin/chunks/en-Bhut8Yay.mjs.map} +1 -1
  25. package/dist/admin/chunks/es-DG8g9igJ.mjs +180 -0
  26. package/dist/admin/chunks/es-DG8g9igJ.mjs.map +1 -0
  27. package/dist/admin/chunks/es-J8kvHlNy.js +184 -0
  28. package/dist/{_chunks/es-DL8lez9W.js.map → admin/chunks/es-J8kvHlNy.js.map} +1 -1
  29. package/dist/admin/chunks/fr-C6y35iY7.js +76 -0
  30. package/dist/admin/chunks/fr-C6y35iY7.js.map +1 -0
  31. package/dist/admin/chunks/fr-UpV34MHY.mjs +73 -0
  32. package/dist/admin/chunks/fr-UpV34MHY.mjs.map +1 -0
  33. package/dist/admin/chunks/id-BWM18ljw.mjs +163 -0
  34. package/dist/{_chunks/ru-DGSjru5m.mjs.map → admin/chunks/id-BWM18ljw.mjs.map} +1 -1
  35. package/dist/admin/chunks/id-BvxV6wLP.js +167 -0
  36. package/dist/{_chunks/ru-C8A_4j0w.js.map → admin/chunks/id-BvxV6wLP.js.map} +1 -1
  37. package/dist/admin/chunks/index-BQ2VO38W.js +7781 -0
  38. package/dist/admin/chunks/index-BQ2VO38W.js.map +1 -0
  39. package/dist/admin/chunks/index-BZeN5KRn.js +1421 -0
  40. package/dist/admin/chunks/index-BZeN5KRn.js.map +1 -0
  41. package/dist/admin/chunks/index-BhX2euW0.mjs +1384 -0
  42. package/dist/admin/chunks/index-BhX2euW0.mjs.map +1 -0
  43. package/dist/admin/chunks/index-Cr5tfW7U.mjs +7754 -0
  44. package/dist/admin/chunks/index-Cr5tfW7U.mjs.map +1 -0
  45. package/dist/admin/chunks/it-1_vd9gV4.mjs +164 -0
  46. package/dist/{_chunks/tr-DsUerr-c.mjs.map → admin/chunks/it-1_vd9gV4.mjs.map} +1 -1
  47. package/dist/admin/chunks/it-C_IgFU-G.js +168 -0
  48. package/dist/{_chunks/sk-raWRcmPT.js.map → admin/chunks/it-C_IgFU-G.js.map} +1 -1
  49. package/dist/admin/chunks/ja-CWo4Qqq6.js +51 -0
  50. package/dist/admin/chunks/ja-CWo4Qqq6.js.map +1 -0
  51. package/dist/admin/chunks/ja-Cx23a2Ui.mjs +48 -0
  52. package/dist/admin/chunks/ja-Cx23a2Ui.mjs.map +1 -0
  53. package/dist/admin/chunks/ko-BsByJNEl.js +184 -0
  54. package/dist/admin/chunks/ko-BsByJNEl.js.map +1 -0
  55. package/dist/admin/chunks/ko-DC7paEx5.mjs +180 -0
  56. package/dist/admin/chunks/ko-DC7paEx5.mjs.map +1 -0
  57. package/dist/admin/chunks/ms-C3s4kxq6.mjs +160 -0
  58. package/dist/{_chunks/id-W1sKBFEw.mjs.map → admin/chunks/ms-C3s4kxq6.mjs.map} +1 -1
  59. package/dist/admin/chunks/ms-DPTzS7SH.js +164 -0
  60. package/dist/{_chunks/th-C83Bb_kR.js.map → admin/chunks/ms-DPTzS7SH.js.map} +1 -1
  61. package/dist/admin/chunks/nl-TzvfktV_.mjs +153 -0
  62. package/dist/{_chunks/nl-BaTAuelQ.mjs.map → admin/chunks/nl-TzvfktV_.mjs.map} +1 -1
  63. package/dist/admin/chunks/nl-db29QMOx.js +157 -0
  64. package/dist/{_chunks/nl-DQjrDEw0.js.map → admin/chunks/nl-db29QMOx.js.map} +1 -1
  65. package/dist/admin/chunks/pl-BdvupIN_.mjs +190 -0
  66. package/dist/admin/chunks/pl-BdvupIN_.mjs.map +1 -0
  67. package/dist/admin/chunks/pl-pYy1djj3.js +194 -0
  68. package/dist/admin/chunks/pl-pYy1djj3.js.map +1 -0
  69. package/dist/admin/chunks/pt-BQmWcdeG.js +52 -0
  70. package/dist/admin/chunks/pt-BQmWcdeG.js.map +1 -0
  71. package/dist/admin/chunks/pt-BR-CTPuXGWF.js +194 -0
  72. package/dist/{_chunks/pt-BR-DPd5nRnl.js.map → admin/chunks/pt-BR-CTPuXGWF.js.map} +1 -1
  73. package/dist/admin/chunks/pt-BR-DPrVmKeZ.mjs +190 -0
  74. package/dist/{_chunks/pt-BR-CCQGwXs0.mjs.map → admin/chunks/pt-BR-DPrVmKeZ.mjs.map} +1 -1
  75. package/dist/admin/chunks/pt-BTLIwmCv.mjs +49 -0
  76. package/dist/admin/chunks/pt-BTLIwmCv.mjs.map +1 -0
  77. package/dist/admin/chunks/ru-D46no502.mjs +165 -0
  78. package/dist/{_chunks/dk-BC7NAQR2.mjs.map → admin/chunks/ru-D46no502.mjs.map} +1 -1
  79. package/dist/admin/chunks/ru-DQiDXgUV.js +169 -0
  80. package/dist/admin/chunks/ru-DQiDXgUV.js.map +1 -0
  81. package/dist/admin/chunks/sk-Byr_l4Jc.mjs +164 -0
  82. package/dist/{_chunks/ko-DoNsXHXA.mjs.map → admin/chunks/sk-Byr_l4Jc.mjs.map} +1 -1
  83. package/dist/admin/chunks/sk-DrnebmXb.js +168 -0
  84. package/dist/{_chunks/it-DS4sM3km.js.map → admin/chunks/sk-DrnebmXb.js.map} +1 -1
  85. package/dist/admin/chunks/sv-Bbam7IDm.mjs +199 -0
  86. package/dist/admin/chunks/sv-Bbam7IDm.mjs.map +1 -0
  87. package/dist/admin/chunks/sv-CrWlNosi.js +203 -0
  88. package/dist/admin/chunks/sv-CrWlNosi.js.map +1 -0
  89. package/dist/admin/chunks/th-BbrCkfgX.js +165 -0
  90. package/dist/{_chunks/id-DYuTgqcc.js.map → admin/chunks/th-BbrCkfgX.js.map} +1 -1
  91. package/dist/admin/chunks/th-hfS0Wmk_.mjs +161 -0
  92. package/dist/{_chunks/it-D04lb2Wc.mjs.map → admin/chunks/th-hfS0Wmk_.mjs.map} +1 -1
  93. package/dist/admin/chunks/tr-CHdMj8m6.js +180 -0
  94. package/dist/admin/chunks/tr-CHdMj8m6.js.map +1 -0
  95. package/dist/admin/chunks/tr-DS7DBOhC.mjs +176 -0
  96. package/dist/admin/chunks/tr-DS7DBOhC.mjs.map +1 -0
  97. package/dist/admin/chunks/uk-BQEQ3weH.js +165 -0
  98. package/dist/{_chunks/uk-VwB0oiuV.js.map → admin/chunks/uk-BQEQ3weH.js.map} +1 -1
  99. package/dist/admin/chunks/uk-Cj8-BKeu.mjs +161 -0
  100. package/dist/{_chunks/sk-DVK4HfSC.mjs.map → admin/chunks/uk-Cj8-BKeu.mjs.map} +1 -1
  101. package/dist/admin/chunks/zh-BUVXH75-.mjs +199 -0
  102. package/dist/admin/chunks/zh-BUVXH75-.mjs.map +1 -0
  103. package/dist/admin/chunks/zh-CWj4avQA.js +203 -0
  104. package/dist/admin/chunks/zh-CWj4avQA.js.map +1 -0
  105. package/dist/admin/chunks/zh-Hans-BElOnuRb.mjs +144 -0
  106. package/dist/{_chunks/zh-Hans-Cc0M5PXr.mjs.map → admin/chunks/zh-Hans-BElOnuRb.mjs.map} +1 -1
  107. package/dist/admin/chunks/zh-Hans-lXbNiMp9.js +148 -0
  108. package/dist/{_chunks/zh-Hans-CLTLm_nt.js.map → admin/chunks/zh-Hans-lXbNiMp9.js.map} +1 -1
  109. package/dist/admin/index.js +24 -4
  110. package/dist/admin/index.js.map +1 -1
  111. package/dist/admin/index.mjs +17 -7
  112. package/dist/admin/index.mjs.map +1 -1
  113. package/dist/admin/src/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.d.ts +5 -2
  114. package/dist/admin/src/components/DataManagerProvider/reducer.d.ts +97 -6
  115. package/dist/admin/src/components/FormModal/reducer.d.ts +110 -4
  116. package/dist/admin/src/components/ListRow.d.ts +2 -1
  117. package/dist/admin/src/contexts/DataManagerContext.d.ts +4 -4
  118. package/dist/admin/src/index.d.ts +2 -1
  119. package/dist/admin/src/pages/ListView/LinkToCMSettingsView.d.ts +1 -2
  120. package/dist/admin/src/pluginId.d.ts +1 -1
  121. package/dist/admin/src/reducers.d.ts +8 -1
  122. package/dist/admin/src/types.d.ts +2 -5
  123. package/dist/server/index.js +2504 -2151
  124. package/dist/server/index.js.map +1 -1
  125. package/dist/server/index.mjs +2500 -2147
  126. package/dist/server/index.mjs.map +1 -1
  127. package/package.json +13 -10
  128. package/dist/_chunks/ListView-B7k6NgwS.mjs +0 -959
  129. package/dist/_chunks/ListView-B7k6NgwS.mjs.map +0 -1
  130. package/dist/_chunks/ListView-CsRxS9zZ.js +0 -964
  131. package/dist/_chunks/ListView-CsRxS9zZ.js.map +0 -1
  132. package/dist/_chunks/ar-BYDB75EB.mjs +0 -51
  133. package/dist/_chunks/ar-BYDB75EB.mjs.map +0 -1
  134. package/dist/_chunks/ar-OCxhAFUy.js +0 -51
  135. package/dist/_chunks/ar-OCxhAFUy.js.map +0 -1
  136. package/dist/_chunks/cs-ChL4LaFY.mjs +0 -139
  137. package/dist/_chunks/cs-Ci3js5EC.js +0 -139
  138. package/dist/_chunks/de-DnlblIOh.js +0 -193
  139. package/dist/_chunks/de-DsHQNzp2.mjs +0 -193
  140. package/dist/_chunks/dk-BC7NAQR2.mjs +0 -183
  141. package/dist/_chunks/dk-D3XnOjYz.js +0 -183
  142. package/dist/_chunks/en-CXG5y_vo.js +0 -216
  143. package/dist/_chunks/en-jBwb53yg.mjs +0 -216
  144. package/dist/_chunks/es-BE_zx2_w.mjs +0 -183
  145. package/dist/_chunks/es-DL8lez9W.js +0 -183
  146. package/dist/_chunks/fr-DnTxugIo.js +0 -75
  147. package/dist/_chunks/fr-DnTxugIo.js.map +0 -1
  148. package/dist/_chunks/fr-lU_OMJma.mjs +0 -75
  149. package/dist/_chunks/fr-lU_OMJma.mjs.map +0 -1
  150. package/dist/_chunks/id-DYuTgqcc.js +0 -166
  151. package/dist/_chunks/id-W1sKBFEw.mjs +0 -166
  152. package/dist/_chunks/index-97hm9i_H.mjs +0 -1331
  153. package/dist/_chunks/index-97hm9i_H.mjs.map +0 -1
  154. package/dist/_chunks/index-B5tHY87r.mjs +0 -6694
  155. package/dist/_chunks/index-B5tHY87r.mjs.map +0 -1
  156. package/dist/_chunks/index-BgMd59JL.js +0 -6730
  157. package/dist/_chunks/index-BgMd59JL.js.map +0 -1
  158. package/dist/_chunks/index-Cr85ijm8.js +0 -1357
  159. package/dist/_chunks/index-Cr85ijm8.js.map +0 -1
  160. package/dist/_chunks/it-D04lb2Wc.mjs +0 -167
  161. package/dist/_chunks/it-DS4sM3km.js +0 -167
  162. package/dist/_chunks/ja-BHLK_2_g.mjs +0 -50
  163. package/dist/_chunks/ja-BHLK_2_g.mjs.map +0 -1
  164. package/dist/_chunks/ja-BjouJgZf.js +0 -50
  165. package/dist/_chunks/ja-BjouJgZf.js.map +0 -1
  166. package/dist/_chunks/ko-D_71Pdfn.js +0 -183
  167. package/dist/_chunks/ko-D_71Pdfn.js.map +0 -1
  168. package/dist/_chunks/ko-DoNsXHXA.mjs +0 -183
  169. package/dist/_chunks/ms-BtGFDB9t.mjs +0 -163
  170. package/dist/_chunks/ms-BtGFDB9t.mjs.map +0 -1
  171. package/dist/_chunks/ms-Re1pSHmx.js +0 -163
  172. package/dist/_chunks/ms-Re1pSHmx.js.map +0 -1
  173. package/dist/_chunks/nl-BaTAuelQ.mjs +0 -156
  174. package/dist/_chunks/nl-DQjrDEw0.js +0 -156
  175. package/dist/_chunks/pl-BGwXgwH7.js +0 -193
  176. package/dist/_chunks/pl-BGwXgwH7.js.map +0 -1
  177. package/dist/_chunks/pl-CP2Zgp01.mjs +0 -193
  178. package/dist/_chunks/pl-CP2Zgp01.mjs.map +0 -1
  179. package/dist/_chunks/pt-BR-CCQGwXs0.mjs +0 -193
  180. package/dist/_chunks/pt-BR-DPd5nRnl.js +0 -193
  181. package/dist/_chunks/pt-CJoUDTHQ.js +0 -51
  182. package/dist/_chunks/pt-CJoUDTHQ.js.map +0 -1
  183. package/dist/_chunks/pt-DMeTMW2x.mjs +0 -51
  184. package/dist/_chunks/pt-DMeTMW2x.mjs.map +0 -1
  185. package/dist/_chunks/ru-C8A_4j0w.js +0 -168
  186. package/dist/_chunks/ru-DGSjru5m.mjs +0 -168
  187. package/dist/_chunks/sk-DVK4HfSC.mjs +0 -167
  188. package/dist/_chunks/sk-raWRcmPT.js +0 -167
  189. package/dist/_chunks/sv-BGb12eW3.mjs +0 -202
  190. package/dist/_chunks/sv-BGb12eW3.mjs.map +0 -1
  191. package/dist/_chunks/sv-BNN71SFE.js +0 -202
  192. package/dist/_chunks/sv-BNN71SFE.js.map +0 -1
  193. package/dist/_chunks/th--u3VqsON.mjs +0 -164
  194. package/dist/_chunks/th--u3VqsON.mjs.map +0 -1
  195. package/dist/_chunks/th-C83Bb_kR.js +0 -164
  196. package/dist/_chunks/tr-BW20CfcO.js +0 -179
  197. package/dist/_chunks/tr-BW20CfcO.js.map +0 -1
  198. package/dist/_chunks/tr-DsUerr-c.mjs +0 -179
  199. package/dist/_chunks/uk-Bx5IlOKX.mjs +0 -164
  200. package/dist/_chunks/uk-Bx5IlOKX.mjs.map +0 -1
  201. package/dist/_chunks/uk-VwB0oiuV.js +0 -164
  202. package/dist/_chunks/zh-BiOCwPJu.js +0 -202
  203. package/dist/_chunks/zh-BiOCwPJu.js.map +0 -1
  204. package/dist/_chunks/zh-CsUDN13W.mjs +0 -202
  205. package/dist/_chunks/zh-CsUDN13W.mjs.map +0 -1
  206. package/dist/_chunks/zh-Hans-CLTLm_nt.js +0 -147
  207. package/dist/_chunks/zh-Hans-Cc0M5PXr.mjs +0 -147
  208. package/dist/admin/src/components/DataManagerProvider/constants.d.ts +0 -17
  209. package/dist/admin/src/components/FormModal/constants.d.ts +0 -12
  210. package/dist/admin/src/pages/RecursivePath/RecursivePath.d.ts +0 -1
@@ -1,1293 +1,1435 @@
1
- import _, { get, has } from "lodash";
2
- import { getOr, snakeCase, isUndefined, has as has$1, flatMap } from "lodash/fp";
3
- import utils, { errors, strings, contentTypes as contentTypes$2, yup, validateYupSchema } from "@strapi/utils";
4
- import * as path from "path";
5
- import path__default, { join } from "path";
6
- import * as fse from "fs-extra";
7
- import fse__default from "fs-extra";
8
- import pluralize from "pluralize";
9
- const config = {
10
- default: {},
11
- validator() {
12
- }
13
- };
14
- const bootstrap = async ({ strapi: strapi2 }) => {
15
- const actions = [
16
- {
17
- section: "plugins",
18
- displayName: "Read",
19
- uid: "read",
20
- pluginName: "content-type-builder"
21
- }
22
- ];
23
- await strapi2.service("admin::permission").actionProvider.registerMany(actions);
24
- };
25
- const { ApplicationError: ApplicationError$3 } = errors;
26
- const isConfigurable = (attribute) => _.get(attribute, "configurable", true);
27
- const isRelation = (attribute) => attribute.type === "relation";
28
- const formatAttributes = (model) => {
29
- const { getVisibleAttributes } = utils.contentTypes;
30
- return getVisibleAttributes(model).reduce((acc, key) => {
31
- acc[key] = formatAttribute(model.attributes[key]);
32
- return acc;
33
- }, {});
34
- };
35
- const formatAttribute = (attribute) => {
36
- const { configurable, required, autoPopulate, pluginOptions } = attribute;
37
- if (attribute.type === "media") {
38
- return {
39
- type: "media",
40
- multiple: !!attribute.multiple,
41
- required: !!required,
42
- configurable: configurable === false ? false : void 0,
43
- private: !!attribute.private,
44
- allowedTypes: attribute.allowedTypes,
45
- pluginOptions
46
- };
47
- }
48
- if (attribute.type === "relation") {
49
- return {
50
- ...attribute,
51
- type: "relation",
52
- target: attribute.target,
53
- targetAttribute: attribute.inversedBy || attribute.mappedBy || null,
54
- configurable: configurable === false ? false : void 0,
55
- private: !!attribute.private,
56
- pluginOptions,
57
- // TODO: remove
58
- autoPopulate
59
- };
60
- }
61
- return attribute;
62
- };
63
- const replaceTemporaryUIDs = (uidMap) => (schema) => {
64
- return {
65
- ...schema,
66
- attributes: Object.keys(schema.attributes).reduce((acc, key) => {
67
- const attr = schema.attributes[key];
68
- if (attr.type === "component") {
69
- if (_.has(uidMap, attr.component)) {
70
- acc[key] = {
71
- ...attr,
72
- component: uidMap[attr.component]
73
- };
74
- return acc;
75
- }
76
- if (!_.has(strapi.components, attr.component)) {
77
- throw new ApplicationError$3("component.notFound");
1
+ import '@strapi/types';
2
+ import _, { get, has } from 'lodash';
3
+ import { getOr, snakeCase, isUndefined, has as has$1, flatMap } from 'lodash/fp';
4
+ import utils, { errors, strings, contentTypes as contentTypes$2, yup, validateYupSchema } from '@strapi/utils';
5
+ import * as path from 'path';
6
+ import path__default, { join } from 'path';
7
+ import * as fse from 'fs-extra';
8
+ import fse__default from 'fs-extra';
9
+ import pluralize from 'pluralize';
10
+
11
+ var config = {
12
+ default: {},
13
+ validator () {}
14
+ };
15
+
16
+ var bootstrap = (async ({ strapi })=>{
17
+ const actions = [
18
+ {
19
+ section: 'plugins',
20
+ displayName: 'Read',
21
+ uid: 'read',
22
+ pluginName: 'content-type-builder'
78
23
  }
79
- }
80
- if (attr.type === "dynamiczone" && _.intersection(attr.components, Object.keys(uidMap)).length > 0) {
81
- acc[key] = {
82
- ...attr,
83
- components: attr.components.map((value) => {
84
- if (_.has(uidMap, value)) return uidMap[value];
85
- if (!_.has(strapi.components, value)) {
86
- throw new ApplicationError$3("component.notFound");
87
- }
88
- return value;
89
- })
90
- };
24
+ ];
25
+ await strapi.service('admin::permission').actionProvider.registerMany(actions);
26
+ });
27
+
28
+ const { ApplicationError: ApplicationError$3 } = errors;
29
+ const isConfigurable = (attribute)=>_.get(attribute, 'configurable', true);
30
+ const isRelation = (attribute)=>attribute.type === 'relation';
31
+ /**
32
+ * Formats a component's attributes
33
+ */ const formatAttributes = (model)=>{
34
+ const { getVisibleAttributes } = utils.contentTypes;
35
+ // only get attributes that can be seen in the CTB
36
+ return getVisibleAttributes(model).reduce((acc, key)=>{
37
+ acc[key] = formatAttribute(model.attributes[key]);
91
38
  return acc;
92
- }
93
- acc[key] = attr;
94
- return acc;
95
- }, {})
96
- };
97
- };
98
- function createSchemaHandler(infos) {
99
- const { category, modelName, plugin, uid, dir, filename, schema } = infos;
100
- const initialState = {
101
- modelName,
102
- plugin,
103
- category,
104
- uid,
105
- dir,
106
- filename,
107
- schema: schema || {
108
- info: {},
109
- options: {},
110
- attributes: {}
39
+ }, {});
40
+ };
41
+ /**
42
+ * Formats a component attribute
43
+ */ const formatAttribute = (attribute)=>{
44
+ const { configurable, required, autoPopulate, pluginOptions } = attribute;
45
+ if (attribute.type === 'media') {
46
+ return {
47
+ type: 'media',
48
+ multiple: !!attribute.multiple,
49
+ required: !!required,
50
+ configurable: configurable === false ? false : undefined,
51
+ private: !!attribute.private,
52
+ allowedTypes: attribute.allowedTypes,
53
+ pluginOptions
54
+ };
111
55
  }
112
- };
113
- const state = _.cloneDeep(initialState);
114
- Object.freeze(initialState.schema);
115
- let modified = false;
116
- let deleted = false;
117
- return {
118
- get modelName() {
119
- return initialState.modelName;
120
- },
121
- get plugin() {
122
- return initialState.plugin;
123
- },
124
- get category() {
125
- return initialState.category;
126
- },
127
- get kind() {
128
- return _.get(state.schema, "kind", "collectionType");
129
- },
130
- get uid() {
131
- return state.uid;
132
- },
133
- get writable() {
134
- return _.get(state, "plugin") !== "admin";
135
- },
136
- setUID(val) {
137
- modified = true;
138
- state.uid = val;
139
- return this;
140
- },
141
- setDir(val) {
142
- modified = true;
143
- state.dir = val;
144
- return this;
145
- },
146
- get schema() {
147
- return _.cloneDeep(state.schema);
148
- },
149
- setSchema(val) {
150
- modified = true;
151
- state.schema = _.cloneDeep(val);
152
- return this;
153
- },
154
- // get a particular path inside the schema
155
- get(path2) {
156
- return _.get(state.schema, path2);
157
- },
158
- // set a particular path inside the schema
159
- set(path2, val) {
160
- if (!state.schema) return this;
161
- modified = true;
162
- const value = _.defaultTo(val, _.get(state.schema, path2));
163
- _.set(state.schema, path2, value);
164
- return this;
165
- },
166
- // delete a particular path inside the schema
167
- unset(path2) {
168
- modified = true;
169
- _.unset(state.schema, path2);
170
- return this;
171
- },
172
- delete() {
173
- deleted = true;
174
- return this;
175
- },
176
- getAttribute(key) {
177
- return this.get(["attributes", key]);
178
- },
179
- setAttribute(key, attribute) {
180
- return this.set(["attributes", key], attribute);
181
- },
182
- deleteAttribute(key) {
183
- return this.unset(["attributes", key]);
184
- },
185
- setAttributes(newAttributes) {
186
- if (!this.schema) return this;
187
- for (const key in this.schema.attributes) {
188
- if (isConfigurable(this.schema.attributes[key])) {
189
- this.deleteAttribute(key);
190
- }
191
- }
192
- for (const key of Object.keys(newAttributes)) {
193
- this.setAttribute(key, newAttributes[key]);
194
- }
195
- return this;
196
- },
197
- removeContentType(uid2) {
198
- if (!state.schema) return this;
199
- const attributes = state.schema.attributes;
200
- Object.keys(attributes).forEach((key) => {
201
- const attribute = attributes[key];
202
- if (attribute.target === uid2) {
203
- this.deleteAttribute(key);
204
- }
205
- });
206
- return this;
207
- },
208
- // utils
209
- removeComponent(uid2) {
210
- if (!state.schema) return this;
211
- const attributes = state.schema.attributes;
212
- Object.keys(attributes).forEach((key) => {
213
- const attr = attributes[key];
214
- if (attr.type === "component" && attr.component === uid2) {
215
- this.deleteAttribute(key);
216
- }
217
- if (attr.type === "dynamiczone" && Array.isArray(attr.components) && attr.components.includes(uid2)) {
218
- const updatedComponentList = attributes[key].components.filter(
219
- (val) => val !== uid2
220
- );
221
- this.set(["attributes", key, "components"], updatedComponentList);
222
- }
223
- });
224
- return this;
225
- },
226
- updateComponent(uid2, newUID) {
227
- if (!state.schema) return this;
228
- const attributes = state.schema.attributes;
229
- Object.keys(attributes).forEach((key) => {
230
- const attr = attributes[key];
231
- if (attr.type === "component" && attr.component === uid2) {
232
- this.set(["attributes", key, "component"], newUID);
233
- }
234
- if (attr.type === "dynamiczone" && Array.isArray(attr.components) && attr.components.includes(uid2)) {
235
- const updatedComponentList = attr.components.map(
236
- (val) => val === uid2 ? newUID : val
237
- );
238
- this.set(["attributes", key, "components"], updatedComponentList);
239
- }
240
- });
241
- return this;
242
- },
243
- // save the schema to disk
244
- async flush() {
245
- if (!this.writable) {
246
- return;
247
- }
248
- const initialPath = path__default.join(initialState.dir, initialState.filename);
249
- const filePath = path__default.join(state.dir, state.filename);
250
- if (deleted) {
251
- await fse__default.remove(initialPath);
252
- const list = await fse__default.readdir(initialState.dir);
253
- if (list.length === 0) {
254
- await fse__default.remove(initialState.dir);
255
- }
256
- return;
257
- }
258
- if (modified) {
259
- if (!state.schema) return Promise.resolve();
260
- await fse__default.ensureFile(filePath);
261
- await fse__default.writeJSON(
262
- filePath,
263
- {
264
- kind: state.schema.kind,
265
- collectionName: state.schema.collectionName,
266
- info: state.schema.info,
267
- options: state.schema.options,
268
- pluginOptions: state.schema.pluginOptions,
269
- attributes: state.schema.attributes,
270
- config: state.schema.config
271
- },
272
- { spaces: 2 }
273
- );
274
- if (initialPath !== filePath) {
275
- await fse__default.remove(initialPath);
276
- const list = await fse__default.readdir(initialState.dir);
277
- if (list.length === 0) {
278
- await fse__default.remove(initialState.dir);
279
- }
280
- }
281
- return;
282
- }
283
- return Promise.resolve();
284
- },
285
- // reset the schema to its initial value
286
- async rollback() {
287
- if (!this.writable) {
288
- return;
289
- }
290
- const initialPath = path__default.join(initialState.dir, initialState.filename);
291
- const filePath = path__default.join(state.dir, state.filename);
292
- if (!initialState.uid) {
293
- await fse__default.remove(filePath);
294
- const list = await fse__default.readdir(state.dir);
295
- if (list.length === 0) {
296
- await fse__default.remove(state.dir);
56
+ if (attribute.type === 'relation') {
57
+ return {
58
+ ...attribute,
59
+ type: 'relation',
60
+ target: attribute.target,
61
+ targetAttribute: attribute.inversedBy || attribute.mappedBy || null,
62
+ configurable: configurable === false ? false : undefined,
63
+ private: !!attribute.private,
64
+ pluginOptions,
65
+ // TODO: remove
66
+ autoPopulate
67
+ };
68
+ }
69
+ return attribute;
70
+ };
71
+ // TODO: move to schema builder
72
+ const replaceTemporaryUIDs = (uidMap)=>(schema)=>{
73
+ return {
74
+ ...schema,
75
+ attributes: Object.keys(schema.attributes).reduce((acc, key)=>{
76
+ const attr = schema.attributes[key];
77
+ if (attr.type === 'component') {
78
+ if (_.has(uidMap, attr.component)) {
79
+ acc[key] = {
80
+ ...attr,
81
+ component: uidMap[attr.component]
82
+ };
83
+ return acc;
84
+ }
85
+ if (!_.has(strapi.components, attr.component)) {
86
+ throw new ApplicationError$3('component.notFound');
87
+ }
88
+ }
89
+ if (attr.type === 'dynamiczone' && _.intersection(attr.components, Object.keys(uidMap)).length > 0) {
90
+ acc[key] = {
91
+ ...attr,
92
+ components: attr.components.map((value)=>{
93
+ if (_.has(uidMap, value)) return uidMap[value];
94
+ if (!_.has(strapi.components, value)) {
95
+ throw new ApplicationError$3('component.notFound');
96
+ }
97
+ return value;
98
+ })
99
+ };
100
+ return acc;
101
+ }
102
+ acc[key] = attr;
103
+ return acc;
104
+ }, {})
105
+ };
106
+ };
107
+
108
+ function createSchemaHandler(infos) {
109
+ const { category, modelName, plugin, uid, dir, filename, schema } = infos;
110
+ const initialState = {
111
+ modelName,
112
+ plugin,
113
+ category,
114
+ uid,
115
+ dir,
116
+ filename,
117
+ schema: schema || {
118
+ info: {},
119
+ options: {},
120
+ attributes: {}
297
121
  }
298
- return;
299
- }
300
- if (modified || deleted) {
301
- await fse__default.ensureFile(initialPath);
302
- await fse__default.writeJSON(initialPath, initialState.schema, { spaces: 2 });
303
- if (initialPath !== filePath) {
304
- await fse__default.remove(filePath);
305
- const list = await fse__default.readdir(state.dir);
306
- if (list.length === 0) {
307
- await fse__default.remove(state.dir);
308
- }
122
+ };
123
+ const state = _.cloneDeep(initialState);
124
+ // always keep it the same to rollback
125
+ Object.freeze(initialState.schema);
126
+ let modified = false;
127
+ let deleted = false;
128
+ return {
129
+ get modelName () {
130
+ return initialState.modelName;
131
+ },
132
+ get plugin () {
133
+ return initialState.plugin;
134
+ },
135
+ get category () {
136
+ return initialState.category;
137
+ },
138
+ get kind () {
139
+ return _.get(state.schema, 'kind', 'collectionType');
140
+ },
141
+ get uid () {
142
+ return state.uid;
143
+ },
144
+ get writable () {
145
+ return _.get(state, 'plugin') !== 'admin';
146
+ },
147
+ setUID (val) {
148
+ modified = true;
149
+ state.uid = val;
150
+ return this;
151
+ },
152
+ setDir (val) {
153
+ modified = true;
154
+ state.dir = val;
155
+ return this;
156
+ },
157
+ get schema () {
158
+ return _.cloneDeep(state.schema);
159
+ },
160
+ setSchema (val) {
161
+ modified = true;
162
+ state.schema = _.cloneDeep(val);
163
+ return this;
164
+ },
165
+ // get a particular path inside the schema
166
+ get (path) {
167
+ return _.get(state.schema, path);
168
+ },
169
+ // set a particular path inside the schema
170
+ set (path, val) {
171
+ if (!state.schema) return this;
172
+ modified = true;
173
+ const value = _.defaultTo(val, _.get(state.schema, path));
174
+ _.set(state.schema, path, value);
175
+ return this;
176
+ },
177
+ // delete a particular path inside the schema
178
+ unset (path) {
179
+ modified = true;
180
+ _.unset(state.schema, path);
181
+ return this;
182
+ },
183
+ delete () {
184
+ deleted = true;
185
+ return this;
186
+ },
187
+ getAttribute (key) {
188
+ return this.get([
189
+ 'attributes',
190
+ key
191
+ ]);
192
+ },
193
+ setAttribute (key, attribute) {
194
+ return this.set([
195
+ 'attributes',
196
+ key
197
+ ], attribute);
198
+ },
199
+ deleteAttribute (key) {
200
+ return this.unset([
201
+ 'attributes',
202
+ key
203
+ ]);
204
+ },
205
+ setAttributes (newAttributes) {
206
+ if (!this.schema) return this;
207
+ // delete old configurable attributes
208
+ for(const key in this.schema.attributes){
209
+ if (isConfigurable(this.schema.attributes[key])) {
210
+ this.deleteAttribute(key);
211
+ }
212
+ }
213
+ // set new Attributes
214
+ for (const key of Object.keys(newAttributes)){
215
+ this.setAttribute(key, newAttributes[key]);
216
+ }
217
+ return this;
218
+ },
219
+ removeContentType (uid) {
220
+ if (!state.schema) return this;
221
+ const attributes = state.schema.attributes;
222
+ Object.keys(attributes).forEach((key)=>{
223
+ const attribute = attributes[key];
224
+ if (attribute.target === uid) {
225
+ this.deleteAttribute(key);
226
+ }
227
+ });
228
+ return this;
229
+ },
230
+ // utils
231
+ removeComponent (uid) {
232
+ if (!state.schema) return this;
233
+ const attributes = state.schema.attributes;
234
+ Object.keys(attributes).forEach((key)=>{
235
+ const attr = attributes[key];
236
+ if (attr.type === 'component' && attr.component === uid) {
237
+ this.deleteAttribute(key);
238
+ }
239
+ if (attr.type === 'dynamiczone' && Array.isArray(attr.components) && attr.components.includes(uid)) {
240
+ const updatedComponentList = attributes[key].components.filter((val)=>val !== uid);
241
+ this.set([
242
+ 'attributes',
243
+ key,
244
+ 'components'
245
+ ], updatedComponentList);
246
+ }
247
+ });
248
+ return this;
249
+ },
250
+ updateComponent (uid, newUID) {
251
+ if (!state.schema) return this;
252
+ const attributes = state.schema.attributes;
253
+ Object.keys(attributes).forEach((key)=>{
254
+ const attr = attributes[key];
255
+ if (attr.type === 'component' && attr.component === uid) {
256
+ this.set([
257
+ 'attributes',
258
+ key,
259
+ 'component'
260
+ ], newUID);
261
+ }
262
+ if (attr.type === 'dynamiczone' && Array.isArray(attr.components) && attr.components.includes(uid)) {
263
+ const updatedComponentList = attr.components.map((val)=>val === uid ? newUID : val);
264
+ this.set([
265
+ 'attributes',
266
+ key,
267
+ 'components'
268
+ ], updatedComponentList);
269
+ }
270
+ });
271
+ return this;
272
+ },
273
+ // save the schema to disk
274
+ async flush () {
275
+ if (!this.writable) {
276
+ return;
277
+ }
278
+ const initialPath = path__default.join(initialState.dir, initialState.filename);
279
+ const filePath = path__default.join(state.dir, state.filename);
280
+ if (deleted) {
281
+ await fse__default.remove(initialPath);
282
+ const list = await fse__default.readdir(initialState.dir);
283
+ if (list.length === 0) {
284
+ await fse__default.remove(initialState.dir);
285
+ }
286
+ return;
287
+ }
288
+ if (modified) {
289
+ if (!state.schema) return Promise.resolve();
290
+ await fse__default.ensureFile(filePath);
291
+ await fse__default.writeJSON(filePath, {
292
+ kind: state.schema.kind,
293
+ collectionName: state.schema.collectionName,
294
+ info: state.schema.info,
295
+ options: state.schema.options,
296
+ pluginOptions: state.schema.pluginOptions,
297
+ attributes: state.schema.attributes,
298
+ config: state.schema.config
299
+ }, {
300
+ spaces: 2
301
+ });
302
+ // remove from oldPath
303
+ if (initialPath !== filePath) {
304
+ await fse__default.remove(initialPath);
305
+ const list = await fse__default.readdir(initialState.dir);
306
+ if (list.length === 0) {
307
+ await fse__default.remove(initialState.dir);
308
+ }
309
+ }
310
+ return;
311
+ }
312
+ return Promise.resolve();
313
+ },
314
+ // reset the schema to its initial value
315
+ async rollback () {
316
+ if (!this.writable) {
317
+ return;
318
+ }
319
+ const initialPath = path__default.join(initialState.dir, initialState.filename);
320
+ const filePath = path__default.join(state.dir, state.filename);
321
+ // it was a creation so it needs to be deleted
322
+ if (!initialState.uid) {
323
+ await fse__default.remove(filePath);
324
+ const list = await fse__default.readdir(state.dir);
325
+ if (list.length === 0) {
326
+ await fse__default.remove(state.dir);
327
+ }
328
+ return;
329
+ }
330
+ if (modified || deleted) {
331
+ await fse__default.ensureFile(initialPath);
332
+ await fse__default.writeJSON(initialPath, initialState.schema, {
333
+ spaces: 2
334
+ });
335
+ // remove
336
+ if (initialPath !== filePath) {
337
+ await fse__default.remove(filePath);
338
+ const list = await fse__default.readdir(state.dir);
339
+ if (list.length === 0) {
340
+ await fse__default.remove(state.dir);
341
+ }
342
+ }
343
+ }
344
+ return Promise.resolve();
309
345
  }
310
- }
311
- return Promise.resolve();
312
- }
313
- };
346
+ };
314
347
  }
348
+
315
349
  const { ApplicationError: ApplicationError$2 } = errors;
316
350
  function createComponentBuilder$1() {
317
- return {
318
- createComponentUID({ category, displayName }) {
319
- return `${strings.nameToSlug(category)}.${strings.nameToSlug(displayName)}`;
320
- },
321
- createNewComponentUIDMap(components2) {
322
- return components2.reduce((uidMap, component) => {
323
- uidMap[component.tmpUID] = this.createComponentUID(component);
324
- return uidMap;
325
- }, {});
326
- },
327
- /**
351
+ return {
352
+ createComponentUID ({ category, displayName }) {
353
+ return `${strings.nameToSlug(category)}.${strings.nameToSlug(displayName)}`;
354
+ },
355
+ createNewComponentUIDMap (components) {
356
+ return components.reduce((uidMap, component)=>{
357
+ uidMap[component.tmpUID] = this.createComponentUID(component);
358
+ return uidMap;
359
+ }, {});
360
+ },
361
+ /**
328
362
  * create a component in the tmpComponent map
329
- */
330
- createComponent(infos) {
331
- const uid = this.createComponentUID(infos);
332
- if (this.components.has(uid)) {
333
- throw new ApplicationError$2("component.alreadyExists");
334
- }
335
- const handler = createSchemaHandler({
336
- dir: path__default.join(strapi.dirs.app.components, strings.nameToSlug(infos.category)),
337
- filename: `${strings.nameToSlug(infos.displayName)}.json`
338
- });
339
- const collectionName = `components_${strings.nameToCollectionName(
340
- infos.category
341
- )}_${strings.nameToCollectionName(pluralize(infos.displayName))}`;
342
- this.components.forEach((compo) => {
343
- if (compo.schema.collectionName === collectionName) {
344
- throw new ApplicationError$2("component.alreadyExists");
345
- }
346
- });
347
- handler.setUID(uid).set("collectionName", collectionName).set(["info", "displayName"], infos.displayName).set(["info", "icon"], infos.icon).set(["info", "description"], infos.description).set("pluginOptions", infos.pluginOptions).set("config", infos.config).setAttributes(this.convertAttributes(infos.attributes));
348
- if (this.components.size === 0) {
349
- strapi.telemetry.send("didCreateFirstComponent");
350
- } else {
351
- strapi.telemetry.send("didCreateComponent");
352
- }
353
- this.components.set(uid, handler);
354
- return handler;
355
- },
356
- /**
363
+ */ createComponent (infos) {
364
+ const uid = this.createComponentUID(infos);
365
+ if (this.components.has(uid)) {
366
+ throw new ApplicationError$2('component.alreadyExists');
367
+ }
368
+ const handler = createSchemaHandler({
369
+ dir: path__default.join(strapi.dirs.app.components, strings.nameToSlug(infos.category)),
370
+ filename: `${strings.nameToSlug(infos.displayName)}.json`
371
+ });
372
+ // TODO: create a utility for this
373
+ // Duplicate in admin/src/components/FormModal/forms/utils/createCollectionName.ts
374
+ const collectionName = `components_${strings.nameToCollectionName(infos.category)}_${strings.nameToCollectionName(pluralize(infos.displayName))}`;
375
+ this.components.forEach((compo)=>{
376
+ if (compo.schema.collectionName === collectionName) {
377
+ throw new ApplicationError$2('component.alreadyExists');
378
+ }
379
+ });
380
+ handler.setUID(uid).set('collectionName', collectionName).set([
381
+ 'info',
382
+ 'displayName'
383
+ ], infos.displayName).set([
384
+ 'info',
385
+ 'icon'
386
+ ], infos.icon).set([
387
+ 'info',
388
+ 'description'
389
+ ], infos.description).set('pluginOptions', infos.pluginOptions).set('config', infos.config).setAttributes(this.convertAttributes(infos.attributes));
390
+ if (this.components.size === 0) {
391
+ strapi.telemetry.send('didCreateFirstComponent');
392
+ } else {
393
+ strapi.telemetry.send('didCreateComponent');
394
+ }
395
+ this.components.set(uid, handler);
396
+ return handler;
397
+ },
398
+ /**
357
399
  * create a component in the tmpComponent map
358
- */
359
- editComponent(infos) {
360
- const { uid } = infos;
361
- if (!this.components.has(uid)) {
362
- throw new errors.ApplicationError("component.notFound");
363
- }
364
- const component = this.components.get(uid);
365
- const [, nameUID] = uid.split(".");
366
- const newCategory = strings.nameToSlug(infos.category);
367
- const newUID = `${newCategory}.${nameUID}`;
368
- if (newUID !== uid && this.components.has(newUID)) {
369
- throw new errors.ApplicationError("component.edit.alreadyExists");
370
- }
371
- const newDir = path__default.join(strapi.dirs.app.components, newCategory);
372
- const oldAttributes = component.schema.attributes;
373
- const newAttributes = _.omitBy(infos.attributes, (attr, key) => {
374
- return _.has(oldAttributes, key) && !isConfigurable(oldAttributes[key]);
375
- });
376
- component.setUID(newUID).setDir(newDir).set(["info", "displayName"], infos.displayName).set(["info", "icon"], infos.icon).set(["info", "description"], infos.description).set("pluginOptions", infos.pluginOptions).setAttributes(this.convertAttributes(newAttributes));
377
- if (newUID !== uid) {
378
- this.components.forEach((compo) => {
379
- compo.updateComponent(uid, newUID);
380
- });
381
- this.contentTypes.forEach((ct) => {
382
- ct.updateComponent(uid, newUID);
383
- });
384
- }
385
- return component;
386
- },
387
- deleteComponent(uid) {
388
- if (!this.components.has(uid)) {
389
- throw new errors.ApplicationError("component.notFound");
390
- }
391
- this.components.forEach((compo) => {
392
- compo.removeComponent(uid);
393
- });
394
- this.contentTypes.forEach((ct) => {
395
- ct.removeComponent(uid);
396
- });
397
- return this.components.get(uid).delete();
398
- }
399
- };
400
+ */ editComponent (infos) {
401
+ const { uid } = infos;
402
+ if (!this.components.has(uid)) {
403
+ throw new errors.ApplicationError('component.notFound');
404
+ }
405
+ const component = this.components.get(uid);
406
+ const [, nameUID] = uid.split('.');
407
+ const newCategory = strings.nameToSlug(infos.category);
408
+ const newUID = `${newCategory}.${nameUID}`;
409
+ if (newUID !== uid && this.components.has(newUID)) {
410
+ throw new errors.ApplicationError('component.edit.alreadyExists');
411
+ }
412
+ const newDir = path__default.join(strapi.dirs.app.components, newCategory);
413
+ const oldAttributes = component.schema.attributes;
414
+ const newAttributes = _.omitBy(infos.attributes, (attr, key)=>{
415
+ return _.has(oldAttributes, key) && !isConfigurable(oldAttributes[key]);
416
+ });
417
+ component.setUID(newUID).setDir(newDir).set([
418
+ 'info',
419
+ 'displayName'
420
+ ], infos.displayName).set([
421
+ 'info',
422
+ 'icon'
423
+ ], infos.icon).set([
424
+ 'info',
425
+ 'description'
426
+ ], infos.description).set('pluginOptions', infos.pluginOptions).setAttributes(this.convertAttributes(newAttributes));
427
+ if (newUID !== uid) {
428
+ this.components.forEach((compo)=>{
429
+ compo.updateComponent(uid, newUID);
430
+ });
431
+ this.contentTypes.forEach((ct)=>{
432
+ ct.updateComponent(uid, newUID);
433
+ });
434
+ }
435
+ return component;
436
+ },
437
+ deleteComponent (uid) {
438
+ if (!this.components.has(uid)) {
439
+ throw new errors.ApplicationError('component.notFound');
440
+ }
441
+ this.components.forEach((compo)=>{
442
+ compo.removeComponent(uid);
443
+ });
444
+ this.contentTypes.forEach((ct)=>{
445
+ ct.removeComponent(uid);
446
+ });
447
+ return this.components.get(uid).delete();
448
+ }
449
+ };
400
450
  }
451
+
401
452
  const modelTypes = {
402
- CONTENT_TYPE: "CONTENT_TYPE",
403
- COMPONENT: "COMPONENT"
453
+ CONTENT_TYPE: 'CONTENT_TYPE',
454
+ COMPONENT: 'COMPONENT'
404
455
  };
405
456
  const typeKinds = {
406
- SINGLE_TYPE: "singleType",
407
- COLLECTION_TYPE: "collectionType"
457
+ SINGLE_TYPE: 'singleType',
458
+ COLLECTION_TYPE: 'collectionType'
408
459
  };
409
460
  const DEFAULT_TYPES = [
410
- // advanced types
411
- "media",
412
- // scalar types
413
- "string",
414
- "text",
415
- "richtext",
416
- "blocks",
417
- "json",
418
- "enumeration",
419
- "password",
420
- "email",
421
- "integer",
422
- "biginteger",
423
- "float",
424
- "decimal",
425
- "date",
426
- "time",
427
- "datetime",
428
- "timestamp",
429
- "boolean",
430
- "relation"
461
+ // advanced types
462
+ 'media',
463
+ // scalar types
464
+ 'string',
465
+ 'text',
466
+ 'richtext',
467
+ 'blocks',
468
+ 'json',
469
+ 'enumeration',
470
+ 'password',
471
+ 'email',
472
+ 'integer',
473
+ 'biginteger',
474
+ 'float',
475
+ 'decimal',
476
+ 'date',
477
+ 'time',
478
+ 'datetime',
479
+ 'timestamp',
480
+ 'boolean',
481
+ 'relation'
482
+ ];
483
+ const VALID_UID_TARGETS = [
484
+ 'string',
485
+ 'text'
431
486
  ];
432
- const VALID_UID_TARGETS = ["string", "text"];
433
487
  const coreUids = {
434
- STRAPI_USER: "admin::user",
435
- PREFIX: "strapi::"
488
+ STRAPI_USER: 'admin::user',
489
+ PREFIX: 'strapi::'
436
490
  };
437
491
  const pluginsUids = {
438
- UPLOAD_FILE: "plugin::upload.file"
492
+ UPLOAD_FILE: 'plugin::upload.file'
439
493
  };
494
+
440
495
  const { ApplicationError: ApplicationError$1 } = errors;
441
- const reuseUnsetPreviousProperties = (newAttribute, oldAttribute) => {
442
- _.defaults(
443
- newAttribute,
444
- _.omit(oldAttribute, [
445
- "configurable",
446
- "required",
447
- "private",
448
- "unique",
449
- "pluginOptions",
450
- "inversedBy",
451
- "mappedBy"
452
- ])
453
- );
496
+ const reuseUnsetPreviousProperties = (newAttribute, oldAttribute)=>{
497
+ _.defaults(newAttribute, _.omit(oldAttribute, [
498
+ 'configurable',
499
+ 'required',
500
+ 'private',
501
+ 'unique',
502
+ 'pluginOptions',
503
+ 'inversedBy',
504
+ 'mappedBy'
505
+ ]));
454
506
  };
455
507
  function createComponentBuilder() {
456
- return {
457
- setRelation({ key, uid, attribute }) {
458
- if (!_.has(attribute, "target")) {
459
- return;
460
- }
461
- const targetCT = this.contentTypes.get(attribute.target);
462
- const targetAttribute = targetCT.getAttribute(attribute.targetAttribute);
463
- if (!attribute.targetAttribute) {
464
- return;
465
- }
466
- targetCT.setAttribute(
467
- attribute.targetAttribute,
468
- generateRelation({ key, attribute, uid, targetAttribute })
469
- );
470
- },
471
- unsetRelation(attribute) {
472
- if (!_.has(attribute, "target")) {
473
- return;
474
- }
475
- const targetCT = this.contentTypes.get(attribute.target);
476
- const targetAttributeName = attribute.inversedBy || attribute.mappedBy;
477
- const targetAttribute = targetCT.getAttribute(targetAttributeName);
478
- if (!targetAttribute) return;
479
- return targetCT.deleteAttribute(targetAttributeName);
480
- },
481
- /**
508
+ return {
509
+ setRelation ({ key, uid, attribute }) {
510
+ if (!_.has(attribute, 'target')) {
511
+ return;
512
+ }
513
+ const targetCT = this.contentTypes.get(attribute.target);
514
+ const targetAttribute = targetCT.getAttribute(attribute.targetAttribute);
515
+ if (!attribute.targetAttribute) {
516
+ return;
517
+ }
518
+ targetCT.setAttribute(attribute.targetAttribute, generateRelation({
519
+ key,
520
+ attribute,
521
+ uid,
522
+ targetAttribute
523
+ }));
524
+ },
525
+ unsetRelation (attribute) {
526
+ if (!_.has(attribute, 'target')) {
527
+ return;
528
+ }
529
+ const targetCT = this.contentTypes.get(attribute.target);
530
+ const targetAttributeName = attribute.inversedBy || attribute.mappedBy;
531
+ const targetAttribute = targetCT.getAttribute(targetAttributeName);
532
+ if (!targetAttribute) return;
533
+ return targetCT.deleteAttribute(targetAttributeName);
534
+ },
535
+ /**
482
536
  * Creates a content type in memory to be written to files later on
483
- */
484
- createContentType(infos) {
485
- const uid = createContentTypeUID(infos);
486
- if (this.contentTypes.has(uid)) {
487
- throw new ApplicationError$1("contentType.alreadyExists");
488
- }
489
- const contentType = createSchemaHandler({
490
- modelName: infos.singularName,
491
- dir: path__default.join(
492
- strapi.dirs.app.api,
493
- infos.singularName,
494
- "content-types",
495
- infos.singularName
496
- ),
497
- filename: `schema.json`
498
- });
499
- this.contentTypes.set(uid, contentType);
500
- Object.keys(infos.attributes).forEach((key) => {
501
- const { target } = infos.attributes[key];
502
- if (target === "__self__") {
503
- infos.attributes[key].target = uid;
504
- }
505
- });
506
- contentType.setUID(uid).set("kind", infos.kind || typeKinds.COLLECTION_TYPE).set(
507
- "collectionName",
508
- infos.collectionName || strings.nameToCollectionName(infos.pluralName)
509
- ).set("info", {
510
- singularName: infos.singularName,
511
- pluralName: infos.pluralName,
512
- displayName: infos.displayName,
513
- description: infos.description
514
- }).set("options", {
515
- ...infos.options ?? {},
516
- draftAndPublish: infos.draftAndPublish
517
- }).set("pluginOptions", infos.pluginOptions).set("config", infos.config).setAttributes(this.convertAttributes(infos.attributes));
518
- Object.keys(infos.attributes).forEach((key) => {
519
- const attribute = infos.attributes[key];
520
- if (isRelation(attribute)) {
521
- if (["manyToMany", "oneToOne"].includes(attribute.relation)) {
522
- if (attribute.target === uid && attribute.targetAttribute !== void 0) {
523
- const targetAttribute = infos.attributes[attribute.targetAttribute];
524
- if (targetAttribute.dominant === void 0) {
525
- attribute.dominant = true;
526
- } else {
527
- attribute.dominant = false;
528
- }
529
- } else {
530
- attribute.dominant = true;
531
- }
532
- }
533
- this.setRelation({
534
- key,
535
- uid,
536
- attribute
537
- });
538
- }
539
- });
540
- return contentType;
541
- },
542
- editContentType(infos) {
543
- const { uid } = infos;
544
- if (!this.contentTypes.has(uid)) {
545
- throw new ApplicationError$1("contentType.notFound");
546
- }
547
- const contentType = this.contentTypes.get(uid);
548
- const oldAttributes = contentType.schema.attributes;
549
- const newAttributes = _.omitBy(infos.attributes, (attr, key) => {
550
- return _.has(oldAttributes, key) && !isConfigurable(oldAttributes[key]);
551
- });
552
- const newKeys = _.difference(Object.keys(newAttributes), Object.keys(oldAttributes));
553
- const deletedKeys = _.difference(Object.keys(oldAttributes), Object.keys(newAttributes));
554
- const remainingKeys = _.intersection(Object.keys(oldAttributes), Object.keys(newAttributes));
555
- deletedKeys.forEach((key) => {
556
- const attribute = oldAttributes[key];
557
- const targetAttributeName = attribute.inversedBy || attribute.mappedBy;
558
- if (isConfigurable(attribute) && isRelation(attribute) && !_.isNil(targetAttributeName)) {
559
- this.unsetRelation(attribute);
560
- }
561
- });
562
- remainingKeys.forEach((key) => {
563
- const oldAttribute = oldAttributes[key];
564
- const newAttribute = newAttributes[key];
565
- if (!isRelation(oldAttribute) && isRelation(newAttribute)) {
566
- return this.setRelation({
567
- key,
568
- uid,
569
- attribute: newAttributes[key]
570
- });
571
- }
572
- if (isRelation(oldAttribute) && !isRelation(newAttribute)) {
573
- return this.unsetRelation(oldAttribute);
574
- }
575
- if (isRelation(oldAttribute) && isRelation(newAttribute)) {
576
- const oldTargetAttributeName = oldAttribute.inversedBy || oldAttribute.mappedBy;
577
- const sameRelation = oldAttribute.relation === newAttribute.relation;
578
- const targetAttributeHasChanged = oldTargetAttributeName !== newAttribute.targetAttribute;
579
- if (!sameRelation || targetAttributeHasChanged) {
580
- this.unsetRelation(oldAttribute);
581
- }
582
- reuseUnsetPreviousProperties(newAttribute, oldAttribute);
583
- if (oldAttribute.inversedBy) {
584
- newAttribute.dominant = true;
585
- } else if (oldAttribute.mappedBy) {
586
- newAttribute.dominant = false;
587
- }
588
- return this.setRelation({
589
- key,
590
- uid,
591
- attribute: newAttribute
592
- });
593
- }
594
- });
595
- newKeys.forEach((key) => {
596
- const attribute = newAttributes[key];
597
- if (isRelation(attribute)) {
598
- if (["manyToMany", "oneToOne"].includes(attribute.relation)) {
599
- if (attribute.target === uid && attribute.targetAttribute !== void 0) {
600
- const targetAttribute = newAttributes[attribute.targetAttribute];
601
- if (targetAttribute.dominant === void 0) {
602
- attribute.dominant = true;
603
- } else {
604
- attribute.dominant = false;
605
- }
606
- } else {
607
- attribute.dominant = true;
608
- }
609
- }
610
- this.setRelation({
611
- key,
612
- uid,
613
- attribute
614
- });
537
+ */ createContentType (infos) {
538
+ const uid = createContentTypeUID(infos);
539
+ if (this.contentTypes.has(uid)) {
540
+ throw new ApplicationError$1('contentType.alreadyExists');
541
+ }
542
+ const contentType = createSchemaHandler({
543
+ modelName: infos.singularName,
544
+ dir: path__default.join(strapi.dirs.app.api, infos.singularName, 'content-types', infos.singularName),
545
+ filename: `schema.json`
546
+ });
547
+ this.contentTypes.set(uid, contentType);
548
+ // support self referencing content type relation
549
+ Object.keys(infos.attributes).forEach((key)=>{
550
+ const { target } = infos.attributes[key];
551
+ if (target === '__self__') {
552
+ infos.attributes[key].target = uid;
553
+ }
554
+ });
555
+ contentType.setUID(uid).set('kind', infos.kind || typeKinds.COLLECTION_TYPE).set('collectionName', infos.collectionName || strings.nameToCollectionName(infos.pluralName)).set('info', {
556
+ singularName: infos.singularName,
557
+ pluralName: infos.pluralName,
558
+ displayName: infos.displayName,
559
+ description: infos.description
560
+ }).set('options', {
561
+ ...infos.options ?? {},
562
+ draftAndPublish: infos.draftAndPublish
563
+ }).set('pluginOptions', infos.pluginOptions).set('config', infos.config).setAttributes(this.convertAttributes(infos.attributes));
564
+ Object.keys(infos.attributes).forEach((key)=>{
565
+ const attribute = infos.attributes[key];
566
+ if (isRelation(attribute)) {
567
+ if ([
568
+ 'manyToMany',
569
+ 'oneToOne'
570
+ ].includes(attribute.relation)) {
571
+ if (attribute.target === uid && attribute.targetAttribute !== undefined) {
572
+ // self referencing relation
573
+ const targetAttribute = infos.attributes[attribute.targetAttribute];
574
+ if (targetAttribute.dominant === undefined) {
575
+ attribute.dominant = true;
576
+ } else {
577
+ attribute.dominant = false;
578
+ }
579
+ } else {
580
+ attribute.dominant = true;
581
+ }
582
+ }
583
+ this.setRelation({
584
+ key,
585
+ uid,
586
+ attribute
587
+ });
588
+ }
589
+ });
590
+ return contentType;
591
+ },
592
+ editContentType (infos) {
593
+ const { uid } = infos;
594
+ if (!this.contentTypes.has(uid)) {
595
+ throw new ApplicationError$1('contentType.notFound');
596
+ }
597
+ const contentType = this.contentTypes.get(uid);
598
+ const oldAttributes = contentType.schema.attributes;
599
+ const newAttributes = _.omitBy(infos.attributes, (attr, key)=>{
600
+ return _.has(oldAttributes, key) && !isConfigurable(oldAttributes[key]);
601
+ });
602
+ const newKeys = _.difference(Object.keys(newAttributes), Object.keys(oldAttributes));
603
+ const deletedKeys = _.difference(Object.keys(oldAttributes), Object.keys(newAttributes));
604
+ const remainingKeys = _.intersection(Object.keys(oldAttributes), Object.keys(newAttributes));
605
+ // remove old relations
606
+ deletedKeys.forEach((key)=>{
607
+ const attribute = oldAttributes[key];
608
+ const targetAttributeName = attribute.inversedBy || attribute.mappedBy;
609
+ // if the old relation has a target attribute. we need to remove it in the target type
610
+ if (isConfigurable(attribute) && isRelation(attribute) && !_.isNil(targetAttributeName)) {
611
+ this.unsetRelation(attribute);
612
+ }
613
+ });
614
+ remainingKeys.forEach((key)=>{
615
+ const oldAttribute = oldAttributes[key];
616
+ const newAttribute = newAttributes[key];
617
+ if (!isRelation(oldAttribute) && isRelation(newAttribute)) {
618
+ return this.setRelation({
619
+ key,
620
+ uid,
621
+ attribute: newAttributes[key]
622
+ });
623
+ }
624
+ if (isRelation(oldAttribute) && !isRelation(newAttribute)) {
625
+ return this.unsetRelation(oldAttribute);
626
+ }
627
+ if (isRelation(oldAttribute) && isRelation(newAttribute)) {
628
+ const oldTargetAttributeName = oldAttribute.inversedBy || oldAttribute.mappedBy;
629
+ const sameRelation = oldAttribute.relation === newAttribute.relation;
630
+ const targetAttributeHasChanged = oldTargetAttributeName !== newAttribute.targetAttribute;
631
+ if (!sameRelation || targetAttributeHasChanged) {
632
+ this.unsetRelation(oldAttribute);
633
+ }
634
+ // keep extra options that were set manually on oldAttribute
635
+ reuseUnsetPreviousProperties(newAttribute, oldAttribute);
636
+ if (oldAttribute.inversedBy) {
637
+ newAttribute.dominant = true;
638
+ } else if (oldAttribute.mappedBy) {
639
+ newAttribute.dominant = false;
640
+ }
641
+ return this.setRelation({
642
+ key,
643
+ uid,
644
+ attribute: newAttribute
645
+ });
646
+ }
647
+ });
648
+ // add new relations
649
+ newKeys.forEach((key)=>{
650
+ const attribute = newAttributes[key];
651
+ if (isRelation(attribute)) {
652
+ if ([
653
+ 'manyToMany',
654
+ 'oneToOne'
655
+ ].includes(attribute.relation)) {
656
+ if (attribute.target === uid && attribute.targetAttribute !== undefined) {
657
+ // self referencing relation
658
+ const targetAttribute = newAttributes[attribute.targetAttribute];
659
+ if (targetAttribute.dominant === undefined) {
660
+ attribute.dominant = true;
661
+ } else {
662
+ attribute.dominant = false;
663
+ }
664
+ } else {
665
+ attribute.dominant = true;
666
+ }
667
+ }
668
+ this.setRelation({
669
+ key,
670
+ uid,
671
+ attribute
672
+ });
673
+ }
674
+ });
675
+ contentType.set('kind', infos.kind || contentType.schema.kind).set([
676
+ 'info',
677
+ 'displayName'
678
+ ], infos.displayName).set([
679
+ 'info',
680
+ 'description'
681
+ ], infos.description).set('options', {
682
+ ...infos.options ?? {},
683
+ draftAndPublish: infos.draftAndPublish
684
+ }).set('pluginOptions', infos.pluginOptions).setAttributes(this.convertAttributes(newAttributes));
685
+ return contentType;
686
+ },
687
+ deleteContentType (uid) {
688
+ if (!this.contentTypes.has(uid)) {
689
+ throw new ApplicationError$1('contentType.notFound');
690
+ }
691
+ this.components.forEach((compo)=>{
692
+ compo.removeContentType(uid);
693
+ });
694
+ this.contentTypes.forEach((ct)=>{
695
+ ct.removeContentType(uid);
696
+ });
697
+ return this.contentTypes.get(uid).delete();
615
698
  }
616
- });
617
- contentType.set("kind", infos.kind || contentType.schema.kind).set(["info", "displayName"], infos.displayName).set(["info", "description"], infos.description).set("options", {
618
- ...infos.options ?? {},
619
- draftAndPublish: infos.draftAndPublish
620
- }).set("pluginOptions", infos.pluginOptions).setAttributes(this.convertAttributes(newAttributes));
621
- return contentType;
622
- },
623
- deleteContentType(uid) {
624
- if (!this.contentTypes.has(uid)) {
625
- throw new ApplicationError$1("contentType.notFound");
626
- }
627
- this.components.forEach((compo) => {
628
- compo.removeContentType(uid);
629
- });
630
- this.contentTypes.forEach((ct) => {
631
- ct.removeContentType(uid);
632
- });
633
- return this.contentTypes.get(uid).delete();
634
- }
635
- };
699
+ };
636
700
  }
637
- const createContentTypeUID = ({
638
- singularName
639
- }) => `api::${singularName}.${singularName}`;
640
- const generateRelation = ({ key, attribute, uid, targetAttribute = {} }) => {
641
- const opts = {
642
- type: "relation",
643
- target: uid,
644
- autoPopulate: targetAttribute.autoPopulate,
645
- private: targetAttribute.private || void 0,
646
- pluginOptions: targetAttribute.pluginOptions || void 0
647
- };
648
- switch (attribute.relation) {
649
- case "oneToOne": {
650
- opts.relation = "oneToOne";
651
- if (attribute.dominant) {
652
- opts.mappedBy = key;
653
- } else {
654
- opts.inversedBy = key;
655
- }
656
- break;
657
- }
658
- case "oneToMany": {
659
- opts.relation = "manyToOne";
660
- opts.inversedBy = key;
661
- break;
662
- }
663
- case "manyToOne": {
664
- opts.relation = "oneToMany";
665
- opts.mappedBy = key;
666
- break;
667
- }
668
- case "manyToMany": {
669
- opts.relation = "manyToMany";
670
- if (attribute.dominant) {
671
- opts.mappedBy = key;
672
- } else {
673
- opts.inversedBy = key;
674
- }
675
- break;
701
+ /**
702
+ * Returns a uid from a content type infos
703
+ *
704
+ * @param {object} options options
705
+ * @param {string} options.singularName content-type singularName
706
+ * @returns {string} uid
707
+ */ const createContentTypeUID = ({ singularName })=>`api::${singularName}.${singularName}`;
708
+ const generateRelation = ({ key, attribute, uid, targetAttribute = {} })=>{
709
+ const opts = {
710
+ type: 'relation',
711
+ target: uid,
712
+ autoPopulate: targetAttribute.autoPopulate,
713
+ private: targetAttribute.private || undefined,
714
+ pluginOptions: targetAttribute.pluginOptions || undefined
715
+ };
716
+ switch(attribute.relation){
717
+ case 'oneToOne':
718
+ {
719
+ opts.relation = 'oneToOne';
720
+ if (attribute.dominant) {
721
+ opts.mappedBy = key;
722
+ } else {
723
+ opts.inversedBy = key;
724
+ }
725
+ break;
726
+ }
727
+ case 'oneToMany':
728
+ {
729
+ opts.relation = 'manyToOne';
730
+ opts.inversedBy = key;
731
+ break;
732
+ }
733
+ case 'manyToOne':
734
+ {
735
+ opts.relation = 'oneToMany';
736
+ opts.mappedBy = key;
737
+ break;
738
+ }
739
+ case 'manyToMany':
740
+ {
741
+ opts.relation = 'manyToMany';
742
+ if (attribute.dominant) {
743
+ opts.mappedBy = key;
744
+ } else {
745
+ opts.inversedBy = key;
746
+ }
747
+ break;
748
+ }
676
749
  }
677
- }
678
- const { type, relation, target, ...restOptions } = opts;
679
- return {
680
- type,
681
- relation,
682
- target,
683
- ...restOptions
684
- };
685
- };
686
- function createBuilder() {
687
- const components2 = Object.values(strapi.components).map((componentInput) => ({
688
- category: componentInput.category,
689
- modelName: componentInput.modelName,
690
- plugin: componentInput.modelName,
691
- uid: componentInput.uid,
692
- filename: componentInput.__filename__,
693
- dir: join(strapi.dirs.app.components, componentInput.category),
694
- schema: componentInput.__schema__,
695
- config: componentInput.config
696
- }));
697
- const contentTypes2 = Object.values(strapi.contentTypes).map((contentTypeInput) => {
698
- const dir = contentTypeInput.plugin ? join(
699
- strapi.dirs.app.extensions,
700
- contentTypeInput.plugin,
701
- "content-types",
702
- contentTypeInput.info.singularName
703
- ) : join(
704
- strapi.dirs.app.api,
705
- contentTypeInput.apiName,
706
- "content-types",
707
- contentTypeInput.info.singularName
708
- );
750
+ // we do this just to make sure we have the same key order when writing to files
751
+ const { type, relation, target, ...restOptions } = opts;
709
752
  return {
710
- modelName: contentTypeInput.modelName,
711
- plugin: contentTypeInput.plugin,
712
- uid: contentTypeInput.uid,
713
- filename: "schema.json",
714
- dir,
715
- schema: contentTypeInput.__schema__,
716
- config: contentTypeInput.config
753
+ type,
754
+ relation,
755
+ target,
756
+ ...restOptions
717
757
  };
718
- });
719
- return createSchemaBuilder({
720
- components: components2,
721
- contentTypes: contentTypes2
722
- });
758
+ };
759
+
760
+ /**
761
+ * Creates a content type schema builder instance
762
+ */ function createBuilder() {
763
+ const components = Object.values(strapi.components).map((componentInput)=>({
764
+ category: componentInput.category,
765
+ modelName: componentInput.modelName,
766
+ plugin: componentInput.modelName,
767
+ uid: componentInput.uid,
768
+ filename: componentInput.__filename__,
769
+ dir: join(strapi.dirs.app.components, componentInput.category),
770
+ schema: componentInput.__schema__,
771
+ config: componentInput.config
772
+ }));
773
+ const contentTypes = Object.values(strapi.contentTypes).map((contentTypeInput)=>{
774
+ const dir = contentTypeInput.plugin ? join(strapi.dirs.app.extensions, contentTypeInput.plugin, 'content-types', contentTypeInput.info.singularName) : join(strapi.dirs.app.api, contentTypeInput.apiName, 'content-types', contentTypeInput.info.singularName);
775
+ return {
776
+ modelName: contentTypeInput.modelName,
777
+ plugin: contentTypeInput.plugin,
778
+ uid: contentTypeInput.uid,
779
+ filename: 'schema.json',
780
+ dir,
781
+ schema: contentTypeInput.__schema__,
782
+ config: contentTypeInput.config
783
+ };
784
+ });
785
+ return createSchemaBuilder({
786
+ components,
787
+ contentTypes
788
+ });
723
789
  }
724
- function createSchemaBuilder({ components: components2, contentTypes: contentTypes2 }) {
725
- const tmpComponents = /* @__PURE__ */ new Map();
726
- const tmpContentTypes = /* @__PURE__ */ new Map();
727
- Object.keys(contentTypes2).forEach((key) => {
728
- tmpContentTypes.set(contentTypes2[key].uid, createSchemaHandler(contentTypes2[key]));
729
- });
730
- Object.keys(components2).forEach((key) => {
731
- tmpComponents.set(components2[key].uid, createSchemaHandler(components2[key]));
732
- });
733
- return {
734
- get components() {
735
- return tmpComponents;
736
- },
737
- get contentTypes() {
738
- return tmpContentTypes;
739
- },
740
- /**
790
+ function createSchemaBuilder({ components, contentTypes }) {
791
+ const tmpComponents = new Map();
792
+ const tmpContentTypes = new Map();
793
+ // init temporary ContentTypes
794
+ Object.keys(contentTypes).forEach((key)=>{
795
+ tmpContentTypes.set(contentTypes[key].uid, createSchemaHandler(contentTypes[key]));
796
+ });
797
+ // init temporary components
798
+ Object.keys(components).forEach((key)=>{
799
+ tmpComponents.set(components[key].uid, createSchemaHandler(components[key]));
800
+ });
801
+ return {
802
+ get components () {
803
+ return tmpComponents;
804
+ },
805
+ get contentTypes () {
806
+ return tmpContentTypes;
807
+ },
808
+ /**
741
809
  * Convert Attributes received from the API to the right syntax
742
- */
743
- convertAttributes(attributes) {
744
- return Object.keys(attributes).reduce(
745
- (acc, key) => {
746
- const attribute = attributes[key];
747
- const { configurable, private: isPrivate } = attribute;
748
- const baseProperties = {
749
- private: isPrivate === true ? true : void 0,
750
- configurable: configurable === false ? false : void 0
751
- };
752
- if (attribute.type === "relation") {
753
- const { target, relation, targetAttribute, dominant, ...restOfProperties } = attribute;
754
- const attr = {
755
- type: "relation",
756
- relation,
757
- target,
758
- ...restOfProperties,
759
- ...baseProperties
760
- };
761
- acc[key] = attr;
762
- if (target && !this.contentTypes.has(target)) {
763
- throw new errors.ApplicationError(`target: ${target} does not exist`);
764
- }
765
- if (_.isNil(targetAttribute)) {
766
- return acc;
767
- }
768
- if (["oneToOne", "manyToMany"].includes(relation) && dominant === true) {
769
- attr.inversedBy = targetAttribute;
770
- } else if (["oneToOne", "manyToMany"].includes(relation) && dominant === false) {
771
- attr.mappedBy = targetAttribute;
772
- } else if (["oneToOne", "manyToOne", "manyToMany"].includes(relation)) {
773
- attr.inversedBy = targetAttribute;
774
- } else if (["oneToMany"].includes(relation)) {
775
- attr.mappedBy = targetAttribute;
776
- }
777
- return acc;
778
- }
779
- acc[key] = {
780
- ...attribute,
781
- ...baseProperties
782
- };
783
- return acc;
810
+ */ convertAttributes (attributes) {
811
+ return Object.keys(attributes).reduce((acc, key)=>{
812
+ const attribute = attributes[key];
813
+ const { configurable, private: isPrivate } = attribute;
814
+ const baseProperties = {
815
+ private: isPrivate === true ? true : undefined,
816
+ configurable: configurable === false ? false : undefined
817
+ };
818
+ if (attribute.type === 'relation') {
819
+ const { target, relation, targetAttribute, dominant, ...restOfProperties } = attribute;
820
+ const attr = {
821
+ type: 'relation',
822
+ relation,
823
+ target,
824
+ ...restOfProperties,
825
+ ...baseProperties
826
+ };
827
+ acc[key] = attr;
828
+ if (target && !this.contentTypes.has(target)) {
829
+ throw new errors.ApplicationError(`target: ${target} does not exist`);
830
+ }
831
+ if (_.isNil(targetAttribute)) {
832
+ return acc;
833
+ }
834
+ if ([
835
+ 'oneToOne',
836
+ 'manyToMany'
837
+ ].includes(relation) && dominant === true) {
838
+ attr.inversedBy = targetAttribute;
839
+ } else if ([
840
+ 'oneToOne',
841
+ 'manyToMany'
842
+ ].includes(relation) && dominant === false) {
843
+ attr.mappedBy = targetAttribute;
844
+ } else if ([
845
+ 'oneToOne',
846
+ 'manyToOne',
847
+ 'manyToMany'
848
+ ].includes(relation)) {
849
+ attr.inversedBy = targetAttribute;
850
+ } else if ([
851
+ 'oneToMany'
852
+ ].includes(relation)) {
853
+ attr.mappedBy = targetAttribute;
854
+ }
855
+ return acc;
856
+ }
857
+ acc[key] = {
858
+ ...attribute,
859
+ ...baseProperties
860
+ };
861
+ return acc;
862
+ }, {});
784
863
  },
785
- {}
786
- );
787
- },
788
- ...createComponentBuilder$1(),
789
- ...createComponentBuilder(),
790
- /**
864
+ ...createComponentBuilder$1(),
865
+ ...createComponentBuilder(),
866
+ /**
791
867
  * Write all type to files
792
- */
793
- writeFiles() {
794
- const schemas = [
795
- ...Array.from(tmpComponents.values()),
796
- ...Array.from(tmpContentTypes.values())
797
- ];
798
- return Promise.all(schemas.map((schema) => schema.flush())).catch((error) => {
799
- strapi.log.error("Error writing schema files");
800
- strapi.log.error(error);
801
- return this.rollback();
802
- }).catch((error) => {
803
- strapi.log.error(
804
- "Error rolling back schema files. You might need to fix your files manually"
805
- );
806
- strapi.log.error(error);
807
- throw new errors.ApplicationError("Invalid schema edition");
808
- });
809
- },
810
- /**
868
+ */ writeFiles () {
869
+ const schemas = [
870
+ ...Array.from(tmpComponents.values()),
871
+ ...Array.from(tmpContentTypes.values())
872
+ ];
873
+ return Promise.all(schemas.map((schema)=>schema.flush())).catch((error)=>{
874
+ strapi.log.error('Error writing schema files');
875
+ strapi.log.error(error);
876
+ return this.rollback();
877
+ }).catch((error)=>{
878
+ strapi.log.error('Error rolling back schema files. You might need to fix your files manually');
879
+ strapi.log.error(error);
880
+ throw new errors.ApplicationError('Invalid schema edition');
881
+ });
882
+ },
883
+ /**
811
884
  * rollback all files
812
- */
813
- rollback() {
814
- return Promise.all(
815
- [...Array.from(tmpComponents.values()), ...Array.from(tmpContentTypes.values())].map(
816
- (schema) => schema.rollback()
817
- )
818
- );
819
- }
820
- };
885
+ */ rollback () {
886
+ return Promise.all([
887
+ ...Array.from(tmpComponents.values()),
888
+ ...Array.from(tmpContentTypes.values())
889
+ ].map((schema)=>schema.rollback()));
890
+ }
891
+ };
821
892
  }
893
+
822
894
  const { ApplicationError } = errors;
823
- const isContentTypeVisible = (model) => getOr(true, "pluginOptions.content-type-builder.visible", model) === true;
824
- const getRestrictRelationsTo = (contentType) => {
825
- const { uid } = contentType;
826
- if (uid === coreUids.STRAPI_USER) {
827
- return ["oneWay", "manyWay"];
828
- }
829
- if (uid.startsWith(coreUids.PREFIX) || uid === pluginsUids.UPLOAD_FILE || !isContentTypeVisible(contentType)) {
830
- return [];
831
- }
832
- return null;
833
- };
834
- const formatContentType = (contentType) => {
835
- const { uid, kind, modelName, plugin, collectionName, info } = contentType;
836
- return {
837
- uid,
838
- plugin,
839
- apiID: modelName,
840
- schema: {
841
- ...contentTypes$2.getOptions(contentType),
842
- displayName: info.displayName,
843
- singularName: info.singularName,
844
- pluralName: info.pluralName,
845
- description: _.get(info, "description", ""),
846
- pluginOptions: contentType.pluginOptions,
847
- kind: kind || "collectionType",
848
- collectionName,
849
- attributes: formatAttributes(contentType),
850
- visible: isContentTypeVisible(contentType),
851
- restrictRelationsTo: getRestrictRelationsTo(contentType)
852
- }
853
- };
854
- };
855
- const createContentTypes = async (contentTypes2) => {
856
- const builder2 = createBuilder();
857
- const createdContentTypes = [];
858
- for (const contentType of contentTypes2) {
859
- createdContentTypes.push(await createContentType(contentType, { defaultBuilder: builder2 }));
860
- }
861
- await builder2.writeFiles();
862
- return createdContentTypes;
863
- };
864
- const createContentType = async ({ contentType, components: components2 }, options = {}) => {
865
- const builder2 = options.defaultBuilder || createBuilder();
866
- const uidMap = builder2.createNewComponentUIDMap(components2 || []);
867
- const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
868
- const newContentType = builder2.createContentType(replaceTmpUIDs(contentType));
869
- const targetContentType = (infos) => {
870
- Object.keys(infos.attributes).forEach((key) => {
871
- const { target } = infos.attributes[key];
872
- if (target === "__contentType__") {
873
- infos.attributes[key].target = newContentType.uid;
874
- }
895
+ const isContentTypeVisible = (model)=>getOr(true, 'pluginOptions.content-type-builder.visible', model) === true;
896
+ const getRestrictRelationsTo = (contentType)=>{
897
+ const { uid } = contentType;
898
+ if (uid === coreUids.STRAPI_USER) {
899
+ // TODO: replace with an obj { relation: 'x', bidirectional: true|false }
900
+ return [
901
+ 'oneWay',
902
+ 'manyWay'
903
+ ];
904
+ }
905
+ if (uid.startsWith(coreUids.PREFIX) || uid === pluginsUids.UPLOAD_FILE || !isContentTypeVisible(contentType)) {
906
+ return [];
907
+ }
908
+ return null;
909
+ };
910
+ /**
911
+ * Format a contentType info to be used by the front-end
912
+ */ const formatContentType = (contentType)=>{
913
+ const { uid, kind, modelName, plugin, collectionName, info } = contentType;
914
+ return {
915
+ uid,
916
+ plugin,
917
+ apiID: modelName,
918
+ schema: {
919
+ ...contentTypes$2.getOptions(contentType),
920
+ displayName: info.displayName,
921
+ singularName: info.singularName,
922
+ pluralName: info.pluralName,
923
+ description: _.get(info, 'description', ''),
924
+ pluginOptions: contentType.pluginOptions,
925
+ kind: kind || 'collectionType',
926
+ collectionName,
927
+ attributes: formatAttributes(contentType),
928
+ visible: isContentTypeVisible(contentType),
929
+ restrictRelationsTo: getRestrictRelationsTo(contentType)
930
+ }
931
+ };
932
+ };
933
+ const createContentTypes = async (contentTypes)=>{
934
+ const builder = createBuilder();
935
+ const createdContentTypes = [];
936
+ for (const contentType of contentTypes){
937
+ createdContentTypes.push(await createContentType(contentType, {
938
+ defaultBuilder: builder
939
+ }));
940
+ }
941
+ await builder.writeFiles();
942
+ return createdContentTypes;
943
+ };
944
+ /**
945
+ * Creates a content type and handle the nested components sent with it
946
+ */ const createContentType = async ({ contentType, components }, options = {})=>{
947
+ const builder = options.defaultBuilder || createBuilder();
948
+ const uidMap = builder.createNewComponentUIDMap(components || []);
949
+ const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
950
+ const newContentType = builder.createContentType(replaceTmpUIDs(contentType));
951
+ // allow components to target the new contentType
952
+ const targetContentType = (infos)=>{
953
+ Object.keys(infos.attributes).forEach((key)=>{
954
+ const { target } = infos.attributes[key];
955
+ if (target === '__contentType__') {
956
+ infos.attributes[key].target = newContentType.uid;
957
+ }
958
+ });
959
+ return infos;
960
+ };
961
+ components?.forEach((component)=>{
962
+ const options = replaceTmpUIDs(targetContentType(component));
963
+ if (!_.has(component, 'uid')) {
964
+ return builder.createComponent(options);
965
+ }
966
+ return builder.editComponent(options);
875
967
  });
876
- return infos;
877
- };
878
- components2?.forEach((component) => {
879
- const options2 = replaceTmpUIDs(targetContentType(component));
880
- if (!_.has(component, "uid")) {
881
- return builder2.createComponent(options2);
882
- }
883
- return builder2.editComponent(options2);
884
- });
885
- await generateAPI({
886
- displayName: contentType.displayName || contentType.info.displayName,
887
- singularName: contentType.singularName,
888
- pluralName: contentType.pluralName,
889
- kind: contentType.kind
890
- });
891
- if (!options.defaultBuilder) {
892
- await builder2.writeFiles();
893
- }
894
- strapi.eventHub.emit("content-type.create", { contentType: newContentType });
895
- return newContentType;
896
- };
897
- const generateAPI = ({
898
- singularName,
899
- kind = "collectionType",
900
- pluralName,
901
- displayName
902
- }) => {
903
- const strapiGenerators = require("@strapi/generators");
904
- return strapiGenerators.generate(
905
- "content-type",
906
- {
907
- kind,
908
- singularName,
909
- id: singularName,
910
- pluralName,
911
- displayName,
912
- destination: "new",
913
- bootstrapApi: true,
914
- attributes: []
915
- },
916
- { dir: strapi.dirs.app.root }
917
- );
918
- };
919
- const editContentType = async (uid, { contentType, components: components2 = [] }) => {
920
- const builder2 = createBuilder();
921
- const previousSchema = builder2.contentTypes.get(uid).schema;
922
- const previousKind = previousSchema.kind;
923
- const newKind = contentType.kind || previousKind;
924
- const previousAttributes = previousSchema.attributes;
925
- const prevNonVisibleAttributes = contentTypes$2.getNonVisibleAttributes(previousSchema).reduce((acc, key) => {
926
- if (key in previousAttributes) {
927
- acc[key] = previousAttributes[key];
928
- }
929
- return acc;
930
- }, {});
931
- contentType.attributes = _.merge(prevNonVisibleAttributes, contentType.attributes);
932
- if (newKind !== previousKind && newKind === "singleType") {
933
- const entryCount = await strapi.db.query(uid).count();
934
- if (entryCount > 1) {
935
- throw new ApplicationError(
936
- "You cannot convert a collectionType to a singleType when having multiple entries in DB"
937
- );
968
+ // generate api skeleton
969
+ await generateAPI({
970
+ displayName: contentType.displayName || contentType.info.displayName,
971
+ singularName: contentType.singularName,
972
+ pluralName: contentType.pluralName,
973
+ kind: contentType.kind
974
+ });
975
+ if (!options.defaultBuilder) {
976
+ await builder.writeFiles();
938
977
  }
939
- }
940
- const uidMap = builder2.createNewComponentUIDMap(components2);
941
- const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
942
- const updatedContentType = builder2.editContentType({
943
- uid,
944
- ...replaceTmpUIDs(contentType)
945
- });
946
- components2.forEach((component) => {
947
- if (!_.has(component, "uid")) {
948
- return builder2.createComponent(replaceTmpUIDs(component));
978
+ strapi.eventHub.emit('content-type.create', {
979
+ contentType: newContentType
980
+ });
981
+ return newContentType;
982
+ };
983
+ /**
984
+ * Generate an API skeleton
985
+ */ const generateAPI = ({ singularName, kind = 'collectionType', pluralName, displayName })=>{
986
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
987
+ const strapiGenerators = require('@strapi/generators');
988
+ return strapiGenerators.generate('content-type', {
989
+ kind,
990
+ singularName,
991
+ id: singularName,
992
+ pluralName,
993
+ displayName,
994
+ destination: 'new',
995
+ bootstrapApi: true,
996
+ attributes: []
997
+ }, {
998
+ dir: strapi.dirs.app.root
999
+ });
1000
+ };
1001
+ /**
1002
+ * Edits a contentType and handle the nested contentTypes sent with it
1003
+ */ const editContentType = async (uid, { contentType, components = [] })=>{
1004
+ const builder = createBuilder();
1005
+ const previousSchema = builder.contentTypes.get(uid).schema;
1006
+ const previousKind = previousSchema.kind;
1007
+ const newKind = contentType.kind || previousKind;
1008
+ // Restore non-visible attributes from previous schema
1009
+ const previousAttributes = previousSchema.attributes;
1010
+ const prevNonVisibleAttributes = contentTypes$2.getNonVisibleAttributes(previousSchema).reduce((acc, key)=>{
1011
+ if (key in previousAttributes) {
1012
+ acc[key] = previousAttributes[key];
1013
+ }
1014
+ return acc;
1015
+ }, {});
1016
+ contentType.attributes = _.merge(prevNonVisibleAttributes, contentType.attributes);
1017
+ if (newKind !== previousKind && newKind === 'singleType') {
1018
+ const entryCount = await strapi.db.query(uid).count();
1019
+ if (entryCount > 1) {
1020
+ throw new ApplicationError('You cannot convert a collectionType to a singleType when having multiple entries in DB');
1021
+ }
949
1022
  }
950
- return builder2.editComponent(replaceTmpUIDs(component));
951
- });
952
- if (newKind !== previousKind) {
953
- const apiHandler2 = strapi.plugin("content-type-builder").service("api-handler");
954
- await apiHandler2.backup(uid);
955
- try {
956
- await apiHandler2.clear(uid);
957
- await generateAPI({
958
- displayName: updatedContentType.schema.info.displayName,
959
- singularName: updatedContentType.schema.info.singularName,
960
- pluralName: updatedContentType.schema.info.pluralName,
961
- kind: updatedContentType.schema.kind
962
- });
963
- await builder2.writeFiles();
964
- } catch (error) {
965
- strapi.log.error(error);
966
- await apiHandler2.rollback(uid);
1023
+ const uidMap = builder.createNewComponentUIDMap(components);
1024
+ const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
1025
+ const updatedContentType = builder.editContentType({
1026
+ uid,
1027
+ ...replaceTmpUIDs(contentType)
1028
+ });
1029
+ components.forEach((component)=>{
1030
+ if (!_.has(component, 'uid')) {
1031
+ return builder.createComponent(replaceTmpUIDs(component));
1032
+ }
1033
+ return builder.editComponent(replaceTmpUIDs(component));
1034
+ });
1035
+ if (newKind !== previousKind) {
1036
+ const apiHandler = strapi.plugin('content-type-builder').service('api-handler');
1037
+ await apiHandler.backup(uid);
1038
+ try {
1039
+ await apiHandler.clear(uid);
1040
+ // generate new api skeleton
1041
+ await generateAPI({
1042
+ displayName: updatedContentType.schema.info.displayName,
1043
+ singularName: updatedContentType.schema.info.singularName,
1044
+ pluralName: updatedContentType.schema.info.pluralName,
1045
+ kind: updatedContentType.schema.kind
1046
+ });
1047
+ await builder.writeFiles();
1048
+ } catch (error) {
1049
+ strapi.log.error(error);
1050
+ await apiHandler.rollback(uid);
1051
+ }
1052
+ return updatedContentType;
967
1053
  }
1054
+ await builder.writeFiles();
1055
+ strapi.eventHub.emit('content-type.update', {
1056
+ contentType: updatedContentType
1057
+ });
968
1058
  return updatedContentType;
969
- }
970
- await builder2.writeFiles();
971
- strapi.eventHub.emit("content-type.update", { contentType: updatedContentType });
972
- return updatedContentType;
973
- };
974
- const deleteContentTypes = async (uids) => {
975
- const builder2 = createBuilder();
976
- const apiHandler2 = strapi.plugin("content-type-builder").service("api-handler");
977
- for (const uid of uids) {
978
- await deleteContentType(uid, builder2);
979
- }
980
- await builder2.writeFiles();
981
- for (const uid of uids) {
982
- try {
983
- await apiHandler2.clear(uid);
984
- } catch (error) {
985
- strapi.log.error(error);
986
- await apiHandler2.rollback(uid);
1059
+ };
1060
+ const deleteContentTypes = async (uids)=>{
1061
+ const builder = createBuilder();
1062
+ const apiHandler = strapi.plugin('content-type-builder').service('api-handler');
1063
+ for (const uid of uids){
1064
+ await deleteContentType(uid, builder);
1065
+ }
1066
+ await builder.writeFiles();
1067
+ for (const uid of uids){
1068
+ try {
1069
+ await apiHandler.clear(uid);
1070
+ } catch (error) {
1071
+ strapi.log.error(error);
1072
+ await apiHandler.rollback(uid);
1073
+ }
987
1074
  }
988
- }
989
- };
990
- const deleteContentType = async (uid, defaultBuilder = void 0) => {
991
- const builder2 = defaultBuilder || createBuilder();
992
- const apiHandler2 = strapi.plugin("content-type-builder").service("api-handler");
993
- await apiHandler2.backup(uid);
994
- const contentType = builder2.deleteContentType(uid);
995
- if (!defaultBuilder) {
996
- try {
997
- await builder2.writeFiles();
998
- await apiHandler2.clear(uid);
999
- } catch (error) {
1000
- await apiHandler2.rollback(uid);
1075
+ };
1076
+ /**
1077
+ * Deletes a content type and the api files related to it
1078
+ */ const deleteContentType = async (uid, defaultBuilder = undefined)=>{
1079
+ const builder = defaultBuilder || createBuilder();
1080
+ // make a backup
1081
+ const apiHandler = strapi.plugin('content-type-builder').service('api-handler');
1082
+ await apiHandler.backup(uid);
1083
+ const contentType = builder.deleteContentType(uid);
1084
+ if (!defaultBuilder) {
1085
+ try {
1086
+ await builder.writeFiles();
1087
+ await apiHandler.clear(uid);
1088
+ } catch (error) {
1089
+ await apiHandler.rollback(uid);
1090
+ }
1001
1091
  }
1002
- }
1003
- strapi.eventHub.emit("content-type.delete", { contentType });
1004
- return contentType;
1092
+ strapi.eventHub.emit('content-type.delete', {
1093
+ contentType
1094
+ });
1095
+ return contentType;
1005
1096
  };
1006
- const contentTypes$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1097
+
1098
+ var contentTypes$1 = /*#__PURE__*/Object.freeze({
1007
1099
  __proto__: null,
1008
- createContentType,
1009
- createContentTypes,
1010
- deleteContentType,
1011
- deleteContentTypes,
1012
- editContentType,
1013
- formatContentType,
1014
- generateAPI,
1015
- getRestrictRelationsTo,
1016
- isContentTypeVisible
1017
- }, Symbol.toStringTag, { value: "Module" }));
1018
- const formatComponent = (component) => {
1019
- const { uid, modelName, connection, collectionName, info, category } = component;
1020
- return {
1021
- uid,
1022
- category,
1023
- apiId: modelName,
1024
- schema: {
1025
- displayName: get(info, "displayName"),
1026
- description: get(info, "description", ""),
1027
- icon: get(info, "icon"),
1028
- connection,
1029
- collectionName,
1030
- pluginOptions: component.pluginOptions,
1031
- attributes: formatAttributes(component)
1032
- }
1033
- };
1034
- };
1035
- const createComponent = async ({ component, components: components2 = [] }) => {
1036
- const builder2 = createBuilder();
1037
- const uidMap = builder2.createNewComponentUIDMap(components2);
1038
- const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
1039
- const newComponent = builder2.createComponent(replaceTmpUIDs(component));
1040
- components2.forEach((component2) => {
1041
- if (!has(component2, "uid")) {
1042
- return builder2.createComponent(replaceTmpUIDs(component2));
1043
- }
1044
- return builder2.editComponent(replaceTmpUIDs(component2));
1045
- });
1046
- await builder2.writeFiles();
1047
- strapi.eventHub.emit("component.create", { component: newComponent });
1048
- return newComponent;
1049
- };
1050
- const editComponent = async (uid, { component, components: components2 = [] }) => {
1051
- const builder2 = createBuilder();
1052
- const uidMap = builder2.createNewComponentUIDMap(components2);
1053
- const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
1054
- const updatedComponent = builder2.editComponent({
1055
- uid,
1056
- ...replaceTmpUIDs(component)
1057
- });
1058
- components2.forEach((component2) => {
1059
- if (!has(component2, "uid")) {
1060
- return builder2.createComponent(replaceTmpUIDs(component2));
1061
- }
1062
- return builder2.editComponent(replaceTmpUIDs(component2));
1063
- });
1064
- await builder2.writeFiles();
1065
- strapi.eventHub.emit("component.update", { component: updatedComponent });
1066
- return updatedComponent;
1067
- };
1068
- const deleteComponent = async (uid) => {
1069
- const builder2 = createBuilder();
1070
- const deletedComponent = builder2.deleteComponent(uid);
1071
- await builder2.writeFiles();
1072
- strapi.eventHub.emit("component.delete", { component: deletedComponent });
1073
- return deletedComponent;
1074
- };
1075
- const components$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1100
+ createContentType: createContentType,
1101
+ createContentTypes: createContentTypes,
1102
+ deleteContentType: deleteContentType,
1103
+ deleteContentTypes: deleteContentTypes,
1104
+ editContentType: editContentType,
1105
+ formatContentType: formatContentType,
1106
+ generateAPI: generateAPI,
1107
+ getRestrictRelationsTo: getRestrictRelationsTo,
1108
+ isContentTypeVisible: isContentTypeVisible
1109
+ });
1110
+
1111
+ /**
1112
+ * Formats a component attributes
1113
+ */ const formatComponent = (component)=>{
1114
+ const { uid, modelName, connection, collectionName, info, category } = component;
1115
+ return {
1116
+ uid,
1117
+ category,
1118
+ apiId: modelName,
1119
+ schema: {
1120
+ displayName: get(info, 'displayName'),
1121
+ description: get(info, 'description', ''),
1122
+ icon: get(info, 'icon'),
1123
+ connection,
1124
+ collectionName,
1125
+ pluginOptions: component.pluginOptions,
1126
+ attributes: formatAttributes(component)
1127
+ }
1128
+ };
1129
+ };
1130
+ /**
1131
+ * Creates a component and handle the nested components sent with it
1132
+ */ const createComponent = async ({ component, components = [] })=>{
1133
+ const builder = createBuilder();
1134
+ const uidMap = builder.createNewComponentUIDMap(components);
1135
+ const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
1136
+ const newComponent = builder.createComponent(replaceTmpUIDs(component));
1137
+ components.forEach((component)=>{
1138
+ if (!has(component, 'uid')) {
1139
+ return builder.createComponent(replaceTmpUIDs(component));
1140
+ }
1141
+ return builder.editComponent(replaceTmpUIDs(component));
1142
+ });
1143
+ await builder.writeFiles();
1144
+ strapi.eventHub.emit('component.create', {
1145
+ component: newComponent
1146
+ });
1147
+ return newComponent;
1148
+ };
1149
+ const editComponent = async (uid, { component, components = [] })=>{
1150
+ const builder = createBuilder();
1151
+ const uidMap = builder.createNewComponentUIDMap(components);
1152
+ const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
1153
+ const updatedComponent = builder.editComponent({
1154
+ uid,
1155
+ ...replaceTmpUIDs(component)
1156
+ });
1157
+ components.forEach((component)=>{
1158
+ if (!has(component, 'uid')) {
1159
+ return builder.createComponent(replaceTmpUIDs(component));
1160
+ }
1161
+ return builder.editComponent(replaceTmpUIDs(component));
1162
+ });
1163
+ await builder.writeFiles();
1164
+ strapi.eventHub.emit('component.update', {
1165
+ component: updatedComponent
1166
+ });
1167
+ return updatedComponent;
1168
+ };
1169
+ const deleteComponent = async (uid)=>{
1170
+ const builder = createBuilder();
1171
+ const deletedComponent = builder.deleteComponent(uid);
1172
+ await builder.writeFiles();
1173
+ strapi.eventHub.emit('component.delete', {
1174
+ component: deletedComponent
1175
+ });
1176
+ return deletedComponent;
1177
+ };
1178
+
1179
+ var components$1 = /*#__PURE__*/Object.freeze({
1076
1180
  __proto__: null,
1077
- createComponent,
1078
- deleteComponent,
1079
- editComponent,
1080
- formatComponent
1081
- }, Symbol.toStringTag, { value: "Module" }));
1082
- const editCategory = async (name, infos) => {
1083
- const newName = strings.nameToSlug(infos.name);
1084
- if (name === newName) return;
1085
- if (!categoryExists(name)) {
1086
- throw new errors.ApplicationError("category not found");
1087
- }
1088
- if (categoryExists(newName)) {
1089
- throw new errors.ApplicationError("Name already taken");
1090
- }
1091
- const builder2 = createBuilder();
1092
- builder2.components.forEach((component) => {
1093
- const oldUID = component.uid;
1094
- const newUID = `${newName}.${component.modelName}`;
1095
- if (component.category !== name) return;
1096
- component.setUID(newUID).setDir(join(strapi.dirs.app.components, newName));
1097
- builder2.components.forEach((compo) => {
1098
- compo.updateComponent(oldUID, newUID);
1181
+ createComponent: createComponent,
1182
+ deleteComponent: deleteComponent,
1183
+ editComponent: editComponent,
1184
+ formatComponent: formatComponent
1185
+ });
1186
+
1187
+ /**
1188
+ * Edit a category name and move components to the write folder
1189
+ */ const editCategory = async (name, infos)=>{
1190
+ const newName = strings.nameToSlug(infos.name);
1191
+ // don't do anything the name doesn't change
1192
+ if (name === newName) return;
1193
+ if (!categoryExists(name)) {
1194
+ throw new errors.ApplicationError('category not found');
1195
+ }
1196
+ if (categoryExists(newName)) {
1197
+ throw new errors.ApplicationError('Name already taken');
1198
+ }
1199
+ const builder = createBuilder();
1200
+ builder.components.forEach((component)=>{
1201
+ const oldUID = component.uid;
1202
+ const newUID = `${newName}.${component.modelName}`;
1203
+ // only edit the components in this specific category
1204
+ if (component.category !== name) return;
1205
+ component.setUID(newUID).setDir(join(strapi.dirs.app.components, newName));
1206
+ builder.components.forEach((compo)=>{
1207
+ compo.updateComponent(oldUID, newUID);
1208
+ });
1209
+ builder.contentTypes.forEach((ct)=>{
1210
+ ct.updateComponent(oldUID, newUID);
1211
+ });
1099
1212
  });
1100
- builder2.contentTypes.forEach((ct) => {
1101
- ct.updateComponent(oldUID, newUID);
1213
+ await builder.writeFiles();
1214
+ return newName;
1215
+ };
1216
+ /**
1217
+ * Deletes a category and its components
1218
+ */ const deleteCategory = async (name)=>{
1219
+ if (!categoryExists(name)) {
1220
+ throw new errors.ApplicationError('category not found');
1221
+ }
1222
+ const builder = createBuilder();
1223
+ builder.components.forEach((component)=>{
1224
+ if (component.category === name) {
1225
+ builder.deleteComponent(component.uid);
1226
+ }
1102
1227
  });
1103
- });
1104
- await builder2.writeFiles();
1105
- return newName;
1106
- };
1107
- const deleteCategory = async (name) => {
1108
- if (!categoryExists(name)) {
1109
- throw new errors.ApplicationError("category not found");
1110
- }
1111
- const builder2 = createBuilder();
1112
- builder2.components.forEach((component) => {
1113
- if (component.category === name) {
1114
- builder2.deleteComponent(component.uid);
1115
- }
1116
- });
1117
- await builder2.writeFiles();
1228
+ await builder.writeFiles();
1118
1229
  };
1119
- const categoryExists = (name) => {
1120
- const matchingIndex = Object.values(strapi.components).findIndex(
1121
- (component) => component.category === name
1122
- );
1123
- return matchingIndex > -1;
1230
+ /**
1231
+ * Checks if a category exists
1232
+ */ const categoryExists = (name)=>{
1233
+ const matchingIndex = Object.values(strapi.components).findIndex((component)=>component.category === name);
1234
+ return matchingIndex > -1;
1124
1235
  };
1125
- const componentCategories$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1236
+
1237
+ var componentCategories$1 = /*#__PURE__*/Object.freeze({
1126
1238
  __proto__: null,
1127
- deleteCategory,
1128
- editCategory
1129
- }, Symbol.toStringTag, { value: "Module" }));
1239
+ deleteCategory: deleteCategory,
1240
+ editCategory: editCategory
1241
+ });
1242
+
1243
+ // use snake_case
1130
1244
  const reservedAttributes = [
1131
- // TODO: these need to come from a centralized place so we don't break things accidentally in the future and can share them outside the CTB, for example on Strapi bootstrap prior to schema db sync
1132
- // ID fields
1133
- "id",
1134
- "document_id",
1135
- // Creator fields
1136
- "created_at",
1137
- "updated_at",
1138
- "published_at",
1139
- "created_by_id",
1140
- "updated_by_id",
1141
- // does not actually conflict because the fields are called *_by_id but we'll leave it to avoid confusion
1142
- "created_by",
1143
- "updated_by",
1144
- // Used for Strapi functionality
1145
- "entry_id",
1146
- "status",
1147
- "localizations",
1148
- "meta",
1149
- "locale",
1150
- "__component",
1151
- "__contentType",
1152
- // We support ending with * to denote prefixes
1153
- "strapi*",
1154
- "_strapi*",
1155
- "__strapi*"
1245
+ // TODO: these need to come from a centralized place so we don't break things accidentally in the future and can share them outside the CTB, for example on Strapi bootstrap prior to schema db sync
1246
+ // ID fields
1247
+ 'id',
1248
+ 'document_id',
1249
+ // Creator fields
1250
+ 'created_at',
1251
+ 'updated_at',
1252
+ 'published_at',
1253
+ 'created_by_id',
1254
+ 'updated_by_id',
1255
+ // does not actually conflict because the fields are called *_by_id but we'll leave it to avoid confusion
1256
+ 'created_by',
1257
+ 'updated_by',
1258
+ // Used for Strapi functionality
1259
+ 'entry_id',
1260
+ 'status',
1261
+ 'localizations',
1262
+ 'meta',
1263
+ 'locale',
1264
+ '__component',
1265
+ '__contentType',
1266
+ // We support ending with * to denote prefixes
1267
+ 'strapi*',
1268
+ '_strapi*',
1269
+ '__strapi*'
1156
1270
  ];
1271
+ // use snake_case
1157
1272
  const reservedModels = [
1158
- "boolean",
1159
- "date",
1160
- "date_time",
1161
- "time",
1162
- "upload",
1163
- "document",
1164
- "then",
1165
- // no longer an issue but still restricting for being a javascript keyword
1166
- // We support ending with * to denote prefixes
1167
- "strapi*",
1168
- "_strapi*",
1169
- "__strapi*"
1273
+ 'boolean',
1274
+ 'date',
1275
+ 'date_time',
1276
+ 'time',
1277
+ 'upload',
1278
+ 'document',
1279
+ 'then',
1280
+ // We support ending with * to denote prefixes
1281
+ 'strapi*',
1282
+ '_strapi*',
1283
+ '__strapi*'
1170
1284
  ];
1171
- const getReservedNames = () => {
1172
- return {
1173
- models: reservedModels,
1174
- attributes: reservedAttributes
1175
- };
1176
- };
1177
- const isReservedModelName = (name) => {
1178
- const snakeCaseName = snakeCase(name);
1179
- if (reservedModels.includes(snakeCaseName)) {
1180
- return true;
1181
- }
1182
- if (reservedModels.filter((key) => key.endsWith("*")).map((key) => key.slice(0, -1)).some((prefix) => snakeCaseName.startsWith(prefix))) {
1183
- return true;
1184
- }
1185
- return false;
1186
- };
1187
- const isReservedAttributeName = (name) => {
1188
- const snakeCaseName = snakeCase(name);
1189
- if (reservedAttributes.includes(snakeCaseName)) {
1190
- return true;
1191
- }
1192
- if (reservedAttributes.filter((key) => key.endsWith("*")).map((key) => key.slice(0, -1)).some((prefix) => snakeCaseName.startsWith(prefix))) {
1193
- return true;
1194
- }
1195
- return false;
1196
- };
1197
- const builder$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1285
+ const getReservedNames = ()=>{
1286
+ return {
1287
+ models: reservedModels,
1288
+ attributes: reservedAttributes
1289
+ };
1290
+ };
1291
+ // compare snake case to check the actual column names that will be used in the database
1292
+ const isReservedModelName = (name)=>{
1293
+ const snakeCaseName = snakeCase(name);
1294
+ if (reservedModels.includes(snakeCaseName)) {
1295
+ return true;
1296
+ }
1297
+ if (reservedModels.filter((key)=>key.endsWith('*')).map((key)=>key.slice(0, -1)).some((prefix)=>snakeCaseName.startsWith(prefix))) {
1298
+ return true;
1299
+ }
1300
+ return false;
1301
+ };
1302
+ // compare snake case to check the actual column names that will be used in the database
1303
+ const isReservedAttributeName = (name)=>{
1304
+ const snakeCaseName = snakeCase(name);
1305
+ if (reservedAttributes.includes(snakeCaseName)) {
1306
+ return true;
1307
+ }
1308
+ if (reservedAttributes.filter((key)=>key.endsWith('*')).map((key)=>key.slice(0, -1)).some((prefix)=>snakeCaseName.startsWith(prefix))) {
1309
+ return true;
1310
+ }
1311
+ return false;
1312
+ };
1313
+
1314
+ var builder$1 = /*#__PURE__*/Object.freeze({
1198
1315
  __proto__: null,
1199
- getReservedNames,
1200
- isReservedAttributeName,
1201
- isReservedModelName,
1202
- reservedAttributes,
1203
- reservedModels
1204
- }, Symbol.toStringTag, { value: "Module" }));
1205
- async function clear(uid) {
1206
- const { apiName, modelName } = strapi.contentTypes[uid];
1207
- const apiFolder = path.join(strapi.dirs.app.api, apiName);
1208
- await recursiveRemoveFiles(apiFolder, createDeleteApiFunction(modelName));
1209
- await deleteBackup(uid);
1316
+ getReservedNames: getReservedNames,
1317
+ isReservedAttributeName: isReservedAttributeName,
1318
+ isReservedModelName: isReservedModelName,
1319
+ reservedAttributes: reservedAttributes,
1320
+ reservedModels: reservedModels
1321
+ });
1322
+
1323
+ /**
1324
+ * Deletes the API folder of a contentType
1325
+ */ async function clear(uid) {
1326
+ // TODO double check if this is the correct way to get the apiName
1327
+ const { apiName, modelName } = strapi.contentTypes[uid];
1328
+ const apiFolder = path.join(strapi.dirs.app.api, apiName);
1329
+ await recursiveRemoveFiles(apiFolder, createDeleteApiFunction(modelName));
1330
+ await deleteBackup(uid);
1210
1331
  }
1211
- async function backup(uid) {
1212
- const { apiName } = strapi.contentTypes[uid];
1213
- const apiFolder = path.join(strapi.dirs.app.api, apiName);
1214
- const backupFolder = path.join(strapi.dirs.app.api, ".backup", apiName);
1215
- await fse.copy(apiFolder, backupFolder);
1332
+ /**
1333
+ * Backups the API folder of a contentType
1334
+ * @param {string} uid content type uid
1335
+ */ async function backup(uid) {
1336
+ const { apiName } = strapi.contentTypes[uid];
1337
+ const apiFolder = path.join(strapi.dirs.app.api, apiName);
1338
+ const backupFolder = path.join(strapi.dirs.app.api, '.backup', apiName);
1339
+ // backup the api folder
1340
+ await fse.copy(apiFolder, backupFolder);
1216
1341
  }
1217
- async function deleteBackup(uid) {
1218
- const { apiName } = strapi.contentTypes[uid];
1219
- const backupFolder = path.join(strapi.dirs.app.api, ".backup");
1220
- const apiBackupFolder = path.join(strapi.dirs.app.api, ".backup", apiName);
1221
- await fse.remove(apiBackupFolder);
1222
- const list = await fse.readdir(backupFolder);
1223
- if (list.length === 0) {
1224
- await fse.remove(backupFolder);
1225
- }
1342
+ /**
1343
+ * Deletes an API backup folder
1344
+ */ async function deleteBackup(uid) {
1345
+ const { apiName } = strapi.contentTypes[uid];
1346
+ const backupFolder = path.join(strapi.dirs.app.api, '.backup');
1347
+ const apiBackupFolder = path.join(strapi.dirs.app.api, '.backup', apiName);
1348
+ await fse.remove(apiBackupFolder);
1349
+ const list = await fse.readdir(backupFolder);
1350
+ if (list.length === 0) {
1351
+ await fse.remove(backupFolder);
1352
+ }
1226
1353
  }
1227
- async function rollback(uid) {
1228
- const { apiName } = strapi.contentTypes[uid];
1229
- const apiFolder = path.join(strapi.dirs.app.api, apiName);
1230
- const backupFolder = path.join(strapi.dirs.app.api, ".backup", apiName);
1231
- try {
1232
- await fse.access(backupFolder);
1233
- } catch {
1234
- throw new Error("Cannot rollback api that was not backed up");
1235
- }
1236
- await fse.remove(apiFolder);
1237
- await fse.copy(backupFolder, apiFolder);
1238
- await deleteBackup(uid);
1354
+ /**
1355
+ * Rollbacks the API folder of a contentType
1356
+ */ async function rollback(uid) {
1357
+ const { apiName } = strapi.contentTypes[uid];
1358
+ const apiFolder = path.join(strapi.dirs.app.api, apiName);
1359
+ const backupFolder = path.join(strapi.dirs.app.api, '.backup', apiName);
1360
+ try {
1361
+ await fse.access(backupFolder);
1362
+ } catch {
1363
+ throw new Error('Cannot rollback api that was not backed up');
1364
+ }
1365
+ await fse.remove(apiFolder);
1366
+ await fse.copy(backupFolder, apiFolder);
1367
+ await deleteBackup(uid);
1239
1368
  }
1240
- const createDeleteApiFunction = (baseName) => {
1241
- return async (filePath) => {
1242
- const fileName = path.basename(filePath, path.extname(filePath));
1243
- const isSchemaFile = filePath.endsWith(`${baseName}/schema.json`);
1244
- if (fileName === baseName || isSchemaFile) {
1245
- return fse.remove(filePath);
1369
+ /**
1370
+ * Creates a delete function to clear an api folder
1371
+ */ const createDeleteApiFunction = (baseName)=>{
1372
+ /**
1373
+ * Delets a file in an api.
1374
+ * Will only update routes.json instead of deleting it if other routes are present
1375
+ */ return async (filePath)=>{
1376
+ const fileName = path.basename(filePath, path.extname(filePath));
1377
+ const isSchemaFile = filePath.endsWith(`${baseName}/schema.json`);
1378
+ if (fileName === baseName || isSchemaFile) {
1379
+ return fse.remove(filePath);
1380
+ }
1381
+ };
1382
+ };
1383
+ /**
1384
+ * Deletes a folder recursively using a delete function
1385
+ * @param {string} folder folder to delete
1386
+ */ const recursiveRemoveFiles = async (folder, deleteFn)=>{
1387
+ const filesName = await fse.readdir(folder);
1388
+ for (const fileName of filesName){
1389
+ const filePath = path.join(folder, fileName);
1390
+ const stat = await fse.stat(filePath);
1391
+ if (stat.isDirectory()) {
1392
+ await recursiveRemoveFiles(filePath, deleteFn);
1393
+ } else {
1394
+ await deleteFn(filePath);
1395
+ }
1246
1396
  }
1247
- };
1248
- };
1249
- const recursiveRemoveFiles = async (folder, deleteFn) => {
1250
- const filesName = await fse.readdir(folder);
1251
- for (const fileName of filesName) {
1252
- const filePath = path.join(folder, fileName);
1253
- const stat = await fse.stat(filePath);
1254
- if (stat.isDirectory()) {
1255
- await recursiveRemoveFiles(filePath, deleteFn);
1256
- } else {
1257
- await deleteFn(filePath);
1397
+ const files = await fse.readdir(folder);
1398
+ if (files.length === 0) {
1399
+ await fse.remove(folder);
1258
1400
  }
1259
- }
1260
- const files = await fse.readdir(folder);
1261
- if (files.length === 0) {
1262
- await fse.remove(folder);
1263
- }
1264
1401
  };
1265
- const apiHandler = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1402
+
1403
+ var apiHandler = /*#__PURE__*/Object.freeze({
1266
1404
  __proto__: null,
1267
- backup,
1268
- clear,
1269
- rollback
1270
- }, Symbol.toStringTag, { value: "Module" }));
1271
- const services = {
1272
- "content-types": contentTypes$1,
1273
- components: components$1,
1274
- "component-categories": componentCategories$1,
1275
- builder: builder$1,
1276
- "api-handler": apiHandler
1277
- };
1405
+ backup: backup,
1406
+ clear: clear,
1407
+ rollback: rollback
1408
+ });
1409
+
1410
+ var services = {
1411
+ 'content-types': contentTypes$1,
1412
+ components: components$1,
1413
+ 'component-categories': componentCategories$1,
1414
+ builder: builder$1,
1415
+ 'api-handler': apiHandler
1416
+ };
1417
+
1278
1418
  function getService(name) {
1279
- return strapi.plugin("content-type-builder").service(name);
1419
+ return strapi.plugin('content-type-builder').service(name);
1280
1420
  }
1281
- const builder = {
1282
- getReservedNames(ctx) {
1283
- ctx.body = getService("builder").getReservedNames();
1284
- }
1421
+
1422
+ var builder = {
1423
+ getReservedNames (ctx) {
1424
+ ctx.body = getService('builder').getReservedNames();
1425
+ }
1285
1426
  };
1427
+
1286
1428
  const validators = {
1287
- required: yup.boolean(),
1288
- unique: yup.boolean(),
1289
- minLength: yup.number().integer().positive(),
1290
- maxLength: yup.number().integer().positive()
1429
+ required: yup.boolean(),
1430
+ unique: yup.boolean(),
1431
+ minLength: yup.number().integer().positive(),
1432
+ maxLength: yup.number().integer().positive()
1291
1433
  };
1292
1434
  const NAME_REGEX = /^[A-Za-z][_0-9A-Za-z]*$/;
1293
1435
  const COLLECTION_NAME_REGEX = /^[A-Za-z][-_0-9A-Za-z]*$/;
@@ -1295,999 +1437,1210 @@ const CATEGORY_NAME_REGEX = /^[A-Za-z][-_0-9A-Za-z]*$/;
1295
1437
  const ICON_REGEX = /^[A-Za-z0-9][-A-Za-z0-9]*$/;
1296
1438
  const UID_REGEX = /^[A-Za-z0-9-_.~]*$/;
1297
1439
  const isValidName = {
1298
- name: "isValidName",
1299
- message: `\${path} must match the following regex: ${NAME_REGEX}`,
1300
- test: (val) => val === "" || NAME_REGEX.test(val)
1440
+ name: 'isValidName',
1441
+ message: `\${path} must match the following regex: ${NAME_REGEX}`,
1442
+ test: (val)=>val === '' || NAME_REGEX.test(val)
1301
1443
  };
1302
1444
  const isValidIcon = {
1303
- name: "isValidIcon",
1304
- message: `\${path} is not a valid icon name. Make sure your icon name starts with an alphanumeric character and only includes alphanumeric characters or dashes.`,
1305
- test: (val) => val === "" || ICON_REGEX.test(val)
1445
+ name: 'isValidIcon',
1446
+ message: `\${path} is not a valid icon name. Make sure your icon name starts with an alphanumeric character and only includes alphanumeric characters or dashes.`,
1447
+ test: (val)=>val === '' || ICON_REGEX.test(val)
1306
1448
  };
1307
1449
  const isValidUID = {
1308
- name: "isValidUID",
1309
- message: `\${path} must match the following regex: ${UID_REGEX}`,
1310
- test: (val) => val === "" || UID_REGEX.test(val)
1450
+ name: 'isValidUID',
1451
+ message: `\${path} must match the following regex: ${UID_REGEX}`,
1452
+ test: (val)=>val === '' || UID_REGEX.test(val)
1311
1453
  };
1312
1454
  const isValidCategoryName = {
1313
- name: "isValidCategoryName",
1314
- message: `\${path} must match the following regex: ${CATEGORY_NAME_REGEX}`,
1315
- test: (val) => val === "" || CATEGORY_NAME_REGEX.test(val)
1455
+ name: 'isValidCategoryName',
1456
+ message: `\${path} must match the following regex: ${CATEGORY_NAME_REGEX}`,
1457
+ test: (val)=>val === '' || CATEGORY_NAME_REGEX.test(val)
1316
1458
  };
1317
1459
  const isValidCollectionName = {
1318
- name: "isValidCollectionName",
1319
- message: `\${path} must match the following regex: ${COLLECTION_NAME_REGEX}`,
1320
- test: (val) => val === "" || COLLECTION_NAME_REGEX.test(val)
1321
- };
1322
- const isValidKey = (key) => ({
1323
- name: "isValidKey",
1324
- message: `Attribute name '${key}' must match the following regex: ${NAME_REGEX}`,
1325
- test: () => NAME_REGEX.test(key)
1326
- });
1460
+ name: 'isValidCollectionName',
1461
+ message: `\${path} must match the following regex: ${COLLECTION_NAME_REGEX}`,
1462
+ test: (val)=>val === '' || COLLECTION_NAME_REGEX.test(val)
1463
+ };
1464
+ const isValidKey = (key)=>({
1465
+ name: 'isValidKey',
1466
+ message: `Attribute name '${key}' must match the following regex: ${NAME_REGEX}`,
1467
+ test: ()=>NAME_REGEX.test(key)
1468
+ });
1327
1469
  const isValidEnum = {
1328
- name: "isValidEnum",
1329
- message: "${path} should not start with number",
1330
- test: (val) => val === "" || !strings.startsWithANumber(val)
1470
+ name: 'isValidEnum',
1471
+ message: '${path} should not start with number',
1472
+ test: (val)=>val === '' || !strings.startsWithANumber(val)
1331
1473
  };
1332
1474
  const areEnumValuesUnique = {
1333
- name: "areEnumValuesUnique",
1334
- message: "${path} cannot contain duplicate values",
1335
- test(values) {
1336
- const filtered = [...new Set(values)];
1337
- return filtered.length === values.length;
1338
- }
1475
+ name: 'areEnumValuesUnique',
1476
+ message: '${path} cannot contain duplicate values',
1477
+ test (values) {
1478
+ const filtered = [
1479
+ ...new Set(values)
1480
+ ];
1481
+ return filtered.length === values.length;
1482
+ }
1339
1483
  };
1340
1484
  const isValidRegExpPattern = {
1341
- name: "isValidRegExpPattern",
1342
- message: "${path} must be a valid RexExp pattern string",
1343
- test: (val) => val === "" || !!new RegExp(val)
1485
+ name: 'isValidRegExpPattern',
1486
+ message: '${path} must be a valid RexExp pattern string',
1487
+ test: (val)=>val === '' || !!new RegExp(val)
1344
1488
  };
1345
1489
  const isValidDefaultJSON = {
1346
- name: "isValidDefaultJSON",
1347
- message: "${path} is not a valid JSON",
1348
- test(val) {
1349
- if (val === void 0) {
1350
- return true;
1351
- }
1352
- if (_.isNumber(val) || _.isNull(val) || _.isObject(val) || _.isArray(val)) {
1353
- return true;
1354
- }
1355
- try {
1356
- JSON.parse(val);
1357
- return true;
1358
- } catch (err) {
1359
- return false;
1490
+ name: 'isValidDefaultJSON',
1491
+ message: '${path} is not a valid JSON',
1492
+ test (val) {
1493
+ if (val === undefined) {
1494
+ return true;
1495
+ }
1496
+ if (_.isNumber(val) || _.isNull(val) || _.isObject(val) || _.isArray(val)) {
1497
+ return true;
1498
+ }
1499
+ try {
1500
+ JSON.parse(val);
1501
+ return true;
1502
+ } catch (err) {
1503
+ return false;
1504
+ }
1360
1505
  }
1361
- }
1362
1506
  };
1507
+
1363
1508
  const componentCategorySchema = yup.object({
1364
- name: yup.string().min(3).test(isValidCategoryName).required("name.required")
1509
+ name: yup.string().min(3).test(isValidCategoryName).required('name.required')
1365
1510
  }).noUnknown();
1366
- const validateComponentCategory = validateYupSchema(componentCategorySchema);
1367
- const componentCategories = {
1368
- async editCategory(ctx) {
1369
- const body = ctx.request.body;
1370
- try {
1371
- await validateComponentCategory(body);
1372
- } catch (error) {
1373
- return ctx.send({ error }, 400);
1511
+ var validateComponentCategory = validateYupSchema(componentCategorySchema);
1512
+
1513
+ var componentCategories = {
1514
+ async editCategory (ctx) {
1515
+ const body = ctx.request.body;
1516
+ try {
1517
+ await validateComponentCategory(body);
1518
+ } catch (error) {
1519
+ return ctx.send({
1520
+ error
1521
+ }, 400);
1522
+ }
1523
+ const { name } = ctx.params;
1524
+ strapi.reload.isWatching = false;
1525
+ const componentCategoryService = getService('component-categories');
1526
+ const newName = await componentCategoryService.editCategory(name, body);
1527
+ setImmediate(()=>strapi.reload());
1528
+ ctx.send({
1529
+ name: newName
1530
+ });
1531
+ },
1532
+ async deleteCategory (ctx) {
1533
+ const { name } = ctx.params;
1534
+ strapi.reload.isWatching = false;
1535
+ const componentCategoryService = getService('component-categories');
1536
+ await componentCategoryService.deleteCategory(name);
1537
+ setImmediate(()=>strapi.reload());
1538
+ ctx.send({
1539
+ name
1540
+ });
1374
1541
  }
1375
- const { name } = ctx.params;
1376
- strapi.reload.isWatching = false;
1377
- const componentCategoryService = getService("component-categories");
1378
- const newName = await componentCategoryService.editCategory(name, body);
1379
- setImmediate(() => strapi.reload());
1380
- ctx.send({ name: newName });
1381
- },
1382
- async deleteCategory(ctx) {
1383
- const { name } = ctx.params;
1384
- strapi.reload.isWatching = false;
1385
- const componentCategoryService = getService("component-categories");
1386
- await componentCategoryService.deleteCategory(name);
1387
- setImmediate(() => strapi.reload());
1388
- ctx.send({ name });
1389
- }
1390
1542
  };
1543
+
1391
1544
  const maxLengthIsGreaterThanOrEqualToMinLength = {
1392
- name: "isGreaterThanMin",
1393
- message: "maxLength must be greater or equal to minLength",
1394
- test(value) {
1395
- const { minLength } = this.parent;
1396
- return !(!_.isUndefined(minLength) && !_.isUndefined(value) && value < minLength);
1397
- }
1398
- };
1399
- const getTypeValidator = (attribute, { types, modelType, attributes }) => {
1400
- return yup.object({
1401
- type: yup.string().oneOf([...types]).required(),
1402
- configurable: yup.boolean().nullable(),
1403
- private: yup.boolean().nullable(),
1404
- pluginOptions: yup.object(),
1405
- ...getTypeShape(attribute, { modelType, attributes })
1406
- });
1407
- };
1408
- const getTypeShape = (attribute, { attributes } = {}) => {
1409
- switch (attribute.type) {
1410
- case "media": {
1411
- return {
1412
- multiple: yup.boolean(),
1413
- required: validators.required,
1414
- allowedTypes: yup.array().of(yup.string().oneOf(["images", "videos", "files", "audios"])).min(1)
1415
- };
1416
- }
1417
- case "uid": {
1418
- return {
1419
- required: validators.required,
1420
- targetField: yup.string().oneOf(
1421
- Object.keys(attributes).filter(
1422
- (key) => VALID_UID_TARGETS.includes(_.get(attributes[key], "type"))
1423
- )
1424
- ).nullable(),
1425
- default: yup.string().test(
1426
- "isValidDefaultUID",
1427
- "cannot define a default UID if the targetField is set",
1428
- function(value) {
1429
- const { targetField } = this.parent;
1430
- return !!(_.isNil(targetField) || _.isNil(value));
1431
- }
1432
- ).test(isValidUID),
1433
- minLength: validators.minLength,
1434
- maxLength: validators.maxLength.max(256).test(maxLengthIsGreaterThanOrEqualToMinLength),
1435
- options: yup.object().shape({
1436
- separator: yup.string(),
1437
- lowercase: yup.boolean(),
1438
- decamelize: yup.boolean(),
1439
- customReplacements: yup.array().of(yup.array().of(yup.string()).min(2).max(2)),
1440
- preserveLeadingUnderscore: yup.boolean()
1545
+ name: 'isGreaterThanMin',
1546
+ message: 'maxLength must be greater or equal to minLength',
1547
+ test (value) {
1548
+ const { minLength } = this.parent;
1549
+ return !(!_.isUndefined(minLength) && !_.isUndefined(value) && value < minLength);
1550
+ }
1551
+ };
1552
+ const getTypeValidator = (attribute, { types, modelType, attributes })=>{
1553
+ return yup.object({
1554
+ type: yup.string().oneOf([
1555
+ ...types
1556
+ ]).required(),
1557
+ configurable: yup.boolean().nullable(),
1558
+ private: yup.boolean().nullable(),
1559
+ pluginOptions: yup.object(),
1560
+ ...getTypeShape(attribute, {
1561
+ modelType,
1562
+ attributes
1441
1563
  })
1442
- };
1443
- }
1444
- case "string":
1445
- case "text": {
1446
- return {
1447
- default: yup.string(),
1448
- required: validators.required,
1449
- unique: validators.unique,
1450
- minLength: validators.minLength,
1451
- maxLength: validators.maxLength,
1452
- regex: yup.string().test(isValidRegExpPattern)
1453
- };
1454
- }
1455
- case "richtext": {
1456
- return {
1457
- default: yup.string(),
1458
- required: validators.required,
1459
- minLength: validators.minLength,
1460
- maxLength: validators.maxLength
1461
- };
1462
- }
1463
- case "blocks": {
1464
- return {
1465
- required: validators.required
1466
- };
1467
- }
1468
- case "json": {
1469
- return {
1470
- default: yup.mixed().test(isValidDefaultJSON),
1471
- required: validators.required
1472
- };
1473
- }
1474
- case "enumeration": {
1475
- return {
1476
- enum: yup.array().of(yup.string().test(isValidEnum).required()).min(1).test(areEnumValuesUnique).required(),
1477
- default: yup.string().when("enum", (enumVal) => yup.string().oneOf(enumVal)),
1478
- enumName: yup.string().test(isValidName),
1479
- required: validators.required
1480
- };
1481
- }
1482
- case "password": {
1483
- return {
1484
- required: validators.required,
1485
- minLength: validators.minLength,
1486
- maxLength: validators.maxLength
1487
- };
1488
- }
1489
- case "email": {
1490
- return {
1491
- default: yup.string().email(),
1492
- required: validators.required,
1493
- unique: validators.unique,
1494
- minLength: validators.minLength,
1495
- maxLength: validators.maxLength
1496
- };
1497
- }
1498
- case "integer": {
1499
- return {
1500
- default: yup.number().integer(),
1501
- required: validators.required,
1502
- unique: validators.unique,
1503
- min: yup.number().integer(),
1504
- max: yup.number().integer()
1505
- };
1506
- }
1507
- case "biginteger": {
1508
- return {
1509
- default: yup.string().nullable().matches(/^\d*$/),
1510
- required: validators.required,
1511
- unique: validators.unique,
1512
- min: yup.string().nullable().matches(/^\d*$/),
1513
- max: yup.string().nullable().matches(/^\d*$/)
1514
- };
1515
- }
1516
- case "float": {
1517
- return {
1518
- default: yup.number(),
1519
- required: validators.required,
1520
- unique: validators.unique,
1521
- min: yup.number(),
1522
- max: yup.number()
1523
- };
1524
- }
1525
- case "decimal": {
1526
- return {
1527
- default: yup.number(),
1528
- required: validators.required,
1529
- unique: validators.unique,
1530
- min: yup.number(),
1531
- max: yup.number()
1532
- };
1533
- }
1534
- case "time":
1535
- case "datetime":
1536
- case "date": {
1537
- return {
1538
- default: yup.string(),
1539
- required: validators.required,
1540
- unique: validators.unique
1541
- };
1542
- }
1543
- case "boolean": {
1544
- return {
1545
- default: yup.boolean(),
1546
- required: validators.required
1547
- };
1548
- }
1549
- case "component": {
1550
- return {
1551
- required: validators.required,
1552
- repeatable: yup.boolean(),
1553
- // TODO: Add correct server validation for nested components
1554
- component: yup.string().required(),
1555
- min: yup.number(),
1556
- max: yup.number()
1557
- };
1558
- }
1559
- case "dynamiczone": {
1560
- return {
1561
- required: validators.required,
1562
- components: yup.array().of(yup.string().required()).test("isArray", "${path} must be an array", (value) => Array.isArray(value)).min(1),
1563
- min: yup.number(),
1564
- max: yup.number()
1565
- };
1566
- }
1567
- default: {
1568
- return {};
1569
- }
1570
- }
1571
- };
1572
- const STRAPI_USER_RELATIONS = ["oneToOne", "oneToMany"];
1573
- const isValidRelation = (validNatures) => function(value) {
1574
- if (value === void 0) {
1575
- return true;
1576
- }
1577
- if (this.parent.target === coreUids.STRAPI_USER) {
1578
- if (!validNatures.includes(value) || !isUndefined(this.parent.targetAttribute)) {
1579
- return this.createError({
1580
- path: this.path,
1581
- message: `must be one of the following values: ${STRAPI_USER_RELATIONS.join(", ")}`
1582
- });
1583
- }
1584
- }
1585
- return validNatures.includes(value) ? true : this.createError({
1586
- path: this.path,
1587
- message: `must be one of the following values: ${validNatures.join(", ")}`
1588
- });
1589
- };
1590
- const getRelationValidator = (attribute, allowedRelations) => {
1591
- const contentTypesUIDs = Object.keys(strapi.contentTypes).filter((key) => strapi.contentTypes[key].kind === typeKinds.COLLECTION_TYPE).filter((key) => !key.startsWith(coreUids.PREFIX) || key === coreUids.STRAPI_USER).concat(["__self__", "__contentType__"]);
1592
- const base = {
1593
- type: yup.string().oneOf(["relation"]).required(),
1594
- relation: yup.string().test("isValidRelation", isValidRelation(allowedRelations)).required(),
1595
- configurable: yup.boolean().nullable(),
1596
- private: yup.boolean().nullable(),
1597
- pluginOptions: yup.object()
1598
- };
1599
- switch (attribute.relation) {
1600
- case "oneToOne":
1601
- case "oneToMany":
1602
- case "manyToOne":
1603
- case "manyToMany":
1604
- case "morphOne":
1605
- case "morphMany": {
1606
- return yup.object({
1607
- ...base,
1608
- target: yup.string().oneOf(contentTypesUIDs).required(),
1609
- targetAttribute: yup.string().test(isValidName).nullable()
1610
- });
1611
- }
1612
- case "morphToOne":
1613
- case "morphToMany":
1614
- default: {
1615
- return yup.object({ ...base });
1564
+ });
1565
+ };
1566
+ const getTypeShape = (attribute, { attributes } = {})=>{
1567
+ switch(attribute.type){
1568
+ /**
1569
+ * complex types
1570
+ */ case 'media':
1571
+ {
1572
+ return {
1573
+ multiple: yup.boolean(),
1574
+ required: validators.required,
1575
+ allowedTypes: yup.array().of(yup.string().oneOf([
1576
+ 'images',
1577
+ 'videos',
1578
+ 'files',
1579
+ 'audios'
1580
+ ])).min(1)
1581
+ };
1582
+ }
1583
+ case 'uid':
1584
+ {
1585
+ return {
1586
+ required: validators.required,
1587
+ targetField: yup.string().oneOf(Object.keys(attributes).filter((key)=>VALID_UID_TARGETS.includes(_.get(attributes[key], 'type')))).nullable(),
1588
+ default: yup.string().test('isValidDefaultUID', 'cannot define a default UID if the targetField is set', function(value) {
1589
+ const { targetField } = this.parent;
1590
+ return !!(_.isNil(targetField) || _.isNil(value));
1591
+ }).test(isValidUID),
1592
+ minLength: validators.minLength,
1593
+ maxLength: validators.maxLength.max(256).test(maxLengthIsGreaterThanOrEqualToMinLength),
1594
+ options: yup.object().shape({
1595
+ separator: yup.string(),
1596
+ lowercase: yup.boolean(),
1597
+ decamelize: yup.boolean(),
1598
+ customReplacements: yup.array().of(yup.array().of(yup.string()).min(2).max(2)),
1599
+ preserveLeadingUnderscore: yup.boolean()
1600
+ })
1601
+ };
1602
+ }
1603
+ /**
1604
+ * scalar types
1605
+ */ case 'string':
1606
+ case 'text':
1607
+ {
1608
+ return {
1609
+ default: yup.string(),
1610
+ required: validators.required,
1611
+ unique: validators.unique,
1612
+ minLength: validators.minLength,
1613
+ maxLength: validators.maxLength,
1614
+ regex: yup.string().test(isValidRegExpPattern)
1615
+ };
1616
+ }
1617
+ case 'richtext':
1618
+ {
1619
+ return {
1620
+ default: yup.string(),
1621
+ required: validators.required,
1622
+ minLength: validators.minLength,
1623
+ maxLength: validators.maxLength
1624
+ };
1625
+ }
1626
+ case 'blocks':
1627
+ {
1628
+ return {
1629
+ required: validators.required
1630
+ };
1631
+ }
1632
+ case 'json':
1633
+ {
1634
+ return {
1635
+ default: yup.mixed().test(isValidDefaultJSON),
1636
+ required: validators.required
1637
+ };
1638
+ }
1639
+ case 'enumeration':
1640
+ {
1641
+ return {
1642
+ enum: yup.array().of(yup.string().test(isValidEnum).required()).min(1).test(areEnumValuesUnique).required(),
1643
+ default: yup.string().when('enum', (enumVal)=>yup.string().oneOf(enumVal)),
1644
+ enumName: yup.string().test(isValidName),
1645
+ required: validators.required
1646
+ };
1647
+ }
1648
+ case 'password':
1649
+ {
1650
+ return {
1651
+ required: validators.required,
1652
+ minLength: validators.minLength,
1653
+ maxLength: validators.maxLength
1654
+ };
1655
+ }
1656
+ case 'email':
1657
+ {
1658
+ return {
1659
+ default: yup.string().email(),
1660
+ required: validators.required,
1661
+ unique: validators.unique,
1662
+ minLength: validators.minLength,
1663
+ maxLength: validators.maxLength
1664
+ };
1665
+ }
1666
+ case 'integer':
1667
+ {
1668
+ return {
1669
+ default: yup.number().integer(),
1670
+ required: validators.required,
1671
+ unique: validators.unique,
1672
+ min: yup.number().integer(),
1673
+ max: yup.number().integer()
1674
+ };
1675
+ }
1676
+ case 'biginteger':
1677
+ {
1678
+ return {
1679
+ default: yup.string().nullable().matches(/^\d*$/),
1680
+ required: validators.required,
1681
+ unique: validators.unique,
1682
+ min: yup.string().nullable().matches(/^\d*$/),
1683
+ max: yup.string().nullable().matches(/^\d*$/)
1684
+ };
1685
+ }
1686
+ case 'float':
1687
+ {
1688
+ return {
1689
+ default: yup.number(),
1690
+ required: validators.required,
1691
+ unique: validators.unique,
1692
+ min: yup.number(),
1693
+ max: yup.number()
1694
+ };
1695
+ }
1696
+ case 'decimal':
1697
+ {
1698
+ return {
1699
+ default: yup.number(),
1700
+ required: validators.required,
1701
+ unique: validators.unique,
1702
+ min: yup.number(),
1703
+ max: yup.number()
1704
+ };
1705
+ }
1706
+ case 'time':
1707
+ case 'datetime':
1708
+ case 'date':
1709
+ {
1710
+ return {
1711
+ default: yup.string(),
1712
+ required: validators.required,
1713
+ unique: validators.unique
1714
+ };
1715
+ }
1716
+ case 'boolean':
1717
+ {
1718
+ return {
1719
+ default: yup.boolean(),
1720
+ required: validators.required
1721
+ };
1722
+ }
1723
+ case 'component':
1724
+ {
1725
+ return {
1726
+ required: validators.required,
1727
+ repeatable: yup.boolean(),
1728
+ // TODO: Add correct server validation for nested components
1729
+ component: yup.string().required(),
1730
+ min: yup.number(),
1731
+ max: yup.number()
1732
+ };
1733
+ }
1734
+ case 'dynamiczone':
1735
+ {
1736
+ return {
1737
+ required: validators.required,
1738
+ components: yup.array().of(yup.string().required()).test('isArray', '${path} must be an array', (value)=>Array.isArray(value)).min(1),
1739
+ min: yup.number(),
1740
+ max: yup.number()
1741
+ };
1742
+ }
1743
+ default:
1744
+ {
1745
+ return {};
1746
+ }
1616
1747
  }
1617
- }
1618
- };
1619
- const createSchema = (types, relations, { modelType } = {}) => {
1620
- const shape = {
1621
- description: yup.string(),
1622
- options: yup.object(),
1623
- pluginOptions: yup.object(),
1624
- collectionName: yup.string().nullable().test(isValidCollectionName),
1625
- attributes: createAttributesValidator({ types, relations, modelType }),
1626
- draftAndPublish: yup.boolean()
1627
- };
1628
- if (modelType === modelTypes.CONTENT_TYPE) {
1629
- shape.kind = yup.string().oneOf([typeKinds.SINGLE_TYPE, typeKinds.COLLECTION_TYPE]).nullable();
1630
- }
1631
- return yup.object(shape).noUnknown();
1632
- };
1633
- const createAttributesValidator = ({ types, modelType, relations }) => {
1634
- return yup.lazy((attributes) => {
1635
- return yup.object().shape(
1636
- _.mapValues(attributes, (attribute, key) => {
1637
- if (isForbiddenKey(key)) {
1638
- return forbiddenValidator();
1639
- }
1640
- if (isConflictingKey(key, attributes)) {
1641
- return conflictingKeysValidator(key);
1642
- }
1643
- if (attribute.type === "relation") {
1644
- return getRelationValidator(attribute, relations).test(isValidKey(key));
1748
+ };
1749
+
1750
+ const STRAPI_USER_RELATIONS = [
1751
+ 'oneToOne',
1752
+ 'oneToMany'
1753
+ ];
1754
+ const isValidRelation = (validNatures)=>function(value) {
1755
+ // NOTE: In case of an undefined value, delegate the check to .required()
1756
+ if (value === undefined) {
1757
+ return true;
1645
1758
  }
1646
- if (_.has(attribute, "type")) {
1647
- return getTypeValidator(attribute, { types, modelType, attributes }).test(
1648
- isValidKey(key)
1649
- );
1759
+ if (this.parent.target === coreUids.STRAPI_USER) {
1760
+ if (!validNatures.includes(value) || !isUndefined(this.parent.targetAttribute)) {
1761
+ return this.createError({
1762
+ path: this.path,
1763
+ message: `must be one of the following values: ${STRAPI_USER_RELATIONS.join(', ')}`
1764
+ });
1765
+ }
1650
1766
  }
1651
- return typeOrRelationValidator;
1652
- })
1653
- ).required("attributes.required");
1654
- });
1655
- };
1656
- const isConflictingKey = (key, attributes) => {
1657
- const snakeCaseKey = snakeCase(key);
1658
- return Object.keys(attributes).some((existingKey) => {
1659
- if (existingKey === key) return false;
1660
- return snakeCase(existingKey) === snakeCaseKey;
1661
- });
1662
- };
1663
- const isForbiddenKey = (key) => {
1664
- return getService("builder").isReservedAttributeName(key);
1665
- };
1666
- const forbiddenValidator = () => {
1667
- const reservedNames = [...getService("builder").getReservedNames().attributes];
1668
- return yup.mixed().test({
1669
- name: "forbiddenKeys",
1670
- message: `Attribute keys cannot be one of ${reservedNames.join(", ")}`,
1671
- test: () => false
1672
- });
1673
- };
1674
- const conflictingKeysValidator = (key) => {
1675
- return yup.mixed().test({
1676
- name: "conflictingKeys",
1677
- message: `Attribute ${key} conflicts with an existing key`,
1678
- test: () => false
1679
- });
1767
+ return validNatures.includes(value) ? true : this.createError({
1768
+ path: this.path,
1769
+ message: `must be one of the following values: ${validNatures.join(', ')}`
1770
+ });
1771
+ };
1772
+ const getRelationValidator = (attribute, allowedRelations)=>{
1773
+ const contentTypesUIDs = Object.keys(strapi.contentTypes).filter((key)=>strapi.contentTypes[key].kind === typeKinds.COLLECTION_TYPE).filter((key)=>!key.startsWith(coreUids.PREFIX) || key === coreUids.STRAPI_USER).concat([
1774
+ '__self__',
1775
+ '__contentType__'
1776
+ ]);
1777
+ const base = {
1778
+ type: yup.string().oneOf([
1779
+ 'relation'
1780
+ ]).required(),
1781
+ relation: yup.string().test('isValidRelation', isValidRelation(allowedRelations)).required(),
1782
+ configurable: yup.boolean().nullable(),
1783
+ private: yup.boolean().nullable(),
1784
+ pluginOptions: yup.object()
1785
+ };
1786
+ switch(attribute.relation){
1787
+ case 'oneToOne':
1788
+ case 'oneToMany':
1789
+ case 'manyToOne':
1790
+ case 'manyToMany':
1791
+ case 'morphOne':
1792
+ case 'morphMany':
1793
+ {
1794
+ return yup.object({
1795
+ ...base,
1796
+ target: yup.string().oneOf(contentTypesUIDs).required(),
1797
+ targetAttribute: yup.string().test(isValidName).nullable()
1798
+ });
1799
+ }
1800
+ case 'morphToOne':
1801
+ case 'morphToMany':
1802
+ default:
1803
+ {
1804
+ return yup.object({
1805
+ ...base
1806
+ });
1807
+ }
1808
+ }
1809
+ };
1810
+
1811
+ const createSchema = (types, relations, { modelType } = {})=>{
1812
+ const shape = {
1813
+ description: yup.string(),
1814
+ options: yup.object(),
1815
+ pluginOptions: yup.object(),
1816
+ collectionName: yup.string().nullable().test(isValidCollectionName),
1817
+ attributes: createAttributesValidator({
1818
+ types,
1819
+ relations,
1820
+ modelType
1821
+ }),
1822
+ draftAndPublish: yup.boolean()
1823
+ };
1824
+ if (modelType === modelTypes.CONTENT_TYPE) {
1825
+ shape.kind = yup.string().oneOf([
1826
+ typeKinds.SINGLE_TYPE,
1827
+ typeKinds.COLLECTION_TYPE
1828
+ ]).nullable();
1829
+ }
1830
+ return yup.object(shape).noUnknown();
1831
+ };
1832
+ const createAttributesValidator = ({ types, modelType, relations })=>{
1833
+ return yup.lazy((attributes)=>{
1834
+ return yup.object().shape(_.mapValues(attributes, (attribute, key)=>{
1835
+ if (isForbiddenKey(key)) {
1836
+ return forbiddenValidator();
1837
+ }
1838
+ if (isConflictingKey(key, attributes)) {
1839
+ return conflictingKeysValidator(key);
1840
+ }
1841
+ if (attribute.type === 'relation') {
1842
+ return getRelationValidator(attribute, relations).test(isValidKey(key));
1843
+ }
1844
+ if (_.has(attribute, 'type')) {
1845
+ return getTypeValidator(attribute, {
1846
+ types,
1847
+ modelType,
1848
+ attributes
1849
+ }).test(isValidKey(key));
1850
+ }
1851
+ return typeOrRelationValidator;
1852
+ })).required('attributes.required');
1853
+ });
1854
+ };
1855
+ const isConflictingKey = (key, attributes)=>{
1856
+ const snakeCaseKey = snakeCase(key);
1857
+ return Object.keys(attributes).some((existingKey)=>{
1858
+ if (existingKey === key) return false; // don't compare against itself
1859
+ return snakeCase(existingKey) === snakeCaseKey;
1860
+ });
1861
+ };
1862
+ const isForbiddenKey = (key)=>{
1863
+ return getService('builder').isReservedAttributeName(key);
1864
+ };
1865
+ const forbiddenValidator = ()=>{
1866
+ const reservedNames = [
1867
+ ...getService('builder').getReservedNames().attributes
1868
+ ];
1869
+ return yup.mixed().test({
1870
+ name: 'forbiddenKeys',
1871
+ message: `Attribute keys cannot be one of ${reservedNames.join(', ')}`,
1872
+ test: ()=>false
1873
+ });
1874
+ };
1875
+ const conflictingKeysValidator = (key)=>{
1876
+ return yup.mixed().test({
1877
+ name: 'conflictingKeys',
1878
+ message: `Attribute ${key} conflicts with an existing key`,
1879
+ test: ()=>false
1880
+ });
1680
1881
  };
1681
1882
  const typeOrRelationValidator = yup.object().test({
1682
- name: "mustHaveTypeOrTarget",
1683
- message: "Attribute must have either a type or a target",
1684
- test: () => false
1883
+ name: 'mustHaveTypeOrTarget',
1884
+ message: 'Attribute must have either a type or a target',
1885
+ test: ()=>false
1685
1886
  });
1686
- const hasDefaultAttribute = (attribute) => {
1687
- return "default" in attribute;
1688
- };
1689
- const removeEmptyDefaults = (data) => {
1690
- const { attributes } = data || {};
1691
- Object.keys(attributes).forEach((attributeName) => {
1692
- const attribute = attributes[attributeName];
1693
- if (hasDefaultAttribute(attribute) && attribute.default === "") {
1694
- attribute.default = void 0;
1695
- }
1696
- });
1697
- };
1698
- const removeDeletedUIDTargetFields = (data) => {
1699
- if (_.has(data, "attributes")) {
1700
- Object.values(data.attributes).forEach((attribute) => {
1701
- if (attribute.type === "uid" && !_.isUndefined(attribute.targetField) && !_.has(data.attributes, attribute.targetField)) {
1702
- attribute.targetField = void 0;
1703
- }
1887
+
1888
+ const hasDefaultAttribute = (attribute)=>{
1889
+ return 'default' in attribute;
1890
+ };
1891
+
1892
+ const removeEmptyDefaults = (data)=>{
1893
+ const { attributes } = data || {};
1894
+ Object.keys(attributes).forEach((attributeName)=>{
1895
+ const attribute = attributes[attributeName];
1896
+ if (hasDefaultAttribute(attribute) && attribute.default === '') {
1897
+ attribute.default = undefined;
1898
+ }
1704
1899
  });
1705
- }
1706
1900
  };
1707
- const VALID_RELATIONS$1 = ["oneToOne", "oneToMany"];
1708
- const VALID_TYPES$1 = [...DEFAULT_TYPES, "component", "customField"];
1901
+ const removeDeletedUIDTargetFields = (data)=>{
1902
+ if (_.has(data, 'attributes')) {
1903
+ Object.values(data.attributes).forEach((attribute)=>{
1904
+ if (attribute.type === 'uid' && !_.isUndefined(attribute.targetField) && !_.has(data.attributes, attribute.targetField)) {
1905
+ attribute.targetField = undefined;
1906
+ }
1907
+ });
1908
+ }
1909
+ };
1910
+
1911
+ const VALID_RELATIONS$1 = [
1912
+ 'oneToOne',
1913
+ 'oneToMany'
1914
+ ];
1915
+ const VALID_TYPES$1 = [
1916
+ ...DEFAULT_TYPES,
1917
+ 'component',
1918
+ 'customField'
1919
+ ];
1709
1920
  const componentSchema = createSchema(VALID_TYPES$1, VALID_RELATIONS$1, {
1710
- modelType: modelTypes.COMPONENT
1921
+ modelType: modelTypes.COMPONENT
1711
1922
  }).shape({
1712
- displayName: yup.string().min(1).required("displayName.required"),
1713
- icon: yup.string().nullable().test(isValidIcon),
1714
- category: yup.string().nullable().test(isValidCategoryName).required("category.required")
1923
+ displayName: yup.string().min(1).required('displayName.required'),
1924
+ icon: yup.string().nullable().test(isValidIcon),
1925
+ category: yup.string().nullable().test(isValidCategoryName).required('category.required')
1715
1926
  }).required().noUnknown();
1716
- const nestedComponentSchema = yup.array().of(
1717
- componentSchema.shape({
1927
+ const nestedComponentSchema = yup.array().of(componentSchema.shape({
1718
1928
  uid: yup.string(),
1719
1929
  tmpUID: yup.string()
1720
- }).test({
1721
- name: "mustHaveUIDOrTmpUID",
1722
- message: "Component must have a uid or a tmpUID",
1723
- test(attr) {
1724
- if (_.has(attr, "uid") && _.has(attr, "tmpUID")) return false;
1725
- if (!_.has(attr, "uid") && !_.has(attr, "tmpUID")) return false;
1726
- return true;
1930
+ }).test({
1931
+ name: 'mustHaveUIDOrTmpUID',
1932
+ message: 'Component must have a uid or a tmpUID',
1933
+ test (attr) {
1934
+ if (_.has(attr, 'uid') && _.has(attr, 'tmpUID')) return false;
1935
+ if (!_.has(attr, 'uid') && !_.has(attr, 'tmpUID')) return false;
1936
+ return true;
1727
1937
  }
1728
- }).required().noUnknown()
1729
- );
1938
+ }).required().noUnknown());
1730
1939
  const componentInputSchema = yup.object({
1731
- component: componentSchema,
1732
- components: nestedComponentSchema
1940
+ component: componentSchema,
1941
+ components: nestedComponentSchema
1733
1942
  }).noUnknown();
1734
1943
  const validateComponentInput = validateYupSchema(componentInputSchema);
1735
1944
  const updateComponentInputSchema = yup.object({
1736
- component: componentSchema,
1737
- components: nestedComponentSchema
1945
+ component: componentSchema,
1946
+ components: nestedComponentSchema
1738
1947
  }).noUnknown();
1739
- const validateUpdateComponentInput = (data) => {
1740
- if (_.has(data, "component") && data.component) {
1741
- removeEmptyDefaults(data.component);
1742
- }
1743
- if (_.has(data, "components") && Array.isArray(data.components)) {
1744
- data.components.forEach((data2) => {
1745
- if (_.has(data2, "uid")) {
1746
- removeEmptyDefaults(data2);
1747
- }
1748
- });
1749
- }
1750
- return validateYupSchema(updateComponentInputSchema)(data);
1948
+ const validateUpdateComponentInput = (data)=>{
1949
+ if (_.has(data, 'component') && data.component) {
1950
+ removeEmptyDefaults(data.component);
1951
+ }
1952
+ if (_.has(data, 'components') && Array.isArray(data.components)) {
1953
+ data.components.forEach((data)=>{
1954
+ if (_.has(data, 'uid')) {
1955
+ removeEmptyDefaults(data);
1956
+ }
1957
+ });
1958
+ }
1959
+ return validateYupSchema(updateComponentInputSchema)(data);
1751
1960
  };
1752
- const components = {
1753
- /**
1961
+
1962
+ /**
1963
+ * Components controller
1964
+ */ var components = {
1965
+ /**
1754
1966
  * GET /components handler
1755
1967
  * Returns a list of available components
1756
1968
  * @param {Object} ctx - koa context
1757
- */
1758
- async getComponents(ctx) {
1759
- const componentService = getService("components");
1760
- const componentUIDs = Object.keys(strapi.components);
1761
- const data = componentUIDs.map((uid) => {
1762
- return componentService.formatComponent(strapi.components[uid]);
1763
- });
1764
- ctx.send({ data });
1765
- },
1766
- /**
1969
+ */ async getComponents (ctx) {
1970
+ const componentService = getService('components');
1971
+ const componentUIDs = Object.keys(strapi.components);
1972
+ const data = componentUIDs.map((uid)=>{
1973
+ return componentService.formatComponent(strapi.components[uid]);
1974
+ });
1975
+ ctx.send({
1976
+ data
1977
+ });
1978
+ },
1979
+ /**
1767
1980
  * GET /components/:uid
1768
1981
  * Returns a specific component
1769
1982
  * @param {Object} ctx - koa context
1770
- */
1771
- async getComponent(ctx) {
1772
- const { uid } = ctx.params;
1773
- const component = strapi.components[uid];
1774
- if (!component) {
1775
- return ctx.send({ error: "component.notFound" }, 404);
1776
- }
1777
- const componentService = getService("components");
1778
- ctx.send({ data: componentService.formatComponent(component) });
1779
- },
1780
- /**
1983
+ */ async getComponent (ctx) {
1984
+ const { uid } = ctx.params;
1985
+ const component = strapi.components[uid];
1986
+ if (!component) {
1987
+ return ctx.send({
1988
+ error: 'component.notFound'
1989
+ }, 404);
1990
+ }
1991
+ const componentService = getService('components');
1992
+ ctx.send({
1993
+ data: componentService.formatComponent(component)
1994
+ });
1995
+ },
1996
+ /**
1781
1997
  * POST /components
1782
1998
  * Creates a component and returns its infos
1783
1999
  * @param {Object} ctx - koa context
1784
- */
1785
- async createComponent(ctx) {
1786
- const body = ctx.request.body;
1787
- try {
1788
- await validateComponentInput(body);
1789
- } catch (error) {
1790
- return ctx.send({ error }, 400);
1791
- }
1792
- try {
1793
- strapi.reload.isWatching = false;
1794
- const componentService = getService("components");
1795
- const component = await componentService.createComponent({
1796
- component: body.component,
1797
- components: body.components
1798
- });
1799
- setImmediate(() => strapi.reload());
1800
- ctx.send({ data: { uid: component.uid } }, 201);
1801
- } catch (error) {
1802
- strapi.log.error(error);
1803
- ctx.send({ error: error?.message || "Unknown error" }, 400);
1804
- }
1805
- },
1806
- /**
2000
+ */ async createComponent (ctx) {
2001
+ const body = ctx.request.body;
2002
+ try {
2003
+ await validateComponentInput(body);
2004
+ } catch (error) {
2005
+ return ctx.send({
2006
+ error
2007
+ }, 400);
2008
+ }
2009
+ try {
2010
+ strapi.reload.isWatching = false;
2011
+ const componentService = getService('components');
2012
+ const component = await componentService.createComponent({
2013
+ component: body.component,
2014
+ components: body.components
2015
+ });
2016
+ setImmediate(()=>strapi.reload());
2017
+ ctx.send({
2018
+ data: {
2019
+ uid: component.uid
2020
+ }
2021
+ }, 201);
2022
+ } catch (error) {
2023
+ strapi.log.error(error);
2024
+ ctx.send({
2025
+ error: error?.message || 'Unknown error'
2026
+ }, 400);
2027
+ }
2028
+ },
2029
+ /**
1807
2030
  * PUT /components/:uid
1808
2031
  * Updates a component and return its infos
1809
2032
  * @param {Object} ctx - koa context - enhanced koa context
1810
- */
1811
- async updateComponent(ctx) {
1812
- const { uid } = ctx.params;
1813
- const body = ctx.request.body;
1814
- if (!_.has(strapi.components, uid)) {
1815
- return ctx.send({ error: "component.notFound" }, 404);
1816
- }
1817
- try {
1818
- await validateUpdateComponentInput(body);
1819
- } catch (error) {
1820
- return ctx.send({ error }, 400);
1821
- }
1822
- try {
1823
- strapi.reload.isWatching = false;
1824
- const componentService = getService("components");
1825
- const component = await componentService.editComponent(uid, {
1826
- component: body.component,
1827
- components: body.components
1828
- });
1829
- setImmediate(() => strapi.reload());
1830
- ctx.send({ data: { uid: component.uid } });
1831
- } catch (error) {
1832
- strapi.log.error(error);
1833
- ctx.send({ error: error?.message || "Unknown error" }, 400);
1834
- }
1835
- },
1836
- /**
2033
+ */ async updateComponent (ctx) {
2034
+ const { uid } = ctx.params;
2035
+ const body = ctx.request.body;
2036
+ if (!_.has(strapi.components, uid)) {
2037
+ return ctx.send({
2038
+ error: 'component.notFound'
2039
+ }, 404);
2040
+ }
2041
+ try {
2042
+ await validateUpdateComponentInput(body);
2043
+ } catch (error) {
2044
+ return ctx.send({
2045
+ error
2046
+ }, 400);
2047
+ }
2048
+ try {
2049
+ strapi.reload.isWatching = false;
2050
+ const componentService = getService('components');
2051
+ const component = await componentService.editComponent(uid, {
2052
+ component: body.component,
2053
+ components: body.components
2054
+ });
2055
+ setImmediate(()=>strapi.reload());
2056
+ ctx.send({
2057
+ data: {
2058
+ uid: component.uid
2059
+ }
2060
+ });
2061
+ } catch (error) {
2062
+ strapi.log.error(error);
2063
+ ctx.send({
2064
+ error: error?.message || 'Unknown error'
2065
+ }, 400);
2066
+ }
2067
+ },
2068
+ /**
1837
2069
  * DELETE /components/:uid
1838
2070
  * Deletes a components and returns its old infos
1839
2071
  * @param {Object} ctx - koa context
1840
- */
1841
- async deleteComponent(ctx) {
1842
- const { uid } = ctx.params;
1843
- if (!_.has(strapi.components, uid)) {
1844
- return ctx.send({ error: "component.notFound" }, 404);
1845
- }
1846
- try {
1847
- strapi.reload.isWatching = false;
1848
- const componentService = getService("components");
1849
- const component = await componentService.deleteComponent(uid);
1850
- setImmediate(() => strapi.reload());
1851
- ctx.send({ data: { uid: component.uid } });
1852
- } catch (error) {
1853
- strapi.log.error(error);
1854
- ctx.send({ error: error?.message || "Unknown error" }, 400);
1855
- }
1856
- }
1857
- };
1858
- const VALID_RELATIONS = {
1859
- [typeKinds.SINGLE_TYPE]: [
1860
- "oneToOne",
1861
- "oneToMany",
1862
- "morphOne",
1863
- "morphMany",
1864
- "morphToOne",
1865
- "morphToMany"
1866
- ],
1867
- [typeKinds.COLLECTION_TYPE]: [
1868
- "oneToOne",
1869
- "oneToMany",
1870
- "manyToOne",
1871
- "manyToMany",
1872
- "morphOne",
1873
- "morphMany",
1874
- "morphToOne",
1875
- "morphToMany"
1876
- ]
1877
- };
1878
- const VALID_TYPES = [...DEFAULT_TYPES, "uid", "component", "dynamiczone", "customField"];
1879
- const createContentTypeSchema = (data, { isEdition = false } = {}) => {
1880
- const kind = getOr(
1881
- typeKinds.COLLECTION_TYPE,
1882
- "contentType.kind",
1883
- data
1884
- );
1885
- const contentTypeSchema = createSchema(VALID_TYPES, VALID_RELATIONS[kind] || [], {
1886
- modelType: modelTypes.CONTENT_TYPE
1887
- }).shape({
1888
- displayName: yup.string().min(1).required(),
1889
- singularName: yup.string().min(1).test(nameIsAvailable(isEdition)).test(forbiddenContentTypeNameValidator()).isKebabCase().required(),
1890
- pluralName: yup.string().min(1).test(nameIsAvailable(isEdition)).test(nameIsNotExistingCollectionName(isEdition)).test(forbiddenContentTypeNameValidator()).isKebabCase().required()
1891
- }).test(
1892
- "singularName-not-equal-pluralName",
1893
- "${path}: singularName and pluralName should be different",
1894
- (value) => value.singularName !== value.pluralName
1895
- );
1896
- return yup.object({
1897
- // FIXME .noUnknown(false) will strip off the unwanted properties without throwing an error
1898
- // Why not having .noUnknown() ? Because we want to be able to add options relatable to EE features
1899
- // without having any reference to them in CE.
1900
- // Why not handle an "options" object in the content-type ? The admin panel needs lots of rework
1901
- // to be able to send this options object instead of top-level attributes.
1902
- // @nathan-pichon 20/02/2023
1903
- contentType: contentTypeSchema.required().noUnknown(false),
1904
- components: nestedComponentSchema
1905
- }).noUnknown();
1906
- };
1907
- const validateContentTypeInput = (data) => {
1908
- return validateYupSchema(createContentTypeSchema(data))(data);
1909
- };
1910
- const validateUpdateContentTypeInput = (data) => {
1911
- if (has$1("contentType", data)) {
1912
- removeEmptyDefaults(data.contentType);
1913
- removeDeletedUIDTargetFields(data.contentType);
1914
- }
1915
- if (has$1("components", data) && Array.isArray(data.components)) {
1916
- data.components.forEach((comp) => {
1917
- if (has$1("uid", comp)) {
1918
- removeEmptyDefaults(comp);
1919
- }
1920
- });
1921
- }
1922
- return validateYupSchema(createContentTypeSchema(data, { isEdition: true }))(data);
1923
- };
1924
- const forbiddenContentTypeNameValidator = () => {
1925
- const reservedNames = getService("builder").getReservedNames().models;
1926
- return {
1927
- name: "forbiddenContentTypeName",
1928
- message: `Content Type name cannot be one of ${reservedNames.join(", ")}`,
1929
- test(value) {
1930
- if (typeof value !== "string") {
1931
- return true;
1932
- }
1933
- return !getService("builder").isReservedModelName(value);
1934
- }
1935
- };
1936
- };
1937
- const nameIsAvailable = (isEdition) => {
1938
- const usedNames = flatMap((ct) => {
1939
- return [ct.info?.singularName, ct.info?.pluralName];
1940
- })(strapi.contentTypes);
1941
- return {
1942
- name: "nameAlreadyUsed",
1943
- message: "contentType: name `${value}` is already being used by another content type.",
1944
- test(value) {
1945
- if (isEdition) return true;
1946
- if (typeof value !== "string") {
1947
- return true;
1948
- }
1949
- return usedNames.every((usedName) => snakeCase(usedName) !== snakeCase(value));
1950
- }
1951
- };
1952
- };
1953
- const nameIsNotExistingCollectionName = (isEdition) => {
1954
- const usedNames = Object.keys(strapi.contentTypes).map(
1955
- (key) => strapi.contentTypes[key].collectionName
1956
- );
1957
- return {
1958
- name: "nameAlreadyUsed",
1959
- message: "contentType: name `${value}` is already being used by another content type.",
1960
- test(value) {
1961
- if (isEdition) return true;
1962
- if (typeof value !== "string") {
1963
- return true;
1964
- }
1965
- return usedNames.every((usedName) => snakeCase(usedName) !== snakeCase(value));
2072
+ */ async deleteComponent (ctx) {
2073
+ const { uid } = ctx.params;
2074
+ if (!_.has(strapi.components, uid)) {
2075
+ return ctx.send({
2076
+ error: 'component.notFound'
2077
+ }, 404);
2078
+ }
2079
+ try {
2080
+ strapi.reload.isWatching = false;
2081
+ const componentService = getService('components');
2082
+ const component = await componentService.deleteComponent(uid);
2083
+ setImmediate(()=>strapi.reload());
2084
+ ctx.send({
2085
+ data: {
2086
+ uid: component.uid
2087
+ }
2088
+ });
2089
+ } catch (error) {
2090
+ strapi.log.error(error);
2091
+ ctx.send({
2092
+ error: error?.message || 'Unknown error'
2093
+ }, 400);
2094
+ }
1966
2095
  }
1967
- };
1968
2096
  };
1969
- const kindSchema = yup.string().oneOf([typeKinds.SINGLE_TYPE, typeKinds.COLLECTION_TYPE]);
1970
- const validateKind = validateYupSchema(kindSchema);
1971
- const contentTypes = {
1972
- async getContentTypes(ctx) {
1973
- const { kind } = ctx.query;
1974
- try {
1975
- await validateKind(kind);
1976
- } catch (error) {
1977
- return ctx.send({ error }, 400);
1978
- }
1979
- const contentTypeService = getService("content-types");
1980
- const contentTypes2 = Object.keys(strapi.contentTypes).filter(
1981
- (uid) => !kind || _.get(strapi.contentTypes[uid], "kind", "collectionType") === kind
1982
- ).map(
1983
- (uid) => contentTypeService.formatContentType(strapi.contentTypes[uid])
1984
- );
1985
- ctx.send({
1986
- data: contentTypes2
1987
- });
1988
- },
1989
- getContentType(ctx) {
1990
- const { uid } = ctx.params;
1991
- const contentType = strapi.contentTypes[uid];
1992
- if (!contentType) {
1993
- return ctx.send({ error: "contentType.notFound" }, 404);
1994
- }
1995
- const contentTypeService = getService("content-types");
1996
- ctx.send({ data: contentTypeService.formatContentType(contentType) });
1997
- },
1998
- async createContentType(ctx) {
1999
- const body = ctx.request.body;
2000
- try {
2001
- await validateContentTypeInput(body);
2002
- } catch (error) {
2003
- return ctx.send({ error }, 400);
2097
+
2098
+ /* eslint-disable no-template-curly-in-string */ // yup templates need to be in this format
2099
+ /**
2100
+ * Allowed relation per type kind
2101
+ */ const VALID_RELATIONS = {
2102
+ [typeKinds.SINGLE_TYPE]: [
2103
+ 'oneToOne',
2104
+ 'oneToMany',
2105
+ 'morphOne',
2106
+ 'morphMany',
2107
+ 'morphToOne',
2108
+ 'morphToMany'
2109
+ ],
2110
+ [typeKinds.COLLECTION_TYPE]: [
2111
+ 'oneToOne',
2112
+ 'oneToMany',
2113
+ 'manyToOne',
2114
+ 'manyToMany',
2115
+ 'morphOne',
2116
+ 'morphMany',
2117
+ 'morphToOne',
2118
+ 'morphToMany'
2119
+ ]
2120
+ };
2121
+ /**
2122
+ * Allowed types
2123
+ */ const VALID_TYPES = [
2124
+ ...DEFAULT_TYPES,
2125
+ 'uid',
2126
+ 'component',
2127
+ 'dynamiczone',
2128
+ 'customField'
2129
+ ];
2130
+ /**
2131
+ * Returns a yup schema to validate a content type payload
2132
+ */ const createContentTypeSchema = (data, { isEdition = false } = {})=>{
2133
+ const kind = getOr(typeKinds.COLLECTION_TYPE, 'contentType.kind', data);
2134
+ const contentTypeSchema = createSchema(VALID_TYPES, VALID_RELATIONS[kind] || [], {
2135
+ modelType: modelTypes.CONTENT_TYPE
2136
+ }).shape({
2137
+ displayName: yup.string().min(1).required(),
2138
+ singularName: yup.string().min(1).test(nameIsAvailable(isEdition)).test(forbiddenContentTypeNameValidator()).isKebabCase().required(),
2139
+ pluralName: yup.string().min(1).test(nameIsAvailable(isEdition)).test(nameIsNotExistingCollectionName(isEdition)) // TODO: v5: require singularName to not match a collection name
2140
+ .test(forbiddenContentTypeNameValidator()).isKebabCase().required()
2141
+ }).test('singularName-not-equal-pluralName', '${path}: singularName and pluralName should be different', (value)=>value.singularName !== value.pluralName);
2142
+ return yup.object({
2143
+ // FIXME .noUnknown(false) will strip off the unwanted properties without throwing an error
2144
+ // Why not having .noUnknown() ? Because we want to be able to add options relatable to EE features
2145
+ // without having any reference to them in CE.
2146
+ // Why not handle an "options" object in the content-type ? The admin panel needs lots of rework
2147
+ // to be able to send this options object instead of top-level attributes.
2148
+ // @nathan-pichon 20/02/2023
2149
+ contentType: contentTypeSchema.required().noUnknown(false),
2150
+ components: nestedComponentSchema
2151
+ }).noUnknown();
2152
+ };
2153
+ /**
2154
+ * Validator for content type creation
2155
+ */ const validateContentTypeInput = (data)=>{
2156
+ return validateYupSchema(createContentTypeSchema(data))(data);
2157
+ };
2158
+ /**
2159
+ * Validator for content type edition
2160
+ */ const validateUpdateContentTypeInput = (data)=>{
2161
+ if (has$1('contentType', data)) {
2162
+ removeEmptyDefaults(data.contentType);
2163
+ removeDeletedUIDTargetFields(data.contentType);
2164
+ }
2165
+ if (has$1('components', data) && Array.isArray(data.components)) {
2166
+ data.components.forEach((comp)=>{
2167
+ if (has$1('uid', comp)) {
2168
+ removeEmptyDefaults(comp);
2169
+ }
2170
+ });
2004
2171
  }
2005
- try {
2006
- strapi.reload.isWatching = false;
2007
- const contentTypeService = getService("content-types");
2008
- const contentType = await contentTypeService.createContentType({
2009
- contentType: body.contentType,
2010
- components: body.components
2011
- });
2012
- const metricsPayload = {
2013
- eventProperties: {
2014
- kind: contentType.kind
2172
+ return validateYupSchema(createContentTypeSchema(data, {
2173
+ isEdition: true
2174
+ }))(data);
2175
+ };
2176
+ const forbiddenContentTypeNameValidator = ()=>{
2177
+ const reservedNames = getService('builder').getReservedNames().models;
2178
+ return {
2179
+ name: 'forbiddenContentTypeName',
2180
+ message: `Content Type name cannot be one of ${reservedNames.join(', ')}`,
2181
+ test (value) {
2182
+ if (typeof value !== 'string') {
2183
+ return true;
2184
+ }
2185
+ return !getService('builder').isReservedModelName(value);
2015
2186
  }
2016
- };
2017
- if (_.isEmpty(strapi.apis)) {
2018
- await strapi.telemetry.send("didCreateFirstContentType", metricsPayload);
2019
- } else {
2020
- await strapi.telemetry.send("didCreateContentType", metricsPayload);
2021
- }
2022
- setImmediate(() => strapi.reload());
2023
- ctx.send({ data: { uid: contentType.uid } }, 201);
2024
- } catch (err) {
2025
- strapi.log.error(err);
2026
- await strapi.telemetry.send("didNotCreateContentType", {
2027
- eventProperties: { error: err.message || err }
2028
- });
2029
- ctx.send({ error: err.message || "Unknown error" }, 400);
2030
- }
2031
- },
2032
- async updateContentType(ctx) {
2033
- const { uid } = ctx.params;
2034
- const body = ctx.request.body;
2035
- if (!_.has(strapi.contentTypes, uid)) {
2036
- return ctx.send({ error: "contentType.notFound" }, 404);
2037
- }
2038
- try {
2039
- await validateUpdateContentTypeInput(body);
2040
- } catch (error) {
2041
- return ctx.send({ error }, 400);
2042
- }
2043
- try {
2044
- strapi.reload.isWatching = false;
2045
- const contentTypeService = getService("content-types");
2046
- const component = await contentTypeService.editContentType(uid, {
2047
- contentType: body.contentType,
2048
- components: body.components
2049
- });
2050
- setImmediate(() => strapi.reload());
2051
- ctx.send({ data: { uid: component.uid } }, 201);
2052
- } catch (error) {
2053
- strapi.log.error(error);
2054
- ctx.send({ error: error?.message || "Unknown error" }, 400);
2055
- }
2056
- },
2057
- async deleteContentType(ctx) {
2058
- const { uid } = ctx.params;
2059
- if (!_.has(strapi.contentTypes, uid)) {
2060
- return ctx.send({ error: "contentType.notFound" }, 404);
2061
- }
2062
- try {
2063
- strapi.reload.isWatching = false;
2064
- const contentTypeService = getService("content-types");
2065
- const component = await contentTypeService.deleteContentType(uid);
2066
- setImmediate(() => strapi.reload());
2067
- ctx.send({ data: { uid: component.uid } });
2068
- } catch (error) {
2069
- strapi.log.error(error);
2070
- ctx.send({ error: error?.message || "Unknown error" }, 400);
2071
- }
2072
- }
2187
+ };
2073
2188
  };
2074
- const exportObject = {
2075
- builder,
2076
- "component-categories": componentCategories,
2077
- components,
2078
- "content-types": contentTypes
2079
- };
2080
- const admin = {
2081
- type: "admin",
2082
- routes: [
2083
- {
2084
- method: "GET",
2085
- path: "/reserved-names",
2086
- handler: "builder.getReservedNames",
2087
- config: {
2088
- policies: [
2089
- {
2090
- name: "admin::hasPermissions",
2091
- config: { actions: ["plugin::content-type-builder.read"] }
2092
- }
2093
- ]
2094
- }
2095
- },
2096
- {
2097
- method: "GET",
2098
- path: "/content-types",
2099
- handler: "content-types.getContentTypes",
2100
- config: {
2101
- policies: [
2102
- {
2103
- name: "admin::hasPermissions",
2104
- config: { actions: ["plugin::content-type-builder.read"] }
2105
- }
2106
- ]
2107
- }
2108
- },
2109
- {
2110
- method: "GET",
2111
- path: "/content-types/:uid",
2112
- handler: "content-types.getContentType",
2113
- config: {
2114
- policies: [
2115
- {
2116
- name: "admin::hasPermissions",
2117
- config: { actions: ["plugin::content-type-builder.read"] }
2118
- }
2119
- ]
2120
- }
2121
- },
2122
- {
2123
- method: "POST",
2124
- path: "/content-types",
2125
- handler: "content-types.createContentType",
2126
- config: {
2127
- policies: [
2128
- {
2129
- name: "admin::hasPermissions",
2130
- config: { actions: ["plugin::content-type-builder.read"] }
2131
- }
2132
- ]
2133
- }
2134
- },
2135
- {
2136
- method: "PUT",
2137
- path: "/content-types/:uid",
2138
- handler: "content-types.updateContentType",
2139
- config: {
2140
- policies: [
2141
- {
2142
- name: "admin::hasPermissions",
2143
- config: { actions: ["plugin::content-type-builder.read"] }
2144
- }
2145
- ]
2146
- }
2147
- },
2148
- {
2149
- method: "DELETE",
2150
- path: "/content-types/:uid",
2151
- handler: "content-types.deleteContentType",
2152
- config: {
2153
- policies: [
2154
- {
2155
- name: "admin::hasPermissions",
2156
- config: { actions: ["plugin::content-type-builder.read"] }
2157
- }
2158
- ]
2159
- }
2160
- },
2161
- {
2162
- method: "GET",
2163
- path: "/components",
2164
- handler: "components.getComponents",
2165
- config: {
2166
- policies: [
2167
- {
2168
- name: "admin::hasPermissions",
2169
- config: { actions: ["plugin::content-type-builder.read"] }
2170
- }
2171
- ]
2172
- }
2173
- },
2174
- {
2175
- method: "GET",
2176
- path: "/components/:uid",
2177
- handler: "components.getComponent",
2178
- config: {
2179
- policies: [
2180
- {
2181
- name: "admin::hasPermissions",
2182
- config: { actions: ["plugin::content-type-builder.read"] }
2183
- }
2184
- ]
2185
- }
2186
- },
2187
- {
2188
- method: "POST",
2189
- path: "/components",
2190
- handler: "components.createComponent",
2191
- config: {
2192
- policies: [
2193
- {
2194
- name: "admin::hasPermissions",
2195
- config: { actions: ["plugin::content-type-builder.read"] }
2196
- }
2197
- ]
2198
- }
2199
- },
2200
- {
2201
- method: "PUT",
2202
- path: "/components/:uid",
2203
- handler: "components.updateComponent",
2204
- config: {
2205
- policies: [
2206
- {
2207
- name: "admin::hasPermissions",
2208
- config: { actions: ["plugin::content-type-builder.read"] }
2209
- }
2210
- ]
2211
- }
2212
- },
2213
- {
2214
- method: "DELETE",
2215
- path: "/components/:uid",
2216
- handler: "components.deleteComponent",
2217
- config: {
2218
- policies: [
2219
- {
2220
- name: "admin::hasPermissions",
2221
- config: { actions: ["plugin::content-type-builder.read"] }
2222
- }
2223
- ]
2224
- }
2225
- },
2226
- {
2227
- method: "PUT",
2228
- path: "/component-categories/:name",
2229
- handler: "component-categories.editCategory",
2230
- config: {
2231
- policies: [
2232
- {
2233
- name: "admin::hasPermissions",
2234
- config: { actions: ["plugin::content-type-builder.read"] }
2235
- }
2236
- ]
2237
- }
2189
+ const nameIsAvailable = (isEdition)=>{
2190
+ // TODO TS: if strapi.contentTypes (ie, ContentTypes) works as an ArrayLike and is used like this, we may want to ensure it is typed so that it can be without using as
2191
+ const usedNames = flatMap((ct)=>{
2192
+ return [
2193
+ ct.info?.singularName,
2194
+ ct.info?.pluralName
2195
+ ];
2196
+ })(strapi.contentTypes);
2197
+ return {
2198
+ name: 'nameAlreadyUsed',
2199
+ message: 'contentType: name `${value}` is already being used by another content type.',
2200
+ test (value) {
2201
+ // don't check on edition
2202
+ if (isEdition) return true;
2203
+ // ignore if not a string (will be caught in another validator)
2204
+ if (typeof value !== 'string') {
2205
+ return true;
2206
+ }
2207
+ // compare snake case to check the actual column names that will be used in the database
2208
+ return usedNames.every((usedName)=>snakeCase(usedName) !== snakeCase(value));
2209
+ }
2210
+ };
2211
+ };
2212
+ const nameIsNotExistingCollectionName = (isEdition)=>{
2213
+ const usedNames = Object.keys(strapi.contentTypes).map((key)=>strapi.contentTypes[key].collectionName);
2214
+ return {
2215
+ name: 'nameAlreadyUsed',
2216
+ message: 'contentType: name `${value}` is already being used by another content type.',
2217
+ test (value) {
2218
+ // don't check on edition
2219
+ if (isEdition) return true;
2220
+ // ignore if not a string (will be caught in another validator)
2221
+ if (typeof value !== 'string') {
2222
+ return true;
2223
+ }
2224
+ // compare snake case to check the actual column names that will be used in the database
2225
+ return usedNames.every((usedName)=>snakeCase(usedName) !== snakeCase(value));
2226
+ }
2227
+ };
2228
+ };
2229
+ /**
2230
+ * Validates type kind
2231
+ */ const kindSchema = yup.string().oneOf([
2232
+ typeKinds.SINGLE_TYPE,
2233
+ typeKinds.COLLECTION_TYPE
2234
+ ]);
2235
+ const validateKind = validateYupSchema(kindSchema);
2236
+
2237
+ var contentTypes = {
2238
+ async getContentTypes (ctx) {
2239
+ const { kind } = ctx.query;
2240
+ try {
2241
+ await validateKind(kind);
2242
+ } catch (error) {
2243
+ return ctx.send({
2244
+ error
2245
+ }, 400);
2246
+ }
2247
+ const contentTypeService = getService('content-types');
2248
+ const contentTypes = Object.keys(strapi.contentTypes).filter((uid)=>!kind || _.get(strapi.contentTypes[uid], 'kind', 'collectionType') === kind).map((uid)=>contentTypeService.formatContentType(strapi.contentTypes[uid]));
2249
+ ctx.send({
2250
+ data: contentTypes
2251
+ });
2238
2252
  },
2239
- {
2240
- method: "DELETE",
2241
- path: "/component-categories/:name",
2242
- handler: "component-categories.deleteCategory",
2243
- config: {
2244
- policies: [
2245
- {
2246
- name: "admin::hasPermissions",
2247
- config: { actions: ["plugin::content-type-builder.read"] }
2248
- }
2249
- ]
2250
- }
2251
- }
2252
- ]
2253
- };
2254
- const contentApi = {
2255
- type: "content-api",
2256
- routes: [
2257
- {
2258
- method: "GET",
2259
- path: "/content-types",
2260
- handler: "content-types.getContentTypes"
2253
+ getContentType (ctx) {
2254
+ const { uid } = ctx.params;
2255
+ const contentType = strapi.contentTypes[uid];
2256
+ if (!contentType) {
2257
+ return ctx.send({
2258
+ error: 'contentType.notFound'
2259
+ }, 404);
2260
+ }
2261
+ const contentTypeService = getService('content-types');
2262
+ ctx.send({
2263
+ data: contentTypeService.formatContentType(contentType)
2264
+ });
2261
2265
  },
2262
- {
2263
- method: "GET",
2264
- path: "/content-types/:uid",
2265
- handler: "content-types.getContentType"
2266
+ async createContentType (ctx) {
2267
+ const body = ctx.request.body;
2268
+ try {
2269
+ await validateContentTypeInput(body);
2270
+ } catch (error) {
2271
+ return ctx.send({
2272
+ error
2273
+ }, 400);
2274
+ }
2275
+ try {
2276
+ strapi.reload.isWatching = false;
2277
+ const contentTypeService = getService('content-types');
2278
+ const contentType = await contentTypeService.createContentType({
2279
+ contentType: body.contentType,
2280
+ components: body.components
2281
+ });
2282
+ const metricsPayload = {
2283
+ eventProperties: {
2284
+ kind: contentType.kind
2285
+ }
2286
+ };
2287
+ if (_.isEmpty(strapi.apis)) {
2288
+ await strapi.telemetry.send('didCreateFirstContentType', metricsPayload);
2289
+ } else {
2290
+ await strapi.telemetry.send('didCreateContentType', metricsPayload);
2291
+ }
2292
+ setImmediate(()=>strapi.reload());
2293
+ ctx.send({
2294
+ data: {
2295
+ uid: contentType.uid
2296
+ }
2297
+ }, 201);
2298
+ } catch (err) {
2299
+ strapi.log.error(err);
2300
+ await strapi.telemetry.send('didNotCreateContentType', {
2301
+ eventProperties: {
2302
+ error: err.message || err
2303
+ }
2304
+ });
2305
+ ctx.send({
2306
+ error: err.message || 'Unknown error'
2307
+ }, 400);
2308
+ }
2266
2309
  },
2267
- {
2268
- method: "GET",
2269
- path: "/components",
2270
- handler: "components.getComponents"
2310
+ async updateContentType (ctx) {
2311
+ const { uid } = ctx.params;
2312
+ const body = ctx.request.body;
2313
+ if (!_.has(strapi.contentTypes, uid)) {
2314
+ return ctx.send({
2315
+ error: 'contentType.notFound'
2316
+ }, 404);
2317
+ }
2318
+ try {
2319
+ await validateUpdateContentTypeInput(body);
2320
+ } catch (error) {
2321
+ return ctx.send({
2322
+ error
2323
+ }, 400);
2324
+ }
2325
+ try {
2326
+ strapi.reload.isWatching = false;
2327
+ const contentTypeService = getService('content-types');
2328
+ const component = await contentTypeService.editContentType(uid, {
2329
+ contentType: body.contentType,
2330
+ components: body.components
2331
+ });
2332
+ setImmediate(()=>strapi.reload());
2333
+ ctx.send({
2334
+ data: {
2335
+ uid: component.uid
2336
+ }
2337
+ }, 201);
2338
+ } catch (error) {
2339
+ strapi.log.error(error);
2340
+ ctx.send({
2341
+ error: error?.message || 'Unknown error'
2342
+ }, 400);
2343
+ }
2271
2344
  },
2272
- {
2273
- method: "GET",
2274
- path: "/components/:uid",
2275
- handler: "components.getComponent"
2345
+ async deleteContentType (ctx) {
2346
+ const { uid } = ctx.params;
2347
+ if (!_.has(strapi.contentTypes, uid)) {
2348
+ return ctx.send({
2349
+ error: 'contentType.notFound'
2350
+ }, 404);
2351
+ }
2352
+ try {
2353
+ strapi.reload.isWatching = false;
2354
+ const contentTypeService = getService('content-types');
2355
+ const component = await contentTypeService.deleteContentType(uid);
2356
+ setImmediate(()=>strapi.reload());
2357
+ ctx.send({
2358
+ data: {
2359
+ uid: component.uid
2360
+ }
2361
+ });
2362
+ } catch (error) {
2363
+ strapi.log.error(error);
2364
+ ctx.send({
2365
+ error: error?.message || 'Unknown error'
2366
+ }, 400);
2367
+ }
2276
2368
  }
2277
- ]
2278
- };
2279
- const routes = {
2280
- admin,
2281
- "content-api": contentApi
2282
- };
2283
- const index = () => ({
2284
- config,
2285
- bootstrap,
2286
- services,
2287
- controllers: exportObject,
2288
- routes
2289
- });
2290
- export {
2291
- index as default
2292
2369
  };
2370
+
2371
+ const exportObject = {
2372
+ builder,
2373
+ 'component-categories': componentCategories,
2374
+ components,
2375
+ 'content-types': contentTypes
2376
+ };
2377
+
2378
+ var admin = {
2379
+ type: 'admin',
2380
+ routes: [
2381
+ {
2382
+ method: 'GET',
2383
+ path: '/reserved-names',
2384
+ handler: 'builder.getReservedNames',
2385
+ config: {
2386
+ policies: [
2387
+ {
2388
+ name: 'admin::hasPermissions',
2389
+ config: {
2390
+ actions: [
2391
+ 'plugin::content-type-builder.read'
2392
+ ]
2393
+ }
2394
+ }
2395
+ ]
2396
+ }
2397
+ },
2398
+ {
2399
+ method: 'GET',
2400
+ path: '/content-types',
2401
+ handler: 'content-types.getContentTypes',
2402
+ config: {
2403
+ policies: [
2404
+ {
2405
+ name: 'admin::hasPermissions',
2406
+ config: {
2407
+ actions: [
2408
+ 'plugin::content-type-builder.read'
2409
+ ]
2410
+ }
2411
+ }
2412
+ ]
2413
+ }
2414
+ },
2415
+ {
2416
+ method: 'GET',
2417
+ path: '/content-types/:uid',
2418
+ handler: 'content-types.getContentType',
2419
+ config: {
2420
+ policies: [
2421
+ {
2422
+ name: 'admin::hasPermissions',
2423
+ config: {
2424
+ actions: [
2425
+ 'plugin::content-type-builder.read'
2426
+ ]
2427
+ }
2428
+ }
2429
+ ]
2430
+ }
2431
+ },
2432
+ {
2433
+ method: 'POST',
2434
+ path: '/content-types',
2435
+ handler: 'content-types.createContentType',
2436
+ config: {
2437
+ policies: [
2438
+ {
2439
+ name: 'admin::hasPermissions',
2440
+ config: {
2441
+ actions: [
2442
+ 'plugin::content-type-builder.read'
2443
+ ]
2444
+ }
2445
+ }
2446
+ ]
2447
+ }
2448
+ },
2449
+ {
2450
+ method: 'PUT',
2451
+ path: '/content-types/:uid',
2452
+ handler: 'content-types.updateContentType',
2453
+ config: {
2454
+ policies: [
2455
+ {
2456
+ name: 'admin::hasPermissions',
2457
+ config: {
2458
+ actions: [
2459
+ 'plugin::content-type-builder.read'
2460
+ ]
2461
+ }
2462
+ }
2463
+ ]
2464
+ }
2465
+ },
2466
+ {
2467
+ method: 'DELETE',
2468
+ path: '/content-types/:uid',
2469
+ handler: 'content-types.deleteContentType',
2470
+ config: {
2471
+ policies: [
2472
+ {
2473
+ name: 'admin::hasPermissions',
2474
+ config: {
2475
+ actions: [
2476
+ 'plugin::content-type-builder.read'
2477
+ ]
2478
+ }
2479
+ }
2480
+ ]
2481
+ }
2482
+ },
2483
+ {
2484
+ method: 'GET',
2485
+ path: '/components',
2486
+ handler: 'components.getComponents',
2487
+ config: {
2488
+ policies: [
2489
+ {
2490
+ name: 'admin::hasPermissions',
2491
+ config: {
2492
+ actions: [
2493
+ 'plugin::content-type-builder.read'
2494
+ ]
2495
+ }
2496
+ }
2497
+ ]
2498
+ }
2499
+ },
2500
+ {
2501
+ method: 'GET',
2502
+ path: '/components/:uid',
2503
+ handler: 'components.getComponent',
2504
+ config: {
2505
+ policies: [
2506
+ {
2507
+ name: 'admin::hasPermissions',
2508
+ config: {
2509
+ actions: [
2510
+ 'plugin::content-type-builder.read'
2511
+ ]
2512
+ }
2513
+ }
2514
+ ]
2515
+ }
2516
+ },
2517
+ {
2518
+ method: 'POST',
2519
+ path: '/components',
2520
+ handler: 'components.createComponent',
2521
+ config: {
2522
+ policies: [
2523
+ {
2524
+ name: 'admin::hasPermissions',
2525
+ config: {
2526
+ actions: [
2527
+ 'plugin::content-type-builder.read'
2528
+ ]
2529
+ }
2530
+ }
2531
+ ]
2532
+ }
2533
+ },
2534
+ {
2535
+ method: 'PUT',
2536
+ path: '/components/:uid',
2537
+ handler: 'components.updateComponent',
2538
+ config: {
2539
+ policies: [
2540
+ {
2541
+ name: 'admin::hasPermissions',
2542
+ config: {
2543
+ actions: [
2544
+ 'plugin::content-type-builder.read'
2545
+ ]
2546
+ }
2547
+ }
2548
+ ]
2549
+ }
2550
+ },
2551
+ {
2552
+ method: 'DELETE',
2553
+ path: '/components/:uid',
2554
+ handler: 'components.deleteComponent',
2555
+ config: {
2556
+ policies: [
2557
+ {
2558
+ name: 'admin::hasPermissions',
2559
+ config: {
2560
+ actions: [
2561
+ 'plugin::content-type-builder.read'
2562
+ ]
2563
+ }
2564
+ }
2565
+ ]
2566
+ }
2567
+ },
2568
+ {
2569
+ method: 'PUT',
2570
+ path: '/component-categories/:name',
2571
+ handler: 'component-categories.editCategory',
2572
+ config: {
2573
+ policies: [
2574
+ {
2575
+ name: 'admin::hasPermissions',
2576
+ config: {
2577
+ actions: [
2578
+ 'plugin::content-type-builder.read'
2579
+ ]
2580
+ }
2581
+ }
2582
+ ]
2583
+ }
2584
+ },
2585
+ {
2586
+ method: 'DELETE',
2587
+ path: '/component-categories/:name',
2588
+ handler: 'component-categories.deleteCategory',
2589
+ config: {
2590
+ policies: [
2591
+ {
2592
+ name: 'admin::hasPermissions',
2593
+ config: {
2594
+ actions: [
2595
+ 'plugin::content-type-builder.read'
2596
+ ]
2597
+ }
2598
+ }
2599
+ ]
2600
+ }
2601
+ }
2602
+ ]
2603
+ };
2604
+
2605
+ var contentApi = {
2606
+ type: 'content-api',
2607
+ routes: [
2608
+ {
2609
+ method: 'GET',
2610
+ path: '/content-types',
2611
+ handler: 'content-types.getContentTypes'
2612
+ },
2613
+ {
2614
+ method: 'GET',
2615
+ path: '/content-types/:uid',
2616
+ handler: 'content-types.getContentType'
2617
+ },
2618
+ {
2619
+ method: 'GET',
2620
+ path: '/components',
2621
+ handler: 'components.getComponents'
2622
+ },
2623
+ {
2624
+ method: 'GET',
2625
+ path: '/components/:uid',
2626
+ handler: 'components.getComponent'
2627
+ }
2628
+ ]
2629
+ };
2630
+
2631
+ var routes = {
2632
+ admin,
2633
+ 'content-api': contentApi
2634
+ };
2635
+
2636
+ // eslint-disable-next-line import/no-extraneous-dependencies
2637
+ var index = (()=>({
2638
+ config,
2639
+ bootstrap,
2640
+ services,
2641
+ controllers: exportObject,
2642
+ routes
2643
+ }));
2644
+
2645
+ export { index as default };
2293
2646
  //# sourceMappingURL=index.mjs.map