Database driven code generation for ts-sql-query

Overview

ts-sql-codegen

This utility generates table mapper classes for ts-sql-query by inspecting a database through tbls.

While it is possible to use ts-sql-query without any code-generation, we have to manually keep the table-mappings in sync with the database which is burdensome.

With this utility, this mapping can be derived from the database itself as part of build process, eliminating the manual effort, reducing room for erroneous mapping and enabling true end-to-end type-safety.

The code-generation process is highly customizable and almost all the defaults (which are sensible for most databases) can be configured if needed.

Features

  1. Generate clean typescript table/view mappers from database schema
  2. Ability to include/exclude tables/fields
  3. Ability to customize field types and type adapters
  4. Idiomatic pascal-cased/camel-cased table/field name mapping by default and ability to customize names if desired.
  5. Auto-detection & mapping of computed fields, primary key columns.
  6. Automatic documentation propagation from SQL comments

Installation:

Step 1: Install tbls and ensure it is available in path

Refer: https://github.com/k1LoW/tbls#install

Step 2: Install ts-sql-codegen

npm i --dev ts-sql-codegen

Note:

  • Global installation (npm i -g ts-sql-codegen) can be convenient, but is preferrable to have ts-sql-codegen as a project dependency to avoid versioning issues.

Usage (CLI):

After every database schema change:

Step 1: Generate yaml schema file from database using tbls

Example: tbls out postgres://postgres:password@localhost:5432/testdb -t yaml -o schema.yaml

Step 2: Pass this schema file to ts-sql-codegen

Example: ts-sql-codegen --schema ./schema.yaml --output-dir ./src/generated --connection-source ./src/db/connection-source

Note:

  • All paths are relative to cwd
  • Above options are default, so you can also just run ts-sql-codegen

Usage (Programmatic):

Programmatic usage enables a wider set of customization options.

Example:

const { Generator } = require('ts-sql-codegen');

const options = {
   schemaPath: './schema.yaml',
   connectionSourcePath: './connection-source.ts'
}
const generator = new Generator(options);
await generator.generate();

Refer to Generator and GeneratorOpts for available options.

The test suite also has examples of more complex customizations.

For advanced use-cases (eg. custom templates, pre/post processing of generated code and custom logic for table/column/field mapping) it is recommended to extend the Generator class in project. We intend to keep the customization options that the constructor accepts focussed on primary common use-cases.

Suggested workflow

This utility is expected to be used in a database-first workflow, where the developers first plan and implement the database level changes, and then adapt their code for the updated schema.

  1. Use a database migration system for versioned database schema evolution. You are free to choose a migration utility that you like (eg. dbmate, liquibase, flyway etc.) - if you are starting out we recommend dbmate, a simple and easy to use solution.
  2. Integrate the following steps into your build lifecycle
    1. Use migration utility to update database schema eg. dbmate up
    2. Dump yaml representation of schema through tbls eg. tbls out postgres://localhost:5432/mydb -t yaml -o schema.yaml
    3. Generate code using ts-sql-codegen eg. ts-sql-codegen --schema ./schema.yaml # additional options

Recipies

Generating mappers for a subset of available tables or fields

const options = {
    schemaPath,
    connectionSourcePath,
    outputDirPath,
    tables: {
        // Include only whitelisted tables
        include: [/authors/, "books"],
        // Similar exclude can be used to blacklist
    },
    fieldMappings: [
        {
            tableName: "authors",
            columnName: "name",
            // Ignore this field when generating table mapper
            generatedField: false,
        }
    ]
}

⚠️ We don't do anything to ensure that database operations will succeed with included columns. Eg. if any omitted columns are mandatory they will cause inserts to fail.

Custom DB types

ts-sql-query supports custom database types through type-adapters.

You can configure the code-generator to use type-adapters for specific columns or specific database types through field mappings.

const options = {
  schemaPath: './schema.yaml',
  connectionSourcePath: './connection-source.ts',
  fieldMappings: [
    {
        // Field matching criteria: 
        //
        // Match any column of any table where column type in database
        // is the class_participation_policy custom type
        columnType: "class_participation_policy",

        // For fields matched by above criteria,
        // use the ClassParticipationPolicyAdapter type adapter
        // which you will have to implement. 
        // 
        // The import paths are resolved relative to cwd, and converted
        // to relative paths wrt the generated file
        // 
        // Generated code will include an import like this:
        //     import { ClassParticipationPolicyAdapter, ClassParticipationPolicy } from '../adapters';
        generatedField: {
            type: {
                kind: 'custom',
                adapter: {
                    name: "ClassParticipationPolicyAdapter",
                    importPath: "src/db/adapters",
                },
                tsType: {
                    name: "ClassParticipationPolicy",
                    importPath: "src/db/adapters",
                },
                dbType: {
                    name: 'class_participation_policy'
                }
            },
        },
    },
    {
        // Alternatively, the field matching criteria
        // can point to specific column of a specific table
        tableName: "classrooms",
        columnName: 'participation_policy',
        // Instead of exact strings above, we could also use regular expressions

        generatedField: { ... }
    }
  ]
}

