Skip to content

Commit c7996b3

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 6806a93 commit c7996b3

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
@@ -60,6 +60,7 @@ For a full list of options, see the [Configuration file format](#configuration-f
6060
"srcDir": "./src/", // Directory to scan or watch for query files
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
63+
"optionalNullParams": true, // Whether nullable parameters are made optional
6364
"dbUrl": "postgres://user:password@host/database", // DB URL (optional - will be merged with db if provided)
6465
"db": {
6566
"dbName": "testdb", // DB name
@@ -92,6 +93,7 @@ Configuration file can be also be written in CommonJS format and default exporte
9293
| `failOnError?` | `boolean` | Whether to fail on a file processing error and abort generation. **Default:** `false` |
9394
| `dbUrl?` | `string` | A connection string to the database. Example: `postgres://user:password@host/database`. Overrides (merged) with `db` config. |
9495
| `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`_. |
96+
| `optionalNullParams?` | `boolean` | Whether nullable parameters are automatically marked as optional. **Default:** `true` |
9597
| `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`._ |
9698
| `maxWorkerThreads` | `number` | The maximum number of worker threads to use for type generation. **The default is based on the number of available CPUs.** |
9799

packages/cli/src/config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const configParser = t.type({
5757
failOnError: t.union([t.boolean, t.undefined]),
5858
camelCaseColumnNames: t.union([t.boolean, t.undefined]),
5959
hungarianNotation: t.union([t.boolean, t.undefined]),
60+
optionalNullParams: t.union([t.boolean, t.undefined]),
6061
dbUrl: t.union([t.string, t.undefined]),
6162
db: t.union([
6263
t.type({
@@ -99,6 +100,7 @@ export interface ParsedConfig {
99100
failOnError: boolean;
100101
camelCaseColumnNames: boolean;
101102
hungarianNotation: boolean;
103+
optionalNullParams: boolean;
102104
transforms: IConfig['transforms'];
103105
srcDir: IConfig['srcDir'];
104106
typesOverrides: Record<string, Partial<TypeDefinition>>;
@@ -198,6 +200,7 @@ export function parseConfig(
198200
failOnError,
199201
camelCaseColumnNames,
200202
hungarianNotation,
203+
optionalNullParams,
201204
typesOverrides,
202205
} = configObject as IConfig;
203206

@@ -242,6 +245,7 @@ export function parseConfig(
242245
failOnError: failOnError ?? false,
243246
camelCaseColumnNames: camelCaseColumnNames ?? false,
244247
hungarianNotation: hungarianNotation ?? true,
248+
optionalNullParams: optionalNullParams ?? true,
245249
typesOverrides: parsedTypesOverrides,
246250
maxWorkerThreads,
247251
};

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
@@ -192,7 +192,9 @@ export async function queryToTypeDeclarations(
192192

193193
// Allow optional scalar parameters to be missing from parameters object
194194
const optional =
195-
param.type === ParameterTransform.Scalar && !param.required;
195+
param.type === ParameterTransform.Scalar &&
196+
!param.required &&
197+
config.optionalNullParams;
196198

197199
paramFieldTypes.push({
198200
optional,

0 commit comments

Comments
 (0)