@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.
Files changed (52) hide show
  1. package/LICENSE +22 -0
  2. package/examples/connections.js +36 -0
  3. package/examples/data.sqlite +0 -0
  4. package/examples/docker-compose.yml +29 -0
  5. package/examples/index.js +73 -0
  6. package/examples/models.js +341 -0
  7. package/examples/typings.ts +17 -0
  8. package/lib/dialects/dialect.js +45 -0
  9. package/lib/dialects/index.js +28 -0
  10. package/lib/dialects/mysql/index.js +51 -0
  11. package/lib/dialects/mysql/schema-inspector.js +203 -0
  12. package/lib/dialects/postgresql/index.js +49 -0
  13. package/lib/dialects/postgresql/schema-inspector.js +229 -0
  14. package/lib/dialects/sqlite/index.js +74 -0
  15. package/lib/dialects/sqlite/schema-inspector.js +151 -0
  16. package/lib/entity-manager.js +886 -0
  17. package/lib/entity-repository.js +110 -0
  18. package/lib/errors.js +14 -0
  19. package/lib/fields.d.ts +9 -0
  20. package/lib/fields.js +232 -0
  21. package/lib/index.d.ts +146 -0
  22. package/lib/index.js +60 -0
  23. package/lib/lifecycles/index.d.ts +50 -0
  24. package/lib/lifecycles/index.js +66 -0
  25. package/lib/lifecycles/subscribers/index.d.ts +9 -0
  26. package/lib/lifecycles/subscribers/models-lifecycles.js +19 -0
  27. package/lib/lifecycles/subscribers/timestamps.js +65 -0
  28. package/lib/metadata/index.js +219 -0
  29. package/lib/metadata/relations.js +488 -0
  30. package/lib/migrations/index.d.ts +9 -0
  31. package/lib/migrations/index.js +69 -0
  32. package/lib/migrations/storage.js +49 -0
  33. package/lib/query/helpers/index.js +10 -0
  34. package/lib/query/helpers/join.js +95 -0
  35. package/lib/query/helpers/order-by.js +70 -0
  36. package/lib/query/helpers/populate.js +652 -0
  37. package/lib/query/helpers/search.js +84 -0
  38. package/lib/query/helpers/transform.js +84 -0
  39. package/lib/query/helpers/where.js +322 -0
  40. package/lib/query/index.js +7 -0
  41. package/lib/query/query-builder.js +348 -0
  42. package/lib/schema/__tests__/schema-diff.test.js +181 -0
  43. package/lib/schema/builder.js +352 -0
  44. package/lib/schema/diff.js +376 -0
  45. package/lib/schema/index.d.ts +49 -0
  46. package/lib/schema/index.js +95 -0
  47. package/lib/schema/schema.js +209 -0
  48. package/lib/schema/storage.js +75 -0
  49. package/lib/types/index.d.ts +6 -0
  50. package/lib/types/index.js +34 -0
  51. package/lib/utils/content-types.js +41 -0
  52. 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
+ };