3 ms·
I've been using a fork of schemats for a while, it is a great tool. We quickly figured that using the table definitions as our app layer models wouldn't cut it
by ht85 5y ago
I've been using a fork of schemats for a while, it is a great tool.
We quickly figured that using the table definitions as our app layer models wouldn't cut it, notably to handle polymorphism and non ideal (sic) table design.
We decided to "hand write" (mostly codegen) the types we use in the business logic and have a translation layer between those and the interfaces generated by schemats. For simple tables the mapping is 1:1, but for complicated tables we centralize type checks and defensive assertions there. For polymorphism it would look like this:
CREATE TYPE invoice_line_type AS ENUM('product', 'shipment');
CREATE TABLE invoice_lines AS (
id SERIAL PRIMARY KEY,
type invoice_line_type NOT NULL,
product_id INT REFERENCES products(id),
shipment_id INT REFERENCES shipments(id),
amount NUMERIC NOT NULL,
CHECK (CASE WHEN type = 'product' THEN product_id IS NOT NULL ELSE TRUE END),
CHECK (CASE WHEN type = 'shipment' THEN shipment_id IS NOT NULL ELSE TRUE END)
);
The translation layer:
import { invoice_lines, invoice_line_type } from 'src/schema'
export type ProductInvoiceLine = {
id: number
type: invoice_line_type.product
product_id: number
amount: number
}
export type ShipmentInvoiceLine = {
id: number
type: invoice_line_type.shipment
shipment_id: number
amount: number
}
export const parse = (row: invoice_lines): ProductInvoiceLine | ShipmentInvoiceLine => {
const base = {
id: row.id,
amount: row.amount,
}
switch (row.type) {
case invoice_line_type.product:
return {
...base,
type: invoice_line_type.product,
product_id: ensureNumber(row.product_id),
}
case invoice_line_type.shipment:
return {
...base,
type: invoice_line_type.shipment,
shipment_id: ensureNumber(row.shipment_id),
}
}
}