commit 81fce43cda89b3a2089d1bdd45edcb43fb1c9553
parent f55b6d65023e475bf71faee8b7261965d7c5dee9
Author: triesap <tyson@radroots.org>
Date: Wed, 24 Jun 2026 09:04:55 +0000
replica: render schema bindings from dto registry
Replace the hand-authored replica binding model with a source-owned dto registry exported by radroots_replica_db_schema.
Teach xtask to render untagged DTO enums and route the replica package through TsSource::DtoRegistry.
Regenerate @radroots/replica-db-schema-bindings so JSON partial fields, nullable find-one results, NostrEventHead, and bigint policy match source.
Validate with cargo fmt/check/test, xtask generate/check, and build/typecheck for the affected TypeScript packages.
Diffstat:
3 files changed, 186 insertions(+), 6 deletions(-)
diff --git a/tools/sdk_xtask_import/src/dto_render.rs b/tools/sdk_xtask_import/src/dto_render.rs
@@ -161,13 +161,57 @@ fn render_enum(
EnumRepr::Adjacent { tag, content } => {
render_tagged_enum(def, tag, Some(content.as_str()), registry, options, imports)
}
- EnumRepr::Untagged => Err(format!(
- "unsupported enum representation for {}",
- enum_type_name(def)
- )),
+ EnumRepr::Untagged => render_untagged_enum(def, registry, options, imports),
}
}
+fn render_untagged_enum(
+ def: &EnumDef,
+ registry: &Registry,
+ options: &DtoRegistryRenderOptions,
+ imports: &mut BTreeMap<String, BTreeSet<String>>,
+) -> Result<String, String> {
+ let variants = def
+ .variants
+ .iter()
+ .map(|variant| render_untagged_variant(def, variant, registry, options, imports))
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(format!(
+ "export type {} = {};",
+ enum_type_name(def),
+ render_union(variants)
+ ))
+}
+
+fn render_untagged_variant(
+ def: &EnumDef,
+ variant: &VariantDef,
+ registry: &Registry,
+ options: &DtoRegistryRenderOptions,
+ imports: &mut BTreeMap<String, BTreeSet<String>>,
+) -> Result<String, String> {
+ let rendered: Result<String, String> = match &variant.shape {
+ VariantShape::Unit => Ok("undefined".to_owned()),
+ VariantShape::Newtype(ty) => render_type_ref(ty, None, registry, options, imports),
+ VariantShape::Tuple(items) => {
+ let rendered = items
+ .iter()
+ .map(|item| render_type_ref(item, None, registry, options, imports))
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(format!("[{}]", rendered.join(", ")))
+ }
+ VariantShape::Struct(fields) => render_object_fields(fields, registry, options, imports),
+ };
+
+ rendered.map_err(|error| {
+ format!(
+ "{error} while rendering untagged enum {}.{}",
+ enum_type_name(def),
+ variant.rust_name
+ )
+ })
+}
+
fn render_external_enum(
def: &EnumDef,
registry: &Registry,
@@ -729,6 +773,70 @@ mod tests {
}
#[test]
+ fn renders_untagged_object_unions() {
+ let mut registry = Registry::new();
+ registry.register_type(
+ RustTypeId::new("sdk", "Query"),
+ TypeDef::Enum(
+ EnumDef::new("Query", "Query", EnumRepr::Untagged, span())
+ .with_variant(VariantDef::new(
+ "ById",
+ "byId",
+ VariantShape::Struct(vec![field("id", "id", TypeRef::String)]),
+ span(),
+ ))
+ .with_variant(VariantDef::new(
+ "BySlug",
+ "bySlug",
+ VariantShape::Struct(vec![field("slug", "slug", TypeRef::String)]),
+ span(),
+ )),
+ ),
+ );
+
+ let rendered = render_registry_types(®istry, &DtoRegistryRenderOptions::default())
+ .expect("registry renders");
+
+ assert_eq!(
+ rendered.body_ts(),
+ "export type Query = { id: string, } | { slug: string, };"
+ );
+ }
+
+ #[test]
+ fn renders_untagged_newtype_aliases() {
+ let mut registry = Registry::new();
+ registry.register_type(
+ RustTypeId::new("sdk", "FindOneResolve"),
+ TypeDef::Enum(
+ EnumDef::new(
+ "FindOneResolve",
+ "FindOneResolve",
+ EnumRepr::Untagged,
+ span(),
+ )
+ .with_variant(VariantDef::new(
+ "Alias",
+ "alias",
+ VariantShape::Newtype(TypeRef::Override(TargetOverride::new(
+ BackendId::TypeScript,
+ "IResult<Farm | null>",
+ ))),
+ span(),
+ )),
+ ),
+ );
+
+ let rendered = render_registry_types(®istry, &DtoRegistryRenderOptions::default())
+ .expect("registry renders");
+
+ assert_eq!(
+ rendered.body_ts(),
+ "export type FindOneResolve = IResult<Farm | null>;"
+ );
+ }
+
+ #[test]
fn requires_explicit_large_integer_policy() {
let mut registry = Registry::new();
registry.register_type(
diff --git a/tools/sdk_xtask_import/src/dto_roots.rs b/tools/sdk_xtask_import/src/dto_roots.rs
@@ -165,6 +165,13 @@ pub fn events_indexed_types_module() -> Result<DtoTypesModule, String> {
))
}
+pub fn replica_db_schema_types_module() -> Result<DtoTypesModule, String> {
+ render_registry_types(
+ &radroots_replica_db_schema_bindings::dto_registry(),
+ &DtoRegistryRenderOptions::default(),
+ )
+}
+
pub fn trade_types_module() -> Result<DtoTypesModule, String> {
let root_set = package_root_set("trade").ok_or_else(|| "missing trade DTO roots".to_owned())?;
let registry = root_set.registry();
@@ -330,6 +337,8 @@ mod tests {
include_str!("../../../packages/events-bindings/src/generated/types.ts");
const EVENTS_INDEXED_BINDINGS_TYPES_TS: &str =
include_str!("../../../packages/events-indexed-bindings/src/generated/types.ts");
+ const REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS: &str =
+ include_str!("../../../packages/replica-db-schema-bindings/src/generated/types.ts");
const TRADE_BINDINGS_TYPES_TS: &str =
include_str!("../../../packages/trade-bindings/src/generated/types.ts");
const EVENTS_TYPE_INVENTORY: &[&str] = &[
@@ -556,6 +565,37 @@ mod tests {
}
#[test]
+ fn replica_db_schema_generated_types_preserve_source_schema_contracts() {
+ let actual = type_inventory(REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS);
+ let trade_product_filter = type_declaration(
+ REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS,
+ "ITradeProductFieldsFilter",
+ );
+ let trade_product_partial = type_declaration(
+ REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS,
+ "ITradeProductFieldsPartial",
+ );
+
+ assert!(actual.contains(&"Farm"));
+ assert!(actual.contains(&"GcsLocation"));
+ assert!(actual.contains(&"NostrEventHead"));
+ assert!(actual.contains(&"ReplicaDbJsonValue"));
+ assert!(actual.contains(&"ITradeProductFieldsPartial"));
+ assert!(!actual.contains(&"NostrEventState"));
+ assert!(REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS.contains(
+ "export type ReplicaDbJsonValue = null | boolean | number | string | Array<ReplicaDbJsonValue> | { [key: string]: ReplicaDbJsonValue };"
+ ));
+ assert!(
+ REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS
+ .contains("export type IFarmFindOneResolve = IResult<Farm | null>;")
+ );
+ assert!(trade_product_filter.contains("year?: bigint"));
+ assert!(trade_product_filter.contains("qty_avail?: bigint"));
+ assert!(trade_product_partial.contains("year?: ReplicaDbJsonValue | null"));
+ assert!(trade_product_partial.contains("qty_avail?: ReplicaDbJsonValue | null"));
+ }
+
+ #[test]
fn trade_package_imports_source_owned_support_types() {
assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/core-bindings\""));
assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/events-bindings\""));
@@ -577,4 +617,11 @@ mod tests {
.map(|rest| rest.split([' ', '<']).next().expect("type name"))
.collect()
}
+
+ fn type_declaration<'a>(types_ts: &'a str, name: &str) -> &'a str {
+ types_ts
+ .lines()
+ .find(|line| line.starts_with(&format!("export type {name} = ")))
+ .unwrap_or_else(|| panic!("missing type declaration for {name}"))
+ }
}
diff --git a/tools/sdk_xtask_import/src/output.rs b/tools/sdk_xtask_import/src/output.rs
@@ -114,8 +114,8 @@ pub fn package_outputs() -> Result<Vec<PackageOutput>, String> {
},
PackageOutput {
spec: spec_by_key("replica_db_schema"),
- types_ts: Some(TsSource::Module(
- radroots_replica_db_schema_bindings::types_module(),
+ types_ts: Some(TsSource::DtoRegistry(
+ dto_roots::replica_db_schema_types_module()?,
)),
types_imports_ts: Some(REPLICA_DB_SCHEMA_TYPES_IMPORTS_TS),
constants_ts: None,
@@ -207,6 +207,8 @@ mod tests {
const TRADE_BINDINGS_TYPES_TS: &str =
include_str!("../../../packages/trade-bindings/src/generated/types.ts");
+ const REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS: &str =
+ include_str!("../../../packages/replica-db-schema-bindings/src/generated/types.ts");
#[test]
fn renders_sdk_header() {
@@ -311,4 +313,27 @@ mod tests {
assert_eq!(types.contents, TRADE_BINDINGS_TYPES_TS);
}
+
+ #[test]
+ fn replica_db_schema_output_uses_dto_registry_and_matches_checked_in_types() {
+ let output = package_outputs()
+ .expect("package outputs")
+ .into_iter()
+ .find(|output| output.spec.key == "replica_db_schema")
+ .expect("replica_db_schema output");
+
+ assert!(matches!(output.types_ts, Some(TsSource::DtoRegistry(_))));
+ assert_eq!(
+ output.types_imports_ts,
+ Some(super::REPLICA_DB_SCHEMA_TYPES_IMPORTS_TS)
+ );
+
+ let types = output
+ .files()
+ .into_iter()
+ .find(|file| file.relative_path == "src/generated/types.ts")
+ .expect("types file");
+
+ assert_eq!(types.contents, REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS);
+ }
}