@nocobase/database 0.11.1-alpha.3 → 0.11.1-alpha.4

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.
@@ -249,6 +249,8 @@ class OptionsParser {
249
249
  }
250
250
  parseAppends(appends, filterParams) {
251
251
  if (!appends) return filterParams;
252
+ // sort appends by path length
253
+ appends = _lodash().default.sortBy(appends, append => append.split('.').length);
252
254
  /**
253
255
  * set include params
254
256
  * @param model
@@ -256,6 +258,7 @@ class OptionsParser {
256
258
  * @param append
257
259
  */
258
260
  const setInclude = (model, queryParams, append) => {
261
+ var _lodash$get;
259
262
  const appendFields = append.split('.');
260
263
  const appendAssociation = appendFields[0];
261
264
  const associations = model.associations;
@@ -286,6 +289,16 @@ class OptionsParser {
286
289
  // if include from filter, remove fromFilter attribute
287
290
  if (existIncludeIndex != -1) {
288
291
  delete queryParams['include'][existIncludeIndex]['fromFilter'];
292
+ // set include attributes to all attributes
293
+ if (Array.isArray(queryParams['include'][existIncludeIndex]['attributes']) && queryParams['include'][existIncludeIndex]['attributes'].length == 0) {
294
+ queryParams['include'][existIncludeIndex]['attributes'] = {
295
+ include: []
296
+ };
297
+ }
298
+ }
299
+ if (lastLevel && existIncludeIndex != -1 && ((_lodash$get = _lodash().default.get(queryParams, ['include', existIncludeIndex, 'attributes', 'include'])) === null || _lodash$get === void 0 ? void 0 : _lodash$get.length) == 0) {
300
+ // if append is last level and association exists, ignore it
301
+ return;
289
302
  }
290
303
  // if association not exist, create it
291
304
  if (existIncludeIndex == -1) {
@@ -64,7 +64,7 @@ class SyncRunner {
64
64
  }
65
65
  const columnDefault = sequenceNameResult[0][0]['column_default'];
66
66
  if (!columnDefault) {
67
- throw new Error(`Can't find sequence name of ${parent}`);
67
+ throw new Error(`Can't find sequence name of parent collection ${parent.options.name}`);
68
68
  }
69
69
  const regex = new RegExp(/nextval\('(.*)'::regclass\)/);
70
70
  const match = regex.exec(columnDefault);
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@nocobase/database",
3
- "version": "0.11.1-alpha.3",
3
+ "version": "0.11.1-alpha.4",
4
4
  "description": "",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",
7
7
  "license": "Apache-2.0",
8
8
  "dependencies": {
9
- "@nocobase/logger": "0.11.1-alpha.3",
10
- "@nocobase/utils": "0.11.1-alpha.3",
9
+ "@nocobase/logger": "0.11.1-alpha.4",
10
+ "@nocobase/utils": "0.11.1-alpha.4",
11
11
  "async-mutex": "^0.3.2",
12
12
  "cron-parser": "4.4.0",
13
13
  "dayjs": "^1.11.8",
@@ -29,5 +29,5 @@
29
29
  "url": "git+https://github.com/nocobase/nocobase.git",
30
30
  "directory": "packages/database"
31
31
  },
32
- "gitHead": "5ed3bd7d5b16bd38d268961b34875d5cd45799ef"
32
+ "gitHead": "d9b5bde913013f1057e1aab49587eb0ad3dcb06e"
33
33
  }
@@ -112,80 +112,114 @@ describe('option parser', () => {
112
112
  ]);
113
113
  });
114
114
 
115
- test('option parser with fields option', async () => {
116
- let options: any = {
117
- fields: ['id', 'posts'],
118
- };
119
- // 转换为 attributes: ['id'], include: [{association: 'posts'}]
120
- let parser = new OptionsParser(options, {
121
- collection: User,
115
+ describe('options parser with fields option', () => {
116
+ it('should handle field and association', () => {
117
+ const options: any = {
118
+ fields: ['id', 'posts'],
119
+ };
120
+
121
+ // 转换为 attributes: ['id'], include: [{association: 'posts'}]
122
+ const parser = new OptionsParser(options, {
123
+ collection: User,
124
+ });
125
+
126
+ const params = parser.toSequelizeParams();
127
+
128
+ console.log(params);
129
+ expect(params['attributes']).toContain('id');
130
+ expect(params['include'][0]['association']).toEqual('posts');
122
131
  });
123
- let params = parser.toSequelizeParams();
124
132
 
125
- expect(params['attributes']).toContain('id');
126
- expect(params['include'][0]['association']).toEqual('posts');
133
+ it('should handle field with association', () => {
134
+ const options = {
135
+ appends: ['posts'],
136
+ };
127
137
 
128
- // only appends
129
- options = {
130
- appends: ['posts'],
131
- };
138
+ const parser = new OptionsParser(options, {
139
+ collection: User,
140
+ });
141
+ const params = parser.toSequelizeParams();
132
142
 
133
- parser = new OptionsParser(options, {
134
- collection: User,
143
+ expect(params['attributes']['include']).toEqual([]);
144
+ expect(params['include'][0]['association']).toEqual('posts');
135
145
  });
136
- params = parser.toSequelizeParams();
137
- expect(params['attributes']['include']).toEqual([]);
138
- expect(params['include'][0]['association']).toEqual('posts');
139
146
 
140
- // fields with association field
141
- options = {
142
- fields: ['id', 'posts.title'],
143
- };
147
+ it('should handle field with association field', () => {
148
+ // fields with association field
149
+ const options = {
150
+ fields: ['id', 'posts.title'],
151
+ };
144
152
 
145
- parser = new OptionsParser(options, {
146
- collection: User,
153
+ const parser = new OptionsParser(options, {
154
+ collection: User,
155
+ });
156
+
157
+ const params = parser.toSequelizeParams();
158
+ expect(params['attributes']).toContain('id');
159
+ expect(params['include'][0]['association']).toEqual('posts');
160
+ expect(params['include'][0]['attributes']).toContain('title');
147
161
  });
148
- params = parser.toSequelizeParams();
149
- expect(params['attributes']).toContain('id');
150
- expect(params['include'][0]['association']).toEqual('posts');
151
- expect(params['include'][0]['attributes']).toContain('title');
152
162
 
153
- // fields with nested field
154
- options = {
155
- fields: ['id', 'posts', 'posts.comments.content'],
156
- };
163
+ it('should handle nested fields option', () => {
164
+ const options = {
165
+ fields: ['posts', 'posts.title'],
166
+ };
157
167
 
158
- parser = new OptionsParser(options, {
159
- collection: User,
160
- });
161
- params = parser.toSequelizeParams();
162
- expect(params['attributes']).toContain('id');
163
- expect(params['include'][0]['association']).toEqual('posts');
164
- expect(params['include'][0]['attributes']).toEqual({ include: [] });
165
- expect(params['include'][0]['include'][0]['association']).toEqual('comments');
168
+ const parser = new OptionsParser(options, {
169
+ collection: User,
170
+ });
166
171
 
167
- // fields with expect
168
- options = {
169
- except: ['id'],
170
- };
171
- parser = new OptionsParser(options, {
172
- collection: User,
172
+ const params = parser.toSequelizeParams();
173
+ const postAssociationParams = params['include'][0];
174
+ expect(postAssociationParams['attributes']).toEqual({ include: [] });
173
175
  });
174
- params = parser.toSequelizeParams();
175
- expect(params['attributes']['exclude']).toContain('id');
176
176
 
177
- // expect with association
178
- options = {
179
- fields: ['posts'],
180
- except: ['posts.id'],
181
- };
177
+ it('should handle fields with association & association field', () => {
178
+ // fields with nested field
179
+ const options = {
180
+ fields: ['id', 'posts', 'posts.comments.content'],
181
+ };
182
182
 
183
- parser = new OptionsParser(options, {
184
- collection: User,
183
+ const parser = new OptionsParser(options, {
184
+ collection: User,
185
+ });
186
+
187
+ const params = parser.toSequelizeParams();
188
+ const postAssociationParams = params['include'][0];
189
+
190
+ expect(params['attributes']).toContain('id');
191
+ expect(postAssociationParams['association']).toEqual('posts');
192
+ expect(postAssociationParams['attributes']).toEqual({ include: [] });
193
+ expect(postAssociationParams['include'][0]['association']).toEqual('comments');
185
194
  });
186
- params = parser.toSequelizeParams();
187
195
 
188
- expect(params['include'][0]['attributes']['exclude']).toContain('id');
196
+ it('should handle except option', () => {
197
+ // fields with expect
198
+ const options = {
199
+ except: ['id'],
200
+ };
201
+ const parser = new OptionsParser(options, {
202
+ collection: User,
203
+ });
204
+
205
+ const params = parser.toSequelizeParams();
206
+ expect(params['attributes']['exclude']).toContain('id');
207
+ });
208
+
209
+ it('should handle fields with except option', () => {
210
+ // expect with association
211
+ const options = {
212
+ fields: ['posts'],
213
+ except: ['posts.id'],
214
+ };
215
+
216
+ const parser = new OptionsParser(options, {
217
+ collection: User,
218
+ });
219
+ const params = parser.toSequelizeParams();
220
+
221
+ expect(params['include'][0]['attributes']['exclude']).toContain('id');
222
+ });
189
223
  });
190
224
 
191
225
  test('option parser with multiple association', () => {
@@ -378,23 +378,33 @@ describe('repository find', () => {
378
378
  });
379
379
  });
380
380
 
381
- it('should only output filed in fields args', async () => {
382
- const resp = await User.model.findOne({
383
- attributes: [],
384
- include: [
385
- {
386
- association: 'profile',
387
- attributes: ['salary'],
388
- },
389
- ],
390
- });
381
+ describe('find with fields', () => {
382
+ it('should only output filed in fields args', async () => {
383
+ const users = await User.repository.find({
384
+ fields: ['profile.salary'],
385
+ });
391
386
 
392
- const users = await User.repository.find({
393
- fields: ['profile', 'profile.salary', 'profile.id'],
387
+ const firstUser = users[0].toJSON();
388
+ expect(Object.keys(firstUser)).toEqual(['profile']);
389
+ expect(Object.keys(firstUser.profile)).toEqual(['salary']);
394
390
  });
395
391
 
396
- const firstUser = users[0].toJSON();
397
- expect(Object.keys(firstUser)).toEqual(['profile']);
392
+ it('should output all fields when field has relation field', async () => {
393
+ const users = await User.repository.find({
394
+ fields: ['profile.salary', 'profile'],
395
+ });
396
+
397
+ const firstUser = users[0].toJSON();
398
+ expect(Object.keys(firstUser)).toEqual(['profile']);
399
+ expect(Object.keys(firstUser.profile)).toEqual([
400
+ 'id',
401
+ 'createdAt',
402
+ 'updatedAt',
403
+ 'salary',
404
+ 'userId',
405
+ 'description',
406
+ ]);
407
+ });
398
408
  });
399
409
 
400
410
  it('append with associations', async () => {
@@ -18,131 +18,155 @@ pgOnly()('', () => {
18
18
  await db.close();
19
19
  });
20
20
 
21
- it('should skip on delete on view collection', async () => {
22
- const Order = db.collection({
23
- name: 'orders',
24
- fields: [
25
- {
26
- type: 'string',
27
- name: 'name',
28
- },
29
- {
30
- type: 'hasMany',
31
- name: 'orderItems',
32
- foreignKey: 'order_id',
33
- target: 'orderItems',
34
- },
35
- ],
36
- });
37
-
38
- const OrderItem = db.collection({
39
- name: 'orderItems',
40
- timestamps: false,
41
- fields: [
42
- {
43
- type: 'integer',
44
- name: 'count',
45
- },
46
- {
47
- type: 'belongsTo',
48
- name: 'item',
49
- target: 'items',
50
- foreignKey: 'item_id',
51
- },
52
- {
53
- type: 'belongsTo',
54
- name: 'order',
55
- target: 'orders',
56
- foreignKey: 'order_id',
57
- onDelete: 'NO ACTION',
58
- },
59
- ],
60
- });
61
-
62
- const Item = db.collection({
63
- name: 'items',
64
- fields: [{ name: 'name', type: 'string' }],
65
- });
66
-
67
- await db.sync();
68
-
69
- const viewName = 'order_item_view';
21
+ describe('view as through table', () => {
22
+ let Order;
23
+ let OrderItem;
24
+ let Item;
25
+ let OrderItemView;
26
+
27
+ beforeEach(async () => {
28
+ Order = db.collection({
29
+ name: 'orders',
30
+ fields: [
31
+ {
32
+ type: 'string',
33
+ name: 'name',
34
+ },
35
+ {
36
+ type: 'hasMany',
37
+ name: 'orderItems',
38
+ foreignKey: 'order_id',
39
+ target: 'orderItems',
40
+ },
41
+ ],
42
+ });
70
43
 
71
- const dropViewSQL = `DROP VIEW IF EXISTS ${viewName}`;
72
- await db.sequelize.query(dropViewSQL);
44
+ OrderItem = db.collection({
45
+ name: 'orderItems',
46
+ timestamps: false,
47
+ fields: [
48
+ {
49
+ type: 'integer',
50
+ name: 'count',
51
+ },
52
+ {
53
+ type: 'belongsTo',
54
+ name: 'item',
55
+ target: 'items',
56
+ foreignKey: 'item_id',
57
+ },
58
+ {
59
+ type: 'belongsTo',
60
+ name: 'order',
61
+ target: 'orders',
62
+ foreignKey: 'order_id',
63
+ onDelete: 'NO ACTION',
64
+ },
65
+ ],
66
+ });
73
67
 
74
- const viewSQL = `CREATE VIEW ${viewName} as SELECT orders.*, items.name as item_name FROM ${OrderItem.quotedTableName()} as orders INNER JOIN ${Item.quotedTableName()} as items ON orders.item_id = items.id`;
68
+ Item = db.collection({
69
+ name: 'items',
70
+ fields: [{ name: 'name', type: 'string' }],
71
+ });
75
72
 
76
- await db.sequelize.query(viewSQL);
73
+ await db.sync();
77
74
 
78
- const OrderItemView = db.collection({
79
- name: viewName,
80
- view: true,
81
- schema: db.inDialect('postgres') ? 'public' : undefined,
82
- fields: [
83
- {
84
- type: 'bigInt',
85
- name: 'order_id',
86
- },
87
- {
88
- type: 'bigInt',
89
- name: 'item_id',
90
- onDelete: 'CASCADE',
91
- },
92
- ],
93
- });
75
+ const viewName = 'order_item_view';
94
76
 
95
- await db.sync();
77
+ const dropViewSQL = `DROP VIEW IF EXISTS ${viewName}`;
78
+ await db.sequelize.query(dropViewSQL);
96
79
 
97
- Order.setField('items', {
98
- type: 'belongsToMany',
99
- target: 'orderItems',
100
- through: viewName,
101
- foreignKey: 'order_id',
102
- otherKey: 'item_id',
103
- sourceKey: 'id',
104
- targetKey: 'id',
105
- onDelete: 'CASCADE',
106
- });
80
+ const viewSQL = `CREATE VIEW ${viewName} as SELECT order_item.order_id as order_id, order_item.item_id as item_id, items.name as item_name FROM ${OrderItem.quotedTableName()} as order_item INNER JOIN ${Item.quotedTableName()} as items ON order_item.item_id = items.id`;
107
81
 
108
- await db.sync();
82
+ await db.sequelize.query(viewSQL);
109
83
 
110
- const order1 = await db.getRepository('orders').create({
111
- values: {
112
- name: 'order1',
113
- orderItems: [
84
+ OrderItemView = db.collection({
85
+ name: viewName,
86
+ view: true,
87
+ schema: db.inDialect('postgres') ? 'public' : undefined,
88
+ fields: [
114
89
  {
115
- count: 1,
116
- item: {
117
- name: 'item1',
118
- },
90
+ type: 'bigInt',
91
+ name: 'order_id',
119
92
  },
120
93
  {
121
- count: 2,
122
- item: {
123
- name: 'item2',
124
- },
94
+ type: 'bigInt',
95
+ name: 'item_id',
96
+ onDelete: 'CASCADE',
125
97
  },
126
98
  ],
127
- },
99
+ });
100
+
101
+ await db.sync();
102
+
103
+ Order.setField('items', {
104
+ type: 'belongsToMany',
105
+ target: 'items',
106
+ through: viewName,
107
+ foreignKey: 'order_id',
108
+ otherKey: 'item_id',
109
+ sourceKey: 'id',
110
+ targetKey: 'id',
111
+ onDelete: 'CASCADE',
112
+ });
113
+
114
+ await db.sync();
115
+
116
+ await db.getRepository('orders').create({
117
+ values: {
118
+ name: 'order1',
119
+ orderItems: [
120
+ {
121
+ count: 1,
122
+ item: {
123
+ name: 'item1',
124
+ },
125
+ },
126
+ {
127
+ count: 2,
128
+ item: {
129
+ name: 'item2',
130
+ },
131
+ },
132
+ ],
133
+ },
134
+ });
128
135
  });
129
136
 
130
- const item1 = await db.getRepository('items').findOne({
131
- filter: {
132
- name: 'item1',
133
- },
137
+ it('should skip on delete on view collection', async () => {
138
+ const order1 = await db.getRepository('orders').findOne({});
139
+
140
+ const item1 = await db.getRepository('items').findOne({
141
+ filter: {
142
+ name: 'item1',
143
+ },
144
+ });
145
+
146
+ let error;
147
+ try {
148
+ await db.getRepository('orders').destroy({
149
+ filterByTk: order1.get('id'),
150
+ });
151
+ } catch (err) {
152
+ error = err;
153
+ }
154
+
155
+ expect(error).toBeUndefined();
134
156
  });
135
157
 
136
- let error;
137
- try {
138
- await db.getRepository('orders').destroy({
139
- filterByTk: order1.get('id'),
158
+ it('should filter by view collection as through table', async () => {
159
+ const orders = await db.getRepository('orders').find({
160
+ appends: ['items'],
161
+ filter: {
162
+ items: {
163
+ name: 'not exists',
164
+ },
165
+ },
140
166
  });
141
- } catch (err) {
142
- error = err;
143
- }
144
167
 
145
- expect(error).toBeUndefined();
168
+ expect(orders).toHaveLength(0);
169
+ });
146
170
  });
147
171
 
148
172
  it('should update view collection', async () => {
@@ -196,7 +220,7 @@ pgOnly()('', () => {
196
220
  });
197
221
 
198
222
  // create INSTEAD OF INSERT trigger
199
- await db.sequelize.query(`
223
+ await db.sequelize.query(`
200
224
  CREATE OR REPLACE FUNCTION insert_users_with_group() RETURNS TRIGGER AS $$
201
225
  DECLARE
202
226
  new_group_id BIGINT;
@@ -240,6 +240,9 @@ export class OptionsParser {
240
240
  protected parseAppends(appends: Appends, filterParams: any) {
241
241
  if (!appends) return filterParams;
242
242
 
243
+ // sort appends by path length
244
+ appends = lodash.sortBy(appends, (append) => append.split('.').length);
245
+
243
246
  /**
244
247
  * set include params
245
248
  * @param model
@@ -287,6 +290,25 @@ export class OptionsParser {
287
290
  // if include from filter, remove fromFilter attribute
288
291
  if (existIncludeIndex != -1) {
289
292
  delete queryParams['include'][existIncludeIndex]['fromFilter'];
293
+
294
+ // set include attributes to all attributes
295
+ if (
296
+ Array.isArray(queryParams['include'][existIncludeIndex]['attributes']) &&
297
+ queryParams['include'][existIncludeIndex]['attributes'].length == 0
298
+ ) {
299
+ queryParams['include'][existIncludeIndex]['attributes'] = {
300
+ include: [],
301
+ };
302
+ }
303
+ }
304
+
305
+ if (
306
+ lastLevel &&
307
+ existIncludeIndex != -1 &&
308
+ lodash.get(queryParams, ['include', existIncludeIndex, 'attributes', 'include'])?.length == 0
309
+ ) {
310
+ // if append is last level and association exists, ignore it
311
+ return;
290
312
  }
291
313
 
292
314
  // if association not exist, create it
@@ -20,7 +20,9 @@ export class SyncRunner {
20
20
 
21
21
  if (!parents) {
22
22
  throw new Error(
23
- `Inherit model ${inheritedCollection.name} can't be created without parents, parents option is ${lodash
23
+ `Inherit model ${
24
+ inheritedCollection.name
25
+ } can't be created without parents, parents option is ${lodash
24
26
  .castArray(inheritedCollection.options.inherits)
25
27
  .join(', ')}`,
26
28
  );
@@ -58,7 +60,7 @@ export class SyncRunner {
58
60
  const columnDefault = sequenceNameResult[0][0]['column_default'];
59
61
 
60
62
  if (!columnDefault) {
61
- throw new Error(`Can't find sequence name of ${parent}`);
63
+ throw new Error(`Can't find sequence name of parent collection ${parent.options.name}`);
62
64
  }
63
65
 
64
66
  const regex = new RegExp(/nextval\('(.*)'::regclass\)/);