Skip to content
Merged
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
135 changes: 55 additions & 80 deletions packages/mizzle-orm/examples/relations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Relations example - REFERENCE, LOOKUP, and EMBED
* Relations example - Showcasing the new include API
*/

import {
Expand All @@ -15,6 +15,7 @@ import {

// Organizations
const organizations = mongoCollection('organizations', {
_id: objectId().internalId(),
name: string(),
createdAt: date().defaultNow(),
});
Expand All @@ -23,23 +24,19 @@ const organizations = mongoCollection('organizations', {
const users = mongoCollection(
'users',
{
_id: objectId().internalId(),
email: string().email(),
name: string(),
orgId: objectId(), // Foreign key to organizations
createdAt: date().defaultNow(),
},
{
relations: (r) => ({
// REFERENCE: Validates that orgId points to existing organization
organization: r.reference(organizations, {
localField: 'orgId',
foreignField: '_id',
}),
// LOOKUP: Populates organization data
organizationData: r.lookup(organizations, {
organization: r.lookup(organizations, {
localField: 'orgId',
foreignField: '_id',
one: true, // Single document
one: true,
}),
}),
}
Expand All @@ -49,6 +46,7 @@ const users = mongoCollection(
const posts = mongoCollection(
'posts',
{
_id: objectId().internalId(),
title: string(),
content: string(),
authorId: objectId(), // Foreign key to users
Expand All @@ -57,13 +55,8 @@ const posts = mongoCollection(
},
{
relations: (r) => ({
// REFERENCE: Validates authorId exists
author: r.reference(users, {
localField: 'authorId',
foreignField: '_id',
}),
// LOOKUP: Populates author data
authorData: r.lookup(users, {
author: r.lookup(users, {
localField: 'authorId',
foreignField: '_id',
one: true,
Expand All @@ -76,6 +69,7 @@ const posts = mongoCollection(
const comments = mongoCollection(
'comments',
{
_id: objectId().internalId(),
postId: objectId(),
authorId: objectId(),
content: string(),
Expand Down Expand Up @@ -111,98 +105,79 @@ async function relationsExample() {
const db = orm.withContext(ctx);

try {
// Note: Using `as any` type assertions to work around TypeScript's
// complexity with union types in multi-collection ORMs.
// The code is fully type-safe at runtime.

// 1. REFERENCE VALIDATION
console.log('\n=== REFERENCE Validation ===');
console.log('\n🎉 World-Class Relations API Demo\n');

// Create an organization
// 1. SETUP: Create test data
console.log('=== Setup ===');
const org = await db.organizations.create({
name: 'Acme Corp',
});
console.log('Created org:', org.name);

// Create a user with valid orgId (REFERENCE validates this)
const user = await db.users.create({
email: 'alice@acme.com',
name: 'Alice',
orgId: org._id, // Must reference existing organization
orgId: org._id,
});
console.log('Created user:', user.name);

// Try to create user with invalid orgId - will throw error
try {
const { ObjectId } = await import('mongodb');
await db.users.create({
email: 'invalid@example.com',
name: 'Invalid User',
orgId: new ObjectId(), // Non-existent org
});
} catch (err) {
console.log('✓ Reference validation caught invalid orgId');
}

// 2. LOOKUP POPULATION
console.log('\n=== LOOKUP Population ===');

// Create a post
const post = await db.posts.create({
title: 'My First Post',
content: 'Hello, World!',
authorId: user._id,
});
console.log('Created post:', post.title);

// Fetch post and populate author
const foundPosts = await db.posts.findMany({ _id: post._id });
const postsWithAuthor = await db.posts.populate(foundPosts, 'authorData');

console.log('Post:', postsWithAuthor[0].title);
console.log('Author:', postsWithAuthor[0].authorData.name);
console.log('Author Email:', postsWithAuthor[0].authorData.email);

// 3. MULTIPLE POPULATIONS
console.log('\n=== Multiple Populations ===');

// Create some comments
await db.comments.create({
postId: post._id,
authorId: user._id,
content: 'Great post!',
});
console.log('Created comment');

// 2. SINGLE INCLUDE - Simple and clean!
console.log('\n=== Single Include ===');
const postsWithAuthor = await db.posts.findMany({}, { include: 'author' });

// Fetch comments and populate both post and author
const foundComments = await db.comments.findMany({});
const populatedComments = await db.comments.populate(foundComments, [
'post',
'author',
]);

console.log('Comment:', populatedComments[0].content);
console.log('On post:', populatedComments[0].post?.title);
console.log('By:', populatedComments[0].author?.name);

// 4. NESTED POPULATION (manually)
console.log('\n=== Nested Population ===');

// Get posts with authors
const allPosts = await db.posts.findMany({});
const postsWithAuthors = await db.posts.populate(allPosts, 'authorData');

// For each author, populate their organization
for (const postWithAuthor of postsWithAuthors) {
const author = postWithAuthor.authorData;
if (author) {
const usersArray = [author];
const usersWithOrg = await db.users.populate(usersArray, 'organizationData');
postWithAuthor.authorData = usersWithOrg[0];
console.log('Post:', postsWithAuthor[0].title);
console.log('Author:', postsWithAuthor[0].author?.name); // ✅ Perfect autocomplete!
console.log('Author Email:', postsWithAuthor[0].author?.email);

// 3. MULTIPLE INCLUDES - Natural object syntax!
console.log('\n=== Multiple Includes ===');
const commentsPopulated = await db.comments.findMany(
{},
{
include: {
post: true,
author: true,
},
}
);

console.log('Comment:', commentsPopulated[0].content);
console.log('On post:', commentsPopulated[0].post?.title); // ✅ Fully typed!
console.log('By:', commentsPopulated[0].author?.name); // ✅ Fully typed!

// 4. NESTED INCLUDES - The power feature! 🚀
console.log('\n=== Nested Includes (Coming Soon) ===');
console.log('Nested includes will support queries like:');
console.log(`
const postsWithAuthorAndOrg = await db.posts.findMany({}, {
include: {
author: {
include: {
organization: true
}
}
}
}
});

// Access: postsWithAuthorAndOrg[0].author?.organization?.name
`);

console.log('Post:', postsWithAuthors[0].title);
console.log('Author:', postsWithAuthors[0].authorData?.name);
console.log('Org:', postsWithAuthors[0].authorData?.organizationData?.name);
console.log('\n✅ All includes use single MongoDB $lookup queries!');
console.log('✅ Perfect TypeScript inference!');
} finally {
await orm.close();
}
Expand Down
26 changes: 14 additions & 12 deletions packages/mizzle-orm/src/collection/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
ReferenceRelation,
EmbedRelation,
LookupRelation,
TypedRelation,
RelationTargets,
} from '../types/collection';
import { RelationType } from '../types/collection';
import type { SchemaDefinition } from '../types/field';
Expand Down Expand Up @@ -83,37 +85,37 @@ export function createIndexBuilder<TSchema extends SchemaDefinition>(_schema: TS
* Relation builder implementation
*/
class RelationBuilderImpl<TSchema extends SchemaDefinition> implements IRelationBuilder<TSchema> {
reference<TOther extends SchemaDefinition>(
otherCollection: CollectionDefinition<TOther>,
reference<TOther extends SchemaDefinition, TTargets extends RelationTargets>(
otherCollection: CollectionDefinition<TOther, TTargets>,
config: Omit<ReferenceRelation, 'type' | 'targetCollection'>,
): ReferenceRelation {
): TypedRelation<ReferenceRelation, CollectionDefinition<TOther, TTargets>> {
return {
type: RelationType.REFERENCE,
targetCollection: otherCollection._meta.name,
...config,
};
} as any; // Runtime object is ReferenceRelation, type system sees TypedRelation
}

embed<TOther extends SchemaDefinition>(
sourceCollection: CollectionDefinition<TOther>,
embed<TOther extends SchemaDefinition, TTargets extends RelationTargets>(
sourceCollection: CollectionDefinition<TOther, TTargets>,
config: Omit<EmbedRelation, 'type' | 'sourceCollection'>,
): EmbedRelation {
): TypedRelation<EmbedRelation, CollectionDefinition<TOther, TTargets>> {
return {
type: RelationType.EMBED,
sourceCollection: sourceCollection._meta.name,
...config,
};
} as any; // Runtime object is EmbedRelation, type system sees TypedRelation
}

lookup<TOther extends SchemaDefinition>(
targetCollection: CollectionDefinition<TOther>,
lookup<TOther extends SchemaDefinition, TTargets extends RelationTargets>(
targetCollection: CollectionDefinition<TOther, TTargets>,
config: Omit<LookupRelation, 'type' | 'targetCollection'>,
): LookupRelation {
): TypedRelation<LookupRelation, CollectionDefinition<TOther, TTargets>> {
return {
type: RelationType.LOOKUP,
targetCollection: targetCollection._meta.name,
...config,
};
} as any; // Runtime object is LookupRelation, type system sees TypedRelation
}
}

Expand Down
64 changes: 33 additions & 31 deletions packages/mizzle-orm/src/collection/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,43 @@ import type {
PolicyConfig,
Hooks,
CollectionAuditConfig,
TypedRelation,
ExtractRelationTargets,
} from '../types/collection';
import type { SchemaDefinition } from '../types/field';
import { createRelationBuilder } from './builders';

/**
* Define a MongoDB collection with schema and metadata
*
* @param name - Collection name
* @param schema - Schema definition object
* @param options - Optional collection configuration
* @returns Collection definition
*
* @example
* ```ts
* import { objectId, publicId, string, date } from 'mizzle-orm';
*
* const users = mongoCollection(
* 'users',
* {
* _id: objectId().internalId(),
* id: publicId('user'),
* email: string().email().unique(),
* displayName: string(),
* createdAt: date().defaultNow(),
* },
* {
* policies: {
* readFilter: (ctx) => ({ orgId: ctx.tenantId }),
* },
* }
* );
* ```
* Define a MongoDB collection without relations
*/
export function mongoCollection<TSchema extends SchemaDefinition>(
name: string,
schema: TSchema,
options: CollectionOptions<TSchema> = {},
): CollectionDefinition<TSchema> {
): CollectionDefinition<TSchema, {}>;

/**
* Define a MongoDB collection with options (including relations)
*/
export function mongoCollection<
TSchema extends SchemaDefinition,
TRels extends Record<string, TypedRelation<any, any>>,
>(
name: string,
schema: TSchema,
options: CollectionOptions<TSchema, TRels>,
): CollectionDefinition<TSchema, ExtractRelationTargets<TRels>>;

/**
* Implementation
*/
export function mongoCollection<
TSchema extends SchemaDefinition,
TRels extends Record<string, TypedRelation<any, any>> = {},
>(
name: string,
schema: TSchema,
options: CollectionOptions<TSchema, TRels> = {} as any,
): CollectionDefinition<TSchema, ExtractRelationTargets<TRels>> {
// Build indexes
const indexes: IndexDef[] = [];
if (options.indexes) {
Expand Down Expand Up @@ -80,7 +79,9 @@ export function mongoCollection<TSchema extends SchemaDefinition>(
let relations: Relations = {};
if (options.relations) {
const relationBuilder = createRelationBuilder<TSchema>();
relations = options.relations(relationBuilder, {} as any);
// Get typed relations from callback, but store as runtime Relations
const typedRelations = options.relations(relationBuilder, {} as any);
relations = typedRelations as any as Relations;
}

// Policies (plain object)
Expand Down Expand Up @@ -110,9 +111,10 @@ export function mongoCollection<TSchema extends SchemaDefinition>(
};

// Create collection definition
const definition: CollectionDefinition<TSchema> = {
const definition: CollectionDefinition<TSchema, ExtractRelationTargets<TRels>> = {
_schema: schema,
_meta: meta,
_relationTargets: null as any, // Phantom type, never accessed at runtime
_brand: 'CollectionDefinition',
$inferDocument: null as any,
$inferInsert: null as any,
Expand Down
2 changes: 2 additions & 0 deletions packages/mizzle-orm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export type { CollectionDefinition, CollectionMeta } from './types/collection';

export type { OrmContext, OrmConfig, MongoOrm } from './types/orm';

export type { IncludeConfig, NestedIncludeConfig, WithIncluded } from './types/include';

// Validation
export {
generateDocumentSchema,
Expand Down
Loading