diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5d47c21 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/CHANGELOG.md b/CHANGELOG.md index fa2d92a..dc09e74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,18 @@ This changelog documents the changes between release versions. Changes to be included in the next upcoming release. +Support for "nullable" types, for example `string | null`, `string | undefined`, `string | null | undefined`, +and optional object properties. + +PR: https://github.com/hasura/ndc-typescript-deno/pull/82 + ## v0.20 Improved support for running the connector in Watch mode, where it will auto-restart when changes to the functions are made. * The Docker container is now compatible with watch mode and can be used for local development -* README documentation about watch mode updated to indicate the need to explicitly watch the functions +* README documentation about watch mode updated to indicate the need to explicitly watch the functions folder (ie `--watch=./functions`) * `functions` configuration property is now required * Type inference is now only done at connector startup, not every time the `/schema` endpoint is called diff --git a/src/connector.ts b/src/connector.ts index cab17cb..4c76500 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -3,15 +3,22 @@ * Using https://github.com/hasura/ndc-qdrant/blob/main/src/index.ts as an example. */ -import { FunctionPositions, ProgramInfo, programInfo, Struct } from "./infer.ts"; +import { FunctionDefinitions, get_ndc_schema, NullOrUndefinability, ObjectTypeDefinitions, ProgramSchema, TypeDefinition } from "./schema.ts"; +import { inferProgramSchema, Struct, } from "./infer.ts"; import { resolve } from "https://deno.land/std@0.208.0/path/mod.ts"; import { JSONSchemaObject } from "npm:@json-schema-tools/meta-schema"; +import { isArray, unreachable } from "./util.ts"; import * as sdk from 'npm:@hasura/ndc-sdk-typescript@1.2.5'; export * as sdk from 'npm:@hasura/ndc-sdk-typescript@1.2.5'; export type State = { - functions: any + functions: RuntimeFunctions +} + +export type RuntimeFunctions = { + // deno-lint-ignore ban-types + [function_name: string]: Function } export interface RawConfiguration { @@ -56,7 +63,7 @@ export const RAW_CONFIGURATION_SCHEMA: JSONSchemaObject = { type Configuration = { inferenceConfig: InferenceConfig, - programInfo: ProgramInfo, + programSchema: ProgramSchema, } type InferenceConfig = { @@ -75,9 +82,9 @@ export const CAPABILITIES_RESPONSE: sdk.CapabilitiesResponse = { }, }; -type Payload = { +type Payload = { function: string, - args: Struct + args: Struct } @@ -91,7 +98,7 @@ type Payload = { * @param cmdObj * @returns Schema and argument position information */ -export function getInfo(cmdObj: InferenceConfig): ProgramInfo { +export function getProgramSchema(cmdObj: InferenceConfig): ProgramSchema { switch(cmdObj.schemaMode) { /** * The READ option is available in case the user wants to pre-cache their schema during development. @@ -108,15 +115,15 @@ export function getInfo(cmdObj: InferenceConfig): ProgramInfo { } case 'INFER': { console.error(`Inferring schema with map location ${cmdObj.vendorDir}`); - const info = programInfo(cmdObj.functions, cmdObj.vendorDir, cmdObj.preVendor); + const programSchema = inferProgramSchema(cmdObj.functions, cmdObj.vendorDir, cmdObj.preVendor); const schemaLocation = cmdObj.schemaLocation; if(schemaLocation) { console.error(`Writing schema to ${cmdObj.schemaLocation}`); - const infoString = JSON.stringify(info); + const infoString = JSON.stringify(programSchema); // NOTE: Using sync functions should be ok since they're run on startup. Deno.writeTextFileSync(schemaLocation, infoString); } - return info; + return programSchema; } default: throw new Error('Invalid schema-mode. Use READ or INFER.'); @@ -129,42 +136,86 @@ export function getInfo(cmdObj: InferenceConfig): ProgramInfo { * This doesn't catch any exceptions. * * @param functions - * @param positions + * @param function_definitions * @param payload * @returns the result of invocation with no wrapper */ -async function invoke(functions: any, positions: FunctionPositions, payload: Payload): Promise { - const ident = payload.function; - const func = functions[ident as any] as any; - const args = reposition(positions, payload); - - let result = func.apply(null, args); - if (typeof result === "object" && 'then' in result && typeof result.then === "function") { - result = await result; +async function invoke(function_name: string, args: Record, functions: RuntimeFunctions, program_schema: ProgramSchema): Promise { + const func = functions[function_name]; + const prepared_args = prepare_arguments(function_name, args, program_schema.functions, program_schema.object_types); + + try { + let result = func.apply(undefined, prepared_args); + if (typeof result === "object" && 'then' in result && typeof result.then === "function") { + result = await result; + } + return result; + } catch (e) { + throw new sdk.InternalServerError(`Error encountered when invoking function ${function_name}`, { message: e.message, stack: e.stack }); } - return result; } /** * This takes argument position information and a payload of function * and named arguments and returns the correctly ordered arguments ready to be applied. * - * @param functions + * @param function_definitions * @param payload * @returns An array of the function's arguments in the definition order */ -function reposition(functions: FunctionPositions, payload: Payload): Array { - const positions = functions[payload.function]; +export function prepare_arguments(function_name: string, args: Record, function_definitions: FunctionDefinitions, object_type_definitions: ObjectTypeDefinitions): unknown[] { + const function_definition = function_definitions[function_name]; - if(!positions) { - throw new Error(`Couldn't find function ${payload.function} in schema.`); + if(!function_definition) { + throw new sdk.InternalServerError(`Couldn't find function ${function_name} in schema.`); } - return positions.map(k => payload.args[k]); + return function_definition.arguments + .map(argDef => coerce_argument_value(args[argDef.argument_name], argDef.type, [argDef.argument_name], object_type_definitions)); +} + +function coerce_argument_value(value: unknown, type: TypeDefinition, value_path: string[], object_type_definitions: ObjectTypeDefinitions): unknown { + switch (type.type) { + case "array": + if (!isArray(value)) + throw new sdk.BadRequest(`Unexpected value in function arguments. Expected an array at '${value_path.join(".")}'.`); + return value.map((element, index) => coerce_argument_value(element, type.element_type, [...value_path, `[${index}]`], object_type_definitions)) + + case "nullable": + if (value === null) { + return type.null_or_undefinability == NullOrUndefinability.AcceptsUndefinedOnly + ? undefined + : null; + } else if (value === undefined) { + return type.null_or_undefinability == NullOrUndefinability.AcceptsNullOnly + ? null + : undefined; + } else { + return coerce_argument_value(value, type.underlying_type, value_path, object_type_definitions) + } + case "named": + if (type.kind === "scalar") { + // Scalars are currently treated as opaque values, which is a bit dodgy + return value; + } else { + const object_type_definition = object_type_definitions[type.name]; + if (!object_type_definition) + throw new sdk.InternalServerError(`Couldn't find object type '${type.name}' in the schema`); + if (value === null || typeof value !== "object") { + throw new sdk.BadRequest(`Unexpected value in function arguments. Expected an object at '${value_path.join(".")}'.`); + } + return Object.fromEntries(object_type_definition.properties.map(property_definition => { + const prop_value = (value as Record)[property_definition.property_name]; + return [property_definition.property_name, coerce_argument_value(prop_value, property_definition.type, [...value_path, property_definition.property_name], object_type_definitions)] + })); + } + default: + return unreachable(type["type"]); + } } // TODO: https://github.com/hasura/ndc-typescript-deno/issues/26 Do deeper field recursion once that's available -function pruneFields(func: string, fields: Struct | null | undefined, result: Struct): Struct { +function pruneFields(func: string, fields: Struct | null | undefined, result: unknown): unknown { // This seems like a bug to request {} fields when expecting a scalar response... // File with engine? if(!fields || Object.keys(fields).length == 0) { @@ -173,12 +224,16 @@ function pruneFields(func: string, fields: Struct | null | undefin return result; } - const response: Struct = {}; + const response: Struct = {}; + + if (result === null || Array.isArray(result) || typeof result !== "object") { + throw new sdk.InternalServerError(`Function '${func}' did not return an object when expected to`); + } for(const [k,v] of Object.entries(fields)) { switch(v.type) { case 'column': - response[k] = result[v.column]; + response[k] = (result as Record)[v.column] ?? null; // Coalesce undefined into null to ensure we always have a value for a requested column break; default: console.error(`Function ${func} field of type ${v.type} is not supported.`); @@ -191,21 +246,12 @@ function pruneFields(func: string, fields: Struct | null | undefin async function query( configuration: Configuration, state: State, - func: string, + functionName: string, requestArgs: Struct, requestFields?: { [k: string]: sdk.Field; } | null | undefined -): Promise> { - const payload: Payload = { - function: func, - args: requestArgs - }; - try { - const result = await invoke(state.functions, configuration.programInfo.positions, payload); - const pruned = pruneFields(func, requestFields, result); - return pruned; - } catch(e) { - throw new sdk.InternalServerError(`Error encountered when invoking function ${func}`, { message: e.message, stack: e.stack }); - } +): Promise { + const result = await invoke(functionName, requestArgs, state.functions, configuration.programSchema); + return pruneFields(functionName, requestFields, result); } function resolveArguments( @@ -274,15 +320,15 @@ export const connector: sdk.Connector = schemaLocation: configuration.schemaLocation, vendorDir: resolve(configuration.vendor || "./vendor"), }; - const programInfo = getInfo(inferenceConfig); + const programSchema = getProgramSchema(inferenceConfig); return Promise.resolve({ inferenceConfig, - programInfo + programSchema }); }, get_schema(config: Configuration): Promise { - return Promise.resolve(config.programInfo.schema); + return Promise.resolve(get_ndc_schema(config.programSchema)); }, // TODO: https://github.com/hasura/ndc-typescript-deno/issues/28 What do we want explain to do in this scenario? diff --git a/src/infer.ts b/src/infer.ts index 9bb7204..d86e5c6 100644 --- a/src/infer.ts +++ b/src/infer.ts @@ -3,7 +3,7 @@ * This module provides the inference implementation for the connector. * It relies on the Typescript compiler to perform the heavy lifting. * - * The exported function that is intended for use is `programInfo`. + * The exported function that is intended for use is `inferProgramSchema`. * * Dependencies are required to be vendored before invocation. */ @@ -11,31 +11,11 @@ import ts, { FunctionDeclaration, StringLiteralLike } from "npm:typescript@5.1.6"; import { resolve, dirname } from "https://deno.land/std@0.208.0/path/mod.ts"; import { existsSync } from "https://deno.land/std@0.208.0/fs/mod.ts"; -import * as sdk from 'npm:@hasura/ndc-sdk-typescript@1.2.5'; +import { mapObject } from "./util.ts"; +import { FunctionDefinitions, FunctionNdcKind, NullOrUndefinability, ObjectTypeDefinitions, ProgramSchema, ScalarTypeDefinitions, TypeDefinition, listing } from "./schema.ts"; export type Struct = Record; -export type FunctionPositions = Struct> - -export type ProgramInfo = { - schema: sdk.SchemaResponse, - positions: FunctionPositions -} - -function mapObject(obj: {[key: string]: I}, fn: ((key: string, value: I) => [string, O])): {[key: string]: O} { - const keys: Array = Object.keys(obj); - const result: {[key: string]: O} = {}; - for(const k of keys) { - const [k2, v] = fn(k, obj[k]); - result[k2] = v; - } - return result; -} - -function isParameterDeclaration(node: ts.Node | undefined): node is ts.ParameterDeclaration { - return node?.kind === ts.SyntaxKind.Parameter; -} - const scalar_mappings: {[key: string]: string} = { "string": "String", "bool": "Boolean", @@ -46,22 +26,11 @@ const scalar_mappings: {[key: string]: string} = { // "void": "Void", // Void type can be included to permit void types as scalars }; -// NOTE: This should be able to be made read only -const no_ops: sdk.ScalarType = { - aggregate_functions: {}, - comparison_operators: {}, -}; - // TODO: https://github.com/hasura/ndc-typescript-deno/issues/21 Use standard logging from SDK const LOG_LEVEL = Deno.env.get("LOG_LEVEL") || "INFO"; const DEBUG = LOG_LEVEL == 'DEBUG'; const MAX_INFERENCE_RECURSION = 20; // Better to abort than get into an infinite loop, this could be increased if required. -type TypeNames = Array<{ - type: ts.Type, - name: string -}>; - function gql_name(n: string): string { // Construct a GraphQL complient name: https://spec.graphql.org/draft/#sec-Type-Name-Introspection // Check if this is actually required. @@ -71,15 +40,15 @@ function gql_name(n: string): string { function qualify_type_name(root_file: string, t: any, name: string): string { let symbol = t.getSymbol(); - if(! symbol) { + if (!symbol) { try { symbol = t.types[0].getSymbol(); - } catch(e) { - throw new Error(`Couldn't find symbol for type ${name}`); + } catch (e) { + throw new Error(`Couldn't find symbol for type ${name}`, { cause: e }); } } - const locations = symbol.declarations.map((d: any) => d.getSourceFile()); + const locations = symbol.declarations.map((d: ts.Declaration) => d.getSourceFile()); for(const f of locations) { const where = f.fileName; const short = where.replace(dirname(root_file) + '/','').replace(/\.ts$/, ''); @@ -99,13 +68,13 @@ function qualify_type_name(root_file: string, t: any, name: string): string { throw new Error(`Couldn't find any declarations for type ${name}`); } -function validate_type(root_file: string, checker: ts.TypeChecker, object_names: TypeNames, schema_response: sdk.SchemaResponse, name: string, ty: any, depth: number): sdk.Type { +function validate_type(root_file: string, checker: ts.TypeChecker, object_type_definitions: ObjectTypeDefinitions, scalar_type_definitions: ScalarTypeDefinitions, name: string, ty: any, depth: number): TypeDefinition { const type_str = checker.typeToString(ty); const type_name = ty.symbol?.escapedName || ty.intrinsicName || 'unknown_type'; const type_name_lower: string = type_name.toLowerCase(); if(depth > MAX_INFERENCE_RECURSION) { - error(`Schema inference validation exceeded depth ${MAX_INFERENCE_RECURSION} for type ${type_str}`); + throw_error(`Schema inference validation exceeded depth ${MAX_INFERENCE_RECURSION} for type ${type_str}`); } // PROMISE @@ -114,66 +83,70 @@ function validate_type(root_file: string, checker: ts.TypeChecker, object_names: // TODO: promises should not be allowed in parameters if (type_name == "Promise") { const inner_type = ty.resolvedTypeArguments[0]; - const inner_type_result = validate_type(root_file, checker, object_names, schema_response, name, inner_type, depth + 1); + const inner_type_result = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, name, inner_type, depth + 1); return inner_type_result; } // ARRAY if (checker.isArrayType(ty)) { const inner_type = ty.resolvedTypeArguments[0]; - const inner_type_result = validate_type(root_file, checker, object_names, schema_response, `Array_of_${name}`, inner_type, depth + 1); + const inner_type_result = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, `Array_of_${name}`, inner_type, depth + 1); return { type: 'array', element_type: inner_type_result }; } + // NULL OR UNDEFINED TYPES (x | null, x | undefined, x | null | undefined) + const not_nullable_result = unwrap_nullable_type(ty); + if (not_nullable_result !== null) { + const [not_nullable_type, null_or_undefinability] = not_nullable_result; + const not_nullable_type_result = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, `Array_of_${name}`, not_nullable_type, depth + 1); + return { type: "nullable", underlying_type: not_nullable_type_result, null_or_undefinability: null_or_undefinability } + } + // Named SCALAR if (scalar_mappings[type_name_lower]) { const type_name_gql = scalar_mappings[type_name_lower]; - schema_response.scalar_types[type_name_gql] = no_ops; - return { type: 'named', name: type_name_gql }; + scalar_type_definitions[type_name_gql] = {}; + return { type: 'named', name: type_name_gql, kind: "scalar" }; } // OBJECT // TODO: https://github.com/hasura/ndc-typescript-deno/issues/33 There should be a library function that allows us to check this case const info = get_object_type_info(root_file, checker, ty, name); if (info) { - const type_str_qualified = info.type_name; // lookup_type_name(root_file, checker, info, object_names, name, ty); + const type_str_qualified = info.type_name; // Shortcut recursion if the type has already been named - if(schema_response.object_types[type_str_qualified]) { - return { type: 'named', name: type_str_qualified }; + if (object_type_definitions[type_str_qualified]) { + return { type: 'named', name: type_str_qualified, kind: "object" }; } - schema_response.object_types[type_str_qualified] = Object(); // Break infinite recursion - const fields = Object.fromEntries(Array.from(info.members, ([k, field_type]) => { - const field_type_validated = validate_type(root_file, checker, object_names, schema_response, `${name}_field_${k}`, field_type, depth + 1); - return [k, { type: field_type_validated }]; - })); + object_type_definitions[type_str_qualified] = { properties: [] }; // Break infinite recursion + + const properties = Array.from(info.members, ([property_name, property_type]) => { + const property_type_validated = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, `${name}_field_${property_name}`, property_type, depth + 1); + return { property_name, type: property_type_validated }; + }); + + object_type_definitions[type_str_qualified] = { properties } - schema_response.object_types[type_str_qualified] = { fields }; - return { type: 'named', name: type_str_qualified} + return { type: 'named', name: type_str_qualified, kind: "object" } } // TODO: We could potentially support classes, but only as return types, not as function arguments if ((ty.objectFlags & ts.ObjectFlags.Class) !== 0) { console.error(`class types are not supported: ${name}`); - error('validate_type failed'); + throw_error('validate_type failed'); } if (ty === checker.getVoidType()) { console.error(`void functions are not supported: ${name}`); - error('validate_type failed'); + throw_error('validate_type failed'); } - // TODO: https://github.com/hasura/ndc-typescript-deno/issues/58 We should resolve generic type parameters somewhere - // - // else if (ty.constraint) { - // return validate_type(root_file, checker, object_names, schema_response, name, ty.constraint, depth + 1) - // } - // UNHANDLED: Assume that the type is a scalar console.error(`Unable to validate type of ${name}: ${type_str} (${type_name}). Assuming that it is a scalar type.`); - schema_response.scalar_types[name] = no_ops; - return { type: 'named', name }; + scalar_type_definitions[name] = {}; + return { type: 'named', name, kind: "scalar" }; } /** @@ -197,33 +170,14 @@ function pre_vendor(vendorPath: string, filename: string) { console.error(`Error: Got code ${code} during deno vendor operation.`) console.error(`stdout: ${new TextDecoder().decode(stdout)}`); console.error(`stderr: ${new TextDecoder().decode(stderr)}`); - error('pre_vendor failed'); + throw_error('pre_vendor failed'); } } -function error(message: string): never { +function throw_error(message: string): never { throw new Error(message); } -/** - * Logs simple listing of functions/procedures on stderr. - * - * @param prompt - * @param positions - * @param info - */ -function listing(prompt: string, positions: FunctionPositions, info: Array) { - if(info.length > 0) { - console.error(``); - console.error(`${prompt}:`) - for(const f of info) { - const args = (positions[f.name] || []).join(', '); - console.error(`* ${f.name}(${args})`); - } - console.error(``); - } -} - /** * Returns the flags associated with a type. */ @@ -305,7 +259,38 @@ function get_object_type_info(root_file: string, checker: ts.TypeChecker, ty: an return null; } -export function programInfo(filename: string, vendorPath: string, perform_vendor: boolean): ProgramInfo { +function unwrap_nullable_type(ty: ts.Type): [ts.Type, NullOrUndefinability] | null { + if (!ty.isUnion()) return null; + + const isNullable = ty.types.find(is_null_type) !== undefined; + const isUndefined = ty.types.find(is_undefined_type) !== undefined; + const nullOrUndefinability = + isNullable + ? ( isUndefined + ? NullOrUndefinability.AcceptsEither + : NullOrUndefinability.AcceptsNullOnly + ) + : ( isUndefined + ? NullOrUndefinability.AcceptsUndefinedOnly + : null + ); + + const typesWithoutNullAndUndefined = ty.types + .filter(t => !is_null_type(t) && !is_undefined_type(t)); + + return typesWithoutNullAndUndefined.length === 1 && nullOrUndefinability + ? [typesWithoutNullAndUndefined[0], nullOrUndefinability] + : null; +} + +function is_null_type(ty: ts.Type): boolean { + return (ty.flags & ts.TypeFlags.Null) !== 0; +} +function is_undefined_type(ty: ts.Type): boolean { + return (ty.flags & ts.TypeFlags.Undefined) !== 0; +} + +export function inferProgramSchema(filename: string, vendorPath: string, perform_vendor: boolean): ProgramSchema { // TODO: https://github.com/hasura/ndc-typescript-deno/issues/27 This should have already been established upstream const importMapPath = `${vendorPath}/import_map.json`; let pathsMap: {[key: string]: Array} = {}; @@ -359,7 +344,8 @@ export function programInfo(filename: string, vendorPath: string, perform_vendor allowImportingTsExtensions: true, noEmit: true, baseUrl: '.', - paths: pathsMap + paths: pathsMap, + strictNullChecks: true, }; const host = ts.createCompilerHost(compilerOptions); @@ -410,23 +396,15 @@ export function programInfo(filename: string, vendorPath: string, perform_vendor }); if(fatal > 0) { - error(`Fatal errors: ${fatal}`); + throw_error(`Fatal errors: ${fatal}`); } } const checker = program.getTypeChecker(); - const schema_response: sdk.SchemaResponse = { - scalar_types: {}, - object_types: {}, - collections: [], - functions: [], - procedures: [], - }; - - const object_names = [] as TypeNames; - - const positions: FunctionPositions = {}; + const object_type_definitions: ObjectTypeDefinitions = {}; + const function_definitions: FunctionDefinitions = {}; + const scalar_type_definitions: ScalarTypeDefinitions = {}; function isExported(node: FunctionDeclaration): boolean { for(const mod of node.modifiers || []) { @@ -454,7 +432,7 @@ export function programInfo(filename: string, vendorPath: string, perform_vendor const root_file = resolve(filename); - ts.forEachChild(src, (node) => { + ts.forEachChild(src, (node: ts.Node) => { if (ts.isFunctionDeclaration(node)) { const fn_sym = checker.getSymbolAtLocation(node.name!)!; const fn_name = fn_sym.escapedName; @@ -473,70 +451,43 @@ export function programInfo(filename: string, vendorPath: string, perform_vendor const result_type = call.getReturnType(); const result_type_name = `${fn_name}_output`; - const result_type_validated = validate_type(root_file, checker, object_names, schema_response, result_type_name, result_type, 0); - const description = fn_desc ? { description: fn_desc } : {} - - const fn: sdk.FunctionInfo = { - name: node.name!.text, - ...description, - arguments: {}, - result_type: result_type_validated, - }; + const result_type_validated = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, result_type_name, result_type, 0); - positions[fn.name] = []; - - call.parameters.forEach((param) => { + const function_arguments = call.parameters.map(param => { const param_name = param.getName(); const param_desc = ts.displayPartsToString(param.getDocumentationComment(checker)).trim(); const param_type = checker.getTypeOfSymbolAtLocation(param, param.valueDeclaration!); // TODO: https://github.com/hasura/ndc-typescript-deno/issues/34 Use the user's given type name if one exists. const type_name = `${fn_name}_arguments_${param_name}`; - const param_type_validated = validate_type(root_file, checker, object_names, schema_response, type_name, param_type, 0); // E.g. `bio_arguments_username` - const description = param_desc ? { description: param_desc } : {} - - positions[fn.name].push(param_name); - - // TODO: https://github.com/hasura/ndc-typescript-deno/issues/36 - // Creating the structure for optional types should be done by 'validate_type'. - // Perhaps give an 'optional' boolean argument to 'validate_type' constructed in this way for params. - function optionalParameterType(): sdk.Type { - if(param) { - for(const declaration of param.getDeclarations() || []) { - if(isParameterDeclaration(declaration)) { - if(checker.isOptionalParameter(declaration)) { - return { - type: 'nullable', underlying_type: param_type_validated - } - } - } - } - } - return param_type_validated; - } + const param_type_validated = validate_type(root_file, checker, object_type_definitions, scalar_type_definitions, type_name, param_type, 0); // E.g. `bio_arguments_username` + const description = param_desc ? param_desc : null - fn.arguments[param_name] = { - ...description, - type: optionalParameterType(), + return { + argument_name: param_name, + description, + type: param_type_validated, } }); - if(fn_pure) { - schema_response.functions.push(fn); - } else { - schema_response.procedures.push(fn); - } + function_definitions[node.name!.text] = { + ndc_kind: fn_pure ? FunctionNdcKind.Function : FunctionNdcKind.Procedure, + description: fn_desc ? fn_desc : null, + arguments: function_arguments, + result_type: result_type_validated + }; } }); } const result = { - schema: schema_response, - positions + functions: function_definitions, + object_types: object_type_definitions, + scalar_types: scalar_type_definitions, } - listing('Functions', result.positions, result.schema.functions) - listing('Procedures', result.positions, result.schema.procedures) + listing(FunctionNdcKind.Function, result.functions) + listing(FunctionNdcKind.Procedure, result.functions) return result; } diff --git a/src/mod.ts b/src/mod.ts index bd7d129..507c5eb 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -4,7 +4,7 @@ // NOTE: Ensure that sdk matches version in connector.ts import * as commander from 'npm:commander@11.0.0'; -import { programInfo } from './infer.ts' +import { inferProgramSchema } from './infer.ts' import { connector } from './connector.ts' import sdk from 'npm:@hasura/ndc-sdk-typescript@1.2.5'; @@ -12,7 +12,7 @@ const inferCommand = new commander.Command("infer") .argument('', 'TypeScript source entrypoint') .option('-v, --vendor ', 'Vendor location (optional)') .action((entrypoint, cmdObj, _command) => { - const output = programInfo(entrypoint, cmdObj.vendor, cmdObj.preVendor); + const output = inferProgramSchema(entrypoint, cmdObj.vendor, cmdObj.preVendor); console.log(JSON.stringify(output)); }); diff --git a/src/schema.ts b/src/schema.ts new file mode 100644 index 0000000..7669c44 --- /dev/null +++ b/src/schema.ts @@ -0,0 +1,152 @@ +import * as sdk from 'npm:@hasura/ndc-sdk-typescript@1.2.5'; +import { mapObjectValues, unreachable } from "./util.ts"; + +export type ProgramSchema = { + functions: FunctionDefinitions + object_types: ObjectTypeDefinitions + scalar_types: ScalarTypeDefinitions +} + +export type FunctionDefinitions = { + [function_name: string]: FunctionDefinition +} + +export type FunctionDefinition = { + ndc_kind: FunctionNdcKind + description: string | null, + arguments: ArgumentDefinition[] // Function arguments are ordered + result_type: TypeDefinition +} + +export enum FunctionNdcKind { + Function = "Function", + Procedure = "Procedure" +} + +export type ArgumentDefinition = { + argument_name: string, + description: string | null, + type: TypeDefinition +} + +export type ObjectTypeDefinitions = { + [object_type_name: string]: ObjectTypeDefinition +} + +export type ObjectTypeDefinition = { + properties: ObjectPropertyDefinition[] +} + +export type ObjectPropertyDefinition = { + property_name: string, + type: TypeDefinition, +} + +export type ScalarTypeDefinitions = { + [scalar_type_name: string]: ScalarTypeDefinition +} + +export type ScalarTypeDefinition = Record // Empty object, for now + +export type TypeDefinition = ArrayTypeDefinition | NullableTypeDefinition | NamedTypeDefinition + +export type ArrayTypeDefinition = { + type: "array" + element_type: TypeDefinition +} + +export type NullableTypeDefinition = { + type: "nullable", + null_or_undefinability: NullOrUndefinability + underlying_type: TypeDefinition +} + +export type NamedTypeDefinition = { + type: "named" + name: string + kind: "scalar" | "object" +} + +export enum NullOrUndefinability { + AcceptsNullOnly = "AcceptsNullOnly", + AcceptsUndefinedOnly = "AcceptsUndefinedOnly", + AcceptsEither = "AcceptsEither", +} + +export function get_ndc_schema(programInfo: ProgramSchema): sdk.SchemaResponse { + const functions = Object.entries(programInfo.functions); + + const object_types = mapObjectValues(programInfo.object_types, obj_def => { + return { + fields: Object.fromEntries(obj_def.properties.map(prop_def => [prop_def.property_name, { type: convert_type_definition_to_sdk_type(prop_def.type)}])) + } + }); + + const scalar_types = mapObjectValues(programInfo.scalar_types, _scalar_def => { + return { + aggregate_functions: {}, + comparison_operators: {}, + } + }) + + return { + functions: functions + .filter(([_, def]) => def.ndc_kind === FunctionNdcKind.Function) + .map(([name, def]) => convert_function_definition_to_sdk_schema_type(name, def)), + procedures: functions + .filter(([_, def]) => def.ndc_kind === FunctionNdcKind.Procedure) + .map(([name, def]) => convert_function_definition_to_sdk_schema_type(name, def)), + collections: [], + object_types, + scalar_types, + } +} + +function convert_type_definition_to_sdk_type(typeDef: TypeDefinition): sdk.Type { + switch (typeDef.type) { + case "array": return { type: "array", element_type: convert_type_definition_to_sdk_type(typeDef.element_type) } + case "nullable": return { type: "nullable", underlying_type: convert_type_definition_to_sdk_type(typeDef.underlying_type) } + case "named": return { type: "named", name: typeDef.name } + default: return unreachable(typeDef["type"]) + } +} + +function convert_function_definition_to_sdk_schema_type(function_name: string, definition: FunctionDefinition): sdk.FunctionInfo | sdk.ProcedureInfo { + const args = + definition.arguments + .map(arg_def => + [ arg_def.argument_name, + { + type: convert_type_definition_to_sdk_type(arg_def.type), + ...(arg_def.description ? { description: arg_def.description } : {}), + } + ] + ); + + return { + name: function_name, + arguments: Object.fromEntries(args), + result_type: convert_type_definition_to_sdk_type(definition.result_type), + ...(definition.description ? { description: definition.description } : {}), + } +} + +/** + * Logs simple listing of functions/procedures on stderr. + * + * @param prompt + * @param functionDefinitions + * @param info + */ +export function listing(functionNdcKind: FunctionNdcKind, functionDefinitions: FunctionDefinitions) { + const functions = Object.entries(functionDefinitions).filter(([_, def]) => def.ndc_kind === functionNdcKind); + if (functions.length > 0) { + console.error(``); + console.error(`${functionNdcKind}s:`) + for (const [function_name, function_definition] of functions) { + const args = function_definition.arguments.join(', '); + console.error(`* ${function_name}(${args})`); + } + console.error(``); + } +} diff --git a/src/test/classes_test.ts b/src/test/classes_test.ts index b2ab68c..f1e27ff 100644 --- a/src/test/classes_test.ts +++ b/src/test/classes_test.ts @@ -1,13 +1,13 @@ -import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; -import * as infer from '../infer.ts'; +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; // Classes are currently not supoported and should throw an error -Deno.test("Complex Dependency", () => { +Deno.test("Classes", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/classes.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); test.assertThrows(() => { - infer.programInfo(program_path, vendor_path, false); + infer.inferProgramSchema(program_path, vendor_path, false); }) }); diff --git a/src/test/complex_dependency_test.ts b/src/test/complex_dependency_test.ts index 3238b18..425ba61 100644 --- a/src/test/complex_dependency_test.ts +++ b/src/test/complex_dependency_test.ts @@ -1,18 +1,20 @@ -import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; -import * as infer from '../infer.ts'; +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; +import { FunctionNdcKind } from "../schema.ts"; // This program omits its return type and it is inferred via the 'fetch' dependency. Deno.test("Inference on Dependency", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/infinite_loop.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, false); - test.assertEquals(program_results.schema.procedures, [ - { - name: "infinite_loop", - arguments: {}, - result_type: { type: "named", name: "Response" } + const program_schema = infer.inferProgramSchema(program_path, vendor_path, false); + test.assertEquals(program_schema.functions, { + infinite_loop: { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [], + result_type: { type: "named", kind: "object", name: "Response" } } - ]); + }); }); diff --git a/src/test/conflicting_names_test.ts b/src/test/conflicting_names_test.ts index c922a82..9d90fd1 100644 --- a/src/test/conflicting_names_test.ts +++ b/src/test/conflicting_names_test.ts @@ -2,78 +2,73 @@ import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; import * as infer from '../infer.ts'; +import { FunctionNdcKind } from "../schema.ts"; Deno.test("Conflicting Type Names in Imports", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/conflicting_names.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, false); + const program_schema = infer.inferProgramSchema(program_path, vendor_path, false); - test.assertEquals(program_results, { - positions: { - foo: [], + test.assertEquals(program_schema, { + functions: { + "foo": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [], + result_type: { + name: "Foo", + kind: "object", + type: "named", + }, + } }, - schema: { - collections: [], - functions: [], - object_types: { - Foo: { - fields: { - x: { - type: { - name: "Boolean", - type: "named", - }, + object_types: { + Foo: { + properties: [ + { + property_name: "x", + type: { + name: "Boolean", + kind: "scalar", + type: "named", }, - y: { - type: { - name: "conflicting_names_dep_Foo", - type: "named", - }, + }, + { + property_name: "y", + type: { + name: "conflicting_names_dep_Foo", + kind: "object", + type: "named", }, }, - }, - conflicting_names_dep_Foo: { - fields: { - a: { - type: { - name: "String", - type: "named", - }, + ] + }, + conflicting_names_dep_Foo: { + properties: [ + { + property_name: "a", + type: { + name: "String", + kind: "scalar", + type: "named", }, - b: { - type: { - name: "Float", - type: "named", - }, + }, + { + property_name: "b", + type: { + name: "Float", + kind: "scalar", + type: "named", }, }, - }, + ] }, - procedures: [ - { - arguments: {}, - name: "foo", - result_type: { - name: "Foo", - type: "named", - }, - }, - ], - scalar_types: { - Boolean: { - aggregate_functions: {}, - comparison_operators: {}, - }, - Float: { - aggregate_functions: {}, - comparison_operators: {}, - }, - String: { - aggregate_functions: {}, - comparison_operators: {}, - }, - } + }, + scalar_types: { + Boolean: {}, + Float: {}, + String: {}, } }) }); diff --git a/src/test/data/nullable_types.ts b/src/test/data/nullable_types.ts new file mode 100644 index 0000000..0fdb30c --- /dev/null +++ b/src/test/data/nullable_types.ts @@ -0,0 +1,19 @@ + +type MyObject = { + string: string, + nullableString: string | null, + optionalString?: string + undefinedString: string | undefined + nullOrUndefinedString: string | undefined | null +} + +export function test( + myObject: MyObject, + nullableParam: string | null, + undefinedParam: string | undefined, + nullOrUndefinedParam: string | undefined | null, + unionWithNull: string | number | null, + optionalParam?: string +): string | null { + return "test" +} diff --git a/src/test/external_dependencies_test.ts b/src/test/external_dependencies_test.ts index ded9f58..5b14e05 100644 --- a/src/test/external_dependencies_test.ts +++ b/src/test/external_dependencies_test.ts @@ -1,50 +1,41 @@ -import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; -import * as infer from '../infer.ts'; +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; +import { FunctionNdcKind } from "../schema.ts"; // Skipped due to NPM dependency resolution not currently being supported. -Deno.test({ - name: "Inference", - fn() { - const program_path = path.fromFileUrl(import.meta.resolve('./data/external_dependencies.ts')); - const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, true); +Deno.test("External Dependencies", () => { + const program_path = path.fromFileUrl(import.meta.resolve('./data/external_dependencies.ts')); + const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); + const program_schema = infer.inferProgramSchema(program_path, vendor_path, true); - test.assertEquals(program_results, { - positions: { - test_deps: [ - "s" - ] - }, - schema: { - scalar_types: { - String: { - aggregate_functions: {}, - comparison_operators: {}, - }, - }, - object_types: {}, - collections: [], - functions: [], - procedures: [ + test.assertEquals(program_schema, { + scalar_types: { + String: {}, + }, + object_types: {}, + functions: { + "test_deps": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [ { - name: "test_deps", - arguments: { - s: { - type: { - name: "String", - type: "named", - }, - }, - }, - result_type: { + argument_name: "s", + description: null, + type: { name: "String", + kind: "scalar", type: "named", - }, - }, + } + } ], + result_type: { + name: "String", + kind: "scalar", + type: "named", + } } - }); - } + } + }); }); diff --git a/src/test/infer_test.ts b/src/test/infer_test.ts index 1018075..35b7c1b 100644 --- a/src/test/infer_test.ts +++ b/src/test/infer_test.ts @@ -1,66 +1,60 @@ -import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; -import * as infer from '../infer.ts'; +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; +import { FunctionNdcKind, NullOrUndefinability } from "../schema.ts"; Deno.test("Inference", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/program.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, false); + const program_schema = infer.inferProgramSchema(program_path, vendor_path, false); - test.assertEquals(program_results, { - positions: { - add: [ - "a", - "b", - ], - hello: [], + test.assertEquals(program_schema, { + scalar_types: { + Float: {}, + String: {}, }, - schema: { - scalar_types: { - Float: { - aggregate_functions: {}, - comparison_operators: {}, - }, - String: { - aggregate_functions: {}, - comparison_operators: {}, - }, + object_types: {}, + functions: { + "hello": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [], + result_type: { + name: "String", + kind: "scalar", + type: "named", + } }, - object_types: {}, - collections: [], - functions: [], - procedures: [ - { - arguments: {}, - name: "hello", - result_type: { - name: "String", - type: "named", - }, - }, - { - arguments: { - a: { - type: { - name: "Float", - type: "named", - }, - }, - b: { - type: { - name: "Float", - type: "named", - }, - }, - }, - name: "add", - result_type: { - name: "Float", - type: "named", + "add": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [ + { + argument_name: "a", + description: null, + type: { + name: "Float", + kind: "scalar", + type: "named", + } }, - }, - ], + { + argument_name: "b", + description: null, + type: { + name: "Float", + kind: "scalar", + type: "named", + } + } + ], + result_type: { + name: "Float", + kind: "scalar", + type: "named", + } + } } }); }); @@ -68,85 +62,86 @@ Deno.test("Inference", () => { Deno.test("Complex Inference", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/complex.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, true); + const program_schema = infer.inferProgramSchema(program_path, vendor_path, true); - test.assertEquals(program_results, { - positions: { - complex: [ - "a", - "b", - "c", - ], - }, - schema: { - collections: [], - functions: [], - object_types: { - Result: { - fields: { - bod: { - type: { - name: "String", - type: "named", - }, - }, - num: { - type: { - name: "Float", - type: "named", - }, - }, - str: { - type: { + test.assertEquals(program_schema, { + functions: { + "complex": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [ + { + argument_name: "a", + description: null, + type: { + name: "Float", + kind: "scalar", + type: "named", + } + }, + { + argument_name: "b", + description: null, + type: { + name: "Float", + kind: "scalar", + type: "named", + } + }, + { + argument_name: "c", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { name: "String", + kind: "scalar", type: "named", - }, - }, - }, + } + } + } + ], + result_type: { + name: "Result", + kind: "object", + type: "named", }, - }, - procedures: [ - { - arguments: { - a: { - type: { - name: "Float", - type: "named", - }, - }, - b: { - type: { - name: "Float", - type: "named", - }, + } + }, + object_types: { + Result: { + properties: [ + { + property_name: "num", + type: { + name: "Float", + kind: "scalar", + type: "named", }, - c: { - type: { - type: "nullable", - underlying_type: { - name: "String", - type: "named", - }, - }, + }, + { + property_name: "str", + type: { + name: "String", + kind: "scalar", + type: "named", }, }, - name: "complex", - result_type: { - name: "Result", - type: "named", + { + property_name: "bod", + type: { + name: "String", + kind: "scalar", + type: "named", + }, }, - }, - ], - scalar_types: { - Float: { - aggregate_functions: {}, - comparison_operators: {}, - }, - String: { - aggregate_functions: {}, - comparison_operators: {}, - }, - } + ] + }, + }, + scalar_types: { + Float: {}, + String: {}, } }); -}); \ No newline at end of file +}); diff --git a/src/test/inline_types_test.ts b/src/test/inline_types_test.ts index 29a0c42..c026d36 100644 --- a/src/test/inline_types_test.ts +++ b/src/test/inline_types_test.ts @@ -1,75 +1,66 @@ -import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; -import * as infer from '../infer.ts'; +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; +import { FunctionNdcKind } from "../schema.ts"; -Deno.test({ name: "Type Parameters", - ignore: false, - fn() { +Deno.test("Inline Types", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/inline_types.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, false); + const program_schema = infer.inferProgramSchema(program_path, vendor_path, false); - // TODO: Currently broken since parameters aren't normalised - - test.assertEquals(program_results, + test.assertEquals(program_schema, { - "schema": { - "scalar_types": { - "String": { - "aggregate_functions": {}, - "comparison_operators": {}, - }, - "Float": { - "aggregate_functions": {}, - "comparison_operators": {}, - } - }, - "object_types": { - "bar_arguments_x": { - "fields": { - "a": { - "type": { - "type": "named", - "name": "Float" - } - }, - "b": { - "type": { - "type": "named", - "name": "String" - } + scalar_types: { + String: {}, + Float: {} + }, + functions: { + "bar": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [ + { + argument_name: "x", + description: null, + type: { + type: "named", + kind: "object", + name: "bar_arguments_x" } } + ], + result_type: { + type: "named", + kind: "scalar", + name: "String" } - }, - "collections": [], - "functions": [], - "procedures": [ - { - "name": "bar", - "arguments": { - "x": { - "type": { - "type": "named", - "name": "bar_arguments_x" - } + } + }, + object_types: { + "bar_arguments_x": { + properties: [ + { + property_name: "a", + type: { + type: "named", + kind: "scalar", + name: "Float" } }, - "result_type": { - "type": "named", - "name": "String" - } - } - ] - }, - "positions": { - "bar": [ - "x" - ] + { + property_name: "b", + type: { + type: "named", + kind: "scalar", + name: "String" + } + }, + ] + } } } ); -}}); \ No newline at end of file +}); diff --git a/src/test/ndc_schema_test.ts b/src/test/ndc_schema_test.ts new file mode 100644 index 0000000..66bc01f --- /dev/null +++ b/src/test/ndc_schema_test.ts @@ -0,0 +1,171 @@ +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import { FunctionNdcKind, NullOrUndefinability, ProgramSchema, get_ndc_schema } from "../schema.ts"; + +Deno.test("NDC Schema Generation", () => { + const program_schema: ProgramSchema = { + functions: { + "test_proc": { + arguments: [ + { + argument_name: "nullableParam", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + ], + description: null, + ndc_kind: FunctionNdcKind.Procedure, + result_type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + } + }, + }, + "test_func": { + arguments: [ + { + argument_name: "myObject", + description: null, + type: { + kind: "object", + name: "MyObject", + type: "named", + }, + }, + ], + description: null, + ndc_kind: FunctionNdcKind.Function, + result_type: { + type: "array", + element_type: { + kind: "scalar", + name: "String", + type: "named", + } + }, + }, + }, + object_types: { + "MyObject": { + properties: [ + { + property_name: "string", + type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + { + property_name: "nullableString", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + ], + }, + }, + scalar_types: { + String: {}, + test_arguments_unionWithNull: {}, + }, + }; + + const schema_response = get_ndc_schema(program_schema) + + test.assertEquals(schema_response, { + collections: [], + functions: [ + { + name: "test_func", + arguments: { + "myObject": { + type: { + name: "MyObject", + type: "named", + }, + }, + }, + result_type: { + type: "array", + element_type: { + name: "String", + type: "named", + } + }, + }, + ], + procedures: [ + { + name: "test_proc", + arguments: { + "nullableParam": { + type: { + type: "nullable", + underlying_type: { + name: "String", + type: "named", + }, + }, + }, + }, + result_type: { + type: "nullable", + underlying_type: { + name: "String", + type: "named", + } + }, + } + ], + object_types: { + "MyObject": { + fields: { + "string": { + type: { + name: "String", + type: "named", + }, + }, + "nullableString": { + type: { + type: "nullable", + underlying_type: { + name: "String", + type: "named", + }, + }, + }, + }, + }, + }, + scalar_types: { + String: { + aggregate_functions: {}, + comparison_operators: {} + }, + test_arguments_unionWithNull: { + aggregate_functions: {}, + comparison_operators: {} + }, + } + }); + +}); diff --git a/src/test/nullable_types_test.ts b/src/test/nullable_types_test.ts new file mode 100644 index 0000000..1f5950a --- /dev/null +++ b/src/test/nullable_types_test.ts @@ -0,0 +1,166 @@ +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; +import { FunctionNdcKind, NullOrUndefinability } from "../schema.ts"; + +Deno.test("Nullable Types", () => { + const program_path = path.fromFileUrl(import.meta.resolve('./data/nullable_types.ts')); + const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); + const program_schema = infer.inferProgramSchema(program_path, vendor_path, false); + + test.assertEquals(program_schema, { + functions: { + "test": { + arguments: [ + { + argument_name: "myObject", + description: null, + type: { + kind: "object", + name: "MyObject", + type: "named", + }, + }, + { + argument_name: "nullableParam", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + { + argument_name: "undefinedParam", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + { + argument_name: "nullOrUndefinedParam", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsEither, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + { + argument_name: "unionWithNull", + description: null, + type: { + kind: "scalar", + name: "test_arguments_unionWithNull", + type: "named", + }, + }, + { + argument_name: "optionalParam", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + ], + description: null, + ndc_kind: FunctionNdcKind.Procedure, + result_type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + } + }, + }, + }, + object_types: { + "MyObject": { + properties: [ + { + property_name: "string", + type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + { + property_name: "nullableString", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + { + property_name: "optionalString", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + { + property_name: "undefinedString", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + { + property_name: "nullOrUndefinedString", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsEither, + underlying_type: { + kind: "scalar", + name: "String", + type: "named", + }, + }, + }, + ], + }, + }, + scalar_types: { + String: {}, + test_arguments_unionWithNull: {}, + }, + }); +}); diff --git a/src/test/pg_dep_test.ts b/src/test/pg_dep_test.ts index 1eef5cd..26cb199 100644 --- a/src/test/pg_dep_test.ts +++ b/src/test/pg_dep_test.ts @@ -1,100 +1,94 @@ -import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; -import * as infer from '../infer.ts'; +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; +import { FunctionNdcKind } from "../schema.ts"; // NOTE: It would be good to have explicit timeout for this // See: https://github.com/denoland/deno/issues/11133 // Test bug: https://github.com/hasura/ndc-typescript-deno/issues/45 -Deno.test("Inferred Dependency Based Result Type", () => { +Deno.test("Postgres NPM Dependency", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/pg_dep.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, true); + const program_schema = infer.inferProgramSchema(program_path, vendor_path, true); - test.assertEquals(program_results, { - positions: { - delete_todos: [ - "todo_id", - ], - insert_todos: [ - "user_id", - "todo", - ], - insert_user: [ - "user_name", - ], - }, - schema: { - collections: [], - functions: [], - procedures: [ - { - arguments: { - user_name: { - type: { - name: "String", - type: "named", - }, - }, - }, - name: "insert_user", - result_type: { - name: "insert_user_output", - type: "named", - }, - }, - { - arguments: { - todo: { - type: { - name: "String", - type: "named", - }, - }, - user_id: { - type: { - name: "String", - type: "named", - }, - }, - }, - name: "insert_todos", - result_type: { - name: "insert_todos_output", - type: "named", - }, - }, - { - arguments: { - todo_id: { - type: { - name: "String", - type: "named", - }, - }, + test.assertEquals(program_schema, { + functions: { + "insert_user": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [ + { + argument_name: "user_name", + description: null, + type: { + type: "named", + kind: "scalar", + name: "String" + } + } + ], + result_type: { + type: "named", + kind: "scalar", + name: "insert_user_output" + } + }, + "insert_todos": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [ + { + argument_name: "user_id", + description: null, + type: { + type: "named", + kind: "scalar", + name: "String" + } }, - name: "delete_todos", - result_type: { - name: "String", - type: "named", + { + argument_name: "todo", + description: null, + type: { + type: "named", + kind: "scalar", + name: "String" + } }, - }, - ], - scalar_types: { - String: { - aggregate_functions: {}, - comparison_operators: {}, - }, - insert_todos_output: { - aggregate_functions: {}, - comparison_operators: {}, - }, - insert_user_output: { - aggregate_functions: {}, - comparison_operators: {}, - }, + ], + result_type: { + type: "named", + kind: "scalar", + name: "insert_todos_output" + } + }, + "delete_todos": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [ + { + argument_name: "todo_id", + description: null, + type: { + type: "named", + kind: "scalar", + name: "String" + } + } + ], + result_type: { + type: "named", + kind: "scalar", + name: "String" + } }, - object_types: {}, - } + }, + scalar_types: { + String: {}, + insert_todos_output: {}, + insert_user_output: {}, + }, + object_types: {}, }); }); diff --git a/src/test/prepare_arguments_test.ts b/src/test/prepare_arguments_test.ts new file mode 100644 index 0000000..d7384a7 --- /dev/null +++ b/src/test/prepare_arguments_test.ts @@ -0,0 +1,319 @@ +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as connector from '../connector.ts'; +import { FunctionDefinitions, FunctionNdcKind, NullOrUndefinability, ObjectTypeDefinitions } from "../schema.ts"; + +Deno.test("argument ordering", () => { + const function_name = "test_fn" + const function_definitions: FunctionDefinitions = { + [function_name]: { + ndc_kind: FunctionNdcKind.Function, + description: null, + arguments: [ + { + argument_name: "c", + description: null, + type: { + type: "named", + kind: "scalar", + name: "Float" + } + }, + { + argument_name: "a", + description: null, + type: { + type: "named", + kind: "scalar", + name: "Float" + } + }, + { + argument_name: "b", + description: null, + type: { + type: "named", + kind: "scalar", + name: "Float" + } + }, + ], + result_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + const object_types: ObjectTypeDefinitions = {} + const args = { + b: 1, + a: 2, + c: 3, + } + + const prepared_args = connector.prepare_arguments(function_name, args, function_definitions, object_types); + + test.assertEquals(prepared_args, [ 3, 2, 1 ]); +}) + +Deno.test("nullable type coercion", async t => { + const function_name = "test_fn" + const function_definitions: FunctionDefinitions = { + [function_name]: { + ndc_kind: FunctionNdcKind.Function, + description: null, + arguments: [ + { + argument_name: "nullOnlyArg", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + argument_name: "undefinedOnlyArg", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + argument_name: "nullOrUndefinedArg", + description: null, + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsEither, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + argument_name: "objectArg", + description: null, + type: { + type: "named", + kind: "object", + name: "MyObject", + } + }, + { + argument_name: "nullOnlyArrayArg", + description: null, + type: { + type: "array", + element_type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + }, + { + argument_name: "undefinedOnlyArrayArg", + description: null, + type: { + type: "array", + element_type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + }, + { + argument_name: "nullOrUndefinedArrayArg", + description: null, + type: { + type: "array", + element_type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsEither, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + }, + ], + result_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + const object_types: ObjectTypeDefinitions = { + "MyObject": { + properties: [ + { + property_name: "nullOnlyProp", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsNullOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + property_name: "undefinedOnlyProp", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsUndefinedOnly, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + }, + { + property_name: "nullOrUndefinedProp", + type: { + type: "nullable", + null_or_undefinability: NullOrUndefinability.AcceptsEither, + underlying_type: { + type: "named", + kind: "scalar", + name: "String" + } + } + } + ] + } + } + const test_cases = [ + { + name: "all nulls", + args: { + nullOnlyArg: null, + undefinedOnlyArg: null, + nullOrUndefinedArg: null, + objectArg: { + nullOnlyProp: null, + undefinedOnlyProp: null, + nullOrUndefinedProp: null, + }, + nullOnlyArrayArg: [null, null], + undefinedOnlyArrayArg: [null, null], + nullOrUndefinedArrayArg: [null, null], + }, + expected: [ + null, + undefined, + null, + { nullOnlyProp: null, undefinedOnlyProp: undefined, nullOrUndefinedProp: null }, + [null, null], + [undefined, undefined], + [null, null], + ] + }, + { + name: "all undefineds", + args: { + nullOnlyArg: undefined, + undefinedOnlyArg: undefined, + nullOrUndefinedArg: undefined, + objectArg: { + nullOnlyProp: undefined, + undefinedOnlyProp: undefined, + nullOrUndefinedProp: undefined, + }, + nullOnlyArrayArg: [undefined, undefined], + undefinedOnlyArrayArg: [undefined, undefined], + nullOrUndefinedArrayArg: [undefined, undefined], + }, + expected: [ + null, + undefined, + undefined, + { nullOnlyProp: null, undefinedOnlyProp: undefined, nullOrUndefinedProp: undefined }, + [null, null], + [undefined, undefined], + [undefined, undefined], + ] + }, + { + name: "all missing", + args: { + objectArg: {}, + nullOnlyArrayArg: [], + undefinedOnlyArrayArg: [], + nullOrUndefinedArrayArg: [], + }, + expected: [ + null, + undefined, + undefined, + { nullOnlyProp: null, undefinedOnlyProp: undefined, nullOrUndefinedProp: undefined }, + [], + [], + [], + ] + }, + { + name: "all valued", + args: { + nullOnlyArg: "a", + undefinedOnlyArg: "b", + nullOrUndefinedArg: "c", + objectArg: { + nullOnlyProp: "d", + undefinedOnlyProp: "e", + nullOrUndefinedProp: "f", + }, + nullOnlyArrayArg: ["g", "h"], + undefinedOnlyArrayArg: ["i", "j"], + nullOrUndefinedArrayArg: ["k", "l"], + }, + expected: [ + "a", + "b", + "c", + { nullOnlyProp: "d", undefinedOnlyProp: "e", nullOrUndefinedProp: "f" }, + ["g", "h"], + ["i", "j"], + ["k", "l"], + ] + }, + ]; + + await Promise.all(test_cases.map(test_case => t.step({ + name: test_case.name, + fn: () => { + const prepared_args = connector.prepare_arguments(function_name, test_case.args, function_definitions, object_types); + test.assertEquals(prepared_args, test_case.expected); + }, + sanitizeOps: false, + sanitizeResources: false, + sanitizeExit: false, + }))); + +}) diff --git a/src/test/recursive_types_test.ts b/src/test/recursive_types_test.ts index 5000b37..32ede89 100644 --- a/src/test/recursive_types_test.ts +++ b/src/test/recursive_types_test.ts @@ -2,57 +2,54 @@ import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; import * as infer from '../infer.ts'; +import { FunctionNdcKind } from "../schema.ts"; Deno.test("Recursive Types", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/recursive.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, false); + const program_schema = infer.inferProgramSchema(program_path, vendor_path, false); - test.assertEquals(program_results, { - positions: { - bar: [], + test.assertEquals(program_schema, { + functions: { + "bar": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [], + result_type: { + type: "named", + kind: "object", + name: "Foo" + } + } }, - schema: { - collections: [], - functions: [], - object_types: { - Foo: { - fields: { - a: { - type: { - name: "Float", - type: "named", - }, - }, - b: { - type: { - element_type: { - name: "Foo", - type: "named", - }, - type: "array", - }, - }, - }, - }, - }, - procedures: [ - { - arguments: {}, - name: "bar", - result_type: { - name: "Foo", - type: "named", + object_types: { + Foo: { + properties: [ + { + property_name: "a", + type: { + type: "named", + kind: "scalar", + name: "Float" + } }, - }, - ], - scalar_types: { - Float: { - aggregate_functions: {}, - comparison_operators: {}, - }, + { + property_name: "b", + type: { + type: "array", + element_type: { + type: "named", + kind: "object", + name: "Foo" + } + } + } + ] }, - } + }, + scalar_types: { + Float: {}, + }, }); }); diff --git a/src/test/type_parameters_test.ts b/src/test/type_parameters_test.ts index 08c712e..2198add 100644 --- a/src/test/type_parameters_test.ts +++ b/src/test/type_parameters_test.ts @@ -1,80 +1,76 @@ -import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; -import * as infer from '../infer.ts'; +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; +import { FunctionNdcKind } from "../schema.ts"; -Deno.test({ name: "Type Parameters", - ignore: false, - fn() { +Deno.test("Type Parameters", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/type_parameters.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, false); + const program_schema = infer.inferProgramSchema(program_path, vendor_path, false); // TODO: Currently broken since parameters aren't normalised - test.assertEquals(program_results, { - positions: { - bar: [], + test.assertEquals(program_schema, { + functions: { + "bar": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [], + result_type: { + name: "Bar", + kind: "object", + type: "named", + } + } }, - schema: { - collections: [], - functions: [], - object_types: { - "Bar": { - fields: { - x: { - type: { - name: "Float", - type: "named", - }, - }, - y: { - type: { - name: "Foo", - type: "named", - }, - }, + object_types: { + "Bar": { + properties: [ + { + property_name: "x", + type: { + type: "named", + kind: "scalar", + name: "Float" + } }, - }, - "Foo": { - fields: { - a: { - type: { - name: "Float", - type: "named", - }, - }, - b: { - type: { - name: "String", - type: "named", - }, - }, + { + property_name: "y", + type: { + type: "named", + kind: "object", + name: "Foo" + } }, - }, + ] }, - scalar_types: { - Float: { - aggregate_functions: {}, - comparison_operators: {}, - }, - String: { - aggregate_functions: {}, - comparison_operators: {}, - }, - }, - procedures: [ - { - arguments: {}, - name: "bar", - result_type: { - name: "Bar", - type: "named", + "Foo": { + properties: [ + { + property_name: "a", + type: { + type: "named", + kind: "scalar", + name: "Float" + } + }, + { + property_name: "b", + type: { + type: "named", + kind: "scalar", + name: "String" + } }, - }, - ], - } + ] + }, + }, + scalar_types: { + Float: {}, + String: {}, + }, }); -}}); \ No newline at end of file +}); diff --git a/src/test/validation_algorithm_test.ts b/src/test/validation_algorithm_test.ts index 9c238dd..b9414ef 100644 --- a/src/test/validation_algorithm_test.ts +++ b/src/test/validation_algorithm_test.ts @@ -1,245 +1,282 @@ -import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; -import * as infer from '../infer.ts'; +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; +import { FunctionNdcKind } from "../schema.ts"; -Deno.test({ name: "Type Parameters", - ignore: false, - fn() { +Deno.test("Validation Algorithm", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/validation_algorithm_update.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); - const program_results = infer.programInfo(program_path, vendor_path, false); + const program_results = infer.inferProgramSchema(program_path, vendor_path, false); test.assertEquals(program_results, { - positions: { - bar: [ - "string", - "aliasedString", - "genericScalar", - "array", - "promise", - "anonObj", - "aliasedObj", - "genericAliasedObj", - "interfce", - "genericInterface", - "aliasedIntersectionObj", - "anonIntersectionObj", - "genericIntersectionObj", - ], - }, - schema: { - collections: [], - functions: [], - object_types: { - "GenericBar": { - fields: { - data: { - type: { - name: "String", - type: "named", - }, - }, + functions: { + "bar": { + ndc_kind: FunctionNdcKind.Procedure, + description: null, + arguments: [ + { + argument_name: "string", + description: null, + type: { + type: "named", + kind: "scalar", + name: "String" + } }, - }, - "GenericIntersectionObject": { - fields: { - data: { - type: { - name: "String", - type: "named", - }, - }, - test: { - type: { - name: "String", - type: "named", - }, - }, + { + argument_name: "aliasedString", + description: null, + type: { + type: "named", + kind: "scalar", + name: "String" + } }, - }, - Bar: { - fields: { - test: { - type: { - name: "String", - type: "named", - }, - }, + { + argument_name: "genericScalar", + description: null, + type: { + type: "named", + kind: "scalar", + name: "String" + } }, - }, - IGenericThing: { - fields: { - data: { - type: { - name: "String", + { + argument_name: "array", + description: null, + type: { + type: "array", + element_type: { type: "named", - }, - }, + kind: "scalar", + name: "String" + } + } }, - }, - IThing: { - fields: { - prop: { - type: { - name: "String", - type: "named", - }, - }, + { + argument_name: "promise", + description: null, + type: { + type: "named", + kind: "scalar", + name: "String" + } }, - }, - IntersectionObject: { - fields: { - test: { - type: { - name: "String", - type: "named", - }, - }, - wow: { - type: { - name: "String", - type: "named", - }, - }, + { + argument_name: "anonObj", + description: null, + type: { + type: "named", + kind: "object", + name: "bar_arguments_anonObj" + } }, - }, - bar_arguments_anonIntersectionObj: { - fields: { - num: { - type: { - name: "Float", - type: "named", - }, - }, - test: { - type: { - name: "String", - type: "named", - }, - }, + { + argument_name: "aliasedObj", + description: null, + type: { + type: "named", + kind: "object", + name: "Bar" + } }, - }, - bar_arguments_anonObj: { - fields: { - a: { - type: { - name: "Float", - type: "named", - }, - }, - b: { - type: { - name: "String", - type: "named", - }, + { + argument_name: "genericAliasedObj", + description: null, + type: { + type: "named", + kind: "object", + name: "GenericBar" + } + }, + { + argument_name: "interfce", + description: null, + type: { + type: "named", + kind: "object", + name: "IThing" + } + }, + { + argument_name: "genericInterface", + description: null, + type: { + type: "named", + kind: "object", + name: "IGenericThing" + } + }, + { + argument_name: "aliasedIntersectionObj", + description: null, + type: { + type: "named", + kind: "object", + name: "IntersectionObject" + } + }, + { + argument_name: "anonIntersectionObj", + description: null, + type: { + type: "named", + kind: "object", + name: "bar_arguments_anonIntersectionObj" + } + }, + { + argument_name: "genericIntersectionObj", + description: null, + type: { + type: "named", + kind: "object", + name: "GenericIntersectionObject" + } + }, + ], + result_type: { + name: "String", + kind: "scalar", + type: "named", + } + } + }, + object_types: { + "GenericBar": { + properties: [ + { + property_name: "data", + type: { + name: "String", + kind: "scalar", + type: "named", }, }, - }, + ], }, - procedures: [ - { - arguments: { - aliasedIntersectionObj: { - type: { - name: "IntersectionObject", - type: "named", - }, + "GenericIntersectionObject": { + properties: [ + { + property_name: "data", + type: { + name: "String", + kind: "scalar", + type: "named", }, - aliasedObj: { - type: { - name: "Bar", - type: "named", - }, - }, - aliasedString: { - type: { - name: "String", - type: "named", - }, - }, - anonIntersectionObj: { - type: { - name: "bar_arguments_anonIntersectionObj", - type: "named", - }, + }, + { + property_name: "test", + type: { + name: "String", + kind: "scalar", + type: "named", }, - anonObj: { - type: { - name: "bar_arguments_anonObj", - type: "named", - }, + }, + ], + }, + Bar: { + properties: [ + { + property_name: "test", + type: { + name: "String", + kind: "scalar", + type: "named", }, - array: { - type: { - element_type: { - name: "String", - type: "named", - }, - type: "array", - }, + }, + ], + }, + IGenericThing: { + properties: [ + { + property_name: "data", + type: { + name: "String", + kind: "scalar", + type: "named", }, - genericAliasedObj: { - type: { - name: "GenericBar", - type: "named", - }, + }, + ], + }, + IThing: { + properties: [ + { + property_name: "prop", + type: { + name: "String", + kind: "scalar", + type: "named", }, - genericInterface: { - type: { - name: "IGenericThing", - type: "named", - }, + }, + ], + }, + IntersectionObject: { + properties: [ + { + property_name: "wow", + type: { + name: "String", + kind: "scalar", + type: "named", }, - genericIntersectionObj: { - type: { - name: "GenericIntersectionObject", - type: "named", - }, + }, + { + property_name: "test", + type: { + name: "String", + kind: "scalar", + type: "named", }, - genericScalar: { - type: { - name: "String", - type: "named", - }, + }, + ], + }, + bar_arguments_anonIntersectionObj: { + properties: [ + { + property_name: "num", + type: { + name: "Float", + kind: "scalar", + type: "named", }, - interfce: { - type: { - name: "IThing", - type: "named", - }, + }, + { + property_name: "test", + type: { + name: "String", + kind: "scalar", + type: "named", }, - promise: { - type: { - name: "String", - type: "named", - }, + }, + ], + }, + bar_arguments_anonObj: { + properties: [ + { + property_name: "a", + type: { + name: "Float", + kind: "scalar", + type: "named", }, - string: { - type: { - name: "String", - type: "named", - }, + }, + { + property_name: "b", + type: { + name: "String", + kind: "scalar", + type: "named", }, }, - name: "bar", - result_type: { - name: "String", - type: "named", - }, - }, - ], - scalar_types: { - Float: { - aggregate_functions: {}, - comparison_operators: {}, - }, - String: { - aggregate_functions: {}, - comparison_operators: {}, - }, - } + ], + }, + }, + scalar_types: { + Float: {}, + String: {}, } }); -}}); +}); diff --git a/src/test/void_test.ts b/src/test/void_test.ts index 88e279c..a31a52e 100644 --- a/src/test/void_test.ts +++ b/src/test/void_test.ts @@ -1,15 +1,15 @@ -import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; -import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; -import * as infer from '../infer.ts'; +import * as test from "https://deno.land/std@0.208.0/assert/mod.ts"; +import * as path from "https://deno.land/std@0.208.0/path/mod.ts"; +import * as infer from '../infer.ts'; // NOTE: It would be good to have explicit timeout for this // See: https://github.com/denoland/deno/issues/11133 // Test bug: https://github.com/hasura/ndc-typescript-deno/issues/45 -Deno.test("Inferred Dependency Based Result Type", () => { +Deno.test("Void", () => { const program_path = path.fromFileUrl(import.meta.resolve('./data/void_types.ts')); const vendor_path = path.fromFileUrl(import.meta.resolve('./vendor')); test.assertThrows(() => { - infer.programInfo(program_path, vendor_path, false); + infer.inferProgramSchema(program_path, vendor_path, false); }) }); diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..fe1b612 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,13 @@ +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[] { + return Array.isArray(x); +} + +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)])); +} + +export function mapObject(obj: {[key: string]: I}, fn: ((key: string, value: I) => [string, O])): {[key: string]: O} { + return Object.fromEntries(Object.entries(obj).map(([prop, val]) => fn(prop,val))); +}