Skip to content

Commit a1b844d

Browse files
committed
Add query serializer from AlexanderArvidsson.
nandorojo#75
1 parent cbe9325 commit a1b844d

File tree

7 files changed

+364
-89
lines changed

7 files changed

+364
-89
lines changed

src/__tests__/index.test.tsx

+94-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,94 @@
1-
it.todo('write a test');
1+
import firebase from 'firebase'
2+
import { CollectionQueryType } from 'src/types/Query'
3+
import { Fuego } from '..'
4+
import { Serializer } from '../helpers/serializer'
5+
6+
it.todo('write a test')
7+
8+
interface User {
9+
id: string
10+
name: string
11+
age: number
12+
joinedAt: Date
13+
}
14+
15+
// This is only to be able to create document references, no querying is actually done
16+
const fuego = new Fuego({
17+
projectId: '123',
18+
})
19+
20+
describe('serializing collection query', () => {
21+
test('single where clause with date', () => {
22+
const query: CollectionQueryType<User> = {
23+
where: ['date', '>', new Date('2020-01-01')],
24+
orderBy: 'name',
25+
limit: 1,
26+
}
27+
28+
const serialized = Serializer.serializeQuery(query, fuego)
29+
expect(serialized).toEqual(
30+
'{"where":["date",">","2020-01-01T00:00:00.000Z",{"type":"date"}],"orderBy":"name","limit":1}'
31+
)
32+
33+
const deserialized = Serializer.deserializeQuery(serialized, fuego)
34+
expect(deserialized).toEqual({
35+
...query,
36+
where: ['date', '>', new Date('2020-01-01'), { type: 'date' }],
37+
})
38+
})
39+
40+
test('multiple where clauses with dates', () => {
41+
const query: CollectionQueryType<User> = {
42+
where: [
43+
['date', '>', new Date('2010-01-01')],
44+
['date', '<', new Date('2020-01-01')],
45+
['name', '==', 'Fernando'],
46+
],
47+
orderBy: 'name',
48+
limit: 1,
49+
}
50+
51+
const serialized = Serializer.serializeQuery(query, fuego)
52+
expect(serialized).toEqual(
53+
'{"where":[["date",">","2010-01-01T00:00:00.000Z",{"type":"date"}],["date","<","2020-01-01T00:00:00.000Z",{"type":"date"}],["name","==","Fernando"]],"orderBy":"name","limit":1}'
54+
)
55+
56+
const deserialized = Serializer.deserializeQuery(serialized, fuego)
57+
expect(deserialized).toEqual({
58+
...query,
59+
where: [
60+
['date', '>', new Date('2010-01-01'), { type: 'date' }],
61+
['date', '<', new Date('2020-01-01'), { type: 'date' }],
62+
['name', '==', 'Fernando'],
63+
],
64+
})
65+
})
66+
67+
test('single where clause with ref', () => {
68+
const query: CollectionQueryType<User> = {
69+
where: [
70+
'user',
71+
'==',
72+
firebase.firestore().doc('users/dqKiW6iFUyFmXN1aVBQ6'),
73+
],
74+
orderBy: 'name',
75+
limit: 1,
76+
}
77+
78+
const serialized = Serializer.serializeQuery(query, fuego)
79+
expect(serialized).toEqual(
80+
'{"where":["user","==","users/dqKiW6iFUyFmXN1aVBQ6",{"type":"ref"}],"orderBy":"name","limit":1}'
81+
)
82+
83+
const deserialized = Serializer.deserializeQuery(serialized, fuego)
84+
expect(deserialized).toEqual({
85+
...query,
86+
where: [
87+
'user',
88+
'==',
89+
firebase.firestore().doc('users/dqKiW6iFUyFmXN1aVBQ6'),
90+
{ type: 'ref' },
91+
],
92+
})
93+
})
94+
})

src/classes/Fuego.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ export class Fuego {
1111
public auth: typeof firebase.auth
1212
public functions: typeof firebase.functions
1313
public storage: typeof firebase.storage
14+
public firestore: typeof firebase.firestore
1415
constructor(config: Config) {
1516
this.db = !firebase.apps.length
1617
? firebase.initializeApp(config).firestore()
1718
: firebase.app().firestore()
1819
this.auth = firebase.auth
1920
this.functions = firebase.functions
2021
this.storage = firebase.storage
22+
this.firestore = firebase.firestore
2123
}
2224
}

