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
7 changes: 7 additions & 0 deletions .changeset/dry-beans-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"monarch-orm": minor
---

Relations now use typed field references instead of plain strings for `from` and `to` fields.

Relations also support default population options via `.options()`, which apply whenever a relation is populated with `true` and can be overridden per query.
7 changes: 7 additions & 0 deletions .changeset/purple-pens-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"monarch-orm": minor
---

The `many` relation now supports all field type combinations: single→single, single→array, array→single, and array→array.

The `refs` relation has been removed. Use `many` with an array `from` field instead.
5 changes: 5 additions & 0 deletions .changeset/witty-needles-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"monarch-orm": minor
---

Bumped minimum required MongoDB driver version to `>= 7.0.0`.
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
*.md
*.md
package.json
pnpm-lock.yaml
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"js/ts.tsdk.path": "node_modules/typescript/lib"
}
110 changes: 91 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Type-safe MongoDB collections, schema parsing, relations, and query helpers for

- **Strongly Typed:** Infer schema inputs and outputs for queries and collection methods.
- **Flexible Schemas:** Use transforms, defaults, validation, virtuals, renames, and default omit rules.
- **Typed Relations:** Define one, many, and refs relations with typed population support.
- **Typed Relations:** Define one and many relations with typed population support.
- **Familiar MongoDB Access:** Use typed query methods, operators, aggregation, and raw collection access.
- **Collection Initialization:** Automatically initialize collections with indexes and JSON Schema validation, or do it manually.

Expand Down Expand Up @@ -111,39 +111,111 @@ Use `withRelations()` on a schemas object to define typed relations.
```ts
const schemas = defineSchemas({ userSchema, postSchema });

const schemasWithRelations = schemas.withRelations((s) => ({
const schemasWithRelations = schemas.withRelations((r) => ({
users: {
posts: s.users.$many.posts({ from: "_id", to: "authorId" }),
posts: r.many.posts({ from: r.users._id, to: r.posts.authorId }),
},
posts: {
author: s.posts.$one.users({ from: "authorId", to: "_id" }),
contributors: s.posts.$refs.users({ from: "contributorIds", to: "_id" }),
author: r.one.users({ from: r.posts.authorId, to: r.users._id }),
contributors: r.many.users({ from: r.posts.contributorIds, to: r.users._id }),
},
}));
```

#### One relations

Use `$one` when a local field points to a single document in another collection.
Use `one` when a single local field points to a single document in another collection.

```ts
author: s.posts.$one.users({ from: "authorId", to: "_id" })
const userSchema = createSchema("users", { name: string() });
const postSchema = createSchema("posts", {
title: string(),
authorId: objectId(),
});

const schemas = defineSchemas({ userSchema, postSchema });

schemas.withRelations((r) => ({
posts: {
author: r.one.users({ from: r.posts.authorId, to: r.users._id }),
},
}));
```

#### Many relations

Use `$many` when one document relates to many documents in another collection by matching a local field against a foreign field.
Use `many` when one document relates to many documents in another collection. The `from` and `to` fields can each be a single value or an array, so you can model different relation patterns:

- **single → single** — a foreign key on the target side (`user._id` → `post.authorId`)
- **single → array** — the target embeds a list of references (`post._id` → `tag.postIds`)
- **array → single** — the source embeds a list of references (`post.tagIds` → `tag._id`)
- **array → array** — match documents that share any element between two arrays (`post.tagIds` → `event.tagIds`)

```ts
posts: s.users.$many.posts({ from: "_id", to: "authorId" })
const userSchema = createSchema("users", { name: string() });
const postSchema = createSchema("posts", {
title: string(),
authorId: objectId(),
tagIds: array(objectId()).default([]),
});
const tagSchema = createSchema("tags", {
name: string(),
postIds: array(objectId()).default([]),
});
const eventSchema = createSchema("events", {
name: string(),
tagIds: array(objectId()).default([]),
});

const schemas = defineSchemas({ userSchema, postSchema, tagSchema, eventSchema });

schemas.withRelations((r) => ({
users: {
// single → single: all posts where post.authorId equals user._id
posts: r.many.posts({ from: r.users._id, to: r.posts.authorId }),
},
posts: {
// single → array: all tags where post._id appears in tag.postIds
taggedBy: r.many.tags({ from: r.posts._id, to: r.tags.postIds }),
// array → single: all tags where tag._id appears in post.tagIds
tags: r.many.tags({ from: r.posts.tagIds, to: r.tags._id }),
// array → array: all events where event.tagIds shares any value with post.tagIds
relatedEvents: r.many.events({ from: r.posts.tagIds, to: r.events.tagIds }),
},
}));
```

