Skip to content
Draft

beta #263

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 133 additions & 155 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,260 +4,238 @@ Generate [Zero](https://zero.rocicorp.dev/) schemas from [Drizzle ORM](https://o

## Installation

`drizzle-zero` now targets Drizzle beta relations only.

```bash
npm install drizzle-zero
npm install drizzle-zero drizzle-orm@beta
# or
bun add drizzle-zero
bun add drizzle-zero drizzle-orm@beta
# or
yarn add drizzle-zero
yarn add drizzle-zero drizzle-orm@beta
# or
pnpm add drizzle-zero
pnpm add drizzle-zero drizzle-orm@beta
```

## Usage

Here's an example of how to convert a Drizzle schema to a Zero schema with bidirectional relationships:
Here is a beta Drizzle example with bidirectional relationships:

### Define Drizzle schema

You should have an existing Drizzle schema, e.g.:

```ts
import {relations} from 'drizzle-orm';
import {pgTable, text, jsonb} from 'drizzle-orm/pg-core';
import {defineRelations} from 'drizzle-orm/relations';
import {jsonb, pgTable, text} from 'drizzle-orm/pg-core';

export const users = pgTable('user', {
id: text('id').primaryKey(),
name: text('name'),
// custom types are supported for any column type!
email: text('email').$type<`${string}@${string}`>().notNull(),
});

export const usersRelations = relations(users, ({many}) => ({
posts: many(posts),
}));

export const posts = pgTable('post', {
id: text('id').primaryKey(),
// this JSON type will be passed to Zero
content: jsonb('content').$type<{textValue: string}>().notNull(),
authorId: text('author_id').references(() => users.id),
});

export const postsRelations = relations(posts, ({one}) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
export const schemaRelations = defineRelations({users, posts}, r => ({
users: {
posts: r.many.posts(),
},
posts: {
author: r.one.users({
from: r.posts.authorId,
to: r.users.id,
}),
},
}));
```

See the [integration test's `schema.ts`](db/drizzle/schema.ts)
for more examples of how to define Drizzle schemas with custom types.
> Legacy Drizzle `relations(...)` exports are not supported.

### Add schema generation script
See `db/drizzle/schema.ts` for a larger beta-only example.

You can then add the schema generation script to your `package.json`:
### Add schema generation script

```json
{
"scripts": {
"generate": "drizzle-zero generate --format",
"postinstall": "npm generate"
"postinstall": "npm run generate"
}
}
```

This command will look for a Drizzle Kit config at `drizzle.config.ts` in the current directory
and use the Drizzle schema defined in it. _This must be a single TS file and
not a folder/glob for type resolution to work_. It will also use the
casing defined in your drizzle config.
This command looks for `drizzle.config.ts` in the current directory and uses the Drizzle schema defined there. The schema entry must resolve to a single TypeScript file so type resolution works correctly.

You can change this behavior with `-s, --schema <input-file>`
as the path to your Drizzle schema file, or
`-k, --drizzle-kit-config <input-file>` with the path to your `drizzle.config.ts` file.
You can override that behavior with:

By default, it will output your schema to `zero-schema.gen.ts`.
You can customize the generated file path with `-o, --output <output-file>`.
- `-s, --schema <input-file>` for the Drizzle schema file
- `-k, --drizzle-kit-config <input-file>` for the Drizzle Kit config file
- `-o, --output <output-file>` for the generated Zero schema path
- `-t, --tsconfig <tsconfig-file>` for a custom tsconfig path

If you have Prettier installed, you can use it to format the generated output
with `-f, --format`.
By default, output goes to `zero-schema.gen.ts`.

To specify a custom tsconfig file, use `-t, --tsconfig <tsconfig-file>`.
It will, by default, look for one in the current directory.
If Prettier is installed, you can format the generated output with `-f, --format`.

The CLI automatically detects whether `.js` file extensions are needed in import statements based on your tsconfig's `moduleResolution` setting. If you're using `"moduleResolution": "node16"` or `"nodenext"`, the generator will automatically add `.js` extensions to imports. You can override this behavior with `-j, --js-file-extension` if needed.
The CLI automatically detects whether `.js` file extensions are needed in imports from your tsconfig `moduleResolution`. You can override that behavior with `-j, --js-file-extension` if needed.

You can also control optional outputs from the generator:
Optional generator flags:

- **--skip-types**: Skip generating table `Row[]` type exports.
- **--skip-builder**: Skip generating the query `createBuilder` export.
- **--skip-declare**: Skip generating the module augmentation for default types in Zero.
- **--enable-legacy-mutators**: Enable legacy CRUD mutators (sets `enableLegacyMutators` to `true` in the generated schema).
- **--enable-legacy-queries**: Enable legacy CRUD queries (sets `enableLegacyQueries` to `true` in the generated schema).
- **--suppress-defaults-warning**: Hide warnings for columns with database default values. By default, drizzle-zero warns when columns use database defaults (`.default()` or `.defaultFn()`) since these won't be available on the Zero client.
- `--skip-types`: skip generating table `Row[]` type exports
- `--skip-builder`: skip generating the query `createBuilder` export
- `--skip-declare`: skip generating the Zero module augmentation
- `--enable-legacy-mutators`: set `enableLegacyMutators` on the generated schema
- `--enable-legacy-queries`: set `enableLegacyQueries` on the generated schema
- `--suppress-defaults-warning`: hide warnings for database defaults that Zero clients cannot use directly

For more information on disabling legacy mutators and queries, see the [Zero documentation](https://zero.rocicorp.dev/docs/custom-mutators#disabling-crud-mutators).

**Important:** the Drizzle schema **must be included in the tsconfig** for
type resolution to work. If they are not included, there will be an error similar to
`Failed to find type definitions`.
**Important:** your Drizzle schema must be included in your tsconfig for type resolution to work.

Please reference the Zero docs for how to use your new Zero schema: [https://zero.rocicorp.dev/docs/reading-data](https://zero.rocicorp.dev/docs/reading-data).
Please reference the Zero docs for how to use your generated schema: [https://zero.rocicorp.dev/docs/reading-data](https://zero.rocicorp.dev/docs/reading-data).

### Customize with `drizzle-zero.config.ts`
## Customize with `drizzle-zero.config.ts`

If you want to customize the tables/columns that are synced by Zero, you can optionally
create a new config file at `drizzle-zero.config.ts` specifying the tables and/or columns you want to
include in the CLI output:
You can optionally create `drizzle-zero.config.ts` to control which tables and columns are synced:

> **Important:** The config file currently struggles with types for large schemas. In those cases,
> stick with the default CLI behavior.
> **Important:** the config file currently struggles with types for very large schemas. In those cases, prefer the default CLI behavior.

```ts
import {drizzleZeroConfig} from 'drizzle-zero';
// directly glob import your original Drizzle schema w/ tables/relations
import * as drizzleSchema from './drizzle-schema';

// Define your configuration file for the CLI
export default drizzleZeroConfig(drizzleSchema, {
// Specify which tables and columns to include in the Zero schema.
// This allows for the "expand/migrate/contract" pattern recommended in the Zero docs.

// All tables/columns must be defined, but can be omitted or set to false to exclude them from the Zero schema.
// Column names match your Drizzle schema definitions
tables: {
// this can be set to false
// e.g. users: false,
users: {
id: true,
name: true,
// omit columns to exclude them
email: true,
},
posts: {
// or this can be set to false
// e.g. id: false,
id: true,
content: true,
// Use the JavaScript field name (authorId), not the DB column name (author_id)
authorId: true,
},
},

// Specify the casing style to use for the schema.
// This is useful for when you want to use a different casing style than the default.
// This works in the same way as the `casing` option in the Drizzle ORM.
//
// @example
// casing: "snake_case",
// Optional casing override.
// casing: 'snake_case',
});
```

You can customize this config file path with `-c, --config <input-file>`.
The imported Drizzle schema must include your beta relation exports too.

**Important:** the `drizzle-zero.config.ts` file **must be included in the tsconfig**
for the type resolution to work. If they are not included, there will be an error similar to
`Failed to find type definitions`.
You can customize the config file path with `-c, --config <input-file>`.

## Many-to-Many Relationships
**Important:** `drizzle-zero.config.ts` must also be included in your tsconfig for type resolution to work.

drizzle-zero supports many-to-many relationships with a junction table. You can configure them in two ways:
## Many-to-Many with `through(...)`

### Simple Configuration
`drizzle-zero` now pulls many-to-many relationships directly from beta Drizzle relations. Do not configure them with the legacy `manyToMany` config.

```ts
export default drizzleZeroConfig(drizzleSchema, {
tables: {
user: {
id: true,
name: true,
},
usersToGroup: {
userId: true,
groupId: true,
import {defineRelations} from 'drizzle-orm/relations';
import {pgTable, primaryKey, text} from 'drizzle-orm/pg-core';

export const users = pgTable('user', {
id: text('id').primaryKey(),
name: text('name'),
});

export const groups = pgTable('group', {
id: text('id').primaryKey(),
name: text('name'),
});

export const usersToGroups = pgTable(
'users_to_group',
{
userId: text('user_id')
.notNull()
.references(() => users.id),
groupId: text('group_id')
.notNull()
.references(() => groups.id),
},
t => [primaryKey({columns: [t.userId, t.groupId]})],
);

export const schemaRelations = defineRelations(
{users, groups, usersToGroups},
r => ({
users: {
usersToGroups: r.many.usersToGroups(),
groups: r.many.groups({
from: r.users.id.through(r.usersToGroups.userId),
to: r.groups.id.through(r.usersToGroups.groupId),
}),
},
group: {
id: true,
name: true,
usersToGroups: {
user: r.one.users({
from: r.usersToGroups.userId,
to: r.users.id,
optional: false,
}),
group: r.one.groups({
from: r.usersToGroups.groupId,
to: r.groups.id,
optional: false,
}),
},
},
manyToMany: {
user: {
// Simple format: [junction table, target table]
groups: ['usersToGroup', 'group'],
groups: {
usersToGroups: r.many.usersToGroups(),
},
},
});
}),
);
```

Then query as usual, skipping the junction table:
Then query as usual, skipping the junction table in your Zero query:

```tsx
const userQuery = syncedQuery(
z.query.user.where('id', '=', '1').related('groups').one(),
);

const [user] = useQuery(userQuery());

console.log(user);
// {
// id: "user_1",
// name: "User 1",
// groups: [
// { id: "group_1", name: "Group 1" },
// { id: "group_2", name: "Group 2" },
// ],
// }
```

### Extended Configuration
## Large Schemas

For more complex scenarios like self-referential relationships:
For large schemas, split relation exports with `defineRelationsPart(...)` and export all parts from the same schema module. `drizzle-zero` will merge them during discovery.

```ts
export default drizzleZeroConfig(drizzleSchema, {
tables: {
user: {
id: true,
name: true,
},
friendship: {
requestingId: true,
acceptingId: true,
},
},
manyToMany: {
user: {
// Extended format with explicit field mappings
friends: [
{
sourceField: ['id'],
destTable: 'friendship',
destField: ['requestingId'],
},
{
sourceField: ['acceptingId'],
destTable: 'user',
destField: ['id'],
},
],
},
},
});
```
## v1 Migration Notes

Upgrade order:

1. Upgrade `drizzle-orm` to the npm `beta` tag or another version in the supported beta range.
2. Upgrade `drizzle-zero` to the matching beta-only release line.
3. Replace legacy `relations(...)` exports with beta `defineRelations(...)` or `defineRelationsPart(...)`.
4. Replace old `manyToMany` config with beta `through(...)` relations in the Drizzle schema itself.

Concept mapping:

- legacy `relations(...)` -> beta `defineRelations(...)` / `defineRelationsPart(...)`
- config `manyToMany` -> beta `through(...)`
- reverse-inferred many-to-many edges -> explicit beta hop definitions via `through(...)`

Dropped support:

- legacy Drizzle `relations(...)`
- `manyToMany` config in `drizzleZeroConfig(...)`

## Features

- Output static schemas from the CLI
- Convert Drizzle ORM schemas to Zero schemas
- Sync a subset of tables and columns
- Handles all Drizzle column types that are supported by Zero
- Type-safe schema generation with inferred types from Drizzle
- Custom ZQL database adapter for using Drizzle in the same `tx` as Zero mutators
- Supports relationships:
- One-to-one relationships
- One-to-many relationships
- Many-to-many relationships with simple or extended configuration
- Self-referential relationships
- Handles custom schemas and column mappings
- Sync a subset of tables and columns
- Handle Drizzle column types supported by Zero
- Preserve custom column types in generated output
- Support beta Drizzle relationships:
- one-to-one
- one-to-many
- many-to-many via `through(...)`
- self-referential relationships
- split relation definitions via `defineRelationsPart(...)`
- Handle custom schemas and column mappings
Loading
Loading