@nocobase/database 0.7.0-alpha.82 → 0.7.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.
Files changed (54) hide show
  1. package/lib/collection-importer.js +25 -42
  2. package/lib/collection.d.ts +6 -2
  3. package/lib/collection.js +37 -5
  4. package/lib/database.d.ts +21 -5
  5. package/lib/database.js +161 -49
  6. package/lib/fields/field.d.ts +4 -1
  7. package/lib/fields/field.js +117 -0
  8. package/lib/fields/formula-field.d.ts +19 -0
  9. package/lib/fields/formula-field.js +184 -0
  10. package/lib/fields/index.d.ts +3 -1
  11. package/lib/fields/index.js +13 -0
  12. package/lib/index.d.ts +1 -0
  13. package/lib/index.js +14 -0
  14. package/lib/migration.d.ts +35 -0
  15. package/lib/migration.js +90 -0
  16. package/lib/mock-database.d.ts +1 -0
  17. package/lib/mock-database.js +2 -1
  18. package/lib/model-hook.d.ts +5 -5
  19. package/lib/model-hook.js +26 -18
  20. package/lib/options-parser.js +65 -43
  21. package/lib/relation-repository/relation-repository.js +11 -1
  22. package/lib/relation-repository/single-relation-repository.js +8 -1
  23. package/lib/repository.js +12 -4
  24. package/lib/update-associations.js +1 -1
  25. package/package.json +9 -4
  26. package/src/__tests__/collection.test.ts +27 -0
  27. package/src/__tests__/database.test.ts +47 -0
  28. package/src/__tests__/fields/formula-field.test.ts +69 -0
  29. package/src/__tests__/fixtures/migrations/m1.ts +7 -0
  30. package/src/__tests__/fixtures/migrations/m2.ts +7 -0
  31. package/src/__tests__/hooks/afterCreateWithAssociations.test.ts +33 -0
  32. package/src/__tests__/migrator.test.ts +70 -0
  33. package/src/__tests__/model-hook.test.ts +54 -0
  34. package/src/__tests__/option-parser.test.ts +10 -6
  35. package/src/__tests__/relation-repository/belongs-to-many-repository.test.ts +1 -1
  36. package/src/__tests__/sequelize-hooks.test.ts +69 -0
  37. package/src/__tests__/sort.test.ts +51 -0
  38. package/src/__tests__/update-associations.test.ts +3 -3
  39. package/src/collection-importer.ts +12 -20
  40. package/src/collection.ts +26 -2
  41. package/src/database.ts +112 -29
  42. package/src/fields/field.ts +88 -1
  43. package/src/fields/formula-field.ts +106 -0
  44. package/src/fields/index.ts +3 -0
  45. package/src/index.ts +1 -0
  46. package/src/migration.ts +76 -0
  47. package/src/mock-database.ts +1 -0
  48. package/src/model-hook.ts +25 -21
  49. package/src/options-parser.ts +13 -9
  50. package/src/relation-repository/multiple-relation-repository.ts +8 -2
  51. package/src/relation-repository/relation-repository.ts +1 -0
  52. package/src/relation-repository/single-relation-repository.ts +5 -1
  53. package/src/repository.ts +16 -4
  54. package/src/update-associations.ts +1 -1
