Skip to content

Commit

Permalink
feat(sdk,gate): node information by path (#498)
Browse files Browse the repository at this point in the history
<!--
Pull requests are squash merged using:
- their title as the commit message
- their description as the commit body

Having a good title and description is important for the users to get
readable changelog and understand when they need to update his code and
how.
-->

### Describe your change

* Fixes renamed function in sdk
* Adds `argInfoByPath` utility function in `typegate.py`

### Motivation and context

Make the task of fetching type information from the graphql function
args easier

### Migration notes

<!-- Explain HOW users should update their code when required -->

### 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

---------

Co-authored-by: Natoandro <[email protected]>
  • Loading branch information
michael-0acf4 and Natoandro authored Dec 8, 2023
1 parent 381d958 commit e19c44d
Show file tree
Hide file tree
Showing 9 changed files with 566 additions and 188 deletions.
204 changes: 204 additions & 0 deletions typegate/src/runtimes/typegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,35 @@ import config from "../config.ts";
import * as semver from "std/semver/mod.ts";
import { Typegate } from "../typegate/mod.ts";
import { TypeGraph } from "../typegraph/mod.ts";
import { closestWord } from "../utils.ts";
import { Type, TypeNode } from "../typegraph/type_node.ts";
import { StringFormat } from "../typegraph/types.ts";

const logger = getLogger(import.meta);

interface ArgInfoResult {
optional: boolean;
as_id: boolean;
title: string;
type: string;
runtime: string;
/** list of json string */
enum: string[] | null;
/** json string */
config: string | null;
/** json string */
default: string | null;
/** json string */
format: StringFormat | null;
fields: Array<ObjectNodeResult> | null;
}

interface ObjectNodeResult {
/** path starting from the parent node */
subPath: Array<string>;
termNode: ArgInfoResult;
}

export class TypeGateRuntime extends Runtime {
static singleton: TypeGateRuntime | null = null;

Expand Down Expand Up @@ -56,6 +82,9 @@ export class TypeGateRuntime extends Runtime {
if (name === "serializedTypegraph") {
return this.serializedTypegraph;
}
if (name === "argInfoByPath") {
return this.argInfoByPath;
}

return async ({ _: { parent }, ...args }) => {
const resolver = parent[stage.props.node];
Expand Down Expand Up @@ -137,4 +166,179 @@ export class TypeGateRuntime extends Runtime {
}
return true;
};

argInfoByPath: Resolver = ({ typegraph, queryType, argPaths, fn }) => {
if (queryType != "query" && queryType != "mutation") {
throw new Error(
`"query" or "mutation" expected, got type "${queryType}"`,
);
}

const paths = argPaths as Array<Array<string>>;
const tg = this.typegate.register.get(typegraph);

const root = tg!.tg.type(0, Type.OBJECT).properties[queryType];
const exposed = tg!.tg.type(root, Type.OBJECT).properties;

const funcIdx = exposed[fn];
if (funcIdx === undefined) {
const available = Object.keys(exposed);
const closest = closestWord(fn, available, true);
const propositions = closest ? [closest] : available;
const prefix = propositions.length > 1 ? " one of " : " ";
throw new Error(
`type named "${fn}" not found, did you mean${prefix}${
propositions
.map((prop) => `"${prop}"`)
.join(", ")
}?`,
);
}

const func = tg!.tg.type(funcIdx, Type.FUNCTION);
const input = tg!.tg.type(func.input, Type.OBJECT);

return paths.map((path) => walkPath(tg!.tg, input, 0, path));
};
}

function resolveOptional(tg: TypeGraph, node: TypeNode) {
let topLevelDefault;
let isOptional = false;
if (node.type == Type.OPTIONAL) {
while (node.type == Type.OPTIONAL) {
if (topLevelDefault == undefined) {
topLevelDefault = node.default_value;
}
isOptional = true;
node = tg.type(node.item);
}
}
const format = node.type == Type.STRING ? node.format : undefined;
return { node, format, topLevelDefault, isOptional };
}

function collectObjectFields(
tg: TypeGraph,
parent: TypeNode,
): Array<ObjectNodeResult> {
// first generate all possible paths

const paths = [] as Array<Array<string>>;

const collectAllPaths = (
parent: TypeNode,
currentPath: Array<string> = [],
): void => {
const node = resolveOptional(tg, parent).node;

if (node.type == Type.OBJECT) {
for (const [keyName, fieldIdx] of Object.entries(node.properties)) {
collectAllPaths(tg.type(fieldIdx), [...currentPath, keyName]);
}
return;
}

// leaf
// TODO: either/union?
paths.push(currentPath);
};

collectAllPaths(parent);

return paths.map((path) => ({
subPath: path,
termNode: walkPath(tg, parent, 0, path),
}));
}

function walkPath(
tg: TypeGraph,
parent: TypeNode,
startCursor: number,
path: Array<string>,
): ArgInfoResult {
let node = parent as TypeNode;
for (let cursor = startCursor; cursor < path.length; cursor += 1) {
const current = path.at(cursor)!;

// if the type is optional and path has not ended yet, the wrapped type needs to be retrieved
node = resolveOptional(tg, node).node;

const prettyPath = path.map((chunk, i) =>
i == cursor ? `[${chunk}]` : chunk
).join(".");

switch (node.type) {
case Type.OBJECT: {
const available = Object.keys(node.properties);
const currNodeIdx = node.properties[current];

if (currNodeIdx === undefined) {
throw new Error(
`invalid path ${prettyPath}, none of ${
available.join(", ")
} match the chunk "${current}"`,
);
}

node = tg.type(currNodeIdx);
break;
}
case Type.EITHER:
case Type.UNION: {
const variantsIdx = "anyOf" in node ? node.anyOf : node.oneOf;
const failures = new Array(variantsIdx.length);
// try to expand each variant, return first compatible with the path
const compat = [];
for (let i = 0; i < variantsIdx.length; i += 1) {
const variant = tg.type(variantsIdx[i]);
try {
compat.push(walkPath(tg, variant, cursor, path));
} catch (err) {
failures[i] = err;
}
}
if (compat.length == 0) {
throw failures.shift();
}
return compat.shift()!;
}
default: {
// optional, list, float are considered as leaf
if (cursor != path.length) {
throw new Error(
`cannot extend path ${prettyPath} with type "${node.type}"`,
);
}
break;
}
}
}

// resulting leaf can be optional
// in that case isOptional is true
const {
node: resNode,
format,
topLevelDefault: defaultValue,
isOptional,
} = resolveOptional(
tg,
node,
);
node = resNode;

return {
optional: isOptional,
as_id: node.as_id,
title: node.title,
type: node.type,
enum: node.enum ?? null,
runtime: tg.runtime(node.runtime).name,
config: node.config ? JSON.stringify(node.config) : null,
default: defaultValue ? JSON.stringify(defaultValue) : null,
format: format ?? null,
fields: node.type == "object" ? collectObjectFields(tg, parent) : null,
};
}
Loading

0 comments on commit e19c44d

Please sign in to comment.