@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.
- package/lib/collection-importer.js +25 -42
- package/lib/collection.d.ts +6 -2
- package/lib/collection.js +37 -5
- package/lib/database.d.ts +21 -5
- package/lib/database.js +161 -49
- package/lib/fields/field.d.ts +4 -1
- package/lib/fields/field.js +117 -0
- package/lib/fields/formula-field.d.ts +19 -0
- package/lib/fields/formula-field.js +184 -0
- package/lib/fields/index.d.ts +3 -1
- package/lib/fields/index.js +13 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +14 -0
- package/lib/migration.d.ts +35 -0
- package/lib/migration.js +90 -0
- package/lib/mock-database.d.ts +1 -0
- package/lib/mock-database.js +2 -1
- package/lib/model-hook.d.ts +5 -5
- package/lib/model-hook.js +26 -18
- package/lib/options-parser.js +65 -43
- package/lib/relation-repository/relation-repository.js +11 -1
- package/lib/relation-repository/single-relation-repository.js +8 -1
- package/lib/repository.js +12 -4
- package/lib/update-associations.js +1 -1
- package/package.json +9 -4
- package/src/__tests__/collection.test.ts +27 -0
- package/src/__tests__/database.test.ts +47 -0
- package/src/__tests__/fields/formula-field.test.ts +69 -0
- package/src/__tests__/fixtures/migrations/m1.ts +7 -0
- package/src/__tests__/fixtures/migrations/m2.ts +7 -0
- package/src/__tests__/hooks/afterCreateWithAssociations.test.ts +33 -0
- package/src/__tests__/migrator.test.ts +70 -0
- package/src/__tests__/model-hook.test.ts +54 -0
- package/src/__tests__/option-parser.test.ts +10 -6
- package/src/__tests__/relation-repository/belongs-to-many-repository.test.ts +1 -1
- package/src/__tests__/sequelize-hooks.test.ts +69 -0
- package/src/__tests__/sort.test.ts +51 -0
- package/src/__tests__/update-associations.test.ts +3 -3
- package/src/collection-importer.ts +12 -20
- package/src/collection.ts +26 -2
- package/src/database.ts +112 -29
- package/src/fields/field.ts +88 -1
- package/src/fields/formula-field.ts +106 -0
- package/src/fields/index.ts +3 -0
- package/src/index.ts +1 -0
- package/src/migration.ts +76 -0
- package/src/mock-database.ts +1 -0
- package/src/model-hook.ts +25 -21
- package/src/options-parser.ts +13 -9
- package/src/relation-repository/multiple-relation-repository.ts +8 -2
- package/src/relation-repository/relation-repository.ts +1 -0
- package/src/relation-repository/single-relation-repository.ts +5 -1
- package/src/repository.ts +16 -4
- 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
|
+
}
|
package/src/fields/index.ts
CHANGED
|
@@ -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';
|
package/src/migration.ts
ADDED
|
@@ -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
|
+
}
|
package/src/mock-database.ts
CHANGED
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
|
-
|
|
12
|
+
|
|
13
|
+
boundEvents = new Set<string>();
|
|
11
14
|
|
|
12
15
|
constructor(database: Database) {
|
|
13
16
|
this.database = database;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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 (
|
|
40
|
-
return
|
|
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(
|
|
49
|
-
this.
|
|
52
|
+
bindEvent(type) {
|
|
53
|
+
this.boundEvents.add(type);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
return this.
|
|
56
|
+
hasBoundEvent(type): boolean {
|
|
57
|
+
return this.boundEvents.has(type);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
|
|
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}.${
|
|
66
|
+
await this.database.emitAsync(`${modelName}.${type}`, ...args);
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
// emit sequelize global event
|
|
66
|
-
await this.database.emitAsync(
|
|
70
|
+
await this.database.emitAsync(type, ...args);
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
}
|
package/src/options-parser.ts
CHANGED
|
@@ -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 =
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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, {
|
|
169
|
-
|
|
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,
|
|
311
|
-
|
|
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,
|
|
366
|
-
|
|
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.
|
|
300
|
+
M = association.target as ModelCtor<Model>;
|
|
301
301
|
dataKey = M.primaryKeyAttribute;
|
|
302
302
|
}
|
|
303
303
|
|