Multiple databases/schema

The codegenerator does not have any special support for multi-db or multi-schema scenarios.

You can simply run ts-sql-codegen multiple times for different databases/different schema.

Filtering tables by schema

The tbls schema dump contains table names with schema prefix. We can target this prefix in table inclusion criteria:

const options = {
  tables: {
    include: [/^public\..*/]
  }
}

This can be helpful, for instance, if we want tables from different schema to be generated with different configurations or different output directories.

Disambiguating tables in multi-schema scenarios

Use of tableMapping.useQualifiedTableName=true is recommended when the application can simultaneously use tables from multiple schema

const options = {
  tableMapping: {
    useQualifiedTableName: true
  }
}

With this option the output looks like:

export class AuthorsTable extends Table<DBConnection, 'PublicAuthorsTable'> {
    //                                                 ~~~~~~
    //                                                   ^
    // .. fields ...
    constructor() {
        super('public.authors')
        //     ~~~~~~~
        //        ^
    }
}

Specifying id prefix for multi-db scenarios

Use of idPrefix is recommended to ensure that table ids passed to ts-sql-query is unique when application can connect to tables with same name from multiple databases.

const options = {
  tableMapping: {
    idPrefix: 'ReportingDB'
  }
}

With this option the output looks like:

export class AuthorsTable extends Table<DBConnection, 'ReportingDBAuthorsTable'> {
    //                                                 ~~~~~~~~~~~
    //                                                     ^
    // .. fields ...
    constructor() {
        super('authors')
    }
}

This option will override the id prefix derived from schema name if tableMapping.useQualifiedTableName is true.

Known Limitations

  1. While ts-sql-codegen works with many databases and adapters, this utility has been tested only with postgresql & sqlite. Please report bugs if you face issues.
  2. Only databases which are supported by both ts-sql-query and tbls can be supported.
  3. Enum/custom type inspection support is currently limited - it is required to manually specify typescript types and adapters for now.
  4. Typescript is assumed - plain js projects are not supported currently

Contributing

Thanks for your interest in contributing to this project. Pull requests and feature enhancements are welcome.

This utility is being used in a project with hundreds of tables, so any backward incompatible changes in generated code are highly undesirable.

Feature flags are recommended for aspects which are not beneficial to all/most users.

Code-generation should be last resort - if some feature can be supported in ts-sql-query itself, we recommending creating a PR there.

You might also like...

🔥 Dreamy-db - A Powerful database for storing, accessing, and managing multiple database.

🔥 Dreamy-db - A Powerful database for storing, accessing, and managing multiple database.

Dreamy-db About Dreamy-db - A Powerful database for storing, accessing, and managing multiple databases. A powerful node.js module that allows you to

Dec 22, 2022

DolphinDB JavaScript API is a JavaScript library that encapsulates the ability to operate the DolphinDB database, such as: connecting to the database, executing scripts, calling functions, uploading variables, etc.

DolphinDB JavaScript API English | 中文 Overview DolphinDB JavaScript API is a JavaScript library that encapsulates the ability to operate the DolphinDB

Dec 12, 2022

An easy-to-use multi SQL dialect ORM tool for Node.js

Sequelize Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite and Microsoft SQL Server. It features solid transaction s

Jan 4, 2023

ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.

ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, SAP Hana, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.

TypeORM is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used

Jan 3, 2023

An SQL-friendly ORM for Node.js

Objection.js Objection.js is an ORM for Node.js that aims to stay out of your way and make it as easy as possible to use the full power of SQL and the

Jan 5, 2023

An adapter-based ORM for Node.js with support for mysql, mongo, postgres, mssql (SQL Server), and more

An adapter-based ORM for Node.js  with support for mysql, mongo, postgres, mssql (SQL Server), and more

Waterline is a next-generation storage and retrieval engine, and the default ORM used in the Sails framework. It provides a uniform API for accessing

Jan 4, 2023

Microsoft SQL Server client for Node.js

node-mssql Microsoft SQL Server client for Node.js Supported TDS drivers: Tedious (pure JavaScript - Windows/macOS/Linux, default) Microsoft / Contrib

