@nocobase/database 0.9.3-alpha.1 → 0.9.4-alpha.1

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 (59) hide show
  1. package/lib/collection.d.ts +9 -9
  2. package/lib/collection.js +100 -96
  3. package/lib/database.d.ts +4 -4
  4. package/lib/database.js +25 -53
  5. package/lib/eager-loading/eager-loading-tree.d.ts +23 -0
  6. package/lib/eager-loading/eager-loading-tree.js +338 -0
  7. package/lib/filter-parser.d.ts +1 -7
  8. package/lib/filter-parser.js +27 -7
  9. package/lib/listeners/append-child-collection-name-after-repository-find.d.ts +5 -0
  10. package/lib/listeners/append-child-collection-name-after-repository-find.js +40 -0
  11. package/lib/listeners/index.js +2 -0
  12. package/lib/mock-database.js +3 -1
  13. package/lib/operators/string.js +1 -1
  14. package/lib/options-parser.js +4 -0
  15. package/lib/query-interface/postgres-query-interface.js +2 -2
  16. package/lib/relation-repository/belongs-to-many-repository.d.ts +2 -1
  17. package/lib/relation-repository/belongs-to-many-repository.js +58 -37
  18. package/lib/relation-repository/hasmany-repository.d.ts +2 -1
  19. package/lib/relation-repository/hasmany-repository.js +31 -16
  20. package/lib/relation-repository/multiple-relation-repository.js +8 -26
  21. package/lib/relation-repository/relation-repository.d.ts +1 -7
  22. package/lib/relation-repository/single-relation-repository.d.ts +1 -1
  23. package/lib/relation-repository/single-relation-repository.js +10 -16
  24. package/lib/repository.d.ts +11 -8
  25. package/lib/repository.js +104 -89
  26. package/lib/sql-parser/postgres.js +41 -0
  27. package/lib/update-guard.d.ts +1 -1
  28. package/lib/update-guard.js +16 -13
  29. package/lib/utils.d.ts +0 -7
  30. package/lib/utils.js +0 -76
  31. package/package.json +4 -4
  32. package/src/__tests__/eager-loading/eager-loading-tree.test.ts +393 -0
  33. package/src/__tests__/migrator.test.ts +4 -0
  34. package/src/__tests__/relation-repository/hasone-repository.test.ts +1 -0
  35. package/src/__tests__/repository/aggregation.test.ts +297 -0
  36. package/src/__tests__/repository/count.test.ts +1 -1
  37. package/src/__tests__/repository/find.test.ts +10 -1
  38. package/src/__tests__/repository.test.ts +30 -0
  39. package/src/__tests__/update-guard.test.ts +13 -0
  40. package/src/collection.ts +74 -66
  41. package/src/database.ts +26 -42
  42. package/src/eager-loading/eager-loading-tree.ts +304 -0
  43. package/src/filter-parser.ts +16 -2
  44. package/src/listeners/adjacency-list.ts +1 -3
  45. package/src/listeners/append-child-collection-name-after-repository-find.ts +31 -0
  46. package/src/listeners/index.ts +2 -0
  47. package/src/mock-database.ts +3 -1
  48. package/src/operators/notIn.ts +1 -0
  49. package/src/operators/string.ts +1 -1
  50. package/src/options-parser.ts +5 -0
  51. package/src/query-interface/postgres-query-interface.ts +1 -1
  52. package/src/relation-repository/belongs-to-many-repository.ts +33 -1
  53. package/src/relation-repository/hasmany-repository.ts +17 -0
  54. package/src/relation-repository/multiple-relation-repository.ts +14 -19
  55. package/src/relation-repository/single-relation-repository.ts +13 -15
  56. package/src/repository.ts +79 -36
  57. package/src/sql-parser/postgres.js +25505 -0
  58. package/src/update-guard.ts +21 -16
  59. package/src/utils.ts +0 -61
package/src/database.ts CHANGED
@@ -62,13 +62,13 @@ import {
62
62
  } from './types';
63
63
  import { patchSequelizeQueryInterface, snakeCase } from './utils';