@@ -0,0 +1,106 @@
1
+ import { DataTypes } from 'sequelize';
2
+ import { BaseColumnFieldOptions, Field } from './field';
3
+ import * as math from 'mathjs';
4
+
5
+ export class FormulaField extends Field {
6
+ get dataType() {
7
+ return DataTypes.FLOAT;
8
+ }
9
+
10
+ caculate(expression, scope) {
11
+ let result = null;
12
+ try {
13
+ result = math.evaluate(expression, scope);
14
+ result = math.round(result, 9);
15
+ } catch {}
16
+ return result;
17
+ }
18
+
19
+ async initFieldData({ transaction }) {
20
+ const { expression, name } = this.options;
21
+
22
+ const records = await this.collection.repository.find({
23
+ order: [this.collection.model.primaryKeyAttribute],
24
+ transaction,
25
+ });
26
+
27
+ for (const record of records) {
28
+ const scope = record.toJSON();
29
+ const result = this.caculate(expression, scope);
30
+ if (result) {
31
+ await record.update(
32
+ {
33
+ [name]: result,
34
+ },
35
+ {
36
+ transaction,
37
+ silent: true,
38
+ hooks: false,
39
+ },
40
+ );
41
+ }
42
+ }
43
+ }
44
+
45
+ async caculateField(instance) {
46
+ const { expression, name } = this.options;
47
+ const scope = instance.toJSON();
48
+ let result;
49
+ try {
50
+ result = math.evaluate(expression, scope);
51
+ result = math.round(result, 9);
52
+ } catch {}
53
+ if (result) {
54
+ instance.set(name, result);
55
+ }
56
+ }
57
+
58
+ async updateFieldData(instance, { transaction }) {
59
+ if (this.collection.name === instance.collectionName && instance.name === this.options.name) {
60
+ this.options = Object.assign(this.options, instance.options);
61
+ const { name, expression } = this.options;
62
+
63
+ const records = await this.collection.repository.find({
64
+ order: [this.collection.model.primaryKeyAttribute],
65
+ transaction,
66
+ });
67
+
68
+ for (const record of records) {
69
+ const scope = record.toJSON();
70
+ const result = this.caculate(expression, scope);
71
+ await record.update(
72
+ {
73
+ [name]: result,
74
+ },
75
+ {
76
+ transaction,
77
+ silent: true,
78
+ hooks: false,
79
+ },
80
+ );
81
+ }
82
+ }
83
+ }
84
+
85
+ bind() {
86
+ super.bind();
87
+ this.on('afterSync', this.initFieldData.bind(this));
88
+ this.database.on('fields.afterUpdate', this.updateFieldData.bind(this));
89
+ this.on('beforeCreate', this.caculateField.bind(this));
90
+ this.on('beforeUpdate', this.caculateField.bind(this));
91
+ }
92
+
93
+ unbind() {
94
+ super.unbind();
95
+ this.off('beforeCreate', this.caculateField.bind(this));
96
+ this.off('beforeUpdate', this.caculateField.bind(this));
97
+ this.database.off('fields.afterUpdate', this.updateFieldData.bind(this));
98
+ this.off('afterSync', this.initFieldData.bind(this));
99
+ }
100
+ }
101
+
102
+ export interface FormulaFieldOptions extends BaseColumnFieldOptions {
103
+ type: 'formula';
104
+
105
+ expression: string;
106
+ }
@@ -24,6 +24,7 @@ import { TimeFieldOptions } from './time-field';
24
24
  import { UidFieldOptions } from './uid-field';
25
25
  import { UUIDFieldOptions } from './uuid-field';
26
26
  import { VirtualFieldOptions } from './virtual-field';
27
+ import { FormulaFieldOptions } from './formula-field'
27
28
 
28
29
  export * from './array-field';
29
30
  export * from './belongs-to-field';
@@ -46,6 +47,7 @@ export * from './time-field';
46
47
  export * from './uid-field';
47
48
  export * from './uuid-field';
48
49
  export * from './virtual-field';
50
+ export * from './formula-field';
49
51
 
50
52
  export type FieldOptions =
51
53
  | BaseFieldOptions
@@ -62,6 +64,7 @@ export type FieldOptions =
62
64
  | SortFieldOptions
63
65
  | TextFieldOptions
64
66
  | VirtualFieldOptions
67
+ | FormulaFieldOptions
65
68
  | ArrayFieldOptions
66
69
  | TimeFieldOptions
67
70
  | DateFieldOptions
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from './database';
4
4
  export { Database as default } from './database';
5
5
  export * from './fields';
6
6
  export * from './magic-attribute-model';
7
+ export * from './migration';
7
8
  export * from './mock-database';
8
9
  export * from './model';
9
10
  export * from './relation-repository/belongs-to-many-repository';
@@ -0,0 +1,76 @@
1
+ import { QueryInterface, Sequelize } from 'sequelize';
2
+ import Database from './database';
3
+
4
+ export interface MigrationContext {
5
+ db: Database;
6
+ queryInterface: QueryInterface;
7
+ sequelize: Sequelize;
8
+ }
9
+
10
+ export class Migration {
11
+ public name: string;
12
+
13
+ public context: { db: Database; [key: string]: any };
14
+
15
+ constructor(context: MigrationContext) {
16
+ this.context = context;
17
+ }
18
+
19
+ get db() {
20
+ return this.context.db;
21
+ }
22
+
23
+ get sequelize() {
24
+ return this.context.db.sequelize;
25
+ }
26
+
27
+ get queryInterface() {
28
+ return this.context.db.sequelize.getQueryInterface();
29
+ }
30
+
31
+ async up() {
32
+ // todo
33
+ }
34
+
35
+ async down() {
36
+ // todo
37
+ }
38
+ }
39
+
40
+ export interface MigrationItem {
41
+ name: string;
42
+ migration?: typeof Migration;
43
+ context?: any;
44
+ up?: any;
45
+ down?: any;
46
+ }
47
+
48
+ export class Migrations {
49
+ items = [];
50
+ context: any;
51
+
52
+ constructor(context: any) {
53
+ this.context = context;
54
+ }
55
+
56
+ clear() {
57
+ this.items = [];
58
+ }
59
+
60
+ add(item: MigrationItem) {
61
+ const Migration = item.migration;
62
+ if (Migration) {
63
+ const migration = new Migration({ ...this.context, ...item.context });
64
+ migration.name = item.name;
65
+ this.items.push(migration);
66
+ } else {
67
+ this.items.push(item);
68
+ }
69
+ }
70
+
71
+ callback() {
72
+ return async (ctx) => {
73
+ return this.items;
74
+ };
75
+ }
76
+ }
@@ -33,6 +33,7 @@ export function getConfigByEnv() {
33
33
  charset: 'utf8mb4',
34
34
  collate: 'utf8mb4_unicode_ci',
35
35
  },
