diff --git a/docs-new/docs/cli.md b/docs-new/docs/cli.md index 0ba766a1..ecd132f3 100644 --- a/docs-new/docs/cli.md +++ b/docs-new/docs/cli.md @@ -60,6 +60,7 @@ For a full list of options, see the [Configuration file format](#configuration-f "srcDir": "./src/", // Directory to scan or watch for query files "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 + "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 @@ -92,6 +93,7 @@ Configuration file can be also be written in CommonJS format and default exporte | `failOnError?` | `boolean` | Whether to fail on a file processing error and abort generation. **Default:** `false` | | `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`_. | +| `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 80bee2d2..9724fe11 100644 --- a/packages/cli/src/config.ts +++ b/packages/cli/src/config.ts @@ -57,6 +57,7 @@ const configParser = t.type({ failOnError: t.union([t.boolean, t.undefined]), camelCaseColumnNames: t.union([t.boolean, t.undefined]), hungarianNotation: 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({ @@ -99,6 +100,7 @@ export interface ParsedConfig { failOnError: boolean; camelCaseColumnNames: boolean; hungarianNotation: boolean; + optionalNullParams: boolean; transforms: IConfig['transforms']; srcDir: IConfig['srcDir']; typesOverrides: Record>; @@ -198,6 +200,7 @@ export function parseConfig( failOnError, camelCaseColumnNames, hungarianNotation, + optionalNullParams, typesOverrides, } = configObject as IConfig; @@ -242,6 +245,7 @@ export function parseConfig( failOnError: failOnError ?? false, camelCaseColumnNames: camelCaseColumnNames ?? false, hungarianNotation: hungarianNotation ?? true, + optionalNullParams: optionalNullParams ?? true, typesOverrides: parsedTypesOverrides, maxWorkerThreads, }; diff --git a/packages/cli/src/generator.test.ts b/packages/cli/src/generator.test.ts index 5e8e6741..b473c4cd 100644 --- a/packages/cli/src/generator.test.ts +++ b/packages/cli/src/generator.test.ts @@ -331,6 +331,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, + { optionalNullParams: false, hungarianNotation: true } 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; diff --git a/packages/cli/src/generator.ts b/packages/cli/src/generator.ts index 45a3efb5..5e0f2847 100644 --- a/packages/cli/src/generator.ts +++ b/packages/cli/src/generator.ts @@ -192,7 +192,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,