64
64
 
65
+ import { Logger } from '@nocobase/logger';
66
+ import { CollectionGroupManager } from './collection-group-manager';
65
67
  import DatabaseUtils from './database-utils';
66
68
  import { registerBuiltInListeners } from './listeners';
67
- import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
68
- import buildQueryInterface from './query-interface/query-interface-builder';
69
69
  import QueryInterface from './query-interface/query-interface';
70
- import { Logger } from '@nocobase/logger';
71
- import { CollectionGroupManager } from './collection-group-manager';
70
+ import buildQueryInterface from './query-interface/query-interface-builder';
71
+ import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
72
72
  import { ViewCollection } from './view-collection';
73
73
 
74
74
  export type MergeOptions = merge.Options;
@@ -188,6 +188,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
188
188
  logger: Logger;
189
189
 
190
190
  collectionGroupManager = new CollectionGroupManager(this);
191
+ declare emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
191
192
 
192
193
  constructor(options: DatabaseOptions) {
193
194
  super();
@@ -275,6 +276,12 @@ export class Database extends EventEmitter implements AsyncEmitter {
275
276
  }),
276
277
  });
277
278
 
279
+ this.sequelize.beforeDefine((model, opts) => {
280
+ if (this.options.tablePrefix) {
281
+ opts.tableName = `${this.options.tablePrefix}${opts.tableName || opts.modelName || opts.name.plural}`;
282
+ }
283
+ });
284
+
278
285
  this.collection({
279
286
  name: 'migrations',
280
287
  autoGenId: false,
@@ -284,12 +291,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
284
291
  fields: [{ type: 'string', name: 'name' }],
285
292
  });
286
293
 
287
- this.sequelize.beforeDefine((model, opts) => {
288
- if (this.options.tablePrefix) {
289
- opts.tableName = `${this.options.tablePrefix}${opts.tableName || opts.modelName || opts.name.plural}`;
290
- }
291
- });
292
-
293
294
  this.initListener();
294
295
  patchSequelizeQueryInterface(this);
295
296
  }
@@ -387,36 +388,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
387
388
  }
388
389
  });
389
390
 
390
- this.on('afterRepositoryFind', ({ findOptions, dataCollection, data }) => {
391
- if (dataCollection.isParent()) {
392
- for (const row of data) {
393
- const rowCollection = this.tableNameCollectionMap.get(
394
- findOptions.raw
395
- ? `${row['__schemaName']}.${row['__tableName']}`
396
- : `${row.get('__schemaName')}.${row.get('__tableName')}`,
397
- );
398
-
399
- if (!rowCollection) {
400
- this.logger.warn(
401
- `Can not find collection by table name ${JSON.stringify(row)}, current collections: ${Array.from(
402
- this.tableNameCollectionMap.keys(),
403
- ).join(', ')}`,
404
- );
405
-
406
- return;
407
- }
408
-
409
- const rowCollectionName = rowCollection.name;
410
-
411
- findOptions.raw
412
- ? (row['__collection'] = rowCollectionName)
413
- : row.set('__collection', rowCollectionName, {
414
- raw: true,
415
- });
416
- }
417
- }
418
- });
419
-
420
391
  registerBuiltInListeners(this);
421
392
  }
422
393
 
@@ -565,7 +536,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
565
536
  }
566
537
 
567
538
  getRepository<R extends Repository>(name: string): R;
539
+
568
540
  getRepository<R extends RelationRepository>(name: string, relationId: string | number): R;
541
+
569
542
  getRepository<R extends ArrayFieldRepository>(name: string, relationId: string | number): R;
570
543
 