src/helpers/serializer.ts

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { DocumentSnapshot } from '@firebase/firestore-types'
2+
import { SerializerOptions } from 'src/types/Serializer'
3+
import { Fuego } from '../classes/Fuego'
4+
import {
5+
CollectionQueryType,
6+
SerializedCollectionQueryType,
7+
WhereArray,
8+
WhereItem,
9+
WhereType,
10+
} from '../types/Query'
11+
12+
export class Serializer {
13+
private static snapshotCache: Record<string, DocumentSnapshot> = {}
14+
15+
private static isSnapshot(value: unknown, fuego: Fuego) {
16+
if (!value) return false
17+
return (
18+
fuego.firestore?.DocumentSnapshot &&
19+
value instanceof fuego.firestore.DocumentSnapshot
20+
)
21+
}
22+
23+
// Helper for where clauses
24+
public static multipleConditions<Doc extends object = {}>(
25+
w: WhereType<Doc>
26+
): w is WhereArray<Doc> {
27+
return !!(w as WhereArray) && Array.isArray(w[0])
28+
}
29+
30+
// Serializer function for where condition
31+
private static serializeWhere<Doc extends object = {}>(
32+
where: WhereType<Doc>,
33+
fuego: Fuego
34+
): WhereType<Doc> {
35+
if (this.multipleConditions(where)) {
36+
return where.map(w => this.serializeWhere(w, fuego)) as WhereArray<Doc>
37+
}
38+
// Date: Inject serializer options if not specified
39+
if (where[2] instanceof Date && !where[3]) {
40+
return [...where.slice(0, 3), { type: 'date' }] as WhereItem<Doc>
41+
}
42+
43+
if (
44+
fuego.firestore?.DocumentReference &&
45+
where[2] instanceof fuego.firestore.DocumentReference &&
46+
!where[3]
47+
) {
48+
return [
49+
...where.slice(0, 2),
50+
where[2].path,
51+
{ type: 'ref' },
52+
] as WhereItem<Doc>
53+
}
54+
55+
return where
56+
}
57+
58+
// Serializer funciton for DocumentSnapshot
59+
private static serializeSnapshot(
60+
snapshot: DocumentSnapshot
61+
): SerializerOptions {
62+
this.snapshotCache[snapshot.ref.path] = snapshot
63+
64+
return {
65+
type: 'snapshot',
66+
path: snapshot.ref.path,
67+
}
68+
}
69+
70+
private static serializeNumberOrSnapshot(
71+
value?: number | DocumentSnapshot
72+
): SerializerOptions | number | undefined {
73+
if (!value) return undefined
74+
if (typeof value === 'number') return value
75+
76+
return this.serializeSnapshot(value)
77+
}
78+
79+
// Serializer function for query
80+
public static serializeQuery<Data extends object = {}>(
81+
query: CollectionQueryType<Data>,
82+
fuego: Fuego
83+
): string {
84+
const { where, startAt, endAt, startAfter, endBefore, ...rest } = query
85+
86+
return JSON.stringify({
87+
where: where ? this.serializeWhere(where, fuego) : undefined,
88+
startAt: this.serializeNumberOrSnapshot(startAt),
89+
endAt: this.serializeNumberOrSnapshot(endAt),
90+
startAfter: this.serializeNumberOrSnapshot(startAfter),
91+
endBefore: this.serializeNumberOrSnapshot(endBefore),
92+
...rest,
93+
})
94+
}
95+
96+
// Deserializer function for where condition
97+
private static deserializeWhere<Doc extends object = {}>(
98+
where: WhereType<Doc>,
99+
fuego: Fuego
100+
): WhereType<Doc> {
101+
if (this.multipleConditions(where)) {
102+
return where.map(w => this.deserializeWhere(w, fuego)) as WhereArray<Doc>
103+
}
104+
105+
if (where[3]?.type === 'date' && typeof where[2] === 'string') {
106+
return [...where.slice(0, 2), new Date(where[2]), where[3]] as WhereItem<
107+
Doc
108+
>
109+
}
110+
111+
if (where[3]?.type === 'ref' && typeof where[2] === 'string') {
112+
return [
113+
...where.slice(0, 2),
114+
fuego.db.doc(where[2]),
115+
where[3],
116+
] as WhereItem<Doc>
117+
}
118+
119+
return where
120+
}
121+
122+
// Deserializer function for document snapshots
123+
private static deserializeSnapshot(
124+
snapshot: SerializerOptions
125+
): DocumentSnapshot | undefined {
126+
if (snapshot.type !== 'snapshot') return
127+
128+
return this.snapshotCache[snapshot.path]
129+
}
130+
131+
private static deserializeNumberOrSnapshot(
132+
value?: number | SerializerOptions
133+
): DocumentSnapshot | number | undefined {
134+
if (!value) return undefined
135+
if (typeof value === 'number') return value
136+
137+
return this.deserializeSnapshot(value)
138+
}
139+
140+
// Deserializer function for query
141+
public static deserializeQuery<Data extends object = {}>(
142+
queryString: string,
143+
fuego: Fuego
144+
): CollectionQueryType<Data> | undefined {
145+
const query: SerializedCollectionQueryType<Data> = JSON.parse(queryString)
146+
if (!query) return
147+
148+
const { where, startAt, endAt, startAfter, endBefore, ...rest } = query
149+
150+
return {
151+
where: where ? this.deserializeWhere(where, fuego) : undefined,
152+
startAt: this.deserializeNumberOrSnapshot(startAt),
153+
endAt: this.deserializeNumberOrSnapshot(endAt),
154+
startAfter: this.deserializeNumberOrSnapshot(startAfter),
155+
endBefore: this.deserializeNumberOrSnapshot(endBefore),
156+
...rest,
157+
}
158+
}
159+
160+
public static cleanQuery<Data extends object = {}>(
161+
query: CollectionQueryType<Data>,
162+
fuego: Fuego
163+
) {
164+
const { startAt, endAt, startAfter, endBefore } = query
165+
166+
if (this.isSnapshot(startAt, fuego)) {
167+
delete this.snapshotCache[(startAt as DocumentSnapshot).ref.path]
168+
}
169+
if (this.isSnapshot(endAt, fuego)) {
170+
delete this.snapshotCache[(endAt as DocumentSnapshot).ref.path]
171+
}
172+
if (this.isSnapshot(startAfter, fuego)) {
173+
delete this.snapshotCache[(startAfter as DocumentSnapshot).ref.path]
174+
}
175+
if (this.isSnapshot(endBefore, fuego)) {
176+
delete this.snapshotCache[(endBefore as DocumentSnapshot).ref.path]
177+
}
178+
}
179+
}

src/hooks/use-swr-collection-group.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1+
import { CollectionQueryType } from '../types/Query'
12
import { Document } from '../types'
2-
import {
3-
CollectionQueryType,
4-
CollectionSWROptions,
5-
useCollection,
6-
} from './use-swr-collection'
3+
import { CollectionSWROptions, useCollection } from './use-swr-collection'
74

85
// type UseCollection = Parameters<typeof useCollection>
96

0 commit comments

Comments
 (0)