Getting started
Quick start
Install
npm install helice
# or
pnpm add helice
# or
yarn add heliceimport { Helice } from 'helice';
// 1. Define your database schema as plain TypeScript types
type User = { id: number; name: string; email: string; active: boolean };
type Post = { id: number; author_id: number; title: string; published: boolean; views: number };
type MyDB = { user: User; post: Post };
// 2. Create a Helice instance typed against your schema
const db = new Helice<MyDB>();
// 3. Build queries
const { query, args } = db.select('post')
.where({ published: true })
.build();
// query: "SELECT *\nFROM post\nWHERE (\n\tpublished = $1\n)"
// args: [true]Pass the generated query and args straight to your PostgreSQL client (pg, postgres, whatever). The generated query follows PostgreSQL syntax.
Concepts
The Environment
The first thing you give Helice is your database schema — just TypeScript types, no schema file, no codegen, no decorators.
An Environment is a type that describes your Database. Its keys are table names and its values describe the shape of a row in that table:
type UserTable = { id: number; name: string; email: string; active: boolean };
type PostTable = { id: number; author_id: number; title: string; published: boolean; views: number };
type CommentTable = { id: number; post_id: number; body: string };
type Environment = { user: UserTable; post: PostTable; comment: CommentTable };
const db = new Helice<Environment>();That's it. From this point, every builder method is fully typed against Environment — table names, column names, their types, valid comparisons — TypeScript will catch anything that doesn't exist or doesn't match.
NOTE: Helice interpolates table and column names as-is, without identifier quoting. Names that collide with PostgreSQL reserved words (
order,group, …), contain uppercase letters, or special characters are not supported — stick to lowercase snake_case identifiers.
SyntaxKeys
I really do enjoy SQL and its vocabulary. With Hélice, I only intend to ease query writing, not replacing SQL syntax. Thus Helice keeps the structure of SQL clauses intact: WHERE, JOIN, WITH etc. However, each clause operators required a specific syntax that I had to define. Should we use compact anotation based on symbols ? Or maybe a syntax as close as possible to plain SQL ? Since I couldn't make up my mind, I implemented a fully customizable syntax through a SyntaxKey object which defines the set of operators and keys to be used. Two sets ship out of the box:
DefaultSyntaxKeys — a compact symbolic set:
.where({ '>=:views': 500, '~~:title': '%hello%' })
.field('id@postId') // @ as alias separator
.join({ 'i#user': '...' }) // i# for INNER JOINVerboseSyntaxKeys — a more SQL close syntax:
.where({ 'views >=': 500, 'title ~~': '%hello%' })
.field('id AS postId')
.join({ 'INNER JOIN user': '...' })You're free to write your own by creating an object that satisfies SyntaxKeysConstant. To use a specific SyntaxKeys, you just need to pass it as both a generic and constructor argument:
import { Helice, VerboseSyntaxKeys } from 'helice';
const db = new Helice<MyDB, VerboseSyntaxKeys>(VerboseSyntaxKeys);Every token Helice uses in types and parsers will follow your custom vocabulary end-to-end.