Schema Kit
Getting started
import * as z from 'zod';import { getSchemaFromZod, zKysely, renderSchemaAsPDF, generateSQLFromSchema } from '@sluice/schema-kit';Define a schema using Zod
Zod is a TypeScript-first schema declaration and validation library.
Zod is designed to be as developer-friendly as possible. The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type, and with SluiceDB’s Schema Kit, it will also generate a SQL schema for you.
Define each table as a zKysely schema
const SeasonSchema = zKysely( z.object({ season: zKysely(z.union([z.literal(0), z.literal(-1), z.literal(-2)]), { primaryKey: true }) .describe( 'An integer representing the season of the event, whether current (0), last year (-1) or the year before (-2)', ) id: z.number().describe('The robotevents unique identifier for this season'), name: z.string().describe('The name for this VEX season, such as Over Under'), start: z.string().pipe(z.coerce.date()).describe('The start date of this season'), end: z.string().pipe(z.coerce.date()).describe('The end date of this season'), years_start: z.number().describe('The starting year of this season'), years_end: z.number().describe('The ending year of this season'), created_date: z.string().pipe(z.coerce.date()).describe('The timestamp when the record was created'), last_updated: z.string().pipe(z.coerce.date()).nullable().optional().describe('The timestamp when the record was last updated'), }), {},).describe('Table storing three rows of season information the current year, previous year, and year before that');Define overall database from table schemas
const DatabaseSchema = zKysely( z .object({ season: SeasonSchema, }) .describe("Schema for 'Test' Database"), { namespace: 'dev.sluice.schemas:test', appName: 'OffGrid Health', },);
//Inferred Database schema for Kysely and TypeScript
type TestDatabase = z.output<typeof DatabaseSchema>;
const db = new Kysely<TestDatabase>({ dialect: ... });Decompose/reuse/merge subschemas
/** Reusable chunk for season sequence number */const seasonSeqSchemaPrimary = z .union([z.literal(0), z.literal(-1), z.literal(-2)]) .describe( 'An integer representing the season of the event, whether current (0), last year (-1) or the year before (-2)', );
/** Add `created_date` and `last_updated` to Schema */function schemaWithCreatedUpdated<T extends z.ZodRawShape>(schema: z.ZodObject<T>) { return schema.merge( z.object({ created_date: z.string().pipe(z.coerce.date()).describe(descriptions.created_date), last_updated: z.string().pipe(z.coerce.date()).nullable().optional().describe(descriptions.last_updated), }), );}
const SeasonSchema = zKysely( schemaWithCreatedUpdated( z.object({ season: zKysely(seasonSeqSchemaPrimary, { primaryKey: true }), id: z.number().describe('The robotevents unique identifier for this season'), name: z.string().describe('The name for this VEX season, such as Over Under'), start: z.string().pipe(z.coerce.date()).describe('The start date of this season'), end: z.string().pipe(z.coerce.date()).describe('The end date of this season'), years_start: z.number().describe('The starting year of this season'), years_end: z.number().describe('The ending year of this season'), }), ), {},).describe('Table storing three rows of season information the current year, previous year, and year before that');Convert Zod schema to internal format for SluiceDB / Kysely Query Builder
const schema = getSchemaFromZod(DatabaseSchema);expect(schema).toEqual({ schema: { season: { columns: { season: { type: 'integer', notNull: true, primaryKey: true, description: 'An integer representing the season of the event, whether current (0), last year (-1) or the year before (-2)', }, id: { type: 'integer', notNull: true, description: 'The robotevents unique identifier for this season', }, name: { type: 'text', notNull: true, description: 'The name for this VEX season, such as Over Under', }, start: { type: 'timestamp', notNull: true, description: 'The start date of this season', }, end: { type: 'timestamp', notNull: true, description: 'The end date of this season', }, years_start: { type: 'integer', notNull: true, description: 'The starting year of this season', }, years_end: { type: 'integer', notNull: true, description: 'The ending year of this season', }, created_date: { type: 'timestamp', notNull: true, description: 'The timestamp when the record was created', }, last_updated: { type: 'timestamp', notNull: false, description: 'The timestamp when the record was last updated', }, }, description: 'Table storing three rows of season information the current year, previous year, and year before that', }, }, description: "Schema for 'Test' Database", namespace: 'dev.sluice.schemas:test', appName: 'OffGrid Health',})Render the schema as a pdf
Get an Entity Relationship Diagram and inventory of tables and column definitions in a high quality formatted PDF file.
const DatabaseSchema = zKysely( z.object({ season: SeasonSchema }) .describe("Schema for 'Robot Events' Database"), { namespace: 'com.ogscout.schemas:robotevents', appName: 'OffGrid Health', },);
const schema = getSchemaFromZod(DatabaseSchema, true);await renderSchemaAsPDF(schema, path.join(CWD, `out/schema-erd.pdf`));Generate SQL from Schema
With a singular source of truth for your schema, generate SQL for your database and generate TypeScript logic for seamless migrations.
Generate SQL for your database
schema-kit generate-sql --schema ./src/schema.ts --output ./out/data.sqlconst DatabaseSchema = zKysely( z.object({ season: zKysely( z.object({ season: zKysely(z.union([z.literal(0), z.literal(-1), z.literal(-2)]), { primaryKey: true }) .describe( 'An integer representing the season of the event, whether current (0), last year (-1) or the year before (-2)', ) id: z.number().describe('The robotevents unique identifier for this season'), name: z.string().describe('The name for this VEX season, such as Over Under'), start: z.string().pipe(z.coerce.date()).describe('The start date of this season'), end: z.string().pipe(z.coerce.date()).describe('The end date of this season'), years_start: z.number().describe('The starting year of this season'), years_end: z.number().describe('The ending year of this season'), created_date: z.string().pipe(z.coerce.date()).describe('The timestamp when the record was created'), last_updated: z.string().pipe(z.coerce.date()).nullable().optional().describe('The timestamp when the record was last updated'), }), {},).describe('Table storing three rows of season information the current year, previous year, and year before that'); }) .describe("Schema for 'Robot Events' Database"), { namespace: 'com.ogscout.schemas:robotevents', appName: 'OffGrid Health', },);
const schema = getSchemaFromZod(DatabaseSchema);const data = await generateSQLFromSchema(schema); -- Schema for 'Test' Database
-- Table storing three rows of season information the current year, previous year, and year before thatCREATE TABLE "season" ( "season" integer PRIMARY key, -- An integer representing the season of the event, whether current (0), last year (-1) or the year before (-2) "id" integer NOT NULL, -- The robotevents unique identifier for this season "name" TEXT NOT NULL, -- The name for this VEX season, such as Over Under "start" timestamp NOT NULL, -- The start date of this season "end" timestamp NOT NULL, -- The end date of this season "years_start" integer NOT NULL, -- The starting year of this season "years_end" integer NOT NULL, -- The ending year of this season "created_date" timestamp NOT NULL, -- The timestamp when the record was created "last_updated" timestamp -- The timestamp when the record was last updated);Generate Migration Logic for your database
schema-kit generate --schema ./src/schema.ts --output ./out/migration.tsconst DatabaseSchema = zKysely( z.object({ season: zKysely( z.object({ season: zKysely(z.union([z.literal(0), z.literal(-1), z.literal(-2)]), { primaryKey: true }) .describe( 'An integer representing the season of the event, whether current (0), last year (-1) or the year before (-2)', ) id: z.number().describe('The robotevents unique identifier for this season'), name: z.string().describe('The name for this VEX season, such as Over Under'), start: z.string().pipe(z.coerce.date()).describe('The start date of this season'), end: z.string().pipe(z.coerce.date()).describe('The end date of this season'), years_start: z.number().describe('The starting year of this season'), years_end: z.number().describe('The ending year of this season'), created_date: z.string().pipe(z.coerce.date()).describe('The timestamp when the record was created'), last_updated: z.string().pipe(z.coerce.date()).nullable().optional().describe('The timestamp when the record was last updated'), }), {},).describe('Table storing three rows of season information the current year, previous year, and year before that'); }) .describe("Schema for 'Robot Events' Database"), { namespace: 'com.ogscout.schemas:robotevents', appName: 'OffGrid Health', },);
const schema = getSchemaFromZod(DatabaseSchema);const migration = await generateSQLFromSchema(schema);/** * Create Schema for 'Test' Database **/export async function up(kysely: Kysely<any>): Promise<void> { // Table storing three rows of season information the current year, previous year, and year before that await kysely.schema .createTable('season') .addColumn('season', 'integer', (col) => col.primaryKey()) // An integer representing the season of the event, whether current (0), last year (-1) or the year before (-2) .addColumn('id', 'integer', (col) => col.notNull()) // The robotevents unique identifier for this season .addColumn('name', 'text', (col) => col.notNull()) // The name for this VEX season, such as Over Under .addColumn('start', 'timestamp', (col) => col.notNull()) // The start date of this season .addColumn('end', 'timestamp', (col) => col.notNull()) // The end date of this season .addColumn('years_start', 'integer', (col) => col.notNull()) // The starting year of this season .addColumn('years_end', 'integer', (col) => col.notNull()) // The ending year of this season .addColumn('created_date', 'timestamp', (col) => col.notNull()) // The timestamp when the record was created .addColumn('last_updated', 'timestamp') // The timestamp when the record was last updated .execute();
}
export async function down(kysely: Kysely<any>): Promise<void> { await kysely.schema.dropTable('season').execute();}