Typed API data serialization for frontends
The main problem solved by superest
is the processing of relational data. It automatically links relational data (by defining getters
) according to the primary key to construct nested data objects, so that you could access your data like this: users[3].friendships[4].target.name
(even longer if you want).
- Process primary key related fields to embed related data
- Fairly strongly typed
- Basic data validations
- Computed attributes
- Permanent effective object reference
A Serializer
is a top-level Field
composed by multiple children Field
objects.
Field | Internal Value | External Value |
---|---|---|
StringField |
string |
string |
NumberField |
number |
number |
BooleanField |
boolean |
boolean |
DateField |
Date |
string |
ListField |
Array |
Array |
// general options
const id = new NumberField({});
const pkField = "id";
// types
export type Label = FieldValues<LabelSerializer<{}>>["internal"];
export type User = FieldValues<UserSerializer<{}>>["internal"];
// object pools for storage
export const labels: Record<PK<Label>, Label> = {};
export const users: Record<PK<User>, User> = {};
export class LabelSerializer<Opts extends FieldOptions> extends build({
fields: {
both: {
// fields in both responses and requests
name: new StringField({}),
},
response: {
// fields in responses
id,
},
request: {
// fields in requests
},
},
pkField, // field name of the primary key field
getters: {}, // computed attributes
})<Opts> {
static readonly storage = new Storage(labels);
}
export class UserSerializer<Opts extends FieldOptions> extends build({
fields: {
both: {
username: new StringField({}),
},
response: {
id,
label: new LabelSerializer({}), // nested input
},
request: {
password: new StringField({}),
label: new NumberField({}), // primary key output
},
},
pkField,
getters: {
idGetter: (data) => data.id,
},
})<Opts> {
static readonly storage = new Storage(users);
}
const labelRawResponseData: FieldValues<LabelSerializer<{}>>["rawInternal"] = {
id: 1,
name: "administrators",
createdAt: new Date().toISOString(),
};
const userRawResponseData: FieldValues<UserSerializer<{}>>["rawInternal"] = {
id: 1,
username: "admin",
label: 1, // primary key
};
const labelInternalData = new LabelSerializer({}).toInternal(
labelRawResponseData
)();
labelInternalData.createdAt instanceof Date; // true
labels[labelInternalData.id] == labelInternalData; // true
const userInternalData = new UserSerializer({}).toInternal(
userRawResponseData
)();
userInternalData.idGetter == userInternalData.id; // true
userInternalData.label == labelInternalData; // true
// duplicated serializing: reference will be permanently effective
const duplicatedUserRawResponseData = {
...userRawResponseData,
username: "updated",
};
const duplicatedUserInternalData = new UserSerializer({}).toInternal(
duplicatedUserRawResponseData
)();
duplicatedUserInternalData == userInternalData; // true
new UserSerializer({}).toExternal({
username: "admin",
label: 1,
password: "abcdefg",
});
const raw = {
a: {
a: [
{
a: null,
},
],
},
};
const ret = transformCase(raw, (v) => v.toUpperCase());
// { A: { A: [ { A: null } ] } }
raw == ret; // true
transformCase("a", v.toUpperCase()); // "a"
class Base {
static both = 1;
static base = null;
}
class Mixin1 {
static both = 2;
static mixin1 = null;
}
class Mixin2 {
static both = 3;
static mixin2 = null;
}
const mixined = mixinStatic(Base, Mixin1, Mixin2);
mixined.base == null; // true
mixined.mixin1 == null; // true
mixined.mixin2 == null; // true
mixined.both = 3; // true