Skip to content

Commit

Permalink
Block usage of enum types but allow them as relaxed types (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-chambers authored Feb 29, 2024
1 parent 32e3352 commit 7fa60b3
Show file tree
Hide file tree
Showing 12 changed files with 704 additions and 29 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This changelog documents the changes between release versions.
## [Unreleased]
Changes to be included in the next upcoming release

- 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))

## [1.1.0] - 2024-02-26
- 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
- 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))
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ These types are unsupported as function parameter types or return types for func
* [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
* [`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
* `null` and `undefined` - unless used in a union with a single other type
* [Enum types](https://www.typescriptlang.org/docs/handbook/enums.html)

### Relaxed Types
"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.
Expand All @@ -161,6 +162,7 @@ The following unsupported types are allowed when using relaxed types, and will b
* Tuple types
* Types with index signatures
* The `any` and `unknown` types
* Enum types

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

Expand Down
8 changes: 4 additions & 4 deletions ndc-lambda-sdk/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ndc-lambda-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"url": "git+https://github.com/hasura/ndc-nodejs-lambda.git"
},
"dependencies": {
"@hasura/ndc-sdk-typescript": "^4.2.0",
"@hasura/ndc-sdk-typescript": "^4.2.1",
"@tsconfig/node18": "^18.2.2",
"commander": "^11.1.0",
"cross-spawn": "^7.0.3",
Expand Down
52 changes: 32 additions & 20 deletions ndc-lambda-sdk/src/inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ function deriveSchemaTypeForTsType(tsType: ts.Type, typePath: schema.TypePathSeg
?? deriveSchemaTypeIfTsArrayType(tsType, typePath, allowRelaxedTypes, context, recursionDepth)
?? deriveSchemaTypeIfScalarType(tsType, context)
?? deriveSchemaTypeIfNullableType(tsType, typePath, allowRelaxedTypes, context, recursionDepth)
?? deriveSchemaTypeIfEnumType(tsType, typePath, allowRelaxedTypes, context)
?? deriveSchemaTypeIfObjectType(tsType, typePath, allowRelaxedTypes, context, recursionDepth)
?? rejectIfClassType(tsType, typePath, context) // This needs to be done after scalars, because JSONValue is a class
?? 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)
Expand Down Expand Up @@ -444,6 +445,31 @@ function deriveSchemaTypeIfScalarType(tsType: ts.Type, context: TypeDerivationCo
}
}

function deriveSchemaTypeIfEnumType(tsType: ts.Type, typePath: schema.TypePathSegment[], allowRelaxedTypes: boolean, context: TypeDerivationContext): Result<schema.TypeReference, string[]> | undefined {
if (tsutils.isUnionType(tsType) && !tsutils.isIntrinsicType(tsType) /* Block booleans */) {
const typeName = context.typeChecker.typeToString(tsType);

// Handles 'enum { First, Second }'
if (tsutils.isTypeFlagSet(tsType, ts.TypeFlags.EnumLiteral)) {
return deriveRelaxedTypeOrError(typeName, typePath, () => `Enum types are not supported, but one was encountered in ${schema.typePathToString(typePath)} (type: ${typeName})`, allowRelaxedTypes, context);
}

// Handles `"first" | "second"`
if (tsType.types.every(unionMemberType => tsutils.isLiteralType(unionMemberType))) {
return deriveRelaxedTypeOrError(typeName, typePath, () => `Literal union types are not supported, but one was encountered in ${schema.typePathToString(typePath)} (type: ${typeName})`, allowRelaxedTypes, context);
}
}
// Handles computed single member enum types: 'enum { OneThing = "test".length }'
else if (tsutils.isEnumType(tsType) && tsutils.isSymbolFlagSet(tsType.symbol, ts.SymbolFlags.EnumMember)) {
const typeName = context.typeChecker.typeToString(tsType);
return deriveRelaxedTypeOrError(typeName, typePath, () => `Enum types are not supported, but one was encountered in ${schema.typePathToString(typePath)} (type: ${typeName})`, allowRelaxedTypes, context);
}

// Note that single member enum types: 'enum { OneThing }' are simplified by the type system
// down to literal types (since they can only be a single thing) and are therefore supported via support
// for literal types in scalars
}

function isDateType(tsType: ts.Type): boolean {
const symbol = tsType.getSymbol()
if (symbol === undefined) return false;
Expand All @@ -464,12 +490,9 @@ function isMapType(tsType: ts.Type): tsType is ts.TypeReference {
function isBooleanUnionType(tsType: ts.Type): boolean {
if (!tsutils.isUnionType(tsType)) return false;

return tsType.types.length === 2 && unionTypeContainsBothBooleanLiterals(tsType);
}

function unionTypeContainsBothBooleanLiterals(tsUnionType: ts.UnionType): boolean {
return tsUnionType.types.find(tsType => tsutils.isBooleanLiteralType(tsType) && tsType.intrinsicName === "true") !== undefined
&& tsUnionType.types.find(tsType => tsutils.isBooleanLiteralType(tsType) && tsType.intrinsicName === "false") !== undefined;
return tsType.types.length === 2
&& tsType.types.find(type => tsutils.isBooleanLiteralType(type) && type.intrinsicName === "true") !== undefined
&& tsType.types.find(type => tsutils.isBooleanLiteralType(type) && type.intrinsicName === "false") !== undefined;
}

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

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

// The case where one type is unioned with either or both of null and undefined
if (typesWithoutNullAndUndefined.length === 1 && nullOrUndefinability) {
return [typesWithoutNullAndUndefined[0]!, nullOrUndefinability];
}
// The weird edge case where null or undefined is unioned with both 'true' and 'false'
// We simplify this to being unioned with 'boolean' instead
else if (nullOrUndefinability && typesWithoutNullAndUndefined.length === 2 && unionTypeContainsBothBooleanLiterals(tsType)) {
return [typeChecker.getBooleanType(), nullOrUndefinability];
}
else {
return null;
}
return nullOrUndefinability
? [typeChecker.getNonNullableType(tsType), nullOrUndefinability]
: null;
}

type PropertyTypeInfo = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,17 @@ describe("basic inference", function() {
name: BuiltInScalarTypeName.Float,
literalValue: 0,
}
}
},
{
propertyName: "singleItemEnum",
description: null,
type: {
type: "named",
kind: "scalar",
name: BuiltInScalarTypeName.String,
literalValue: "SingleItem",
}
},
],
isRelaxedType: false,
}
Expand Down
13 changes: 11 additions & 2 deletions ndc-lambda-sdk/test/inference/basic-inference/literal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ type LiteralProps = {
literalBigInt: -123n,
literalStringEnum: StringEnum.EnumItem
literalNumericEnum: NumericEnum.EnumItem
singleItemEnum: SingleItemEnum
}

enum StringEnum {
EnumItem = "EnumItem"
EnumItem = "EnumItem",
SecondEnumItem = "SecondEnumItem"
}

enum NumericEnum {
EnumItem
EnumItem,
SecondEnumItem
}

// Single item enums are simplified by the compiler to a literal type
enum SingleItemEnum {
SingleItem = "SingleItem"
}

export function literalTypes(): LiteralProps {
Expand All @@ -23,5 +31,6 @@ export function literalTypes(): LiteralProps {
literalBigInt: -123n,
literalStringEnum: StringEnum.EnumItem,
literalNumericEnum: NumericEnum.EnumItem,
singleItemEnum: SingleItemEnum.SingleItem
};
}
12 changes: 12 additions & 0 deletions ndc-lambda-sdk/test/inference/relaxed-types/enum-types-invalid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type StringLiteralEnum = "1st" | "2nd" | Promise<"3rd">

/**
* @readonly
* @allowrelaxedtypes
*/
export function enumTypesFunction(
stringLiteralEnum: StringLiteralEnum,
inlineStringLiteralEnum: "1st" | "2nd" | void,
): string {
return ""
}
70 changes: 70 additions & 0 deletions ndc-lambda-sdk/test/inference/relaxed-types/enum-types-valid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
enum StringEnum {
First = "First",
Second = "Second"
}

enum NumberEnum {
First = 1,
Second = 2
}

enum MixedEnum {
Dog = "Dog",
Cat = "Cat",
Other = 1234,
}

const enum ConstEnum {
First = "first",
Second = "second",
}

enum ComputedEnum {
Gross = "Gross".length,
Foul = "Foul".length
}

enum ComputedSingleItemEnum {
Single = "Single".length
}

type StringLiteralEnum = "1st" | "2nd"

type NumberLiteralEnum = 0 | 1 | 2

type MixedLiteralEnum = true | false | 0 | 1 | "1st" | "2nd"

const ConstObjEnumVal = {
Plant: "plant",
Animal: "animal"
} as const;

type ConstObjEnum = typeof ConstObjEnumVal[keyof typeof ConstObjEnumVal]

/**
* @readonly
* @allowrelaxedtypes
*/
export function enumTypesFunction(
stringEnum: StringEnum,
numberEnum: NumberEnum,
mixedEnum: MixedEnum,
constEnum: ConstEnum,
computedEnum: ComputedEnum,
computedSingleItemEnum: ComputedSingleItemEnum,
stringLiteralEnum: StringLiteralEnum,
stringLiteralEnumMaybe: StringLiteralEnum | undefined,
inlineStringLiteralEnum: "1st" | "2nd",
inlineStringLiteralEnumMaybe: "1st" | "2nd" | null,
numberLiteralEnum: NumberLiteralEnum,
numberLiteralEnumMaybe: NumberLiteralEnum | null,
inlineNumberLiteralEnum: 0 | 1 | 2,
inlineNumberLiteralEnumMaybe: 0 | 1 | 2 | undefined,
mixedLiteralEnum: MixedLiteralEnum,
mixedLiteralEnumMaybe: MixedLiteralEnum | undefined | null,
inlineMixedLiteralEnum: true | 1 | "first",
inlineMixedLiteralEnumMaybe: true | 1 | "first" | undefined | null,
constObjEnum: ConstObjEnum,
): string {
return ""
}
Loading

0 comments on commit 7fa60b3

Please sign in to comment.