Skip to content

Commit be987a2

Browse files
authored
feat: order by implementation (#50)
feat: order by implementation
2 parents fb9bd49 + 5921929 commit be987a2

File tree

6 files changed

+220
-82
lines changed

6 files changed

+220
-82
lines changed

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@
3737
"@commitlint/cli": "^7.6.1",
3838
"@commitlint/config-conventional": "^7.6.0",
3939
"@commitlint/travis-cli": "^7.6.1",
40-
"@google-cloud/firestore": "^2.1.0",
40+
"@google-cloud/firestore": "^2.2.1",
4141
"@qiwi/semantic-release-gh-pages-plugin": "^1.9.1",
4242
"@types/chai": "^4.1.7",
4343
"@types/mocha": "^5.2.5",
4444
"@types/pluralize": "^0.0.29",
4545
"@types/sinon": "^7.0.6",
4646
"chai": "^4.2.0",
47-
"firebase-admin": "^8.0.0",
47+
"firebase-admin": "^8.1.0",
4848
"husky": "^2.3.0",
4949
"mocha": "^6.1.4",
5050
"mock-cloud-firestore": "^0.9.0",
@@ -60,6 +60,9 @@
6060
"peerDependencies": {
6161
"reflect-metadata": "^0.1.13"
6262
},
63+
"resolutions": {
64+
"@google-cloud/firestore": "2.2.1"
65+
},
6366
"files": [
6467
"/lib",
6568
"!**/*.map",

src/BaseFirestoreRepository.spec.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,101 @@ describe('BaseRepository', () => {
6767
const albumsLimited = await albumsSubColl.limit(2).find();
6868
expect(albumsLimited.length).to.equal(2);
6969
});
70-
})
70+
});
71+
72+
describe('orderByAscending', () => {
73+
it('must order repository objects', async () => {
74+
const bands = await bandRepository
75+
.orderByAscending('formationYear')
76+
.find();
77+
expect(bands[0].id).to.equal('pink-floyd');
78+
});
79+
80+
it('must order the objects in a subcollection', async () => {
81+
const pt = await bandRepository.findById('porcupine-tree');
82+
const albumsSubColl = pt.albums;
83+
const discographyNewestFirst = await albumsSubColl
84+
.orderByAscending('releaseDate')
85+
.find();
86+
expect(discographyNewestFirst[0].id).to.equal('lightbulb-sun');
87+
});
88+
89+
it('must be chainable with where* filters', async () => {
90+
const pt = await bandRepository.findById('porcupine-tree');
91+
const albumsSubColl = pt.albums;
92+
const discographyNewestFirst = await albumsSubColl
93+
.whereGreaterOrEqualThan('releaseDate', new Date('2001-01-01'))
94+
.orderByAscending('releaseDate')
95+
.find();
96+
expect(discographyNewestFirst[0].id).to.equal('in-absentia');
97+
});
98+
99+
it('must be chainable with limit', async () => {
100+
const bands = await bandRepository
101+
.orderByAscending('formationYear')
102+
.limit(2)
103+
.find();
104+
const lastBand = bands[bands.length - 1];
105+
expect(lastBand.id).to.equal('red-hot-chili-peppers');
106+
});
107+
108+
it('must throw an Error if an orderBy* function is called more than once in the same expression', async () => {
109+
const pt = await bandRepository.findById('porcupine-tree');
110+
const albumsSubColl = pt.albums;
111+
expect(() => {
112+
albumsSubColl
113+
.orderByAscending('releaseDate')
114+
.orderByDescending('releaseDate');
115+
}).to.throw;
116+
});
117+
});
118+
119+
describe('orderByDescending', () => {
120+
it('must order repository objects', async () => {
121+
const bands = await bandRepository
122+
.orderByDescending('formationYear')
123+
.find();
124+
expect(bands[0].id).to.equal('porcupine-tree');
125+
});
126+
127+
it('must order the objects in a subcollection', async () => {
128+
const pt = await bandRepository.findById('porcupine-tree');
129+
const albumsSubColl = pt.albums;
130+
const discographyNewestFirst = await albumsSubColl
131+
.orderByDescending('releaseDate')
132+
.find();
133+
expect(discographyNewestFirst[0].id).to.equal('fear-blank-planet');
134+
});
135+
136+
it('must be chainable with where* filters', async () => {
137+
const pt = await bandRepository.findById('porcupine-tree');
138+
const albumsSubColl = pt.albums;
139+
const discographyNewestFirst = await albumsSubColl
140+
.whereGreaterOrEqualThan('releaseDate', new Date('2001-01-01'))
141+
.orderByDescending('releaseDate')
142+
.find();
143+
expect(discographyNewestFirst[0].id).to.equal('fear-blank-planet');
144+
});
145+
146+
it('must be chainable with limit', async () => {
147+
const bands = await bandRepository
148+
.orderByDescending('formationYear')
149+
.limit(2)
150+
.find();
151+
const lastBand = bands[bands.length - 1];
152+
expect(lastBand.id).to.equal('red-hot-chili-peppers');
153+
});
154+
155+
it('must throw an Error if an orderBy* function is called more than once in the same expression', async () => {
156+
const pt = await bandRepository.findById('porcupine-tree');
157+
const albumsSubColl = pt.albums;
158+
expect(() => {
159+
albumsSubColl
160+
.orderByAscending('releaseDate')
161+
.orderByDescending('releaseDate');
162+
}).to.throw;
163+
});
164+
});
71165

