Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apollo Server 4 Upgrade #123

Merged
merged 23 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7765b1d
Removed apollo datasource and initialize method from MongoDataSource
nnoce14 May 8, 2023
ea05c59
Added type for constructor options argument to include modelOrCollect…
nnoce14 May 8, 2023
7e8c494
Updated the tests for datasource to accomodate the recent changes
nnoce14 May 8, 2023
ef0ed9e
Removed deprecated packages and installed new cache package, updated …
nnoce14 May 8, 2023
05e0cfc
Added two tests to ensure that you can add a context to the construct…
nnoce14 May 8, 2023
652760e
Updated README and package version
nnoce14 May 8, 2023
7c90f2d
Merge pull request #1 from nnoce14:apollo-server-4-upgrade
nnoce14 May 8, 2023
2fa5932
Fixed some mistakes and missing info in the README
nnoce14 May 8, 2023
5f4abd7
Minor fixes to the README
nnoce14 May 8, 2023
0f73cd8
Final changes
nnoce14 May 8, 2023
b00de1b
Final commit
nnoce14 May 8, 2023
881d50c
Merge pull request #2 from nnoce14:fix-readme
nnoce14 May 8, 2023
2c9ef99
Fixed typing issue
nnoce14 May 8, 2023
c4317c0
Merge pull request #3 from nnoce14:typescript-generics-fix
nnoce14 May 8, 2023
bff0e91
Updated README and some package.json info
nnoce14 May 9, 2023
4080c43
Merge pull request #4 from nnoce14:readme-and-package-changes
nnoce14 May 9, 2023
eb58203
Removed npm version comment
nnoce14 May 9, 2023
37d932e
Fixed typo in readme
nnoce14 May 9, 2023
1556ea8
Fixed module name
nnoce14 Jul 5, 2023
a46e7ab
Added previous README and added note on versioning to both
nnoce14 Jul 7, 2023
90cf043
Fixed README links
nnoce14 Jul 7, 2023
e4940e6
Updated mongoose from v5 to v7 and updated mongodb from v3 to v4
nnoce14 Jul 7, 2023
4d9f038
Updated bson to 5.4.0 and mongodb to 5.7.0
nnoce14 Jul 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 190 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
[![npm version](https://badge.fury.io/js/apollo-datasource-mongodb.svg)](https://www.npmjs.com/package/apollo-datasource-mongodb)

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

Note: This README applies to the current version 0.6.0 and is meant to be paired with Apollo Server 4.
See the old [README](README.old.md) for versions 0.5.4 and below, if you are using Apollo Server 3.

**Installation**
```
npm i apollo-datasource-mongodb
```

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:
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:

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


<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

**Contents:**

- [Usage](#usage)
Expand All @@ -31,7 +35,6 @@ This package uses [DataLoader](https://github.com/graphql/dataloader) for batchi

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


## Usage

### Basic
Expand All @@ -54,6 +57,8 @@ and:

```js
import { MongoClient } from 'mongodb'
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'

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

Expand All @@ -62,23 +67,31 @@ client.connect()

const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
users: new Users(client.db().collection('users'))
// OR
// users: new Users(UserModel)
})
resolvers
})

const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
dataSources: {
users: new Users({ modelOrCollection: client.db().collection('users') })
// OR
// users: new Users({ modelOrCollection: UserModel })
}
}),
})
```

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`:
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`:

```js
class Users extends MongoDataSource {
...
constructor(options) {
super(options)
this.currentUserId = options.currentUserId
}

async getPrivateUserData(userId) {
const isAuthorized = this.context.currentUserId === userId
const isAuthorized = this.currentUserId === userId
if (isAuthorized) {
const user = await this.findOneById(userId)
return user && user.privateData
Expand All @@ -87,15 +100,65 @@ class Users extends MongoDataSource {
}
```

If you want to implement an initialize method, it must call the parent method:
and you would instantiate the Users data source in the context like this

```js
...
const server = new ApolloServer({
typeDefs,
resolvers
})

const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const currentUserId = getCurrentUserId(req) // not a real function, for demo purposes only
return {
currentUserId,
dataSources: {
users: new Users({ modelOrCollection: UserModel, currentUserId })
},
}
},
});
```

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.
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.

```js
class Users extends MongoDataSource {
initialize(config) {
super.initialize(config)
...
constructor(options) {
super(options)
this.context = options.context
}

async getPrivateUserData(userId) {
const isAuthorized = this.context.currentUserId === userId
if (isAuthorized) {
const user = await this.findOneById(userId)
return user && user.privateData
}
}
}

...

class Context {
constructor(req) {
this.currentUserId = getCurrentUserId(req), // not a real function, for demo purposes only
this.dataSources = {
users: new Users({ modelOrCollection: UserModel, context: this })
},
}
}

...

const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
return new Context(req)
},
});
```

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).
Expand All @@ -119,7 +182,8 @@ class Posts extends MongoDataSource {

const resolvers = {
Post: {
author: (post, _, { dataSources: { users } }) => users.getUser(post.authorId)
author: (post, _, { dataSources: { users } }) =>
users.getUser(post.authorId)
},
User: {
posts: (user, _, { dataSources: { posts } }) => posts.getPosts(user.postIds)
Expand All @@ -128,11 +192,16 @@ const resolvers = {

const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
users: new Users(db.collection('users')),
posts: new Posts(db.collection('posts'))
})
resolvers
})

const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
dataSources: {
users: new Users({ modelOrCollection: db.collection('users') }),
posts: new Posts({ modelOrCollection: db.collection('posts') })
}
}),
})
```

Expand All @@ -150,11 +219,14 @@ class Users extends MongoDataSource {

updateUserName(userId, newName) {
this.deleteFromCacheById(userId)
return this.collection.updateOne({
_id: userId
}, {
$set: { name: newName }
})
return this.collection.updateOne(
{
_id: userId
},
{
$set: { name: newName }
}
)
}
}

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

### TypeScript

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:
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:

`data-sources/Users.ts`

Expand All @@ -189,12 +261,91 @@ interface UserDocument {
interests: [string]
}

// This is optional
interface Context {
loggedInUser: UserDocument
dataSources: any
}

export default class Users extends MongoDataSource<UserDocument> {
protected loggedInUser: UserDocument

constructor(options: { loggedInUser: UserDocument } & MongoDataSourceConfig<UserDocument>) {
super(options)
this.loggedInUser = options.loggedInUser
}

getUser(userId) {
// this.loggedInUser has type `UserDocument` as defined above
// this.findOneById has type `(id: ObjectId) => Promise<UserDocument | null | undefined>`
return this.findOneById(userId)
}
}
```

