UNPKG

graphql-scalars

Version:

A collection of scalar types not included in base GraphQL.

110 lines (109 loc) 3.9 kB
import { GraphQLScalarType, Kind } from 'graphql'; import { createGraphQLError } from '../../error.js'; // Swedish Personal Number also known as 'personnummer' in swedish: // https://www.skatteverket.se/privat/folkbokforing/personnummer.4.3810a01c150939e893f18c29.html // Algorithm: // https://swedish.identityinfo.net/personalidentitynumber const SESSN_PATTERNS = ['YYYYMMDDXXXX', 'YYMMDDXXXX']; function _isValidSwedishPersonalNumber(value) { // Remove any non-digit characters const pno = value.replace(/\D/g, ''); // Check if the cleaned number has the correct length (10 or 12 digits) if (pno.length !== 10 && pno.length !== 12) { return false; } // Validate the birthdate if (!_isValidDate(pno)) { return false; } // Check the checksum for numbers if (!_isValidChecksum(pno)) { return false; } // If all checks pass, the personal number is valid return true; } function _isValidDate(pno) { let year; let month; let day; if (pno.length === 10) { year = Number(pno.substring(0, 2)); // Adjust the input 'year' to a four-digit year based on the assumption that two-digit years greater than the current year are in the past century (1900s), // while two-digit years less than or equal to the current year are in the current or upcoming century (2000s). year = year > Number(String(new Date().getFullYear()).substring(2)) ? 1900 + year : 2000 + year; month = Number(pno.substring(2, 4)); day = Number(pno.substring(4, 6)); } else { year = Number(pno.substring(0, 4)); month = Number(pno.substring(4, 6)); day = Number(pno.substring(6, 8)); } const date = new Date(year, month - 1, day); return date.getFullYear() === year && date.getMonth() + 1 === month && date.getDate() === day; } function _isValidChecksum(pno) { const shortPno = pno.length === 12 ? pno.substring(2, 12) : pno; const digits = shortPno.split('').map(Number); let sum = 0; for (let i = 0; i < digits.length; i++) { let digit = digits[i]; // Double every second digit from the right if (i % 2 === digits.length % 2) { digit *= 2; if (digit > 9) { digit -= 9; } } sum += digit; } // Check if the sum is a multiple of 10 return sum % 10 === 0; } function _checkString(value) { if (typeof value !== 'string') { throw createGraphQLError(`Value is not string: ${value}`); } } function _checkSSN(value) { if (!_isValidSwedishPersonalNumber(value)) { throw createGraphQLError(`Value is not a valid swedish personal number: ${value}`); } } export const GraphQLSESSN = /*#__PURE__*/ new GraphQLScalarType({ name: 'SESSN', description: 'A field whose value conforms to the standard personal number (personnummer) formats for Sweden', serialize(value) { _checkString(value); _checkSSN(value); return value; }, parseValue(value) { _checkString(value); _checkSSN(value); return value; }, parseLiteral(ast) { if (ast.kind !== Kind.STRING) { throw createGraphQLError(`Can only validate strings as swedish personal number but got a: ${ast.kind}`, { nodes: ast }); } if (!_isValidSwedishPersonalNumber(ast.value)) { throw createGraphQLError(`Value is not a valid swedish personal number: ${ast.value}`, { nodes: ast, }); } return ast.value; }, extensions: { codegenScalarType: 'string', jsonSchema: { title: 'SESSN', oneOf: SESSN_PATTERNS.map((pattern) => ({ type: 'string', length: pattern.length, pattern, })), }, }, });