@strapi/database 4.4.3 → 4.5.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.
@@ -1,649 +0,0 @@
1
- 'use strict';
2
-
3
- const _ = require('lodash/fp');
4
-
5
- const types = require('../../types');
6
- const { fromRow } = require('./transform');
7
-
8
- const getRootLevelPopulate = (meta) => {
9
- const populate = {};
10
-
11
- for (const attributeName of Object.keys(meta.attributes)) {
12
- const attribute = meta.attributes[attributeName];
13
- if (attribute.type === 'relation') {
14
- populate[attributeName] = true;
15
- }
16
- }
17
-
18
- return populate;
19
- };
20
-
21
- /**
22
- * Converts and prepares the query for populate
23
- *
24
- * @param {boolean|string[]|object} populate populate param
25
- * @param {object} ctx query context
26
- * @param {object} ctx.db database instance
27
- * @param {object} ctx.qb query builder instance
28
- * @param {string} ctx.uid model uid
29
- */
30
- const processPopulate = (populate, ctx) => {
31
- const { qb, db, uid } = ctx;
32
- const meta = db.metadata.get(uid);
33
-
34
- let populateMap = {};
35
-
36
- if (populate === false || _.isNil(populate)) {
37
- return null;
38
- }
39
-
40
- if (populate === true) {
41
- populateMap = getRootLevelPopulate(meta);
42
- } else if (Array.isArray(populate)) {
43
- for (const key of populate) {
44
- const [root, ...rest] = key.split('.');
45
-
46
- if (rest.length > 0) {
47
- const subPopulate = rest.join('.');
48
-
49
- if (populateMap[root]) {
50
- if (populateMap[root] === true) {
51
- populateMap[root] = {
52
- populate: [subPopulate],
53
- };
54
- } else {
55
- populateMap[root].populate = [subPopulate].concat(populateMap[root].populate || []);
56
- }
57
- } else {
58
- populateMap[root] = {
59
- populate: [subPopulate],
60
- };
61
- }
62
- } else {
63
- populateMap[root] = populateMap[root] ? populateMap[root] : true;
64
- }
65
- }
66
- } else {
67
- populateMap = populate;
68
- }
69
-
70
- if (!_.isPlainObject(populateMap)) {
71
- throw new Error('Populate must be an object');
72
- }
73
-
74
- const finalPopulate = {};
75
- for (const key of Object.keys(populateMap)) {
76
- const attribute = meta.attributes[key];
77
-
78
- if (!attribute) {
79
- continue;
80
- }
81
-
82
- if (!types.isRelation(attribute.type)) {
83
- continue;
84
- }
85
-
86
- // make sure id is present for future populate queries
87
- if (_.has('id', meta.attributes)) {
88
- qb.addSelect('id');
89
- }
90
-
91
- finalPopulate[key] = populateMap[key];
92
- }
93
-
94
- return finalPopulate;
95
- };
96
-
97
- // TODO: Omit limit & offset to avoid needing a query per result to avoid making too many queries
98
- const pickPopulateParams = _.pick([
99
- 'select',
100
- 'count',
101
- 'where',
102
- 'populate',
103
- 'orderBy',
104
- 'limit',
105
- 'offset',
106
- 'filters',
107
- ]);
108
-
109
- // TODO: cleanup code
110
- // TODO: create aliases for pivot columns
111
- // TODO: optimize depth to avoid overfetching
112
- // TODO: handle count for join columns
113
- // TODO: cleanup count
114
- const applyPopulate = async (results, populate, ctx) => {
115
- const { db, uid, qb } = ctx;
116
- const meta = db.metadata.get(uid);
117
-
118
- if (_.isEmpty(results)) {
119
- return results;
120
- }
121
-
122
- for (const key of Object.keys(populate)) {
123
- const attribute = meta.attributes[key];
124
- const targetMeta = db.metadata.get(attribute.target);
125
-
126
- const populateValue = {
127
- filters: qb.state.filters,
128
- ...pickPopulateParams(populate[key]),
129
- };
130
-
131
- const isCount = populateValue.count === true;
132
-
133
- const fromTargetRow = (rowOrRows) => fromRow(targetMeta, rowOrRows);
134
-
135
- if (attribute.relation === 'oneToOne' || attribute.relation === 'manyToOne') {
136
- if (attribute.joinColumn) {
137
- const { name: joinColumnName, referencedColumn: referencedColumnName } =
138
- attribute.joinColumn;
139
-
140
- const referencedValues = _.uniq(
141
- results.map((r) => r[joinColumnName]).filter((value) => !_.isNil(value))
142
- );
143
-
144
- if (_.isEmpty(referencedValues)) {
145
- results.forEach((result) => {
146
- result[key] = null;
147
- });
148
-
149
- continue;
150
- }
151
-
152
- const rows = await db.entityManager
153
- .createQueryBuilder(targetMeta.uid)
154
- .init(populateValue)
155
- .addSelect(`${qb.alias}.${referencedColumnName}`)
156
- .where({ [referencedColumnName]: referencedValues })
157
- .execute({ mapResults: false });
158
-
159
- const map = _.groupBy(referencedColumnName, rows);
160
-
161
- results.forEach((result) => {
162
- result[key] = fromTargetRow(_.first(map[result[joinColumnName]]));
163
- });
164
-
165
- continue;
166
- }
167
-
168
- if (attribute.joinTable) {
169
- const { joinTable } = attribute;
170
-
171
- const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
172
-
173
- const { name: joinColumnName, referencedColumn: referencedColumnName } =
174
- joinTable.joinColumn;
175
-
176
- const alias = qb.getAlias();
177
- const joinColAlias = `${alias}.${joinColumnName}`;
178
-
179
- const referencedValues = _.uniq(
180
- results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
181
- );
182
-
183
- if (_.isEmpty(referencedValues)) {
184
- results.forEach((result) => {
185
- result[key] = null;
186
- });
187
- continue;
188
- }
189
-
190
- const rows = await qb
191
- .init(populateValue)
192
- .join({
193
- alias,
194
- referencedTable: joinTable.name,
195
- referencedColumn: joinTable.inverseJoinColumn.name,
196
- rootColumn: joinTable.inverseJoinColumn.referencedColumn,
197
- rootTable: qb.alias,
198
- on: joinTable.on,
199
- orderBy: joinTable.orderBy,
200
- })
201
- .addSelect(joinColAlias)
202
- .where({ [joinColAlias]: referencedValues })
203
- .execute({ mapResults: false });
204
-
205
- const map = _.groupBy(joinColumnName, rows);
206
-
207
- results.forEach((result) => {
208
- result[key] = fromTargetRow(_.first(map[result[referencedColumnName]]));
209
- });
210
-
211
- continue;
212
- }
213
-
214
- continue;
215
- } else if (attribute.relation === 'oneToMany') {
216
- if (attribute.joinColumn) {
217
- const { name: joinColumnName, referencedColumn: referencedColumnName } =
218
- attribute.joinColumn;
219
-
220
- const referencedValues = _.uniq(
221
- results.map((r) => r[joinColumnName]).filter((value) => !_.isNil(value))
222
- );
223
-
224
- if (_.isEmpty(referencedValues)) {
225
- results.forEach((result) => {
226
- result[key] = null;
227
- });
228
- continue;
229
- }
230
-
231
- const rows = await db.entityManager
232
- .createQueryBuilder(targetMeta.uid)
233
- .init(populateValue)
234
- .addSelect(`${qb.alias}.${referencedColumnName}`)
235
- .where({ [referencedColumnName]: referencedValues })
236
- .execute({ mapResults: false });
237
-
238
- const map = _.groupBy(referencedColumnName, rows);
239
-
240
- results.forEach((result) => {
241
- result[key] = fromTargetRow(map[result[joinColumnName]] || []);
242
- });
243
-
244
- continue;
245
- }
246
-
247
- if (attribute.joinTable) {
248
- const { joinTable } = attribute;
249
-
250
- const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
251
-
252
- const { name: joinColumnName, referencedColumn: referencedColumnName } =
253
- joinTable.joinColumn;
254
-
255
- const alias = qb.getAlias();
256
- const joinColAlias = `${alias}.${joinColumnName}`;
257
-
258
- const referencedValues = _.uniq(
259
- results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
260
- );
261
-
262
- if (isCount) {
263
- if (_.isEmpty(referencedValues)) {
264
- results.forEach((result) => {
265
- result[key] = { count: 0 };
266
- });
267
- continue;
268
- }
269
-
270
- const rows = await qb
271
- .init(populateValue)
272
- .join({
273
- alias,
274
- referencedTable: joinTable.name,
275
- referencedColumn: joinTable.inverseJoinColumn.name,
276
- rootColumn: joinTable.inverseJoinColumn.referencedColumn,
277
- rootTable: qb.alias,
278
- on: joinTable.on,
279
- })
280
- .select([joinColAlias, qb.raw('count(*) AS count')])
281
- .where({ [joinColAlias]: referencedValues })
282
- .groupBy(joinColAlias)
283
- .execute({ mapResults: false });
284
-
285
- const map = rows.reduce((map, row) => {
286
- map[row[joinColumnName]] = { count: Number(row.count) };
287
- return map;
288
- }, {});
289
-
290
- results.forEach((result) => {
291
- result[key] = map[result[referencedColumnName]] || { count: 0 };
292
- });
293
-
294
- continue;
295
- }
296
-
297
- if (_.isEmpty(referencedValues)) {
298
- results.forEach((result) => {
299
- result[key] = [];
300
- });
301
- continue;
302
- }
303
-
304
- const rows = await qb
305
- .init(populateValue)
306
- .join({
307
- alias,
308
- referencedTable: joinTable.name,
309
- referencedColumn: joinTable.inverseJoinColumn.name,
310
- rootColumn: joinTable.inverseJoinColumn.referencedColumn,
311
- rootTable: qb.alias,
312
- on: joinTable.on,
313
- orderBy: joinTable.orderBy,
314
- })
315
- .addSelect(joinColAlias)
316
- .where({ [joinColAlias]: referencedValues })
317
- .execute({ mapResults: false });
318
-
319
- const map = _.groupBy(joinColumnName, rows);
320
-
321
- results.forEach((r) => {
322
- r[key] = fromTargetRow(map[r[referencedColumnName]] || []);
323
- });
324
- continue;
325
- }
326
-
327
- continue;
328
- } else if (attribute.relation === 'manyToMany') {
329
- const { joinTable } = attribute;
330
-
331
- const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
332
-
333
- const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
334
-
335
- const alias = qb.getAlias();
336
- const joinColAlias = `${alias}.${joinColumnName}`;
337
- const referencedValues = _.uniq(
338
- results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
339
- );
340
-
341
- if (isCount) {
342
- if (_.isEmpty(referencedValues)) {
343
- results.forEach((result) => {
344
- result[key] = { count: 0 };
345
- });
346
- continue;
347
- }
348
-
349
- const rows = await qb
350
- .init(populateValue)
351
- .join({
352
- alias,
353
- referencedTable: joinTable.name,
354
- referencedColumn: joinTable.inverseJoinColumn.name,
355
- rootColumn: joinTable.inverseJoinColumn.referencedColumn,
356
- rootTable: qb.alias,
357
- on: joinTable.on,
358
- })
359
- .select([joinColAlias, qb.raw('count(*) AS count')])
360
- .where({ [joinColAlias]: referencedValues })
361
- .groupBy(joinColAlias)
362
- .execute({ mapResults: false });
363
-
364
- const map = rows.reduce((map, row) => {
365
- map[row[joinColumnName]] = { count: Number(row.count) };
366
- return map;
367
- }, {});
368
-
369
- results.forEach((result) => {
370
- result[key] = map[result[referencedColumnName]] || { count: 0 };
371
- });
372
-
373
- continue;
374
- }
375
-
376
- if (_.isEmpty(referencedValues)) {
377
- results.forEach((result) => {
378
- result[key] = [];
379
- });
380
- continue;
381
- }
382
-
383
- const rows = await qb
384
- .init(populateValue)
385
- .join({
386
- alias,
387
- referencedTable: joinTable.name,
388
- referencedColumn: joinTable.inverseJoinColumn.name,
389
- rootColumn: joinTable.inverseJoinColumn.referencedColumn,
390
- rootTable: qb.alias,
391
- on: joinTable.on,
392
- orderBy: joinTable.orderBy,
393
- })
394
- .addSelect(joinColAlias)
395
- .where({ [joinColAlias]: referencedValues })
396
- .execute({ mapResults: false });
397
-
398
- const map = _.groupBy(joinColumnName, rows);
399
-
400
- results.forEach((result) => {
401
- result[key] = fromTargetRow(map[result[referencedColumnName]] || []);
402
- });
403
-
404
- continue;
405
- } else if (['morphOne', 'morphMany'].includes(attribute.relation)) {
406
- const { target, morphBy } = attribute;
407
-
408
- const targetAttribute = db.metadata.get(target).attributes[morphBy];
409
-
410
- if (targetAttribute.relation === 'morphToOne') {
411
- const { idColumn, typeColumn } = targetAttribute.morphColumn;
412
-
413
- const referencedValues = _.uniq(
414
- results.map((r) => r[idColumn.referencedColumn]).filter((value) => !_.isNil(value))
415
- );
416
-
417
- if (_.isEmpty(referencedValues)) {
418
- results.forEach((result) => {
419
- result[key] = null;
420
- });
421
-
422
- continue;
423
- }
424
-
425
- const rows = await db.entityManager
426
- .createQueryBuilder(target)
427
- .init(populateValue)
428
- // .addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
429
- .where({ [idColumn.name]: referencedValues, [typeColumn.name]: uid })
430
- .execute({ mapResults: false });
431
-
432
- const map = _.groupBy(idColumn.name, rows);
433
-
434
- results.forEach((result) => {
435
- const matchingRows = map[result[idColumn.referencedColumn]];
436
-
437
- const matchingValue =
438
- attribute.relation === 'morphOne' ? _.first(matchingRows) : matchingRows;
439
-
440
- result[key] = fromTargetRow(matchingValue);
441
- });
442
- } else if (targetAttribute.relation === 'morphToMany') {
443
- const { joinTable } = targetAttribute;
444
-
445
- const { joinColumn, morphColumn } = joinTable;
446
-
447
- const { idColumn, typeColumn } = morphColumn;
448
-
449
- const referencedValues = _.uniq(
450
- results.map((r) => r[idColumn.referencedColumn]).filter((value) => !_.isNil(value))
451
- );
452
-
453
- if (_.isEmpty(referencedValues)) {
454
- results.forEach((result) => {
455
- result[key] = attribute.relation === 'morphOne' ? null : [];
456
- });
457
-
458
- continue;
459
- }
460
-
461
- // find with join table
462
- const qb = db.entityManager.createQueryBuilder(target);
463
-
464
- const alias = qb.getAlias();
465
-
466
- const rows = await qb
467
- .init(populateValue)
468
- .join({
469
- alias,
470
- referencedTable: joinTable.name,
471
- referencedColumn: joinColumn.name,
472
- rootColumn: joinColumn.referencedColumn,
473
- rootTable: qb.alias,
474
- on: {
475
- ...(joinTable.on || {}),
476
- field: key,
477
- },
478
- orderBy: joinTable.orderBy,
479
- })
480
- .addSelect([`${alias}.${idColumn.name}`, `${alias}.${typeColumn.name}`])
481
- .where({
482
- [`${alias}.${idColumn.name}`]: referencedValues,
483
- [`${alias}.${typeColumn.name}`]: uid,
484
- })
485
- .execute({ mapResults: false });
486
-
487
- const map = _.groupBy(idColumn.name, rows);
488
-
489
- results.forEach((result) => {
490
- const matchingRows = map[result[idColumn.referencedColumn]];
491
-
492
- const matchingValue =
493
- attribute.relation === 'morphOne' ? _.first(matchingRows) : matchingRows;
494
-
495
- result[key] = fromTargetRow(matchingValue);
496
- });
497
- }
498
-
499
- continue;
500
- } else if (attribute.relation === 'morphToMany') {
501
- // find with join table
502
- const { joinTable } = attribute;
503
-
504
- const { joinColumn, morphColumn } = joinTable;
505
- const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
506
-
507
- // fetch join table to create the ids map then do the same as morphToOne without the first
508
-
509
- const referencedValues = _.uniq(
510
- results.map((r) => r[joinColumn.referencedColumn]).filter((value) => !_.isNil(value))
511
- );
512
-
513
- const qb = db.entityManager.createQueryBuilder(joinTable.name);
514
-
515
- const joinRows = await qb
516
- .where({
517
- [joinColumn.name]: referencedValues,
518
- ...(joinTable.on || {}),
519
- })
520
- .orderBy([joinColumn.name, 'order'])
521
- .execute({ mapResults: false });
522
-
523
- const joinMap = _.groupBy(joinColumn.name, joinRows);
524
-
525
- const idsByType = joinRows.reduce((acc, result) => {
526
- const idValue = result[morphColumn.idColumn.name];
527
- const typeValue = result[morphColumn.typeColumn.name];
528
-
529
- if (!idValue || !typeValue) {
530
- return acc;
531
- }
532
-
533
- if (!_.has(typeValue, acc)) {
534
- acc[typeValue] = [];
535
- }
536
-
537
- acc[typeValue].push(idValue);
538
-
539
- return acc;
540
- }, {});
541
-
542
- const map = {};
543
- for (const type of Object.keys(idsByType)) {
544
- const ids = idsByType[type];
545
-
546
- // type was removed but still in morph relation
547
- if (!db.metadata.get(type)) {
548
- map[type] = {};
549
- continue;
550
- }
551
-
552
- const qb = db.entityManager.createQueryBuilder(type);
553
-
554
- const rows = await qb
555
- .init(populateValue)
556
- .addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
557
- .where({ [idColumn.referencedColumn]: ids })
558
- .execute({ mapResults: false });
559
-
560
- map[type] = _.groupBy(idColumn.referencedColumn, rows);
561
- }
562
-
563
- results.forEach((result) => {
564
- const joinResults = joinMap[result[joinColumn.referencedColumn]] || [];
565
-
566
- const matchingRows = joinResults.flatMap((joinResult) => {
567
- const id = joinResult[idColumn.name];
568
- const type = joinResult[typeColumn.name];
569
-
570
- const fromTargetRow = (rowOrRows) => fromRow(db.metadata.get(type), rowOrRows);
571
-
572
- return (map[type][id] || []).map((row) => {
573
- return {
574
- [typeField]: type,
575
- ...fromTargetRow(row),
576
- };
577
- });
578
- });
579
-
580
- result[key] = matchingRows;
581
- });
582
- } else if (attribute.relation === 'morphToOne') {
583
- const { morphColumn } = attribute;
584
- const { idColumn, typeColumn } = morphColumn;
585
-
586
- // make a map for each type what ids to return
587
- // make a nested map per id
588
-
589
- const idsByType = results.reduce((acc, result) => {
590
- const idValue = result[morphColumn.idColumn.name];
591
- const typeValue = result[morphColumn.typeColumn.name];
592
-
593
- if (!idValue || !typeValue) {
594
- return acc;
595
- }
596
-
597
- if (!_.has(typeValue, acc)) {
598
- acc[typeValue] = [];
599
- }
600
-
601
- acc[typeValue].push(idValue);
602
-
603
- return acc;
604
- }, {});
605
-
606
- const map = {};
607
- for (const type of Object.keys(idsByType)) {
608
- const ids = idsByType[type];
609
-
610
- // type was removed but still in morph relation
611
- if (!db.metadata.get(type)) {
612
- map[type] = {};
613
- continue;
614
- }
615
-
616
- const qb = db.entityManager.createQueryBuilder(type);
617
-
618
- const rows = await qb
619
- .init(populateValue)
620
- .addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
621
- .where({ [idColumn.referencedColumn]: ids })
622
- .execute({ mapResults: false });
623
-
624
- map[type] = _.groupBy(idColumn.referencedColumn, rows);
625
- }
626
-
627
- results.forEach((result) => {
628
- const id = result[idColumn.name];
629
- const type = result[typeColumn.name];
630
-
631
- if (!type || !id) {
632
- result[key] = null;
633
- return;
634
- }
635
-
636
- const matchingRows = map[type][id];
637
-
638
- const fromTargetRow = (rowOrRows) => fromRow(db.metadata.get(type), rowOrRows);
639
-
640
- result[key] = fromTargetRow(_.first(matchingRows));
641
- });
642
- }
643
- }
644
- };
645
-
646
- module.exports = {
647
- processPopulate,
648
- applyPopulate,
649
- };