mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
feat(database): add firstOrCreate and updateOrCreate methods to Relation repository (#5894)
* feat(database): add firstOrCreate and updateOrCreate methods to BelongsToManyRepository * fix: build * fix: test
This commit is contained in:
parent
edf31adac5
commit
347d43c0c5
@ -248,41 +248,66 @@ describe('belongs to many with target key', function () {
|
|||||||
expect(count).toEqual(0);
|
expect(count).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('destroy with target key and filter', async () => {
|
test('firstOrCreate', async () => {
|
||||||
const t1 = await Tag.repository.create({
|
|
||||||
values: {
|
|
||||||
name: 't1',
|
|
||||||
status: 'published',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const t2 = await Tag.repository.create({
|
|
||||||
values: {
|
|
||||||
name: 't2',
|
|
||||||
status: 'draft',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const p1 = await Post.repository.create({
|
const p1 = await Post.repository.create({
|
||||||
values: { title: 'p1' },
|
values: { title: 'p1' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.get('title') as string);
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.get('title') as string);
|
||||||
|
|
||||||
await PostTagRepository.set([t1.get('name') as string, t2.get('name') as string]);
|
// 测试基本创建
|
||||||
|
const tag1 = await PostTagRepository.firstOrCreate({
|
||||||
let [_, count] = await PostTagRepository.findAndCount();
|
filterKeys: ['name'],
|
||||||
expect(count).toEqual(2);
|
values: {
|
||||||
|
name: 't1',
|
||||||
await PostTagRepository.destroy({
|
status: 'active',
|
||||||
filterByTk: t1.get('name') as string,
|
|
||||||
filter: {
|
|
||||||
status: 'draft',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
[_, count] = await PostTagRepository.findAndCount();
|
expect(tag1.name).toEqual('t1');
|
||||||
expect(count).toEqual(2);
|
expect(tag1.status).toEqual('active');
|
||||||
|
|
||||||
|
// 测试查找已存在记录
|
||||||
|
const tag2 = await PostTagRepository.firstOrCreate({
|
||||||
|
filterKeys: ['name'],
|
||||||
|
values: {
|
||||||
|
name: 't1',
|
||||||
|
status: 'inactive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tag2.id).toEqual(tag1.id);
|
||||||
|
expect(tag2.status).toEqual('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateOrCreate', async () => {
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: { title: 'p1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.get('title') as string);
|
||||||
|
|
||||||
|
const tag1 = await PostTagRepository.updateOrCreate({
|
||||||
|
filterKeys: ['name'],
|
||||||
|
values: {
|
||||||
|
name: 't1',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tag1.name).toEqual('t1');
|
||||||
|
expect(tag1.status).toEqual('active');
|
||||||
|
|
||||||
|
const tag2 = await PostTagRepository.updateOrCreate({
|
||||||
|
filterKeys: ['name'],
|
||||||
|
values: {
|
||||||
|
name: 't1',
|
||||||
|
status: 'inactive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tag2.id).toEqual(tag1.id);
|
||||||
|
expect(tag2.status).toEqual('inactive');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,138 @@
|
|||||||
|
import { Collection } from '@nocobase/database';
|
||||||
|
import Database from '../../database';
|
||||||
|
import { BelongsToRepository } from '../../relation-repository/belongs-to-repository';
|
||||||
|
import { mockDatabase } from '../index';
|
||||||
|
|
||||||
|
describe('belongs to repository', () => {
|
||||||
|
let db: Database;
|
||||||
|
let User: Collection;
|
||||||
|
let Post: Collection;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
await db.clean({ drop: true });
|
||||||
|
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'string', name: 'status' },
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'belongsTo', name: 'user' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('firstOrCreate', async () => {
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: { title: 'p1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostUserRepository = new BelongsToRepository(Post, 'user', p1.id);
|
||||||
|
|
||||||
|
// 测试基本创建
|
||||||
|
const user1 = await PostUserRepository.firstOrCreate({
|
||||||
|
filterKeys: ['name'],
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user1.name).toEqual('u1');
|
||||||
|
expect(user1.status).toEqual('active');
|
||||||
|
|
||||||
|
// 验证关联是否建立
|
||||||
|
await p1.reload();
|
||||||
|
expect(p1.userId).toEqual(user1.id);
|
||||||
|
|
||||||
|
// 测试查找已存在记录
|
||||||
|
const user2 = await PostUserRepository.firstOrCreate({
|
||||||
|
filterKeys: ['name'],
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
status: 'inactive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user2.id).toEqual(user1.id);
|
||||||
|
expect(user2.status).toEqual('active');
|
||||||
|
|
||||||
|
// 测试多个 filterKeys
|
||||||
|
const user3 = await PostUserRepository.firstOrCreate({
|
||||||
|
filterKeys: ['name', 'status'],
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
status: 'draft',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user3.id).not.toEqual(user1.id);
|
||||||
|
expect(user3.status).toEqual('draft');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateOrCreate', async () => {
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: { title: 'p1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostUserRepository = new BelongsToRepository(Post, 'user', p1.id);
|
||||||
|
|
||||||
|
// 测试基本创建
|
||||||
|
const user1 = await PostUserRepository.updateOrCreate({
|
||||||
|
filterKeys: ['name'],
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user1.name).toEqual('u1');
|
||||||
|
expect(user1.status).toEqual('active');
|
||||||
|
|
||||||
|
// 验证关联是否建立
|
||||||
|
await p1.reload();
|
||||||
|
expect(p1.userId).toEqual(user1.id);
|
||||||
|
|
||||||
|
// 测试更新已存在记录
|
||||||
|
const user2 = await PostUserRepository.updateOrCreate({
|
||||||
|
filterKeys: ['name'],
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
status: 'inactive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user2.id).toEqual(user1.id);
|
||||||
|
expect(user2.status).toEqual('inactive');
|
||||||
|
|
||||||
|
// 测试多个 filterKeys 的创建
|
||||||
|
const user3 = await PostUserRepository.updateOrCreate({
|
||||||
|
filterKeys: ['name', 'status'],
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
status: 'draft',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user3.id).not.toEqual(user1.id);
|
||||||
|
expect(user3.status).toEqual('draft');
|
||||||
|
|
||||||
|
// 验证关联是否更新
|
||||||
|
await p1.reload();
|
||||||
|
expect(p1.userId).toEqual(user3.id);
|
||||||
|
});
|
||||||
|
});
|
@ -149,6 +149,7 @@ describe('has many repository', () => {
|
|||||||
fields: [
|
fields: [
|
||||||
{ type: 'string', name: 'name' },
|
{ type: 'string', name: 'name' },
|
||||||
{ type: 'hasMany', name: 'posts' },
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
{ type: 'string', name: 'status' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -260,6 +261,118 @@ describe('has many repository', () => {
|
|||||||
expect(p1.title).toEqual('u1t1');
|
expect(p1.title).toEqual('u1t1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('firstOrCreate', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: { name: 'u1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserPostRepository = new HasManyRepository(User, 'posts', u1.id);
|
||||||
|
|
||||||
|
// 测试基本创建
|
||||||
|
const post1 = await UserPostRepository.firstOrCreate({
|
||||||
|
filterKeys: ['title'],
|
||||||
|
values: {
|
||||||
|
title: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post1.title).toEqual('t1');
|
||||||
|
expect(post1.userId).toEqual(u1.id);
|
||||||
|
|
||||||
|
// 测试查找已存在记录
|
||||||
|
const post2 = await UserPostRepository.firstOrCreate({
|
||||||
|
filterKeys: ['title'],
|
||||||
|
values: {
|
||||||
|
title: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post2.id).toEqual(post1.id);
|
||||||
|
|
||||||
|
// 测试带关联数据的创建
|
||||||
|
const post3 = await UserPostRepository.firstOrCreate({
|
||||||
|
filterKeys: ['title'],
|
||||||
|
values: {
|
||||||
|
title: 't2',
|
||||||
|
comments: [{ content: 'comment1' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post3.title).toEqual('t2');
|
||||||
|
expect(await post3.countComments()).toEqual(1);
|
||||||
|
|
||||||
|
// 测试多个 filterKeys
|
||||||
|
const post4 = await UserPostRepository.firstOrCreate({
|
||||||
|
filterKeys: ['title', 'status'],
|
||||||
|
values: {
|
||||||
|
title: 't2',
|
||||||
|
status: 'draft',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post4.id).not.toEqual(post3.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateOrCreate', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: { name: 'u1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserPostRepository = new HasManyRepository(User, 'posts', u1.id);
|
||||||
|
|
||||||
|
// 测试基本创建
|
||||||
|
const post1 = await UserPostRepository.updateOrCreate({
|
||||||
|
filterKeys: ['title'],
|
||||||
|
values: {
|
||||||
|
title: 't1',
|
||||||
|
status: 'draft',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post1.title).toEqual('t1');
|
||||||
|
expect(post1.status).toEqual('draft');
|
||||||
|
expect(post1.userId).toEqual(u1.id);
|
||||||
|
|
||||||
|
// 测试更新已存在记录
|
||||||
|
const post2 = await UserPostRepository.updateOrCreate({
|
||||||
|
filterKeys: ['title'],
|
||||||
|
values: {
|
||||||
|
title: 't1',
|
||||||
|
status: 'published',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post2.id).toEqual(post1.id);
|
||||||
|
expect(post2.status).toEqual('published');
|
||||||
|
|
||||||
|
// 测试带关联数据的更新
|
||||||
|
const post3 = await UserPostRepository.updateOrCreate({
|
||||||
|
filterKeys: ['title'],
|
||||||
|
values: {
|
||||||
|
title: 't1',
|
||||||
|
status: 'archived',
|
||||||
|
comments: [{ content: 'new comment' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post3.id).toEqual(post1.id);
|
||||||
|
expect(post3.status).toEqual('archived');
|
||||||
|
expect(await post3.countComments()).toEqual(1);
|
||||||
|
|
||||||
|
// 测试多个 filterKeys 的创建
|
||||||
|
const post4 = await UserPostRepository.updateOrCreate({
|
||||||
|
filterKeys: ['title', 'status'],
|
||||||
|
values: {
|
||||||
|
title: 't1',
|
||||||
|
status: 'draft',
|
||||||
|
comments: [{ content: 'another comment' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post4.id).not.toEqual(post1.id);
|
||||||
|
expect(await post4.countComments()).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('find with has many', async () => {
|
test('find with has many', async () => {
|
||||||
const u1 = await User.repository.create({ values: { name: 'u1' } });
|
const u1 = await User.repository.create({ values: { name: 'u1' } });
|
||||||
|
|
||||||
|
@ -0,0 +1,128 @@
|
|||||||
|
import { Collection } from '@nocobase/database';
|
||||||
|
import Database from '../../database';
|
||||||
|
import { HasOneRepository } from '../../relation-repository/hasone-repository';
|
||||||
|
import { mockDatabase } from '../index';
|
||||||
|
|
||||||
|
describe('has one repository', () => {
|
||||||
|
let db: Database;
|
||||||
|
let User: Collection;
|
||||||
|
let Profile: Collection;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
await db.clean({ drop: true });
|
||||||
|
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'hasOne', name: 'profile' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Profile = db.collection({
|
||||||
|
name: 'profiles',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'avatar' },
|
||||||
|
{ type: 'string', name: 'status' },
|
||||||
|
{ type: 'belongsTo', name: 'user' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('firstOrCreate', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: { name: 'u1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserProfileRepository = new HasOneRepository(User, 'profile', u1.id);
|
||||||
|
|
||||||
|
// 测试基本创建
|
||||||
|
const profile1 = await UserProfileRepository.firstOrCreate({
|
||||||
|
filterKeys: ['avatar'],
|
||||||
|
values: {
|
||||||
|
avatar: 'avatar1.jpg',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(profile1.avatar).toEqual('avatar1.jpg');
|
||||||
|
expect(profile1.status).toEqual('active');
|
||||||
|
expect(profile1.userId).toEqual(u1.id);
|
||||||
|
|
||||||
|
// 测试查找已存在记录
|
||||||
|
const profile2 = await UserProfileRepository.firstOrCreate({
|
||||||
|
filterKeys: ['avatar'],
|
||||||
|
values: {
|
||||||
|
avatar: 'avatar1.jpg',
|
||||||
|
status: 'inactive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(profile2.id).toEqual(profile1.id);
|
||||||
|
expect(profile2.status).toEqual('active');
|
||||||
|
|
||||||
|
// 测试多个 filterKeys
|
||||||
|
const profile3 = await UserProfileRepository.firstOrCreate({
|
||||||
|
filterKeys: ['avatar', 'status'],
|
||||||
|
values: {
|
||||||
|
avatar: 'avatar1.jpg',
|
||||||
|
status: 'draft',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(profile3.id).not.toEqual(profile1.id);
|
||||||
|
expect(profile3.status).toEqual('draft');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateOrCreate', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: { name: 'u1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserProfileRepository = new HasOneRepository(User, 'profile', u1.id);
|
||||||
|
|
||||||
|
// 测试基本创建
|
||||||
|
const profile1 = await UserProfileRepository.updateOrCreate({
|
||||||
|
filterKeys: ['avatar'],
|
||||||
|
values: {
|
||||||
|
avatar: 'avatar1.jpg',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(profile1.avatar).toEqual('avatar1.jpg');
|
||||||
|
expect(profile1.status).toEqual('active');
|
||||||
|
expect(profile1.userId).toEqual(u1.id);
|
||||||
|
|
||||||
|
// 测试更新已存在记录
|
||||||
|
const profile2 = await UserProfileRepository.updateOrCreate({
|
||||||
|
filterKeys: ['avatar'],
|
||||||
|
values: {
|
||||||
|
avatar: 'avatar1.jpg',
|
||||||
|
status: 'inactive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(profile2.id).toEqual(profile1.id);
|
||||||
|
expect(profile2.status).toEqual('inactive');
|
||||||
|
|
||||||
|
// 测试多个 filterKeys 的创建
|
||||||
|
const profile3 = await UserProfileRepository.updateOrCreate({
|
||||||
|
filterKeys: ['avatar', 'status'],
|
||||||
|
values: {
|
||||||
|
avatar: 'avatar1.jpg',
|
||||||
|
status: 'draft',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(profile3.id).not.toEqual(profile1.id);
|
||||||
|
expect(profile3.status).toEqual('draft');
|
||||||
|
});
|
||||||
|
});
|
@ -13,6 +13,7 @@ import { AggregateOptions, CreateOptions, DestroyOptions, TargetKey } from '../r
|
|||||||
import { updateAssociations, updateThroughTableValue } from '../update-associations';
|
import { updateAssociations, updateThroughTableValue } from '../update-associations';
|
||||||
import { MultipleRelationRepository } from './multiple-relation-repository';
|
import { MultipleRelationRepository } from './multiple-relation-repository';
|
||||||
import { transaction } from './relation-repository';
|
import { transaction } from './relation-repository';
|
||||||
|
|
||||||
import { AssociatedOptions, PrimaryKeyWithThroughValues } from './types';
|
import { AssociatedOptions, PrimaryKeyWithThroughValues } from './types';
|
||||||
|
|
||||||
type CreateBelongsToManyOptions = CreateOptions;
|
type CreateBelongsToManyOptions = CreateOptions;
|
||||||
|
@ -8,9 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { BelongsTo } from 'sequelize';
|
import { BelongsTo } from 'sequelize';
|
||||||
import { SingleRelationFindOption, SingleRelationRepository } from './single-relation-repository';
|
import { SingleRelationRepository } from './single-relation-repository';
|
||||||
|
|
||||||
type BelongsToFindOptions = SingleRelationFindOption;
|
|
||||||
|
|
||||||
export class BelongsToRepository extends SingleRelationRepository {
|
export class BelongsToRepository extends SingleRelationRepository {
|
||||||
/**
|
/**
|
||||||
|
@ -10,8 +10,9 @@
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { HasMany, Op } from 'sequelize';
|
import { HasMany, Op } from 'sequelize';
|
||||||
import { AggregateOptions, DestroyOptions, FindOptions, TargetKey, TK } from '../repository';
|
import { AggregateOptions, DestroyOptions, FindOptions, TargetKey, TK } from '../repository';
|
||||||
import { AssociatedOptions, MultipleRelationRepository } from './multiple-relation-repository';
|
import { MultipleRelationRepository } from './multiple-relation-repository';
|
||||||
import { transaction } from './relation-repository';
|
import { transaction } from './relation-repository';
|
||||||
|
import { AssociatedOptions } from './types';
|
||||||
|
|
||||||
export class HasManyRepository extends MultipleRelationRepository {
|
export class HasManyRepository extends MultipleRelationRepository {
|
||||||
async find(options?: FindOptions): Promise<any> {
|
async find(options?: FindOptions): Promise<any> {
|
||||||
|
@ -8,28 +8,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import { HasOne, MultiAssociationAccessors, Sequelize, Transaction, Transactionable } from 'sequelize';
|
import { HasOne, MultiAssociationAccessors, Sequelize, Transaction } from 'sequelize';
|
||||||
import injectTargetCollection from '../decorators/target-collection-decorator';
|
import injectTargetCollection from '../decorators/target-collection-decorator';
|
||||||
import {
|
|
||||||
CommonFindOptions,
|
|
||||||
CountOptions,
|
|
||||||
DestroyOptions,
|
|
||||||
Filter,
|
|
||||||
FindOneOptions,
|
|
||||||
FindOptions,
|
|
||||||
TargetKey,
|
|
||||||
TK,
|
|
||||||
UpdateOptions,
|
|
||||||
} from '../repository';
|
|
||||||
import { updateModelByValues } from '../update-associations';
|
import { updateModelByValues } from '../update-associations';
|
||||||
import { UpdateGuard } from '../update-guard';
|
import { UpdateGuard } from '../update-guard';
|
||||||
import { RelationRepository, transaction } from './relation-repository';
|
import { RelationRepository, transaction } from './relation-repository';
|
||||||
|
import {
|
||||||
type FindAndCountOptions = CommonFindOptions;
|
AssociatedOptions,
|
||||||
|
CountOptions,
|
||||||
export interface AssociatedOptions extends Transactionable {
|
DestroyOptions,
|
||||||
tk?: TK;
|
Filter,
|
||||||
}
|
FindOptions,
|
||||||
|
TargetKey,
|
||||||
|
UpdateOptions,
|
||||||
|
FirstOrCreateOptions,
|
||||||
|
} from './types';
|
||||||
|
import { valuesToFilter } from '../utils/filter-utils';
|
||||||
|
|
||||||
export abstract class MultipleRelationRepository extends RelationRepository {
|
export abstract class MultipleRelationRepository extends RelationRepository {
|
||||||
async targetRepositoryFilterOptionsBySourceValue(): Promise<any> {
|
async targetRepositoryFilterOptionsBySourceValue(): Promise<any> {
|
||||||
@ -73,7 +67,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]> {
|
async findAndCount(options?: FindOptions): Promise<[any[], number]> {
|
||||||
const transaction = await this.getTransaction(options, false);
|
const transaction = await this.getTransaction(options, false);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -121,7 +115,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
|||||||
return parseInt(count.count);
|
return parseInt(count.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(options?: FindOneOptions): Promise<any> {
|
async findOne(options?: FindOptions): Promise<any> {
|
||||||
const transaction = await this.getTransaction(options, false);
|
const transaction = await this.getTransaction(options, false);
|
||||||
const rows = await this.find({ ...options, limit: 1, transaction });
|
const rows = await this.find({ ...options, limit: 1, transaction });
|
||||||
return rows.length == 1 ? rows[0] : null;
|
return rows.length == 1 ? rows[0] : null;
|
||||||
@ -181,7 +175,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
|||||||
return instances;
|
return instances;
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy(options?: TK | DestroyOptions): Promise<boolean> {
|
async destroy(options?: TargetKey | DestroyOptions): Promise<boolean> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,4 +199,10 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
|||||||
protected accessors() {
|
protected accessors() {
|
||||||
return <MultiAssociationAccessors>super.accessors();
|
return <MultiAssociationAccessors>super.accessors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@transaction()
|
||||||
|
async updateOrCreate(options: FirstOrCreateOptions) {
|
||||||
|
const result = await super.updateOrCreate(options);
|
||||||
|
return Array.isArray(result) ? result[0] : result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,10 @@ import { RelationField } from '../fields/relation-field';
|
|||||||
import FilterParser from '../filter-parser';
|
import FilterParser from '../filter-parser';
|
||||||
import { Model } from '../model';
|
import { Model } from '../model';
|
||||||
import { OptionsParser } from '../options-parser';
|
import { OptionsParser } from '../options-parser';
|
||||||
import { CreateOptions, Filter, FindOptions, TargetKey } from '../repository';
|
|
||||||
import { updateAssociations } from '../update-associations';
|
import { updateAssociations } from '../update-associations';
|
||||||
import { UpdateGuard } from '../update-guard';
|
import { UpdateGuard } from '../update-guard';
|
||||||
|
import { CreateOptions, Filter, FindOptions, TargetKey, UpdateOptions, FirstOrCreateOptions } from './types';
|
||||||
|
import { valuesToFilter } from '../utils/filter-utils';
|
||||||
|
|
||||||
export const transaction = transactionWrapperBuilder(function () {
|
export const transaction = transactionWrapperBuilder(function () {
|
||||||
return this.sourceCollection.model.sequelize.transaction();
|
return this.sourceCollection.model.sequelize.transaction();
|
||||||
@ -76,6 +77,8 @@ export abstract class RelationRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract find(options?: FindOptions): Promise<any>;
|
abstract find(options?: FindOptions): Promise<any>;
|
||||||
|
abstract findOne(options?: FindOptions): Promise<any>;
|
||||||
|
abstract update(options: UpdateOptions): Promise<any>;
|
||||||
|
|
||||||
async chunk(
|
async chunk(
|
||||||
options: FindOptions & { chunkSize: number; callback: (rows: Model[], options: FindOptions) => Promise<void> },
|
options: FindOptions & { chunkSize: number; callback: (rows: Model[], options: FindOptions) => Promise<void> },
|
||||||
@ -138,6 +141,41 @@ export abstract class RelationRepository {
|
|||||||
return this.associationField.targetKey;
|
return this.associationField.targetKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@transaction()
|
||||||
|
async firstOrCreate(options: FirstOrCreateOptions) {
|
||||||
|
const { filterKeys, values, transaction, hooks } = options;
|
||||||
|
const filter = valuesToFilter(values, filterKeys);
|
||||||
|
|
||||||
|
const instance = await this.findOne({ filter, transaction });
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.create({ values, transaction, hooks });
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction()
|
||||||
|
async updateOrCreate(options: FirstOrCreateOptions) {
|
||||||
|
const { filterKeys, values, transaction, hooks } = options;
|
||||||
|
const filter = valuesToFilter(values, filterKeys);
|
||||||
|
|
||||||
|
const instance = await this.findOne({ filter, transaction });
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
return await this.update({
|
||||||
|
filterByTk: instance.get(
|
||||||
|
this.targetCollection.filterTargetKey || this.targetCollection.model.primaryKeyAttribute,
|
||||||
|
),
|
||||||
|
values,
|
||||||
|
transaction,
|
||||||
|
hooks,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.create({ values, transaction, hooks });
|
||||||
|
}
|
||||||
|
|
||||||
@transaction()
|
@transaction()
|
||||||
async create(options?: CreateOptions): Promise<any> {
|
async create(options?: CreateOptions): Promise<any> {
|
||||||
if (Array.isArray(options.values)) {
|
if (Array.isArray(options.values)) {
|
||||||
|
@ -7,22 +7,13 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import lodash from 'lodash';
|
|
||||||
import { SingleAssociationAccessors, Transactionable } from 'sequelize';
|
import { SingleAssociationAccessors, Transactionable } from 'sequelize';
|
||||||
import injectTargetCollection from '../decorators/target-collection-decorator';
|
import injectTargetCollection from '../decorators/target-collection-decorator';
|
||||||
import { Model } from '../model';
|
import { Model } from '../model';
|
||||||
import { Appends, Except, Fields, Filter, TargetKey, UpdateOptions } from '../repository';
|
import { FindOptions, TargetKey, UpdateOptions } from './types';
|
||||||
import { updateModelByValues } from '../update-associations';
|
import { updateModelByValues } from '../update-associations';
|
||||||
import { RelationRepository, transaction } from './relation-repository';
|
import { RelationRepository, transaction } from './relation-repository';
|
||||||
|
|
||||||
export interface SingleRelationFindOption extends Transactionable {
|
|
||||||
fields?: Fields;
|
|
||||||
except?: Except;
|
|
||||||
appends?: Appends;
|
|
||||||
filter?: Filter;
|
|
||||||
targetCollection?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetOption extends Transactionable {
|
interface SetOption extends Transactionable {
|
||||||
tk?: TargetKey;
|
tk?: TargetKey;
|
||||||
}
|
}
|
||||||
@ -55,7 +46,7 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async find(options?: SingleRelationFindOption): Promise<any> {
|
async find(options?: FindOptions): Promise<any> {
|
||||||
const targetRepository = this.targetCollection.repository;
|
const targetRepository = this.targetCollection.repository;
|
||||||
|
|
||||||
const sourceModel = await this.getSourceModel(await this.getTransaction(options));
|
const sourceModel = await this.getSourceModel(await this.getTransaction(options));
|
||||||
@ -74,7 +65,7 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
|||||||
return await targetRepository.findOne(findOptions);
|
return await targetRepository.findOne(findOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(options?: SingleRelationFindOption): Promise<Model<any>> {
|
async findOne(options?: FindOptions): Promise<Model<any>> {
|
||||||
return this.find({ ...options, filterByTk: null } as any);
|
return this.find({ ...options, filterByTk: null } as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +91,7 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
|||||||
|
|
||||||
const target = await this.find({
|
const target = await this.find({
|
||||||
transaction,
|
transaction,
|
||||||
|
// @ts-ignore
|
||||||
targetCollection: options.targetCollection,
|
targetCollection: options.targetCollection,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,18 +7,96 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TargetKey, Values } from '../repository';
|
import { Transaction } from 'sequelize';
|
||||||
import { Transactionable } from 'sequelize';
|
import {
|
||||||
|
CreateOptions as SequelizeCreateOptions,
|
||||||
|
UpdateOptions as SequelizeUpdateOptions,
|
||||||
|
} from 'sequelize/types/model';
|
||||||
|
import { AssociationKeysToBeUpdate, BlackList, Values, WhiteList } from '@nocobase/database';
|
||||||
|
|
||||||
export type PrimaryKeyWithThroughValues = [TargetKey, Values];
|
export type TargetKey = string | number | { [key: string]: any };
|
||||||
|
|
||||||
export interface AssociatedOptions extends Transactionable {
|
export interface Filter {
|
||||||
tk?: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[];
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type setAssociationOptions =
|
export interface Appends {
|
||||||
| TargetKey
|
[key: string]: true | Appends;
|
||||||
| TargetKey[]
|
}
|
||||||
| PrimaryKeyWithThroughValues
|
|
||||||
| PrimaryKeyWithThroughValues[]
|
export interface Except {
|
||||||
| AssociatedOptions;
|
[key: string]: true | Except;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommonOptions {
|
||||||
|
transaction?: Transaction;
|
||||||
|
context?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FindOptions extends CommonOptions {
|
||||||
|
filter?: Filter;
|
||||||
|
filterByTk?: TargetKey;
|
||||||
|
fields?: string[];
|
||||||
|
appends?: string[];
|
||||||
|
except?: string[];
|
||||||
|
sort?: string[];
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
raw?: boolean;
|
||||||
|
targetCollection?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CountOptions extends CommonOptions {
|
||||||
|
filter?: Filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateOptions extends SequelizeCreateOptions {
|
||||||
|
values?: Values | Values[];
|
||||||
|
whitelist?: WhiteList;
|
||||||
|
blacklist?: BlackList;
|
||||||
|
updateAssociationValues?: AssociationKeysToBeUpdate;
|
||||||
|
context?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateOptions extends Omit<SequelizeUpdateOptions, 'where'> {
|
||||||
|
values: Values;
|
||||||
|
filter?: Filter;
|
||||||
|
filterByTk?: TargetKey;
|
||||||
|
whitelist?: WhiteList;
|
||||||
|
blacklist?: BlackList;
|
||||||
|
updateAssociationValues?: AssociationKeysToBeUpdate;
|
||||||
|
targetCollection?: string;
|
||||||
|
context?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DestroyOptions extends CommonOptions {
|
||||||
|
filter?: Filter;
|
||||||
|
filterByTk?: TargetKey;
|
||||||
|
truncate?: boolean;
|
||||||
|
context?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirstOrCreateOptions extends CommonOptions {
|
||||||
|
filterKeys: string[];
|
||||||
|
values: any;
|
||||||
|
hooks?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThroughValues {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssociatedOptions extends CommonOptions {
|
||||||
|
tk?: TargetKey | TargetKey[];
|
||||||
|
transaction?: Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrimaryKeyWithThroughValues {
|
||||||
|
pk: any;
|
||||||
|
throughValues?: ThroughValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleOptions extends CommonOptions {
|
||||||
|
tk?: TargetKey;
|
||||||
|
transaction?: Transaction;
|
||||||
|
}
|
||||||
|
@ -46,6 +46,7 @@ import { RelationRepository } from './relation-repository/relation-repository';
|
|||||||
import { updateAssociations, updateModelByValues } from './update-associations';
|
import { updateAssociations, updateModelByValues } from './update-associations';
|
||||||
import { UpdateGuard } from './update-guard';
|
import { UpdateGuard } from './update-guard';
|
||||||
import { BelongsToArrayRepository } from './relation-repository/belongs-to-array-repository';
|
import { BelongsToArrayRepository } from './relation-repository/belongs-to-array-repository';
|
||||||
|
import { valuesToFilter } from './utils/filter-utils';
|
||||||
|
|
||||||
const debug = require('debug')('noco-database');
|
const debug = require('debug')('noco-database');
|
||||||
|
|
||||||
@ -234,7 +235,7 @@ export interface AggregateOptions {
|
|||||||
distinct?: boolean;
|
distinct?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FirstOrCreateOptions extends Transactionable {
|
export interface FirstOrCreateOptions extends Transactionable {
|
||||||
filterKeys: string[];
|
filterKeys: string[];
|
||||||
values?: Values;
|
values?: Values;
|
||||||
hooks?: boolean;
|
hooks?: boolean;
|
||||||
@ -251,53 +252,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
this.model = collection.model;
|
this.model = collection.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static valuesToFilter(values: Values = {}, filterKeys: Array<string>) {
|
public static valuesToFilter = valuesToFilter;
|
||||||
const removeArrayIndexInKey = (key) => {
|
|
||||||
const chunks = key.split('.');
|
|
||||||
return chunks
|
|
||||||
.filter((chunk) => {
|
|
||||||
return !chunk.match(/\d+/);
|
|
||||||
})
|
|
||||||
.join('.');
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterAnd = [];
|
|
||||||
const flattedValues = flatten(values);
|
|
||||||
const flattedValuesObject = {};
|
|
||||||
|
|
||||||
for (const key in flattedValues) {
|
|
||||||
const keyWithoutArrayIndex = removeArrayIndexInKey(key);
|
|
||||||
if (flattedValuesObject[keyWithoutArrayIndex]) {
|
|
||||||
if (!Array.isArray(flattedValuesObject[keyWithoutArrayIndex])) {
|
|
||||||
flattedValuesObject[keyWithoutArrayIndex] = [flattedValuesObject[keyWithoutArrayIndex]];
|
|
||||||
}
|
|
||||||
|
|
||||||
flattedValuesObject[keyWithoutArrayIndex].push(flattedValues[key]);
|
|
||||||
} else {
|
|
||||||
flattedValuesObject[keyWithoutArrayIndex] = [flattedValues[key]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const filterKey of filterKeys) {
|
|
||||||
const filterValue = flattedValuesObject[filterKey]
|
|
||||||
? flattedValuesObject[filterKey]
|
|
||||||
: lodash.get(values, filterKey);
|
|
||||||
|
|
||||||
if (filterValue) {
|
|
||||||
filterAnd.push({
|
|
||||||
[filterKey]: filterValue,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
filterAnd.push({
|
|
||||||
[filterKey]: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
$and: filterAnd,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return count by filter
|
* return count by filter
|
||||||
|
59
packages/core/database/src/utils/filter-utils.ts
Normal file
59
packages/core/database/src/utils/filter-utils.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import { flatten } from 'flat';
|
||||||
|
|
||||||
|
type Values = Record<string, any>;
|
||||||
|
|
||||||
|
export function valuesToFilter(values: Values = {}, filterKeys: Array<string>) {
|
||||||
|
const removeArrayIndexInKey = (key) => {
|
||||||
|
const chunks = key.split('.');
|
||||||
|
return chunks
|
||||||
|
.filter((chunk) => {
|
||||||
|
return !chunk.match(/\d+/);
|
||||||
|
})
|
||||||
|
.join('.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterAnd = [];
|
||||||
|
const flattedValues = flatten(values);
|
||||||
|
const flattedValuesObject = {};
|
||||||
|
|
||||||
|
for (const key in flattedValues) {
|
||||||
|
const keyWithoutArrayIndex = removeArrayIndexInKey(key);
|
||||||
|
if (flattedValuesObject[keyWithoutArrayIndex]) {
|
||||||
|
if (!Array.isArray(flattedValuesObject[keyWithoutArrayIndex])) {
|
||||||
|
flattedValuesObject[keyWithoutArrayIndex] = [flattedValuesObject[keyWithoutArrayIndex]];
|
||||||
|
}
|
||||||
|
|
||||||
|
flattedValuesObject[keyWithoutArrayIndex].push(flattedValues[key]);
|
||||||
|
} else {
|
||||||
|
flattedValuesObject[keyWithoutArrayIndex] = [flattedValues[key]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const filterKey of filterKeys) {
|
||||||
|
const filterValue = flattedValuesObject[filterKey] ? flattedValuesObject[filterKey] : lodash.get(values, filterKey);
|
||||||
|
|
||||||
|
if (filterValue) {
|
||||||
|
filterAnd.push({
|
||||||
|
[filterKey]: filterValue,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
filterAnd.push({
|
||||||
|
[filterKey]: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
$and: filterAnd,
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user