diff --git a/docs-new/docs/cli.md b/docs-new/docs/cli.md index 2a5a637c..30667c81 100644 --- a/docs-new/docs/cli.md +++ b/docs-new/docs/cli.md @@ -61,6 +61,7 @@ For a full list of options, see the [Configuration file format](#configuration-f "failOnError": false, // Whether to fail on a file processing error and abort generation (can be omitted - default is false) "camelCaseColumnNames": false, // convert to camelCase column names of result interface "nonEmptyArrayParams": false, // Whether the type for an array parameter should exclude empty arrays + "optionalNullParams": true, // Whether nullable parameters are made optional "dbUrl": "postgres://user:password@host/database", // DB URL (optional - will be merged with db if provided) "db": { "dbName": "testdb", // DB name @@ -94,6 +95,7 @@ Configuration file can be also be written in CommonJS format and default exporte | `dbUrl?` | `string` | A connection string to the database. Example: `postgres://user:password@host/database`. Overrides (merged) with `db` config. | | `camelCaseColumnNames?` | `boolean` | Whether to convert column names to camelCase. _Note that this only coverts the types. You need to do this at runtime independently using a library like `pg-camelcase`_. | | `nonEmptyArrayParams?` | `boolean` | Whether the types for arrays parameters exclude empty arrays. This helps prevent runtime errors when accidentally providing empty input to a query. | +| `optionalNullParams?` | `boolean` | Whether nullable parameters are automatically marked as optional. **Default:** `true` | | `typesOverrides?` | `Record` | A map of type overrides. Similarly to `camelCaseColumnNames`, this only affects the types. _You need to do this at runtime independently using a library like `pg-types`._ | | `maxWorkerThreads` | `number` | The maximum number of worker threads to use for type generation. **The default is based on the number of available CPUs.** | diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts index a94b1a1e..4b412d6b 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -58,6 +58,7 @@ const configParser = t.type({ camelCaseColumnNames: t.union([t.boolean, t.undefined]), hungarianNotation: t.union([t.boolean, t.undefined]), nonEmptyArrayParams: t.union([t.boolean, t.undefined]), + optionalNullParams: t.union([t.boolean, t.undefined]), dbUrl: t.union([t.string, t.undefined]), db: t.union([ t.type({ @@ -101,6 +102,7 @@ export interface ParsedConfig { camelCaseColumnNames: boolean; hungarianNotation: boolean; nonEmptyArrayParams: boolean; + optionalNullParams: boolean; transforms: IConfig['transforms']; srcDir: IConfig['srcDir']; typesOverrides: Record>; @@ -201,6 +203,7 @@ export function parseConfig( camelCaseColumnNames, hungarianNotation, nonEmptyArrayParams, + optionalNullParams, typesOverrides, } = configObject as IConfig; @@ -246,6 +249,7 @@ export function parseConfig( camelCaseColumnNames: camelCaseColumnNames ?? false, hungarianNotation: hungarianNotation ?? true, nonEmptyArrayParams: nonEmptyArrayParams ?? false, + optionalNullParams: optionalNullParams ?? true, typesOverrides: parsedTypesOverrides, maxWorkerThreads, }; diff --git a/packages/cli/src/generator.test.ts b/packages/cli/src/generator.test.ts index 769af11e..ab10c78b 100644 --- a/packages/cli/src/generator.test.ts +++ b/packages/cli/src/generator.test.ts @@ -14,7 +14,12 @@ import { import { parseCode as parseTypeScriptFile } from './parseTypescript.js'; import { TypeAllocator, TypeMapping, TypeScope } from './types.js'; -const partialConfig = { hungarianNotation: true } as ParsedConfig; +// Note: You are required to add any default values to this config to make it +// compatible with the default behavior. See cli/src/config.ts for values. +const partialConfig = { + hungarianNotation: true, + optionalNullParams: true, +} as ParsedConfig; function parsedQuery( mode: ProcessingMode, @@ -311,7 +316,7 @@ export interface IDeleteUsersQuery { parsedQuery(mode, queryString), typeSource, types, - { camelCaseColumnNames: true, hungarianNotation: true } as ParsedConfig, + { ...partialConfig, camelCaseColumnNames: true } as ParsedConfig, ); const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime'; @@ -331,6 +336,82 @@ export interface IGetNotificationsResult { typeCamelCase: PayloadType; } +/** 'GetNotifications' query type */ +export interface IGetNotificationsQuery { + params: IGetNotificationsParams; + result: IGetNotificationsResult; +}\n\n`; + expect(result).toEqual(expected); + }); + + test(`Null parameters generation as required (${mode})`, async () => { + const queryStringSQL = ` + /* @name GetNotifications */ + SELECT payload, type FROM notifications WHERE id = :userId; + `; + const queryStringTS = ` + const getNotifications = sql\`SELECT payload, type FROM notifications WHERE id = $userId\`; + `; + const queryString = + mode === ProcessingMode.SQL ? queryStringSQL : queryStringTS; + const mockTypes: IQueryTypes = { + returnTypes: [ + { + returnName: 'payload', + columnName: 'payload', + type: 'json', + nullable: false, + }, + { + returnName: 'type', + columnName: 'type', + type: { name: 'PayloadType', enumValues: ['message', 'dynamite'] }, + nullable: false, + }, + ], + paramMetadata: { + params: ['uuid'], + mapping: [ + { + name: 'userId', + type: ParameterTransform.Scalar, + required: false, + assignedIndex: 1, + }, + ], + }, + }; + const typeSource = async (_: any) => mockTypes; + const types = new TypeAllocator(TypeMapping()); + // Test out imports + types.use( + { name: 'PreparedQuery', from: '@pgtyped/runtime' }, + TypeScope.Return, + ); + const result = await queryToTypeDeclarations( + parsedQuery(mode, queryString), + typeSource, + types, + { ...partialConfig, optionalNullParams: false } as ParsedConfig, + ); + const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime'; + +export type PayloadType = 'dynamite' | 'message'; + +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };\n`; + + expect(types.declaration('file.ts')).toEqual(expectedTypes); + const expected = `/** 'GetNotifications' parameters type */ +export interface IGetNotificationsParams { + userId: string | null | void; +} + +/** 'GetNotifications' return type */ +export interface IGetNotificationsResult { + payload: Json; + type: PayloadType; +} + /** 'GetNotifications' query type */ export interface IGetNotificationsQuery { params: IGetNotificationsParams; @@ -390,7 +471,7 @@ export interface IGetNotificationsQuery { parsedQuery(mode, queryString), typeSource, types, - { camelCaseColumnNames: true, hungarianNotation: true } as ParsedConfig, + { ...partialConfig, camelCaseColumnNames: true } as ParsedConfig, ); const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime'; diff --git a/packages/cli/src/generator.ts b/packages/cli/src/generator.ts index bc060cee..3b861f7d 100644 --- a/packages/cli/src/generator.ts +++ b/packages/cli/src/generator.ts @@ -210,7 +210,9 @@ export async function queryToTypeDeclarations( // Allow optional scalar parameters to be missing from parameters object const optional = - param.type === ParameterTransform.Scalar && !param.required; + param.type === ParameterTransform.Scalar && + !param.required && + config.optionalNullParams; paramFieldTypes.push({ optional,