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[] { diff --git a/packages/rest/src/writer.ts b/packages/rest/src/writer.ts index 1236be6bd425..81782436843f 100644 --- a/packages/rest/src/writer.ts +++ b/packages/rest/src/writer.ts @@ -50,8 +50,32 @@ export function writeResultToResponse( // TODO(ritch) remove this, should be configurable // See https://github.com/loopbackio/loopback-next/issues/436 response.setHeader('Content-Type', 'application/json'); + let customResult = result; + let org: {[key: string]: Object[]} = {}; + if (result && typeof result === 'object') { + if (Array.isArray(result)) { + customResult = []; + result.forEach((item: {[key: string]: Object[]}) => { + org = {}; + if (typeof item === 'object') { + Object.keys(item).forEach(key => { + org[key] = item[key]; + }); + customResult.push(org); + } else { + customResult.push(item); + } + }); + } else { + org = {}; + Object.keys(result).forEach(key => { + org[key] = result[key]; + }); + customResult = org; + } + } // TODO(bajtos) handle errors - JSON.stringify can throw - result = JSON.stringify(result); + result = JSON.stringify(customResult); } break; default: