Skip to content

Commit

Permalink
fix: apply from context (#616)
Browse files Browse the repository at this point in the history
Fix type validators for apply from context.

#### Motivation and context

Bug.

#### Migration notes

_N/A_

### Checklist

- [x] The change come with new or modified tests
- [ ] Hard-to-understand functions have explanatory comments
- [ ] End-user documentation is updated to reflect the change
  • Loading branch information
Natoandro committed Mar 7, 2024
1 parent 1cb26cf commit 04c7565
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 70 deletions.
97 changes: 49 additions & 48 deletions typegate/src/engine/planner/parameter_transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { QueryFn, QueryFunction } from "../../libs/jsonpath.ts";
import { TypeGraph } from "../../typegraph/mod.ts";
import { Type } from "../../typegraph/type_node.ts";
import { ParameterTransformNode } from "../../typegraph/types.ts";
import { generateBooleanValidator } from "../typecheck/inline_validators/boolean.ts";
import { ValidationContext, validationContext } from "../typecheck/common.ts";
import { generateListValidator } from "../typecheck/inline_validators/list.ts";
import { generateNumberValidator } from "../typecheck/inline_validators/number.ts";
import {
generateObjectValidator,
getKeys,
} from "../typecheck/inline_validators/object.ts";
import { generateStringValidator } from "../typecheck/inline_validators/string.ts";
import { InputValidationCompiler } from "../typecheck/input.ts";

export type TransformParamsInput = {
args: Record<string, any>;
Expand All @@ -35,7 +36,10 @@ type CompiledTransformerInput = {
};

type CompiledTransformer = {
(input: CompiledTransformerInput): Record<string, any>;
(
input: CompiledTransformerInput,
contxt: ValidationContext,
): Record<string, any>;
};

export function compileParameterTransformer(
Expand All @@ -45,10 +49,10 @@ export function compileParameterTransformer(
): TransformParams {
const ctx = new TransformerCompilationContext(typegraph, parentProps);
const { fnBody, deps } = ctx.compile(transformerTreeRoot);
const fn = new Function("input", fnBody) as CompiledTransformer;
const fn = new Function("input", "context", fnBody) as CompiledTransformer;
return ({ args, context, parent }) => {
const getContext = compileContextQueries(deps.contexts)(context);
const res = fn({ args, getContext, parent });
const res = fn({ args, getContext, parent }, validationContext);
return res;
};
}
Expand Down Expand Up @@ -105,10 +109,16 @@ class TransformerCompilationContext {
nonStrictMode: new Set(),
},
};
#inputValidatorCompiler: InputValidationCompiler;
#typesWithCustomValidator: Set<number> = new Set();

constructor(typegraph: TypeGraph, parentProps: Record<string, number>) {
this.#tg = typegraph;
this.#parentProps = parentProps;
this.#inputValidatorCompiler = new InputValidationCompiler(
typegraph,
(idx) => `validate_${idx}`,
);
}

#reset() {
Expand All @@ -127,8 +137,11 @@ class TransformerCompilationContext {
this.#reset();
const varName = this.#compileNode(rootNode);
this.#collector.push(`return ${varName};`);
const customValidators = [...this.#typesWithCustomValidator]
.map((idx) => this.#inputValidatorCompiler.codes.get(idx))
.join("\n");
const res = {
fnBody: this.#collector.join("\n"),
fnBody: customValidators + this.#collector.join("\n"),
deps: this.#dependencies,
};

Expand Down Expand Up @@ -232,7 +245,7 @@ class TransformerCompilationContext {
const varName = this.#createVarName();
let typeNode = this.#tg.type(typeIdx);
let optional = false;
if (typeNode.type === Type.OPTIONAL) {
while (typeNode.type === Type.OPTIONAL) {
typeNode = this.#tg.type(typeNode.item);
optional = true;
}
Expand All @@ -248,45 +261,24 @@ class TransformerCompilationContext {

this.#collector.push(`if (${varName} != null) {`);

switch (typeNode.type) {
case Type.OPTIONAL:
throw new Error(`At "${path}": nested optional not supported`);
case Type.INTEGER: {
const parsedVar = this.#createVarName();
this.#collector.push(
`const ${parsedVar} = parseInt(${varName}, 10);`,
...generateNumberValidator(typeNode, parsedVar, path),
);
break;
}
case Type.FLOAT: {
const parsedVar = this.#createVarName();
this.#collector.push(
`const ${parsedVar} = parseFloat(${varName});`,
...generateNumberValidator(typeNode, parsedVar, path),
);
break;
}
case Type.STRING:
this.#collector.push(
...generateStringValidator(typeNode, varName, path),
);
break;
case Type.BOOLEAN: {
const parsedVar = this.#createVarName();

this.#collector.push(
`const ${varName} = Boolean(${varName});`,
...generateBooleanValidator(typeNode, parsedVar, path),
);
break;
}

default:
throw new Error(
`At "${path}": Unsupported type "${typeNode.type}" for context injection`,
);
const types = this.#inputValidatorCompiler.generateValidators(typeIdx);
for (const idx of types) {
this.#typesWithCustomValidator.add(idx);
}
const errorVar = this.#createVarName();
this.#collector.push(`const ${errorVar} = [];`);
const args = [varName, JSON.stringify(path), errorVar, "context"];
this.#collector.push(` validate_${typeIdx}(${args.join(", ")});`);
this.#collector.push(` if (${errorVar}.length > 0) {`);
const errorStrVar = this.#createVarName();
const errMap = `([path, msg]) => \` - at \${path}: \${msg}\``;
this.#collector.push(
` const ${errorStrVar} = ${errorVar}.map(${errMap}).join("\\n");`,
);
this.#collector.push(
` throw new Error(\`Context validation failed:\\n\${${errorStrVar}}\`);`,
);
this.#collector.push(` }`);

this.#collector.push("}");
if (!optional) {
Expand Down Expand Up @@ -316,14 +308,23 @@ class TransformerCompilationContext {
...generateStringValidator(typeNode, varName, path),
);
break;
case Type.INTEGER:
case Type.FLOAT:
case Type.INTEGER: {
const parsedVar = this.#createVarName();
this.#collector.push(
`const ${parsedVar} = parseInt(${varName}, 10);`,
...generateNumberValidator(typeNode, parsedVar, path),
);
break;
}
case Type.FLOAT: {
const parsedVar = this.#createVarName();
this.#collector.push(
...generateNumberValidator(typeNode, varName, path),
`const ${parsedVar} = parseFloat(${varName});`,
...generateNumberValidator(typeNode, parsedVar, path),
);
break;
}
default:
// TODO optional??
throw new Error(
`At "${path}": Unsupported type "${typeNode.type}" for secret injection`,
);
Expand Down
45 changes: 28 additions & 17 deletions typegate/src/engine/typecheck/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {

export function generateValidator(tg: TypeGraph, typeIdx: number): Validator {
const validator = new Function(
new InputValidationCompiler(tg).generate(typeIdx),
new InputValidationCompiler(tg, (typeIdx) => `validate_${typeIdx}`)
.generate(typeIdx),
)() as ValidatorFn;
return (value: unknown) => {
const errors: ErrorEntry[] = [];
Expand Down Expand Up @@ -103,16 +104,15 @@ function filterDeclaredFields(
}
}

function functionName(typeIdx: number) {
return `validate_${typeIdx}`;
}

export class InputValidationCompiler {
codes: Map<number, string> = new Map();
#getFunctionName: (idx: number) => string;

constructor(private tg: TypeGraph) {}
constructor(private tg: TypeGraph, getFunctionName: (idx: number) => string) {
this.#getFunctionName = getFunctionName;
}

generate(rootTypeIdx: number): string {
generateValidators(rootTypeIdx: number) {
const cg = new CodeGenerator();
const queue = [rootTypeIdx];
const refs = new Set([rootTypeIdx]);
Expand Down Expand Up @@ -145,31 +145,37 @@ export class InputValidationCompiler {
cg.generateFileValidator(typeNode);
break;
case "optional":
cg.generateOptionalValidator(typeNode, functionName(typeNode.item));
cg.generateOptionalValidator(
typeNode,
this.#getFunctionName(typeNode.item),
);
queue.push(typeNode.item);
break;
case "list":
cg.generateArrayValidator(typeNode, functionName(typeNode.items));
cg.generateArrayValidator(
typeNode,
this.#getFunctionName(typeNode.items),
);
queue.push(typeNode.items);
break;
case "object":
cg.generateObjectValidator(
typeNode,
mapValues(typeNode.properties, functionName),
mapValues(typeNode.properties, this.#getFunctionName),
);
queue.push(...Object.values(typeNode.properties));
break;
case "union":
cg.generateUnionValidator(
typeNode,
typeNode.anyOf.map(functionName),
typeNode.anyOf.map(this.#getFunctionName),
);
queue.push(...typeNode.anyOf);
break;
case "either":
cg.generateEitherValidator(
typeNode,
typeNode.oneOf.map(functionName),
typeNode.oneOf.map(this.#getFunctionName),
);
queue.push(...typeNode.oneOf);
break;
Expand All @@ -178,18 +184,23 @@ export class InputValidationCompiler {
}
}

const fnName = functionName(typeIdx);
const fnName = this.#getFunctionName(typeIdx);
const fnBody = cg.reset().join("\n");
this.codes.set(
typeIdx,
`function ${fnName}(value, path, errors, context) {\n${fnBody}\n}`,
);
}

const rootValidatorName = functionName(rootTypeIdx);
const rootValidator = `\nreturn ${rootValidatorName}`;
return refs;
}

generate(rootTypeIdx: number): string {
const fns = this.generateValidators(rootTypeIdx);
const rootValidatorName = this.#getFunctionName(rootTypeIdx);
const codes = [...fns].map((idx) => this.codes.get(idx));
codes.push(`\nreturn ${rootValidatorName}`);

return [...refs].map((idx) => this.codes.get(idx))
.join("\n") + rootValidator;
return codes.join("\n");
}
}
7 changes: 7 additions & 0 deletions typegate/tests/params/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,11 @@ def test_apply(g: Graph):
]
}
),
contextToUnionType=deno.identity(
t.struct(
{
"a": t.union([t.integer(), t.string()]),
}
)
).apply({"a": g.from_context("context_key")}),
)
15 changes: 15 additions & 0 deletions typegate/tests/params/apply_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ Meta.test("(python (sdk): apply)", async (t) => {
})
.on(e);
});

await t.should("fail for context to union type", async () => {
await gql`
query {
contextToUnionType {
a
}
}
`
.withContext({
context_key: "hum",
}).expectData({
contextToUnionType: { a: "hum" },
}).on(e);
});
});

Meta.test("nested context access", async (t) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function validate_44(value, path, errors, context) {
errors.push([path, \`expected maximum length: 10, got \${value.length}\`]);
}
}
return validate_38;
"
`;
Expand Down Expand Up @@ -143,6 +144,7 @@ function validate_50(value, path, errors, context) {
errors.push([path, "value did not match to any of the enum values"]);
}
}
return validate_46;
'
`;
Expand Down Expand Up @@ -357,6 +359,7 @@ function validate_26(value, path, errors, context) {
errors.push([path, \`expected minimum length: 3, got \${value.length}\`]);
}
}
return validate_9;
"
`;
Expand Down
17 changes: 12 additions & 5 deletions typegate/tests/typecheck/input_validator_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { nativeResult } from "../../src/utils.ts";
import * as native from "native";
import { assert, assertEquals } from "std/assert/mod.ts";

function getFunctionName(idx: number): string {
return `validate_${idx}`;
}

Meta.test("input validator compiler", async (t) => {
const e = await t.engine("typecheck/typecheck.py");
const { tg } = e;
Expand All @@ -22,9 +26,10 @@ Meta.test("input validator compiler", async (t) => {
Type.FUNCTION,
);

const generatedCode = new InputValidationCompiler(tg).generate(
createPost.input,
);
const generatedCode = new InputValidationCompiler(tg, getFunctionName)
.generate(
createPost.input,
);
const code = nativeResult(native.typescript_format_code({
source: generatedCode,
}))!.formatted_code;
Expand Down Expand Up @@ -76,7 +81,8 @@ Meta.test("input validator compiler", async (t) => {
Type.FUNCTION,
);

const generatedCode = new InputValidationCompiler(tg).generate(enums.input);
const generatedCode = new InputValidationCompiler(tg, getFunctionName)
.generate(enums.input);
const code = nativeResult(native.typescript_format_code({
source: generatedCode,
}))!.formatted_code;
Expand Down Expand Up @@ -127,7 +133,8 @@ Meta.test("input validator compiler", async (t) => {
Type.FUNCTION,
);

const generatedCode = new InputValidationCompiler(tg).generate(posts.input);
const generatedCode = new InputValidationCompiler(tg, getFunctionName)
.generate(posts.input);
const code = nativeResult(native.typescript_format_code({
source: generatedCode,
}))!.formatted_code;
Expand Down

0 comments on commit 04c7565

Please sign in to comment.