From 834539bb27abda3626541068f1484def3a683583 Mon Sep 17 00:00:00 2001 From: Muhammad Aaqil Date: Thu, 28 Dec 2023 08:38:24 +0500 Subject: [PATCH 1/2] feat: support groupby Signed-off-by: Muhammad Aaqil --- packages/filter/src/query.ts | 24 ++++++ .../unit/decorators/query.decorator.unit.ts | 84 ++++++++++++++++++- .../src/__tests__/unit/filter-schema.unit.ts | 84 ++++++++++++++++++- .../__tests__/unit/filter-json-schema.unit.ts | 9 ++ .../src/filter-json-schema.ts | 63 ++++++++++++++ .../src/repositories/legacy-juggler-bridge.ts | 2 +- 6 files changed, 259 insertions(+), 7 deletions(-) diff --git a/packages/filter/src/query.ts b/packages/filter/src/query.ts index f7ad6f3ac8c1..d6150da48337 100644 --- a/packages/filter/src/query.ts +++ b/packages/filter/src/query.ts @@ -230,6 +230,30 @@ export interface Filter { * To include related objects */ include?: InclusionFilter[]; + /** + * return groupBy of + */ + groupBy?: string[]; + /** + * return sum of + */ + sum?: string; + /** + * return min of + */ + min?: string; + /** + * return max of + */ + max?: string; + /** + * return avg of + */ + avg?: string; + /** + * return count of + */ + count?: string; } /** diff --git a/packages/openapi-v3/src/__tests__/unit/decorators/query.decorator.unit.ts b/packages/openapi-v3/src/__tests__/unit/decorators/query.decorator.unit.ts index 8845b46e6e03..88465c9fd656 100644 --- a/packages/openapi-v3/src/__tests__/unit/decorators/query.decorator.unit.ts +++ b/packages/openapi-v3/src/__tests__/unit/decorators/query.decorator.unit.ts @@ -34,6 +34,14 @@ describe('sugar decorators for filter and where', () => { title: 'MyModel.Filter', 'x-typescript-type': '@loopback/repository#Filter', properties: { + avg: { + example: 'column1', + type: 'string', + }, + count: { + example: 'column1', + type: 'string', + }, fields: { oneOf: [ { @@ -55,11 +63,29 @@ describe('sugar decorators for filter and where', () => { }, }, ], - title: 'MyModel.Fields', + title: 'MyModel.GroupBy', }, offset: {type: 'integer', minimum: 0}, + groupBy: { + items: { + type: 'string', + }, + type: 'array', + }, limit: {type: 'integer', minimum: 1, example: 100}, + max: { + example: 'column1', + type: 'string', + }, + min: { + example: 'column1', + type: 'string', + }, skip: {type: 'integer', minimum: 0}, + sum: { + example: 'column1', + type: 'string', + }, order: { oneOf: [ {type: 'string'}, @@ -92,6 +118,14 @@ describe('sugar decorators for filter and where', () => { title: 'MyModel.Filter', 'x-typescript-type': '@loopback/repository#Filter', properties: { + avg: { + example: 'column1', + type: 'string', + }, + count: { + example: 'column1', + type: 'string', + }, fields: { oneOf: [ { @@ -113,11 +147,29 @@ describe('sugar decorators for filter and where', () => { }, }, ], - title: 'MyModel.Fields', + title: 'MyModel.GroupBy', }, offset: {type: 'integer', minimum: 0}, limit: {type: 'integer', minimum: 1, example: 100}, + groupBy: { + items: { + type: 'string', + }, + type: 'array', + }, skip: {type: 'integer', minimum: 0}, + max: { + example: 'column1', + type: 'string', + }, + min: { + example: 'column1', + type: 'string', + }, + sum: { + example: 'column1', + type: 'string', + }, order: { oneOf: [ {type: 'string'}, @@ -157,6 +209,14 @@ describe('sugar decorators for filter and where', () => { title: 'MyModel.Filter', 'x-typescript-type': '@loopback/repository#Filter', properties: { + avg: { + example: 'column1', + type: 'string', + }, + count: { + example: 'column1', + type: 'string', + }, fields: { oneOf: [ { @@ -178,11 +238,29 @@ describe('sugar decorators for filter and where', () => { }, }, ], - title: 'MyModel.Fields', + title: 'MyModel.GroupBy', }, offset: {type: 'integer', minimum: 0}, limit: {type: 'integer', minimum: 1, example: 100}, + groupBy: { + items: { + type: 'string', + }, + type: 'array', + }, skip: {type: 'integer', minimum: 0}, + max: { + example: 'column1', + type: 'string', + }, + min: { + example: 'column1', + type: 'string', + }, + sum: { + example: 'column1', + type: 'string', + }, order: { oneOf: [ {type: 'string'}, diff --git a/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts b/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts index 714b672e492a..0d573888f7af 100644 --- a/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts +++ b/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts @@ -25,6 +25,14 @@ describe('filterSchema', () => { type: 'object', 'x-typescript-type': '@loopback/repository#Filter', properties: { + avg: { + example: 'column1', + type: 'string', + }, + count: { + example: 'column1', + type: 'string', + }, where: { type: 'object', title: 'my-user-model.WhereFilter', @@ -54,11 +62,29 @@ describe('filterSchema', () => { }, }, ], - title: 'my-user-model.Fields', + title: 'my-user-model.GroupBy', }, offset: {type: 'integer', minimum: 0}, limit: {type: 'integer', minimum: 1, example: 100}, + max: { + example: 'column1', + type: 'string', + }, + min: { + example: 'column1', + type: 'string', + }, + groupBy: { + items: { + type: 'string', + }, + type: 'array', + }, skip: {type: 'integer', minimum: 0}, + sum: { + example: 'column1', + type: 'string', + }, order: { oneOf: [{type: 'string'}, {type: 'array', items: {type: 'string'}}], }, @@ -75,6 +101,14 @@ describe('filterSchema', () => { type: 'object', 'x-typescript-type': '@loopback/repository#Filter', properties: { + avg: { + example: 'column1', + type: 'string', + }, + count: { + example: 'column1', + type: 'string', + }, fields: { oneOf: [ { @@ -99,11 +133,29 @@ describe('filterSchema', () => { }, }, ], - title: 'my-user-model.Fields', + title: 'my-user-model.GroupBy', }, offset: {type: 'integer', minimum: 0}, + groupBy: { + items: { + type: 'string', + }, + type: 'array', + }, limit: {type: 'integer', minimum: 1, example: 100}, + max: { + example: 'column1', + type: 'string', + }, + min: { + example: 'column1', + type: 'string', + }, skip: {type: 'integer', minimum: 0}, + sum: { + example: 'column1', + type: 'string', + }, order: { oneOf: [{type: 'string'}, {type: 'array', items: {type: 'string'}}], }, @@ -129,6 +181,14 @@ describe('filterSchema', () => { type: 'object', 'x-typescript-type': '@loopback/repository#Filter', properties: { + avg: { + example: 'column1', + type: 'string', + }, + count: { + example: 'column1', + type: 'string', + }, where: { type: 'object', title: 'CustomUserModel.WhereFilter', @@ -158,11 +218,29 @@ describe('filterSchema', () => { }, }, ], - title: 'CustomUserModel.Fields', + title: 'CustomUserModel.GroupBy', }, offset: {type: 'integer', minimum: 0}, + groupBy: { + items: { + type: 'string', + }, + type: 'array', + }, limit: {type: 'integer', minimum: 1, example: 100}, + max: { + example: 'column1', + type: 'string', + }, + min: { + example: 'column1', + type: 'string', + }, skip: {type: 'integer', minimum: 0}, + sum: { + example: 'column1', + type: 'string', + }, order: { oneOf: [{type: 'string'}, {type: 'array', items: {type: 'string'}}], }, diff --git a/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts b/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts index 2e30fd2b5dcb..9946432f55f3 100644 --- a/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts +++ b/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts @@ -72,6 +72,12 @@ describe('getFilterJsonSchemaFor', () => { limit: 10, order: ['id DESC'], skip: 0, + sum: 'salary', + min: 'salary', + max: 'salary', + avg: 'salary', + count: 'salary', + groupBy: ['salary'], }; expectSchemaToAllowFilter(customerFilterSchema, filter); @@ -560,6 +566,9 @@ class Customer extends Entity { @property() name: string; + @property() + salary: number; + @hasMany(() => Order) orders?: Order[]; } diff --git a/packages/repository-json-schema/src/filter-json-schema.ts b/packages/repository-json-schema/src/filter-json-schema.ts index 9ced979baaa7..337b3dcac1c8 100644 --- a/packages/repository-json-schema/src/filter-json-schema.ts +++ b/packages/repository-json-schema/src/filter-json-schema.ts @@ -94,6 +94,32 @@ export function getFilterJsonSchemaFor( examples: [100], }, + sum: { + type: 'string', + examples: ['column1'], + }, + min: { + type: 'string', + examples: ['column1'], + }, + max: { + type: 'string', + examples: ['column1'], + }, + avg: { + type: 'string', + examples: ['column1'], + }, + count: { + type: 'string', + examples: ['column1'], + }, + groupBy: { + type: 'array', + items: { + type: 'string', + }, + }, skip: { type: 'integer', minimum: 0, @@ -120,6 +146,9 @@ export function getFilterJsonSchemaFor( if (!excluded.includes('fields')) { properties.fields = getFieldsJsonSchemaFor(modelCtor, options); } + if (!excluded.includes('groupBy')) { + properties.fields = getGroupByJsonSchemaFor(modelCtor, options); + } // Remove excluded properties for (const p of excluded) { @@ -235,3 +264,37 @@ export function getFieldsJsonSchemaFor( return schema; } + +export function getGroupByJsonSchemaFor( + modelCtor: typeof Model, + options: FilterSchemaOptions = {}, +): JsonSchema { + const schema: JsonSchema = {oneOf: []}; + if (options.setTitle !== false) { + schema.title = `${modelCtor.modelName}.GroupBy`; + } + + const properties = Object.keys(modelCtor.definition.properties); + const additionalProperties = modelCtor.definition.settings.strict === false; + + schema.oneOf?.push({ + type: 'object', + properties: properties.reduce( + (prev, crr) => ({...prev, [crr]: {type: 'boolean'}}), + {}, + ), + additionalProperties, + }); + + schema.oneOf?.push({ + type: 'array', + items: { + type: 'string', + enum: properties.length && !additionalProperties ? properties : undefined, + examples: properties, + }, + uniqueItems: true, + }); + + return schema; +} diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 3df52ed95514..2f89a34514a8 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -739,7 +739,7 @@ export class DefaultCrudRepository< } protected toEntity(model: juggler.PersistedModel): R { - return new this.entityClass(model.toObject()) as R; + return new this.entityClass(model.toObject({onlySchema: false})) as R; } protected toEntities(models: juggler.PersistedModel[]): R[] { From 823ae435f3c8de4416387225baab2baed67b25d5 Mon Sep 17 00:00:00 2001 From: Muhammad Aaqil Date: Sat, 28 Sep 2024 13:15:47 +0500 Subject: [PATCH 2/2] fix: a new test Signed-off-by: Muhammad Aaqil --- .../__tests__/acceptance/todo.acceptance.ts | 29 +++++++++++++++++++ examples/todo-list/src/models/todo.model.ts | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts b/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts index 330f1fea3069..46c10d439cfd 100644 --- a/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts +++ b/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts @@ -150,6 +150,35 @@ describe('TodoListApplication', () => { .expect(200, [toJSON(todoInProgress)]); }); + it('queries todos with a filter and group by', async () => { + const todoInProgress = await givenTodoInstance(todoRepo, { + title: 'go to sleep', + isComplete: false, + }); + await client + .get('/todos') + .query({ + filter: { + count: 'title', + sum: 'id', + avg: 'id', + min: 'id', + max: 'id', + groupBy: {title: true}, + }, + }) + .expect(200, [ + toJSON({ + ...todoInProgress, + count: 1, + sum: 6, + avg: 6, + min: 6, + max: 6, + }), + ]); + }); + it('updates todos using a filter', async () => { await givenTodoInstance(todoRepo, { title: 'hello', diff --git a/examples/todo-list/src/models/todo.model.ts b/examples/todo-list/src/models/todo.model.ts index 5f3ded0cb8f8..48c9a8e5e998 100644 --- a/examples/todo-list/src/models/todo.model.ts +++ b/examples/todo-list/src/models/todo.model.ts @@ -6,7 +6,7 @@ import {belongsTo, Entity, model, property} from '@loopback/repository'; import {TodoList, TodoListWithRelations} from './todo-list.model'; -@model() +@model({settings: {strict: false}}) export class Todo extends Entity { @property({ type: 'number',