diff --git a/packages/core/database/src/__tests__/filter-match.test.ts b/packages/core/database/src/__tests__/filter-match.test.ts new file mode 100644 index 000000000..397042309 --- /dev/null +++ b/packages/core/database/src/__tests__/filter-match.test.ts @@ -0,0 +1,52 @@ +import { Database, Model } from '..'; +import { filterMatch } from '../filter-match'; +import { mockDatabase } from './index'; + +describe('filterMatch', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + }); + + afterEach(async () => { + await db.close(); + }); + + test('filter match', async () => { + const Post = db.collection({ + name: 'posts', + fields: [{ type: 'string', name: 'title' }], + }); + + await db.sync(); + + const post = await Post.repository.create({ + values: { title: 't1' }, + }); + + expect( + filterMatch(post, { + title: 't1', + }), + ).toBeTruthy(); + + expect( + filterMatch(post, { + $or: [{ title: 't1' }, { title: 't2' }], + }), + ).toBeTruthy(); + + expect( + filterMatch(post, { + $and: [{ title: 't1' }, { title: 't2' }], + }), + ).toBeFalsy(); + + expect( + filterMatch(post, { + title: 't2', + }), + ).toBeFalsy(); + }); +}); diff --git a/packages/core/database/src/field-repository/array-field-repository.ts b/packages/core/database/src/field-repository/array-field-repository.ts index 90a2c701a..5aa048930 100644 --- a/packages/core/database/src/field-repository/array-field-repository.ts +++ b/packages/core/database/src/field-repository/array-field-repository.ts @@ -36,6 +36,7 @@ export class ArrayFieldRepository { async set( options: Transactionable & { values: Array | string | number; + hooks?: boolean; }, ) { const { transaction } = options; @@ -45,7 +46,19 @@ export class ArrayFieldRepository { }); instance.set(this.fieldName, lodash.castArray(options.values)); + await instance.save({ transaction }); + + if (options.hooks !== false) { + await this.emitAfterSave(instance, options); + } + } + + protected async emitAfterSave(instance, options) { + await this.collection.db.emitAsync(`${this.collection.name}.afterSaveWithAssociations`, instance, { + ...options, + }); + instance.clearChangedWithAssociations(); } @transaction((args, transaction) => { @@ -57,6 +70,7 @@ export class ArrayFieldRepository { async toggle( options: Transactionable & { value: string | number; + hooks?: boolean; }, ) { const { transaction } = options; @@ -71,6 +85,10 @@ export class ArrayFieldRepository { : [...oldValue, options.value]; instance.set(this.fieldName, newValue); await instance.save({ transaction }); + + if (options.hooks !== false) { + await this.emitAfterSave(instance, options); + } } @transaction((args, transaction) => { @@ -82,6 +100,7 @@ export class ArrayFieldRepository { async add( options: Transactionable & { values: Array | string | number; + hooks?: boolean; }, ) { const { transaction } = options; @@ -95,6 +114,10 @@ export class ArrayFieldRepository { const newValue = [...oldValue, ...lodash.castArray(options.values)]; instance.set(this.fieldName, newValue); await instance.save({ transaction }); + + if (options.hooks !== false) { + await this.emitAfterSave(instance, options); + } } @transaction((args, transaction) => { @@ -106,6 +129,7 @@ export class ArrayFieldRepository { async remove( options: Transactionable & { values: Array | string | number; + hooks?: boolean; }, ) { const { transaction } = options; @@ -117,6 +141,10 @@ export class ArrayFieldRepository { const oldValue = instance.get(this.fieldName) || []; instance.set(this.fieldName, lodash.without(oldValue, ...lodash.castArray(options.values))); await instance.save({ transaction }); + + if (options.hooks !== false) { + await this.emitAfterSave(instance, options); + } } protected getInstance(options: Transactionable) { diff --git a/packages/core/database/src/fields/belongs-to-many-field.ts b/packages/core/database/src/fields/belongs-to-many-field.ts index dab447c7a..bf49f5cbb 100644 --- a/packages/core/database/src/fields/belongs-to-many-field.ts +++ b/packages/core/database/src/fields/belongs-to-many-field.ts @@ -63,6 +63,10 @@ export class BelongsToManyField extends RelationField { Object.defineProperty(Through.model, 'isThrough', { value: true }); } + if (!this.options.onDelete) { + this.options.onDelete = 'CASCADE'; + } + const association = collection.model.belongsToMany(Target, { constraints: false, ...omit(this.options, ['name', 'type', 'target']), diff --git a/packages/core/database/src/filter-match.ts b/packages/core/database/src/filter-match.ts new file mode 100644 index 000000000..7384e8c82 --- /dev/null +++ b/packages/core/database/src/filter-match.ts @@ -0,0 +1,49 @@ +import { filter } from 'mathjs'; + +export function filterMatch(model, where) { + if (where.filter !== undefined) { + where = filter; + } + + // Create an object that maps operator names to functions + const operatorFunctions = { + $eq: (value, condition) => value === condition, + $not: (value, condition) => !filterMatch(model, condition), + $gt: (value, condition) => value > condition, + $gte: (value, condition) => value >= condition, + $lt: (value, condition) => value < condition, + $lte: (value, condition) => value <= condition, + $ne: (value, condition) => value !== condition, + $in: (value, condition) => condition.includes(value), + $or: (model, conditions) => Object.values(conditions).some((condition) => filterMatch(model, condition)), + $and: (model, conditions) => Object.values(conditions).every((condition) => filterMatch(model, condition)), + }; + + for (const [key, value] of Object.entries(where)) { + // Check if the property value contains a logical operator + if (operatorFunctions[key] !== undefined) { + // Check if the conditions specified in the property value are satisfied + if (!operatorFunctions[key](model, value)) { + return false; + } + } else { + // Check if the property value is an object (which would contain operators) + if (typeof value === 'object') { + // Loop through each operator in the property value + for (const [operator, condition] of Object.entries(value)) { + // Check if the property value satisfies the condition + if (!operatorFunctions[operator](model[key], condition)) { + return false; + } + } + } else { + // Assume the default operator is "eq" + if (!operatorFunctions['$eq'](model[key], value)) { + return false; + } + } + } + } + + return true; +} diff --git a/packages/core/database/src/index.ts b/packages/core/database/src/index.ts index 44f473f02..ba9a5144e 100644 --- a/packages/core/database/src/index.ts +++ b/packages/core/database/src/index.ts @@ -15,4 +15,6 @@ export * from './relation-repository/multiple-relation-repository'; export * from './relation-repository/single-relation-repository'; export * from './repository'; export * from './update-associations'; +export * from './collection-importer'; +export * from './filter-match'; export * from './field-repository/array-field-repository'; diff --git a/packages/core/database/src/options-parser.ts b/packages/core/database/src/options-parser.ts index a310dc8bc..85b3c2c8e 100644 --- a/packages/core/database/src/options-parser.ts +++ b/packages/core/database/src/options-parser.ts @@ -132,7 +132,12 @@ export class OptionsParser { appends.push(field); } else { // field is model attribute, change attributes to array type - if (!Array.isArray(attributes)) attributes = []; + if (!Array.isArray(attributes)) { + attributes = []; + if (this.collection.isParent()) { + attributes.push(this.inheritFromSubQuery()); + } + } attributes.push(field); } diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index dc78545cc..426c09ae6 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -87,10 +87,11 @@ export type AssociationKeysToBeUpdate = string[]; export type Values = any; -export interface CountOptions extends Omit, Transactionable { - filter?: Filter; - context?: any; -} +export type CountOptions = Omit & + Transactionable & { + filter?: Filter; + context?: any; + } & FilterByTk; export interface FilterByTk { filterByTk?: TargetKey; @@ -225,6 +226,17 @@ export class Repository(this.collection, association); } - protected buildQueryOptions(options: any) { + public buildQueryOptions(options: any) { const parser = new OptionsParser(options, { collection: this.collection, });