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