From 54da2797417cb9e0e84f541bfbbe174b0737276f Mon Sep 17 00:00:00 2001 From: FITAHIANA Nomeniavo joe <24nomeniavo@gmail.com> Date: Tue, 13 Aug 2024 11:58:36 +0300 Subject: [PATCH] feat: kv runtime (#797) #### Migration notes ... - [x] The change comes with new or modified tests - [x] End-user documentation is updated to reflect the change - [ ] Hard-to-understand functions have explanatory comments --- examples/metatype.yaml | 2 + examples/typegraphs/kv.py | 24 + examples/typegraphs/kv.ts | 24 + examples/typegraphs/metagen/rs/mdk.rs | 2 +- libs/common/src/typegraph/runtimes/kv.rs | 9 + libs/common/src/typegraph/runtimes/mod.rs | 4 + typegate/src/runtimes/kv.ts | 89 +++ typegate/src/runtimes/mod.ts | 1 + typegate/src/typegraph/types.ts | 4 + typegate/tests/e2e/website/website_test.ts | 1 + .../runtimes/kv/__snapshots__/kv_test.ts.snap | 557 ++++++++++++++++++ typegate/tests/runtimes/kv/kv.py | 19 + typegate/tests/runtimes/kv/kv.ts | 17 + typegate/tests/runtimes/kv/kv_test.ts | 90 +++ typegraph/core/src/conversion/runtimes.rs | 29 +- typegraph/core/src/runtimes/mod.rs | 27 +- typegraph/core/wit/typegraph.wit | 17 + typegraph/deno/sdk/src/runtimes/kv.ts | 73 +++ .../python/typegraph/runtimes/__init__.py | 1 + typegraph/python/typegraph/runtimes/kv.py | 70 +++ website/docs/reference/runtimes/index.mdx | 1 + website/docs/reference/runtimes/kv/index.mdx | 23 + 22 files changed, 1080 insertions(+), 4 deletions(-) create mode 100644 examples/typegraphs/kv.py create mode 100644 examples/typegraphs/kv.ts create mode 100644 libs/common/src/typegraph/runtimes/kv.rs create mode 100644 typegate/src/runtimes/kv.ts create mode 100644 typegate/tests/runtimes/kv/__snapshots__/kv_test.ts.snap create mode 100644 typegate/tests/runtimes/kv/kv.py create mode 100644 typegate/tests/runtimes/kv/kv.ts create mode 100644 typegate/tests/runtimes/kv/kv_test.ts create mode 100644 typegraph/deno/sdk/src/runtimes/kv.ts create mode 100644 typegraph/python/typegraph/runtimes/kv.py create mode 100644 website/docs/reference/runtimes/kv/index.mdx diff --git a/examples/metatype.yaml b/examples/metatype.yaml index c92d021a7b..4a9a64f08f 100644 --- a/examples/metatype.yaml +++ b/examples/metatype.yaml @@ -151,9 +151,11 @@ typegraphs: exclude: - "**/*" - "typegraphs/temporal.py" + - "typegraphs/kv.py" typescript: exclude: - "typegraphs/temporal.ts" + - "typegraphs/kv.ts" include: - "typegraphs/*.ts" javascript: diff --git a/examples/typegraphs/kv.py b/examples/typegraphs/kv.py new file mode 100644 index 0000000000..325871210c --- /dev/null +++ b/examples/typegraphs/kv.py @@ -0,0 +1,24 @@ +# skip:start +from typegraph import Graph, Policy, typegraph +from typegraph.graph.params import Cors +from typegraph.runtimes.kv import KvRuntime + +# skip:end + + +@typegraph( + # skip:start + cors=Cors(allow_origin=["https://metatype.dev", "http://localhost:3000"]), + # skip:end +) +def key_value(g: Graph): + kv = KvRuntime("REDIS") + + g.expose( + Policy.public(), + get=kv.get(), + set=kv.set(), + delete=kv.delete(), + keys=kv.keys(), + values=kv.values(), + ) diff --git a/examples/typegraphs/kv.ts b/examples/typegraphs/kv.ts new file mode 100644 index 0000000000..d383da1ab4 --- /dev/null +++ b/examples/typegraphs/kv.ts @@ -0,0 +1,24 @@ +// skip:start +import { Policy, typegraph } from "@typegraph/sdk/index.ts"; +import { KvRuntime } from "@typegraph/sdk/runtimes/kv.ts"; + +// skip:end + +export const tg = await typegraph( + { + name: "key-value", + // skip:next-line + cors: { allowOrigin: ["https://metatype.dev", "http://localhost:3000"] }, + }, + (g) => { + const kv = new KvRuntime("REDIS"); + const pub = Policy.public(); + g.expose({ + get: kv.get(), + set: kv.set(), + delete: kv.delete(), + keys: kv.keys(), + values: kv.values(), + }, pub); + }, +); diff --git a/examples/typegraphs/metagen/rs/mdk.rs b/examples/typegraphs/metagen/rs/mdk.rs index 55cdab8475..ff54e03c39 100644 --- a/examples/typegraphs/metagen/rs/mdk.rs +++ b/examples/typegraphs/metagen/rs/mdk.rs @@ -109,7 +109,7 @@ impl Router { } pub fn init(&self, args: InitArgs) -> Result { - static MT_VERSION: &str = "0.4.7-0"; + static MT_VERSION: &str = "0.4.8-0"; if args.metatype_version != MT_VERSION { return Err(InitError::VersionMismatch(MT_VERSION.into())); } diff --git a/libs/common/src/typegraph/runtimes/kv.rs b/libs/common/src/typegraph/runtimes/kv.rs new file mode 100644 index 0000000000..8767a97b92 --- /dev/null +++ b/libs/common/src/typegraph/runtimes/kv.rs @@ -0,0 +1,9 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct KvRuntimeData { + pub url: String, +} diff --git a/libs/common/src/typegraph/runtimes/mod.rs b/libs/common/src/typegraph/runtimes/mod.rs index 0cb991bd05..0d5b438216 100644 --- a/libs/common/src/typegraph/runtimes/mod.rs +++ b/libs/common/src/typegraph/runtimes/mod.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use self::deno::DenoRuntimeData; use self::graphql::GraphQLRuntimeData; use self::http::HTTPRuntimeData; +use self::kv::KvRuntimeData; use self::prisma::PrismaRuntimeData; use self::python::PythonRuntimeData; use self::random::RandomRuntimeData; @@ -19,6 +20,7 @@ use self::wasm::WasmRuntimeData; pub mod deno; pub mod graphql; pub mod http; +pub mod kv; pub mod prisma; pub mod python; pub mod random; @@ -53,6 +55,7 @@ pub enum KnownRuntime { WasmWire(WasmRuntimeData), Typegate(TypegateRuntimeData), Typegraph(TypegraphRuntimeData), + Kv(KvRuntimeData), } #[derive(Serialize, Deserialize, Clone, Debug, Default)] @@ -85,6 +88,7 @@ impl TGRuntime { KnownRuntime::WasmReflected(_) => "wasm_reflected", KnownRuntime::Typegate(_) => "typegate", KnownRuntime::Typegraph(_) => "typegraph", + KnownRuntime::Kv(_) => "kv", }, TGRuntime::Unknown(UnknownRuntime { name, .. }) => name, } diff --git a/typegate/src/runtimes/kv.ts b/typegate/src/runtimes/kv.ts new file mode 100644 index 0000000000..d171f983d5 --- /dev/null +++ b/typegate/src/runtimes/kv.ts @@ -0,0 +1,89 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +import { connect, parseURL, Redis } from "redis"; +import { ComputeStage } from "../engine/query_engine.ts"; +import { getLogger, Logger } from "../log.ts"; +import { TypeGraph } from "../typegraph/mod.ts"; +import { KvRuntimeData } from "../typegraph/types.ts"; +import { Resolver, RuntimeInitParams } from "../types.ts"; +import { registerRuntime } from "./mod.ts"; +import { Runtime } from "./Runtime.ts"; + +const logger = getLogger(import.meta); + +@registerRuntime("kv") +export class KvRuntime extends Runtime { + private logger: Logger; + private redis: Redis; + + private constructor(typegraphName: string, redis: Redis) { + super(typegraphName); + this.logger = getLogger(`kv:'${typegraphName}'`); + this.redis = redis; + } + + static async init(params: RuntimeInitParams): Promise { + logger.info("Initializing KvRuntime"); + logger.debug(`Init params: ${JSON.stringify(params)}`); + const { typegraph, args, secretManager } = params as RuntimeInitParams< + KvRuntimeData + >; + const typegraphName = TypeGraph.formatName(typegraph); + const url = secretManager.secretOrFail(args.url); + const redisConnectionOption = parseURL(url); + const connection = await connect(redisConnectionOption); + const instance = new KvRuntime(typegraphName, connection); + instance.logger.info("Registered KvRuntime"); + + return instance; + } + + // deno-lint-ignore require-await + async deinit(): Promise { + this.redis.close(); + } + + materialize( + stage: ComputeStage, + _waitlist: ComputeStage[], + _verbose: boolean, + ): ComputeStage[] | Promise { + const name = stage.props.materializer?.name; + + const resolver: Resolver = async (args) => { + if (name == "kv_set") { + const { key, value } = args; + return await this.redis.set(key, value); + } + + if (name == "kv_get") { + const { key } = args; + return await this.redis.get(key); + } + + if (name == "kv_delete") { + const { key } = args; + return await this.redis.del(key); + } + + if (name == "kv_keys") { + const { filter } = args; + return await this.redis.keys(filter ?? "*"); + } + + if (name === "kv_values") { + const { filter } = args; + const keys = await this.redis.keys(filter ?? "*"); + const values = await Promise.all( + keys.map(async (key) => { + const value = await this.redis.get(key); + return value; + }), + ); + return values; + } + }; + return [new ComputeStage({ ...stage.props, resolver })]; + } +} diff --git a/typegate/src/runtimes/mod.ts b/typegate/src/runtimes/mod.ts index 56727de88c..1c59cf149e 100644 --- a/typegate/src/runtimes/mod.ts +++ b/typegate/src/runtimes/mod.ts @@ -48,5 +48,6 @@ export async function init_runtimes(): Promise { import("./typegraph.ts"), import("./wasm_wire.ts"), import("./wasm_reflected.ts"), + import("./kv.ts"), ]); } diff --git a/typegate/src/typegraph/types.ts b/typegate/src/typegraph/types.ts index 4dd07fc3a6..81902b9e0d 100644 --- a/typegate/src/typegraph/types.ts +++ b/typegate/src/typegraph/types.ts @@ -561,3 +561,7 @@ export interface WasiMatData { func: string; wasmArtifact: string; } + +export interface KvRuntimeData { + url: string; +} diff --git a/typegate/tests/e2e/website/website_test.ts b/typegate/tests/e2e/website/website_test.ts index 23a7026e2c..82c288c6a0 100644 --- a/typegate/tests/e2e/website/website_test.ts +++ b/typegate/tests/e2e/website/website_test.ts @@ -34,6 +34,7 @@ const list = [ "index", "injections", "jwt", + "kv", "math", "metagen-deno", "metagen-py", diff --git a/typegate/tests/runtimes/kv/__snapshots__/kv_test.ts.snap b/typegate/tests/runtimes/kv/__snapshots__/kv_test.ts.snap new file mode 100644 index 0000000000..7f4e01cbc0 --- /dev/null +++ b/typegate/tests/runtimes/kv/__snapshots__/kv_test.ts.snap @@ -0,0 +1,557 @@ +export const snapshot = {}; + +snapshot[`Typegraph using kv 1`] = ` +'[ + { + "types": [ + { + "type": "object", + "title": "kv", + "runtime": 0, + "policies": [], + "config": {}, + "as_id": false, + "properties": { + "get": 1, + "set": 4, + "delete": 6, + "keys": 8, + "values": 12 + }, + "required": [ + "get", + "set", + "delete", + "keys", + "values" + ] + }, + { + "type": "function", + "title": "func_25", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 2, + "output": 3, + "materializer": 0, + "rate_weight": null, + "rate_calls": false + }, + { + "type": "object", + "title": "object_1", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "properties": { + "key": 3 + }, + "required": [] + }, + { + "type": "string", + "title": "string_0", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false + }, + { + "type": "function", + "title": "func_26", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 5, + "output": 3, + "materializer": 2, + "rate_weight": null, + "rate_calls": false + }, + { + "type": "object", + "title": "object_6", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "properties": { + "key": 3, + "value": 3 + }, + "required": [] + }, + { + "type": "function", + "title": "func_27", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 2, + "output": 7, + "materializer": 3, + "rate_weight": null, + "rate_calls": false + }, + { + "type": "integer", + "title": "integer_11", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false + }, + { + "type": "function", + "title": "func_28", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 9, + "output": 11, + "materializer": 4, + "rate_weight": null, + "rate_calls": false + }, + { + "type": "object", + "title": "object_15", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "properties": { + "filter": 10 + }, + "required": [] + }, + { + "type": "optional", + "title": "optional_14", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "item": 3, + "default_value": null + }, + { + "type": "list", + "title": "list_17", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "items": 3 + }, + { + "type": "function", + "title": "func_29", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 9, + "output": 11, + "materializer": 5, + "rate_weight": null, + "rate_calls": false + } + ], + "materializers": [ + { + "name": "kv_get", + "runtime": 1, + "effect": { + "effect": "read", + "idempotent": true + }, + "data": {} + }, + { + "name": "predefined_function", + "runtime": 0, + "effect": { + "effect": "read", + "idempotent": true + }, + "data": { + "name": "true" + } + }, + { + "name": "kv_set", + "runtime": 1, + "effect": { + "effect": "update", + "idempotent": false + }, + "data": {} + }, + { + "name": "kv_delete", + "runtime": 1, + "effect": { + "effect": "delete", + "idempotent": true + }, + "data": {} + }, + { + "name": "kv_keys", + "runtime": 1, + "effect": { + "effect": "read", + "idempotent": true + }, + "data": {} + }, + { + "name": "kv_values", + "runtime": 1, + "effect": { + "effect": "read", + "idempotent": true + }, + "data": {} + } + ], + "runtimes": [ + { + "name": "deno", + "data": { + "worker": "default", + "permissions": {} + } + }, + { + "name": "kv", + "data": { + "url": "REDIS" + } + } + ], + "policies": [ + { + "name": "__public", + "materializer": 1 + } + ], + "meta": { + "prefix": null, + "secrets": [], + "queries": { + "dynamic": true, + "endpoints": [] + }, + "cors": { + "allow_origin": [], + "allow_headers": [], + "expose_headers": [], + "allow_methods": [], + "allow_credentials": true, + "max_age_sec": null + }, + "auths": [], + "rate": null, + "version": "0.0.3", + "randomSeed": null, + "artifacts": {} + } + } +]' +`; + +snapshot[`Typegraph using kv 2`] = ` +'[ + { + "types": [ + { + "type": "object", + "title": "kv", + "runtime": 0, + "policies": [], + "config": {}, + "as_id": false, + "properties": { + "get": 1, + "set": 4, + "delete": 6, + "keys": 8, + "values": 12 + }, + "required": [ + "get", + "set", + "delete", + "keys", + "values" + ] + }, + { + "type": "function", + "title": "func_25", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 2, + "output": 3, + "materializer": 0, + "rate_weight": null, + "rate_calls": false + }, + { + "type": "object", + "title": "object_1", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "properties": { + "key": 3 + }, + "required": [] + }, + { + "type": "string", + "title": "string_0", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false + }, + { + "type": "function", + "title": "func_26", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 5, + "output": 3, + "materializer": 2, + "rate_weight": null, + "rate_calls": false + }, + { + "type": "object", + "title": "object_6", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "properties": { + "key": 3, + "value": 3 + }, + "required": [] + }, + { + "type": "function", + "title": "func_27", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 2, + "output": 7, + "materializer": 3, + "rate_weight": null, + "rate_calls": false + }, + { + "type": "integer", + "title": "integer_11", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false + }, + { + "type": "function", + "title": "func_28", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 9, + "output": 11, + "materializer": 4, + "rate_weight": null, + "rate_calls": false + }, + { + "type": "object", + "title": "object_15", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "properties": { + "filter": 10 + }, + "required": [] + }, + { + "type": "optional", + "title": "optional_14", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "item": 3, + "default_value": null + }, + { + "type": "list", + "title": "list_17", + "runtime": 1, + "policies": [], + "config": {}, + "as_id": false, + "items": 3 + }, + { + "type": "function", + "title": "func_29", + "runtime": 1, + "policies": [ + 0 + ], + "config": {}, + "as_id": false, + "input": 9, + "output": 11, + "materializer": 5, + "rate_weight": null, + "rate_calls": false + } + ], + "materializers": [ + { + "name": "kv_get", + "runtime": 1, + "effect": { + "effect": "read", + "idempotent": true + }, + "data": {} + }, + { + "name": "predefined_function", + "runtime": 0, + "effect": { + "effect": "read", + "idempotent": true + }, + "data": { + "name": "true" + } + }, + { + "name": "kv_set", + "runtime": 1, + "effect": { + "effect": "update", + "idempotent": false + }, + "data": {} + }, + { + "name": "kv_delete", + "runtime": 1, + "effect": { + "effect": "delete", + "idempotent": true + }, + "data": {} + }, + { + "name": "kv_keys", + "runtime": 1, + "effect": { + "effect": "read", + "idempotent": true + }, + "data": {} + }, + { + "name": "kv_values", + "runtime": 1, + "effect": { + "effect": "read", + "idempotent": true + }, + "data": {} + } + ], + "runtimes": [ + { + "name": "deno", + "data": { + "worker": "default", + "permissions": {} + } + }, + { + "name": "kv", + "data": { + "url": "REDIS" + } + } + ], + "policies": [ + { + "name": "__public", + "materializer": 1 + } + ], + "meta": { + "prefix": null, + "secrets": [], + "queries": { + "dynamic": true, + "endpoints": [] + }, + "cors": { + "allow_origin": [], + "allow_headers": [], + "expose_headers": [], + "allow_methods": [], + "allow_credentials": true, + "max_age_sec": null + }, + "auths": [], + "rate": null, + "version": "0.0.3", + "randomSeed": null, + "artifacts": {} + } + } +]' +`; diff --git a/typegate/tests/runtimes/kv/kv.py b/typegate/tests/runtimes/kv/kv.py new file mode 100644 index 0000000000..b93a71ee08 --- /dev/null +++ b/typegate/tests/runtimes/kv/kv.py @@ -0,0 +1,19 @@ +# Copyright Metatype OÜ, licensed under the Elastic License 2.0. +# SPDX-License-Identifier: Elastic-2.0 + +from typegraph import Graph, Policy, typegraph +from typegraph.runtimes.kv import KvRuntime + + +@typegraph() +def kv(g: Graph): + kv = KvRuntime("REDIS") + + g.expose( + Policy.public(), + get=kv.get(), + set=kv.set(), + delete=kv.delete(), + keys=kv.keys(), + values=kv.values(), + ) diff --git a/typegate/tests/runtimes/kv/kv.ts b/typegate/tests/runtimes/kv/kv.ts new file mode 100644 index 0000000000..1654e6e31b --- /dev/null +++ b/typegate/tests/runtimes/kv/kv.ts @@ -0,0 +1,17 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +import { Policy, typegraph } from "@typegraph/sdk/index.ts"; +import { KvRuntime } from "@typegraph/sdk/runtimes/kv.ts"; + +export const tg = await typegraph("kv", (g: any) => { + const kv = new KvRuntime("REDIS"); + const pub = Policy.public(); + g.expose({ + get: kv.get(), + set: kv.set(), + delete: kv.delete(), + keys: kv.keys(), + values: kv.values(), + }, pub); +}); diff --git a/typegate/tests/runtimes/kv/kv_test.ts b/typegate/tests/runtimes/kv/kv_test.ts new file mode 100644 index 0000000000..03335d5b54 --- /dev/null +++ b/typegate/tests/runtimes/kv/kv_test.ts @@ -0,0 +1,90 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +import { MetaTest } from "test-utils/test.ts"; +import { gql, Meta } from "test-utils/mod.ts"; + +async function testSerialize(t: MetaTest, file: string) { + await t.should(`serialize typegraph ${file}`, async () => { + const { stdout: tg } = await Meta.cli("serialize", "--pretty", "-f", file); + await t.assertSnapshot(tg); + }); +} + +Meta.test({ name: "Typegraph using kv" }, async (t) => { + await testSerialize(t, "runtimes/kv/kv.ts"); + await testSerialize(t, "runtimes/kv/kv.py"); +}); + +Meta.test( + { + name: "Kv runtime", + }, + async (t) => { + const e = await t.engine("runtimes/kv/kv.ts", { + secrets: { + REDIS: "redis://:password@localhost:6379", + }, + }); + + await t.should("set key to value", async () => { + await gql` + mutation { + set(key: "name", value: "joe") + } + ` + .expectData({ + set: "OK", + }) + .on(e); + }); + + await t.should("get value from redis by key", async () => { + await gql` + query { + get(key: "name") + } + ` + .expectData({ + get: "joe", + }) + .on(e); + }); + + await t.should("get all keys from redis", async () => { + await gql` + query { + keys(filter: "*") + } + ` + .expectData({ + keys: ["name"], + }) + .on(e); + }); + + await t.should("get values from redis", async () => { + await gql` + query { + values(filter: "*") + } + ` + .expectData({ + values: ["joe"], + }) + .on(e); + }); + + await t.should("delete key", async () => { + await gql` + mutation { + delete(key: "name") + } + ` + .expectData({ + delete: 1, + }) + .on(e); + }); + }, +); diff --git a/typegraph/core/src/conversion/runtimes.rs b/typegraph/core/src/conversion/runtimes.rs index 345affffda..ea626245d5 100644 --- a/typegraph/core/src/conversion/runtimes.rs +++ b/typegraph/core/src/conversion/runtimes.rs @@ -8,11 +8,12 @@ use crate::runtimes::{ Runtime, TemporalMaterializer, WasmMaterializer, }; use crate::wit::core::{Artifact as WitArtifact, RuntimeId}; -use crate::wit::runtimes::{HttpMethod, MaterializerHttpRequest}; +use crate::wit::runtimes::{HttpMethod, KvMaterializer, MaterializerHttpRequest}; use crate::{typegraph::TypegraphContext, wit::runtimes::Effect as WitEffect}; use common::typegraph::runtimes::deno::DenoRuntimeData; use common::typegraph::runtimes::graphql::GraphQLRuntimeData; use common::typegraph::runtimes::http::HTTPRuntimeData; +use common::typegraph::runtimes::kv::KvRuntimeData; use common::typegraph::runtimes::python::PythonRuntimeData; use common::typegraph::runtimes::random::RandomRuntimeData; use common::typegraph::runtimes::s3::S3RuntimeData; @@ -377,6 +378,31 @@ impl MaterializerConverter for TemporalMaterializer { } } +impl MaterializerConverter for KvMaterializer { + fn convert( + &self, + c: &mut TypegraphContext, + runtime_id: RuntimeId, + effect: WitEffect, + ) -> Result { + let runtime = c.register_runtime(runtime_id)?; + let data = serde_json::from_value(json!({})).map_err(|e| e.to_string())?; + let name = match self { + KvMaterializer::Get => "kv_get".to_string(), + KvMaterializer::Set => "kv_set".to_string(), + KvMaterializer::Delete => "kv_delete".to_string(), + KvMaterializer::Keys => "kv_keys".to_string(), + KvMaterializer::Values => "kv_values".to_string(), + }; + Ok(Materializer { + name, + runtime, + effect: effect.into(), + data, + }) + } +} + pub fn convert_materializer( c: &mut TypegraphContext, mat: RawMaterializer, @@ -470,5 +496,6 @@ pub fn convert_runtime(_c: &mut TypegraphContext, runtime: Runtime) -> Result Ok(TGRuntime::Known(Rt::Kv(KvRuntimeData { url: d.url.clone() })).into()), } } diff --git a/typegraph/core/src/runtimes/mod.rs b/typegraph/core/src/runtimes/mod.rs index ad0953083d..7e068466a8 100644 --- a/typegraph/core/src/runtimes/mod.rs +++ b/typegraph/core/src/runtimes/mod.rs @@ -27,8 +27,9 @@ use crate::wit::aws::S3RuntimeData; use crate::wit::core::{FuncParams, MaterializerId, RuntimeId, TypeId as CoreTypeId}; use crate::wit::runtimes::{ self as wit, BaseMaterializer, Error as TgError, GraphqlRuntimeData, HttpRuntimeData, - MaterializerHttpRequest, PrismaLinkData, PrismaMigrationOperation, PrismaRuntimeData, - RandomRuntimeData, TemporalOperationData, TemporalRuntimeData, WasmRuntimeData, + KvMaterializer, KvRuntimeData, MaterializerHttpRequest, PrismaLinkData, + PrismaMigrationOperation, PrismaRuntimeData, RandomRuntimeData, TemporalOperationData, + TemporalRuntimeData, WasmRuntimeData, }; use crate::{typegraph::TypegraphContext, wit::runtimes::Effect as WitEffect}; use enum_dispatch::enum_dispatch; @@ -65,6 +66,7 @@ pub enum Runtime { Typegate, Typegraph, S3(Rc), + Kv(Rc), } #[derive(Debug, Clone)] @@ -166,6 +168,14 @@ impl Materializer { data: data.into(), } } + + fn kv(runtime_id: RuntimeId, data: KvMaterializer, effect: wit::Effect) -> Self { + Self { + runtime_id, + effect, + data: Rc::new(data).into(), + } + } } #[derive(Debug, Clone)] @@ -183,6 +193,7 @@ pub enum MaterializerData { Typegate(TypegateOperation), Typegraph(TypegraphOperation), S3(Rc), + Kv(Rc), } macro_rules! prisma_op { @@ -643,4 +654,16 @@ impl crate::wit::runtimes::Guest for crate::Lib { effect, ))) } + + fn register_kv_runtime(data: KvRuntimeData) -> Result { + Ok(Store::register_runtime(Runtime::Kv(data.into()))) + } + + fn kv_operation( + base: BaseMaterializer, + data: KvMaterializer, + ) -> Result { + let mat = Materializer::kv(base.runtime, data, base.effect); + Ok(Store::register_materializer(mat)) + } } diff --git a/typegraph/core/wit/typegraph.wit b/typegraph/core/wit/typegraph.wit index b8f2c17aca..deaf46f98a 100644 --- a/typegraph/core/wit/typegraph.wit +++ b/typegraph/core/wit/typegraph.wit @@ -483,6 +483,23 @@ interface runtimes { } register-typegraph-materializer: func(operation: typegraph-operation) -> result; + + // kv + record kv-runtime-data { + url: string + } + + register-kv-runtime: func(data: kv-runtime-data) -> result; + + enum kv-materializer { + get, + set, + delete, + keys, + values, + } + + kv-operation: func(base: base-materializer, data: kv-materializer) -> result; } interface aws { diff --git a/typegraph/deno/sdk/src/runtimes/kv.ts b/typegraph/deno/sdk/src/runtimes/kv.ts new file mode 100644 index 0000000000..e510619dd8 --- /dev/null +++ b/typegraph/deno/sdk/src/runtimes/kv.ts @@ -0,0 +1,73 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +import { Materializer, Runtime } from "./mod.ts"; +import * as t from "../types.ts"; +import { runtimes } from "../wit.ts"; +import { Effect, KvMaterializer } from "../gen/typegraph_core.d.ts"; + +import { fx } from "../index.ts"; + +class KvOperationMat implements Materializer { + operation: KvMaterializer; + _id: number; + constructor(id: number, operation: KvMaterializer) { + this._id = id; + this.operation = operation; + } +} + +export class KvRuntime extends Runtime { + url: string; + constructor(url: string) { + const id = runtimes.registerKvRuntime({ url }); + super(id); + this.url = url; + } + + #operation(operation: KvMaterializer, effect: Effect) { + const mad_id = runtimes.kvOperation({ + runtime: this._id, + effect: effect, + }, operation); + + return new KvOperationMat(mad_id, operation); + } + + set() { + const mat = this.#operation("set", fx.update(false)); + return t.func( + t.struct({ "key": t.string(), "value": t.string() }), + t.string(), + mat, + ); + } + + get() { + const mat = this.#operation("get", fx.read()); + return t.func(t.struct({ "key": t.string() }), t.string(), mat); + } + + delete() { + const mat = this.#operation("delete", fx.delete_()); + return t.func(t.struct({ "key": t.string() }), t.integer(), mat); + } + + keys() { + const mat = this.#operation("keys", fx.read()); + return t.func( + t.struct({ "filter": t.string().optional() }), + t.list(t.string()), + mat, + ); + } + + values() { + const mat = this.#operation("values", fx.read()); + return t.func( + t.struct({ "filter": t.string().optional() }), + t.list(t.string()), + mat, + ); + } +} diff --git a/typegraph/python/typegraph/runtimes/__init__.py b/typegraph/python/typegraph/runtimes/__init__.py index 8744e1ad36..88d9bb01ac 100644 --- a/typegraph/python/typegraph/runtimes/__init__.py +++ b/typegraph/python/typegraph/runtimes/__init__.py @@ -8,3 +8,4 @@ from typegraph.runtimes.python import PythonRuntime # noqa from typegraph.runtimes.random import RandomRuntime # noqa from typegraph.runtimes.wasm import WasmRuntime # noqa +from typegraph.runtimes.kv import KvRuntime # noqa diff --git a/typegraph/python/typegraph/runtimes/kv.py b/typegraph/python/typegraph/runtimes/kv.py new file mode 100644 index 0000000000..4c7090ce14 --- /dev/null +++ b/typegraph/python/typegraph/runtimes/kv.py @@ -0,0 +1,70 @@ +# Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +# SPDX-License-Identifier: MPL-2.0 + +from dataclasses import dataclass + +from typegraph import fx, t +from typegraph.gen.exports.runtimes import ( + BaseMaterializer, + Effect, + KvMaterializer, + KvRuntimeData, +) +from typegraph.gen.types import Err +from typegraph.runtimes.base import Materializer, Runtime +from typegraph.wit import runtimes, store + + +class KvRuntime(Runtime): + url: str + + def __init__(self, url: str): + data = KvRuntimeData(url) + runtime_id = runtimes.register_kv_runtime(store, data) + if isinstance(runtime_id, Err): + raise Exception(runtime_id.value) + + super().__init__(runtime_id.value) + self.url = url + + def get(self): + mat = self.__operation(KvMaterializer.GET, fx.read()) + return t.func(t.struct({"key": t.string()}), t.string(), mat) + + def set(self): + mat = self.__operation(KvMaterializer.SET, fx.update()) + return t.func( + t.struct({"key": t.string(), "value": t.string()}), t.string(), mat + ) + + def delete(self): + mat = self.__operation(KvMaterializer.DELETE, fx.delete()) + return t.func(t.struct({"key": t.string()}), t.integer(), mat) + + def keys(self): + mat = self.__operation(KvMaterializer.KEYS, fx.read()) + return t.func( + t.struct({"filter": t.optional(t.string())}), t.list(t.string()), mat + ) + + def values(self): + mat = self.__operation(KvMaterializer.VALUES, fx.read()) + return t.func( + t.struct({"filter": t.optional(t.string())}), + t.list(t.string()), + mat, + ) + + def __operation(self, operation: KvMaterializer, effect: Effect): + mat_id = runtimes.kv_operation( + store, BaseMaterializer(self.id, effect), operation + ) + if isinstance(mat_id, Err): + raise Exception(mat_id.value) + + return KvOperationMat(mat_id.value, effect=effect, operation=operation) + + +@dataclass +class KvOperationMat(Materializer): + operation: KvMaterializer diff --git a/website/docs/reference/runtimes/index.mdx b/website/docs/reference/runtimes/index.mdx index a65a0165bf..f51ed7e59a 100644 --- a/website/docs/reference/runtimes/index.mdx +++ b/website/docs/reference/runtimes/index.mdx @@ -16,6 +16,7 @@ This includes: - [Random](./runtimes/random) - [Temporal](./runtimes/temporal) - [S3](./runtimes/s3) +- [KV](./runtimes/kv) :::tip Missing your favorite runtime? Submit your request and vote for your preferred ones [here](https://github.com/metatypedev/metatype/discussions/305). diff --git a/website/docs/reference/runtimes/kv/index.mdx b/website/docs/reference/runtimes/kv/index.mdx new file mode 100644 index 0000000000..42d5db842c --- /dev/null +++ b/website/docs/reference/runtimes/kv/index.mdx @@ -0,0 +1,23 @@ +import TGExample from "@site/src/components/TGExample"; + +# Kv + +## Kv Runtime + +The KvRuntime enables interaction with a Redis database by setting, retrieving, deleting, and managing keys and values. + + + ++| **Operation** | **Description** | **Method** | ++| ------------- | -------------------------------------------------- | ------------- | ++| `get` | Retrieve the value associated with a specific key. | `kv.get()` | ++| `set` | Assign a value to a specific key. | `kv.set()` | ++| `delete` | Remove a key and its associated value from Redis. | `kv.delete()` | ++| `keys` | List all keys currently stored in Redis. | `kv.keys()` | ++| `values` | List all values currently stored in Redis. | `kv.values()` |