├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── datetype.js ├── examples └── now.js ├── package.json └── test ├── dateTimeTypeTest.js ├── mocha.opts └── setup.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | 12 | # Matches the exact files either package.json or .travis.yml 13 | [*.{json,js}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | npm-debug.log 4 | .env 5 | dist -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Custom Date Type for GraphQL 2 | ============================ 3 | 4 | This is a custom date type implementation for GraphQL. GraphQL does not contain 5 | a native date type but it does allow you to specify [custom scalar types](https://facebook.github.io/graphql/#sec-Scalars) 6 | that serializes to strings, or other scalar types, but conforms to certain 7 | standards. 8 | 9 | This date type accepts and outputs this format: '2015-07-24T13:15:34.814Z' 10 | which is commonly used in JSON since it is the default format used by 11 | JavaScript when serializing dates to JSON. 12 | 13 | [![npm version](https://badge.fury.io/js/graphql-custom-datetype.svg)](https://badge.fury.io/js/graphql-custom-datetype) 14 | [![Build Status](https://travis-ci.org/soundtrackyourbrand/graphql-custom-datetype.svg?branch=master)](https://travis-ci.org/soundtrackyourbrand/graphql-custom-datetype) 15 | 16 | ## Usage 17 | 18 | To use this type you in your GraphQL schema you simply install this module with 19 | `npm install --save graphql-custom-datetype` and use it. In the minimal example 20 | below we expose the query `now` that simply returns the current date and time. 21 | 22 | The important part is that your resolve function needs to return a JavaScript 23 | Date object. 24 | 25 | ```javascript 26 | // examples/now.js 27 | 28 | import { 29 | graphql, 30 | GraphQLSchema, 31 | GraphQLObjectType, 32 | } from 'graphql'; 33 | import CustomGraphQLDateType from '..'; 34 | 35 | let schema = new GraphQLSchema({ 36 | query: new GraphQLObjectType({ 37 | name: 'Query', 38 | fields: { 39 | now: { 40 | type: CustomGraphQLDateType, 41 | // Resolve fields with the custom date type to a valid Date object 42 | resolve: () => new Date() 43 | } 44 | } 45 | }) 46 | }); 47 | 48 | graphql(schema, "{ now }") 49 | .then(console.log) 50 | .catch(console.error); 51 | ``` 52 | 53 | Running this prints the current date: 54 | 55 | ```shell 56 | $ babel-node examples/now.js 57 | { data: { now: '2015-07-24T13:23:15.580Z' } } 58 | ``` 59 | -------------------------------------------------------------------------------- /datetype.js: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql' 2 | import { GraphQLError } from 'graphql/error' 3 | import { Kind } from 'graphql/language' 4 | 5 | function parseDate (value) { 6 | let result = new Date(value) 7 | if (isNaN(result.getTime())) { 8 | throw new TypeError('Invalid date: ' + value) 9 | } 10 | if (value !== result.toJSON()) { 11 | throw new TypeError('Invalid date format, only accepts: YYYY-MM-DDTHH:MM:SS.SSSZ: ' + value) 12 | } 13 | return result 14 | } 15 | 16 | export default new GraphQLScalarType({ 17 | name: 'DateTime', 18 | 19 | // Serialize a date to send to the client. 20 | serialize (value) { 21 | if (!(value instanceof Date)) { 22 | throw new TypeError('Field error: value is not an instance of Date') 23 | } 24 | if (isNaN(value.getTime())) { 25 | throw new TypeError('Field error: value is an invalid Date') 26 | } 27 | return value.toJSON() 28 | }, 29 | 30 | // Parse a date received as a query variable. 31 | parseValue (value) { 32 | if (typeof value !== 'string') { 33 | throw new TypeError('Field error: value is not an instance of string') 34 | } 35 | return parseDate(value) 36 | }, 37 | 38 | // Parse a date received as an inline value. 39 | parseLiteral (ast) { 40 | if (ast.kind !== Kind.STRING) { 41 | throw new GraphQLError('Query error: Can only parse strings to dates but got a: ' + ast.kind, [ast]) 42 | } 43 | try { 44 | return parseDate(ast.value) 45 | } catch (e) { 46 | throw new GraphQLError('Query error: ' + e.message, [ast]) 47 | } 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /examples/now.js: -------------------------------------------------------------------------------- 1 | import { 2 | graphql, 3 | GraphQLSchema, 4 | GraphQLObjectType 5 | } from 'graphql' 6 | import CustomGraphQLDateType from '..' 7 | 8 | let schema = new GraphQLSchema({ 9 | query: new GraphQLObjectType({ 10 | name: 'Query', 11 | fields: { 12 | now: { 13 | type: CustomGraphQLDateType, 14 | // Resolve fields with the custom date type to a valid Date object 15 | resolve: () => new Date() 16 | } 17 | } 18 | }) 19 | }) 20 | 21 | graphql(schema, '{ now }') 22 | .then(console.log) 23 | .catch(console.error) 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-custom-datetype", 3 | "version": "0.5.0", 4 | "description": "Custom date type for graphql", 5 | "main": "dist/datetype.js", 6 | "homepage": "https://github.com/soundtrackyourbrand/graphql-custom-datetype", 7 | "bugs": { 8 | "url": "https://github.com/soundtrackyourbrand/graphql-custom-datetype/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/soundtrackyourbrand/graphql-custom-datetype.git" 13 | }, 14 | "peerDependencies": { 15 | "graphql": ">=0.5.0" 16 | }, 17 | "devDependencies": { 18 | "babel": "5.8.3", 19 | "babel-core": "5.8.3", 20 | "babel-eslint": "^4.1.3", 21 | "babel-runtime": "5.8.3", 22 | "chai": "3.2.0", 23 | "chai-subset": "1.0.1", 24 | "graphql": "^0.6.0", 25 | "mocha": "2.2.5", 26 | "standard": "^5.3.1" 27 | }, 28 | "scripts": { 29 | "prepublish": "npm test && npm run build", 30 | "test": "standard && mocha", 31 | "test-watch": "mocha -w", 32 | "build": "rm -rf dist/* && babel datetype.js --out-dir dist", 33 | "example": "babel-node examples/now.js" 34 | }, 35 | "files": [ 36 | "dist", 37 | "README.md" 38 | ], 39 | "license": "MIT", 40 | "standard": { 41 | "parser": "babel-eslint" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/dateTimeTypeTest.js: -------------------------------------------------------------------------------- 1 | import { 2 | graphql, 3 | GraphQLSchema, 4 | GraphQLObjectType 5 | } from 'graphql' 6 | import { describe, it } from 'mocha' 7 | import { expect } from 'chai' 8 | 9 | import CustomGraphQLDateType from '../datetype.js' 10 | 11 | describe('GraphQL date type', () => { 12 | it('coerses date object to string', () => { 13 | let aDateStr = '2015-07-24T10:56:42.744Z' 14 | let aDateObj = new Date(aDateStr) 15 | 16 | expect( 17 | CustomGraphQLDateType.serialize(aDateObj) 18 | ).to.equal(aDateStr) 19 | }) 20 | 21 | it('stringifies dates', async () => { 22 | let now = new Date() 23 | 24 | let schema = new GraphQLSchema({ 25 | query: new GraphQLObjectType({ 26 | name: 'Query', 27 | fields: { 28 | now: { 29 | type: CustomGraphQLDateType, 30 | resolve: () => now 31 | } 32 | } 33 | }) 34 | }) 35 | 36 | return expect( 37 | await graphql(schema, `{ now }`) 38 | ).to.deep.equal({ 39 | data: { 40 | now: now.toJSON() 41 | } 42 | }) 43 | }) 44 | 45 | it('handles null', async () => { 46 | let now = null 47 | 48 | let schema = new GraphQLSchema({ 49 | query: new GraphQLObjectType({ 50 | name: 'Query', 51 | fields: { 52 | now: { 53 | type: CustomGraphQLDateType, 54 | resolve: () => now 55 | } 56 | } 57 | }) 58 | }) 59 | 60 | return expect( 61 | await graphql(schema, `{ now }`) 62 | ).to.deep.equal({ 63 | data: { 64 | now: null 65 | } 66 | }) 67 | }) 68 | 69 | it('fails when now is not a date', async () => { 70 | let now = 'invalid date' 71 | 72 | let schema = new GraphQLSchema({ 73 | query: new GraphQLObjectType({ 74 | name: 'Query', 75 | fields: { 76 | now: { 77 | type: CustomGraphQLDateType, 78 | resolve: () => now 79 | } 80 | } 81 | }) 82 | }) 83 | 84 | return expect( 85 | await graphql(schema, `{ now }`) 86 | ).to.containSubset({ 87 | errors: [{ 88 | message: 'Field error: value is not an instance of Date' 89 | }] 90 | }) 91 | }) 92 | 93 | describe('dates as input', () => { 94 | let schema = new GraphQLSchema({ 95 | query: new GraphQLObjectType({ 96 | name: 'Query', 97 | fields: { 98 | nextDay: { 99 | type: CustomGraphQLDateType, 100 | args: { 101 | date: { 102 | type: CustomGraphQLDateType 103 | } 104 | }, 105 | resolve: (_, {date}) => { 106 | return new Date(date.getTime() + 24 * 3600 * 1000) 107 | } 108 | } 109 | } 110 | }) 111 | }) 112 | 113 | it('handles dates as input', async () => { 114 | let someday = '2015-07-24T10:56:42.744Z' 115 | let nextDay = '2015-07-25T10:56:42.744Z' 116 | 117 | return expect( 118 | await graphql(schema, `{ nextDay(date: "${someday}") }`) 119 | ).to.deep.equal({ 120 | data: { 121 | nextDay: nextDay 122 | } 123 | }) 124 | }) 125 | 126 | it('does not accept alternative date formats', async () => { 127 | let someday = 'Fri Jul 24 2015 12:56:42 GMT+0200 (CEST)' 128 | 129 | return expect( 130 | await graphql(schema, `{ nextDay(date: "${someday}") }`) 131 | ).to.containSubset({ 132 | errors: [{ 133 | locations: [], 134 | message: 'Query error: Invalid date format, only accepts: YYYY-MM-DDTHH:MM:SS.SSSZ: ' + someday 135 | }] 136 | }) 137 | }) 138 | 139 | it('chokes on invalid dates as input', async () => { 140 | let invalidDate = 'invalid data' 141 | 142 | return expect( 143 | await graphql(schema, `{ nextDay(date: "${invalidDate}") }`) 144 | ).to.containSubset({ 145 | errors: [{ 146 | locations: [], 147 | message: 'Query error: Invalid date: ' + invalidDate 148 | }] 149 | }) 150 | }) 151 | }) 152 | 153 | describe('dates as variable', () => { 154 | let schema = new GraphQLSchema({ 155 | query: new GraphQLObjectType({ 156 | name: 'Query', 157 | fields: { 158 | nextDay: { 159 | type: CustomGraphQLDateType, 160 | args: { 161 | date: { 162 | type: CustomGraphQLDateType 163 | } 164 | }, 165 | resolve: (_, {date}) => { 166 | return new Date(date.getTime() + 24 * 3600 * 1000) 167 | } 168 | } 169 | } 170 | }) 171 | }) 172 | 173 | it('handles dates as variable', async () => { 174 | let someday = '2015-07-24T10:56:42.744Z' 175 | let nextDay = '2015-07-25T10:56:42.744Z' 176 | 177 | return expect( 178 | await graphql(schema, 179 | `query ($date: DateTime!) { nextDay(date: $date) }`, 180 | null, null, { date: someday }) 181 | ).to.deep.equal({ 182 | data: { 183 | nextDay: nextDay 184 | } 185 | }) 186 | }) 187 | 188 | it('does not accept alternative date formats as variable', async () => { 189 | let someday = 'Fri Jul 24 2015 12:56:42 GMT+0200 (CEST)' 190 | 191 | let result = await graphql(schema, 192 | `query ($date: DateTime!) { nextDay(date: $date) }`, 193 | null, null, { date: someday }) 194 | 195 | expect(result).to.have.deep.property( 196 | 'errors[0].message', 197 | 'Invalid date format, only accepts: YYYY-MM-DDTHH:MM:SS.SSSZ: ' + someday 198 | ) 199 | }) 200 | 201 | it('chokes on invalid dates as variable', async () => { 202 | let invalidDate = 'invalid data' 203 | 204 | let result = await graphql(schema, 205 | `query ($date: DateTime!) { nextDay(date: $date) }`, 206 | null, null, { date: invalidDate }) 207 | 208 | expect(result).to.have.deep.property( 209 | 'errors[0].message', 210 | 'Invalid date: ' + invalidDate 211 | ) 212 | }) 213 | }) 214 | }) 215 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel/register 2 | --require ./test/setup.js 3 | --reporter spec 4 | --timeout 5000 5 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | require('babel/register')({ 2 | optional: ['runtime', 'es7.asyncFunctions'] 3 | }) 4 | 5 | var chai = require('chai') 6 | 7 | var chaiSubset = require('chai-subset') 8 | chai.use(chaiSubset) 9 | --------------------------------------------------------------------------------