@nocobase/database 0.7.7-alpha.1 → 0.8.0-alpha.5

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 +1 -1
  2. package/lib/collection.js +1 -0
  3. package/lib/database.d.ts +17 -2
  4. package/lib/database.js +53 -41
  5. package/lib/features/ReferencesMap.d.ts +15 -0
  6. package/lib/features/ReferencesMap.js +60 -0
  7. package/lib/features/referential-integrity-check.d.ts +8 -0
  8. package/lib/features/referential-integrity-check.js +89 -0
  9. package/lib/fields/belongs-to-field.d.ts +2 -0
  10. package/lib/fields/belongs-to-field.js +16 -2
  11. package/lib/fields/belongs-to-many-field.d.ts +1 -0
  12. package/lib/fields/belongs-to-many-field.js +8 -2
  13. package/lib/fields/context-field.d.ts +2 -1
  14. package/lib/fields/context-field.js +12 -8
  15. package/lib/fields/field.d.ts +2 -1
  16. package/lib/fields/field.js +1 -0
  17. package/lib/fields/has-many-field.d.ts +2 -0
  18. package/lib/fields/has-many-field.js +19 -1
  19. package/lib/fields/has-one-field.d.ts +2 -0
  20. package/lib/fields/has-one-field.js +16 -2
  21. package/lib/filter-parser.js +5 -3
  22. package/lib/model.d.ts +1 -0
  23. package/lib/options-parser.js +3 -2
  24. package/lib/relation-repository/belongs-to-many-repository.d.ts +3 -3
  25. package/lib/relation-repository/hasmany-repository.d.ts +2 -2
  26. package/lib/relation-repository/hasone-repository.d.ts +2 -4
  27. package/lib/relation-repository/multiple-relation-repository.d.ts +2 -5
  28. package/lib/relation-repository/relation-repository.js +2 -2
  29. package/lib/relation-repository/single-relation-repository.d.ts +1 -1
  30. package/lib/repository.d.ts +25 -15
  31. package/lib/repository.js +3 -1
  32. package/lib/types.d.ts +43 -0
  33. package/lib/types.js +5 -0
  34. package/package.json +3 -3
  35. package/src/__tests__/fields/belongs-to-field.test.ts +112 -4
  36. package/src/__tests__/fields/has-many-field.test.ts +83 -0
  37. package/src/__tests__/relation-repository/hasone-repository.test.ts +3 -3
  38. package/src/__tests__/repository/find.test.ts +10 -0
  39. package/src/collection.ts +3 -1
  40. package/src/database.ts +64 -15
  41. package/src/features/ReferencesMap.ts +64 -0
  42. package/src/features/referential-integrity-check.ts +61 -0
  43. package/src/fields/belongs-to-field.ts +21 -1
  44. package/src/fields/belongs-to-many-field.ts +6 -0
  45. package/src/fields/context-field.ts +5 -7
  46. package/src/fields/field.ts +4 -3
  47. package/src/fields/has-many-field.ts +25 -2
  48. package/src/fields/has-one-field.ts +22 -2
  49. package/src/filter-parser.ts +5 -1
  50. package/src/model.ts +1 -0
  51. package/src/options-parser.ts +4 -2
  52. package/src/relation-repository/belongs-to-many-repository.ts +3 -3
  53. package/src/relation-repository/hasmany-repository.ts +8 -5
  54. package/src/relation-repository/hasone-repository.ts +2 -4
  55. package/src/relation-repository/multiple-relation-repository.ts +3 -4
  56. package/src/relation-repository/relation-repository.ts +1 -1
  57. package/src/relation-repository/single-relation-repository.ts +1 -1
  58. package/src/repository.ts +41 -19
  59. package/src/types.ts +64 -0
@@ -1,7 +1,7 @@
1
- import { mockDatabase } from '../index';
2
- import { HasOneRepository } from '../../relation-repository/hasone-repository';
3
- import Database from '../../database';
4
1
  import { Collection } from '../../collection';
