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