Configuring
Here is a semi-complex example .kanelrc.js
configuration file, taken from the example:
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: 'postgres-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: 'postgres-bytea', isAbsolute: true, isDefault: true }] },
// If you want to use BigInt for bigserial columns, you can use the following.
'pg_catalog.int8': 'BigInt',
// Columns with the following types would probably just be strings in TypeScript.
'pg_catalog.bpchar': 'string',
'public.citext': 'string'
},
importsExtension: ""
};
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:
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[];
importsExtension?: ".ts" | ".js" | ".mjs" | ".cjs";
};
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'
:
type Fruit = "apples" | "oranges" | "bananas";
..or, with enumStyle === 'enum'
:
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, which for built-in types means that they should be prefixed with pg_catalog
. So for instance if you want to map float8
to number
(as opposed to the default string
), you would set it like this:
{
'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.
importsExtension
To use a different file extension for project file import references, set importsExtension
to .ts
, .js
, .mjs
, or .cjs
.
If no value is set, no extension will be appended.