diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5ad89f95e..54536b4789 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,6 +72,7 @@ repos: - ts - tsx files: ^website/ + exclude: ^website/typegraphs/ - id: devtools-lint name: ESLint the dev-tools language: system @@ -150,3 +151,4 @@ repos: - ts - tsx files: ^website/ + exclude: website/typegraphs diff --git a/dev/tg-py2ts.ts b/dev/tg-py2ts.ts new file mode 100644 index 0000000000..d427d0ec74 --- /dev/null +++ b/dev/tg-py2ts.ts @@ -0,0 +1,335 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +/** + * Translate typegraph in python to deno (experimental) for version 0.3.2 + * A better implementation would be to + * (1) parse the python source directly and 1-1 map the imports/functions + refer to the ambient node sdk to validate the imports or attempt to autoresolve them. + * (2) emit typescript code directly from the serialized json. + * + * Usage: + * deno run -A --config=typegate/deno.jsonc dev/tg-py2ts.ts --file [--force] + */ + +import { basename, dirname, join } from "std/path/mod.ts"; +import { parseFlags, resolve } from "./deps.ts"; +import { camelCase, Cursor, findCursors, nextMatch } from "./utils.ts"; + +const args = parseFlags(Deno.args, { + string: ["file"], + boolean: ["force", "print"], +}); + +if (!args.file) { + console.error("No typegraph was given"); + Deno.exit(-1); +} + +// Concept: +// source => step1 => step2(input step1) => .. => output + +type ReplaceStep = { description: string; apply: (text: string) => string }; +type Failure = { error: Error | string; stepDescription: string }; +type StepResult = { output: string; errors: Array }; + +/** + * Capture args string in function-calls + */ +function captureInsideParenthesis( + text: string, + prefix: string | RegExp, + offset?: number, // inclusive +): Cursor | null { + const res = nextMatch(text, prefix, offset); + + if (res == null) { + return null; + } + + const searchStart = res.end + 1; + let startPos = null; + let parenthStack = 0; + let lastOpenedParenth = -1; + let expr = ""; + for (let i = searchStart; i < text.length; i++) { + const char = text.charAt(i); + const isParenthesis = char == "(" || char == ")"; + if (lastOpenedParenth < 0 && !isParenthesis) { + if (/\s/.test(char)) { + continue; // prefix\s*() + } else { + break; // prefix\s*something\s*() + } + } + + if (isParenthesis) { + if (char == "(") { + lastOpenedParenth = i; + parenthStack += 1; + } else { + parenthStack -= 1; + } + // first "(" has closed or we reached a premature end + if (parenthStack <= 0) break; + } + if (i > searchStart) { + startPos = startPos == null ? i : startPos; + expr += char; + } + } + + // no-op + if (lastOpenedParenth == null) { + return null; + } + + if (parenthStack != 0) { + const peekRadius = 10; + throw new Error( + `invalid parenthesis near .. ${ + text.substring( + Math.max(0, lastOpenedParenth - peekRadius), + Math.min(text.length, lastOpenedParenth + peekRadius), + ).replace(/[\n\r]/g, "\\n") + .trim() + } ..`, + ); + } + + const start = startPos ?? lastOpenedParenth; // handle 0 length epxr: prefix() + return { + start, + end: start + expr.length, + length: expr.length, + match: expr, + }; +} + +// Concept: +// source => step1 => step2(input step1) => .. => output +const chain: Array = [ + { + description: "imports", + apply(text: string) { + return text.replace( + /from\s+(.+?)\s+import\s+(.+,?)\s/g, + (m: string, pkg, imp) => { + if (typeof pkg != "string") { + throw new Error( + `package expr invalid at "${m}", got ${typeof pkg}`, + ); + } + if (typeof imp != "string") { + throw new Error( + `import expr invalid at "${m}", got ${typeof imp}`, + ); + } + + const pkgSplit = pkg.split("."); + const [start, ...rem] = pkgSplit; + const first = start == "typegraph" ? "" : start; + const relPath = `${pkgSplit.length == 1 ? "/index" : ""}${ + [first, ...rem] + .join("/") + }`; + const imports = imp.split(/\s*,\s*/) + .map(camelCase) + .join(", "); + return `import { ${imports} } from "@typegraph/sdk${relPath}.js"\n`; + }, + ); + }, + }, + { + description: "Translate inline comments", + apply(text: string) { + return text + .replace(/#(.*)(\s*?)/g, (_, start, end) => `//${start}${end}`); + }, + }, + { + description: "Translate typegraph body", + apply(text: string) { + // match name near def + const tmp = text.match(/def\s+(\w*)\(.*\)\s*\:/); + if (tmp == null) { + throw new Error("could not extract typegraph name"); + } + const [tgDefExpr, tgName] = tmp!; + const defOffset = text.lastIndexOf(tgDefExpr); + const body = text.substring(defOffset + tgDefExpr.length); + + // match in @typegraph() + const prefixTg = "@typegraph"; + const cursor = text.lastIndexOf(prefixTg) + prefixTg.length; + if (cursor < 0) { + throw new Error(`could not find ${prefixTg}`); + } + + const name = tgName.replace(/_+/g, "-"); + const nextParenthesis = captureInsideParenthesis(text, prefixTg) ?? + { match: "" }; + const config = nextParenthesis.match.replace( + /=/g, + ":", + ); + const header = text.substring(0, text.lastIndexOf(prefixTg)); + + return `${header}\ntypegraph({\nname: "${name}",\n${config}}, (g) => \n{${body}\n});`; + }, + }, + { + description: "Function name on an object: foo.some_func => foo.someFunc", + apply(text: string) { + return text + .replace( + /\.(\w+_)+?(\w+)/g, + camelCase, + ); + }, + }, + { + description: "Expose expression", + apply(text: string) { + const exposed = captureInsideParenthesis(text, "g.expose") ?? + { match: "" }; + return text.replace( + exposed.match, + `{${exposed.match.replace(/=/g, ":")}}`, + ); + }, + }, + { + description: "Keyword translation", + apply(text: string) { + const replMap = Object.entries({ + "True": "true", + "False": "false", + "None": "null", + }); + return replMap + .reduce((prev, [tk, repl]) => prev.replaceAll(tk, repl), text); + }, + }, + { + description: "Scalar type argument translation", + apply(text: string) { + const objectify = (list: Array<[string, string]>) => { + return `{${list.map(([k, v]) => `${k}: ${v}`).join(", ")}}`; + }; + const prefix = /t\.(integer|float|string|boolean|uuid)/g; + const cursors = findCursors(text, prefix) + .map(({ start }) => + captureInsideParenthesis(text, prefix, start) ?? { match: "" } + ); + + return cursors.reduce((prev, { match }) => { + if (match == "") return prev; + const basic = ["min", "max", "xmin", "xmax"]; + const splits = match.split(",") + .map((arg) => arg.split("=")); + const left = [] as Array<[string, string]>; + const right = [] as Array<[string, string]>; + for (const [k, v] of splits) { + const ptr = basic.includes(k.trim()) ? left : right; + ptr.push([camelCase(k.trim()), v]); + } + return prev.replace( + match, + `${objectify(left)}, ${objectify(right)}`, + ); + }, text); + }, + }, + { + description: "Comment name=..", + apply(text: string) { + // struc({}, name=..) => struct({}/*rename("..")*/) + const prefix = "t.struct"; + return findCursors(text, prefix) + .map(({ start }) => { + const insideParenth = captureInsideParenthesis(text, prefix, start) ?? + { match: "" }; + return insideParenth.match.match(/,?\s*name\s*=\s*("\w+")/); + }).reduce( + (prev, rename) => + rename === null ? prev : ({ + value: prev.value.replace( + rename[0], + `/*rename(${rename[1].trim()})*/`, + ), + }), + { value: text }, + ).value; + }, + }, + // { + // description: "Variable assignements", + // apply(text: string) { + // // at this stage g.expose is already translated + // return text.split(/\n/g).map((line) => { + // // assignement test + // if (!/\s*\w+\s*=.+\s/.test(line)) { + // return line; + // } + // const [left, right] = line.split("="); + // if (right && !/config/.test(left)) { + // const indent = left.match(/\s*/)?.[0] ?? ""; + // return `${indent}const ${left.trim()} = ${right.trim()}`; + // } + // return line; + // }).join("\n"); + // }, + // }, + // { + // description: "Translate multiline comments", + // apply(text: string) { + // return text; + // }, + // }, + // { + // description: "Translate multiline string", + // apply(text: string) { + // return text; + // }, + // }, +]; + +const path = resolve(args.file); +const source = Deno.readTextFileSync(path); + +const result = chain + .reduce((prev: StepResult, step: ReplaceStep) => { + let { output, errors } = prev; + try { + output = step.apply(output); + } catch (error) { + errors = [...errors, { + error, + stepDescription: step.description, + }]; + } + return { output, errors }; + }, { output: source, errors: [] } as StepResult); + +if (!args.force && result.errors.length > 0) { + console.error( + "Failed the following steps:\n", + result + .errors + .map(({ stepDescription, error }) => + `- ${stepDescription}: "${ + typeof error == "string" ? error : error.message + }"` + ) + .join("\n"), + ); + Deno.exit(-2); +} + +if (args.print) { + console.log(result.output); +} else { + const outputFile = basename(path).replace(/\.py$/, ".ts"); + Deno.writeTextFileSync(join(dirname(path), outputFile), result.output); +} diff --git a/dev/tree-view.ts b/dev/tree-view.ts index e1b26c723f..36d6c1d638 100644 --- a/dev/tree-view.ts +++ b/dev/tree-view.ts @@ -3,7 +3,7 @@ /** * Usage: - * deno run -A dev/tree-view.ts --confg typegate/deno.json [] + * deno run -A dev/tree-view.ts --config=typegate/deno.jsonc [] * * Options: * --depth The depth of the tree diff --git a/dev/utils.ts b/dev/utils.ts index 2108176512..d792a6f4c7 100644 --- a/dev/utils.ts +++ b/dev/utils.ts @@ -55,3 +55,77 @@ export async function getLockfile() { file, ) as Lockfile; } + +export type Cursor = { + start: number; + end: number; + length: number; + match: string; +}; + +export function upperFirst(str: string) { + return str.charAt(0).toUpperCase() + str.substring(1); +} + +export function camelCase(str: string) { + return str + .split(/_+/g) + .map((chunk, idx) => idx > 0 ? upperFirst(chunk) : chunk) + .join(""); +} + +/** + * Enhanced `indexOf` with regex support and position information + */ +export function nextMatch( + text: string, + word: string | RegExp, + pos = 0, +): Cursor | null { + if (word instanceof RegExp) { + const searchPos = Math.min(text.length, pos); + const nextText = text.substring(searchPos); + const res = word.exec(nextText); + word.lastIndex = 0; // always reset (js!) + return res + ? { + match: res[0], + start: searchPos + res.index, + end: searchPos + res.index + res[0].length - 1, + length: res[0].length, + } + : null; + } + const start = text.indexOf(word, pos); + return start >= 0 + ? { + start, + end: start + word.length - 1, + match: word, + length: word.length, + } + : null; +} + +/** + * Determine all indexOf with position information + */ +export function findCursors( + text: string, + word: string | RegExp, +): Array { + const matches = [] as Array; + + let cursor = 0; + while (true) { + const res = nextMatch(text, word, cursor); + if (res != null) { + cursor = res.end; + matches.push(res); + } else { + break; + } + } + + return matches; +} diff --git a/website/metatype.yaml b/examples/metatype.yaml similarity index 100% rename from website/metatype.yaml rename to examples/metatype.yaml diff --git a/website/docs/guides/securing-requests/authentication.py b/examples/typegraphs/authentication.py similarity index 100% rename from website/docs/guides/securing-requests/authentication.py rename to examples/typegraphs/authentication.py diff --git a/examples/typegraphs/authentication.ts b/examples/typegraphs/authentication.ts new file mode 100644 index 0000000000..80bbaf8203 --- /dev/null +++ b/examples/typegraphs/authentication.ts @@ -0,0 +1,30 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +// skip:end + +typegraph({ + name: "authentication", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const deno = new DenoRuntime(); + const pub = Policy.public(); + + const ctx = t.struct({ + "username": t.string().optional().fromContext("username"), + }); + + // highlight-start + // expects a secret in metatype.yml + // `TG_[typegraph]_BASIC_[username]` + // highlight-next-line + g.auth(Auth.basic(["admin"])); + // highlight-end + + g.expose({ + get_context: deno.identity(ctx).withPolicy(pub), + }); +}); diff --git a/website/use-cases/backend-for-frontend/t.py b/examples/typegraphs/backend-for-frontend.py similarity index 100% rename from website/use-cases/backend-for-frontend/t.py rename to examples/typegraphs/backend-for-frontend.py diff --git a/examples/typegraphs/backend-for-frontend.ts b/examples/typegraphs/backend-for-frontend.ts new file mode 100644 index 0000000000..a5fec3a461 --- /dev/null +++ b/examples/typegraphs/backend-for-frontend.ts @@ -0,0 +1,33 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { HttpRuntime } from "@typegraph/sdk/runtimes/http.js"; + +// skip:end + +typegraph({ + name: "backend-for-frontend", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const github = new HttpRuntime("https://api.github.com"); + const pub = Policy.public(); + + const stargazer = t.struct( + { + "login": t.string({}, { name: "login" }), + "user": github.get( + t.struct({ "user": t.string().fromParent("login") }), + t.struct({ "name": t.string().optional() }), + { path: "/users/{user}" }, + ), + }, + ); + + g.expose({ + stargazers: github.get( + t.struct({}), + t.list(stargazer), + { path: "/repos/metatypedev/metatype/stargazers?per_page=2" }, + ).withPolicy(pub), + }); +}); diff --git a/website/docs/reference/typegate/authentication/basic.py b/examples/typegraphs/basic.py similarity index 100% rename from website/docs/reference/typegate/authentication/basic.py rename to examples/typegraphs/basic.py diff --git a/examples/typegraphs/basic.ts b/examples/typegraphs/basic.ts new file mode 100644 index 0000000000..36c79b6a6d --- /dev/null +++ b/examples/typegraphs/basic.ts @@ -0,0 +1,26 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +// skip:end + +typegraph({ + name: "basic-authentication", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const deno = new DenoRuntime(); + const pub = Policy.public(); + + const ctx = t.struct({ + "username": t.string().optional().fromContext("username"), + }); + + // highlight-next-line + g.auth(Auth.basic(["admin"])); + + g.expose({ + get_context: deno.identity(ctx).withPolicy(pub), + }); +}); diff --git a/website/docs/reference/typegate/cors/cors.py b/examples/typegraphs/cors.py similarity index 93% rename from website/docs/reference/typegate/cors/cors.py rename to examples/typegraphs/cors.py index 58cf3b9372..c252dfb758 100644 --- a/website/docs/reference/typegate/cors/cors.py +++ b/examples/typegraphs/cors.py @@ -22,7 +22,7 @@ ), ) def auth(g: Graph): - random = RandomRuntime(seed=0) + random = RandomRuntime(seed=0, reset=None) public = Policy.public() g.expose( diff --git a/examples/typegraphs/cors.ts b/examples/typegraphs/cors.ts new file mode 100644 index 0000000000..7600e556bf --- /dev/null +++ b/examples/typegraphs/cors.ts @@ -0,0 +1,29 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { RandomRuntime } from "@typegraph/sdk/runtimes/random.js"; + +// skip:end + +typegraph({ + name: "auth", + // highlight-next-line + cors: { + // highlight-next-line + allowOrigin: ["https://not-this.domain"], + // highlight-next-line + allowHeaders: ["x-custom-header"], + // highlight-next-line + exposeHeaders: ["header-1"], + // highlight-next-line + allowCredentials: true, + // highlight-next-line + maxAgeSec: 60, + }, +}, (g) => { + const random = new RandomRuntime({ seed: 0 }); + const pub = Policy.public(); + + g.expose({ + catch_me_if_you_can: random.gen(t.string()).withPolicy(pub), + }); +}); diff --git a/website/docs/reference/runtimes/prisma/database.py b/examples/typegraphs/database.py similarity index 100% rename from website/docs/reference/runtimes/prisma/database.py rename to examples/typegraphs/database.py diff --git a/examples/typegraphs/database.ts b/examples/typegraphs/database.ts new file mode 100644 index 0000000000..650e190b06 --- /dev/null +++ b/examples/typegraphs/database.ts @@ -0,0 +1,33 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; + +// isort: off +// skip:end +// highlight-next-line +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; + +typegraph({ + name: "database", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const db = new PrismaRuntime("database", "POSTGRES_CONN"); + const pub = Policy.public(); + + const message = t.struct( + { + // highlight-next-line + "id": t.integer({}, { asId: true, config: { auto: true } }), + "title": t.string(), + "body": t.string(), + }, + // highlight-next-line + { name: "message" }, + ); + + g.expose({ + // highlight-next-line + create_message: db.create(message), + list_messages: db.findMany(message), + }, pub); +}); diff --git a/website/docs/reference/runtimes/deno/deno.py b/examples/typegraphs/deno.py similarity index 100% rename from website/docs/reference/runtimes/deno/deno.py rename to examples/typegraphs/deno.py diff --git a/examples/typegraphs/deno.ts b/examples/typegraphs/deno.ts new file mode 100644 index 0000000000..624ba507ce --- /dev/null +++ b/examples/typegraphs/deno.ts @@ -0,0 +1,40 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +// skip:end + +typegraph({ + name: "deno", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const deno = new DenoRuntime(); + const pub = Policy.public(); + + const fib = deno.func( + t.struct({ "n": t.float() }), + t.struct({ "res": t.integer(), "ms": t.float() }), + { + code: ` + ({ n }) => { + let a = 0, b = 1, c; + const start = performance.now(); + for ( + let i = 0; + i < Math.min(n, 10); + c = a + b, a = b, b = c, i += 1 + ); + return { + res: b, + ms: performance.now() - start, + }; + } + `, + }, + ); + + g.expose({ + compute_fib: fib, + }, pub); +}); diff --git a/website/docs.py b/examples/typegraphs/docs.py similarity index 100% rename from website/docs.py rename to examples/typegraphs/docs.py diff --git a/website/docs/guides/rest/example_rest.py b/examples/typegraphs/example_rest.py similarity index 100% rename from website/docs/guides/rest/example_rest.py rename to examples/typegraphs/example_rest.py diff --git a/examples/typegraphs/example_rest.ts b/examples/typegraphs/example_rest.ts new file mode 100644 index 0000000000..b90584b0c5 --- /dev/null +++ b/examples/typegraphs/example_rest.ts @@ -0,0 +1,43 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +typegraph({ + name: "example-rest", + dynamic: false, +}, (g) => { + const deno = new DenoRuntime(); + const pub = Policy.public(); + + const user = t.struct({ "id": t.integer() }, { name: "User" }); + + const post = t.struct( + { + "id": t.integer(), + "author": user, + }, + { name: "Post" }, + ); + + // API docs {typegate_url}/example-rest/rest + // In this example, the query below maps to {typegate_url}/example-rest/rest/get_post?id=.. + g.rest( + ` + query get_post($id: Integer) { + postFromUser(id: $id) { + id + author { + id + } + } + } + `, + ); + + g.expose({ + postFromUser: deno.func( + user, + post, + { code: "(_) => ({ id: 12, author: {id: 1} })" }, + ).withPolicy(pub), + }); +}); diff --git a/website/docs/tutorials/building-feature-roadmap-api/execute.py b/examples/typegraphs/execute.py similarity index 100% rename from website/docs/tutorials/building-feature-roadmap-api/execute.py rename to examples/typegraphs/execute.py diff --git a/examples/typegraphs/execute.ts b/examples/typegraphs/execute.ts new file mode 100644 index 0000000000..86258351b5 --- /dev/null +++ b/examples/typegraphs/execute.ts @@ -0,0 +1,87 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import * as effects from "@typegraph/sdk/effects.js"; + +typegraph({ + // skip:start + name: "roadmap-execute", + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, + // skip:end +}, (g) => { + const pub = Policy.public(); + const db = new PrismaRuntime("db", "POSTGRES"); + const deno = new DenoRuntime(); + + const bucket = t.struct( + { + "id": t.integer({}, { asId: true, config: { auto: true } }), + "name": t.string(), + "ideas": t.list(g.ref("idea")), + }, + { name: "bucket" }, + ); + + const idea = t.struct( + { + "id": t.uuid({ asId: true, config: { auto: true } }), + "name": t.string(), + "authorEmail": t.email(), + "votes": t.list(g.ref("vote")), + "bucket": g.ref("bucket"), + }, + { name: "idea" }, + ); + + const vote = t.struct( + { + "id": t.uuid({ asId: true, config: { auto: true } }), + "authorEmail": t.email(), + "importance": t.enum_(["medium", "important", "critical"]).optional(), + "desc": t.string().optional(), + "idea": g.ref("idea"), + }, + { name: "vote" }, + ); + + g.auth(Auth.basic(["andim"])); + + const admins = deno.policy( + "admins", + ` + (_args, { context }) => !!context.username + `, + ); + + g.expose({ + create_bucket: db.create(bucket).withPolicy(admins), + get_buckets: db.findMany(bucket), + get_bucket: db.findFirst(bucket), + get_idea: db.findMany(idea), + create_idea: db.create(idea).reduce( + { + "data": { + "name": g.inherit(), + "authorEmail": g.inherit(), + "votes": g.inherit(), + "bucket": { "connect": g.inherit() }, + }, + }, + ), + create_vote: db.create(vote), + set_vote_importance: db.execute( + 'UPDATE "vote" SET importance = ${importance} WHERE id = ${vote_id}::uuid', + t.struct( + { + "vote_id": t.uuid(), + "importance": t.enum_(["medium", "important", "critical"]), + }, + ), + effects.update(), + ), + get_context: deno.identity( + t.struct({ "username": t.string().optional().fromContext("username") }), + ), + }, pub); +}); diff --git a/website/use-cases/faas-runner/t.py b/examples/typegraphs/faas-runner.py similarity index 100% rename from website/use-cases/faas-runner/t.py rename to examples/typegraphs/faas-runner.py diff --git a/examples/typegraphs/faas-runner.ts b/examples/typegraphs/faas-runner.ts new file mode 100644 index 0000000000..9d8a1115e7 --- /dev/null +++ b/examples/typegraphs/faas-runner.ts @@ -0,0 +1,34 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; +import { PythonRuntime } from "@typegraph/sdk/runtimes/python.js"; + +// skip:end + +typegraph({ + name: "faas-runner", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const pub = Policy.public(); + + const deno = new DenoRuntime(); + const python = new PythonRuntime(); + + const inp = t.struct({ "n": t.integer({ min: 0, max: 100 }) }); + const out = t.integer(); + + g.expose({ + pycumsum: python.fromLambda(inp, out, { + code: `lambda inp: sum(range(inp["n"])`, + }), + tscumsum: deno.func( + inp, + out, + { + code: + "({n}) => Array.from(Array(5).keys()).reduce((sum, e) => sum + e, 0)", + }, + ), + }, pub); +}); diff --git a/website/docs/guides/files-upload/t.py b/examples/typegraphs/files-upload.py similarity index 100% rename from website/docs/guides/files-upload/t.py rename to examples/typegraphs/files-upload.py diff --git a/examples/typegraphs/files-upload.ts b/examples/typegraphs/files-upload.ts new file mode 100644 index 0000000000..2406de2ac7 --- /dev/null +++ b/examples/typegraphs/files-upload.ts @@ -0,0 +1,24 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { S3Runtime } from "@typegraph/sdk/providers/aws.js"; + +typegraph({ + name: "retrend", +}, (g) => { + const pub = Policy.public(); + + const s3 = new S3Runtime({ + hostSecret: "S3_HOST", + regionSecret: "S3_REGION", + accessKeySecret: "S3_ACCESS_KEY", + secretKeySecret: "S3_SECRET_KEY", + pathStyleSecret: "S3_PATH_STYLE", + }); + + g.expose({ + listObjects: s3.list("bucket"), + getDownloadUrl: s3.presignGet({ bucket: "bucket" }), + signUploadUrl: s3.presignPut({ bucket: "bucket" }), + upload: s3.upload("bucket", t.file({ allow: ["image/png", "image/jpeg"] })), + uploadMany: s3.uploadAll("bucket"), + }, pub); +}); diff --git a/website/docs/tutorials/metatype-basics/t.py b/examples/typegraphs/first-typegraph.py similarity index 94% rename from website/docs/tutorials/metatype-basics/t.py rename to examples/typegraphs/first-typegraph.py index 0b706f6f66..433dfd3d1c 100644 --- a/website/docs/tutorials/metatype-basics/t.py +++ b/examples/typegraphs/first-typegraph.py @@ -9,7 +9,7 @@ ) def first_typegraph(g: Graph): # declare runtimes and policies - random = RandomRuntime() + random = RandomRuntime(reset=None) public = Policy.public() # declare types diff --git a/examples/typegraphs/first-typegraph.ts b/examples/typegraphs/first-typegraph.ts new file mode 100644 index 0000000000..a05d30e0a2 --- /dev/null +++ b/examples/typegraphs/first-typegraph.ts @@ -0,0 +1,27 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { RandomRuntime } from "@typegraph/sdk/runtimes/random.js"; + +typegraph({ + name: "first-typegraph", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + // declare runtimes and policies + const random = new RandomRuntime({}); + const pub = Policy.public(); + + // declare types + const message = t.struct( + { + "id": t.integer(), + "title": t.string(), + "user_id": t.integer(), + }, + ); + + // expose them with policies + g.expose({ + // input → output via materializer + get_message: random.gen(message), + }, pub); +}); diff --git a/website/docs/tutorials/building-feature-roadmap-api/func.py b/examples/typegraphs/func.py similarity index 96% rename from website/docs/tutorials/building-feature-roadmap-api/func.py rename to examples/typegraphs/func.py index 904b54a771..831fd82f58 100644 --- a/website/docs/tutorials/building-feature-roadmap-api/func.py +++ b/examples/typegraphs/func.py @@ -57,9 +57,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - """ - (_args, { context }) => !!context.username -""", + "(_args, { context }) => !!context.username", ) # skip:end @@ -85,7 +83,7 @@ def roadmap(g: Graph): parse_markdown=deno.import_( t.struct({"raw": t.string()}), t.string(), - module="md2html.ts.src", + module="scripts/md2html.ts.src", name="parse", ), ) diff --git a/examples/typegraphs/func.ts b/examples/typegraphs/func.ts new file mode 100644 index 0000000000..d9bb496e8b --- /dev/null +++ b/examples/typegraphs/func.ts @@ -0,0 +1,124 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +// skip:start +import { Auth } from "@typegraph/sdk/params.js"; +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; +// skip:end + +typegraph({ + // skip:start + name: "roadmap-func", + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, + // skip:end +}, (g) => { + // skip:start + const pub = Policy.public(); + const db = new PrismaRuntime("db", "POSTGRES"); + // skip:end + const deno = new DenoRuntime(); + + // skip:start + const bucket = t.struct( + { + "id": t.integer({}, { asId: true, config: { "auto": true } }), + "name": t.string(), + "ideas": t.list(g.ref("idea")), + }, + { name: "bucket" }, + ); + + const idea = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "name": t.string(), + "authorEmail": t.email(), + "votes": t.list(g.ref("vote")), + "bucket": g.ref("bucket"), + }, + { name: "idea" }, + ); + + const vote = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "authorEmail": t.email(), + "importance": t.enum_(["medium", "important", "critical"]).optional(), + "desc": t.string().optional(), + "idea": g.ref("idea"), + }, + { name: "vote" }, + ); + + g.auth(Auth.basic(["andim"])); + + const admins = deno.policy( + "admins", + "(_args, { context }) => !!context.username", + ); + // skip:end + + g.expose({ + // skip:start + create_bucket: db.create(bucket).withPolicy(admins), + get_buckets: db.findMany(bucket), + get_bucket: db.findFirst(bucket), + get_idea: db.findMany(idea), + create_idea: db.create(idea).reduce( + { + "data": { + "name": g.inherit(), + "authorEmail": g.inherit(), + "votes": g.inherit(), + "bucket": { "connect": g.inherit() }, + }, + }, + ), + create_vote: db.create(vote), + // skip:end + parse_markdown: deno.import( + t.struct({ "raw": t.string() }), + t.string(), + { + module: "scripts/md2html.ts.src", + name: "parse", + }, + ), + }, pub); + + // skip:start + g.rest( + ` + query get_buckets { + get_buckets { + id + name + ideas { + id + name + authorEmail + } + } + } + `, + ); + + g.rest( + ` + query get_bucket($id: Integer) { + get_bucket(where:{ + id: $id + }) { + id + name + ideas { + id + name + authorEmail + } + } + } + `, + ); + // skip:end +}); diff --git a/website/docs/concepts/mental-model/functions.py b/examples/typegraphs/functions.py similarity index 100% rename from website/docs/concepts/mental-model/functions.py rename to examples/typegraphs/functions.py diff --git a/website/use-cases/graphql-server/t.py b/examples/typegraphs/graphql-server.py similarity index 94% rename from website/use-cases/graphql-server/t.py rename to examples/typegraphs/graphql-server.py index ecfcb2de3f..2c5d89becc 100644 --- a/website/use-cases/graphql-server/t.py +++ b/examples/typegraphs/graphql-server.py @@ -16,7 +16,7 @@ def graphql_server(g: Graph): stargazer = t.struct( { - "login": t.string(name="login"), + "login": t.string().rename("login"), "user": github.get( "/users/{user}", t.struct({"user": t.string().from_parent("login")}), diff --git a/examples/typegraphs/graphql-server.ts b/examples/typegraphs/graphql-server.ts new file mode 100644 index 0000000000..3e88f9ebdc --- /dev/null +++ b/examples/typegraphs/graphql-server.ts @@ -0,0 +1,34 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { HttpRuntime } from "@typegraph/sdk/runtimes/http.js"; + +// skip:end + +typegraph({ + name: "graphql-server", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const pub = Policy.public(); + + const github = new HttpRuntime("https://api.github.com"); + + const stargazer = t.struct( + { + "login": t.string().rename("login"), + "user": github.get( + t.struct({ "user": t.string().fromParent("login") }), + t.struct({ "name": t.string().optional() }), + { path: "/users/{user}" }, + ), + }, + ); + + g.expose({ + stargazers: github.get( + t.struct({}), + t.list(stargazer), + { path: "/repos/metatypedev/metatype/stargazers?per_page=2" }, + ), + }, pub); +}); diff --git a/website/docs/reference/runtimes/graphql/graphql.py b/examples/typegraphs/graphql.py similarity index 100% rename from website/docs/reference/runtimes/graphql/graphql.py rename to examples/typegraphs/graphql.py diff --git a/examples/typegraphs/graphql.ts b/examples/typegraphs/graphql.ts new file mode 100644 index 0000000000..b57266ebe8 --- /dev/null +++ b/examples/typegraphs/graphql.ts @@ -0,0 +1,49 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; + +// isort: off +// skip:end +// highlight-next-line +import { GraphQLRuntime } from "@typegraph/sdk/runtimes/graphql.js"; + +typegraph({ + name: "graphql", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const db = new PrismaRuntime("database", "POSTGRES_CONN"); + // highlight-next-line + const gql = new GraphQLRuntime("https://graphqlzero.almansi.me/api"); + const pub = Policy.public(); + + // highlight-next-line + const user = t.struct({ "id": t.string(), "name": t.string() }); + + const message = t.struct( + { + "id": t.integer({}, { asId: true, config: { auto: true } }), + "title": t.string(), + // highlight-next-line + "user_id": t.string({}, { name: "uid" }), + // highlight-next-line + "user": gql.query( + t.struct( + { + // highlight-next-line + "id": t.string({}, { asId: true }).fromParent("uid"), + }, + ), + t.optional(user), + ), + }, + { name: "message" }, + ); + + g.expose({ + create_message: db.create(message), + messages: db.findMany(message), + // highlight-next-line + users: gql.query(t.struct({}), t.struct({ "data": t.list(user) })), + }, pub); +}); diff --git a/website/use-cases/iam-provider/t.py b/examples/typegraphs/iam-provider.py similarity index 80% rename from website/use-cases/iam-provider/t.py rename to examples/typegraphs/iam-provider.py index 44ed4dd5a6..bf0580fd60 100644 --- a/website/use-cases/iam-provider/t.py +++ b/examples/typegraphs/iam-provider.py @@ -13,14 +13,7 @@ cors=Cors(allow_origin=["https://metatype.dev", "http://localhost:3000"]), ) def iam_provider(g: Graph): - g.auth( - Auth.oauth2( - "github", - "https://github.com/login/oauth/authorize", - "https://github.com/login/oauth/access_token", - "openid profile email", - ) - ) + g.auth(Auth.oauth2_github("openid profile email")) public = Policy.public() diff --git a/examples/typegraphs/iam-provider.ts b/examples/typegraphs/iam-provider.ts new file mode 100644 index 0000000000..c1c7dced0c --- /dev/null +++ b/examples/typegraphs/iam-provider.ts @@ -0,0 +1,44 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +function getEnvOrDefault(key: string, defaultValue: string) { + const glob = globalThis as any; + const value = glob?.process + ? glob?.process.env?.[key] + : glob?.Deno.env.get(key); + return value ?? defaultValue; +} +// skip:end + +typegraph({ + name: "iam-provider", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + g.auth(Auth.oauth2Github("openid profile email")); + + const pub = Policy.public(); + + const deno = new DenoRuntime(); + const host = getEnvOrDefault("TG_URL", "http://localhost:7890"); + const url = `${host}/iam-provider/auth/github?redirect_uri=${ + encodeURIComponent(host) + }`; + + g.expose({ + loginUrl: deno.static(t.string(), url), + logoutUrl: deno.static(t.string(), `${url}&clear`), + context: deno.func( + t.struct({}), + t.struct({ "username": t.string() }).optional(), + { + code: + "(_, { context }) => Object.keys(context).length === 0 ? null : context", + }, + ), + }, pub); +}); diff --git a/website/src/pages/index.py b/examples/typegraphs/index.py similarity index 100% rename from website/src/pages/index.py rename to examples/typegraphs/index.py diff --git a/examples/typegraphs/index.ts b/examples/typegraphs/index.ts new file mode 100644 index 0000000000..dbc7f44f9b --- /dev/null +++ b/examples/typegraphs/index.ts @@ -0,0 +1,64 @@ +// skip:start + +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; +import { HttpRuntime } from "@typegraph/sdk/runtimes/http.js"; + +// skip:end + +typegraph({ + name: "homepage", + // skip:start + rate: { windowLimit: 2000, windowSec: 60, queryLimit: 200, localExcess: 0 }, + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, + // skip:end +}, (g) => { + // every field may be controlled by a policy + const pub = Policy.public(); + const metaOnly = Policy.context("email", /.+@metatype.dev/); + const publicWriteOnly = Policy.on({ create: pub, read: metaOnly }); + + // define runtimes where your queries are executed + const github = new HttpRuntime("https://api.github.com"); + const db = new PrismaRuntime("demo", "POSTGRES_CONN"); + + // a feedback object stored in Postgres + const feedback = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "email": t.email().withPolicy(publicWriteOnly), + "message": t.string({ min: 1, max: 2000 }, {}), + }, + { name: "feedback" }, + ); + + // a stargazer object from Github + const stargazer = t.struct( + { + "login": t.string({}, { name: "login" }), + // link with the feedback across runtimes + "user": github.get( + t.struct({ "user": t.string().fromParent("login") }), + t.struct({ "name": t.string().optional() }), + { path: "/users/{user}" }, + ), + }, + ); + + // skip:next-line + // out of the box authenfication support + g.auth(Auth.oauth2Github("openid email")); + + // expose part of the graph for queries + g.expose({ + stargazers: github.get( + t.struct({}), + t.list(stargazer), + { path: "/repos/metatypedev/metatype/stargazers?per_page=2" }, + ), + // automatically generate crud operations + send_feedback: db.create(feedback), + list_feedback: db.findMany(feedback), + }, pub); +}); diff --git a/website/docs/reference/typegate/authentication/jwt.py b/examples/typegraphs/jwt.py similarity index 100% rename from website/docs/reference/typegate/authentication/jwt.py rename to examples/typegraphs/jwt.py diff --git a/examples/typegraphs/jwt.ts b/examples/typegraphs/jwt.ts new file mode 100644 index 0000000000..33e0f74583 --- /dev/null +++ b/examples/typegraphs/jwt.ts @@ -0,0 +1,27 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +// skip:end + +typegraph({ + name: "jwt-authentication", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const deno = new DenoRuntime(); + const pub = Policy.public(); + + const ctx = t.struct( + { + "your_own_content": t.string().optional().fromContext("your_own_content"), + }, + ); + // highlight-next-line + g.auth(Auth.hmac256("custom")); + + g.expose({ + get_context: deno.identity(ctx), + }, pub); +}); diff --git a/website/docs/guides/external-functions/math.py b/examples/typegraphs/math.py similarity index 96% rename from website/docs/guides/external-functions/math.py rename to examples/typegraphs/math.py index 56d84c5e32..25549547f8 100644 --- a/website/docs/guides/external-functions/math.py +++ b/examples/typegraphs/math.py @@ -19,7 +19,7 @@ def math(g: Graph): fib=deno.import_( t.struct({"size": t.integer()}), t.list(t.float()), - module="fib.ts", + module="scripts/fib.ts", name="default", ).with_policy(restrict_referer), random=deno.func( diff --git a/examples/typegraphs/math.ts b/examples/typegraphs/math.ts new file mode 100644 index 0000000000..83f49c366c --- /dev/null +++ b/examples/typegraphs/math.ts @@ -0,0 +1,36 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +typegraph({ + name: "math", +}, (g) => { + const deno = new DenoRuntime(); + + const pub = Policy.public(); + + const restrict_referer = deno.policy( + "restrict_referer_policy", + '(_, context) => context["headers"]["referer"] && new URL(context["headers"]["referer"]).pathname === "/math"', + ); + + const random_item_fn = + "({ items }) => items[Math.floor(Math.random() * items.length)]"; + + g.expose({ + fib: deno.import( + t.struct({ "size": t.integer() }), + t.list(t.float()), + { module: "scripts/fib.ts", name: "default" }, + ).withPolicy(restrict_referer), + random: deno.func( + t.struct({}), + t.float(), + { code: "() => Math.random()" }, + ).withPolicy(pub), + randomItem: deno.func( + t.struct({ "items": t.list(t.string()) }), + t.string(), + { code: random_item_fn }, + ).withPolicy(pub), + }); +}); diff --git a/website/use-cases/microservice-orchestration/t.py b/examples/typegraphs/microservice-orchestration.py similarity index 100% rename from website/use-cases/microservice-orchestration/t.py rename to examples/typegraphs/microservice-orchestration.py diff --git a/examples/typegraphs/microservice-orchestration.ts b/examples/typegraphs/microservice-orchestration.ts new file mode 100644 index 0000000000..92fa2780f3 --- /dev/null +++ b/examples/typegraphs/microservice-orchestration.ts @@ -0,0 +1,34 @@ +// skip:start + +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; +import { GraphQLRuntime } from "@typegraph/sdk/runtimes/graphql.js"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +function getEnvOrDefault(key: string, defaultValue: string) { + const glob = globalThis as any; // @typescript-eslint/no-explicit-any + const value = glob?.process + ? glob?.process.env?.[key] + : glob?.Deno.env.get(key); + return value ?? defaultValue; +} +// skip:end + +typegraph({ + name: "team-a", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const pub = Policy.public(); + + const deno = new DenoRuntime(); + const records = new GraphQLRuntime( + getEnvOrDefault("TG_URL", "http://localhost:7890" + "/team-b"), + ); + + g.expose({ + version_team_b: records.query(t.struct({}), t.integer(), ["version"]), + version_team_a: deno.static(t.integer(), 3), + }, pub); +}); diff --git a/website/docs/reference/typegate/authentication/oauth2.py b/examples/typegraphs/oauth2.py similarity index 71% rename from website/docs/reference/typegate/authentication/oauth2.py rename to examples/typegraphs/oauth2.py index fbd9e040e6..8f744031fb 100644 --- a/website/docs/reference/typegate/authentication/oauth2.py +++ b/examples/typegraphs/oauth2.py @@ -17,14 +17,7 @@ def oauth2_authentication(g: Graph): ctx = t.struct({"exp": t.integer().optional().from_context("exp")}) # highlight-start - g.auth( - Auth.oauth2( - "github", - "https://github.com/login/oauth/authorize", - "https://github.com/login/oauth/access_token", - "openid profile email", - ) - ) + g.auth(Auth.oauth2_github("openid profile email")) # highlight-end g.expose( diff --git a/examples/typegraphs/oauth2.ts b/examples/typegraphs/oauth2.ts new file mode 100644 index 0000000000..d3de8b84b8 --- /dev/null +++ b/examples/typegraphs/oauth2.ts @@ -0,0 +1,27 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +// skip:end + +typegraph({ + name: "oauth2-authentication", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const deno = new DenoRuntime(); + const pub = Policy.public(); + + const ctx = t.struct({ "exp": t.integer().optional().fromContext("exp") }); + + // highlight-start + g.auth( + Auth.oauth2Github("openid profile email"), + ); + // highlight-end + + g.expose({ + get_context: deno.identity(ctx), + }, pub); +}); diff --git a/website/docs/concepts/mental-model/policies.py b/examples/typegraphs/policies-example.py similarity index 91% rename from website/docs/concepts/mental-model/policies.py rename to examples/typegraphs/policies-example.py index cd6b4c569a..784b4ffe51 100644 --- a/website/docs/concepts/mental-model/policies.py +++ b/examples/typegraphs/policies-example.py @@ -4,7 +4,7 @@ @typegraph() -def policies(g): +def policies_example(g): # skip:end deno = DenoRuntime() public = deno.policy("public", "() => true") # noqa diff --git a/website/docs/reference/policies/policies.py b/examples/typegraphs/policies.py similarity index 95% rename from website/docs/reference/policies/policies.py rename to examples/typegraphs/policies.py index 03b82e4a00..7ad37fed9b 100644 --- a/website/docs/reference/policies/policies.py +++ b/examples/typegraphs/policies.py @@ -14,7 +14,7 @@ ) def policies(g: Graph): deno = DenoRuntime() - random = RandomRuntime(seed=0) + random = RandomRuntime(seed=0, reset=None) public = Policy.public() admin_only = deno.policy( diff --git a/examples/typegraphs/policies.ts b/examples/typegraphs/policies.ts new file mode 100644 index 0000000000..1db8b64f88 --- /dev/null +++ b/examples/typegraphs/policies.ts @@ -0,0 +1,34 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; +import { RandomRuntime } from "@typegraph/sdk/runtimes/random.js"; + +// skip:end + +typegraph({ + name: "policies", + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const deno = new DenoRuntime(); + const random = new RandomRuntime({ seed: 0 }); + const pub = Policy.public(); + + const admin_only = deno.policy( + "admin_only", + "(args, { context }) => context.username ? context.username === 'admin' : null", + ); + const user_only = deno.policy( + "user_only", + "(args, { context }) => context.username ? context.username === 'user' : null", + ); + + g.auth(Auth.basic(["admin", "user"])); + + g.expose({ + public: random.gen(t.string()).withPolicy(pub), + admin_only: random.gen(t.string()).withPolicy(admin_only), + user_only: random.gen(t.string()).withPolicy(user_only), + both: random.gen(t.string()).withPolicy([user_only, admin_only]), + }); +}); diff --git a/website/docs/reference/runtimes/prisma/prisma-no-sugar.py b/examples/typegraphs/prisma-no-sugar.py similarity index 100% rename from website/docs/reference/runtimes/prisma/prisma-no-sugar.py rename to examples/typegraphs/prisma-no-sugar.py diff --git a/website/use-cases/prisma.py b/examples/typegraphs/prisma-runtime.py similarity index 86% rename from website/use-cases/prisma.py rename to examples/typegraphs/prisma-runtime.py index ef2766678e..0c4266d74f 100644 --- a/website/use-cases/prisma.py +++ b/examples/typegraphs/prisma-runtime.py @@ -36,10 +36,7 @@ def prisma_runtime(g: Graph): create_user=db.create(user), read_user=db.find_many(user), find_user=db.query_raw( - """ - SELECT id, firstname, email FROM "user" - WHERE CAST(id as VARCHAR) = ${id} OR email LIKE ${term} OR firstname LIKE ${term} - """, + 'SELECT id, firstname, email FROM "user" WHERE CAST(id as VARCHAR) = ${id} OR email LIKE ${term} OR firstname LIKE ${term}', t.struct( { "id": t.string(), diff --git a/examples/typegraphs/prisma-runtime.ts b/examples/typegraphs/prisma-runtime.ts new file mode 100644 index 0000000000..1e65828345 --- /dev/null +++ b/examples/typegraphs/prisma-runtime.ts @@ -0,0 +1,47 @@ +// skip:start +import { Policy, t } from "@typegraph/sdk/index.js"; +import { typegraph } from "@typegraph/sdk/typegraph.js"; +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; + +// skip:end + +typegraph({ + name: "prisma-runtime", + cors: { + // skip:start + allowCredentials: false, + allowHeaders: [], + allowMethods: [], + exposeHeaders: [], + maxAgeSec: undefined, + // skip:end + // .. + allowOrigin: ["https://metatype.dev", "http://localhost:3000"], + }, +}, (g) => { + const pub = Policy.public(); + const db = new PrismaRuntime("legacy", "POSTGRES_CONN"); + const user = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "email": t.email(), + "firstname": t.string({ min: 2, max: 2000 }, {}), + }, + { name: "user" }, + ); + + g.expose({ + create_user: db.create(user), + read_user: db.findMany(user), + find_user: db.queryRaw( + `SELECT id, firstname, email FROM "user" WHERE CAST(id as VARCHAR) = $\{id} OR email LIKE $\{term} OR firstname LIKE $\{term}`, + t.struct( + { + "id": t.string(), + "term": t.string(), + }, + ), + t.list(user), + ), + }, pub); +}); diff --git a/website/docs/tutorials/building-feature-roadmap-api/prisma.py b/examples/typegraphs/prisma.py similarity index 100% rename from website/docs/tutorials/building-feature-roadmap-api/prisma.py rename to examples/typegraphs/prisma.py diff --git a/examples/typegraphs/prisma.ts b/examples/typegraphs/prisma.ts new file mode 100644 index 0000000000..1fd00aa11d --- /dev/null +++ b/examples/typegraphs/prisma.ts @@ -0,0 +1,48 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; + +typegraph({ + name: "roadmap-prisma", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const pub = Policy.public(); + const db = new PrismaRuntime("db", "POSTGRES"); + + const bucket = t.struct( + { + "id": t.integer({}, { asId: true, config: { "auto": true } }), + "name": t.string(), + "ideas": t.list(g.ref("idea")), + }, + { name: "bucket" }, + ); + const idea = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "name": t.string(), + "authorEmail": t.email(), + "votes": t.list(g.ref("vote")), + "bucket": g.ref("bucket"), + }, + { name: "idea" }, + ); + const vote = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "authorEmail": t.email(), + "importance": t.enum_(["medium", "important", "critical"]).optional(), + "desc": t.string().optional(), + "idea": g.ref("idea"), + }, + { name: "vote" }, + ); + + g.expose({ + get_buckets: db.findMany(bucket), + create_bucket: db.create(bucket), + get_idea: db.findMany(idea), + create_idea: db.create(idea), + get_vote: db.create(vote), + }, pub); +}); diff --git a/website/use-cases/programmable-api-gateway/t.py b/examples/typegraphs/programmable-api-gateway.py similarity index 100% rename from website/use-cases/programmable-api-gateway/t.py rename to examples/typegraphs/programmable-api-gateway.py diff --git a/examples/typegraphs/programmable-api-gateway.ts b/examples/typegraphs/programmable-api-gateway.ts new file mode 100644 index 0000000000..00e5ad0bba --- /dev/null +++ b/examples/typegraphs/programmable-api-gateway.ts @@ -0,0 +1,32 @@ +// skip:start + +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +// skip:end + +typegraph({ + name: "programmable-api-gateway", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const deno = new DenoRuntime(); + + const pub = Policy.public(); + const roulette_access = deno.policy("roulette", "() => Math.random() < 0.5"); + + // skip:next-line + const myApiFormat = { + "static_a": { foo: "rab", access: "roulette_access" }, + "static_b": { foo: "bar", access: "public" }, + }; + + for (const [k, static_vals] of Object.entries(myApiFormat)) { + const policy = static_vals["access"] == "public" ? pub : roulette_access; + g.expose({ + [k]: deno.static(t.struct({ "foo": t.string() }), { + foo: static_vals["foo"], + }), + }, policy); + } +}); diff --git a/website/docs/tutorials/building-feature-roadmap-api/random.py b/examples/typegraphs/random.py similarity index 88% rename from website/docs/tutorials/building-feature-roadmap-api/random.py rename to examples/typegraphs/random.py index 054d60b2c6..032f76ce82 100644 --- a/website/docs/tutorials/building-feature-roadmap-api/random.py +++ b/examples/typegraphs/random.py @@ -1,6 +1,7 @@ from typegraph import typegraph, t, Graph from typegraph.runtimes.random import RandomRuntime from typegraph.graph.params import Cors +from typegraph.policy import Policy @typegraph( @@ -36,5 +37,6 @@ def roadmap(g: Graph): "desc": t.string().optional(), # makes it optional } ) - random = RandomRuntime() - g.expose(get_idea=random.gen(idea)) + random = RandomRuntime(reset=None) + pub = Policy.public() + g.expose(pub, get_idea=random.gen(idea)) diff --git a/examples/typegraphs/random.ts b/examples/typegraphs/random.ts new file mode 100644 index 0000000000..4bbf6c2df8 --- /dev/null +++ b/examples/typegraphs/random.ts @@ -0,0 +1,42 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { RandomRuntime } from "@typegraph/sdk/runtimes/random.js"; + +// skip:next-line +/* eslint-disable @typescript-eslint/no-unused-vars */ + +typegraph({ + name: "roadmap-random", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const _bucket = t.struct( + { + "id": t.integer({}, { asId: true }), + "name": t.string(), + }, + ); + + const idea = t.struct( + { + "id": t.uuid({ asId: true }), // email is just a shorthand alias for `t.string({}, {{format: "uuid"}: undefined})` + "name": t.string(), + "authorEmail": t.email(), // another string shorthand + }, + ); + + const _vote = t.struct( + { + "id": t.uuid(), + "authorEmail": t.email(), + "importance": t.enum_( + ["medium", "important", "critical"], + ).optional(), // `enum_` is also a shorthand over `t.string` + "createdAt": t.datetime(), + "desc": t.string().optional(), // makes it optional + }, + ); + + const random = new RandomRuntime({}); + const pub = Policy.public(); + g.expose({ get_idea: random.gen(idea) }, pub); +}); diff --git a/website/docs/reference/typegate/rate-limiting/rate.py b/examples/typegraphs/rate.py similarity index 95% rename from website/docs/reference/typegate/rate-limiting/rate.py rename to examples/typegraphs/rate.py index 9016fe9328..f0c4dfb243 100644 --- a/website/docs/reference/typegate/rate-limiting/rate.py +++ b/examples/typegraphs/rate.py @@ -24,7 +24,7 @@ cors=Cors(allow_origin=["https://metatype.dev", "http://localhost:3000"]), ) def rate(g: Graph): - random = RandomRuntime(seed=0) + random = RandomRuntime(seed=0, reset=None) public = Policy.public() g.expose( diff --git a/examples/typegraphs/rate.ts b/examples/typegraphs/rate.ts new file mode 100644 index 0000000000..bba72af797 --- /dev/null +++ b/examples/typegraphs/rate.ts @@ -0,0 +1,37 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { RandomRuntime } from "@typegraph/sdk/runtimes/random.js"; + +// skip:end + +typegraph({ + name: "rate", + // highlight-next-line + rate: { + // highlight-next-line + windowLimit: 35, + // highlight-next-line + windowSec: 15, + // highlight-next-line + queryLimit: 25, + // highlight-next-line + contextIdentifier: undefined, + // highlight-next-line + localExcess: 0, + // highlight-next-line + }, + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const random = new RandomRuntime({ seed: 0 }); + const pub = Policy.public(); + + g.expose({ + lightweight_call: random.gen(t.string()).rate({ calls: true, weight: 1 }), + medium_call: random.gen(t.string()).rate({ calls: true, weight: 5 }), + heavy_call: random.gen(t.string()).rate({ calls: true, weight: 15 }), + by_result_count: random.gen( + t.list(t.string()), + ).rate({ calls: false, weight: 2 }), // increment by # of results returned + }, pub); +}); diff --git a/website/docs/tutorials/building-feature-roadmap-api/reduce.py b/examples/typegraphs/reduce.py similarity index 96% rename from website/docs/tutorials/building-feature-roadmap-api/reduce.py rename to examples/typegraphs/reduce.py index 363bcae35a..f8e0993216 100644 --- a/website/docs/tutorials/building-feature-roadmap-api/reduce.py +++ b/examples/typegraphs/reduce.py @@ -51,9 +51,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - """ - (_args, { context }) => !!context.username -""", + "(_args, { context }) => !!context.username", ) g.expose( diff --git a/examples/typegraphs/reduce.ts b/examples/typegraphs/reduce.ts new file mode 100644 index 0000000000..c0b94a89ec --- /dev/null +++ b/examples/typegraphs/reduce.ts @@ -0,0 +1,70 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +typegraph({ + name: "roadmap-reduce", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const pub = Policy.public(); + const db = new PrismaRuntime("db", "POSTGRES"); + const deno = new DenoRuntime(); + + const bucket = t.struct( + { + "id": t.integer({}, { asId: true, config: { "auto": true } }), + "name": t.string(), + "ideas": t.list(g.ref("idea")), + }, + { name: "bucket" }, + ); + + const idea = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "name": t.string(), + "authorEmail": t.email(), + "votes": t.list(g.ref("vote")), + "bucket": g.ref("bucket"), + }, + { name: "idea" }, + ); + + const vote = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "authorEmail": t.email(), + "importance": t.enum_(["medium", "important", "critical"]).optional(), + "desc": t.string().optional(), + "idea": g.ref("idea"), + }, + { name: "vote" }, + ); + + g.auth(Auth.basic(["andim"])); + + const admins = deno.policy( + "admins", + "(_args, { context }) => !!context.username", + ); + + g.expose({ + create_bucket: db.create(bucket).withPolicy(admins), + get_buckets: db.findMany(bucket), + get_bucket: db.findFirst(bucket), + get_idea: db.findMany(idea), + create_idea: db.create(idea).reduce( + { + "data": { + "name": g.inherit(), + "authorEmail": g.inherit(), + "votes": g.inherit(), + "bucket": { "connect": g.inherit() }, + }, + }, + ), + create_vote: db.create(vote), + }, pub); +}); diff --git a/website/docs/tutorials/building-feature-roadmap-api/rest.py b/examples/typegraphs/rest.py similarity index 97% rename from website/docs/tutorials/building-feature-roadmap-api/rest.py rename to examples/typegraphs/rest.py index a01d71d46e..c0b62c8dce 100644 --- a/website/docs/tutorials/building-feature-roadmap-api/rest.py +++ b/examples/typegraphs/rest.py @@ -51,9 +51,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - """ - (_args, { context }) => !!context.username -""", + "(_args, { context }) => !!context.username", ) g.expose( diff --git a/examples/typegraphs/rest.ts b/examples/typegraphs/rest.ts new file mode 100644 index 0000000000..acd43f6961 --- /dev/null +++ b/examples/typegraphs/rest.ts @@ -0,0 +1,104 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; + +typegraph({ + name: "roadmap-rest", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const pub = Policy.public(); + const db = new PrismaRuntime("db", "POSTGRES"); + const deno = new DenoRuntime(); + + const bucket = t.struct( + { + "id": t.integer({}, { asId: true, config: { "auto": true } }), + "name": t.string(), + "ideas": t.list(g.ref("idea")), + }, + { name: "bucket" }, + ); + + const idea = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "name": t.string(), + "authorEmail": t.email(), + "votes": t.list(g.ref("vote")), + "bucket": g.ref("bucket"), + }, + { name: "idea" }, + ); + + const vote = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "authorEmail": t.email(), + "importance": t.enum_(["medium", "important", "critical"]).optional(), + "desc": t.string().optional(), + "idea": g.ref("idea"), + }, + { name: "vote" }, + ); + + g.auth(Auth.basic(["andim"])); + + const admins = deno.policy( + "admins", + "(_args, { context }) => !!context.username", + ); + + g.expose({ + create_bucket: db.create(bucket).withPolicy(admins), + get_buckets: db.findMany(bucket), + get_bucket: db.findFirst(bucket), + get_idea: db.findMany(idea), + create_idea: db.create(idea).reduce( + { + "data": { + "name": g.inherit(), + "authorEmail": g.inherit(), + "votes": g.inherit(), + "bucket": { "connect": g.inherit() }, + }, + }, + ), + create_vote: db.create(vote), + }, pub); + + g.rest( + ` + query get_buckets { + get_buckets { + id + name + ideas { + id + name + authorEmail + } + } + } + `, + ); + + g.rest( + ` + query get_bucket($id: Integer) { + get_bucket(where:{ + id: $id + }) { + id + name + ideas { + id + name + authorEmail + } + } + } + `, + ); +}); diff --git a/website/docs/tutorials/building-feature-roadmap-api/policies.py b/examples/typegraphs/roadmap-policies.py similarity index 96% rename from website/docs/tutorials/building-feature-roadmap-api/policies.py rename to examples/typegraphs/roadmap-policies.py index fcfe12c1af..5d33cd3be9 100644 --- a/website/docs/tutorials/building-feature-roadmap-api/policies.py +++ b/examples/typegraphs/roadmap-policies.py @@ -52,9 +52,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - """ - (_args, { context }) => !!context.username -""", + "(_args, { context }) => !!context.username", ) g.expose( diff --git a/examples/typegraphs/roadmap-policies.ts b/examples/typegraphs/roadmap-policies.ts new file mode 100644 index 0000000000..bf8ca15658 --- /dev/null +++ b/examples/typegraphs/roadmap-policies.ts @@ -0,0 +1,61 @@ +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { Auth } from "@typegraph/sdk/params.js"; +import { DenoRuntime } from "@typegraph/sdk/runtimes/deno.js"; +import { PrismaRuntime } from "@typegraph/sdk/providers/prisma.js"; + +typegraph({ + name: "roadmap-policies", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, +}, (g) => { + const pub = Policy.public(); + const db = new PrismaRuntime("db", "POSTGRES"); + const deno = new DenoRuntime(); + + const bucket = t.struct( + { + // auto generate ids during creation + "id": t.integer({}, { asId: true, config: { "auto": true } }), + "name": t.string(), + "ideas": t.list(g.ref("idea")), + }, + { name: "bucket" }, + ); + + const idea = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "name": t.string(), + "authorEmail": t.email(), + "votes": t.list(g.ref("vote")), + "bucket": g.ref("bucket"), + }, + { name: "idea" }, + ); + + const vote = t.struct( + { + "id": t.uuid({ asId: true, config: { "auto": true } }), + "authorEmail": t.email(), + "importance": t.enum_(["medium", "important", "critical"]).optional(), + "desc": t.string().optional(), + "idea": g.ref("idea"), + }, + { name: "vote" }, + ); + + g.auth(Auth.basic(["andim"])); + + const admins = deno.policy( + "admins", + "(_args, { context }) => !!context.username", + ); + + g.expose({ + create_bucket: db.create(bucket).withPolicy(admins), + get_buckets: db.findMany(bucket), + get_idea: db.findMany(idea), + create_idea: db.create(idea), + create_vote: db.create(vote), + }, pub); +}); diff --git a/website/docs/concepts/mental-model/runtimes.py b/examples/typegraphs/runtimes.py similarity index 100% rename from website/docs/concepts/mental-model/runtimes.py rename to examples/typegraphs/runtimes.py diff --git a/website/docs/guides/external-functions/fib.ts b/examples/typegraphs/scripts/fib.ts similarity index 100% rename from website/docs/guides/external-functions/fib.ts rename to examples/typegraphs/scripts/fib.ts diff --git a/website/docs/tutorials/building-feature-roadmap-api/md2html.ts.src b/examples/typegraphs/scripts/md2html.ts.src similarity index 100% rename from website/docs/tutorials/building-feature-roadmap-api/md2html.ts.src rename to examples/typegraphs/scripts/md2html.ts.src diff --git a/website/docs/concepts/mental-model/triggers.py b/examples/typegraphs/triggers.py similarity index 100% rename from website/docs/concepts/mental-model/triggers.py rename to examples/typegraphs/triggers.py diff --git a/examples/typegraphs/triggers.ts b/examples/typegraphs/triggers.ts new file mode 100644 index 0000000000..5c5142e58f --- /dev/null +++ b/examples/typegraphs/triggers.ts @@ -0,0 +1,20 @@ +// skip:start +import { Policy, t, typegraph } from "@typegraph/sdk/index.js"; +import { HttpRuntime } from "@typegraph/sdk/runtimes/http.js"; + +// skip:end + +typegraph({ + name: "triggers", +}, (g) => { + // skip:start + const pub = Policy.public(); + const http = new HttpRuntime("https://random.org/api"); + // skip:end + // ... + g.expose({ + flip: http.get(t.struct({}), t.enum_(["head", "tail"]), { + path: "/flip_coin", + }), + }, pub); +}); diff --git a/website/docs/tutorials/metatype-basics/types.py b/examples/typegraphs/typecheck.py similarity index 100% rename from website/docs/tutorials/metatype-basics/types.py rename to examples/typegraphs/typecheck.py diff --git a/website/docs/concepts/mental-model/types.py b/examples/typegraphs/types.py similarity index 100% rename from website/docs/concepts/mental-model/types.py rename to examples/typegraphs/types.py diff --git a/typegate/tests/e2e/website/website_test.ts b/typegate/tests/e2e/website/website_test.ts new file mode 100644 index 0000000000..e0de3714ed --- /dev/null +++ b/typegate/tests/e2e/website/website_test.ts @@ -0,0 +1,88 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +import { expandGlob } from "std/fs/expand_glob.ts"; +import { dirname, fromFileUrl, join, resolve } from "std/path/mod.ts"; +import { existsSync } from "std/fs/exists.ts"; +import { copySync } from "std/fs/copy.ts"; +import { Meta } from "../../utils/mod.ts"; +import { MetaTest } from "../../utils/test.ts"; +import { assertEquals } from "std/assert/assert_equals.ts"; +import config from "../../../src/config.ts"; + +export const thisDir = dirname(fromFileUrl(import.meta.url)); + +function stripIncomparable(json: string) { + return [ + // FIXME: python and deno does not produce the same tarball + (source: string) => source.replace(/"file:scripts(.)+?"/g, '""'), + ].reduce((prev, op) => op(prev), json); +} + +async function testSerializeAllPairs(t: MetaTest, dirPath: string) { + const tempDir = Deno.makeTempDirSync({ + dir: config.tmp_dir, + }); + + copySync(resolve(dirPath), tempDir, { overwrite: true }); + + for await ( + const file of expandGlob(join(tempDir, "*.py"), { + root: thisDir, + includeDirs: false, + globstar: true, + }) + ) { + const name = file.name.replace(/\.py$/, ""); + + const pyPath = file.path; + const tsPath = pyPath.replace(/\.py$/, ".ts"); + + if (existsSync(tsPath)) { + // for now, run the typegraph assuming it is deno + // FIXME: type hint issue with deno + const data = (await Deno.readTextFile(tsPath)).replace( + /\(\s*g\s*\)/, + "(g: any)", + ); + + // FIXME: + if (/fromLambda|from_lambda|fromDef|from_def/.test(data)) { + // skip these for now, reasons: + // deno directly use the given string value + // python parse its own source + continue; + } + + const { stdout: pyVersion } = await Meta.cli( + "serialize", + "--pretty", + "-f", + pyPath, + ); + + const tsTempPath = join(tempDir, `${name}.ts`); + Deno.writeTextFileSync(tsTempPath, data); + const { stdout: tsVersion } = await Meta.cli( + "serialize", + "--pretty", + "-f", + tsTempPath, + ); + + await t.should( + `serialize and compare python and typescript version of ${name}`, + () => { + assertEquals( + stripIncomparable(pyVersion), + stripIncomparable(tsVersion), + ); + }, + ); + } + } +} + +Meta.test("typegraphs comparison", async (t) => { + await testSerializeAllPairs(t, "examples/typegraphs"); +}); diff --git a/typegraph/node/sdk/src/runtimes/random.ts b/typegraph/node/sdk/src/runtimes/random.ts index 44ea35b106..aaa58d967a 100644 --- a/typegraph/node/sdk/src/runtimes/random.ts +++ b/typegraph/node/sdk/src/runtimes/random.ts @@ -18,7 +18,7 @@ export class RandomRuntime extends Runtime { super(runtimes.registerRandomRuntime(data)); } - gen(inp: t.Typedef) { + gen(out: t.Typedef) { const effect = fx.read(); const matId = runtimes.createRandomMat( @@ -33,7 +33,7 @@ export class RandomRuntime extends Runtime { return t.func( t.struct({}), - inp, + out, { _id: matId, runtime: this._id } as RandomMat, ); } diff --git a/website/blog/2023-06-18-programmable-glue/index.mdx b/website/blog/2023-06-18-programmable-glue/index.mdx index b8f46b5f06..fee73f4a47 100644 --- a/website/blog/2023-06-18-programmable-glue/index.mdx +++ b/website/blog/2023-06-18-programmable-glue/index.mdx @@ -15,7 +15,8 @@ We are introducing Metatype, a new project that allows developers to build modul Typegraphs are a declarative way to expose all APIs, storage and business logic of your stack as a single graph. They take inspiration from domain-driven design principles and in the idea that the relation between of the data is as important as data itself, even though they might be in different locations or shapes. {require("./types.py").content} +{require("../../../../examples/typegraphs/types.py").content} **Analogy in SQL**: types are similar to the Data Definition Language (DDL) with the extended capacity of describing any type of data. @@ -86,7 +86,7 @@ In theory, all frameworks and languages can produce typegraphs respecting the sp Types can also describe functions and **materializers** define how the input type gets transformed into the output type. The input and output types are similar to a function signature and a materializer to its implementation, except that it might not always know what the function body is. In such case, the materializer knows at least where and how to access it. -{require("./functions.py").content} +{require("../../../../examples/typegraphs/functions.py").content} **Analogy in SQL**: a materializer is similar to a join, a function, or an alias. @@ -96,7 +96,7 @@ Every type and materializer have a runtime associated to it. This runtime descri In practice, materializers are often not explicitly used and the usage of runtime sugar syntax is preferred. -{require("./runtimes.py").content} +{require("../../../../examples/typegraphs/runtimes.py").content} **Analogy in SQL**: a runtime is similar to a database instance running some requests. @@ -110,7 +110,7 @@ The policy decision can be: - `false`: the access is denied - `null`: the access in inherited from the parent types -{require("./policies.py").content} +{require("../../../../examples/typegraphs/policies-example.py").content} **Analogy in SQL**: policies are similar to Row Security Policies (RSP) or Row Level Security (RLS) concepts. @@ -118,6 +118,6 @@ The policy decision can be: Triggers are events launching the execution of one or multiple functions. They fire when a GraphQL request is received for the specific typegraph. -{require("./triggers.py").content} +{require("../../../../examples/typegraphs/triggers.py").content} **Analogy in SQL**: a trigger is similar to receiving a new query. diff --git a/website/docs/guides/external-functions/index.mdx b/website/docs/guides/external-functions/index.mdx index a0de7d740a..0d02986246 100644 --- a/website/docs/guides/external-functions/index.mdx +++ b/website/docs/guides/external-functions/index.mdx @@ -14,13 +14,13 @@ import CodeBlock from "@theme-original/CodeBlock"; ## External runner ## Typegraph -{require("./math.py").content} +{require("../../../../examples/typegraphs/math.py").content} ## External function definition - + ```typescript -// fib.ts +// scripts/fib.ts const CACHE = [1, 1]; const MAX_CACHE_SIZE = 1000; @@ -34,3 +34,4 @@ export default function fib({ size }: { size: number }) { } return CACHE.slice(0, size); } +``` diff --git a/website/docs/guides/files-upload/index.mdx b/website/docs/guides/files-upload/index.mdx index efa6590a97..bd52e65526 100644 --- a/website/docs/guides/files-upload/index.mdx +++ b/website/docs/guides/files-upload/index.mdx @@ -17,7 +17,7 @@ TG_RETREND_S3_SECRET_KEY=password TG_RETREND_S3_PATH_STYLE=true ``` -{require("./t.py").content} +{require("../../../../examples/typegraphs/files-upload.py").content} ## Uploading file using presigned url diff --git a/website/docs/guides/rest/index.mdx b/website/docs/guides/rest/index.mdx index 3549faa106..9eeef1fed4 100644 --- a/website/docs/guides/rest/index.mdx +++ b/website/docs/guides/rest/index.mdx @@ -10,4 +10,4 @@ import CodeBlock from "@theme-original/CodeBlock"; ## Typegraph -{require("./example_rest.py").content} +{require("../../../../examples/typegraphs/example_rest.py").content} diff --git a/website/docs/guides/securing-requests/index.mdx b/website/docs/guides/securing-requests/index.mdx index d06c6a42ca..059cc0b138 100644 --- a/website/docs/guides/securing-requests/index.mdx +++ b/website/docs/guides/securing-requests/index.mdx @@ -14,7 +14,8 @@ For your app, you will use basic authentication in order to restrict some action diff --git a/website/docs/reference/runtimes/deno/index.mdx b/website/docs/reference/runtimes/deno/index.mdx index e004c67d84..f4624fe0f0 100644 --- a/website/docs/reference/runtimes/deno/index.mdx +++ b/website/docs/reference/runtimes/deno/index.mdx @@ -13,7 +13,8 @@ This enables to run lightweight and short-lived typescript function in a sandbox diff --git a/website/docs/reference/runtimes/graphql/index.mdx b/website/docs/reference/runtimes/graphql/index.mdx index e85611a281..a5a300d0ea 100644 --- a/website/docs/reference/runtimes/graphql/index.mdx +++ b/website/docs/reference/runtimes/graphql/index.mdx @@ -10,7 +10,8 @@ Update `typegraph.py` with the highlighted lines below: diff --git a/website/docs/reference/runtimes/prisma/index.mdx b/website/docs/reference/runtimes/prisma/index.mdx index 37e064988e..38cdd5e947 100644 --- a/website/docs/reference/runtimes/prisma/index.mdx +++ b/website/docs/reference/runtimes/prisma/index.mdx @@ -37,7 +37,8 @@ Go ahead and update `typegraph.py` with the highlighted lines below: @@ -49,7 +50,7 @@ A few things to note on the changes: 1. Runtimes often come with some sugar syntax to generate types and avoid manipulating materializers directly. A corresponding declaration would have looked like this: - {require("./prisma-no-sugar.py").content} + {require("../../../../../examples/typegraphs/prisma-no-sugar.py").content} In order to use the Prisma runtime, you need to add a new environment variable. Runtimes don't take raw secrets, but instead a secret key used to look up environment variables named under the format `TG_[typegraph name]_[key]`. You can either add it in your `metatype.yml` (recommended) or in your `compose.yml`. diff --git a/website/docs/reference/typegate/authentication/index.mdx b/website/docs/reference/typegate/authentication/index.mdx index b133b4b890..e55ddee58c 100644 --- a/website/docs/reference/typegate/authentication/index.mdx +++ b/website/docs/reference/typegate/authentication/index.mdx @@ -18,7 +18,9 @@ Basic authentication is the simplest way to authenticate requests. It is done by diff --git a/website/docs/reference/typegate/rate-limiting/index.mdx b/website/docs/reference/typegate/rate-limiting/index.mdx index b7b5c00c24..e05bde06fc 100644 --- a/website/docs/reference/typegate/rate-limiting/index.mdx +++ b/website/docs/reference/typegate/rate-limiting/index.mdx @@ -13,7 +13,9 @@ The rate limiting algorithm works as follows: diff --git a/website/docs/tutorials/building-feature-roadmap-api/index.mdx b/website/docs/tutorials/building-feature-roadmap-api/index.mdx index 702b24a4db..74e98458f8 100644 --- a/website/docs/tutorials/building-feature-roadmap-api/index.mdx +++ b/website/docs/tutorials/building-feature-roadmap-api/index.mdx @@ -345,7 +345,8 @@ It also bundles the GrahpiQl API explorer and you should be able to access it at @@ -601,7 +602,8 @@ We should be able to add a few buckets and ideas now. @@ -716,7 +718,8 @@ If you're using the GraphiQl interface from earlier, there should be a panel in @@ -814,7 +817,8 @@ Requests are now only able to `connect` new ideas with pre-existing buckets and @@ -896,7 +900,8 @@ Our query is exposed like any other materializer in the GraphQl api. @@ -1074,7 +1079,8 @@ def roadmap(g: Graph): We can now access our func through the GraphQl api. diff --git a/website/docs/tutorials/metatype-basics/index.mdx b/website/docs/tutorials/metatype-basics/index.mdx index bc79d2ea91..2b0688caf3 100644 --- a/website/docs/tutorials/metatype-basics/index.mdx +++ b/website/docs/tutorials/metatype-basics/index.mdx @@ -41,7 +41,7 @@ There is no "object" or "primitive" type, only 4 main categories of types: You can combine them with each other to describe almost any data type you may need. The typegate enforces the data validation when data flows through it. Some syntactic sugar is available to make the type definition shorter: - {require("./types.py").content} + {require("../../../../examples/typegraphs/typecheck.py").content} ## The typegraph package @@ -62,7 +62,8 @@ A complete typegraph definition may look like the following: diff --git a/website/package.json b/website/package.json index 214aa98879..e8a2e9c727 100644 --- a/website/package.json +++ b/website/package.json @@ -49,8 +49,8 @@ "@docusaurus/eslint-plugin": "^3.0.1", "@docusaurus/module-type-aliases": "^3.0.1", "@docusaurus/plugin-content-docs": "^3.0.1", - "@docusaurus/types": "^3.0.1", "@docusaurus/tsconfig": "^3.0.1", + "@docusaurus/types": "^3.0.1", "@types/react": "^18.2.46", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", diff --git a/website/src/components/MiniQL/index.tsx b/website/src/components/MiniQL/index.tsx index 16aa567688..20d06dc52f 100644 --- a/website/src/components/MiniQL/index.tsx +++ b/website/src/components/MiniQL/index.tsx @@ -22,9 +22,11 @@ import { ChoicePicker } from "../ChoicePicker"; export interface MiniQLProps { typegraph: string; query: ast.DocumentNode; - code?: string; - codeLanguage?: string; - codeFileUrl?: string; + code?: Array<{ + content: string, + codeLanguage?: string; + codeFileUrl?: string; + }>; headers?: Record; variables?: Record; tab?: Tab; @@ -46,8 +48,6 @@ function MiniQLBrowser({ typegraph, query, code, - codeLanguage, - codeFileUrl, headers = {}, variables = {}, tab = "", @@ -98,23 +98,26 @@ function MiniQLBrowser({ } gap-2 w-full order-first`} > {!defaultMode || mode === "typegraph" ? ( -
- {codeFileUrl ? ( -
- See/edit full code on{" "} - - {codeFileUrl} - + code?.map(lang => +
+ {lang?.codeFileUrl ? ( +
+ See/edit full code on{" "} + + {lang?.codeFileUrl} + +
+ ) : null} + {lang ? ( + + {lang.content} + + ) : null}
- ) : null} - {code ? ( - - {code} - - ) : null} -
+ ) + ) : null} {!defaultMode || mode === "playground" ? (
diff --git a/website/src/components/TGExample/index.tsx b/website/src/components/TGExample/index.tsx index e930822d68..3945957b9e 100644 --- a/website/src/components/TGExample/index.tsx +++ b/website/src/components/TGExample/index.tsx @@ -5,15 +5,27 @@ import MiniQL, { MiniQLProps } from "@site/src/components/MiniQL"; import React from "react"; interface TGExampleProps extends MiniQLProps { - python: { content: string; path: string }; + python?: { content: string; path: string }; + typescript?: { content: string; path: string} } -export default function TGExample({ python, ...props }: TGExampleProps) { +export default function TGExample({ python, typescript, ...props }: TGExampleProps) { + const code = [ + python && { + content: python.content, + codeLanguage: "python", + codeFileUrl: python.path + }, + typescript && { + content: typescript.content, + codeLanguage: "typescript", + codeFileUrl: typescript.path + } + ].filter((v) => !!v); + return ( ); diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 1ec78be19f..8b00af7349 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -14,6 +14,7 @@ import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css"; import { ChoicePicker } from "../components/ChoicePicker"; import { CompareLandscape } from "../components/CompareLandscape"; +import Heading from "@theme/Heading"; function Header() { return ( @@ -22,13 +23,13 @@ function Header() {
Metatype logo
-

+ Declarative{" "} API development {" "} platform -

+

Build serverless backends with{" "} zero-trust and{" "} @@ -72,10 +73,10 @@ function Intro({ onChange={setProfile} />

-

+ Programming is like{" "} castle building -

+

And castle building is{" "} hard. Even the best teams can struggle to build @@ -97,10 +98,10 @@ function Stability(): JSX.Element { return (

-

+ Build stable castle with{" "} typegraphs -

+

Typegraphs are programmable virtual graphs{" "} describing all the components of your stack. They enable you to @@ -121,10 +122,10 @@ function Modularity(): JSX.Element { return (

-

+ Build modulable castle with{" "} typegate -

+

Typegate is a distributed GraphQL/REST query engine {" "} @@ -146,10 +147,10 @@ function Reusability(): JSX.Element { return (

-

+ Build reusable castle with{" "} Metatype -

+

Install third parties as dependencies{" "} and start reusing components. The Meta CLI offers you live reloading @@ -246,7 +247,7 @@ function Features(): JSX.Element {

-

{props.title}

+ {props.title}

{props.description}

@@ -260,10 +261,10 @@ function TryIt(): JSX.Element { return (
-

+ Try the playground and{" "} deploy -

+

Metatype's unique approach combines the{" "} best of the two worlds. You are quickly productive @@ -273,7 +274,8 @@ function TryIt(): JSX.Element {

-

+ Easily add your{" "} own runtime -

+

More than 12 runtimes are natively supported. Usually it takes less than a day to integrate a new one and support the most frequent @@ -373,10 +375,10 @@ function DemoVideo(): JSX.Element { return (

-

+ Forget weeks, deliver APIs{" "} in hours -

+

Watch the 3 minutes introduction{" "} of the Metatype platform and start designing your own typegraph. Once @@ -403,10 +405,10 @@ function Landscape(): JSX.Element { return (

-

+ Bringing speed and{" "} novelty to backend development -

+

Metatype fills a gap in the tech landscape by introducing a new way to build fast and developer-friendly APIs that are{" "} diff --git a/website/tsconfig.json b/website/tsconfig.json index cc328b4a41..37c7cd4c6b 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -6,6 +6,7 @@ "strict": true, "allowJs": false, "baseUrl": ".", + "esModuleInterop": true, "types": [ "docusaurus-theme-frontmatter" ] diff --git a/website/use-cases/automatic-crud-validation/index.mdx b/website/use-cases/automatic-crud-validation/index.mdx index 5e0bbd091d..1ab25e7b1d 100644 --- a/website/use-cases/automatic-crud-validation/index.mdx +++ b/website/use-cases/automatic-crud-validation/index.mdx @@ -24,6 +24,7 @@ Metatype simplifies the development of CRUD APIs by providing the [Prisma runtim diff --git a/website/use-cases/backend-for-frontend/index.mdx b/website/use-cases/backend-for-frontend/index.mdx index 36dcc08926..e650865c8b 100644 --- a/website/use-cases/backend-for-frontend/index.mdx +++ b/website/use-cases/backend-for-frontend/index.mdx @@ -24,6 +24,7 @@ Metatype can act as a generic BFF component, serving multiple dedicated APIs and diff --git a/website/use-cases/faas-runner/index.mdx b/website/use-cases/faas-runner/index.mdx index 869c958dd7..e94068af60 100644 --- a/website/use-cases/faas-runner/index.mdx +++ b/website/use-cases/faas-runner/index.mdx @@ -28,6 +28,7 @@ To solve the use case of executing multiple functions and collecting their resul diff --git a/website/use-cases/graphql-server/index.mdx b/website/use-cases/graphql-server/index.mdx index 1f4bca03be..b54cc14d18 100644 --- a/website/use-cases/graphql-server/index.mdx +++ b/website/use-cases/graphql-server/index.mdx @@ -26,6 +26,7 @@ This can be seen as a declarative GraphQL servers, where the server is orchestra diff --git a/website/use-cases/iam-provider/index.mdx b/website/use-cases/iam-provider/index.mdx index 98dc0a2fee..dad2977898 100644 --- a/website/use-cases/iam-provider/index.mdx +++ b/website/use-cases/iam-provider/index.mdx @@ -26,6 +26,7 @@ Once the user is authenticated, you can use policy access based control (PBAC) t diff --git a/website/use-cases/microservice-orchestration/index.mdx b/website/use-cases/microservice-orchestration/index.mdx index 650d229f18..36de2ce718 100644 --- a/website/use-cases/microservice-orchestration/index.mdx +++ b/website/use-cases/microservice-orchestration/index.mdx @@ -26,6 +26,7 @@ Additionally, Metatype gateway can provide other important features such as rate diff --git a/website/use-cases/orm-for-the-edge/index.mdx b/website/use-cases/orm-for-the-edge/index.mdx index f74b7fc791..6c75b9af1e 100644 --- a/website/use-cases/orm-for-the-edge/index.mdx +++ b/website/use-cases/orm-for-the-edge/index.mdx @@ -24,6 +24,7 @@ Metatype can act out of the box as a lightweight relay API, simplifying database diff --git a/website/use-cases/programmable-api-gateway/index.mdx b/website/use-cases/programmable-api-gateway/index.mdx index b09f2ec24f..c620bc347e 100644 --- a/website/use-cases/programmable-api-gateway/index.mdx +++ b/website/use-cases/programmable-api-gateway/index.mdx @@ -26,6 +26,7 @@ This enables developer to quickly build and deploy any update the API or the bus diff --git a/whiz.yaml b/whiz.yaml index ca1fd57f53..2c07d22f23 100644 --- a/whiz.yaml +++ b/whiz.yaml @@ -37,7 +37,7 @@ meta-cli: - "typegate/src/**/*.ts" - "typegate/core/**/*.rs" - "libs/deno/**/*" - command: "cargo run -p meta-cli -- -C website dev --run-destructive-migrations" + command: "cargo run -p meta-cli -- -C examples/typegraphs dev --run-destructive-migrations" depends_on: - typegraph