36
+ timezone: process.env.DB_TIMEZONE,
36
37
  };
37
38
  }
38
39
 
package/src/model-hook.ts CHANGED
@@ -5,24 +5,26 @@ import { Model } from './model';
5
5
 
6
6
  const { hooks } = require('sequelize/lib/hooks');
7
7
 
8
+
9
+
8
10
  export class ModelHook {
9
11
  database: Database;
10
- boundEvent = new Set<string>();
12
+
13
+ boundEvents = new Set<string>();
11
14
 
12
15
  constructor(database: Database) {
13
16
  this.database = database;
14
17
  }
15
18
 
16
- isModelHook(eventName: string | symbol): keyof SequelizeHooks | false {
17
- if (lodash.isString(eventName)) {
18
- const hookType = eventName.split('.').pop();
19
-
20
- if (hooks[hookType]) {
21
- return <keyof SequelizeHooks>hookType;
22
- }
19
+ match(event: string | Symbol): keyof SequelizeHooks | null {
20
+ // NOTE: skip Symbol event
21
+ if (!lodash.isString(event)) {
22
+ return null;
23
23
  }
24
24
 
25
- return false;
25
+ const type = event.split('.').pop();
26
+
27
+ return type in hooks ? <keyof SequelizeHooks>type : null;
26
28
  }
27
29
 
28
30
  findModelName(hookArgs) {
@@ -30,40 +32,42 @@ export class ModelHook {
30
32
  if (arg?._previousDataValues) {
31
33
  return (<Model>arg).constructor.name;
32
34
  }
33
-
34
35
  if (lodash.isPlainObject(arg)) {
35
36
  if (arg['model']) {
36
37
  return arg['model'].name;
37
38
  }
38
-
39
- if (lodash.get(arg, 'name.plural')) {
40
- return lodash.get(arg, 'name.plural');
39
+ const plural = arg?.name?.plural;
40
+ if (this.database.sequelize.isDefined(plural)) {
41
+ return plural;
42
+ }
43
+ const singular = arg?.name?.singular;
44
+ if (this.database.sequelize.isDefined(singular)) {
45
+ return singular;
41
46
  }
42
47
  }
43
48
  }
44
-
45
49
  return null;
46
50
  }
47
51
 
48
- bindEvent(eventName) {
49
- this.boundEvent.add(eventName);
52
+ bindEvent(type) {
53
+ this.boundEvents.add(type);
50
54
  }
51
55
 
52
- hasBindEvent(eventName) {
53
- return this.boundEvent.has(eventName);
56
+ hasBoundEvent(type): boolean {
57
+ return this.boundEvents.has(type);
54
58
  }
55
59
 
56
- sequelizeHookBuilder(eventName) {
60
+ buildSequelizeHook(type) {
57
61
  return async (...args: any[]) => {
58
62
  const modelName = this.findModelName(args);
59
63
 
60
64
  if (modelName) {
61
65
  // emit model event
62
- await this.database.emitAsync(`${modelName}.${eventName}`, ...args);
66
+ await this.database.emitAsync(`${modelName}.${type}`, ...args);
63
67
  }
64
68
 
65
69
  // emit sequelize global event
66
- await this.database.emitAsync(eventName, ...args);
70
+ await this.database.emitAsync(type, ...args);
67
71
  };
68
72
  }
69
73
  }
@@ -1,4 +1,4 @@
1
- import { FindAttributeOptions, ModelCtor, Op } from 'sequelize';
1
+ import { FindAttributeOptions, ModelCtor, Op, Sequelize } from 'sequelize';
2
2
  import { Collection } from './collection';
3
3
  import { Database } from './database';
4
4
  import FilterParser from './filter-parser';
@@ -70,24 +70,28 @@ export class OptionsParser {
70
70
  if (typeof sort === 'string') {
71
71
  sort = sort.split(',');
72
72
  }
73
- const orderParams = sort.map((sortKey: string) => {
74
- const direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
75
- const sortField: Array<any> = sortKey.replace('-', '').split('.');
76
-
73
+ const orderParams = [];
74
+ for (const sortKey of sort) {
75
+ let direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
76
+ let sortField: Array<any> = sortKey.replace('-', '').split('.');
77
+ if (this.database.inDialect('postgres', 'sqlite')) {
78
+ direction = `${direction} NULLS LAST`;
79
+ }
77
80
  // handle sort by association
78
81
  if (sortField.length > 1) {
79
82
  let associationModel = this.model;
80
-
81
83
  for (let i = 0; i < sortField.length - 1; i++) {
82
84
  const associationKey = sortField[i];
83
85
  sortField[i] = associationModel.associations[associationKey].target;
84
86
  associationModel = sortField[i];
85
87
  }
86
88
  }
87
-
88
89
  sortField.push(direction);
89
- return sortField;
90
- });
90
+ if (this.database.inDialect('mysql')) {
91
+ orderParams.push([Sequelize.fn('ISNULL', Sequelize.col(`${this.model.name}.${sortField[0]}`))]);
92
+ }
93
+ orderParams.push(sortField);
94
+ }
91
95
 
92
96
  if (orderParams.length > 0) {
93
97
  return {
@@ -165,8 +165,14 @@ export abstract class MultipleRelationRepository extends RelationRepository {
165
165
 
166
166
  for (const instance of instances) {
167
167
  if (options.hooks !== false) {
168
- await this.db.emitAsync(`${this.targetCollection.name}.afterUpdateWithAssociations`, instance, {...options, transaction});
169
- await this.db.emitAsync(`${this.targetCollection.name}.afterSaveWithAssociations`, instance, {...options, transaction});
168
+ await this.db.emitAsync(`${this.targetCollection.name}.afterUpdateWithAssociations`, instance, {
169
+ ...options,
170
+ transaction,
171
+ });
172
+ await this.db.emitAsync(`${this.targetCollection.name}.afterSaveWithAssociations`, instance, {
173
+ ...options,
174
+ transaction,
175
+ });
170
176
  }
171
177
  }
172
178
 
@@ -48,6 +48,7 @@ export abstract class RelationRepository {
48
48
  return (<BelongsTo | HasOne | HasMany | BelongsToMany>this.association).accessors;
49
49
  }
50
50
 
51
+ @transaction()
51
52
  async create(options?: CreateOptions): Promise<any> {
52
53
  const createAccessor = this.accessors().create;
53
54
 
@@ -59,7 +59,7 @@ export abstract class SingleRelationRepository extends RelationRepository {
59
59
  }
60
60
 
61
61
  async findOne(options?: SingleRelationFindOption): Promise<Model<any>> {
62
- return this.find(options);
62
+ return this.find({ ...options, filterByTk: null } as any);
63
63
  }
64
64
 
65
65
  @transaction()
@@ -85,6 +85,10 @@ export abstract class SingleRelationRepository extends RelationRepository {
85
85
  transaction,
86
86
  });
87
87
 
88
+ if (!target) {
89
+ throw new Error('The record does not exist');
90
+ }
91
+
88
92
  await updateModelByValues(target, options?.values, {
89
93
  ...lodash.omit(options, 'values'),
90
94
  transaction,
package/src/repository.ts CHANGED
@@ -307,8 +307,14 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
307
307
  });
308
308
 
309
309
  if (options.hooks !== false) {
310
- await this.database.emitAsync(`${this.collection.name}.afterCreateWithAssociations`, instance, options);
311
- await this.database.emitAsync(`${this.collection.name}.afterSaveWithAssociations`, instance, options);
310
+ await this.database.emitAsync(`${this.collection.name}.afterCreateWithAssociations`, instance, {
311
+ ...options,
312
+ transaction,
313
+ });
314
+ await this.database.emitAsync(`${this.collection.name}.afterSaveWithAssociations`, instance, {
315
+ ...options,
316
+ transaction,
317
+ });
312
318
  }
313
319
 
314
320
  return instance;
@@ -362,8 +368,14 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
362
368
 
363
369
  if (options.hooks !== false) {
364
370
  for (const instance of instances) {
365
- await this.database.emitAsync(`${this.collection.name}.afterUpdateWithAssociations`, instance, options);
366
- await this.database.emitAsync(`${this.collection.name}.afterSaveWithAssociations`, instance, options);
371
+ await this.database.emitAsync(`${this.collection.name}.afterUpdateWithAssociations`, instance, {
372
+ ...options,
373
+ transaction,
374
+ });
375
+ await this.database.emitAsync(`${this.collection.name}.afterSaveWithAssociations`, instance, {
376
+ ...options,
377
+ transaction,
378
+ });
367
379
  }
368
380
  }
369
381
 
@@ -297,7 +297,7 @@ export async function updateSingleAssociation(
297
297
  // @ts-ignore
298
298
  dataKey = association.targetKey;
299
299
  } else {
300
- M = association.source as ModelCtor<Model>;
300
+ M = association.target as ModelCtor<Model>;
301
301
  dataKey = M.primaryKeyAttribute;
302
302
  }
303
303