571
544
  getRepository<R extends RelationRepository>(name: string, relationId?: string | number): Repository | R {
@@ -779,21 +752,34 @@ export class Database extends EventEmitter implements AsyncEmitter {
779
752
  }
780
753
 
781
754
  on(event: EventType, listener: any): this;
755
+
782
756
  on(event: ModelValidateEventTypes, listener: SyncListener): this;
757
+
783
758
  on(event: ModelValidateEventTypes, listener: ValidateListener): this;
759
+
784
760
  on(event: ModelCreateEventTypes, listener: CreateListener): this;
761
+
785
762
  on(event: ModelUpdateEventTypes, listener: UpdateListener): this;
763
+
786
764
  on(event: ModelSaveEventTypes, listener: SaveListener): this;
765
+
787
766
  on(event: ModelDestroyEventTypes, listener: DestroyListener): this;
767
+
788
768
  on(event: ModelCreateWithAssociationsEventTypes, listener: CreateWithAssociationsListener): this;
769
+
789
770
  on(event: ModelUpdateWithAssociationsEventTypes, listener: UpdateWithAssociationsListener): this;
771
+
790
772
  on(event: ModelSaveWithAssociationsEventTypes, listener: SaveWithAssociationsListener): this;
773
+
791
774
  on(event: DatabaseBeforeDefineCollectionEventType, listener: BeforeDefineCollectionListener): this;
775
+
792
776
  on(event: DatabaseAfterDefineCollectionEventType, listener: AfterDefineCollectionListener): this;
777
+
793
778
  on(
794
779
  event: DatabaseBeforeRemoveCollectionEventType | DatabaseAfterRemoveCollectionEventType,
795
780
  listener: RemoveCollectionListener,
796
781
  ): this;
782
+
797
783
  on(event: EventType, listener: any): this {
798
784
  // NOTE: to match if event is a sequelize or model type
799
785
  const type = this.modelHook.match(event);
@@ -844,8 +830,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
844
830
 
845
831
  return result;
846
832
  }
847
-
848
- declare emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
849
833
  }
850
834
 
851
835
  export function extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions) {
@@ -0,0 +1,304 @@
1
+ import { Association, Includeable, Model, ModelStatic, Transaction } from 'sequelize';
2
+ import lodash from 'lodash';
3
+
4
+ interface EagerLoadingNode {
5
+ model: ModelStatic<any>;
6
+ association: Association;
7
+ attributes: Array<string>;
8
+ rawAttributes: Array<string>;
9
+ children: Array<EagerLoadingNode>;
10
+ parent?: EagerLoadingNode;
11
+ instances?: Array<Model>;
12
+ order?: any;
13
+ }
14
+
15
+ export class EagerLoadingTree {
16
+ public root: EagerLoadingNode;
17
+
18
+ constructor(root: EagerLoadingNode) {
19
+ this.root = root;
20
+ }
21
+
22
+ static buildFromSequelizeOptions(options: {
23
+ model: ModelStatic<any>;
24
+ rootAttributes: Array<string>;
25
+ rootOrder?: any;
26
+ includeOption: Includeable | Includeable[];
27
+ }): EagerLoadingTree {
28
+ const { model, rootAttributes, includeOption } = options;
29
+
30
+ const root = {
31
+ model,
32
+ association: null,
33
+ rawAttributes: lodash.cloneDeep(rootAttributes),
34
+ attributes: lodash.cloneDeep(rootAttributes),
35
+ order: options.rootOrder,
36
+ children: [],
37
+ };
38
+
39
+ const pushAttribute = (node, attribute) => {
40
+ if (lodash.isArray(node.attributes) && !node.attributes.includes(attribute)) {
41
+ node.attributes.push(attribute);
42
+ }
43
+ };
44
+
45
+ const traverseIncludeOption = (includeOption, eagerLoadingTreeParent) => {
46
+ const includeOptions = lodash.castArray(includeOption);
47
+
48
+ if (includeOption.length > 0) {
49
+ const modelPrimaryKey = eagerLoadingTreeParent.model.primaryKeyAttribute;
50
+ pushAttribute(eagerLoadingTreeParent, modelPrimaryKey);
51
+ }
52
+
53
+ for (const include of includeOptions) {
54
+ // skip fromFilter include option
55
+ if (include.fromFilter) {
56
+ continue;
57
+ }
58
+
59
+ const association = eagerLoadingTreeParent.model.associations[include.association];
60
+ const associationType = association.associationType;
61
+
62
+ const child = {
63
+ model: association.target,
64
+ association,
65
+ rawAttributes: lodash.cloneDeep(include.attributes),
66
+ attributes: lodash.cloneDeep(include.attributes),
67
+ parent: eagerLoadingTreeParent,
68
+ children: [],
69
+ };
70
+
71
+ if (associationType == 'HasOne' || associationType == 'HasMany') {
72
+ const { sourceKey, foreignKey } = association;
73
+
74
+ pushAttribute(eagerLoadingTreeParent, sourceKey);
75
+ pushAttribute(child, foreignKey);
76
+ }
77
+
78
+ if (associationType == 'BelongsTo') {
79
+ const { targetKey, foreignKey } = association;
80
+
81
+ pushAttribute(eagerLoadingTreeParent, foreignKey);
82
+ pushAttribute(child, targetKey);
83
+ }
84
+
85
+ if (associationType == 'BelongsToMany') {
86
+ const { sourceKey } = association;
87
+ pushAttribute(eagerLoadingTreeParent, sourceKey);
88
+ }
89
+
90
+ eagerLoadingTreeParent.children.push(child);
91
+
92
+ if (include.include) {
93
+ traverseIncludeOption(include.include, child);
94
+ }
95
+ }
96
+ };
97
+
98
+ traverseIncludeOption(includeOption, root);
99
+
100
+ return new EagerLoadingTree(root);
101
+ }
102
+
103
+ async load(pks: Array<string | number>, transaction?: Transaction) {
104
+ const result = {};
105
+
106
+ const loadRecursive = async (node, ids) => {
107
+ const modelPrimaryKey = node.model.primaryKeyAttribute;
108
+
109
+ let instances = [];
110
+
111
+ // load instances from database
112
+ if (!node.parent) {
113
+ const findOptions = {
114
+ where: { [modelPrimaryKey]: ids },
115
+ attributes: node.attributes,
116
+ };
117
+
118
+ if (node.order) {
119
+ findOptions['order'] = node.order;
120
+ }
121
+
122
+ instances = await node.model.findAll({
123
+ ...findOptions,
124
+ transaction,
125
+ });
126
+ } else if (ids.length > 0) {
127
+ const association = node.association;
128
+ const associationType = association.associationType;
129
+
130
+ if (associationType == 'HasOne' || associationType == 'HasMany') {
131
+ const foreignKey = association.foreignKey;
132
+ const foreignKeyValues = node.parent.instances.map((instance) => instance.get(association.sourceKey));
133
+
134
+ const findOptions = {
135
+ where: { [foreignKey]: foreignKeyValues },
136
+ attributes: node.attributes,
137
+ transaction,
138
+ };
139
+
140
+ instances = await node.model.findAll(findOptions);
141
+ }
142
+
143
+ if (associationType == 'BelongsTo') {
144
+ const foreignKey = association.foreignKey;
145
+
146
+ const parentInstancesForeignKeyValues = node.parent.instances.map((instance) => instance.get(foreignKey));
147
+
148
+ instances = await node.model.findAll({
149
+ transaction,
150
+ where: {
151
+ [association.targetKey]: parentInstancesForeignKeyValues,
152
+ },
153
+ attributes: node.attributes,
154
+ });
155
+ }
156
+
157
+ if (associationType == 'BelongsToMany') {
158
+ const foreignKeyValues = node.parent.instances.map((instance) => instance.get(association.sourceKey));
159
+
160
+ instances = await node.model.findAll({
161
+ transaction,
162
+ attributes: node.attributes,
163
+ include: [
164
+ {
165
+ association: association.oneFromTarget,
166
+ where: {
167
+ [association.foreignKey]: foreignKeyValues,
168
+ },
169
+ },
170
+ ],
171
+ });
172
+ }
173
+ }
174
+
175
+ node.instances = instances;
176
+
177
+ for (const child of node.children) {
178
+ const nodeIds = instances.map((instance) => instance.get(modelPrimaryKey));
179
+
180
+ await loadRecursive(child, nodeIds);
181
+ }
182
+
183
+ // merge instances to parent
184
+ if (!node.parent) {
185
+ return;
186
+ } else {
187
+ const association = node.association;
188
+ const associationType = association.associationType;
189
+
190
+ const setParentAccessor = (parentInstance) => {
191
+ const key = association.as;
192
+
193
+ const children = parentInstance.getDataValue(association.as);
194
+
195
+ if (association.isSingleAssociation) {
196
+ const isEmpty = !children;
197
+ parentInstance[key] = parentInstance.dataValues[key] = isEmpty ? null : children;
198
+ } else {
199
+ const isEmpty = !children || children.length == 0;
200
+ parentInstance[key] = parentInstance.dataValues[key] = isEmpty ? [] : children;
201
+ }
202
+ };
203
+
204
+ if (associationType == 'HasMany' || associationType == 'HasOne') {
205
+ const foreignKey = association.foreignKey;
206
+ const sourceKey = association.sourceKey;
207
+
208
+ for (const instance of node.instances) {
209
+ const parentInstance = node.parent.instances.find(
210
+ (parentInstance) => parentInstance.get(sourceKey) == instance.get(foreignKey),
211
+ );
212
+
213
+ if (parentInstance) {
214
+ if (associationType == 'HasMany') {
215
+ const children = parentInstance.getDataValue(association.as);
216
+ if (!children) {
217
+ parentInstance.setDataValue(association.as, [instance]);
218
+ } else {
219
+ children.push(instance);
220
+ }
221
+ }
222
+
223
+ if (associationType == 'HasOne') {
224
+ parentInstance.setDataValue(association.as, instance);
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ if (associationType == 'BelongsTo') {
231
+ const foreignKey = association.foreignKey;
232
+ const targetKey = association.targetKey;
233
+
234
+ for (const instance of node.instances) {
235
+ const parentInstances = node.parent.instances.filter(
236
+ (parentInstance) => parentInstance.get(foreignKey) == instance.get(targetKey),
237
+ );
238
+
239
+ for (const parentInstance of parentInstances) {
240
+ parentInstance.setDataValue(association.as, instance);
241
+ }
242
+ }
243
+ }
244
+
245
+ if (associationType == 'BelongsToMany') {
246
+ const sourceKey = association.sourceKey;
247
+ const foreignKey = association.foreignKey;
248
+
249
+ const oneFromTarget = association.oneFromTarget;
250
+
251
+ for (const instance of node.instances) {
252
+ const parentInstance = node.parent.instances.find(
253
+ (parentInstance) => parentInstance.get(sourceKey) == instance[oneFromTarget.as].get(foreignKey),
254
+ );
255
+
256
+ if (parentInstance) {
257
+ const children = parentInstance.getDataValue(association.as);
258
+
259
+ if (!children) {
260
+ parentInstance.setDataValue(association.as, [instance]);
261
+ } else {
262
+ children.push(instance);
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ for (const parent of node.parent.instances) {
269
+ setParentAccessor(parent);
270
+ }
271
+ }
272
+ };
273
+
274
+ await loadRecursive(this.root, pks);
275
+
276
+ const setInstanceAttributes = (node) => {
277
+ const nodeRawAttributes = node.rawAttributes;
278
+
279
+ if (!lodash.isArray(nodeRawAttributes)) {
280
+ return;
281
+ }
282
+
283
+ const nodeChildrenAs = node.children.map((child) => child.association.as);
284
+ const includeAttributes = [...nodeRawAttributes, ...nodeChildrenAs];
285
+
286
+ for (const instance of node.instances) {
287
+ const attributes = lodash.pick(instance.dataValues, includeAttributes);
288
+ instance.dataValues = attributes;
289
+ }
290
+ };
291
+
292
+ // traverse tree and set instance attributes
293
+ const traverse = (node) => {
294
+ setInstanceAttributes(node);
295
+
296
+ for (const child of node.children) {
297
+ traverse(child);
298
+ }
299
+ };
300
+
301
+ traverse(this.root);
302
+ return result;
303
+ }
304
+ }
@@ -56,7 +56,7 @@ export default class FilterParser {
56
56
  return filter;
57
57
  }
58
58
 
59
- toSequelizeParams() {
59
+ toSequelizeParams(): any {
60
60
  debug('filter %o', this.filter);
61
61
 
62
62
  if (!this.filter) {
@@ -230,7 +230,21 @@ export default class FilterParser {
230
230
  });
231
231
  };
232
232
  debug('where %o, include %o', where, include);
233
- return { where, include: toInclude(include) };
233
+ const results = { where, include: toInclude(include) };
234
+
235
+ //traverse filter include, set fromFiler to true
236
+ const traverseInclude = (include) => {
237
+ for (const item of include) {
238
+ if (item.include) {
239
+ traverseInclude(item.include);
240
+ }
241
+ item.fromFilter = true;
242
+ }
243
+ };
244
+
245
+ traverseInclude(results.include);
246
+
247
+ return results;
234
248
  }
235
249
 
236
250
  private getFieldNameFromQueryPath(queryPath: string) {
@@ -1,6 +1,4 @@
1
- import lodash from 'lodash';
2
- import { Collection, CollectionOptions } from '../collection';
3
- import { Model } from '../model';
1
+ import { CollectionOptions } from '../collection';
4
2
 
5
3
  export const beforeDefineAdjacencyListCollection = (options: CollectionOptions) => {
6
4
  if (!options.tree) {
@@ -0,0 +1,31 @@
1
+ export const appendChildCollectionNameAfterRepositoryFind = (db) => {
2
+ return ({ findOptions, dataCollection, data }) => {
3
+ if (dataCollection.isParent()) {
4
+ for (const row of data) {
5
+ const rowCollection = db.tableNameCollectionMap.get(
6
+ findOptions.raw
7
+ ? `${row['__schemaName']}.${row['__tableName']}`
8
+ : `${row.get('__schemaName')}.${row.get('__tableName')}`,
9
+ );
10
+
11
+ if (!rowCollection) {
12
+ db.logger.warn(
13
+ `Can not find collection by table name ${JSON.stringify(row)}, current collections: ${Array.from(
14
+ db.tableNameCollectionMap.keys(),
15
+ ).join(', ')}`,
16
+ );
17
+
18
+ return;
19
+ }
20
+
21
+ const rowCollectionName = rowCollection.name;
22
+
23
+ findOptions.raw
24
+ ? (row['__collection'] = rowCollectionName)
25
+ : row.set('__collection', rowCollectionName, {
26
+ raw: true,
27
+ });
28
+ }
29
+ }
30
+ };
31
+ };
@@ -1,6 +1,8 @@
1
1
  import { Database } from '../database';
2
2
  import { beforeDefineAdjacencyListCollection } from './adjacency-list';
3
+ import { appendChildCollectionNameAfterRepositoryFind } from './append-child-collection-name-after-repository-find';
3
4
 
4
5
  export const registerBuiltInListeners = (db: Database) => {
5
6
  db.on('beforeDefineCollection', beforeDefineAdjacencyListCollection);
7
+ db.on('afterRepositoryFind', appendChildCollectionNameAfterRepositoryFind(db));
6
8
  };
@@ -45,7 +45,9 @@ export function getConfigByEnv() {
45
45
 
46
46
  function customLogger(queryString, queryObject) {
47
47
  console.log(queryString); // outputs a string
48
- console.log(queryObject.bind); // outputs an array
48
+ if (queryObject.bind) {
49
+ console.log(queryObject.bind); // outputs an array
50
+ }
49
51
  }
50
52
 
51
53
  export function mockDatabase(options: IDatabaseOptions = {}): MockDatabase {
@@ -10,3 +10,4 @@ export default {
10
10
  };
11
11
  },
12
12
  } as Record<string, any>;
13
+
@@ -1,5 +1,5 @@
1
- import { isPg } from './utils';
2
1
  import { Op } from 'sequelize';
2
+ import { isPg } from './utils';
3
3
 
4
4
  export default {
5
5
  $includes(value, ctx) {
@@ -269,6 +269,11 @@ export class OptionsParser {
269
269
  (include) => include['association'] == appendAssociation,
270
270
  );
271
271
 
272
+ // if include from filter, remove fromFilter attribute
273
+ if (existIncludeIndex != -1) {
274
+ delete queryParams['include'][existIncludeIndex]['fromFilter'];
275
+ }
276
+
272
277
  // if association not exist, create it
273
278
  if (existIncludeIndex == -1) {
274
279
  // association not exists
@@ -1,6 +1,6 @@
1
1
  import QueryInterface from './query-interface';
2
2
  import { Collection } from '../collection';
3
- import sqlParser from '../sql-parser';
3
+ import sqlParser from '../sql-parser/postgres';
4
4
 
5
5
  export default class PostgresQueryInterface extends QueryInterface {
6
6
  constructor(db) {
@@ -1,7 +1,15 @@
1
1
  import lodash from 'lodash';
2
2
  import { BelongsToMany, Op, Transaction } from 'sequelize';
3
3
  import { Model } from '../model';
4
- import { CreateOptions, DestroyOptions, FindOneOptions, FindOptions, TargetKey, UpdateOptions } from '../repository';
4
+ import {
5
+ AggregateOptions,
6
+ CreateOptions,
7
+ DestroyOptions,
8
+ FindOneOptions,
9
+ FindOptions,
10
+ TargetKey,
11
+ UpdateOptions,
12
+ } from '../repository';
5
13
  import { updateThroughTableValue } from '../update-associations';
6
14
  import { FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository';
7
15
  import { transaction } from './relation-repository';
@@ -38,6 +46,30 @@ interface IBelongsToManyRepository<M extends Model> {
38
46
  }
39
47
 
40
48
  export class BelongsToManyRepository extends MultipleRelationRepository implements IBelongsToManyRepository<any> {
49
+ async aggregate(options: AggregateOptions) {
50
+ const targetRepository = this.targetCollection.repository;
51
+
52
+ const sourceModel = await this.getSourceModel();
53
+
54
+ const association = this.association as any;
55
+
56
+ return await targetRepository.aggregate({
57
+ ...options,
58
+ optionsTransformer: (modelOptions) => {
59
+ modelOptions.include = modelOptions.include || [];
60
+ const throughWhere = {};
61
+ throughWhere[association.foreignKey] = sourceModel.get(association.sourceKey);
62
+
63
+ modelOptions.include.push({
64
+ association: association.oneFromTarget,
65
+ required: true,
66
+ attributes: [],
67
+ where: throughWhere,
68
+ });
69
+ },
70
+ });
71
+ }
72
+
41
73
  @transaction()
42
74
  async create(options?: CreateBelongsToManyOptions): Promise<any> {
43
75
  if (Array.isArray(options.values)) {
@@ -2,6 +2,7 @@ import { omit } from 'lodash';
2
2
  import { HasMany, Op } from 'sequelize';
3
3
  import { Model } from '../model';
4
4
  import {
5
+ AggregateOptions,
5
6
  CreateOptions,
6
7
  DestroyOptions,
7
8
  FindOneOptions,
@@ -53,6 +54,22 @@ export class HasManyRepository extends MultipleRelationRepository implements IHa
53
54
  return await targetRepository.find(findOptions);
54
55
  }
55
56
 
57
+ async aggregate(options: AggregateOptions) {
58
+ const targetRepository = this.targetCollection.repository;
59
+ const addFilter = {
60
+ [this.association.foreignKey]: this.sourceKeyValue,
61
+ };
62
+
63
+ const aggOptions = {
64
+ ...options,
65
+ filter: {
66
+ $and: [options.filter || {}, addFilter],
67
+ },
68
+ };
69
+
70
+ return await targetRepository.aggregate(aggOptions);
71
+ }
72
+
56
73
  @transaction((args, transaction) => {
57
74
  return {
58
75
  filterByTk: args[0],