Skip to content

Commit

Permalink
Add support for literal types
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-chambers committed Jan 15, 2024
1 parent 0b3d035 commit 449826c
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 28 deletions.
58 changes: 36 additions & 22 deletions ndc-lambda-sdk/src/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,8 @@ function coerceArgumentValue(value: unknown, type: schema.TypeDefinition, valueP
}
case "named":
if (type.kind === "scalar") {
const builtInScalarType = schema.isTypeNameBuiltInScalar(type.name);
if (builtInScalarType)
return convertNdcJsonScalarToJsScalar(value, valuePath, builtInScalarType);
if (schema.isBuiltInScalarTypeDefinition(type))
return convertBuiltInNdcJsonScalarToJsScalar(value, valuePath, type);
// Scalars are currently treated as opaque values, which is a bit dodgy
return value;
} else {
Expand Down Expand Up @@ -179,9 +178,8 @@ export function reshapeResultToNdcResponseValue(value: unknown, type: schema.Typ
case "named":
switch (type.kind) {
case "scalar":
const builtInScalarType = schema.isTypeNameBuiltInScalar(type.name);
return builtInScalarType
? convertJsScalarToNdcJsonScalar(value, builtInScalarType)
return schema.isTypeNameBuiltInScalar(type.name)
? convertJsScalarToNdcJsonScalar(value, type.name)
: value; // YOLO? Just try to serialize it to JSON as is as an opaque scalar.

case "object":
Expand Down Expand Up @@ -242,41 +240,56 @@ function pruneFields(result: unknown, fields: Record<string, sdk.Field> | null |
return response;
}

function convertNdcJsonScalarToJsScalar(value: unknown, valuePath: string[], scalarType: schema.BuiltInScalarTypeName): string | number | boolean | BigInt | Date {
switch (scalarType) {
function convertBuiltInNdcJsonScalarToJsScalar(value: unknown, valuePath: string[], scalarType: schema.BuiltInScalarTypeDefinition): string | number | boolean | BigInt | Date {
switch (scalarType.name) {
case schema.BuiltInScalarTypeName.String:
if (typeof value === "string") {
if (scalarType.literalValue !== undefined && value !== scalarType.literalValue)
throw new sdk.BadRequest(`Invalid value in function arguments. Only the value '${scalarType.literalValue}' is accepted at '${valuePath.join(".")}', got '${value}'`);
return value;
} else {
throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a string at '${valuePath.join(".")}', got a ${typeof value}`);
}

case schema.BuiltInScalarTypeName.Float:
if (typeof value === "number") {
if (scalarType.literalValue !== undefined && value !== scalarType.literalValue)
throw new sdk.BadRequest(`Invalid value in function arguments. Only the value '${scalarType.literalValue}' is accepted at '${valuePath.join(".")}', got '${value}'`);
return value;
} else {
throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a number at '${valuePath.join(".")}', got a ${typeof value}`);
}

case schema.BuiltInScalarTypeName.Boolean:
if (typeof value === "boolean") {
if (scalarType.literalValue !== undefined && value !== scalarType.literalValue)
throw new sdk.BadRequest(`Invalid value in function arguments. Only the value '${scalarType.literalValue}' is accepted at '${valuePath.join(".")}', got '${value}'`);
return value;
} else {
throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a boolean at '${valuePath.join(".")}', got a ${typeof value}`);
}

case schema.BuiltInScalarTypeName.BigInt:
if (typeof value === "number") {
if (!Number.isInteger(value))
throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a integer number at '${valuePath.join(".")}', got a float`);
return BigInt(value);
}
else if (typeof value === "string") {
try { return BigInt(value) }
catch { throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a bigint string at '${valuePath.join(".")}', got a non-integer string: '${value}'`); }
}
else if (typeof value === "bigint") { // This won't happen since JSON doesn't have a bigint type, but I'll just put it here for completeness
return value;
} else {
throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a bigint at '${valuePath.join(".")}', got a ${typeof value}`);
}
const bigIntValue = (() => {
if (typeof value === "number") {
if (!Number.isInteger(value))
throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a integer number at '${valuePath.join(".")}', got a float`);
return BigInt(value);
}
else if (typeof value === "string") {
try { return BigInt(value) }
catch { throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a bigint string at '${valuePath.join(".")}', got a non-integer string: '${value}'`); }
}
else if (typeof value === "bigint") { // This won't happen since JSON doesn't have a bigint type, but I'll just put it here for completeness
return value;
} else {
throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a bigint at '${valuePath.join(".")}', got a ${typeof value}`);
}
})();
if (scalarType.literalValue !== undefined && bigIntValue !== scalarType.literalValue)
throw new sdk.BadRequest(`Invalid value in function arguments. Only the value '${scalarType.literalValue}' is accepted at '${valuePath.join(".")}', got '${value}'`);
return bigIntValue;

case schema.BuiltInScalarTypeName.DateTime:
if (typeof value === "string") {
const parsedDate = Date.parse(value);
Expand All @@ -286,6 +299,7 @@ function convertNdcJsonScalarToJsScalar(value: unknown, valuePath: string[], sca
} else {
throw new sdk.BadRequest(`Unexpected value in function arguments. Expected a Date string at '${valuePath.join(".")}', got a ${typeof value}`);
}

default:
return unreachable(scalarType);
}
Expand Down
18 changes: 18 additions & 0 deletions ndc-lambda-sdk/src/inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,18 +297,36 @@ function deriveSchemaTypeIfScalarType(tsType: ts.Type, context: TypeDerivationCo
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.Boolean] = {};
return new Ok({ typeDefinition: { type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.Boolean }, warnings: [] });
}
if (tsutils.isBooleanLiteralType(tsType)) {
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.Boolean] = {};
const literalValue = tsType.intrinsicName === "true" ? true : false; // Unfortunately the types lie, tsType.value is undefined here :(
return new Ok({ typeDefinition: { type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.Boolean, literalValue: literalValue }, warnings: [] });
}
if (tsutils.isIntrinsicStringType(tsType)) {
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.String] = {};
return new Ok({ typeDefinition: { type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.String }, warnings: [] });
}
if (tsutils.isStringLiteralType(tsType)) {
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.String] = {};
return new Ok({ typeDefinition: { type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.String, literalValue: tsType.value }, warnings: [] });
}
if (tsutils.isIntrinsicNumberType(tsType)) {
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.Float] = {};
return new Ok({ typeDefinition: { type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.Float }, warnings: [] });
}
if (tsutils.isNumberLiteralType(tsType)) {
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.Float] = {};
return new Ok({ typeDefinition: { type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.Float, literalValue: tsType.value }, warnings: [] });
}
if (tsutils.isIntrinsicBigIntType(tsType)) {
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.BigInt] = {};
return new Ok({ typeDefinition: { type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.BigInt }, warnings: [] });
}
if (tsutils.isBigIntLiteralType(tsType)) {
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.BigInt] = {};
const literalValue = BigInt(`${tsType.value.negative ? "-" : ""}${tsType.value.base10Value}`);
return new Ok({ typeDefinition: { type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.BigInt, literalValue: literalValue }, warnings: [] });
}
if (isDateType(tsType)) {
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.DateTime] = {};
return new Ok({ typeDefinition: { type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.DateTime }, warnings: [] });
Expand Down
65 changes: 61 additions & 4 deletions ndc-lambda-sdk/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,63 @@ export type NullableTypeDefinition = {
underlyingType: TypeDefinition
}

export type NamedTypeDefinition = {
export type NamedTypeDefinition = NamedObjectTypeDefinition | NamedScalarTypeDefinition | StringScalarTypeDefinition | FloatScalarTypeDefinition | BooleanScalarTypeDefinition | BigIntScalarTypeDefinition

export type NamedObjectTypeDefinition = {
type: "named"
name: string
kind: "object"
}

export type NamedScalarTypeDefinition = CustomNamedScalarTypeDefinition | BuiltInScalarTypeDefinition

export type BuiltInScalarTypeDefinition = StringScalarTypeDefinition | FloatScalarTypeDefinition | BooleanScalarTypeDefinition | BigIntScalarTypeDefinition | DateTimeScalarTypeDefinition

export type CustomNamedScalarTypeDefinition = {
type: "named"
name: string
kind: "scalar" | "object"
kind: "scalar"
}

export type StringScalarTypeDefinition = {
type: "named"
name: BuiltInScalarTypeName.String
kind: "scalar"
literalValue?: string
}

export type FloatScalarTypeDefinition = {
type: "named"
name: BuiltInScalarTypeName.Float
kind: "scalar"
literalValue?: number
}

export type BooleanScalarTypeDefinition = {
type: "named"
name: BuiltInScalarTypeName.Boolean
kind: "scalar"
literalValue?: boolean
}

export type BigIntScalarTypeDefinition = {
type: "named"
name: BuiltInScalarTypeName.BigInt
kind: "scalar"
literalValue?: bigint
}

export type DateTimeScalarTypeDefinition = {
type: "named"
name: BuiltInScalarTypeName.DateTime
kind: "scalar"
}

// If there are compiler errors on this function, ensure that BuiltInScalarTypeDefinition has a type in
// its union for every BuiltInScalarTypeName enum member, and vice versa.
function builtInScalarTypeAssertionTest(a: BuiltInScalarTypeDefinition["name"], b: BuiltInScalarTypeName): void {
a = b;
b = a;
}

export enum NullOrUndefinability {
Expand Down Expand Up @@ -152,6 +205,10 @@ export function printSchemaListing(functionNdcKind: FunctionNdcKind, functionDef
}
}

export function isTypeNameBuiltInScalar(typeName: string): BuiltInScalarTypeName | undefined {
return Object.values(BuiltInScalarTypeName).find(builtInScalarTypeName => typeName === builtInScalarTypeName);
export function isTypeNameBuiltInScalar(typeName: string): typeName is BuiltInScalarTypeName {
return Object.values(BuiltInScalarTypeName).find(builtInScalarTypeName => typeName === builtInScalarTypeName) !== undefined;
}

export function isBuiltInScalarTypeDefinition(typeDefinition: NamedScalarTypeDefinition): typeDefinition is BuiltInScalarTypeDefinition {
return isTypeNameBuiltInScalar(typeDefinition.name);
}
97 changes: 96 additions & 1 deletion ndc-lambda-sdk/test/execution/prepare-arguments.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, it } from "mocha";
import { assert } from "chai";
import { prepareArguments } from "../../src/execution";
import { FunctionDefinition, FunctionNdcKind, NullOrUndefinability, ObjectTypeDefinitions } from "../../src/schema";
import { BuiltInScalarTypeName, FunctionDefinition, FunctionNdcKind, NullOrUndefinability, ObjectTypeDefinitions } from "../../src/schema";
import { BadRequest } from "@hasura/ndc-sdk-typescript";

describe("prepare arguments", function() {
it("argument ordering", function() {
Expand Down Expand Up @@ -378,4 +379,98 @@ describe("prepare arguments", function() {
const preparedArgs = prepareArguments(args, functionDefinition, {});
assert.deepStrictEqual(preparedArgs, ["test", true, 123.456, BigInt(1234), new Date("2024-01-11T15:17:56Z")]);
});

describe("validation of literal types", function() {
const functionDefinition: FunctionDefinition = {
ndcKind: FunctionNdcKind.Function,
description: null,
arguments: [
{
argumentName: "literalString",
description: null,
type: {
type: "named",
kind: "scalar",
name: BuiltInScalarTypeName.String,
literalValue: "literal-string"
}
},
{
argumentName: "literalFloat",
description: null,
type: {
type: "named",
kind: "scalar",
name: BuiltInScalarTypeName.Float,
literalValue: 123.567
}
},
{
argumentName: "literalBool",
description: null,
type: {
type: "named",
kind: "scalar",
name: BuiltInScalarTypeName.Boolean,
literalValue: true
}
},
{
argumentName: "literalBigInt",
description: null,
type: {
type: "named",
kind: "scalar",
name: BuiltInScalarTypeName.BigInt,
literalValue: 678n
}
},
],
resultType: {
type: "named",
kind: "scalar",
name: "String"
}
}
const objectTypes: ObjectTypeDefinitions = {}

it("passes validation", function() {
const args = {
literalString: "literal-string",
literalFloat: 123.567,
literalBool: true,
literalBigInt: 678n,
}

const preparedArgs = prepareArguments(args, functionDefinition, objectTypes);

assert.deepStrictEqual(preparedArgs, [ "literal-string", 123.567, true, 678n ]);
});

describe("fails validation", function() {
const correctArgs = {
literalString: "literal-string",
literalFloat: 123.567,
literalBool: true,
literalBigInt: 678n,
};

it("String", function() {
const args = { ...correctArgs, literalString: "something else" };
assert.throws(() => prepareArguments(args, functionDefinition, objectTypes), BadRequest, "Invalid value in function arguments. Only the value 'literal-string' is accepted at 'literalString', got 'something else'")
});
it("Float", function() {
const args = { ...correctArgs, literalFloat: 10 };
assert.throws(() => prepareArguments(args, functionDefinition, objectTypes), BadRequest, "Invalid value in function arguments. Only the value '123.567' is accepted at 'literalFloat', got '10'")
});
it("Boolean", function() {
const args = { ...correctArgs, literalBool: false };
assert.throws(() => prepareArguments(args, functionDefinition, objectTypes), BadRequest, "Invalid value in function arguments. Only the value 'true' is accepted at 'literalBool', got 'false'")
});
it("BigInt", function() {
const args = { ...correctArgs, literalBigInt: 789n };
assert.throws(() => prepareArguments(args, functionDefinition, objectTypes), BadRequest, "Invalid value in function arguments. Only the value '678' is accepted at 'literalBigInt', got '789'")
});
});
});
});
Loading

0 comments on commit 449826c

Please sign in to comment.