@nocobase/database 0.9.4-alpha.1 → 0.10.0-alpha.2

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 (47) hide show
  1. package/lib/database.js +2 -1
  2. package/lib/eager-loading/eager-loading-tree.d.ts +4 -0
  3. package/lib/eager-loading/eager-loading-tree.js +79 -16
  4. package/lib/fields/belongs-to-field.d.ts +1 -1
  5. package/lib/fields/belongs-to-field.js +3 -2
  6. package/lib/fields/belongs-to-many-field.js +1 -1
  7. package/lib/fields/has-many-field.js +1 -1
  8. package/lib/fields/has-one-field.js +1 -1
  9. package/lib/filter-parser.js +15 -9
  10. package/lib/listeners/append-child-collection-name-after-repository-find.js +5 -1
  11. package/lib/options-parser.d.ts +1 -1
  12. package/lib/options-parser.js +15 -11
  13. package/lib/relation-repository/multiple-relation-repository.js +2 -1
  14. package/lib/relation-repository/single-relation-repository.js +2 -1
  15. package/lib/repository.d.ts +12 -0
  16. package/lib/repository.js +166 -66
  17. package/lib/update-associations.js +1 -1
  18. package/lib/view/field-type-map.d.ts +2 -0
  19. package/lib/view/field-type-map.js +2 -0
  20. package/lib/view/view-inference.js +52 -26
  21. package/lib/view-collection.js +0 -1
  22. package/package.json +4 -4
  23. package/src/__tests__/eager-loading/eager-loading-tree.test.ts +149 -0
  24. package/src/__tests__/field-options/sort-by.test.ts +3 -1
  25. package/src/__tests__/filter.test.ts +54 -0
  26. package/src/__tests__/inhertits/collection-inherits.test.ts +165 -0
  27. package/src/__tests__/repository/create.test.ts +129 -6
  28. package/src/__tests__/repository/find.test.ts +11 -0
  29. package/src/__tests__/repository.test.ts +24 -0
  30. package/src/__tests__/update-associations.test.ts +109 -1
  31. package/src/__tests__/view/view-inference.test.ts +1 -0
  32. package/src/database.ts +4 -1
  33. package/src/eager-loading/eager-loading-tree.ts +92 -17
  34. package/src/fields/belongs-to-field.ts +6 -4
  35. package/src/fields/belongs-to-many-field.ts +2 -1
  36. package/src/fields/has-many-field.ts +1 -1
  37. package/src/fields/has-one-field.ts +1 -1
  38. package/src/filter-parser.ts +17 -9
  39. package/src/listeners/append-child-collection-name-after-repository-find.ts +9 -5
  40. package/src/options-parser.ts +25 -19
  41. package/src/relation-repository/multiple-relation-repository.ts +1 -0
  42. package/src/relation-repository/single-relation-repository.ts +1 -0
  43. package/src/repository.ts +84 -0
  44. package/src/update-associations.ts +3 -0
  45. package/src/view/field-type-map.ts +2 -0
  46. package/src/view/view-inference.ts +75 -43
  47. package/src/view-collection.ts +0 -1
@@ -15,6 +15,149 @@ describe('Eager loading tree', () => {
15
15
  await db.close();
16
16
  });
17
17
 
18
+ it('should sort has many default by primary key', async () => {
19
+ const Source = db.collection({
20
+ name: 'source',
21
+ fields: [
22
+ { type: 'string', name: 'name' },
23
+ {
24
+ type: 'hasMany',
25
+ name: 'targets',
26
+ target: 'target',
27
+ foreignKey: 'source_id',
28
+ },
29
+ ],
30
+ });
31
+
32
+ const Target = db.collection({
33
+ name: 'target',
34
+ fields: [{ type: 'integer', name: 'seq_number' }],
35
+ });
36
+
37
+ await db.sync();
38
+
39
+ const target1 = await Target.repository.create({ values: { seq_number: 1 } });
40
+ const target2 = await Target.repository.create({ values: { seq_number: 3 } });
41
+
42
+ await target1.update({ values: { seq_number: 2 } });
43
+
44
+ await Source.repository.create({
45
+ values: { name: 's1', targets: [{ id: target2.get('id') }, { id: target1.get('id') }] },
46
+ });
47
+
48
+ const source = await Source.repository.findOne({
49
+ appends: ['targets'],
50
+ });
51
+
52
+ expect(source.get('targets').map((item: any) => item.get('id'))).toEqual([1, 2]);
53
+ });
54
+
55
+ it('should sort belongs to many default by target primary key', async () => {
56
+ const Through = db.collection({
57
+ name: 'through',
58
+ fields: [{ type: 'string', name: 'name' }],
59
+ });
60
+
61
+ const Source = db.collection({
62
+ name: 'source',
63
+ fields: [
64
+ { type: 'string', name: 'name' },
65
+ {
66
+ type: 'belongsToMany',
67
+ name: 'targets',
68
+ target: 'target',
69
+ through: 'through',
70
+ foreignKey: 'source_id',
71
+ otherKey: 'target_id',
72
+ sourceKey: 'id',
73
+ targetKey: 'id',
74
+ },
75
+ ],
76
+ });
77
+
78
+ const Target = db.collection({
79
+ name: 'target',
80
+ fields: [{ type: 'integer', name: 'seq_number' }],
81
+ });
82
+
83
+ await db.sync({
84
+ force: true,
85
+ });
86
+
87
+ const targets = await Target.repository.create({
88
+ values: [
89
+ { seq_number: 1 },
90
+ { seq_number: 2 },
91
+ { seq_number: 3 },
92
+ { seq_number: 4 },
93
+ { seq_number: 5 },
94
+ { seq_number: 6 },
95
+ ],
96
+ });
97
+
98
+ await Source.repository.create({
99
+ values: {
100
+ name: 'source1',
101
+ targets: [targets[2], targets[0], targets[1]],
102
+ },
103
+ });
104
+
105
+ const source = await Source.repository.findOne({
106
+ appends: ['targets'],
107
+ });
108
+
109
+ expect(source.targets.map((t) => t.get('id'))).toEqual([1, 2, 3]);
110
+ });
111
+
112
+ it('should handle eager loading with long field', async () => {
113
+ const Through = db.collection({
114
+ name: 'abc_abcd_abcd_abcdefg_abc_abc_a_abcdefghijk',
115
+ });
116
+
117
+ const A = db.collection({
118
+ name: 'a',
119
+ fields: [
120
+ { type: 'string', name: 'name' },
121
+ {
122
+ type: 'belongsToMany',
123
+ name: 'bs',
124
+ target: 'b',
125
+ through: 'abc_abcd_abcd_abcdefg_abc_abc_a_abcdefghijk',
126
+ foreignKey: 'abc_abcd_abcdefg_abcd_abc',
127
+ sourceKey: 'id',
128
+ otherKey: 'b_id',
129
+ targetKey: 'id',
130
+ },
131
+ ],
132
+ });
133
+
134
+ const B = db.collection({
135
+ name: 'b',
136
+ fields: [{ type: 'string', name: 'name' }],
137
+ });
138
+
139
+ await db.sync();
140
+
141
+ await A.repository.create({
142
+ values: {
143
+ name: 'a1',
144
+ bs: [{ name: 'b1' }, { name: 'b2' }],
145
+ },
146
+ });
147
+
148
+ const a = await A.repository.findOne({
149
+ appends: ['bs'],
150
+ });
151
+
152
+ expect(a.get('bs')).toHaveLength(2);
153
+ const data = a.toJSON();
154
+
155
+ // @ts-ignore
156
+ const as = A.model.associations.bs.oneFromTarget.as;
157
+
158
+ expect(data['bs'][0][as]).toBeDefined();
159
+ });
160
+
18
161
  it('should handle fields filter', async () => {
19
162
  const User = db.collection({
20
163
  name: 'users',
@@ -55,6 +198,7 @@ describe('Eager loading tree', () => {
55
198
  model: User.model,
56
199
  rootAttributes: findOptions.attributes,
57
200
  includeOption: findOptions.include,
201
+ db: db,
58
202
  });
59
203
 
60
204
  await eagerLoadingTree.load(users.map((item) => item.id));
@@ -105,6 +249,7 @@ describe('Eager loading tree', () => {
105
249
  model: User.model,
106
250
  rootAttributes: findOptions.attributes,
107
251
  includeOption: findOptions.include,
252
+ db: db,
108
253
  });
109
254
 
110
255
  await eagerLoadingTree.load([1, 2]);
@@ -155,6 +300,7 @@ describe('Eager loading tree', () => {
155
300
  model: User.model,
156
301
  rootAttributes: findOptions.attributes,
157
302
  includeOption: findOptions.include,
303
+ db: db,
158
304
  });
159
305
 
160
306
  await eagerLoadingTree.load(users.map((item) => item.id));
@@ -210,6 +356,7 @@ describe('Eager loading tree', () => {
210
356
  model: Post.model,
211
357
  rootAttributes: findOptions.attributes,
212
358
  includeOption: findOptions.include,
359
+ db: db,
213
360
  });
214
361
 
215
362
  await eagerLoadingTree.load([1, 2]);
@@ -272,6 +419,7 @@ describe('Eager loading tree', () => {
272
419
  model: Post.model,
273
420
  rootAttributes: findOptions.attributes,
274
421
  includeOption: findOptions.include,
422
+ db: db,
275
423
  });
276
424
  await eagerLoadingTree.load([1, 2]);
277
425
  const root = eagerLoadingTree.root;
@@ -374,6 +522,7 @@ describe('Eager loading tree', () => {
374
522
  model: User.model,
375
523
  rootAttributes: findOptions.attributes,
376
524
  includeOption: findOptions.include,
525
+ db: db,
377
526
  });
378
527
 
379
528
  expect(eagerLoadingTree.root.children).toHaveLength(1);
@@ -151,10 +151,11 @@ describe('associated field order', () => {
151
151
  });
152
152
 
153
153
  const u1 = await db.getRepository('users').findOne({
154
- appends: ['posts.tags'],
154
+ appends: ['posts.tags', 'posts.title'],
155
155
  });
156
156
 
157
157
  const u1Json = u1.toJSON();
158
+
158
159
  const u1Posts = u1Json['posts'];
159
160
  expect(u1Posts.map((p) => p['title'])).toEqual(['a', 'b', 'c']);
160
161
 
@@ -214,6 +215,7 @@ describe('associated field order', () => {
214
215
  });
215
216
 
216
217
  const p1JSON = p1Result.toJSON();
218
+
217
219
  const p1Images = p1JSON['images'];
218
220
  expect(p1Images.map((i) => i['url'])).toEqual(['t2', 't1']);
219
221
  });
@@ -14,6 +14,60 @@ describe('filter', () => {
14
14
  await db.close();
15
15
  });
16
16
 
17
+ it('should filter by belongs to many association', async () => {
18
+ const A = db.collection({
19
+ name: 'a',
20
+ fields: [
21
+ { type: 'string', name: 'name' },
22
+ { type: 'hasMany', name: 'b', target: 'b' },
23
+ ],
24
+ });
25
+
26
+ const B = db.collection({
27
+ name: 'b',
28
+ fields: [
29
+ { type: 'string', name: 'name' },
30
+ { type: 'belongsTo', name: 'c', target: 'c' },
31
+ { type: 'belongsTo', name: 'd', target: 'd' },
32
+ ],
33
+ });
34
+
35
+ const C = db.collection({
36
+ name: 'c',
37
+ fields: [{ type: 'string', name: 'name' }],
38
+ });
39
+
40
+ const D = db.collection({
41
+ name: 'd',
42
+ fields: [{ type: 'string', name: 'name' }],
43
+ });
44
+
45
+ await db.sync();
46
+
47
+ const findArgs = {
48
+ filter: {
49
+ $and: [{ b: { c: { name: { $eq: 'c1' } } } }, { b: { d: { name: { $eq: 'd1' } } } }],
50
+ },
51
+ };
52
+
53
+ const findOptions = A.repository.buildQueryOptions(findArgs);
54
+
55
+ const include = findOptions.include;
56
+
57
+ const associationB = include.find((i) => i.association === 'b');
58
+ expect(associationB).toBeDefined();
59
+ expect(associationB.include).toHaveLength(2);
60
+
61
+ let err;
62
+ try {
63
+ await A.repository.find({ ...findArgs });
64
+ } catch (e) {
65
+ err = e;
66
+ }
67
+
68
+ expect(err).toBeUndefined();
69
+ });
70
+
17
71
  it('should filter by hasMany association field', async () => {
18
72
  const DeptCollection = db.collection({
19
73
  name: 'depts',
@@ -16,6 +16,171 @@ pgOnly()('collection inherits', () => {
16
16
  await db.close();
17
17
  });
18
18
 
19
+ it('should append __collection with eager load', async () => {
20
+ const Root = db.collection({
21
+ name: 'root',
22
+ fields: [
23
+ { name: 'name', type: 'string' },
24
+ {
25
+ name: 'bs',
26
+ type: 'hasMany',
27
+ target: 'b',
28
+ foreignKey: 'root_id',
29
+ },
30
+ ],
31
+ });
32
+
33
+ const Child = db.collection({
34
+ name: 'child',
35
+ inherits: ['root'],
36
+ });
37
+
38
+ const B = db.collection({
39
+ name: 'b',
40
+ fields: [{ name: 'name', type: 'string' }],
41
+ });
42
+
43
+ await db.sync();
44
+
45
+ await Child.repository.create({
46
+ values: {
47
+ name: 'child1',
48
+ bs: [
49
+ {
50
+ name: 'b1',
51
+ },
52
+ ],
53
+ },
54
+ });
55
+
56
+ const data = await Root.repository.findOne({
57
+ fields: ['bs.name'],
58
+ });
59
+
60
+ expect(data.get('__collection')).toEqual('child');
61
+ expect(data.get('bs')[0].get('name')).toEqual('b1');
62
+ });
63
+
64
+ it('should not remove parent field reference map after child rewrite field', async () => {
65
+ const through = db.collection({
66
+ name: 'through',
67
+ fields: [{ name: 'name', type: 'string' }],
68
+ });
69
+
70
+ const rootCollection = db.collection({
71
+ name: 'root',
72
+ fields: [
73
+ { name: 'name', type: 'string' },
74
+ {
75
+ name: 'targets',
76
+ type: 'belongsToMany',
77
+ target: 'root-target',
78
+ through: 'through',
79
+ foreignKey: 'rootId',
80
+ otherKey: 'targetId',
81
+ },
82
+ ],
83
+ });
84
+
85
+ const rootTarget = db.collection({
86
+ name: 'root-target',
87
+ fields: [{ name: 'name', type: 'string' }],
88
+ });
89
+
90
+ await db.sync({ force: true });
91
+
92
+ expect(db.referenceMap.getReferences('root-target')).toHaveLength(1);
93
+
94
+ const child = db.collection({
95
+ name: 'child',
96
+ inherits: ['root'],
97
+ });
98
+
99
+ const childTarget = db.collection({
100
+ name: 'child-target',
101
+ inherits: ['root-target'],
102
+ });
103
+
104
+ await child.setField('targets', {
105
+ name: 'targets',
106
+ type: 'belongsToMany',
107
+ target: 'child-target',
108
+ through: 'through',
109
+ foreignKey: 'rootId',
110
+ otherKey: 'targetId',
111
+ });
112
+
113
+ await db.sync();
114
+
115
+ expect(db.referenceMap.getReferences('root-target')).toHaveLength(1);
116
+ });
117
+
118
+ it('should append collection name in eager load', async () => {
119
+ const rootCollection = db.collection({
120
+ name: 'assoc',
121
+ fields: [
122
+ { name: 'name', type: 'string' },
123
+ { type: 'belongsToMany', name: 'other-assocs' },
124
+ ],
125
+ });
126
+
127
+ const childCollection = db.collection({
128
+ name: 'child',
129
+ inherits: ['assoc'],
130
+ });
131
+
132
+ const otherAssoc = db.collection({
133
+ name: 'other-assocs',
134
+ fields: [{ name: 'name', type: 'string' }],
135
+ });
136
+
137
+ const User = db.collection({
138
+ name: 'users',
139
+ fields: [
140
+ {
141
+ name: 'name',
142
+ type: 'string',
143
+ },
144
+ {
145
+ name: 'assocs',
146
+ type: 'hasMany',
147
+ target: 'assoc',
148
+ },
149
+ ],
150
+ });
151
+
152
+ await db.sync();
153
+
154
+ const child = await childCollection.repository.create({
155
+ values: {
156
+ name: 'child1',
157
+ },
158
+ });
159
+
160
+ await User.repository.create({
161
+ values: {
162
+ name: 'user1',
163
+ assocs: [
164
+ {
165
+ id: child.get('id'),
166
+ },
167
+ ],
168
+ },
169
+ });
170
+
171
+ const users = await User.repository.find({
172
+ appends: ['assocs.other-assocs'],
173
+ });
174
+
175
+ const user = users[0];
176
+
177
+ const assoc = user.get('assocs')[0];
178
+ expect(assoc.get('__tableName')).toEqual(childCollection.model.tableName);
179
+ expect(assoc.get('__schemaName')).toEqual(childCollection.collectionSchema());
180
+
181
+ expect(user.get('assocs')[0].get('__collection')).toBe('child');
182
+ });
183
+
19
184
  it('should list data filtered by child type', async () => {
20
185
  const rootCollection = db.collection({
21
186
  name: 'root',
@@ -1,10 +1,11 @@
1
1
  import { mockDatabase } from '../index';
2
2
  import Database from '../../database';
3
+ import { Collection } from '../../collection';
3
4
 
4
5
  describe('create with hasMany', () => {
5
6
  let db: Database;
6
- let Post;
7
- let User;
7
+ let Post: Collection;
8
+ let User: Collection;
8
9
 
9
10
  afterEach(async () => {
10
11
  await db.close();
@@ -61,8 +62,8 @@ describe('create with hasMany', () => {
61
62
 
62
63
  describe('create with belongsToMany', () => {
63
64
  let db: Database;
64
- let Post;
65
- let Tag;
65
+ let Post: Collection;
66
+ let Tag: Collection;
66
67
 
67
68
  afterEach(async () => {
68
69
  await db.close();
@@ -122,16 +123,36 @@ describe('create with belongsToMany', () => {
122
123
 
123
124
  describe('create', () => {
124
125
  let db: Database;
125
- let User;
126
- let Post;
126
+ let User: Collection;
127
+ let Post: Collection;
128
+ let Group: Collection;
129
+ let Role: Collection;
127
130
 
128
131
  beforeEach(async () => {
129
132
  db = mockDatabase();
133
+ await db.clean({ drop: true });
134
+
135
+ Group = db.collection({
136
+ name: 'groups',
137
+ fields: [{ type: 'string', name: 'name' }],
138
+ });
139
+
130
140
  User = db.collection({
131
141
  name: 'users',
132
142
  fields: [
133
143
  { type: 'string', name: 'name' },
144
+ { type: 'integer', name: 'age' },
134
145
  { type: 'hasMany', name: 'posts' },
146
+ { type: 'belongsTo', name: 'group' },
147
+ { type: 'belongsToMany', name: 'roles' },
148
+ ],
149
+ });
150
+
151
+ Role = db.collection({
152
+ name: 'roles',
153
+ fields: [
154
+ { type: 'string', name: 'name' },
155
+ { type: 'belongsToMany', name: 'users' },
135
156
  ],
136
157
  });
137
158
 
@@ -149,6 +170,108 @@ describe('create', () => {
149
170
  await db.close();
150
171
  });
151
172
 
173
+ test('firstOrCreate by filterKeys', async () => {
174
+ const u1 = await User.repository.firstOrCreate({
175
+ filterKeys: ['name'],
176
+ values: {
177
+ name: 'u1',
178
+ age: 10,
179
+ group: {
180
+ name: 'g1',
181
+ },
182
+ },
183
+ });
184
+ expect(u1.name).toEqual('u1');
185
+ const group = await u1.get('group');
186
+ expect(group.name).toEqual('g1');
187
+
188
+ const u2 = await User.repository.firstOrCreate({
189
+ filterKeys: ['group'],
190
+ values: {
191
+ group: {
192
+ name: 'g1',
193
+ },
194
+ },
195
+ });
196
+
197
+ expect(u2.name).toEqual('u1');
198
+ });
199
+
200
+ test('firstOrCreate by many to many associations', async () => {
201
+ const roles = await Role.repository.create({
202
+ values: [{ name: 'r1' }, { name: 'r2' }],
203
+ });
204
+
205
+ const u1 = await User.repository.firstOrCreate({
206
+ values: {
207
+ name: 'u1',
208
+ roles: [{ name: 'r1' }, { name: 'r2' }],
209
+ },
210
+ filterKeys: ['roles.name'],
211
+ });
212
+
213
+ expect(u1.get('roles')).toHaveLength(2);
214
+
215
+ const u1a = await User.repository.firstOrCreate({
216
+ values: {
217
+ name: 'u1',
218
+ roles: [{ name: 'r1' }, { name: 'r2' }],
219
+ },
220
+ filterKeys: ['roles.name'],
221
+ });
222
+
223
+ expect(u1a.get('id')).toEqual(u1.get('id'));
224
+ });
225
+
226
+ test('firstOrCreate', async () => {
227
+ const u1 = await User.repository.firstOrCreate({
228
+ filterKeys: ['name'],
229
+ values: {
230
+ name: 'u1',
231
+ age: 10,
232
+ },
233
+ });
234
+
235
+ expect(u1.name).toEqual('u1');
236
+ expect(u1.age).toEqual(10);
237
+
238
+ expect(
239
+ (
240
+ await User.repository.firstOrCreate({
241
+ filterKeys: ['name'],
242
+ values: {
243
+ name: 'u1',
244
+ age: 10,
245
+ },
246
+ })
247
+ ).get('id'),
248
+ ).toEqual(u1.get('id'));
249
+ });
250
+
251
+ test('updateOrCreate', async () => {
252
+ const u1 = await User.repository.updateOrCreate({
253
+ filterKeys: ['name'],
254
+ values: {
255
+ name: 'u1',
256
+ age: 10,
257
+ },
258
+ });
259
+
260
+ expect(u1.name).toEqual('u1');
261
+ expect(u1.age).toEqual(10);
262
+
263
+ await User.repository.updateOrCreate({
264
+ filterKeys: ['name'],
265
+ values: {
266
+ name: 'u1',
267
+ age: 20,
268
+ },
269
+ });
270
+
271
+ await u1.reload();
272
+ expect(u1.age).toEqual(20);
273
+ });
274
+
152
275
  test('create with association', async () => {
153
276
  const u1 = await User.repository.create({
154
277
  values: {
@@ -488,6 +488,17 @@ describe('repository find', () => {
488
488
  expect(user['posts']).toBeDefined();
489
489
  });
490
490
 
491
+ test('find with nested fields', async () => {
492
+ const user = await User.repository.findOne({
493
+ fields: ['posts.comments.content'],
494
+ });
495
+
496
+ const userData = user.toJSON();
497
+
498
+ const post = userData['posts'][0];
499
+ expect(Object.keys(post)).toEqual(['comments']);
500
+ });
501
+
491
502
  describe('find with appends', () => {
492
503
  test('toJSON', async () => {
493
504
  const user = await User.repository.findOne({
@@ -1,6 +1,29 @@
1
1
  import { Collection } from '../collection';
2
2
  import { Database } from '../database';
3
3
  import { mockDatabase } from './';
4
+ import { Repository } from '@nocobase/database';
5
+
6
+ describe('repository', () => {
7
+ test('value to filter', async () => {
8
+ const value = {
9
+ tags: [
10
+ {
11
+ categories: [{ name: 'c1' }, { name: 'c2' }],
12
+ },
13
+ {
14
+ categories: [{ name: 'c3' }, { name: 'c4' }],
15
+ },
16
+ ],
17
+ };
18
+
19
+ const filter = Repository.valuesToFilter(value, ['tags.categories.name']);
20
+ expect(filter.$and).toEqual([
21
+ {
22
+ 'tags.categories.name': ['c1', 'c2', 'c3', 'c4'],
23
+ },
24
+ ]);
25
+ });
26
+ });
4
27
 
5
28
  describe('find by targetKey', function () {
6
29
  let db: Database;
@@ -563,6 +586,7 @@ describe('repository.relatedQuery', () => {
563
586
  const post2 = await userPostRepository.create({
564
587
  values: { name: 'post2' },
565
588
  });
589
+
566
590
  expect(post2).toMatchObject({
567
591
  name: 'post2',
568
592
  userId: user.get('id'),