diff --git a/common_knowledge/ADRs/003-ModelSchemas.md b/common_knowledge/ADRs/003-ModelSchemas.md new file mode 100644 index 00000000000..f9426c49115 --- /dev/null +++ b/common_knowledge/ADRs/003-ModelSchemas.md @@ -0,0 +1,74 @@ +# ADR 003: Conventions for Defining Zod Schemas for Sequelize Models + +## Status + +Proposed: 240812 by Roger Torres (@rotorsoft) + +## Context + +In our application, we are using Sequelize as the ORM to interact with our database and Zod to define schemas for model validation. To maintain consistency and ensure reliable data handling, we are adopting specific conventions for defining Zod schemas for Sequelize models. These conventions address how we handle Sequelize-managed timestamps, model associations, and nullable fields. + +## Decision + +We have decided to adopt the following conventions for defining Zod schemas for Sequelize models: + +1. **Primary Key ID Columns:** + - **Schema Representation:** Primary key ID columns that are generated by Sequelize will be defined as optional but not nullable, similar to timestamps. + - **Reasoning:** Primary key IDs are auto-generated by Sequelize, so they are not required when creating a record but will be present afterward. Ensuring these IDs are correctly typed as integers or strings is crucial for consistency. + - **Example:** We'll use the `PG_INT` schema util in `libs/schemas` to keep integers inside PG range: + + ```typescript + export const PG_INT = z.number().int().min(MIN_SCHEMA_INT).max(MAX_SCHEMA_INT); + + const Comment = z.object({ + id: PG_INT.optional(), + ... + }) + ``` + +2. **Sequelize-Managed Timestamps (`created_at` and `updated_at`):** + - **Schema Representation:** These fields will be defined as optional but not nullable in the Zod schema. + - **Reasoning:** Since these timestamps are automatically managed by Sequelize, they are not required when creating a record but will always be present once the record is created. + - **Coercion:** We will use Zod's coercion feature to convert ISO string representations of these timestamps to JavaScript `Date` objects for consistency in how dates are handled within our application. + - **Example:**: + + ```typescript + const Comment = z.object({ + ... + created_at: z.coerce.date().optional(), + updated_at: z.coerce.date().optional() + }) + ``` + +3. **Model Associations Represented as Foreign Key Constraints:** + - **Schema Representation:** Associations that are represented as foreign key constraints in the data model will be defined using Zod's nullish rules. + - **Reasoning:** In queries that use outer joins, associated records might not exist, resulting in null references. Using Zod's nullish rule allows us to accommodate these cases without validation errors. + - **Zod Example:** + + ```typescript + z.object({ + foreignKeyId: z.string().nullish(), + }) + ``` + +4. **Nullable Fields (`allowNull: true` in Sequelize Model):** + - **Schema Representation:** Fields that are allowed to be null in the Sequelize model (`allowNull: true`) will also be represented as nullish in the Zod schema. + - **Reasoning:** This ensures that the Zod schema accurately reflects the database schema, allowing null values where they are permitted by the database. + - **Zod Example:** + + ```typescript + z.object({ + nullableField: z.string().nullish(), + }) + ``` + +## Consequences + +- **Consistency:** These conventions will lead to consistent handling of timestamps, associations, and nullable fields across all models in our application. +- **Validation:** By accurately representing nullable fields and associations, we reduce the risk of validation errors during data processing. +- **Type Safety:** Coercing timestamp fields to `Date` objects ensures that date manipulations are type-safe and consistent throughout the codebase. + +## References + +- [Sequelize Documentation](https://sequelize.org/) +- [Zod Documentation](https://zod.dev/)