A flexible library for serializing records into customizable views written in TypeScript. Inspired by Blueprinter, Record Sculptor aims to provide a simple yet powerful way to present and manipulate data objects.
Using npm:
npm install --save record-sculptor
See examples/README.md for a more realistic example.
TODO: validate this code
// src/serializers/user-serialiser.ts
import { Serializer } from "record-sculptor"
import { User, Role } from "@/models"
import { RoleSerializer } from "@/serializers"
const UserSerializer = Serializer.define<User>(({ addView }) => {
// Default view, put common stuff here, but prefer named views for anything specific or complex
addView((view) => {
view.addFields("id", "email", "firstName", "lastName", "isAdmin", "createdAt")
view.addField("displayName", (user: User): string => `${user.firstName} ${user.lastName}`)
})
// Reuses all the fields from the default view, and adds a new roles field
// TODO: implement this fallback to default feature
addView("table", (view) => {
view.addField("roles", (user: User): string[] => user.roles.map((r) => r.name))
})
// Reuses all the fields from the default view, and makes use of another serializer
addView("detailed", (view) => {
view.addField("roles", (user: User) => RoleSerializer.serialize(user.roles))
})
})
// src/serializers/role-serializer.ts
import { serializer } from "record-sculptor"
import { Role } from "@/models"
class RoleSerializer extends Serializer<Role> {
constructor(roleOrRoles: Role | Array<Role>) {
super(roleOrRoles)
this.addView((view) => {
view.addFields("id", "userId", "name")
})
}
}
// src/routes.ts
import express, { type Request, type Response } from "express"
import { User } from "@/models"
import { UserSerializer } from "@/serializers"
export const router = express.Router()
router.get("/users", async (request: Request, response: Response) => {
const users = await User.findAll() // Retrieval from database, using Sequelize in this example
const serializedUsers = UserSerializer.serialize(users, { view: "table" }) // Data presentation/serialization
return response.status(200).json({ users: serializedUsers }) // Send data
})
router.post("/users", async (request: Request, response: Response) => {
const newAttributes = request.body
return User.create(newAttributes)
.then((user) => {
// Save to database, using Sequelize in this example
const serializedUser = UserSerializer.serialize(user, { view: "detailed" }) // Data presentation/serialization
return response.status(201).json({ user: serializedUser }) // Send data
})
.catch((error) => {
return response.status(422).json({ error: error.message }) // Handle errors
})
})
router.get("/users/:id", async (request: Request, response: Response) => {
const id = request.params.id
const user = await User.findByPk(id) // Retrieval from database, using Sequelize in this example
if (user === null) {
return response.status(404).json({ message: "User not found" }) // Handle errors
}
const serializedUser = UserSerializer.serialize(user, { view: "detailed" }) // Data presentation/serialization
return response.status(200).json({ user: serializedUser }) // Send data
})
router.put("/users/:id", async (request: Request, response: Response) => {
const id = request.params.id
const user = await User.findByPk(id) // Retrieval from database, using Sequelize in this example
if (user === null) {
return response.status(404).json({ message: "User not found" }) // Handle errors
}
const newAttributes = request.body
return user
.update(newAttributes)
.then((updatedUser) => {
const serializedUser = UserSerializer.serialize(updatedUser, { view: "detailed" }) // Data presentation/serialization
return response.status(200).json({ user: serializedUser }) // Send data
})
.catch((error) => {
return response.status(422).json({ error: error.message }) // Handle errors
})
})
// Delete does use serializer as there is no point in returning anything
router.delete("/users/:id", async (request: Request, response: Response) => {
const id = request.params.id
// You _could_ peform a direct delete with Sequelize, but that makes the code harder to read,
// and the optimization probably isn't worth it anyway.
const user = await User.findByPk(id) // Retrieval from database, using Sequelize in this example
if (user === null) {
return response.status(404).json({ message: "User not found" }) // Handle errors
}
return user
.destroy()
.then(() => {
return response.status(204).send() // Send empty response implies success
})
.catch((error) => {
return response.status(422).json({ error: error.message }) // Handle errors
})
})
// src/models/user.ts
import { Role } from "@/models"
class User {
constructor(
public id: number,
public email: string,
public firstName: string,
public lastName: string,
public isAdmin?: boolean,
public createdAt?: Date,
public roles?: Array<Role>,
) {}
}
// src/modles/role.ts
class Role {
constructor(
public id: number,
public userId: number,
public name: string,
) {}
}
-
Install
asdf
from https://asdf-vm.com/guide/getting-started.html -
Install the
nodejs
plugin for asdf viaasdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git
-
Install the appropriate local
nodejs
version viaasdf install nodejs
-
Install the project dependencies via
npm install
-
Run the test suite via
npm run test
See https://www.freecodecamp.org/news/how-to-create-and-publish-your-first-npm-package/
-
Build this project via
npm run clean && npm run build
-
Make a directory relative to the project directory called
test-record-sculptor-import
-
Change into the new project directory.
-
Run
npm link ../record-sculptor
. Or if you are testing against a published version you can usenpm install record-sculptor
. -
Create a
test-record-sculptor-import.ts
file in the new project with this code in it.// test-record-sculptor-import/test-record-sculptor-import.ts import { Serializer } from "record-sculptor" class User { constructor( public id: number, public email: string, public firstName: string, public lastName: string, public isAdmin?: boolean, public createdAt?: Date, // public roles?: Array<Role> ) {} } const UserSerializer = Serializer.define<User>(({ addView }) => { addView((view) => { view.addFields("id", "email", "firstName", "lastName", "isAdmin", "createdAt") view.addField("displayName", (user: User): string => `${user.firstName} ${user.lastName}`) }) }) console.log( UserSerializer.serialize( new User(1, "john.doe@example.com", "John", "Doe", true, new Date("2021-01-01T12:00:00Z")), ), )
-
Run
npx ts-node test-record-sculpture-import.ts
and check that it prints the appropriate record info.
See https://www.freecodecamp.org/news/how-to-create-and-publish-your-first-npm-package/
npm pack
will generate the tar file that npm publish will publish, use it to test if you are publishing what you want to publish.
-
Run
npm login
-
Run
npm publish
ornpm publish --tag alpha
(defaults to latest)
To update your version number:
-
npm version patch
(see https://docs.npmjs.com/about-semantic-versioning) -
npm run publish
(see https://docs.npmjs.com/adding-dist-tags-to-packages) -
git push --tags
to push release tags.
TODO: move away from lodash after I've built the basics, so as to keep this project light.