From 9b257263cc5e8edad76dad0a6105f8bfa857bf82 Mon Sep 17 00:00:00 2001 From: Jesse van der Velden Date: Thu, 27 Apr 2023 01:39:52 +0200 Subject: [PATCH 1/9] feat: Add Typed SQL Function Tags --- packages/cli/src/config.ts | 29 ++- packages/cli/src/generator.test.ts | 36 +++- packages/cli/src/generator.ts | 185 ++++++++++-------- packages/cli/src/index.ts | 141 +++++-------- packages/cli/src/parseTypescript.ts | 33 +++- packages/cli/src/typedSQLTagTransformer.ts | 164 ++++++++++++++++ packages/cli/src/types.ts | 64 ++++-- .../cli/src/typescriptAndSQLTransformer.ts | 91 +++++++++ packages/cli/src/worker.ts | 91 ++++++--- packages/example/config.json | 6 + packages/example/src/books/books.queries.ts | 4 +- packages/example/src/sql/index.ts | 111 +++++++++++ 12 files changed, 715 insertions(+), 240 deletions(-) create mode 100644 packages/cli/src/typedSQLTagTransformer.ts create mode 100644 packages/cli/src/typescriptAndSQLTransformer.ts create mode 100644 packages/example/src/sql/index.ts diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 7a73eae1..31db9347 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -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; @@ -25,12 +25,27 @@ const TSTransformCodec = t.type({ ...transformCodecProps, }); +const TSTypedSQLTagTransformCodec = t.type({ + mode: t.literal('ts-typed-sql-tag'), + 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; @@ -189,7 +204,9 @@ export function parseConfig( ? convertParsedURLToDBConfig(parseDatabaseUri(dbUri)) : {}; - if (transforms.some((tr) => !!tr.emitFileName)) { + if ( + transforms.some((tr) => tr.mode !== 'ts-typed-sql-tag' && !!tr.emitFileName) + ) { // tslint:disable:no-console console.log( 'Warning: Setting "emitFileName" is deprecated. Consider using "emitTemplate" instead.', diff --git a/packages/cli/src/generator.test.ts b/packages/cli/src/generator.test.ts index e94f6a4b..c4b32bce 100644 --- a/packages/cli/src/generator.test.ts +++ b/packages/cli/src/generator.test.ts @@ -1,16 +1,18 @@ -import { ParameterTransform } from '@pgtyped/runtime'; - -import { parseSQLFile } from '@pgtyped/parser'; +import { parseSQLFile, TSQueryAST } from '@pgtyped/parser'; import { IQueryTypes } from '@pgtyped/query/lib/actions'; +import { ParameterTransform } from '@pgtyped/runtime'; +import { pascalCase } from 'pascal-case'; import { ParsedConfig } from './config.js'; import { escapeComment, generateInterface, + genTypedSQLOverloadFunctions, + ITSTypedQuery, ProcessingMode, queryToTypeDeclarations, } from './generator'; -import { parseCode as parseTypeScriptFile } from './parseTypescript.js'; -import { TypeAllocator, TypeMapping, TypeScope } from './types.js'; +import { parseCode as parseTypeScriptFile } from './parseTypescript'; +import { TypeAllocator, TypeMapping, TypeScope } from './types'; const partialConfig = { hungarianNotation: true } as ParsedConfig; @@ -641,3 +643,27 @@ 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: ITSTypedQuery = { + 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', { + typedQueries: [typedQuery], + typeDefinitions: { imports: {}, aliases: [], enums: [] }, + fileName: 'test.ts', + }); + const expected = `export function sqlFunc(s: \`SELECT id from users\`): ReturnType>;`; + expect(result).toEqual(expected); +}); diff --git a/packages/cli/src/generator.ts b/packages/cli/src/generator.ts index 72fe663f..38c11615 100644 --- a/packages/cli/src/generator.ts +++ b/packages/cli/src/generator.ts @@ -1,9 +1,3 @@ -import { - ParameterTransform, - processSQLQueryIR, - processTSQueryAST, -} from '@pgtyped/runtime'; - import { parseSQLFile, prettyPrintEvents, @@ -14,13 +8,18 @@ import { } from '@pgtyped/parser'; import { getTypes, TypeSource } from '@pgtyped/query'; +import { IQueryTypes } from '@pgtyped/query/lib/actions'; +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 { IQueryTypes } from '@pgtyped/query/lib/actions'; +import { TypeAllocator, TypeDefinitions, TypeScope } from './types.js'; export enum ProcessingMode { SQL = 'sql-file', @@ -249,54 +248,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 ITSTypedQuery = { + mode: 'ts'; + fileName: string; + query: { + name: string; + ast: TSQueryAST; + queryTypeAlias: string; + }; + typeDeclaration: string; +}; + +type ISQLTypedQuery = { + mode: 'sql'; + fileName: string; + query: { + name: string; + ast: SQLQueryAST; + ir: SQLQueryIR; + paramTypeAlias: string; + returnTypeAlias: string; + }; + typeDeclaration: string; +}; -async function generateTypedecsFromFile( +export type ITypedQuery = ITSTypedQuery | ISQLTypedQuery; +export type TypeDeclarationSet = { + typedQueries: ITypedQuery[]; + typeDefinitions: TypeDefinitions; + fileName: string; +}; +export async function generateTypedecsFromFile( contents: string, fileName: string, connection: any, - mode: 'ts' | 'sql', + transform: TransformConfig, types: TypeAllocator, config: ParsedConfig, -): Promise { - const results: ITypedQuery[] = []; +): Promise { + const typedQueries: ITypedQuery[] = []; 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') { + if (transform.mode === 'sql') { const sqlQueryAST = queryAST as SQLQueryAST; const result = await queryToTypeDeclarations( { ast: sqlQueryAST, mode: ProcessingMode.SQL }, @@ -337,55 +349,22 @@ 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: ITypedQuery[]): string { + let typeDeclarations = ''; for (const typeDec of typeDecs) { - declarationFileContents += typeDec.typeDeclaration; + typeDeclarations += typeDec.typeDeclaration; if (typeDec.mode === 'ts') { continue; } @@ -393,20 +372,52 @@ export async function generateDeclarationFile( .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, + typeDecSet: TypeDeclarationSet, +) { + return (typeDecSet.typedQueries as ITSTypedQuery[]) + .map( + (typeDec) => + `export function ${functionName}(s: \`${typeDec.query.ast.text}\`): ReturnType>;`, + ) + .filter((s) => s) + .join('\n'); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 644a616c..c9f8d934 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,31 +3,29 @@ import { startup } from '@pgtyped/query'; import { AsyncQueue } from '@pgtyped/wire'; import chokidar from 'chokidar'; -import { globSync } from 'glob'; import nun from 'nunjucks'; + +import PiscinaPool from 'piscina'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { debug } from './util.js'; import { parseConfig, ParsedConfig, TransformConfig } from './config.js'; -import path from 'path'; - -import WorkerPool from 'piscina'; +import { TypedSQLTagTransformer } from './typedSQLTagTransformer.js'; +import { typescriptAndSQLTransformer } from './typescriptAndSQLTransformer.js'; +import { debug } from './util.js'; // tslint:disable:no-console nun.configure({ autoescape: false }); -interface TransformJob { +export interface TransformJob { files: string[]; - transform: TransformConfig; } -class FileProcessor { - private readonly pool: WorkerPool; - public readonly workQueue: Promise[] = []; +export class WorkerPool { + private pool: PiscinaPool; constructor(private readonly config: ParsedConfig) { - this.pool = new WorkerPool({ + this.pool = new PiscinaPool({ filename: new URL('./worker.js', import.meta.url).href, workerData: config, }); @@ -38,46 +36,24 @@ class FileProcessor { await this.pool.destroy(); } - public push(job: TransformJob) { - this.workQueue.push( - ...job.files.map(async (fileName) => { - try { - fileName = path.relative(process.cwd(), fileName); - console.log(`Processing ${fileName}`); - const result = await this.pool.run({ - fileName, - transform: job.transform, - }); - if (result.skipped) { - console.log( - `Skipped ${fileName}: no changes or no queries detected`, - ); - } else { - console.log( - `Saved ${result.typeDecsLength} query types from ${fileName} to ${result.relativePath}`, - ); - } - } catch (err) { - if (err instanceof Error) { - const isWorkerTermination = - err.message === 'Terminating worker thread'; - if (isWorkerTermination) { - return; - } - - console.log( - `Error processing file: ${err.stack || JSON.stringify(err)}`, - ); - } else { - console.log(`Error processing file: ${JSON.stringify(err)}`); - } - if (this.config.failOnError) { - await this.pool.destroy(); - process.exit(1); - } + public async run(opts: T, functionName: string) { + try { + return this.pool.run(opts, { name: functionName }); + } catch (err) { + if (err instanceof Error) { + const isWorkerTermination = err.message === 'Terminating worker thread'; + if (isWorkerTermination) { + return; } - }), - ); + console.log( + `Error processing file: ${err.stack || JSON.stringify(err)}`, + ); + if (this.config.failOnError) { + await this.pool.destroy(); + process.exit(1); + } + } + } } } @@ -95,49 +71,36 @@ async function main( debug('connected to database %o', config.db.dbName); - const fileProcessor = new FileProcessor(config); - let fileOverrideUsed = false; - for (const transform of config.transforms) { - const pattern = `${config.srcDir}/**/${transform.include}`; - if (isWatchMode) { - const cb = (filePath: string) => { - fileProcessor.push({ - files: [filePath], - transform, - }); - }; - chokidar - .watch(pattern, { persistent: true }) - .on('add', cb) - .on('change', cb); + const pool = new WorkerPool(config); + + const transformTask = async (transform: TransformConfig) => { + if (transform.mode === 'ts-typed-sql-tag') { + const typedSQLTagTransformer = new TypedSQLTagTransformer( + pool, + config, + transform, + ); + return typedSQLTagTransformer.start(isWatchMode); } else { - /** - * If the user didn't provide the -f paramter, we're using the list of files we got from glob. - * If he did, we're using glob file list to detect if his provided file should be used with this transform. - */ - let fileList = globSync(pattern); - if (fileOverride) { - fileList = fileList.includes(fileOverride) ? [fileOverride] : []; - if (fileList.length > 0) { - fileOverrideUsed = true; - } - } - debug('found query files %o', fileList); - const transformJob = { - files: fileList, + const sqlAndTSTransformer = new typescriptAndSQLTransformer( + pool, + config, transform, - }; - fileProcessor.push(transformJob); + ); + return sqlAndTSTransformer.start(isWatchMode); } - } - if (fileOverride && !fileOverrideUsed) { - console.log( - 'File override specified, but file was not found in provided transforms', - ); - } + }; + + const tasks = config.transforms.map(transformTask); + if (!isWatchMode) { - await Promise.all(fileProcessor.workQueue); - await fileProcessor.shutdown(); + const transforms = await Promise.all(tasks); + if (fileOverride && !transforms.some((x) => x)) { + console.log( + 'File override specified, but file was not found in provided transforms', + ); + } + await pool.shutdown(); process.exit(0); } } diff --git a/packages/cli/src/parseTypescript.ts b/packages/cli/src/parseTypescript.ts index b15efdee..f1133506 100644 --- a/packages/cli/src/parseTypescript.ts +++ b/packages/cli/src/parseTypescript.ts @@ -1,19 +1,38 @@ +import { ParseEvent, parseTSQuery, TSQueryAST } from '@pgtyped/parser'; import ts from 'typescript'; +import { TransformConfig } from './config'; interface INode { queryName: string; queryText: string; } -import { parseTSQuery, TSQueryAST, ParseEvent } from '@pgtyped/parser'; - export type TSParseResult = { queries: TSQueryAST[]; events: ParseEvent[] }; -export function parseFile(sourceFile: ts.SourceFile): TSParseResult { +export function parseFile( + sourceFile: ts.SourceFile, + transformConfig: TransformConfig | undefined, +): TSParseResult { const foundNodes: INode[] = []; parseNode(sourceFile); function parseNode(node: ts.Node) { + if ( + transformConfig?.mode === 'ts-typed-sql-tag' && + node.kind === ts.SyntaxKind.CallExpression + ) { + const callNode = node as ts.CallExpression; + const functionName = callNode.expression.getText(); + if (functionName === transformConfig.functionName) { + const queryName = callNode.parent.getChildren()[0].getText(); + const queryText = callNode.arguments[0].getText().slice(1, -1).trim(); + foundNodes.push({ + queryName, + queryText, + }); + } + } + if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) { const queryName = node.parent.getChildren()[0].getText(); const taggedTemplateNode = node as ts.TaggedTemplateExpression; @@ -48,12 +67,16 @@ export function parseFile(sourceFile: ts.SourceFile): TSParseResult { return { queries, events }; } -export const parseCode = (fileContent: string, fileName = 'unnamed.ts') => { +export const parseCode = ( + fileContent: string, + fileName = 'unnamed.ts', + transformConfig?: TransformConfig, +) => { const sourceFile = ts.createSourceFile( fileName, fileContent, ts.ScriptTarget.ES2015, true, ); - return parseFile(sourceFile); + return parseFile(sourceFile, transformConfig); }; diff --git a/packages/cli/src/typedSQLTagTransformer.ts b/packages/cli/src/typedSQLTagTransformer.ts new file mode 100644 index 00000000..dad562cd --- /dev/null +++ b/packages/cli/src/typedSQLTagTransformer.ts @@ -0,0 +1,164 @@ +import chokidar from 'chokidar'; +import fs from 'fs-extra'; +import { globSync } from 'glob'; +import path from 'path'; +import { ParsedConfig, TSTypedSQLTagTransformConfig } from './config.js'; +import { + generateDeclarations, + genTypedSQLOverloadFunctions, + TypeDeclarationSet, +} from './generator.js'; +import { TransformJob, WorkerPool } from './index.js'; +import { TypeAllocator } from './types.js'; +import { debug } from './util.js'; +import { getTypeDecsFnResult } from './worker.js'; + +type TypedSQLTagTransformResult = TypeDeclarationSet | undefined; + +export class TypedSQLTagTransformer { + public readonly workQueue: Promise[] = []; + private readonly cache: Record = {}; + private readonly includePattern: string; + private readonly localFileName: string; + private readonly fullFileName: string; + + constructor( + private readonly pool: WorkerPool, + private readonly config: ParsedConfig, + private readonly transform: TSTypedSQLTagTransformConfig, + ) { + this.includePattern = `${this.config.srcDir}/**/${transform.include}`; + this.localFileName = `${this.config.srcDir}${this.transform.emitFileName}`; + this.fullFileName = path.relative(process.cwd(), this.localFileName); + } + + private async watch() { + let initialized = false; + + const cb = async (fileName: string) => { + const job = { + files: [fileName], + }; + !initialized + ? this.pushToQueue(job) + : await this.generateTypedSQLTagFileForJob(job, true); + }; + + chokidar + .watch(this.includePattern, { + persistent: true, + ignored: [this.localFileName], + }) + .on('add', cb) + .on('change', cb) + .on('unlink', async (file) => await this.removeFileFromCache(file)) + .on('ready', async () => { + initialized = true; + await this.waitForTypedSQLQueueAndGenerate(true); + }); + } + + public async start(watch: boolean) { + if (watch) { + return this.watch(); + } + + let fileList = globSync(this.includePattern, { + ignore: [this.localFileName], + }); + + debug('found query files %o', fileList); + + await this.generateTypedSQLTagFileForJob({ + files: fileList, + }); + } + + private pushToQueue(job: TransformJob) { + this.workQueue.push( + ...job.files.map((fileName) => this.getTsTypeDecs(fileName)), + ); + } + + private async getTsTypeDecs( + fileName: string, + ): Promise { + console.log(`Processing ${fileName}`); + return (await this.pool.run( + { + fileName, + transform: this.transform, + }, + 'getTypeDecs', + )) as Awaited; + // Result should be serializable! + } + + private async generateTypedSQLTagFileForJob( + job: TransformJob, + useCache?: boolean, + ) { + this.pushToQueue(job); + return this.waitForTypedSQLQueueAndGenerate(useCache); + } + + private async waitForTypedSQLQueueAndGenerate(useCache?: boolean) { + const queueResults = await Promise.all(this.workQueue); + this.workQueue.length = 0; + + const typeDecsSets: TypeDeclarationSet[] = []; + + for (const result of queueResults) { + if (result) { + if (result.typedQueries.length > 0) typeDecsSets.push(result); + if (useCache) this.cache[result.fileName] = result; + } + } + + return this.generateTypedSQLTagFile(typeDecsSets); + } + + private async removeFileFromCache(fileToRemove: string) { + delete this.cache[fileToRemove]; + return this.generateTypedSQLTagFile(Object.values(this.cache)); + } + + private contentStart = `/* eslint-disable */\nimport { ${this.transform.functionName} as sourceSql } from '@pgtyped/runtime';\n\n`; + private contentEnd = [ + `export function ${this.transform.functionName}(s: string): unknown;`, + `export function ${this.transform.functionName}(s: string): unknown {`, + ` return sourceSql([s] as any);`, + `}`, + ]; + + private async generateTypedSQLTagFile(typeDecsSets: TypeDeclarationSet[]) { + console.log(`Generating ${this.fullFileName}...`); + const typeDefinitions = typeDecsSets + .map((typeDecSet) => + TypeAllocator.typeDefinitionDeclarations( + this.transform.emitFileName, + typeDecSet.typeDefinitions, + ), + ) + .filter((s) => s) + .join('\n'); + + const queryTypes = typeDecsSets + .map((typeDecSet) => generateDeclarations(typeDecSet.typedQueries)) + .join('\n'); + + const typedSQLOverloadFns = typeDecsSets + .map((set) => + genTypedSQLOverloadFunctions(this.transform.functionName, set), + ) + .join('\n'); + + let content = this.contentStart; + content += typeDefinitions; + content += queryTypes; + content += typedSQLOverloadFns; + content += this.contentEnd.join('\n'); + await fs.outputFile(this.fullFileName, content); + console.log(`Saved ${this.fullFileName}`); + } +} diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 9f06834c..cc609685 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,13 +1,14 @@ // Default types import { + ImportedType, isAlias, isEnum, isEnumArray, isImport, MappableType, Type, - ImportedType, } from '@pgtyped/query'; +import { AliasedType, EnumType } from '@pgtyped/query/lib/type.js'; import path from 'path'; const String: Type = { name: 'string' }; @@ -206,6 +207,14 @@ export enum TypeScope { Return = 'return', } +type importsType = { [k: string]: ImportedType[] }; + +export type TypeDefinitions = { + imports: importsType; + enums: EnumType[]; + aliases: AliasedType[]; +}; + /** Wraps a TypeMapping to track which types have been used, to accumulate errors, * and emit necessary type definitions. */ export class TypeAllocator { @@ -281,26 +290,43 @@ export class TypeAllocator { return typ.name; } - /** Emit a typescript definition for all types that have been used */ - declaration(decsFileName: string): string { - const imports = Object.values(this.imports) - .map((imports) => declareImport(imports, decsFileName)) - .sort() - .join('\n'); - - // Declare database enums as string unions to maintain assignability of their values between query files - const enums = Object.values(this.types) - .filter(isEnum) - .map((t) => declareStringUnion(t.name, t.enumValues)) - .sort() - .join('\n'); + // In order to get the results out of the Piscina pool, we need to have + // a serializable variant + public toTypeDefinitions(): TypeDefinitions { + return { + imports: this.imports, + enums: Object.values(this.types).filter(isEnum), + aliases: Object.values(this.types).filter(isAlias), + }; + } - const aliases = Object.values(this.types) - .filter(isAlias) - .map((t) => declareAlias(t.name, t.definition)) - .sort() + // Static so we can also use this for serialized typeDefinitions + public static typeDefinitionDeclarations( + decsFileName: string, + types: TypeDefinitions, + ): string { + return [ + Object.values(types.imports) + .map((i) => declareImport(i, decsFileName)) + .join('\n'), + types.enums + .map((t) => declareStringUnion(t.name, t.enumValues)) + .sort() + .join('\n'), + types.aliases + .map((t) => declareAlias(t.name, t.definition)) + .sort() + .join('\n'), + ] + .filter((s) => s) .join('\n'); + } - return [imports, enums, aliases].filter((s) => s).join('\n'); + /** Emit a typescript definition for all types that have been used */ + declaration(decsFileName: string): string { + return TypeAllocator.typeDefinitionDeclarations( + decsFileName, + this.toTypeDefinitions(), + ); } } diff --git a/packages/cli/src/typescriptAndSQLTransformer.ts b/packages/cli/src/typescriptAndSQLTransformer.ts new file mode 100644 index 00000000..d7d95baa --- /dev/null +++ b/packages/cli/src/typescriptAndSQLTransformer.ts @@ -0,0 +1,91 @@ +import chokidar from 'chokidar'; +import { globSync } from 'glob'; +import path from 'path'; +import { ParsedConfig, TransformConfig } from './config.js'; +import { TransformJob, WorkerPool } from './index.js'; +import { debug } from './util.js'; +import { processFileFnResult } from './worker.js'; + +export class typescriptAndSQLTransformer { + public readonly workQueue: Promise[] = []; + private readonly includePattern: string; + private fileOverrideUsed = false; + + constructor( + private readonly pool: WorkerPool, + private readonly config: ParsedConfig, + private readonly transform: TransformConfig, + ) { + this.includePattern = `${this.config.srcDir}/**/${transform.include}`; + } + + private async watch() { + const cb = async (fileName: string) => { + // we will not push it to the queue to not consume more memory + return this.processFile(fileName); + }; + + chokidar + .watch(this.includePattern, { + persistent: true, + }) + .on('add', cb) + .on('change', cb); + } + + public async start(watch: boolean, fileOverride?: string) { + if (watch) { + return this.watch(); + } + + /** + * If the user didn't provide the -f paramter, we're using the list of files we got from glob. + * If he did, we're using glob file list to detect if his provided file should be used with this transform. + */ + let fileList = globSync(this.includePattern, { + ...(this.transform.emitFileName && { + ignore: [`${this.config.srcDir}${this.transform.emitFileName}`], + }), + }); + if (fileOverride) { + fileList = fileList.includes(fileOverride) ? [fileOverride] : []; + if (fileList.length > 0) { + this.fileOverrideUsed = true; + } + } + debug('found query files %o', fileList); + + this.pushToQueue({ + files: fileList, + }); + + await Promise.all(this.workQueue); + return this.fileOverrideUsed; + } + + private async processFile(fileName: string) { + fileName = path.relative(process.cwd(), fileName); + console.log(`Processing ${fileName}`); + const result = (await this.pool.run( + { + fileName, + transform: this.transform, + }, + 'processFile', + )) as Awaited; + + if (result.skipped) { + console.log(`Skipped ${fileName}: no changes or no queries detected`); + } else { + console.log( + `Saved ${result.typeDecsLength} query types from ${fileName} to ${result.relativePath}`, + ); + } + } + + public pushToQueue(job: TransformJob) { + this.workQueue.push( + ...job.files.map((fileName) => this.processFile(fileName)), + ); + } +} diff --git a/packages/cli/src/worker.ts b/packages/cli/src/worker.ts index 41339252..471eb741 100644 --- a/packages/cli/src/worker.ts +++ b/packages/cli/src/worker.ts @@ -1,53 +1,88 @@ -import nun from 'nunjucks'; -import path from 'path'; -import fs from 'fs-extra'; -import { generateDeclarationFile } from './generator.js'; import { startup } from '@pgtyped/query'; -import { ParsedConfig, TransformConfig } from './config.js'; import { AsyncQueue } from '@pgtyped/wire'; +import fs from 'fs-extra'; +import nun from 'nunjucks'; +import path from 'path'; import worker from 'piscina'; +import { ParsedConfig, TransformConfig } from './config.js'; +import { + generateDeclarationFile, + generateTypedecsFromFile, +} from './generator.js'; +import { TypeAllocator, TypeMapping, TypeScope } from './types.js'; let connected = false; const connection = new AsyncQueue(); const config: ParsedConfig = worker.workerData; -export default async function processFile({ - fileName, - transform, -}: { - fileName: string; - transform: TransformConfig; -}): Promise<{ +export type IWorkerResult = { skipped: boolean; typeDecsLength: number; relativePath: string; -}> { +}; + +async function connectAndGetFileContents(fileName: string) { if (!connected) { await startup(config.db, connection); connected = true; } - const ppath = path.parse(fileName); - let decsFileName; - if (transform.emitTemplate) { - decsFileName = nun.renderString(transform.emitTemplate, ppath); - } else { - const suffix = transform.mode === 'ts' ? 'types.ts' : 'ts'; - decsFileName = path.resolve(ppath.dir, `${ppath.name}.${suffix}`); - } // last part fixes https://github.com/adelsz/pgtyped/issues/390 - const contents = fs.readFileSync(fileName).toString().replace(/\r\n/g, '\n'); + return fs.readFileSync(fileName).toString().replace(/\r\n/g, '\n'); +} + +export async function getTypeDecs({ + fileName, + transform, +}: { + fileName: string; + transform: TransformConfig; +}) { + const contents = await connectAndGetFileContents(fileName); + const types = new TypeAllocator(TypeMapping(config.typesOverrides)); - const { declarationFileContents, typeDecs } = await generateDeclarationFile( + if (transform.mode === 'sql') { + // Second parameter has no effect here, we could have used any value + types.use( + { name: 'PreparedQuery', from: '@pgtyped/runtime' }, + TypeScope.Return, + ); + } + return await generateTypedecsFromFile( contents, fileName, connection, - transform.mode, + transform, + types, config, - decsFileName, ); +} + +export type getTypeDecsFnResult = ReturnType; + +export async function processFile({ + fileName, + transform, +}: { + fileName: string; + transform: TransformConfig; +}): Promise { + const contents = await connectAndGetFileContents(fileName); + + const ppath = path.parse(fileName); + let decsFileName; + if ('emitTemplate' in transform && transform.emitTemplate) { + decsFileName = nun.renderString(transform.emitTemplate, ppath); + } else { + const suffix = transform.mode === 'ts' ? 'types.ts' : 'ts'; + decsFileName = path.resolve(ppath.dir, `${ppath.name}.${suffix}`); + } + + const typeDecSet = await getTypeDecs({ fileName, transform }); const relativePath = path.relative(process.cwd(), decsFileName); - if (typeDecs.length > 0) { + + if (typeDecSet.typedQueries.length > 0) { + const declarationFileContents = await generateDeclarationFile(typeDecSet); const oldDeclarationFileContents = (await fs.pathExists(decsFileName)) ? await fs.readFile(decsFileName, { encoding: 'utf-8' }) : null; @@ -55,7 +90,7 @@ export default async function processFile({ await fs.outputFile(decsFileName, declarationFileContents); return { skipped: false, - typeDecsLength: typeDecs.length, + typeDecsLength: typeDecSet.typedQueries.length, relativePath, }; } @@ -66,3 +101,5 @@ export default async function processFile({ relativePath, }; } + +export type processFileFnResult = ReturnType; diff --git a/packages/example/config.json b/packages/example/config.json index 482b3b9f..5550dc8d 100644 --- a/packages/example/config.json +++ b/packages/example/config.json @@ -9,6 +9,12 @@ "mode": "ts", "include": "**/*.ts", "emitTemplate": "{{dir}}/{{name}}.types.ts" + }, + { + "mode": "ts-typed-sql-tag", + "include": "**/*.ts", + "functionName": "sql", + "emitFileName": "sql/index.ts" } ], "typesOverrides": { diff --git a/packages/example/src/books/books.queries.ts b/packages/example/src/books/books.queries.ts index 260f6ae5..8d9f6092 100644 --- a/packages/example/src/books/books.queries.ts +++ b/packages/example/src/books/books.queries.ts @@ -1,8 +1,8 @@ /** Types generated for queries found in "src/books/books.sql" */ -import { Category } from '../customTypes'; - import { PreparedQuery } from '@pgtyped/runtime'; +import { Category } from '../customTypes'; + export type Iso31661Alpha2 = 'AD' | 'AE' | 'AF' | 'AG' | 'AI' | 'AL' | 'AM' | 'AO' | 'AQ' | 'AR' | 'AS' | 'AT' | 'AU' | 'AW' | 'AX' | 'AZ' | 'BA' | 'BB' | 'BD' | 'BE' | 'BF' | 'BG' | 'BH' | 'BI' | 'BJ' | 'BL' | 'BM' | 'BN' | 'BO' | 'BQ' | 'BR' | 'BS' | 'BT' | 'BV' | 'BW' | 'BY' | 'BZ' | 'CA' | 'CC' | 'CD' | 'CF' | 'CG' | 'CH' | 'CI' | 'CK' | 'CL' | 'CM' | 'CN' | 'CO' | 'CR' | 'CU' | 'CV' | 'CW' | 'CX' | 'CY' | 'CZ' | 'DE' | 'DJ' | 'DK' | 'DM' | 'DO' | 'DZ' | 'EC' | 'EE' | 'EG' | 'EH' | 'ER' | 'ES' | 'ET' | 'FI' | 'FJ' | 'FK' | 'FM' | 'FO' | 'FR' | 'GA' | 'GB' | 'GD' | 'GE' | 'GF' | 'GG' | 'GH' | 'GI' | 'GL' | 'GM' | 'GN' | 'GP' | 'GQ' | 'GR' | 'GS' | 'GT' | 'GU' | 'GW' | 'GY' | 'HK' | 'HM' | 'HN' | 'HR' | 'HT' | 'HU' | 'ID' | 'IE' | 'IL' | 'IM' | 'IN' | 'IO' | 'IQ' | 'IR' | 'IS' | 'IT' | 'JE' | 'JM' | 'JO' | 'JP' | 'KE' | 'KG' | 'KH' | 'KI' | 'KM' | 'KN' | 'KP' | 'KR' | 'KW' | 'KY' | 'KZ' | 'LA' | 'LB' | 'LC' | 'LI' | 'LK' | 'LR' | 'LS' | 'LT' | 'LU' | 'LV' | 'LY' | 'MA' | 'MC' | 'MD' | 'ME' | 'MF' | 'MG' | 'MH' | 'MK' | 'ML' | 'MM' | 'MN' | 'MO' | 'MP' | 'MQ' | 'MR' | 'MS' | 'MT' | 'MU' | 'MV' | 'MW' | 'MX' | 'MY' | 'MZ' | 'NA' | 'NC' | 'NE' | 'NF' | 'NG' | 'NI' | 'NL' | 'NO' | 'NP' | 'NR' | 'NU' | 'NZ' | 'OM' | 'PA' | 'PE' | 'PF' | 'PG' | 'PH' | 'PK' | 'PL' | 'PM' | 'PN' | 'PR' | 'PS' | 'PT' | 'PW' | 'PY' | 'QA' | 'RE' | 'RO' | 'RS' | 'RU' | 'RW' | 'SA' | 'SB' | 'SC' | 'SD' | 'SE' | 'SG' | 'SH' | 'SI' | 'SJ' | 'SK' | 'SL' | 'SM' | 'SN' | 'SO' | 'SR' | 'SS' | 'ST' | 'SV' | 'SX' | 'SY' | 'SZ' | 'TC' | 'TD' | 'TF' | 'TG' | 'TH' | 'TJ' | 'TK' | 'TL' | 'TM'; export type category = 'novel' | 'science-fiction' | 'thriller'; diff --git a/packages/example/src/sql/index.ts b/packages/example/src/sql/index.ts new file mode 100644 index 00000000..a6c2eb13 --- /dev/null +++ b/packages/example/src/sql/index.ts @@ -0,0 +1,111 @@ +/* eslint-disable */ +import { sql as sourceSql } from "@pgtyped/runtime"; + +export type notification_type = 'deadline' | 'notification' | 'reminder'; + +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; +/** 'GetUsersWithComments' parameters type */ +export interface IGetUsersWithCommentsParams { + minCommentCount: number; +} + +/** 'GetUsersWithComments' return type */ +export interface IGetUsersWithCommentsResult { + /** Age (in years) */ + age: number | null; + email: string; + first_name: string | null; + id: number; + last_name: string | null; + registration_date: string; + user_name: string; +} + +/** 'GetUsersWithComments' query type */ +export interface IGetUsersWithCommentsQuery { + params: IGetUsersWithCommentsParams; + result: IGetUsersWithCommentsResult; +} + +/** 'SelectExistsQuery' parameters type */ +export type ISelectExistsQueryParams = void; + +/** 'SelectExistsQuery' return type */ +export interface ISelectExistsQueryResult { + isTransactionExists: boolean | null; +} + +/** 'SelectExistsQuery' query type */ +export interface ISelectExistsQueryQuery { + params: ISelectExistsQueryParams; + result: ISelectExistsQueryResult; +} + + +/** 'InsertNotifications' parameters type */ +export interface IInsertNotificationsParams { + params: readonly ({ + payload: Json, + user_id: number, + type: notification_type + })[]; +} + +/** 'InsertNotifications' return type */ +export type IInsertNotificationsResult = void; + +/** 'InsertNotifications' query type */ +export interface IInsertNotificationsQuery { + params: IInsertNotificationsParams; + result: IInsertNotificationsResult; +} + +/** 'InsertNotification' parameters type */ +export interface IInsertNotificationParams { + notification: { + payload: Json, + user_id: number, + type: notification_type + }; +} + +/** 'InsertNotification' return type */ +export type IInsertNotificationResult = void; + +/** 'InsertNotification' query type */ +export interface IInsertNotificationQuery { + params: IInsertNotificationParams; + result: IInsertNotificationResult; +} + +/** 'GetAllNotifications' parameters type */ +export type IGetAllNotificationsParams = void; + +/** 'GetAllNotifications' return type */ +export interface IGetAllNotificationsResult { + created_at: string; + id: number; + payload: Json; + type: notification_type; + user_id: number | null; +} + +/** 'GetAllNotifications' query type */ +export interface IGetAllNotificationsQuery { + params: IGetAllNotificationsParams; + result: IGetAllNotificationsResult; +} + +export function sql(s: `SELECT u.* FROM users u + INNER JOIN book_comments bc ON u.id = bc.user_id + GROUP BY u.id + HAVING count(bc.id) > $minCommentCount!::int`): ReturnType>; +export function sql(s: `SELECT EXISTS ( SELECT 1 WHERE true ) AS "isTransactionExists"`): ReturnType>; +export function sql(s: `INSERT INTO notifications (payload, user_id, type) +values $$params(payload!, user_id!, type!)`): ReturnType>; +export function sql(s: `INSERT INTO notifications (payload, user_id, type) + values $notification(payload!, user_id!, type!)`): ReturnType>; +export function sql(s: `SELECT * FROM notifications`): ReturnType>;export function sql(s: string): unknown; +export function sql(s: string): unknown { + return sourceSql([s] as any); +} From 31755f7c4ffd4c39a01904941b3e8d971dd9c316 Mon Sep 17 00:00:00 2001 From: Jesse van der Velden Date: Tue, 2 May 2023 19:41:18 +0200 Subject: [PATCH 2/9] feat: Add Typed SQL Function Tags --- packages/cli/src/generator.test.ts | 6 +-- packages/cli/src/generator.ts | 4 +- packages/cli/src/index.ts | 16 +++--- ...ansformer.ts => typedSqlTagTransformer.ts} | 49 ++++++++++--------- ...rmer.ts => typescriptAndSqlTransformer.ts} | 4 +- packages/cli/src/worker.ts | 2 - packages/example/src/sql/index.ts | 12 ++--- 7 files changed, 43 insertions(+), 50 deletions(-) rename packages/cli/src/{typedSQLTagTransformer.ts => typedSqlTagTransformer.ts} (82%) rename packages/cli/src/{typescriptAndSQLTransformer.ts => typescriptAndSqlTransformer.ts} (97%) diff --git a/packages/cli/src/generator.test.ts b/packages/cli/src/generator.test.ts index c4b32bce..84b7ee9a 100644 --- a/packages/cli/src/generator.test.ts +++ b/packages/cli/src/generator.test.ts @@ -659,11 +659,7 @@ test('should generate the correct SQL overload functions', async () => { }, typeDeclaration: '', }; - const result = genTypedSQLOverloadFunctions('sqlFunc', { - typedQueries: [typedQuery], - typeDefinitions: { imports: {}, aliases: [], enums: [] }, - fileName: 'test.ts', - }); + const result = genTypedSQLOverloadFunctions('sqlFunc', [typedQuery]); const expected = `export function sqlFunc(s: \`SELECT id from users\`): ReturnType>;`; expect(result).toEqual(expected); }); diff --git a/packages/cli/src/generator.ts b/packages/cli/src/generator.ts index 38c11615..46836fbb 100644 --- a/packages/cli/src/generator.ts +++ b/packages/cli/src/generator.ts @@ -411,9 +411,9 @@ export function generateDeclarationFile(typeDecSet: TypeDeclarationSet) { export function genTypedSQLOverloadFunctions( functionName: string, - typeDecSet: TypeDeclarationSet, + typedQueries: ITSTypedQuery[], ) { - return (typeDecSet.typedQueries as ITSTypedQuery[]) + return typedQueries .map( (typeDec) => `export function ${functionName}(s: \`${typeDec.query.ast.text}\`): ReturnType>;`, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c9f8d934..a65c757f 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -9,8 +9,8 @@ import PiscinaPool from 'piscina'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { parseConfig, ParsedConfig, TransformConfig } from './config.js'; -import { TypedSQLTagTransformer } from './typedSQLTagTransformer.js'; -import { typescriptAndSQLTransformer } from './typescriptAndSQLTransformer.js'; +import { TypedSqlTagTransformer } from './typedSqlTagTransformer.js'; +import { TypescriptAndSqlTransformer } from './typescriptAndSqlTransformer.js'; import { debug } from './util.js'; // tslint:disable:no-console @@ -75,19 +75,15 @@ async function main( const transformTask = async (transform: TransformConfig) => { if (transform.mode === 'ts-typed-sql-tag') { - const typedSQLTagTransformer = new TypedSQLTagTransformer( - pool, - config, - transform, - ); - return typedSQLTagTransformer.start(isWatchMode); + const transformer = new TypedSqlTagTransformer(pool, config, transform); + return transformer.start(isWatchMode); } else { - const sqlAndTSTransformer = new typescriptAndSQLTransformer( + const transformer = new TypescriptAndSqlTransformer( pool, config, transform, ); - return sqlAndTSTransformer.start(isWatchMode); + return transformer.start(isWatchMode); } }; diff --git a/packages/cli/src/typedSQLTagTransformer.ts b/packages/cli/src/typedSqlTagTransformer.ts similarity index 82% rename from packages/cli/src/typedSQLTagTransformer.ts rename to packages/cli/src/typedSqlTagTransformer.ts index dad562cd..7e7e4a6d 100644 --- a/packages/cli/src/typedSQLTagTransformer.ts +++ b/packages/cli/src/typedSqlTagTransformer.ts @@ -6,6 +6,7 @@ import { ParsedConfig, TSTypedSQLTagTransformConfig } from './config.js'; import { generateDeclarations, genTypedSQLOverloadFunctions, + ITSTypedQuery, TypeDeclarationSet, } from './generator.js'; import { TransformJob, WorkerPool } from './index.js'; @@ -15,7 +16,8 @@ import { getTypeDecsFnResult } from './worker.js'; type TypedSQLTagTransformResult = TypeDeclarationSet | undefined; -export class TypedSQLTagTransformer { +// tslint:disable:no-console +export class TypedSqlTagTransformer { public readonly workQueue: Promise[] = []; private readonly cache: Record = {}; private readonly includePattern: string; @@ -63,7 +65,7 @@ export class TypedSQLTagTransformer { return this.watch(); } - let fileList = globSync(this.includePattern, { + const fileList = globSync(this.includePattern, { ignore: [this.localFileName], }); @@ -109,13 +111,15 @@ export class TypedSQLTagTransformer { const typeDecsSets: TypeDeclarationSet[] = []; for (const result of queueResults) { - if (result) { - if (result.typedQueries.length > 0) typeDecsSets.push(result); + if (result?.typedQueries.length) { + typeDecsSets.push(result); if (useCache) this.cache[result.fileName] = result; } } - return this.generateTypedSQLTagFile(typeDecsSets); + return this.generateTypedSQLTagFile( + useCache ? Object.values(this.cache) : typeDecsSets, + ); } private async removeFileFromCache(fileToRemove: string) { @@ -133,30 +137,27 @@ export class TypedSQLTagTransformer { private async generateTypedSQLTagFile(typeDecsSets: TypeDeclarationSet[]) { console.log(`Generating ${this.fullFileName}...`); - const typeDefinitions = typeDecsSets - .map((typeDecSet) => - TypeAllocator.typeDefinitionDeclarations( - this.transform.emitFileName, - typeDecSet.typeDefinitions, - ), - ) - .filter((s) => s) - .join('\n'); - - const queryTypes = typeDecsSets - .map((typeDecSet) => generateDeclarations(typeDecSet.typedQueries)) - .join('\n'); - - const typedSQLOverloadFns = typeDecsSets - .map((set) => - genTypedSQLOverloadFunctions(this.transform.functionName, set), - ) - .join('\n'); + let typeDefinitions = ''; + let queryTypes = ''; + let typedSQLOverloadFns = ''; + + for (const typeDecSet of typeDecsSets) { + typeDefinitions += TypeAllocator.typeDefinitionDeclarations( + this.transform.emitFileName, + typeDecSet.typeDefinitions, + ); + queryTypes += generateDeclarations(typeDecSet.typedQueries); + typedSQLOverloadFns += genTypedSQLOverloadFunctions( + this.transform.functionName, + typeDecSet.typedQueries as ITSTypedQuery[], + ); + } let content = this.contentStart; content += typeDefinitions; content += queryTypes; content += typedSQLOverloadFns; + content += '\n\n'; content += this.contentEnd.join('\n'); await fs.outputFile(this.fullFileName, content); console.log(`Saved ${this.fullFileName}`); diff --git a/packages/cli/src/typescriptAndSQLTransformer.ts b/packages/cli/src/typescriptAndSqlTransformer.ts similarity index 97% rename from packages/cli/src/typescriptAndSQLTransformer.ts rename to packages/cli/src/typescriptAndSqlTransformer.ts index d7d95baa..899e9d82 100644 --- a/packages/cli/src/typescriptAndSQLTransformer.ts +++ b/packages/cli/src/typescriptAndSqlTransformer.ts @@ -6,7 +6,9 @@ import { TransformJob, WorkerPool } from './index.js'; import { debug } from './util.js'; import { processFileFnResult } from './worker.js'; -export class typescriptAndSQLTransformer { +// tslint:disable:no-console + +export class TypescriptAndSqlTransformer { public readonly workQueue: Promise[] = []; private readonly includePattern: string; private fileOverrideUsed = false; diff --git a/packages/cli/src/worker.ts b/packages/cli/src/worker.ts index 471eb741..b97ff493 100644 --- a/packages/cli/src/worker.ts +++ b/packages/cli/src/worker.ts @@ -67,8 +67,6 @@ export async function processFile({ fileName: string; transform: TransformConfig; }): Promise { - const contents = await connectAndGetFileContents(fileName); - const ppath = path.parse(fileName); let decsFileName; if ('emitTemplate' in transform && transform.emitTemplate) { diff --git a/packages/example/src/sql/index.ts b/packages/example/src/sql/index.ts index a6c2eb13..3fb5ff92 100644 --- a/packages/example/src/sql/index.ts +++ b/packages/example/src/sql/index.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { sql as sourceSql } from "@pgtyped/runtime"; +import { sql as sourceSql } from '@pgtyped/runtime'; export type notification_type = 'deadline' | 'notification' | 'reminder'; @@ -41,7 +41,6 @@ export interface ISelectExistsQueryQuery { result: ISelectExistsQueryResult; } - /** 'InsertNotifications' parameters type */ export interface IInsertNotificationsParams { params: readonly ({ @@ -100,12 +99,13 @@ export function sql(s: `SELECT u.* FROM users u INNER JOIN book_comments bc ON u.id = bc.user_id GROUP BY u.id HAVING count(bc.id) > $minCommentCount!::int`): ReturnType>; -export function sql(s: `SELECT EXISTS ( SELECT 1 WHERE true ) AS "isTransactionExists"`): ReturnType>; -export function sql(s: `INSERT INTO notifications (payload, user_id, type) +export function sql(s: `SELECT EXISTS ( SELECT 1 WHERE true ) AS "isTransactionExists"`): ReturnType>;export function sql(s: `INSERT INTO notifications (payload, user_id, type) values $$params(payload!, user_id!, type!)`): ReturnType>; export function sql(s: `INSERT INTO notifications (payload, user_id, type) values $notification(payload!, user_id!, type!)`): ReturnType>; -export function sql(s: `SELECT * FROM notifications`): ReturnType>;export function sql(s: string): unknown; +export function sql(s: `SELECT * FROM notifications`): ReturnType>; + +export function sql(s: string): unknown; export function sql(s: string): unknown { return sourceSql([s] as any); -} +} \ No newline at end of file From 29c93027f05f42b8e6432a8bf2e741bec23d31d4 Mon Sep 17 00:00:00 2001 From: Adel Salakh Date: Fri, 29 Sep 2023 01:21:07 +0200 Subject: [PATCH 3/9] remove hungarian notation for new types --- packages/cli/src/generator.test.ts | 4 ++-- packages/cli/src/generator.ts | 16 ++++++++-------- packages/cli/src/typedSqlTagTransformer.ts | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/generator.test.ts b/packages/cli/src/generator.test.ts index d8f4b22f..5e8e6741 100644 --- a/packages/cli/src/generator.test.ts +++ b/packages/cli/src/generator.test.ts @@ -7,7 +7,7 @@ import { escapeComment, generateInterface, genTypedSQLOverloadFunctions, - ITSTypedQuery, + TSTypedQuery, ProcessingMode, queryToTypeDeclarations, } from './generator.js'; @@ -665,7 +665,7 @@ test('should generate the correct SQL overload functions', async () => { const getUsers = sql\`SELECT id from users\`; `; const query = parsedQuery(ProcessingMode.TS, queryStringTS); - const typedQuery: ITSTypedQuery = { + const typedQuery: TSTypedQuery = { mode: 'ts' as const, fileName: 'test.ts', query: { diff --git a/packages/cli/src/generator.ts b/packages/cli/src/generator.ts index a89a1663..45a3efb5 100644 --- a/packages/cli/src/generator.ts +++ b/packages/cli/src/generator.ts @@ -261,7 +261,7 @@ export async function queryToTypeDeclarations( ); } -export type ITSTypedQuery = { +export type TSTypedQuery = { mode: 'ts'; fileName: string; query: { @@ -272,7 +272,7 @@ export type ITSTypedQuery = { typeDeclaration: string; }; -type ISQLTypedQuery = { +type SQLTypedQuery = { mode: 'sql'; fileName: string; query: { @@ -285,9 +285,9 @@ type ISQLTypedQuery = { typeDeclaration: string; }; -export type ITypedQuery = ITSTypedQuery | ISQLTypedQuery; +export type TypedQuery = TSTypedQuery | SQLTypedQuery; export type TypeDeclarationSet = { - typedQueries: ITypedQuery[]; + typedQueries: TypedQuery[]; typeDefinitions: TypeDefinitions; fileName: string; }; @@ -299,7 +299,7 @@ export async function generateTypedecsFromFile( types: TypeAllocator, config: ParsedConfig, ): Promise { - const typedQueries: ITypedQuery[] = []; + const typedQueries: TypedQuery[] = []; const interfacePrefix = config.hungarianNotation ? 'I' : ''; const typeSource: TypeSource = (query) => getTypes(query, connection); @@ -320,7 +320,7 @@ export async function generateTypedecsFromFile( } for (const queryAST of queries) { - let typedQuery: ITypedQuery; + let typedQuery: TypedQuery; if (transform.mode === 'sql') { const sqlQueryAST = queryAST as SQLQueryAST; const result = await queryToTypeDeclarations( @@ -374,7 +374,7 @@ export async function generateTypedecsFromFile( return { typedQueries, typeDefinitions: types.toTypeDefinitions(), fileName }; } -export function generateDeclarations(typeDecs: ITypedQuery[]): string { +export function generateDeclarations(typeDecs: TypedQuery[]): string { let typeDeclarations = ''; for (const typeDec of typeDecs) { typeDeclarations += typeDec.typeDeclaration; @@ -424,7 +424,7 @@ export function generateDeclarationFile(typeDecSet: TypeDeclarationSet) { export function genTypedSQLOverloadFunctions( functionName: string, - typedQueries: ITSTypedQuery[], + typedQueries: TSTypedQuery[], ) { return typedQueries .map( diff --git a/packages/cli/src/typedSqlTagTransformer.ts b/packages/cli/src/typedSqlTagTransformer.ts index 7e7e4a6d..8f05734f 100644 --- a/packages/cli/src/typedSqlTagTransformer.ts +++ b/packages/cli/src/typedSqlTagTransformer.ts @@ -6,7 +6,7 @@ import { ParsedConfig, TSTypedSQLTagTransformConfig } from './config.js'; import { generateDeclarations, genTypedSQLOverloadFunctions, - ITSTypedQuery, + TSTypedQuery, TypeDeclarationSet, } from './generator.js'; import { TransformJob, WorkerPool } from './index.js'; @@ -149,7 +149,7 @@ export class TypedSqlTagTransformer { queryTypes += generateDeclarations(typeDecSet.typedQueries); typedSQLOverloadFns += genTypedSQLOverloadFunctions( this.transform.functionName, - typeDecSet.typedQueries as ITSTypedQuery[], + typeDecSet.typedQueries as TSTypedQuery[], ); } From 67fc5f4d9cb7d0a650d5a901c5b5b749842842a4 Mon Sep 17 00:00:00 2001 From: Adel Salakh Date: Fri, 29 Sep 2023 01:25:12 +0200 Subject: [PATCH 4/9] rename new mode to ts-implicit --- packages/cli/src/config.ts | 4 ++-- packages/cli/src/index.ts | 2 +- packages/cli/src/parseTypescript.ts | 2 +- packages/example/config.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 04563668..6c825ca5 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -26,7 +26,7 @@ const TSTransformCodec = t.type({ }); const TSTypedSQLTagTransformCodec = t.type({ - mode: t.literal('ts-typed-sql-tag'), + mode: t.literal('ts-implicit'), include: t.string, functionName: t.string, emitFileName: t.string, @@ -209,7 +209,7 @@ export function parseConfig( : {}; if ( - transforms.some((tr) => tr.mode !== 'ts-typed-sql-tag' && !!tr.emitFileName) + transforms.some((tr) => tr.mode !== 'ts-implicit' && !!tr.emitFileName) ) { // tslint:disable:no-console console.log( diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c63753bd..a0e5cd99 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -75,7 +75,7 @@ async function main( const pool = new WorkerPool(config); const transformTask = async (transform: TransformConfig) => { - if (transform.mode === 'ts-typed-sql-tag') { + if (transform.mode === 'ts-implicit') { const transformer = new TypedSqlTagTransformer(pool, config, transform); return transformer.start(isWatchMode); } else { diff --git a/packages/cli/src/parseTypescript.ts b/packages/cli/src/parseTypescript.ts index 1b95bd88..a09b38f1 100644 --- a/packages/cli/src/parseTypescript.ts +++ b/packages/cli/src/parseTypescript.ts @@ -18,7 +18,7 @@ export function parseFile( function parseNode(node: ts.Node) { if ( - transformConfig?.mode === 'ts-typed-sql-tag' && + transformConfig?.mode === 'ts-implicit' && node.kind === ts.SyntaxKind.CallExpression ) { const callNode = node as ts.CallExpression; diff --git a/packages/example/config.json b/packages/example/config.json index 5550dc8d..9a5999a4 100644 --- a/packages/example/config.json +++ b/packages/example/config.json @@ -11,7 +11,7 @@ "emitTemplate": "{{dir}}/{{name}}.types.ts" }, { - "mode": "ts-typed-sql-tag", + "mode": "ts-implicit", "include": "**/*.ts", "functionName": "sql", "emitFileName": "sql/index.ts" From 5d725b9164ee9107427c8bfbd070946a45a3e8ff Mon Sep 17 00:00:00 2001 From: Adel Salakh Date: Fri, 29 Sep 2023 17:34:42 +0200 Subject: [PATCH 5/9] cleanup --- packages/cli/src/config.ts | 4 +--- packages/example/src/index.test.ts | 2 +- packages/example/src/sql/index.ts | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index 6c825ca5..80bee2d2 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -208,9 +208,7 @@ export function parseConfig( ? convertParsedURLToDBConfig(parseDatabaseUri(dbUri)) : {}; - if ( - transforms.some((tr) => tr.mode !== 'ts-implicit' && !!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.', diff --git a/packages/example/src/index.test.ts b/packages/example/src/index.test.ts index 0947e7b7..ec4c191e 100644 --- a/packages/example/src/index.test.ts +++ b/packages/example/src/index.test.ts @@ -28,7 +28,7 @@ import { thresholdFrogs, } from './notifications/notifications.queries.js'; import { getUsersWithComment } from './users/sample.js'; -import { Category } from './customTypes'; +import { Category } from './customTypes.js'; const { Client } = pg; diff --git a/packages/example/src/sql/index.ts b/packages/example/src/sql/index.ts index 3fb5ff92..cafd7067 100644 --- a/packages/example/src/sql/index.ts +++ b/packages/example/src/sql/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ import { sql as sourceSql } from '@pgtyped/runtime'; export type notification_type = 'deadline' | 'notification' | 'reminder'; @@ -108,4 +107,4 @@ export function sql(s: `SELECT * FROM notifications`): ReturnType Date: Fri, 29 Sep 2023 18:48:17 +0200 Subject: [PATCH 6/9] fix wrong imports in generated files --- packages/cli/src/typedSqlTagTransformer.ts | 2 +- packages/example/config.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/typedSqlTagTransformer.ts b/packages/cli/src/typedSqlTagTransformer.ts index 8f05734f..a32dbd0b 100644 --- a/packages/cli/src/typedSqlTagTransformer.ts +++ b/packages/cli/src/typedSqlTagTransformer.ts @@ -30,7 +30,7 @@ export class TypedSqlTagTransformer { private readonly transform: TSTypedSQLTagTransformConfig, ) { this.includePattern = `${this.config.srcDir}/**/${transform.include}`; - this.localFileName = `${this.config.srcDir}${this.transform.emitFileName}`; + this.localFileName = this.transform.emitFileName; this.fullFileName = path.relative(process.cwd(), this.localFileName); } diff --git a/packages/example/config.json b/packages/example/config.json index 9a5999a4..97cdc9c5 100644 --- a/packages/example/config.json +++ b/packages/example/config.json @@ -14,7 +14,7 @@ "mode": "ts-implicit", "include": "**/*.ts", "functionName": "sql", - "emitFileName": "sql/index.ts" + "emitFileName": "./src/sql/index.ts" } ], "typesOverrides": { From 3d20e59aebfb19caa2ae5cc17842aa5de9820f9d Mon Sep 17 00:00:00 2001 From: Adel Salakh Date: Fri, 29 Sep 2023 18:49:13 +0200 Subject: [PATCH 7/9] prevent watch mode crashes on errors --- packages/cli/src/worker.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/worker.ts b/packages/cli/src/worker.ts index 67f1bc57..240e74ad 100644 --- a/packages/cli/src/worker.ts +++ b/packages/cli/src/worker.ts @@ -23,11 +23,16 @@ interface ExtendedParsedPath extends path.ParsedPath { dir_base: string; } -export type IWorkerResult = { - skipped: boolean; - typeDecsLength: number; - relativePath: string; -}; +export type IWorkerResult = + | { + skipped: boolean; + typeDecsLength: number; + relativePath: string; + } + | { + error: any; + relativePath: string; + }; async function connectAndGetFileContents(fileName: string) { if (!connected) { @@ -85,7 +90,15 @@ export async function processFile({ decsFileName = path.resolve(ppath.dir, `${ppath.name}.${suffix}`); } - const typeDecSet = await getTypeDecs({ fileName, transform }); + let typeDecSet; + try { + typeDecSet = await getTypeDecs({ fileName, transform }); + } catch (e) { + return { + error: e, + relativePath: path.relative(process.cwd(), fileName), + }; + } const relativePath = path.relative(process.cwd(), decsFileName); if (typeDecSet.typedQueries.length > 0) { From 20499f24390eb4169613ccd457c04c7f60ac611b Mon Sep 17 00:00:00 2001 From: Adel Salakh Date: Fri, 29 Sep 2023 18:50:00 +0200 Subject: [PATCH 8/9] add e2e test for ts-implicit mode --- packages/cli/src/typedSqlTagTransformer.ts | 2 +- .../cli/src/typescriptAndSqlTransformer.ts | 6 +- packages/example/config.json | 4 +- packages/example/src/books/books.queries.ts | 2 +- packages/example/src/index.test.ts | 7 ++ packages/example/src/sql/index.ts | 113 +++--------------- 6 files changed, 33 insertions(+), 101 deletions(-) diff --git a/packages/cli/src/typedSqlTagTransformer.ts b/packages/cli/src/typedSqlTagTransformer.ts index a32dbd0b..d6a13d75 100644 --- a/packages/cli/src/typedSqlTagTransformer.ts +++ b/packages/cli/src/typedSqlTagTransformer.ts @@ -127,7 +127,7 @@ export class TypedSqlTagTransformer { return this.generateTypedSQLTagFile(Object.values(this.cache)); } - private contentStart = `/* eslint-disable */\nimport { ${this.transform.functionName} as sourceSql } from '@pgtyped/runtime';\n\n`; + private contentStart = `import { ${this.transform.functionName} as sourceSql } from '@pgtyped/runtime';\n\n`; private contentEnd = [ `export function ${this.transform.functionName}(s: string): unknown;`, `export function ${this.transform.functionName}(s: string): unknown {`, diff --git a/packages/cli/src/typescriptAndSqlTransformer.ts b/packages/cli/src/typescriptAndSqlTransformer.ts index 899e9d82..8fac8e7d 100644 --- a/packages/cli/src/typescriptAndSqlTransformer.ts +++ b/packages/cli/src/typescriptAndSqlTransformer.ts @@ -76,8 +76,12 @@ export class TypescriptAndSqlTransformer { 'processFile', )) as Awaited; - if (result.skipped) { + if ('skipped' in result && result.skipped) { console.log(`Skipped ${fileName}: no changes or no queries detected`); + } else if ('error' in result) { + console.error( + `Error processing ${fileName}: ${result.error.message}\n${result.error.stack}`, + ); } else { console.log( `Saved ${result.typeDecsLength} query types from ${fileName} to ${result.relativePath}`, diff --git a/packages/example/config.json b/packages/example/config.json index 97cdc9c5..6dc053dd 100644 --- a/packages/example/config.json +++ b/packages/example/config.json @@ -12,7 +12,7 @@ }, { "mode": "ts-implicit", - "include": "**/*.ts", + "include": "index.test.ts", "functionName": "sql", "emitFileName": "./src/sql/index.ts" } @@ -22,7 +22,7 @@ "return": "string" }, "int8": "BigInt", - "category": { "return": "./src/customTypes#Category" } + "category": { "return": "./src/customTypes.js#Category" } }, "srcDir": "./src/", "dbUrl": "postgres://postgres:password@localhost/postgres" diff --git a/packages/example/src/books/books.queries.ts b/packages/example/src/books/books.queries.ts index 66ae39f3..c3a675ea 100644 --- a/packages/example/src/books/books.queries.ts +++ b/packages/example/src/books/books.queries.ts @@ -1,7 +1,7 @@ /** Types generated for queries found in "src/books/books.sql" */ import { PreparedQuery } from '@pgtyped/runtime'; -import type { Category } from '../customTypes'; +import type { Category } from '../customTypes.js'; export type Iso31661Alpha2 = 'AD' | 'AE' | 'AF' | 'AG' | 'AI' | 'AL' | 'AM' | 'AO' | 'AQ' | 'AR' | 'AS' | 'AT' | 'AU' | 'AW' | 'AX' | 'AZ' | 'BA' | 'BB' | 'BD' | 'BE' | 'BF' | 'BG' | 'BH' | 'BI' | 'BJ' | 'BL' | 'BM' | 'BN' | 'BO' | 'BQ' | 'BR' | 'BS' | 'BT' | 'BV' | 'BW' | 'BY' | 'BZ' | 'CA' | 'CC' | 'CD' | 'CF' | 'CG' | 'CH' | 'CI' | 'CK' | 'CL' | 'CM' | 'CN' | 'CO' | 'CR' | 'CU' | 'CV' | 'CW' | 'CX' | 'CY' | 'CZ' | 'DE' | 'DJ' | 'DK' | 'DM' | 'DO' | 'DZ' | 'EC' | 'EE' | 'EG' | 'EH' | 'ER' | 'ES' | 'ET' | 'FI' | 'FJ' | 'FK' | 'FM' | 'FO' | 'FR' | 'GA' | 'GB' | 'GD' | 'GE' | 'GF' | 'GG' | 'GH' | 'GI' | 'GL' | 'GM' | 'GN' | 'GP' | 'GQ' | 'GR' | 'GS' | 'GT' | 'GU' | 'GW' | 'GY' | 'HK' | 'HM' | 'HN' | 'HR' | 'HT' | 'HU' | 'ID' | 'IE' | 'IL' | 'IM' | 'IN' | 'IO' | 'IQ' | 'IR' | 'IS' | 'IT' | 'JE' | 'JM' | 'JO' | 'JP' | 'KE' | 'KG' | 'KH' | 'KI' | 'KM' | 'KN' | 'KP' | 'KR' | 'KW' | 'KY' | 'KZ' | 'LA' | 'LB' | 'LC' | 'LI' | 'LK' | 'LR' | 'LS' | 'LT' | 'LU' | 'LV' | 'LY' | 'MA' | 'MC' | 'MD' | 'ME' | 'MF' | 'MG' | 'MH' | 'MK' | 'ML' | 'MM' | 'MN' | 'MO' | 'MP' | 'MQ' | 'MR' | 'MS' | 'MT' | 'MU' | 'MV' | 'MW' | 'MX' | 'MY' | 'MZ' | 'NA' | 'NC' | 'NE' | 'NF' | 'NG' | 'NI' | 'NL' | 'NO' | 'NP' | 'NR' | 'NU' | 'NZ' | 'OM' | 'PA' | 'PE' | 'PF' | 'PG' | 'PH' | 'PK' | 'PL' | 'PM' | 'PN' | 'PR' | 'PS' | 'PT' | 'PW' | 'PY' | 'QA' | 'RE' | 'RO' | 'RS' | 'RU' | 'RW' | 'SA' | 'SB' | 'SC' | 'SD' | 'SE' | 'SG' | 'SH' | 'SI' | 'SJ' | 'SK' | 'SL' | 'SM' | 'SN' | 'SO' | 'SR' | 'SS' | 'ST' | 'SV' | 'SX' | 'SY' | 'SZ' | 'TC' | 'TD' | 'TF' | 'TG' | 'TH' | 'TJ' | 'TK' | 'TL' | 'TM'; diff --git a/packages/example/src/index.test.ts b/packages/example/src/index.test.ts index ec4c191e..dd6629cd 100644 --- a/packages/example/src/index.test.ts +++ b/packages/example/src/index.test.ts @@ -29,6 +29,7 @@ import { } from './notifications/notifications.queries.js'; import { getUsersWithComment } from './users/sample.js'; import { Category } from './customTypes.js'; +import { sql } from './sql/index.js' const { Client } = pg; @@ -230,3 +231,9 @@ test('select query with a bigint field', async () => { expect(typeof row.book_count).toBe('bigint'); expect(row.book_count).toBe(BigInt(4)); }); + + +test('ts-implicit mode query', async () => { + const books = await sql(`SELECT * FROM books WHERE id = $id`).run({id: 1}, client); + expect(books).toMatchSnapshot(); +}); diff --git a/packages/example/src/sql/index.ts b/packages/example/src/sql/index.ts index cafd7067..f8016d21 100644 --- a/packages/example/src/sql/index.ts +++ b/packages/example/src/sql/index.ts @@ -1,110 +1,31 @@ import { sql as sourceSql } from '@pgtyped/runtime'; -export type notification_type = 'deadline' | 'notification' | 'reminder'; +import type { Category } from '../customTypes.js'; -export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; -/** 'GetUsersWithComments' parameters type */ -export interface IGetUsersWithCommentsParams { - minCommentCount: number; +export type categoryArray = (Category)[]; +/** 'SqlSelectFromBooksWhereIdId' parameters type */ +export interface ISqlSelectFromBooksWhereIdIdParams { + id?: number | null | void; } -/** 'GetUsersWithComments' return type */ -export interface IGetUsersWithCommentsResult { - /** Age (in years) */ - age: number | null; - email: string; - first_name: string | null; +/** 'SqlSelectFromBooksWhereIdId' return type */ +export interface ISqlSelectFromBooksWhereIdIdResult { + author_id: number | null; + categories: categoryArray | null; id: number; - last_name: string | null; - registration_date: string; - user_name: string; + name: string | null; + rank: number | null; } -/** 'GetUsersWithComments' query type */ -export interface IGetUsersWithCommentsQuery { - params: IGetUsersWithCommentsParams; - result: IGetUsersWithCommentsResult; +/** 'SqlSelectFromBooksWhereIdId' query type */ +export interface ISqlSelectFromBooksWhereIdIdQuery { + params: ISqlSelectFromBooksWhereIdIdParams; + result: ISqlSelectFromBooksWhereIdIdResult; } -/** 'SelectExistsQuery' parameters type */ -export type ISelectExistsQueryParams = void; - -/** 'SelectExistsQuery' return type */ -export interface ISelectExistsQueryResult { - isTransactionExists: boolean | null; -} - -/** 'SelectExistsQuery' query type */ -export interface ISelectExistsQueryQuery { - params: ISelectExistsQueryParams; - result: ISelectExistsQueryResult; -} - -/** 'InsertNotifications' parameters type */ -export interface IInsertNotificationsParams { - params: readonly ({ - payload: Json, - user_id: number, - type: notification_type - })[]; -} - -/** 'InsertNotifications' return type */ -export type IInsertNotificationsResult = void; - -/** 'InsertNotifications' query type */ -export interface IInsertNotificationsQuery { - params: IInsertNotificationsParams; - result: IInsertNotificationsResult; -} - -/** 'InsertNotification' parameters type */ -export interface IInsertNotificationParams { - notification: { - payload: Json, - user_id: number, - type: notification_type - }; -} - -/** 'InsertNotification' return type */ -export type IInsertNotificationResult = void; - -/** 'InsertNotification' query type */ -export interface IInsertNotificationQuery { - params: IInsertNotificationParams; - result: IInsertNotificationResult; -} - -/** 'GetAllNotifications' parameters type */ -export type IGetAllNotificationsParams = void; - -/** 'GetAllNotifications' return type */ -export interface IGetAllNotificationsResult { - created_at: string; - id: number; - payload: Json; - type: notification_type; - user_id: number | null; -} - -/** 'GetAllNotifications' query type */ -export interface IGetAllNotificationsQuery { - params: IGetAllNotificationsParams; - result: IGetAllNotificationsResult; -} - -export function sql(s: `SELECT u.* FROM users u - INNER JOIN book_comments bc ON u.id = bc.user_id - GROUP BY u.id - HAVING count(bc.id) > $minCommentCount!::int`): ReturnType>; -export function sql(s: `SELECT EXISTS ( SELECT 1 WHERE true ) AS "isTransactionExists"`): ReturnType>;export function sql(s: `INSERT INTO notifications (payload, user_id, type) -values $$params(payload!, user_id!, type!)`): ReturnType>; -export function sql(s: `INSERT INTO notifications (payload, user_id, type) - values $notification(payload!, user_id!, type!)`): ReturnType>; -export function sql(s: `SELECT * FROM notifications`): ReturnType>; +export function sql(s: `SELECT * FROM books WHERE id = $id`): ReturnType>; export function sql(s: string): unknown; export function sql(s: string): unknown { return sourceSql([s] as any); -} +} \ No newline at end of file From 5adb13bd5c51ead180582d05af127f68808fe95f Mon Sep 17 00:00:00 2001 From: Adel Salakh Date: Fri, 29 Sep 2023 18:54:51 +0200 Subject: [PATCH 9/9] update test snapshots --- .../example/src/__snapshots__/index.test.ts.snap | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/example/src/__snapshots__/index.test.ts.snap b/packages/example/src/__snapshots__/index.test.ts.snap index a8179b60..f98c6f6d 100644 --- a/packages/example/src/__snapshots__/index.test.ts.snap +++ b/packages/example/src/__snapshots__/index.test.ts.snap @@ -79,3 +79,15 @@ Array [ `; exports[`select query with unicode characters 1`] = `Array []`; + +exports[`ts-implicit mode query 1`] = ` +Array [ + Object { + "author_id": 1, + "categories": null, + "id": 1, + "name": "Black Swan", + "rank": 1, + }, +] +`;