-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathindex.js
227 lines (189 loc) · 6.64 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
const { google } = require('googleapis')
const { convert } = require('firestore-adapter')
const moment = require('moment')
const uniqid = require('uniqid');
const firestore = google.firestore('v1')
// regex to get Id of the doc being requested (which is the end of the path/name/url)
const regex = /([^/]+$)/g
/**
* Processes the item for compatibility with the existing Firestore API
*
* @param {Object} item - the item to be processed, which is a single document
*/
const processItem = (item) => {
// we need to inject the `id` and the `data()` method to make it compatible with existing API
const id = item.name.match(regex)[0]
// normalize data from typed values into usable values
// https://cloud.google.com/firestore/docs/reference/rest/v1/Value
return {
id,
data: () => convert.docToData(item.fields)
}
}
const checkEnv = () => {
if (!process.env.GCLOUD_PROJECT) {
throw new Error('Missing GCLOUD_PROJECT environment variable')
}
if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
throw new Error('Missing GOOGLE_APPLICATION_CREDENTIALS environment variable')
}
}
/**
* A class that models the result of get
*/
class QueryResult {
constructor(response, queries) {
/** save whole data from response */
this.docs = response.data.documents.map(doc => processItem(doc))
/** filter the result (for where() function) */
if (queries) {
/** comparison operators */
const operators = {
'>': (a, b) => a > b,
'<': (a, b) => a < b,
'>=': (a, b) => a >= b,
'<=': (a, b) => a <= b,
'==': (a, b) => a == b,
'array-contains': (a, b) => a.indexOf(b) > -1
}
/** filter the result based on the queries */
this.docs = this.docs.filter(doc => {
const data = doc.data()
/** check if this doc matches the query */
const isValid = queries.every(query => {
const { fieldPath, opStr, value } = query
return operators[opStr](data[fieldPath], value)
})
return isValid
})
}
this.exists = !!this.docs.length
}
forEach(cb) {
this.docs.forEach(doc => cb(doc))
}
}
// https://schier.co/blog/2013/11/14/method-chaining-in-javascript.html
class Firestore {
constructor (value, query = null) {
this.path = (value || '')
if (query) {
this.queries ? this.queries.push(query) : this.queries = [query]
}
}
collection (path) {
const ref = new Firestore(`${this.path}/${path}`)
ref.set = ref.delete = undefined
return ref
}
doc (path) {
const ref = new Firestore(`${this.path}/${path}`)
ref.add = ref.where = undefined
return ref
}
where(fieldPath, opStr, value) {
/** new Firestore instance with path and query */
const ref = new Firestore(`${this.path}`, { fieldPath, opStr, value })
ref.add = ref.set = ref.doc = ref.collection = ref.delete = undefined
return ref
}
async get () {
checkEnv()
const auth = await google.auth.getClient({
scopes: 'https://www.googleapis.com/auth/cloud-platform'
})
// https://cloud.google.com/firestore/docs/reference/rest/v1/projects.databases.documents/get
const name = `projects/${process.env.GCLOUD_PROJECT}/databases/(default)/documents${this.path}`
let response
try {
response = await firestore.projects.databases.documents.get({
name,
auth
})
} catch (err) {
console.error(err)
}
/**
* There are a few things we still need to manage to make the API compatible with the existing
* Firestore api. The Firestore API returns a snapshot, which requires a `.data()` call to get
* the data, and it gives the `id` of the item.
*/
// first check if this is the result for a collection or a document. if it's for a collection,
// there will be a `data.documents` field. if not, there will just be `data`.
if (!response.data.documents) {
// since there is no documents field, we know that this is just a `.doc` call and there
// is only one item that will be returned
return processItem(response.data)
}
// if there is `result.data.documents`, then we are returning a collection, which means we
// need to map over each item being returned and add the same values
return new QueryResult(response, this.queries)
}
async set(data, options = {}) {
checkEnv()
const auth = await google.auth.getClient({
scopes: 'https://www.googleapis.com/auth/cloud-platform'
})
// https://cloud.google.com/firestore/docs/reference/rest/v1/projects.databases.documents/get
const name = `projects/${process.env.GCLOUD_PROJECT}/databases/(default)/documents${this.path}`
/** convert the data which is a json to Firestore document - https://cloud.google.com/firestore/docs/reference/rest/v1/projects.databases.documents#Document */
const resource = {
name,
...convert.dataToDoc(data),
}
const opts = {}
/** If merge set to `true`, the existing fields on server should be remained.
* If the document exists on the server and has fields not referenced in the mask, they are left unchanged.
* Patch function handles merge with `updateMask.fieldPaths` option. If it's not provided, it just set with the new data
*/
if (options.merge)
opts['updateMask.fieldPaths'] = Object.keys(data) // Set the fields of data
/** Update only specific fields */
if (options.mergeFields)
opts['updateMask.fieldPaths'] = options.mergeFields
let response
try {
response = await firestore.projects.databases.documents.patch({
name,
auth,
resource,
...opts
})
} catch (err) {
console.error(err)
}
const result = {
/** Javascript moment object */
writeTime: moment(response.data.updateTime),
/** isEqual function */
isEqual: (value) => {
const data = convert.docToData(response.data.fields)
return JSON.stringify(data) === JSON.stringify(value)
}
}
return result
}
async add(data) {
const id = uniqid()
return this.doc(id).set(data)
}
async delete() {
checkEnv()
const auth = await google.auth.getClient({
scopes: 'https://www.googleapis.com/auth/cloud-platform'
})
// https://cloud.google.com/firestore/docs/reference/rest/v1/projects.databases.documents/get
const name = `projects/${process.env.GCLOUD_PROJECT}/databases/(default)/documents${this.path}`
let response
try {
response = await firestore.projects.databases.documents.delete({
name,
auth
})
} catch (err) {
console.error(err)
}
return response.data
}
}
module.exports = Firestore