Skip to content

Commit 14c54e5

Browse files
committed
Apollo Server v4 Upgrade #123
1 parent 6eff2ac commit 14c54e5

File tree

7 files changed

+8348
-5654
lines changed

7 files changed

+8348
-5654
lines changed

README.md

+200-41
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
[![npm version](https://badge.fury.io/js/apollo-datasource-mongodb.svg)](https://www.npmjs.com/package/apollo-datasource-mongodb)
22

3-
Apollo [data source](https://www.apollographql.com/docs/apollo-server/features/data-sources) for MongoDB
3+
Apollo [data source](https://www.apollographql.com/docs/apollo-server/data/fetching-data) for MongoDB
44

5-
```
6-
npm i apollo-datasource-mongodb
7-
```
8-
9-
This package uses [DataLoader](https://github.com/graphql/dataloader) for batching and per-request memoization caching. It also optionally (if you provide a `ttl`) does shared application-level caching (using either the default Apollo `InMemoryLRUCache` or the [cache you provide to ApolloServer()](https://www.apollographql.com/docs/apollo-server/features/data-sources#using-memcachedredis-as-a-cache-storage-backend)). It does this for the following methods:
5+
This package uses [DataLoader](https://github.com/graphql/dataloader) for batching and per-request memoization caching. It also optionally (if you provide a `ttl`) does shared application-level caching (using either the default Apollo `InMemoryLRUCache` or the [cache you provide to ApolloServer()](https://www.apollographql.com/docs/apollo-server/performance/cache-backends#configuring-external-caching)). It does this for the following methods:
106

117
- [`findOneById(id, options)`](#findonebyid)
128
- [`findManyByIds(ids, options)`](#findmanybyids)
139
- [`findByFields(fields, options)`](#findbyfields)
1410

15-
1611
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
1712
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
18-
**Contents:**
1913

14+
- [Compatibility](#compatibility)
2015
- [Usage](#usage)
16+
- [Install](#install)
2117
- [Basic](#basic)
2218
- [Batching](#batching)
2319
- [Caching](#caching)
@@ -28,12 +24,25 @@ This package uses [DataLoader](https://github.com/graphql/dataloader) for batchi
2824
- [findByFields](#findbyfields)
2925
- [Examples](#examples)
3026
- [deleteFromCacheById](#deletefromcachebyid)
27+
- [deleteFromCacheByFields](#deletefromcachebyfields)
3128

3229
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
3330

31+
## Compatibility
32+
33+
| Apollo Server Version | `apollo-datasource-mongodb` |
34+
|-----------------------|-----------------------------------------------------------------------------------------|
35+
| 4.* | >= 0.6.0 |
36+
| 3.* | <= [0.5.4](https://github.com/GraphQLGuide/apollo-datasource-mongodb/tree/0.5.4#readme) |
3437

3538
## Usage
3639

40+
### Install
41+
42+
```
43+
npm i apollo-datasource-mongodb
44+
```
45+
3746
### Basic
3847

3948
The basic setup is subclassing `MongoDataSource`, passing your collection or Mongoose model to the constructor, and using the [API methods](#API):
@@ -54,6 +63,8 @@ and:
5463

5564
```js
5665
import { MongoClient } from 'mongodb'
66+
import { ApolloServer } from '@apollo/server'
67+
import { startStandaloneServer } from '@apollo/server/standalone'
5768

5869
import Users from './data-sources/Users.js'
5970

@@ -62,23 +73,31 @@ client.connect()
6273

6374
const server = new ApolloServer({
6475
typeDefs,
65-
resolvers,
66-
dataSources: () => ({
67-
users: new Users(client.db().collection('users'))
68-
// OR
69-
// users: new Users(UserModel)
70-
})
76+
resolvers
77+
})
78+
79+
const { url } = await startStandaloneServer(server, {
80+
context: async ({ req }) => ({
81+
dataSources: {
82+
users: new Users({ modelOrCollection: client.db().collection('users') })
83+
// OR
84+
// users: new Users({ modelOrCollection: UserModel })
85+
}
86+
}),
7187
})
7288
```
7389

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`:
90+
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' })`). By default, the API classes you create will not have access to the context. You can either choose to add the data that your API class needs on a case-by-case basis as members of the class, or you can add the entire context as a member of the class if you wish. All you need to do is add the field(s) to the options argument of the constructor and call super passing in options. For example, if you put the logged-in user's ID on context as `context.currentUserId` and you want your Users class to have access to `currentUserId`:
7591

7692
```js
7793
class Users extends MongoDataSource {
78-
...
94+
constructor(options) {
95+
super(options)
96+
this.currentUserId = options.currentUserId
97+
}
7998

8099
async getPrivateUserData(userId) {
81-
const isAuthorized = this.context.currentUserId === userId
100+
const isAuthorized = this.currentUserId === userId
82101
if (isAuthorized) {
83102
const user = await this.findOneById(userId)
84103
return user && user.privateData
@@ -87,15 +106,65 @@ class Users extends MongoDataSource {
87106
}
88107
```
89108

90-
If you want to implement an initialize method, it must call the parent method:
109+
and you would instantiate the Users data source in the context like this
110+
111+
```js
112+
...
113+
const server = new ApolloServer({
114+
typeDefs,
115+
resolvers
116+
})
117+
118+
const { url } = await startStandaloneServer(server, {
119+
context: async ({ req }) => {
120+
const currentUserId = getCurrentUserId(req) // not a real function, for demo purposes only
121+
return {
122+
currentUserId,
123+
dataSources: {
124+
users: new Users({ modelOrCollection: UserModel, currentUserId })
125+
},
126+
}
127+
},
128+
});
129+
```
130+
131+
If you want your data source to have access to the entire context at `this.context`, you need to create a `Context` class so the context can refer to itself as `this` in the constructor for the data source.
132+
See [dataSources](https://www.apollographql.com/docs/apollo-server/migration/#datasources) for more information regarding how data sources changed from Apollo Server 3 to Apollo Server 4.
91133

92134
```js
93135
class Users extends MongoDataSource {
94-
initialize(config) {
95-
super.initialize(config)
96-
...
136+
constructor(options) {
137+
super(options)
138+
this.context = options.context
139+
}
140+
141+
async getPrivateUserData(userId) {
142+
const isAuthorized = this.context.currentUserId === userId
143+
if (isAuthorized) {
144+
const user = await this.findOneById(userId)
145+
return user && user.privateData
146+
}
147+
}
148+
}
149+
150+
...
151+
152+
class Context {
153+
constructor(req) {
154+
this.currentUserId = getCurrentUserId(req), // not a real function, for demo purposes only
155+
this.dataSources = {
156+
users: new Users({ modelOrCollection: UserModel, context: this })
157+
},
97158
}
98159
}
160+
161+
...
162+
163+
const { url } = await startStandaloneServer(server, {
164+
context: async ({ req }) => {
165+
return new Context(req)
166+
},
167+
});
99168
```
100169

101170
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).
@@ -119,7 +188,8 @@ class Posts extends MongoDataSource {
119188

120189
const resolvers = {
121190
Post: {
122-
author: (post, _, { dataSources: { users } }) => users.getUser(post.authorId)
191+
author: (post, _, { dataSources: { users } }) =>
192+
users.getUser(post.authorId)
123193
},
124194
User: {
125195
posts: (user, _, { dataSources: { posts } }) => posts.getPosts(user.postIds)
@@ -128,11 +198,16 @@ const resolvers = {
128198

129199
const server = new ApolloServer({
130200
typeDefs,
131-
resolvers,
132-
dataSources: () => ({
133-
users: new Users(db.collection('users')),
134-
posts: new Posts(db.collection('posts'))
135-
})
201+
resolvers
202+
})
203+
204+
const { url } = await startStandaloneServer(server, {
205+
context: async ({ req }) => ({
206+
dataSources: {
207+
users: new Users({ modelOrCollection: db.collection('users') }),
208+
posts: new Posts({ modelOrCollection: db.collection('posts') })
209+
}
210+
}),
136211
})
137212
```
138213

@@ -150,11 +225,14 @@ class Users extends MongoDataSource {
150225

151226
updateUserName(userId, newName) {
152227
this.deleteFromCacheById(userId)
153-
return this.collection.updateOne({
154-
_id: userId
155-
}, {
156-
$set: { name: newName }
157-
})
228+
return this.collection.updateOne(
229+
{
230+
_id: userId
231+
},
232+
{
233+
$set: { name: newName }
234+
}
235+
)
158236
}
159237
}
160238

@@ -173,7 +251,7 @@ Here we also call [`deleteFromCacheById()`](#deletefromcachebyid) to remove the
173251

174252
### TypeScript
175253

176-
Since we are using a typed language, we want the provided methods to be correctly typed as well. This requires us to make the `MongoDataSource` class polymorphic. It requires 1-2 template arguments. The first argument is the type of the document in our collection. The second argument is the type of context in our GraphQL server, which defaults to `any`. For example:
254+
Since we are using a typed language, we want the provided methods to be correctly typed as well. This requires us to make the `MongoDataSource` class polymorphic. It requires 1 template argument, which is the type of the document in our collection. If you wish to add additional fields to your data source class, you can extend the typing on constructor options argument to include any fields that you need. For example:
177255

178256
`data-sources/Users.ts`
179257

@@ -189,12 +267,91 @@ interface UserDocument {
189267
interests: [string]
190268
}
191269

192-
// This is optional
193270
interface Context {
194271
loggedInUser: UserDocument
272+
dataSources: any
195273
}
196274

197-
export default class Users extends MongoDataSource<UserDocument, Context> {
275+
export default class Users extends MongoDataSource<UserDocument> {
276+
protected loggedInUser: UserDocument
277+
278+
constructor(options: { loggedInUser: UserDocument } & MongoDataSourceConfig<UserDocument>) {
279+
super(options)
280+
this.loggedInUser = options.loggedInUser
281+
}
282+
283+
getUser(userId) {
284+
// this.loggedInUser has type `UserDocument` as defined above
285+
// this.findOneById has type `(id: ObjectId) => Promise<UserDocument | null | undefined>`
286+
return this.findOneById(userId)
287+
}
288+
}
289+
```
290+
291+
and:
292+
293+
```ts
294+
import { MongoClient } from 'mongodb'
295+
296+
import Users from './data-sources/Users.ts'
297+
298+
const client = new MongoClient('mongodb://localhost:27017/test')
299+
client.connect()
300+
301+
const server = new ApolloServer({
302+
typeDefs,
303+
resolvers
304+
})
305+
306+
const { url } = await startStandaloneServer(server, {
307+
context: async ({ req }) => {
308+
const loggedInUser = getLoggedInUser(req) // this function does not exist, just for demo purposes
309+
return {
310+
loggedInUser,
311+
dataSources: {
312+
users: new Users({ modelOrCollection: client.db().collection('users'), loggedInUser }),
313+
},
314+
}
315+
},
316+
});
317+
```
318+
319+
You can also opt to pass the entire context into your data source class. You can do so by adding a protected context member
320+
to your data source class and modifying to options argument of the constructor to add a field for the context. Then, call super and
321+
assign the context to the member field on your data source class. Note: context needs to be a class in order to do this.
322+
323+
```ts
324+
import { MongoDataSource } from 'apollo-datasource-mongodb'
325+
import { ObjectId } from 'mongodb'
326+
327+
interface UserDocument {
328+
_id: ObjectId
329+
username: string
330+
password: string
331+
email: string
332+
interests: [string]
333+
}
334+
335+
class Context {
336+
loggedInUser: UserDocument
337+
dataSources: any
338+
339+
constructor(req: any) {
340+
this.loggedInUser = getLoggedInUser(req)
341+
this.dataSources = {
342+
users: new Users({ modelOrCollection: client.db().collection('users'), context: this }),
343+
}
344+
}
345+
}
346+
347+
export default class Users extends MongoDataSource<UserDocument> {
348+
protected context: Context
349+
350+
constructor(options: { context: Context } & MongoDataSourceConfig<UserDocument>) {
351+
super(options)
352+
this.context = options.context
353+
}
354+
198355
getUser(userId) {
199356
// this.context has type `Context` as defined above
200357
// this.findOneById has type `(id: ObjectId) => Promise<UserDocument | null | undefined>`
@@ -215,15 +372,17 @@ client.connect()
215372

216373
const server = new ApolloServer({
217374
typeDefs,
218-
resolvers,
219-
dataSources: () => ({
220-
users: new Users(client.db().collection('users'))
221-
// OR
222-
// users: new Users(UserModel)
223-
})
375+
resolvers
224376
})
377+
378+
const { url } = await startStandaloneServer(server, {
379+
context: async ({ req }) => {
380+
return new Context(req)
381+
},
382+
});
225383
```
226384

385+
227386
## API
228387

229388
The type of the `id` argument must match the type used in the database. We currently support ObjectId and string types.

index.d.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
declare module 'apollo-datasource-mongodb' {
2-
import { DataSource } from 'apollo-datasource'
2+
import { KeyValueCache } from '@apollo/utils.keyvaluecache'
33
import { Collection as MongoCollection, ObjectId } from 'mongodb'
44
import {
55
Collection as MongooseCollection,
66
Document,
77
Model as MongooseModel,
88
} from 'mongoose'
99

10-
export type Collection<T, U = MongoCollection<T>> = T extends Document
10+
export type Collection<T extends { [key: string]: any }, U = MongoCollection<T>> = T extends Document
1111
? MongooseCollection
1212
: U
1313

1414
export type Model<T, U = MongooseModel<T>> = T extends Document
1515
? U
1616
: undefined
1717

18-
export type ModelOrCollection<T, U = Model<T>> = T extends Document
18+
export type ModelOrCollection<T extends { [key: string]: any }, U = Model<T>> = T extends Document
1919
? U
2020
: Collection<T>
2121

@@ -32,14 +32,16 @@ declare module 'apollo-datasource-mongodb' {
3232
ttl: number
3333
}
3434

35-
export class MongoDataSource<TData, TContext = any> extends DataSource<
36-
TContext
37-
> {
38-
protected context: TContext
35+
export interface MongoDataSourceConfig<TData extends { [key: string]: any }> {
36+
modelOrCollection: ModelOrCollection<TData>
37+
cache?: KeyValueCache<TData>
38+
}
39+
40+
export class MongoDataSource<TData extends { [key: string]: any }> {
3941
protected collection: Collection<TData>
4042
protected model: Model<TData>
4143

42-
constructor(modelOrCollection: ModelOrCollection<TData>)
44+
constructor(options: MongoDataSourceConfig<TData>)
4345

4446
findOneById(
4547
id: ObjectId | string,

0 commit comments

Comments
 (0)