fix(database): filter match (#1319)

Co-authored-by: Chareice <chareice@live.com>
This commit is contained in:
chenos 2023-01-03 13:38:20 +08:00 committed by GitHub
parent 43db2b641b
commit 9d618315ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 158 additions and 6 deletions

View File

@ -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();
});
});

View File

@ -36,6 +36,7 @@ export class ArrayFieldRepository {
async set( async set(
options: Transactionable & { options: Transactionable & {
values: Array<string | number> | string | number; values: Array<string | number> | string | number;
hooks?: boolean;
}, },
) { ) {
const { transaction } = options; const { transaction } = options;
@ -45,7 +46,19 @@ export class ArrayFieldRepository {
}); });
instance.set(this.fieldName, lodash.castArray(options.values)); instance.set(this.fieldName, lodash.castArray(options.values));
await instance.save({ transaction }); 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) => { @transaction((args, transaction) => {
@ -57,6 +70,7 @@ export class ArrayFieldRepository {
async toggle( async toggle(
options: Transactionable & { options: Transactionable & {
value: string | number; value: string | number;
hooks?: boolean;
}, },
) { ) {
const { transaction } = options; const { transaction } = options;
@ -71,6 +85,10 @@ export class ArrayFieldRepository {
: [...oldValue, options.value]; : [...oldValue, options.value];
instance.set(this.fieldName, newValue); instance.set(this.fieldName, newValue);
await instance.save({ transaction }); await instance.save({ transaction });
if (options.hooks !== false) {
await this.emitAfterSave(instance, options);
}
} }
@transaction((args, transaction) => { @transaction((args, transaction) => {
@ -82,6 +100,7 @@ export class ArrayFieldRepository {
async add( async add(
options: Transactionable & { options: Transactionable & {
values: Array<string | number> | string | number; values: Array<string | number> | string | number;
hooks?: boolean;
}, },
) { ) {
const { transaction } = options; const { transaction } = options;
@ -95,6 +114,10 @@ export class ArrayFieldRepository {
const newValue = [...oldValue, ...lodash.castArray(options.values)]; const newValue = [...oldValue, ...lodash.castArray(options.values)];
instance.set(this.fieldName, newValue); instance.set(this.fieldName, newValue);
await instance.save({ transaction }); await instance.save({ transaction });
if (options.hooks !== false) {
await this.emitAfterSave(instance, options);
}
} }
@transaction((args, transaction) => { @transaction((args, transaction) => {
@ -106,6 +129,7 @@ export class ArrayFieldRepository {
async remove( async remove(
options: Transactionable & { options: Transactionable & {
values: Array<string | number> | string | number; values: Array<string | number> | string | number;
hooks?: boolean;
}, },
) { ) {
const { transaction } = options; const { transaction } = options;
@ -117,6 +141,10 @@ export class ArrayFieldRepository {
const oldValue = instance.get(this.fieldName) || []; const oldValue = instance.get(this.fieldName) || [];
instance.set(this.fieldName, lodash.without(oldValue, ...lodash.castArray(options.values))); instance.set(this.fieldName, lodash.without(oldValue, ...lodash.castArray(options.values)));
await instance.save({ transaction }); await instance.save({ transaction });
if (options.hooks !== false) {
await this.emitAfterSave(instance, options);
}
} }
protected getInstance(options: Transactionable) { protected getInstance(options: Transactionable) {

View File

@ -63,6 +63,10 @@ export class BelongsToManyField extends RelationField {
Object.defineProperty(Through.model, 'isThrough', { value: true }); Object.defineProperty(Through.model, 'isThrough', { value: true });
} }
if (!this.options.onDelete) {
this.options.onDelete = 'CASCADE';
}
const association = collection.model.belongsToMany(Target, { const association = collection.model.belongsToMany(Target, {
constraints: false, constraints: false,
...omit(this.options, ['name', 'type', 'target']), ...omit(this.options, ['name', 'type', 'target']),

View File

@ -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;
}

View File

@ -15,4 +15,6 @@ export * from './relation-repository/multiple-relation-repository';
export * from './relation-repository/single-relation-repository'; export * from './relation-repository/single-relation-repository';
export * from './repository'; export * from './repository';
export * from './update-associations'; export * from './update-associations';
export * from './collection-importer';
export * from './filter-match';
export * from './field-repository/array-field-repository'; export * from './field-repository/array-field-repository';

View File

@ -132,7 +132,12 @@ export class OptionsParser {
appends.push(field); appends.push(field);
} else { } else {
// field is model attribute, change attributes to array type // 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); attributes.push(field);
} }

View File

@ -87,10 +87,11 @@ export type AssociationKeysToBeUpdate = string[];
export type Values = any; export type Values = any;
export interface CountOptions extends Omit<SequelizeCountOptions, 'distinct' | 'where' | 'include'>, Transactionable { export type CountOptions = Omit<SequelizeCountOptions, 'distinct' | 'where' | 'include'> &
filter?: Filter; Transactionable & {
context?: any; filter?: Filter;
} context?: any;
} & FilterByTk;
export interface FilterByTk { export interface FilterByTk {
filterByTk?: TargetKey; filterByTk?: TargetKey;
@ -225,6 +226,17 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
}; };
} }
if (countOptions?.filterByTk) {
options['where'] = {
[Op.and]: [
options['where'] || {},
{
[this.collection.filterTargetKey]: options.filterByTk,
},
],
};
}
const queryOptions: any = { const queryOptions: any = {
...options, ...options,
distinct: Boolean(this.collection.model.primaryKeyAttribute), distinct: Boolean(this.collection.model.primaryKeyAttribute),
@ -608,7 +620,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
return new RelationRepositoryBuilder<R>(this.collection, association); return new RelationRepositoryBuilder<R>(this.collection, association);
} }
protected buildQueryOptions(options: any) { public buildQueryOptions(options: any) {
const parser = new OptionsParser(options, { const parser = new OptionsParser(options, {
collection: this.collection, collection: this.collection,
}); });