Skip to content

Commit

Permalink
feat: add json schema annotations for SOFA
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Oct 22, 2022
1 parent b3776e9 commit 89a867a
Show file tree
Hide file tree
Showing 56 changed files with 964 additions and 902 deletions.
5 changes: 5 additions & 0 deletions .changeset/slow-comics-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-scalars': minor
---

Add JSON Schema annotations for SOFA
42 changes: 17 additions & 25 deletions src/scalars/AccountNumber.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,29 @@
import {
GraphQLScalarType,
GraphQLScalarTypeConfig,
Kind,
locatedError,
} from 'graphql';
import { GraphQLScalarType, GraphQLScalarTypeConfig, Kind, locatedError } from 'graphql';

interface Validator {
(rtn: string): boolean;
}

const validator: Validator = (rtn) => /^([a-zA-Z0-9]){5,17}$/.test(rtn);
const regexp = /^([a-zA-Z0-9]){5,17}$/;

const validator: Validator = rtn => regexp.test(rtn);

const validate = (account: unknown): string => {
if (typeof account !== 'string') {
throw locatedError(new TypeError('can only parse String'), null);
}

if (!validator(account)) {
throw locatedError(
new TypeError('must be alphanumeric between 5-17'),
null,
);
throw locatedError(new TypeError('must be alphanumeric between 5-17'), null);
}

return account;
};

export const GraphQLAccountNumberConfig: GraphQLScalarTypeConfig<
string,
string
> = {
export const GraphQLAccountNumberConfig: GraphQLScalarTypeConfig<string, string> = {
name: 'AccountNumber',
description:
'Banking account number is a string of 5 to 17 alphanumeric values for ' +
'representing an generic account number',
'Banking account number is a string of 5 to 17 alphanumeric values for ' + 'representing an generic account number',

serialize(value: unknown) {
return validate(value);
Expand All @@ -48,14 +38,16 @@ export const GraphQLAccountNumberConfig: GraphQLScalarTypeConfig<
return validate(ast.value);
}

throw locatedError(
new TypeError(
`Account Number can only parse String but got '${ast.kind}'`,
),
ast,
);
throw locatedError(new TypeError(`Account Number can only parse String but got '${ast.kind}'`), ast);
},
extensions: {
codegenScalarType: 'string',
jsonSchema: {
title: 'AccountNumber',
type: 'string',
pattern: regexp.source,
},
},
};

export const GraphQLAccountNumber: GraphQLScalarType =
/*#__PURE__*/ new GraphQLScalarType(GraphQLAccountNumberConfig);
export const GraphQLAccountNumber: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType(GraphQLAccountNumberConfig);
4 changes: 4 additions & 0 deletions src/scalars/BigInt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ export const GraphQLBigIntConfig: GraphQLScalarTypeConfig<bigint, bigint | BigIn
},
extensions: {
codegenScalarType: 'bigint',
jsonSchema: {
type: 'integer',
format: 'int64',
},
},
};

Expand Down
54 changes: 15 additions & 39 deletions src/scalars/Byte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,16 @@ import {
} from 'graphql';

