feat: improve code...

This commit is contained in:
chenos 2021-09-27 15:28:32 +08:00
parent 4651d3dddd
commit 60bbbb5f35
32 changed files with 1149 additions and 403 deletions

View File

@ -23,7 +23,7 @@ const app = new Application({
// 配置一张 users 表 // 配置一张 users 表
app.collection({ app.collection({
name: 'users', name: 'users',
schema: [ fields: [
{ type: 'string', name: 'username' }, { type: 'string', name: 'username' },
{ type: 'password', name: 'password' } { type: 'password', name: 'password' }
], ],
@ -114,7 +114,6 @@ NocoBase 的 Application 继承了 Koa集成了 DB 和 CLI添加了一些
- `app.db`:数据库实例,每个 app 都有自己的 db。 - `app.db`:数据库实例,每个 app 都有自己的 db。
- `db.getCollection()` 数据表/数据集 - `db.getCollection()` 数据表/数据集
- `collection.schema` 数据结构
- `collection.repository` 数据仓库 - `collection.repository` 数据仓库
- `collection.model` 数据模型 - `collection.model` 数据模型
- `db.on()` 添加事件监听,由 EventEmitter 提供 - `db.on()` 添加事件监听,由 EventEmitter 提供
@ -175,7 +174,7 @@ NocoBase 通过 `app.collection()` 方法定义数据的 SchemaSchema 的类
// 用户 // 用户
app.collection({ app.collection({
name: 'users', name: 'users',
schema: { fields: {
username: { type: 'string', unique: true }, username: { type: 'string', unique: true },
password: { type: 'password', unique: true }, password: { type: 'password', unique: true },
posts: { type: 'hasMany' }, posts: { type: 'hasMany' },
@ -185,7 +184,7 @@ app.collection({
// 文章 // 文章
app.collection({ app.collection({
name: 'posts', name: 'posts',
schema: { fields: {
title: 'string', title: 'string',
content: 'text', content: 'text',
tags: 'belongsToMany', tags: 'belongsToMany',
@ -197,7 +196,7 @@ app.collection({
// 标签 // 标签
app.collection({ app.collection({
name: 'tags', name: 'tags',
schema: [ fields: [
{ type: 'string', name: 'name' }, { type: 'string', name: 'name' },
{ type: 'belongsToMany', name: 'posts' }, { type: 'belongsToMany', name: 'posts' },
], ],
@ -206,7 +205,7 @@ app.collection({
// 评论 // 评论
app.collection({ app.collection({
name: 'comments', name: 'comments',
schema: [ fields: [
{ type: 'text', name: 'content' }, { type: 'text', name: 'content' },
{ type: 'belongsTo', name: 'user' }, { type: 'belongsTo', name: 'user' },
], ],
@ -215,43 +214,52 @@ app.collection({
除了通过 `app.collection()` 配置 schema也可以直接调用 api 插入或修改 schemacollection 的核心 API 有: 除了通过 `app.collection()` 配置 schema也可以直接调用 api 插入或修改 schemacollection 的核心 API 有:
- `collection.schema` 当前 collection 的数据结构 - `collection` 当前 collection 的数据结构
- `schema.has()` 判断是否存在 - `collection.hasField()` 判断字段是否存在
- `schema.get()` 获取 - `collection.addField()` 添加字段配置
- `schema.set()` 添加或更新 - `collection.getField()` 获取字段配置
- `schema.merge()` 添加、或指定 key path 替换 - `collection.removeField()` 移除字段配置
- `schema.replace()` 替换 - `collection.sync()` 与数据库表结构同步
- `schema.delete()` 删除
- `collection.repository` 当前 collection 的数据仓库 - `collection.repository` 当前 collection 的数据仓库
- `repository.findAll()` - `repository.findMany()`
- `repository.findOne()` - `repository.findOne()`
- `repository.create()` - `repository.create()`
- `repository.update()` - `repository.update()`
- `repository.destroy()` - `repository.destroy()`
- `repository.relatedQuery().for()`
- `create()`
- `update()`
- `destroy()`
- `findMany()`
- `findOne()`
- `set()`
- `add()`
- `remove()`
- `toggle()`
- `collection.model` 当前 collection 的数据模型 - `collection.model` 当前 collection 的数据模型
Schema 示例: Collection 示例:
```ts ```ts
const collection = app.db.getCollection('posts'); const collection = app.db.getCollection('posts');
collection.schema.has('title'); collection.hasField('title');
collection.schema.get('title'); collection.getField('title');
// 添加或更新 // 添加或更新
collection.schema.set('content', { collection.addField({
type: 'string', type: 'string',
name: 'content',
}); });
// 移除 // 移除
collection.schema.delete('content'); collection.removeField('content');
// 添加、或指定 key path 替换 // 添加、或指定 key path 替换
collection.schema.merge({ collection.mergeField({
content: { name: 'content',
type: 'content', type: 'string',
},
}); });
除了全局的 `db.sync()`,也有 `collection.sync()` 方法。 除了全局的 `db.sync()`,也有 `collection.sync()` 方法。
@ -268,9 +276,9 @@ await collection.sync();
通过 Repository 创建数据 通过 Repository 创建数据
```ts ```ts
const repository = app.db.getRepository('users'); const User = app.db.getCollection('users');
const user = await repository.create({ const user = await User.repository.create({
title: 't1', title: 't1',
content: 'c1', content: 'c1',
author: 1, author: 1,
@ -280,7 +288,7 @@ const user = await repository.create({
blacklist: [], blacklist: [],
}); });
await repository.findAll({ await User.repository.findMany({
filter: { filter: {
title: 't1', title: 't1',
}, },
@ -290,7 +298,7 @@ await repository.findAll({
perPage: 20, perPage: 20,
}); });
await repository.findOne({ await User.repository.findOne({
filter: { filter: {
title: 't1', title: 't1',
}, },
@ -300,7 +308,7 @@ await repository.findOne({
perPage: 20, perPage: 20,
}); });
await repository.update({ await User.repository.update({
title: 't1', title: 't1',
content: 'c1', content: 'c1',
author: 1, author: 1,
@ -311,7 +319,7 @@ await repository.update({
blacklist: [], blacklist: [],
}); });
await repository.destroy({ await User.repository.destroy({
filter: {}, filter: {},
}); });
``` ```
@ -319,15 +327,11 @@ await repository.destroy({
通过 Model 创建数据 通过 Model 创建数据
```ts ```ts
const User = db.getModel('users'); const User = db.getCollection('users');
const user = await User.create({ const user = await User.model.create({
title: 't1', title: 't1',
content: 'c1', content: 'c1',
}); });
await user.updateAssociations({
author: 1,
tags: [1,2,3],
});
``` ```
## 资源 & 操作 - Resource & Action ## 资源 & 操作 - Resource & Action

View File

@ -15,7 +15,7 @@ describe('belongs to field', () => {
it('association undefined', async () => { it('association undefined', async () => {
const Comment = db.collection({ const Comment = db.collection({
name: 'comments', name: 'comments',
schema: [{ type: 'belongsTo', name: 'post' }], fields: [{ type: 'belongsTo', name: 'post' }],
}); });
expect(Comment.model.associations['post']).toBeUndefined(); expect(Comment.model.associations['post']).toBeUndefined();
}); });
@ -23,7 +23,7 @@ describe('belongs to field', () => {
it('association defined', async () => { it('association defined', async () => {
const Comment = db.collection({ const Comment = db.collection({
name: 'comments', name: 'comments',
schema: [ fields: [
{ type: 'string', name: 'content' }, { type: 'string', name: 'content' },
{ type: 'belongsTo', name: 'post' }, { type: 'belongsTo', name: 'post' },
], ],
@ -31,7 +31,7 @@ describe('belongs to field', () => {
expect(Comment.model.associations.post).toBeUndefined(); expect(Comment.model.associations.post).toBeUndefined();
const Post = db.collection({ const Post = db.collection({
name: 'posts', name: 'posts',
schema: [ fields: [
{ type: 'string', name: 'title' }, { type: 'string', name: 'title' },
], ],
}); });
@ -63,13 +63,13 @@ describe('belongs to field', () => {
it('custom targetKey and foreignKey', async () => { it('custom targetKey and foreignKey', async () => {
const Post = db.collection({ const Post = db.collection({
name: 'posts', name: 'posts',
schema: [ fields: [
{ type: 'string', name: 'key', unique: true }, { type: 'string', name: 'key', unique: true },
], ],
}); });
const Comment = db.collection({ const Comment = db.collection({
name: 'comments', name: 'comments',
schema: [ fields: [
{ {
type: 'belongsTo', type: 'belongsTo',
name: 'post', name: 'post',
@ -89,7 +89,7 @@ describe('belongs to field', () => {
it('custom name and target', async () => { it('custom name and target', async () => {
const Comment = db.collection({ const Comment = db.collection({
name: 'comments', name: 'comments',
schema: [ fields: [
{ type: 'string', name: 'content' }, { type: 'string', name: 'content' },
{ {
type: 'belongsTo', type: 'belongsTo',
@ -103,7 +103,7 @@ describe('belongs to field', () => {
expect(Comment.model.associations.article).toBeUndefined(); expect(Comment.model.associations.article).toBeUndefined();
const Post = db.collection({ const Post = db.collection({
name: 'posts', name: 'posts',
schema: [ fields: [
{ type: 'string', name: 'key', unique: true }, { type: 'string', name: 'key', unique: true },
], ],
}); });
@ -135,17 +135,17 @@ describe('belongs to field', () => {
it('schema delete', async () => { it('schema delete', async () => {
const Comment = db.collection({ const Comment = db.collection({
name: 'comments', name: 'comments',
schema: [{ type: 'belongsTo', name: 'post' }], fields: [{ type: 'belongsTo', name: 'post' }],
}); });
const Post = db.collection({ const Post = db.collection({
name: 'posts', name: 'posts',
schema: [{ type: 'hasMany', name: 'comments' }], fields: [{ type: 'hasMany', name: 'comments' }],
}); });
// await db.sync(); // await db.sync();
Comment.schema.delete('post'); Comment.removeField('post');
expect(Comment.model.associations.post).toBeUndefined(); expect(Comment.model.associations.post).toBeUndefined();
expect(Comment.model.rawAttributes.postId).toBeDefined(); expect(Comment.model.rawAttributes.postId).toBeDefined();
Post.schema.delete('comments'); Post.removeField('comments');
expect(Comment.model.rawAttributes.postId).toBeUndefined(); expect(Comment.model.rawAttributes.postId).toBeUndefined();
}); });
}); });

View File

@ -15,7 +15,7 @@ describe('belongs to many field', () => {
it('association undefined', async () => { it('association undefined', async () => {
const Post = db.collection({ const Post = db.collection({
name: 'posts', name: 'posts',
schema: [ fields: [
{ type: 'string', name: 'name' }, { type: 'string', name: 'name' },
{ type: 'belongsToMany', name: 'tags' }, { type: 'belongsToMany', name: 'tags' },
], ],
@ -24,7 +24,7 @@ describe('belongs to many field', () => {
expect(db.getCollection('posts_tags')).toBeUndefined(); expect(db.getCollection('posts_tags')).toBeUndefined();
const Tag = db.collection({ const Tag = db.collection({
name: 'tags', name: 'tags',
schema: [ fields: [
{ type: 'string', name: 'name' }, { type: 'string', name: 'name' },
], ],
}); });

View File

@ -15,7 +15,7 @@ describe('has many field', () => {
it('association undefined', async () => { it('association undefined', async () => {
const collection = db.collection({ const collection = db.collection({
name: 'posts', name: 'posts',
schema: [{ type: 'hasMany', name: 'comments' }], fields: [{ type: 'hasMany', name: 'comments' }],
}); });
await db.sync(); await db.sync();
expect(collection.model.associations['comments']).toBeUndefined(); expect(collection.model.associations['comments']).toBeUndefined();
@ -24,12 +24,12 @@ describe('has many field', () => {
it('association defined', async () => { it('association defined', async () => {
const { model } = db.collection({ const { model } = db.collection({
name: 'posts', name: 'posts',
schema: [{ type: 'hasMany', name: 'comments' }], fields: [{ type: 'hasMany', name: 'comments' }],
}); });
expect(model.associations['comments']).toBeUndefined(); expect(model.associations['comments']).toBeUndefined();
const comments = db.collection({ const comments = db.collection({
name: 'comments', name: 'comments',
schema: [{ type: 'string', name: 'content' }], fields: [{ type: 'string', name: 'content' }],
}); });
const association = model.associations.comments; const association = model.associations.comments;
expect(association).toBeDefined(); expect(association).toBeDefined();
@ -48,10 +48,10 @@ describe('has many field', () => {
]); ]);
}); });
it.only('custom sourceKey', async () => { it('custom sourceKey', async () => {
const collection = db.collection({ const collection = db.collection({
name: 'posts', name: 'posts',
schema: [ fields: [
{ type: 'string', name: 'key', unique: true }, { type: 'string', name: 'key', unique: true },
{ {
type: 'hasMany', type: 'hasMany',
@ -63,7 +63,7 @@ describe('has many field', () => {
}); });
const comments = db.collection({ const comments = db.collection({
name: 'comments', name: 'comments',
schema: [], fields: [],
}); });
const association = collection.model.associations.comments; const association = collection.model.associations.comments;
expect(association).toBeDefined(); expect(association).toBeDefined();
@ -77,7 +77,7 @@ describe('has many field', () => {
it('custom sourceKey and foreignKey', async () => { it('custom sourceKey and foreignKey', async () => {
const collection = db.collection({ const collection = db.collection({
name: 'posts', name: 'posts',
schema: [ fields: [
{ type: 'string', name: 'key', unique: true }, { type: 'string', name: 'key', unique: true },
{ {
type: 'hasMany', type: 'hasMany',
@ -89,7 +89,7 @@ describe('has many field', () => {
}); });
const comments = db.collection({ const comments = db.collection({
name: 'comments', name: 'comments',
schema: [], fields: [],
}); });
const association = collection.model.associations.comments; const association = collection.model.associations.comments;
expect(association).toBeDefined(); expect(association).toBeDefined();
@ -103,7 +103,7 @@ describe('has many field', () => {
it('custom name and target', async () => { it('custom name and target', async () => {
const collection = db.collection({ const collection = db.collection({
name: 'posts', name: 'posts',
schema: [ fields: [
{ type: 'string', name: 'key', unique: true }, { type: 'string', name: 'key', unique: true },
{ {
type: 'hasMany', type: 'hasMany',
@ -116,7 +116,7 @@ describe('has many field', () => {
}); });
db.collection({ db.collection({
name: 'comments', name: 'comments',
schema: [{ type: 'string', name: 'content' }], fields: [{ type: 'string', name: 'content' }],
}); });
const association = collection.model.associations.reviews; const association = collection.model.associations.reviews;
expect(association).toBeDefined(); expect(association).toBeDefined();
@ -139,17 +139,17 @@ describe('has many field', () => {
it('schema delete', async () => { it('schema delete', async () => {
const Post = db.collection({ const Post = db.collection({
name: 'posts', name: 'posts',
schema: [{ type: 'hasMany', name: 'comments' }], fields: [{ type: 'hasMany', name: 'comments' }],
}); });
const Comment = db.collection({ const Comment = db.collection({
name: 'comments', name: 'comments',
schema: [{ type: 'belongsTo', name: 'post' }], fields: [{ type: 'belongsTo', name: 'post' }],
}); });
await db.sync(); await db.sync();
Post.schema.delete('comments'); Post.removeField('comments');
expect(Post.model.associations.comments).toBeUndefined(); expect(Post.model.associations.comments).toBeUndefined();
expect(Comment.model.rawAttributes.postId).toBeDefined(); expect(Comment.model.rawAttributes.postId).toBeDefined();
Comment.schema.delete('post'); Comment.removeField('post');
expect(Comment.model.rawAttributes.postId).toBeUndefined(); expect(Comment.model.rawAttributes.postId).toBeUndefined();
}); });
}); });

View File

@ -15,7 +15,7 @@ describe('has many field', () => {
it('association undefined', async () => { it('association undefined', async () => {
const User = db.collection({ const User = db.collection({
name: 'users', name: 'users',
schema: [{ type: 'hasOne', name: 'profile' }], fields: [{ type: 'hasOne', name: 'profile' }],
}); });
await db.sync(); await db.sync();
expect(User.model.associations.profile).toBeUndefined(); expect(User.model.associations.profile).toBeUndefined();
@ -24,12 +24,12 @@ describe('has many field', () => {
it('association defined', async () => { it('association defined', async () => {
const User = db.collection({ const User = db.collection({
name: 'users', name: 'users',
schema: [{ type: 'hasOne', name: 'profile' }], fields: [{ type: 'hasOne', name: 'profile' }],
}); });
expect(User.model.associations.phone).toBeUndefined(); expect(User.model.associations.phone).toBeUndefined();
const Profile = db.collection({ const Profile = db.collection({
name: 'profiles', name: 'profiles',
schema: [{ type: 'string', name: 'content' }], fields: [{ type: 'string', name: 'content' }],
}); });
const association = User.model.associations.profile; const association = User.model.associations.profile;
expect(association).toBeDefined(); expect(association).toBeDefined();
@ -51,17 +51,17 @@ describe('has many field', () => {
it('schema delete', async () => { it('schema delete', async () => {
const User = db.collection({ const User = db.collection({
name: 'users', name: 'users',
schema: [{ type: 'hasOne', name: 'profile' }], fields: [{ type: 'hasOne', name: 'profile' }],
}); });
const Profile = db.collection({ const Profile = db.collection({
name: 'profiles', name: 'profiles',
schema: [{ type: 'belongsTo', name: 'user' }], fields: [{ type: 'belongsTo', name: 'user' }],
}); });
await db.sync(); await db.sync();
User.schema.delete('profile'); User.removeField('profile');
expect(User.model.associations.profile).toBeUndefined(); expect(User.model.associations.profile).toBeUndefined();
expect(Profile.model.rawAttributes.userId).toBeDefined(); expect(Profile.model.rawAttributes.userId).toBeDefined();
Profile.schema.delete('user'); Profile.removeField('user');
expect(Profile.model.rawAttributes.userId).toBeUndefined(); expect(Profile.model.rawAttributes.userId).toBeUndefined();
}); });
}); });

View File

@ -1,13 +1,13 @@
import { Database } from '../../database'; import { Database } from '../../database';
import { mockDatabase } from '../'; import { mockDatabase } from '../';
import { SortField } from '../../schema-fields'; import { SortField } from '../../fields';
describe('string field', () => { describe('string field', () => {
let db: Database; let db: Database;
beforeEach(() => { beforeEach(() => {
db = mockDatabase(); db = mockDatabase();
db.registerSchemaTypes({ db.registerFieldTypes({
sort: SortField sort: SortField
}); });
}); });
@ -19,7 +19,7 @@ describe('string field', () => {
it('sort', async () => { it('sort', async () => {
const Test = db.collection({ const Test = db.collection({
name: 'tests', name: 'tests',
schema: [ fields: [
{ type: 'sort', name: 'sort' }, { type: 'sort', name: 'sort' },
], ],
}); });
@ -35,7 +35,7 @@ describe('string field', () => {
it('skip if sort value not empty', async () => { it('skip if sort value not empty', async () => {
const Test = db.collection({ const Test = db.collection({
name: 'tests', name: 'tests',
schema: [ fields: [
{ type: 'sort', name: 'sort' }, { type: 'sort', name: 'sort' },
], ],
}); });
@ -51,7 +51,7 @@ describe('string field', () => {
it('scopeKey', async () => { it('scopeKey', async () => {
const Test = db.collection({ const Test = db.collection({
name: 'tests', name: 'tests',
schema: [ fields: [
{ type: 'sort', name: 'sort', scopeKey: 'status' }, { type: 'sort', name: 'sort', scopeKey: 'status' },
{ type: 'string', name: 'status' }, { type: 'string', name: 'status' },
], ],

View File

@ -15,7 +15,7 @@ describe('string field', () => {
it('define', async () => { it('define', async () => {
const Test = db.collection({ const Test = db.collection({
name: 'tests', name: 'tests',
schema: [ fields: [
{ type: 'string', name: 'name' }, { type: 'string', name: 'name' },
], ],
}); });
@ -32,12 +32,12 @@ describe('string field', () => {
it('set', async () => { it('set', async () => {
const Test = db.collection({ const Test = db.collection({
name: 'tests', name: 'tests',
schema: [ fields: [
{ type: 'string', name: 'name1' }, { type: 'string', name: 'name1' },
], ],
}); });
await db.sync(); await db.sync();
Test.schema.set('name2', { type: 'string' }); Test.addField({ type: 'string', name: 'name2' });
await db.sync(); await db.sync();
expect(Test.model.rawAttributes['name1']).toBeDefined(); expect(Test.model.rawAttributes['name1']).toBeDefined();
expect(Test.model.rawAttributes['name2']).toBeDefined(); expect(Test.model.rawAttributes['name2']).toBeDefined();
@ -54,7 +54,7 @@ describe('string field', () => {
it('model hook', async () => { it('model hook', async () => {
const collection = db.collection({ const collection = db.collection({
name: 'tests', name: 'tests',
schema: [ fields: [
{ type: 'string', name: 'name' }, { type: 'string', name: 'name' },
], ],
}); });
@ -65,7 +65,7 @@ describe('string field', () => {
model.set(name, `${model.get(name)}111`); model.set(name, `${model.get(name)}111`);
} }
}); });
collection.schema.set('name2', { type: 'string' }); collection.addField({ type: 'string', name: 'name2' });
await db.sync(); await db.sync();
const model = await collection.model.create({ const model = await collection.model.create({
name: 'n1', name: 'n1',

View File

@ -20,7 +20,7 @@ export function getConfig(config = {}, options?: any): DatabaseOptions {
host: process.env.DB_HOST, host: process.env.DB_HOST,
port: process.env.DB_PORT, port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT, dialect: process.env.DB_DIALECT,
// logging: process.env.DB_LOG_SQL === 'on', logging: process.env.DB_LOG_SQL === 'on',
sync: { sync: {
force: true, force: true,
alter: { alter: {

View File

@ -1,9 +1,8 @@
import { Collection } from '../collection'; import { Collection } from '../collection';
import { Database } from '../database'; import { Database } from '../database';
import { updateAssociation, updateAssociations } from '../update-associations';
import { mockDatabase } from './'; import { mockDatabase } from './';
describe('repository', () => { describe('repository.find', () => {
let db: Database; let db: Database;
let User: Collection; let User: Collection;
let Post: Collection; let Post: Collection;
@ -13,24 +12,24 @@ describe('repository', () => {
db = mockDatabase(); db = mockDatabase();
User = db.collection({ User = db.collection({
name: 'users', name: 'users',
schema: [ fields: [
{ type: 'string', name: 'name' }, { type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' }, { type: 'hasMany', name: 'posts' },
], ],
}); });
Post = db.collection({ Post = db.collection({
name: 'posts', name: 'posts',
schema: [ fields: [
{ type: 'string', name: 'name' }, { type: 'string', name: 'name' },
{ type: 'hasMany', name: 'comments' }, { type: 'hasMany', name: 'comments' },
], ],
}); });
Comment = db.collection({ Comment = db.collection({
name: 'comments', name: 'comments',
schema: [{ type: 'string', name: 'name' }], fields: [{ type: 'string', name: 'name' }],
}); });
await db.sync(); await db.sync();
await User.repository.bulkCreate([ await User.repository.createMany([
{ {
name: 'user1', name: 'user1',
posts: [ posts: [
@ -128,29 +127,300 @@ describe('repository', () => {
await db.close(); await db.close();
}); });
it.only('findAll', async () => {
const data = await User.repository.findAll({
filter: {
'posts.comments.id': null,
},
page: 1,
pageSize: 1,
});
console.log(data.count, JSON.stringify(data.rows.map(row => row.toJSON()), null, 2));
// expect(data.toJSON()).toMatchObject({
// name: 'user3',
// });
});
it('findOne', async () => { it('findOne', async () => {
const data = await User.repository.findOne({ const data = await User.repository.findOne({
filter: { filter: {
'posts.comments.name': 'comment331', 'posts.comments.name': 'comment331',
}, },
}); });
console.log(data);
});
it('findMany', async () => {
const data = await User.repository.findMany({
filter: {
'posts.comments.id': null,
},
page: 1,
pageSize: 1,
});
console.log(
data.count,
JSON.stringify(
data.rows.map((row) => row.toJSON()),
null,
2,
),
);
// expect(data.toJSON()).toMatchObject({ // expect(data.toJSON()).toMatchObject({
// name: 'user3', // name: 'user3',
// }); // });
}); });
});
describe('repository.create', () => {
let db: Database;
let User: Collection;
let Post: Collection;
let Comment: Collection;
beforeEach(async () => {
db = mockDatabase();
User = db.collection({
name: 'users',
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'comments' },
],
});
Comment = db.collection({
name: 'comments',
fields: [{ type: 'string', name: 'name' }],
});
await db.sync();
});
afterEach(async () => {
await db.close();
});
it('create', async () => {
const user = await User.repository.create({
name: 'user1',
posts: [
{
name: 'post11',
comments: [
{ name: 'comment111' },
{ name: 'comment112' },
{ name: 'comment113' },
],
},
],
});
const post = await Post.model.findOne();
expect(post).toMatchObject({
name: 'post11',
userId: user.get('id'),
});
const comments = await Comment.model.findAll();
expect(comments.map((m) => m.get('postId'))).toEqual([
post.get('id'),
post.get('id'),
post.get('id'),
]);
});
});
describe('repository.update', () => {
let db: Database;
let User: Collection;
let Post: Collection;
let Comment: Collection;
beforeEach(async () => {
db = mockDatabase();
User = db.collection({
name: 'users',
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'comments' },
],
});
Comment = db.collection({
name: 'comments',
fields: [{ type: 'string', name: 'name' }],
});
await db.sync();
});
afterEach(async () => {
await db.close();
});
it('update1', async () => {
const user = await User.model.create<any>({
name: 'user1',
});
await User.repository.update(
{
name: 'user11',
posts: [{ name: 'post1' }],
},
user,
);
const updated = await User.model.findByPk(user.id);
expect(updated).toMatchObject({
name: 'user11',
});
const post = await Post.model.findOne({
where: {
name: 'post1',
},
});
expect(post).toMatchObject({
name: 'post1',
userId: user.id,
});
});
it('update2', async () => {
const user = await User.model.create<any>({
name: 'user1',
posts: [{ name: 'post1' }],
});
await User.repository.update(
{
name: 'user11',
posts: [{ name: 'post1' }],
},
user.id,
);
const updated = await User.model.findByPk(user.id);
expect(updated).toMatchObject({
name: 'user11',
});
const post = await Post.model.findOne({
where: {
name: 'post1',
},
});
expect(post).toMatchObject({
name: 'post1',
userId: user.id,
});
});
});
describe('repository.destroy', () => {
let db: Database;
let User: Collection;
let Post: Collection;
let Comment: Collection;
beforeEach(async () => {
db = mockDatabase();
User = db.collection({
name: 'users',
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'comments' },
],
});
Comment = db.collection({
name: 'comments',
fields: [{ type: 'string', name: 'name' }],
});
await db.sync();
});
afterEach(async () => {
await db.close();
});
it('destroy1', async () => {
const user = await User.model.create<any>();
await User.repository.destroy(user.id);
const user1 = await User.model.findByPk(user.id);
expect(user1).toBeNull();
});
it('destroy2', async () => {
const user = await User.model.create<any>();
await User.repository.destroy({
filter: {
id: user.id,
},
});
const user1 = await User.model.findByPk(user.id);
expect(user1).toBeNull();
});
});
describe('repository.relatedQuery', () => {
let db: Database;
let User: Collection;
let Post: Collection;
let Comment: Collection;
beforeEach(async () => {
db = mockDatabase();
User = db.collection({
name: 'users',
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'name' },
{ type: 'belongsTo', name: 'user' },
{ type: 'hasMany', name: 'comments' },
],
});
Comment = db.collection({
name: 'comments',
fields: [{ type: 'string', name: 'name' }],
});
await db.sync();
});
afterEach(async () => {
await db.close();
});
it('create', async () => {
const user = await User.repository.create();
const post = await User.repository.relatedQuery('posts').for(user).create({
name: 'post1',
});
expect(post).toMatchObject({
name: 'post1',
userId: user.id,
});
const post2 = await User.repository
.relatedQuery('posts')
.for(user.id)
.create({
name: 'post2',
});
expect(post2).toMatchObject({
name: 'post2',
userId: user.id,
});
});
it('update', async () => {
const post = await Post.repository.create({
user: {
name: 'user11',
}
});
await Post.repository.relatedQuery('user').for(post).update({
name: 'user12',
});
});
}); });

View File

@ -1,101 +1,294 @@
import { Collection } from '../collection';
import { Database } from '../database'; import { Database } from '../database';
import { updateAssociation, updateAssociations } from '../update-associations'; import { updateAssociation, updateAssociations } from '../update-associations';
import { mockDatabase } from './'; import { mockDatabase } from './';
describe('update associations', () => { describe('update associations', () => {
let db: Database;
beforeEach(() => { describe('belongsTo', () => {
db = mockDatabase(); let db: Database;
}); beforeEach(() => {
db = mockDatabase();
afterEach(async () => { });
await db.close(); afterEach(async () => {
await db.close();
});
it('post.user', async () => {
const User = db.collection({
name: 'users',
fields: [{ type: 'string', name: 'name' }],
});
const Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'name' },
{ type: 'belongsTo', name: 'user' },
],
});
await db.sync();
const user = await User.model.create<any>({ name: 'user1' });
const post1 = await Post.model.create({ name: 'post1' });
await updateAssociations(post1, {
user,
});
expect(post1.toJSON()).toMatchObject({
id: 1,
name: 'post1',
userId: 1,
user: {
id: 1,
name: 'user1',
},
});
const post2 = await Post.model.create({ name: 'post2' });
await updateAssociations(post2, {
user: user.id,
});
expect(post2.toJSON()).toMatchObject({
id: 2,
name: 'post2',
userId: 1,
});
const post3 = await Post.model.create({ name: 'post3' });
await updateAssociations(post3, {
user: {
name: 'user3',
},
});
expect(post3.toJSON()).toMatchObject({
id: 3,
name: 'post3',
userId: 2,
user: {
id: 2,
name: 'user3',
},
});
const post4 = await Post.model.create({ name: 'post4' });
await updateAssociations(post4, {
user: {
id: user.id,
name: 'user4',
},
});
expect(post4.toJSON()).toMatchObject({
id: 4,
name: 'post4',
userId: 1,
user: {
id: 1,
name: 'user1',
},
});
});
}); });
describe('hasMany', () => { describe('hasMany', () => {
it.only('model', async () => { let db: Database;
const User = db.collection({ let User: Collection;
let Post: Collection;
beforeEach(async () => {
db = mockDatabase();
User = db.collection({
name: 'users', name: 'users',
schema: [ fields: [
{ type: 'string', name: 'name' }, { type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' }, { type: 'hasMany', name: 'posts' },
], ],
}); });
const Post = db.collection({ Post = db.collection({
name: 'posts', name: 'posts',
schema: [ fields: [
{ type: 'string', name: 'title' }, { type: 'string', name: 'name' },
], ],
}); });
await db.sync(); await db.sync();
const user = await User.model.create(); });
const post1 = await Post.model.create(); afterEach(async () => {
const post2 = await Post.model.create<any>(); await db.close();
const post3 = await Post.model.create<any>(); });
const post4 = await Post.model.create<any>(); it('user.posts', async () => {
await updateAssociations(user, { const user1 = await User.model.create<any>({ name: 'user1' });
await updateAssociations(user1, {
posts: { posts: {
title: 'post0', name: 'post1',
}, },
}); });
await updateAssociations(user, { expect(user1.toJSON()).toMatchObject({
name: 'user1',
posts: [
{
name: 'post1',
userId: user1.id,
}
],
});
});
it('user.posts', async () => {
const user1 = await User.model.create<any>({ name: 'user1' });
await updateAssociations(user1, {
posts: [{
name: 'post1',
}],
});
expect(user1.toJSON()).toMatchObject({
name: 'user1',
posts: [
{
name: 'post1',
userId: user1.id,
}
],
});
});
it('user.posts', async () => {
const user1 = await User.model.create<any>({ name: 'user1' });
const post1 = await Post.model.create<any>({ name: 'post1' });
await updateAssociations(user1, {
posts: post1.id,
});
expect(user1.toJSON()).toMatchObject({
name: 'user1',
});
const post11 = await Post.model.findByPk(post1.id);
expect(post11.toJSON()).toMatchObject({
userId: user1.id,
});
});
it('user.posts', async () => {
const user1 = await User.model.create<any>({ name: 'user1' });
const post1 = await Post.model.create<any>({ name: 'post1' });
await updateAssociations(user1, {
posts: post1, posts: post1,
}); });
await updateAssociations(user, { console.log(JSON.stringify(user1, null, 2));
posts: post2.id, expect(user1.toJSON()).toMatchObject({
name: 'user1',
}); });
await updateAssociations(user, { const post11 = await Post.model.findByPk(post1.id);
posts: [post3.id], expect(post11.toJSON()).toMatchObject({
userId: user1.id,
}); });
await updateAssociations(user, { });
it('user.posts', async () => {
const user1 = await User.model.create<any>({ name: 'user1' });
const post1 = await Post.model.create<any>({ name: 'post1' });
await updateAssociations(user1, {
posts: { posts: {
id: post4.id, id: post1.id,
title: 'post4', name: 'post111',
}, },
}); });
console.log(JSON.stringify(user1, null, 2));
expect(user1.toJSON()).toMatchObject({
name: 'user1',
});
const post11 = await Post.model.findByPk(post1.id);
expect(post11.toJSON()).toMatchObject({
userId: user1.id,
name: 'post1',
});
});
it('user.posts', async () => {
const user1 = await User.model.create<any>({ name: 'user1' });
const post1 = await Post.model.create<any>({ name: 'post1' });
const post2 = await Post.model.create<any>({ name: 'post2' });
const post3 = await Post.model.create<any>({ name: 'post3' });
await updateAssociations(user1, {
posts: [
{
id: post1.id,
name: 'post111',
},
post2.id,
post3,
]
});
console.log(JSON.stringify(user1, null, 2));
expect(user1.toJSON()).toMatchObject({
name: 'user1',
});
const post11 = await Post.model.findByPk(post1.id);
expect(post11.toJSON()).toMatchObject({
userId: user1.id,
name: 'post1',
});
const post22 = await Post.model.findByPk(post2.id);
expect(post22.toJSON()).toMatchObject({
userId: user1.id,
name: 'post2',
});
const post33 = await Post.model.findByPk(post3.id);
expect(post33.toJSON()).toMatchObject({
userId: user1.id,
name: 'post3',
});
}); });
}); });
it('nested', async () => { describe('nested', () => {
const User = db.collection({ let db: Database;
name: 'users', let User: Collection;
schema: [ let Post: Collection;
{ type: 'string', name: 'name' }, let Comment: Collection;
{ type: 'hasMany', name: 'posts' },
], beforeEach(async () => {
db = mockDatabase();
User = db.collection({
name: 'users',
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'name' },
{ type: 'belongsTo', name: 'user' },
{ type: 'hasMany', name: 'comments' },
],
});
Comment = db.collection({
name: 'comments',
fields: [
{ type: 'string', name: 'name' },
{ type: 'belongsTo', name: 'post' },
],
});
await db.sync();
}); });
const Post = db.collection({
name: 'posts', afterEach(async () => {
schema: [ await db.close();
{ type: 'string', name: 'title' },
{ type: 'belongsTo', name: 'user' },
{ type: 'hasMany', name: 'comments' },
],
}); });
const Comment = db.collection({
name: 'comments', it('nested', async () => {
schema: [ const user = await User.model.create<any>({ name: 'user1' });
{ type: 'string', name: 'content' }, await updateAssociations(user, {
{ type: 'belongsTo', name: 'post' }, posts: [
], {
}); name: 'post1',
await db.sync(); comments: [
const user = await User.model.create(); {
await updateAssociations(user, { name: 'comment1',
posts: [ },
{ ],
title: 'post1', },
// user: { ],
// name: 'user1', });
// }, const post1 = await Post.model.findOne({
comments: [ where: { name: 'post1' }
{ });
content: 'content1', const comment1 = await Comment.model.findOne({
}, where: { name: 'comment1' }
], });
}, expect(post1).toMatchObject({
], userId: user.get('id'),
});
expect(comment1).toMatchObject({
postId: post1.get('id'),
});
}); });
}); });
}); });

View File

@ -1,12 +1,14 @@
import { ModelCtor, Model } from 'sequelize'; import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
import { EventEmitter } from 'events';
import { Database } from './database'; import { Database } from './database';
import { Schema } from './schema'; import { Field } from './fields';
import { RelationField } from './schema-fields';
import _ from 'lodash'; import _ from 'lodash';
import { Repository } from './repository'; import { Repository } from './repository';
export interface CollectionOptions { export interface CollectionOptions {
schema?: any; name: string;
tableName?: string;
fields?: any;
[key: string]: any; [key: string]: any;
} }
@ -14,52 +16,103 @@ export interface CollectionContext {
database: Database; database: Database;
} }
export class Collection { export class Collection extends EventEmitter {
schema: Schema;
model: ModelCtor<Model>;
repository: Repository;
options: CollectionOptions; options: CollectionOptions;
context: CollectionContext; context: CollectionContext;
fields: Map<string, any>;
model: ModelCtor<Model>;
repository: Repository;
get name() { get name() {
return this.options.name; return this.options.name;
} }
constructor(options: CollectionOptions, context: CollectionContext) { constructor(options: CollectionOptions, context?: CollectionContext) {
super();
this.options = options; this.options = options;
this.context = context; this.context = context;
const { name, tableName } = options; this.fields = new Map<string, any>();
this.model = class extends Model<any, any> {}; this.model = class extends Model<any, any> {};
const attributes = {}; const attributes = {};
const { name, tableName } = options;
// TODO: 不能重复 model.init如果有涉及 InitOptions 参数修改,需要另外处理。 // TODO: 不能重复 model.init如果有涉及 InitOptions 参数修改,需要另外处理。
this.model.init(attributes, { this.model.init(attributes, {
..._.omit(options, ['name', 'schema']), ..._.omit(options, ['name', 'fields']),
sequelize: context.database.sequelize, sequelize: context.database.sequelize,
modelName: name, modelName: name,
tableName: tableName || name, tableName: tableName || name,
}); });
// schema 只针对字段,对应 Sequelize 的 Attributes this.on('field.afterAdd', (field) => field.bind());
// 其他 InitOptions 参数放在 Collection 里,通过其他方法同步给 model this.on('field.afterRemove', (field) => field.unbind());
this.schema = new Schema(options.schema, { this.setFields(options.fields);
...context,
collection: this,
});
this.schema2model();
this.context.database.emit('collection.init', this);
this.repository = new Repository(this); this.repository = new Repository(this);
} }
schema2model() { forEachField(callback: (field: Field) => void) {
this.schema.forEach((field) => { return [...this.fields.values()].forEach(callback);
field.bind(); }
});
this.schema.on('setted', (field) => { findField(callback: (field: Field) => boolean) {
// console.log('setted', field); return [...this.fields.values()].find(callback);
field.bind(); }
});
this.schema.on('deleted', (field) => field.unbind()); hasField(name: string) {
this.schema.on('merged', (field) => { return this.fields.has(name);
// }
getField(name: string) {
return this.fields.get(name);
}
addField(options) {
const { name, ...others } = options;
if (!name) {
return this;
}
const { database } = this.context;
const field = database.buildField({ name, ...others }, {
...this.context,
collection: this,
model: this.model,
}); });
this.fields.set(name, field);
this.emit('field.afterAdd', field);
}
setFields(fields: any, reset = true) {
if (!fields) {
return this;
}
if (reset) {
this.fields.clear();
}
if (Array.isArray(fields)) {
for (const field of fields) {
this.addField(field);
}
} else if (typeof fields === 'object') {
for (const [name, options] of Object.entries<any>(fields)) {
this.addField({...options, name});
}
}
}
removeField(name) {
const field = this.fields.get(name);
const bool = this.fields.delete(name);
if (bool) {
this.emit('field.afterRemove', field);
}
return bool;
}
// TODO
extend(options) {
const { fields } = options;
this.setFields(fields);
}
sync() {
} }
} }

View File

@ -1,16 +1,16 @@
import { Sequelize, ModelCtor, Model, Options, SyncOptions, Op, Utils } from 'sequelize'; import {
Sequelize,
ModelCtor,
Model,
Options,
SyncOptions,
Op,
Utils,
} from 'sequelize';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Collection, CollectionOptions } from './collection'; import { Collection, CollectionOptions } from './collection';
import { import * as FieldTypes from './fields';
RelationField, import { RelationField } from './fields';
StringField,
HasOneField,
HasManyField,
BelongsToField,
BelongsToManyField,
JsonField,
JsonbField,
} from './schema-fields';
export interface PendingOptions { export interface PendingOptions {
field: RelationField; field: RelationField;
@ -21,7 +21,7 @@ export type DatabaseOptions = Options | Sequelize;
export class Database extends EventEmitter { export class Database extends EventEmitter {
sequelize: Sequelize; sequelize: Sequelize;
schemaTypes = new Map(); fieldTypes = new Map();
models = new Map(); models = new Map();
repositories = new Map(); repositories = new Map();
operators = new Map(); operators = new Map();
@ -30,27 +30,32 @@ export class Database extends EventEmitter {
constructor(options: DatabaseOptions) { constructor(options: DatabaseOptions) {
super(); super();
if (options instanceof Sequelize) { if (options instanceof Sequelize) {
this.sequelize = options; this.sequelize = options;
} else { } else {
this.sequelize = new Sequelize(options); this.sequelize = new Sequelize(options);
} }
this.collections = new Map(); this.collections = new Map();
this.on('collection.init', (collection) => {
this.on('collection.afterDefine', (collection) => {
const items = this.pendingFields.get(collection.name); const items = this.pendingFields.get(collection.name);
for (const field of items || []) { for (const field of items || []) {
field.bind(); field.bind();
} }
}); });
this.registerSchemaTypes({
string: StringField, for (const [name, field] of Object.entries(FieldTypes)) {
json: JsonField, if (['Field', 'RelationField'].includes(name)) {
jsonb: JsonbField, continue;
hasOne: HasOneField, }
hasMany: HasManyField, let key = name.replace(/Field$/g, '');
belongsTo: BelongsToField, key = key.substring(0, 1).toLowerCase() + key.substring(1);
belongsToMany: BelongsToManyField, this.registerFieldTypes({
}); [key]: field,
});
}
const operators = new Map(); const operators = new Map();
@ -68,11 +73,12 @@ export class Database extends EventEmitter {
collection(options: CollectionOptions) { collection(options: CollectionOptions) {
let collection = this.collections.get(options.name); let collection = this.collections.get(options.name);
if (collection) { if (collection) {
collection.schema.set(options.schema); collection.extend(options);
} else { } else {
collection = new Collection(options, { database: this }); collection = new Collection(options, { database: this });
} }
this.collections.set(collection.name, collection); this.collections.set(collection.name, collection);
this.emit('collection.afterDefine', collection);
return collection; return collection;
} }
@ -96,9 +102,9 @@ export class Database extends EventEmitter {
} }
} }
registerSchemaTypes(schemaTypes: any) { registerFieldTypes(fieldTypes: any) {
for (const [type, schemaType] of Object.entries(schemaTypes)) { for (const [type, fieldType] of Object.entries(fieldTypes)) {
this.schemaTypes.set(type, schemaType); this.fieldTypes.set(type, fieldType);
} }
} }
@ -120,9 +126,9 @@ export class Database extends EventEmitter {
} }
} }
buildSchemaField(options, context) { buildField(options, context) {
const { type } = options; const { type } = options;
const Field = this.schemaTypes.get(type); const Field = this.fieldTypes.get(type);
return new Field(options, context); return new Field(options, context);
} }

View File

@ -3,6 +3,9 @@ import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
import { RelationField } from './relation-field'; import { RelationField } from './relation-field';
export class BelongsToField extends RelationField { export class BelongsToField extends RelationField {
static type = 'belongsTo';
get target() { get target() {
const { target, name } = this.options; const { target, name } = this.options;
return target || Utils.pluralize(name); return target || Utils.pluralize(name);
@ -38,8 +41,8 @@ export class BelongsToField extends RelationField {
// 如果外键没有显式的创建,关系表也无反向关联字段,删除关系时,外键也删除掉 // 如果外键没有显式的创建,关系表也无反向关联字段,删除关系时,外键也删除掉
const tcoll = database.collections.get(this.target); const tcoll = database.collections.get(this.target);
const foreignKey = this.options.foreignKey; const foreignKey = this.options.foreignKey;
const field1 = collection.schema.get(foreignKey); const field1 = collection.getField(foreignKey);
const field2 = tcoll.schema.find((field) => { const field2 = tcoll.findField((field) => {
return field.type === 'hasMany' && field.foreignKey === foreignKey; return field.type === 'hasMany' && field.foreignKey === foreignKey;
}); });
if (!field1 && !field2) { if (!field1 && !field2) {

View File

@ -1,7 +1,7 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class DateField extends SchemaField { export class DateField extends Field {
get dataType() { get dataType() {
return DataTypes.DATE; return DataTypes.DATE;
} }

View File

@ -2,19 +2,18 @@ import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Collection } from '../collection'; import { Collection } from '../collection';
import { Database } from '../database'; import { Database } from '../database';
import { Schema } from '../schema';
import { RelationField } from './relation-field';
import _ from 'lodash'; import _ from 'lodash';
export interface SchemaFieldContext { export interface FieldContext {
database: Database; database: Database;
collection: Collection; collection: Collection;
schema: Schema;
} }
export abstract class SchemaField { export abstract class Field {
options: any; options: any;
context: SchemaFieldContext; context: FieldContext;
database: Database;
collection: Collection;
[key: string]: any; [key: string]: any;
get name() { get name() {
@ -29,8 +28,10 @@ export abstract class SchemaField {
return this.options.dataType; return this.options.dataType;
} }
constructor(options?: any, context?: SchemaFieldContext) { constructor(options?: any, context?: FieldContext) {
this.context = context; this.context = context;
this.database = context.database;
this.collection = context.collection;
this.options = options || {}; this.options = options || {};
this.init(); this.init();
} }

View File

@ -1,7 +1,7 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class FloatField extends SchemaField { export class FloatField extends Field {
get dataType() { get dataType() {
return DataTypes.FLOAT; return DataTypes.FLOAT;
} }

View File

@ -7,6 +7,7 @@ import {
AssociationScope, AssociationScope,
ForeignKeyOptions, ForeignKeyOptions,
HasManyOptions, HasManyOptions,
Utils,
} from 'sequelize'; } from 'sequelize';
import { RelationField } from './relation-field'; import { RelationField } from './relation-field';
@ -73,6 +74,19 @@ export interface HasManyFieldOptions extends HasManyOptions {
export class HasManyField extends RelationField { export class HasManyField extends RelationField {
get foreignKey() {
if (this.options.foreignKey) {
return this.options.foreignKey;
}
const { model } = this.context.collection;
return Utils.camelize(
[
model.options.name.singular,
this.sourceKey || model.primaryKeyAttribute
].join('_')
);
}
bind() { bind() {
const { database, collection } = this.context; const { database, collection } = this.context;
const Target = this.TargetModel; const Target = this.TargetModel;
@ -82,6 +96,7 @@ export class HasManyField extends RelationField {
} }
const association = collection.model.hasMany(Target, { const association = collection.model.hasMany(Target, {
as: this.name, as: this.name,
foreignKey: this.foreignKey,
...omit(this.options, ['name', 'type', 'target']), ...omit(this.options, ['name', 'type', 'target']),
}); });
// 建立关系之后从 pending 列表中删除 // 建立关系之后从 pending 列表中删除
@ -103,7 +118,7 @@ export class HasManyField extends RelationField {
// 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉 // 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉
const tcoll = database.collections.get(this.target); const tcoll = database.collections.get(this.target);
const foreignKey = this.options.foreignKey; const foreignKey = this.options.foreignKey;
const field = tcoll.schema.find((field) => { const field = tcoll.findField((field) => {
if (field.name === foreignKey) { if (field.name === foreignKey) {
return true; return true;
} }

View File

@ -123,7 +123,7 @@ export class HasOneField extends RelationField {
// 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉 // 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉
const tcoll = database.collections.get(this.target); const tcoll = database.collections.get(this.target);
const foreignKey = this.options.foreignKey; const foreignKey = this.options.foreignKey;
const field = tcoll.schema.find((field) => { const field = tcoll.findField((field) => {
if (field.name === foreignKey) { if (field.name === foreignKey) {
return true; return true;
} }

View File

@ -1,4 +1,4 @@
export * from './schema-field'; export * from './field';
export * from './string-field'; export * from './string-field';
export * from './relation-field' export * from './relation-field'
export * from './belongs-to-field' export * from './belongs-to-field'

View File

@ -1,7 +1,7 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class IntegerField extends SchemaField { export class IntegerField extends Field {
get dataType() { get dataType() {
return DataTypes.INTEGER; return DataTypes.INTEGER;
} }

View File

@ -1,13 +1,13 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class JsonField extends SchemaField { export class JsonField extends Field {
get dataType() { get dataType() {
return DataTypes.JSON; return DataTypes.JSON;
} }
} }
export class JsonbField extends SchemaField { export class JsonbField extends Field {
get dataType() { get dataType() {
const dialect = this.context.database.sequelize.getDialect(); const dialect = this.context.database.sequelize.getDialect();
if (dialect === 'postgres') { if (dialect === 'postgres') {

View File

@ -1,31 +1,31 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class IntegerField extends SchemaField { export class IntegerField extends Field {
get dataType() { get dataType() {
return DataTypes.INTEGER; return DataTypes.INTEGER;
} }
} }
export class FloatField extends SchemaField { export class FloatField extends Field {
get dataType() { get dataType() {
return DataTypes.FLOAT; return DataTypes.FLOAT;
} }
} }
export class DoubleField extends SchemaField { export class DoubleField extends Field {
get dataType() { get dataType() {
return DataTypes.DOUBLE; return DataTypes.DOUBLE;
} }
} }
export class RealField extends SchemaField { export class RealField extends Field {
get dataType() { get dataType() {
return DataTypes.REAL; return DataTypes.REAL;
} }
} }
export class DecimalField extends SchemaField { export class DecimalField extends Field {
get dataType() { get dataType() {
return DataTypes.DECIMAL; return DataTypes.DECIMAL;
} }

View File

@ -1,6 +1,6 @@
import { SchemaField } from './schema-field'; import { Field } from './field';
export abstract class RelationField extends SchemaField { export abstract class RelationField extends Field {
get target() { get target() {
const { target, name } = this.options; const { target, name } = this.options;
return target || name; return target || name;

View File

@ -1,8 +1,8 @@
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class SortField extends SchemaField { export class SortField extends Field {
get dataType() { get dataType() {
return DataTypes.INTEGER; return DataTypes.INTEGER;
} }

View File

@ -1,7 +1,7 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class StringField extends SchemaField { export class StringField extends Field {
get dataType() { get dataType() {
return DataTypes.STRING; return DataTypes.STRING;
} }

View File

@ -1,7 +1,7 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class TextField extends SchemaField { export class TextField extends Field {
get dataType() { get dataType() {
return DataTypes.TEXT; return DataTypes.TEXT;
} }

View File

@ -1,7 +1,7 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class TimeField extends SchemaField { export class TimeField extends Field {
get dataType() { get dataType() {
return DataTypes.TIME; return DataTypes.TIME;
} }

View File

@ -1,7 +1,7 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field'; import { Field } from './field';
export class VirtualField extends SchemaField { export class VirtualField extends Field {
get dataType() { get dataType() {
return DataTypes.VIRTUAL; return DataTypes.VIRTUAL;
} }

View File

@ -1,21 +1,30 @@
import { import {
ModelCtor,
Model,
BulkCreateOptions,
FindOptions,
Op, Op,
Model,
ModelCtor,
Association,
FindOptions,
BulkCreateOptions,
DestroyOptions as SequelizeDestroyOptions,
CreateOptions as SequelizeCreateOptions,
UpdateOptions as SequelizeUpdateOptions,
} from 'sequelize'; } from 'sequelize';
import { flatten } from 'flat'; import { flatten } from 'flat';
import { Collection } from './collection'; import { Collection } from './collection';
import _ from 'lodash'; import _ from 'lodash';
import { Database } from './database'; import { Database } from './database';
import { updateAssociations } from './update-associations'; import { updateAssociations } from './update-associations';
import { RelationField } from './fields';
export interface IRepository {} export interface IRepository {}
interface FindAllOptions extends FindOptions { interface CreateManyOptions extends BulkCreateOptions {}
interface FindManyOptions extends FindOptions {
filter?: any; filter?: any;
fields?: any; fields?: any;
appends?: any;
expect?: any;
page?: any; page?: any;
pageSize?: any; pageSize?: any;
sort?: any; sort?: any;
@ -24,23 +33,152 @@ interface FindAllOptions extends FindOptions {
interface FindOneOptions extends FindOptions { interface FindOneOptions extends FindOptions {
filter?: any; filter?: any;
fields?: any; fields?: any;
appends?: any;
expect?: any;
sort?: any; sort?: any;
} }
export class Repository implements IRepository { interface CreateOptions extends SequelizeCreateOptions {
collection: Collection; values?: any;
whitelist?: any;
blacklist?: any;
}
interface UpdateOptions extends SequelizeUpdateOptions {
values?: any;
whitelist?: any;
blacklist?: any;
}
interface DestroyOptions extends SequelizeDestroyOptions {
filter?: any;
}
interface RelatedQueryOptions {
database: Database; database: Database;
field: RelationField;
source: {
idOrInstance: any;
collection: Collection;
};
target: {
association: Association & {
accessors: any;
};
collection: Collection;
};
}
type Identity = string | number;
class RelatedQuery {
options: RelatedQueryOptions;
sourceInstance: Model;
constructor(options: RelatedQueryOptions) {
this.options = options;
}
async getSourceInstance() {
if (this.sourceInstance) {
return this.sourceInstance;
}
const { idOrInstance, collection } = this.options.source;
if (idOrInstance instanceof Model) {
return (this.sourceInstance = idOrInstance);
}
this.sourceInstance = await collection.model.findByPk(idOrInstance);
return this.sourceInstance;
}
async findMany(options?: any) {
const { collection } = this.options.target;
return await collection.repository.findMany(options);
}
async findOne(options?: any) {
const { collection } = this.options.target;
return await collection.repository.findOne(options);
}
async create(values?: any, options?: any) {
const { association } = this.options.target;
const createAccessor = association.accessors.create;
const source = await this.getSourceInstance();
const instance = await source[createAccessor](values, options);
if (!instance) {
return;
}
await updateAssociations(instance, values);
return instance;
}
async update(values: any, options?: Identity | Model | UpdateOptions) {
const { association, collection } = this.options.target;
if (options instanceof Model) {
return await collection.repository.update(values, options);
}
const { field } = this.options;
if (field.type === 'hasOne' || field.type === 'belongsTo') {
const getAccessor = association.accessors.get;
const source = await this.getSourceInstance();
const instance = await source[getAccessor]();
return await collection.repository.update(values, instance);
}
// TODO
return await collection.repository.update(values, options);
}
async destroy(options?: any) {
const { association, collection } = this.options.target;
const { field } = this.options;
if (field.type === 'hasOne' || field.type === 'belongsTo') {
const getAccessor = association.accessors.get;
const source = await this.getSourceInstance();
const instance = await source[getAccessor]();
if (!instance) {
return;
}
return await collection.repository.destroy(instance.id);
}
return await collection.repository.destroy(options);
}
async set(options?: any) {}
async add(options?: any) {}
async remove(options?: any) {}
async toggle(options?: any) {}
async sync(options?: any) {}
}
class HasOneQuery extends RelatedQuery {}
class HasManyQuery extends RelatedQuery {}
class BelongsToQuery extends RelatedQuery {}
class BelongsToManyQuery extends RelatedQuery {}
export class Repository implements IRepository {
database: Database;
collection: Collection;
model: ModelCtor<Model>;
constructor(collection: Collection) { constructor(collection: Collection) {
this.database = collection.context.database; this.database = collection.context.database;
this.collection = collection; this.collection = collection;
this.model = collection.model;
} }
async findAll(options?: FindAllOptions) { async findMany(options?: FindManyOptions) {
const model = this.collection.model; const model = this.collection.model;
const opts = { const opts = {
subQuery: false, subQuery: false,
...this.parseApiJson(options), ...this.buildQueryOptions(options),
}; };
let rows = []; let rows = [];
if (opts.include) { if (opts.include) {
@ -52,14 +190,16 @@ export class Repository implements IRepository {
group: `${model.name}.${model.primaryKeyAttribute}`, group: `${model.name}.${model.primaryKeyAttribute}`,
}) })
).map((item) => item[model.primaryKeyAttribute]); ).map((item) => item[model.primaryKeyAttribute]);
rows = await model.findAll({ if (ids.length > 0) {
...opts, rows = await model.findAll({
where: { ...opts,
[model.primaryKeyAttribute]: { where: {
[Op.in]: ids, [model.primaryKeyAttribute]: {
[Op.in]: ids,
},
}, },
}, });
}); }
} else { } else {
rows = await model.findAll({ rows = await model.findAll({
...opts, ...opts,
@ -73,19 +213,44 @@ export class Repository implements IRepository {
} }
async findOne(options?: FindOneOptions) { async findOne(options?: FindOneOptions) {
const opts = this.parseApiJson(options); const model = this.collection.model;
console.log({ opts }); const opts = {
const data = await this.collection.model.findOne(opts); subQuery: false,
...this.buildQueryOptions(options),
};
let data: Model;
if (opts.include) {
const item = await model.findOne({
...opts,
includeIgnoreAttributes: false,
attributes: [model.primaryKeyAttribute],
group: `${model.name}.${model.primaryKeyAttribute}`,
});
if (!item) {
return;
}
data = await model.findOne({
...opts,
where: item.toJSON(),
});
} else {
data = await model.findOne({
...opts,
});
}
return data; return data;
} }
create() {} async create(values?: any, options?: CreateOptions) {
const instance = await this.model.create<any>(values, options);
if (!instance) {
return;
}
await updateAssociations(instance, values, options);
return instance;
}
update() {} async createMany(records: any[], options?: CreateManyOptions) {
destroy() {}
async bulkCreate(records: any[], options?: BulkCreateOptions) {
const instances = await this.collection.model.bulkCreate(records, options); const instances = await this.collection.model.bulkCreate(records, options);
const promises = instances.map((instance, index) => { const promises = instances.map((instance, index) => {
return updateAssociations(instance, records[index]); return updateAssociations(instance, records[index]);
@ -93,9 +258,97 @@ export class Repository implements IRepository {
return Promise.all(promises); return Promise.all(promises);
} }
parseApiJson(options: any) { async update(values: any, options: Identity | Model | UpdateOptions) {
const filter = options.filter || {}; if (options instanceof Model) {
await options.update(values);
await updateAssociations(options, values);
return options;
}
let instance: Model;
if (typeof options === 'string' || typeof options === 'number') {
instance = await this.model.findByPk(options);
} else {
// TODO
instance = await this.findOne(options);
}
await instance.update(values);
await updateAssociations(instance, values);
return instance;
}
async destroy(options: Identity | Identity[] | DestroyOptions) {
if (typeof options === 'number' || typeof options === 'string') {
return await this.model.destroy({
where: {
[this.model.primaryKeyAttribute]: options,
},
});
}
if (Array.isArray(options)) {
return await this.model.destroy({
where: {
[this.model.primaryKeyAttribute]: {
[Op.in]: options,
},
},
});
}
const opts = this.buildQueryOptions(options);
return await this.model.destroy(opts);
}
// TODO
async sort() {}
relatedQuery(name: string) {
return {
for: (sourceIdOrInstance: any) => {
const field = this.collection.getField(name) as RelationField;
const database = this.collection.context.database;
const collection = database.getCollection(field.target);
const options: RelatedQueryOptions = {
field,
database: database,
source: {
collection: this.collection,
idOrInstance: sourceIdOrInstance,
},
target: {
collection,
association: this.collection.model.associations[name] as any,
},
};
switch (field.type) {
case 'hasOne':
return new HasOneQuery(options);
case 'hasMany':
return new HasManyQuery(options);
case 'belongsTo':
return new BelongsToQuery(options);
case 'belongsToMany':
return new BelongsToManyQuery(options);
}
},
};
}
buildQueryOptions(options: any) {
const opts = this.parseFilter(options.filter);
return { ...options, ...opts };
}
parseFilter(filter?: any) {
if (!filter) {
return {};
}
const model = this.collection.model; const model = this.collection.model;
if (typeof filter === 'number' || typeof filter === 'string') {
return {
where: {
[model.primaryKeyAttribute]: filter,
},
};
}
const operators = this.database.operators; const operators = this.database.operators;
const obj = flatten(filter || {}); const obj = flatten(filter || {});
const include = {}; const include = {};
@ -145,6 +398,7 @@ export class Repository implements IRepository {
associationKeys.push(k); associationKeys.push(k);
_.set(include, k, { _.set(include, k, {
association: k, association: k,
attributes: [],
}); });
let target = associations[k].target; let target = associations[k].target;
while (target) { while (target) {
@ -163,6 +417,7 @@ export class Repository implements IRepository {
}); });
_.set(include, assoc, { _.set(include, assoc, {
association: attr, association: attr,
attributes: [],
}); });
target = target.associations[attr].target; target = target.associations[attr].target;
} }
@ -193,7 +448,6 @@ export class Repository implements IRepository {
return item; return item;
}); });
}; };
console.log(JSON.stringify({ include: toInclude(include) }, null, 2)); return { where, include: toInclude(include) };
return { ...options, where, include: toInclude(include) };
} }
} }

View File

@ -1,83 +0,0 @@
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,
});
// console.log('field', field);
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);
if (bool) {
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);
}
}

View File

@ -16,18 +16,18 @@ function isStringOrNumber(value: any) {
} }
export async function updateAssociations( export async function updateAssociations(
model: Model, instance: Model,
values: any, values: any,
options: any = {}, options: any = {},
) { ) {
const { transaction = await model.sequelize.transaction() } = options; const { transaction = await instance.sequelize.transaction() } = options;
// @ts-ignore // @ts-ignore
for (const key of Object.keys(model.constructor.associations)) { for (const key of Object.keys(instance.constructor.associations)) {
// 如果 key 不存在才跳过 // 如果 key 不存在才跳过
if (!Object.keys(values).includes(key)) { if (!Object.keys(values||{}).includes(key)) {
continue; continue;
} }
await updateAssociation(model, key, values[key], { await updateAssociation(instance, key, values[key], {
...options, ...options,
transaction, transaction,
}); });
@ -38,23 +38,23 @@ export async function updateAssociations(
} }
export async function updateAssociation( export async function updateAssociation(
model: Model, instance: Model,
key: string, key: string,
value: any, value: any,
options: any = {}, options: any = {},
) { ) {
// @ts-ignore // @ts-ignore
const association = model.constructor.associations[key] as Association; const association = instance.constructor.associations[key] as Association;
if (!association) { if (!association) {
return false; return false;
} }
switch (association.associationType) { switch (association.associationType) {
case 'HasOne': case 'HasOne':
case 'BelongsTo': case 'BelongsTo':
return updateSingleAssociation(model, key, value, options); return updateSingleAssociation(instance, key, value, options);
case 'HasMany': case 'HasMany':
case 'BelongsToMany': case 'BelongsToMany':
return updateMultipleAssociation(model, key, value, options); return updateMultipleAssociation(instance, key, value, options);
} }
} }
@ -77,39 +77,63 @@ export async function updateSingleAssociation(
// @ts-ignore // @ts-ignore
const setAccessor = association.accessors.set; const setAccessor = association.accessors.set;
if (isUndefinedOrNull(value)) { if (isUndefinedOrNull(value)) {
return await model[setAccessor](null, { transaction }); await model[setAccessor](null, { transaction });
model.setDataValue(key, null);
if (!options.transaction) {
await transaction.commit();
}
return true;
} }
if (isStringOrNumber(value)) { if (isStringOrNumber(value)) {
return await model[setAccessor](value, { transaction }); await model[setAccessor](value, { transaction });
if (!options.transaction) {
await transaction.commit();
}
return true;
}
if (value instanceof Model) {
await model[setAccessor](value);
model.setDataValue(key, value);
if (!options.transaction) {
await transaction.commit();
}
return true;
} }
// @ts-ignore // @ts-ignore
const createAccessor = association.accessors.create; const createAccessor = association.accessors.create;
let key: string; let dataKey: string;
let M: ModelCtor<Model>; let M: ModelCtor<Model>;
if (association.associationType === 'BelongsTo') { if (association.associationType === 'BelongsTo') {
// @ts-ignore
key = association.targetKey;
M = association.target; M = association.target;
} else {
// @ts-ignore // @ts-ignore
key = association.sourceKey; dataKey = association.targetKey;
} else {
M = association.source; M = association.source;
dataKey = M.primaryKeyAttribute;
} }
if (isStringOrNumber(value)) { if (isStringOrNumber(value[dataKey])) {
let instance: any = await M.findOne({ let instance: any = await M.findOne({
where: { where: {
[key]: value[key], [dataKey]: value[dataKey],
}, },
transaction, transaction,
}); });
if (!instance) { if (instance) {
instance = await M.create(value, { transaction }); await model[setAccessor](instance);
await updateAssociations(instance, value, { transaction, ...options });
model.setDataValue(key, instance);
if (!options.transaction) {
await transaction.commit();
}
return true;
} }
await model[setAccessor](value[key]); }
await updateAssociations(instance, value, { transaction, ...options }); const instance = await model[createAccessor](value, { transaction });
} else { await updateAssociations(instance, value, { transaction, ...options });
const instance = await model[createAccessor](value, { transaction }); model.setDataValue(key, instance);
await updateAssociations(instance, value, { transaction, ...options }); // @ts-ignore
if (association.targetKey) {
model.setDataValue(association.foreignKey, instance[dataKey]);
} }
if (!options.transaction) { if (!options.transaction) {
await transaction.commit(); await transaction.commit();
@ -143,10 +167,13 @@ export async function updateMultipleAssociation(
// @ts-ignore // @ts-ignore
const createAccessor = association.accessors.create; const createAccessor = association.accessors.create;
if (isUndefinedOrNull(value)) { if (isUndefinedOrNull(value)) {
return await model[setAccessor](null, { transaction }); await model[setAccessor](null, { transaction });
model.setDataValue(key, null);
return;
} }
if (isStringOrNumber(value)) { if (isStringOrNumber(value)) {
return await model[setAccessor](value, { transaction }); await model[setAccessor](value, { transaction });
return;
} }
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
value = [value]; value = [value];
@ -167,21 +194,24 @@ export async function updateMultipleAssociation(
list2.push(item); list2.push(item);
} }
} }
console.log('updateMultipleAssociation', list1, list2);
await model[setAccessor](list1, { transaction }); await model[setAccessor](list1, { transaction });
const list3 = [];
for (const item of list2) { for (const item of list2) {
const pk = association.target.primaryKeyAttribute; const pk = association.target.primaryKeyAttribute;
if (isUndefinedOrNull(item[pk])) { if (isUndefinedOrNull(item[pk])) {
const instance = await model[createAccessor](item, { transaction }); const instance = await model[createAccessor](item, { transaction });
await updateAssociations(instance, item, { transaction, ...options }); await updateAssociations(instance, item, { transaction, ...options });
list3.push(instance);
} else { } else {
const instance = await association.target.findByPk(item[pk], { transaction }); const instance = await association.target.findByPk(item[pk], { transaction });
// @ts-ignore // @ts-ignore
const addAccessor = association.accessors.add; const addAccessor = association.accessors.add;
await model[addAccessor](item[pk], { transaction }); await model[addAccessor](item[pk], { transaction });
await updateAssociations(instance, item, { transaction, ...options }); await updateAssociations(instance, item, { transaction, ...options });
list3.push(instance);
} }
} }
model.setDataValue(key, list1.concat(list3));
if (!options.transaction) { if (!options.transaction) {
await transaction.commit(); await transaction.commit();
} }