Skip to content

Commit c0072ea

Browse files
feat: add Cuid2 scalar (#2857)
* feat: add Cuid2 scalar Closes #2399 * New CUID2 Scalar * Prettier --------- Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
1 parent ded0a29 commit c0072ea

File tree

13 files changed

+174
-18
lines changed

13 files changed

+174
-18
lines changed

.changeset/cuddly-spoons-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'graphql-scalars': minor
3+
---
4+
5+
New CUID2 Scalar

.eslintrc.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@
33
"parserOptions": {
44
"project": "./tsconfig.all.json"
55
},
6-
"extends": [
7-
"eslint:recommended",
8-
"standard",
9-
"prettier",
10-
"plugin:@typescript-eslint/recommended"
11-
],
6+
"extends": ["eslint:recommended", "prettier", "plugin:@typescript-eslint/recommended"],
127
"plugins": ["@typescript-eslint"],
138
"rules": {
149
"no-empty": "off",

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
"cross-env": "10.1.0",
7272
"eslint": "9.37.0",
7373
"eslint-config-prettier": "10.1.8",
74-
"eslint-config-standard": "17.1.0",
7574
"eslint-plugin-import": "2.32.0",
7675
"eslint-plugin-n": "17.23.1",
7776
"eslint-plugin-promise": "7.2.1",

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
GraphQLCountryCode,
88
GraphQLCountryName,
99
GraphQLCuid,
10+
GraphQLCuid2,
1011
GraphQLCurrency,
1112
GraphQLDate,
1213
GraphQLDateTime,
@@ -220,6 +221,7 @@ export const resolvers: Record<string, GraphQLScalarType> = {
220221
CountryCode: GraphQLCountryCode,
221222
CountryName: GraphQLCountryName,
222223
Cuid: GraphQLCuid,
224+
Cuid2: GraphQLCuid2,
223225
Currency: GraphQLCurrency,
224226
Date: GraphQLDate,
225227
DateTime: GraphQLDateTime,
@@ -437,12 +439,15 @@ export {
437439
export { GeoJSON as GeoJSONTypeDefinition } from './typeDefs.js';
438440
export { CountryName as CountryNameTypeDefinition } from './typeDefs.js';
439441
export { ULID as ULIDTypeDefinition } from './typeDefs.js';
442+
export { Cuid2 as Cuid2TypeDefinition } from './typeDefs.js';
440443
export { GraphQLCountryName as CountryNameResolver };
441444
export { GraphQLCountryName };
442445
export { CountryName as CountryNameMock } from './mocks.js';
443446
export { GraphQLGeoJSON as GeoJSONResolver };
444447
export { GraphQLULID as ULIDResolver };
448+
export { GraphQLCuid2 as Cuid2Resolver };
445449
export { GraphQLGeoJSON };
446450
export { GraphQLULID };
447451
export { GeoJSON as GeoJSONMock } from './mocks.js';
452+
export { Cuid2 as Cuid2Mock } from './mocks.js';
448453
export { ULID as ULIDMock } from './mocks.js';

src/mocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const ULID = () => '01J9GHQM8N5X4TQ3M1ZV9J6CBK';
2+
export const Cuid2 = () => 'dp71y53f6eykvl5g1393rmhl';
23
export const GeoJSON = () => 'Example GeoJSON';
34
export const CountryName = () => 'Example CountryName';
45
const BigIntMock = () => BigInt(Number.MAX_SAFE_INTEGER);

src/scalars/Cuid2.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { GraphQLScalarType, Kind, ValueNode } from 'graphql';
2+
import { createGraphQLError } from '../error.js';
3+
4+
const CUID2_REGEX = /^[a-z][a-z0-9]{1,31}$/;
5+
6+
const validateCuid2 = (value: any, ast?: ValueNode) => {
7+
if (typeof value !== 'string') {
8+
throw createGraphQLError(`Value is not string: ${value}`, ast ? { nodes: ast } : undefined);
9+
}
10+
11+
if (!CUID2_REGEX.test(value)) {
12+
throw createGraphQLError(
13+
`Value is not a valid cuid2: ${value}`,
14+
ast ? { nodes: ast } : undefined,
15+
);
16+
}
17+
18+
return value;
19+
};
20+
21+
const specifiedByURL = 'https://github.com/paralleldrive/cuid2';
22+
23+
export const GraphQLCuid2 = /*#__PURE__*/ new GraphQLScalarType({
24+
name: 'Cuid2',
25+
description: `A field whose value conforms to the cuid2 format, as specified in ${specifiedByURL}`,
26+
27+
serialize: validateCuid2,
28+
parseValue: validateCuid2,
29+
30+
parseLiteral(ast) {
31+
if (ast.kind !== Kind.STRING) {
32+
throw createGraphQLError(`Can only validate strings as cuid2 but got: ${ast.kind}`, {
33+
nodes: [ast],
34+
});
35+
}
36+
37+
return validateCuid2(ast.value, ast);
38+
},
39+
40+
specifiedByURL,
41+
extensions: {
42+
codegenScalarType: 'string',
43+
jsonSchema: {
44+
title: 'Cuid2',
45+
type: 'string',
46+
pattern: CUID2_REGEX.source,
47+
},
48+
},
49+
});

src/scalars/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,4 @@ export { GraphQLDeweyDecimal } from './library/DeweyDecimal.js';
6868
export { GraphQLLCCSubclass } from './library/LCCSubclass.js';
6969
export { GraphQLIPCPatent } from './patent/IPCPatent.js';
7070
export { GraphQLULID } from './ULID.js';
71+
export { GraphQLCuid2 } from './Cuid2.js';

src/typeDefs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const ULID = 'scalar ULID';
2+
export const Cuid2 = 'scalar Cuid2';
23
export const GeoJSON = 'scalar GeoJSON';
34
export const CountryName = 'scalar CountryName';
45
export const BigInt = 'scalar BigInt';
@@ -80,6 +81,7 @@ export const typeDefs = [
8081
CountryCode,
8182
CountryName,
8283
Cuid,
84+
Cuid2,
8385
Currency,
8486
Date,
8587
DateTime,

tests/Cuid2.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/* global describe, test, expect */
2+
3+
import { Kind } from 'graphql/language';
4+
import { GraphQLCuid2 } from '../src/scalars/Cuid2.js';
5+
6+
describe('Cuid2', () => {
7+
describe('valid', () => {
8+
const validCuid2 = 'dp71y53f6eykvl5g1393rmhl';
9+
10+
test('serialize', () => {
11+
expect(GraphQLCuid2.serialize(validCuid2)).toBe(validCuid2);
12+
});
13+
14+
test('parseValue', () => {
15+
expect(GraphQLCuid2.parseValue(validCuid2)).toBe(validCuid2);
16+
});
17+
18+
test('parseLiteral', () => {
19+
expect(
20+
GraphQLCuid2.parseLiteral(
21+
{
22+
value: validCuid2,
23+
kind: Kind.STRING,
24+
},
25+
{},
26+
),
27+
).toBe(validCuid2);
28+
});
29+
});
30+
31+
describe('invalid', () => {
32+
describe('not a cuid2', () => {
33+
test('serialize', () => {
34+
expect(() => GraphQLCuid2.serialize('not-a-valid-cuid2')).toThrow(
35+
/Value is not a valid cuid2/,
36+
);
37+
});
38+
39+
test('parseValue', () => {
40+
expect(() => GraphQLCuid2.parseValue('not-a-valid-cuid2')).toThrow(
41+
/Value is not a valid cuid2/,
42+
);
43+
});
44+
45+
test('parseLiteral', () => {
46+
expect(() =>
47+
GraphQLCuid2.parseLiteral(
48+
{
49+
value: 'not-a-valid-cuid2',
50+
kind: Kind.STRING,
51+
},
52+
{},
53+
),
54+
).toThrow(/Value is not a valid cuid2/);
55+
});
56+
});
57+
58+
describe('not a string', () => {
59+
test('serialize', () => {
60+
expect(() => GraphQLCuid2.serialize(123)).toThrow(/Value is not string/);
61+
});
62+
63+
test('parseValue', () => {
64+
expect(() => GraphQLCuid2.parseValue(123)).toThrow(/Value is not string/);
65+
});
66+
67+
test('parseLiteral', () => {
68+
expect(() => GraphQLCuid2.parseLiteral({ value: '123', kind: Kind.INT }, {})).toThrow(
69+
/Can only validate strings as cuid2 but got/,
70+
);
71+
});
72+
});
73+
74+
describe('boundary cases', () => {
75+
test('minimum length (2 characters)', () => {
76+
const minCuid2 = 'a1';
77+
expect(GraphQLCuid2.serialize(minCuid2)).toBe(minCuid2);
78+
expect(GraphQLCuid2.parseValue(minCuid2)).toBe(minCuid2);
79+
expect(GraphQLCuid2.parseLiteral({ value: minCuid2, kind: Kind.STRING }, {})).toBe(
80+
minCuid2,
81+
);
82+
});
83+
84+
test('maximum length (32 characters)', () => {
85+
const maxCuid2 = 'a123456789abcdef123456789abcdef1'; // starts with a letter
86+
expect(GraphQLCuid2.serialize(maxCuid2)).toBe(maxCuid2);
87+
expect(GraphQLCuid2.parseValue(maxCuid2)).toBe(maxCuid2);
88+
expect(GraphQLCuid2.parseLiteral({ value: maxCuid2, kind: Kind.STRING }, {})).toBe(
89+
maxCuid2,
90+
);
91+
});
92+
93+
test('too short (1 character)', () => {
94+
const tooShort = 'a';
95+
expect(() => GraphQLCuid2.serialize(tooShort)).toThrow(/Value is not a valid cuid2/);
96+
});
97+
98+
test('too long (33 characters)', () => {
99+
const tooLong = 'a1234567890abcdef1234567890abcdef1';
100+
expect(() => GraphQLCuid2.serialize(tooLong)).toThrow(/Value is not a valid cuid2/);
101+
});
102+
});
103+
});
104+
});

website/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
"@types/react": "19.2.2",
2424
"cross-env": "10.1.0",
2525
"next-sitemap": "4.2.3",
26+
"pagefind": "1.4.0",
2627
"postcss-import": "16.1.1",
2728
"postcss-lightningcss": "1.0.2",
28-
"pagefind": "1.4.0",
2929
"tailwindcss": "3.4.18",
3030
"typescript": "5.9.3"
3131
},

0 commit comments

Comments
 (0)