Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Nested context query #595

Merged
merged 6 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 14 additions & 10 deletions typegate/src/engine/planner/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
compileParameterTransformer,
defaultParameterTransformer,
} from "./parameter_transformer.ts";
import { QueryFunction as JsonPathQuery } from "../../libs/jsonpath.ts";

class MandatoryArgumentError extends Error {
constructor(argDetails: string) {
Expand Down Expand Up @@ -657,20 +658,23 @@ class ArgumentCollector {
return () => this.tg.parseSecret(typ, secretName);
}
case "context": {
const contextKey = selectInjection(injection.data, this.effect);
if (contextKey == null) {
const contextPath = selectInjection(injection.data, this.effect);
if (contextPath == null) {
return null;
}
this.deps.context.add(contextKey);
this.deps.context.add(contextPath);
const queryContext = JsonPathQuery.create(contextPath, {
strict: typ.type !== Type.OPTIONAL,
rootPath: "<context>",
})
.asFunction();
return ({ context }) => {
const { [contextKey]: value = null } = context;
if (value === null && typ.type != Type.OPTIONAL) {
const suggestions = Object.keys(context).join(", ");
throw new BadContext(
`Non optional injection '${contextKey}' was not found in the context, available context keys are ${suggestions}`,
);
try {
return queryContext(context) ?? null;
} catch (e) {
const msg = e.message;
throw new BadContext("Error while querying context: " + msg);
}
return value;
};
}

Expand Down
95 changes: 86 additions & 9 deletions typegate/src/engine/planner/parameter_transformer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright Metatype OÜ, licensed under the Elastic License 2.0.
// SPDX-License-Identifier: Elastic-2.0

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";
Expand All @@ -15,8 +16,8 @@ import { generateStringValidator } from "../typecheck/inline_validators/string.t

export type TransformParamsInput = {
args: Record<string, any>;
context: Record<string, any>;
parent: Record<string, any>;
context: Record<string, any>;
};

export function defaultParameterTransformer(input: TransformParamsInput) {
Expand All @@ -27,26 +28,83 @@ export type TransformParams = {
(input: TransformParamsInput): Record<string, any>;
};

type CompiledTransformerInput = {
args: Record<string, any>;
parent: Record<string, any>;
getContext: ContextQuery;
};

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

export function compileParameterTransformer(
typegraph: TypeGraph,
parentProps: Record<string, number>,
transformerTreeRoot: ParameterTransformNode,
): TransformParams {
const ctx = new TransformerCompilationContext(typegraph, parentProps);
const fnBody = ctx.compile(transformerTreeRoot);
const fn = new Function("input", fnBody) as TransformParams;
return (input) => {
const res = fn(input);
const { fnBody, deps } = ctx.compile(transformerTreeRoot);
const fn = new Function("input", fnBody) as CompiledTransformer;
return ({ args, context, parent }) => {
const getContext = compileContextQueries(deps.contexts)(context);
const res = fn({ args, getContext, parent });
return res;
};
}

type Dependencies = {
contexts: {
strictMode: Set<string>;
nonStrictMode: Set<string>;
};
};

type ContextQuery = (path: string, options: { strict: boolean }) => unknown;

function compileContextQueries(contexts: Dependencies["contexts"]) {
return (context: Record<string, any>): ContextQuery => {
const strictMode = new Map<string, QueryFn>();
const nonStrictMode = new Map<string, QueryFn>();

for (const path of contexts.strictMode) {
strictMode.set(
path,
QueryFunction.create(path, { strict: true }).asFunction(),
);
}

for (const path of contexts.nonStrictMode) {
nonStrictMode.set(
path,
QueryFunction.create(path, { strict: false }).asFunction(),
);
}

return (path, options) => {
const fn = options.strict
? strictMode.get(path)
: nonStrictMode.get(path);
if (!fn) {
throw new Error(`Unknown context query: ${path}`);
}
return fn(context);
};
};
}

class TransformerCompilationContext {
#tg: TypeGraph;
#parentProps: Record<string, number>;
#path: string[] = [];
#latestVarIndex = 0;
#collector: string[] = [];
#dependencies: Dependencies = {
contexts: {
strictMode: new Set(),
nonStrictMode: new Set(),
},
};

constructor(typegraph: TypeGraph, parentProps: Record<string, number>) {
this.#tg = typegraph;
Expand All @@ -55,15 +113,28 @@ class TransformerCompilationContext {

#reset() {
this.#collector = [
"const { args, context, parent } = input;\n",
"const { args, parent, getContext } = input;\n",
];
this.#dependencies = {
contexts: {
strictMode: new Set(),
nonStrictMode: new Set(),
},
};
}

compile(rootNode: ParameterTransformNode) {
this.#reset();
const varName = this.#compileNode(rootNode);
this.#collector.push(`return ${varName};`);
return this.#collector.join("\n");
const res = {
fnBody: this.#collector.join("\n"),
deps: this.#dependencies,
};

this.#reset();

return res;
}

#compileNode(node: ParameterTransformNode) {
Expand Down Expand Up @@ -165,7 +236,13 @@ class TransformerCompilationContext {
typeNode = this.#tg.type(typeNode.item);
optional = true;
}
this.#collector.push(`const ${varName} = context[${JSON.stringify(key)}];`);

const opts = `{ strict: ${!optional} }`;
this.#collector.push(
`const ${varName} = getContext(${JSON.stringify(key)}, ${opts});`,
);
const mode = optional ? "nonStrictMode" : "strictMode";
this.#dependencies.contexts[mode].add(key);

const path = this.#path.join(".");

Expand Down Expand Up @@ -270,6 +347,6 @@ class TransformerCompilationContext {
}

#createVarName() {
return `var${++this.#latestVarIndex}`;
return `_var${++this.#latestVarIndex}`;
}
}
4 changes: 2 additions & 2 deletions typegate/src/engine/typecheck/inline_validators/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export function generateNumberValidator(
): string[] {
return [
check(
`typeof ${varName} === "number && !isNan(${varName})"`,
`"Expected number at ${path}"`,
`typeof ${varName} === "number" && !isNaN(${varName})`,
`"Expected number at '${path}'"`,
),
...generateConstraintValidatorsFor(numberConstraints, typeNode),
];
Expand Down