From 7f3dcbd0c6f91e4754f63a7a1b1ea693b990c5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Cholewi=C5=84ski?= Date: Tue, 12 Aug 2025 12:59:51 +0200 Subject: [PATCH 1/2] feat: modularize the library for external use of internal processing function --- package-lock.json | 88 ++++++++++++++----- package.json | 2 +- packages/cli/package.json | 26 ++---- packages/cli/src/config.ts | 64 ++++---------- packages/cli/src/index.ts | 7 +- packages/cli/src/typedSqlTagTransformer.ts | 14 +-- .../cli/src/typescriptAndSqlTransformer.ts | 8 +- packages/cli/src/worker.ts | 77 ++++++---------- packages/example/config.json | 3 +- packages/example/src/sql/index.ts | 23 ----- packages/typegen/.gitignore | 3 + packages/typegen/jest.config.ts | 24 +++++ packages/typegen/package.json | 58 ++++++++++++ .../parseTypescript.test.ts.snap | 0 packages/typegen/src/config.ts | 56 ++++++++++++ .../src/declareImport.test.ts | 0 .../{cli => typegen}/src/generator.test.ts | 20 +++-- packages/{cli => typegen}/src/generator.ts | 28 +++--- packages/typegen/src/index.ts | 70 +++++++++++++++ .../src/parseTypescript.test.ts | 0 .../{cli => typegen}/src/parseTypescript.ts | 12 +-- packages/{cli => typegen}/src/types.test.ts | 0 packages/{cli => typegen}/src/types.ts | 4 +- packages/{cli => typegen}/src/util.ts | 0 packages/typegen/tsconfig.json | 8 ++ 25 files changed, 388 insertions(+), 207 deletions(-) create mode 100644 packages/typegen/.gitignore create mode 100644 packages/typegen/jest.config.ts create mode 100644 packages/typegen/package.json rename packages/{cli => typegen}/src/__snapshots__/parseTypescript.test.ts.snap (100%) create mode 100644 packages/typegen/src/config.ts rename packages/{cli => typegen}/src/declareImport.test.ts (100%) rename packages/{cli => typegen}/src/generator.test.ts (98%) rename packages/{cli => typegen}/src/generator.ts (95%) create mode 100644 packages/typegen/src/index.ts rename packages/{cli => typegen}/src/parseTypescript.test.ts (100%) rename packages/{cli => typegen}/src/parseTypescript.ts (86%) rename packages/{cli => typegen}/src/types.test.ts (100%) rename packages/{cli => typegen}/src/types.ts (99%) rename packages/{cli => typegen}/src/util.ts (100%) create mode 100644 packages/typegen/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 5cb00163..51e4707f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "packages/*" ], "dependencies": { - "io-ts": "^2.1.2" + "io-ts": "^2.2.3" }, "devDependencies": { "@types/jest": "29.5.14", @@ -3218,6 +3218,10 @@ "resolved": "packages/runtime", "link": true }, + "node_modules/@pgtyped/typegen": { + "resolved": "packages/typegen", + "link": true + }, "node_modules/@pgtyped/wire": { "resolved": "packages/wire", "link": true @@ -12019,23 +12023,14 @@ "version": "2.4.3", "license": "MIT", "dependencies": { - "@pgtyped/parser": "^2.4.2", - "@pgtyped/query": "^2.4.2", - "@pgtyped/wire": "^2.4.2", - "camel-case": "^5.0.0", - "chalk": "^4.0.0", + "@pgtyped/typegen": "^2.4.2", "chokidar": "^4.0.0", "debug": "^4.1.1", - "fp-ts": "^2.5.3", - "fs-extra": "^11.3.1", "glob": "^11.0.0", "io-ts": "^2.2.20", "io-ts-reporters": "^2.0.1", - "minimatch": "^10.0.1", "nunjucks": "3.2.4", - "pascal-case": "^4.0.0", "piscina": "^5.0.0", - "tinypool": "^1.0.0", "ts-parse-database-url": "^1.0.3", "yargs": "^18.0.0" }, @@ -12045,8 +12040,7 @@ "devDependencies": { "@types/debug": "4.1.12", "@types/fs-extra": "11.0.4", - "@types/nunjucks": "^3.1.3", - "@types/yargs": "17.0.33" + "@types/nunjucks": "^3.1.3" }, "engines": { "node": ">=14.16" @@ -12059,6 +12053,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", "dependencies": { "readdirp": "^4.0.1" }, @@ -12073,6 +12068,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", "engines": { "node": ">= 14.18.0" }, @@ -12166,6 +12162,38 @@ "node": ">=14.16" } }, + "packages/typegen": { + "name": "@pgtyped/typegen", + "version": "2.4.2", + "license": "MIT", + "dependencies": { + "@pgtyped/parser": "^2.4.2", + "@pgtyped/query": "^2.4.2", + "@pgtyped/wire": "^2.4.2", + "camel-case": "^5.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "fp-ts": "^2.5.3", + "fs-extra": "^11.0.0", + "glob": "^11.0.0", + "io-ts": "^2.2.20", + "io-ts-reporters": "^2.0.1", + "nunjucks": "3.2.4", + "pascal-case": "^4.0.0", + "tinypool": "^1.0.0" + }, + "devDependencies": { + "@types/debug": "4.1.12", + "@types/fs-extra": "11.0.4", + "@types/nunjucks": "^3.1.3" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "typescript": "3.1 - 5" + } + }, "packages/wire": { "name": "@pgtyped/wire", "version": "2.4.2", @@ -14338,27 +14366,17 @@ "@pgtyped/cli": { "version": "file:packages/cli", "requires": { - "@pgtyped/parser": "^2.4.2", - "@pgtyped/query": "^2.4.2", - "@pgtyped/wire": "^2.4.2", + "@pgtyped/typegen": "^2.4.2", "@types/debug": "4.1.12", "@types/fs-extra": "11.0.4", "@types/nunjucks": "^3.1.3", - "@types/yargs": "17.0.33", - "camel-case": "^5.0.0", - "chalk": "^4.0.0", "chokidar": "^4.0.0", "debug": "^4.1.1", - "fp-ts": "^2.5.3", - "fs-extra": "11.3.1", "glob": "^11.0.0", "io-ts": "^2.2.20", "io-ts-reporters": "^2.0.1", - "minimatch": "^10.0.1", "nunjucks": "3.2.4", - "pascal-case": "^4.0.0", "piscina": "^5.0.0", - "tinypool": "^1.0.0", "ts-parse-database-url": "^1.0.3", "yargs": "^18.0.0" }, @@ -14430,6 +14448,28 @@ "esbuild": "^0.25.0" } }, + "@pgtyped/typegen": { + "version": "file:packages/typegen", + "requires": { + "@pgtyped/parser": "^2.4.2", + "@pgtyped/query": "^2.4.2", + "@pgtyped/wire": "^2.4.2", + "@types/debug": "4.1.12", + "@types/fs-extra": "11.0.4", + "@types/nunjucks": "^3.1.3", + "camel-case": "^5.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "fp-ts": "^2.5.3", + "fs-extra": "^11.0.0", + "glob": "^11.0.0", + "io-ts": "^2.2.20", + "io-ts-reporters": "^2.0.1", + "nunjucks": "3.2.4", + "pascal-case": "^4.0.0", + "tinypool": "^1.0.0" + } + }, "@pgtyped/wire": { "version": "file:packages/wire", "requires": { diff --git a/package.json b/package.json index 86f0f171..e3e2c9cb 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "typescript": "5.0.4" }, "dependencies": { - "io-ts": "^2.1.2" + "io-ts": "^2.2.3" }, "engines": { "node": ">=18" diff --git a/packages/cli/package.json b/packages/cli/package.json index 2c89caa6..7689551d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,31 +34,21 @@ "pgtyped": "lib/index.js" }, "dependencies": { - "@pgtyped/parser": "^2.4.2", - "@pgtyped/query": "^2.4.2", - "@pgtyped/wire": "^2.4.2", - "camel-case": "^5.0.0", - "chalk": "^4.0.0", - "chokidar": "^4.0.0", - "debug": "^4.1.1", - "fp-ts": "^2.5.3", - "fs-extra": "^11.0.0", - "glob": "^11.0.0", + "@pgtyped/typegen": "^2.4.2", + "piscina": "^5.0.0", + "yargs": "^18.0.0", "io-ts": "^2.2.20", "io-ts-reporters": "^2.0.1", - "minimatch": "^10.0.1", - "nunjucks": "3.2.4", - "pascal-case": "^4.0.0", - "piscina": "^5.0.0", - "tinypool": "^1.0.0", "ts-parse-database-url": "^1.0.3", - "yargs": "^18.0.0" + "chokidar": "^4.0.0", + "nunjucks": "3.2.4", + "glob": "^11.0.0", + "debug": "^4.1.1" }, "devDependencies": { "@types/debug": "4.1.12", "@types/fs-extra": "11.0.4", - "@types/nunjucks": "^3.1.3", - "@types/yargs": "17.0.33" + "@types/nunjucks": "^3.1.3" }, "peerDependencies": { "typescript": "3.1 - 5" diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index a94b1a1e..caa612a9 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -1,14 +1,17 @@ -/** @fileoverview Config file parser */ - import { Type } from '@pgtyped/query'; -import * as Either from 'fp-ts/lib/Either.js'; import * as t from 'io-ts'; -import { reporter } from 'io-ts-reporters'; -import { createRequire } from 'module'; +import * as Either from 'fp-ts/lib/Either.js'; import { isAbsolute, join } from 'path'; -import tls from 'tls'; +import { reporter } from 'io-ts-reporters'; import { DatabaseConfig, default as dbUrlModule } from 'ts-parse-database-url'; -import { TypeDefinition } from './types.js'; +import { + DbCodec, + DbConfig, + TypegenCodec, + TypegenConfig, + TypeDefinition, +} from '@pgtyped/typegen'; +import { createRequire } from 'module'; // module import hack const { default: parseDatabaseUri } = dbUrlModule as any; @@ -54,56 +57,19 @@ const configParser = t.type({ maxWorkerThreads: t.union([t.number, t.undefined]), transforms: t.array(TransformCodec), srcDir: t.string, - failOnError: t.union([t.boolean, t.undefined]), - camelCaseColumnNames: t.union([t.boolean, t.undefined]), - hungarianNotation: t.union([t.boolean, t.undefined]), - nonEmptyArrayParams: t.union([t.boolean, t.undefined]), dbUrl: 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]), - ssl: t.union([t.UnknownRecord, t.boolean, t.undefined]), - }), - t.undefined, - ]), - typesOverrides: t.union([ - t.record( - t.string, - t.union([ - t.string, - t.type({ - parameter: t.union([t.string, t.undefined]), - return: t.union([t.string, t.undefined]), - }), - ]), - ), - t.undefined, - ]), + db: t.union([DbCodec, t.undefined]), + ...TypegenCodec.props, }); export type IConfig = typeof configParser._O; -export interface ParsedConfig { - db: { - host: string; - user: string; - password: string | undefined; - dbName: string; - port: number; - ssl?: tls.ConnectionOptions | boolean; - }; +export interface ParsedConfig extends TypegenConfig { + db: DbConfig; maxWorkerThreads: number | undefined; - failOnError: boolean; - camelCaseColumnNames: boolean; - hungarianNotation: boolean; - nonEmptyArrayParams: boolean; transforms: IConfig['transforms']; srcDir: IConfig['srcDir']; - typesOverrides: Record>; + nonEmptyArrayParams: boolean; } function merge(base: T, ...overrides: Partial[]): T { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d70b4bef..ed8dc8b1 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -5,13 +5,14 @@ import { AsyncQueue } from '@pgtyped/wire'; import chokidar from 'chokidar'; import nun from 'nunjucks'; -import { Piscina as PiscinaPool } from 'piscina'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { parseConfig, ParsedConfig, TransformConfig } from './config.js'; +import { Piscina as PiscinaPool } from 'piscina'; +import { ParsedConfig, TransformConfig, parseConfig } from './config.js'; +import debugBase from 'debug'; import { TypedSqlTagTransformer } from './typedSqlTagTransformer.js'; import { TypescriptAndSqlTransformer } from './typescriptAndSqlTransformer.js'; -import { debug } from './util.js'; +export const debug = debugBase('pg-typegen'); // tslint:disable:no-console diff --git a/packages/cli/src/typedSqlTagTransformer.ts b/packages/cli/src/typedSqlTagTransformer.ts index b78e6971..c0c13946 100644 --- a/packages/cli/src/typedSqlTagTransformer.ts +++ b/packages/cli/src/typedSqlTagTransformer.ts @@ -2,17 +2,19 @@ import chokidar from 'chokidar'; import fs from 'fs-extra'; import { globSync } from 'glob'; import path from 'path'; -import { ParsedConfig, TSTypedSQLTagTransformConfig } from './config.js'; import { + TypeDeclarationSet, + getTypeDecsFnResult, + TypeAllocator, generateDeclarations, genTypedSQLOverloadFunctions, TSTypedQuery, - TypeDeclarationSet, -} from './generator.js'; +} from '@pgtyped/typegen'; import { TransformJob, WorkerPool } from './index.js'; -import { TypeAllocator } from './types.js'; -import { debug } from './util.js'; -import { getTypeDecsFnResult } from './worker.js'; +import { ParsedConfig, TSTypedSQLTagTransformConfig } from './config.js'; +import debugBase from 'debug'; + +const debug = debugBase('pg-typegen'); type TypedSQLTagTransformResult = TypeDeclarationSet | undefined; diff --git a/packages/cli/src/typescriptAndSqlTransformer.ts b/packages/cli/src/typescriptAndSqlTransformer.ts index 9ae344c5..86eaecbc 100644 --- a/packages/cli/src/typescriptAndSqlTransformer.ts +++ b/packages/cli/src/typescriptAndSqlTransformer.ts @@ -1,11 +1,13 @@ import chokidar from 'chokidar'; import { globSync } from 'glob'; import path from 'path'; -import { ParsedConfig, TransformConfig } from './config.js'; +import { minimatch } from 'minimatch'; import { TransformJob, WorkerPool } from './index.js'; -import { debug } from './util.js'; +import { ParsedConfig, TransformConfig } from './config.js'; import { processFileFnResult } from './worker.js'; -import { minimatch } from 'minimatch'; +import debugBase from 'debug'; + +const debug = debugBase('pg-typegen'); // tslint:disable:no-console diff --git a/packages/cli/src/worker.ts b/packages/cli/src/worker.ts index 240e74ad..b60b37db 100644 --- a/packages/cli/src/worker.ts +++ b/packages/cli/src/worker.ts @@ -1,15 +1,15 @@ -import { startup } from '@pgtyped/query'; -import { AsyncQueue } from '@pgtyped/wire'; +import path from 'path'; import fs from 'fs-extra'; import nun from 'nunjucks'; -import path from 'path'; import worker from 'piscina'; -import { ParsedConfig, TransformConfig } from './config.js'; +import { AsyncQueue } from '@pgtyped/wire'; +import { startup } from '@pgtyped/query'; import { + getTypes, generateDeclarationFile, - generateTypedecsFromFile, -} from './generator.js'; -import { TypeAllocator, TypeMapping, TypeScope } from './types.js'; + TransformMode, +} from '@pgtyped/typegen'; +import { ParsedConfig, TransformConfig } from './config.js'; // disable autoescape as it breaks windows paths // see https://github.com/adelsz/pgtyped/issues/519 for details @@ -19,10 +19,6 @@ let connected = false; const connection = new AsyncQueue(); const config: ParsedConfig = worker.workerData; -interface ExtendedParsedPath extends path.ParsedPath { - dir_base: string; -} - export type IWorkerResult = | { skipped: boolean; @@ -34,45 +30,10 @@ export type IWorkerResult = relativePath: string; }; -async function connectAndGetFileContents(fileName: string) { - if (!connected) { - await startup(config.db, connection); - connected = true; - } - - // last part fixes https://github.com/adelsz/pgtyped/issues/390 - 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)); - - 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, - types, - config, - ); +interface ExtendedParsedPath extends path.ParsedPath { + dir_base: string; } -export type getTypeDecsFnResult = ReturnType; - export async function processFile({ fileName, transform, @@ -90,9 +51,25 @@ export async function processFile({ decsFileName = path.resolve(ppath.dir, `${ppath.name}.${suffix}`); } + let mode: TransformMode; + if (transform.mode === 'ts-implicit') { + mode = { mode: 'ts-implicit', functionName: transform.functionName }; + } else { + mode = { mode: transform.mode }; + } + + if (!connected) { + await startup(config.db, connection); + connected = true; + } + let typeDecSet; try { - typeDecSet = await getTypeDecs({ fileName, transform }); + typeDecSet = await getTypes(fileName, { + connection, + config, + mode, + }); } catch (e) { return { error: e, @@ -102,7 +79,7 @@ export async function processFile({ const relativePath = path.relative(process.cwd(), decsFileName); if (typeDecSet.typedQueries.length > 0) { - const declarationFileContents = await generateDeclarationFile(typeDecSet); + const declarationFileContents = generateDeclarationFile(typeDecSet); const oldDeclarationFileContents = (await fs.pathExists(decsFileName)) ? await fs.readFile(decsFileName, { encoding: 'utf-8' }) : null; diff --git a/packages/example/config.json b/packages/example/config.json index 6dc053dd..a82dd57f 100644 --- a/packages/example/config.json +++ b/packages/example/config.json @@ -25,5 +25,6 @@ "category": { "return": "./src/customTypes.js#Category" } }, "srcDir": "./src/", - "dbUrl": "postgres://postgres:password@localhost/postgres" + "dbUrl": "postgres://postgres:postgres@localhost/postgres", + "maxWorkerThreads": 1 } diff --git a/packages/example/src/sql/index.ts b/packages/example/src/sql/index.ts index f8016d21..6b00238c 100644 --- a/packages/example/src/sql/index.ts +++ b/packages/example/src/sql/index.ts @@ -1,29 +1,6 @@ import { sql as sourceSql } from '@pgtyped/runtime'; -import type { Category } from '../customTypes.js'; -export type categoryArray = (Category)[]; -/** 'SqlSelectFromBooksWhereIdId' parameters type */ -export interface ISqlSelectFromBooksWhereIdIdParams { - id?: number | null | void; -} - -/** 'SqlSelectFromBooksWhereIdId' return type */ -export interface ISqlSelectFromBooksWhereIdIdResult { - author_id: number | null; - categories: categoryArray | null; - id: number; - name: string | null; - rank: number | null; -} - -/** 'SqlSelectFromBooksWhereIdId' query type */ -export interface ISqlSelectFromBooksWhereIdIdQuery { - params: ISqlSelectFromBooksWhereIdIdParams; - result: ISqlSelectFromBooksWhereIdIdResult; -} - -export function sql(s: `SELECT * FROM books WHERE id = $id`): ReturnType>; export function sql(s: string): unknown; export function sql(s: string): unknown { diff --git a/packages/typegen/.gitignore b/packages/typegen/.gitignore new file mode 100644 index 00000000..9ecc448c --- /dev/null +++ b/packages/typegen/.gitignore @@ -0,0 +1,3 @@ +node_modules +coverage +lib \ No newline at end of file diff --git a/packages/typegen/jest.config.ts b/packages/typegen/jest.config.ts new file mode 100644 index 00000000..bb75cce2 --- /dev/null +++ b/packages/typegen/jest.config.ts @@ -0,0 +1,24 @@ +import type { Config } from 'jest'; + +const config: Config = { + snapshotFormat: { + escapeString: true, + printBasicPrototype: true, + }, + roots: ['src'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, + preset: 'ts-jest/presets/default-esm', + testRegex: '\\.test\\.tsx?$', +}; + +export default config; diff --git a/packages/typegen/package.json b/packages/typegen/package.json new file mode 100644 index 00000000..c578362a --- /dev/null +++ b/packages/typegen/package.json @@ -0,0 +1,58 @@ +{ + "name": "@pgtyped/typegen", + "version": "2.4.2", + "type": "module", + "exports": { + ".": { + "import": "./lib/index.js", + "types": "./lib/index.d.ts" + } + }, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "files": [ + "lib" + ], + "engines": { + "node": ">=14.16" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/adelsz/pgtyped.git" + }, + "homepage": "https://github.com/adelsz/pgtyped", + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "NODE_OPTIONS='--experimental-vm-modules' jest", + "build": "tsc", + "check": "tsc --noEmit", + "watch": "tsc --watch --preserveWatchOutput" + }, + "dependencies": { + "@pgtyped/parser": "^2.4.2", + "@pgtyped/query": "^2.4.2", + "@pgtyped/wire": "^2.4.2", + "camel-case": "^5.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "fp-ts": "^2.5.3", + "fs-extra": "^11.0.0", + "glob": "^11.0.0", + "io-ts": "^2.2.20", + "io-ts-reporters": "^2.0.1", + "nunjucks": "3.2.4", + "pascal-case": "^4.0.0", + "tinypool": "^1.0.0" + }, + "devDependencies": { + "@types/debug": "4.1.12", + "@types/fs-extra": "11.0.4", + "@types/nunjucks": "^3.1.3" + }, + "peerDependencies": { + "typescript": "3.1 - 5" + } +} diff --git a/packages/cli/src/__snapshots__/parseTypescript.test.ts.snap b/packages/typegen/src/__snapshots__/parseTypescript.test.ts.snap similarity index 100% rename from packages/cli/src/__snapshots__/parseTypescript.test.ts.snap rename to packages/typegen/src/__snapshots__/parseTypescript.test.ts.snap diff --git a/packages/typegen/src/config.ts b/packages/typegen/src/config.ts new file mode 100644 index 00000000..6f2ea223 --- /dev/null +++ b/packages/typegen/src/config.ts @@ -0,0 +1,56 @@ +/** @fileoverview Config file parser */ + +import * as t from 'io-ts'; +import tls from 'tls'; +import { TypeDefinition } from './types.js'; + +export type TransformMode = + | { mode: 'sql' } + | { mode: 'ts' } + | { mode: 'ts-implicit'; functionName: string }; + +export const DbCodec = 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]), + ssl: t.union([t.UnknownRecord, t.boolean, t.undefined]), +}); + +export interface DbConfig { + host: string; + user: string; + password: string | undefined; + dbName: string; + port: number; + ssl?: tls.ConnectionOptions | boolean; +} + +export const TypegenCodec = t.type({ + failOnError: t.union([t.boolean, t.undefined]), + camelCaseColumnNames: t.union([t.boolean, t.undefined]), + hungarianNotation: t.union([t.boolean, t.undefined]), + nonEmptyArrayParams: t.union([t.boolean, t.undefined]), + typesOverrides: t.union([ + t.record( + t.string, + t.union([ + t.string, + t.type({ + parameter: t.union([t.string, t.undefined]), + return: t.union([t.string, t.undefined]), + }), + ]), + ), + t.undefined, + ]), +}); + +export interface TypegenConfig { + failOnError?: boolean; + camelCaseColumnNames?: boolean; + hungarianNotation?: boolean; + nonEmptyArrayParams?: boolean; + typesOverrides?: Record>; +} diff --git a/packages/cli/src/declareImport.test.ts b/packages/typegen/src/declareImport.test.ts similarity index 100% rename from packages/cli/src/declareImport.test.ts rename to packages/typegen/src/declareImport.test.ts diff --git a/packages/cli/src/generator.test.ts b/packages/typegen/src/generator.test.ts similarity index 98% rename from packages/cli/src/generator.test.ts rename to packages/typegen/src/generator.test.ts index 769af11e..3137192b 100644 --- a/packages/cli/src/generator.test.ts +++ b/packages/typegen/src/generator.test.ts @@ -2,7 +2,7 @@ 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 { TypegenConfig } from './config.js'; import { escapeComment, generateInterface, @@ -14,7 +14,7 @@ import { import { parseCode as parseTypeScriptFile } from './parseTypescript.js'; import { TypeAllocator, TypeMapping, TypeScope } from './types.js'; -const partialConfig = { hungarianNotation: true } as ParsedConfig; +const partialConfig = { hungarianNotation: true } as TypegenConfig; function parsedQuery( mode: ProcessingMode, @@ -311,7 +311,10 @@ export interface IDeleteUsersQuery { parsedQuery(mode, queryString), typeSource, types, - { camelCaseColumnNames: true, hungarianNotation: true } as ParsedConfig, + { + camelCaseColumnNames: true, + hungarianNotation: true, + } as TypegenConfig, ); const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime'; @@ -390,7 +393,10 @@ export interface IGetNotificationsQuery { parsedQuery(mode, queryString), typeSource, types, - { camelCaseColumnNames: true, hungarianNotation: true } as ParsedConfig, + { + camelCaseColumnNames: true, + hungarianNotation: true, + } as TypegenConfig, ); const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime'; @@ -469,7 +475,7 @@ export interface IGetNotificationsQuery { parsedQuery(mode, queryString), typeSource, types, - { nonEmptyArrayParams: true, hungarianNotation: true } as ParsedConfig, + { nonEmptyArrayParams: true, hungarianNotation: true } as TypegenConfig, ); const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime'; @@ -548,7 +554,7 @@ export interface IGetNotificationsQuery { parsedQuery(mode, queryString), typeSource, types, - { nonEmptyArrayParams: true, hungarianNotation: true } as ParsedConfig, + { nonEmptyArrayParams: true, hungarianNotation: true } as TypegenConfig, ); const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime'; @@ -625,7 +631,7 @@ export interface IGetNotificationsQuery { parsedQuery(mode, queryString), typeSource, types, - { nonEmptyArrayParams: true, hungarianNotation: true } as ParsedConfig, + { nonEmptyArrayParams: true, hungarianNotation: true } as TypegenConfig, ); const expected = `/** 'InsertNotifications' parameters type */ export interface IInsertNotificationsParams { diff --git a/packages/cli/src/generator.ts b/packages/typegen/src/generator.ts similarity index 95% rename from packages/cli/src/generator.ts rename to packages/typegen/src/generator.ts index bc060cee..a77ec353 100644 --- a/packages/cli/src/generator.ts +++ b/packages/typegen/src/generator.ts @@ -16,10 +16,10 @@ import { import { camelCase } from 'camel-case'; import { pascalCase } from 'pascal-case'; import path from 'path'; -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'; +import { TransformMode, TypegenConfig } from './config.js'; export enum ProcessingMode { SQL = 'sql-file', @@ -86,7 +86,7 @@ export async function queryToTypeDeclarations( parsedQuery: ParsedQuery, typeSource: TypeSource, types: TypeAllocator, - config: ParsedConfig, + typegenConfig: TypegenConfig, ): Promise { let queryData; let queryName; @@ -100,7 +100,7 @@ export async function queryToTypeDeclarations( const typeData = await typeSource(queryData); const interfaceName = pascalCase(queryName); - const interfacePrefix = config.hungarianNotation ? 'I' : ''; + const interfacePrefix = typegenConfig.hungarianNotation ? 'I' : ''; const typeError = 'errorCode' in typeData; const hasAnonymousColumns = @@ -113,7 +113,7 @@ export async function queryToTypeDeclarations( // tslint:disable:no-console if (typeError) { console.error('Error in query. Details: %o', typeData); - if (config.failOnError) { + if (typegenConfig.failOnError) { throw new Error( `Query "${queryName}" is invalid. Can't generate types.`, ); @@ -164,7 +164,7 @@ export async function queryToTypeDeclarations( } returnFieldTypes.push({ - fieldName: config.camelCaseColumnNames + fieldName: typegenConfig.camelCaseColumnNames ? camelCase(returnName) : returnName, fieldType: tsTypeName, @@ -173,7 +173,7 @@ export async function queryToTypeDeclarations( }); const formatArrayType = (type: string): string => { - if (config.nonEmptyArrayParams) { + if (typegenConfig.nonEmptyArrayParams) { // Only allow accepting an non-empty array. This prevents runtime errors. // For example, the following query will throw a runtime error if an empty // array is passed: @@ -313,18 +313,18 @@ export async function generateTypedecsFromFile( contents: string, fileName: string, connection: any, - transform: TransformConfig, types: TypeAllocator, - config: ParsedConfig, + mode: TransformMode, + typegenConfig: TypegenConfig, ): Promise { const typedQueries: TypedQuery[] = []; - const interfacePrefix = config.hungarianNotation ? 'I' : ''; + const interfacePrefix = typegenConfig.hungarianNotation ? 'I' : ''; const typeSource: TypeSource = (query) => getTypes(query, connection); const { queries, events } = - transform.mode === 'sql' + mode.mode === 'sql' ? parseSQLFile(contents) - : parseTypescriptFile(contents, fileName, transform); + : parseTypescriptFile(contents, fileName, mode); if (events.length > 0) { prettyPrintEvents(contents, events); @@ -339,13 +339,13 @@ export async function generateTypedecsFromFile( for (const queryAST of queries) { let typedQuery: TypedQuery; - if (transform.mode === 'sql') { + if (mode.mode === 'sql') { const sqlQueryAST = queryAST as SQLQueryAST; const result = await queryToTypeDeclarations( { ast: sqlQueryAST, mode: ProcessingMode.SQL }, typeSource, types, - config, + typegenConfig, ); typedQuery = { mode: 'sql' as const, @@ -372,7 +372,7 @@ export async function generateTypedecsFromFile( }, typeSource, types, - config, + typegenConfig, ); typedQuery = { mode: 'ts' as const, diff --git a/packages/typegen/src/index.ts b/packages/typegen/src/index.ts new file mode 100644 index 00000000..d2818898 --- /dev/null +++ b/packages/typegen/src/index.ts @@ -0,0 +1,70 @@ +import nun from 'nunjucks'; +import fs from 'fs-extra'; +import { DbConfig, TransformMode, TypegenConfig } from './config.js'; +import { TypeAllocator, TypeMapping, TypeScope } from './types.js'; +import { TypeDeclarationSet, generateTypedecsFromFile } from './generator.js'; +import { AsyncQueue } from '@pgtyped/wire'; +import { startup } from '@pgtyped/query'; + +nun.configure({ autoescape: false }); + +export { TypeDeclarationSet, generateDeclarationFile } from './generator.js'; +export { + DbCodec, + DbConfig, + TypegenCodec, + TypegenConfig, + TransformMode, +} from './config.js'; +export { + generateDeclarations, + genTypedSQLOverloadFunctions, + TSTypedQuery, +} from './generator.js'; +export { TypeAllocator, TypeDefinition } from './types.js'; + +export async function getTypes( + fileName: string, + { + connection, + config = {}, + mode, + }: { + connection: DbConfig | AsyncQueue; + config?: TypegenConfig; + mode?: TransformMode; + }, +): Promise { + mode = mode ?? (fileName.endsWith('.ts') ? { mode: 'ts' } : { mode: 'sql' }); + + let queue: AsyncQueue; + if (!(connection instanceof AsyncQueue)) { + queue = new AsyncQueue(); + await startup(connection, queue); + } else { + queue = connection; + } + + // last part fixes https://github.com/adelsz/pgtyped/issues/390 + const contents = fs.readFileSync(fileName).toString().replace(/\r\n/g, '\n'); + const types = new TypeAllocator(TypeMapping(config.typesOverrides)); + + if (mode.mode === 'sql') { + // Second parameter has no effect here, we could have used any value + types.use( + { name: 'PreparedQuery', from: '@pgtyped/runtime' }, + TypeScope.Return, + ); + } + + return generateTypedecsFromFile( + contents, + fileName, + queue, + types, + mode, + config, + ); +} + +export type getTypeDecsFnResult = ReturnType; diff --git a/packages/cli/src/parseTypescript.test.ts b/packages/typegen/src/parseTypescript.test.ts similarity index 100% rename from packages/cli/src/parseTypescript.test.ts rename to packages/typegen/src/parseTypescript.test.ts diff --git a/packages/cli/src/parseTypescript.ts b/packages/typegen/src/parseTypescript.ts similarity index 86% rename from packages/cli/src/parseTypescript.ts rename to packages/typegen/src/parseTypescript.ts index a09b38f1..a76b8322 100644 --- a/packages/cli/src/parseTypescript.ts +++ b/packages/typegen/src/parseTypescript.ts @@ -1,6 +1,6 @@ import { ParseEvent, parseTSQuery, TSQueryAST } from '@pgtyped/parser'; import ts from 'typescript'; -import { TransformConfig } from './config.js'; +import { TransformMode } from './config.js'; interface INode { queryName: string; @@ -11,19 +11,19 @@ export type TSParseResult = { queries: TSQueryAST[]; events: ParseEvent[] }; export function parseFile( sourceFile: ts.SourceFile, - transformConfig: TransformConfig | undefined, + mode: TransformMode | undefined, ): TSParseResult { const foundNodes: INode[] = []; parseNode(sourceFile); function parseNode(node: ts.Node) { if ( - transformConfig?.mode === 'ts-implicit' && + mode?.mode === 'ts-implicit' && node.kind === ts.SyntaxKind.CallExpression ) { const callNode = node as ts.CallExpression; const functionName = callNode.expression.getText(); - if (functionName === transformConfig.functionName) { + if (functionName === mode.functionName) { const queryName = callNode.parent.getChildren()[0].getText(); const queryText = callNode.arguments[0].getText().slice(1, -1).trim(); foundNodes.push({ @@ -70,7 +70,7 @@ export function parseFile( export const parseCode = ( fileContent: string, fileName = 'unnamed.ts', - transformConfig?: TransformConfig, + mode?: TransformMode, ) => { const sourceFile = ts.createSourceFile( fileName, @@ -78,5 +78,5 @@ export const parseCode = ( ts.ScriptTarget.ES2015, true, ); - return parseFile(sourceFile, transformConfig); + return parseFile(sourceFile, mode); }; diff --git a/packages/cli/src/types.test.ts b/packages/typegen/src/types.test.ts similarity index 100% rename from packages/cli/src/types.test.ts rename to packages/typegen/src/types.test.ts diff --git a/packages/cli/src/types.ts b/packages/typegen/src/types.ts similarity index 99% rename from packages/cli/src/types.ts rename to packages/typegen/src/types.ts index 4e61f43e..71c5ea7a 100644 --- a/packages/cli/src/types.ts +++ b/packages/typegen/src/types.ts @@ -168,7 +168,7 @@ export function declareImport( if (from.startsWith('.')) { from = path.relative(path.dirname(decsFileName), imports[0].from); - if (os.platform() === "win32") { + if (os.platform() === 'win32') { // make sure we use posix separators in TS import declarations (see #533) from = from.split(path.sep).join(path.posix.sep); } @@ -186,7 +186,7 @@ export function declareImport( // A type-only import can specify a default import or named bindings, but not both. lines.push(defaultImportDec); } else { - return `${defaultImportDec}\n` + return `${defaultImportDec}\n`; } } diff --git a/packages/cli/src/util.ts b/packages/typegen/src/util.ts similarity index 100% rename from packages/cli/src/util.ts rename to packages/typegen/src/util.ts diff --git a/packages/typegen/tsconfig.json b/packages/typegen/tsconfig.json new file mode 100644 index 00000000..8e870392 --- /dev/null +++ b/packages/typegen/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib/" /* Redirect output structure to the directory. */, + "rootDir": "./src/" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + }, + "exclude": ["lib", "**/*.test.ts", "jest.config.ts"] +} From 973d736b748a8335d86975e2b4bbd454664b4f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Cholewi=C5=84ski?= Date: Wed, 13 Aug 2025 09:51:46 +0200 Subject: [PATCH 2/2] close the socket on managed-connection usage --- packages/typegen/src/index.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/typegen/src/index.ts b/packages/typegen/src/index.ts index d2818898..64a35a94 100644 --- a/packages/typegen/src/index.ts +++ b/packages/typegen/src/index.ts @@ -36,9 +36,10 @@ export async function getTypes( }, ): Promise { mode = mode ?? (fileName.endsWith('.ts') ? { mode: 'ts' } : { mode: 'sql' }); + const manageConnection = !(connection instanceof AsyncQueue); let queue: AsyncQueue; - if (!(connection instanceof AsyncQueue)) { + if (manageConnection) { queue = new AsyncQueue(); await startup(connection, queue); } else { @@ -57,7 +58,7 @@ export async function getTypes( ); } - return generateTypedecsFromFile( + const result = await generateTypedecsFromFile( contents, fileName, queue, @@ -65,6 +66,12 @@ export async function getTypes( mode, config, ); + + if (manageConnection) { + queue.socket.end(); + } + + return result; } export type getTypeDecsFnResult = ReturnType;