diff --git a/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts index 56d946286b..0e568824f0 100644 --- a/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts +++ b/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts @@ -248,41 +248,66 @@ describe('belongs to many with target key', function () { expect(count).toEqual(0); }); - test('destroy with target key and filter', async () => { - const t1 = await Tag.repository.create({ - values: { - name: 't1', - status: 'published', - }, - }); - - const t2 = await Tag.repository.create({ - values: { - name: 't2', - status: 'draft', - }, - }); - + test('firstOrCreate', async () => { const p1 = await Post.repository.create({ values: { title: 'p1' }, }); const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.get('title') as string); - await PostTagRepository.set([t1.get('name') as string, t2.get('name') as string]); - - let [_, count] = await PostTagRepository.findAndCount(); - expect(count).toEqual(2); - - await PostTagRepository.destroy({ - filterByTk: t1.get('name') as string, - filter: { - status: 'draft', + // 测试基本创建 + const tag1 = await PostTagRepository.firstOrCreate({ + filterKeys: ['name'], + values: { + name: 't1', + status: 'active', }, }); - [_, count] = await PostTagRepository.findAndCount(); - expect(count).toEqual(2); + expect(tag1.name).toEqual('t1'); + 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'); }); }); diff --git a/packages/core/database/src/__tests__/relation-repository/belongs-to-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/belongs-to-repository.test.ts new file mode 100644 index 0000000000..332967733a --- /dev/null +++ b/packages/core/database/src/__tests__/relation-repository/belongs-to-repository.test.ts @@ -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); + }); +}); diff --git a/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts index 1afe7121f5..c7b5cff062 100644 --- a/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts +++ b/packages/core/database/src/__tests__/relation-repository/has-many-repository.test.ts @@ -149,6 +149,7 @@ describe('has many repository', () => { fields: [ { type: 'string', name: 'name' }, { type: 'hasMany', name: 'posts' }, + { type: 'string', name: 'status' }, ], }); @@ -260,6 +261,118 @@ describe('has many repository', () => { 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 () => { const u1 = await User.repository.create({ values: { name: 'u1' } }); diff --git a/packages/core/database/src/__tests__/relation-repository/has-one-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/has-one-repository.test.ts new file mode 100644 index 0000000000..0b913f696f --- /dev/null +++ b/packages/core/database/src/__tests__/relation-repository/has-one-repository.test.ts @@ -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'); + }); +}); diff --git a/packages/core/database/src/relation-repository/belongs-to-many-repository.ts b/packages/core/database/src/relation-repository/belongs-to-many-repository.ts index 8b754e5c6f..eca56eafa3 100644 --- a/packages/core/database/src/relation-repository/belongs-to-many-repository.ts +++ b/packages/core/database/src/relation-repository/belongs-to-many-repository.ts @@ -13,6 +13,7 @@ import { AggregateOptions, CreateOptions, DestroyOptions, TargetKey } from '../r import { updateAssociations, updateThroughTableValue } from '../update-associations'; import { MultipleRelationRepository } from './multiple-relation-repository'; import { transaction } from './relation-repository'; + import { AssociatedOptions, PrimaryKeyWithThroughValues } from './types'; type CreateBelongsToManyOptions = CreateOptions; diff --git a/packages/core/database/src/relation-repository/belongs-to-repository.ts b/packages/core/database/src/relation-repository/belongs-to-repository.ts index fab312f93b..6cc8a5781e 100644 --- a/packages/core/database/src/relation-repository/belongs-to-repository.ts +++ b/packages/core/database/src/relation-repository/belongs-to-repository.ts @@ -8,9 +8,7 @@ */ import { BelongsTo } from 'sequelize'; -import { SingleRelationFindOption, SingleRelationRepository } from './single-relation-repository'; - -type BelongsToFindOptions = SingleRelationFindOption; +import { SingleRelationRepository } from './single-relation-repository'; export class BelongsToRepository extends SingleRelationRepository { /** diff --git a/packages/core/database/src/relation-repository/hasmany-repository.ts b/packages/core/database/src/relation-repository/hasmany-repository.ts index 8a5974f2ee..f831166d67 100644 --- a/packages/core/database/src/relation-repository/hasmany-repository.ts +++ b/packages/core/database/src/relation-repository/hasmany-repository.ts @@ -10,8 +10,9 @@ import { omit } from 'lodash'; import { HasMany, Op } from 'sequelize'; 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 { AssociatedOptions } from './types'; export class HasManyRepository extends MultipleRelationRepository { async find(options?: FindOptions): Promise { diff --git a/packages/core/database/src/relation-repository/multiple-relation-repository.ts b/packages/core/database/src/relation-repository/multiple-relation-repository.ts index 0909702fc1..af3ed91525 100644 --- a/packages/core/database/src/relation-repository/multiple-relation-repository.ts +++ b/packages/core/database/src/relation-repository/multiple-relation-repository.ts @@ -8,28 +8,22 @@ */ 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 { - CommonFindOptions, - CountOptions, - DestroyOptions, - Filter, - FindOneOptions, - FindOptions, - TargetKey, - TK, - UpdateOptions, -} from '../repository'; import { updateModelByValues } from '../update-associations'; import { UpdateGuard } from '../update-guard'; import { RelationRepository, transaction } from './relation-repository'; - -type FindAndCountOptions = CommonFindOptions; - -export interface AssociatedOptions extends Transactionable { - tk?: TK; -} +import { + AssociatedOptions, + CountOptions, + DestroyOptions, + Filter, + FindOptions, + TargetKey, + UpdateOptions, + FirstOrCreateOptions, +} from './types'; +import { valuesToFilter } from '../utils/filter-utils'; export abstract class MultipleRelationRepository extends RelationRepository { async targetRepositoryFilterOptionsBySourceValue(): Promise { @@ -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); return [ @@ -121,7 +115,7 @@ export abstract class MultipleRelationRepository extends RelationRepository { return parseInt(count.count); } - async findOne(options?: FindOneOptions): Promise { + async findOne(options?: FindOptions): Promise { const transaction = await this.getTransaction(options, false); const rows = await this.find({ ...options, limit: 1, transaction }); return rows.length == 1 ? rows[0] : null; @@ -181,7 +175,7 @@ export abstract class MultipleRelationRepository extends RelationRepository { return instances; } - async destroy(options?: TK | DestroyOptions): Promise { + async destroy(options?: TargetKey | DestroyOptions): Promise { return false; } @@ -205,4 +199,10 @@ export abstract class MultipleRelationRepository extends RelationRepository { protected accessors() { return super.accessors(); } + + @transaction() + async updateOrCreate(options: FirstOrCreateOptions) { + const result = await super.updateOrCreate(options); + return Array.isArray(result) ? result[0] : result; + } } diff --git a/packages/core/database/src/relation-repository/relation-repository.ts b/packages/core/database/src/relation-repository/relation-repository.ts index df933c0019..4306e63fbd 100644 --- a/packages/core/database/src/relation-repository/relation-repository.ts +++ b/packages/core/database/src/relation-repository/relation-repository.ts @@ -16,9 +16,10 @@ import { RelationField } from '../fields/relation-field'; import FilterParser from '../filter-parser'; import { Model } from '../model'; import { OptionsParser } from '../options-parser'; -import { CreateOptions, Filter, FindOptions, TargetKey } from '../repository'; import { updateAssociations } from '../update-associations'; 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 () { return this.sourceCollection.model.sequelize.transaction(); @@ -76,6 +77,8 @@ export abstract class RelationRepository { } abstract find(options?: FindOptions): Promise; + abstract findOne(options?: FindOptions): Promise; + abstract update(options: UpdateOptions): Promise; async chunk( options: FindOptions & { chunkSize: number; callback: (rows: Model[], options: FindOptions) => Promise }, @@ -138,6 +141,41 @@ export abstract class RelationRepository { 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() async create(options?: CreateOptions): Promise { if (Array.isArray(options.values)) { diff --git a/packages/core/database/src/relation-repository/single-relation-repository.ts b/packages/core/database/src/relation-repository/single-relation-repository.ts index 02f9c16004..710609d1b5 100644 --- a/packages/core/database/src/relation-repository/single-relation-repository.ts +++ b/packages/core/database/src/relation-repository/single-relation-repository.ts @@ -7,22 +7,13 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import lodash from 'lodash'; import { SingleAssociationAccessors, Transactionable } from 'sequelize'; import injectTargetCollection from '../decorators/target-collection-decorator'; 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 { RelationRepository, transaction } from './relation-repository'; -export interface SingleRelationFindOption extends Transactionable { - fields?: Fields; - except?: Except; - appends?: Appends; - filter?: Filter; - targetCollection?: string; -} - interface SetOption extends Transactionable { tk?: TargetKey; } @@ -55,7 +46,7 @@ export abstract class SingleRelationRepository extends RelationRepository { }); } - async find(options?: SingleRelationFindOption): Promise { + async find(options?: FindOptions): Promise { const targetRepository = this.targetCollection.repository; const sourceModel = await this.getSourceModel(await this.getTransaction(options)); @@ -74,7 +65,7 @@ export abstract class SingleRelationRepository extends RelationRepository { return await targetRepository.findOne(findOptions); } - async findOne(options?: SingleRelationFindOption): Promise> { + async findOne(options?: FindOptions): Promise> { return this.find({ ...options, filterByTk: null } as any); } @@ -100,6 +91,7 @@ export abstract class SingleRelationRepository extends RelationRepository { const target = await this.find({ transaction, + // @ts-ignore targetCollection: options.targetCollection, }); diff --git a/packages/core/database/src/relation-repository/types.ts b/packages/core/database/src/relation-repository/types.ts index 79c3d0ea63..68171db958 100644 --- a/packages/core/database/src/relation-repository/types.ts +++ b/packages/core/database/src/relation-repository/types.ts @@ -7,18 +7,96 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { TargetKey, Values } from '../repository'; -import { Transactionable } from 'sequelize'; +import { Transaction } 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 { - tk?: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[]; +export interface Filter { + [key: string]: any; } -export type setAssociationOptions = - | TargetKey - | TargetKey[] - | PrimaryKeyWithThroughValues - | PrimaryKeyWithThroughValues[] - | AssociatedOptions; +export interface Appends { + [key: string]: true | Appends; +} + +export interface Except { + [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 { + 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; +} diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index 9de0227551..1d03954c1a 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -46,6 +46,7 @@ import { RelationRepository } from './relation-repository/relation-repository'; import { updateAssociations, updateModelByValues } from './update-associations'; import { UpdateGuard } from './update-guard'; import { BelongsToArrayRepository } from './relation-repository/belongs-to-array-repository'; +import { valuesToFilter } from './utils/filter-utils'; const debug = require('debug')('noco-database'); @@ -234,7 +235,7 @@ export interface AggregateOptions { distinct?: boolean; } -interface FirstOrCreateOptions extends Transactionable { +export interface FirstOrCreateOptions extends Transactionable { filterKeys: string[]; values?: Values; hooks?: boolean; @@ -251,53 +252,7 @@ export class Repository) { - 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, - }; - } + public static valuesToFilter = valuesToFilter; /** * return count by filter diff --git a/packages/core/database/src/utils/filter-utils.ts b/packages/core/database/src/utils/filter-utils.ts new file mode 100644 index 0000000000..3dbd4ad870 --- /dev/null +++ b/packages/core/database/src/utils/filter-utils.ts @@ -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; + +export function valuesToFilter(values: Values = {}, filterKeys: Array) { + 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, + }; +}