Skip to content

Commit 7fa60b3

Browse files
Block usage of enum types but allow them as relaxed types (#17)
1 parent 32e3352 commit 7fa60b3

File tree

12 files changed

+704
-29
lines changed

12 files changed

+704
-29
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ This changelog documents the changes between release versions.
77
## [Unreleased]
88
Changes to be included in the next upcoming release
99

10+
- Improved error messages when unsupported enum types or unions of literal types are found, and allow these types to be used in relaxed types mode ([#17](https://github.com/hasura/ndc-nodejs-lambda/pull/17))
11+
1012
## [1.1.0] - 2024-02-26
1113
- Updated to [NDC TypeScript SDK v4.2.0](https://github.com/hasura/ndc-sdk-typescript/releases/tag/v4.2.0) to include OpenTelemetry improvements. Traced spans should now appear in the Hasura Console
1214
- Custom OpenTelemetry trace spans can now be emitted by creating an OpenTelemetry tracer and using it with `sdk.withActiveSpan` ([#16](https://github.com/hasura/ndc-nodejs-lambda/pull/16))

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ These types are unsupported as function parameter types or return types for func
149149
* [Function types](https://www.typescriptlang.org/docs/handbook/2/functions.html#function-type-expressions) - function types can't be accepted as arguments or returned as values
150150
* [`void`](https://www.typescriptlang.org/docs/handbook/2/functions.html#void), [`object`](https://www.typescriptlang.org/docs/handbook/2/functions.html#object), [`unknown`](https://www.typescriptlang.org/docs/handbook/2/functions.html#unknown), [`never`](https://www.typescriptlang.org/docs/handbook/2/functions.html#never), [`any`](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#any) types - to accept and return arbitrary JSON, use `sdk.JSONValue` instead
151151
* `null` and `undefined` - unless used in a union with a single other type
152+
* [Enum types](https://www.typescriptlang.org/docs/handbook/enums.html)
152153

153154
### Relaxed Types
154155
"Relaxed types" are types that are otherwise unsupported, but instead of being rejected are instead converted into opaque custom scalar types. These scalar types are entirely unvalidated when used as input (ie. the caller of the function can send arbitrary JSON values), making it incumbent on the function itself to ensure the incoming value for that relaxed type actually matches its type. Because relaxed types are represented as custom scalar types, in GraphQL you will be unable to select into the type, if it is an object, and will only be able to select the whole thing.
@@ -161,6 +162,7 @@ The following unsupported types are allowed when using relaxed types, and will b
161162
* Tuple types
162163
* Types with index signatures
163164
* The `any` and `unknown` types
165+
* Enum types
164166

165167
Here's an example of a function that uses some relaxed types:
166168

ndc-lambda-sdk/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ndc-lambda-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"url": "git+https://github.com/hasura/ndc-nodejs-lambda.git"
3131
},
3232
"dependencies": {
33-
"@hasura/ndc-sdk-typescript": "^4.2.0",
33+
"@hasura/ndc-sdk-typescript": "^4.2.1",
3434
"@tsconfig/node18": "^18.2.2",
3535
"commander": "^11.1.0",
3636
"cross-spawn": "^7.0.3",

ndc-lambda-sdk/src/inference.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ function deriveSchemaTypeForTsType(tsType: ts.Type, typePath: schema.TypePathSeg
281281
?? deriveSchemaTypeIfTsArrayType(tsType, typePath, allowRelaxedTypes, context, recursionDepth)
282282
?? deriveSchemaTypeIfScalarType(tsType, context)
283283
?? deriveSchemaTypeIfNullableType(tsType, typePath, allowRelaxedTypes, context, recursionDepth)
284+
?? deriveSchemaTypeIfEnumType(tsType, typePath, allowRelaxedTypes, context)
284285
?? deriveSchemaTypeIfObjectType(tsType, typePath, allowRelaxedTypes, context, recursionDepth)
285286
?? rejectIfClassType(tsType, typePath, context) // This needs to be done after scalars, because JSONValue is a class
286287
?? deriveSchemaTypeIfTsIndexSignatureType(tsType, typePath, allowRelaxedTypes, context, recursionDepth) // This needs to be done after scalars and classes, etc because some of those types do have index signatures (eg. strings)
@@ -444,6 +445,31 @@ function deriveSchemaTypeIfScalarType(tsType: ts.Type, context: TypeDerivationCo
444445
}
445446
}
446447

448+
function deriveSchemaTypeIfEnumType(tsType: ts.Type, typePath: schema.TypePathSegment[], allowRelaxedTypes: boolean, context: TypeDerivationContext): Result<schema.TypeReference, string[]> | undefined {
449+
if (tsutils.isUnionType(tsType) && !tsutils.isIntrinsicType(tsType) /* Block booleans */) {
450+
const typeName = context.typeChecker.typeToString(tsType);
451+
452+
// Handles 'enum { First, Second }'
453+
if (tsutils.isTypeFlagSet(tsType, ts.TypeFlags.EnumLiteral)) {
454+
return deriveRelaxedTypeOrError(typeName, typePath, () => `Enum types are not supported, but one was encountered in ${schema.typePathToString(typePath)} (type: ${typeName})`, allowRelaxedTypes, context);
455+
}
456+
457+
// Handles `"first" | "second"`
458+
if (tsType.types.every(unionMemberType => tsutils.isLiteralType(unionMemberType))) {
459+
return deriveRelaxedTypeOrError(typeName, typePath, () => `Literal union types are not supported, but one was encountered in ${schema.typePathToString(typePath)} (type: ${typeName})`, allowRelaxedTypes, context);
460+
}
461+
}
462+
// Handles computed single member enum types: 'enum { OneThing = "test".length }'
463+
else if (tsutils.isEnumType(tsType) && tsutils.isSymbolFlagSet(tsType.symbol, ts.SymbolFlags.EnumMember)) {
464+
const typeName = context.typeChecker.typeToString(tsType);
465+
return deriveRelaxedTypeOrError(typeName, typePath, () => `Enum types are not supported, but one was encountered in ${schema.typePathToString(typePath)} (type: ${typeName})`, allowRelaxedTypes, context);
466+
}
467+
468+
// Note that single member enum types: 'enum { OneThing }' are simplified by the type system
469+
// down to literal types (since they can only be a single thing) and are therefore supported via support
470+
// for literal types in scalars
471+
}
472+
447473
function isDateType(tsType: ts.Type): boolean {
448474
const symbol = tsType.getSymbol()
449475
if (symbol === undefined) return false;
@@ -464,12 +490,9 @@ function isMapType(tsType: ts.Type): tsType is ts.TypeReference {
464490
function isBooleanUnionType(tsType: ts.Type): boolean {
465491
if (!tsutils.isUnionType(tsType)) return false;
466492

467-
return tsType.types.length === 2 && unionTypeContainsBothBooleanLiterals(tsType);
468-
}
469-
470-
function unionTypeContainsBothBooleanLiterals(tsUnionType: ts.UnionType): boolean {
471-
return tsUnionType.types.find(tsType => tsutils.isBooleanLiteralType(tsType) && tsType.intrinsicName === "true") !== undefined
472-
&& tsUnionType.types.find(tsType => tsutils.isBooleanLiteralType(tsType) && tsType.intrinsicName === "false") !== undefined;
493+
return tsType.types.length === 2
494+
&& tsType.types.find(type => tsutils.isBooleanLiteralType(type) && type.intrinsicName === "true") !== undefined
495+
&& tsType.types.find(type => tsutils.isBooleanLiteralType(type) && type.intrinsicName === "false") !== undefined;
473496
}
474497

475498
function isJSONValueType(tsType: ts.Type, ndcLambdaSdkModule: ts.ResolvedModuleFull): boolean {
@@ -581,21 +604,10 @@ function unwrapNullableType(tsType: ts.Type, typeChecker: ts.TypeChecker): [ts.T
581604
: null
582605
);
583606

584-
const typesWithoutNullAndUndefined = tsType.types
585-
.filter(t => !tsutils.isIntrinsicNullType(t) && !tsutils.isIntrinsicUndefinedType(t));
586607

587-
// The case where one type is unioned with either or both of null and undefined
588-
if (typesWithoutNullAndUndefined.length === 1 && nullOrUndefinability) {
589-
return [typesWithoutNullAndUndefined[0]!, nullOrUndefinability];
590-
}
591-
// The weird edge case where null or undefined is unioned with both 'true' and 'false'
592-
// We simplify this to being unioned with 'boolean' instead
593-
else if (nullOrUndefinability && typesWithoutNullAndUndefined.length === 2 && unionTypeContainsBothBooleanLiterals(tsType)) {
594-
return [typeChecker.getBooleanType(), nullOrUndefinability];
595-
}
596-
else {
597-
return null;
598-
}
608+
return nullOrUndefinability
609+
? [typeChecker.getNonNullableType(tsType), nullOrUndefinability]
610+
: null;
599611
}
600612

601613
type PropertyTypeInfo = {

ndc-lambda-sdk/test/inference/basic-inference/basic-inference.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,17 @@ describe("basic inference", function() {
925925
name: BuiltInScalarTypeName.Float,
926926
literalValue: 0,
927927
}
928-
}
928+
},
929+
{
930+
propertyName: "singleItemEnum",
931+
description: null,
932+
type: {
933+
type: "named",
934+
kind: "scalar",
935+
name: BuiltInScalarTypeName.String,
936+
literalValue: "SingleItem",
937+
}
938+
},
929939
],
930940
isRelaxedType: false,
931941
}

ndc-lambda-sdk/test/inference/basic-inference/literal-types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@ type LiteralProps = {
55
literalBigInt: -123n,
66
literalStringEnum: StringEnum.EnumItem
77
literalNumericEnum: NumericEnum.EnumItem
8+
singleItemEnum: SingleItemEnum
89
}
910

1011
enum StringEnum {
11-
EnumItem = "EnumItem"
12+
EnumItem = "EnumItem",
13+
SecondEnumItem = "SecondEnumItem"
1214
}
1315

1416
enum NumericEnum {
15-
EnumItem
17+
EnumItem,
18+
SecondEnumItem
19+
}
20+
21+
// Single item enums are simplified by the compiler to a literal type
22+
enum SingleItemEnum {
23+
SingleItem = "SingleItem"
1624
}
1725

1826
export function literalTypes(): LiteralProps {
@@ -23,5 +31,6 @@ export function literalTypes(): LiteralProps {
2331
literalBigInt: -123n,
2432
literalStringEnum: StringEnum.EnumItem,
2533
literalNumericEnum: NumericEnum.EnumItem,
34+
singleItemEnum: SingleItemEnum.SingleItem
2635
};
2736
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
type StringLiteralEnum = "1st" | "2nd" | Promise<"3rd">
2+
3+
/**
4+
* @readonly
5+
* @allowrelaxedtypes
6+
*/
7+
export function enumTypesFunction(
8+
stringLiteralEnum: StringLiteralEnum,
9+
inlineStringLiteralEnum: "1st" | "2nd" | void,
10+
): string {
11+
return ""
12+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
enum StringEnum {
2+
First = "First",
3+
Second = "Second"
4+
}
5+
6+
enum NumberEnum {
7+
First = 1,
8+
Second = 2
9+
}
10+
11+
enum MixedEnum {
12+
Dog = "Dog",
13+
Cat = "Cat",
14+
Other = 1234,
15+
}
16+
17+
const enum ConstEnum {
18+
First = "first",
19+
Second = "second",
20+
}
21+
22+
enum ComputedEnum {
23+
Gross = "Gross".length,
24+
Foul = "Foul".length
25+
}
26+
27+
enum ComputedSingleItemEnum {
28+
Single = "Single".length
29+
}
30+
31+
type StringLiteralEnum = "1st" | "2nd"
32+
33+
type NumberLiteralEnum = 0 | 1 | 2
34+
35+
type MixedLiteralEnum = true | false | 0 | 1 | "1st" | "2nd"
36+
37+
const ConstObjEnumVal = {
38+
Plant: "plant",
39+
Animal: "animal"
40+
} as const;
41+
42+
type ConstObjEnum = typeof ConstObjEnumVal[keyof typeof ConstObjEnumVal]
43+
44+
/**
45+
* @readonly
46+
* @allowrelaxedtypes
47+
*/
48+
export function enumTypesFunction(
49+
stringEnum: StringEnum,
50+
numberEnum: NumberEnum,
51+
mixedEnum: MixedEnum,
52+
constEnum: ConstEnum,
53+
computedEnum: ComputedEnum,
54+
computedSingleItemEnum: ComputedSingleItemEnum,
55+
stringLiteralEnum: StringLiteralEnum,
56+
stringLiteralEnumMaybe: StringLiteralEnum | undefined,
57+
inlineStringLiteralEnum: "1st" | "2nd",
58+
inlineStringLiteralEnumMaybe: "1st" | "2nd" | null,
59+
numberLiteralEnum: NumberLiteralEnum,
60+
numberLiteralEnumMaybe: NumberLiteralEnum | null,
61+
inlineNumberLiteralEnum: 0 | 1 | 2,
62+
inlineNumberLiteralEnumMaybe: 0 | 1 | 2 | undefined,
63+
mixedLiteralEnum: MixedLiteralEnum,
64+
mixedLiteralEnumMaybe: MixedLiteralEnum | undefined | null,
65+
inlineMixedLiteralEnum: true | 1 | "first",
66+
inlineMixedLiteralEnumMaybe: true | 1 | "first" | undefined | null,
67+
constObjEnum: ConstObjEnum,
68+
): string {
69+
return ""
70+
}

0 commit comments

Comments
 (0)