Skip to content

Commit

Permalink
feat!: Nested context query (#595)
Browse files Browse the repository at this point in the history
- Revert context flattening
- Enable jsonpath-like key to access nested object fields or array items
on the context.

#### Migration notes

If you access the context directly in your application (through the
token), access to nested fields shall be updated.
E.g. the expression `context["profile.id"]` have to turned to
`context.profile.id`.

### Checklist

- [x] The change come with new or modified tests
- [x] Hard-to-understand functions have explanatory comments
- [ ] End-user documentation is updated to reflect the change
  • Loading branch information
Natoandro committed Feb 24, 2024
1 parent bbf0b95 commit a2c2a80
Show file tree
Hide file tree
Showing 17 changed files with 683 additions and 55 deletions.
8 changes: 0 additions & 8 deletions typegate/deno.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1106,13 +1106,5 @@
"https://raw.githubusercontent.com/metatypedev/metatype/feat/MET-250/refactor-ffi/typegate/src/typegraph/visitor.ts": "854f2dd1adadc62ea2050f6e0f293c88f76d4feefb7620bcc490049fb8967043",
"https://raw.githubusercontent.com/metatypedev/metatype/feat/MET-250/refactor-ffi/typegate/src/types.ts": "1857e6bf96b0642e15352e10dd4e175c4983edc421868ae0158ce271e075926d",
"https://raw.githubusercontent.com/metatypedev/metatype/feat/MET-250/refactor-ffi/typegate/src/utils.ts": "8a34944dc326d1759c67fcdd4a714f99838d5eac040773bcdb7287a00118923b"
},
"workspace": {
"packageJson": {
"dependencies": [
"npm:chance@^1.1.11",
"npm:yarn@^1.22.19"
]
}
}
}
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
2 changes: 1 addition & 1 deletion typegate/src/engine/typecheck/inline_validators/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function generateStringValidator(
`const ${validatorName} = context.formatValidators[${format}];`,
check(
`${varName} != null`,
`Unknown format: ${format}`,
`'Unknown format: ${typeNode.format}'`,
),
check(
`${validatorName}(${varName})`,
Expand Down

0 comments on commit a2c2a80

Please sign in to comment.