diff --git a/packages/database/src/__tests__/index.ts b/packages/database/src/__tests__/index.ts index 4c8753191..f1843a43f 100644 --- a/packages/database/src/__tests__/index.ts +++ b/packages/database/src/__tests__/index.ts @@ -4,7 +4,9 @@ import { Dialect } from 'sequelize'; function getTestKey() { const { id } = require.main; const key = id - .replace(process.env.PWD, '') + // .replace(process.env.PWD, '') + .replace(`${process.env.PWD}/packages`, '') + .replace(/src\/__tests__/g, '') .replace('.test.ts', '') .replace(/[^\w]/g, '_'); return key @@ -32,6 +34,9 @@ export function getDatabase() { ...config, sync: { force: true, + alter: { + drop: true, + } }, hooks: { beforeDefine(columns, model) { diff --git a/packages/database/src/__tests__/model/updateAssociations.test.ts b/packages/database/src/__tests__/model/updateAssociations.test.ts new file mode 100644 index 000000000..21a1e6690 --- /dev/null +++ b/packages/database/src/__tests__/model/updateAssociations.test.ts @@ -0,0 +1,107 @@ +import { getDatabase } from '..'; +import Database from '../..'; + +let db: Database; + +beforeEach(async () => { + db = getDatabase(); + db.table({ + name: 'users', + fields: [ + { + type: 'hasMany', + name: 'posts', + }, + { + type: 'hasOne', + name: 'profile', + } + ], + }); + db.table({ + name: 'profiles', + }); + db.table({ + name: 'posts', + fields: [ + { + type: 'hasMany', + name: 'comments', + }, + { + type: 'belongsTo', + name: 'user', + }, + { + type: 'belongsToMany', + name: 'tags', + } + ], + }); + db.table({ + name: 'tags', + fields: [ + { + type: 'belongsToMany', + name: 'posts', + } + ] + }); + db.table({ + name: 'comments', + fields: [ + { + type: 'belongsTo', + name: 'post', + }, + ] + }); + await db.sync(); +}); + +afterEach(async () => { + await db.close(); +}); + +describe('updateAssociations', () => { + describe('set null', () => { + it('belongsTo', async () => { + const [User, Post] = db.getModels(['users', 'posts']); + const user = await User.create(); + const post = await Post.create(); + await post.updateAssociations({ + user, + }); + await post.updateAssociations({}); + const postUser = await post.getUser(); + expect(postUser.id).toBe(user.id); + await post.updateAssociations({ + user: null, + }); + expect(await post.getUser()).toBeNull(); + }); + + it('hasMany', async () => { + const [User, Post] = db.getModels(['users', 'posts']); + const user = await User.create(); + const post = await Post.create(); + await user.updateAssociations({ + posts: [post], + }); + expect(await user.countPosts()).toBe(1); + await user.updateAssociations({ + posts: [], // 空数组 + }); + expect(await user.countPosts()).toBe(0); + await user.updateAssociations({ + posts: [post], + }); + expect(await user.countPosts()).toBe(1); + await user.updateAssociations({ + posts: null, + }); + expect(await user.countPosts()).toBe(0); + }); + }); +}); + diff --git a/packages/database/src/model.ts b/packages/database/src/model.ts index afdbda12c..d8de02fe4 100644 --- a/packages/database/src/model.ts +++ b/packages/database/src/model.ts @@ -137,6 +137,10 @@ export const DEFAULT_OFFSET = 0; export const DEFAULT_LIMIT = 100; export const MAX_LIMIT = 500; +export interface UpdateAssociationOptions extends SaveOptions { + context?: any; +} + /** * Model 相关 * @@ -281,7 +285,7 @@ export abstract class Model extends SequelizeModel { return where; } - async updateSingleAssociation(key: string, data: any, options: SaveOptions & { context?: any; } = {}) { + async updateSingleAssociation(key: string, data: any, options: UpdateAssociationOptions = {}) { const { fields, transaction = await this.sequelize.transaction(), @@ -293,6 +297,11 @@ export abstract class Model extends SequelizeModel { const association = table.getAssociations().get(key); const accessors = association.getAccessors(); + if (data == null) { + await this[accessors.set](null, opts); + return; + } + if (typeof data === 'number' || typeof data === 'string' || data instanceof SequelizeModel) { await this[accessors.set](data, opts); } else if (typeof data === 'object') { @@ -323,11 +332,8 @@ export abstract class Model extends SequelizeModel { } } - async updateMultipleAssociation(associationName: string, data: any, options: SaveOptions & { context?: any; } = {}) { - const items = Array.isArray(data) ? data : [data]; - if (!items.length) { - return; - } + async updateMultipleAssociation(associationName: string, data: any, options: UpdateAssociationOptions = {}) { + const items = Array.isArray(data) ? data : data == null ? [] : [data]; const { fields, @@ -339,6 +345,12 @@ export abstract class Model extends SequelizeModel { const table = this.database.getTable(this.constructor.name); const association = table.getAssociations().get(associationName); const accessors = association.getAccessors(); + + if (!items.length) { + await this[accessors.set](null, opts); + return; + } + const Target = association.getTargetModel(); // 当前表关联 target 表的外键(大部分情况与 target 表主键相同,但可以设置为不同的,要考虑) const { targetKey = Target.primaryKeyAttribute } = association.options; @@ -481,7 +493,7 @@ export abstract class Model extends SequelizeModel { } } - async updateAssociation(key: string, data: any, options: SaveOptions & { context?: any; }) { + async updateAssociation(key: string, data: any, options: UpdateAssociationOptions = {}) { const table = this.database.getTable(this.constructor.name); const association = table.getAssociations().get(key); switch (true) { @@ -497,15 +509,14 @@ export abstract class Model extends SequelizeModel { /** * 关联数据的更新 * - * TODO: 暂不支持除主键以外关联字段的更新 - * * @param data */ - async updateAssociations(data: any, options: SaveOptions & { context?: any } = {}) { + async updateAssociations(data: any, options: UpdateAssociationOptions = {}) { const { transaction = await this.sequelize.transaction() } = options; const table = this.database.getTable(this.constructor.name); for (const key of table.getAssociations().keys()) { - if (!data[key]) { + // 如果 key 不存在才跳过 + if (!Object.keys(data).includes(key)) { continue; } await this.updateAssociation(key, data[key], {