From 60bbbb5f355183c121aee419cbe07ec146f57866 Mon Sep 17 00:00:00 2001 From: chenos Date: Mon, 27 Sep 2021 15:28:32 +0800 Subject: [PATCH] feat: improve code... --- docs/index.md | 74 ++-- .../belongs-to-field.test.ts | 22 +- .../belongs-to-many-field.test.ts | 4 +- .../has-many-field.test.ts | 28 +- .../has-one-field.test.ts | 14 +- .../sort-field.test.ts | 10 +- .../string-field.test.ts | 10 +- packages/collections/src/__tests__/index.ts | 2 +- .../src/__tests__/repository.test.ts | 312 +++++++++++++++-- .../src/__tests__/update-associations.test.ts | 323 ++++++++++++++---- packages/collections/src/collection.ts | 113 ++++-- packages/collections/src/database.ts | 62 ++-- .../belongs-to-field.ts | 7 +- .../belongs-to-many-field.ts | 0 .../{schema-fields => fields}/date-field.ts | 4 +- .../schema-field.ts => fields/field.ts} | 15 +- .../{schema-fields => fields}/float-field.ts | 4 +- .../has-many-field.ts | 17 +- .../has-one-field.ts | 2 +- .../src/{schema-fields => fields}/index.ts | 2 +- .../integer-field.ts | 4 +- .../{schema-fields => fields}/json-field.ts | 6 +- .../{schema-fields => fields}/number-field.ts | 12 +- .../relation-field.ts | 4 +- .../{schema-fields => fields}/sort-field.ts | 4 +- .../{schema-fields => fields}/string-field.ts | 4 +- .../{schema-fields => fields}/text-field.ts | 4 +- .../{schema-fields => fields}/time-field.ts | 4 +- .../virtual-field.ts | 4 +- packages/collections/src/repository.ts | 312 +++++++++++++++-- packages/collections/src/schema.ts | 83 ----- .../collections/src/update-associations.ts | 86 +++-- 32 files changed, 1149 insertions(+), 403 deletions(-) rename packages/collections/src/__tests__/{schema-fields => fields}/belongs-to-field.test.ts (92%) rename packages/collections/src/__tests__/{schema-fields => fields}/belongs-to-many-field.test.ts (97%) rename packages/collections/src/__tests__/{schema-fields => fields}/has-many-field.test.ts (88%) rename packages/collections/src/__tests__/{schema-fields => fields}/has-one-field.test.ts (83%) rename packages/collections/src/__tests__/{schema-fields => fields}/sort-field.test.ts (93%) rename packages/collections/src/__tests__/{schema-fields => fields}/string-field.test.ts (91%) rename packages/collections/src/{schema-fields => fields}/belongs-to-field.ts (92%) rename packages/collections/src/{schema-fields => fields}/belongs-to-many-field.ts (100%) rename packages/collections/src/{schema-fields => fields}/date-field.ts (50%) rename packages/collections/src/{schema-fields/schema-field.ts => fields/field.ts} (81%) rename packages/collections/src/{schema-fields => fields}/float-field.ts (50%) rename packages/collections/src/{schema-fields => fields}/has-many-field.ts (90%) rename packages/collections/src/{schema-fields => fields}/has-one-field.ts (98%) rename packages/collections/src/{schema-fields => fields}/index.ts (89%) rename packages/collections/src/{schema-fields => fields}/integer-field.ts (50%) rename packages/collections/src/{schema-fields => fields}/json-field.ts (67%) rename packages/collections/src/{schema-fields => fields}/number-field.ts (52%) rename packages/collections/src/{schema-fields => fields}/relation-field.ts (74%) rename packages/collections/src/{schema-fields => fields}/sort-field.ts (86%) rename packages/collections/src/{schema-fields => fields}/string-field.ts (50%) rename packages/collections/src/{schema-fields => fields}/text-field.ts (50%) rename packages/collections/src/{schema-fields => fields}/time-field.ts (50%) rename packages/collections/src/{schema-fields => fields}/virtual-field.ts (50%) delete mode 100644 packages/collections/src/schema.ts diff --git a/docs/index.md b/docs/index.md index 353a741c94..b5046162d6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/packages/collections/src/__tests__/schema-fields/belongs-to-field.test.ts b/packages/collections/src/__tests__/fields/belongs-to-field.test.ts similarity index 92% rename from packages/collections/src/__tests__/schema-fields/belongs-to-field.test.ts rename to packages/collections/src/__tests__/fields/belongs-to-field.test.ts index e43a6ba583..f17fe84288 100644 --- a/packages/collections/src/__tests__/schema-fields/belongs-to-field.test.ts +++ b/packages/collections/src/__tests__/fields/belongs-to-field.test.ts @@ -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(); }); }); diff --git a/packages/collections/src/__tests__/schema-fields/belongs-to-many-field.test.ts b/packages/collections/src/__tests__/fields/belongs-to-many-field.test.ts similarity index 97% rename from packages/collections/src/__tests__/schema-fields/belongs-to-many-field.test.ts rename to packages/collections/src/__tests__/fields/belongs-to-many-field.test.ts index 1e41015b04..4301610bee 100644 --- a/packages/collections/src/__tests__/schema-fields/belongs-to-many-field.test.ts +++ b/packages/collections/src/__tests__/fields/belongs-to-many-field.test.ts @@ -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' }, ], }); diff --git a/packages/collections/src/__tests__/schema-fields/has-many-field.test.ts b/packages/collections/src/__tests__/fields/has-many-field.test.ts similarity index 88% rename from packages/collections/src/__tests__/schema-fields/has-many-field.test.ts rename to packages/collections/src/__tests__/fields/has-many-field.test.ts index bdd5595d3b..40b9f9576e 100644 --- a/packages/collections/src/__tests__/schema-fields/has-many-field.test.ts +++ b/packages/collections/src/__tests__/fields/has-many-field.test.ts @@ -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(); }); }); diff --git a/packages/collections/src/__tests__/schema-fields/has-one-field.test.ts b/packages/collections/src/__tests__/fields/has-one-field.test.ts similarity index 83% rename from packages/collections/src/__tests__/schema-fields/has-one-field.test.ts rename to packages/collections/src/__tests__/fields/has-one-field.test.ts index 0bb1132ed5..146378abbe 100644 --- a/packages/collections/src/__tests__/schema-fields/has-one-field.test.ts +++ b/packages/collections/src/__tests__/fields/has-one-field.test.ts @@ -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(); }); }); diff --git a/packages/collections/src/__tests__/schema-fields/sort-field.test.ts b/packages/collections/src/__tests__/fields/sort-field.test.ts similarity index 93% rename from packages/collections/src/__tests__/schema-fields/sort-field.test.ts rename to packages/collections/src/__tests__/fields/sort-field.test.ts index e53fa543c2..6376446239 100644 --- a/packages/collections/src/__tests__/schema-fields/sort-field.test.ts +++ b/packages/collections/src/__tests__/fields/sort-field.test.ts @@ -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' }, ], diff --git a/packages/collections/src/__tests__/schema-fields/string-field.test.ts b/packages/collections/src/__tests__/fields/string-field.test.ts similarity index 91% rename from packages/collections/src/__tests__/schema-fields/string-field.test.ts rename to packages/collections/src/__tests__/fields/string-field.test.ts index acd389817b..817fffedce 100644 --- a/packages/collections/src/__tests__/schema-fields/string-field.test.ts +++ b/packages/collections/src/__tests__/fields/string-field.test.ts @@ -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', diff --git a/packages/collections/src/__tests__/index.ts b/packages/collections/src/__tests__/index.ts index 248c3791fd..bc70cdd806 100644 --- a/packages/collections/src/__tests__/index.ts +++ b/packages/collections/src/__tests__/index.ts @@ -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: { diff --git a/packages/collections/src/__tests__/repository.test.ts b/packages/collections/src/__tests__/repository.test.ts index bfd45848d4..a1d6668f1d 100644 --- a/packages/collections/src/__tests__/repository.test.ts +++ b/packages/collections/src/__tests__/repository.test.ts @@ -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({ + 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({ + 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(); + 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(); + 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', + }); + }); }); diff --git a/packages/collections/src/__tests__/update-associations.test.ts b/packages/collections/src/__tests__/update-associations.test.ts index 9c3858822d..4148c75aa0 100644 --- a/packages/collections/src/__tests__/update-associations.test.ts +++ b/packages/collections/src/__tests__/update-associations.test.ts @@ -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({ 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(); - const post3 = await Post.model.create(); - const post4 = await Post.model.create(); - await updateAssociations(user, { + }); + afterEach(async () => { + await db.close(); + }); + it('user.posts', async () => { + const user1 = await User.model.create({ 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({ 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({ name: 'user1' }); + const post1 = await Post.model.create({ 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({ name: 'user1' }); + const post1 = await Post.model.create({ 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({ name: 'user1' }); + const post1 = await Post.model.create({ 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({ name: 'user1' }); + const post1 = await Post.model.create({ name: 'post1' }); + const post2 = await Post.model.create({ name: 'post2' }); + const post3 = await Post.model.create({ 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({ 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'), + }); }); }); }); diff --git a/packages/collections/src/collection.ts b/packages/collections/src/collection.ts index e7da3051a4..cea603c3d9 100644 --- a/packages/collections/src/collection.ts +++ b/packages/collections/src/collection.ts @@ -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; - repository: Repository; +export class Collection extends EventEmitter { options: CollectionOptions; context: CollectionContext; + fields: Map; + model: ModelCtor; + 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(); this.model = class extends Model {}; 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(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() { + } } diff --git a/packages/collections/src/database.ts b/packages/collections/src/database.ts index b7b8e07d63..5aae72e00c 100644 --- a/packages/collections/src/database.ts +++ b/packages/collections/src/database.ts @@ -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); } diff --git a/packages/collections/src/schema-fields/belongs-to-field.ts b/packages/collections/src/fields/belongs-to-field.ts similarity index 92% rename from packages/collections/src/schema-fields/belongs-to-field.ts rename to packages/collections/src/fields/belongs-to-field.ts index 297a1a537b..ef095eee2d 100644 --- a/packages/collections/src/schema-fields/belongs-to-field.ts +++ b/packages/collections/src/fields/belongs-to-field.ts @@ -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) { diff --git a/packages/collections/src/schema-fields/belongs-to-many-field.ts b/packages/collections/src/fields/belongs-to-many-field.ts similarity index 100% rename from packages/collections/src/schema-fields/belongs-to-many-field.ts rename to packages/collections/src/fields/belongs-to-many-field.ts diff --git a/packages/collections/src/schema-fields/date-field.ts b/packages/collections/src/fields/date-field.ts similarity index 50% rename from packages/collections/src/schema-fields/date-field.ts rename to packages/collections/src/fields/date-field.ts index 76545666a7..5de85c9544 100644 --- a/packages/collections/src/schema-fields/date-field.ts +++ b/packages/collections/src/fields/date-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/schema-field.ts b/packages/collections/src/fields/field.ts similarity index 81% rename from packages/collections/src/schema-fields/schema-field.ts rename to packages/collections/src/fields/field.ts index 5905066c81..02502a0cbf 100644 --- a/packages/collections/src/schema-fields/schema-field.ts +++ b/packages/collections/src/fields/field.ts @@ -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(); } diff --git a/packages/collections/src/schema-fields/float-field.ts b/packages/collections/src/fields/float-field.ts similarity index 50% rename from packages/collections/src/schema-fields/float-field.ts rename to packages/collections/src/fields/float-field.ts index c4849c1f90..8f83a44f60 100644 --- a/packages/collections/src/schema-fields/float-field.ts +++ b/packages/collections/src/fields/float-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/has-many-field.ts b/packages/collections/src/fields/has-many-field.ts similarity index 90% rename from packages/collections/src/schema-fields/has-many-field.ts rename to packages/collections/src/fields/has-many-field.ts index 5507426fb7..fb9d062fc1 100644 --- a/packages/collections/src/schema-fields/has-many-field.ts +++ b/packages/collections/src/fields/has-many-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/has-one-field.ts b/packages/collections/src/fields/has-one-field.ts similarity index 98% rename from packages/collections/src/schema-fields/has-one-field.ts rename to packages/collections/src/fields/has-one-field.ts index 4863e76e59..8d21f7e7ac 100644 --- a/packages/collections/src/schema-fields/has-one-field.ts +++ b/packages/collections/src/fields/has-one-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/index.ts b/packages/collections/src/fields/index.ts similarity index 89% rename from packages/collections/src/schema-fields/index.ts rename to packages/collections/src/fields/index.ts index d9a398dc1e..7e9dab58a6 100644 --- a/packages/collections/src/schema-fields/index.ts +++ b/packages/collections/src/fields/index.ts @@ -1,4 +1,4 @@ -export * from './schema-field'; +export * from './field'; export * from './string-field'; export * from './relation-field' export * from './belongs-to-field' diff --git a/packages/collections/src/schema-fields/integer-field.ts b/packages/collections/src/fields/integer-field.ts similarity index 50% rename from packages/collections/src/schema-fields/integer-field.ts rename to packages/collections/src/fields/integer-field.ts index f87d1ecb7f..eda93e17fa 100644 --- a/packages/collections/src/schema-fields/integer-field.ts +++ b/packages/collections/src/fields/integer-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/json-field.ts b/packages/collections/src/fields/json-field.ts similarity index 67% rename from packages/collections/src/schema-fields/json-field.ts rename to packages/collections/src/fields/json-field.ts index a5a6e8efae..3641bbf7d0 100644 --- a/packages/collections/src/schema-fields/json-field.ts +++ b/packages/collections/src/fields/json-field.ts @@ -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') { diff --git a/packages/collections/src/schema-fields/number-field.ts b/packages/collections/src/fields/number-field.ts similarity index 52% rename from packages/collections/src/schema-fields/number-field.ts rename to packages/collections/src/fields/number-field.ts index 23b70ec8ad..b012d3d54c 100644 --- a/packages/collections/src/schema-fields/number-field.ts +++ b/packages/collections/src/fields/number-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/relation-field.ts b/packages/collections/src/fields/relation-field.ts similarity index 74% rename from packages/collections/src/schema-fields/relation-field.ts rename to packages/collections/src/fields/relation-field.ts index 1efde44cd6..fbefd216cc 100644 --- a/packages/collections/src/schema-fields/relation-field.ts +++ b/packages/collections/src/fields/relation-field.ts @@ -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; diff --git a/packages/collections/src/schema-fields/sort-field.ts b/packages/collections/src/fields/sort-field.ts similarity index 86% rename from packages/collections/src/schema-fields/sort-field.ts rename to packages/collections/src/fields/sort-field.ts index 2d53dfc471..a47b980374 100644 --- a/packages/collections/src/schema-fields/sort-field.ts +++ b/packages/collections/src/fields/sort-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/string-field.ts b/packages/collections/src/fields/string-field.ts similarity index 50% rename from packages/collections/src/schema-fields/string-field.ts rename to packages/collections/src/fields/string-field.ts index 9e495215a7..b29d0061d2 100644 --- a/packages/collections/src/schema-fields/string-field.ts +++ b/packages/collections/src/fields/string-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/text-field.ts b/packages/collections/src/fields/text-field.ts similarity index 50% rename from packages/collections/src/schema-fields/text-field.ts rename to packages/collections/src/fields/text-field.ts index 182d3e38b3..f049e407d7 100644 --- a/packages/collections/src/schema-fields/text-field.ts +++ b/packages/collections/src/fields/text-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/time-field.ts b/packages/collections/src/fields/time-field.ts similarity index 50% rename from packages/collections/src/schema-fields/time-field.ts rename to packages/collections/src/fields/time-field.ts index ca57886a75..9de30d5b7f 100644 --- a/packages/collections/src/schema-fields/time-field.ts +++ b/packages/collections/src/fields/time-field.ts @@ -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; } diff --git a/packages/collections/src/schema-fields/virtual-field.ts b/packages/collections/src/fields/virtual-field.ts similarity index 50% rename from packages/collections/src/schema-fields/virtual-field.ts rename to packages/collections/src/fields/virtual-field.ts index 231642be31..2a706f47ab 100644 --- a/packages/collections/src/schema-fields/virtual-field.ts +++ b/packages/collections/src/fields/virtual-field.ts @@ -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; } diff --git a/packages/collections/src/repository.ts b/packages/collections/src/repository.ts index 9647ae1b41..70c44f1d2b 100644 --- a/packages/collections/src/repository.ts +++ b/packages/collections/src/repository.ts @@ -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; 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(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) }; } } diff --git a/packages/collections/src/schema.ts b/packages/collections/src/schema.ts deleted file mode 100644 index 833b7f4615..0000000000 --- a/packages/collections/src/schema.ts +++ /dev/null @@ -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; - context: SchemaContext; - options: any; - - constructor(options?: any, context?: SchemaContext) { - super(); - this.options = options; - this.context = context; - this.fields = new Map(); - 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); - } -} diff --git a/packages/collections/src/update-associations.ts b/packages/collections/src/update-associations.ts index 9d3434374d..e5018700ad 100644 --- a/packages/collections/src/update-associations.ts +++ b/packages/collections/src/update-associations.ts @@ -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; 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(); }