Skip to content

Commit 2b50986

Browse files
authored
Merge pull request #3519 from SeedCompany/fix-nulls
2 parents 669b8f1 + fc8c087 commit 2b50986

File tree

121 files changed

+515
-470
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+515
-470
lines changed

.eslintrc.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ const config = {
207207
{ fixMixedExportsWithInlineTypeSpecifier: true },
208208
],
209209
// TODO Enable this and fix errors (both types & logic changes will be needed)
210+
// Remaining errors probably need individual thought.
211+
// ESLint 9.24 allows bulk suppression,
212+
// which we can use to enable this, and handle those cases one by one.
210213
'@typescript-eslint/no-unnecessary-condition': 'off',
211214

212215
// Allow unused session while we migrate

src/common/date-filter.input.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Field, InputType } from '@nestjs/graphql';
2-
import { DateTime } from 'luxon';
2+
import { type DateTime } from 'luxon';
33
import { DateField, DateTimeField } from './luxon.graphql';
4-
import { CalendarDate } from './temporal';
4+
import { type CalendarDate } from './temporal';
55

66
@InputType({
77
description: 'A filter range designed for date fields',
@@ -11,25 +11,25 @@ export abstract class DateFilter {
1111
description: 'After this day',
1212
nullable: true,
1313
})
14-
after?: CalendarDate;
14+
after?: CalendarDate | null;
1515

1616
@DateField({
1717
description: 'After or equal to this day',
1818
nullable: true,
1919
})
20-
afterInclusive?: CalendarDate;
20+
afterInclusive?: CalendarDate | null;
2121

2222
@DateField({
2323
description: 'Before this day',
2424
nullable: true,
2525
})
26-
before?: CalendarDate;
26+
before?: CalendarDate | null;
2727

2828
@DateField({
2929
description: 'Before or equal to this day',
3030
nullable: true,
3131
})
32-
beforeInclusive?: CalendarDate;
32+
beforeInclusive?: CalendarDate | null;
3333

3434
@Field({ description: 'Whether the field is null or not', nullable: true })
3535
isNull?: boolean;
@@ -43,25 +43,25 @@ export abstract class DateTimeFilter {
4343
description: 'After this time',
4444
nullable: true,
4545
})
46-
after?: DateTime;
46+
after?: DateTime | null;
4747

4848
@DateTimeField({
4949
description: 'After or equal to this time',
5050
nullable: true,
5151
})
52-
afterInclusive?: DateTime;
52+
afterInclusive?: DateTime | null;
5353

5454
@DateTimeField({
5555
description: 'Before this time',
5656
nullable: true,
5757
})
58-
before?: DateTime;
58+
before?: DateTime | null;
5959

6060
@DateTimeField({
6161
description: 'Before or equal to this time',
6262
nullable: true,
6363
})
64-
beforeInclusive?: DateTime;
64+
beforeInclusive?: DateTime | null;
6565

6666
@Field({ description: 'Whether the field is null or not', nullable: true })
6767
isNull?: boolean;

src/common/exceptions/service-unavailable.exception.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export class TransientException extends ServerException {}
55

