Skip to content

Configuring

Here is a semi-complex example .kanelrc.js configuration file, taken from the example:

js
const { join } = require('path');
const { recase } = require('@kristiandupont/recase');
const { tryParse } = require('tagged-comment-parser')

const { generateIndexFile } = require('kanel');
const { 
  makeGenerateZodSchemas, 
  defaultGetZodSchemaMetadata, 
  defaultGetZodIdentifierMetadata, 
  defaultZodTypeMap 
} = require('kanel-zod');

const toPascalCase = recase('snake', 'pascal');
const outputPath = './example/models';

const generateZodSchemas = makeGenerateZodSchemas({
  getZodSchemaMetadata: defaultGetZodSchemaMetadata,
  getZodIdentifierMetadata: defaultGetZodIdentifierMetadata,
  zodTypeMap: {
    ...defaultZodTypeMap,
    'pg_catalog.tsvector': 'z.set(z.string())',
    'pg_catalog.bytea': { name:'z.custom<Bytea>(v => v)', typeImports: [{ name: 'Bytea', path: 'bytea', isAbsolute: true, isDefault: false }] }
  },
  castToSchema: true
})

/** @type {import('../src/Config').default} */
module.exports = {
  connection: {
    host: 'localhost',
    user: 'postgres',
    password: 'postgres',
    database: 'dvdrental',
    charset: 'utf8',
    port: 54321,
  },

  outputPath,
  resolveViews: true,
  preDeleteOutputFolder: true,

  // Add a comment about the entity that the type represents above each type.
  getMetadata: (details, generateFor) => {
    const { comment: strippedComment } = tryParse(details.comment);
    const isAgentNoun = ['initializer', 'mutator'].includes(generateFor);

    const relationComment = isAgentNoun
      ? `Represents the ${generateFor} for the ${details.kind} ${details.schemaName}.${details.name}`
      : `Represents the ${details.kind} ${details.schemaName}.${details.name}`;

    const suffix = isAgentNoun ? `_${generateFor}` : '';

    return {
      name: toPascalCase(details.name + suffix),
      comment: [relationComment, ...(strippedComment ? [strippedComment] : [])],
      path: join(outputPath, toPascalCase(details.name)),
    };
  },

  // Add a comment that says what the type of the column/attribute is in the database.
  getPropertyMetadata: (property, _details, generateFor) => {
    const { comment: strippedComment } = tryParse(property.comment);

    return {
      name: property.name,
      comment: [
        `Database type: ${property.expandedType}`,
        ...(generateFor === 'initializer' && property.defaultValue
          ? [`Default value: ${property.defaultValue}`]
          : []),
        ...(strippedComment ? [strippedComment] : []),
      ]
    }
  },


  // This implementation will generate flavored instead of branded types.
  // See: https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/
  generateIdentifierType: (c, d) => {
    // Id columns are already prefixed with the table name, so we don't need to add it here
    const name = toPascalCase(c.name);

    return {
      declarationType: 'typeDeclaration',
      name,
      exportAs: 'named',
      typeDefinition: [`number & { __flavor?: '${name}' }`],
      comment: [`Identifier type for ${d.name}`],
    };
  },

  // Generate an index file with exports of everything
  preRenderHooks: [generateZodSchemas, generateIndexFile],

  customTypeMap: {
    // A text search vector could be stored as a set of strings. See Film.ts for an example.
    'pg_catalog.tsvector': 'Set<string>',

    // The bytea package (https://www.npmjs.com/package/postgres-bytea) could be used for byte arrays.
    // See Staff.ts for an example.
    'pg_catalog.bytea': { name: 'bytea', typeImports: [{ name: 'bytea', path: 'bytea', isAbsolute: true, isDefault: true }] },

    // Columns with the following types would probably just be strings in TypeScript.
    'pg_catalog.bpchar': 'string',
    'public.citext': 'string'
  },
};

Migrating from v2

The update to version 3 introduced several breaking changes. If you are doing this migration, check out the guide for help.


The Config type

The configuration type is defined in the Config.ts file:

ts
export type Config = {
  connection: string | ConnectionConfig;
  schemas?: string[];
  typeFilter?: (pgType: PgType) => boolean;
  getMetadata?: GetMetadata;
  getPropertyMetadata?: GetPropertyMetadata;
  generateIdentifierType?: GenerateIdentifierType;
  propertySortFunction?: (a: CompositeProperty, b: CompositeProperty) => number;

  enumStyle?: "enum" | "type";

  outputPath?: string;
  preDeleteOutputFolder?: boolean;
  customTypeMap?: TypeMap;
  resolveViews?: boolean;

  preRenderHooks?: PreRenderHook[];
  postRenderHooks?: PostRenderHook[];
};

connection

The only required property in the config object is connection.

This is the database connection object. It follows the client constructor in pg. As you will typically want to run Kanel on your development machine, you probably want a simple localhost connection as in the example above.

schemas

The schemas property can be an array of strings. This will be used as the list of schema names to include when generating types. If omitted, all the non-system schemas found in the database will be processed.

typeFilter

The typeFilter property allows you to choose which types (be that tables, views, enums, or whatever else) to include.

getMetadata, getPropertyMetadata and generateIdentifierType

If you really want to customize the behavior of Kanel, you can provide values for the getMetaData, the getPropertyMetadata and/or the generateIdentifierType functions.

propertySortFunction

The propertySortFunction can be supplied if you want to customize how the properties in types should be sorted. The default behavior is to put primary keys at the top and otherwise follow the ordinal order as is specified in the database.

enumStyle

The enumStyle can be either type or enum (default). Postgres enums will then be turned into either string unions or Typescript enums.

This, if you have an enum Fruit consisting of the values apples, oranges and bananas, you will get this type with enumStyle === 'type':

typescript
type Fruit = "apples" | "oranges" | "bananas";

..or, with enumStyle === 'enum':

typescript
enum Fruit {
  apples = "apples",
  oranges = "oranges",
  bananas = "bananas",
}

outputPath

The outputPath specifies the root for the output files. The default implementation of getMetadata will place files in ${outputPath}/${schemaName}/${typeName}.ts.

preDeleteOutputFolder

If you set preDeleteOutputFolder to true, Kanel will delete all contents in the folder before writing types. This is recommended as it will make sure you don't have old model files of no-longer-existing database entities sitting around. Obviously it means that you shouldn't mix in any manually written code in that same folder though.

customTypeMap

The customTypeMap property can be set if you want to specify what a given type should map to. It's a record of a postgres typename to a Typescript type. The key is qualified with schema name, so for instance if you want to map float8 to number (as opposed to the default string), you would set it like this:

typescript
{
  'pg_catalog.float8': 'number'
}

resolveViews

If you set resolveViews to true, Kanel will attempt to give you better types for views. If a view returns, say, a column called account_id that represents a foreign key in the original table, we would like the resulting type to be AccountId or whatever we call our identifier types. Similarly, we would like to mark it as nullable only if the "source" column is nullable. Postgres will per default not give us these data about views, so Kanel attempts to infer them by parsing the SQL that defines the view. Obviously, this non-trivial and there are situations where it fails so use with slight caution.

preRenderHooks

The preRenderHooks property can be set if you want to supply one or more hooks that will run before the render step. At this point, Kanel has gathered a record of file paths and corresponding Declaration arrays. A declaration is an abstract bit of Typescript like an interface or type definition.

See the preRenderHooks section for more info.

postRenderHooks

If you need to do something more brute-force like, you might prefer to create one or more postRenderHooks, which will be called with a filename and an array of strings which are the raw contents, just before the file is written.

See the postRenderHooks section for more info.