Add TinyBase schema generation to specta-zod and create store-types crate#3491
Add TinyBase schema generation to specta-zod and create store-types crate#3491devin-ai-integration[bot] wants to merge 6 commits intomainfrom
Conversation
…rate - Add TinyBase exporter module to specta-zod for generating TinyBase table schemas - Fix double z.preprocess issue for optional fields that are already nullable - Create store-types crate with Rust type definitions matching packages/store schemas - Add generator binary to output both Zod and TinyBase schemas from Rust types - Include snapshot tests for TinyBase schema generation Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
✅ Deploy Preview for hyprnote-storybook canceled.
|
✅ Deploy Preview for hyprnote canceled.
|
- Add enum schemas (calendarProviderSchema, participantSourceSchema) - Add providerSpeakerIndexSchema and aiProviderSchema - Generate tableSchemaForTinybase as combined object - Generate valueSchemaForTinybase for General values - Add all type exports and Storage type exports - Handle special cases: defaults, JSON fields, enum references Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
- Add auto-generated header comment to indicate file is generated - Run generator to produce complete schema.ts replacement - Format with dprint The schema.ts file is now generated from Rust types defined in crates/store-types/src/lib.rs. To regenerate: cargo run -p store-types --bin generate-store-types > packages/store/src/schema.ts Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
- Remove duplicate type exports (Zod exporter already generates inline types) - Add reorder_template_schemas function to ensure templateSectionSchema is defined before templateSchema (which references it) - Typecheck now passes Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
| if let Some(schema) = &attrs.schema { | ||
| s.push_str(schema); | ||
| if let Some(default) = &attrs.default { | ||
| write!(s, ".default({})", default)?; | ||
| } | ||
| return Ok(()); |
There was a problem hiding this comment.
🔴 Missing z.preprocess wrapper when @zod.schema is used with optional fields
When a field has @zod.schema(...) annotation and is also optional (via #[specta(optional)] or Option<T>), the generated Zod schema doesn't include the z.preprocess((val) => val ?? undefined, ...) wrapper that handles null values.
Click to expand
Details
For MappingSessionParticipant.source in crates/store-types/src/types.rs:98-100:
/// @zod.schema(participantSourceSchema.optional())
#[specta(optional)]
pub source: Option<String>,The old generated output was:
source: z.preprocess(
(val) => val ?? undefined,
participantSourceSchema.optional(),
),The new generated output is:
source: participantSourceSchema.optional(),The field_type function at lines 278-283 returns early when @zod.schema is present without checking if the field needs the z.preprocess wrapper for handling null values.
Impact
If the data source contains null values for this field (e.g., from a database or API), the Zod validation will fail because null is not a valid value for participantSourceSchema.optional() (which only accepts undefined or valid enum values).
Recommendation: When @zod.schema is used and the field is optional (field.optional() or is_nullable), wrap the schema in z.preprocess((val) => val ?? undefined, ...) before returning.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if let Some(schema) = &attrs.schema { | ||
| s.push_str(schema); | ||
| if let Some(default) = &attrs.default { | ||
| write!(s, ".default({})", default)?; | ||
| } | ||
| return Ok(()); | ||
| } |
There was a problem hiding this comment.
Missing z.preprocess wrapper for optional fields when using @zod.schema. When a field has both #[specta(optional)] and @zod.schema(), the code outputs the schema directly without the z.preprocess((val) => val ?? undefined, ...) wrapper that other optional fields receive. This causes inconsistent behavior.
Impact: The mappingSessionParticipantSchema.source field will not handle null values the same way as the original schema, potentially causing runtime validation errors.
Fix:
if let Some(schema) = &attrs.schema {
if field.optional() && !is_nullable {
s.push_str("z.preprocess((val) => val ?? undefined, ");
s.push_str(schema);
s.push(')'));
} else {
s.push_str(schema);
}
if let Some(default) = &attrs.default {
write!(s, ".default({})", default)?;
}
return Ok(());
}| if let Some(schema) = &attrs.schema { | |
| s.push_str(schema); | |
| if let Some(default) = &attrs.default { | |
| write!(s, ".default({})", default)?; | |
| } | |
| return Ok(()); | |
| } | |
| if let Some(schema) = &attrs.schema { | |
| if field.optional() && !is_nullable { | |
| s.push_str("z.preprocess((val) => val ?? undefined, "); | |
| s.push_str(schema); | |
| s.push(')'); | |
| } else { | |
| s.push_str(schema); | |
| } | |
| if let Some(default) = &attrs.default { | |
| write!(s, ".default({})", default)?; | |
| } | |
| return Ok(()); | |
| } |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
Add TinyBase schema generation to specta-zod and create store-types crate
Summary
This PR extends the
specta-zodcrate to generate TinyBase table schemas alongside Zod schemas, and creates a newstore-typescrate that defines Rust types matching the frontend store schemas.Changes:
tinybase.rsmodule tospecta-zodthat exports types to TinyBase format:{ field_name: { type: "string" | "number" | "boolean" } }store-typescrate with 19 Rust struct definitions (Human, Event, Calendar, Session, etc.) matchingpackages/storeschemasgenerate-store-types) that outputs both Zod and TinyBase schemaszod.rswhere fields marked with#[specta(optional)]that were alreadyOption<T>would get doublez.preprocesswrappingReview & Testing Checklist for Human
crates/store-types/src/lib.rsagainstpackages/store/src/schema.tsto ensure all field names, types, and optional flags match exactly{ field_name: { type: "string" } }is what TinyBase expectscargo run -p store-types --bin generate-store-typesand verify the output can replace/match the existingpackages/storeschemaszod.rs(line 240-242) doesn't break other optional field handlingRecommended test plan:
packages/store/src/schema.tsNotes
The Rust types were manually defined based on the existing TypeScript schemas. Complex types (arrays, objects) are mapped to
"string"in TinyBase since they're stored as JSON strings.Link to Devin run: https://app.devin.ai/sessions/2d7ebd5b20334aabbaef2224e9c6620d
Requested by: @yujonglee