npm package: @libar-dev/zod-convex-tables
Description: Table and schema utilities for Convex with Zod v4
Author: Libar AI
Latest version: 0.1.0

# @libar-dev/zod-convex-tables

[![npm version](https://img.shields.io/npm/v/@libar-dev/zod-convex-tables.svg)](https://www.npmjs.com/package/@libar-dev/zod-convex-tables)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.5-blue)](https://www.typescriptlang.org/)

Table definition helpers and schema manipulation utilities for Convex with Zod v4.

## Features

- **Table Definition Helpers** - `zodTable` for quick table setup with Convex validators
- **Document Type Schemas** - Automatic `_id` and `_creationTime` field addition
- **Type-Safe Pick/Omit** - Extract schema subsets with full type preservation
- **Zero Type Depth Issues** - Avoids Zod's recursive type limitations

## Installation

```bash
npm install @libar-dev/zod-convex-tables zod@^4.1.0
```

**Peer Dependencies:**

- `convex` >= 1.30.0 < 2.0.0
- `convex-helpers` >= 0.1.111
- `zod` ^4.1.0

## Quick Start

### Table Definition

```typescript
import { zodTable } from '@libar-dev/zod-convex-tables';
import { z } from 'zod';

// Create table with raw shape
export const Users = zodTable('users', {
  name: z.string(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest']),
});

// Use in schema.ts:
export default defineSchema({
  users: Users.table,
});

// Type-safe document:
type User = z.infer<typeof Users.zDoc>;
// { name: string; email: string; role: 'admin' | 'user' | 'guest'; _id: Id<'users'>; _creationTime: number }
```

### Schema Picking

Extract subsets of schemas with full type inference:

```typescript
import { safePick, safeOmit } from '@libar-dev/zod-convex-tables';

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  firstName: z.string(),
  lastName: z.string(),
  password: z.string(),
});

// Pick specific fields - type-safe!
const PublicUserSchema = safePick(UserSchema, ['email', 'firstName', 'lastName']);
type PublicUser = z.infer<typeof PublicUserSchema>;
// { email: string; firstName: string; lastName: string }

// Omit sensitive fields - type-safe!
const SafeUserSchema = safeOmit(UserSchema, ['password']);
type SafeUser = z.infer<typeof SafeUserSchema>;
// { id: string; email: string; firstName: string; lastName: string }
```

## API Reference

### `zodTable(name, shape)`

Creates a Convex table definition from a Zod schema shape.

```typescript
import { zodTable } from '@libar-dev/zod-convex-tables';
import { zid } from '@libar-dev/zod-convex-ids';

export const Posts = zodTable('posts', {
  title: z.string(),
  content: z.string(),
  authorId: zid('users'),
  published: z.boolean(),
});

// Properties available:
Posts.table;     // TableDefinition for schema.ts
Posts.shape;     // Raw Zod shape { title: z.string(), ... }
Posts.zDoc;      // Schema with _id and _creationTime
Posts.name;      // 'posts'
```

**Query Return Types:**

```typescript
export const getPost = query({
  args: { id: v.id('posts') },
  handler: async (ctx, { id }) => ctx.db.get(id),
  returns: Posts.zDoc.nullable(),
});

export const listPosts = query({
  handler: async (ctx) => ctx.db.query('posts').collect(),
  returns: z.array(Posts.zDoc),
});
```

### `zodDoc(tableName, schema)`

Adds document fields (`_id`, `_creationTime`) to any Zod object schema:

```typescript
const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

const UserDocSchema = zodDoc('users', UserSchema);
// Adds _id: zid('users') and _creationTime: z.number()

type UserDoc = z.infer<typeof UserDocSchema>;
// { name: string; email: string; _id: Id<'users'>; _creationTime: number }
```

### `zodDocOrNull(tableName, schema)`

Creates a nullable document schema (useful for query returns):

```typescript
const MaybeUserSchema = zodDocOrNull('users', UserSchema);
// Same as zodDoc but with | null
```

### `pickShape(schemaOrShape, keys)`

Extracts raw shape for function arguments:

```typescript
import { pickShape } from '@libar-dev/zod-convex-tables';
import { zodToConvexFields } from '@libar-dev/zod-convex-core';

const loginShape = pickShape(UserSchema, ['email', 'password']);

export const login = mutation({
  args: zodToConvexFields(loginShape),
  handler: async (ctx, { email, password }) => {
    // Type-safe: email and password are inferred
  },
});
```

### `safePick(schema, keys)`

Creates new schema with selected fields (type-safe):

```typescript
import { safePick } from '@libar-dev/zod-convex-tables';

const UpdateSchema = safePick(UserSchema, ['firstName', 'lastName', 'email']);

type UpdateData = z.infer<typeof UpdateSchema>;
// { firstName: string; lastName: string; email: string }
```

### `safeOmit(schema, keys)`

Creates schema without specified fields (type-safe):

```typescript
import { safeOmit } from '@libar-dev/zod-convex-tables';

const PublicUserSchema = safeOmit(UserSchema, ['password', 'internalId']);

type PublicUser = z.infer<typeof PublicUserSchema>;
// All fields except password and internalId
```

## Why Safe Pick/Omit?

Zod's built-in `pick()` and `omit()` can trigger TypeScript's type depth limits with complex schemas. Our safe versions avoid this:

```typescript
// ❌ Can cause: "Type instantiation is excessively deep"
const Picked = ComplexSchema.pick({ field1: true, field2: true });

// ✅ Works with any schema complexity
const Picked = safePick(ComplexSchema, ['field1', 'field2']);
```

## Usage Patterns

### Table-Centric Development

```typescript
// convex/schema/users.ts
export const Users = zodTable('users', {
  email: z.string().email(),
  name: z.string(),
  bio: z.string().optional(),
  verified: z.boolean(),
});

// convex/users.ts
import { Users } from './schema/users';

export const createUser = mutation({
  args: zodToConvexFields(Users.shape),
  handler: async (ctx, userData) => {
    return ctx.db.insert('users', userData);
  },
});

export const getUser = query({
  args: { id: v.id('users') },
  handler: async (ctx, { id }) => ctx.db.get(id),
  returns: Users.zDoc.nullable(),
});
```

### Partial Updates

```typescript
const Users = zodTable('users', {
  email: z.string().email(),
  name: z.string(),
  bio: z.string().optional(),
});

// Partial update schema
const UpdateUserSchema = safePick(z.object(Users.shape), ['name', 'bio']);

export const updateProfile = mutation({
  args: {
    id: v.id('users'),
    ...zodToConvexFields(UpdateUserSchema.partial().shape),
  },
  handler: async (ctx, { id, ...updates }) => {
    await ctx.db.patch(id, updates);
  },
});
```

### With Authentication Context

```typescript
const Users = zodTable('users', {
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
  profile: z.object({
    name: z.string(),
    bio: z.string().optional(),
  }),
});

// Public info only
const PublicUserSchema = safePick(z.object(Users.shape), ['profile']);

export const getPublicProfile = query({
  args: { id: v.id('users') },
  handler: async (ctx, { id }) => {
    const user = await ctx.db.get(id);
    return user ? PublicUserSchema.parse(user) : null;
  },
  returns: PublicUserSchema.nullable(),
});
```

## Type Safety

All utilities maintain full TypeScript type inference:

```typescript
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  password: z.string(),
});

// Full type inference
const PublicSchema = safeOmit(UserSchema, ['password']);

type PublicUser = z.infer<typeof PublicSchema>;
// { id: string; name: string; email: string }
// ^? TypeScript knows exactly which fields are present

// Compile-time error for invalid keys
const Invalid = safePick(UserSchema, ['nonexistent']);
//                                     ^^^^^^^^^^^
// Error: Type '"nonexistent"' is not assignable to type 'keyof ...'
```

## Migration from convex-helpers

If you're migrating from `convex-helpers` table utilities, here's how to adapt your code.

### Table Definitions

**convex-helpers approach:**

```typescript
// With convex-helpers
import { zodToConvex } from 'convex-helpers/server/zod';
import { defineSchema, defineTable } from 'convex/server';
import { z } from 'zod/v3';  // Note: Zod v3

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

export default defineSchema({
  users: defineTable(zodToConvex(UserSchema))
    .index('by_email', ['email']),
});
```

**@libar-dev/zod-convex-tables approach:**

```typescript
// With zod-convex-tables
import { zodTable } from '@libar-dev/zod-convex-tables';
import { defineSchema } from 'convex/server';
import { z } from 'zod';  // Zod v4 native

export const Users = zodTable('users', {
  name: z.string(),
  email: z.string().email(),
});

export default defineSchema({
  users: Users.table
    .index('by_email', ['email']),
});

// Bonus: Get document type with _id and _creationTime
type UserDoc = z.infer<typeof Users.zDoc>;
```

### Key Differences

| Feature | convex-helpers | @libar-dev/zod-convex-tables |
|---------|---------------|------------------------------|
| Zod Version | v3 (via `zod/v3`) | v4 native |
| Table Definition | Manual `defineTable()` | `zodTable()` helper |
| Document Schema | Manual composition | Automatic `zDoc` property |
| Pick/Omit | Uses Zod's `.pick()` | Type-safe `safePick()` / `safeOmit()` |
| Type Depth Issues | Can hit TS2589 | Avoided by design |

### Pick/Omit Migration

**convex-helpers:**

```typescript
// Can cause TS2589 with complex schemas
const PublicUser = UserSchema.pick({ email: true, name: true });
const SafeUser = UserSchema.omit({ password: true });
```

**zod-convex-tables:**

```typescript
// Type-safe and TS2589-resistant
import { safePick, safeOmit } from '@libar-dev/zod-convex-tables';

const PublicUser = safePick(UserSchema, ['email', 'name']);
const SafeUser = safeOmit(UserSchema, ['password']);
```

### Step-by-Step Migration

1. **Update dependencies:**
   ```bash
   npm install @libar-dev/zod-convex-tables zod@^4
   # convex-helpers is still required (peer dependency used by zodTable)
   ```

2. **Update imports:**
   ```typescript
   // Before
   import { zodToConvex } from 'convex-helpers/server/zod';
   import { z } from 'zod/v3';

   // After
   import { zodTable } from '@libar-dev/zod-convex-tables';
   import { z } from 'zod';
   ```

3. **Replace table definitions:**
   ```typescript
   // Before
   export default defineSchema({
     users: defineTable(zodToConvex(UserSchema)),
   });

   // After
   export const Users = zodTable('users', {
     name: z.string(),
     email: z.string().email(),
   });

   export default defineSchema({
     users: Users.table,
   });
   ```

4. **Replace pick/omit:**
   ```typescript
   // Before
   const Partial = Schema.pick({ field1: true });

   // After
   import { safePick } from '@libar-dev/zod-convex-tables';
   const Partial = safePick(Schema, ['field1']);
   ```

## Related Packages

- [`@libar-dev/zod-convex-core`](https://www.npmjs.com/package/@libar-dev/zod-convex-core) - Core conversion engine
- [`@libar-dev/zod-convex-ids`](https://www.npmjs.com/package/@libar-dev/zod-convex-ids) - Type-safe ID validation
- [`@libar-dev/zod-convex-codecs`](https://www.npmjs.com/package/@libar-dev/zod-convex-codecs) - Bidirectional codecs

## License

MIT License - See [LICENSE](./LICENSE) file for details.

Copyright (c) 2025 Libar AI