Skip to content

Commit 0ba4d4c

Browse files
Support for relaxed types (#10)
1 parent 8ced40d commit 0ba4d4c

29 files changed

+1142
-200
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ This changelog documents the changes between release versions.
44
## main
55
Changes to be included in the next upcoming release
66

7+
* Support for "relaxed types" ([#10](https://github.com/hasura/ndc-nodejs-lambda/pull/10))
8+
79
## v0.13.0
810
- Add support for treating 'true | false' as a Boolean type ([#7](https://github.com/hasura/ndc-nodejs-lambda/pull/7))
911

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,41 @@ These types are unsupported as function parameter types or return types for func
147147
* [`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
148148
* `null` and `undefined` - unless used in a union with a single other type
149149

150+
### Relaxed Types
151+
"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.
152+
153+
Relaxed types are designed to be an escape hatch to help people get up and running using existing code quickly, where their existing code uses types that are unsupported. They are **not intended to be used long term**. You should prefer to modify your code to use only supported types. To opt into using relaxed types, one must apply the `@allowrelaxedtypes` JSDoc tag to the function that will be using the unsupported types.
154+
155+
The following unsupported types are allowed when using relaxed types, and will be converted into opaque unvalidated scalar types:
156+
157+
* Union types
158+
* Tuple types
159+
* Types with index signatures
160+
* The `any` and `unknown` types
161+
162+
Here's an example of a function that uses some relaxed types:
163+
164+
```typescript
165+
/**
166+
* @allowrelaxedtypes
167+
* @readonly
168+
*/
169+
export function findEmptyRecords(record: Record<string, string>): { emptyKeys: string[] } | string {
170+
const emptyKeys: string[] = [];
171+
const entries = Object.entries(record);
172+
173+
if (entries.length === 0)
174+
return "Error: record was empty";
175+
176+
for (const [key, value] of entries) {
177+
if (value === "")
178+
emptyKeys.push(key);
179+
}
180+
181+
return { emptyKeys };
182+
}
183+
```
184+
150185
### Error handling
151186
By default, unhandled errors thrown from functions are caught by the Lambda SDK host, and an `InternalServerError` is returned to Hasura. The details of the uncaught error (the message and stack trace) is captured and will be logged in the OpenTelemetry trace associated with the GraphQL request. However, the GraphQL API caller will receive a generic "internal error" response to their query. This is to ensure internal error details are not leaked to GraphQL API clients.
152187

ndc-lambda-sdk/src/connector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as sdk from "@hasura/ndc-sdk-typescript";
22
import { JSONSchemaObject } from "@json-schema-tools/meta-schema";
33
import path from "node:path"
4-
import { FunctionsSchema, getNdcSchema } from "./schema";
4+
import { FunctionsSchema, getNdcSchema, printRelaxedTypesWarning } from "./schema";
55
import { deriveSchema, printCompilerDiagnostics, printFunctionIssues } from "./inference";
66
import { RuntimeFunctions, executeMutation, executeQuery } from "./execution";
77

@@ -45,6 +45,7 @@ export function createConnector(options: ConnectorOptions): sdk.Connector<RawCon
4545
const schemaResults = deriveSchema(functionsFilePath);
4646
printCompilerDiagnostics(schemaResults.compilerDiagnostics);
4747
printFunctionIssues(schemaResults.functionIssues);
48+
printRelaxedTypesWarning(schemaResults.functionsSchema);
4849
return {
4950
functionsSchema: schemaResults.functionsSchema
5051
}

ndc-lambda-sdk/src/execution.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export function prepareArguments(args: Record<string, unknown>, functionDefiniti
107107
return functionDefinition.arguments.map(argDef => coerceArgumentValue(args[argDef.argumentName], argDef.type, [argDef.argumentName], objectTypes));
108108
}
109109

110-
function coerceArgumentValue(value: unknown, type: schema.TypeDefinition, valuePath: string[], objectTypeDefinitions: schema.ObjectTypeDefinitions): unknown {
110+
function coerceArgumentValue(value: unknown, type: schema.TypeReference, valuePath: string[], objectTypeDefinitions: schema.ObjectTypeDefinitions): unknown {
111111
switch (type.type) {
112112
case "array":
113113
if (!isArray(value))
@@ -128,7 +128,7 @@ function coerceArgumentValue(value: unknown, type: schema.TypeDefinition, valueP
128128
}
129129
case "named":
130130
if (type.kind === "scalar") {
131-
if (schema.isBuiltInScalarTypeDefinition(type))
131+
if (schema.isBuiltInScalarTypeReference(type))
132132
return convertBuiltInNdcJsonScalarToJsScalar(value, valuePath, type);
133133
// Scalars are currently treated as opaque values, which is a bit dodgy
134134
return value;
@@ -211,7 +211,7 @@ function buildCausalStackTrace(error: Error): string {
211211
return stackTrace;
212212
}
213213

214-
export function reshapeResultToNdcResponseValue(value: unknown, type: schema.TypeDefinition, valuePath: string[], fields: Record<string, sdk.Field> | "AllColumns", objectTypes: schema.ObjectTypeDefinitions): unknown {
214+
export function reshapeResultToNdcResponseValue(value: unknown, type: schema.TypeReference, valuePath: string[], fields: Record<string, sdk.Field> | "AllColumns", objectTypes: schema.ObjectTypeDefinitions): unknown {
215215
switch (type.type) {
216216
case "array":
217217
if (isArray(value)) {
@@ -268,7 +268,7 @@ export function reshapeResultToNdcResponseValue(value: unknown, type: schema.Typ
268268
}
269269
}
270270

271-
function convertBuiltInNdcJsonScalarToJsScalar(value: unknown, valuePath: string[], scalarType: schema.BuiltInScalarTypeDefinition): string | number | boolean | BigInt | Date | schema.JSONValue {
271+
function convertBuiltInNdcJsonScalarToJsScalar(value: unknown, valuePath: string[], scalarType: schema.BuiltInScalarTypeReference): string | number | boolean | BigInt | Date | schema.JSONValue {
272272
switch (scalarType.name) {
273273
case schema.BuiltInScalarTypeName.String:
274274
if (typeof value === "string") {

0 commit comments

Comments
 (0)