@strapi/strapi 4.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +144 -0
- package/bin/strapi.js +186 -0
- package/lib/Strapi.js +470 -0
- package/lib/commands/admin-reset.js +51 -0
- package/lib/commands/build.js +56 -0
- package/lib/commands/configurationDump.js +50 -0
- package/lib/commands/configurationRestore.js +169 -0
- package/lib/commands/console.js +26 -0
- package/lib/commands/develop.js +157 -0
- package/lib/commands/generate-template.js +97 -0
- package/lib/commands/install.js +48 -0
- package/lib/commands/new.js +11 -0
- package/lib/commands/start.js +8 -0
- package/lib/commands/uninstall.js +68 -0
- package/lib/commands/watchAdmin.js +45 -0
- package/lib/container.js +45 -0
- package/lib/core/app-configuration/config-loader.js +20 -0
- package/lib/core/app-configuration/index.js +75 -0
- package/lib/core/app-configuration/load-config-file.js +43 -0
- package/lib/core/app-configuration/load-functions.js +28 -0
- package/lib/core/bootstrap.js +60 -0
- package/lib/core/domain/component/index.js +24 -0
- package/lib/core/domain/component/validator.js +29 -0
- package/lib/core/domain/content-type/index.js +140 -0
- package/lib/core/domain/content-type/validator.js +64 -0
- package/lib/core/domain/module/index.js +106 -0
- package/lib/core/domain/module/validation.js +36 -0
- package/lib/core/loaders/admin.js +16 -0
- package/lib/core/loaders/apis.js +157 -0
- package/lib/core/loaders/components.js +41 -0
- package/lib/core/loaders/index.js +11 -0
- package/lib/core/loaders/middlewares.js +86 -0
- package/lib/core/loaders/plugins/get-enabled-plugins.js +100 -0
- package/lib/core/loaders/plugins/index.js +109 -0
- package/lib/core/loaders/policies.js +28 -0
- package/lib/core/loaders/src-index.js +38 -0
- package/lib/core/registries/apis.js +43 -0
- package/lib/core/registries/config.js +21 -0
- package/lib/core/registries/content-types.js +53 -0
- package/lib/core/registries/controllers.js +43 -0
- package/lib/core/registries/hooks.js +37 -0
- package/lib/core/registries/middlewares.js +30 -0
- package/lib/core/registries/modules.js +44 -0
- package/lib/core/registries/plugins.js +28 -0
- package/lib/core/registries/policies.js +38 -0
- package/lib/core/registries/services.js +58 -0
- package/lib/core/utils.js +35 -0
- package/lib/core-api/controller/collection-type.js +84 -0
- package/lib/core-api/controller/index.js +26 -0
- package/lib/core-api/controller/single-type.js +44 -0
- package/lib/core-api/controller/transform.js +97 -0
- package/lib/core-api/index.js +39 -0
- package/lib/core-api/service/collection-type.js +84 -0
- package/lib/core-api/service/index.js +55 -0
- package/lib/core-api/service/pagination.js +125 -0
- package/lib/core-api/service/single-type.js +58 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +3 -0
- package/lib/load/filepath-to-prop-path.js +22 -0
- package/lib/load/glob.js +15 -0
- package/lib/load/index.js +9 -0
- package/lib/load/load-files.js +56 -0
- package/lib/load/package-path.js +9 -0
- package/lib/middlewares/cors/index.js +66 -0
- package/lib/middlewares/error/defaults.json +5 -0
- package/lib/middlewares/error/index.js +147 -0
- package/lib/middlewares/favicon/defaults.json +7 -0
- package/lib/middlewares/favicon/index.js +31 -0
- package/lib/middlewares/gzip/defaults.json +6 -0
- package/lib/middlewares/gzip/index.js +19 -0
- package/lib/middlewares/helmet/defaults.json +18 -0
- package/lib/middlewares/helmet/index.js +9 -0
- package/lib/middlewares/index.js +120 -0
- package/lib/middlewares/ip/defaults.json +7 -0
- package/lib/middlewares/ip/index.js +25 -0
- package/lib/middlewares/logger/defaults.json +5 -0
- package/lib/middlewares/logger/index.js +37 -0
- package/lib/middlewares/parser/defaults.json +11 -0
- package/lib/middlewares/parser/index.js +75 -0
- package/lib/middlewares/poweredBy/defaults.json +5 -0
- package/lib/middlewares/poweredBy/index.js +16 -0
- package/lib/middlewares/public/assets/images/group_people_1.png +0 -0
- package/lib/middlewares/public/assets/images/group_people_2.png +0 -0
- package/lib/middlewares/public/assets/images/group_people_3.png +0 -0
- package/lib/middlewares/public/assets/images/logo_login.png +0 -0
- package/lib/middlewares/public/defaults.json +8 -0
- package/lib/middlewares/public/index.html +66 -0
- package/lib/middlewares/public/index.js +130 -0
- package/lib/middlewares/public/serve-static.js +23 -0
- package/lib/middlewares/responseTime/defaults.json +5 -0
- package/lib/middlewares/responseTime/index.js +25 -0
- package/lib/middlewares/responses/defaults.json +5 -0
- package/lib/middlewares/responses/index.js +19 -0
- package/lib/middlewares/router/defaults.json +7 -0
- package/lib/middlewares/router/index.js +97 -0
- package/lib/middlewares/session/defaults.json +18 -0
- package/lib/middlewares/session/index.js +140 -0
- package/lib/migrations/draft-publish.js +57 -0
- package/lib/services/auth/index.js +92 -0
- package/lib/services/core-store.js +145 -0
- package/lib/services/cron.js +54 -0
- package/lib/services/entity-service/components.js +365 -0
- package/lib/services/entity-service/index.d.ts +91 -0
- package/lib/services/entity-service/index.js +244 -0
- package/lib/services/entity-service/params.js +145 -0
- package/lib/services/entity-validator/index.js +187 -0
- package/lib/services/entity-validator/validators.js +123 -0
- package/lib/services/event-hub.js +15 -0
- package/lib/services/fs.js +58 -0
- package/lib/services/metrics/index.js +104 -0
- package/lib/services/metrics/is-truthy.js +9 -0
- package/lib/services/metrics/middleware.js +33 -0
- package/lib/services/metrics/rate-limiter.js +27 -0
- package/lib/services/metrics/sender.js +76 -0
- package/lib/services/metrics/stringify-deep.js +22 -0
- package/lib/services/server/admin-api.js +14 -0
- package/lib/services/server/api.js +32 -0
- package/lib/services/server/compose-endpoint.js +112 -0
- package/lib/services/server/content-api.js +16 -0
- package/lib/services/server/http-server.js +64 -0
- package/lib/services/server/index.js +108 -0
- package/lib/services/server/middleware.js +28 -0
- package/lib/services/server/policy.js +34 -0
- package/lib/services/server/routing.js +107 -0
- package/lib/services/utils/upload-files.js +79 -0
- package/lib/services/webhook-runner.js +155 -0
- package/lib/services/webhook-store.js +91 -0
- package/lib/services/worker-queue.js +58 -0
- package/lib/utils/addSlash.js +10 -0
- package/lib/utils/ee.js +123 -0
- package/lib/utils/get-dirs.js +15 -0
- package/lib/utils/get-prefixed-dependencies.js +7 -0
- package/lib/utils/index.js +11 -0
- package/lib/utils/is-initialized.js +23 -0
- package/lib/utils/open-browser.js +12 -0
- package/lib/utils/resources/key.pub +9 -0
- package/lib/utils/run-checks.js +22 -0
- package/lib/utils/startup-logger.js +90 -0
- package/lib/utils/success.js +31 -0
- package/lib/utils/update-notifier/index.js +97 -0
- package/lib/utils/url-from-segments.js +12 -0
- package/package.json +133 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const { has, prop, omit, toString } = require('lodash/fp');
|
|
5
|
+
|
|
6
|
+
const { contentTypes: contentTypesUtils } = require('@strapi/utils');
|
|
7
|
+
|
|
8
|
+
const omitComponentData = (contentType, data) => {
|
|
9
|
+
const { attributes } = contentType;
|
|
10
|
+
const componentAttributes = Object.keys(attributes).filter(attributeName =>
|
|
11
|
+
contentTypesUtils.isComponentAttribute(attributes[attributeName])
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
return omit(componentAttributes, data);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// NOTE: we could generalize the logic to allow CRUD of relation directly in the DB layer
|
|
18
|
+
const createComponents = async (uid, data) => {
|
|
19
|
+
const { attributes } = strapi.getModel(uid);
|
|
20
|
+
|
|
21
|
+
const componentBody = {};
|
|
22
|
+
|
|
23
|
+
for (const attributeName in attributes) {
|
|
24
|
+
const attribute = attributes[attributeName];
|
|
25
|
+
|
|
26
|
+
if (!has(attributeName, data) || !contentTypesUtils.isComponentAttribute(attribute)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (attribute.type === 'component') {
|
|
31
|
+
const { component: componentUID, repeatable = false } = attribute;
|
|
32
|
+
|
|
33
|
+
const componentValue = data[attributeName];
|
|
34
|
+
|
|
35
|
+
if (componentValue === null) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (repeatable === true) {
|
|
40
|
+
if (!Array.isArray(componentValue)) {
|
|
41
|
+
throw new Error('Expected an array to create repeatable component');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const components = await Promise.all(
|
|
45
|
+
componentValue.map(value => createComponent(componentUID, value))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// TODO: add order
|
|
49
|
+
componentBody[attributeName] = components.map(({ id }, idx) => {
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
__pivot: {
|
|
53
|
+
order: idx + 1,
|
|
54
|
+
field: attributeName,
|
|
55
|
+
component_type: componentUID,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
const component = await createComponent(componentUID, componentValue);
|
|
61
|
+
componentBody[attributeName] = {
|
|
62
|
+
id: component.id,
|
|
63
|
+
__pivot: {
|
|
64
|
+
order: 1,
|
|
65
|
+
field: attributeName,
|
|
66
|
+
component_type: componentUID,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (attribute.type === 'dynamiczone') {
|
|
75
|
+
const dynamiczoneValues = data[attributeName];
|
|
76
|
+
|
|
77
|
+
if (!Array.isArray(dynamiczoneValues)) {
|
|
78
|
+
throw new Error('Expected an array to create repeatable component');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
componentBody[attributeName] = await Promise.all(
|
|
82
|
+
dynamiczoneValues.map(async (value, idx) => {
|
|
83
|
+
const { id } = await createComponent(value.__component, value);
|
|
84
|
+
return {
|
|
85
|
+
id,
|
|
86
|
+
__component: value.__component,
|
|
87
|
+
__pivot: {
|
|
88
|
+
order: idx + 1,
|
|
89
|
+
field: attributeName,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return componentBody;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
delete old components
|
|
104
|
+
create or update
|
|
105
|
+
*/
|
|
106
|
+
const updateComponents = async (uid, entityToUpdate, data) => {
|
|
107
|
+
const { attributes } = strapi.getModel(uid);
|
|
108
|
+
|
|
109
|
+
const componentBody = {};
|
|
110
|
+
|
|
111
|
+
for (const attributeName in attributes) {
|
|
112
|
+
const attribute = attributes[attributeName];
|
|
113
|
+
|
|
114
|
+
if (!has(attributeName, data)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (attribute.type === 'component') {
|
|
119
|
+
const { component: componentUID, repeatable = false } = attribute;
|
|
120
|
+
|
|
121
|
+
const componentValue = data[attributeName];
|
|
122
|
+
|
|
123
|
+
await deleteOldComponents(uid, componentUID, entityToUpdate, attributeName, componentValue);
|
|
124
|
+
|
|
125
|
+
if (repeatable === true) {
|
|
126
|
+
if (!Array.isArray(componentValue)) {
|
|
127
|
+
throw new Error('Expected an array to create repeatable component');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const components = await Promise.all(
|
|
131
|
+
componentValue.map(value => updateOrCreateComponent(componentUID, value))
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }, idx) => {
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
__pivot: {
|
|
138
|
+
order: idx + 1,
|
|
139
|
+
field: attributeName,
|
|
140
|
+
component_type: componentUID,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
const component = await updateOrCreateComponent(componentUID, componentValue);
|
|
146
|
+
componentBody[attributeName] = component && {
|
|
147
|
+
id: component.id,
|
|
148
|
+
__pivot: {
|
|
149
|
+
order: 1,
|
|
150
|
+
field: attributeName,
|
|
151
|
+
component_type: componentUID,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (attribute.type === 'dynamiczone') {
|
|
160
|
+
const dynamiczoneValues = data[attributeName];
|
|
161
|
+
|
|
162
|
+
await deleteOldDZComponents(uid, entityToUpdate, attributeName, dynamiczoneValues);
|
|
163
|
+
|
|
164
|
+
if (!Array.isArray(dynamiczoneValues)) {
|
|
165
|
+
throw new Error('Expected an array to create repeatable component');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
componentBody[attributeName] = await Promise.all(
|
|
169
|
+
dynamiczoneValues.map(async (value, idx) => {
|
|
170
|
+
const { id } = await updateOrCreateComponent(value.__component, value);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
id,
|
|
174
|
+
__component: value.__component,
|
|
175
|
+
__pivot: {
|
|
176
|
+
order: idx + 1,
|
|
177
|
+
field: attributeName,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return componentBody;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const deleteOldComponents = async (
|
|
191
|
+
uid,
|
|
192
|
+
componentUID,
|
|
193
|
+
entityToUpdate,
|
|
194
|
+
attributeName,
|
|
195
|
+
componentValue
|
|
196
|
+
) => {
|
|
197
|
+
const previousValue = await strapi.query(uid).load(entityToUpdate, attributeName);
|
|
198
|
+
|
|
199
|
+
const idsToKeep = _.castArray(componentValue)
|
|
200
|
+
.filter(has('id'))
|
|
201
|
+
.map(prop('id'))
|
|
202
|
+
.map(toString);
|
|
203
|
+
|
|
204
|
+
const allIds = _.castArray(previousValue)
|
|
205
|
+
.filter(has('id'))
|
|
206
|
+
.map(prop('id'))
|
|
207
|
+
.map(toString);
|
|
208
|
+
|
|
209
|
+
idsToKeep.forEach(id => {
|
|
210
|
+
if (!allIds.includes(id)) {
|
|
211
|
+
const err = new Error(
|
|
212
|
+
`Some of the provided components in ${attributeName} are not related to the entity`
|
|
213
|
+
);
|
|
214
|
+
err.status = 400;
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const idsToDelete = _.difference(allIds, idsToKeep);
|
|
220
|
+
|
|
221
|
+
if (idsToDelete.length > 0) {
|
|
222
|
+
for (const idToDelete of idsToDelete) {
|
|
223
|
+
await deleteComponent(componentUID, { id: idToDelete });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const deleteOldDZComponents = async (uid, entityToUpdate, attributeName, dynamiczoneValues) => {
|
|
229
|
+
const previousValue = await strapi.query(uid).load(entityToUpdate, attributeName);
|
|
230
|
+
|
|
231
|
+
const idsToKeep = _.castArray(dynamiczoneValues)
|
|
232
|
+
.filter(has('id'))
|
|
233
|
+
.map(({ id, __component }) => ({
|
|
234
|
+
id: toString(id),
|
|
235
|
+
__component,
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
const allIds = _.castArray(previousValue)
|
|
239
|
+
.filter(has('id'))
|
|
240
|
+
.map(({ id, __component }) => ({
|
|
241
|
+
id: toString(id),
|
|
242
|
+
__component,
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
idsToKeep.forEach(({ id, __component }) => {
|
|
246
|
+
if (!allIds.find(el => el.id === id && el.__component === __component)) {
|
|
247
|
+
const err = new Error(
|
|
248
|
+
`Some of the provided components in ${attributeName} are not related to the entity`
|
|
249
|
+
);
|
|
250
|
+
err.status = 400;
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const idsToDelete = allIds.reduce((acc, { id, __component }) => {
|
|
256
|
+
if (!idsToKeep.find(el => el.id === id && el.__component === __component)) {
|
|
257
|
+
acc.push({ id, __component });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return acc;
|
|
261
|
+
}, []);
|
|
262
|
+
|
|
263
|
+
if (idsToDelete.length > 0) {
|
|
264
|
+
for (const idToDelete of idsToDelete) {
|
|
265
|
+
const { id, __component } = idToDelete;
|
|
266
|
+
await deleteComponent(__component, { id });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const deleteComponents = async (uid, entityToDelete) => {
|
|
272
|
+
const { attributes } = strapi.getModel(uid);
|
|
273
|
+
|
|
274
|
+
for (const attributeName in attributes) {
|
|
275
|
+
const attribute = attributes[attributeName];
|
|
276
|
+
|
|
277
|
+
if (attribute.type === 'component') {
|
|
278
|
+
const { component: componentUID } = attribute;
|
|
279
|
+
|
|
280
|
+
const value = await strapi.query(uid).load(entityToDelete, attributeName);
|
|
281
|
+
|
|
282
|
+
if (!value) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (Array.isArray(value)) {
|
|
287
|
+
await Promise.all(value.map(subValue => deleteComponent(componentUID, subValue)));
|
|
288
|
+
} else {
|
|
289
|
+
await deleteComponent(componentUID, value);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (attribute.type === 'dynamiczone') {
|
|
296
|
+
const value = await strapi.query(uid).load(entityToDelete, attributeName);
|
|
297
|
+
|
|
298
|
+
if (!value) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (Array.isArray(value)) {
|
|
303
|
+
await Promise.all(value.map(subValue => deleteComponent(subValue.__component, subValue)));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/***************************
|
|
312
|
+
Component queries
|
|
313
|
+
***************************/
|
|
314
|
+
|
|
315
|
+
// components can have nested compos so this must be recursive
|
|
316
|
+
const createComponent = async (uid, data) => {
|
|
317
|
+
const model = strapi.getModel(uid);
|
|
318
|
+
|
|
319
|
+
const componentData = await createComponents(uid, data);
|
|
320
|
+
|
|
321
|
+
return strapi.query(uid).create({
|
|
322
|
+
data: Object.assign(omitComponentData(model, data), componentData),
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// components can have nested compos so this must be recursive
|
|
327
|
+
const updateComponent = async (uid, componentToUpdate, data) => {
|
|
328
|
+
const model = strapi.getModel(uid);
|
|
329
|
+
|
|
330
|
+
const componentData = await updateComponents(uid, componentToUpdate, data);
|
|
331
|
+
|
|
332
|
+
return strapi.query(uid).update({
|
|
333
|
+
where: {
|
|
334
|
+
id: componentToUpdate.id,
|
|
335
|
+
},
|
|
336
|
+
data: Object.assign(omitComponentData(model, data), componentData),
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const updateOrCreateComponent = (componentUID, value) => {
|
|
341
|
+
if (value === null) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// update
|
|
346
|
+
if (has('id', value)) {
|
|
347
|
+
// TODO: verify the compo is associated with the entity
|
|
348
|
+
return updateComponent(componentUID, { id: value.id }, value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// create
|
|
352
|
+
return createComponent(componentUID, value);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const deleteComponent = async (uid, componentToDelete) => {
|
|
356
|
+
await deleteComponents(uid, componentToDelete);
|
|
357
|
+
await strapi.query(uid).delete({ where: { id: componentToDelete.id } });
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
omitComponentData,
|
|
362
|
+
createComponents,
|
|
363
|
+
updateComponents,
|
|
364
|
+
deleteComponents,
|
|
365
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Database } from '@strapi/database';
|
|
2
|
+
import { Strapi } from '../../';
|
|
3
|
+
|
|
4
|
+
type ID = number | string;
|
|
5
|
+
|
|
6
|
+
type EntityServiceAction =
|
|
7
|
+
| 'findMany'
|
|
8
|
+
| 'findPage'
|
|
9
|
+
| 'findWithRelationCounts'
|
|
10
|
+
| 'findOne'
|
|
11
|
+
| 'count'
|
|
12
|
+
| 'create'
|
|
13
|
+
| 'update'
|
|
14
|
+
| 'delete';
|
|
15
|
+
|
|
16
|
+
type PaginationInfo = {
|
|
17
|
+
page: number;
|
|
18
|
+
pageSize: number;
|
|
19
|
+
pageCount: number;
|
|
20
|
+
total: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type Params<T> = {
|
|
24
|
+
fields?: (keyof T)[];
|
|
25
|
+
filters?: any;
|
|
26
|
+
_q?: string;
|
|
27
|
+
populate?: any;
|
|
28
|
+
sort?: any;
|
|
29
|
+
start?: number;
|
|
30
|
+
limit?: number;
|
|
31
|
+
page?: number;
|
|
32
|
+
pageSize?: number;
|
|
33
|
+
publicationState?: string;
|
|
34
|
+
data?: any;
|
|
35
|
+
files?: any;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface EntityService {
|
|
39
|
+
uploadFiles<K extends keyof AllTypes, T extends AllTypes[K]>(uid: K, entity, files);
|
|
40
|
+
wrapParams<K extends keyof AllTypes, T extends AllTypes[K]>(
|
|
41
|
+
params: Params<T>,
|
|
42
|
+
{ uid: K, action: EntityServiceAction }
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
findMany<K extends keyof AllTypes, T extends AllTypes[K]>(
|
|
46
|
+
uid: K,
|
|
47
|
+
params: Params<T>
|
|
48
|
+
): Promise<T[]>;
|
|
49
|
+
findPage<K extends keyof AllTypes, T extends AllTypes[K]>(
|
|
50
|
+
uid: K,
|
|
51
|
+
params: Params<T>
|
|
52
|
+
): Promise<{
|
|
53
|
+
results: T[];
|
|
54
|
+
pagination: PaginationInfo;
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
findWithRelationCounts<K extends keyof AllTypes, T extends AllTypes[K]>(
|
|
58
|
+
uid: K,
|
|
59
|
+
params: Params<T>
|
|
60
|
+
): Promise<{
|
|
61
|
+
results: T[];
|
|
62
|
+
pagination: PaginationInfo;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
findOne<K extends keyof AllTypes, T extends AllTypes[K]>(
|
|
66
|
+
uid: K,
|
|
67
|
+
entityId: ID,
|
|
68
|
+
params: Params<T>
|
|
69
|
+
): Promise<T>;
|
|
70
|
+
|
|
71
|
+
count<K extends keyof AllTypes, T extends AllTypes[K]>(uid: K, params: Params<T>): Promise<any>;
|
|
72
|
+
create<K extends keyof AllTypes, T extends AllTypes[K]>(uid: K, params: Params<T>): Promise<any>;
|
|
73
|
+
update<K extends keyof AllTypes, T extends AllTypes[K]>(
|
|
74
|
+
uid: K,
|
|
75
|
+
entityId: ID,
|
|
76
|
+
params: Params<T>
|
|
77
|
+
): Promise<any>;
|
|
78
|
+
delete<K extends keyof AllTypes, T extends AllTypes[K]>(
|
|
79
|
+
uid: K,
|
|
80
|
+
entityId: ID,
|
|
81
|
+
params: Params<T>
|
|
82
|
+
): Promise<any>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default function(opts: {
|
|
86
|
+
strapi: Strapi;
|
|
87
|
+
db: Database;
|
|
88
|
+
// TODO: define types
|
|
89
|
+
eventHub: any;
|
|
90
|
+
entityValidator: any;
|
|
91
|
+
}): EntityService;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const delegate = require('delegates');
|
|
4
|
+
const { pipe } = require('lodash/fp');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
sanitizeEntity,
|
|
8
|
+
webhook: webhookUtils,
|
|
9
|
+
contentTypes: contentTypesUtils,
|
|
10
|
+
} = require('@strapi/utils');
|
|
11
|
+
const uploadFiles = require('../utils/upload-files');
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
omitComponentData,
|
|
15
|
+
createComponents,
|
|
16
|
+
updateComponents,
|
|
17
|
+
deleteComponents,
|
|
18
|
+
} = require('./components');
|
|
19
|
+
const {
|
|
20
|
+
transformCommonParams,
|
|
21
|
+
transformPaginationParams,
|
|
22
|
+
transformParamsToQuery,
|
|
23
|
+
pickSelectionParams,
|
|
24
|
+
} = require('./params');
|
|
25
|
+
|
|
26
|
+
// TODO: those should be strapi events used by the webhooks not the other way arround
|
|
27
|
+
const { ENTRY_CREATE, ENTRY_UPDATE, ENTRY_DELETE } = webhookUtils.webhookEvents;
|
|
28
|
+
|
|
29
|
+
module.exports = ctx => {
|
|
30
|
+
const implementation = createDefaultImplementation(ctx);
|
|
31
|
+
|
|
32
|
+
const service = {
|
|
33
|
+
implementation,
|
|
34
|
+
decorate(decorator) {
|
|
35
|
+
if (typeof decorator !== 'function') {
|
|
36
|
+
throw new Error(`Decorator must be a function, received ${typeof decorator}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.implementation = Object.assign({}, this.implementation, decorator(this.implementation));
|
|
40
|
+
return this;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const delegator = delegate(service, 'implementation');
|
|
45
|
+
|
|
46
|
+
// delegate every method in implementation
|
|
47
|
+
Object.keys(service.implementation).forEach(key => delegator.method(key));
|
|
48
|
+
|
|
49
|
+
return service;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @type {import('.').default}
|
|
54
|
+
*/
|
|
55
|
+
const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator }) => ({
|
|
56
|
+
uploadFiles,
|
|
57
|
+
|
|
58
|
+
async wrapParams(options = {}) {
|
|
59
|
+
return options;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
emitEvent(uid, event, entity) {
|
|
63
|
+
const model = strapi.getModel(uid);
|
|
64
|
+
|
|
65
|
+
eventHub.emit(event, {
|
|
66
|
+
model: model.modelName,
|
|
67
|
+
entry: sanitizeEntity(entity, { model }),
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async findMany(uid, opts) {
|
|
72
|
+
const { kind } = strapi.getModel(uid);
|
|
73
|
+
|
|
74
|
+
const wrappedParams = await this.wrapParams(opts, { uid, action: 'findMany' });
|
|
75
|
+
|
|
76
|
+
const query = transformParamsToQuery(uid, wrappedParams);
|
|
77
|
+
|
|
78
|
+
if (kind === 'singleType') {
|
|
79
|
+
return db.query(uid).findOne(query);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return db.query(uid).findMany(query);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async findPage(uid, opts) {
|
|
86
|
+
const wrappedParams = await this.wrapParams(opts, { uid, action: 'findPage' });
|
|
87
|
+
|
|
88
|
+
const query = transformParamsToQuery(uid, wrappedParams);
|
|
89
|
+
|
|
90
|
+
return db.query(uid).findPage(query);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// TODO: streamline the logic based on the populate option
|
|
94
|
+
async findWithRelationCounts(uid, opts) {
|
|
95
|
+
const wrappedParams = await this.wrapParams(opts, { uid, action: 'findWithRelationCounts' });
|
|
96
|
+
|
|
97
|
+
const query = transformParamsToQuery(uid, wrappedParams);
|
|
98
|
+
|
|
99
|
+
const { results, pagination } = await db.query(uid).findPage(query);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
results,
|
|
103
|
+
pagination,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async findOne(uid, entityId, opts) {
|
|
108
|
+
const wrappedParams = await this.wrapParams(opts, { uid, action: 'findOne' });
|
|
109
|
+
|
|
110
|
+
const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams));
|
|
111
|
+
|
|
112
|
+
return db.query(uid).findOne({ ...query, where: { id: entityId } });
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async count(uid, opts) {
|
|
116
|
+
const wrappedParams = await this.wrapParams(opts, { uid, action: 'count' });
|
|
117
|
+
|
|
118
|
+
const query = transformParamsToQuery(uid, wrappedParams);
|
|
119
|
+
|
|
120
|
+
return db.query(uid).count(query);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async create(uid, opts) {
|
|
124
|
+
const wrappedParams = await this.wrapParams(opts, { uid, action: 'create' });
|
|
125
|
+
const { data, files } = wrappedParams;
|
|
126
|
+
|
|
127
|
+
const model = strapi.getModel(uid);
|
|
128
|
+
|
|
129
|
+
const isDraft = contentTypesUtils.isDraft(data, model);
|
|
130
|
+
const validData = await entityValidator.validateEntityCreation(model, data, { isDraft });
|
|
131
|
+
|
|
132
|
+
// select / populate
|
|
133
|
+
const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams));
|
|
134
|
+
|
|
135
|
+
// TODO: wrap into transaction
|
|
136
|
+
const componentData = await createComponents(uid, validData);
|
|
137
|
+
|
|
138
|
+
let entity = await db.query(uid).create({
|
|
139
|
+
...query,
|
|
140
|
+
data: Object.assign(omitComponentData(model, validData), componentData),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// TODO: upload the files then set the links in the entity like with compo to avoid making too many queries
|
|
144
|
+
// FIXME: upload in components
|
|
145
|
+
if (files && Object.keys(files).length > 0) {
|
|
146
|
+
await this.uploadFiles(uid, entity, files);
|
|
147
|
+
entity = await this.findOne(uid, entity.id, wrappedParams);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.emitEvent(uid, ENTRY_CREATE, entity);
|
|
151
|
+
|
|
152
|
+
return entity;
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
async update(uid, entityId, opts) {
|
|
156
|
+
const wrappedParams = await this.wrapParams(opts, { uid, action: 'update' });
|
|
157
|
+
const { data, files } = wrappedParams;
|
|
158
|
+
|
|
159
|
+
const model = strapi.getModel(uid);
|
|
160
|
+
|
|
161
|
+
const entityToUpdate = await db.query(uid).findOne({ where: { id: entityId } });
|
|
162
|
+
|
|
163
|
+
if (!entityToUpdate) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isDraft = contentTypesUtils.isDraft(entityToUpdate, model);
|
|
168
|
+
|
|
169
|
+
const validData = await entityValidator.validateEntityUpdate(model, data, {
|
|
170
|
+
isDraft,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams));
|
|
174
|
+
|
|
175
|
+
// TODO: wrap in transaction
|
|
176
|
+
const componentData = await updateComponents(uid, entityToUpdate, validData);
|
|
177
|
+
|
|
178
|
+
let entity = await db.query(uid).update({
|
|
179
|
+
...query,
|
|
180
|
+
where: { id: entityId },
|
|
181
|
+
data: Object.assign(omitComponentData(model, validData), componentData),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// TODO: upload the files then set the links in the entity like with compo to avoid making too many queries
|
|
185
|
+
// FIXME: upload in components
|
|
186
|
+
if (files && Object.keys(files).length > 0) {
|
|
187
|
+
await this.uploadFiles(uid, entity, files);
|
|
188
|
+
entity = await this.findOne(uid, entity.id, wrappedParams);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.emitEvent(uid, ENTRY_UPDATE, entity);
|
|
192
|
+
|
|
193
|
+
return entity;
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
async delete(uid, entityId, opts) {
|
|
197
|
+
const wrappedParams = await this.wrapParams(opts, { uid, action: 'delete' });
|
|
198
|
+
|
|
199
|
+
// select / populate
|
|
200
|
+
const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams));
|
|
201
|
+
|
|
202
|
+
const entityToDelete = await db.query(uid).findOne({
|
|
203
|
+
...query,
|
|
204
|
+
where: { id: entityId },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!entityToDelete) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await deleteComponents(uid, entityToDelete);
|
|
212
|
+
await db.query(uid).delete({ where: { id: entityToDelete.id } });
|
|
213
|
+
|
|
214
|
+
this.emitEvent(uid, ENTRY_DELETE, entityToDelete);
|
|
215
|
+
|
|
216
|
+
return entityToDelete;
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
// FIXME: used only for the CM to be removed
|
|
220
|
+
async deleteMany(uid, opts) {
|
|
221
|
+
const wrappedParams = await this.wrapParams(opts, { uid, action: 'delete' });
|
|
222
|
+
|
|
223
|
+
// select / populate
|
|
224
|
+
const query = transformParamsToQuery(uid, wrappedParams);
|
|
225
|
+
|
|
226
|
+
return db.query(uid).deleteMany(query);
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
load(uid, entity, field, params) {
|
|
230
|
+
const { attributes } = strapi.getModel(uid);
|
|
231
|
+
|
|
232
|
+
const attribute = attributes[field];
|
|
233
|
+
|
|
234
|
+
const loadParams =
|
|
235
|
+
attribute.type === 'relation'
|
|
236
|
+
? transformParamsToQuery(attribute.target, params)
|
|
237
|
+
: pipe(
|
|
238
|
+
transformCommonParams,
|
|
239
|
+
transformPaginationParams
|
|
240
|
+
)(params);
|
|
241
|
+
|
|
242
|
+
return db.query(uid).load(entity, field, loadParams);
|
|
243
|
+
},
|
|
244
|
+
});
|