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) {
|
function customLogger(queryString, queryObject) {
|
||||||
console.log(queryString); // outputs a string
|
console.log(queryString); // outputs a string
|
||||||
|
if (queryObject.bind) {
|
||||||
console.log(queryObject.bind); // outputs an array
|
console.log(queryObject.bind); // outputs an array
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockDatabase(options: IDatabaseOptions = {}): MockDatabase {
|
export function mockDatabase(options: IDatabaseOptions = {}): MockDatabase {
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import { BelongsToMany, Op, Transaction } from 'sequelize';
|
import { BelongsToMany, Op, Transaction } from 'sequelize';
|
||||||
import { Model } from '../model';
|
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 { updateThroughTableValue } from '../update-associations';
|
||||||
import { FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository';
|
import { FindAndCountOptions, MultipleRelationRepository } from './multiple-relation-repository';
|
||||||
import { transaction } from './relation-repository';
|
import { transaction } from './relation-repository';
|
||||||
@ -38,6 +46,30 @@ interface IBelongsToManyRepository<M extends Model> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class BelongsToManyRepository extends MultipleRelationRepository implements IBelongsToManyRepository<any> {
|
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()
|
@transaction()
|
||||||
async create(options?: CreateBelongsToManyOptions): Promise<any> {
|
async create(options?: CreateBelongsToManyOptions): Promise<any> {
|
||||||
if (Array.isArray(options.values)) {
|
if (Array.isArray(options.values)) {
|
||||||
|
@ -2,6 +2,7 @@ import { omit } from 'lodash';
|
|||||||
import { HasMany, Op } from 'sequelize';
|
import { HasMany, Op } from 'sequelize';
|
||||||
import { Model } from '../model';
|
import { Model } from '../model';
|
||||||
import {
|
import {
|
||||||
|
AggregateOptions,
|
||||||
CreateOptions,
|
CreateOptions,
|
||||||
DestroyOptions,
|
DestroyOptions,
|
||||||
FindOneOptions,
|
FindOneOptions,
|
||||||
@ -53,6 +54,22 @@ export class HasManyRepository extends MultipleRelationRepository implements IHa
|
|||||||
return await targetRepository.find(findOptions);
|
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) => {
|
@transaction((args, transaction) => {
|
||||||
return {
|
return {
|
||||||
filterByTk: args[0],
|
filterByTk: args[0],
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
FindOptions as SequelizeFindOptions,
|
FindOptions as SequelizeFindOptions,
|
||||||
ModelStatic,
|
ModelStatic,
|
||||||
Op,
|
Op,
|
||||||
|
Sequelize,
|
||||||
Transactionable,
|
Transactionable,
|
||||||
UpdateOptions as SequelizeUpdateOptions,
|
UpdateOptions as SequelizeUpdateOptions,
|
||||||
WhereOperators,
|
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>
|
export class Repository<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes>
|
||||||
implements IRepository
|
implements IRepository
|
||||||
{
|
{
|
||||||
@ -259,6 +267,59 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
return count;
|
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
|
* find
|
||||||
* @param options
|
* @param options
|
||||||
|
Loading…
Reference in New Issue
Block a user