Skip to content

Add TinyBase schema generation to specta-zod and create store-types crate#3491

Open
devin-ai-integration[bot] wants to merge 6 commits intomainfrom
devin/1769776301-specta-zod-tinybase
Open

Add TinyBase schema generation to specta-zod and create store-types crate#3491
devin-ai-integration[bot] wants to merge 6 commits intomainfrom
devin/1769776301-specta-zod-tinybase

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Jan 30, 2026

Add TinyBase schema generation to specta-zod and create store-types crate

Summary

This PR extends the specta-zod crate to generate TinyBase table schemas alongside Zod schemas, and creates a new store-types crate that defines Rust types matching the frontend store schemas.

Changes:

  • Added tinybase.rs module to specta-zod that exports types to TinyBase format: { field_name: { type: "string" | "number" | "boolean" } }
  • Created store-types crate with 19 Rust struct definitions (Human, Event, Calendar, Session, etc.) matching packages/store schemas
  • Added generator binary (generate-store-types) that outputs both Zod and TinyBase schemas
  • Fixed a bug in zod.rs where fields marked with #[specta(optional)] that were already Option<T> would get double z.preprocess wrapping

Review & Testing Checklist for Human

  • Verify type definitions match existing schemas: Compare crates/store-types/src/lib.rs against packages/store/src/schema.ts to ensure all field names, types, and optional flags match exactly
  • Verify TinyBase schema format: Confirm the generated format { field_name: { type: "string" } } is what TinyBase expects
  • Test generator output: Run cargo run -p store-types --bin generate-store-types and verify the output can replace/match the existing packages/store schemas
  • Verify the double-preprocess fix: Check that the change in zod.rs (line 240-242) doesn't break other optional field handling

Recommended test plan:

  1. Run the generator and compare output against current packages/store/src/schema.ts
  2. If planning to use generated output, replace the TypeScript file and run the frontend tests

Notes

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


Open with Devin

…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-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Jan 30, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit 012042c
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/697dc6804f60c70008c061bc

@netlify
Copy link

netlify bot commented Jan 30, 2026

Deploy Preview for hyprnote canceled.

Name Link
🔨 Latest commit 012042c
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/697dc6804527d40008d166e5

Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional flags.

Open in Devin Review

devin-ai-integration bot and others added 3 commits January 30, 2026 12:53
- 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>
Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View issue and 8 additional flags in Devin Review.

Open in Devin Review

devin-ai-integration bot and others added 2 commits January 30, 2026 13:08
- 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>
Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View issue and 8 additional flags in Devin Review.

Open in Devin Review

Comment on lines +278 to +283
if let Some(schema) = &attrs.schema {
s.push_str(schema);
if let Some(default) = &attrs.default {
write!(s, ".default({})", default)?;
}
return Ok(());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +278 to +284
if let Some(schema) = &attrs.schema {
s.push_str(schema);
if let Some(default) = &attrs.default {
write!(s, ".default({})", default)?;
}
return Ok(());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(());
}
Suggested change
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

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant