@nocobase/database 0.9.2-alpha.3 → 0.9.3-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.
@@ -1,7 +1,264 @@
1
1
  import { mockDatabase } from '../index';
2
2
  import Database from '@nocobase/database';
3
3
  import { Collection } from '../../collection';
4
- import { OptionsParser } from '../../options-parser';
4
+
5
+ describe('find with associations', () => {
6
+ let db: Database;
7
+ beforeEach(async () => {
8
+ db = mockDatabase();
9
+
10
+ await db.clean({ drop: true });
11
+ });
12
+
13
+ afterEach(async () => {
14
+ await db.close();
15
+ });
16
+
17
+ it('should filter by association array field', async () => {
18
+ const User = db.collection({
19
+ name: 'users',
20
+ fields: [
21
+ {
22
+ type: 'string',
23
+ name: 'name',
24
+ },
25
+ {
26
+ type: 'hasMany',
27
+ name: 'posts',
28
+ },
29
+ ],
30
+ });
31
+
32
+ const Post = db.collection({
33
+ name: 'posts',
34
+ fields: [
35
+ {
36
+ type: 'array',
37
+ name: 'tags',
38
+ },
39
+ {
40
+ type: 'string',
41
+ name: 'title',
42
+ },
43
+ ],
44
+ });
45
+
46
+ await db.sync();
47
+
48
+ await User.repository.create({
49
+ values: [
50
+ {
51
+ name: 'u1',
52
+ posts: [
53
+ {
54
+ tags: ['t1'],
55
+ title: 'u1p1',
56
+ },
57
+ ],
58
+ },
59
+ ],
60
+ });
61
+
62
+ const posts = await Post.repository.find({
63
+ filter: {
64
+ tags: {
65
+ $match: ['t1'],
66
+ },
67
+ },
68
+ });
69
+
70
+ expect(posts.length).toEqual(1);
71
+
72
+ const filter = {
73
+ $and: [
74
+ {
75
+ posts: {
76
+ tags: {
77
+ $match: ['t1'],
78
+ },
79
+ },
80
+ },
81
+ ],
82
+ };
83
+
84
+ const results = await User.repository.find({
85
+ filter,
86
+ });
87
+
88
+ expect(results[0].get('name')).toEqual('u1');
89
+ });
90
+
91
+ it('should filter with append', async () => {
92
+ const Post = db.collection({
93
+ name: 'posts',
94
+ fields: [
95
+ { name: 'title', type: 'string' },
96
+ {
97
+ name: 'user',
98
+ type: 'belongsTo',
99
+ },
100
+ {
101
+ name: 'category',
102
+ type: 'belongsTo',
103
+ },
104
+ ],
105
+ });
106
+
107
+ const Category = db.collection({
108
+ name: 'categories',
109
+ fields: [
110
+ {
111
+ name: 'name',
112
+ type: 'string',
113
+ },
114
+ ],
115
+ });
116
+
117
+ const User = db.collection({
118
+ name: 'users',
119
+ fields: [
120
+ { name: 'name', type: 'string' },
121
+ { type: 'belongsTo', name: 'organization' },
122
+ {
123
+ type: 'belongsTo',
124
+ name: 'department',
125
+ },
126
+ ],
127
+ });
128
+
129
+ const Org = db.collection({
130
+ name: 'organizations',
131
+ fields: [{ name: 'name', type: 'string' }],
132
+ });
133
+
134
+ const Dept = db.collection({
135
+ name: 'departments',
136
+ fields: [{ name: 'name', type: 'string' }],
137
+ });
138
+
139
+ await db.sync();
140
+
141
+ await Post.repository.create({
142
+ values: [
143
+ {
144
+ title: 'p1',
145
+ category: { name: 'c1' },
146
+ user: {
147
+ name: 'u1',
148
+ organization: {
149
+ name: 'o1',
150
+ },
151
+ department: {
152
+ name: 'd1',
153
+ },
154
+ },
155
+ },
156
+ {
157
+ title: 'p2',
158
+ category: { name: 'c2' },
159
+ user: {
160
+ name: 'u2',
161
+ organization: {
162
+ name: 'o2',
163
+ },
164
+ department: {
165
+ name: 'd2',
166
+ },
167
+ },
168
+ },
169
+ ],
170
+ });
171
+
172
+ const filterResult = await Post.repository.find({
173
+ appends: ['user.department'],
174
+ filter: {
175
+ 'user.name': 'u1',
176
+ },
177
+ });
178
+
179
+ expect(filterResult[0].user.department).toBeDefined();
180
+ });
181
+
182
+ it('should filter by association field', async () => {
183
+ const User = db.collection({
184
+ name: 'users',
185
+ tree: 'adjacency-list',
186
+ fields: [
187
+ { type: 'string', name: 'name' },
188
+ { type: 'hasMany', name: 'posts', target: 'posts', foreignKey: 'user_id' },
189
+ {
190
+ type: 'belongsTo',
191
+ name: 'parent',
192
+ foreignKey: 'parent_id',
193
+ treeParent: true,
194
+ },
195
+ {
196
+ type: 'hasMany',
197
+ name: 'children',
198
+ foreignKey: 'parent_id',
199
+ treeChildren: true,
200
+ },
201
+ ],
202
+ });
203
+
204
+ const Post = db.collection({
205
+ name: 'posts',
206
+ fields: [
207
+ { type: 'string', name: 'title' },
208
+ { type: 'belongsTo', name: 'user', target: 'users', foreignKey: 'user_id' },
209
+ ],
210
+ });
211
+
212
+ await db.sync();
213
+
214
+ expect(User.options.tree).toBeTruthy();
215
+
216
+ await User.repository.create({
217
+ values: [
218
+ {
219
+ name: 'u1',
220
+ posts: [
221
+ {
222
+ title: 'u1p1',
223
+ },
224
+ ],
225
+ children: [
226
+ {
227
+ name: 'u2',
228
+ posts: [
229
+ {
230
+ title: '标题2',
231
+ },
232
+ ],
233
+ },
234
+ ],
235
+ },
236
+ ],
237
+ });
238
+
239
+ const filter = {
240
+ $and: [
241
+ {
242
+ children: {
243
+ posts: {
244
+ title: {
245
+ $eq: '标题2',
246
+ },
247
+ },
248
+ },
249
+ },
250
+ ],
251
+ };
252
+
253
+ const [findResult, count] = await User.repository.findAndCount({
254
+ filter,
255
+ offset: 0,
256
+ limit: 20,
257
+ });
258
+
259
+ expect(findResult[0].get('name')).toEqual('u1');
260
+ });
261
+ });
5
262
 
6
263
  describe('repository find', () => {
7
264
  let db: Database;
@@ -1,17 +1,184 @@
1
1
  import { Database } from '../database';
2
2
  import { mockDatabase } from './';
3
+ import { AdjacencyListRepository } from '../tree-repository/adjacency-list-repository';
3
4
 
4
- describe('sort', function () {
5
+ describe('tree test', function () {
5
6
  let db: Database;
6
7
 
7
8
  beforeEach(async () => {
8
- db = mockDatabase();
9
+ db = mockDatabase({
10
+ tablePrefix: '',
11
+ });
12
+ await db.clean({ drop: true });
9
13
  });
10
14
 
11
15
  afterEach(async () => {
12
16
  await db.close();
13
17
  });
14
18
 
19
+ it('should works with appends option', async () => {
20
+ const collection = db.collection({
21
+ name: 'categories',
22
+ tree: 'adjacency-list',
23
+ fields: [
24
+ { type: 'string', name: 'name' },
25
+ {
26
+ type: 'belongsTo',
27
+ name: 'parent',
28
+ treeParent: true,
29
+ },
30
+ {
31
+ type: 'hasMany',
32
+ name: 'children',
33
+ treeChildren: true,
34
+ },
35
+ ],
36
+ });
37
+
38
+ await db.sync();
39
+
40
+ await collection.repository.create({
41
+ values: [
42
+ {
43
+ name: 'c1',
44
+ children: [
45
+ {
46
+ name: 'c11',
47
+ },
48
+ {
49
+ name: 'c12',
50
+ },
51
+ ],
52
+ },
53
+ {
54
+ name: 'c2',
55
+ },
56
+ ],
57
+ });
58
+
59
+ const tree = await collection.repository.find({
60
+ tree: true,
61
+ filter: {
62
+ parentId: null,
63
+ },
64
+ fields: ['name'],
65
+ });
66
+
67
+ expect(tree.length).toBe(2);
68
+ });
69
+
70
+ it('should not return children property when child nodes are empty', async () => {
71
+ const collection = db.collection({
72
+ name: 'categories',
73
+ tree: 'adjacency-list',
74
+ fields: [
75
+ { type: 'string', name: 'name' },
76
+ {
77
+ type: 'belongsTo',
78
+ name: 'parent',
79
+ treeParent: true,
80
+ },
81
+ {
82
+ type: 'hasMany',
83
+ name: 'children',
84
+ treeChildren: true,
85
+ },
86
+ ],
87
+ });
88
+
89
+ await db.sync();
90
+
91
+ await collection.repository.create({
92
+ values: [
93
+ {
94
+ name: 'c1',
95
+ children: [
96
+ {
97
+ name: 'c11',
98
+ },
99
+ {
100
+ name: 'c12',
101
+ },
102
+ ],
103
+ },
104
+ {
105
+ name: 'c2',
106
+ },
107
+ ],
108
+ });
109
+
110
+ const tree = await collection.repository.find({
111
+ filter: {
112
+ parentId: null,
113
+ },
114
+ tree: true,
115
+ });
116
+
117
+ const c2 = tree.find((item) => item.name === 'c2');
118
+ expect(c2.toJSON()['children']).toBeUndefined();
119
+
120
+ const c11 = tree
121
+ .find((item) => item.name === 'c1')
122
+ .get('children')
123
+ .find((item) => item.name === 'c11');
124
+
125
+ expect(c11.toJSON()['children']).toBeUndefined();
126
+ });
127
+
128
+ it('should add sort field', async () => {
129
+ const Tasks = db.collection({
130
+ name: 'tasks',
131
+ tree: 'adjacency-list',
132
+ fields: [
133
+ {
134
+ type: 'string',
135
+ name: 'name',
136
+ },
137
+ {
138
+ type: 'belongsTo',
139
+ name: 'parent',
140
+ treeParent: true,
141
+ },
142
+ {
143
+ type: 'hasMany',
144
+ name: 'children',
145
+ treeChildren: true,
146
+ },
147
+ {
148
+ type: 'string',
149
+ name: 'status',
150
+ },
151
+ ],
152
+ });
153
+
154
+ await db.sync();
155
+
156
+ await Tasks.repository.create({
157
+ values: {
158
+ name: 'task1',
159
+ status: 'doing',
160
+ },
161
+ });
162
+
163
+ await Tasks.repository.create({
164
+ values: {
165
+ name: 'task2',
166
+ status: 'pending',
167
+ },
168
+ });
169
+
170
+ await Tasks.repository.create({
171
+ values: {
172
+ name: 'task3',
173
+ status: 'pending',
174
+ },
175
+ });
176
+
177
+ Tasks.setField('sort', { type: 'sort', scopeKey: 'status' });
178
+
179
+ await db.sync();
180
+ });
181
+
15
182
  it('should be auto completed', () => {
16
183
  const collection = db.collection({
17
184
  name: 'categories',
@@ -162,7 +329,7 @@ describe('sort', function () {
162
329
  expect(instance.toJSON()).toMatchObject(values[0]);
163
330
  });
164
331
 
165
- it('should be tree', async () => {
332
+ it('should find tree collection', async () => {
166
333
  const collection = db.collection({
167
334
  name: 'categories',
168
335
  tree: 'adjacency-list',
@@ -214,4 +381,100 @@ describe('sort', function () {
214
381
 
215
382
  expect(instance.toJSON()).toMatchObject(values[0]);
216
383
  });
384
+
385
+ it('should get adjacency list repository', async () => {
386
+ const collection = db.collection({
387
+ name: 'categories',
388
+ tree: 'adjacency-list',
389
+ fields: [
390
+ {
391
+ type: 'string',
392
+ name: 'name',
393
+ },
394
+ {
395
+ type: 'belongsTo',
396
+ name: 'parent',
397
+ foreignKey: 'parentId',
398
+ treeParent: true,
399
+ },
400
+ {
401
+ type: 'hasMany',
402
+ name: 'children',
403
+ foreignKey: 'parentId',
404
+ treeChildren: true,
405
+ },
406
+ ],
407
+ });
408
+
409
+ const repository = db.getRepository('categories');
410
+ expect(repository).toBeInstanceOf(AdjacencyListRepository);
411
+ });
412
+
413
+ test('performance', async () => {
414
+ const collection = db.collection({
415
+ name: 'categories',
416
+ tree: 'adjacency-list',
417
+ fields: [
418
+ {
419
+ type: 'string',
420
+ name: 'name',
421
+ },
422
+ {
423
+ type: 'belongsTo',
424
+ name: 'parent',
425
+ foreignKey: 'parentId',
426
+ treeParent: true,
427
+ },
428
+ {
429
+ type: 'hasMany',
430
+ name: 'children',
431
+ foreignKey: 'parentId',
432
+ treeChildren: true,
433
+ },
434
+ ],
435
+ });
436
+ await db.sync();
437
+
438
+ const values = [];
439
+ for (let i = 0; i < 10; i++) {
440
+ const children = [];
441
+ for (let j = 0; j < 10; j++) {
442
+ const grandchildren = [];
443
+ for (let k = 0; k < 10; k++) {
444
+ grandchildren.push({
445
+ name: `name-${i}-${j}-${k}`,
446
+ });
447
+ }
448
+ children.push({
449
+ name: `name-${i}-${j}`,
450
+ children: grandchildren,
451
+ });
452
+ }
453
+
454
+ values.push({
455
+ name: `name-${i}`,
456
+ description: `description-${i}`,
457
+ children,
458
+ });
459
+ }
460
+
461
+ await db.getRepository('categories').create({
462
+ values,
463
+ });
464
+
465
+ const before = Date.now();
466
+
467
+ const instances = await db.getRepository('categories').find({
468
+ filter: {
469
+ parentId: null,
470
+ },
471
+ tree: true,
472
+ fields: ['id', 'name'],
473
+ sort: 'id',
474
+ limit: 10,
475
+ });
476
+
477
+ const after = Date.now();
478
+ console.log(after - before);
479
+ });
217
480
  });
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
 
@@ -223,6 +224,11 @@ export class Collection<
223
224
  if (typeof repository === 'string') {
224
225
  repo = this.context.database.repositories.get(repository) || Repository;
225
226
  }
227
+
228
+ if (this.options.tree == 'adjacency-list' || this.options.tree == 'adjacencyList') {
229
+ repo = AdjacencyListRepository;
230
+ }
231
+
226
232
  this.repository = new repo(this);
227
233
  }
228
234
 
@@ -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]);
@@ -17,44 +17,3 @@ export const beforeDefineAdjacencyListCollection = (options: CollectionOptions)
17
17
  }
18
18
  });
19
19
  };
20
-
21
- export const afterDefineAdjacencyListCollection = (collection: Collection) => {
22
- if (!collection.options.tree) {
23
- return;
24
- }
25
- collection.model.afterFind(async (instances, options: any) => {
26
- if (!options.tree) {
27
- return;
28
- }
29
- const foreignKey = collection.treeParentField?.foreignKey ?? 'parentId';
30
- const childrenKey = collection.treeChildrenField?.name ?? 'children';
31
- const arr: Model[] = Array.isArray(instances) ? instances : [instances];
32
- let index = 0;
33
- for (const instance of arr) {
34
- const opts = {
35
- ...lodash.pick(options, ['tree', 'fields', 'appends', 'except', 'sort']),
36
- };
37
- let __index = `${index++}`;
38
- if (options.parentIndex) {
39
- __index = `${options.parentIndex}.${__index}`;
40
- }
41
- instance.setDataValue('__index', __index);
42
- const children = await collection.repository.find({
43
- filter: {
44
- [foreignKey]: instance.id,
45
- },
46
- transaction: options.transaction,
47
- ...opts,
48
- // @ts-ignore
49
- parentIndex: `${__index}.${childrenKey}`,
50
- context: options.context,
51
- });
52
- if (children?.length > 0) {
53
- instance.setDataValue(
54
- childrenKey,
55
- children.map((r) => r.toJSON()),
56
- );
57
- }
58
- }
59
- });
60
- };
@@ -1,7 +1,6 @@
1
1
  import { Database } from '../database';
2
- import { afterDefineAdjacencyListCollection, beforeDefineAdjacencyListCollection } from './adjacency-list';
2
+ import { beforeDefineAdjacencyListCollection } from './adjacency-list';
3
3
 
4
4
  export const registerBuiltInListeners = (db: Database) => {
5
5
  db.on('beforeDefineCollection', beforeDefineAdjacencyListCollection);
6
- db.on('afterDefineCollection', afterDefineAdjacencyListCollection);
7
6
  };
@@ -11,6 +11,11 @@ const escape = (value, ctx) => {
11
11
  return sequelize.escape(value);
12
12
  };
13
13
 
14
+ const getQueryInterface = (ctx) => {
15
+ const sequelize = ctx.db.sequelize;
16
+ return sequelize.getQueryInterface();
17
+ };
18
+
14
19
  const sqliteExistQuery = (value, ctx) => {
15
20
  const fieldName = getFieldName(ctx);
16
21
  const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
@@ -45,10 +50,9 @@ export default {
45
50
  const fieldName = getFieldName(ctx);
46
51
 
47
52
  if (isPg(ctx)) {
48
- return {
49
- [Op.contained]: value,
50
- [Op.contains]: value,
51
- };
53
+ const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
54
+ const queryValue = escape(JSON.stringify(value), ctx);
55
+ return Sequelize.literal(`${name} @> ${queryValue}::JSONB AND ${name} <@ ${queryValue}::JSONB`);
52
56
  }
53
57
 
54
58
  value = escape(JSON.stringify(value.sort()), ctx);
@@ -249,6 +249,11 @@ export class OptionsParser {
249
249
  }
250
250
 
251
251
  if (appendFields.length == 2) {
252
+ const association = associations[appendFields[0]];
253
+ if (!association) {
254
+ throw new Error(`association ${appendFields[0]} in ${model.name} not found`);
255
+ }
256
+
252
257
  const associationModel = associations[appendFields[0]].target;
253
258
  if (associationModel.rawAttributes[appendFields[1]]) {
254
259
  lastLevel = true;
@@ -307,6 +312,13 @@ export class OptionsParser {
307
312
  attributes,
308
313
  };
309
314
  } else {
315
+ const existInclude = queryParams['include'][existIncludeIndex];
316
+ if (existInclude.attributes && Array.isArray(existInclude.attributes) && existInclude.attributes.length == 0) {
317
+ existInclude.attributes = {
318
+ include: [],
319
+ };
320
+ }
321
+
310
322
  setInclude(
311
323
  model.associations[queryParams['include'][existIncludeIndex].association].target,
312
324
  queryParams['include'][existIncludeIndex],