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