Adapt model serialization to work with the new Single Fetch data loading strategy and streaming format#74
Adapt model serialization to work with the new Single Fetch data loading strategy and streaming format#74acusti wants to merge 10 commits intojplhomer:mainfrom
Conversation
follow-up to 905ba4c
using serialize skips the potential toJSON() override used to e.g. not include User.password in the serialized form of a User when it is serialized as a relation of another model (e.g. Article in examples/remix-cms)
allows for e.g.:
const articles = await Article.with("user").orderBy("createdAt", "desc").toJSON();
from a loader to explicitly return the serialized version of a model to provide it to a component. this makes it so that the version of the model that is provided during SSR is exactly equivalent to the version of the model that is made available to the component when rendered on the client. otherwise, the data made available to the component during SSR when using single fetch is the actual model instance.
single fetch uses turbo-stream to serialize data, meaning that model.toJSON() is no longer automatically invoked as was the case with the remix json() helper. turbo-stream’s encode function calls flatten, which defines a partsForObj util that gets called for POJOs and uses Object.keys(): https://github.com/jacob-ebey/turbo-stream/blob/main/src/flatten.ts#L50 customizing ownKeys() to use toJSON() to return the appropriate keys ensures that the object gets serialized by turbo-stream in the form defined by the model ( including any customizations, e.g. the default User model, which removes the password field from the result) however, calling target.toJSON() means that relation field keys are also returned, which causes the relation model name to exist on the model (though undefined). this is why we need to check in the get() proxy trap if the prop is not undefined on target, rather than just check for the existence of the prop in target.
commit: |
• add an IsSingle type argument to QueryBuilder to track if first() has been invoked • add the toJSON method to QueryBuilder • update return types of BaseModel query methods to return a QueryBuilder • update QueryBuilder promise methods (then/catch) to resolve query results based on if IsSingle (i.e. if first() has been invoked) or not
|
Nice catch. My thoughts are that, as a developer, Superflare should provide a way for you to either explicitly hide sensitive fields from serialization, whether that's in a I like the approach of encoding it into the Looks like turbo-stream has a I'll keep thinking on it! |
this allows proper type checking of resolved instances of the model when using single fetch
the placeholder GITHUB_TOKEN in .dev.vars ensures that wrangler includes it in its typegen (worker-configuration.d.ts)
Remix v2 introduced Single Fetch as the preferred new data loading strategy, which uses
turbo-streamas the streaming format to encode and decode data between the server and the client. as documented in the Breaking Changes section of that guide, this means that:as a result, model instances’
toJSON()method is no longer invoked for any model data / query results returned from route loaders, so the raw model instance winds up being serialized instead. i found two different ways to work around this:Updating auto-serialization to be compatible with new streaming format 3e59403
one approach is to add a
ownKeystrap to the model proxy object and override it to provide the keys produced by invokingmodelInstance.toJSON(). this approach works becauseturbo-stream’sencodefunction uses a utility calledflattento kick off its encoding, andflattencallspartsForObjto handle plain objects, abdpartsForObjcallsObject.keys()to figure out how to encode the plain object, so customizingownKeys()to usetoJSON()to return the appropriate keys ensures that the object gets serialized byturbo-streamin the form defined by the model (including any customizations, e.g. with the defaultUsermodel, which removes thepasswordfield from the result).this has a couple of consequences: one, obviously, is that
Object.keys()andObject.getOwnPropertyNames()will no longer return the actual raw model fields (e.g. no moreattributes, no morerelations, no${prop}, and no fields that the user has chosen to redact from the serialized (“public-facing”) version fo the model. i could see this being a helpful feature and i could also see this being a footgun. the second consequence is that relation field keys are also returned, causing the relation model name to exist on the model (thoughundefined), which is why we need to check in theget()proxy trap if the value of the prop is notundefinedon target, rather than just check for the existence of theprop in target.Explicit serialization by adding a
toJSONmethod to queries 80ed4bfanother approach is to allow chaining
toJSON()to the end of a query to explicitly return a sanitized/serialized version of the model, e.g.:using this from a
loaderensures that it explicitly returns the serialized version of a model to provide it to a component, which makes it so that the version of the model that is provided during SSR is exactly equivalent to the version of the model that is made available to the component when rendered on the client. otherwise, the data made available to the component during SSR when using single fetch is the actual model instance, which means it is effectively a superset of the serialized version of the model that is made available to the same component when rendered on the client.this PR implements both approaches, so that returning the raw query result from a loader will work as expected, but you can also choose to explicitly serialize the result by calling
toJSON()at the end of the query. i’m happy to remove either and just go with one or the other if that seems better.also, while debugging this issue, i discovered that model relations will leak fields redacted by the optional
toJSON()overrides, so i fixed that by replacingvalue.serialize()→value.toJSON(): c834819i also migrated the docs site,
examples/remix-cms, andtemplates/remixto use single fetch: 4e94c50the lack of any explicit JSON serialization calls on the models when using single fetch meant i needed to refactor the superflare
BaseModelandQueryBuildertypes. i think the new types are largely more accurate than the previous versions, but it was a headache, and i eventually had to just call it good enough: b63ea4f. one result of the new types is that having the relation type be defined as a union of theRelation | Promise<Relation>no longer works, so i updated those types to all just be the relations, e.g.user: User;: f687a23and lastly, some assorted cleanup: e59a15c (defunct build.d.ts files, adding
sites/appenv var) and 16a71ad (typo/grammar fixes in comments + docs).