diff --git a/src/SDK/Language/Node.php b/src/SDK/Language/Node.php index f7e819150..3f6754505 100644 --- a/src/SDK/Language/Node.php +++ b/src/SDK/Language/Node.php @@ -246,6 +246,11 @@ public function getFiles(): array 'destination' => 'src/enums/{{ enum.name | caseDash }}.ts', 'template' => 'web/src/enums/enum.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/helper/json.ts', + 'template' => 'web/src/helper/json.ts.twig', + ], ]; } } diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 94b48343d..94776d3ba 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -120,6 +120,11 @@ public function getFiles(): array 'destination' => 'src/enums/{{ enum.name | caseDash }}.ts', 'template' => 'web/src/enums/enum.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/helper/json.ts', + 'template' => 'web/src/helper/json.ts.twig', + ], ]; } diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 7dffac1cf..1982e17f9 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -1,6 +1,7 @@ import { fetch, FormData, File } from 'node-fetch-native-with-agent'; import { createAgent } from 'node-fetch-native-with-agent/agent'; import { Models } from './models'; +import JsonBigInt from './helper/json'; type Payload = { [key: string]: any; @@ -170,7 +171,7 @@ class Client { } else { switch (headers['content-type']) { case 'application/json': - options.body = JSON.stringify(params); + options.body = JsonBigInt.stringify(params); break; case 'multipart/form-data': diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 4d0cac6df..55f77b494 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -1,4 +1,5 @@ import { Models } from './models'; +import JsonBigInt from './helper/json'; /** * Payload type representing a key-value pair with string keys and any values. @@ -442,7 +443,7 @@ class Client { }, onMessage: (event) => { try { - const message: RealtimeResponse = JSON.parse(event.data); + const message: RealtimeResponse = JsonBigInt.parse(event.data); this.realtime.lastMessage = message; switch (message.type) { case 'connected': @@ -451,7 +452,7 @@ class Client { const messageData = message.data; if (session && !messageData.user) { - this.realtime.socket?.send(JSON.stringify({ + this.realtime.socket?.send(JsonBigInt.stringify({ type: 'authentication', data: { session @@ -564,7 +565,7 @@ class Client { } else { switch (headers['content-type']) { case 'application/json': - options.body = JSON.stringify(params); + options.body = JsonBigInt.stringify(params); break; case 'multipart/form-data': @@ -644,6 +645,7 @@ class Client { let data: any = null; const response = await fetch(uri, options); + const responseData = await response.text(); const warnings = response.headers.get('x-{{ spec.title | lower }}-warning'); if (warnings) { @@ -651,12 +653,12 @@ class Client { } if (response.headers.get('content-type')?.includes('application/json')) { - data = await response.json(); + data = JsonBigInt.parse(responseData) } else if (responseType === 'arrayBuffer') { data = await response.arrayBuffer(); } else { data = { - message: await response.text() + message: responseData }; } diff --git a/templates/web/src/helper/json.ts.twig b/templates/web/src/helper/json.ts.twig new file mode 100644 index 000000000..d611e3b0e --- /dev/null +++ b/templates/web/src/helper/json.ts.twig @@ -0,0 +1,46 @@ +/** + * Helper class for JSON parsing/stringifying with `BigInt` support. + */ +export default class JsonBigInt { + + // Preprocesses JSON to wrap large numbers in quotes. + static #preprocessJson(jsonString: string): string { + return jsonString.replaceAll( + /([:\s\[,]*)(-?\d+)([\s,\]]*)/g, + (match, prefix, num, suffix) => + !Number.isSafeInteger(+num) ? `${prefix}"${num}"${suffix}` : match + ); + } + + /** + * Parses JSON, converting large numbers to `BigInt`. + * + * @param {string} jsonString - The JSON string. + * @returns {any} - The parsed object. + */ + static parse(jsonString: string): any { + const processedJsonString = this.#preprocessJson(jsonString); + + return JSON.parse(processedJsonString, (key, value) => { + if (typeof value === 'string' && /^-?\d+$/.test(value)) { + return BigInt(value); + } + return value; + }); + } + + /** + * Stringifies an object, converting `BigInt` to strings. + * + * @param {object} obj - The object to stringify. + * @returns {string} - The JSON string. + */ + static stringify(obj: object): string { + return JSON.stringify(obj, (_, value) => { + if (typeof value === 'bigint') { + return value.toString(); + } + return value; + }); + } +} diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index acad03823..5736d02a0 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -1,3 +1,5 @@ +import JsonBigInt from './helper/json'; + type QueryTypesSingle = string | number | boolean; export type QueryTypesList = string[] | number[] | boolean[] | Query[]; export type QueryTypes = QueryTypesSingle | QueryTypesList; @@ -41,7 +43,7 @@ export class Query { * @returns {string} */ toString(): string { - return JSON.stringify({ + return JsonBigInt.stringify({ method: this.method, attribute: this.attribute, values: this.values, @@ -248,7 +250,7 @@ export class Query { * @returns {string} */ static or = (queries: string[]) => - new Query("or", undefined, queries.map((query) => JSON.parse(query))).toString(); + new Query("or", undefined, queries.map((query) => JsonBigInt.parse(query))).toString(); /** * Combine multiple queries using logical AND operator. @@ -257,5 +259,5 @@ export class Query { * @returns {string} */ static and = (queries: string[]) => - new Query("and", undefined, queries.map((query) => JSON.parse(query))).toString(); + new Query("and", undefined, queries.map((query) => JsonBigInt.parse(query))).toString(); }