type BufferJson = { type: 'Buffer'; data: number[] };
const base64Validator =
/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/;
const base64Validator = /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/;
function hexValidator(value: string) {
// Ensure that any leading 0 is removed from the hex string to avoid false negatives.
const sanitizedValue = value.charAt(0) === '0' ? value.slice(1) : value;
// For larger strings, we run into issues with MAX_SAFE_INTEGER, so split the string
// into smaller pieces to avoid this issue.
if (value.length > 8) {
let parsedString = '';
for (
let startIndex = 0, endIndex = 8;
startIndex < value.length;
startIndex += 8, endIndex += 8
) {
parsedString += parseInt(value.slice(startIndex, endIndex), 16).toString(
16,
);
for (let startIndex = 0, endIndex = 8; startIndex < value.length; startIndex += 8, endIndex += 8) {
parsedString += parseInt(value.slice(startIndex, endIndex), 16).toString(16);
}
return parsedString === sanitizedValue;
}
Expand All @@ -34,19 +27,13 @@ function hexValidator(value: string) {

function validate(value: Buffer | string | BufferJson) {
if (typeof value !== 'string' && !(value instanceof global.Buffer)) {
throw new TypeError(
`Value is not an instance of Buffer: ${JSON.stringify(value)}`,
);
throw new TypeError(`Value is not an instance of Buffer: ${JSON.stringify(value)}`);
}
if (typeof value === 'string') {
const isBase64 = base64Validator.test(value);
const isHex = hexValidator(value);
if (!isBase64 && !isHex) {
throw new TypeError(
`Value is not a valid base64 or hex encoded string: ${JSON.stringify(
value,
)}`,
);
throw new TypeError(`Value is not a valid base64 or hex encoded string: ${JSON.stringify(value)}`);
}
return global.Buffer.from(value, isHex ? 'hex' : 'base64');
}
Expand All @@ -57,25 +44,13 @@ function validate(value: Buffer | string | BufferJson) {
function parseObject(ast: ObjectValueNode) {
const key = ast.fields[0].value;
const value = ast.fields[1].value;
if (
ast.fields.length === 2 &&
key.kind === Kind.STRING &&
key.value === 'Buffer' &&
value.kind === Kind.LIST
) {
return global.Buffer.from(
value.values.map((astValue: IntValueNode) => parseInt(astValue.value)),
);
if (ast.fields.length === 2 && key.kind === Kind.STRING && key.value === 'Buffer' && value.kind === Kind.LIST) {
return global.Buffer.from(value.values.map((astValue: IntValueNode) => parseInt(astValue.value)));
}
throw new TypeError(
`Value is not a JSON representation of Buffer: ${print(ast)}`,
);
throw new TypeError(`Value is not a JSON representation of Buffer: ${print(ast)}`);
}

export const GraphQLByteConfig: GraphQLScalarTypeConfig<
Buffer | string | BufferJson,
Buffer
> = /*#__PURE__*/ {
export const GraphQLByteConfig: GraphQLScalarTypeConfig<Buffer | string | BufferJson, Buffer> = /*#__PURE__*/ {
name: 'Byte',
description: 'The `Byte` scalar type represents byte value as a Buffer',
serialize: validate,
Expand All @@ -87,15 +62,16 @@ export const GraphQLByteConfig: GraphQLScalarTypeConfig<
case Kind.OBJECT:
return parseObject(ast);
default:
throw new TypeError(
`Can only parse base64 or hex encoded strings as Byte, but got a: ${ast.kind}`,
);
throw new TypeError(`Can only parse base64 or hex encoded strings as Byte, but got a: ${ast.kind}`);
}
},
extensions: {
codegenScalarType: 'Buffer | string',
jsonSchema: {
type: 'string',
format: 'byte',
},
},
};

export const GraphQLByte: GraphQLScalarType =
/*#__PURE__*/ new GraphQLScalarType(GraphQLByteConfig);
export const GraphQLByte: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType(GraphQLByteConfig);
50 changes: 26 additions & 24 deletions src/scalars/CountryCode.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Kind, GraphQLError, GraphQLScalarType } from 'graphql';

const validate = (value: any) => {
const COUNTRY_CODE_REGEX =
/^(AD|AE|AF|AG|AI|AL|AM|AO|AQ|AR|AS|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BJ|BL|BM|BN|BO|BQ|BR|BS|BT|BV|BW|BY|BZ|CA|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|CR|CU|CV|CW|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EE|EG|EH|ER|ES|ET|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|IO|IQ|IR|IS|IT|JE|JM|JO|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MF|MG|MH|MK|ML|MM|MN|MO|MP|MQ|MR|MS|MT|MU|MV|MW|MX|MY|MZ|NA|NC|NE|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|SS|ST|SV|SX|SY|SZ|TC|TD|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TR|TT|TV|TW|TZ|UA|UG|UM|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|YE|YT|ZA|ZM|ZW)$/i;
const COUNTRY_CODE_REGEX =
/^(AD|AE|AF|AG|AI|AL|AM|AO|AQ|AR|AS|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BJ|BL|BM|BN|BO|BQ|BR|BS|BT|BV|BW|BY|BZ|CA|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|CR|CU|CV|CW|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EE|EG|EH|ER|ES|ET|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|IO|IQ|IR|IS|IT|JE|JM|JO|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MF|MG|MH|MK|ML|MM|MN|MO|MP|MQ|MR|MS|MT|MU|MV|MW|MX|MY|MZ|NA|NC|NE|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|SS|ST|SV|SX|SY|SZ|TC|TD|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TR|TT|TV|TW|TZ|UA|UG|UM|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|YE|YT|ZA|ZM|ZW)$/i;

const validate = (value: any) => {
if (typeof value !== 'string') {
throw new TypeError(`Value is not string: ${value}`);
}
Expand All @@ -14,27 +14,29 @@ const validate = (value: any) => {
return value;
};

export const GraphQLCountryCode: GraphQLScalarType =
/*#__PURE__*/ new GraphQLScalarType({
name: 'CountryCode',
description: 'A country code as defined by ISO 3166-1 alpha-2',
serialize(value) {
return validate(value);
},
export const GraphQLCountryCode: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType({
name: 'CountryCode',
description: 'A country code as defined by ISO 3166-1 alpha-2',
serialize(value) {
return validate(value);
},

parseValue(value) {
return validate(value);
},
parseValue(value) {
return validate(value);
},

parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(
`Can only validate strings as country codes but got a: ${ast.kind}`,
);
}
return validate(ast.value);
},
extensions: {
codegenScalarType: 'string',
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(`Can only validate strings as country codes but got a: ${ast.kind}`);
}
return validate(ast.value);
},
extensions: {
codegenScalarType: 'string',
jsonSchema: {
title: 'CountryCode',
type: 'string',
pattern: COUNTRY_CODE_REGEX.source,
},
});
},
});
23 changes: 10 additions & 13 deletions src/scalars/Cuid.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import {
Kind,
GraphQLError,
GraphQLScalarType,
GraphQLScalarTypeConfig,
} from 'graphql';
import { Kind, GraphQLError, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';

const validate = (value: any) => {
const CUID_REGEX = /^c[^\s-]{8,}$/i;
const CUID_REGEX = /^c[^\s-]{8,}$/i;

const validate = (value: any) => {
if (typeof value !== 'string') {
throw new TypeError(`Value is not string: ${value}`);
}
Expand All @@ -33,9 +28,7 @@ export const GraphQLCuidConfig = /*#__PURE__*/ {

parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(
`Can only validate strings as cuids but got a: ${ast.kind}`,
);
throw new GraphQLError(`Can only validate strings as cuids but got a: ${ast.kind}`);
}

return validate(ast.value);
Expand All @@ -45,8 +38,12 @@ export const GraphQLCuidConfig = /*#__PURE__*/ {
specifiedByUrl: specifiedByURL,
extensions: {
codegenScalarType: 'string',
jsonSchema: {
title: 'Cuid',
type: 'string',
pattern: CUID_REGEX.source,
},
},
} as GraphQLScalarTypeConfig<string, string>;

export const GraphQLCuid: GraphQLScalarType =
/*#__PURE__*/ new GraphQLScalarType(GraphQLCuidConfig);
export const GraphQLCuid: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType(GraphQLCuidConfig);
25 changes: 11 additions & 14 deletions src/scalars/Currency.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import {
Kind,
GraphQLError,
GraphQLScalarType,
GraphQLScalarTypeConfig,
} from 'graphql';
import { Kind, GraphQLError, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';

const validate = (value: any) => {
const CURRENCY_REGEX =
/^(AED|AFN|ALL|AMD|ANG|AOA|ARS|AUD|AWG|AZN|BAM|BBD|BDT|BGN|BHD|BIF|BMD|BND|BOB|BOV|BRL|BSD|BTN|BWP|BYN|BZD|CAD|CDF|CHE|CHF|CHW|CLF|CLP|CNY|COP|COU|CRC|CUC|CUP|CVE|CZK|DJF|DKK|DOP|DZD|EGP|ERN|ETB|EUR|FJD|FKP|GBP|GEL|GHS|GIP|GMD|GNF|GTQ|GYD|HKD|HNL|HRK|HTG|HUF|IDR|ILS|INR|IQD|IRR|ISK|JMD|JOD|JPY|KES|KGS|KHR|KMF|KPW|KRW|KWD|KYD|KZT|LAK|LBP|LKR|LRD|LSL|LYD|MAD|MDL|MGA|MKD|MMK|MNT|MOP|MRU|MUR|MVR|MWK|MXN|MXV|MYR|MZN|NAD|NGN|NIO|NOK|NPR|NZD|OMR|PAB|PEN|PGK|PHP|PKR|PLN|PYG|QAR|RON|RSD|RUB|RWF|SAR|SBD|SCR|SDG|SEK|SGD|SHP|SLL|SOS|SRD|SSP|STN|SVC|SYP|SZL|THB|TJS|TMT|TND|TOP|TRY|TTD|TWD|TZS|UAH|UGX|USD|USN|UYI|UYU|UYW|UZS|VES|VND|VUV|WST|XAF|XAG|XAU|XBA|XBB|XBC|XBD|XCD|XDR|XOF|XPD|XPF|XPT|XSU|XTS|XUA|XXX|YER|ZAR|ZMW|ZWL)$/i;
const CURRENCY_REGEX =
/^(AED|AFN|ALL|AMD|ANG|AOA|ARS|AUD|AWG|AZN|BAM|BBD|BDT|BGN|BHD|BIF|BMD|BND|BOB|BOV|BRL|BSD|BTN|BWP|BYN|BZD|CAD|CDF|CHE|CHF|CHW|CLF|CLP|CNY|COP|COU|CRC|CUC|CUP|CVE|CZK|DJF|DKK|DOP|DZD|EGP|ERN|ETB|EUR|FJD|FKP|GBP|GEL|GHS|GIP|GMD|GNF|GTQ|GYD|HKD|HNL|HRK|HTG|HUF|IDR|ILS|INR|IQD|IRR|ISK|JMD|JOD|JPY|KES|KGS|KHR|KMF|KPW|KRW|KWD|KYD|KZT|LAK|LBP|LKR|LRD|LSL|LYD|MAD|MDL|MGA|MKD|MMK|MNT|MOP|MRU|MUR|MVR|MWK|MXN|MXV|MYR|MZN|NAD|NGN|NIO|NOK|NPR|NZD|OMR|PAB|PEN|PGK|PHP|PKR|PLN|PYG|QAR|RON|RSD|RUB|RWF|SAR|SBD|SCR|SDG|SEK|SGD|SHP|SLL|SOS|SRD|SSP|STN|SVC|SYP|SZL|THB|TJS|TMT|TND|TOP|TRY|TTD|TWD|TZS|UAH|UGX|USD|USN|UYI|UYU|UYW|UZS|VES|VND|VUV|WST|XAF|XAG|XAU|XBA|XBB|XBC|XBD|XCD|XDR|XOF|XPD|XPF|XPT|XSU|XTS|XUA|XXX|YER|ZAR|ZMW|ZWL)$/i;

const validate = (value: any) => {
if (typeof value !== 'string') {
throw new TypeError(`Value is not string: ${value}`);
}
Expand Down Expand Up @@ -37,9 +32,7 @@ export const GraphQLCurrencyConfig = /*#__PURE__*/ {

parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(
`Can only validate strings as a currency but got a: ${ast.kind}`,
);
throw new GraphQLError(`Can only validate strings as a currency but got a: ${ast.kind}`);
}

return validate(ast.value);
Expand All @@ -49,8 +42,12 @@ export const GraphQLCurrencyConfig = /*#__PURE__*/ {
specifiedByUrl: specifiedByURL,
extensions: {
codegenScalarType: 'string',
jsonSchema: {
title: 'Currency',
type: 'string',
pattern: CURRENCY_REGEX.source,
},
},
} as GraphQLScalarTypeConfig<string, string>;

export const GraphQLCurrency: GraphQLScalarType =
/*#__PURE__*/ new GraphQLScalarType(GraphQLCurrencyConfig);
export const GraphQLCurrency: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType(GraphQLCurrencyConfig);
22 changes: 9 additions & 13 deletions src/scalars/DID.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import {
GraphQLScalarType,
Kind,
GraphQLError,
GraphQLScalarTypeConfig,
} from 'graphql';
import { GraphQLScalarType, Kind, GraphQLError, GraphQLScalarTypeConfig } from 'graphql';

// See: https://www.w3.org/TR/2021/PR-did-core-20210803/#did-syntax
const DID_REGEX =
/^did:[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+:[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+$/;
const DID_REGEX = /^did:[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+:[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+$/;

const validate = (value: any) => {
if (typeof value !== 'string') {
Expand Down Expand Up @@ -35,9 +29,7 @@ export const GraphQLDIDConfig = {

parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(
`Can only validate strings as DID but got a: ${ast.kind}`,
);
throw new GraphQLError(`Can only validate strings as DID but got a: ${ast.kind}`);
}

return validate(ast.value);
Expand All @@ -47,8 +39,12 @@ export const GraphQLDIDConfig = {
specifiedByUrl: specifiedByURL,
extensions: {
codegenScalarType: 'string',
jsonSchema: {
title: 'DID',
type: 'string',
pattern: DID_REGEX.source,
},
},
} as GraphQLScalarTypeConfig<string, string>;

export const GraphQLDID: GraphQLScalarType =
/*#__PURE__*/ new GraphQLScalarType(GraphQLDIDConfig);
export const GraphQLDID: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType(GraphQLDIDConfig);
Loading

0 comments on commit 89a867a

Please sign in to comment.