Skip to content

Commit a10d1ff

Browse files
committed
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.
1 parent 1ed822d commit a10d1ff

File tree

4 files changed

+93
-4
lines changed

4 files changed

+93
-4
lines changed

docs-new/docs/cli.md

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ For a full list of options, see the [Configuration file format](#configuration-f
6161
"failOnError": false, // Whether to fail on a file processing error and abort generation (can be omitted - default is false)
6262
"camelCaseColumnNames": false, // convert to camelCase column names of result interface
6363
"nonEmptyArrayParams": false, // Whether the type for an array parameter should exclude empty arrays
64+
"optionalNullParams": true, // Whether nullable parameters are made optional
6465
"dbUrl": "postgres://user:password@host/database", // DB URL (optional - will be merged with db if provided)
6566
"db": {
6667
"dbName": "testdb", // DB name
@@ -94,6 +95,7 @@ Configuration file can be also be written in CommonJS format and default exporte
9495
| `dbUrl?` | `string` | A connection string to the database. Example: `postgres://user:password@host/database`. Overrides (merged) with `db` config. |
9596
| `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`_. |
9697
| `nonEmptyArrayParams?` | `boolean` | Whether the types for arrays parameters exclude empty arrays. This helps prevent runtime errors when accidentally providing empty input to a query. |
98+
| `optionalNullParams?` | `boolean` | Whether nullable parameters are automatically marked as optional. **Default:** `true` |
9799
| `typesOverrides?` | `Record<string, string>` | 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`._ |
98100
| `maxWorkerThreads` | `number` | The maximum number of worker threads to use for type generation. **The default is based on the number of available CPUs.** |
99101

packages/cli/src/config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const configParser = t.type({
5858
camelCaseColumnNames: t.union([t.boolean, t.undefined]),
5959
hungarianNotation: t.union([t.boolean, t.undefined]),
6060
nonEmptyArrayParams: t.union([t.boolean, t.undefined]),
61+
optionalNullParams: t.union([t.boolean, t.undefined]),
6162
dbUrl: t.union([t.string, t.undefined]),
6263
db: t.union([
6364
t.type({
@@ -101,6 +102,7 @@ export interface ParsedConfig {
101102
camelCaseColumnNames: boolean;
102103
hungarianNotation: boolean;
103104
nonEmptyArrayParams: boolean;
105+
optionalNullParams: boolean;
104106
transforms: IConfig['transforms'];
105107
srcDir: IConfig['srcDir'];
106108
typesOverrides: Record<string, Partial<TypeDefinition>>;
@@ -201,6 +203,7 @@ export function parseConfig(
201203
camelCaseColumnNames,
202204
hungarianNotation,
203205
nonEmptyArrayParams,
206+
optionalNullParams,
204207
typesOverrides,
205208
} = configObject as IConfig;
206209

@@ -246,6 +249,7 @@ export function parseConfig(
246249
camelCaseColumnNames: camelCaseColumnNames ?? false,
247250
hungarianNotation: hungarianNotation ?? true,
248251
nonEmptyArrayParams: nonEmptyArrayParams ?? false,
252+
optionalNullParams: optionalNullParams ?? true,
249253
typesOverrides: parsedTypesOverrides,
250254
maxWorkerThreads,
251255
};

packages/cli/src/generator.test.ts

+84-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import {
1414
import { parseCode as parseTypeScriptFile } from './parseTypescript.js';
1515
import { TypeAllocator, TypeMapping, TypeScope } from './types.js';
1616

17-
const partialConfig = { hungarianNotation: true } as ParsedConfig;
17+
// Note: You are required to add any default values to this config to make it
18+
// compatible with the default behavior. See cli/src/config.ts for values.
19+
const partialConfig = {
20+
hungarianNotation: true,
21+
optionalNullParams: true,
22+
} as ParsedConfig;
1823

1924
function parsedQuery(
2025
mode: ProcessingMode,
@@ -311,7 +316,7 @@ export interface IDeleteUsersQuery {
311316
parsedQuery(mode, queryString),
312317
typeSource,
313318
types,
314-
{ camelCaseColumnNames: true, hungarianNotation: true } as ParsedConfig,
319+
{ ...partialConfig, camelCaseColumnNames: true } as ParsedConfig,
315320
);
316321
const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime';
317322
@@ -331,6 +336,82 @@ export interface IGetNotificationsResult {
331336
typeCamelCase: PayloadType;
332337
}
333338
339+
/** 'GetNotifications' query type */
340+
export interface IGetNotificationsQuery {
341+
params: IGetNotificationsParams;
342+
result: IGetNotificationsResult;
343+
}\n\n`;
344+
expect(result).toEqual(expected);
345+
});
346+
347+
test(`Null parameters generation as required (${mode})`, async () => {
348+
const queryStringSQL = `
349+
/* @name GetNotifications */
350+
SELECT payload, type FROM notifications WHERE id = :userId;
351+
`;
352+
const queryStringTS = `
353+
const getNotifications = sql\`SELECT payload, type FROM notifications WHERE id = $userId\`;
354+
`;
355+
const queryString =
356+
mode === ProcessingMode.SQL ? queryStringSQL : queryStringTS;
357+
const mockTypes: IQueryTypes = {
358+
returnTypes: [
359+
{
360+
returnName: 'payload',
361+
columnName: 'payload',
362+
type: 'json',
363+
nullable: false,
364+
},
365+
{
366+
returnName: 'type',
367+
columnName: 'type',
368+
type: { name: 'PayloadType', enumValues: ['message', 'dynamite'] },
369+
nullable: false,
370+
},
371+
],
372+
paramMetadata: {
373+
params: ['uuid'],
374+
mapping: [
375+
{
376+
name: 'userId',
377+
type: ParameterTransform.Scalar,
378+
required: false,
379+
assignedIndex: 1,
380+
},
381+
],
382+
},
383+
};
384+
const typeSource = async (_: any) => mockTypes;
385+
const types = new TypeAllocator(TypeMapping());
386+
// Test out imports
387+
types.use(
388+
{ name: 'PreparedQuery', from: '@pgtyped/runtime' },
389+
TypeScope.Return,
390+
);
391+
const result = await queryToTypeDeclarations(
392+
parsedQuery(mode, queryString),
393+
typeSource,
394+
types,
395+
{ ...partialConfig, optionalNullParams: false } as ParsedConfig,
396+
);
397+
const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime';
398+
399+
export type PayloadType = 'dynamite' | 'message';
400+
401+
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };\n`;
402+
403+
expect(types.declaration('file.ts')).toEqual(expectedTypes);
404+
const expected = `/** 'GetNotifications' parameters type */
405+
export interface IGetNotificationsParams {
406+
userId: string | null | void;
407+
}
408+
409+
/** 'GetNotifications' return type */
410+
export interface IGetNotificationsResult {
411+
payload: Json;
412+
type: PayloadType;
413+
}
414+
334415
/** 'GetNotifications' query type */
335416
export interface IGetNotificationsQuery {
336417
params: IGetNotificationsParams;
@@ -390,7 +471,7 @@ export interface IGetNotificationsQuery {
390471
parsedQuery(mode, queryString),
391472
typeSource,
392473
types,
393-
{ camelCaseColumnNames: true, hungarianNotation: true } as ParsedConfig,
474+
{ ...partialConfig, camelCaseColumnNames: true } as ParsedConfig,
394475
);
395476
const expectedTypes = `import { PreparedQuery } from '@pgtyped/runtime';
396477

packages/cli/src/generator.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,9 @@ export async function queryToTypeDeclarations(
210210

211211
// Allow optional scalar parameters to be missing from parameters object
212212
const optional =
213-
param.type === ParameterTransform.Scalar && !param.required;
213+
param.type === ParameterTransform.Scalar &&
214+
!param.required &&
215+
config.optionalNullParams;
214216

215217
paramFieldTypes.push({
216218
optional,

0 commit comments

Comments
 (0)