Skip to content

Commit

Permalink
Implement support for Postgres enums
Browse files Browse the repository at this point in the history
This allows pgtyped to generate code from queries involving database
enum types. The postgres enums will be converted to a string-valued const enum
in the generated code.

The `getTypes` reflection query now joins on pg_enum and accumulates
enum information as it collects other type information. I think it may
be possible to accomodate composite types by aggregating over the object
graph given by a suitable join query in a similar way.

@pgtyped/query now returns `MappableTypes` which are either a string
referencing a database type name or a fully-described `Type`. `Type` now
includes `EnumType`. This allows type definitions to be realised at
different stages in the reflection/mapping process and should allow for
custom overries in config.

Signed-off-by: Silas Davis <silas@monax.io>
  • Loading branch information
Silas Davis committed May 6, 2020
1 parent d38230b commit e651302
Show file tree
Hide file tree
Showing 19 changed files with 696 additions and 176 deletions.
7 changes: 5 additions & 2 deletions docs/annotated-sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ PgTyped has a number of requirements for SQL file contents:

## Parameter expansions

PgTyped supports parameter expansions that help build more complicated queries.
You always define parameters by a colon-prefixed token in your query, like `:age`. Ordinary parameters (those that do not need to be expanded and can be directly substituted) do not need any additional annotations within the query comment block.

PgTyped also supports parameter expansions that help build more complicated queries by expanding parameters into their components arrays and fields, which are then spliced into the query.

For example, a typical insert query looks like this:

```sql
Expand All @@ -36,7 +39,7 @@ INSERT INTO book_comments (user_id, body)
VALUES :comments;
```

Here `comments -> ((userId, commentBody)...)` is a parameter expansion.
Here `comments -> ((userId, commentBody)...)` is a parameter expansion that instructs pgtyped to expand `comments` into an array of objects with each object having a field `userId` and `commentBody`.

