@strapi/database 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/examples/connections.js +36 -0
- package/examples/data.sqlite +0 -0
- package/examples/docker-compose.yml +29 -0
- package/examples/index.js +73 -0
- package/examples/models.js +341 -0
- package/examples/typings.ts +17 -0
- package/lib/dialects/dialect.js +45 -0
- package/lib/dialects/index.js +28 -0
- package/lib/dialects/mysql/index.js +51 -0
- package/lib/dialects/mysql/schema-inspector.js +203 -0
- package/lib/dialects/postgresql/index.js +49 -0
- package/lib/dialects/postgresql/schema-inspector.js +229 -0
- package/lib/dialects/sqlite/index.js +74 -0
- package/lib/dialects/sqlite/schema-inspector.js +151 -0
- package/lib/entity-manager.js +886 -0
- package/lib/entity-repository.js +110 -0
- package/lib/errors.js +14 -0
- package/lib/fields.d.ts +9 -0
- package/lib/fields.js +232 -0
- package/lib/index.d.ts +146 -0
- package/lib/index.js +60 -0
- package/lib/lifecycles/index.d.ts +50 -0
- package/lib/lifecycles/index.js +66 -0
- package/lib/lifecycles/subscribers/index.d.ts +9 -0
- package/lib/lifecycles/subscribers/models-lifecycles.js +19 -0
- package/lib/lifecycles/subscribers/timestamps.js +65 -0
- package/lib/metadata/index.js +219 -0
- package/lib/metadata/relations.js +488 -0
- package/lib/migrations/index.d.ts +9 -0
- package/lib/migrations/index.js +69 -0
- package/lib/migrations/storage.js +49 -0
- package/lib/query/helpers/index.js +10 -0
- package/lib/query/helpers/join.js +95 -0
- package/lib/query/helpers/order-by.js +70 -0
- package/lib/query/helpers/populate.js +652 -0
- package/lib/query/helpers/search.js +84 -0
- package/lib/query/helpers/transform.js +84 -0
- package/lib/query/helpers/where.js +322 -0
- package/lib/query/index.js +7 -0
- package/lib/query/query-builder.js +348 -0
- package/lib/schema/__tests__/schema-diff.test.js +181 -0
- package/lib/schema/builder.js +352 -0
- package/lib/schema/diff.js +376 -0
- package/lib/schema/index.d.ts +49 -0
- package/lib/schema/index.js +95 -0
- package/lib/schema/schema.js +209 -0
- package/lib/schema/storage.js +75 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/index.js +34 -0
- package/lib/utils/content-types.js +41 -0
- package/package.json +39 -0
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash/fp');
|
|
4
|
+
const types = require('./types');
|
|
5
|
+
const { createField } = require('./fields');
|
|
6
|
+
const { createQueryBuilder } = require('./query');
|
|
7
|
+
const { createRepository } = require('./entity-repository');
|
|
8
|
+
const { isBidirectional, isOneToAny } = require('./metadata/relations');
|
|
9
|
+
|
|
10
|
+
const toId = value => value.id || value;
|
|
11
|
+
const toIds = value => _.castArray(value || []).map(toId);
|
|
12
|
+
|
|
13
|
+
const isValidId = value => _.isString(value) || _.isInteger(value);
|
|
14
|
+
const toAssocs = data => {
|
|
15
|
+
return _.castArray(data)
|
|
16
|
+
.filter(datum => !_.isNil(datum))
|
|
17
|
+
.map(datum => {
|
|
18
|
+
// if it is a string or an integer return an obj with id = to datum
|
|
19
|
+
if (isValidId(datum)) {
|
|
20
|
+
return { id: datum, __pivot: {} };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// if it is an object check it has at least a valid id
|
|
24
|
+
if (!_.has('id', datum) || !isValidId(datum.id)) {
|
|
25
|
+
throw new Error(`Invalid id, expected a string or integer, got ${datum}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return datum;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const processData = (metadata, data = {}, { withDefaults = false } = {}) => {
|
|
33
|
+
const { attributes } = metadata;
|
|
34
|
+
|
|
35
|
+
const obj = {};
|
|
36
|
+
|
|
37
|
+
for (const attributeName in attributes) {
|
|
38
|
+
const attribute = attributes[attributeName];
|
|
39
|
+
|
|
40
|
+
if (types.isScalar(attribute.type)) {
|
|
41
|
+
const field = createField(attribute);
|
|
42
|
+
|
|
43
|
+
if (_.isUndefined(data[attributeName])) {
|
|
44
|
+
if (!_.isUndefined(attribute.default) && withDefaults) {
|
|
45
|
+
if (typeof attribute.default === 'function') {
|
|
46
|
+
obj[attributeName] = attribute.default();
|
|
47
|
+
} else {
|
|
48
|
+
obj[attributeName] = attribute.default;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof field.validate === 'function' && data[attributeName] !== null) {
|
|
55
|
+
field.validate(data[attributeName]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const val = data[attributeName] === null ? null : field.toDB(data[attributeName]);
|
|
59
|
+
|
|
60
|
+
obj[attributeName] = val;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (types.isRelation(attribute.type)) {
|
|
64
|
+
// oneToOne & manyToOne
|
|
65
|
+
if (attribute.joinColumn && attribute.owner) {
|
|
66
|
+
const joinColumnName = attribute.joinColumn.name;
|
|
67
|
+
|
|
68
|
+
// allow setting to null
|
|
69
|
+
const attrValue = !_.isUndefined(data[attributeName])
|
|
70
|
+
? data[attributeName]
|
|
71
|
+
: data[joinColumnName];
|
|
72
|
+
|
|
73
|
+
if (!_.isUndefined(attrValue)) {
|
|
74
|
+
obj[joinColumnName] = attrValue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (attribute.morphColumn && attribute.owner) {
|
|
81
|
+
const { idColumn, typeColumn, typeField = '__type' } = attribute.morphColumn;
|
|
82
|
+
|
|
83
|
+
const value = data[attributeName];
|
|
84
|
+
|
|
85
|
+
if (value === null) {
|
|
86
|
+
Object.assign(obj, {
|
|
87
|
+
[idColumn.name]: null,
|
|
88
|
+
[typeColumn.name]: null,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!_.isUndefined(value)) {
|
|
95
|
+
if (!_.has('id', value) || !_.has(typeField, value)) {
|
|
96
|
+
throw new Error(`Expects properties ${typeField} an id to make a morph association`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Object.assign(obj, {
|
|
100
|
+
[idColumn.name]: value.id,
|
|
101
|
+
[typeColumn.name]: value[typeField],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return obj;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const createEntityManager = db => {
|
|
112
|
+
const repoMap = {};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
async findOne(uid, params) {
|
|
116
|
+
await db.lifecycles.run('beforeFindOne', uid, { params });
|
|
117
|
+
|
|
118
|
+
const result = await this.createQueryBuilder(uid)
|
|
119
|
+
.init(params)
|
|
120
|
+
.first()
|
|
121
|
+
.execute();
|
|
122
|
+
|
|
123
|
+
await db.lifecycles.run('afterFindOne', uid, { params, result });
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// should we name it findOne because people are used to it ?
|
|
129
|
+
async findMany(uid, params) {
|
|
130
|
+
await db.lifecycles.run('beforeFindMany', uid, { params });
|
|
131
|
+
|
|
132
|
+
const result = await this.createQueryBuilder(uid)
|
|
133
|
+
.init(params)
|
|
134
|
+
.execute();
|
|
135
|
+
|
|
136
|
+
await db.lifecycles.run('afterFindMany', uid, { params, result });
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async count(uid, params = {}) {
|
|
142
|
+
await db.lifecycles.run('beforeCount', uid, { params });
|
|
143
|
+
|
|
144
|
+
const res = await this.createQueryBuilder(uid)
|
|
145
|
+
.init(_.pick(['_q', 'where'], params))
|
|
146
|
+
.count()
|
|
147
|
+
.first()
|
|
148
|
+
.execute();
|
|
149
|
+
|
|
150
|
+
const result = Number(res.count);
|
|
151
|
+
|
|
152
|
+
await db.lifecycles.run('afterCount', uid, { params, result });
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async create(uid, params = {}) {
|
|
158
|
+
await db.lifecycles.run('beforeCreate', uid, { params });
|
|
159
|
+
|
|
160
|
+
const metadata = db.metadata.get(uid);
|
|
161
|
+
const { data } = params;
|
|
162
|
+
|
|
163
|
+
if (!_.isPlainObject(data)) {
|
|
164
|
+
throw new Error('Create expects a data object');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const dataToInsert = processData(metadata, data, { withDefaults: true });
|
|
168
|
+
|
|
169
|
+
const [id] = await this.createQueryBuilder(uid)
|
|
170
|
+
.insert(dataToInsert)
|
|
171
|
+
.execute();
|
|
172
|
+
|
|
173
|
+
await this.attachRelations(uid, id, data);
|
|
174
|
+
|
|
175
|
+
// TODO: in case there is not select or populate specified return the inserted data ?
|
|
176
|
+
// TODO: do not trigger the findOne lifecycles ?
|
|
177
|
+
const result = await this.findOne(uid, {
|
|
178
|
+
where: { id },
|
|
179
|
+
select: params.select,
|
|
180
|
+
populate: params.populate,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await db.lifecycles.run('afterCreate', uid, { params, result });
|
|
184
|
+
|
|
185
|
+
return result;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// TODO: where do we handle relation processing for many queries ?
|
|
189
|
+
async createMany(uid, params = {}) {
|
|
190
|
+
await db.lifecycles.run('beforeCreateMany', uid, { params });
|
|
191
|
+
|
|
192
|
+
const metadata = db.metadata.get(uid);
|
|
193
|
+
const { data } = params;
|
|
194
|
+
|
|
195
|
+
if (!_.isArray(data)) {
|
|
196
|
+
throw new Error('CreateMany expects data to be an array');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const dataToInsert = data.map(datum => processData(metadata, datum, { withDefaults: true }));
|
|
200
|
+
|
|
201
|
+
if (_.isEmpty(dataToInsert)) {
|
|
202
|
+
throw new Error('Nothing to insert');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await this.createQueryBuilder(uid)
|
|
206
|
+
.insert(dataToInsert)
|
|
207
|
+
.execute();
|
|
208
|
+
|
|
209
|
+
const result = { count: data.length };
|
|
210
|
+
|
|
211
|
+
await db.lifecycles.run('afterCreateMany', uid, { params, result });
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
async update(uid, params = {}) {
|
|
217
|
+
await db.lifecycles.run('beforeUpdate', uid, { params });
|
|
218
|
+
|
|
219
|
+
const metadata = db.metadata.get(uid);
|
|
220
|
+
const { where, data } = params;
|
|
221
|
+
|
|
222
|
+
if (!_.isPlainObject(data)) {
|
|
223
|
+
throw new Error('Update requires a data object');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (_.isEmpty(where)) {
|
|
227
|
+
throw new Error('Update requires a where parameter');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const entity = await this.createQueryBuilder(uid)
|
|
231
|
+
.select('id')
|
|
232
|
+
.where(where)
|
|
233
|
+
.first()
|
|
234
|
+
.execute();
|
|
235
|
+
|
|
236
|
+
if (!entity) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { id } = entity;
|
|
241
|
+
|
|
242
|
+
const dataToUpdate = processData(metadata, data);
|
|
243
|
+
|
|
244
|
+
if (!_.isEmpty(dataToUpdate)) {
|
|
245
|
+
await this.createQueryBuilder(uid)
|
|
246
|
+
.where({ id })
|
|
247
|
+
.update(dataToUpdate)
|
|
248
|
+
.execute();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await this.updateRelations(uid, id, data);
|
|
252
|
+
|
|
253
|
+
// TODO: do not trigger the findOne lifecycles ?
|
|
254
|
+
const result = await this.findOne(uid, {
|
|
255
|
+
where: { id },
|
|
256
|
+
select: params.select,
|
|
257
|
+
populate: params.populate,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await db.lifecycles.run('afterUpdate', uid, { params, result });
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// TODO: where do we handle relation processing for many queries ?
|
|
266
|
+
async updateMany(uid, params = {}) {
|
|
267
|
+
await db.lifecycles.run('beforeUpdateMany', uid, { params });
|
|
268
|
+
|
|
269
|
+
const metadata = db.metadata.get(uid);
|
|
270
|
+
const { where, data } = params;
|
|
271
|
+
|
|
272
|
+
const dataToUpdate = processData(metadata, data);
|
|
273
|
+
|
|
274
|
+
if (_.isEmpty(dataToUpdate)) {
|
|
275
|
+
throw new Error('Update requires data');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const updatedRows = await this.createQueryBuilder(uid)
|
|
279
|
+
.where(where)
|
|
280
|
+
.update(dataToUpdate)
|
|
281
|
+
.execute();
|
|
282
|
+
|
|
283
|
+
const result = { count: updatedRows };
|
|
284
|
+
|
|
285
|
+
await db.lifecycles.run('afterUpdateMany', uid, { params, result });
|
|
286
|
+
|
|
287
|
+
return result;
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
async delete(uid, params = {}) {
|
|
291
|
+
await db.lifecycles.run('beforeDelete', uid, { params });
|
|
292
|
+
|
|
293
|
+
const { where, select, populate } = params;
|
|
294
|
+
|
|
295
|
+
if (_.isEmpty(where)) {
|
|
296
|
+
throw new Error('Delete requires a where parameter');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// TODO: avoid trigger the findOne lifecycles in the case ?
|
|
300
|
+
const entity = await this.findOne(uid, {
|
|
301
|
+
select: select && ['id'].concat(select),
|
|
302
|
+
where,
|
|
303
|
+
populate,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!entity) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const { id } = entity;
|
|
311
|
+
|
|
312
|
+
await this.createQueryBuilder(uid)
|
|
313
|
+
.where({ id })
|
|
314
|
+
.delete()
|
|
315
|
+
.execute();
|
|
316
|
+
|
|
317
|
+
await this.deleteRelations(uid, id);
|
|
318
|
+
|
|
319
|
+
await db.lifecycles.run('afterDelete', uid, { params, result: entity });
|
|
320
|
+
|
|
321
|
+
return entity;
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
// TODO: where do we handle relation processing for many queries ?
|
|
325
|
+
async deleteMany(uid, params = {}) {
|
|
326
|
+
await db.lifecycles.run('beforeDeleteMany', uid, { params });
|
|
327
|
+
|
|
328
|
+
const { where } = params;
|
|
329
|
+
|
|
330
|
+
const deletedRows = await this.createQueryBuilder(uid)
|
|
331
|
+
.where(where)
|
|
332
|
+
.delete()
|
|
333
|
+
.execute();
|
|
334
|
+
|
|
335
|
+
const result = { count: deletedRows };
|
|
336
|
+
|
|
337
|
+
await db.lifecycles.run('afterDelete', uid, { params, result });
|
|
338
|
+
|
|
339
|
+
return result;
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Attach relations to a new entity
|
|
344
|
+
*
|
|
345
|
+
* @param {EntityManager} em - entity manager instance
|
|
346
|
+
* @param {Metadata} metadata - model metadta
|
|
347
|
+
* @param {ID} id - entity ID
|
|
348
|
+
* @param {object} data - data received for creation
|
|
349
|
+
*/
|
|
350
|
+
// TODO: wrap Transaction
|
|
351
|
+
async attachRelations(uid, id, data) {
|
|
352
|
+
const { attributes } = db.metadata.get(uid);
|
|
353
|
+
|
|
354
|
+
for (const attributeName in attributes) {
|
|
355
|
+
const attribute = attributes[attributeName];
|
|
356
|
+
|
|
357
|
+
const isValidLink = _.has(attributeName, data) && !_.isNil(data[attributeName]);
|
|
358
|
+
|
|
359
|
+
if (attribute.type !== 'relation' || !isValidLink) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
|
|
364
|
+
const { target, morphBy } = attribute;
|
|
365
|
+
|
|
366
|
+
const targetAttribute = db.metadata.get(target).attributes[morphBy];
|
|
367
|
+
|
|
368
|
+
if (targetAttribute.relation === 'morphToOne') {
|
|
369
|
+
// set columns
|
|
370
|
+
const { idColumn, typeColumn } = targetAttribute.morphColumn;
|
|
371
|
+
|
|
372
|
+
await this.createQueryBuilder(target)
|
|
373
|
+
.update({ [idColumn.name]: id, [typeColumn.name]: uid })
|
|
374
|
+
.where({ id: toId(data[attributeName]) })
|
|
375
|
+
.execute();
|
|
376
|
+
} else if (targetAttribute.relation === 'morphToMany') {
|
|
377
|
+
const { joinTable } = targetAttribute;
|
|
378
|
+
const { joinColumn, morphColumn } = joinTable;
|
|
379
|
+
|
|
380
|
+
const { idColumn, typeColumn } = morphColumn;
|
|
381
|
+
|
|
382
|
+
const rows = toAssocs(data[attributeName]).map((data, idx) => {
|
|
383
|
+
return {
|
|
384
|
+
[joinColumn.name]: data.id,
|
|
385
|
+
[idColumn.name]: id,
|
|
386
|
+
[typeColumn.name]: uid,
|
|
387
|
+
...(joinTable.on || {}),
|
|
388
|
+
...(data.__pivot || {}),
|
|
389
|
+
order: idx + 1,
|
|
390
|
+
field: attributeName,
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (_.isEmpty(rows)) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await this.createQueryBuilder(joinTable.name)
|
|
399
|
+
.insert(rows)
|
|
400
|
+
.execute();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
continue;
|
|
404
|
+
} else if (attribute.relation === 'morphToOne') {
|
|
405
|
+
// handled on the entry itself
|
|
406
|
+
continue;
|
|
407
|
+
} else if (attribute.relation === 'morphToMany') {
|
|
408
|
+
const { joinTable } = attribute;
|
|
409
|
+
const { joinColumn, morphColumn } = joinTable;
|
|
410
|
+
|
|
411
|
+
const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
|
|
412
|
+
|
|
413
|
+
const rows = toAssocs(data[attributeName]).map(data => ({
|
|
414
|
+
[joinColumn.name]: id,
|
|
415
|
+
[idColumn.name]: data.id,
|
|
416
|
+
[typeColumn.name]: data[typeField],
|
|
417
|
+
...(joinTable.on || {}),
|
|
418
|
+
...(data.__pivot || {}),
|
|
419
|
+
}));
|
|
420
|
+
|
|
421
|
+
if (_.isEmpty(rows)) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await this.createQueryBuilder(joinTable.name)
|
|
426
|
+
.insert(rows)
|
|
427
|
+
.execute();
|
|
428
|
+
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (attribute.joinColumn && attribute.owner) {
|
|
433
|
+
if (
|
|
434
|
+
attribute.relation === 'oneToOne' &&
|
|
435
|
+
isBidirectional(attribute) &&
|
|
436
|
+
data[attributeName]
|
|
437
|
+
) {
|
|
438
|
+
await this.createQueryBuilder(uid)
|
|
439
|
+
.where({ [attribute.joinColumn.name]: data[attributeName], id: { $ne: id } })
|
|
440
|
+
.update({ [attribute.joinColumn.name]: null })
|
|
441
|
+
.execute();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// oneToOne oneToMany on the non owning side
|
|
448
|
+
if (attribute.joinColumn && !attribute.owner) {
|
|
449
|
+
// need to set the column on the target
|
|
450
|
+
const { target } = attribute;
|
|
451
|
+
|
|
452
|
+
// TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL)
|
|
453
|
+
|
|
454
|
+
await this.createQueryBuilder(target)
|
|
455
|
+
.where({ [attribute.joinColumn.referencedColumn]: id })
|
|
456
|
+
.update({ [attribute.joinColumn.referencedColumn]: null })
|
|
457
|
+
.execute();
|
|
458
|
+
|
|
459
|
+
await this.createQueryBuilder(target)
|
|
460
|
+
.update({ [attribute.joinColumn.referencedColumn]: id })
|
|
461
|
+
// NOTE: works if it is an array or a single id
|
|
462
|
+
.where({ id: data[attributeName] })
|
|
463
|
+
.execute();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (attribute.joinTable) {
|
|
467
|
+
// need to set the column on the target
|
|
468
|
+
|
|
469
|
+
const { joinTable } = attribute;
|
|
470
|
+
const { joinColumn, inverseJoinColumn } = joinTable;
|
|
471
|
+
|
|
472
|
+
// TODO: validate logic of delete
|
|
473
|
+
if (isOneToAny(attribute) && isBidirectional(attribute)) {
|
|
474
|
+
await this.createQueryBuilder(joinTable.name)
|
|
475
|
+
.delete()
|
|
476
|
+
.where({ [inverseJoinColumn.name]: _.castArray(data[attributeName]) })
|
|
477
|
+
.where(joinTable.on || {})
|
|
478
|
+
.execute();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const insert = toAssocs(data[attributeName]).map(data => {
|
|
482
|
+
return {
|
|
483
|
+
[joinColumn.name]: id,
|
|
484
|
+
[inverseJoinColumn.name]: data.id,
|
|
485
|
+
...(joinTable.on || {}),
|
|
486
|
+
...(data.__pivot || {}),
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// if there is nothing to insert
|
|
491
|
+
if (insert.length === 0) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
await this.createQueryBuilder(joinTable.name)
|
|
496
|
+
.insert(insert)
|
|
497
|
+
.execute();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Updates relations of an existing entity
|
|
504
|
+
*
|
|
505
|
+
* @param {EntityManager} em - entity manager instance
|
|
506
|
+
* @param {Metadata} metadata - model metadta
|
|
507
|
+
* @param {ID} id - entity ID
|
|
508
|
+
* @param {object} data - data received for creation
|
|
509
|
+
*/
|
|
510
|
+
// TODO: check relation exists (handled by FKs except for polymorphics)
|
|
511
|
+
// TODO: wrap Transaction
|
|
512
|
+
async updateRelations(uid, id, data) {
|
|
513
|
+
const { attributes } = db.metadata.get(uid);
|
|
514
|
+
|
|
515
|
+
for (const attributeName in attributes) {
|
|
516
|
+
const attribute = attributes[attributeName];
|
|
517
|
+
|
|
518
|
+
if (attribute.type !== 'relation' || !_.has(attributeName, data)) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
|
|
523
|
+
const { target, morphBy } = attribute;
|
|
524
|
+
|
|
525
|
+
const targetAttribute = db.metadata.get(target).attributes[morphBy];
|
|
526
|
+
|
|
527
|
+
if (targetAttribute.relation === 'morphToOne') {
|
|
528
|
+
// set columns
|
|
529
|
+
const { idColumn, typeColumn } = targetAttribute.morphColumn;
|
|
530
|
+
|
|
531
|
+
await this.createQueryBuilder(target)
|
|
532
|
+
.update({ [idColumn.name]: null, [typeColumn.name]: null })
|
|
533
|
+
.where({ [idColumn.name]: id, [typeColumn.name]: uid })
|
|
534
|
+
.execute();
|
|
535
|
+
|
|
536
|
+
if (!_.isNull(data[attributeName])) {
|
|
537
|
+
await this.createQueryBuilder(target)
|
|
538
|
+
.update({ [idColumn.name]: id, [typeColumn.name]: uid })
|
|
539
|
+
.where({ id: toId(data[attributeName]) })
|
|
540
|
+
.execute();
|
|
541
|
+
}
|
|
542
|
+
} else if (targetAttribute.relation === 'morphToMany') {
|
|
543
|
+
const { joinTable } = targetAttribute;
|
|
544
|
+
const { joinColumn, morphColumn } = joinTable;
|
|
545
|
+
|
|
546
|
+
const { idColumn, typeColumn } = morphColumn;
|
|
547
|
+
|
|
548
|
+
await this.createQueryBuilder(joinTable.name)
|
|
549
|
+
.delete()
|
|
550
|
+
.where({
|
|
551
|
+
[idColumn.name]: id,
|
|
552
|
+
[typeColumn.name]: uid,
|
|
553
|
+
...(joinTable.on || {}),
|
|
554
|
+
field: attributeName,
|
|
555
|
+
})
|
|
556
|
+
.execute();
|
|
557
|
+
|
|
558
|
+
const rows = toAssocs(data[attributeName]).map((data, idx) => ({
|
|
559
|
+
[joinColumn.name]: data.id,
|
|
560
|
+
[idColumn.name]: id,
|
|
561
|
+
[typeColumn.name]: uid,
|
|
562
|
+
...(joinTable.on || {}),
|
|
563
|
+
...(data.__pivot || {}),
|
|
564
|
+
order: idx + 1,
|
|
565
|
+
field: attributeName,
|
|
566
|
+
}));
|
|
567
|
+
|
|
568
|
+
if (_.isEmpty(rows)) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
await this.createQueryBuilder(joinTable.name)
|
|
573
|
+
.insert(rows)
|
|
574
|
+
.execute();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (attribute.relation === 'morphToOne') {
|
|
581
|
+
// handled on the entry itself
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (attribute.relation === 'morphToMany') {
|
|
586
|
+
const { joinTable } = attribute;
|
|
587
|
+
const { joinColumn, morphColumn } = joinTable;
|
|
588
|
+
|
|
589
|
+
const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
|
|
590
|
+
|
|
591
|
+
await this.createQueryBuilder(joinTable.name)
|
|
592
|
+
.delete()
|
|
593
|
+
.where({
|
|
594
|
+
[joinColumn.name]: id,
|
|
595
|
+
...(joinTable.on || {}),
|
|
596
|
+
})
|
|
597
|
+
.execute();
|
|
598
|
+
|
|
599
|
+
const rows = toAssocs(data[attributeName]).map(data => ({
|
|
600
|
+
[joinColumn.name]: id,
|
|
601
|
+
[idColumn.name]: data.id,
|
|
602
|
+
[typeColumn.name]: data[typeField],
|
|
603
|
+
...(joinTable.on || {}),
|
|
604
|
+
...(data.__pivot || {}),
|
|
605
|
+
}));
|
|
606
|
+
|
|
607
|
+
if (_.isEmpty(rows)) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
await this.createQueryBuilder(joinTable.name)
|
|
612
|
+
.insert(rows)
|
|
613
|
+
.execute();
|
|
614
|
+
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (attribute.joinColumn && attribute.owner) {
|
|
619
|
+
// handled in the row itslef
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// oneToOne oneToMany on the non owning side.
|
|
624
|
+
// Since it is a join column no need to remove previous relations
|
|
625
|
+
if (attribute.joinColumn && !attribute.owner) {
|
|
626
|
+
// need to set the column on the target
|
|
627
|
+
const { target } = attribute;
|
|
628
|
+
|
|
629
|
+
await this.createQueryBuilder(target)
|
|
630
|
+
.where({ [attribute.joinColumn.referencedColumn]: id })
|
|
631
|
+
.update({ [attribute.joinColumn.referencedColumn]: null })
|
|
632
|
+
.execute();
|
|
633
|
+
|
|
634
|
+
if (!_.isNull(data[attributeName])) {
|
|
635
|
+
await this.createQueryBuilder(target)
|
|
636
|
+
// NOTE: works if it is an array or a single id
|
|
637
|
+
.where({ id: data[attributeName] })
|
|
638
|
+
.update({ [attribute.joinColumn.referencedColumn]: id })
|
|
639
|
+
.execute();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (attribute.joinTable) {
|
|
644
|
+
const { joinTable } = attribute;
|
|
645
|
+
const { joinColumn, inverseJoinColumn } = joinTable;
|
|
646
|
+
|
|
647
|
+
// clear previous associations in the joinTable
|
|
648
|
+
await this.createQueryBuilder(joinTable.name)
|
|
649
|
+
.delete()
|
|
650
|
+
.where({ [joinColumn.name]: id })
|
|
651
|
+
.where(joinTable.on || {})
|
|
652
|
+
.execute();
|
|
653
|
+
|
|
654
|
+
if (
|
|
655
|
+
isBidirectional(attribute) &&
|
|
656
|
+
['oneToOne', 'oneToMany'].includes(attribute.relation)
|
|
657
|
+
) {
|
|
658
|
+
await this.createQueryBuilder(joinTable.name)
|
|
659
|
+
.delete()
|
|
660
|
+
.where({ [inverseJoinColumn.name]: toIds(data[attributeName]) })
|
|
661
|
+
.where(joinTable.on || {})
|
|
662
|
+
.execute();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (!_.isNull(data[attributeName])) {
|
|
666
|
+
const insert = toAssocs(data[attributeName]).map(data => {
|
|
667
|
+
return {
|
|
668
|
+
[joinColumn.name]: id,
|
|
669
|
+
[inverseJoinColumn.name]: data.id,
|
|
670
|
+
...(joinTable.on || {}),
|
|
671
|
+
...(data.__pivot || {}),
|
|
672
|
+
};
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// if there is nothing to insert
|
|
676
|
+
if (insert.length === 0) {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
await this.createQueryBuilder(joinTable.name)
|
|
681
|
+
.insert(insert)
|
|
682
|
+
.execute();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Delete relational associations of an existing entity
|
|
690
|
+
* This removes associations but doesn't do cascade deletions for components for example. This will be handled on the entity service layer instead
|
|
691
|
+
* NOTE: Most of the deletion should be handled by ON DELETE CASCADE for dialects that have FKs
|
|
692
|
+
*
|
|
693
|
+
* @param {EntityManager} em - entity manager instance
|
|
694
|
+
* @param {Metadata} metadata - model metadta
|
|
695
|
+
* @param {ID} id - entity ID
|
|
696
|
+
*/
|
|
697
|
+
// TODO: wrap Transaction
|
|
698
|
+
async deleteRelations(uid, id) {
|
|
699
|
+
const { attributes } = db.metadata.get(uid);
|
|
700
|
+
|
|
701
|
+
for (const attributeName in attributes) {
|
|
702
|
+
const attribute = attributes[attributeName];
|
|
703
|
+
|
|
704
|
+
if (attribute.type !== 'relation') {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/*
|
|
709
|
+
if morphOne | morphMany
|
|
710
|
+
if morphBy is morphToOne
|
|
711
|
+
set null
|
|
712
|
+
if morphBy is morphToOne
|
|
713
|
+
delete links
|
|
714
|
+
*/
|
|
715
|
+
if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
|
|
716
|
+
const { target, morphBy } = attribute;
|
|
717
|
+
|
|
718
|
+
const targetAttribute = db.metadata.get(target).attributes[morphBy];
|
|
719
|
+
|
|
720
|
+
if (targetAttribute.relation === 'morphToOne') {
|
|
721
|
+
// set columns
|
|
722
|
+
const { idColumn, typeColumn } = targetAttribute.morphColumn;
|
|
723
|
+
|
|
724
|
+
await this.createQueryBuilder(target)
|
|
725
|
+
.update({ [idColumn.name]: null, [typeColumn.name]: null })
|
|
726
|
+
.where({ [idColumn.name]: id, [typeColumn.name]: uid })
|
|
727
|
+
.execute();
|
|
728
|
+
} else if (targetAttribute.relation === 'morphToMany') {
|
|
729
|
+
const { joinTable } = targetAttribute;
|
|
730
|
+
const { morphColumn } = joinTable;
|
|
731
|
+
|
|
732
|
+
const { idColumn, typeColumn } = morphColumn;
|
|
733
|
+
|
|
734
|
+
await this.createQueryBuilder(joinTable.name)
|
|
735
|
+
.delete()
|
|
736
|
+
.where({
|
|
737
|
+
[idColumn.name]: id,
|
|
738
|
+
[typeColumn.name]: uid,
|
|
739
|
+
...(joinTable.on || {}),
|
|
740
|
+
field: attributeName,
|
|
741
|
+
})
|
|
742
|
+
.execute();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/*
|
|
749
|
+
if morphToOne
|
|
750
|
+
nothing to do
|
|
751
|
+
*/
|
|
752
|
+
if (attribute.relation === 'morphToOne') {
|
|
753
|
+
// do nothing
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/*
|
|
757
|
+
if morphToMany
|
|
758
|
+
delete links
|
|
759
|
+
*/
|
|
760
|
+
if (attribute.relation === 'morphToMany') {
|
|
761
|
+
const { joinTable } = attribute;
|
|
762
|
+
const { joinColumn } = joinTable;
|
|
763
|
+
|
|
764
|
+
await this.createQueryBuilder(joinTable.name)
|
|
765
|
+
.delete()
|
|
766
|
+
.where({
|
|
767
|
+
[joinColumn.name]: id,
|
|
768
|
+
...(joinTable.on || {}),
|
|
769
|
+
})
|
|
770
|
+
.execute();
|
|
771
|
+
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// do not need to delete links when using foreign keys
|
|
776
|
+
if (db.dialect.usesForeignKeys()) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// NOTE: we do not remove existing associations with the target as it should handled by unique FKs instead
|
|
781
|
+
if (attribute.joinColumn && attribute.owner) {
|
|
782
|
+
// nothing to do => relation already added on the table
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// oneToOne oneToMany on the non owning side.
|
|
787
|
+
if (attribute.joinColumn && !attribute.owner) {
|
|
788
|
+
// need to set the column on the target
|
|
789
|
+
const { target } = attribute;
|
|
790
|
+
|
|
791
|
+
await this.createQueryBuilder(target)
|
|
792
|
+
.where({ [attribute.joinColumn.referencedColumn]: id })
|
|
793
|
+
.update({ [attribute.joinColumn.referencedColumn]: null })
|
|
794
|
+
.execute();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (attribute.joinTable) {
|
|
798
|
+
const { joinTable } = attribute;
|
|
799
|
+
const { joinColumn } = joinTable;
|
|
800
|
+
|
|
801
|
+
await this.createQueryBuilder(joinTable.name)
|
|
802
|
+
.delete()
|
|
803
|
+
.where({ [joinColumn.name]: id })
|
|
804
|
+
.where(joinTable.on || {})
|
|
805
|
+
.execute();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
|
|
810
|
+
// TODO: support multiple relations at once with the populate syntax
|
|
811
|
+
// TODO: add lifecycle events
|
|
812
|
+
async populate(uid, entity, populate) {
|
|
813
|
+
const entry = await this.findOne(uid, {
|
|
814
|
+
select: ['id'],
|
|
815
|
+
where: { id: entity.id },
|
|
816
|
+
populate,
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
return Object.assign({}, entity, entry);
|
|
820
|
+
},
|
|
821
|
+
|
|
822
|
+
// TODO: support multiple relations at once with the populate syntax
|
|
823
|
+
// TODO: add lifecycle events
|
|
824
|
+
async load(uid, entity, field, params) {
|
|
825
|
+
const { attributes } = db.metadata.get(uid);
|
|
826
|
+
|
|
827
|
+
const attribute = attributes[field];
|
|
828
|
+
|
|
829
|
+
if (!attribute || attribute.type !== 'relation') {
|
|
830
|
+
throw new Error('Invalid load. Expected a relational attribute');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const entry = await this.findOne(uid, {
|
|
834
|
+
select: ['id'],
|
|
835
|
+
where: { id: entity.id },
|
|
836
|
+
populate: {
|
|
837
|
+
[field]: params || true,
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
if (!entry) {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return entry[field];
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
// cascading
|
|
849
|
+
// aggregations
|
|
850
|
+
// -> avg
|
|
851
|
+
// -> min
|
|
852
|
+
// -> max
|
|
853
|
+
// -> grouping
|
|
854
|
+
|
|
855
|
+
// formulas
|
|
856
|
+
// custom queries
|
|
857
|
+
|
|
858
|
+
// utilities
|
|
859
|
+
// -> map result
|
|
860
|
+
// -> map input
|
|
861
|
+
|
|
862
|
+
// extra features
|
|
863
|
+
// -> virtuals
|
|
864
|
+
// -> private
|
|
865
|
+
|
|
866
|
+
createQueryBuilder(uid) {
|
|
867
|
+
return createQueryBuilder(uid, db);
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
getRepository(uid) {
|
|
871
|
+
if (!repoMap[uid]) {
|
|
872
|
+
repoMap[uid] = createRepository(uid, db);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return repoMap[uid];
|
|
876
|
+
},
|
|
877
|
+
|
|
878
|
+
clearRepositories() {
|
|
879
|
+
repoMap.clear();
|
|
880
|
+
},
|
|
881
|
+
};
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
module.exports = {
|
|
885
|
+
createEntityManager,
|
|
886
|
+
};
|