Jan 4, 2023

TypeScript clients for databases that prevent SQL Injection

Safe From HTML Injection Using tagged template literals for queries, e.g. db.query(sql`SELECT * FROM users WHERE id=${userID}`); makes it virtually im

Dec 21, 2022

Conjure SQL from GraphQL queries 🧙🔮✨

Conjure SQL from GraphQL queries 🧙🔮✨

Sqlmancer Conjure SQL from your GraphQL queries 🧙 🔮 ✨ ⚠️ This project is currently on hiatus. I am hoping to resume working on Sqlmancer once I have

Oct 30, 2022
Comments
  • Add support to customise the naming of the generated items

    Add support to customise the naming of the generated items

    I kept the naming rules you already implemented:

    export const NamingOptionsSchema = z.object({
        /**
         * Prefix to be used in the name of the class that reprecents a table
         */
        tableClassNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the class that reprecents a table
         */
        tableClassNameSuffix: z.string().default('Table'),
        /**
         * Prefix to be used in the name of the class that reprecents a view
         */
        viewClassNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the class that reprecents a view
         */
        viewClassNameSuffix: z.string().default('Table'),
        /**
         * Prefix to be used in the name of the instance of the class that reprecents a table
         */
        tableInstanceNamePrefix: z.string().default('t'),
        /**
         * Suffix to be used in the name of the instance of the class that reprecents a table
         */
        tableInstanceNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the instance of the class that reprecents a view
         */
        viewInstanceNamePrefix: z.string().default('t'),
        /**
         * Suffix to be used in the name of the the instance of class that reprecents a view
         */
        viewInstanceNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the InsertableRow type
         */
        insertableRowTypeNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the InsertableRow type
         */
        insertableRowTypeNameSuffix: z.string().default('IRow'),
        /**
         * Prefix to be used in the name of the UpdatableRow type
         */
        updatableRowTypeNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the UpdatableRow type
         */
        updatableRowTypeNameSuffix: z.string().default('URow'),
        /**
         * Prefix to be used in the name of the SelectedRow type
         */
        selectedRowTypeNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the SelectedRow type
         */
        selectedRowTypeNameSuffix: z.string().default('SRow'),
        /**
         * Prefix to be used in the name of the InsertableValues type
         */
        insertableValuesTypeNamePrefix: z.string().default('Insertable'),
        /**
         * Suffix to be used in the name of the InsertableValues type
         */
        insertableValuesTypeNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the UpdatableValues type
         */
        updatableValuesTypeNamePrefix: z.string().default('Updatable'),
        /**
         * Suffix to be used in the name of the UpdatableValues type
         */
        updatableValuesTypeNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the SelectedValues type
         */
        selectedValuesTypeNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the SelectedValues type
         */
        selectedValuesTypeNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the const with the column list of a table
         */
        tableColumnsNamePrefix: z.string().default('t'),
        /**
         * Suffix to be used in the name of the const with the column list of a table
         */
        tableColumnsNameSuffix: z.string().default('Cols'),
        /**
         * Prefix to be used in the name of the const with the column list of a view
         */
        viewColumnsNamePrefix: z.string().default('t'),
        /**
         * Suffix to be used in the name of the const with the column list of a view
         */
        viewColumnsNameSuffix: z.string().default('Cols'),
    });
    

    But, I suggest to put better defaults:

    export const NamingOptionsSchema = z.object({
        /**
         * Prefix to be used in the name of the class that reprecents a table
         */
        tableClassNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the class that reprecents a table
         */
        tableClassNameSuffix: z.string().default('Table'),
        /**
         * Prefix to be used in the name of the class that reprecents a view
         */
        viewClassNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the class that reprecents a view
         */
        viewClassNameSuffix: z.string().default('View'),
        /**
         * Prefix to be used in the name of the instance of the class that reprecents a table
         */
        tableInstanceNamePrefix: z.string().default('t'),
        /**
         * Suffix to be used in the name of the instance of the class that reprecents a table
         */
        tableInstanceNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the instance of the class that reprecents a view
         */
        viewInstanceNamePrefix: z.string().default('v'),
        /**
         * Suffix to be used in the name of the the instance of class that reprecents a view
         */
        viewInstanceNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the InsertableRow type
         */
        insertableRowTypeNamePrefix: z.string().default('Insertable'),
        /**
         * Suffix to be used in the name of the InsertableRow type
         */
        insertableRowTypeNameSuffix: z.string().default('Row'),
        /**
         * Prefix to be used in the name of the UpdatableRow type
         */
        updatableRowTypeNamePrefix: z.string().default('Updatable'),
        /**
         * Suffix to be used in the name of the UpdatableRow type
         */
        updatableRowTypeNameSuffix: z.string().default('Row'),
        /**
         * Prefix to be used in the name of the SelectedRow type
         */
        selectedRowTypeNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the SelectedRow type
         */
        selectedRowTypeNameSuffix: z.string().default('Row'),
        /**
         * Prefix to be used in the name of the InsertableValues type
         */
        insertableValuesTypeNamePrefix: z.string().default('Insertable'),
        /**
         * Suffix to be used in the name of the InsertableValues type
         */
        insertableValuesTypeNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the UpdatableValues type
         */
        updatableValuesTypeNamePrefix: z.string().default('Updatable'),
        /**
         * Suffix to be used in the name of the UpdatableValues type
         */
        updatableValuesTypeNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the SelectedValues type
         */
        selectedValuesTypeNamePrefix: z.string().default(''),
        /**
         * Suffix to be used in the name of the SelectedValues type
         */
        selectedValuesTypeNameSuffix: z.string().default(''),
        /**
         * Prefix to be used in the name of the const with the column list of a table
         */
        tableColumnsNamePrefix: z.string().default('t'),
        /**
         * Suffix to be used in the name of the const with the column list of a table
         */
        tableColumnsNameSuffix: z.string().default('Columns'),
        /**
         * Prefix to be used in the name of the const with the column list of a view
         */
        viewColumnsNamePrefix: z.string().default('v'),
        /**
         * Suffix to be used in the name of the const with the column list of a view
         */
        viewColumnsNameSuffix: z.string().default('Columns'),
    });
    
    opened by juanluispaz 2
  • Add support to customComparable fields

    Add support to customComparable fields

    and allow to include in the custom, customComparable and enum the type name in the column definition to make easier understand the column.

    Please, release a new version with this changes.

    opened by juanluispaz 1
