Skip to content

Commit a06f5a8

Browse files
committed
Merge branch 'bitops-master'
2 parents 3881cb2 + 90411e3 commit a06f5a8

File tree

6 files changed

+114
-59
lines changed

6 files changed

+114
-59
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const server = new ApolloServer({
7171
})
7272
```
7373

74-
Inside the data source, the collection is available at `this.collection` (e.g. `this.collection.update({_id: 'foo, { $set: { name: 'me' }}})`). The model (if applicable) is available at `this.model` (`new this.model({ name: 'Alice' })`). The request's context is available at `this.context`. For example, if you put the logged-in user's ID on context as `context.currentUserId`:
74+
Inside the data source, the collection is available at `this.collection` (e.g. `this.collection.update({_id: 'foo, { $set: { name: 'me' }}})`). The model (if you're using Mongoose) is available at `this.model` (`new this.model({ name: 'Alice' })`). The request's context is available at `this.context`. For example, if you put the logged-in user's ID on context as `context.currentUserId`:
7575

7676
```js
7777
class Users extends MongoDataSource {
@@ -98,7 +98,7 @@ class Users extends MongoDataSource {
9898
}
9999
```
100100

101-
If you're passing a Mongoose model rather than a collection, Mongoose will be used for data fetching. All transformations definded on that model (virtuals, plugins, etc.) will be applied to your data before caching, just like you would expect it. If you're using reference fields, you might be interested in checking out [mongoose-autopopulate](https://www.npmjs.com/package/mongoose-autopopulate).
101+
If you're passing a Mongoose model rather than a collection, Mongoose will be used for data fetching. All transformations defined on that model (virtuals, plugins, etc.) will be applied to your data before caching, just like you would expect it. If you're using reference fields, you might be interested in checking out [mongoose-autopopulate](https://www.npmjs.com/package/mongoose-autopopulate).
102102

103103
### Batching
104104

@@ -287,3 +287,9 @@ this.findByFields({
287287
`this.deleteFromCacheById(id)`
288288

289289
Deletes a document from the cache that was fetched with `findOneById` or `findManyByIds`.
290+
291+
### deleteFromCacheByFields
292+
293+
`this.deleteFromCacheByFields(fields)`
294+
295+
Deletes a document from the cache that was fetched with `findByFields`. Fields should be passed in exactly the same way they were used to find with.

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ declare module 'apollo-datasource-mongodb' {
5757
): Promise<(TData | null | undefined)[]>
5858

5959
deleteFromCacheById(id: ObjectId | string): Promise<void>
60+
deleteFromCacheByFields(fields: Fields): Promise<void>
6061
}
6162
}

src/__tests__/cache.test.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import { EJSON } from 'bson'
66
import {
77
createCachingMethods,
88
idToString,
9-
isValidObjectIdString
9+
isValidObjectIdString,
10+
prepFields
1011
} from '../cache'
1112

13+
import { log } from '../helpers'
14+
1215
const hexId = '5cf82e14a220a607eb64a7d4'
1316

1417
const docs = {
@@ -31,17 +34,9 @@ const stringDoc = {
3134
const collectionName = 'test'
3235
const cacheKeyById = id => `mongo-${collectionName}-${idToString(id)}`
3336
const cacheKeyByFields = fields => {
34-
const cleanedFields = {}
35-
36-
Object.keys(fields).forEach(key => {
37-
if (typeof key !== 'undefined') {
38-
cleanedFields[key] = Array.isArray(fields[key])
39-
? fields[key]
40-
: [fields[key]]
41-
}
42-
})
37+
const { loaderKey } = prepFields(fields)
4338

44-
return `mongo-${collectionName}-${JSON.stringify(cleanedFields)}`
39+
return `mongo-${collectionName}-${loaderKey}`
4540
}
4641

4742
describe('createCachingMethods', () => {
@@ -111,8 +106,10 @@ describe('createCachingMethods', () => {
111106
it('adds the right methods', () => {
112107
expect(api.findOneById).toBeDefined()
113108
expect(api.findManyByIds).toBeDefined()
114-
expect(api.findByFields).toBeDefined()
115109
expect(api.deleteFromCacheById).toBeDefined()
110+
111+
expect(api.findByFields).toBeDefined()
112+
expect(api.deleteFromCacheByFields).toBeDefined()
116113
})
117114

118115
it('finds one with ObjectId', async () => {
@@ -171,7 +168,7 @@ describe('createCachingMethods', () => {
171168
expect(collection.find.mock.calls.length).toBe(1)
172169
})
173170

174-
it('finds by mutiple fields', async () => {
171+
it('finds by multiple fields', async () => {
175172
const foundDocs = await api.findByFields({
176173
tags: ['foo', 'bar'],
177174
foo: 'bar'
@@ -222,7 +219,7 @@ describe('createCachingMethods', () => {
222219
expect(value).toBeUndefined()
223220
})
224221

225-
it(`caches`, async () => {
222+
it(`caches by ID`, async () => {
226223
await api.findOneById(docs.one._id, { ttl: 1 })
227224
const value = await cache.get(cacheKeyById(docs.one._id))
228225
expect(value).toEqual(EJSON.stringify(docs.one))
@@ -241,15 +238,15 @@ describe('createCachingMethods', () => {
241238
expect(collection.find.mock.calls.length).toBe(1)
242239
})
243240

244-
it(`caches with ttl`, async () => {
241+
it(`caches ID with ttl`, async () => {
245242
await api.findOneById(docs.one._id, { ttl: 1 })
246243
await wait(1001)
247244

248245
const value = await cache.get(cacheKeyById(docs.one._id))
249246
expect(value).toBeUndefined()
250247
})
251248

252-
it(`deletes from cache`, async () => {
249+
it(`deletes from cache by ID`, async () => {
253250
for (const doc of [docs.one, docs.two, stringDoc]) {
254251
await api.findOneById(doc._id, { ttl: 1 })
255252

@@ -263,7 +260,7 @@ describe('createCachingMethods', () => {
263260
}
264261
})
265262

266-
it('deletes from DataLoader cache', async () => {
263+
it('deletes from DataLoader cache by ID', async () => {
267264
for (const id of [docs.one._id, docs.two._id, stringDoc._id]) {
268265
await api.findOneById(id)
269266
expect(collection.find).toHaveBeenCalled()
@@ -277,6 +274,19 @@ describe('createCachingMethods', () => {
277274
expect(collection.find).toHaveBeenCalled()
278275
}
279276
})
277+
278+
it(`deletes from cache by fields`, async () => {
279+
const fields = { foo: 'bar' }
280+
await api.findByFields(fields, { ttl: 1 })
281+
282+
const valueBefore = await cache.get(cacheKeyByFields(fields))
283+
expect(valueBefore).toEqual(EJSON.stringify([docs.one, docs.two]))
284+
285+
await api.deleteFromCacheByFields(fields)
286+
287+
const valueAfter = await cache.get(cacheKeyByFields(fields))
288+
expect(valueAfter).toBeUndefined()
289+
})
280290
})
281291

282292
describe('isValidObjectIdString', () => {

src/__tests__/datasource.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ describe('MongoDataSource', () => {
1818
const source = new Users(users)
1919
source.initialize()
2020
expect(source.findOneById).toBeDefined()
21+
expect(source.findByFields).toBeDefined()
22+
expect(source.deleteFromCacheById).toBeDefined()
23+
expect(source.deleteFromCacheByFields).toBeDefined()
2124
expect(source.collection).toEqual(users)
2225
})
2326
})

src/cache.js

Lines changed: 71 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@ import DataLoader from 'dataloader'
22
import { ObjectId } from 'mongodb'
33
import { EJSON } from 'bson'
44

5-
import { getCollection } from './helpers'
5+
import { getCollection, log } from './helpers'
66

7-
export const idToString = id => (id instanceof ObjectId ? id.toHexString() : id)
7+
export const idToString = id => {
8+
if (id instanceof ObjectId) {
9+
return id.toHexString()
10+
} else {
11+
return id && id.toString ? id.toString() : id
12+
}
13+
}
814

915
// https://www.geeksforgeeks.org/how-to-check-if-a-string-is-valid-mongodb-objectid-in-nodejs/
1016
export const isValidObjectIdString = string =>
@@ -22,7 +28,21 @@ export const stringToId = string => {
2228
return string
2329
}
2430

25-
const fieldToDocField = key => (key === 'id' ? '_id' : key)
31+
export function prepFields(fields) {
32+
const cleanedFields = {}
33+
34+
Object.keys(fields)
35+
.sort()
36+
.forEach(key => {
37+
if (typeof key !== 'undefined') {
38+
cleanedFields[key] = Array.isArray(fields[key])
39+
? fields[key]
40+
: [fields[key]]
41+
}
42+
})
43+
44+
return { loaderKey: EJSON.stringify(cleanedFields), cleanedFields }
45+
}
2646

2747
// https://github.com/graphql/dataloader#batch-function
2848
// "The Array of values must be the same length as the Array of keys."
@@ -32,82 +52,97 @@ const orderDocs = fieldsArray => docs =>
3252
docs.filter(doc => {
3353
for (let fieldName of Object.keys(fields)) {
3454
const fieldValue = fields[fieldName]
55+
3556
if (typeof fieldValue === 'undefined') continue
57+
3658
const filterValuesArr = Array.isArray(fieldValue)
3759
? fieldValue.map(val => idToString(val))
3860
: [idToString(fieldValue)]
39-
const docValue = doc[fieldToDocField(fieldName)]
61+
62+
const docValue = doc[fieldName]
4063
const docValuesArr = Array.isArray(docValue)
4164
? docValue.map(val => idToString(val))
4265
: [idToString(docValue)]
66+
4367
let isMatch = false
4468
for (const filterVal of filterValuesArr) {
4569
if (docValuesArr.includes(filterVal)) {
4670
isMatch = true
4771
}
4872
}
73+
4974
if (!isMatch) return false
5075
}
5176
return true
5277
})
5378
)
5479

5580
export const createCachingMethods = ({ collection, model, cache }) => {
56-
const loader = new DataLoader(jsonArray => {
57-
const fieldsArray = jsonArray.map(JSON.parse)
81+
const loader = new DataLoader(async ejsonArray => {
82+
const fieldsArray = ejsonArray.map(EJSON.parse)
83+
log('fieldsArray', fieldsArray)
84+
5885
const filterArray = fieldsArray.reduce((filterArray, fields) => {
5986
const existingFieldsFilter = filterArray.find(
6087
filter =>
6188
[...Object.keys(filter)].sort().join() ===
6289
[...Object.keys(fields)].sort().join()
6390
)
6491
const filter = existingFieldsFilter || {}
92+
6593
for (const fieldName in fields) {
6694
if (typeof fields[fieldName] === 'undefined') continue
67-
const docFieldName = fieldToDocField(fieldName)
68-
if (!filter[docFieldName]) filter[docFieldName] = { $in: [] }
95+
if (!filter[fieldName]) filter[fieldName] = { $in: [] }
6996
let newVals = Array.isArray(fields[fieldName])
7097
? fields[fieldName]
7198
: [fields[fieldName]]
7299

73-
filter[docFieldName].$in = [
74-
...filter[docFieldName].$in,
100+
filter[fieldName].$in = [
101+
...filter[fieldName].$in,
75102
...newVals
76103
.map(stringToId)
77-
.filter(val => !filter[docFieldName].$in.includes(val))
104+
.filter(val => !filter[fieldName].$in.includes(val))
78105
]
79106
}
80107
if (existingFieldsFilter) return filterArray
81108
return [...filterArray, filter]
82109
}, [])
110+
83111
const filter =
84112
filterArray.length === 1
85113
? filterArray[0]
86114
: {
87115
$or: filterArray
88116
}
89-
const promise = model
117+
118+
const findPromise = model
90119
? model.find(filter).exec()
91120
: collection.find(filter).toArray()
92121

93-
return promise.then(orderDocs(fieldsArray))
122+
const results = await findPromise
123+
const orderedDocs = orderDocs(fieldsArray)(results)
124+
log('orderedDocs: ', orderedDocs)
125+
return orderedDocs
94126
})
95127

96128
const cachePrefix = `mongo-${getCollection(collection).collectionName}-`
97129

98130
const methods = {
99-
findOneById: async (id, { ttl } = {}) => {
100-
const key = cachePrefix + idToString(id)
131+
findOneById: async (_id, { ttl } = {}) => {
132+
const cacheKey = cachePrefix + idToString(_id)
101133

102-
const cacheDoc = await cache.get(key)
134+
const cacheDoc = await cache.get(cacheKey)
135+
log('findOneById found in cache:', cacheDoc)
103136
if (cacheDoc) {
104137
return EJSON.parse(cacheDoc)
105138
}
106139

107-
const docs = await loader.load(JSON.stringify({ id }))
140+
log(`Dataloader.load: ${EJSON.stringify({ _id })}`)
141+
const docs = await loader.load(EJSON.stringify({ _id }))
142+
log('Dataloader.load returned: ', docs)
108143
if (Number.isInteger(ttl)) {
109144
// https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-caching#apollo-server-caching
110-
cache.set(key, EJSON.stringify(docs[0]), { ttl })
145+
cache.set(cacheKey, EJSON.stringify(docs[0]), { ttl })
111146
}
112147

113148
return docs[0]
@@ -116,23 +151,10 @@ export const createCachingMethods = ({ collection, model, cache }) => {
116151
return Promise.all(ids.map(id => methods.findOneById(id, { ttl })))
117152
},
118153
findByFields: async (fields, { ttl } = {}) => {
119-
const cleanedFields = {}
120-
121-
Object.keys(fields)
122-
.sort()
123-
.forEach(key => {
124-
if (typeof key !== 'undefined') {
125-
cleanedFields[key] = Array.isArray(fields[key])
126-
? fields[key]
127-
: [fields[key]]
128-
}
129-
})
130-
131-
const loaderJSON = JSON.stringify(cleanedFields)
132-
133-
const key = cachePrefix + loaderJSON
154+
const { cleanedFields, loaderKey } = prepFields(fields)
155+
const cacheKey = cachePrefix + loaderKey
134156

135-
const cacheDoc = await cache.get(key)
157+
const cacheDoc = await cache.get(cacheKey)
136158
if (cacheDoc) {
137159
return EJSON.parse(cacheDoc)
138160
}
@@ -147,24 +169,33 @@ export const createCachingMethods = ({ collection, model, cache }) => {
147169
fieldArray.map(value => {
148170
const filter = {}
149171
filter[fieldNames[0]] = value
150-
return loader.load(JSON.stringify(filter))
172+
return loader.load(EJSON.stringify(filter))
151173
})
152174
)
153175
docs = [].concat(...docsArray)
154176
} else {
155-
docs = await loader.load(loaderJSON)
177+
docs = await loader.load(loaderKey)
156178
}
157179

158180
if (Number.isInteger(ttl)) {
159181
// https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-caching#apollo-server-caching
160-
cache.set(key, EJSON.stringify(docs), { ttl })
182+
cache.set(cacheKey, EJSON.stringify(docs), { ttl })
161183
}
162184

163185
return docs
164186
},
165-
deleteFromCacheById: async id => {
166-
loader.clear(JSON.stringify({ id }))
167-
await cache.delete(cachePrefix + idToString(id))
187+
deleteFromCacheById: async _id => {
188+
loader.clear(EJSON.stringify({ _id }))
189+
const cacheKey = cachePrefix + idToString(_id)
190+
log('Deleting cache key: ', cacheKey)
191+
await cache.delete(cacheKey)
192+
},
193+
deleteFromCacheByFields: async fields => {
194+
const { loaderKey } = prepFields(fields)
195+
const cacheKey = cachePrefix + loaderKey
196+
197+
loader.clear(loaderKey)
198+
await cache.delete(cacheKey)
168199
}
169200
}
170201

src/helpers.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ export const isCollectionOrModel = x =>
1414
Boolean(x && (typeof x === TYPEOF_COLLECTION || isModel(x)))
1515

1616
export const getCollection = x => (isModel(x) ? x.collection : x)
17+
18+
const DEBUG = false
19+
20+
export const log = (...args) => DEBUG && console.log(...args)

0 commit comments

Comments
 (0)