Skip to content

Commit

Permalink
New LocalDateTimeString scalar
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed May 18, 2023
1 parent e42352a commit 2e9ebad
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-llamas-worry.md
@@ -0,0 +1,5 @@
---
'graphql-scalars': minor
---

New `LocalDateTimeString` scalar
6 changes: 6 additions & 0 deletions src/index.ts
Expand Up @@ -30,6 +30,7 @@ import {
GraphQLLatitude,
GraphQLLCCSubclass,
GraphQLLocalDate,
GraphQLLocalDateTime,
GraphQLLocale,
GraphQLLocalEndTime,
GraphQLLocalTime,
Expand Down Expand Up @@ -76,6 +77,7 @@ export {
UtcOffset as UtcOffsetTypeDefinition,
LocalDate as LocalDateTypeDefinition,
LocalTime as LocalTimeTypeDefinition,
LocalDateTime as LocalDateTimeTypeDefinition,
LocalEndTime as LocalEndTimeTypeDefinition,
EmailAddress as EmailAddressTypeDefinition,
NegativeFloat as NegativeFloatTypeDefinition,
Expand Down Expand Up @@ -146,6 +148,7 @@ export {
GraphQLISO8601Duration as ISO8601DurationResolver,
GraphQLLocalDate as LocalDateResolver,
GraphQLLocalTime as LocalTimeResolver,
GraphQLLocalDateTime as LocalDateTimeResolver,
GraphQLLocalEndTime as LocalEndTimeResolver,
GraphQLEmailAddress as EmailAddressResolver,
GraphQLNegativeFloat as NegativeFloatResolver,
Expand Down Expand Up @@ -212,6 +215,7 @@ export const resolvers: Record<string, GraphQLScalarType> = {
ISO8601Duration: GraphQLISO8601Duration,
LocalDate: GraphQLLocalDate,
LocalTime: GraphQLLocalTime,
LocalDateTime: GraphQLLocalDateTime,
LocalEndTime: GraphQLLocalEndTime,
EmailAddress: GraphQLEmailAddress,
NegativeFloat: GraphQLNegativeFloat,
Expand Down Expand Up @@ -279,6 +283,7 @@ export {
UtcOffset as UtcOffsetMock,
LocalDate as LocalDateMock,
LocalTime as LocalTimeMock,
LocalDateTime as LocalDateTimeMock,
LocalEndTime as LocalEndTimeMock,
EmailAddress as EmailAddressMock,
NegativeFloat as NegativeFloatMock,
Expand Down Expand Up @@ -354,6 +359,7 @@ export {
GraphQLISO8601Duration,
GraphQLLocalDate,
GraphQLLocalTime,
GraphQLLocalDateTime,
GraphQLLocalEndTime,
GraphQLEmailAddress,
GraphQLNegativeFloat,
Expand Down
11 changes: 8 additions & 3 deletions src/mocks.ts
Expand Up @@ -9,6 +9,7 @@ export const UtcOffset = () => '+03:00';
export const Duration = () => 'P3Y6M4DT12H30M5S';
export const LocalDate = () => '2020-07-19';
export const LocalTime = () => '08:45:59';
export const LocalDateTime = () => '2020-07-19T08:45:59';
export const LocalEndTime = () => '24:00:00';
export const EmailAddress = () => 'test@test.com';
export const NegativeFloat = () => -123.45;
Expand Down Expand Up @@ -47,7 +48,8 @@ const randomVal = (min: number, max: number) => {
};
// https://codepen.io/meowwwls/pen/jbEJRp
export const HSL = () => `hsl(${randomVal(0, 360)}, ${randomVal(30, 95)}%, ${randomVal(30, 80)}%)`;
export const HSLA = () => `hsla(${randomVal(0, 360)}, ${randomVal(30, 95)}%, ${randomVal(30, 80)}%, ${Math.random()})`;
export const HSLA = () =>
`hsla(${randomVal(0, 360)}, ${randomVal(30, 95)}%, ${randomVal(30, 80)}%, ${Math.random()})`;

export const IP = () => '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
// https://stackoverflow.com/questions/43464519/creating-fake-ip-address-using-javascript
Expand All @@ -63,10 +65,13 @@ export const IPv4 = () =>
export const IPv6 = () => '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
// http://jsfiddle.net/guest271314/qhbC9/
export const MAC = () =>
'XX:XX:XX:XX:XX:XX'.replace(/X/g, () => '0123456789ABCDEF'.charAt(Math.floor(Math.random() * 16)));
'XX:XX:XX:XX:XX:XX'.replace(/X/g, () =>
'0123456789ABCDEF'.charAt(Math.floor(Math.random() * 16)),
);
export const Port = () => randomVal(0, 65535);
export const RGB = () => `rgb(${randomVal(0, 255)}, ${randomVal(0, 255)}, ${randomVal(0, 255)})`;
export const RGBA = () => `rgba(${randomVal(0, 255)}, ${randomVal(0, 255)}, ${randomVal(0, 255)}, ${Math.random()})`;
export const RGBA = () =>
`rgba(${randomVal(0, 255)}, ${randomVal(0, 255)}, ${randomVal(0, 255)}, ${Math.random()})`;
export const ISBN = () => `978-3-16-148410-0`;
export const JWT = () => {
// HEADER: {
Expand Down
76 changes: 76 additions & 0 deletions src/scalars/LocalDateTime.ts
@@ -0,0 +1,76 @@
import { ASTNode, GraphQLScalarType, GraphQLScalarTypeConfig, Kind } from 'graphql';
import { createGraphQLError } from '../error.js';

const LOCAL_DATE_TIME_REGEX =
/^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$/;

function validateLocalDateTime(value: any, ast?: ASTNode): string {
if (typeof value !== 'string') {
throw createGraphQLError(
`Value is not string: ${value}`,
ast
? {
nodes: [ast],
}
: undefined,
);
}
if (!LOCAL_DATE_TIME_REGEX.test(value.toUpperCase())) {
throw createGraphQLError(
`LocalDateTime cannot represent an invalid local date-time-string ${value}.`,
ast
? {
nodes: [ast],
}
: undefined,
);
}
const valueAsDate = new Date(value);
const isValidDate = !isNaN(valueAsDate.getTime());
if (!isValidDate) {
throw createGraphQLError(
`Value is not a valid LocalDateTime: ${value}`,
ast
? {
nodes: [ast],
}
: undefined,
);
}
return value;
}

export const LocalDateTimeConfig: GraphQLScalarTypeConfig<string, string> = /*#__PURE__*/ {
name: 'LocalDateTime',
description:
'A local date-time string (i.e., with no associated timezone) in `YYYY-MM-DDTHH:mm:ss` format, e.g. `2020-01-01T00:00:00`.',
serialize(value) {
return validateLocalDateTime(value);
},
parseValue(value) {
return validateLocalDateTime(value);
},
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw createGraphQLError(
`Can only validate strings as local date-times but got a: ${ast.kind}`,
{
nodes: [ast],
},
);
}
return validateLocalDateTime(ast.value, ast);
},
extensions: {
codegenScalarType:
// eslint-disable-next-line no-template-curly-in-string
'`${number}${number}${number}${number}-${number}${number}-${number}${number}T${number}${number}:${number}${number}:${number}${number}`',
jsonSchema: {
title: 'LocalDateTime',
type: 'string',
format: 'date-time',
},
},
};

export const GraphQLLocalDateTime = /*#__PURE__*/ new GraphQLScalarType(LocalDateTimeConfig);
1 change: 1 addition & 0 deletions src/scalars/index.ts
Expand Up @@ -7,6 +7,7 @@ export { GraphQLUtcOffset } from './UtcOffset.js';
export { GraphQLISO8601Duration } from './iso-date/Duration.js';
export { GraphQLLocalDate } from './LocalDate.js';
export { GraphQLLocalTime } from './LocalTime.js';
export { GraphQLLocalDateTime } from './LocalDateTime.js';
export { GraphQLLocalEndTime } from './LocalEndTime.js';
export { GraphQLEmailAddress } from './EmailAddress.js';
export { GraphQLNegativeFloat } from './NegativeFloat.js';
Expand Down
2 changes: 2 additions & 0 deletions src/typeDefs.ts
Expand Up @@ -10,6 +10,7 @@ export const Duration = 'scalar Duration';
export const ISO8601Duration = 'scalar ISO8601Duration';
export const LocalDate = 'scalar LocalDate';
export const LocalTime = 'scalar LocalTime';
export const LocalDateTime = 'scalar LocalDateTime';
export const LocalEndTime = 'scalar LocalEndTime';
export const EmailAddress = 'scalar EmailAddress';
export const UUID = `scalar UUID`;
Expand Down Expand Up @@ -78,6 +79,7 @@ export const typeDefs = [
ISO8601Duration,
LocalDate,
LocalTime,
LocalDateTime,
LocalEndTime,
EmailAddress,
NegativeFloat,
Expand Down
48 changes: 16 additions & 32 deletions tests/LocalDate.test.ts
@@ -1,5 +1,5 @@
const { Kind } = require('graphql/language');
const { GraphQLLocalDate } = require('../src/scalars/LocalDate');
import { Kind } from 'graphql';
import { GraphQLLocalDate } from '../src/scalars/LocalDate';

const VALID_LOCAL_DATES = [
'2020-01-01',
Expand All @@ -21,19 +21,19 @@ const INVALID_LOCAL_DATES = [
describe(`LocalDate`, () => {
describe(`valid`, () => {
it(`serialize`, () => {
VALID_LOCAL_DATES.forEach((date) => {
VALID_LOCAL_DATES.forEach(date => {
expect(GraphQLLocalDate.serialize(date)).toEqual(date);
});
});

it(`parseValue`, () => {
VALID_LOCAL_DATES.forEach((date) => {
VALID_LOCAL_DATES.forEach(date => {
expect(GraphQLLocalDate.parseValue(date)).toEqual(date);
});
});

it(`parseLiteral`, () => {
VALID_LOCAL_DATES.forEach((testValue) => {
VALID_LOCAL_DATES.forEach(testValue => {
expect(
GraphQLLocalDate.parseLiteral(
{
Expand All @@ -50,52 +50,36 @@ describe(`LocalDate`, () => {
describe(`invalid`, () => {
describe(`not a valid LocalDate`, () => {
it(`serialize`, () => {
expect(() => GraphQLLocalDate.serialize(123)).toThrow(
/Value is not string/,
);
expect(() => GraphQLLocalDate.serialize(123)).toThrow(/Value is not string/);

expect(() => GraphQLLocalDate.serialize(false)).toThrow(
/Value is not string/,
);
INVALID_LOCAL_DATES.forEach((testValue) => {
expect(() => GraphQLLocalDate.serialize(false)).toThrow(/Value is not string/);
INVALID_LOCAL_DATES.forEach(testValue => {
expect(() => GraphQLLocalDate.serialize(testValue)).toThrow(
/Value is not a valid LocalDate/,
);
});
});

it(`parseValue`, () => {
expect(() => GraphQLLocalDate.parseValue(123)).toThrow(
/Value is not string/,
);
expect(() => GraphQLLocalDate.parseValue(123)).toThrow(/Value is not string/);

expect(() => GraphQLLocalDate.parseValue(false)).toThrow(
/Value is not string/,
);
INVALID_LOCAL_DATES.forEach((date) => {
expect(() => GraphQLLocalDate.parseValue(date)).toThrow(
/Value is not a valid LocalDate/,
);
expect(() => GraphQLLocalDate.parseValue(false)).toThrow(/Value is not string/);
INVALID_LOCAL_DATES.forEach(date => {
expect(() => GraphQLLocalDate.parseValue(date)).toThrow(/Value is not a valid LocalDate/);
});
});

it(`parseLiteral`, () => {
expect(() =>
GraphQLLocalDate.parseLiteral({ value: 123, kind: Kind.INT }, {}),
GraphQLLocalDate.parseLiteral({ value: 123 as any, kind: Kind.INT }, {}),
).toThrow(/Can only validate strings as local dates but got a/);

expect(() =>
GraphQLLocalDate.parseLiteral(
{ value: false, kind: Kind.BOOLEAN },
{},
),
GraphQLLocalDate.parseLiteral({ value: false, kind: Kind.BOOLEAN }, {}),
).toThrow(/Can only validate strings as local dates but got a/);
INVALID_LOCAL_DATES.forEach((testValue) => {
INVALID_LOCAL_DATES.forEach(testValue => {
expect(() =>
GraphQLLocalDate.parseLiteral(
{ value: testValue, kind: Kind.STRING },
{},
),
GraphQLLocalDate.parseLiteral({ value: testValue, kind: Kind.STRING }, {}),
).toThrow(/Value is not a valid LocalDate/);
});
});
Expand Down
101 changes: 101 additions & 0 deletions tests/LocalDateTime.test.ts
@@ -0,0 +1,101 @@
import { Kind } from 'graphql';
import { GraphQLLocalDateTime } from '../src';

const VALID_LOCAL_DATE_TIMES = [
'2016-02-01T00:00:15Z',
'2016-02-01T00:00:00-11:00',
'2017-01-07T11:25:00+01:00',
'2017-01-07T00:00:00+01:20',
'2016-02-01T00:00:00.1Z',
'2016-02-01T00:00:00.000Z',
'2016-02-01T00:00:00.990Z',
'2016-02-01T00:00:00.23498Z',
'2017-01-07T11:25:00.450+01:00',
'2016-02-01t00:00:00.000z',
];

describe(`LocalDateTime`, () => {
describe('valid', () => {
it('serialize', () => {
VALID_LOCAL_DATE_TIMES.forEach(value => {
expect(GraphQLLocalDateTime.serialize(value)).toEqual(value);
});
});

it('parseValue', () => {
VALID_LOCAL_DATE_TIMES.forEach(value => {
expect(GraphQLLocalDateTime.parseValue(value)).toEqual(value);
});
});

it('parseLiteral', () => {
VALID_LOCAL_DATE_TIMES.forEach(value => {
expect(
GraphQLLocalDateTime.parseLiteral(
{
value,
kind: Kind.STRING,
},
{},
),
).toEqual(value);
});
});
});
describe('invalid', () => {
describe('not string', () => {
it('serialize', () => {
expect(() => GraphQLLocalDateTime.serialize(123)).toThrow(/Value is not string/);
expect(() => GraphQLLocalDateTime.serialize(false)).toThrow(/Value is not string/);
});
it('parseValue', () => {
expect(() => GraphQLLocalDateTime.parseValue(123)).toThrow(/Value is not string/);
expect(() => GraphQLLocalDateTime.parseValue(false)).toThrow(/Value is not string/);
});
it('parseLiteral', () => {
expect(() =>
GraphQLLocalDateTime.parseLiteral({ value: 123 as any, kind: Kind.INT }, {}),
).toThrow(/Can only validate strings as local date-times but got a: IntValue/);
expect(() =>
GraphQLLocalDateTime.parseLiteral({ value: false, kind: Kind.BOOLEAN }, {}),
).toThrow(/Can only validate strings as local date-times but got a: BooleanValue/);
});
});
describe('not a valid local date time string', () => {
const invalidDateTime = '2015-02-24T00:00:00.000+0100';
it('serialize', () => {
expect(() => GraphQLLocalDateTime.serialize(invalidDateTime)).toThrow(
/LocalDateTime cannot represent an invalid local date-time-string/,
);
});
it('parseValue', () => {
expect(() => GraphQLLocalDateTime.parseValue(invalidDateTime)).toThrow(
/LocalDateTime cannot represent an invalid local date-time-string/,
);
});
it('parseLiteral', () => {
expect(() =>
GraphQLLocalDateTime.parseLiteral({ value: invalidDateTime, kind: Kind.STRING }, {}),
).toThrow(/LocalDateTime cannot represent an invalid local date-time-string/);
});
});
describe('invalid date', () => {
const invalidDateTime = '2017-13-46T11:25:00.450+01:00';
it('serialize', () => {
expect(() => GraphQLLocalDateTime.serialize(invalidDateTime)).toThrow(
/Value is not a valid LocalDateTime/,
);
});
it('parseValue', () => {
expect(() => GraphQLLocalDateTime.parseValue(invalidDateTime)).toThrow(
/Value is not a valid LocalDateTime/,
);
});
it('parseLiteral', () => {
expect(() =>
GraphQLLocalDateTime.parseLiteral({ value: invalidDateTime, kind: Kind.STRING }, {}),
).toThrow(/Value is not a valid LocalDateTime/);
});
});
});
});

0 comments on commit 2e9ebad

Please sign in to comment.