2
+ import Database from '../../database';
3
+ import { HasOneRepository } from '../../relation-repository/hasone-repository';
4
+ import { mockDatabase } from '../index';
5
5
 
6
6
  describe('has one repository', () => {
7
7
  let db: Database;
@@ -163,6 +163,16 @@ describe('repository find', () => {
163
163
  expect(users[1]).toEqual(2);
164
164
  });
165
165
 
166
+ test('find with empty or', async () => {
167
+ const usersCount = await User.repository.count({
168
+ filter: {
169
+ $or: [],
170
+ },
171
+ });
172
+
173
+ expect(usersCount).toEqual(await User.repository.count());
174
+ });
175
+
166
176
  test('find with fields', async () => {
167
177
  let user = await User.repository.findOne({
168
178
  fields: ['name'],
package/src/collection.ts CHANGED
@@ -65,7 +65,7 @@ export class Collection<
65
65
  return this.context.database;
66
66
  }
67
67
 
68
- constructor(options: CollectionOptions, context?: CollectionContext) {
68
+ constructor(options: CollectionOptions, context: CollectionContext) {
69
69
  super();
70
70
  this.checkOptions(options);
71
71
 
@@ -74,6 +74,8 @@ export class Collection<
74
74
 
75
75
  this.bindFieldEventListener();
76
76
  this.modelInit();
77
+ this.db.modelCollection.set(this.model, this);
78
+
77
79
  this.setFields(options.fields);
78
80
  this.setRepository(options.repository);
79
81
  this.setSortable(options.sortable);
package/src/database.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { applyMixins, AsyncEmitter } from '@nocobase/utils';
1
+ import { applyMixins, AsyncEmitter, requireModule } from '@nocobase/utils';
2
2
  import merge from 'deepmerge';
3
3
  import { EventEmitter } from 'events';
4
4
  import glob from 'glob';
@@ -14,7 +14,7 @@ import {
14
14
  Sequelize,
15
15
  SyncOptions,
16
16
  Transactionable,
17
- Utils
17
+ Utils,
18
18
  } from 'sequelize';
19
19
  import { SequelizeStorage, Umzug } from 'umzug';
20
20
  import { Collection, CollectionOptions, RepositoryType } from './collection';
@@ -27,6 +27,35 @@ import { ModelHook } from './model-hook';
27
27
  import extendOperators from './operators';
28
28
  import { RelationRepository } from './relation-repository/relation-repository';
29
29
  import { Repository } from './repository';
30
+ import {
31
+ AfterDefineCollectionListener,
32
+ BeforeDefineCollectionListener,
33
+ CreateListener,
34
+ CreateWithAssociationsListener,
35
+ DatabaseAfterDefineCollectionEventType,
36
+ DatabaseAfterRemoveCollectionEventType,
37
+ DatabaseBeforeDefineCollectionEventType,
38
+ DatabaseBeforeRemoveCollectionEventType,
39
+ DestroyListener,
40
+ EventType,
41
+ ModelCreateEventTypes,
42
+ ModelCreateWithAssociationsEventTypes,
43
+ ModelDestroyEventTypes,
44
+ ModelSaveEventTypes,
45
+ ModelSaveWithAssociationsEventTypes,
46
+ ModelUpdateEventTypes,
47
+ ModelUpdateWithAssociationsEventTypes,
48
+ ModelValidateEventTypes,
49
+ RemoveCollectionListener,
50
+ SaveListener,
51
+ SaveWithAssociationsListener,
52
+ SyncListener,
53
+ UpdateListener,
54
+ UpdateWithAssociationsListener,
55
+ ValidateListener,
56
+ } from './types';
57
+ import { referentialIntegrityCheck } from './features/referential-integrity-check';
58
+ import ReferencesMap from './features/ReferencesMap';
30
59
 
31
60
  export interface MergeOptions extends merge.Options {}
32
61
 
@@ -119,6 +148,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
119
148
  collections = new Map<string, Collection>();
120
149
  pendingFields = new Map<string, RelationField[]>();
121
150
  modelCollection = new Map<ModelCtor<any>, Collection>();
151
+ referenceMap = new ReferencesMap();
122
152
 
123
153
  modelHook: ModelHook;
124
154
  version: DatabaseVersion;
@@ -128,6 +158,8 @@ export class Database extends EventEmitter implements AsyncEmitter {
128
158
  constructor(options: DatabaseOptions) {
129
159
  super();
130
160
 
161
+ // this.setMaxListeners(100);
162
+
131
163
  this.version = new DatabaseVersion(this);
132
164
 
133
165
  const opts = {
@@ -206,6 +238,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
206
238
  }
207
239
  });
208
240
 
241
+ this.initListener();
242
+ }
243
+
244
+ initListener() {
209
245
  this.on('afterCreate', async (instance) => {
210
246
  instance?.toChangedWithAssociations?.();
211
247
  });
@@ -213,6 +249,14 @@ export class Database extends EventEmitter implements AsyncEmitter {
213
249
  this.on('afterUpdate', async (instance) => {
214
250
  instance?.toChangedWithAssociations?.();
215
251
  });
252
+
253
+ this.on('beforeDestroy', async (instance, options) => {
254
+ await referentialIntegrityCheck({
255
+ db: this,
256
+ referencedInstance: instance,
257
+ transaction: options.transaction,
258
+ });
259
+ });
216
260
  }
217
261
 
218
262
  addMigration(item: MigrationItem) {
@@ -230,7 +274,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
230
274
  filename = filename.substring(0, filename.lastIndexOf('.')) || filename;
231
275
  this.migrations.add({
232
276
  name: namespace ? `${namespace}/${filename}` : filename,
233
- migration: this.requireModule(file),
277
+ migration: requireModule(file),
234
278
  context,
235
279
  });
236
280
  }
@@ -240,16 +284,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
240
284
  return dialect.includes(this.sequelize.getDialect());
241
285
  }
242
286
 
243
- private requireModule(module: any) {
244
- if (typeof module === 'string') {
245
- module = require(module);
246
- }
247
- if (typeof module !== 'object') {
248
- return module;
249
- }
250
- return module.__esModule ? module.default : module;
251
- }
252
-
253
287
  /**
254
288
  * Add collection to database
255
289
  * @param options
@@ -264,7 +298,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
264
298
  });
265
299
 
266
300
  this.collections.set(collection.name, collection);
267
- this.modelCollection.set(collection.model, collection);
268
301
 
269
302
  this.emit('afterDefineCollection', collection);
270
303
 
@@ -466,7 +499,23 @@ export class Database extends EventEmitter implements AsyncEmitter {
466
499
  return this.sequelize.close();
467
500
  }
468
501
 
469
- on(event: string | symbol, listener): this {
502
+ on(event: EventType, listener: any): this;
503
+ on(event: ModelValidateEventTypes, listener: SyncListener): this;
504
+ on(event: ModelValidateEventTypes, listener: ValidateListener): this;
505
+ on(event: ModelCreateEventTypes, listener: CreateListener): this;
506
+ on(event: ModelUpdateEventTypes, listener: UpdateListener): this;
507
+ on(event: ModelSaveEventTypes, listener: SaveListener): this;
508
+ on(event: ModelDestroyEventTypes, listener: DestroyListener): this;
509
+ on(event: ModelCreateWithAssociationsEventTypes, listener: CreateWithAssociationsListener): this;
510
+ on(event: ModelUpdateWithAssociationsEventTypes, listener: UpdateWithAssociationsListener): this;
511
+ on(event: ModelSaveWithAssociationsEventTypes, listener: SaveWithAssociationsListener): this;
512
+ on(event: DatabaseBeforeDefineCollectionEventType, listener: BeforeDefineCollectionListener): this;
513
+ on(event: DatabaseAfterDefineCollectionEventType, listener: AfterDefineCollectionListener): this;
514
+ on(
515
+ event: DatabaseBeforeRemoveCollectionEventType | DatabaseAfterRemoveCollectionEventType,
516
+ listener: RemoveCollectionListener,
517
+ ): this;
518
+ on(event: EventType, listener: any): this {
470
519
  // NOTE: to match if event is a sequelize or model type
471
520
  const type = this.modelHook.match(event);
472
521
 
@@ -0,0 +1,64 @@
1
+ export interface Reference {
2
+ sourceCollectionName: string;
3
+ sourceField: string;
4
+ targetField: string;
5
+ targetCollectionName: string;
6
+ onDelete: string;
7
+ }
8
+
9
+ class ReferencesMap {
10
+ protected map: Map<string, Reference[]> = new Map();
11
+
12
+ addReference(reference: Reference) {
13
+ const existReference = this.existReference(reference);
14
+
15
+ if (existReference) {
16
+ if (reference.onDelete && existReference.onDelete !== reference.onDelete) {
17
+ throw new Error(
18
+ `On Delete Conflict, exist reference ${JSON.stringify(existReference)}, new reference ${JSON.stringify(
19
+ reference,
20
+ )}`,
21
+ );
22
+ }
23
+
24
+ return;
25
+ }
26
+
27
+ if (!reference.onDelete) {
28
+ reference.onDelete = 'SET NULL';
29
+ }
30
+
31
+ this.map.set(reference.targetCollectionName, [...(this.map.get(reference.targetCollectionName) || []), reference]);
32
+ }
33
+
34
+ getReferences(collectionName) {
35
+ return this.map.get(collectionName);
36
+ }
37
+
38
+ existReference(reference: Reference) {
39
+ const references = this.map.get(reference.targetCollectionName);
40
+
41
+ if (!references) {
42
+ return null;
43
+ }
44
+
45
+ const keys = Object.keys(reference).filter((k) => k !== 'onDelete');
46
+
47
+ return references.find((ref) => keys.every((key) => ref[key] === reference[key]));
48
+ }
49
+
50
+ removeReference(reference: Reference) {
51
+ const references = this.map.get(reference.targetCollectionName);
52
+ if (!references) {
53
+ return;
54
+ }
55
+ const keys = Object.keys(reference);
56
+
57
+ this.map.set(
58
+ reference.targetCollectionName,
59
+ references.filter((ref) => !keys.every((key) => ref[key] === reference[key])),
60
+ );
61
+ }
62
+ }
63
+
64
+ export default ReferencesMap;
@@ -0,0 +1,61 @@
1
+ import Database from '../database';
2
+ import { Model, Transactionable } from 'sequelize';
3
+
4
+ interface ReferentialIntegrityCheckOptions extends Transactionable {
5
+ db: Database;
6
+ referencedInstance: Model;
7
+ }
8
+
9
+ export async function referentialIntegrityCheck(options: ReferentialIntegrityCheckOptions) {
10
+ const { referencedInstance, db, transaction } = options;
11
+
12
+ // @ts-ignore
13
+ const collection = db.modelCollection.get(referencedInstance.constructor);
14
+
15
+ const collectionName = collection.name;
16
+ const references = db.referenceMap.getReferences(collectionName);
17
+
18
+ if (!references) {
19
+ return;
20
+ }
21
+
22
+ for (const reference of references) {
23
+ const { sourceCollectionName, sourceField, targetField, onDelete } = reference;
24
+ const sourceCollection = db.collections.get(sourceCollectionName);
25
+ const sourceRepository = sourceCollection.repository;
26
+
27
+ const filter = {
28
+ [sourceField]: referencedInstance[targetField],
29
+ };
30
+ const referencingExists = await sourceRepository.count({
31
+ filter,
32
+ transaction,
33
+ });
34
+
35
+ if (!referencingExists) {
36
+ continue;
37
+ }
38
+
39
+ if (onDelete === 'RESTRICT') {
40
+ throw new Error('RESTRICT');
41
+ }
42
+
43
+ if (onDelete === 'CASCADE') {
44
+ await sourceRepository.destroy({
45
+ filter: filter,
46
+ transaction,
47
+ });
48
+ }
49
+
50
+ if (onDelete === 'SET NULL') {
51
+ await sourceRepository.update({
52
+ filter,
53
+ values: {
54
+ [sourceField]: null,
55
+ },
56
+ hooks: false,
57
+ transaction,
58
+ });
59
+ }
60
+ }
61
+ }
@@ -2,6 +2,7 @@ import { omit } from 'lodash';
2
2
  import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
3
3
  import { checkIdentifier } from '../utils';
4
4
  import { BaseRelationFieldOptions, RelationField } from './relation-field';
5
+ import { Reference } from '../features/ReferencesMap';
5
6
 
6
7
  export class BelongsToField extends RelationField {
7
8
  static type = 'belongsTo';
@@ -11,6 +12,18 @@ export class BelongsToField extends RelationField {
11
12
  return target || Utils.pluralize(name);
12
13
  }
13
14
 
15
+ reference(association): Reference {
16
+ const targetKey = association.targetKey;
17
+
18
+ return {
19
+ sourceCollectionName: this.database.modelCollection.get(association.source).name,
20
+ sourceField: association.foreignKey,
21
+ targetField: targetKey,
22
+ targetCollectionName: this.database.modelCollection.get(association.target).name,
23
+ onDelete: this.options.onDelete,
24
+ };
25
+ }
26
+
14
27
  bind() {
15
28
  const { database, collection } = this.context;
16
29
  const Target = this.TargetModel;
@@ -30,7 +43,7 @@ export class BelongsToField extends RelationField {
30
43
  const association = collection.model.belongsTo(Target, {
31
44
  as: this.name,
32
45
  constraints: false,
33
- ...omit(this.options, ['name', 'type', 'target']),
46
+ ...omit(this.options, ['name', 'type', 'target', 'onDelete']),
34
47
  });
35
48
 
36
49
  // inverse relation
@@ -56,6 +69,9 @@ export class BelongsToField extends RelationField {
56
69
  }
57
70
 
58
71
  this.collection.addIndex([this.options.foreignKey]);
72
+
73
+ this.database.referenceMap.addReference(this.reference(association));
74
+
59
75
  return true;
60
76
  }
61
77
 
@@ -73,6 +89,10 @@ export class BelongsToField extends RelationField {
73
89
  if (!field1 && !field2) {
74
90
  collection.model.removeAttribute(foreignKey);
75
91
  }
92
+
93
+ const association = collection.model.associations[this.name];
94
+ this.database.referenceMap.removeReference(this.reference(association));
95
+
76
96
  // 删掉 model 的关联字段
77
97
  delete collection.model.associations[this.name];
78
98
  // @ts-ignore
@@ -17,6 +17,10 @@ export class BelongsToManyField extends RelationField {
17
17
  );
18
18
  }
19
19
 
20
+ get otherKey() {
21
+ return this.options.otherKey;
22
+ }
23
+
20
24
  bind() {
21
25
  const { database, collection } = this.context;
22
26
  const Target = this.TargetModel;
@@ -34,6 +38,7 @@ export class BelongsToManyField extends RelationField {
34
38
  } else {
35
39
  Through = database.collection({
36
40
  name: through,
41
+ // timestamps: false,
37
42
  });
38
43
 
39
44
  Object.defineProperty(Through.model, 'isThrough', { value: true });
@@ -80,6 +85,7 @@ export class BelongsToManyField extends RelationField {
80
85
 
81
86
  unbind() {
82
87
  const { database, collection } = this.context;
88
+ const Through = database.getCollection(this.through);
83
89
  // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
84
90
  database.removePendingField(this);
85
91
  // 删掉 model 的关联字段
@@ -9,14 +9,12 @@ export class ContextField extends Field {
9
9
  return DataTypes[type.toUpperCase()] || DataTypes.STRING;
10
10
  }
11
11
 
12
- init() {
12
+ listener = async (model: Model, options) => {
13
13
  const { name, dataIndex } = this.options;
14
- this.listener = async (model: Model, options) => {
15
- const { context } = options;
16
- model.set(name, lodash.get(context, dataIndex));
17
- model.changed(name, true);
18
- };
19
- }
14
+ const { context } = options;
15
+ model.set(name, lodash.get(context, dataIndex));
16
+ model.changed(name, true);
17
+ };
20
18
 
21
19
  bind() {
22
20
  super.bind();
@@ -5,11 +5,11 @@ import {
5
5
  ModelIndexesOptions,
6
6
  QueryInterfaceOptions,
7
7
  SyncOptions,
8
- Transactionable,
8
+ Transactionable
9
9
  } from 'sequelize';
10
10
  import { Collection } from '../collection';
11
11
  import { Database } from '../database';
12
- import { checkIdentifier } from '../utils';
12
+ import { ModelEventTypes } from '../types';
13
13
 
14
14
  export interface FieldContext {
15
15
  database: Database;
@@ -68,7 +68,7 @@ export abstract class Field {
68
68
  // code
69
69
  }
70
70
 
71
- on(eventName: string, listener: (...args: any[]) => void) {
71
+ on(eventName: ModelEventTypes, listener: (...args: any[]) => void) {
72
72
  this.database.on(`${this.collection.name}.${eventName}`, listener);
73
73
  return this;
74
74
  }
@@ -83,6 +83,7 @@ export abstract class Field {
83
83
  }
84
84
 
85
85
  remove() {
86
+ this.collection.removeIndex([this.name]);
86
87
  return this.collection.removeField(this.name);
87
88
  }
88
89
 
@@ -5,11 +5,12 @@ import {
5
5
  ForeignKeyOptions,
6
6
  HasManyOptions,
7
7
  HasManyOptions as SequelizeHasManyOptions,
8
- Utils
8
+ Utils,
9
9
  } from 'sequelize';
10
10
  import { Collection } from '../collection';
11
11
  import { checkIdentifier } from '../utils';
12
12
  import { MultipleRelationFieldOptions, RelationField } from './relation-field';
13
+ import { Reference } from '../features/ReferencesMap';
13
14
 
14
15
  export interface HasManyFieldOptions extends HasManyOptions {
15
16
  /**
@@ -81,6 +82,18 @@ export class HasManyField extends RelationField {
81
82
  return Utils.camelize([model.options.name.singular, this.sourceKey || model.primaryKeyAttribute].join('_'));
82
83
  }
83
84
 
85
+ reference(association): Reference {
86
+ const sourceKey = association.sourceKey;
87
+
88
+ return {
89
+ sourceCollectionName: this.database.modelCollection.get(association.target).name,
90
+ sourceField: association.foreignKey,
91
+ targetField: sourceKey,
92
+ targetCollectionName: this.database.modelCollection.get(association.source).name,
93
+ onDelete: this.options.onDelete,
94
+ };
95
+ }
96
+
84
97
  bind() {
85
98
  const { database, collection } = this.context;
86
99
  const Target = this.TargetModel;
@@ -95,7 +108,7 @@ export class HasManyField extends RelationField {
95
108
 
96
109
  const association = collection.model.hasMany(Target, {
97
110
  constraints: false,
98
- ...omit(this.options, ['name', 'type', 'target']),
111
+ ...omit(this.options, ['name', 'type', 'target', 'onDelete']),
99
112
  as: this.name,
100
113
  foreignKey: this.foreignKey,
101
114
  });
@@ -130,6 +143,9 @@ export class HasManyField extends RelationField {
130
143
  if (tcoll) {
131
144
  tcoll.addIndex([this.options.foreignKey]);
132
145
  }
146
+
147
+ this.database.referenceMap.addReference(this.reference(association));
148
+
133
149
  return true;
134
150
  }
135
151
 
@@ -149,6 +165,13 @@ export class HasManyField extends RelationField {
149
165
  if (!field) {
150
166
  tcoll.model.removeAttribute(foreignKey);
151
167
  }
168
+
169
+ const association = collection.model.associations[this.name];
170
+
171
+ if (association) {
172
+ this.database.referenceMap.removeReference(this.reference(association));
173
+ }
174
+
152
175
  // 删掉 model 的关联字段
153
176
  delete collection.model.associations[this.name];
154
177
  // @ts-ignore
@@ -5,11 +5,12 @@ import {
5
5
  ForeignKeyOptions,
6
6
  HasOneOptions,
7
7
  HasOneOptions as SequelizeHasOneOptions,
8
- Utils
8
+ Utils,
9
9
  } from 'sequelize';
10
10
  import { Collection } from '../collection';
11
11
  import { checkIdentifier } from '../utils';
12
12
  import { BaseRelationFieldOptions, RelationField } from './relation-field';
13
+ import { Reference } from '../features/ReferencesMap';
13
14
 
14
15
  export interface HasOneFieldOptions extends HasOneOptions {
15
16
  /**
@@ -86,9 +87,22 @@ export class HasOneField extends RelationField {
86
87
  return Utils.camelize([model.options.name.singular, model.primaryKeyAttribute].join('_'));
87
88
  }
88
89
 
90
+ reference(association): Reference {
91
+ const sourceKey = association.sourceKey;
92
+
93
+ return {
94
+ sourceCollectionName: this.database.modelCollection.get(association.target).name,
95
+ sourceField: association.foreignKey,
96
+ targetField: sourceKey,
97
+ targetCollectionName: this.database.modelCollection.get(association.source).name,
98
+ onDelete: this.options.onDelete,
99
+ };
100
+ }
101
+
89
102
  bind() {
90
103
  const { database, collection } = this.context;
91
104
  const Target = this.TargetModel;
105
+
92
106
  if (!Target) {
93
107
  database.addPendingField(this);
94
108
  return false;
@@ -96,7 +110,7 @@ export class HasOneField extends RelationField {
96
110
 
97
111
  const association = collection.model.hasOne(Target, {
98
112
  constraints: false,
99
- ...omit(this.options, ['name', 'type', 'target']),
113
+ ...omit(this.options, ['name', 'type', 'target', 'onDelete']),
100
114
  as: this.name,
101
115
  foreignKey: this.foreignKey,
102
116
  });
@@ -129,6 +143,8 @@ export class HasOneField extends RelationField {
129
143
  if (tcoll) {
130
144
  tcoll.addIndex([this.options.foreignKey]);
131
145
  }
146
+
147
+ this.database.referenceMap.addReference(this.reference(association));
132
148
  return true;
133
149
  }
134
150
 
@@ -148,6 +164,10 @@ export class HasOneField extends RelationField {
148
164
  if (!field) {
149
165
  tcoll.model.removeAttribute(foreignKey);
150
166
  }
167
+
168
+ const association = collection.model.associations[this.name];
169
+ this.database.referenceMap.removeReference(this.reference(association));
170
+
151
171
  // 删掉 model 的关联字段
152
172
  delete collection.model.associations[this.name];
153
173
  // @ts-ignore
@@ -78,7 +78,6 @@ export default class FilterParser {
78
78
 
79
79
  const include = {};
80
80
  const where = {};
81
- const filter2 = lodash.cloneDeep(flattenedFilter);
82
81
 
83
82
  let skipPrefix = null;
84
83
  const associations = model.associations;
@@ -91,6 +90,11 @@ export default class FilterParser {
91
90
  continue;
92
91
  }
93
92
 
93
+ // skip empty logic operator value
94
+ if ((key == '$or' || key == '$and') && Array.isArray(value) && value.length == 0) {
95
+ continue;
96
+ }
97
+
94
98
  debug('handle filter key "%s: "%s"', key, value);
95
99
  let keys = key.split('.');
96
100
 
package/src/model.ts CHANGED
@@ -23,6 +23,7 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
23
23
  public static database: Database;
24
24
  public static collection: Collection;
25
25
 
26
+ [key: string]: any;
26
27
  protected _changedWithAssociations = new Set();
27
28
  protected _previousDataValuesWithAssociations = {};
28
29
 
@@ -70,6 +70,7 @@ export class OptionsParser {
70
70
  if (typeof sort === 'string') {
71
71
  sort = sort.split(',');
72
72
  }
73
+
73
74
  const orderParams = [];
74
75
  for (const sortKey of sort) {
75
76
  let direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
@@ -191,8 +192,9 @@ export class OptionsParser {
191
192
 
192
193
  /**
193
194
  * set include params
194
- * @param includeRoot
195
- * @param appends
195
+ * @param model
196
+ * @param queryParams
197
+ * @param append
196
198
  */
197
199
  const setInclude = (model: ModelCtor<any>, queryParams: any, append: string) => {
198
200
  const appendFields = append.split('.');
@@ -1,9 +1,9 @@
1
1
  import lodash from 'lodash';
2
2
  import { BelongsToMany, Op, Transaction } from 'sequelize';
3
3
  import { Model } from '../model';
4
- import { CreateOptions, DestroyOptions, FindOptions, TargetKey, UpdateOptions } from '../repository';
4
+ import { CreateOptions, DestroyOptions, FindOneOptions, FindOptions, TargetKey, UpdateOptions } from '../repository';
5
5
  import { updateThroughTableValue } from '../update-associations';
6
- import { FindAndCountOptions, FindOneOptions, MultipleRelationRepository } from './multiple-relation-repository';
6
+ import { FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository';
7
7
  import { transaction } from './relation-repository';
8
8
  import { AssociatedOptions, PrimaryKeyWithThroughValues } from './types';
9
9
 
@@ -14,7 +14,7 @@ interface IBelongsToManyRepository<M extends Model> {
14
14
  findAndCount(options?: FindAndCountOptions): Promise<[M[], number]>;
15
15
  findOne(options?: FindOneOptions): Promise<M>;
16
16
  // 新增并关联,存在中间表数据
17
- create(options?: CreateBelongsToManyOptions): Promise<M>;
17
+ create(options?: CreateOptions): Promise<M>;
18
18
  // 更新,存在中间表数据
19
19
  update(options?: UpdateOptions): Promise<M>;
20
20
  // 删除
@@ -1,13 +1,16 @@
1
1
  import { omit } from 'lodash';
2
2
  import { HasMany, Op } from 'sequelize';
3
3
  import { Model } from '../model';
4
- import { CreateOptions, DestroyOptions, FindOptions, TargetKey, TK, UpdateOptions } from '../repository';
5
4
  import {
6
- AssociatedOptions,
7
- FindAndCountOptions,
5
+ CreateOptions,
6
+ DestroyOptions,
8
7
  FindOneOptions,
9
- MultipleRelationRepository
10
- } from './multiple-relation-repository';
8
+ FindOptions,
9
+ TargetKey,
10
+ TK,
11
+ UpdateOptions,
12
+ } from '../repository';
13
+ import { AssociatedOptions, FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository';
11
14
  import { transaction } from './relation-repository';
12
15
 
13
16
  interface IHasManyRepository<M extends Model> {