feat: improve database
This commit is contained in:
parent
17db8b8afb
commit
a51c5058fb
21
packages/collections/package.json
Normal file
21
packages/collections/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@nocobase/collections",
|
||||
"version": "0.4.0-alpha.7",
|
||||
"description": "",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"glob": "^7.1.6",
|
||||
"sequelize": "^6.3.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nocobase/nocobase.git",
|
||||
"directory": "packages/database"
|
||||
},
|
||||
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
|
||||
}
|
40
packages/collections/src/__tests__/index.ts
Normal file
40
packages/collections/src/__tests__/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import merge from 'deepmerge';
|
||||
import { Database, DatabaseOptions } from '../database';
|
||||
|
||||
export function generatePrefixByPath() {
|
||||
const { id } = require.main;
|
||||
const key = id
|
||||
.replace(`${process.env.PWD}/packages`, '')
|
||||
.replace(/src\/__tests__/g, '')
|
||||
.replace('.test.ts', '')
|
||||
.replace(/[^\w]/g, '_')
|
||||
.replace(/_+/g, '_');
|
||||
return key
|
||||
}
|
||||
|
||||
export function getConfig(config = {}, options?: any): DatabaseOptions {
|
||||
return merge({
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
dialect: process.env.DB_DIALECT,
|
||||
logging: process.env.DB_LOG_SQL === 'on',
|
||||
sync: {
|
||||
force: true,
|
||||
alter: {
|
||||
drop: true,
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
beforeDefine(model, options) {
|
||||
options.tableName = `${generatePrefixByPath()}_${options.tableName || options.modelName || options.name.plural}`;
|
||||
},
|
||||
},
|
||||
}, config || {}, options) as any;
|
||||
};
|
||||
|
||||
export function mockDatabase(options?: DatabaseOptions): Database {
|
||||
return new Database(getConfig(options));
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../';
|
||||
|
||||
describe('belongs to field', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('association undefined', async () => {
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
schema: [{ type: 'belongsTo', name: 'post' }],
|
||||
});
|
||||
expect(Comment.model.associations['post']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('association defined', async () => {
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
schema: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
expect(Comment.model.associations.post).toBeUndefined();
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'title' },
|
||||
],
|
||||
});
|
||||
const association = Comment.model.associations.post;
|
||||
expect(Comment.model.associations.post).toBeDefined();
|
||||
expect(association.foreignKey).toBe('postId');
|
||||
// @ts-ignore
|
||||
expect(association.targetKey).toBe('id');
|
||||
expect(Comment.model.rawAttributes.postId).toBeDefined();
|
||||
await db.sync();
|
||||
const comment = await Comment.model.create<any>();
|
||||
await comment.createPost({
|
||||
title: 'title222',
|
||||
});
|
||||
const post1 = await comment.getPost();
|
||||
expect(post1.toJSON()).toMatchObject({
|
||||
title: 'title222',
|
||||
});
|
||||
const post = await Post.model.create<any>({
|
||||
title: 'title111'
|
||||
});
|
||||
await comment.setPost(post);
|
||||
const post2 = await comment.getPost();
|
||||
expect(post2.toJSON()).toMatchObject({
|
||||
title: 'title111',
|
||||
});
|
||||
});
|
||||
|
||||
it('custom targetKey and foreignKey', async () => {
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'key', unique: true },
|
||||
],
|
||||
});
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
schema: [
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'post',
|
||||
targetKey: 'key',
|
||||
foreignKey: 'postKey',
|
||||
},
|
||||
],
|
||||
});
|
||||
const association = Comment.model.associations.post;
|
||||
expect(association).toBeDefined();
|
||||
expect(association.foreignKey).toBe('postKey');
|
||||
// @ts-ignore
|
||||
expect(association.targetKey).toBe('key');
|
||||
expect(Comment.model.rawAttributes['postKey']).toBeDefined();
|
||||
});
|
||||
|
||||
it('custom name and target', async () => {
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
schema: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'article',
|
||||
target: 'posts',
|
||||
targetKey: 'key',
|
||||
foreignKey: 'postKey',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(Comment.model.associations.article).toBeUndefined();
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'key', unique: true },
|
||||
],
|
||||
});
|
||||
const association = Comment.model.associations.article;
|
||||
expect(Comment.model.associations.article).toBeDefined();
|
||||
expect(association.foreignKey).toBe('postKey');
|
||||
// @ts-ignore
|
||||
expect(association.targetKey).toBe('key');
|
||||
expect(Comment.model.rawAttributes.postKey).toBeDefined();
|
||||
await db.sync();
|
||||
const comment = await Comment.model.create<any>();
|
||||
await comment.createArticle({
|
||||
key: 'title222',
|
||||
});
|
||||
const post1 = await comment.getArticle();
|
||||
expect(post1.toJSON()).toMatchObject({
|
||||
key: 'title222',
|
||||
});
|
||||
const post = await Post.model.create<any>({
|
||||
key: 'title111'
|
||||
});
|
||||
await comment.setArticle(post);
|
||||
const post2 = await comment.getArticle();
|
||||
expect(post2.toJSON()).toMatchObject({
|
||||
key: 'title111',
|
||||
});
|
||||
});
|
||||
|
||||
it('schema delete', async () => {
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
schema: [{ type: 'belongsTo', name: 'post' }],
|
||||
});
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [{ type: 'hasMany', name: 'comments' }],
|
||||
});
|
||||
// await db.sync();
|
||||
Comment.schema.delete('post');
|
||||
expect(Comment.model.associations.post).toBeUndefined();
|
||||
expect(Comment.model.rawAttributes.postId).toBeDefined();
|
||||
Post.schema.delete('comments');
|
||||
expect(Comment.model.rawAttributes.postId).toBeUndefined();
|
||||
});
|
||||
});
|
@ -0,0 +1,42 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../';
|
||||
|
||||
describe('belongs to many field', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('association undefined', async () => {
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'tags' },
|
||||
],
|
||||
});
|
||||
expect(Post.model.associations.tags).toBeUndefined();
|
||||
expect(db.getCollection('posts_tags')).toBeUndefined();
|
||||
const Tag = db.collection({
|
||||
name: 'tags',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name' },
|
||||
],
|
||||
});
|
||||
expect(Post.model.associations.tags).toBeDefined();
|
||||
const Through = db.getCollection('posts_tags');
|
||||
expect(Through).toBeDefined();
|
||||
expect(Through.model.rawAttributes['postId']).toBeDefined();
|
||||
expect(Through.model.rawAttributes['tagId']).toBeDefined();
|
||||
const PostTag = db.collection({
|
||||
name: 'posts_tags',
|
||||
});
|
||||
expect(PostTag.model.rawAttributes['postId']).toBeDefined();
|
||||
expect(PostTag.model.rawAttributes['tagId']).toBeDefined();
|
||||
});
|
||||
});
|
@ -0,0 +1,129 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../';
|
||||
|
||||
describe('has many field', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('association undefined', async () => {
|
||||
const collection = db.collection({
|
||||
name: 'posts',
|
||||
schema: [{ type: 'hasMany', name: 'comments' }],
|
||||
});
|
||||
await db.sync();
|
||||
expect(collection.model.associations['comments']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('association defined', async () => {
|
||||
const { model } = db.collection({
|
||||
name: 'posts',
|
||||
schema: [{ type: 'hasMany', name: 'comments' }],
|
||||
});
|
||||
expect(model.associations['comments']).toBeUndefined();
|
||||
const comments = db.collection({
|
||||
name: 'comments',
|
||||
schema: [{ type: 'string', name: 'content' }],
|
||||
});
|
||||
const association = model.associations.comments;
|
||||
expect(association).toBeDefined();
|
||||
expect(association.foreignKey).toBe('postId');
|
||||
// @ts-ignore
|
||||
expect(association.sourceKey).toBe('id');
|
||||
expect(comments.model.rawAttributes['postId']).toBeDefined();
|
||||
await db.sync();
|
||||
const post = await model.create<any>();
|
||||
await post.createComment({
|
||||
content: 'content111',
|
||||
});
|
||||
const postComments = await post.getComments();
|
||||
expect(postComments.map((comment) => comment.content)).toEqual([
|
||||
'content111',
|
||||
]);
|
||||
});
|
||||
|
||||
it('custom sourceKey and foreignKey', async () => {
|
||||
const collection = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'key', unique: true },
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'comments',
|
||||
sourceKey: 'key',
|
||||
foreignKey: 'postKey',
|
||||
},
|
||||
],
|
||||
});
|
||||
const comments = db.collection({
|
||||
name: 'comments',
|
||||
schema: [],
|
||||
});
|
||||
const association = collection.model.associations.comments;
|
||||
expect(association).toBeDefined();
|
||||
expect(association.foreignKey).toBe('postKey');
|
||||
// @ts-ignore
|
||||
expect(association.sourceKey).toBe('key');
|
||||
expect(comments.model.rawAttributes['postKey']).toBeDefined();
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
it('custom name and target', async () => {
|
||||
const collection = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'key', unique: true },
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'reviews',
|
||||
target: 'comments',
|
||||
sourceKey: 'key',
|
||||
foreignKey: 'postKey',
|
||||
},
|
||||
],
|
||||
});
|
||||
db.collection({
|
||||
name: 'comments',
|
||||
schema: [{ type: 'string', name: 'content' }],
|
||||
});
|
||||
const association = collection.model.associations.reviews;
|
||||
expect(association).toBeDefined();
|
||||
expect(association.foreignKey).toBe('postKey');
|
||||
// @ts-ignore
|
||||
expect(association.sourceKey).toBe('key');
|
||||
await db.sync();
|
||||
const post = await collection.model.create<any>({
|
||||
key: 'key1',
|
||||
});
|
||||
await post.createReview({
|
||||
content: 'content111',
|
||||
});
|
||||
const postComments = await post.getReviews();
|
||||
expect(postComments.map((comment) => comment.content)).toEqual([
|
||||
'content111',
|
||||
]);
|
||||
});
|
||||
|
||||
it('schema delete', async () => {
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [{ type: 'hasMany', name: 'comments' }],
|
||||
});
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
schema: [{ type: 'belongsTo', name: 'post' }],
|
||||
});
|
||||
await db.sync();
|
||||
Post.schema.delete('comments');
|
||||
expect(Post.model.associations.comments).toBeUndefined();
|
||||
expect(Comment.model.rawAttributes.postId).toBeDefined();
|
||||
Comment.schema.delete('post');
|
||||
expect(Comment.model.rawAttributes.postId).toBeUndefined();
|
||||
});
|
||||
});
|
@ -0,0 +1,79 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../';
|
||||
|
||||
describe('string field', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('define', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
expect(Test.model.rawAttributes['name']).toBeDefined();
|
||||
const model = await Test.model.create({
|
||||
name: 'abc',
|
||||
});
|
||||
expect(model.toJSON()).toMatchObject({
|
||||
name: 'abc',
|
||||
});
|
||||
});
|
||||
|
||||
it('set', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name1' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
Test.schema.set('name2', { type: 'string' });
|
||||
await db.sync();
|
||||
expect(Test.model.rawAttributes['name1']).toBeDefined();
|
||||
expect(Test.model.rawAttributes['name2']).toBeDefined();
|
||||
const model = await Test.model.create({
|
||||
name1: 'a1',
|
||||
name2: 'a2',
|
||||
});
|
||||
expect(model.toJSON()).toMatchObject({
|
||||
name1: 'a1',
|
||||
name2: 'a2',
|
||||
});
|
||||
});
|
||||
|
||||
it('model hook', async () => {
|
||||
const collection = db.collection({
|
||||
name: 'tests',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
collection.model.beforeCreate((model) => {
|
||||
const changed = model.changed();
|
||||
for (const name of changed || []) {
|
||||
model.set(name, `${model.get(name)}111`);
|
||||
}
|
||||
});
|
||||
collection.schema.set('name2', { type: 'string' });
|
||||
await db.sync();
|
||||
const model = await collection.model.create({
|
||||
name: 'n1',
|
||||
name2: 'n2',
|
||||
});
|
||||
expect(model.toJSON()).toMatchObject({
|
||||
name: 'n1111',
|
||||
name2: 'n2111',
|
||||
});
|
||||
});
|
||||
});
|
59
packages/collections/src/collection.ts
Normal file
59
packages/collections/src/collection.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { ModelCtor, Model } from 'sequelize';
|
||||
import { Database } from './database';
|
||||
import { Schema } from './schema';
|
||||
import { RelationField } from './schema-fields';
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface CollectionOptions {
|
||||
schema?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface CollectionContext {
|
||||
database: Database;
|
||||
}
|
||||
|
||||
export class Collection {
|
||||
schema: Schema;
|
||||
model: ModelCtor<Model>;
|
||||
options: CollectionOptions;
|
||||
context: CollectionContext;
|
||||
|
||||
get name() {
|
||||
return this.options.name;
|
||||
}
|
||||
|
||||
constructor(options: CollectionOptions, context: CollectionContext) {
|
||||
this.options = options;
|
||||
this.context = context;
|
||||
const { name, tableName } = options;
|
||||
this.model = class extends Model<any, any> {};
|
||||
const attributes = {};
|
||||
this.model.init(attributes, {
|
||||
..._.omit(options, ['name', 'schema']),
|
||||
sequelize: context.database.sequelize,
|
||||
modelName: name,
|
||||
tableName: tableName || name,
|
||||
});
|
||||
this.schema = new Schema(options.schema, {
|
||||
...context,
|
||||
collection: this,
|
||||
});
|
||||
this.schema2model();
|
||||
this.context.database.emit('collection.init', this);
|
||||
}
|
||||
|
||||
schema2model() {
|
||||
this.schema.forEach((field) => {
|
||||
field.bind();
|
||||
});
|
||||
this.schema.on('setted', (field) => {
|
||||
// console.log('setted', field);
|
||||
field.bind();
|
||||
});
|
||||
this.schema.on('deleted', (field) => field.unbind());
|
||||
this.schema.on('merged', (field) => {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
103
packages/collections/src/database.ts
Normal file
103
packages/collections/src/database.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { Sequelize, ModelCtor, Model, Options, SyncOptions } from 'sequelize';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Collection, CollectionOptions } from './collection';
|
||||
import {
|
||||
RelationField,
|
||||
StringField,
|
||||
HasManyField,
|
||||
BelongsToField,
|
||||
BelongsToManyField,
|
||||
JsonField,
|
||||
JsonbField,
|
||||
} from './schema-fields';
|
||||
|
||||
export interface PendingOptions {
|
||||
field: RelationField;
|
||||
model: ModelCtor<Model>;
|
||||
}
|
||||
|
||||
export type DatabaseOptions = Options | Sequelize;
|
||||
|
||||
export class Database extends EventEmitter {
|
||||
sequelize: Sequelize;
|
||||
schemaTypes = new Map();
|
||||
collections: Map<string, Collection>;
|
||||
pendingFields = new Map<string, RelationField[]>();
|
||||
|
||||
constructor(options: DatabaseOptions) {
|
||||
super();
|
||||
if (options instanceof Sequelize) {
|
||||
this.sequelize = options;
|
||||
} else {
|
||||
this.sequelize = new Sequelize(options);
|
||||
}
|
||||
this.collections = new Map();
|
||||
this.on('collection.init', (collection) => {
|
||||
const items = this.pendingFields.get(collection.name);
|
||||
for (const field of items || []) {
|
||||
field.bind();
|
||||
}
|
||||
});
|
||||
this.registerSchemaTypes({
|
||||
string: StringField,
|
||||
json: JsonField,
|
||||
jsonb: JsonbField,
|
||||
hasMany: HasManyField,
|
||||
belongsTo: BelongsToField,
|
||||
belongsToMany: BelongsToManyField,
|
||||
});
|
||||
}
|
||||
|
||||
collection(options: CollectionOptions) {
|
||||
let collection = this.collections.get(options.name);
|
||||
if (collection) {
|
||||
collection.schema.set(options.schema);
|
||||
} else {
|
||||
collection = new Collection(options, { database: this });
|
||||
}
|
||||
this.collections.set(collection.name, collection);
|
||||
return collection;
|
||||
}
|
||||
|
||||
getCollection(name: string) {
|
||||
return this.collections.get(name);
|
||||
}
|
||||
|
||||
addPendingField(field: RelationField) {
|
||||
const associating = this.pendingFields;
|
||||
const items = this.pendingFields.get(field.target) || [];
|
||||
items.push(field);
|
||||
associating.set(field.target, items);
|
||||
}
|
||||
|
||||
removePendingField(field: RelationField) {
|
||||
const items = this.pendingFields.get(field.target) || [];
|
||||
const index = items.findIndex(
|
||||
(item) => item && item.name === field.name,
|
||||
);
|
||||
if (index !== -1) {
|
||||
delete items[index];
|
||||
this.pendingFields.set(field.target, items);
|
||||
}
|
||||
}
|
||||
|
||||
registerSchemaTypes(schemaTypes: any) {
|
||||
for (const [type, schemaType] of Object.entries(schemaTypes)) {
|
||||
this.schemaTypes.set(type, schemaType);
|
||||
}
|
||||
}
|
||||
|
||||
buildSchemaField(options, context) {
|
||||
const { type } = options;
|
||||
const Field = this.schemaTypes.get(type);
|
||||
return new Field(options, context);
|
||||
}
|
||||
|
||||
async sync(options?: SyncOptions) {
|
||||
return this.sequelize.sync(options);
|
||||
}
|
||||
|
||||
async close() {
|
||||
return this.sequelize.close();
|
||||
}
|
||||
}
|
0
packages/collections/src/index.ts
Normal file
0
packages/collections/src/index.ts
Normal file
53
packages/collections/src/schema-fields/belongs-to-field.ts
Normal file
53
packages/collections/src/schema-fields/belongs-to-field.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { omit } from 'lodash';
|
||||
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
||||
import { RelationField } from './relation-field';
|
||||
|
||||
export class BelongsToField extends RelationField {
|
||||
get target() {
|
||||
const { target, name } = this.options;
|
||||
return target || Utils.pluralize(name);
|
||||
}
|
||||
|
||||
bind() {
|
||||
const { database, collection } = this.context;
|
||||
const Target = this.TargetModel;
|
||||
if (!Target) {
|
||||
database.addPendingField(this);
|
||||
return false;
|
||||
}
|
||||
const association = collection.model.belongsTo(Target, {
|
||||
as: this.name,
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
});
|
||||
// 建立关系之后从 pending 列表中删除
|
||||
database.removePendingField(this);
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
if (!this.options.sourceKey) {
|
||||
// @ts-ignore
|
||||
this.options.sourceKey = association.sourceKey;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
unbind() {
|
||||
const { database, collection } = this.context;
|
||||
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
|
||||
database.removePendingField(this);
|
||||
// 如果外键没有显式的创建,关系表也无反向关联字段,删除关系时,外键也删除掉
|
||||
const tcoll = database.collections.get(this.target);
|
||||
const foreignKey = this.options.foreignKey;
|
||||
const field1 = collection.schema.get(foreignKey);
|
||||
const field2 = tcoll.schema.find((field) => {
|
||||
return field.type === 'hasMany' && field.foreignKey === foreignKey;
|
||||
});
|
||||
if (!field1 && !field2) {
|
||||
collection.model.removeAttribute(foreignKey);
|
||||
}
|
||||
// 删掉 model 的关联字段
|
||||
delete collection.model.associations[this.name];
|
||||
// @ts-ignore
|
||||
collection.model.refreshAttributes();
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import { omit } from 'lodash';
|
||||
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
||||
import { RelationField } from './relation-field';
|
||||
|
||||
export class BelongsToManyField extends RelationField {
|
||||
get target() {
|
||||
const { target, name } = this.options;
|
||||
return target || name;
|
||||
}
|
||||
|
||||
get through() {
|
||||
return (
|
||||
this.options.through ||
|
||||
[this.context.collection.model.name, this.target]
|
||||
.map((name) => name.toLowerCase())
|
||||
.sort()
|
||||
.join('_')
|
||||
);
|
||||
}
|
||||
|
||||
bind() {
|
||||
const { database, collection } = this.context;
|
||||
const Target = this.TargetModel;
|
||||
if (!Target) {
|
||||
database.addPendingField(this);
|
||||
return false;
|
||||
}
|
||||
const through = this.through;
|
||||
let Through =
|
||||
database.getCollection(through) ||
|
||||
database.collection({
|
||||
name: through,
|
||||
});
|
||||
const association = collection.model.belongsToMany(Target, {
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
as: this.name,
|
||||
through: Through.model,
|
||||
});
|
||||
// 建立关系之后从 pending 列表中删除
|
||||
database.removePendingField(this);
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
if (!this.options.sourceKey) {
|
||||
this.options.sourceKey = association.sourceKey;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
unbind() {
|
||||
// const { database, collection } = this.context;
|
||||
// // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
|
||||
// database.removePendingField(this);
|
||||
// // 如果外键没有显式的创建,关系表也无反向关联字段,删除关系时,外键也删除掉
|
||||
// const tcoll = database.collections.get(this.target);
|
||||
// const foreignKey = this.options.foreignKey;
|
||||
// const field1 = collection.schema.get(foreignKey);
|
||||
// const field2 = tcoll.schema.find((field) => {
|
||||
// return field.type === 'hasMany' && field.foreignKey === foreignKey;
|
||||
// });
|
||||
// if (!field1 && !field2) {
|
||||
// collection.model.removeAttribute(foreignKey);
|
||||
// }
|
||||
// // 删掉 model 的关联字段
|
||||
// delete collection.model.associations[this.name];
|
||||
// // @ts-ignore
|
||||
// collection.model.refreshAttributes();
|
||||
}
|
||||
}
|
119
packages/collections/src/schema-fields/has-many-field.ts
Normal file
119
packages/collections/src/schema-fields/has-many-field.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
Sequelize,
|
||||
ModelCtor,
|
||||
Model,
|
||||
DataType,
|
||||
AssociationScope,
|
||||
ForeignKeyOptions,
|
||||
HasManyOptions,
|
||||
} from 'sequelize';
|
||||
import { RelationField } from './relation-field';
|
||||
|
||||
export interface HasManyFieldOptions extends HasManyOptions {
|
||||
/**
|
||||
* The name of the field to use as the key for the association in the source table. Defaults to the primary
|
||||
* key of the source table
|
||||
*/
|
||||
sourceKey?: string;
|
||||
|
||||
/**
|
||||
* A string or a data type to represent the identifier in the table
|
||||
*/
|
||||
keyType?: DataType;
|
||||
|
||||
scope?: AssociationScope;
|
||||
|
||||
/**
|
||||
* The alias of this model, in singular form. See also the `name` option passed to `sequelize.define`. If
|
||||
* you create multiple associations between the same tables, you should provide an alias to be able to
|
||||
* distinguish between them. If you provide an alias when creating the assocition, you should provide the
|
||||
* same alias when eager loading and when getting associated models. Defaults to the singularized name of
|
||||
* target
|
||||
*/
|
||||
as?: string | { singular: string; plural: string };
|
||||
|
||||
/**
|
||||
* The name of the foreign key in the target table or an object representing the type definition for the
|
||||
* foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property
|
||||
* to set the name of the column. Defaults to the name of source + primary key of source
|
||||
*/
|
||||
foreignKey?: string | ForeignKeyOptions;
|
||||
|
||||
/**
|
||||
* What happens when delete occurs.
|
||||
*
|
||||
* Cascade if this is a n:m, and set null if it is a 1:m
|
||||
*
|
||||
* @default 'SET NULL' or 'CASCADE'
|
||||
*/
|
||||
onDelete?: string;
|
||||
|
||||
/**
|
||||
* What happens when update occurs
|
||||
*
|
||||
* @default 'CASCADE'
|
||||
*/
|
||||
onUpdate?: string;
|
||||
|
||||
/**
|
||||
* Should on update and on delete constraints be enabled on the foreign key.
|
||||
*/
|
||||
constraints?: boolean;
|
||||
foreignKeyConstraint?: boolean;
|
||||
|
||||
// scope?: AssociationScope;
|
||||
|
||||
/**
|
||||
* If `false` the applicable hooks will not be called.
|
||||
* The default value depends on the context.
|
||||
*/
|
||||
hooks?: boolean;
|
||||
}
|
||||
|
||||
export class HasManyField extends RelationField {
|
||||
bind() {
|
||||
const { database, collection } = this.context;
|
||||
const Target = this.TargetModel;
|
||||
if (!Target) {
|
||||
database.addPendingField(this);
|
||||
return false;
|
||||
}
|
||||
const association = collection.model.hasMany(Target, {
|
||||
as: this.name,
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
});
|
||||
// 建立关系之后从 pending 列表中删除
|
||||
database.removePendingField(this);
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
if (!this.options.sourceKey) {
|
||||
// @ts-ignore
|
||||
this.options.sourceKey = association.sourceKey;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
unbind() {
|
||||
const { database, collection } = this.context;
|
||||
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
|
||||
database.removePendingField(this);
|
||||
// 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉
|
||||
const tcoll = database.collections.get(this.target);
|
||||
const foreignKey = this.options.foreignKey;
|
||||
const field = tcoll.schema.find((field) => {
|
||||
if (field.name === foreignKey) {
|
||||
return true;
|
||||
}
|
||||
return field.type === 'belongsTo' && field.foreignKey === foreignKey;
|
||||
});
|
||||
if (!field) {
|
||||
tcoll.model.removeAttribute(foreignKey);
|
||||
}
|
||||
// 删掉 model 的关联字段
|
||||
delete collection.model.associations[this.name];
|
||||
// @ts-ignore
|
||||
collection.model.refreshAttributes();
|
||||
}
|
||||
}
|
7
packages/collections/src/schema-fields/index.ts
Normal file
7
packages/collections/src/schema-fields/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './schema-field';
|
||||
export * from './string-field';
|
||||
export * from './relation-field'
|
||||
export * from './belongs-to-field'
|
||||
export * from './belongs-to-many-field';
|
||||
export * from './has-many-field';
|
||||
export * from './json-field';
|
32
packages/collections/src/schema-fields/json-field.ts
Normal file
32
packages/collections/src/schema-fields/json-field.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class JsonField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.JSON;
|
||||
}
|
||||
|
||||
toSequelize() {
|
||||
return {
|
||||
...this.options,
|
||||
type: this.dataType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonbField extends SchemaField {
|
||||
get dataType() {
|
||||
const dialect = this.context.database.sequelize.getDialect();
|
||||
if (dialect === 'postgres') {
|
||||
return DataTypes.JSONB;
|
||||
}
|
||||
return DataTypes.JSON;
|
||||
}
|
||||
|
||||
toSequelize() {
|
||||
return {
|
||||
...this.options,
|
||||
type: this.dataType,
|
||||
};
|
||||
}
|
||||
}
|
20
packages/collections/src/schema-fields/relation-field.ts
Normal file
20
packages/collections/src/schema-fields/relation-field.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export abstract class RelationField extends SchemaField {
|
||||
get target() {
|
||||
const { target, name } = this.options;
|
||||
return target || name;
|
||||
}
|
||||
|
||||
get foreignKey() {
|
||||
return this.options.foreignKey;
|
||||
}
|
||||
|
||||
get sourceKey() {
|
||||
return this.options.sourceKey;
|
||||
}
|
||||
|
||||
get TargetModel() {
|
||||
return this.context.database.sequelize.models[this.target];
|
||||
}
|
||||
}
|
65
packages/collections/src/schema-fields/schema-field.ts
Normal file
65
packages/collections/src/schema-fields/schema-field.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Collection } from '../collection';
|
||||
import { Database } from '../database';
|
||||
import { Schema } from '../schema';
|
||||
import { RelationField } from './relation-field';
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface SchemaFieldContext {
|
||||
database: Database;
|
||||
collection: Collection;
|
||||
schema: Schema;
|
||||
}
|
||||
|
||||
export abstract class SchemaField {
|
||||
options: any;
|
||||
context: SchemaFieldContext;
|
||||
[key: string]: any;
|
||||
|
||||
get name() {
|
||||
return this.options.name;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.options.type;
|
||||
}
|
||||
|
||||
get dataType() {
|
||||
return this.options.dataType;
|
||||
}
|
||||
|
||||
constructor(options?: any, context?: SchemaFieldContext) {
|
||||
this.context = context;
|
||||
this.options = options || {};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// code
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
return this.options[name];
|
||||
}
|
||||
|
||||
merge(obj: any) {
|
||||
Object.assign(this.options, obj);
|
||||
}
|
||||
|
||||
bind() {
|
||||
const { model } = this.context.collection;
|
||||
model.rawAttributes[this.name] = this.toSequelize();
|
||||
// @ts-ignore
|
||||
model.refreshAttributes();
|
||||
}
|
||||
|
||||
unbind() {
|
||||
const { model } = this.context.collection;
|
||||
model.removeAttribute(this.name);
|
||||
}
|
||||
|
||||
toSequelize(): any {
|
||||
return _.omit(this.options, ['name'])
|
||||
}
|
||||
}
|
15
packages/collections/src/schema-fields/string-field.ts
Normal file
15
packages/collections/src/schema-fields/string-field.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { SchemaField } from './schema-field';
|
||||
|
||||
export class StringField extends SchemaField {
|
||||
get dataType() {
|
||||
return DataTypes.STRING;
|
||||
}
|
||||
|
||||
toSequelize() {
|
||||
return {
|
||||
...this.options,
|
||||
type: this.dataType,
|
||||
};
|
||||
}
|
||||
}
|
80
packages/collections/src/schema.ts
Normal file
80
packages/collections/src/schema.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Database } from './database';
|
||||
import { Collection } from './collection';
|
||||
import { SchemaField } from './schema-fields';
|
||||
|
||||
export interface SchemaContext {
|
||||
database: Database;
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
export class Schema extends EventEmitter {
|
||||
fields: Map<string, any>;
|
||||
context: SchemaContext;
|
||||
options: any;
|
||||
|
||||
constructor(options?: any, context?: SchemaContext) {
|
||||
super();
|
||||
this.options = options;
|
||||
this.context = context;
|
||||
this.fields = new Map<string, any>();
|
||||
this.set(options);
|
||||
}
|
||||
|
||||
has(name: string) {
|
||||
return this.fields.has(name);
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
return this.fields.get(name);
|
||||
}
|
||||
|
||||
set(name: string | object, obj?: any) {
|
||||
if (!name) {
|
||||
return this;
|
||||
}
|
||||
if (typeof name === 'string') {
|
||||
const { database } = this.context;
|
||||
const field = database.buildSchemaField({ name, ...obj }, {
|
||||
...this.context,
|
||||
schema: this,
|
||||
model: this.context.collection.model,
|
||||
});
|
||||
this.fields.set(name, field);
|
||||
this.emit('setted', field);
|
||||
} else if (Array.isArray(name)) {
|
||||
for (const value of name) {
|
||||
this.set(value.name, value);
|
||||
}
|
||||
} else if (typeof name === 'object') {
|
||||
for (const [key, value] of Object.entries(name)) {
|
||||
console.log({ key, value })
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(name: string) {
|
||||
const field = this.fields.get(name);
|
||||
const bool = this.fields.delete(name);
|
||||
this.emit('deleted', field);
|
||||
return bool;
|
||||
}
|
||||
|
||||
merge(name: string, obj) {
|
||||
const field = this.get(name);
|
||||
field.merge(obj);
|
||||
this.emit('merged', field);
|
||||
return field;
|
||||
}
|
||||
|
||||
forEach(callback: (field: SchemaField) => void) {
|
||||
return [...this.fields.values()].forEach(callback);
|
||||
}
|
||||
|
||||
find(callback: (field: SchemaField) => boolean) {
|
||||
return [...this.fields.values()].find(callback);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user