66
export class ServiceUnavailableException extends TransientException {
77
readonly status = HttpStatus.SERVICE_UNAVAILABLE;
8-
constructor(message: string, previous?: Error) {
8+
constructor(message?: string, previous?: Error) {
99
super(message ?? 'Service Unavailable', previous);
1010
}
1111
}

src/common/name-field.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ export const NameField = (options: NameFieldParams = {}) =>
5151
*/
5252
const InferredTypeOrStringField =
5353
(options: FieldOptions): PropertyDecorator | MethodDecorator =>
54-
(prototype, property, descriptor) => {
54+
(prototype, property, descriptorRaw) => {
5555
const propertyKey = property as string;
5656
const applyMetadataFn = () => {
57+
// fix linter false positive thinking it always exists, property decorators don't have it
58+
const descriptor = descriptorRaw as
59+
| TypedPropertyDescriptor<unknown>
60+
| undefined;
5761
const isResolver = !!descriptor;
5862
const isResolverMethod = !!descriptor?.value;
5963
const resolveType = (typeFn?: ReturnTypeFunc) =>

src/common/poll.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class PollResults<T> {
4848
/** Returns the unanimous vote, if there was one */
4949
get unanimous() {
5050
const all = this.sorted;
51-
return all.length === 1 ? all[0][0] : undefined;
51+
return all.length === 1 ? all[0]![0] : undefined;
5252
}
5353

5454
/** Returns all votes sorted by most voted first (ties are unaccounted for) */

src/common/required-when.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { groupBy } from '@seedcompany/common';
1+
import {
2+
asNonEmptyArray,
3+
groupBy,
4+
type NonEmptyArray,
5+
} from '@seedcompany/common';
26
import { createMetadataDecorator } from '@seedcompany/nest';
37
import { InputException } from './exceptions';
48
import { type ID } from './id-field';
@@ -50,7 +54,7 @@ RequiredWhen.calc = <TResourceStatic extends ResourceShape<any>>(
5054
const condition = RequiredWhenMetadata.get(resource, prop);
5155
return condition ? { ...condition, field: prop } : [];
5256
});
53-
const missing = conditions.flatMap((condition) => {
57+
const missingList = conditions.flatMap((condition) => {
5458
return condition.isEnabled(obj) &&
5559
(condition.isMissing?.(obj) ?? obj[condition.field] == null)
5660
? {
@@ -59,8 +63,10 @@ RequiredWhen.calc = <TResourceStatic extends ResourceShape<any>>(
5963
}
6064
: [];
6165
});
62-
if (missing.length > 0) {
63-
return new MissingRequiredFieldsException(res, { id: obj.id }, missing);
66+
const missing = asNonEmptyArray(missingList);
67+
if (missing) {
68+
const id = obj.id as ID;
69+
return new MissingRequiredFieldsException(res, { id }, missing);
6470
}
6571
return undefined;
6672
};
@@ -79,7 +85,7 @@ export class MissingRequiredFieldsException extends InputException {
7985
constructor(
8086
readonly resource: EnhancedResource<any>,
8187
readonly object: { id: ID },
82-
readonly missing: ReadonlyArray<{
88+
readonly missing: NonEmptyArray<{
8389
readonly field: string;
8490
readonly description: string;
8591
}>,

src/common/resource.dto.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CLASS_TYPE_METADATA, Field, InterfaceType } from '@nestjs/graphql';
22
import { type ClassType as ClassTypeVal } from '@nestjs/graphql/dist/enums/class-type.enum.js';
33
import {
4+
asNonEmptyArray,
45
cached,
56
type FnLike,
67
mapValues,
@@ -109,7 +110,12 @@ export class EnhancedResource<T extends ResourceShape<any>> {
109110
>();
110111

111112
static resolve(ref: ResourceLike) {
112-
if (ref && typeof ref !== 'string') {
113+
// Safety check; since this very dynamic code, it is very possible the types are lying.
114+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
115+
if (ref == null) {
116+
throw new ServerException('Resource reference is actually null');
117+
}
118+
if (typeof ref !== 'string') {
113119
return EnhancedResource.of(ref);
114120
}
115121
if (!EnhancedResource.resourcesHost) {
@@ -316,7 +322,7 @@ export class EnhancedResource<T extends ResourceShape<any>> {
316322
const declared = DbLabel.getOwn(cls);
317323
return declared ? [...declared] : [cls.name];
318324
});
319-
return [...new Set([...labels, 'BaseNode'])];
325+
return asNonEmptyArray([...new Set([...labels, 'BaseNode'])])!;
320326
}
321327
get dbLabel() {
322328
return this.dbLabels[0];

src/common/retry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ export type RetryOptions = Merge<
3030

3131
const parseOptions = (options: RetryOptions = {}): Options => ({
3232
...options,
33-
maxRetryTime: options?.maxRetryTime
33+
maxRetryTime: options.maxRetryTime
3434
? Duration.from(options.maxRetryTime).toMillis()
3535
: undefined,
36-
minTimeout: options?.minTimeout
36+
minTimeout: options.minTimeout
3737
? Duration.from(options.minTimeout).toMillis()
3838
: undefined,
39-
maxTimeout: options?.maxTimeout
39+
maxTimeout: options.maxTimeout
4040
? Duration.from(options.maxTimeout).toMillis()
4141
: undefined,
4242
});

src/common/search-camel-case.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { startCase } from 'lodash';
2020
export function searchCamelCase<T extends string>(
2121
items: Iterable<T>,
2222
needle: string,
23-
) {
23+
): readonly T[] {
2424
const itemArr = [...items];
2525
const collator = new Intl.Collator('en');
2626
const fuzzy = new Fuzzy({
@@ -41,22 +41,22 @@ export function searchCamelCase<T extends string>(
4141
.sort(
4242
(ia, ib) =>
4343
// most contiguous chars matched
44-
chars[ib] - chars[ia] ||
44+
chars[ib]! - chars[ia]! ||
4545
// least char intra-fuzz (most contiguous)
46-
intraIns[ia] - intraIns[ib] ||
46+
intraIns[ia]! - intraIns[ib]! ||
4747
// earliest start of match
48-
start[ia] - start[ib] ||
48+
start[ia]! - start[ib]! ||
4949
// shortest match first
50-
haystack[idx[ia]].length - haystack[idx[ib]].length ||
50+
haystack[idx[ia]!]!.length - haystack[idx[ib]!]!.length ||
5151
// most prefix bounds, boosted by full term matches
52-
terms[ib] +
53-
interLft2[ib] +
54-
0.5 * interLft1[ib] -
55-
(terms[ia] + interLft2[ia] + 0.5 * interLft1[ia]) ||
52+
terms[ib]! +
53+
interLft2[ib]! +
54+
0.5 * interLft1[ib]! -
55+
(terms[ia]! + interLft2[ia]! + 0.5 * interLft1[ia]!) ||
5656
// the highest density of match (the least term inter-fuzz)
57-
interIns[ia] - interIns[ib] ||
57+
interIns[ia]! - interIns[ib]! ||
5858
// alphabetic
59-
collator.compare(haystack[idx[ia]], haystack[idx[ib]]),
59+
collator.compare(haystack[idx[ia]!]!, haystack[idx[ib]!]!),
6060
);
6161
},
6262
});
@@ -74,6 +74,6 @@ export function searchCamelCase<T extends string>(
7474
return [];
7575
}
7676

77-
const results = order.map((idx) => itemArr[indexes[idx]]);
77+
const results = order.map((idx) => itemArr[indexes[idx]!]!);
7878
return results;
7979
}

src/common/variant.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const VariantInputField = <
9090
? resource.Variants.find((v) => v.key === value)
9191
: value;
9292
const defaultVariant = many
93-
? (defaultValue as any[])?.map(resolveVariant)
93+
? (defaultValue as any[] | undefined)?.map(resolveVariant)
9494
: resolveVariant(defaultValue as VariantOf<Res>);
9595

9696
return applyDecorators(

0 commit comments

Comments
 (0)