kit icon indicating copy to clipboard operation
kit copied to clipboard

Ability to serialize arbitrary classes to send to client

Open HamishWHC opened this issue 2 years ago • 4 comments

Describe the problem

My problem is that my database client returns data that includes classes, and I can't return this from my +page.server.ts file and use it in my +page.{ts,svelte}. Examples of these classes include Prisma.Decimal, model instances from Type/MikroORM, objects with extra methods returned by an extended Prisma client (see Prisma docs) or anything else that might be a class inside a deeply nested object.

Describe the proposed solution

Having read through #6008 (and the pull request), my understanding is that devalue creates JS code that recreates the JS object is it passed, and that getting the generated code to reference constructors for these classes would be difficult (that said, I have an idea for this that I'd like to propose, not certain if it'd work, see below).

Instead, what I really am looking for is the ability to hand some arbitrary data to Svelte and have it transform it to data that is compatible with devalue. i.e. I register a function (server-side) that identifies my class, then provide a function that takes an instance of this class and returns an object that is already compatible with devalue. This could then be incorporated into the type generation system (similar to param matchers) so that PageLoad functions have corrected types.

I don't think my explanation was very clear, so here's an example:

// src/routes/+page.server.ts
import {PageServerLoad} from "./$types"

export const load: PageServerLoad = () => {
	// getCurrentUser returns a User class
	return {
		user: await db.getCurrentUser()
	}
}
// src/serde/User.ts (or elsewhere)
import {Serializer, SerializerTest} from "@sveltejs/kit"
import {User} from "$lib/server/models"

// devalue can use this function to test if a non-POJO it encounters is a User object.
export const test = (v: any): v is User => v instanceof User
// If so, it uses this function to convert the class to a POJO (which devalue can serialize).
export const serialize: Serializer = (u: User) => ({id: u.id, username: u.username, email: u.email})
// src/routes/+page.ts
import {PageLoad} from "./$types";

export const load: PageLoad = ({data}) => {
	// data.user is now the object returned by our serialize function, and is typed as such.
	return {user, other: "data"}
}
Idea for deserializing classes Rich's initial explanation of using devalue in #6008 says the deserialization process is (roughly):
await import(`${url.pathname}/__data.js`);
const data = window.__data;
delete window.__data;

Could devalue be made to set window.__data to a function that can be passed functions to deserialize the data? This would avoid the issue of ensuring constructors are available to devalue's generated code. The deserialization functions could also be lazy loaded if needed.

This isn't really useful in the above example (a database model likely has server-side only functions attached to it, in which case I only want the POJO), but for custom scalars such as Prisma.Decimal, this would be quite helpful.

Alternatives considered

My current solution is to write a function that 'jsonifies' my complex objects in a type-safe-ish way (Range is from edgedb):

// Replace Non-Serializable
type RNS<T> = T extends Range<infer U>
	? { lower: U | null; upper: U | null }
	: T extends Array<infer U>
	? Array<RNS<U>>
	: T extends object
	? { [K in keyof T]: RNS<T[K]> }
	: T;

export const isRange = (obj: any): obj is Range<any> => obj instanceof Range;

export const jsonify = <T extends any>(data: T): RNS<T> => {
	if (isRange(data)) {
		return { lower: data.lower, upper: data.upper } as RNS<T>;
	} else if (data instanceof Array) {
		return data.map(jsonify) as RNS<T>;
	} else if (typeof data === "object" && data !== null) {
		return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, jsonify(v)])) as RNS<T>;
	} else {
		return data as RNS<T>;
	}
};

I can then wrap database queries in this (or add it with .then(jsonify)). This doesn't allow for deserializing classes.

I've also been looking at potentially using superjson, but I lose the ability to modify the PageData type without touching anything else - I have to modify the types that I am reporting to superjson.

Importance

nice to have

Additional Information

No response

HamishWHC avatar Jan 27 '23 00:01 HamishWHC