Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Typed sql overload functions #520

Merged
merged 10 commits into from
Sep 29, 2023
Merged
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