Skip to content

Commit

Permalink
feat: Typed sql overload functions (#520)
Browse files Browse the repository at this point in the history
* feat: Add Typed SQL Function Tags

* feat: Add Typed SQL Function Tags

* remove hungarian notation for new types

* rename new mode to ts-implicit

* cleanup

* fix wrong imports in generated files

* prevent watch mode crashes on errors

* add e2e test for ts-implicit mode

* update test snapshots

---------

Co-authored-by: Adel Salakh <adel@zetico.io>
  • Loading branch information
JesseVelden and adelsz authored Sep 29, 2023
1 parent cdfadfb commit e5f920e
Show file tree
Hide file tree
Showing 14 changed files with 663 additions and 242 deletions.
27 changes: 21 additions & 6 deletions packages/cli/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/** @fileoverview Config file parser */

import { createRequire } from 'module';
import { Type } from '@pgtyped/query';
import * as Either from 'fp-ts/lib/Either.js';
import { join, isAbsolute } from 'path';
import * as t from 'io-ts';
import { reporter } from 'io-ts-reporters';
import { createRequire } from 'module';
import { isAbsolute, join } from 'path';
import tls from 'tls';
import { default as dbUrlModule, DatabaseConfig } from 'ts-parse-database-url';
import { DatabaseConfig, default as dbUrlModule } from 'ts-parse-database-url';
import { TypeDefinition } from './types.js';
import { Type } from '@pgtyped/query';

// module import hack
const { default: parseDatabaseUri } = dbUrlModule as any;
Expand All @@ -25,12 +25,27 @@ const TSTransformCodec = t.type({
...transformCodecProps,
});

const TSTypedSQLTagTransformCodec = t.type({
mode: t.literal('ts-implicit'),
include: t.string,
functionName: t.string,
emitFileName: t.string,
});

export type TSTypedSQLTagTransformConfig = t.TypeOf<
typeof TSTypedSQLTagTransformCodec
>;

const SQLTransformCodec = t.type({
mode: t.literal('sql'),
...transformCodecProps,
});

const TransformCodec = t.union([TSTransformCodec, SQLTransformCodec]);
const TransformCodec = t.union([
TSTransformCodec,
SQLTransformCodec,
TSTypedSQLTagTransformCodec,
]);

export type TransformConfig = t.TypeOf<typeof TransformCodec>;

Expand Down Expand Up @@ -193,7 +208,7 @@ export function parseConfig(
? convertParsedURLToDBConfig(parseDatabaseUri(dbUri))
: {};

if (transforms.some((tr) => !!tr.emitFileName)) {
if (transforms.some((tr) => tr.mode !== 'ts-implicit' && !!tr.emitFileName)) {
// tslint:disable:no-console
console.log(
'Warning: Setting "emitFileName" is deprecated. Consider using "emitTemplate" instead.',
Expand Down
28 changes: 25 additions & 3 deletions packages/cli/src/generator.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ParameterTransform } from '@pgtyped/runtime';

import { parseSQLFile } from '@pgtyped/parser';
import { parseSQLFile, TSQueryAST } from '@pgtyped/parser';
import { IQueryTypes } from '@pgtyped/query/lib/actions.js';
import { ParameterTransform } from '@pgtyped/runtime';
import { pascalCase } from 'pascal-case';
import { ParsedConfig } from './config.js';
import {
escapeComment,
generateInterface,
genTypedSQLOverloadFunctions,
TSTypedQuery,
ProcessingMode,
queryToTypeDeclarations,
} from './generator.js';
Expand Down Expand Up @@ -657,3 +659,23 @@ export type IGetNotificationsParams = never;
`;
expect(result).toEqual(expected);
});

test('should generate the correct SQL overload functions', async () => {
const queryStringTS = `
const getUsers = sql\`SELECT id from users\`;
`;
const query = parsedQuery(ProcessingMode.TS, queryStringTS);
const typedQuery: TSTypedQuery = {
mode: 'ts' as const,
fileName: 'test.ts',
query: {
name: query.ast.name,
ast: query.ast as TSQueryAST,
queryTypeAlias: `I${pascalCase(query.ast.name)}Query`,
},
typeDeclaration: '',
};
const result = genTypedSQLOverloadFunctions('sqlFunc', [typedQuery]);
const expected = `export function sqlFunc(s: \`SELECT id from users\`): ReturnType<typeof sourceSql<IGetUsersQuery>>;`;
expect(result).toEqual(expected);
});
185 changes: 98 additions & 87 deletions packages/cli/src/generator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import {
ParameterTransform,
processSQLQueryIR,
processTSQueryAST,
} from '@pgtyped/runtime';

import {
parseSQLFile,
prettyPrintEvents,
Expand All @@ -14,12 +8,17 @@ import {
} from '@pgtyped/parser';

import { getTypes, TypeSource } from '@pgtyped/query';
import {
ParameterTransform,
processSQLQueryIR,
processTSQueryAST,
} from '@pgtyped/runtime';
import { camelCase } from 'camel-case';
import { pascalCase } from 'pascal-case';
import path from 'path';
import { ParsedConfig } from './config.js';
import { TypeAllocator, TypeMapping, TypeScope } from './types.js';
import { ParsedConfig, TransformConfig } from './config.js';
import { parseCode as parseTypescriptFile } from './parseTypescript.js';
import { TypeAllocator, TypeDefinitions, TypeScope } from './types.js';
import { IQueryTypes } from '@pgtyped/query/lib/actions.js';

export enum ProcessingMode {
Expand Down Expand Up @@ -262,54 +261,67 @@ export async function queryToTypeDeclarations(
);
}

type ITypedQuery =
| {
mode: 'ts';
fileName: string;
query: {
name: string;
ast: TSQueryAST;
};
typeDeclaration: string;
}
| {
mode: 'sql';
fileName: string;
query: {
name: string;
ast: SQLQueryAST;
ir: SQLQueryIR;
paramTypeAlias: string;
returnTypeAlias: string;
};
typeDeclaration: string;
};
export type TSTypedQuery = {
mode: 'ts';
fileName: string;
query: {
name: string;
ast: TSQueryAST;
queryTypeAlias: string;
};
typeDeclaration: string;
};

type SQLTypedQuery = {
mode: 'sql';
fileName: string;
query: {
name: string;
ast: SQLQueryAST;
ir: SQLQueryIR;
paramTypeAlias: string;
returnTypeAlias: string;
};
typeDeclaration: string;
};

async function generateTypedecsFromFile(
export type TypedQuery = TSTypedQuery | SQLTypedQuery;
export type TypeDeclarationSet = {
typedQueries: TypedQuery[];
typeDefinitions: TypeDefinitions;
fileName: string;
};
export async function generateTypedecsFromFile(
contents: string,
fileName: string,
connection: any,
mode: 'ts' | 'sql',
transform: TransformConfig,
types: TypeAllocator,
config: ParsedConfig,
): Promise<ITypedQuery[]> {
const results: ITypedQuery[] = [];
): Promise<TypeDeclarationSet> {
const typedQueries: TypedQuery[] = [];
const interfacePrefix = config.hungarianNotation ? 'I' : '';
const typeSource: TypeSource = (query) => getTypes(query, connection);

const { queries, events } =
mode === 'ts'
? parseTypescriptFile(contents, fileName)
: parseSQLFile(contents);
transform.mode === 'sql'
? parseSQLFile(contents)
: parseTypescriptFile(contents, fileName, transform);

if (events.length > 0) {
prettyPrintEvents(contents, events);
if (events.find((e) => 'critical' in e)) {
return results;
return {
typedQueries,
typeDefinitions: types.toTypeDefinitions(),
fileName,
};
}
}

for (const queryAST of queries) {
let typedQuery: ITypedQuery;
if (mode === 'sql') {
let typedQuery: TypedQuery;
if (transform.mode === 'sql') {
const sqlQueryAST = queryAST as SQLQueryAST;
const result = await queryToTypeDeclarations(
{ ast: sqlQueryAST, mode: ProcessingMode.SQL },
Expand Down Expand Up @@ -350,76 +362,75 @@ async function generateTypedecsFromFile(
query: {
name: tsQueryAST.name,
ast: tsQueryAST,
queryTypeAlias: `${interfacePrefix}${pascalCase(
tsQueryAST.name,
)}Query`,
},
typeDeclaration: result,
};
}
results.push(typedQuery);
typedQueries.push(typedQuery);
}
return results;
return { typedQueries, typeDefinitions: types.toTypeDefinitions(), fileName };
}

export async function generateDeclarationFile(
contents: string,
fileName: string,
connection: any,
mode: 'ts' | 'sql',
config: ParsedConfig,
decsFileName: string,
): Promise<{ typeDecs: ITypedQuery[]; declarationFileContents: string }> {
const types = new TypeAllocator(TypeMapping(config.typesOverrides));

if (mode === 'sql') {
// Second parameter has no effect here, we could have used any value
types.use(
{ name: 'PreparedQuery', from: '@pgtyped/runtime' },
TypeScope.Return,
);
}
const typeDecs = await generateTypedecsFromFile(
contents,
fileName,
connection,
mode,
types,
config,
);

// file paths in generated files must be stable across platforms
// https://github.com/adelsz/pgtyped/issues/230
const isWindowsPath = path.sep === '\\';
// always emit POSIX paths
const stableFilePath = isWindowsPath
? fileName.replace(/\\/g, '/')
: fileName;

let declarationFileContents = '';
declarationFileContents += `/** Types generated for queries found in "${stableFilePath}" */\n`;
declarationFileContents += types.declaration(decsFileName);
declarationFileContents += '\n';
export function generateDeclarations(typeDecs: TypedQuery[]): string {
let typeDeclarations = '';
for (const typeDec of typeDecs) {
declarationFileContents += typeDec.typeDeclaration;
typeDeclarations += typeDec.typeDeclaration;
if (typeDec.mode === 'ts') {
continue;
}
const queryPP = typeDec.query.ast.statement.body
.split('\n')
.map((s: string) => ' * ' + s)
.join('\n');
declarationFileContents += `const ${
typeDec.query.name
}IR: any = ${JSON.stringify(typeDec.query.ir)};\n\n`;
declarationFileContents +=
typeDeclarations += `const ${typeDec.query.name}IR: any = ${JSON.stringify(
typeDec.query.ir,
)};\n\n`;
typeDeclarations +=
`/**\n` +
` * Query generated from SQL:\n` +
` * \`\`\`\n` +
`${queryPP}\n` +
` * \`\`\`\n` +
` */\n`;
declarationFileContents +=
typeDeclarations +=
`export const ${typeDec.query.name} = ` +
`new PreparedQuery<${typeDec.query.paramTypeAlias},${typeDec.query.returnTypeAlias}>` +
`(${typeDec.query.name}IR);\n\n\n`;
}
return { declarationFileContents, typeDecs };
return typeDeclarations;
}

export function generateDeclarationFile(typeDecSet: TypeDeclarationSet) {
// file paths in generated files must be stable across platforms
// https://github.com/adelsz/pgtyped/issues/230
const isWindowsPath = path.sep === '\\';
// always emit POSIX paths
const stableFilePath = isWindowsPath
? typeDecSet.fileName.replace(/\\/g, '/')
: typeDecSet.fileName;

let content = `/** Types generated for queries found in "${stableFilePath}" */\n`;
content += TypeAllocator.typeDefinitionDeclarations(
typeDecSet.fileName,
typeDecSet.typeDefinitions,
);
content += '\n';
content += generateDeclarations(typeDecSet.typedQueries);
return content;
}

export function genTypedSQLOverloadFunctions(
functionName: string,
typedQueries: TSTypedQuery[],
) {
return typedQueries
.map(
(typeDec) =>
`export function ${functionName}(s: \`${typeDec.query.ast.text}\`): ReturnType<typeof sourceSql<${typeDec.query.queryTypeAlias}>>;`,
)
.filter((s) => s)
.join('\n');
}
Loading

0 comments on commit e5f920e

Please sign in to comment.