@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
@@ -0,0 +1,297 @@
1
+ import { BelongsToManyRepository, HasManyRepository, mockDatabase } from '../../index';
2
+ import Database from '../../database';
3
+ import { Collection } from '../../collection';
4
+
5
+ describe('association aggregation', () => {
6
+ let db: Database;
7
+
8
+ let User: Collection;
9
+ let Post: Collection;
10
+ let Tag: Collection;
11
+
12
+ afterEach(async () => {
13
+ await db.close();
14
+ });
15
+
16
+ beforeEach(async () => {
17
+ db = mockDatabase();
18
+ await db.clean({ drop: true });
19
+
20
+ User = db.collection({
21
+ name: 'users',
22
+ fields: [
23
+ { type: 'string', name: 'name' },
24
+ { type: 'integer', name: 'age' },
25
+ { type: 'hasMany', name: 'posts' },
26
+ {
27
+ type: 'belongsToMany',
28
+ name: 'tags',
29
+ },
30
+ ],
31
+ });
32
+
33
+ Post = db.collection({
34
+ name: 'posts',
35
+ fields: [
36
+ {
37
+ type: 'string',
38
+ name: 'title',
39
+ },
40
+ {
41
+ type: 'string',
42
+ name: 'category',
43
+ },
44
+ {
45
+ type: 'integer',
46
+ name: 'readCount',
47
+ },
48
+ {
49
+ type: 'belongsTo',
50
+ name: 'user',
51
+ },
52
+ ],
53
+ });
54
+
55
+ Tag = db.collection({
56
+ name: 'tags',
57
+ fields: [
58
+ {
59
+ type: 'string',
60
+ name: 'name',
61
+ },
62
+ {
63
+ type: 'integer',
64
+ name: 'score',
65
+ },
66
+ ],
67
+ });
68
+
69
+ await db.sync();
70
+ });
71
+
72
+ describe('belongs to many', () => {
73
+ beforeEach(async () => {
74
+ await User.repository.create({
75
+ values: [
76
+ {
77
+ name: 'u1',
78
+ age: 1,
79
+ tags: [
80
+ { name: 't1', score: 1 },
81
+ { name: 't2', score: 2 },
82
+ ],
83
+ },
84
+ {
85
+ name: 'u2',
86
+ age: 2,
87
+ tags: [
88
+ { name: 't3', score: 3 },
89
+ { name: 't4', score: 4 },
90
+ { name: 't5', score: 4 },
91
+ ],
92
+ },
93
+ ],
94
+ });
95
+ });
96
+
97
+ it('should sum field', async () => {
98
+ const user1 = await User.repository.findOne({
99
+ filter: {
100
+ name: 'u1',
101
+ },
102
+ });
103
+
104
+ const TagRepository = await db.getRepository<BelongsToManyRepository>('users.tags', user1.get('id'));
105
+
106
+ const sumResult = await TagRepository.aggregate({
107
+ field: 'score',
108
+ method: 'sum',
109
+ });
110
+
111
+ expect(sumResult).toEqual(3);
112
+ });
113
+
114
+ it('should sum with filter', async () => {
115
+ const user1 = await User.repository.findOne({
116
+ filter: {
117
+ name: 'u2',
118
+ },
119
+ });
120
+
121
+ const TagRepository = await db.getRepository<BelongsToManyRepository>('users.tags', user1.get('id'));
122
+
123
+ const sumResult = await TagRepository.aggregate({
124
+ field: 'score',
125
+ method: 'sum',
126
+ filter: {
127
+ score: 4,
128
+ },
129
+ });
130
+
131
+ expect(sumResult).toEqual(8);
132
+ });
133
+
134
+ it('should sum with distinct', async () => {
135
+ const user1 = await User.repository.findOne({
136
+ filter: {
137
+ name: 'u2',
138
+ },
139
+ });
140
+
141
+ const TagRepository = await db.getRepository<BelongsToManyRepository>('users.tags', user1.get('id'));
142
+
143
+ const sumResult = await TagRepository.aggregate({
144
+ field: 'score',
145
+ method: 'sum',
146
+ distinct: true,
147
+ filter: {
148
+ score: 4,
149
+ },
150
+ });
151
+
152
+ expect(sumResult).toEqual(4);
153
+ });
154
+ it('should sum with association filter', async () => {
155
+ const sumResult = await User.repository.aggregate({
156
+ field: 'age',
157
+ method: 'sum',
158
+ filter: {
159
+ 'tags.score': 4,
160
+ },
161
+ });
162
+
163
+ expect(sumResult).toEqual(2);
164
+ });
165
+ });
166
+
167
+ describe('has many', () => {
168
+ beforeEach(async () => {
169
+ await User.repository.create({
170
+ values: [
171
+ {
172
+ name: 'u1',
173
+ age: 1,
174
+ posts: [
175
+ { title: 'p1', category: 'c1', readCount: 1 },
176
+ { title: 'p2', category: 'c2', readCount: 2 },
177
+ ],
178
+ },
179
+ {
180
+ name: 'u2',
181
+ age: 2,
182
+ posts: [
183
+ { title: 'p3', category: 'c3', readCount: 3 },
184
+ { title: 'p4', category: 'c4', readCount: 4 },
185
+ ],
186
+ },
187
+ ],
188
+ });
189
+ });
190
+
191
+ it('should sum field', async () => {
192
+ const user1 = await User.repository.findOne({
193
+ filter: {
194
+ name: 'u1',
195
+ },
196
+ });
197
+
198
+ const PostRepository = await db.getRepository<HasManyRepository>('users.posts', user1.get('id'));
199
+ const sumResult = await PostRepository.aggregate({
200
+ field: 'readCount',
201
+ method: 'sum',
202
+ });
203
+
204
+ expect(sumResult).toEqual(3);
205
+ });
206
+
207
+ it('should sum with filter', async () => {
208
+ const user1 = await User.repository.findOne({
209
+ filter: {
210
+ name: 'u1',
211
+ },
212
+ });
213
+
214
+ const PostRepository = await db.getRepository<HasManyRepository>('users.posts', user1.get('id'));
215
+ const sumResult = await PostRepository.aggregate({
216
+ field: 'readCount',
217
+ method: 'sum',
218
+ });
219
+
220
+ expect(sumResult).toEqual(3);
221
+ });
222
+ });
223
+ });
224
+
225
+ describe('Aggregation', () => {
226
+ let db: Database;
227
+
228
+ let User: Collection;
229
+ afterEach(async () => {
230
+ await db.close();
231
+ });
232
+
233
+ beforeEach(async () => {
234
+ db = mockDatabase();
235
+ await db.clean({ drop: true });
236
+
237
+ User = db.collection({
238
+ name: 'users',
239
+ fields: [
240
+ {
241
+ type: 'string',
242
+ name: 'name',
243
+ },
244
+ {
245
+ type: 'integer',
246
+ name: 'age',
247
+ },
248
+ ],
249
+ });
250
+
251
+ await db.sync();
252
+
253
+ await User.repository.create({
254
+ values: [
255
+ { name: 'u1', age: 1 },
256
+ { name: 'u2', age: 2 },
257
+ { name: 'u3', age: 3 },
258
+ { name: 'u4', age: 4 },
259
+ { name: 'u5', age: 5 },
260
+ { name: 'u5', age: 5 },
261
+ ],
262
+ });
263
+ });
264
+
265
+ describe('sum', () => {
266
+ it('should sum field', async () => {
267
+ const sumResult = await User.repository.aggregate({
268
+ method: 'sum',
269
+ field: 'age',
270
+ });
271
+
272
+ expect(sumResult).toEqual(20);
273
+ });
274
+
275
+ it('should sum with distinct', async () => {
276
+ const sumResult = await User.repository.aggregate({
277
+ method: 'sum',
278
+ field: 'age',
279
+ distinct: true,
280
+ });
281
+
282
+ expect(sumResult).toEqual(15);
283
+ });
284
+
285
+ it('should sum with filter', async () => {
286
+ const sumResult = await User.repository.aggregate({
287
+ method: 'sum',
288
+ field: 'age',
289
+ filter: {
290
+ name: 'u5',
291
+ },
292
+ });
293
+
294
+ expect(sumResult).toEqual(10);
295
+ });
296
+ });
297
+ });
@@ -117,7 +117,7 @@ describe('count', () => {
117
117
  appends: ['tags'],
118
118
  });
119
119
 
120
- expect(posts[0][0]['tags']).toBeDefined();
120
+ expect(posts[0][0].get('tags')).toBeDefined();
121
121
  });
122
122
 
123
123
  test('without filter params', async () => {
@@ -176,7 +176,7 @@ describe('find with associations', () => {
176
176
  },
177
177
  });
178
178
 
179
- expect(filterResult[0].user.department).toBeDefined();
179
+ expect(filterResult[0].get('user').get('department')).toBeDefined();
180
180
  });
181
181
 
182
182
  it('should filter by association field', async () => {
@@ -440,6 +440,15 @@ describe('repository find', () => {
440
440
  expect(Object.keys(data)).toEqual(['id', 'posts']);
441
441
  expect(Object.keys(data['posts'])).not.toContain('id');
442
442
  });
443
+
444
+ test('find one with appends', async () => {
445
+ const profile = await Profile.repository.findOne({
446
+ filterByTk: 1,
447
+ appends: ['user.name'],
448
+ });
449
+
450
+ expect(profile.get('user').get('name')).toEqual('u1');
451
+ });
443
452
  });
444
453
 
445
454
  describe('find', () => {
@@ -54,6 +54,7 @@ describe('repository.find', () => {
54
54
  let User: Collection;
55
55
  let Post: Collection;
56
56
  let Comment: Collection;
57
+ let Tag: Collection;
57
58
 
58
59
  beforeEach(async () => {
59
60
  db = mockDatabase();
@@ -70,8 +71,18 @@ describe('repository.find', () => {
70
71
  { type: 'string', name: 'name' },
71
72
  { type: 'belongsTo', name: 'user' },
72
73
  { type: 'hasMany', name: 'comments' },
74
+ { type: 'belongsToMany', name: 'tags' },
73
75
  ],
74
76
  });
77
+
78
+ Tag = db.collection({
79
+ name: 'tags',
80
+ fields: [
81
+ { type: 'string', name: 'name' },
82
+ { type: 'belongsToMany', name: 'posts' },
83
+ ],
84
+ });
85
+
75
86
  Comment = db.collection({
76
87
  name: 'comments',
77
88
  fields: [
@@ -80,6 +91,10 @@ describe('repository.find', () => {
80
91
  ],
81
92
  });
82
93
  await db.sync();
94
+
95
+ const tags = await Tag.repository.create({
96
+ values: [{ name: 't1' }, { name: 't2' }],
97
+ });
83
98
  await User.repository.createMany({
84
99
  records: [
85
100
  {
@@ -88,18 +103,22 @@ describe('repository.find', () => {
88
103
  {
89
104
  name: 'post11',
90
105
  comments: [{ name: 'comment111' }, { name: 'comment112' }, { name: 'comment113' }],
106
+ tags: [{ id: tags[0].get('id') }],
91
107
  },
92
108
  {
93
109
  name: 'post12',
94
110
  comments: [{ name: 'comment121' }, { name: 'comment122' }, { name: 'comment123' }],
111
+ tags: [{ id: tags[1].get('id') }, { id: tags[0].get('id') }],
95
112
  },
96
113
  {
97
114
  name: 'post13',
98
115
  comments: [{ name: 'comment131' }, { name: 'comment132' }, { name: 'comment133' }],
116
+ tags: [{ id: tags[0].get('id') }],
99
117
  },
100
118
  {
101
119
  name: 'post14',
102
120
  comments: [{ name: 'comment141' }, { name: 'comment142' }, { name: 'comment143' }],
121
+ tags: [{ id: tags[1].get('id') }],
103
122
  },
104
123
  ],
105
124
  },
@@ -109,6 +128,7 @@ describe('repository.find', () => {
109
128
  {
110
129
  name: 'post21',
111
130
  comments: [{ name: 'comment211' }, { name: 'comment212' }, { name: 'comment213' }],
131
+ tags: [{ id: tags[0].get('id') }, { id: tags[1].get('id') }],
112
132
  },
113
133
  {
114
134
  name: 'post22',
@@ -144,6 +164,16 @@ describe('repository.find', () => {
144
164
  await db.close();
145
165
  });
146
166
 
167
+ it('should appends with belongs to association', async () => {
168
+ const posts = await Post.repository.find({
169
+ appends: ['user'],
170
+ });
171
+
172
+ posts.forEach((post) => {
173
+ expect(post.get('user')).toBeDefined();
174
+ });
175
+ });
176
+
147
177
  test('find pk with filter', async () => {
148
178
  const Test = db.collection({
149
179
  name: 'tests',
@@ -108,6 +108,19 @@ describe('update-guard', () => {
108
108
  });
109
109
  });
110
110
 
111
+ test('association with null array', () => {
112
+ const values = {
113
+ name: 'u1',
114
+ posts: [null],
115
+ };
116
+
117
+ const guard = new UpdateGuard();
118
+ guard.setModel(User.model);
119
+ const sanitized = guard.sanitize(values);
120
+
121
+ expect(sanitized).toEqual({ name: 'u1', posts: [null] });
122
+ });
123
+
111
124
  test('association black list', () => {
112
125
  const values = {
113
126
  name: 'username123',
package/src/collection.ts CHANGED
@@ -79,6 +79,31 @@ export class Collection<
79
79
  model: ModelStatic<Model>;
80
80
  repository: Repository<TModelAttributes, TCreationAttributes>;
81
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
+
82
107
  get filterTargetKey() {
83
108
  const targetKey = lodash.get(this.options, 'filterTargetKey', this.model.primaryKeyAttribute);
84
109
  if (!targetKey && this.model.rawAttributes['id']) {
@@ -116,65 +141,12 @@ export class Collection<
116
141
  }
117
142
  }
118
143
 
119
- constructor(options: CollectionOptions, context: CollectionContext) {
120
- super();
121
- this.context = context;
122
- this.options = options;
123
-
124
- this.checkOptions(options);
125
-
126
- this.bindFieldEventListener();
127
- this.modelInit();
128
-
129
- this.db.modelCollection.set(this.model, this);
130
-
131
- // set tableName to collection map
132
- // the form of key is `${schema}.${tableName}` if schema exists
133
- // otherwise is `${tableName}`
134
- this.db.tableNameCollectionMap.set(this.getTableNameWithSchemaAsString(), this);
135
-
136
- if (!options.inherits) {
137
- this.setFields(options.fields);
138
- }
139
-
140
- this.setRepository(options.repository);
141
- this.setSortable(options.sortable);
142
- }
143
-
144
- private checkOptions(options: CollectionOptions) {
145
- checkIdentifier(options.name);
146
- this.checkTableName();
147
- }
148
-
149
- private checkTableName() {
150
- const tableName = this.tableName();
151
- for (const [k, collection] of this.db.collections) {
152
- if (
153
- collection.name != this.options.name &&
154
- tableName === collection.tableName() &&
155
- collection.collectionSchema() === this.collectionSchema()
156
- ) {
157
- throw new Error(`collection ${collection.name} and ${this.name} have same tableName "${tableName}"`);
158
- }
159
- }
160
- }
161
-
162
144
  tableName() {
163
145
  const { name, tableName } = this.options;
164
146
  const tName = tableName || name;
165
147
  return this.options.underscored ? snakeCase(tName) : tName;
166
148
  }
167
149
 
168
- protected sequelizeModelOptions() {
169
- const { name } = this.options;
170
- return {
171
- ..._.omit(this.options, ['name', 'fields', 'model', 'targetKey']),
172
- modelName: name,
173
- sequelize: this.context.database.sequelize,
174
- tableName: this.tableName(),
175
- };
176
- }
177
-
178
150
  /**
179
151
  * TODO
180
152
  */
@@ -232,17 +204,6 @@ export class Collection<
232
204
  this.repository = new repo(this);
233
205
  }
234
206
 
235
- private bindFieldEventListener() {
236
- this.on('field.afterAdd', (field: Field) => {
237
- field.bind();
238
- });
239
-
240
- this.on('field.afterRemove', (field: Field) => {
241
- field.unbind();
242
- this.db.emit('field.afterRemove', field);
243
- });
244
- }
245
-
246
207
  forEachField(callback: (field: Field) => void) {
247
208
  return [...this.fields.values()].forEach(callback);
248
209
  }
@@ -299,12 +260,20 @@ export class Collection<
299
260
  const [sourceCollectionName, sourceFieldName] = options.source.split('.');
300
261
  const sourceCollection = this.db.collections.get(sourceCollectionName);
301
262
  if (!sourceCollection) {
302
- throw new Error(
263
+ this.db.logger.warn(
303
264
  `source collection "${sourceCollectionName}" not found for field "${name}" at collection "${this.name}"`,
304
265
  );
305
266
  }
267
+
306
268
  const sourceField = sourceCollection.fields.get(sourceFieldName);
307
- options = { ...sourceField.options, ...options };
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
+ }
308
277
  }
309
278
 
310
279
  this.emit('field.beforeAdd', name, options, { collection: this });
@@ -678,4 +647,43 @@ export class Collection<
678
647
  public isView() {
679
648
  return false;
680
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
+ }
681
689
  }