Skip to content

Commit b3e362e

Browse files
authored
Merge pull request #3517 from SeedCompany/refactor/optional-fields
2 parents ce65d2e + 1730f09 commit b3e362e

File tree

10 files changed

+77
-52
lines changed

10 files changed

+77
-52
lines changed

src/common/list-field.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { applyDecorators } from '@nestjs/common';
22
import { ArrayNotEmpty } from 'class-validator';
3-
import { OptionalField, type OptionalFieldOptions } from './optional-field';
3+
import {
4+
OptionalField,
5+
type OptionalFieldOptions,
6+
withDefaultTransform,
7+
} from './optional-field';
48

59
export type ListFieldOptions = OptionalFieldOptions & {
610
/**
@@ -14,11 +18,12 @@ export const ListField = (typeFn: () => any, options: ListFieldOptions) =>
1418
OptionalField(() => [typeFn()], {
1519
optional: false,
1620
...options,
17-
transform: (value) => {
21+
transform: withDefaultTransform(options.transform, (prev) => (raw) => {
22+
const value = prev(raw);
1823
let out = value ? [...new Set(value)] : value;
1924
out = options.empty === 'omit' && out.length === 0 ? undefined : out;
20-
return options.transform ? options.transform(out) : out;
21-
},
25+
return out;
26+
}),
2227
}),
2328
...(options.empty === 'deny' ? [ArrayNotEmpty()] : []),
2429
);

src/common/optional-field.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,19 @@ export type OptionalFieldOptions = FieldOptions & {
1313
* If true, values can be omitted/undefined but not null.
1414
*/
1515
optional?: boolean;
16-
transform?: (value: any) => unknown;
16+
transform?: TransformerLink;
1717
};
1818