Owner
Lorefnon
Software Developer and Consultant enthusiastic about web technologies & cloud architecture.
Lorefnon
Connect to private Google Cloud SQL instance through Cloud SQL Auth Proxy running in Kubernetes.

⛅ google-cloud-sql A CLI app which establishes a connection to a private Google Cloud SQL instance and port-forwards it to a local machine. Connection

Dinko Osrecki 10 Oct 16, 2022
Next-generation ORM for Node.js & TypeScript | PostgreSQL, MySQL, MariaDB, SQL Server & SQLite

Prisma Quickstart • Website • Docs • Examples • Blog • Slack • Twitter • Prisma 1 What is Prisma? Prisma is a next-generation ORM that consists of the

Prisma 28k Jan 2, 2023
Lovefield is a relational database for web apps. Written in JavaScript, works cross-browser. Provides SQL-like APIs that are fast, safe, and easy to use.

Lovefield Lovefield is a relational database written in pure JavaScript. It provides SQL-like syntax and works cross-browser (currently supporting Chr

Google 6.8k Jan 3, 2023
AlaSQL.js - JavaScript SQL database for browser and Node.js. Handles both traditional relational tables and nested JSON data (NoSQL). Export, store, and import data from localStorage, IndexedDB, or Excel.

Please use version 1.x as prior versions has a security flaw if you use user generated data to concat your SQL strings instead of providing them as a

Andrey Gershun 6.1k Jan 9, 2023
Database manager for MySQL, PostgreSQL, SQL Server, MongoDB, SQLite and others. Runs under Windows, Linux, Mac or as web application

Database manager for MySQL, PostgreSQL, SQL Server, MongoDB, SQLite and others. Runs under Windows, Linux, Mac or as web application

DbGate 2k Dec 30, 2022
Run SPARQL/SQL queries directly on Virtuoso database with connection pool support.

?? virtuoso-connector Package that allows you to create a direct connection to the Virtuoso database and run queries on it. Connection can be used to

Tomáš Dvořák 6 Nov 15, 2022
A typesafe database ORM that exposes the full power of handwritten sql statements to the developer.

TORM A typesafe database ORM that exposes the full power of handwritten sql statements to the developer. import { torm, z } from 'https://deno.land/x/

Andrew Kaiser 15 Dec 22, 2022
A Node.js library for retrieving data from a PostgreSQL database with an interesting query language included.

RefQL A Node.js library for retrieving data from a PostgreSQL database with an interesting query language included. Introduction RefQL is about retrie

Rafael Tureluren 7 Nov 2, 2022
Type Driven Database Framework.

cosmotype Type Driven Database Framework. Features Compatibility. Complete driver-independent. Supports many drivers with a unified API. Powerful. It

null 8 Dec 15, 2022