Skip to content

feat: add Cuid2 scalar #2857

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
GraphQLCountryCode,
GraphQLCountryName,
GraphQLCuid,
GraphQLCuid2,
GraphQLCurrency,
GraphQLDate,
GraphQLDateTime,
Expand Down Expand Up @@ -219,6 +220,7 @@ export const resolvers: Record<string, GraphQLScalarType> = {
CountryCode: GraphQLCountryCode,
CountryName: GraphQLCountryName,
Cuid: GraphQLCuid,
Cuid2: GraphQLCuid2,
Currency: GraphQLCurrency,
Date: GraphQLDate,
DateTime: GraphQLDateTime,
Expand Down Expand Up @@ -434,9 +436,13 @@ export {

export { GeoJSON as GeoJSONTypeDefinition } from './typeDefs.js';
export { CountryName as CountryNameTypeDefinition } from './typeDefs.js';
export { Cuid2 as Cuid2TypeDefinition } from './typeDefs.js';
export { GraphQLCountryName as CountryNameResolver };
export { GraphQLCountryName };
export { CountryName as CountryNameMock } from './mocks.js';
export { GraphQLGeoJSON as GeoJSONResolver };
export { GraphQLCuid2 as Cuid2Resolver };
export { GraphQLGeoJSON };
export { GraphQLCuid2 };
export { GeoJSON as GeoJSONMock } from './mocks.js';
export { Cuid2 as Cuid2Mock } from './mocks.js';
1 change: 1 addition & 0 deletions src/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const Cuid2 = () => 'dp71y53f6eykvl5g1393rmhl';
export const GeoJSON = () => 'Example GeoJSON';
export const CountryName = () => 'Example CountryName';
const BigIntMock = () => BigInt(Number.MAX_SAFE_INTEGER);
Expand Down
49 changes: 49 additions & 0 deletions src/scalars/Cuid2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { GraphQLScalarType, Kind, ValueNode } from 'graphql';
import { createGraphQLError } from '../error.js';

const CUID2_REGEX = /^[a-z][a-z0-9]{1,31}$/;

const validateCuid2 = (value: any, ast?: ValueNode) => {
if (typeof value !== 'string') {
throw createGraphQLError(`Value is not string: ${value}`, ast ? { nodes: ast } : undefined);
}

if (!CUID2_REGEX.test(value)) {
throw createGraphQLError(
`Value is not a valid cuid2: ${value}`,
ast ? { nodes: ast } : undefined,
);
}

return value;
};

const specifiedByURL = 'https://github.com/paralleldrive/cuid2';

export const GraphQLCuid2 = /*#__PURE__*/ new GraphQLScalarType({
name: 'Cuid2',
description: `A field whose value conforms to the cuid2 format, as specified in ${specifiedByURL}`,

serialize: validateCuid2,
parseValue: validateCuid2,

parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw createGraphQLError(`Can only validate strings as cuid2 but got: ${ast.kind}`, {
nodes: [ast],
});
}

return validateCuid2(ast.value, ast);
},

specifiedByURL,
extensions: {
codegenScalarType: 'string',
jsonSchema: {
title: 'Cuid2',
type: 'string',
pattern: CUID2_REGEX.source,
},
},
});
1 change: 1 addition & 0 deletions src/scalars/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ export { GraphQLSESSN } from './ssn/SE.js';
export { GraphQLDeweyDecimal } from './library/DeweyDecimal.js';
export { GraphQLLCCSubclass } from './library/LCCSubclass.js';
export { GraphQLIPCPatent } from './patent/IPCPatent.js';
export { GraphQLCuid2 } from './Cuid2.js';
2 changes: 2 additions & 0 deletions src/typeDefs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const Cuid2 = 'scalar Cuid2';
export const GeoJSON = 'scalar GeoJSON';
export const CountryName = 'scalar CountryName';
export const BigInt = 'scalar BigInt';
Expand Down Expand Up @@ -79,6 +80,7 @@ export const typeDefs = [
CountryCode,
CountryName,
Cuid,
Cuid2,
Currency,
Date,
DateTime,
Expand Down
104 changes: 104 additions & 0 deletions tests/Cuid2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* global describe, test, expect */

import { Kind } from 'graphql/language';
import { GraphQLCuid2 } from '../src/scalars/Cuid2.js';

describe('Cuid2', () => {
describe('valid', () => {
const validCuid2 = 'dp71y53f6eykvl5g1393rmhl';

test('serialize', () => {
expect(GraphQLCuid2.serialize(validCuid2)).toBe(validCuid2);
});

test('parseValue', () => {
expect(GraphQLCuid2.parseValue(validCuid2)).toBe(validCuid2);
});

test('parseLiteral', () => {
expect(
GraphQLCuid2.parseLiteral(
{
value: validCuid2,
kind: Kind.STRING,
},
{},
),
).toBe(validCuid2);
});
});

describe('invalid', () => {
describe('not a cuid2', () => {
test('serialize', () => {
expect(() => GraphQLCuid2.serialize('not-a-valid-cuid2')).toThrow(
/Value is not a valid cuid2/,
);
});

test('parseValue', () => {
expect(() => GraphQLCuid2.parseValue('not-a-valid-cuid2')).toThrow(
/Value is not a valid cuid2/,
);
});

test('parseLiteral', () => {
expect(() =>
GraphQLCuid2.parseLiteral(
{
value: 'not-a-valid-cuid2',
kind: Kind.STRING,
},
{},
),
).toThrow(/Value is not a valid cuid2/);
});
});

describe('not a string', () => {
test('serialize', () => {
expect(() => GraphQLCuid2.serialize(123)).toThrow(/Value is not string/);
});

test('parseValue', () => {
expect(() => GraphQLCuid2.parseValue(123)).toThrow(/Value is not string/);
});

test('parseLiteral', () => {
expect(() => GraphQLCuid2.parseLiteral({ value: '123', kind: Kind.INT }, {})).toThrow(
/Can only validate strings as cuid2 but got/,
);
});
});

describe('boundary cases', () => {
test('minimum length (2 characters)', () => {
const minCuid2 = 'a1';
expect(GraphQLCuid2.serialize(minCuid2)).toBe(minCuid2);
expect(GraphQLCuid2.parseValue(minCuid2)).toBe(minCuid2);
expect(GraphQLCuid2.parseLiteral({ value: minCuid2, kind: Kind.STRING }, {})).toBe(
minCuid2,
);
});

test('maximum length (32 characters)', () => {
const maxCuid2 = 'a123456789abcdef123456789abcdef1'; // starts with a letter
expect(GraphQLCuid2.serialize(maxCuid2)).toBe(maxCuid2);
expect(GraphQLCuid2.parseValue(maxCuid2)).toBe(maxCuid2);
expect(GraphQLCuid2.parseLiteral({ value: maxCuid2, kind: Kind.STRING }, {})).toBe(
maxCuid2,
);
});

test('too short (1 character)', () => {
const tooShort = 'a';
expect(() => GraphQLCuid2.serialize(tooShort)).toThrow(/Value is not a valid cuid2/);
});

test('too long (33 characters)', () => {
const tooLong = 'a1234567890abcdef1234567890abcdef1';
expect(() => GraphQLCuid2.serialize(tooLong)).toThrow(/Value is not a valid cuid2/);
});
});
});
});
1 change: 1 addition & 0 deletions website/src/content/scalars/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default {
'country-code': 'CountryCode',
'country-name': 'CountryName',
cuid: 'Cuid',
cuid2: 'Cuid2',
currency: 'Currency',
date: 'Date',
'date-time': 'DateTime',
Expand Down
4 changes: 4 additions & 0 deletions website/src/content/scalars/cuid2.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Cuid2

A field whose value conforms to the `cuid2` format as specified in
[ParallelDrive/cuid2](https://github.com/paralleldrive/cuid2).