Skip to content

Commit d4d4b16

Browse files
add relationship support and try to solve some of the ts errors.
1 parent de9af9c commit d4d4b16

11 files changed

Lines changed: 988 additions & 33 deletions

File tree

packages/mizzle-orm/examples/crud-operations.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import {
66
createMongoOrm,
7+
defineCollections,
78
mongoCollection,
89
objectId,
910
publicId,
@@ -12,7 +13,6 @@ import {
1213
date,
1314
array,
1415
} from '../src/index';
15-
import type { InferDocument, InferInsert } from '../src/index';
1616

1717
// Define collections
1818
const users = mongoCollection(
@@ -85,9 +85,6 @@ const projects = mongoCollection(
8585
},
8686
);
8787

88-
// Types
89-
type User = InferDocument<typeof users>;
90-
type NewUser = InferInsert<typeof users>;
9188

9289
/**
9390
* Example usage
@@ -97,10 +94,11 @@ async function main() {
9794
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
9895

9996
// Create ORM instance
97+
const collections = defineCollections({ users, projects });
10098
const orm = await createMongoOrm({
10199
uri: MONGO_URI,
102100
dbName: 'mizzle_example',
103-
collections: [users, projects],
101+
collections,
104102
});
105103

106104
console.log('✓ Connected to MongoDB');
@@ -121,14 +119,14 @@ async function main() {
121119
// ========== CREATE ==========
122120
console.log('\n--- CREATE ---');
123121

124-
const newUser: NewUser = {
122+
const alice = await db.users.create({
123+
// TypeScript note: id, _id, createdAt, updatedAt, deletedAt, isActive, role
124+
// are automatically generated or have defaults, so they're optional here
125125
orgId: ctx.tenantIdObjectId!,
126126
email: 'alice@example.com',
127127
displayName: 'Alice Smith',
128128
tags: ['developer', 'typescript'],
129-
};
130-
131-
const alice = await db.users.create(newUser);
129+
});
132130
console.log('Created user:', alice.id, alice.email);
133131

134132
const bob = await db.users.create({
@@ -173,7 +171,10 @@ async function main() {
173171
tags: ['developer', 'typescript', 'mongodb'],
174172
});
175173
console.log('Updated user:', updatedAlice?.displayName);
176-
console.log('Updated timestamp changed:', updatedAlice?.updatedAt > alice.updatedAt);
174+
console.log(
175+
'Updated timestamp changed:',
176+
updatedAlice?.updatedAt && updatedAlice.updatedAt > alice.updatedAt
177+
);
177178

178179
// Update many
179180
const updated = await db.users.updateMany({ role: 'user' }, { isActive: true });
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/**
2+
* Relations example - REFERENCE, LOOKUP, and EMBED
3+
*/
4+
5+
import {
6+
mongoCollection,
7+
objectId,
8+
string,
9+
number,
10+
date,
11+
createMongoOrm,
12+
} from '../src/index';
13+
14+
// ============ COLLECTIONS ============
15+
16+
// Organizations
17+
const organizations = mongoCollection('organizations', {
18+
name: string(),
19+
createdAt: date().defaultNow(),
20+
});
21+
22+
// Users with REFERENCE to organizations
23+
const users = mongoCollection(
24+
'users',
25+
{
26+
email: string().email(),
27+
name: string(),
28+
orgId: objectId(), // Foreign key to organizations
29+
createdAt: date().defaultNow(),
30+
},
31+
{
32+
relations: (r) => ({
33+
// REFERENCE: Validates that orgId points to existing organization
34+
organization: r.reference(organizations, {
35+
localField: 'orgId',
36+
foreignField: '_id',
37+
}),
38+
// LOOKUP: Populates organization data
39+
organizationData: r.lookup(organizations, {
40+
localField: 'orgId',
41+
foreignField: '_id',
42+
one: true, // Single document
43+
}),
44+
}),
45+
}
46+
);
47+
48+
// Posts with REFERENCE and LOOKUP to users
49+
const posts = mongoCollection(
50+
'posts',
51+
{
52+
title: string(),
53+
content: string(),
54+
authorId: objectId(), // Foreign key to users
55+
likes: number().default(0),
56+
createdAt: date().defaultNow(),
57+
},
58+
{
59+
relations: (r) => ({
60+
// REFERENCE: Validates authorId exists
61+
author: r.reference(users, {
62+
localField: 'authorId',
63+
foreignField: '_id',
64+
}),
65+
// LOOKUP: Populates author data
66+
authorData: r.lookup(users, {
67+
localField: 'authorId',
68+
foreignField: '_id',
69+
one: true,
70+
}),
71+
}),
72+
}
73+
);
74+
75+
// Comments with nested LOOKUP
76+
const comments = mongoCollection(
77+
'comments',
78+
{
79+
postId: objectId(),
80+
authorId: objectId(),
81+
content: string(),
82+
createdAt: date().defaultNow(),
83+
},
84+
{
85+
relations: (r) => ({
86+
post: r.lookup(posts, {
87+
localField: 'postId',
88+
foreignField: '_id',
89+
one: true,
90+
}),
91+
author: r.lookup(users, {
92+
localField: 'authorId',
93+
foreignField: '_id',
94+
one: true,
95+
}),
96+
}),
97+
}
98+
);
99+
100+
// ============ USAGE EXAMPLE ============
101+
102+
async function relationsExample() {
103+
// Connect to MongoDB
104+
const orm = await createMongoOrm({
105+
uri: process.env.MONGO_URI || 'mongodb://localhost:27017',
106+
dbName: 'mizzle_relations_example',
107+
collections: { organizations, users, posts, comments },
108+
});
109+
110+
const ctx = orm.createContext({});
111+
const db = orm.withContext(ctx);
112+
113+
try {
114+
// Note: Using `as any` type assertions to work around TypeScript's
115+
// complexity with union types in multi-collection ORMs.
116+
// The code is fully type-safe at runtime.
117+
118+
// 1. REFERENCE VALIDATION
119+
console.log('\n=== REFERENCE Validation ===');
120+
121+
// Create an organization
122+
const org = await db.organizations.create({
123+
name: 'Acme Corp',
124+
});
125+
console.log('Created org:', org.name);
126+
127+
// Create a user with valid orgId (REFERENCE validates this)
128+
const user = await db.users.create({
129+
email: 'alice@acme.com',
130+
name: 'Alice',
131+
orgId: org._id, // Must reference existing organization
132+
});
133+
console.log('Created user:', user.name);
134+
135+
// Try to create user with invalid orgId - will throw error
136+
try {
137+
const { ObjectId } = await import('mongodb');
138+
await db.users.create({
139+
email: 'invalid@example.com',
140+
name: 'Invalid User',
141+
orgId: new ObjectId(), // Non-existent org
142+
});
143+
} catch (err) {
144+
console.log('✓ Reference validation caught invalid orgId');
145+
}
146+
147+
// 2. LOOKUP POPULATION
148+
console.log('\n=== LOOKUP Population ===');
149+
150+
// Create a post
151+
const post = await db.posts.create({
152+
title: 'My First Post',
153+
content: 'Hello, World!',
154+
authorId: user._id,
155+
});
156+
157+
// Fetch post and populate author
158+
const foundPosts = await db.posts.findMany({ _id: post._id });
159+
const postsWithAuthor = await db.posts.populate(foundPosts, 'authorData');
160+
161+
console.log('Post:', postsWithAuthor[0].title);
162+
console.log('Author:', postsWithAuthor[0].authorData.name);
163+
console.log('Author Email:', postsWithAuthor[0].authorData.email);
164+
165+
// 3. MULTIPLE POPULATIONS
166+
console.log('\n=== Multiple Populations ===');
167+
168+
// Create some comments
169+
await db.comments.create({
170+
postId: post._id,
171+
authorId: user._id,
172+
content: 'Great post!',
173+
});
174+
175+
// Fetch comments and populate both post and author
176+
const foundComments = await db.comments.findMany({});
177+
const populatedComments = await db.comments.populate(foundComments, [
178+
'post',
179+
'author',
180+
]);
181+
182+
console.log('Comment:', populatedComments[0].content);
183+
console.log('On post:', populatedComments[0].post?.title);
184+
console.log('By:', populatedComments[0].author?.name);
185+
186+
// 4. NESTED POPULATION (manually)
187+
console.log('\n=== Nested Population ===');
188+
189+
// Get posts with authors
190+
const allPosts = await db.posts.findMany({});
191+
const postsWithAuthors = await db.posts.populate(allPosts, 'authorData');
192+
193+
// For each author, populate their organization
194+
for (const postWithAuthor of postsWithAuthors) {
195+
const author = postWithAuthor.authorData;
196+
if (author) {
197+
const usersArray = [author];
198+
const usersWithOrg = await db.users.populate(usersArray, 'organizationData');
199+
postWithAuthor.authorData = usersWithOrg[0];
200+
}
201+
}
202+
203+
console.log('Post:', postsWithAuthors[0].title);
204+
console.log('Author:', postsWithAuthors[0].authorData?.name);
205+
console.log('Org:', postsWithAuthors[0].authorData?.organizationData?.name);
206+
} finally {
207+
await orm.close();
208+
}
209+
}
210+
211+
// Run example if called directly
212+
if (require.main === module) {
213+
relationsExample()
214+
.then(() => {
215+
console.log('\n✓ Relations example completed!');
216+
process.exit(0);
217+
})
218+
.catch((err) => {
219+
console.error('Error:', err);
220+
process.exit(1);
221+
});
222+
}

packages/mizzle-orm/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
// Core exports
7-
export { createMongoOrm } from './orm/orm';
7+
export { createMongoOrm, defineCollections } from './orm/orm';
88
export { mongoCollection } from './collection/collection';
99

1010
// Field factory functions

packages/mizzle-orm/src/orm/orm.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ import { ObjectId } from 'mongodb';
99
import { nanoid } from 'nanoid';
1010
import { CollectionFacade } from '../query/collection-facade';
1111

12+
/**
13+
* Helper function to create properly typed collections object.
14+
* This preserves exact types for perfect type inference.
15+
*
16+
* @example
17+
* ```ts
18+
* const collections = defineCollections({ users, posts });
19+
* const orm = await createMongoOrm({
20+
* uri: process.env.MONGO_URI!,
21+
* dbName: 'myapp',
22+
* collections,
23+
* });
24+
* ```
25+
*/
26+
export function defineCollections<T extends Record<string, CollectionDefinition<any>>>(
27+
collections: T,
28+
): T {
29+
return collections;
30+
}
31+
1232
/**
1333
* Create a Mizzle ORM instance
1434
*
@@ -28,27 +48,24 @@ import { CollectionFacade } from '../query/collection-facade';
2848
* const user = await db.users.findOne({ email: 'alice@example.com' });
2949
* ```
3050
*/
31-
export async function createMongoOrm<TCollections extends CollectionDefinition[]>(
32-
config: OrmConfig & { collections: TCollections },
33-
): Promise<MongoOrm<CollectionsToRecord<TCollections>>> {
51+
export async function createMongoOrm<TCollections extends Record<string, CollectionDefinition<any>>>(
52+
config: OrmConfig<TCollections>,
53+
): Promise<MongoOrm<TCollections>> {
3454
// Connect to MongoDB
3555
const client = new MongoClient(config.uri, config.clientOptions);
3656
await client.connect();
3757

3858
// Get database instance
3959
const db = client.db(config.dbName);
4060

41-
// Build collection registry
61+
// Build collection registry from the collections object
4262
const collectionRegistry = new Map<string, CollectionDefinition>();
43-
for (const collectionDef of config.collections) {
63+
for (const [_, collectionDef] of Object.entries(config.collections)) {
4464
collectionRegistry.set(collectionDef._meta.name, collectionDef);
4565
}
4666

47-
// Build collections object for type-safe access
48-
const collections = {} as CollectionsToRecord<TCollections>;
49-
for (const collectionDef of config.collections) {
50-
(collections as any)[collectionDef._meta.name] = collectionDef;
51-
}
67+
// Collections are already in the right format
68+
const collections = config.collections;
5269

5370
/**
5471
* Create a context object
@@ -81,7 +98,7 @@ export async function createMongoOrm<TCollections extends CollectionDefinition[]
8198
/**
8299
* Get a context-bound database facade
83100
*/
84-
function withContext(ctx: OrmContext): DbFacade<CollectionsToRecord<TCollections>> {
101+
function withContext(ctx: OrmContext): DbFacade<TCollections> {
85102
// Create a proxy that dynamically creates collection facades
86103
return new Proxy({} as any, {
87104
get(_target, collectionName: string) {
@@ -93,7 +110,7 @@ export async function createMongoOrm<TCollections extends CollectionDefinition[]
93110
// Create and return a CollectionFacade for this collection
94111
return new CollectionFacade(db, collectionDef, ctx);
95112
},
96-
}) as DbFacade<CollectionsToRecord<TCollections>>;
113+
}) as DbFacade<TCollections>;
97114
}
98115

99116
/**
@@ -138,9 +155,3 @@ export async function createMongoOrm<TCollections extends CollectionDefinition[]
138155
};
139156
}
140157

141-
/**
142-
* Helper type to convert array of collection definitions to a record
143-
*/
144-
type CollectionsToRecord<T extends CollectionDefinition[]> = {
145-
[K in T[number]['_meta']['name']]: Extract<T[number], { _meta: { name: K } }>;
146-
};

0 commit comments

Comments
 (0)