Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ Singularize generated type aliases, e.g. as `BlogPost` instead of `BlogPosts`. T

You can specify custom singularization rules in the [configuration file](#configuration-file).

#### --timestamp-parser <!-- omit from toc -->

Specify which parser to use for PostgreSQL timestamp values. (values: `js-date`/`string`, default: `js-date`)

#### --type-only-imports <!-- omit from toc -->

Generate code using the TypeScript 3.8+ `import type` syntax. (default: `true`)
Expand Down Expand Up @@ -328,6 +332,7 @@ The default configuration:
"print": false,
"runtimeEnums": false,
"singularize": false,
"timestampParser": "js-date",
"typeOnlyImports": true,
"url": "env(DATABASE_URL)",
"verify": false
Expand Down
6 changes: 6 additions & 0 deletions src/cli/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ describe(Cli.name, () => {
);
assert(['--print'], { print: true });
assert(['--singularize'], { singularize: true });
assert(['--timestamp-parser=js-date'], { timestampParser: 'js-date' });
assert(['--timestamp-parser=string'], { timestampParser: 'string' });
assert(['--type-only-imports'], { typeOnlyImports: true });
assert(['--type-only-imports=false'], { typeOnlyImports: false });
assert(['--type-only-imports=true'], { typeOnlyImports: true });
Expand Down Expand Up @@ -311,6 +313,10 @@ describe(Cli.name, () => {
assert({ print: 'true' }, 'Expected boolean, received string');
assert({ runtimeEnums: 'true' }, 'Invalid input');
assert({ singularize: 'true' }, 'Invalid input');
assert(
{ timestampParser: 'date' },
"Invalid enum value. Expected 'js-date' | 'string', received 'date'",
);
assert({ typeOnlyImports: 'true' }, 'Expected boolean, received string');
assert({ url: null }, 'Expected string, received null');
assert({ verify: 'true' }, 'Expected boolean, received string');
Expand Down
12 changes: 12 additions & 0 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ConnectionStringParser } from '../generator/connection-string-parser';
import { generate } from '../generator/generator/generate';
import { DEFAULT_LOG_LEVEL } from '../generator/logger/log-level';
import { Logger } from '../generator/logger/logger';
import type { TimestampParser } from '../introspector';
import type { DateParser } from '../introspector/dialects/postgres/date-parser';
import type { NumericParser } from '../introspector/dialects/postgres/numeric-parser';
import type { Config, DialectName } from './config';
Expand Down Expand Up @@ -54,6 +55,7 @@ export class Cli {
domains: options.domains,
numericParser: options.numericParser,
partitions: options.partitions,
timestampParser: options.timestampParser,
});

const db = await dialect.introspector.connect({
Expand Down Expand Up @@ -145,6 +147,15 @@ export class Cli {
return input.map(String);
}

#parseTimestampParser(input: any): TimestampParser | undefined {
if (input === undefined) return undefined;
switch (input) {
case 'js-date':
case 'string':
return input;
}
}

#showHelp() {
console.info(
['', 'kysely-codegen [options]', '', serializeFlags(FLAGS), ''].join(
Expand Down Expand Up @@ -246,6 +257,7 @@ export class Cli {
print: this.#parseBoolean(argv.print),
runtimeEnums: this.#parseRuntimeEnums(argv['runtime-enums']),
singularize: this.#parseBoolean(argv.singularize),
timestampParser: this.#parseTimestampParser(argv['timestamp-parser']),
typeOnlyImports: this.#parseBoolean(argv['type-only-imports']),
url: this.#parseString(argv.url),
verify: this.#parseBoolean(argv.verify),
Expand Down
10 changes: 9 additions & 1 deletion src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
RawExpressionNode,
UnionExpressionNode,
} from '../generator';
import type { DateParser, NumericParser } from '../introspector';
import type {
DateParser,
NumericParser,
TimestampParser,
} from '../introspector';
import { DatabaseMetadata, IntrospectorDialect } from '../introspector';

export type Config = {
Expand All @@ -42,6 +46,7 @@ export type Config = {
serializer?: Serializer;
singularize?: boolean | Record<string, string>;
skipAutogeneratedFileComment?: boolean;
timestampParser?: TimestampParser;
typeOnlyImports?: boolean;
url?: string;
verify?: boolean;
Expand Down Expand Up @@ -132,6 +137,9 @@ export const configSchema = z.object({
.union([z.boolean(), z.record(z.string(), z.string())])
.optional(),
skipAutogeneratedFileComment: z.boolean().optional(),
timestampParser: z
.enum<TimestampParser, ['js-date', 'string']>(['js-date', 'string'])
.optional(),
typeOnlyImports: z.boolean().optional(),
url: z.string().optional(),
verify: z.boolean().optional(),
Expand Down
6 changes: 6 additions & 0 deletions src/cli/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ export const FLAGS = [
'Singularize generated table names, e.g. `BlogPost` instead of `BlogPosts`.',
longName: 'singularize',
},
{
default: 'js-date',
description: 'Specify which parser to use for PostgreSQL timestamp values.',
longName: 'timestamp-parser',
values: ['js-date', 'string'],
},
{
default: 'true',
description:
Expand Down
16 changes: 16 additions & 0 deletions src/generator/dialects/postgres/postgres-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DateParser } from '../../../introspector/dialects/postgres/date-parser';
import type { NumericParser } from '../../../introspector/dialects/postgres/numeric-parser';
import type { TimestampParser } from '../../../introspector/dialects/postgres/timestamp-parser';
import { Adapter } from '../../adapter';
import { ColumnTypeNode } from '../../ast/column-type-node';
import { IdentifierNode } from '../../ast/identifier-node';
Expand All @@ -18,6 +19,7 @@ import {
type PostgresAdapterOptions = {
dateParser?: DateParser;
numericParser?: NumericParser;
timestampParser?: TimestampParser;
};

export class PostgresAdapter extends Adapter {
Expand Down Expand Up @@ -165,5 +167,19 @@ export class PostgresAdapter extends Adapter {
]),
);
}

if (options?.timestampParser === 'string') {
this.definitions.Timestamp = new ColumnTypeNode(
new IdentifierNode('string'),
new UnionExpressionNode([
new IdentifierNode('Date'),
new IdentifierNode('string'),
]),
new UnionExpressionNode([
new IdentifierNode('Date'),
new IdentifierNode('string'),
]),
);
}
}
}
4 changes: 4 additions & 0 deletions src/generator/dialects/postgres/postgres-dialect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DateParser } from '../../../introspector/dialects/postgres/date-parser';
import type { NumericParser } from '../../../introspector/dialects/postgres/numeric-parser';
import { PostgresIntrospectorDialect } from '../../../introspector/dialects/postgres/postgres-dialect';
import type { TimestampParser } from '../../../introspector/dialects/postgres/timestamp-parser';
import type { GeneratorDialect } from '../../dialect';
import { PostgresAdapter } from './postgres-adapter';

Expand All @@ -10,6 +11,7 @@ export type PostgresDialectOptions = {
domains?: boolean;
numericParser?: NumericParser;
partitions?: boolean;
timestampParser?: TimestampParser;
};

export class PostgresDialect
Expand All @@ -25,11 +27,13 @@ export class PostgresDialect
domains: options?.domains,
numericParser: options?.numericParser,
partitions: options?.partitions,
timestampParser: options?.timestampParser,
});

this.adapter = new PostgresAdapter({
dateParser: this.options.dateParser,
numericParser: this.options.numericParser,
timestampParser: this.options?.timestampParser,
});
}
}
8 changes: 8 additions & 0 deletions src/generator/generator/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const TESTS: Test[] = [
dialect: new PostgresDialect({
dateParser: 'string',
numericParser: 'number-or-string',
timestampParser: 'string',
}),
generateOptions: { runtimeEnums: 'screaming-snake-case' },
name: 'postgres2',
Expand Down Expand Up @@ -360,13 +361,16 @@ describe(serializeFromMetadata.name, () => {
dialect: new PostgresDialect({
dateParser: 'string',
numericParser: 'number',
timestampParser: 'string',
}),
metadata: {
tables: [
{
columns: [
{ dataType: 'date', name: 'date' },
{ dataType: 'numeric', name: 'numeric' },
{ dataType: 'timestamp', name: 'timestamp' },
{ dataType: 'timestamptz', name: 'timestamptz' },
],
name: 'table',
},
Expand All @@ -379,9 +383,13 @@ describe(serializeFromMetadata.name, () => {

export type Numeric = ColumnType<number, number | string, number | string>;

export type Timestamp = ColumnType<string, Date | string, Date | string>;

export interface Table {
date: string;
numeric: Numeric;
timestamp: Timestamp;
timestamptz: Timestamp;
}

export interface DB {
Expand Down
2 changes: 1 addition & 1 deletion src/generator/generator/snapshots/postgres2.snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type JsonValue = JsonArray | JsonObject | JsonPrimitive;

export type Numeric = ColumnType<number | string>;

export type Timestamp = ColumnType<Date, Date | string, Date | string>;
export type Timestamp = ColumnType<string, Date | string, Date | string>;

export interface Enum {
name: string;
Expand Down
68 changes: 67 additions & 1 deletion src/generator/transformer/transformer.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { deepStrictEqual } from 'node:assert';
import type { DateParser } from '../../introspector/dialects/postgres/date-parser';
import type { NumericParser } from '../../introspector/dialects/postgres/numeric-parser';
import type { TimestampParser } from '../../introspector/dialects/postgres/timestamp-parser';
import { EnumCollection } from '../../introspector/enum-collection';
import { ColumnMetadata } from '../../introspector/metadata/column-metadata';
import { DatabaseMetadata } from '../../introspector/metadata/database-metadata';
import { TableMetadata } from '../../introspector/metadata/table-metadata';
import { AliasDeclarationNode } from '../ast/alias-declaration-node';
import { ArrayExpressionNode } from '../ast/array-expression-node';
import { ColumnTypeNode } from '../ast/column-type-node';
import { ExportStatementNode } from '../ast/export-statement-node';
import { GenericExpressionNode } from '../ast/generic-expression-node';
import { IdentifierNode, TableIdentifierNode } from '../ast/identifier-node';
Expand Down Expand Up @@ -38,16 +40,22 @@ describe(transform.name, () => {
numericParser,
runtimeEnums,
tables,
timestampParser,
}: {
camelCase?: boolean;
dateParser?: DateParser;
numericParser?: NumericParser;
runtimeEnums?: boolean | RuntimeEnumsStyle;
tables: TableMetadata[];
timestampParser?: TimestampParser;
}) => {
return transform({
camelCase,
dialect: new PostgresDialect({ dateParser, numericParser }),
dialect: new PostgresDialect({
dateParser,
numericParser,
timestampParser,
}),
metadata: new DatabaseMetadata({ enums, tables }),
overrides: {
columns: {
Expand Down Expand Up @@ -316,6 +324,64 @@ describe(transform.name, () => {
deepStrictEqual((nodes[1] as any).argument.body.args[0].name, 'number');
});

it('should be able to transform using an alternative Postgres timestamp parser', () => {
const nodes = transformWithDefaults({
timestampParser: 'string',
tables: [
new TableMetadata({
columns: [
new ColumnMetadata({
dataType: 'timestamp',
name: 'timestamp',
}),
new ColumnMetadata({
dataType: 'timestamptz',
name: 'timestamptz',
}),
],
name: 'table',
}),
],
});

deepStrictEqual(nodes, [
new ImportStatementNode('kysely', [new ImportClauseNode('ColumnType')]),
new ExportStatementNode(
new AliasDeclarationNode(
'Timestamp',
new ColumnTypeNode(
new IdentifierNode('string'),
new UnionExpressionNode([
new IdentifierNode('Date'),
new IdentifierNode('string'),
]),
new UnionExpressionNode([
new IdentifierNode('Date'),
new IdentifierNode('string'),
]),
),
),
),
new ExportStatementNode(
new InterfaceDeclarationNode(
new TableIdentifierNode('Table'),
new ObjectExpressionNode([
new PropertyNode('timestamp', new IdentifierNode('Timestamp')),
new PropertyNode('timestamptz', new IdentifierNode('Timestamp')),
]),
),
),
new ExportStatementNode(
new InterfaceDeclarationNode(
new IdentifierNode('DB'),
new ObjectExpressionNode([
new PropertyNode('table', new TableIdentifierNode('Table')),
]),
),
),
]);
});

it('should transform Postgres enums correctly', () => {
const nodes = transformWithDefaults({
tables: [
Expand Down
15 changes: 15 additions & 0 deletions src/introspector/dialects/postgres/postgres-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import { DEFAULT_DATE_PARSER } from './date-parser';
import type { NumericParser } from './numeric-parser';
import { DEFAULT_NUMERIC_PARSER } from './numeric-parser';
import { PostgresIntrospector } from './postgres-introspector';
import type { TimestampParser } from './timestamp-parser';
import { DEFAULT_TIMESTAMP_PARSER } from './timestamp-parser';

type PostgresDialectOptions = {
dateParser?: DateParser;
defaultSchemas?: string[];
domains?: boolean;
numericParser?: NumericParser;
partitions?: boolean;
timestampParser?: TimestampParser;
};

export class PostgresIntrospectorDialect extends IntrospectorDialect {
Expand All @@ -32,6 +35,7 @@ export class PostgresIntrospectorDialect extends IntrospectorDialect {
defaultSchemas: options?.defaultSchemas,
domains: options?.domains ?? true,
numericParser: options?.numericParser ?? DEFAULT_NUMERIC_PARSER,
timestampParser: options?.timestampParser ?? DEFAULT_TIMESTAMP_PARSER,
};
}

Expand All @@ -54,6 +58,17 @@ export class PostgresIntrospectorDialect extends IntrospectorDialect {
});
}

if (this.options.timestampParser === 'string') {
/**
* 13234: time_stamp
* 1114: timestamp
* 1184: timestamptz
*/
pg.types.setTypeParser(13_234, (ts) => ts);
pg.types.setTypeParser(1114, (ts) => ts);
pg.types.setTypeParser(1184, (ts) => ts);
}

return new KyselyPostgresDialect({
pool: new pg.Pool({
connectionString: options.connectionString,
Expand Down
3 changes: 3 additions & 0 deletions src/introspector/dialects/postgres/timestamp-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type TimestampParser = 'js-date' | 'string';

export const DEFAULT_TIMESTAMP_PARSER: TimestampParser = 'js-date';
1 change: 1 addition & 0 deletions src/introspector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './dialects/postgres/numeric-parser';
export * from './dialects/postgres/postgres-db';
export * from './dialects/postgres/postgres-dialect';
export * from './dialects/postgres/postgres-introspector';
export * from './dialects/postgres/timestamp-parser';
export * from './dialects/sqlite/sqlite-dialect';
export * from './dialects/sqlite/sqlite-introspector';
export * from './enum-collection';
Expand Down