feat: repository aggregate method (#1829)
* feat: repository aggregate method * chore: aggregate with distinct * feat: has many repository aggregation * feat: belongs to many repository aggregation * chore: using model aggregate method * chore: aggregate with association filter * chore: aggregate type * chore: type error
This commit is contained in:
parent
a6c7b417de
commit
f8fa36d016
@ -0,0 +1,297 @@
|
||||
import { BelongsToManyRepository, HasManyRepository, mockDatabase } from '../../index';
|
||||
import Database from '../../database';
|
||||
import { Collection } from '../../collection';
|
||||
|
||||
describe('association aggregation', () => {
|
||||
let db: Database;
|
||||
|
||||
let User: Collection;
|
||||
let Post: Collection;
|
||||
let Tag: Collection;
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
|
||||
User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'integer', name: 'age' },
|
||||
{ type: 'hasMany', name: 'posts' },
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'tags',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'readCount',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'user',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Tag = db.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'score',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
describe('belongs to many', () => {
|
||||
beforeEach(async () => {
|
||||
await User.repository.create({
|
||||
values: [
|
||||
{
|
||||
name: 'u1',
|
||||
age: 1,
|
||||
tags: [
|
||||
{ name: 't1', score: 1 },
|
||||
{ name: 't2', score: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'u2',
|
||||
age: 2,
|
||||
tags: [
|
||||
{ name: 't3', score: 3 },
|
||||
{ name: 't4', score: 4 },
|
||||
{ name: 't5', score: 4 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should sum field', async () => {
|
||||
const user1 = await User.repository.findOne({
|
||||
filter: {
|
||||
name: 'u1',
|
||||
},
|
||||
});
|
||||
|
||||
const TagRepository = await db.getRepository<BelongsToManyRepository>('users.tags', user1.get('id'));
|
||||
|
||||
const sumResult = await TagRepository.aggregate({
|
||||
field: 'score',
|
||||
method: 'sum',
|
||||
});
|
||||
|
||||
expect(sumResult).toEqual(3);
|
||||
});
|
||||
|
||||
it('should sum with filter', async () => {
|
||||
const user1 = await User.repository.findOne({
|
||||
filter: {
|
||||
name: 'u2',
|
||||
},
|
||||
});
|
||||
|
||||
const TagRepository = await db.getRepository<BelongsToManyRepository>('users.tags', user1.get('id'));
|
||||
|
||||
const sumResult = await TagRepository.aggregate({
|
||||
field: 'score',
|
||||
method: 'sum',
|
||||
filter: {
|
||||
score: 4,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sumResult).toEqual(8);
|
||||
});
|
||||
|
||||
it('should sum with distinct', async () => {
|
||||
const user1 = await User.repository.findOne({
|
||||
filter: {
|
||||
name: 'u2',
|
||||
},
|
||||
});
|
||||
|
||||
const TagRepository = await db.getRepository<BelongsToManyRepository>('users.tags', user1.get('id'));
|
||||
|
||||
const sumResult = await TagRepository.aggregate({
|
||||
field: 'score',
|
||||
method: 'sum',
|
||||
distinct: true,
|
||||
filter: {
|
||||
score: 4,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sumResult).toEqual(4);
|
||||
});
|
||||
it('should sum with association filter', async () => {
|
||||
const sumResult = await User.repository.aggregate({
|
||||
field: 'age',
|
||||
method: 'sum',
|
||||
filter: {
|
||||
'tags.score': 4,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sumResult).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('has many', () => {
|
||||
beforeEach(async () => {
|
||||
await User.repository.create({
|
||||
values: [
|
||||
{
|
||||
name: 'u1',
|
||||
age: 1,
|
||||
posts: [
|
||||
{ title: 'p1', category: 'c1', readCount: 1 },
|
||||
{ title: 'p2', category: 'c2', readCount: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'u2',
|
||||
age: 2,
|
||||
posts: [
|
||||
{ title: 'p3', category: 'c3', readCount: 3 },
|
||||
{ title: 'p4', category: 'c4', readCount: 4 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should sum field', async () => {
|
||||
const user1 = await User.repository.findOne({
|
||||
filter: {
|
||||
name: 'u1',
|
||||
},
|
||||
});
|
||||
|
||||
const PostRepository = await db.getRepository<HasManyRepository>('users.posts', user1.get('id'));
|
||||
const sumResult = await PostRepository.aggregate({
|
||||
field: 'readCount',
|
||||
method: 'sum',
|
||||
});
|
||||
|
||||
expect(sumResult).toEqual(3);
|
||||
});
|
||||
|
||||
it('should sum with filter', async () => {
|
||||
const user1 = await User.repository.findOne({
|
||||
filter: {
|
||||
name: 'u1',
|
||||
},
|
||||
});
|
||||
|
||||
const PostRepository = await db.getRepository<HasManyRepository>('users.posts', user1.get('id'));
|
||||
const sumResult = await PostRepository.aggregate({
|
||||
field: 'readCount',
|
||||
method: 'sum',
|
||||
});
|
||||
|
||||
expect(sumResult).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Aggregation', () => {
|
||||
let db: Database;
|
||||
|
||||
let User: Collection;
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
|
||||
User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'age',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await User.repository.create({
|
||||
values: [
|
||||
{ name: 'u1', age: 1 },
|
||||
{ name: 'u2', age: 2 },
|
||||
{ name: 'u3', age: 3 },
|
||||
{ name: 'u4', age: 4 },
|
||||
{ name: 'u5', age: 5 },
|
||||
{ name: 'u5', age: 5 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('sum', () => {
|
||||
it('should sum field', async () => {
|
||||
const sumResult = await User.repository.aggregate({
|
||||
method: 'sum',
|
||||
field: 'age',
|
||||
});
|
||||
|
||||
expect(sumResult).toEqual(20);
|
||||
});
|
||||
|
||||
it('should sum with distinct', async () => {
|
||||
const sumResult = await User.repository.aggregate({
|
||||
method: 'sum',
|
||||
field: 'age',
|
||||
distinct: true,
|
||||
});
|
||||
|
||||
expect(sumResult).toEqual(15);
|
||||
});
|
||||
|
||||
it('should sum with filter', async () => {
|
||||
const sumResult = await User.repository.aggregate({
|
||||
method: 'sum',
|
||||
field: 'age',
|
||||
filter: {
|
||||
name: 'u5',
|
||||
},
|
||||
});
|
||||
|
||||
expect(sumResult).toEqual(10);
|
||||
});
|
||||
});
|
||||
});
|
@ -45,7 +45,9 @@ export function getConfigByEnv() {
|
||||
|
||||
function customLogger(queryString, queryObject) {
|
||||
console.log(queryString); // outputs a string
|
||||
console.log(queryObject.bind); // outputs an array
|
||||
if (queryObject.bind) {
|
||||
console.log(queryObject.bind); // outputs an array
|
||||
}
|
||||
}
|
||||
|
||||
export function mockDatabase(options: IDatabaseOptions = {}): MockDatabase {
|
||||
|
@ -1,7 +1,15 @@
|
||||
import lodash from 'lodash';
|
||||
import { BelongsToMany, Op, Transaction } from 'sequelize';
|
||||
import { Model } from '../model';
|
||||
import { CreateOptions, DestroyOptions, FindOneOptions, FindOptions, TargetKey, UpdateOptions } from '../repository';
|
||||
import {
|
||||
AggregateOptions,
|
||||
CreateOptions,
|
||||
DestroyOptions,
|
||||
FindOneOptions,
|
||||
FindOptions,
|
||||
TargetKey,
|
||||
UpdateOptions,
|
||||
} from '../repository';
|
||||
import { updateThroughTableValue } from '../update-associations';
|
||||
import { FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository';
|
||||
import { transaction } from './relation-repository';
|
||||
@ -38,6 +46,30 @@ interface IBelongsToManyRepository<M extends Model> {
|
||||
}
|
||||
|
||||
export class BelongsToManyRepository extends MultipleRelationRepository implements IBelongsToManyRepository<any> {
|
||||
async aggregate(options: AggregateOptions) {
|
||||
const targetRepository = this.targetCollection.repository;
|
||||
|
||||
const sourceModel = await this.getSourceModel();
|
||||
|
||||
const association = this.association as any;
|
||||
|
||||
return await targetRepository.aggregate({
|
||||
...options,
|
||||
optionsTransformer: (modelOptions) => {
|
||||
modelOptions.include = modelOptions.include || [];
|
||||
const throughWhere = {};
|
||||
throughWhere[association.foreignKey] = sourceModel.get(association.sourceKey);
|
||||
|
||||
modelOptions.include.push({
|
||||
association: association.oneFromTarget,
|
||||
required: true,
|
||||
attributes: [],
|
||||
where: throughWhere,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@transaction()
|
||||
async create(options?: CreateBelongsToManyOptions): Promise<any> {
|
||||
if (Array.isArray(options.values)) {
|
||||
|
@ -2,6 +2,7 @@ import { omit } from 'lodash';
|
||||
import { HasMany, Op } from 'sequelize';
|
||||
import { Model } from '../model';
|
||||
import {
|
||||
AggregateOptions,
|
||||
CreateOptions,
|
||||
DestroyOptions,
|
||||
FindOneOptions,
|
||||
@ -53,6 +54,22 @@ export class HasManyRepository extends MultipleRelationRepository implements IHa
|
||||
return await targetRepository.find(findOptions);
|
||||
}
|
||||
|
||||
async aggregate(options: AggregateOptions) {
|
||||
const targetRepository = this.targetCollection.repository;
|
||||
const addFilter = {
|
||||
[this.association.foreignKey]: this.sourceKeyValue,
|
||||
};
|
||||
|
||||
const aggOptions = {
|
||||
...options,
|
||||
filter: {
|
||||
$and: [options.filter || {}, addFilter],
|
||||
},
|
||||
};
|
||||
|
||||
return await targetRepository.aggregate(aggOptions);
|
||||
}
|
||||
|
||||
@transaction((args, transaction) => {
|
||||
return {
|
||||
filterByTk: args[0],
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
FindOptions as SequelizeFindOptions,
|
||||
ModelStatic,
|
||||
Op,
|
||||
Sequelize,
|
||||
Transactionable,
|
||||
UpdateOptions as SequelizeUpdateOptions,
|
||||
WhereOperators,
|
||||
@ -202,6 +203,13 @@ class RelationRepositoryBuilder<R extends RelationRepository> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface AggregateOptions {
|
||||
method: 'avg' | 'count' | 'min' | 'max' | 'sum';
|
||||
field?: string;
|
||||
filter?: Filter;
|
||||
distinct?: boolean;
|
||||
}
|
||||
|
||||
export class Repository<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes>
|
||||
implements IRepository
|
||||
{
|
||||
@ -259,6 +267,59 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
return count;
|
||||
}
|
||||
|
||||
async aggregate(options: AggregateOptions & { optionsTransformer?: (options: any) => any }): Promise<any> {
|
||||
const { method, field } = options;
|
||||
|
||||
const queryOptions = this.buildQueryOptions({
|
||||
...options,
|
||||
fields: [],
|
||||
});
|
||||
|
||||
options.optionsTransformer?.(queryOptions);
|
||||
|
||||
const hasAssociationFilter = () => {
|
||||
if (queryOptions.include && queryOptions.include.length > 0) {
|
||||
const filterInclude = queryOptions.include.filter((include) => {
|
||||
return (
|
||||
Object.keys(include.where || {}).length > 0 ||
|
||||
JSON.stringify(queryOptions?.filter)?.includes(include.association)
|
||||
);
|
||||
});
|
||||
return filterInclude.length > 0;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (hasAssociationFilter()) {
|
||||
const primaryKeyField = this.model.primaryKeyAttribute;
|
||||
const queryInterface = this.database.sequelize.getQueryInterface();
|
||||
|
||||
const findOptions = {
|
||||
...queryOptions,
|
||||
raw: true,
|
||||
includeIgnoreAttributes: false,
|
||||
attributes: [
|
||||
[
|
||||
Sequelize.literal(
|
||||
`DISTINCT ${queryInterface.quoteIdentifiers(`${this.collection.name}.${primaryKeyField}`)}`,
|
||||
),
|
||||
primaryKeyField,
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const ids = await this.model.findAll(findOptions);
|
||||
|
||||
return await this.model.aggregate(field, method, {
|
||||
...lodash.omit(queryOptions, ['where', 'include']),
|
||||
where: {
|
||||
[primaryKeyField]: ids.map((node) => node[primaryKeyField]),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await this.model.aggregate(field, method, queryOptions);
|
||||
}
|
||||
/**
|
||||
* find
|
||||
* @param options
|
||||
|
Loading…
Reference in New Issue
Block a user