feat: improve code...
This commit is contained in:
parent
4651d3dddd
commit
60bbbb5f35
@ -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()` 方法定义数据的 Schema,Schema 的类
|
||||
// 用户
|
||||
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 插入或修改 schema,collection 的核心 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
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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' },
|
||||
],
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
@ -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' },
|
||||
],
|
@ -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',
|
@ -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: {
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
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', () => {
|
||||
it.only('model', async () => {
|
||||
const User = db.collection({
|
||||
let db: Database;
|
||||
let User: Collection;
|
||||
let Post: Collection;
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
User = db.collection({
|
||||
name: 'users',
|
||||
schema: [
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'hasMany', name: 'posts' },
|
||||
],
|
||||
});
|
||||
const Post = db.collection({
|
||||
Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'title' },
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
],
|
||||
});
|
||||
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, {
|
||||
});
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
it('user.posts', async () => {
|
||||
const user1 = await User.model.create<any>({ name: 'user1' });
|
||||
await updateAssociations(user1, {
|
||||
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,
|
||||
});
|
||||
await updateAssociations(user, {
|
||||
posts: post2.id,
|
||||
console.log(JSON.stringify(user1, null, 2));
|
||||
expect(user1.toJSON()).toMatchObject({
|
||||
name: 'user1',
|
||||
});
|
||||
await updateAssociations(user, {
|
||||
posts: [post3.id],
|
||||
const post11 = await Post.model.findByPk(post1.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: {
|
||||
id: post4.id,
|
||||
title: 'post4',
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('nested', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
schema: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'hasMany', name: 'posts' },
|
||||
],
|
||||
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();
|
||||
});
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
schema: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'belongsTo', name: 'user' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
],
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
schema: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
const user = await User.model.create();
|
||||
await updateAssociations(user, {
|
||||
posts: [
|
||||
{
|
||||
title: 'post1',
|
||||
// user: {
|
||||
// name: 'user1',
|
||||
// },
|
||||
comments: [
|
||||
{
|
||||
content: 'content1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
it('nested', async () => {
|
||||
const user = await User.model.create<any>({ name: 'user1' });
|
||||
await updateAssociations(user, {
|
||||
posts: [
|
||||
{
|
||||
name: 'post1',
|
||||
comments: [
|
||||
{
|
||||
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'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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) {
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export * from './schema-field';
|
||||
export * from './field';
|
||||
export * from './string-field';
|
||||
export * from './relation-field'
|
||||
export * from './belongs-to-field'
|
@ -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;
|
||||
}
|
@ -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') {
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,14 +190,16 @@ export class Repository implements IRepository {
|
||||
group: `${model.name}.${model.primaryKeyAttribute}`,
|
||||
})
|
||||
).map((item) => item[model.primaryKeyAttribute]);
|
||||
rows = await model.findAll({
|
||||
...opts,
|
||||
where: {
|
||||
[model.primaryKeyAttribute]: {
|
||||
[Op.in]: ids,
|
||||
if (ids.length > 0) {
|
||||
rows = await model.findAll({
|
||||
...opts,
|
||||
where: {
|
||||
[model.primaryKeyAttribute]: {
|
||||
[Op.in]: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
} 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) };
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 });
|
||||
if (instance) {
|
||||
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 });
|
||||
} else {
|
||||
const instance = await model[createAccessor](value, { transaction });
|
||||
await updateAssociations(instance, value, { transaction, ...options });
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user