72166
describe('findById', () => {
73167
it('must find by id', async () => {

src/BaseFirestoreRepository.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
IQueryBuilder,
99
FirestoreCollectionType,
1010
IFireOrmQueryLine,
11+
IOrderByParams,
1112
IQueryExecutor,
1213
IEntity,
1314
} from './types';
@@ -163,24 +164,37 @@ export default class BaseFirestoreRepository<T extends IEntity>
163164
}
164165

165166
limit(limitVal: number): QueryBuilder<T> {
166-
return new QueryBuilder<T>(this).limit(limitVal);;
167+
return new QueryBuilder<T>(this).limit(limitVal);
168+
}
169+
170+
orderByAscending(prop: keyof T & string): QueryBuilder<T> {
171+
return new QueryBuilder<T>(this).orderByAscending(prop);
172+
}
173+
174+
orderByDescending(prop: keyof T & string): QueryBuilder<T> {
175+
return new QueryBuilder<T>(this).orderByDescending(prop);
167176
}
168177

169178
find(): Promise<T[]> {
170179
return new QueryBuilder<T>(this).find();
171180
}
172181

173-
execute(queries: Array<IFireOrmQueryLine>, limitVal?: number): Promise<T[]> {
174-
let query = queries
175-
.reduce((acc, cur) => {
176-
const op = cur.operator as WhereFilterOp;
177-
return acc.where(cur.prop, op, cur.val);
178-
}, this.firestoreCollection);
179-
if (limitVal) {
180-
query = query.limit(limitVal)
181-
}
182-
return query.get()
183-
.then(this.extractTFromColSnap);
182+
execute(
183+
queries: Array<IFireOrmQueryLine>,
184+
limitVal?: number,
185+
orderByObj?: IOrderByParams
186+
): Promise<T[]> {
187+
let query = queries.reduce((acc, cur) => {
188+
const op = cur.operator as WhereFilterOp;
189+
return acc.where(cur.prop, op, cur.val);
190+
}, this.firestoreCollection);
191+
if (orderByObj) {
192+
query = query.orderBy(orderByObj.fieldPath, orderByObj.directionStr);
193+
}
194+
if (limitVal) {
195+
query = query.limit(limitVal);
196+
}
197+
return query.get().then(this.extractTFromColSnap);
184198
}
185199

186200
whereEqualTo(prop: keyof T, val: IFirestoreVal): QueryBuilder<T> {

src/QueryBuilder.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
IQueryBuilder,
33
IFireOrmQueryLine,
4+
IOrderByParams,
45
IFirestoreVal,
56
FirestoreOperators,
67
IQueryExecutor,
@@ -11,6 +12,7 @@ export default class QueryBuilder<T extends IEntity>
1112
implements IQueryBuilder<T> {
1213
protected queries: Array<IFireOrmQueryLine> = [];
1314
protected limitVal: number;
15+
protected orderByObj: IOrderByParams;
1416

1517
// TODO: validate not doing range fields in different
1618
// fields if the indexes are not created
@@ -75,7 +77,29 @@ export default class QueryBuilder<T extends IEntity>
7577
return this;
7678
}
7779

80+
orderByAscending(prop: keyof T & string): QueryBuilder<T> {
81+
if (this.orderByObj) {
82+
throw new Error('An orderBy function cannot be called more than once in the same query expression')
83+
}
84+
this.orderByObj = {
85+
fieldPath: prop,
86+
directionStr: 'asc',
87+
};
88+
return this;
89+
}
90+
91+
orderByDescending(prop: keyof T & string): QueryBuilder<T> {
92+
if (this.orderByObj) {
93+
throw new Error('An orderBy function cannot be called more than once in the same query expression')
94+
}
95+
this.orderByObj = {
96+
fieldPath: prop,
97+
directionStr: 'desc',
98+
};
99+
return this;
100+
}
101+
78102
find(): Promise<T[]> {
79-
return this.executor.execute(this.queries, this.limitVal);
103+
return this.executor.execute(this.queries, this.limitVal, this.orderByObj);
80104
}
81105
}

src/types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import QueryBuilder from './QueryBuilder';
2+
import { OrderByDirection } from '@google-cloud/firestore';
23

34
// TODO: separate Read/Write interfaces to achieve readonly?
45
export interface IRepository<T extends { id: string }> {
56
limit(limitVal: number): QueryBuilder<T>;
7+
orderByAscending(prop: keyof T & string): QueryBuilder<T>;
8+
orderByDescending(prop: keyof T & string): QueryBuilder<T>;
69
findById(id: string): Promise<T>;
710
create(item: T): Promise<T>;
811
update(item: T): Promise<T>;
@@ -30,6 +33,11 @@ export interface IFireOrmQueryLine {
3033
operator: FirestoreOperators;
3134
}
3235

36+
export interface IOrderByParams {
37+
fieldPath: string;
38+
directionStr: OrderByDirection;
39+
}
40+
3341
export type IQueryBuilderResult = IFireOrmQueryLine[];
3442

3543
export interface IQueryBuilder<T extends IEntity> {
@@ -39,11 +47,17 @@ export interface IQueryBuilder<T extends IEntity> {
3947
whereLessThan(prop: keyof T, val: IFirestoreVal): IQueryBuilder<T>;
4048
whereLessOrEqualThan(prop: keyof T, val: IFirestoreVal): IQueryBuilder<T>;
4149
whereArrayContains(prop: keyof T, val: IFirestoreVal): IQueryBuilder<T>;
50+
orderByAscending(prop: keyof T & string): IQueryBuilder<T>;
51+
orderByDescending(prop: keyof T & string): IQueryBuilder<T>;
4252
find(): Promise<T[]>;
4353
}
4454

4555
export interface IQueryExecutor<T> {
46-
execute(queries: IFireOrmQueryLine[], limitVal?: number): Promise<T[]>;
56+
execute(
57+
queries: IFireOrmQueryLine[],
58+
limitVal?: number,
59+
orderByObj?: IOrderByParams
60+
): Promise<T[]>;
4761
}
4862

4963
export type ISubCollection<T extends IEntity> = IRepository<T> &

0 commit comments

Comments
 (0)