@nocobase/database 0.9.2-alpha.4 → 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.
- package/lib/collection.d.ts +9 -9
- package/lib/collection.js +104 -96
- package/lib/database.d.ts +4 -4
- package/lib/database.js +25 -53
- package/lib/eager-loading/eager-loading-tree.d.ts +23 -0
- package/lib/eager-loading/eager-loading-tree.js +338 -0
- package/lib/fields/field.js +1 -0
- package/lib/filter-parser.d.ts +1 -7
- package/lib/filter-parser.js +27 -7
- package/lib/listeners/adjacency-list.d.ts +1 -2
- package/lib/listeners/adjacency-list.js +2 -71
- package/lib/listeners/append-child-collection-name-after-repository-find.d.ts +5 -0
- package/lib/listeners/append-child-collection-name-after-repository-find.js +40 -0
- package/lib/listeners/index.js +2 -1
- package/lib/mock-database.js +3 -1
- package/lib/operators/array.js +7 -4
- package/lib/operators/string.js +1 -1
- package/lib/options-parser.js +14 -0
- package/lib/query-interface/postgres-query-interface.js +2 -2
- package/lib/relation-repository/belongs-to-many-repository.d.ts +2 -1
- package/lib/relation-repository/belongs-to-many-repository.js +58 -37
- package/lib/relation-repository/hasmany-repository.d.ts +2 -1
- package/lib/relation-repository/hasmany-repository.js +31 -16
- package/lib/relation-repository/multiple-relation-repository.js +8 -26
- package/lib/relation-repository/relation-repository.d.ts +1 -7
- package/lib/relation-repository/single-relation-repository.d.ts +1 -1
- package/lib/relation-repository/single-relation-repository.js +10 -16
- package/lib/repository.d.ts +11 -8
- package/lib/repository.js +106 -90
- package/lib/sql-parser/postgres.js +41 -0
- package/lib/tree-repository/adjacency-list-repository.d.ts +9 -0
- package/lib/tree-repository/adjacency-list-repository.js +165 -0
- package/lib/update-guard.d.ts +1 -1
- package/lib/update-guard.js +16 -13
- package/lib/utils.d.ts +0 -7
- package/lib/utils.js +0 -76
- package/package.json +4 -4
- package/src/__tests__/collection.test.ts +19 -0
- package/src/__tests__/eager-loading/eager-loading-tree.test.ts +393 -0
- package/src/__tests__/migrator.test.ts +4 -0
- package/src/__tests__/relation-repository/hasone-repository.test.ts +1 -0
- package/src/__tests__/repository/aggregation.test.ts +297 -0
- package/src/__tests__/repository/count.test.ts +1 -1
- package/src/__tests__/repository/find.test.ts +267 -1
- package/src/__tests__/repository.test.ts +30 -0
- package/src/__tests__/tree.test.ts +266 -3
- package/src/__tests__/update-guard.test.ts +13 -0
- package/src/collection.ts +79 -65
- package/src/database.ts +26 -42
- package/src/eager-loading/eager-loading-tree.ts +304 -0
- package/src/fields/field.ts +4 -0
- package/src/filter-parser.ts +16 -2
- package/src/listeners/adjacency-list.ts +1 -44
- package/src/listeners/append-child-collection-name-after-repository-find.ts +31 -0
- package/src/listeners/index.ts +3 -2
- package/src/mock-database.ts +3 -1
- package/src/operators/array.ts +8 -4
- package/src/operators/notIn.ts +1 -0
- package/src/operators/string.ts +1 -1
- package/src/options-parser.ts +17 -0
- package/src/query-interface/postgres-query-interface.ts +1 -1
- package/src/relation-repository/belongs-to-many-repository.ts +33 -1
- package/src/relation-repository/hasmany-repository.ts +17 -0
- package/src/relation-repository/multiple-relation-repository.ts +14 -19
- package/src/relation-repository/single-relation-repository.ts +13 -15
- package/src/repository.ts +83 -38
- package/src/sql-parser/postgres.js +25505 -0
- package/src/tree-repository/adjacency-list-repository.ts +159 -0
- package/src/update-guard.ts +21 -16
- package/src/utils.ts +0 -61
package/src/collection.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { BelongsToField, Field, FieldOptions, HasManyField } from './fields';
|
|
|
14
14
|
import { Model } from './model';
|
|
15
15
|
import { Repository } from './repository';
|
|
16
16
|
import { checkIdentifier, md5, snakeCase } from './utils';
|
|
17
|
+
import { AdjacencyListRepository } from './tree-repository/adjacency-list-repository';
|
|
17
18
|
|
|
18
19
|
export type RepositoryType = typeof Repository;
|
|
19
20
|
|
|
@@ -78,6 +79,31 @@ export class Collection<
|
|
|
78
79
|
model: ModelStatic<Model>;
|
|
79
80
|
repository: Repository<TModelAttributes, TCreationAttributes>;
|
|
80
81
|
|
|
82
|
+
constructor(options: CollectionOptions, context: CollectionContext) {
|
|
83
|
+
super();
|
|
84
|
+
this.context = context;
|
|
85
|
+
this.options = options;
|
|
86
|
+
|
|
87
|
+
this.checkOptions(options);
|
|
88
|
+
|
|
89
|
+
this.bindFieldEventListener();
|
|
90
|
+
this.modelInit();
|
|
91
|
+
|
|
92
|
+
this.db.modelCollection.set(this.model, this);
|
|
93
|
+
|
|
94
|
+
// set tableName to collection map
|
|
95
|
+
// the form of key is `${schema}.${tableName}` if schema exists
|
|
96
|
+
// otherwise is `${tableName}`
|
|
97
|
+
this.db.tableNameCollectionMap.set(this.getTableNameWithSchemaAsString(), this);
|
|
98
|
+
|
|
99
|
+
if (!options.inherits) {
|
|
100
|
+
this.setFields(options.fields);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.setRepository(options.repository);
|
|
104
|
+
this.setSortable(options.sortable);
|
|
105
|
+
}
|
|
106
|
+
|
|
81
107
|
get filterTargetKey() {
|
|
82
108
|
const targetKey = lodash.get(this.options, 'filterTargetKey', this.model.primaryKeyAttribute);
|
|
83
109
|
if (!targetKey && this.model.rawAttributes['id']) {
|
|
@@ -115,65 +141,12 @@ export class Collection<
|
|
|
115
141
|
}
|
|
116
142
|
}
|
|
117
143
|
|
|
118
|
-
constructor(options: CollectionOptions, context: CollectionContext) {
|
|
119
|
-
super();
|
|
120
|
-
this.context = context;
|
|
121
|
-
this.options = options;
|
|
122
|
-
|
|
123
|
-
this.checkOptions(options);
|
|
124
|
-
|
|
125
|
-
this.bindFieldEventListener();
|
|
126
|
-
this.modelInit();
|
|
127
|
-
|
|
128
|
-
this.db.modelCollection.set(this.model, this);
|
|
129
|
-
|
|
130
|
-
// set tableName to collection map
|
|
131
|
-
// the form of key is `${schema}.${tableName}` if schema exists
|
|
132
|
-
// otherwise is `${tableName}`
|
|
133
|
-
this.db.tableNameCollectionMap.set(this.getTableNameWithSchemaAsString(), this);
|
|
134
|
-
|
|
135
|
-
if (!options.inherits) {
|
|
136
|
-
this.setFields(options.fields);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
this.setRepository(options.repository);
|
|
140
|
-
this.setSortable(options.sortable);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
private checkOptions(options: CollectionOptions) {
|
|
144
|
-
checkIdentifier(options.name);
|
|
145
|
-
this.checkTableName();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private checkTableName() {
|
|
149
|
-
const tableName = this.tableName();
|
|
150
|
-
for (const [k, collection] of this.db.collections) {
|
|
151
|
-
if (
|
|
152
|
-
collection.name != this.options.name &&
|
|
153
|
-
tableName === collection.tableName() &&
|
|
154
|
-
collection.collectionSchema() === this.collectionSchema()
|
|
155
|
-
) {
|
|
156
|
-
throw new Error(`collection ${collection.name} and ${this.name} have same tableName "${tableName}"`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
144
|
tableName() {
|
|
162
145
|
const { name, tableName } = this.options;
|
|
163
146
|
const tName = tableName || name;
|
|
164
147
|
return this.options.underscored ? snakeCase(tName) : tName;
|
|
165
148
|
}
|
|
166
149
|
|
|
167
|
-
protected sequelizeModelOptions() {
|
|
168
|
-
const { name } = this.options;
|
|
169
|
-
return {
|
|
170
|
-
..._.omit(this.options, ['name', 'fields', 'model', 'targetKey']),
|
|
171
|
-
modelName: name,
|
|
172
|
-
sequelize: this.context.database.sequelize,
|
|
173
|
-
tableName: this.tableName(),
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
150
|
/**
|
|
178
151
|
* TODO
|
|
179
152
|
*/
|
|
@@ -223,18 +196,12 @@ export class Collection<
|
|
|
223
196
|
if (typeof repository === 'string') {
|
|
224
197
|
repo = this.context.database.repositories.get(repository) || Repository;
|
|
225
198
|
}
|
|
226
|
-
this.repository = new repo(this);
|
|
227
|
-
}
|
|
228
199
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
200
|
+
if (this.options.tree == 'adjacency-list' || this.options.tree == 'adjacencyList') {
|
|
201
|
+
repo = AdjacencyListRepository;
|
|
202
|
+
}
|
|
233
203
|
|
|
234
|
-
this.
|
|
235
|
-
field.unbind();
|
|
236
|
-
this.db.emit('field.afterRemove', field);
|
|
237
|
-
});
|
|
204
|
+
this.repository = new repo(this);
|
|
238
205
|
}
|
|
239
206
|
|
|
240
207
|
forEachField(callback: (field: Field) => void) {
|
|
@@ -293,12 +260,20 @@ export class Collection<
|
|
|
293
260
|
const [sourceCollectionName, sourceFieldName] = options.source.split('.');
|
|
294
261
|
const sourceCollection = this.db.collections.get(sourceCollectionName);
|
|
295
262
|
if (!sourceCollection) {
|
|
296
|
-
|
|
263
|
+
this.db.logger.warn(
|
|
297
264
|
`source collection "${sourceCollectionName}" not found for field "${name}" at collection "${this.name}"`,
|
|
298
265
|
);
|
|
299
266
|
}
|
|
267
|
+
|
|
300
268
|
const sourceField = sourceCollection.fields.get(sourceFieldName);
|
|
301
|
-
|
|
269
|
+
|
|
270
|
+
if (!sourceField) {
|
|
271
|
+
this.db.logger.warn(
|
|
272
|
+
`source field "${sourceFieldName}" not found for field "${name}" at collection "${this.name}"`,
|
|
273
|
+
);
|
|
274
|
+
} else {
|
|
275
|
+
options = { ...sourceField.options, ...options };
|
|
276
|
+
}
|
|
302
277
|
}
|
|
303
278
|
|
|
304
279
|
this.emit('field.beforeAdd', name, options, { collection: this });
|
|
@@ -672,4 +647,43 @@ export class Collection<
|
|
|
672
647
|
public isView() {
|
|
673
648
|
return false;
|
|
674
649
|
}
|
|
650
|
+
|
|
651
|
+
protected sequelizeModelOptions() {
|
|
652
|
+
const { name } = this.options;
|
|
653
|
+
return {
|
|
654
|
+
..._.omit(this.options, ['name', 'fields', 'model', 'targetKey']),
|
|
655
|
+
modelName: name,
|
|
656
|
+
sequelize: this.context.database.sequelize,
|
|
657
|
+
tableName: this.tableName(),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private checkOptions(options: CollectionOptions) {
|
|
662
|
+
checkIdentifier(options.name);
|
|
663
|
+
this.checkTableName();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private checkTableName() {
|
|
667
|
+
const tableName = this.tableName();
|
|
668
|
+
for (const [k, collection] of this.db.collections) {
|
|
669
|
+
if (
|
|
670
|
+
collection.name != this.options.name &&
|
|
671
|
+
tableName === collection.tableName() &&
|
|
672
|
+
collection.collectionSchema() === this.collectionSchema()
|
|
673
|
+
) {
|
|
674
|
+
throw new Error(`collection ${collection.name} and ${this.name} have same tableName "${tableName}"`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private bindFieldEventListener() {
|
|
680
|
+
this.on('field.afterAdd', (field: Field) => {
|
|
681
|
+
field.bind();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
this.on('field.afterRemove', (field: Field) => {
|
|
685
|
+
field.unbind();
|
|
686
|
+
this.db.emit('field.afterRemove', field);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
675
689
|
}
|
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
|
|
71
|
-
import {
|
|
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
|
+
}
|
package/src/fields/field.ts
CHANGED
|
@@ -217,6 +217,7 @@ export abstract class Field {
|
|
|
217
217
|
bind() {
|
|
218
218
|
const { model } = this.context.collection;
|
|
219
219
|
model.rawAttributes[this.name] = this.toSequelize();
|
|
220
|
+
|
|
220
221
|
// @ts-ignore
|
|
221
222
|
model.refreshAttributes();
|
|
222
223
|
if (this.options.index) {
|
|
@@ -226,6 +227,9 @@ export abstract class Field {
|
|
|
226
227
|
|
|
227
228
|
unbind() {
|
|
228
229
|
const { model } = this.context.collection;
|
|
230
|
+
|
|
231
|
+
delete model.prototype[this.name];
|
|
232
|
+
|
|
229
233
|
model.removeAttribute(this.name);
|
|
230
234
|
if (this.options.index || this.options.unique) {
|
|
231
235
|
this.context.collection.removeIndex([this.name]);
|
package/src/filter-parser.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|