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