Skip to content

Commit b4bba3d

Browse files
committed
feat: add filtering to model queries via REST
1 parent 820d8e3 commit b4bba3d

File tree

10 files changed

+110
-39
lines changed

10 files changed

+110
-39
lines changed

examples/todo-list/test/acceptance/todo.acceptance.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {EntityNotFoundError} from '@loopback/repository';
7-
import {createClientForHandler, expect, supertest} from '@loopback/testlab';
7+
import {
8+
createClientForHandler,
9+
deserializedFromJson,
10+
expect,
11+
supertest,
12+
} from '@loopback/testlab';
813
import {TodoListApplication} from '../../src/application';
914
import {Todo} from '../../src/models/';
1015
import {TodoRepository} from '../../src/repositories/';
@@ -54,14 +59,11 @@ describe('Application', () => {
5459
persistedTodo = await givenTodoInstance();
5560
});
5661

57-
it('gets a todo by ID', async () => {
58-
const result = await client
62+
it('gets a todo by ID', () => {
63+
return client
5964
.get(`/todos/${persistedTodo.id}`)
6065
.send()
61-
.expect(200);
62-
// Remove any undefined properties that cannot be represented in JSON/REST
63-
const expected = JSON.parse(JSON.stringify(persistedTodo));
64-
expect(result.body).to.deepEqual(expected);
66+
.expect(200, deserializedFromJson(persistedTodo));
6567
});
6668

6769
it('returns 404 when a todo does not exist', () => {

examples/todo/src/controllers/todo.controller.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,17 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {inject} from '@loopback/core';
7-
import {repository} from '@loopback/repository';
8-
import {del, get, param, patch, post, put, requestBody} from '@loopback/rest';
7+
import {repository, Filter} from '@loopback/repository';
8+
import {
9+
del,
10+
get,
11+
param,
12+
patch,
13+
post,
14+
put,
15+
requestBody,
16+
getFilterSchemaFor,
17+
} from '@loopback/rest';
918
import {Todo} from '../models';
1019
import {TodoRepository} from '../repositories';
1120
import {GeocoderService} from '../services';
@@ -63,8 +72,10 @@ export class TodoController {
6372
},
6473
},
6574
})
66-
async findTodos(): Promise<Todo[]> {
67-
return await this.todoRepo.find();
75+
async findTodos(
76+
@param.query.object('filter', getFilterSchemaFor(Todo)) filter?: Filter,
77+
): Promise<Todo[]> {
78+
return await this.todoRepo.find(filter);
6879
}
6980

7081
@put('/todos/{id}', {

examples/todo/test/acceptance/todo.acceptance.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {EntityNotFoundError} from '@loopback/repository';
7-
import {createClientForHandler, expect, supertest} from '@loopback/testlab';
7+
import {
8+
createClientForHandler,
9+
deserializedFromJson,
10+
expect,
11+
supertest,
12+
} from '@loopback/testlab';
813
import {TodoListApplication} from '../../src/application';
914
import {Todo} from '../../src/models/';
1015
import {TodoRepository} from '../../src/repositories/';
@@ -86,14 +91,11 @@ describe('Application', () => {
8691
persistedTodo = await givenTodoInstance();
8792
});
8893

89-
it('gets a todo by ID', async () => {
90-
const result = await client
94+
it('gets a todo by ID', () => {
95+
return client
9196
.get(`/todos/${persistedTodo.id}`)
9297
.send()
93-
.expect(200);
94-
// Remove any undefined properties that cannot be represented in JSON/REST
95-
const expected = JSON.parse(JSON.stringify(persistedTodo));
96-
expect(result.body).to.deepEqual(expected);
98+
.expect(200, deserializedFromJson(persistedTodo));
9799
});
98100

99101
it('returns 404 when a todo does not exist', () => {
@@ -138,6 +140,20 @@ describe('Application', () => {
138140
});
139141
});
140142

143+
it('queries todos with a filter', async () => {
144+
await givenTodoInstance({title: 'wake up', isComplete: true});
145+
146+
const todoInProgress = await givenTodoInstance({
147+
title: 'go to sleep',
148+
isComplete: false,
149+
});
150+
151+
await client
152+
.get('/todos')
153+
.query({filter: {where: {isComplete: false}}})
154+
.expect(200, [deserializedFromJson(todoInProgress)]);
155+
});
156+
141157
/*
142158
============================================================================
143159
TEST HELPERS

examples/todo/test/unit/controllers/todo.controller.unit.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6+
import {Filter} from '@loopback/repository';
67
import {expect, sinon} from '@loopback/testlab';
78
import {TodoController} from '../../../src/controllers';
89
import {Todo} from '../../../src/models/index';
@@ -99,6 +100,14 @@ describe('TodoController', () => {
99100
expect(await controller.findTodos()).to.eql(expected);
100101
sinon.assert.called(find);
101102
});
103+
104+
it('uses the provided filter', async () => {
105+
const filter: Filter = {where: {isCompleted: false}};
106+
107+
find.resolves(aListOfTodos);
108+
await controller.findTodos(filter);
109+
sinon.assert.calledWith(find, filter);
110+
});
102111
});
103112

104113
describe('replaceTodo', () => {

packages/openapi-v3/src/decorators/parameter.decorator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export namespace param {
224224
name,
225225
in: 'query',
226226
style: 'deepObject',
227+
explode: true,
227228
schema,
228229
});
229230
},
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright IBM Corp. 2018. All Rights Reserved.
2+
// Node module: @loopback/openapi-v3
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {SchemaObject} from '@loopback/openapi-v3-types';
7+
import {getFilterJsonSchemaFor} from '@loopback/repository-json-schema';
8+
import {jsonToSchemaObject} from './json-to-schema';
9+
10+
export function getFilterSchemaFor(modelCtor: Function): SchemaObject {
11+
const jsonSchema = getFilterJsonSchemaFor(modelCtor);
12+
const schema = jsonToSchemaObject(jsonSchema);
13+
return schema;
14+
}

packages/openapi-v3/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
export * from './decorators';
77
export * from './controller-spec';
88
export * from './json-to-schema';
9+
export * from './filter-schema';
910

1011
export * from '@loopback/repository-json-schema';

packages/repository-json-schema/test/unit/filter-json-schema.unit.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,6 @@ import {getFilterJsonSchemaFor} from '../../src/filter-json-schema';
1010
import {JsonSchema} from '../../src';
1111

1212
describe('getFilterJsonSchemaFor', () => {
13-
@model()
14-
class Order extends Entity {
15-
@property({id: true})
16-
id: number;
17-
18-
@property()
19-
customerId: number;
20-
}
21-
22-
@model()
23-
class Customer extends Entity {
24-
@property({id: true})
25-
id: number;
26-
27-
@property()
28-
name: string;
29-
30-
@hasMany(Order)
31-
orders?: Order[];
32-
}
33-
3413
let ajv: Ajv.Ajv;
3514
let customerFilterSchema: JsonSchema;
3615
let orderFilterSchema: JsonSchema;
@@ -162,4 +141,25 @@ describe('getFilterJsonSchemaFor', () => {
162141
const result = isValid ? SUCCESS_MSG : ajv.errorsText(ajv.errors!);
163142
expect(result).to.equal(SUCCESS_MSG);
164143
}
144+
145+
@model()
146+
class Order extends Entity {
147+
@property({id: true})
148+
id: number;
149+
150+
@property()
151+
customerId: number;
152+
}
153+
154+
@model()
155+
class Customer extends Entity {
156+
@property({id: true})
157+
id: number;
158+
159+
@property()
160+
name: string;
161+
162+
@hasMany(Order)
163+
orders?: Order[];
164+
}
165165
});

packages/testlab/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from './test-sandbox';
1212
export * from './skip-travis';
1313
export * from './request';
1414
export * from './http-server-config';
15+
export * from './misc';

packages/testlab/src/misc.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright IBM Corp. 2018. All Rights Reserved.
2+
// Node module: @loopback/testlab
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
/**
7+
* JSON transport does not preserve properties that are undefined
8+
* As a result, deepEqual checks fail because the expected model
9+
* value contains these undefined property values, while the actual
10+
* result returned by REST API does not.
11+
* Use this function to convert a model instance into a data object
12+
* as returned by REST API
13+
*/
14+
export function deserializedFromJson<T extends object>(value: T): T {
15+
return JSON.parse(JSON.stringify(value));
16+
}

0 commit comments

Comments
 (0)