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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,8 @@
import { Collection } from '../collection';
import { Database } from '../database';
import { updateAssociation, updateAssociations } from '../update-associations';
import { mockDatabase } from './';
describe('repository', () => {
describe('repository.find', () => {
let db: Database;
let User: Collection;
let Post: Collection;
@ -13,24 +12,24 @@ describe('repository', () => {
db = mockDatabase();
User = db.collection({
name: 'users',
schema: [
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
Post = db.collection({
name: 'posts',
schema: [
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'comments' },
],
});
Comment = db.collection({
name: 'comments',
schema: [{ type: 'string', name: 'name' }],
fields: [{ type: 'string', name: 'name' }],
});
await db.sync();
await User.repository.bulkCreate([
await User.repository.createMany([
{
name: 'user1',
posts: [
@ -128,29 +127,300 @@ describe('repository', () => {
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 () => {
const data = await User.repository.findOne({
filter: {
'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({
// 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 { updateAssociation, updateAssociations } from '../update-associations';
import { mockDatabase } from './';
describe('update associations', () => {
let db: Database;
describe('belongsTo', () => {
let db: Database;
beforeEach(() => {
db = mockDatabase();
});
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', () => {
let db: Database;
let User: Collection;
let Post: 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' },
],
});
await db.sync();
});
afterEach(async () => {
await db.close();
});
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' });
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,
});
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,
});
});
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: {
id: post1.id,
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',
});
});
});
describe('nested', () => {
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' },
{ type: 'belongsTo', name: 'post' },
],
});
await db.sync();
});
afterEach(async () => {
await db.close();
});
describe('hasMany', () => {
it.only('model', async () => {
const User = db.collection({
name: 'users',
schema: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
const Post = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'title' },
],
});
await db.sync();
const user = await User.model.create();
const post1 = await Post.model.create();
const post2 = await Post.model.create<any>();
const post3 = await Post.model.create<any>();
const post4 = await Post.model.create<any>();
await updateAssociations(user, {
posts: {
title: 'post0',
},
});
await updateAssociations(user, {
posts: post1,
});
await updateAssociations(user, {
posts: post2.id,
});
await updateAssociations(user, {
posts: [post3.id],
});
await updateAssociations(user, {
posts: {
id: post4.id,
title: 'post4',
},
});
});
});
it('nested', async () => {
const User = db.collection({
name: 'users',
schema: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
const Post = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'title' },
{ type: 'belongsTo', name: 'user' },
{ type: 'hasMany', name: 'comments' },
],
});
const Comment = db.collection({
name: 'comments',
schema: [
{ type: 'string', name: 'content' },
{ type: 'belongsTo', name: 'post' },
],
});
await db.sync();
const user = await User.model.create();
const user = await User.model.create<any>({ name: 'user1' });
await updateAssociations(user, {
posts: [
{
title: 'post1',
// user: {
// name: 'user1',
// },
name: 'post1',
comments: [
{
content: 'content1',
name: 'comment1',
},
],
},
],
});
const post1 = await Post.model.findOne({
where: { name: 'post1' }
});
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 { Schema } from './schema';
import { RelationField } from './schema-fields';
import { Field } from './fields';
import _ from 'lodash';
import { Repository } from './repository';
export interface CollectionOptions {
schema?: any;
name: string;
tableName?: string;
fields?: any;
[key: string]: any;
}
@ -14,52 +16,103 @@ export interface CollectionContext {
database: Database;
}
export class Collection {
schema: Schema;
model: ModelCtor<Model>;
repository: Repository;
export class Collection extends EventEmitter {
options: CollectionOptions;
context: CollectionContext;
fields: Map<string, any>;
model: ModelCtor<Model>;
repository: Repository;
get name() {
return this.options.name;
}
constructor(options: CollectionOptions, context: CollectionContext) {
constructor(options: CollectionOptions, context?: CollectionContext) {
super();
this.options = options;
this.context = context;
const { name, tableName } = options;
this.fields = new Map<string, any>();
this.model = class extends Model<any, any> {};
const attributes = {};
const { name, tableName } = options;
// TODO: 不能重复 model.init如果有涉及 InitOptions 参数修改,需要另外处理。
this.model.init(attributes, {
..._.omit(options, ['name', 'schema']),
..._.omit(options, ['name', 'fields']),
sequelize: context.database.sequelize,
modelName: name,
tableName: tableName || name,
});
// schema 只针对字段,对应 Sequelize 的 Attributes
// 其他 InitOptions 参数放在 Collection 里,通过其他方法同步给 model
this.schema = new Schema(options.schema, {
...context,
collection: this,
});
this.schema2model();
this.context.database.emit('collection.init', this);
this.on('field.afterAdd', (field) => field.bind());
this.on('field.afterRemove', (field) => field.unbind());
this.setFields(options.fields);
this.repository = new Repository(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) => {
//
forEachField(callback: (field: Field) => void) {
return [...this.fields.values()].forEach(callback);
}
findField(callback: (field: Field) => boolean) {
return [...this.fields.values()].find(callback);
}
hasField(name: string) {
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 { Collection, CollectionOptions } from './collection';
import {
RelationField,
StringField,
HasOneField,
HasManyField,
BelongsToField,
BelongsToManyField,
JsonField,
JsonbField,
} from './schema-fields';
import * as FieldTypes from './fields';
import { RelationField } from './fields';
export interface PendingOptions {
field: RelationField;
@ -21,7 +21,7 @@ export type DatabaseOptions = Options | Sequelize;
export class Database extends EventEmitter {
sequelize: Sequelize;
schemaTypes = new Map();
fieldTypes = new Map();
models = new Map();
repositories = new Map();
operators = new Map();
@ -30,27 +30,32 @@ export class Database extends EventEmitter {
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) => {
this.on('collection.afterDefine', (collection) => {
const items = this.pendingFields.get(collection.name);
for (const field of items || []) {
field.bind();
}
});
this.registerSchemaTypes({
string: StringField,
json: JsonField,
jsonb: JsonbField,
hasOne: HasOneField,
hasMany: HasManyField,
belongsTo: BelongsToField,
belongsToMany: BelongsToManyField,
for (const [name, field] of Object.entries(FieldTypes)) {
if (['Field', 'RelationField'].includes(name)) {
continue;
}
let key = name.replace(/Field$/g, '');
key = key.substring(0, 1).toLowerCase() + key.substring(1);
this.registerFieldTypes({
[key]: field,
});
}
const operators = new Map();
@ -68,11 +73,12 @@ export class Database extends EventEmitter {
collection(options: CollectionOptions) {
let collection = this.collections.get(options.name);
if (collection) {
collection.schema.set(options.schema);
collection.extend(options);
} else {
collection = new Collection(options, { database: this });
}
this.collections.set(collection.name, collection);
this.emit('collection.afterDefine', collection);
return collection;
}
@ -96,9 +102,9 @@ export class Database extends EventEmitter {
}
}
registerSchemaTypes(schemaTypes: any) {
for (const [type, schemaType] of Object.entries(schemaTypes)) {
this.schemaTypes.set(type, schemaType);
registerFieldTypes(fieldTypes: any) {
for (const [type, fieldType] of Object.entries(fieldTypes)) {
this.fieldTypes.set(type, fieldType);
}
}
@ -120,9 +126,9 @@ export class Database extends EventEmitter {
}
}
buildSchemaField(options, context) {
buildField(options, context) {
const { type } = options;
const Field = this.schemaTypes.get(type);
const Field = this.fieldTypes.get(type);
return new Field(options, context);
}

View File

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

View File

@ -1,7 +1,7 @@
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() {
return DataTypes.DATE;
}

View File

@ -2,19 +2,18 @@ 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 {
export interface FieldContext {
database: Database;
collection: Collection;
schema: Schema;
}
export abstract class SchemaField {
export abstract class Field {
options: any;
context: SchemaFieldContext;
context: FieldContext;
database: Database;
collection: Collection;
[key: string]: any;
get name() {
@ -29,8 +28,10 @@ export abstract class SchemaField {
return this.options.dataType;
}
constructor(options?: any, context?: SchemaFieldContext) {
constructor(options?: any, context?: FieldContext) {
this.context = context;
this.database = context.database;
this.collection = context.collection;
this.options = options || {};
this.init();
}

View File

@ -1,7 +1,7 @@
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() {
return DataTypes.FLOAT;
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
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() {
return DataTypes.INTEGER;
}

View File

@ -1,13 +1,13 @@
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() {
return DataTypes.JSON;
}
}
export class JsonbField extends SchemaField {
export class JsonbField extends Field {
get dataType() {
const dialect = this.context.database.sequelize.getDialect();
if (dialect === 'postgres') {

View File

@ -1,31 +1,31 @@
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() {
return DataTypes.INTEGER;
}
}
export class FloatField extends SchemaField {
export class FloatField extends Field {
get dataType() {
return DataTypes.FLOAT;
}
}
export class DoubleField extends SchemaField {
export class DoubleField extends Field {
get dataType() {
return DataTypes.DOUBLE;
}
}
export class RealField extends SchemaField {
export class RealField extends Field {
get dataType() {
return DataTypes.REAL;
}
}
export class DecimalField extends SchemaField {
export class DecimalField extends Field {
get dataType() {
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() {
const { target, name } = this.options;
return target || name;

View File

@ -1,8 +1,8 @@
import { isNumber } from 'lodash';
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() {
return DataTypes.INTEGER;
}

View File

@ -1,7 +1,7 @@
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() {
return DataTypes.STRING;
}

View File

@ -1,7 +1,7 @@
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() {
return DataTypes.TEXT;
}

View File

@ -1,7 +1,7 @@
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() {
return DataTypes.TIME;
}

View File

@ -1,7 +1,7 @@
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() {
return DataTypes.VIRTUAL;
}

View File

@ -1,21 +1,30 @@
import {
ModelCtor,
Model,
BulkCreateOptions,
FindOptions,
Op,
Model,
ModelCtor,
Association,
FindOptions,
BulkCreateOptions,
DestroyOptions as SequelizeDestroyOptions,
CreateOptions as SequelizeCreateOptions,
UpdateOptions as SequelizeUpdateOptions,
} from 'sequelize';
import { flatten } from 'flat';
import { Collection } from './collection';
import _ from 'lodash';
import { Database } from './database';
import { updateAssociations } from './update-associations';
import { RelationField } from './fields';
export interface IRepository {}
interface FindAllOptions extends FindOptions {
interface CreateManyOptions extends BulkCreateOptions {}
interface FindManyOptions extends FindOptions {
filter?: any;
fields?: any;
appends?: any;
expect?: any;
page?: any;
pageSize?: any;
sort?: any;
@ -24,23 +33,152 @@ interface FindAllOptions extends FindOptions {
interface FindOneOptions extends FindOptions {
filter?: any;
fields?: any;
appends?: any;
expect?: any;
sort?: any;
}
export class Repository implements IRepository {
collection: Collection;
interface CreateOptions extends SequelizeCreateOptions {
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;
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) {
this.database = collection.context.database;
this.collection = collection;
this.model = collection.model;
}
async findAll(options?: FindAllOptions) {
async findMany(options?: FindManyOptions) {
const model = this.collection.model;
const opts = {
subQuery: false,
...this.parseApiJson(options),
...this.buildQueryOptions(options),
};
let rows = [];
if (opts.include) {
@ -52,6 +190,7 @@ export class Repository implements IRepository {
group: `${model.name}.${model.primaryKeyAttribute}`,
})
).map((item) => item[model.primaryKeyAttribute]);
if (ids.length > 0) {
rows = await model.findAll({
...opts,
where: {
@ -60,6 +199,7 @@ export class Repository implements IRepository {
},
},
});
}
} else {
rows = await model.findAll({
...opts,
@ -73,19 +213,44 @@ export class Repository implements IRepository {
}
async findOne(options?: FindOneOptions) {
const opts = this.parseApiJson(options);
console.log({ opts });
const data = await this.collection.model.findOne(opts);
const model = this.collection.model;
const 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;
}
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() {}
destroy() {}
async bulkCreate(records: any[], options?: BulkCreateOptions) {
async createMany(records: any[], options?: CreateManyOptions) {
const instances = await this.collection.model.bulkCreate(records, options);
const promises = instances.map((instance, index) => {
return updateAssociations(instance, records[index]);
@ -93,9 +258,97 @@ export class Repository implements IRepository {
return Promise.all(promises);
}
parseApiJson(options: any) {
const filter = options.filter || {};
async update(values: any, options: Identity | Model | UpdateOptions) {
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;
if (typeof filter === 'number' || typeof filter === 'string') {
return {
where: {
[model.primaryKeyAttribute]: filter,
},
};
}
const operators = this.database.operators;
const obj = flatten(filter || {});
const include = {};
@ -145,6 +398,7 @@ export class Repository implements IRepository {
associationKeys.push(k);
_.set(include, k, {
association: k,
attributes: [],
});
let target = associations[k].target;
while (target) {
@ -163,6 +417,7 @@ export class Repository implements IRepository {
});
_.set(include, assoc, {
association: attr,
attributes: [],
});
target = target.associations[attr].target;
}
@ -193,7 +448,6 @@ export class Repository implements IRepository {
return item;
});
};
console.log(JSON.stringify({ include: toInclude(include) }, null, 2));
return { ...options, where, include: toInclude(include) };
return { 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(
model: Model,
instance: Model,
values: any,
options: any = {},
) {
const { transaction = await model.sequelize.transaction() } = options;
const { transaction = await instance.sequelize.transaction() } = options;
// @ts-ignore
for (const key of Object.keys(model.constructor.associations)) {
for (const key of Object.keys(instance.constructor.associations)) {
// 如果 key 不存在才跳过
if (!Object.keys(values).includes(key)) {
if (!Object.keys(values||{}).includes(key)) {
continue;
}
await updateAssociation(model, key, values[key], {
await updateAssociation(instance, key, values[key], {
...options,
transaction,
});
@ -38,23 +38,23 @@ export async function updateAssociations(
}
export async function updateAssociation(
model: Model,
instance: Model,
key: string,
value: any,
options: any = {},
) {
// @ts-ignore
const association = model.constructor.associations[key] as Association;
const association = instance.constructor.associations[key] as Association;
if (!association) {
return false;
}
switch (association.associationType) {
case 'HasOne':
case 'BelongsTo':
return updateSingleAssociation(model, key, value, options);
return updateSingleAssociation(instance, key, value, options);
case 'HasMany':
case 'BelongsToMany':
return updateMultipleAssociation(model, key, value, options);
return updateMultipleAssociation(instance, key, value, options);
}
}
@ -77,39 +77,63 @@ export async function updateSingleAssociation(
// @ts-ignore
const setAccessor = association.accessors.set;
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)) {
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
const createAccessor = association.accessors.create;
let key: string;
let dataKey: string;
let M: ModelCtor<Model>;
if (association.associationType === 'BelongsTo') {
// @ts-ignore
key = association.targetKey;
M = association.target;
} else {
// @ts-ignore
key = association.sourceKey;
dataKey = association.targetKey;
} else {
M = association.source;
dataKey = M.primaryKeyAttribute;
}
if (isStringOrNumber(value)) {
if (isStringOrNumber(value[dataKey])) {
let instance: any = await M.findOne({
where: {
[key]: value[key],
[dataKey]: value[dataKey],
},
transaction,
});
if (!instance) {
instance = await M.create(value, { transaction });
}
await model[setAccessor](value[key]);
if (instance) {
await model[setAccessor](instance);
await updateAssociations(instance, value, { transaction, ...options });
} else {
model.setDataValue(key, instance);
if (!options.transaction) {
await transaction.commit();
}
return true;
}
}
const instance = await model[createAccessor](value, { transaction });
await updateAssociations(instance, value, { transaction, ...options });
model.setDataValue(key, instance);
// @ts-ignore
if (association.targetKey) {
model.setDataValue(association.foreignKey, instance[dataKey]);
}
if (!options.transaction) {
await transaction.commit();
@ -143,10 +167,13 @@ export async function updateMultipleAssociation(
// @ts-ignore
const createAccessor = association.accessors.create;
if (isUndefinedOrNull(value)) {
return await model[setAccessor](null, { transaction });
await model[setAccessor](null, { transaction });
model.setDataValue(key, null);
return;
}
if (isStringOrNumber(value)) {
return await model[setAccessor](value, { transaction });
await model[setAccessor](value, { transaction });
return;
}
if (!Array.isArray(value)) {
value = [value];
@ -167,21 +194,24 @@ export async function updateMultipleAssociation(
list2.push(item);
}
}
console.log('updateMultipleAssociation', list1, list2);
await model[setAccessor](list1, { transaction });
const list3 = [];
for (const item of list2) {
const pk = association.target.primaryKeyAttribute;
if (isUndefinedOrNull(item[pk])) {
const instance = await model[createAccessor](item, { transaction });
await updateAssociations(instance, item, { transaction, ...options });
list3.push(instance);
} else {
const instance = await association.target.findByPk(item[pk], { transaction });
// @ts-ignore
const addAccessor = association.accessors.add;
await model[addAccessor](item[pk], { transaction });
await updateAssociations(instance, item, { transaction, ...options });
list3.push(instance);
}
}
model.setDataValue(key, list1.concat(list3));
if (!options.transaction) {
await transaction.commit();
}