19+
type TransformerLink = (prev: Transformer) => Transformer;
20+
type Transformer<I = any, O = any> = (value: I) => O;
21+
export const withDefaultTransform =
22+
(
23+
input: TransformerLink | undefined,
24+
wrapping: TransformerLink,
25+
): TransformerLink =>
26+
(base) =>
27+
input?.(wrapping(base)) ?? wrapping(base);
28+
1929
/**
2030
* A field that is optional/omissible/can be undefined.
2131
* Whether it can be explicitly null is based on `nullable`.
@@ -30,19 +40,27 @@ export function OptionalField(
3040
export function OptionalField(...args: any) {
3141
const typeFn: (() => any) | undefined =
3242
typeof args[0] === 'function' ? (args[0] as () => any) : undefined;
33-
const options: OptionalFieldOptions | undefined =
34-
typeof args[0] === 'function' ? args[1] : args[0];
35-
const opts = {
43+
const options: OptionalFieldOptions =
44+
(typeof args[0] === 'function' ? args[1] : args[0]) ?? {};
45+
const nilIn =
46+
options.nullable === 'items' && options.optional
47+
? 'itemsAndList'
48+
: options.nullable ?? options.optional ?? true;
49+
const nullOut = !!options.nullable && options.nullable !== 'items';
50+
const schemaOptions = {
3651
...options,
37-
nullable: options?.nullable ?? options?.optional ?? true,
52+
nullable: nilIn,
53+
};
54+
const defaultTransformer: Transformer = (value) => {
55+
if (value === null && !nullOut) {
56+
return undefined;
57+
}
58+
return value;
3859
};
60+
const finalTransformer =
61+
options.transform?.(defaultTransformer) ?? defaultTransformer;
3962
return applyDecorators(
40-
typeFn ? Field(typeFn, opts) : Field(opts),
41-
Transform(({ value }) => {
42-
if (!options?.nullable && (options?.optional ?? true) && value == null) {
43-
return undefined;
44-
}
45-
return options?.transform ? options.transform(value) : value;
46-
}),
63+
typeFn ? Field(typeFn, schemaOptions) : Field(schemaOptions),
64+
Transform(({ value }) => finalTransformer(value)),
4765
);
4866
}

src/common/rich-text.scalar.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { applyDecorators } from '@nestjs/common';
22
import { ObjectType } from '@nestjs/graphql';
3-
import { setToStringTag } from '@seedcompany/common';
3+
import { type Nil, setToStringTag } from '@seedcompany/common';
44
import { markSkipClassTransformation } from '@seedcompany/nest';
55
import { IsObject } from 'class-validator';
66
import { createHash } from 'crypto';
@@ -82,23 +82,19 @@ export const RichTextField = (options?: OptionalFieldOptions) =>
8282
OptionalField(() => RichTextScalar, {
8383
optional: false,
8484
...options,
85-
transform: (value) => {
86-
const doc = RichTextDocument.fromMaybe(value);
87-
if (doc) {
88-
return doc;
85+
transform: (prev) => (value) => {
86+
const doc: RichTextDocument | Nil = prev(
87+
RichTextDocument.fromMaybe(value),
88+
);
89+
if (doc == null && !options?.nullable && !options?.optional) {
90+
// Should never _really_ get here.
91+
// UI should understand & send null instead of an empty document.
92+
// Would prefer this to be done with validators.
93+
// But I believe this needs `null`s to be validated.
94+
// skipMissingProperties -> skipUndefinedProperties
95+
throw new InputException('RichText must be given');
8996
}
90-
if (options?.nullable) {
91-
return null;
92-
}
93-
if (options?.optional) {
94-
return undefined;
95-
}
96-
// Should never _really_ get here.
97-
// UI should understand & send null instead of an empty document.
98-
// Would prefer this to be done with validators.
99-
// But I believe this needs to `null`s to be validated.
100-
// skipMissingProperties -> skipUndefinedProperties
101-
throw new InputException('RichText must be given');
97+
return doc;
10298
},
10399
}),
104100
IsObject(),

src/common/sensitivity.enum.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export const SensitivitiesFilterField = (options?: ListFieldOptions) =>
2727
...options,
2828
optional: true,
2929
empty: 'omit',
30-
transform: (value) =>
30+
transform: (prev) => (raw) => {
31+
const value = prev(raw);
3132
// If given all options, there is no need to filter
32-
!value || value.length === Sensitivity.values.size ? undefined : value,
33+
return !value || value.length === Sensitivity.values.size
34+
? undefined
35+
: value;
36+
},
3337
});

src/components/changeset/dto/changeset.args.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export class ChangesetIds {
1111
@IdField()
1212
id: ID;
1313

14-
@IdField({ nullable: true })
15-
changeset: ID;
14+
@IdField({ optional: true })
15+
changeset?: ID;
1616
}
1717

1818
export type IdsAndView = ChangesetIds & { view: ObjectView };
@@ -25,10 +25,10 @@ export class ObjectViewPipe implements PipeTransform {
2525
constructor(private readonly validator: ValidationPipe) {}
2626

2727
async transform(input: ChangesetIds) {
28-
const { id, changeset } = await this.validator.transform(input, {
28+
const { id, changeset } = (await this.validator.transform(input, {
2929
metatype: ChangesetIds,
3030
type: 'body',
31-
});
31+
})) as ChangesetIds;
3232
const view: ObjectView = changeset ? { changeset } : { active: true };
3333
return { id, changeset, view };
3434
}

src/components/partnership/dto/list-partnership.dto.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ export abstract class PartnershipFilters {
2020
@ListField(() => PartnerType, {
2121
optional: true,
2222
empty: 'omit',
23-
transform: (value) =>
23+
transform: (prev) => (raw) => {
24+
const value = prev(raw);
2425
// If given all options, there is no need to filter
25-
!value || value.length === PartnerType.values.size ? undefined : value,
26+
return !value || value.length === PartnerType.values.size
27+
? undefined
28+
: value;
29+
},
2630
})
2731
readonly types?: readonly PartnerType[];
2832
}

src/components/progress-report/variance-explanation/variance-explanation.dto.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ArrayMaxSize, IsIn } from 'class-validator';
33
import {
44
type ID,
55
IdField,
6+
ListField,
67
type RichTextDocument,
78
RichTextField,
89
SecuredRichTextNullable,
@@ -36,7 +37,10 @@ export abstract class ProgressReportVarianceExplanationInput {
3637
})
3738
readonly report: ID;
3839

39-
@Field(() => [String], { nullable: true })
40+
@ListField(() => String, {
41+
optional: true,
42+
transform: (prev) => (value) => value === null ? [] : prev(value),
43+
})
4044
@IsIn([...ReasonOptions.instance.all], {
4145
each: true,
4246
message: 'Reason is not one of our available choices',

src/components/progress-report/variance-explanation/variance-explanation.service.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,7 @@ export class ProgressReportVarianceExplanationService {
3737
}
3838

3939
const changes = this.repo.getActualChanges(existing, {
40-
reasons:
41-
input.reasons === undefined
42-
? existing.reasons.value
43-
: input.reasons === null
44-
? []
45-
: input.reasons,
40+
reasons: input.reasons,
4641
comments: input.comments,
4742
});
4843
if (Object.keys(changes).length === 0) {

src/components/progress-summary/dto/progress-summary-filters.dto.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ import { ScheduleStatus } from './schedule-status.enum';
66
@InputType()
77
export abstract class ProgressSummaryFilters {
88
@ListField(() => ScheduleStatus, {
9-
nullable: 'itemsAndList',
9+
optional: true,
10+
nullable: 'items',
1011
description: stripIndent`
1112
Filter by schedule status.
1213
- \`[X, Y]\` will allow summaries with either X or Y status.
1314
- \`[null, X]\` will allow missing summaries or summaries with X status.
1415
- \`[null]\` will filter to only missing summaries.
1516
- \`null\` and \`[]\` will be ignored.
1617
`,
18+
empty: 'omit',
1719
})
1820
readonly scheduleStatus?: ReadonlyArray<ScheduleStatus | null>;
1921
}

src/components/progress-summary/progress-summary.repository.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,6 @@ export const progressSummaryFilters = filter.define(
108108
{
109109
scheduleStatus: ({ value, query }) => {
110110
const status = setOf(value);
111-
if (status.size === 0) {
112-
return undefined;
113-
}
114111
if (status.size === 1 && status.has(null)) {
115112
return query.where(new WhereExp('node IS NULL'));
116113
}

0 commit comments

Comments
 (0)