diff --git a/src/query.ts b/src/query.ts index 3b4346f..9f1f38f 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,6 +1,6 @@ import { Argument, BadRequest, Field, InternalServerError, NotSupported, ObjectType, QueryRequest, QueryResponse, Type } from "@hasura/ndc-sdk-typescript" import { ByKeysFunction, ConnectorSchema, QueryFields, QueryFunction, RowFieldValue, schemaConstants } from "./schema-ndc"; -import { ifNotNull, isArray, mapObjectValues, unreachable } from "./util"; +import { ifNotNull, isArray, mapObjectValues, throwBadRequest, unreachable } from "./util"; import { AttributeValue, BatchGetItemCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoAttributeType, DynamoType, ObjectNamedType, ScalarNamedType, ScalarType, determineNamedTypeKind } from "./schema-dynamo"; import { fromBase64, toBase64 } from "@smithy/util-base64"; @@ -338,7 +338,8 @@ function mkNdcResponseRowFromDynamoResponseRow(dynamoResponseRow: Record Result.traverseAndCollectErrors( - tableSchema.map((table, tableIndex) => - createByKeysFunctionForTable(table, tableIndex, tableRowTypeNames[table.tableName], { ...objectTypes, ...tableRowTypes }) - ) + tableSchema.map((table, tableIndex) => { + const tableRowTypeName = tableRowTypeNames[table.tableName] ?? throwInternalServerError(`Table row type name not found: ${table.tableName}`); + return createByKeysFunctionForTable(table, tableIndex, tableRowTypeName, { ...objectTypes, ...tableRowTypes }); + }) ) .bind(generatedFunctionDefinitions => { const functionDefinitions = combineGenerated(...generatedFunctionDefinitions); @@ -162,36 +163,36 @@ function createTablePkObjectType(tableSchema: TableSchema, tableSchemaIndex: num : new Ok(tablePkObjectTypeName) ); - const hashAttrIndex = tableSchema.attributeSchema.findIndex(attr => attr.name === tableSchema.keySchema.hashKeyAttributeName) const hashAttrField: Result<[{[k: string]: ObjectField}, KeySchema], ConfigurationRangeError[]> = - hashAttrIndex !== -1 - ? attributeSchemaAsObjectField(tableSchema.attributeSchema[hashAttrIndex], true, tableSchemaIndex, hashAttrIndex, objectTypes) + findWithIndex(tableSchema.attributeSchema, attr => attr.name === tableSchema.keySchema.hashKeyAttributeName) + .mapErr(_ => [{ + path: ["tables", tableSchemaIndex, "keySchema", "hashKeyAttributeName"], + message: `Cannot find an attribute schema defined for the specified hash key attribute '${tableSchema.keySchema.hashKeyAttributeName}'` + }]) + .bind(([hashAttrSchema, hashAttrIndex]) => + attributeSchemaAsObjectField(hashAttrSchema, true, tableSchemaIndex, hashAttrIndex, objectTypes) .map(([key, value]) => { const hashAttrField = {[key]: value}; - const hashKeySchema = { attributeName: tableSchema.attributeSchema[hashAttrIndex].name, schemaType: value.type, dynamoType: tableSchema.attributeSchema[hashAttrIndex].dynamoType }; + const hashKeySchema = { attributeName: tableSchema.attributeSchema[hashAttrIndex]!.name, schemaType: value.type, dynamoType: tableSchema.attributeSchema[hashAttrIndex]!.dynamoType }; return [hashAttrField, hashKeySchema]; }) - : new Err([{ - path: ["tables", tableSchemaIndex, "keySchema", "hashKeyAttributeName"], - message: `Cannot find an attribute schema defined for the specified hash key attribute '${tableSchema.keySchema.hashKeyAttributeName}'` - }]); + ); const rangeAttrField: Result<[{[k: string]: ObjectField}, KeySchema | null], ConfigurationRangeError[]> = tableSchema.keySchema.rangeKeyAttributeName === null ? new Ok([{}, null]) - : new Ok(tableSchema.attributeSchema.findIndex(attr => attr.name === tableSchema.keySchema.rangeKeyAttributeName)) - .bind(rangeAttrIndex => - rangeAttrIndex !== -1 - ? attributeSchemaAsObjectField(tableSchema.attributeSchema[rangeAttrIndex], true, tableSchemaIndex, rangeAttrIndex, objectTypes) - .map(([key, value]) => { - const rangeAttrField = {[key]: value}; - const rangeKeySchema = { attributeName: tableSchema.attributeSchema[rangeAttrIndex].name, schemaType: value.type, dynamoType: tableSchema.attributeSchema[rangeAttrIndex].dynamoType }; - return [rangeAttrField, rangeKeySchema]; - }) - : new Err([{ - path: ["tables", tableSchemaIndex, "keySchema", "rangeKeyAttributeName"], - message: `Cannot find an attribute schema defined for the specified range key attribute '${tableSchema.keySchema.rangeKeyAttributeName}'` - }]) + : findWithIndex(tableSchema.attributeSchema, attr => attr.name === tableSchema.keySchema.rangeKeyAttributeName) + .mapErr(_ => [{ + path: ["tables", tableSchemaIndex, "keySchema", "rangeKeyAttributeName"], + message: `Cannot find an attribute schema defined for the specified range key attribute '${tableSchema.keySchema.rangeKeyAttributeName}'` + }]) + .bind(([rangeAttrSchema, rangeAttrIndex]) => + attributeSchemaAsObjectField(rangeAttrSchema, true, tableSchemaIndex, rangeAttrIndex, objectTypes) + .map(([key, value]) => { + const rangeAttrField = {[key]: value}; + const rangeKeySchema = { attributeName: rangeAttrSchema.name, schemaType: value.type, dynamoType: rangeAttrSchema.dynamoType }; + return [rangeAttrField, rangeKeySchema]; + }) ); return Result.collectErrors3(tablePkObjectTypeName, hashAttrField, rangeAttrField) @@ -254,7 +255,7 @@ function createByKeysFunctionForTable(tableSchema: TableSchema, tableSchemaIndex }, }, primaryKeySchema, - tableRowType: objectTypes[tableRowTypeName], + tableRowType: objectTypes[tableRowTypeName] ?? throwInternalServerError(`Expected table row ObjectType ${tableRowTypeName} not found`), }, newObjectTypes: { [tablePkObjectTypeName]: tablePkObjectType @@ -275,7 +276,7 @@ function validateTypeUsages(functionInfos: FunctionInfo[], allObjectTypes: Objec .flatMap<[string, ObjectType]>(usedType => { const underlyingType = getUnderlyingNamedType(usedType, []); return underlyingType.kind === "object" - ? [[underlyingType.name, allObjectTypes[underlyingType.name]]] + ? [[underlyingType.name, allObjectTypes[underlyingType.name] ?? throwInternalServerError(`Could not find object type '${underlyingType.name}'`)]] : [] }); @@ -304,7 +305,7 @@ function validateTypeUsages(functionInfos: FunctionInfo[], allObjectTypes: Objec fieldTypeValidationResults.oks .flatMap<[string, ObjectType]>(underlyingFieldType => underlyingFieldType.kind === "object" - ? [[underlyingFieldType.name, allObjectTypes[underlyingFieldType.name]]] + ? [[underlyingFieldType.name, (allObjectTypes[underlyingFieldType.name] ?? throwInternalServerError(`Could not find object type '${underlyingFieldType.name}'`))]] : [] ); diff --git a/src/util.ts b/src/util.ts index 7182226..f6a514d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,6 @@ +import { BadRequest, InternalServerError } from "@hasura/ndc-sdk-typescript"; +import { Err, Ok, Result } from "./result"; + export const unreachable = (x: never): never => { throw new Error(`Unreachable code reached! The types lied! 😭 Unexpected value: ${x}`) }; export function isArray(x: unknown): x is unknown[] { @@ -15,3 +18,20 @@ export function mapObjectValues(obj: { [k: string]: T }, fn: (value: T, pr export function mapObjectValues(obj: { [k: string]: T }, fn: (value: T, propertyName: string) => U): Record { return Object.fromEntries(Object.entries(obj).map(([prop, val]) => [prop, fn(val, prop)])); } + +// Throws an internal server error. Useful for using after a short-circuiting ?? operator to eliminate null/undefined from the type +export function throwInternalServerError(...args: ConstructorParameters): NonNullable { + throw new InternalServerError(...args); +} + +// Throws an bad request error. Useful for using after a short-circuiting ?? operator to eliminate null/undefined from the type +export function throwBadRequest(...args: ConstructorParameters): NonNullable { + throw new BadRequest(...args); +} + +export function findWithIndex(array: T[], predicate: (value: T, index: number, obj: T[]) => boolean): Result<[T, number], undefined> { + const index = array.findIndex(predicate); + return index !== -1 + ? new Ok([array[index]!, index]) + : new Err(undefined); +} diff --git a/tsconfig.json b/tsconfig.json index 8b5db51..60a9990 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "@tsconfig/node18/tsconfig.json", "compilerOptions": { - "resolveJsonModule": true + "resolveJsonModule": true, + "noUncheckedIndexedAccess": true }, "include": [ "src/**/*.ts",