#### Refs relations
#### Default relation options

Call `.options()` on any relation to set default population behavior. These defaults apply whenever the relation is populated with `true`. They can always be overridden by passing explicit options at query time.

```ts
const schemasWithRelations = schemas.withRelations((r) => ({
users: {
posts: r.many.posts({ from: r.users._id, to: r.posts.authorId }).options({
sort: { createdAt: -1 },
limit: 10,
select: { title: true, createdAt: true },
}),
},
posts: {
author: r.one.users({ from: r.posts.authorId, to: r.users._id }).options({
omit: { passwordHash: true },
}),
},
}));
```

Use `$refs` when a local array field stores multiple references to another collection.
With these defaults in place, populating with `true` applies them automatically:

```ts
contributors: s.posts.$refs.users({ from: "contributorIds", to: "_id" })
// applies sort, limit, and select from the relation definition
const users = await db.collections.users.find().populate({ posts: true });

// override the defaults for this query
const users2 = await db.collections.users.find().populate({
posts: { sort: { title: 1 }, limit: 5 },
});
```

### Schema Groups
Expand All @@ -159,9 +231,9 @@ const userSchema = createSchema("users", {
tutorId: objectId().optional(),
});

const userGroup = defineSchemas({ userSchema }).withRelations((s) => ({
const userGroup = defineSchemas({ userSchema }).withRelations((r) => ({
users: {
tutor: s.users.$one.users({ from: "tutorId", to: "_id" }),
tutor: r.one.users({ from: r.users.tutorId, to: r.users._id }),
},
}));

Expand All @@ -175,9 +247,9 @@ const categorySchema = createSchema("categories", {
parentId: objectId().optional(),
});

const contentGroup = defineSchemas({ postSchema, categorySchema }).withRelations((s) => ({
const contentGroup = defineSchemas({ postSchema, categorySchema }).withRelations((r) => ({
categories: {
parent: s.categories.$one.categories({ from: "parentId", to: "_id" }),
parent: r.one.categories({ from: r.categories.parentId, to: r.categories._id }),
},
}));

Expand All @@ -187,12 +259,12 @@ const schemas = mergeSchemas(userGroup, contentGroup);
You can also add cross-group relations after merging:

```ts
const schemasWithCrossGroupRelations = schemas.withRelations((s) => ({
const schemasWithCrossGroupRelations = schemas.withRelations((r) => ({
users: {
posts: s.users.$many.posts({ from: "_id", to: "authorId" }),
posts: r.many.posts({ from: r.users._id, to: r.posts.authorId }),
},
posts: {
author: s.posts.$one.users({ from: "authorId", to: "_id" }),
author: r.one.users({ from: r.posts.authorId, to: r.users._id }),
},
}));
```
Expand Down
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,37 @@
"exports": {
".": {
"require": {
"types": "./dist/index.d.cts",
"types": "./dist/index.d.ts",
"default": "./dist/index.cjs"
},
"import": {
"types": "./dist/index.d.mts",
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
},
"./types": {
"require": {
"types": "./dist/types/index.d.cts",
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.cjs"
},
"import": {
"types": "./dist/types/index.d.mts",
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.mjs"
}
},
"./operators": {
"require": {
"types": "./dist/operators/index.d.cts",
"types": "./dist/operators/index.d.ts",
"default": "./dist/operators/index.cjs"
},
"import": {
"types": "./dist/operators/index.d.mts",
"types": "./dist/operators/index.d.ts",
"default": "./dist/operators/index.mjs"
}
}
},
"scripts": {
"build": "tsdown",
"build": "tsdown && tsc",
"release": "pnpm run build && changeset publish",
"check": "tsc && pnpm run format",
"format": "prettier . --check",
Expand Down Expand Up @@ -74,11 +74,11 @@
},
"homepage": "https://github.com/monarch-orm/monarch#readme",
"peerDependencies": {
"mongodb": ">= 6.0.0"
"mongodb": ">= 7.0.0"
},
"devDependencies": {
"@changesets/cli": "^2.29.8",
"@types/node": "^20.19.27",
"@types/node": "^25.5.2",
"@vitest/coverage-v8": "^4.0.16",
"mongodb-memory-server": "^11.0.1",
"prettier": "^3.7.4",
Expand All @@ -87,4 +87,4 @@
"typescript": "^5.9.3",
"vitest": "^4.0.16"
}
}
}
Loading
Loading