0.6.0 • Published 3 years ago
superest v0.6.0
superest
Typed API data serialization for frontends
Features
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
Usage
Creating Serializers
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);
}
Serializing
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
Deserializing
new UserSerializer({}).toExternal({
username: "admin",
label: 1,
password: "abcdefg",
});
Utilities
transformCase()
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"
mixinStatic()
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