and:

```ts
import { MongoClient } from 'mongodb'

import Users from './data-sources/Users.ts'

const client = new MongoClient('mongodb://localhost:27017/test')
client.connect()

const server = new ApolloServer({
typeDefs,
resolvers
})

const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const loggedInUser = getLoggedInUser(req) // this function does not exist, just for demo purposes
return {
loggedInUser,
dataSources: {
users: new Users({ modelOrCollection: client.db().collection('users'), loggedInUser }),
},
}
},
});
```

You can also opt to pass the entire context into your data source class. You can do so by adding a protected context member
to your data source class and modifying to options argument of the constructor to add a field for the context. Then, call super and
assign the context to the member field on your data source class. Note: context needs to be a class in order to do this.

```ts
import { MongoDataSource } from 'apollo-datasource-mongodb'
import { ObjectId } from 'mongodb'

interface UserDocument {
_id: ObjectId
username: string
password: string
email: string
interests: [string]
}

class Context {
loggedInUser: UserDocument
dataSources: any

constructor(req: any) {
this.loggedInUser = getLoggedInUser(req)
this.dataSources = {
users: new Users({ modelOrCollection: client.db().collection('users'), context: this }),
}
}
}

export default class Users extends MongoDataSource<UserDocument, Context> {
export default class Users extends MongoDataSource<UserDocument> {
protected context: Context

constructor(options: { context: Context } & MongoDataSourceConfig<UserDocument>) {
super(options)
this.context = options.context
}

getUser(userId) {
// this.context has type `Context` as defined above
// this.findOneById has type `(id: ObjectId) => Promise<UserDocument | null | undefined>`
Expand All @@ -215,15 +366,17 @@ client.connect()

const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
users: new Users(client.db().collection('users'))
// OR
// users: new Users(UserModel)
})
resolvers
})

const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
return new Context(req)
},
});
```


## API

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