@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Collection } from '../collection';
|
|
2
|
-
import { mockDatabase } from './index';
|
|
3
|
-
import { OptionsParser } from '../options-parser';
|
|
4
2
|
import { Database } from '../database';
|
|
3
|
+
import { OptionsParser } from '../options-parser';
|
|
4
|
+
import { mockDatabase } from './index';
|
|
5
5
|
|
|
6
6
|
describe('option parser', () => {
|
|
7
7
|
let db: Database;
|
|
@@ -83,6 +83,10 @@ describe('option parser', () => {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
test('with sort option', () => {
|
|
86
|
+
if (db.inDialect('mysql')) {
|
|
87
|
+
expect(1).toBe(1);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
86
90
|
let options: any = {
|
|
87
91
|
sort: ['id'],
|
|
88
92
|
};
|
|
@@ -91,7 +95,7 @@ describe('option parser', () => {
|
|
|
91
95
|
collection: User,
|
|
92
96
|
});
|
|
93
97
|
let params = parser.toSequelizeParams();
|
|
94
|
-
expect(params['order']).toEqual([['id', 'ASC']]);
|
|
98
|
+
expect(params['order']).toEqual([['id', 'ASC NULLS LAST']]);
|
|
95
99
|
|
|
96
100
|
options = {
|
|
97
101
|
sort: ['id', '-posts.title', 'posts.comments.createdAt'],
|
|
@@ -102,9 +106,9 @@ describe('option parser', () => {
|
|
|
102
106
|
});
|
|
103
107
|
params = parser.toSequelizeParams();
|
|
104
108
|
expect(params['order']).toEqual([
|
|
105
|
-
['id', 'ASC'],
|
|
106
|
-
[Post.model, 'title', 'DESC'],
|
|
107
|
-
[Post.model, Comment.model, 'createdAt', 'ASC'],
|
|
109
|
+
['id', 'ASC NULLS LAST'],
|
|
110
|
+
[Post.model, 'title', 'DESC NULLS LAST'],
|
|
111
|
+
[Post.model, Comment.model, 'createdAt', 'ASC NULLS LAST'],
|
|
108
112
|
]);
|
|
109
113
|
});
|
|
110
114
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Database } from '../database';
|
|
2
|
+
import { mockDatabase } from './index';
|
|
3
|
+
|
|
4
|
+
// TODO
|
|
5
|
+
describe('sequelize-hooks', () => {
|
|
6
|
+
let db: Database;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
db = mockDatabase();
|
|
10
|
+
await db.sync();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await db.close();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('exec order', async () => {
|
|
18
|
+
const collection = db.collection({
|
|
19
|
+
name: 't_test',
|
|
20
|
+
});
|
|
21
|
+
const orders = [];
|
|
22
|
+
db.on('beforeCreate', () => {
|
|
23
|
+
orders.push('beforeCreate');
|
|
24
|
+
});
|
|
25
|
+
db.on('t_test.beforeCreate', () => {
|
|
26
|
+
orders.push('model.beforeCreate');
|
|
27
|
+
});
|
|
28
|
+
db.on('afterCreate', () => {
|
|
29
|
+
orders.push('afterCreate');
|
|
30
|
+
});
|
|
31
|
+
db.on('t_test.afterCreate', () => {
|
|
32
|
+
orders.push('model.afterCreate');
|
|
33
|
+
});
|
|
34
|
+
await collection.sync();
|
|
35
|
+
await collection.model.create();
|
|
36
|
+
expect(orders).toEqual([
|
|
37
|
+
'model.beforeCreate',
|
|
38
|
+
'beforeCreate',
|
|
39
|
+
'model.afterCreate',
|
|
40
|
+
'afterCreate'
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('afterSync', () => {
|
|
45
|
+
test('singular name', async () => {
|
|
46
|
+
const collection = db.collection({
|
|
47
|
+
name: 't_test',
|
|
48
|
+
});
|
|
49
|
+
const spy = jest.fn();
|
|
50
|
+
db.on('t_test.afterSync', () => {
|
|
51
|
+
spy('afterSync');
|
|
52
|
+
});
|
|
53
|
+
await collection.sync();
|
|
54
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('plural name', async () => {
|
|
58
|
+
const collection = db.collection({
|
|
59
|
+
name: 't_tests',
|
|
60
|
+
});
|
|
61
|
+
const spy = jest.fn();
|
|
62
|
+
db.on('t_tests.afterSync', () => {
|
|
63
|
+
spy('afterSync');
|
|
64
|
+
});
|
|
65
|
+
await collection.sync();
|
|
66
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Database } from '../database';
|
|
2
|
+
import { mockDatabase } from './';
|
|
3
|
+
|
|
4
|
+
describe('sort', function () {
|
|
5
|
+
let db: Database;
|
|
6
|
+
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
db = mockDatabase();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await db.close();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('order nulls last', async () => {
|
|
16
|
+
const Test = db.collection({
|
|
17
|
+
name: 'test',
|
|
18
|
+
fields: [
|
|
19
|
+
{
|
|
20
|
+
type: 'integer',
|
|
21
|
+
name: 'num',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
await Test.sync();
|
|
26
|
+
await Test.model.bulkCreate([
|
|
27
|
+
{
|
|
28
|
+
num: 3,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
num: 2,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
num: null,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
num: 1,
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
const items = await Test.repository.find({
|
|
41
|
+
sort: '-num',
|
|
42
|
+
});
|
|
43
|
+
const nums = items.map((item) => item.get('num'));
|
|
44
|
+
expect(nums).toEqual([3,2,1,null]);
|
|
45
|
+
const items2 = await Test.repository.find({
|
|
46
|
+
sort: 'num',
|
|
47
|
+
});
|
|
48
|
+
const nums2 = items2.map((item) => item.get('num'));
|
|
49
|
+
expect(nums2).toEqual([1,2,3,null]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -173,7 +173,7 @@ describe('update associations', () => {
|
|
|
173
173
|
await updateAssociations(user1, {
|
|
174
174
|
posts: post1,
|
|
175
175
|
});
|
|
176
|
-
|
|
176
|
+
|
|
177
177
|
expect(user1.toJSON()).toMatchObject({
|
|
178
178
|
name: 'user1',
|
|
179
179
|
});
|
|
@@ -191,7 +191,7 @@ describe('update associations', () => {
|
|
|
191
191
|
name: 'post111',
|
|
192
192
|
},
|
|
193
193
|
});
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
expect(user1.toJSON()).toMatchObject({
|
|
196
196
|
name: 'user1',
|
|
197
197
|
});
|
|
@@ -216,7 +216,7 @@ describe('update associations', () => {
|
|
|
216
216
|
post3,
|
|
217
217
|
],
|
|
218
218
|
});
|
|
219
|
-
|
|
219
|
+
|
|
220
220
|
expect(user1.toJSON()).toMatchObject({
|
|
221
221
|
name: 'user1',
|
|
222
222
|
});
|
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import lodash from 'lodash';
|
|
3
1
|
import path from 'path';
|
|
2
|
+
import { readdir } from 'fs/promises';
|
|
3
|
+
import { isPlainObject } from 'lodash';
|
|
4
|
+
import { requireModule } from '@nocobase/utils';
|
|
4
5
|
|
|
5
6
|
export type ImportFileExtension = 'js' | 'ts' | 'json';
|
|
6
7
|
|
|
7
|
-
async function requireModule(module: any) {
|
|
8
|
-
if (typeof module === 'string') {
|
|
9
|
-
module = require(module);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (typeof module !== 'object') {
|
|
13
|
-
return module;
|
|
14
|
-
}
|
|
15
|
-
return module.__esModule ? module.default : module;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
8
|
export class ImporterReader {
|
|
19
9
|
directory: string;
|
|
20
10
|
extensions: Set<string>;
|
|
@@ -30,11 +20,10 @@ export class ImporterReader {
|
|
|
30
20
|
}
|
|
31
21
|
|
|
32
22
|
async read() {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
23
|
+
const files = await readdir(this.directory, {
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
});
|
|
26
|
+
const modules = files
|
|
38
27
|
.filter((fileName) => {
|
|
39
28
|
if (fileName.endsWith('.d.ts')) {
|
|
40
29
|
return false;
|
|
@@ -42,8 +31,11 @@ export class ImporterReader {
|
|
|
42
31
|
const ext = path.parse(fileName).ext.replace('.', '');
|
|
43
32
|
return this.extensions.has(ext);
|
|
44
33
|
})
|
|
45
|
-
.map(
|
|
34
|
+
.map((fileName) => {
|
|
35
|
+
const mod = requireModule(path.join(this.directory, fileName));
|
|
36
|
+
return typeof mod === 'function' ? mod() : mod;
|
|
37
|
+
});
|
|
46
38
|
|
|
47
|
-
return (await Promise.all(modules)).filter((module) =>
|
|
39
|
+
return (await Promise.all(modules)).filter((module) => isPlainObject(module));
|
|
48
40
|
}
|
|
49
41
|
}
|
package/src/collection.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import merge from 'deepmerge';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
import { default as lodash, default as _ } from 'lodash';
|
|
4
|
-
import {
|
|
4
|
+
import { ModelCtor, ModelOptions, QueryInterfaceDropTableOptions, SyncOptions, Transactionable } from 'sequelize';
|
|
5
5
|
import { Database } from './database';
|
|
6
6
|
import { Field, FieldOptions } from './fields';
|
|
7
7
|
import { Model } from './model';
|
|
@@ -53,6 +53,10 @@ export class Collection<
|
|
|
53
53
|
return this.options.name;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
get db() {
|
|
57
|
+
return this.context.database;
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
constructor(options: CollectionOptions, context?: CollectionContext) {
|
|
57
61
|
super();
|
|
58
62
|
this.context = context;
|
|
@@ -186,6 +190,26 @@ export class Collection<
|
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
192
|
|
|
193
|
+
remove() {
|
|
194
|
+
this.context.database.removeCollection(this.name);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async removeFromDb(options?: QueryInterfaceDropTableOptions) {
|
|
198
|
+
if (
|
|
199
|
+
await this.existsInDb({
|
|
200
|
+
transaction: options?.transaction,
|
|
201
|
+
})
|
|
202
|
+
) {
|
|
203
|
+
const queryInterface = this.db.sequelize.getQueryInterface();
|
|
204
|
+
await queryInterface.dropTable(this.model.tableName, options);
|
|
205
|
+
}
|
|
206
|
+
this.remove();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async existsInDb(options?: Transactionable) {
|
|
210
|
+
return this.db.collectionExistsInDb(this.name, options);
|
|
211
|
+
}
|
|
212
|
+
|
|
189
213
|
removeField(name) {
|
|
190
214
|
if (!this.fields.has(name)) {
|
|
191
215
|
return;
|
|
@@ -195,7 +219,7 @@ export class Collection<
|
|
|
195
219
|
if (bool) {
|
|
196
220
|
this.emit('field.afterRemove', field);
|
|
197
221
|
}
|
|
198
|
-
return
|
|
222
|
+
return field as Field;
|
|
199
223
|
}
|
|
200
224
|
|
|
201
225
|
/**
|
package/src/database.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
|
2
2
|
import merge from 'deepmerge';
|
|
3
3
|
import { EventEmitter } from 'events';
|
|
4
|
+
import glob from 'glob';
|
|
4
5
|
import lodash from 'lodash';
|
|
5
|
-
import { isAbsolute, resolve } from 'path';
|
|
6
|
+
import { basename, isAbsolute, resolve } from 'path';
|
|
6
7
|
import {
|
|
7
8
|
ModelCtor,
|
|
8
9
|
Op,
|
|
@@ -11,12 +12,15 @@ import {
|
|
|
11
12
|
QueryOptions,
|
|
12
13
|
Sequelize,
|
|
13
14
|
SyncOptions,
|
|
15
|
+
Transactionable,
|
|
14
16
|
Utils
|
|
15
17
|
} from 'sequelize';
|
|
18
|
+
import { SequelizeStorage, Umzug } from 'umzug';
|
|
16
19
|
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
|
17
20
|
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
|
18
21
|
import * as FieldTypes from './fields';
|
|
19
22
|
import { Field, FieldContext, RelationField } from './fields';
|
|
23
|
+
import { Migrations } from './migration';
|
|
20
24
|
import { Model } from './model';
|
|
21
25
|
import { ModelHook } from './model-hook';
|
|
22
26
|
import extendOperators from './operators';
|
|
@@ -36,9 +40,10 @@ interface MapOf<T> {
|
|
|
36
40
|
|
|
37
41
|
export interface IDatabaseOptions extends Options {
|
|
38
42
|
tablePrefix?: string;
|
|
43
|
+
migrator?: any;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
export type DatabaseOptions = IDatabaseOptions
|
|
46
|
+
export type DatabaseOptions = IDatabaseOptions;
|
|
42
47
|
|
|
43
48
|
interface RegisterOperatorsContext {
|
|
44
49
|
db?: Database;
|
|
@@ -51,10 +56,19 @@ export interface CleanOptions extends QueryInterfaceDropAllTablesOptions {
|
|
|
51
56
|
drop?: boolean;
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
export type AddMigrationsOptions = {
|
|
60
|
+
context?: any;
|
|
61
|
+
namespace?: string;
|
|
62
|
+
extensions?: string[];
|
|
63
|
+
directory: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
54
66
|
type OperatorFunc = (value: any, ctx?: RegisterOperatorsContext) => any;
|
|
55
67
|
|
|
56
68
|
export class Database extends EventEmitter implements AsyncEmitter {
|
|
57
69
|
sequelize: Sequelize;
|
|
70
|
+
migrator: Umzug;
|
|
71
|
+
migrations: Migrations;
|
|
58
72
|
fieldTypes = new Map();
|
|
59
73
|
options: IDatabaseOptions;
|
|
60
74
|
models = new Map<string, ModelCtor<Model>>();
|
|
@@ -71,27 +85,30 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|
|
71
85
|
constructor(options: DatabaseOptions) {
|
|
72
86
|
super();
|
|
73
87
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
sync: {
|
|
79
|
-
alter: {
|
|
80
|
-
drop: false,
|
|
81
|
-
},
|
|
82
|
-
force: false,
|
|
88
|
+
const opts = {
|
|
89
|
+
sync: {
|
|
90
|
+
alter: {
|
|
91
|
+
drop: false,
|
|
83
92
|
},
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
force: false,
|
|
94
|
+
},
|
|
95
|
+
...options,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (options.storage && options.storage !== ':memory:') {
|
|
99
|
+
if (!isAbsolute(options.storage)) {
|
|
100
|
+
opts.storage = resolve(process.cwd(), options.storage);
|
|
90
101
|
}
|
|
91
|
-
this.sequelize = new Sequelize(opts);
|
|
92
|
-
this.options = opts;
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
if (options.dialect === 'sqlite') {
|
|
105
|
+
delete opts.timezone;
|
|
106
|
+
} else if (!opts.timezone) {
|
|
107
|
+
opts.timezone = '+00:00';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.sequelize = new Sequelize(opts);
|
|
111
|
+
this.options = opts;
|
|
95
112
|
this.collections = new Map();
|
|
96
113
|
this.modelHook = new ModelHook(this);
|
|
97
114
|
|
|
@@ -116,6 +133,62 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
this.initOperators();
|
|
136
|
+
|
|
137
|
+
const migratorOptions: any = this.options.migrator || {};
|
|
138
|
+
|
|
139
|
+
const context = {
|
|
140
|
+
db: this,
|
|
141
|
+
sequelize: this.sequelize,
|
|
142
|
+
queryInterface: this.sequelize.getQueryInterface(),
|
|
143
|
+
...migratorOptions.context,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
this.migrations = new Migrations(context);
|
|
147
|
+
this.migrator = new Umzug({
|
|
148
|
+
logger: migratorOptions.logger || console,
|
|
149
|
+
migrations: this.migrations.callback(),
|
|
150
|
+
context,
|
|
151
|
+
storage: new SequelizeStorage({
|
|
152
|
+
modelName: `${this.options.tablePrefix || ''}migrations`,
|
|
153
|
+
...migratorOptions.storage,
|
|
154
|
+
sequelize: this.sequelize,
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
addMigration(item) {
|
|
160
|
+
return this.migrations.add(item);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
addMigrations(options: AddMigrationsOptions) {
|
|
164
|
+
const { namespace, context, extensions = ['js', 'ts'], directory } = options;
|
|
165
|
+
const patten = `${directory}/*.{${extensions.join(',')}}`;
|
|
166
|
+
const files = glob.sync(patten, {
|
|
167
|
+
ignore: ['**/*.d.ts'],
|
|
168
|
+
});
|
|
169
|
+
for (const file of files) {
|
|
170
|
+
let filename = basename(file);
|
|
171
|
+
filename = filename.substring(0, filename.lastIndexOf('.')) || filename;
|
|
172
|
+
this.migrations.add({
|
|
173
|
+
name: namespace ? `${namespace}/${filename}` : filename,
|
|
174
|
+
migration: this.requireModule(file),
|
|
175
|
+
context,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
inDialect(...dialect: string[]) {
|
|
181
|
+
return dialect.includes(this.sequelize.getDialect());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private requireModule(module: any) {
|
|
185
|
+
if (typeof module === 'string') {
|
|
186
|
+
module = require(module);
|
|
187
|
+
}
|
|
188
|
+
if (typeof module !== 'object') {
|
|
189
|
+
return module;
|
|
190
|
+
}
|
|
191
|
+
return module.__esModule ? module.default : module;
|
|
119
192
|
}
|
|
120
193
|
|
|
121
194
|
/**
|
|
@@ -161,9 +234,13 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|
|
161
234
|
|
|
162
235
|
const result = this.collections.delete(name);
|
|
163
236
|
|
|
237
|
+
this.sequelize.modelManager.removeModel(collection.model);
|
|
238
|
+
|
|
164
239
|
if (result) {
|
|
165
240
|
this.emit('afterRemoveCollection', collection);
|
|
166
241
|
}
|
|
242
|
+
|
|
243
|
+
return collection;
|
|
167
244
|
}
|
|
168
245
|
|
|
169
246
|
getModel<M extends Model>(name: string) {
|
|
@@ -268,12 +345,19 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|
|
268
345
|
}
|
|
269
346
|
}
|
|
270
347
|
|
|
348
|
+
async collectionExistsInDb(name, options?: Transactionable) {
|
|
349
|
+
const tables = await this.sequelize.getQueryInterface().showAllTables({
|
|
350
|
+
transaction: options?.transaction,
|
|
351
|
+
});
|
|
352
|
+
return !!tables.find((table) => table === `${this.getTablePrefix()}${name}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
271
355
|
public isSqliteMemory() {
|
|
272
356
|
return this.sequelize.getDialect() === 'sqlite' && lodash.get(this.options, 'storage') == ':memory:';
|
|
273
357
|
}
|
|
274
358
|
|
|
275
|
-
async auth(options: QueryOptions & {
|
|
276
|
-
const {
|
|
359
|
+
async auth(options: QueryOptions & { retry?: number } = {}) {
|
|
360
|
+
const { retry = 10, ...others } = options;
|
|
277
361
|
const delay = (ms) => new Promise((yea) => setTimeout(yea, ms));
|
|
278
362
|
let count = 1;
|
|
279
363
|
const authenticate = async () => {
|
|
@@ -282,7 +366,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|
|
282
366
|
console.log('Connection has been established successfully.');
|
|
283
367
|
return true;
|
|
284
368
|
} catch (error) {
|
|
285
|
-
if (count >=
|
|
369
|
+
if (count >= retry) {
|
|
286
370
|
throw error;
|
|
287
371
|
}
|
|
288
372
|
console.log('reconnecting...', count);
|
|
@@ -291,7 +375,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|
|
291
375
|
return await authenticate();
|
|
292
376
|
}
|
|
293
377
|
};
|
|
294
|
-
|
|
295
378
|
return await authenticate();
|
|
296
379
|
}
|
|
297
380
|
|
|
@@ -322,13 +405,13 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|
|
322
405
|
return this.sequelize.close();
|
|
323
406
|
}
|
|
324
407
|
|
|
325
|
-
on(event: string | symbol, listener
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (modelEventName && !this.modelHook.hasBindEvent(modelEventName)) {
|
|
329
|
-
this.sequelize.addHook(modelEventName, this.modelHook.sequelizeHookBuilder(modelEventName));
|
|
408
|
+
on(event: string | symbol, listener): this {
|
|
409
|
+
// NOTE: to match if event is a sequelize or model type
|
|
410
|
+
const type = this.modelHook.match(event);
|
|
330
411
|
|
|
331
|
-
|
|
412
|
+
if (type && !this.modelHook.hasBoundEvent(type)) {
|
|
413
|
+
this.sequelize.addHook(type, this.modelHook.buildSequelizeHook(type));
|
|
414
|
+
this.modelHook.bindEvent(type);
|
|
332
415
|
}
|
|
333
416
|
|
|
334
417
|
return super.on(event, listener);
|
package/src/fields/field.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DataType,
|
|
4
|
+
ModelAttributeColumnOptions,
|
|
5
|
+
ModelIndexesOptions,
|
|
6
|
+
QueryInterfaceOptions,
|
|
7
|
+
SyncOptions,
|
|
8
|
+
Transactionable
|
|
9
|
+
} from 'sequelize';
|
|
3
10
|
import { Collection } from '../collection';
|
|
4
11
|
import { Database } from '../database';
|
|
5
12
|
|
|
@@ -75,6 +82,86 @@ export abstract class Field {
|
|
|
75
82
|
return this.options[name];
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
remove() {
|
|
86
|
+
return this.collection.removeField(this.name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async removeFromDb(options?: QueryInterfaceOptions) {
|
|
90
|
+
if (!this.collection.model.rawAttributes[this.name]) {
|
|
91
|
+
this.remove();
|
|
92
|
+
// console.log('field is not attribute');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if ((this.collection.model as any)._virtualAttributes.has(this.name)) {
|
|
96
|
+
this.remove();
|
|
97
|
+
// console.log('field is virtual attribute');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (this.collection.model.primaryKeyAttributes.includes(this.name)) {
|
|
101
|
+
// 主键不能删除
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (this.collection.model.options.timestamps !== false) {
|
|
105
|
+
// timestamps 相关字段不删除
|
|
106
|
+
if (['createdAt', 'updatedAt', 'deletedAt'].includes(this.name)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 排序字段通过 sortable 控制
|
|
111
|
+
const sortable = this.collection.options.sortable;
|
|
112
|
+
if (sortable) {
|
|
113
|
+
let sortField: string;
|
|
114
|
+
if (sortable === true) {
|
|
115
|
+
sortField = 'sort';
|
|
116
|
+
} else if (typeof sortable === 'string') {
|
|
117
|
+
sortField = sortable;
|
|
118
|
+
} else if (sortable.name) {
|
|
119
|
+
sortField = sortable.name || 'sort';
|
|
120
|
+
}
|
|
121
|
+
if (this.name === sortField) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (this.options.field && this.name !== this.options.field) {
|
|
126
|
+
// field 指向的是真实的字段名,如果与 name 不一样,说明字段只是引用
|
|
127
|
+
this.remove();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
await this.existsInDb({
|
|
132
|
+
transaction: options?.transaction,
|
|
133
|
+
})
|
|
134
|
+
) {
|
|
135
|
+
const queryInterface = this.database.sequelize.getQueryInterface();
|
|
136
|
+
await queryInterface.removeColumn(this.collection.model.tableName, this.name, options);
|
|
137
|
+
}
|
|
138
|
+
this.remove();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async existsInDb(options?: Transactionable) {
|
|
142
|
+
const opts = {
|
|
143
|
+
transaction: options?.transaction,
|
|
144
|
+
};
|
|
145
|
+
let sql;
|
|
146
|
+
if (this.database.sequelize.getDialect() === 'sqlite') {
|
|
147
|
+
sql = `SELECT * from pragma_table_info('${this.collection.model.tableName}') WHERE name = '${this.name}'`;
|
|
148
|
+
} else if (this.database.inDialect('mysql')) {
|
|
149
|
+
sql = `
|
|
150
|
+
select column_name
|
|
151
|
+
from INFORMATION_SCHEMA.COLUMNS
|
|
152
|
+
where TABLE_SCHEMA='${this.database.options.database}' AND TABLE_NAME='${this.collection.model.tableName}' AND column_name='${this.name}'
|
|
153
|
+
`;
|
|
154
|
+
} else {
|
|
155
|
+
sql = `
|
|
156
|
+
select column_name
|
|
157
|
+
from INFORMATION_SCHEMA.COLUMNS
|
|
158
|
+
where TABLE_NAME='${this.collection.model.tableName}' AND column_name='${this.name}'
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
const [rows] = await this.database.sequelize.query(sql, opts);
|
|
162
|
+
return rows.length > 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
78
165
|
merge(obj: any) {
|
|
79
166
|
Object.assign(this.options, obj);
|
|
80
167
|
}
|