A query can also contain multiple expansions if needed:
```sql
Expand Down
66 changes: 43 additions & 23 deletions packages/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ export type TransformConfig = t.TypeOf<typeof TransformCodec>;
const configParser = t.type({
transforms: t.array(TransformCodec),
srcDir: t.string,
db: t.type({
host: t.union([t.string, t.undefined]),
password: t.union([t.string, t.undefined]),
port: t.union([t.number, t.undefined]),
user: t.union([t.string, t.undefined]),
dbName: t.union([t.string, t.undefined]),
}),
db: t.union([
t.type({
host: t.union([t.string, t.undefined]),
password: t.union([t.string, t.undefined]),
port: t.union([t.number, t.undefined]),
user: t.union([t.string, t.undefined]),
dbName: t.union([t.string, t.undefined]),
}),
t.undefined,
]),
});

export type IConfig = typeof configParser._O;
Expand All @@ -51,6 +54,17 @@ export interface ParsedConfig {
srcDir: IConfig['srcDir'];
}

function merge<T>(base: T, ...overrides: Partial<T>[]): T {
return overrides.reduce<T>(
(acc, o) =>
Object.entries(o).reduce(
(oAcc, [k, v]) => (v ? { ...oAcc, [k]: v } : oAcc),
acc,
),
{ ...base },
);
}

export function parseConfig(path: string): ParsedConfig {
const configStr = readFileSync(path);
let configObject;
Expand All @@ -60,28 +74,34 @@ export function parseConfig(path: string): ParsedConfig {
const message = reporter(result);
throw new Error(message[0]);
}
const { db, transforms, srcDir } = configObject as IConfig;
const host = process.env.PGHOST ?? db.host ?? '127.0.0.1';
const user = process.env.PGUSER ?? db.user ?? 'postgres';
const password = process.env.PGPASSWORD ?? db.password;
const dbName = process.env.PGDATABASE ?? db.dbName ?? 'postgres';
const port = parseInt(
process.env.PGPORT ?? db.port?.toString() ?? '5432',
10,
);

const defaultDBConfig = {
host: '127.0.0.1',
user: 'postgres',
password: '',
dbName: 'postgres',
port: 5432,
};

const envDBConfig = {
host: process.env.PGHOST,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
dbName: process.env.PGDATABASE,
port: process.env.PGPORT ? Number(process.env.PGPORT) : undefined,
};

const { db = defaultDBConfig, transforms, srcDir } = configObject as IConfig;

if (transforms.some((tr) => !!tr.emitFileName)) {
// tslint:disable:no-console
console.log(
'Warning: Setting "emitFileName" is deprecated. Consider using "emitTemplate" instead.',
);
}
const finalDBConfig = {
host,
user,
password,
dbName,
port,
};

const finalDBConfig = merge(defaultDBConfig, db, envDBConfig);

return {
db: finalDBConfig,
transforms,
Expand Down
20 changes: 10 additions & 10 deletions packages/cli/src/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('query-to-interface translation (SQL)', () => {
{
returnName: 'payload',
columnName: 'payload',
typeName: 'json',
type: 'json',
nullable: false,
},
],
Expand Down Expand Up @@ -78,19 +78,19 @@ export interface IGetNotificationsQuery {
{
returnName: 'id',
columnName: 'id',
typeName: 'uuid',
type: 'uuid',
nullable: false,
},
{
returnName: 'name',
columnName: 'name',
typeName: 'text',
type: 'text',
nullable: false,
},
{
returnName: 'bote',
columnName: 'note',
typeName: 'text',
type: 'text',
nullable: true,
},
],
Expand Down Expand Up @@ -145,28 +145,28 @@ export interface IDeleteUsersQuery {

test('query-to-interface translation (TS)', async () => {
const query = `
delete
from users *
where name = :userName and id = :userId and note = :userNote returning id, id, name, note as bote;
DELETE
FROM users *
WHERE NAME = :userName AND id = :userId AND note = :userNote RETURNING id, id, NAME, note AS bote;
`;
const mockTypes: IQueryTypes = {
returnTypes: [
{
returnName: 'id',
columnName: 'id',
typeName: 'uuid',
type: 'uuid',
nullable: false,
},
{
returnName: 'name',
columnName: 'name',
typeName: 'text',
type: 'text',
nullable: false,
},
{
returnName: 'bote',
columnName: 'note',
typeName: 'text',
type: 'text',
nullable: true,
},
],
Expand Down
11 changes: 7 additions & 4 deletions packages/cli/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ export async function queryToTypeDeclarations(
const returnFieldTypes: IField[] = [];
const paramFieldTypes: IField[] = [];

returnTypes.forEach(({ returnName, typeName, nullable }) => {
let tsTypeName = types.use(typeName);
returnTypes.forEach(({ returnName, type, nullable }) => {
let tsTypeName = types.use(type);
if (nullable) {
tsTypeName += ' | null';
}
Expand Down Expand Up @@ -126,8 +126,11 @@ export async function queryToTypeDeclarations(
}
}

// TODO: revisit as part of error handling task
// await types.check();
// TypeAllocator errors are currently considered non-fatal since a `never`
// type is emitted which can be caught later when compiling the generated
// code
// tslint:disable-next-line:no-console
types.errors.forEach((err) => console.log(err));

const resultInterfaceName = `I${interfaceName}Result`;
const returnTypesInterface =
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ class FileProcessor {

private processQueue = () => {
if (this.activePromise) {
this.activePromise.then(this.onFileProcessed);
// TODO: handle promise rejection
this.activePromise
.then(this.onFileProcessed)
.catch((err) => console.log(`Error processing file: ${err.stack}`));
return;
}
const nextJob = this.jobQueue.pop();
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DefaultTypeMapping, TypeAllocator } from './types';

describe('TypeAllocator', () => {
test('Allows overrides', () => {
const types = new TypeAllocator({
...DefaultTypeMapping,
foo: { name: 'bar' },
});
expect(types.use('foo')).toEqual('bar');
});
});
Loading

0 comments on commit e651302

Please sign in to comment.