diff --git a/package-lock.json b/package-lock.json index 6f7af7bac..643e842f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,6 @@ "remark-emoji": "4.0.0", "remark-gfm": "3.0.1", "styled-components": "5.3.11", - "superjson": "1.12.3", "throttle-debounce": "5.0.0", "tinykeys": "1.4.0", "zod": "3.22.3" diff --git a/package.json b/package.json index 92ecb7d47..032a69cf0 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "remark-emoji": "4.0.0", "remark-gfm": "3.0.1", "styled-components": "5.3.11", - "superjson": "1.12.3", "throttle-debounce": "5.0.0", "tinykeys": "1.4.0", "zod": "3.22.3" diff --git a/src/utils/declareSsrProps.ts b/src/utils/declareSsrProps.ts index a218a1af6..c88fff1c7 100644 --- a/src/utils/declareSsrProps.ts +++ b/src/utils/declareSsrProps.ts @@ -2,12 +2,13 @@ import { GetServerSidePropsContext } from 'next'; import { Session } from 'next-auth'; import { getSession } from 'next-auth/react'; import { createServerSideHelpers, DecoratedProcedureSSGRecord } from '@trpc/react-query/server'; -import superjson from 'superjson'; import { routes } from '../hooks/router'; import { trpcRouter } from '../../trpc/router'; import type { TrpcRouter } from '../../trpc/router'; +import { transformer } from './transformer'; + export interface SSRProps

{ user: Session['user']; req: GetServerSidePropsContext['req']; @@ -45,7 +46,7 @@ export function declareSsrProps( session, headers: req.headers, }, - transformer: superjson, + transformer, }); const ssrTime = Date.now(); diff --git a/src/utils/transformer.test.ts b/src/utils/transformer.test.ts new file mode 100644 index 000000000..79575c7c9 --- /dev/null +++ b/src/utils/transformer.test.ts @@ -0,0 +1,30 @@ +import { expect } from '@jest/globals'; + +import { deserialize, serialize } from './transformer'; + +const dateString = '_serializedDate_1970-01-01T00:00:00.000Z'; +const undefinedString = '_undefined_'; + +describe('Transformer', () => { + it('should serialize and deserialize', async () => { + const values = [ + { from: 1, to: 1 }, + { from: 'a', to: 'a' }, + { from: new Date(0), to: dateString }, + { from: undefined, to: undefinedString }, + { from: [], to: [] }, + { from: ['a', 1, new Date(0), [new Date(0)]], to: ['a', 1, dateString, [dateString]] }, + { from: ['a', [new Date(0)]], to: ['a', [dateString]] }, + { from: [undefined, [[undefined]]], to: [undefinedString, [[undefinedString]]] }, + { from: {}, to: {} }, + { from: { a: { b: new Date(0), c: {} } }, to: { a: { b: dateString, c: {} } } }, + { from: { a: { b: null } }, to: { a: { b: null } } }, + ]; + for (const { from, to } of values) { + const serialized = serialize(from); + expect(serialized).toStrictEqual(to); + const deserialized = deserialize(to); + expect(deserialized).toStrictEqual(from); + } + }); +}); diff --git a/src/utils/transformer.ts b/src/utils/transformer.ts new file mode 100644 index 000000000..4cb0f5922 --- /dev/null +++ b/src/utils/transformer.ts @@ -0,0 +1,36 @@ +import { CombinedDataTransformer } from '@trpc/server'; + +const datePrefix = '_serializedDate_'; +const undefinedPlaceholder = '_undefined_'; + +export const serialize = (data: any): any => { + if (typeof data === 'function') throw new Error('Cannot serialize function'); + if (typeof data === 'symbol') throw new Error('Cannot serialize symbol'); + if (typeof data === 'bigint') throw new Error('Cannot serialize bigint'); + if (typeof data === 'undefined') return undefinedPlaceholder; + if (typeof data === 'object') { + if (data === null) return data; + if (data instanceof Date) return `${datePrefix}${data.toISOString()}`; + if (Array.isArray(data)) return data.map(serialize); + return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, serialize(v)])); + } + return data; +}; + +export const deserialize = (data: any): any => { + if (typeof data === 'string') { + if (data.startsWith(datePrefix)) return new Date(data.slice(datePrefix.length)); + if (data === undefinedPlaceholder) return undefined; + } + if (typeof data === 'object') { + if (data === null) return data; + if (Array.isArray(data)) return data.map(deserialize); + return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, deserialize(v)])); + } + return data; +}; + +export const transformer: CombinedDataTransformer = { + input: { serialize, deserialize }, + output: { serialize, deserialize }, +}; diff --git a/src/utils/trpcClient.ts b/src/utils/trpcClient.ts index b1de60a24..869548da4 100644 --- a/src/utils/trpcClient.ts +++ b/src/utils/trpcClient.ts @@ -1,9 +1,10 @@ import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; -import superjson from 'superjson'; import type { TrpcRouter } from '../../trpc/router'; +import { transformer } from './transformer'; + function getBaseUrl() { if (typeof window !== 'undefined') { // browser should use relative path @@ -15,7 +16,7 @@ function getBaseUrl() { export const trpc = createTRPCNext({ config: ({ ctx }) => { return { - transformer: superjson, + transformer, queryClientConfig: { defaultOptions: { diff --git a/trpc/trpcBackend.ts b/trpc/trpcBackend.ts index 3b6256f39..6b40c6c07 100644 --- a/trpc/trpcBackend.ts +++ b/trpc/trpcBackend.ts @@ -1,10 +1,11 @@ import { TRPCError, initTRPC } from '@trpc/server'; -import superjson from 'superjson'; + +import { transformer } from '../src/utils/transformer'; import type { TrpcContext } from './context'; const t = initTRPC.context().create({ - transformer: superjson, + transformer, }); const sessionCheck = t.middleware(({ next, ctx }) => {