From a10d1ffa9ba87dd85e9fc9f626d4c68992d69394 Mon Sep 17 00:00:00 2001 From: Patrick Nappa Date: Tue, 11 Jun 2024 10:38:49 +1000 Subject: [PATCH] feat!: Allow non-optional generation of params with optionalNullParams This feature allows specifying whether nullable params should be generated as optional via the config. The default behaviour stays the same, so this feature is backwards compatible. --- docs-new/docs/cli.md | 2 + packages/cli/src/config.ts | 4 ++ packages/cli/src/generator.test.ts | 87 ++++++++++++++++++++++++++++-- packages/cli/src/generator.ts | 4 +- 4 files changed, 93 insertions(+), 4 deletions(-) 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,