├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── package.json ├── src ├── __tests__ │ ├── index.ts │ └── samples.ts ├── defs.ts └── index.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | coverage 4 | dist 5 | node_modules 6 | npm-debug.log 7 | package-lock.json 8 | typings 9 | yarn-error.log 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | typings 4 | tsconfig.json 5 | typings.json 6 | tslint.json 7 | dist/test 8 | yarn.lock 9 | coverage 10 | .vscode 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | node_js: 4 | - "8" 5 | 6 | install: 7 | - npm install apollo-link@1.2.13 graphql@14.5.8 apollo-utilities@1.3.2 8 | - npm install -g coveralls 9 | - npm install 10 | 11 | script: 12 | - npm test 13 | - npm run coverage 14 | - coveralls < ./coverage/lcov.info || true # ignore coveralls error 15 | 16 | # Allow Travis tests to run in containers. 17 | # sudo: false 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present Theodor Diaconu 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apollo Client Transformers 2 | 3 | [![Build Status](https://travis-ci.org/cult-of-coders/apollo-client-transformers.svg?branch=master)](https://travis-ci.org/cult-of-coders/apollo-client-transformers) 4 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 5 | 6 | This package is useful for when you have scalars, but you receive them serialized on the client and you don't really want to do the deserialisation in your view layer. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm i -S apollo-client-transform 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```js 17 | import { createTransformerLink } from 'apollo-client-transform'; 18 | 19 | const DateTransformer = { 20 | parseValue(time) { 21 | return new Date(time); 22 | }, 23 | }; 24 | 25 | const transformers = { 26 | User: { 27 | createdAt: DateTransformer, 28 | }, 29 | }; 30 | 31 | const transformerLink = createTransformerLink(transformers); 32 | 33 | // You can now concatenate it with your http link before creating the client like so: 34 | const enhancedHttpLink = transformerLink.concat(httpLink); 35 | ``` 36 | 37 | ## Usage with subscriptions 38 | 39 | ```js 40 | import { createTransformerLink, isSubscription } from "apollo-client-transform"; 41 | import { split } from "apollo-link"; 42 | 43 | ... 44 | 45 | const link = split(({ query }) => isSubscription(query), wsLink, httpLink); 46 | 47 | const client = new ApolloClient({ 48 | ... 49 | link: transformerLink.concat(link) 50 | }); 51 | ``` 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-client-transform", 3 | "version": "0.1.1", 4 | "description": "Bringing the ability to parse data using Apollo Client", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/cult-of-coders/apollo-client-transformers.git" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf dist coverage", 12 | "compile": "tsc", 13 | "pretest": "npm run compile", 14 | "test": "npm run testonly --", 15 | "lint": "tslint --project ./tsconfig.json ./src/**/*", 16 | "watch": "tsc -w", 17 | "testonly": "mocha --reporter spec --full-trace ./dist/__tests__/*.js", 18 | "testonly-watch": "mocha --reporter spec --full-trace ./dist/__tests__/*.js --watch", 19 | "coverage": "node ./node_modules/istanbul/lib/cli.js cover _mocha -- --full-trace ./dist/__tests__/*.js", 20 | "postcoverage": "remap-istanbul --input coverage/coverage.json --type lcovonly --output coverage/lcov.info", 21 | "prepublishOnly": "npm run clean && npm run compile" 22 | }, 23 | "peerDependencies": { 24 | "apollo-link": "1.x", 25 | "graphql": "^14.5.8", 26 | "apollo-utilities": "^1.3.2" 27 | }, 28 | "devDependencies": { 29 | "chai": "^4.2.0", 30 | "chai-as-promised": "^7.1.1", 31 | "istanbul": "^0.4.5", 32 | "mocha": "^6.2.2", 33 | "remap-istanbul": "^0.13.0", 34 | "rimraf": "^3.0.0", 35 | "simpl-schema": "^1.5.6", 36 | "tslint": "^5.20.0", 37 | "typescript": "^3.6.4" 38 | }, 39 | "typings": "dist/index.d.ts", 40 | "typescript": { 41 | "definition": "dist/index.d.ts" 42 | }, 43 | "license": "MIT", 44 | "dependencies": { 45 | "is-plain-object": "^3.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { parseObject } from '../index'; 5 | 6 | const DateScalar = { 7 | parseValue(time) { 8 | return new Date(time); 9 | }, 10 | 11 | serialize(time: Date) { 12 | return time.getTime(); 13 | }, 14 | }; 15 | 16 | describe('It should work', () => { 17 | it('Should parseValue properly', () => { 18 | const config = { 19 | User: { 20 | createdAt: DateScalar, 21 | }, 22 | }; 23 | 24 | const user = { 25 | __typename: 'User', 26 | _id: 'xxx', 27 | createdAt: new Date().getTime(), 28 | } as any; 29 | 30 | parseObject(config, user); 31 | 32 | assert.isTrue(user.createdAt instanceof Date); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/samples.ts: -------------------------------------------------------------------------------- 1 | export const querySample = { 2 | variables: { 3 | params: { 4 | filters: {}, 5 | options: { 6 | limit: 5, 7 | skip: 0, 8 | }, 9 | }, 10 | }, 11 | extensions: {}, 12 | operationName: 'posts', 13 | query: { 14 | kind: 'Document', 15 | definitions: [ 16 | { 17 | kind: 'OperationDefinition', 18 | operation: 'query', 19 | name: { 20 | kind: 'Name', 21 | value: 'posts', 22 | }, 23 | variableDefinitions: [ 24 | { 25 | kind: 'VariableDefinition', 26 | variable: { 27 | kind: 'Variable', 28 | name: { 29 | kind: 'Name', 30 | value: 'params', 31 | }, 32 | }, 33 | type: { 34 | kind: 'NonNullType', 35 | type: { 36 | kind: 'NamedType', 37 | name: { 38 | kind: 'Name', 39 | value: 'JSON', 40 | }, 41 | }, 42 | }, 43 | }, 44 | ], 45 | directives: [], 46 | selectionSet: { 47 | kind: 'SelectionSet', 48 | selections: [ 49 | { 50 | kind: 'Field', 51 | name: { 52 | kind: 'Name', 53 | value: 'posts', 54 | }, 55 | arguments: [ 56 | { 57 | kind: 'Argument', 58 | name: { 59 | kind: 'Name', 60 | value: 'params', 61 | }, 62 | value: { 63 | kind: 'Variable', 64 | name: { 65 | kind: 'Name', 66 | value: 'params', 67 | }, 68 | }, 69 | }, 70 | ], 71 | directives: [], 72 | selectionSet: { 73 | kind: 'SelectionSet', 74 | selections: [ 75 | { 76 | kind: 'Field', 77 | name: { 78 | kind: 'Name', 79 | value: '_id', 80 | }, 81 | arguments: [], 82 | directives: [], 83 | }, 84 | { 85 | kind: 'Field', 86 | name: { 87 | kind: 'Name', 88 | value: 'title', 89 | }, 90 | arguments: [], 91 | directives: [], 92 | }, 93 | { 94 | kind: 'Field', 95 | name: { 96 | kind: 'Name', 97 | value: 'user', 98 | }, 99 | arguments: [], 100 | directives: [], 101 | selectionSet: { 102 | kind: 'SelectionSet', 103 | selections: [ 104 | { 105 | kind: 'Field', 106 | name: { 107 | kind: 'Name', 108 | value: 'email', 109 | }, 110 | arguments: [], 111 | directives: [], 112 | }, 113 | { 114 | kind: 'Field', 115 | name: { 116 | kind: 'Name', 117 | value: '__typename', 118 | }, 119 | }, 120 | ], 121 | }, 122 | }, 123 | { 124 | kind: 'Field', 125 | name: { 126 | kind: 'Name', 127 | value: 'tags', 128 | }, 129 | arguments: [], 130 | directives: [], 131 | selectionSet: { 132 | kind: 'SelectionSet', 133 | selections: [ 134 | { 135 | kind: 'Field', 136 | name: { 137 | kind: 'Name', 138 | value: 'name', 139 | }, 140 | arguments: [], 141 | directives: [], 142 | }, 143 | { 144 | kind: 'Field', 145 | name: { 146 | kind: 'Name', 147 | value: '__typename', 148 | }, 149 | }, 150 | ], 151 | }, 152 | }, 153 | { 154 | kind: 'Field', 155 | name: { 156 | kind: 'Name', 157 | value: '__typename', 158 | }, 159 | }, 160 | ], 161 | }, 162 | }, 163 | ], 164 | }, 165 | }, 166 | ], 167 | loc: { 168 | start: 0, 169 | end: 195, 170 | source: { 171 | body: 172 | '\n query posts($params: JSON!) {\n posts(params: $params) {\n _id\ntitle\nuser {\n email\n\n }\n \ntags {\n name\n\n }\n \n\n } \n }\n ', 173 | name: 'GraphQL request', 174 | locationOffset: { 175 | line: 1, 176 | column: 1, 177 | }, 178 | }, 179 | }, 180 | }, 181 | }; 182 | 183 | export const mutationSample = { 184 | variables: { 185 | document: { 186 | title: 'Z Newly', 187 | date: '2018-06-27T16:31:45.367Z', 188 | }, 189 | }, 190 | extensions: {}, 191 | operationName: 'postsInsert', 192 | query: { 193 | kind: 'Document', 194 | definitions: [ 195 | { 196 | kind: 'OperationDefinition', 197 | operation: 'mutation', 198 | name: { 199 | kind: 'Name', 200 | value: 'postsInsert', 201 | }, 202 | variableDefinitions: [ 203 | { 204 | kind: 'VariableDefinition', 205 | variable: { 206 | kind: 'Variable', 207 | name: { 208 | kind: 'Name', 209 | value: 'document', 210 | }, 211 | }, 212 | type: { 213 | kind: 'NamedType', 214 | name: { 215 | kind: 'Name', 216 | value: 'JSON', 217 | }, 218 | }, 219 | }, 220 | ], 221 | directives: [], 222 | selectionSet: { 223 | kind: 'SelectionSet', 224 | selections: [ 225 | { 226 | kind: 'Field', 227 | name: { 228 | kind: 'Name', 229 | value: 'postsInsert', 230 | }, 231 | arguments: [ 232 | { 233 | kind: 'Argument', 234 | name: { 235 | kind: 'Name', 236 | value: 'document', 237 | }, 238 | value: { 239 | kind: 'Variable', 240 | name: { 241 | kind: 'Name', 242 | value: 'document', 243 | }, 244 | }, 245 | }, 246 | ], 247 | directives: [], 248 | selectionSet: { 249 | kind: 'SelectionSet', 250 | selections: [ 251 | { 252 | kind: 'Field', 253 | name: { 254 | kind: 'Name', 255 | value: '_id', 256 | }, 257 | arguments: [], 258 | directives: [], 259 | }, 260 | { 261 | kind: 'Field', 262 | name: { 263 | kind: 'Name', 264 | value: '__typename', 265 | }, 266 | }, 267 | ], 268 | }, 269 | }, 270 | ], 271 | }, 272 | }, 273 | ], 274 | loc: { 275 | start: 0, 276 | end: 126, 277 | source: { 278 | body: 279 | '\n mutation postsInsert($document: JSON) {\n postsInsert(document: $document) {\n _id\n }\n }\n ', 280 | name: 'GraphQL request', 281 | locationOffset: { 282 | line: 1, 283 | column: 1, 284 | }, 285 | }, 286 | }, 287 | }, 288 | }; 289 | -------------------------------------------------------------------------------- /src/defs.ts: -------------------------------------------------------------------------------- 1 | export type Transformer = { 2 | parseValue: (value: any) => any; 3 | serialize: (value: any) => any; 4 | }; 5 | 6 | export type TransformerMap = { 7 | [index: string]: Transformer; 8 | }; 9 | 10 | export type Transformers = { 11 | [typename: string]: TransformerMap; 12 | }; 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloLink, FetchResult, Observable } from 'apollo-link'; 2 | import { getMainDefinition } from 'apollo-utilities'; 3 | import { DocumentNode } from 'graphql'; 4 | import isPlainObject from 'is-plain-object'; 5 | 6 | import { TransformerMap, Transformers } from './defs'; 7 | 8 | export function parseObject(transformers: Transformers, object) { 9 | if (!object) { 10 | return; 11 | } 12 | 13 | if (Array.isArray(object)) { 14 | return object.forEach(i => parseObject(transformers, i)); 15 | } 16 | 17 | if (!isPlainObject(object)) { 18 | return; 19 | } 20 | 21 | for (let key in object) { 22 | parseObject(transformers, object[key]); 23 | } 24 | 25 | const typename = object.__typename; 26 | 27 | if (typename && transformers[typename]) { 28 | parseObjectValues(transformers[typename], object); 29 | } 30 | } 31 | 32 | export function parseObjectValues(map: TransformerMap, object) { 33 | for (let key in map) { 34 | if (object[key]) { 35 | object[key] = map[key].parseValue(object[key]); 36 | } 37 | } 38 | } 39 | 40 | export function isSubscription(query: DocumentNode) { 41 | const definition = getMainDefinition(query); 42 | 43 | return ( 44 | definition.kind === 'OperationDefinition' && 45 | definition.operation === 'subscription' 46 | ); 47 | } 48 | 49 | function parseResponse(response: FetchResult, transformers: Transformers) { 50 | if (response.data) { 51 | parseObject(transformers, response.data); 52 | } 53 | 54 | return response; 55 | } 56 | 57 | export function createTransformerLink(transformers: Transformers) { 58 | return new ApolloLink((operation, forward) => { 59 | if (isSubscription(operation.query)) { 60 | return new Observable(observer => 61 | forward(operation).subscribe(response => 62 | observer.next(parseResponse(response, transformers)), 63 | ), 64 | ); 65 | } 66 | 67 | return forward(operation).map(response => 68 | parseResponse(response, transformers), 69 | ); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "lib": ["es6", "dom", "esnext"], 9 | "noImplicitAny": false, 10 | "strict": true, 11 | "rootDir": "./src", 12 | "outDir": "./dist", 13 | "allowSyntheticDefaultImports": true, 14 | "esModuleInterop": true, 15 | "pretty": true, 16 | "jsx": "react", 17 | "removeComments": true, 18 | "typeRoots": ["node_modules/@types"] 19 | }, 20 | 21 | "include": ["**/*.ts", "**/*.tsx", "**/*.jsx"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "interface-name": false, 19 | "jsdoc-format": true, 20 | "label-position": true, 21 | "max-line-length": [ 22 | true, 23 | 140 24 | ], 25 | "member-access": true, 26 | "member-ordering": [ 27 | true, 28 | "public-before-private", 29 | "static-before-instance", 30 | "variables-before-functions" 31 | ], 32 | "no-any": false, 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-conditional-assignment": true, 36 | "no-consecutive-blank-lines": false, 37 | "no-console": [ 38 | true, 39 | "log", 40 | "debug", 41 | "info", 42 | "time", 43 | "timeEnd", 44 | "trace" 45 | ], 46 | "no-construct": true, 47 | "no-debugger": true, 48 | "no-duplicate-variable": true, 49 | "no-empty": true, 50 | "no-eval": true, 51 | "no-inferrable-types": false, 52 | "no-internal-module": true, 53 | "no-null-keyword": false, 54 | "no-require-imports": false, 55 | "no-shadowed-variable": true, 56 | "no-switch-case-fall-through": true, 57 | "no-trailing-whitespace": true, 58 | "no-unused-expression": true, 59 | "no-var-keyword": true, 60 | "no-var-requires": true, 61 | "object-literal-sort-keys": false, 62 | "one-line": [ 63 | true, 64 | "check-open-brace", 65 | "check-catch", 66 | "check-else", 67 | "check-finally", 68 | "check-whitespace" 69 | ], 70 | "quotemark": [ 71 | true, 72 | "single", 73 | "avoid-escape" 74 | ], 75 | "radix": true, 76 | "semicolon": [ 77 | true, 78 | "always" 79 | ], 80 | "switch-default": true, 81 | "trailing-comma": [ 82 | true, 83 | { 84 | "multiline": "always", 85 | "singleline": "never" 86 | } 87 | ], 88 | "triple-equals": [ 89 | true, 90 | "allow-null-check" 91 | ], 92 | "typedef": [ 93 | false, 94 | "call-signature", 95 | "parameter", 96 | "arrow-parameter", 97 | "property-declaration", 98 | "variable-declaration", 99 | "member-variable-declaration" 100 | ], 101 | "typedef-whitespace": [ 102 | true, 103 | { 104 | "call-signature": "nospace", 105 | "index-signature": "nospace", 106 | "parameter": "nospace", 107 | "property-declaration": "nospace", 108 | "variable-declaration": "nospace" 109 | }, 110 | { 111 | "call-signature": "space", 112 | "index-signature": "space", 113 | "parameter": "space", 114 | "property-declaration": "space", 115 | "variable-declaration": "space" 116 | } 117 | ], 118 | "variable-name": [ 119 | true, 120 | "check-format", 121 | "allow-leading-underscore", 122 | "ban-keywords", 123 | "allow-pascal-case" 124 | ], 125 | "whitespace": [ 126 | true, 127 | "check-branch", 128 | "check-decl", 129 | "check-operator", 130 | "check-separator", 131 | "check-type" 132 | ] 133 | } 134 | } 135 | --------------------------------------------------------------------------------