From 8ade5d199b7aaf1f5883303b63911aa2e053fefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl?= Date: Thu, 21 Sep 2023 15:41:55 +0300 Subject: [PATCH] feat(sdk): apply syntax (#410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michaël --- typegate/tests/typecheck/apply.py | 81 +++++ typegate/tests/typecheck/apply.ts | 100 ++++++ typegate/tests/typecheck/apply_syntax_test.ts | 290 ++++++++++++++++++ typegraph/core/src/errors.rs | 21 ++ typegraph/core/src/global_store.rs | 58 ++++ typegraph/core/src/lib.rs | 3 +- typegraph/core/src/typedef/with_injection.rs | 11 +- typegraph/core/src/typegraph.rs | 10 + typegraph/core/src/utils/apply.rs | 201 ++++++++++++ typegraph/core/src/utils/mod.rs | 129 ++++++++ typegraph/core/wit/typegraph.wit | 33 +- typegraph/deno/src/typegraph.ts | 38 +++ typegraph/deno/src/types.ts | 79 ++--- typegraph/deno/src/utils/func_utils.ts | 88 +++--- typegraph/deno/src/utils/injection_utils.ts | 107 +++++++ typegraph/deno/src/utils/type_utils.ts | 1 - typegraph/deno/src/wit.ts | 2 + .../typegraph_next/graph/typegraph.py | 5 + .../python_next/typegraph_next/injection.py | 87 ++++++ typegraph/python_next/typegraph_next/t.py | 59 ++-- typegraph/python_next/typegraph_next/utils.py | 64 ++-- typegraph/python_next/typegraph_next/wit.py | 2 + 22 files changed, 1312 insertions(+), 157 deletions(-) create mode 100644 typegate/tests/typecheck/apply.py create mode 100644 typegate/tests/typecheck/apply.ts create mode 100644 typegate/tests/typecheck/apply_syntax_test.ts create mode 100644 typegraph/core/src/utils/apply.rs create mode 100644 typegraph/core/src/utils/mod.rs create mode 100644 typegraph/deno/src/utils/injection_utils.ts create mode 100644 typegraph/python_next/typegraph_next/injection.py diff --git a/typegate/tests/typecheck/apply.py b/typegate/tests/typecheck/apply.py new file mode 100644 index 0000000000..d164022c22 --- /dev/null +++ b/typegate/tests/typecheck/apply.py @@ -0,0 +1,81 @@ +from typegraph_next import t, typegraph, Policy, Graph +from typegraph_next.runtimes.deno import DenoRuntime + +simple_tpe = t.struct( + { + "one": t.string(), + "two": t.struct( + { + "apply": t.integer(), + "user": t.integer(), + "set": t.integer().optional(), + "context": t.string().optional(), + } + ), + } +) + + +self_ref_tpe = t.struct( + { + "a": t.string(), + "b": t.struct({"nested": t.ref("SelfRef")}).optional(), + "direct": t.ref("SelfRef").optional(), + }, + name="SelfRef", +) + + +@typegraph() +def test_apply_python(g: Graph): + deno = DenoRuntime() + public = Policy.public() + identity_simple = deno.func( + simple_tpe, simple_tpe, code="({ one, two }) => { return { one, two } }" + ) + + identity_self_ref = deno.func( + self_ref_tpe, self_ref_tpe, code="({ a, b }) => { return { a, b } }" + ) + + g.expose( + invariantApply=identity_simple.apply( + { + "two": { + "apply": g.inherit(), + "user": g.inherit(), + "set": g.inherit(), + "context": g.inherit(), + } + # "one": g.inherit() # implicit + } + ).with_policy(public), + simpleInjection=identity_simple.apply({"one": "ONE!"}) + .apply( + { + "two": { + "user": g.inherit(), + "set": g.inherit().set(2), + "context": g.inherit().from_context("someValue"), + }, + } + ) + .with_policy(public), + selfReferingType=identity_self_ref.apply( + { + "a": g.inherit(), # A1 + "b": { + "nested": { + "a": "A2", + "b": { + "nested": { + "a": g.inherit(), # A3 + "b": g.inherit().from_context("nestedB"), + "direct": {"a": "direct A3"}, + } + }, + } + }, + } + ).with_policy(public), + ) diff --git a/typegate/tests/typecheck/apply.ts b/typegate/tests/typecheck/apply.ts new file mode 100644 index 0000000000..6a1190a009 --- /dev/null +++ b/typegate/tests/typecheck/apply.ts @@ -0,0 +1,100 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +import { Policy, t, typegraph } from "@typegraph/deno/src/mod.ts"; +import { DenoRuntime } from "@typegraph/deno/src/runtimes/deno.ts"; +import { NONE } from "@typegraph/deno/src/effects.ts"; + +const student = t.struct({ + id: t.integer(), + name: t.string(), + infos: t.struct({ + age: t.integer({ min: 10 }), + school: t.string().optional(), + }), + distinctions: t.struct({ + awards: t.array(t.struct({ + name: t.string(), + points: t.integer(), + })).optional(), + medals: t.integer().optional(), + }).optional(), +}, { name: "Student" }); + +const grades = t.struct({ + year: t.integer({ min: 2000 }), + subjects: t.array( + t.struct({ + name: t.string(), + score: t.integer(), + }), + ), +}); + +const tpe = t.struct({ student, grades: grades.optional() }); + +typegraph("test-apply-deno", (g) => { + const deno = new DenoRuntime(); + const pub = Policy.public(); + const identityStudent = deno.func( + tpe, + tpe, + { code: "({ student, grades }) => { return { student, grades } }" }, + ); + + g.expose({ + testInvariant: identityStudent.apply({ + student: { + id: g.inherit(), + name: g.inherit(), + infos: { + age: g.inherit(), + school: g.inherit(), + }, + }, + // grades: g.inherit(), // implicit + }).withPolicy(pub), + applyComposition: identityStudent + .apply({ + student: { + id: 1234, + name: g.inherit(), + infos: g.inherit(), + distinctions: { + awards: [ + { name: "Chess", points: 1000 }, + { name: "Math Olympiad", points: 100 }, + ], + medals: g.inherit(), + }, + }, + // grades: g.inherit(), // implicit + }) + .apply({ + // student: g.inherit(), // implicit + grades: { + year: g.inherit(), + subjects: g.inherit().set([ // sugar + { name: "Math", score: 60 }, + ]), + }, + }) + .withPolicy(pub), + + injectionInherit: identityStudent + .apply({ + student: { + id: 1234, + name: g.inherit(), + infos: g.inherit().fromContext("personalInfos"), + }, + grades: { + year: g.inherit().set(2000), + subjects: g.inherit().fromContext({ + [NONE]: "subjects", + }), + }, + }) + .withPolicy(pub), + }); +}); diff --git a/typegate/tests/typecheck/apply_syntax_test.ts b/typegate/tests/typecheck/apply_syntax_test.ts new file mode 100644 index 0000000000..559abfc684 --- /dev/null +++ b/typegate/tests/typecheck/apply_syntax_test.ts @@ -0,0 +1,290 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +import { gql, Meta } from "../utils/mod.ts"; + +Meta.test("deno(sdk): apply", async (t) => { + const e = await t.engine("typecheck/apply.ts"); + + await t.should( + "work as normal if all nodes have g.inherit() flag", + async () => { + await gql` + query { + testInvariant ( + student: { + id: 1 + name: "Jake" + infos: { age: 15 } + } + ) { + student { + id + name + infos { age school } + } + } + } + `.expectData({ + testInvariant: { + student: { + id: 1, + name: "Jake", + infos: { age: 15 }, + }, + }, + }) + .on(e); + }, + ); + + await t.should( + "compose apply and work with partial static injections", + async () => { + await gql` + query { + applyComposition ( + student: { + name: "Jake" + infos: { age: 15 } + distinctions: { medals: 7 } + } + grades: { year: 2023 } + ) { + student { + id + name + infos { age school } + distinctions { + awards { name points } + medals + } + } + grades { + year + subjects { name score } + } + } + } + `.expectData({ + applyComposition: { + student: { + id: 1234, // from apply 1 + name: "Jake", // from user + infos: { age: 15 }, // from user + distinctions: { + awards: [ // from apply 1 + { name: "Chess", points: 1000 }, + { name: "Math Olympiad", points: 100 }, + ], + medals: 7, // from user + }, + }, + grades: { + year: 2023, // from user + subjects: [ // from apply 2 + { name: "Math", score: 60 }, + ], + }, + }, + }) + .on(e); + }, + ); + + await t.should( + "work with injections", + async () => { + await gql` + query { + injectionInherit ( + student: { + name: "Kyle" + } + ) { + student { + id + name + infos { age school } + } + grades { + year + subjects { name score } + } + } + } + ` + .withContext({ + subjects: [ + { name: "Math", score: 24 }, + { name: "English", score: 68 }, + ], + personalInfos: { age: 17 }, + }) + .expectData({ + injectionInherit: { + student: { + id: 1234, // from apply + name: "Kyle", // from user + infos: { age: 17 }, // from context + }, + grades: { + year: 2000, // from explicit injection set(..) + subjects: [ // from context + { name: "Math", score: 24 }, + { name: "English", score: 68 }, + ], + }, + }, + }) + .on(e); + }, + ); +}); + +Meta.test("python(sdk): apply", async (t) => { + const e = await t.engine("typecheck/apply.py"); + await t.should( + "work as normal if all nodes have g.inherit() flag", + async () => { + await gql` + query { + invariantApply ( + one: "1" + two: { + apply: 2 + set: 3 + user: 4 + context: "5" + } + ) { + one + two { + apply + set + user + context + } + } + } + ` + .expectData({ + invariantApply: { + one: "1", + two: { + apply: 2, + set: 3, + user: 4, + context: "5", + }, + }, + }) + .on(e); + }, + ); + + await t.should( + "work with apply composition and injections", + async () => { + await gql` + query { + simpleInjection ( + two: { user: 4444 } + ) { + one + two { + set + user + context + } + } + } + ` + .withContext({ + someValue: "THREE!!", + }) + .expectData({ + simpleInjection: { + one: "ONE!", // apply + two: { + set: 2, + user: 4444, + context: "THREE!!", + }, + }, + }) + .on(e); + }, + ); + + await t.should( + "work with self-refering type", + async () => { + await gql` + query { + selfReferingType ( + a: "A1" + b: { + nested: { + b: { + nested: { + a: "A3" + } + } + } + } + ) { + a # A1 + b { + nested { + a # A2 (set) + b { + nested { + a # A3 + b { + nested { + a # A4 (context) + } + } + direct { a } + } + } + } + } + } + } + ` + .withContext({ + nestedB: { + nested: { + a: "A4 from context", + }, + }, + }) + .expectData({ + selfReferingType: { + a: "A1", + b: { + nested: { + a: "A2", + b: { + nested: { + a: "A3", + b: { + nested: { + a: "A4 from context", + }, + }, + direct: { + a: "direct A3", + }, + }, + }, + }, + }, + }, + }) + .on(e); + }, + ); +}); diff --git a/typegraph/core/src/errors.rs b/typegraph/core/src/errors.rs index e28e844830..3945b346df 100644 --- a/typegraph/core/src/errors.rs +++ b/typegraph/core/src/errors.rs @@ -59,6 +59,27 @@ pub fn object_not_found(kind: &str, id: u32) -> TgError { format!("{kind} #{id} not found") } +pub fn invalid_path(pos: usize, path: &[String], curr_keys: &[String]) -> TgError { + let mut path_with_cursor = vec![]; + for (i, chunk) in path.iter().enumerate() { + if i == pos { + path_with_cursor.push(format!("[{}]", chunk)); + } else { + path_with_cursor.push(chunk.clone()); + } + } + format!( + "invalid path {:?}, none of {} match the chunk {:?}", + path_with_cursor.join("."), + curr_keys.join(", "), + path.get(pos).unwrap_or(&"".to_string()), + ) +} + +pub fn expect_object_at_path(path: &[String]) -> TgError { + format!("object was expected at path {:?}", path.join(".")) +} + pub fn unknown_predefined_function(name: &str, runtime: &str) -> TgError { format!("unknown predefined function {name} for runtime {runtime}") } diff --git a/typegraph/core/src/global_store.rs b/typegraph/core/src/global_store.rs index 2fca610fdf..34e8ff8dcf 100644 --- a/typegraph/core/src/global_store.rs +++ b/typegraph/core/src/global_store.rs @@ -71,6 +71,26 @@ impl Store { } } + /// unwrap type id inside array, optional, or WithInjection + pub fn resolve_wrapper(&self, type_id: TypeId) -> Result { + let mut id = self.resolve_proxy(type_id)?; + loop { + let tpe = self.get_type(id)?; + let new_id = match tpe { + Type::Array(t) => t.data.of.into(), + Type::Optional(t) => t.data.of.into(), + Type::WithInjection(t) => t.data.tpe.into(), + Type::Proxy(t) => self.resolve_proxy(t.id)?, + _ => id, + }; + if id == new_id { + break; + } + id = new_id; + } + Ok(id) + } + /// Collect all the data from all wrapper types, and get the concrete type pub fn get_attributes(&self, type_id: TypeId) -> Result { let mut type_id = type_id; @@ -120,6 +140,44 @@ impl Store { } } + pub fn get_type_by_path( + &self, + struct_id: TypeId, + path: &[String], + ) -> Result<(&Type, TypeId), TgError> { + let mut ret = (self.get_type(struct_id)?, struct_id); + + let mut curr_path = vec![]; + for (pos, chunk) in path.iter().enumerate() { + let unwrapped_id = self.resolve_wrapper(ret.1)?; + match self.get_type(unwrapped_id)? { + Type::Struct(t) => { + let result = t.data.props.iter().find(|(k, _)| k.eq(chunk)); + curr_path.push(chunk.clone()); + ret = match result { + Some((_, id)) => { + (self.get_type(id.to_owned().into())?, id.to_owned().into()) + } + None => { + return Err(errors::invalid_path( + pos, + path, + &t.data + .props + .iter() + .map(|v| format!("{:?}", v.0.clone())) + .collect::>(), + )); + } + }; + } + _ => return Err(errors::expect_object_at_path(&curr_path)), + } + } + + Ok(ret) + } + pub fn get_type_mut(&mut self, type_id: TypeId) -> Result<&mut Type, TgError> { self.types .get_mut(type_id.0 as usize) diff --git a/typegraph/core/src/lib.rs b/typegraph/core/src/lib.rs index d907985c1d..85a3c613c2 100644 --- a/typegraph/core/src/lib.rs +++ b/typegraph/core/src/lib.rs @@ -9,6 +9,7 @@ mod t; mod typedef; mod typegraph; mod types; +mod utils; mod validation; #[cfg(test)] @@ -39,7 +40,7 @@ pub mod wit { export_typegraph!(Lib); - pub use exports::metatype::typegraph::{core, runtimes}; + pub use exports::metatype::typegraph::{core, runtimes, utils}; } #[cfg(feature = "wasm")] diff --git a/typegraph/core/src/typedef/with_injection.rs b/typegraph/core/src/typedef/with_injection.rs index 1bc6131e44..b53b27f378 100644 --- a/typegraph/core/src/typedef/with_injection.rs +++ b/typegraph/core/src/typedef/with_injection.rs @@ -22,23 +22,16 @@ impl TypeConversion for WithInjection { let value: Injection = serde_json::from_str(&self.data.injection).map_err(|e| e.to_string())?; if let Injection::Parent(data) = value { - let get_correct_id = |v: u32| -> Result { - let id = s.resolve_proxy(v.into())?; - if let Some(index) = ctx.find_type_index_by_store_id(id) { - return Ok(index); - } - Err(format!("unable to find type for store id {}", id.0)) - }; let new_data = match data { InjectionData::SingleValue(SingleValue { value }) => { InjectionData::SingleValue(SingleValue { - value: get_correct_id(value)?, + value: ctx.get_correct_id(value.into())?, }) } InjectionData::ValueByEffect(per_effect) => { let mut new_per_effect: HashMap = HashMap::new(); for (k, v) in per_effect.iter() { - new_per_effect.insert(*k, get_correct_id(*v)?); + new_per_effect.insert(*k, ctx.get_correct_id(v.into())?); } InjectionData::ValueByEffect(new_per_effect) } diff --git a/typegraph/core/src/typegraph.rs b/typegraph/core/src/typegraph.rs index bcc7b93880..09aeabc372 100644 --- a/typegraph/core/src/typegraph.rs +++ b/typegraph/core/src/typegraph.rs @@ -401,4 +401,14 @@ impl TypegraphContext { pub fn find_type_index_by_store_id(&self, id: TypeId) -> Option { self.mapping.types.get(&id.into()).copied() } + + pub fn get_correct_id(&self, id: TypeId) -> Result { + with_store(|s| { + let id = s.resolve_proxy(id)?; + self.find_type_index_by_store_id(id).ok_or(format!( + "unable to find type for store id {}", + u32::from(id) + )) + }) + } } diff --git a/typegraph/core/src/utils/apply.rs b/typegraph/core/src/utils/apply.rs new file mode 100644 index 0000000000..e30faa0729 --- /dev/null +++ b/typegraph/core/src/utils/apply.rs @@ -0,0 +1,201 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +use std::collections::HashMap; + +use crate::{ + errors::Result, + wit::utils::{Apply, ApplyPath, ApplyValue}, +}; + +#[derive(Debug, Clone)] +pub struct PathTree { + pub entries: Vec, + pub name: String, + pub path_infos: ApplyPath, +} + +impl PathTree { + fn new(name: String, path_infos: ApplyPath) -> PathTree { + Self { + entries: vec![], + name, + path_infos, + } + } + + pub fn is_leaf(&self) -> bool { + self.entries.is_empty() + } + + fn build_helper( + parent: &mut PathTree, + description: &ApplyPath, + depth: usize, + ) -> Result<(), String> { + if depth < description.path.len() { + let chunk = &description.path[depth]; + let child = match parent.find(chunk) { + Some(child) => child, + None => { + parent.add(PathTree::new(chunk.to_string(), description.clone())); + parent + .find(chunk) + .ok_or("node incorrectly added into tree".to_string())? + } + }; + PathTree::build_helper(child, description, depth + 1)? + } + Ok(()) + } + + pub fn build_from(apply: &Apply) -> Result { + let mut root = PathTree::new( + "root".to_string(), + ApplyPath { + path: vec![], + value: ApplyValue { + inherit: false, + payload: None, + }, + }, + ); + for descr in apply.paths.iter() { + PathTree::build_helper(&mut root, descr, 0)?; + } + Ok(root) + } + + pub fn find(&mut self, other_name: &str) -> Option<&mut PathTree> { + self.entries + .iter_mut() + .find(|entry| entry.name.eq(other_name)) + } + + pub fn add(&mut self, entry: PathTree) { + self.entries.push(entry); + } + + fn print_helper(lines: &mut String, node: &PathTree, depth: u32) { + if depth >= 1 { + let name = node.name.clone(); + let payload = node + .path_infos + .value + .payload + .clone() + .unwrap_or("--".to_string()); + let spaces = 4; + let symbol = if node.entries.len() == 1 || depth != 0 { + "└─" + } else { + "├─" + }; + lines.push_str(&format!( + "{:indent$}{symbol} [{name} ({payload})]", + "", + indent = ((depth as usize) - 1) * spaces + )); + } else { + lines.push_str(&node.name); + } + + for entry in node.entries.iter() { + PathTree::print_helper(lines, entry, depth + 1); + } + } +} + +impl ToString for PathTree { + fn to_string(&self) -> String { + let mut lines = String::new(); + PathTree::print_helper(&mut lines, self, 0); + lines + } +} + +// Type utility for wrapping a node with index information +#[derive(Debug, Clone)] +pub struct ItemNode<'a> { + pub parent_index: Option, + pub index: u32, + pub node: &'a PathTree, +} + +// Scheme similar to `types: TypeNode[]` in typegate +// Item node wrappers are ordered in such a way that +// 1. The last items are the leaves +// 2. The first item is guaranteed to be the root node +pub fn flatten_to_sorted_items_array(path_tree: &PathTree) -> Result, String> { + let mut index = 0; + let mut levels = vec![vec![ItemNode { + parent_index: None, + index, + node: path_tree, + }]]; + + loop { + let previous_level = levels.last(); + match previous_level { + Some(parents) => { + let mut current_level = vec![]; + for parent in parents { + for entry in parent.node.entries.iter() { + index += 1; + current_level.push(ItemNode { + parent_index: Some(parent.index), + index, + node: entry, + }); + } + } + if current_level.is_empty() { + // all nodes traversed + break; + } + levels.push(current_level); + } + None => panic!("first level must be populated"), + } + } + + // flatten the tree to a 1D array + let final_size = (index + 1) as usize; + let mut tmp_result = vec![None; final_size]; + + for level in levels { + for item in level { + let pos = item.index as usize; + match tmp_result.get(pos).unwrap() { + None => { + tmp_result[pos] = Some(item); + } + Some(value) => { + return Err(format!( + "index {} is already filled with {:?}", + item.index, value + )) + } + } + } + } + + let mut result = vec![]; + for (i, item) in tmp_result.iter().enumerate() { + result.push(item.clone().ok_or(format!("index {} is vacant", i))?) + } + + Ok(result) +} + +pub fn build_parent_to_child_indices(item_list: &Vec) -> HashMap> { + let mut map: HashMap> = HashMap::new(); + for item in item_list { + if let Some(parent_index) = item.parent_index { + map.entry(parent_index) + .or_insert_with(Vec::new) + .push(item.index); + } + } + map +} diff --git a/typegraph/core/src/utils/mod.rs b/typegraph/core/src/utils/mod.rs new file mode 100644 index 0000000000..a1de3f05c5 --- /dev/null +++ b/typegraph/core/src/utils/mod.rs @@ -0,0 +1,129 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +use std::collections::HashMap; + +use crate::{ + errors::Result, + global_store::with_store, + wit::core::{Core, TypeBase, TypeId, TypeStruct, TypeWithInjection}, + Lib, +}; + +mod apply; + +fn find_missing_props( + supertype_id: TypeId, + new_props: &Vec<(String, u32)>, +) -> Result> { + let old_props = with_store(|s| -> Result> { + let tpe = s.get_type(supertype_id.into())?; + match tpe { + crate::types::Type::Struct(t) => Ok(t.data.props.clone()), + _ => Err(format!( + "supertype (store id {}) is not a struct", + supertype_id + )), + } + })?; + + let mut missing_props = vec![]; + for (k_old, v_old) in old_props { + let mut is_missing = true; + for (k_new, _) in new_props { + if k_old.eq(k_new) { + is_missing = false; + break; + } + } + if is_missing { + missing_props.push((k_old, v_old)); + } + } + + Ok(missing_props) +} + +impl crate::wit::utils::Utils for crate::Lib { + fn gen_applyb(supertype_id: TypeId, apply: crate::wit::utils::Apply) -> Result { + if apply.paths.is_empty() { + return Err("apply object is empty".to_string()); + } + let apply_tree = apply::PathTree::build_from(&apply)?; + let mut item_list = apply::flatten_to_sorted_items_array(&apply_tree)?; + let p2c_indices = apply::build_parent_to_child_indices(&item_list); + // item_list index => (node name, store id) + let mut idx_to_store_id_cache: HashMap = HashMap::new(); + + while !item_list.is_empty() { + let item = match item_list.pop() { + Some(value) => value, + None => break, + }; + + if item.node.is_leaf() { + let path_infos = item.node.path_infos.clone(); + let apply_value = path_infos.value; + let id = with_store(|s| -> Result { + let id = s.get_type_by_path(supertype_id.into(), &path_infos.path)?.1; + Ok(id.into()) + })?; + + if apply_value.inherit && apply_value.payload.is_none() { + // if inherit and no injection, keep original id + idx_to_store_id_cache.insert(item.index, (item.node.name.clone(), id)); + } else { + // has injection + let payload = apply_value.payload.ok_or(format!( + "cannot set undefined value at {:?}", + path_infos.path.join(".") + ))?; + let new_id = Lib::with_injection(TypeWithInjection { + tpe: id, + injection: payload, + })?; + + idx_to_store_id_cache.insert(item.index, (item.node.name.clone(), new_id)); + } + } else { + // parent node => must be a struct + let child_indices = p2c_indices.get(&item.index).unwrap(); + if child_indices.is_empty() { + return Err(format!("parent item at index {} has no child", item.index)); + } + + let mut props = vec![]; + for idx in child_indices { + // cache must hit + let prop = idx_to_store_id_cache.get(idx).ok_or(format!( + "store id for item at index {idx} was not yet generated" + ))?; + props.push(prop.clone()); + } + + if item.parent_index.is_none() { + // if root, props g.inherit() should be implicit + let missing_props = find_missing_props(supertype_id, &props)?; + for pair in missing_props { + props.push(pair); + } + } + + let id = Lib::structb( + TypeStruct { + props, + ..Default::default() + }, + TypeBase::default(), + )?; + idx_to_store_id_cache.insert(item.index, (item.node.name.clone(), id)); + } + } + + let (_root_name, root_id) = idx_to_store_id_cache + .get(&0) + .ok_or("root type does not have any field".to_string())?; + + Ok(*root_id) + } +} diff --git a/typegraph/core/wit/typegraph.wit b/typegraph/core/wit/typegraph.wit index abdfd91381..233d101144 100644 --- a/typegraph/core/wit/typegraph.wit +++ b/typegraph/core/wit/typegraph.wit @@ -144,7 +144,6 @@ interface core { } funcb: func(data: type-func) -> result - type policy-id = u32 record policy { @@ -351,7 +350,39 @@ interface runtimes { prisma-link: func(data: prisma-link-data) -> result } + +interface utils { + type error = string + type type-id = u32 + + + // Example: + // apply({a: 1, { b: {c: g.inherit(), d: [1, 2, 3]}}) + // produces a list of apply-path + // [ + // { path: [a], value: { inherit: false, payload: 1 } } + // { path: [a, b, c], value: { inherit: true } } + // { path: [a, b, d], value: { inherit: false, payload: [1, 2, 3] } } + // ] + record apply-value { + inherit: bool, + payload: option + } + + record apply-path { + path: list, + value: apply-value, + } + + record apply { + paths: list + } + + gen-applyb: func(supertype-id: type-id, data: apply) -> result +} + world typegraph { export core export runtimes + export utils } diff --git a/typegraph/deno/src/typegraph.ts b/typegraph/deno/src/typegraph.ts index ee48fa9b75..ad1e66f2fd 100644 --- a/typegraph/deno/src/typegraph.ts +++ b/typegraph/deno/src/typegraph.ts @@ -4,6 +4,12 @@ import * as t from "./types.ts"; import { core } from "../gen/typegraph_core.js"; import { caller, dirname, fromFileUrl } from "./deps.ts"; +import { InjectionValue } from "./utils/type_utils.ts"; +import { + serializeFromParentInjection, + serializeGenericInjection, + serializeStaticInjection, +} from "./utils/injection_utils.ts"; import { Auth, Cors, Rate } from "./wit.ts"; type Exports = Record; @@ -22,6 +28,35 @@ interface TypegraphArgs { interface TypegraphBuilderArgs { expose: (exports: Exports) => void; + inherit: () => InheritDef; +} + +export class InheritDef { + public payload: string | undefined; + set(value: InjectionValue) { + this.payload = serializeStaticInjection(value); + return this; + } + + inject(value: InjectionValue) { + this.payload = serializeGenericInjection("dynamic", value); + return this; + } + + fromContext(value: InjectionValue) { + this.payload = serializeGenericInjection("context", value); + return this; + } + + fromSecret(value: InjectionValue) { + this.payload = serializeGenericInjection("secret", value); + return this; + } + + fromParent(value: InjectionValue) { + this.payload = serializeFromParentInjection(value); + return this; + } } type TypegraphBuilder = (g: TypegraphBuilderArgs) => void; @@ -89,6 +124,9 @@ export function typegraph( [], ); }, + inherit: () => { + return new InheritDef(); + }, }; builder(g); diff --git a/typegraph/deno/src/types.ts b/typegraph/deno/src/types.ts index 8ca1ee3f07..99202d7fe4 100644 --- a/typegraph/deno/src/types.ts +++ b/typegraph/deno/src/types.ts @@ -1,7 +1,7 @@ // Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. // SPDX-License-Identifier: MPL-2.0 -import { core } from "./wit.ts"; +import { core, wit_utils } from "./wit.ts"; import { PolicyPerEffect, TypeArray, @@ -13,15 +13,18 @@ import { TypeString, TypeUnion, } from "../gen/exports/metatype-typegraph-core.d.ts"; +import { Apply } from "../gen/exports/metatype-typegraph-utils.d.ts"; import { Materializer } from "./runtimes/mod.ts"; import { mapValues } from "./deps.ts"; import Policy from "./policy.ts"; +import { buildApplyData, serializeRecordValues } from "./utils/func_utils.ts"; import { - serializeInjection, - serializeRecordValues, -} from "./utils/func_utils.ts"; -import { CREATE, DELETE, NONE, UPDATE } from "./effects.ts"; + serializeFromParentInjection, + serializeGenericInjection, + serializeStaticInjection, +} from "./utils/injection_utils.ts"; import { InjectionValue } from "./utils/type_utils.ts"; +import { InheritDef } from "./typegraph.ts"; export type PolicySpec = Policy | { none: Policy; @@ -109,70 +112,31 @@ export class Typedef { set(value: InjectionValue) { return this.withInjection( - serializeInjection("static", value, (x: unknown) => JSON.stringify(x)), + serializeStaticInjection(value), ); } inject(value: InjectionValue) { return this.withInjection( - serializeInjection("dynamic", value), + serializeGenericInjection("dynamic", value), ); } fromContext(value: InjectionValue) { return this.withInjection( - serializeInjection("context", value), + serializeGenericInjection("context", value), ); } fromSecret(value: InjectionValue) { return this.withInjection( - serializeInjection("secret", value), + serializeGenericInjection("secret", value), ); } fromParent(value: InjectionValue) { - let correctValue: any = null; - if (typeof value === "string") { - correctValue = proxy(value)._id; - } else { - const isObject = typeof value === "object" && !Array.isArray(value) && - value !== null; - if (!isObject) { - throw new Error("type not supported"); - } - - // Note: - // Symbol changes the behavior of keys, values, entries => props are skipped - const symbols = [UPDATE, DELETE, CREATE, NONE]; - const noOtherType = Object.keys(value).length == 0; - const isPerEffect = noOtherType && - symbols - .some((symbol) => (value as any)?.[symbol] !== undefined); - - if (!isPerEffect) { - throw new Error("object keys should be of type EffectType"); - } - - correctValue = {}; - for (const symbol of symbols) { - const v = (value as any)?.[symbol]; - if (v === undefined) continue; - if (typeof v !== "string") { - throw new Error( - `value for field ${symbol.toString()} must be a string`, - ); - } - correctValue[symbol] = proxy(v)._id; - } - } - return this.withInjection( - serializeInjection( - "parent", - correctValue, - (x: unknown) => x as number, - ), + serializeFromParentInjection(value), ); } } @@ -517,6 +481,23 @@ export class Func< this.out = out; this.mat = mat; } + + apply(value: Record) { + const data: Apply = { + paths: buildApplyData(value), + }; + + const applyId = wit_utils.genApplyb( + this.inp._id, + data, + ); + + return func( + new Typedef(applyId, {}) as Struct

, + this.out, + this.mat, + ); + } } export function func< diff --git a/typegraph/deno/src/utils/func_utils.ts b/typegraph/deno/src/utils/func_utils.ts index 76d0232739..a39bc14f25 100644 --- a/typegraph/deno/src/utils/func_utils.ts +++ b/typegraph/deno/src/utils/func_utils.ts @@ -1,8 +1,9 @@ // Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. // SPDX-License-Identifier: MPL-2.0 -import { CREATE, DELETE, NONE, UPDATE } from "../effects.ts"; -import { InjectionSource, InjectionValue } from "./type_utils.ts"; +import { InheritDef } from "../typegraph.ts"; +import { ApplyPath } from "../../gen/exports/metatype-typegraph-utils.d.ts"; +import { serializeStaticInjection } from "./injection_utils.ts"; export function stringifySymbol(symbol: symbol) { const name = symbol.toString().match(/\((.+)\)/)?.[1]; @@ -12,50 +13,53 @@ export function stringifySymbol(symbol: symbol) { return name; } -export function serializeInjection( - source: InjectionSource, - value: InjectionValue, - valueMapper = (value: InjectionValue) => value, -) { - if ( - typeof value === "object" && - !Array.isArray(value) && - value !== null - ) { - // Note: - // Symbol changes the behavior of keys, values, entries => props are skipped - const symbols = [UPDATE, DELETE, CREATE, NONE]; - const noOtherType = Object.keys(value).length == 0; - const isPerEffect = noOtherType && - symbols - .some((symbol) => (value as any)?.[symbol] !== undefined); +export function serializeRecordValues( + obj: Record, +): Array<[string, string]> { + return Object.entries(obj).map(([k, v]) => [k, JSON.stringify(v)]); +} - if (isPerEffect) { - const dataEntries = symbols.map( - (symbol) => { - const valueGiven = (value as any)?.[symbol]; - return [ - stringifySymbol(symbol), - valueGiven && valueMapper(valueGiven), - ]; - }, - ); +export function buildApplyData( + node: InheritDef | unknown, + paths: ApplyPath[] = [], + currPath: string[] = [], +): ApplyPath[] { + if (node === null || node === undefined) { + throw new Error( + `unsupported value "${node}" at ${currPath.join(".")}`, + ); + } + if (node instanceof InheritDef) { + paths.push({ + path: currPath, + value: { inherit: true, payload: node.payload }, + }); + return paths; + } - return JSON.stringify({ - source, - data: Object.fromEntries(dataEntries), + if (typeof node === "object") { + if (Array.isArray(node)) { + paths.push({ + path: currPath, + value: { inherit: false, payload: serializeStaticInjection(node) }, }); + return paths; + } + for (const [k, v] of Object.entries(node)) { + buildApplyData(v, paths, [...currPath, k]); } + return paths; } - return JSON.stringify({ - source, - data: { value: valueMapper(value) }, - }); -} - -export function serializeRecordValues( - obj: Record, -): Array<[string, string]> { - return Object.entries(obj).map(([k, v]) => [k, JSON.stringify(v)]); + const allowed = ["number", "string", "boolean"]; + if (allowed.includes(typeof node)) { + paths.push({ + path: currPath, + value: { inherit: false, payload: serializeStaticInjection(node) }, + }); + return paths; + } + throw new Error( + `unsupported type "${typeof node}" at ${currPath.join(".")}`, + ); } diff --git a/typegraph/deno/src/utils/injection_utils.ts b/typegraph/deno/src/utils/injection_utils.ts new file mode 100644 index 0000000000..c3fd5bddaa --- /dev/null +++ b/typegraph/deno/src/utils/injection_utils.ts @@ -0,0 +1,107 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +import { CREATE, DELETE, NONE, UPDATE } from "../effects.ts"; +import { InjectionSource, InjectionValue } from "./type_utils.ts"; +import { stringifySymbol } from "./func_utils.ts"; +import * as t from "../types.ts"; + +export function serializeInjection( + source: InjectionSource, + value: InjectionValue, + valueMapper = (value: InjectionValue) => value, +) { + if ( + typeof value === "object" && + !Array.isArray(value) && + value !== null + ) { + // Note: + // Symbol changes the behavior of keys, values, entries => props are skipped + const symbols = [UPDATE, DELETE, CREATE, NONE]; + const noOtherType = Object.keys(value).length == 0; + const isPerEffect = noOtherType && + symbols + .some((symbol) => (value as any)?.[symbol] !== undefined); + + if (isPerEffect) { + const dataEntries = symbols.map( + (symbol) => { + const valueGiven = (value as any)?.[symbol]; + return [ + stringifySymbol(symbol), + valueGiven && valueMapper(valueGiven), + ]; + }, + ); + + return JSON.stringify({ + source, + data: Object.fromEntries(dataEntries), + }); + } + } + + return JSON.stringify({ + source, + data: { value: valueMapper(value) }, + }); +} + +export function serializeGenericInjection( + source: InjectionSource, + value: InjectionValue, +) { + const allowed: InjectionSource[] = ["dynamic", "context", "secret"]; + if (allowed.includes(source)) { + return serializeInjection(source, value); + } + throw new Error(`source must be one of ${allowed.join(", ")}`); +} + +export function serializeStaticInjection(value: InjectionValue) { + return serializeInjection("static", value, (x: unknown) => JSON.stringify(x)); +} + +export function serializeFromParentInjection(value: InjectionValue) { + let correctValue: any = null; + if (typeof value === "string") { + correctValue = t.proxy(value)._id; + } else { + const isObject = typeof value === "object" && !Array.isArray(value) && + value !== null; + if (!isObject) { + throw new Error("type not supported"); + } + + // Note: + // Symbol changes the behavior of keys, values, entries => props are skipped + const symbols = [UPDATE, DELETE, CREATE, NONE]; + const noOtherType = Object.keys(value).length == 0; + const isPerEffect = noOtherType && + symbols + .some((symbol) => (value as any)?.[symbol] !== undefined); + + if (!isPerEffect) { + throw new Error("object keys should be of type EffectType"); + } + + correctValue = {}; + for (const symbol of symbols) { + const v = (value as any)?.[symbol]; + if (v === undefined) continue; + if (typeof v !== "string") { + throw new Error( + `value for field ${symbol.toString()} must be a string`, + ); + } + correctValue[symbol] = t.proxy(v)._id; + } + } + + return serializeInjection( + "parent", + correctValue, + (x: unknown) => x as number, + ); +} diff --git a/typegraph/deno/src/utils/type_utils.ts b/typegraph/deno/src/utils/type_utils.ts index fc35f3ea80..29dbeab086 100644 --- a/typegraph/deno/src/utils/type_utils.ts +++ b/typegraph/deno/src/utils/type_utils.ts @@ -24,5 +24,4 @@ export type InjectionSource = | "context" | "parent" | "secret"; - export type InjectionValue = T | PerEffect; diff --git a/typegraph/deno/src/wit.ts b/typegraph/deno/src/wit.ts index 557e70f8be..1a1db63b20 100644 --- a/typegraph/deno/src/wit.ts +++ b/typegraph/deno/src/wit.ts @@ -3,10 +3,12 @@ import { ExportsMetatypeTypegraphCore } from "../gen/exports/metatype-typegraph-core.d.ts"; import { ExportsMetatypeTypegraphRuntimes } from "../gen/exports/metatype-typegraph-runtimes.d.ts"; +import { ExportsMetatypeTypegraphUtils } from "../gen/exports/metatype-typegraph-utils.d.ts"; import * as js from "../gen/typegraph_core.js"; export const core = js.core as typeof ExportsMetatypeTypegraphCore; export const runtimes = js.runtimes as typeof ExportsMetatypeTypegraphRuntimes; +export const wit_utils = js.utils as typeof ExportsMetatypeTypegraphUtils; export type { Auth, diff --git a/typegraph/python_next/typegraph_next/graph/typegraph.py b/typegraph/python_next/typegraph_next/graph/typegraph.py index 700de7c694..5e5b1350d4 100644 --- a/typegraph/python_next/typegraph_next/graph/typegraph.py +++ b/typegraph/python_next/typegraph_next/graph/typegraph.py @@ -99,6 +99,11 @@ def expose( ): self.typegraph.expose(default_policy, **kwargs) + def inherit(self): + from typegraph_next.injection import InheritDef + + return InheritDef() + def typegraph( name: Optional[str] = None, diff --git a/typegraph/python_next/typegraph_next/injection.py b/typegraph/python_next/typegraph_next/injection.py new file mode 100644 index 0000000000..a78011527f --- /dev/null +++ b/typegraph/python_next/typegraph_next/injection.py @@ -0,0 +1,87 @@ +# Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +# SPDX-License-Identifier: MPL-2.0 + +import json +from typing import Callable, Dict, Union +from typegraph_next.effects import EffectType +from typegraph_next import t + + +def serialize_injection( + source: str, + value: Union[any, Dict[EffectType, any]], + value_mapper: Callable[[any], any] = lambda x: x, +): + if ( + isinstance(value, dict) + and len(value) > 0 + and all(isinstance(k, EffectType) for k in value.keys()) + ): + value_per_effect = { + str(k.name.lower()): value_mapper(v) for k, v in value.items() + } + return json.dumps({"source": source, "data": value_per_effect}) + + return json.dumps({"source": source, "data": {"value": value_mapper(value)}}) + + +def serialize_static_injection(value: Union[any, Dict[EffectType, any]]): + return serialize_injection( + "static", value=value, value_mapper=lambda x: json.dumps(x) + ) + + +def serialize_generic_injection(source: str, value: Union[any, Dict[EffectType, any]]): + allowed = ["dynamic", "context", "secret"] + if source in allowed: + return serialize_injection(source, value=value) + raise Exception(f"source must be one of ${', '.join(allowed)}") + + +def serialize_parent_injection(value: Union[str, Dict[EffectType, str]]): + correct_value = None + if isinstance(value, str): + correct_value = t.proxy(value).id + else: + if not isinstance(value, dict): + raise Exception("type not supported") + + is_per_effect = len(value) > 0 and all( + isinstance(k, EffectType) for k in value.keys() + ) + if not is_per_effect: + raise Exception("object keys should be of type EffectType") + + correct_value = {} + for k, v in value.items(): + if not isinstance(v, str): + raise Exception(f"value for field {k.name} must be a string") + correct_value[k] = t.proxy(v).id + + assert correct_value is not None + + return serialize_injection("parent", value=correct_value, value_mapper=lambda x: x) + + +class InheritDef: + payload: str = None + + def set(self, value: Union[any, Dict[EffectType, any]]): + self.payload = serialize_static_injection(value) + return self + + def inject(self, value: Union[any, Dict[EffectType, any]]): + self.payload = serialize_generic_injection("dynamic", value) + return self + + def from_context(self, value: Union[str, Dict[EffectType, str]]): + self.payload = serialize_generic_injection("context", value) + return self + + def from_secret(self, value: Union[str, Dict[EffectType, str]]): + self.payload = serialize_generic_injection("secret", value) + return self + + def from_parent(self, value: Union[str, Dict[EffectType, str]]): + self.payload = serialize_parent_injection(value) + return self diff --git a/typegraph/python_next/typegraph_next/t.py b/typegraph/python_next/typegraph_next/t.py index a361f153ab..16e4c955d4 100644 --- a/typegraph/python_next/typegraph_next/t.py +++ b/typegraph/python_next/typegraph_next/t.py @@ -3,7 +3,7 @@ import json from typing import Dict, List, Optional, Tuple, Union -from typegraph_next.utils import serialize_record_values, serialize_injection +from typegraph_next.utils import serialize_record_values, build_apply_data from typing_extensions import Self @@ -24,11 +24,19 @@ TypeProxy, TypeStruct, ) +from typegraph_next.gen.exports.utils import Apply + from typegraph_next.gen.types import Err from typegraph_next.graph.typegraph import core, store +from typegraph_next.wit import wit_utils from typegraph_next.policy import Policy, PolicyPerEffect, PolicySpec, get_policy_chain from typegraph_next.runtimes.deno import Materializer from typegraph_next.effects import EffectType +from typegraph_next.injection import ( + serialize_generic_injection, + serialize_parent_injection, + serialize_static_injection, +) class typedef: @@ -78,48 +86,19 @@ def optional( return optional(self, default_item=default_value, config=config) def set(self, value: Union[any, Dict[EffectType, any]]): - return self._with_injection( - serialize_injection( - "static", value=value, value_mapper=lambda x: json.dumps(x) - ), - ) + return self._with_injection(serialize_static_injection(value)) def inject(self, value: Union[any, Dict[EffectType, any]]): - return self._with_injection(serialize_injection("dynamic", value=value)) + return self._with_injection(serialize_generic_injection("dynamic", value)) def from_context(self, value: Union[str, Dict[EffectType, str]]): - return self._with_injection(serialize_injection("context", value=value)) + return self._with_injection(serialize_generic_injection("context", value)) def from_secret(self, value: Union[str, Dict[EffectType, str]]): - return self._with_injection(serialize_injection("secret", value=value)) + return self._with_injection(serialize_generic_injection("secret", value)) def from_parent(self, value: Union[str, Dict[EffectType, str]]): - correct_value = None - if isinstance(value, str): - correct_value = proxy(value).id - else: - if not isinstance(value, dict): - raise Exception("type not supported") - - is_per_effect = len(value) > 0 and all( - isinstance(k, EffectType) for k in value.keys() - ) - if not is_per_effect: - raise Exception("object keys should be of type EffectType") - - correct_value = {} - for k, v in value.items(): - if not isinstance(v, str): - raise Exception(f"value for field {k.name} must be a string") - correct_value[k] = proxy(v).id - - assert correct_value is not None - - return self._with_injection( - serialize_injection( - "parent", value=correct_value, value_mapper=lambda x: x - ), - ) + return self._with_injection(serialize_parent_injection(value)) class _TypeWithPolicy(typedef): @@ -528,6 +507,16 @@ def __init__(self, inp: struct, out: typedef, mat: Materializer): super().__init__(id) self.inp = inp self.out = out + self.mat = mat + + def apply(self, value: Dict[str, any]) -> "func": + data = Apply(paths=build_apply_data(value, [], [])) + apply_id = wit_utils.gen_applyb(store, self.inp.id, data=data) + + if isinstance(apply_id, Err): + raise Exception(apply_id.value) + + return func(typedef(id=apply_id.value), self.out, self.mat) def gen(out: typedef, mat: Materializer): diff --git a/typegraph/python_next/typegraph_next/utils.py b/typegraph/python_next/typegraph_next/utils.py index 8c4f4476ac..51419f5f51 100644 --- a/typegraph/python_next/typegraph_next/utils.py +++ b/typegraph/python_next/typegraph_next/utils.py @@ -2,27 +2,53 @@ # SPDX-License-Identifier: MPL-2.0 import json -from typing import Dict, Union, Callable -from typegraph_next.effects import EffectType +from typing import Dict, List, Union +from typegraph_next.injection import InheritDef +from typegraph_next.gen.exports.utils import ApplyPath, ApplyValue +from typegraph_next.injection import serialize_static_injection -def serialize_injection( - source: str, - value: Union[any, Dict[EffectType, any]], - value_mapper: Callable[[any], any] = lambda x: x, -): - if ( - isinstance(value, dict) - and len(value) > 0 - and all(isinstance(k, EffectType) for k in value.keys()) - ): - value_per_effect = { - str(k.name.lower()): value_mapper(v) for k, v in value.items() - } - return json.dumps({"source": source, "data": value_per_effect}) +def serialize_record_values(obj: Union[Dict[str, any], None]): + return [(k, json.dumps(v)) for k, v in obj.items()] if obj is not None else None - return json.dumps({"source": source, "data": {"value": value_mapper(value)}}) +def build_apply_data(node: any, paths: List[ApplyPath], curr_path: List[str]): + if node is None: + raise Exception(f"unsupported value {str(node)} at {'.'.join(curr_path)},") -def serialize_record_values(obj: Union[Dict[str, any], None]): - return [(k, json.dumps(v)) for k, v in obj.items()] if obj is not None else None + if isinstance(node, InheritDef): + paths.append( + ApplyPath( + path=curr_path, value=ApplyValue(inherit=True, payload=node.payload) + ) + ) + return paths + + if isinstance(node, list): + paths.append( + ApplyPath( + path=curr_path, + value=ApplyValue( + inherit=False, payload=serialize_static_injection(node) + ), + ) + ) + return paths + + if isinstance(node, dict): + for k, v in node.items(): + build_apply_data(v, paths, curr_path + [k]) + return paths + + if isinstance(node, int) or isinstance(node, str) or isinstance(node, bool): + paths.append( + ApplyPath( + path=curr_path, + value=ApplyValue( + inherit=False, payload=serialize_static_injection(node) + ), + ) + ) + return paths + + raise Exception(f"unsupported type {type(node)} at {'.'.join(curr_path)}") diff --git a/typegraph/python_next/typegraph_next/wit.py b/typegraph/python_next/typegraph_next/wit.py index 8bed47cace..1fe97f9b5b 100644 --- a/typegraph/python_next/typegraph_next/wit.py +++ b/typegraph/python_next/typegraph_next/wit.py @@ -6,6 +6,7 @@ from typegraph_next.gen import Root, RootImports from typegraph_next.gen.exports.core import Core from typegraph_next.gen.exports.runtimes import Runtimes +from typegraph_next.gen.exports.utils import Utils from typegraph_next.imports import Abi store = Store() @@ -14,3 +15,4 @@ core = Core(_typegraph_core) runtimes = Runtimes(_typegraph_core) +wit_utils = Utils(_typegraph_core)