├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── link_store.js ├── package.json ├── src ├── base64.js ├── connection.js └── index.js └── test └── index-test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "indent": [2, 2], 9 | "brace-style": [2, "1tbs"], 10 | "comma-style": [2, "last"], 11 | "curly": [2, "multi-line"], 12 | "quotes": [2, "single"], 13 | "strict": 0, 14 | "camelcase": 0, 15 | "no-debugger": 2, 16 | "no-console": 2, 17 | "no-undef": 2, 18 | "no-underscore-dangle": 0, 19 | "no-mixed-requires": 0, 20 | "no-use-before-define": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | coverage 4 | *.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | coverage 3 | *.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | - 4.1 6 | 7 | branches: 8 | only: 9 | - master 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dow Jones & Company 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL DynamoDB Connections 2 | [![Build Status](https://secure.travis-ci.org/dowjones/graphql-dynamodb-connections.png)](http://travis-ci.org/dowjones/graphql-dynamodb-connections) [![NPM version](https://badge.fury.io/js/graphql-dynamodb-connections.svg)](http://badge.fury.io/js/graphql-dynamodb-connections) 3 | 4 | This is an adapter library that converts DynamoDB-style pagination to 5 | [GraphQL Connection](https://facebook.github.io/relay/graphql/connections.htm)-style pagination. 6 | 7 | 8 | ## Usage 9 | 10 | ```js 11 | import { 12 | paginationToParams, 13 | dataToConnection 14 | } from 'graphql-dynamodb-connections'; 15 | 16 | const userConnections = { 17 | type: userConnection, 18 | args: connectionArgs, 19 | resolve: ((_, args) => { 20 | return promisifiedDocumentClient.scan({ 21 | TableName: 'users', 22 | ...paginationToParams(args) 23 | }) 24 | .then(dataToConnection); 25 | }) 26 | }; 27 | ``` 28 | 29 | You can find more examples in [the examples folder](/examples). 30 | 31 | 32 | ## API 33 | 34 | - `paginationToParams(connectionArgs)` -- adapts connection-args to DynamoDB params 35 | - `dataToConnection(data)` -- converts the data returned by DynamoDB into a [Connection type](https://facebook.github.io/relay/graphql/connections.htm#sec-Connection-Types) 36 | 37 | 38 | ## Related 39 | 40 | [GraphQL REST Connections](https://github.com/dowjones/graphql-rest-connections) 41 | 42 | 43 | ## License 44 | 45 | [MIT](/LICENSE) 46 | -------------------------------------------------------------------------------- /examples/link_store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This example shows how to use relay, graphql and dynamoDB with AWS lambda 5 | * - It uses graphql-dynamodb-connections to convert DynamoDB-style pagination to 6 | * GraphQL Connection-style pagination 7 | */ 8 | 9 | const AWS = require('aws-sdk') 10 | 11 | const GraphQLObjectType = require('graphql').GraphQLObjectType 12 | const GraphQLSchema = require('graphql').GraphQLSchema 13 | const GraphQLString = require('graphql').GraphQLString 14 | const GraphQLNonNull = require('graphql').GraphQLNonNull 15 | const GraphQLID = require('graphql').GraphQLID 16 | 17 | const connectionArgs = require('graphql-relay').connectionArgs 18 | const connectionDefinitions = require('graphql-relay').connectionDefinitions 19 | 20 | const paginationToParams = require('graphql-dynamodb-connections').paginationToParams 21 | const dataToConnection = require('graphql-dynamodb-connections').dataToConnection 22 | 23 | const dynamoConfig = { 24 | sessionToken: process.env.AWS_SESSION_TOKEN, 25 | region: process.env.AWS_REGION 26 | } 27 | 28 | const docClient = new AWS.DynamoDB.DocumentClient(dynamoConfig) 29 | 30 | const store = {} 31 | 32 | const Store = new GraphQLObjectType({ 33 | name: 'Store', 34 | fields: () => ({ 35 | linkConnection: { 36 | type: linkConnection.connectionType, 37 | args: connectionArgs, 38 | resolve: (_, args) => { 39 | return docClient.scan( 40 | Object.assign( 41 | {}, 42 | {TableName: 'links'}, 43 | paginationToParams(args) 44 | ) 45 | ).promise().then(dataToConnection) 46 | } 47 | } 48 | }) 49 | }) 50 | 51 | const Link = new GraphQLObjectType({ 52 | name: 'Link', 53 | fields: () => ({ 54 | id: { 55 | type: new GraphQLNonNull(GraphQLID), 56 | resolve: (obj) => obj.id 57 | }, 58 | title: { type: GraphQLString }, 59 | url: { type: GraphQLString } 60 | }) 61 | }) 62 | 63 | const linkConnection = connectionDefinitions({ 64 | name: 'Link', 65 | nodeType: Link 66 | }) 67 | 68 | const schema = new GraphQLSchema({ 69 | query: new GraphQLObjectType({ 70 | name: 'Query', 71 | fields: () => ({ 72 | store: { 73 | type: Store, 74 | resolve: () => store 75 | } 76 | }) 77 | }) 78 | }) 79 | 80 | module.exports = schema 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-dynamodb-connections", 3 | "version": "1.0.1", 4 | "description": "AWS DynamoDB utilities to simplify working with GraphQL connections", 5 | "keywords": [ 6 | "graphql", 7 | "dynamodb", 8 | "connections", 9 | "pagination" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/dowjones/graphql-dynamodb-connections.git" 14 | }, 15 | "author": "nemtsov@gmail.com", 16 | "main": "lib/index", 17 | "main-es6": "src/index", 18 | "license": "MIT", 19 | "scripts": { 20 | "prepublish": "babel src -d lib --optional runtime", 21 | "lint": "eslint src test", 22 | "test": "npm run lint && npm run test-cover && npm run test-check-coverage", 23 | "test-watch": "babel-node ./node_modules/.bin/_mocha --require should --recursive --reporter min --watch", 24 | "test-cover": "babel-node ./node_modules/.bin/babel-istanbul cover _mocha -- --require should --recursive", 25 | "test-check-coverage": "babel-istanbul check-coverage --statements 100 --functions 100 --branches 100 --lines 100" 26 | }, 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "babel-cli": "^6.4.0", 30 | "babel-eslint": "^5.0.0-beta6", 31 | "babel-istanbul": "^0.6.0", 32 | "babel-plugin-transform-runtime": "^6.4.3", 33 | "babel-preset-es2015": "^6.3.13", 34 | "babel-preset-stage-0": "^6.3.13", 35 | "babel-register": "^6.4.3", 36 | "eslint": "^1.10.3", 37 | "istanbul": "^0.4.2", 38 | "mocha": "^2.3.4", 39 | "should": "^8.1.1" 40 | }, 41 | "babel": { 42 | "presets": [ 43 | "es2015", 44 | "stage-0" 45 | ] 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/base64.js: -------------------------------------------------------------------------------- 1 | export function base64(i) { 2 | return ((new Buffer(i, 'ascii')).toString('base64')); 3 | } 4 | 5 | export function unbase64(i) { 6 | return ((new Buffer(i, 'base64')).toString('ascii')); 7 | } 8 | -------------------------------------------------------------------------------- /src/connection.js: -------------------------------------------------------------------------------- 1 | import {base64, unbase64} from './base64'; 2 | 3 | const PREFIX = 'dynamodbconnection:'; 4 | 5 | export function paginationToParams({first, after}) { 6 | const params = {Limit: first}; 7 | if (after) params.ExclusiveStartKey = {id: cursorToId(after)}; 8 | return params; 9 | } 10 | 11 | /** 12 | * Accepts the `data` (serialized response) from DynamoDB 13 | * and converts that output to a Connection object that 14 | * follows the (GraphQL) Relay Cursor Connections Specification: 15 | * https://facebook.github.io/relay/graphql/connections.htm. 16 | * 17 | * Inspired by the `graphql-relay-js` `connection/arrayconnection.js` 18 | * https://github.com/graphql/graphql-relay-js/blob/87d865e0623d6ee0c799dcbc\ 19 | * 266e9e4c68bfc5d3/src/connection/arrayconnection.js 20 | */ 21 | 22 | export function dataToConnection({Items, LastEvaluatedKey}) { 23 | const edges = Items.map(value => ({ 24 | cursor: idToCursor(value.id), 25 | node: value, 26 | })); 27 | 28 | const firstEdge = edges[0]; 29 | const lastEdge = edges[edges.length - 1]; 30 | 31 | return { 32 | edges, 33 | pageInfo: { 34 | startCursor: firstEdge ? firstEdge.cursor : null, 35 | endCursor: lastEdge ? lastEdge.cursor : null, 36 | hasPreviousPage: false, 37 | 38 | //TODO: fix bug: if count=2 and first=2 LastEvaluatedKey will exist 39 | //not sure how to resolve this edge case yet 40 | hasNextPage: !!LastEvaluatedKey, 41 | } 42 | }; 43 | } 44 | 45 | const idToCursor = id => base64(PREFIX + id); 46 | const cursorToId = cursor => 47 | unbase64(cursor).substring(PREFIX.length); 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './connection'; 2 | -------------------------------------------------------------------------------- /test/index-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | paginationToParams, 3 | dataToConnection 4 | } from '../src'; 5 | 6 | describe('graphql-dynamodb-connections', () => { 7 | describe('paginationToParams', () => { 8 | it('should convert pagination to params', () => { 9 | paginationToParams({ 10 | first: 10, 11 | after: 'ZHluYW1vZGJjb25uZWN0aW9uOmI=' 12 | }).should.eql({ 13 | ExclusiveStartKey: {id: 'b'}, 14 | Limit: 10 15 | }); 16 | }); 17 | 18 | it('should work without after', () => { 19 | paginationToParams({ 20 | first: 10, 21 | }).should.eql({ 22 | Limit: 10 23 | }); 24 | }); 25 | }); 26 | 27 | describe('dataToConnection', () => { 28 | it('should convert dynamodb data to connection', () => { 29 | dataToConnection({ 30 | Items: [{id: 'b'}], 31 | LastEvaluatedKey: 'b' 32 | }).should.eql({ 33 | edges: [{ 34 | cursor: 'ZHluYW1vZGJjb25uZWN0aW9uOmI=', 35 | node: {id: 'b'} 36 | }], 37 | pageInfo: { 38 | endCursor: 'ZHluYW1vZGJjb25uZWN0aW9uOmI=', 39 | hasNextPage: true, 40 | hasPreviousPage: false, 41 | startCursor: 'ZHluYW1vZGJjb25uZWN0aW9uOmI=' 42 | } 43 | }); 44 | }); 45 | 46 | it('should support an empty list of Items', () => { 47 | dataToConnection({ 48 | Items: [] 49 | }).should.eql({ 50 | edges: [], 51 | pageInfo: { 52 | endCursor: null, 53 | hasNextPage: false, 54 | hasPreviousPage: false, 55 | startCursor: null 56 | } 57 | }); 58 | }); 59 | }); 60 | }); 61 | --------------------------------------------------------------------------------