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

Replace `createRelations` with `schema.withRelations` and support merging multiple relations per schema
5 changes: 5 additions & 0 deletions .changeset/small-aliens-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"monarch-orm": minor
---

Support splitting schemas by adding `defineSchemas` and `mergeSchema` functions which requires a `Schemas` class as argument to `createDatabase`
4 changes: 3 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ tests
coverage
pnpm-lock.yaml
tsconfig.json
.prettierignore
prettier.config.mts
vitest.config.mts
tsup.config.ts
tsdown.config.mts
CHANGELOG.md
README.md
CONTRIBUTING.md
Expand Down
43 changes: 18 additions & 25 deletions src/collection/utils/population.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ function createPopulationVarGenerator() {
return (relation: AnyRelation): string => {
const key = [
relation.relation,
relation.schema.name,
relation.schemaField,
relation.target.name,
relation.targetField,
relation.schema.schema.name,
relation.schema.field,
relation.target.schema.name,
relation.target.field,
].join("_");

const base = `mn_${hashString(key)}`;
Expand Down Expand Up @@ -61,21 +61,14 @@ export function addPopulations(
throw new MonarchError(`No relations found for schema '${opts.schema.name}'`);
}

// Validate relation target exists
if (!relation.target) {
throw new MonarchError(`Target schema not found for relation '${field}' in schema '${opts.schema.name}'
This might happen if relations were declared before the target schema was initialized.
Ensure all schemas are initialized before defining their relations.`);
}

const _options = options === true ? {} : (options as PopulationOptions<any, any, any>);

// get population projection or fallback to schema omit projection
const projection =
makePopulationProjection(_options) ?? makeProjection("omit", relation.target.options?.omit ?? {});
makePopulationProjection(_options) ?? makeProjection("omit", relation.target.schema.options?.omit ?? {});

// ensure required fields are in projection
const extras = addExtraInputsToProjection(projection, relation.target.options?.virtuals, _options.populate);
const extras = addExtraInputsToProjection(projection, relation.target.schema.options?.virtuals, _options.populate);

// create pipeline for this poulation
const populationPipeline: Lookup<any>["$lookup"]["pipeline"] = [];
Expand All @@ -91,7 +84,7 @@ export function addPopulations(
? addPopulations(populationPipeline, {
population: _options.populate,
relations: opts.relations,
schema: relation.target,
schema: relation.target.schema,
nextVar,
})
: undefined;
Expand Down Expand Up @@ -136,11 +129,11 @@ export function expandPopulations(opts: {
populations: population.populations,
projection: population.projection,
extras: population.extras,
schema: population.relation.target,
schema: population.relation.target.schema,
doc,
});
}
return Schema.decode(population.relation.target, doc, population.projection, population.extras);
return Schema.decode(population.relation.target.schema, doc, population.projection, population.extras);
});
delete populatedDoc[population.fieldVariable];
}
Expand All @@ -159,15 +152,15 @@ function addPopulationPipeline(
},
): { fieldVariable: string } {
const { relation } = opts;
const collectionName = relation.target.name;
const collectionName = relation.target.schema.name;
const fieldVariable = opts.nextVar(relation);

if (relation.relation === "many") {
if (relation.relation === "refs") {
pipeline.push({
$lookup: {
from: collectionName,
localField: relation.schemaField,
foreignField: relation.targetField,
localField: relation.schema.field,
foreignField: relation.target.field,
as: fieldVariable,
pipeline: opts.populationPipeline,
},
Expand All @@ -185,20 +178,20 @@ function addPopulationPipeline(
});
}

if (relation.relation === "ref") {
if (relation.relation === "many") {
pipeline.push({
$lookup: {
from: collectionName,
let: {
[fieldVariable]: `$${relation.schemaField}`,
[fieldVariable]: `$${relation.schema.field}`,
},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $ne: [`$$${fieldVariable}`, null] },
{ $eq: [`$${relation.targetField}`, `$$${fieldVariable}`] },
{ $eq: [`$${relation.target.field}`, `$$${fieldVariable}`] },
],
},
},
Expand All @@ -215,13 +208,13 @@ function addPopulationPipeline(
$lookup: {
from: collectionName,
let: {
[fieldVariable]: `$${relation.schemaField}`,
[fieldVariable]: `$${relation.schema.field}`,
},
pipeline: [
{
$match: {
$expr: {
$eq: [`$${relation.targetField}`, `$$${fieldVariable}`],
$eq: [`$${relation.target.field}`, `$$${fieldVariable}`],
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions src/collection/utils/projection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Population, PopulationOptions } from "../../relations/type-helpers";
import type { Virtual } from "../../schema/virtuals";
import type { AnyVirtual } from "../../schema/virtuals";
import type { BoolProjection, Projection } from "../types/query-options";

export function makeProjection<T>(type: "omit" | "select", projection: BoolProjection<T>) {
Expand Down Expand Up @@ -34,7 +34,7 @@ export function detectProjection<T>(projection: Projection<T>) {

export function addExtraInputsToProjection<T>(
projection: Projection<T>,
virtuals: Record<string, Virtual<any, any, any>> | undefined,
virtuals: Record<string, AnyVirtual> | undefined,
populations?: Population<any, any>,
): string[] | null {
const { isProjected, type } = detectProjection(projection);
Expand Down
88 changes: 22 additions & 66 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import { MongoClient, type Db, type MongoClientOptions } from "mongodb";
import { version } from "../package.json";
import { Collection } from "./collection/collection";
import type { BoolProjection, WithProjection } from "./collection/types/query-options";
import { MonarchError } from "./errors";
import { Relations, type AnyRelations } from "./relations/relations";
import { type AnyRelations } from "./relations/relations";
import type { InferRelationObjectPopulation, Population, PopulationBaseOptions } from "./relations/type-helpers";
import type { AnySchema } from "./schema/schema";
import type { AnySchema, Schemas } from "./schema/schema";
import type { InferSchemaInput, InferSchemaOmit, InferSchemaOutput } from "./schema/type-helpers";
import type { ExtractObject, IdFirst, Merge, Pretty } from "./utils/type-helpers";
import type { IdFirst, Merge, Pretty } from "./utils/type-helpers";

/**
* Creates a MongoDB client configured with Monarch ORM driver information.
Expand All @@ -24,17 +23,15 @@ export function createClient(uri: string, options: MongoClientOptions = {}) {
}

/**
* Manages database collections and relations for MongoDB operations.
*
* Database collections and relations for MongoDB operations.
*/
export class Database<
TSchemas extends Record<string, AnySchema> = {},
TRelations extends Record<string, Relations<any, any>> = {},
> {
/** Relation definitions for each schema */
public relations: DbRelations<TRelations>;
/** Collection instances for each schema */
public collections: DbCollections<TSchemas, DbRelations<TRelations>>;
export class Database<TSchemas extends Schemas<any, any>> {
/** Schema definitions*/
public schemas: TSchemas["schemas"];
/** Relation definitions*/
public relations: TSchemas["relations"];
/** Collection instances */
public collections: DbCollections<TSchemas["schemas"], TSchemas["relations"]>;

/**
* Creates a Database instance with collections and relations.
Expand All @@ -47,36 +44,14 @@ export class Database<
constructor(
public db: Db,
schemas: TSchemas,
relations: TRelations,
) {
const _relations = {} as DbRelations<TRelations>;
const _seenRelations = new Set<string>();
for (const relation of Object.values(relations)) {
if (_seenRelations.has(relation.name)) {
throw new MonarchError(`Relations for schema '${relation.name}' already exists.`);
}
_seenRelations.add(relation.name);
_relations[relation.name as keyof typeof _relations] = {
..._relations[relation.name as keyof typeof _relations],
...relation.relations,
};
}
this.relations = _relations;
this.schemas = schemas.schemas;
this.relations = schemas.relations;
this.collections = {} as typeof this.collections;

const _collections = {} as DbCollections<TSchemas, DbRelations<TRelations>>;
const _seenCollection = new Set<string>();
for (const [key, schema] of Object.entries(schemas)) {
if (_seenCollection.has(schema.name)) {
throw new MonarchError(`Schema with name '${schema.name}' already exists.`);
}
_seenCollection.add(schema.name);
_collections[key as keyof typeof _collections] = new Collection(
db,
schema,
this.relations,
) as unknown as (typeof _collections)[keyof typeof _collections];
for (const [key, schema] of Object.entries(this.schemas as Record<string, AnySchema>)) {
this.collections[key as keyof typeof this.collections] = new Collection(db, schema, this.relations);
}
this.collections = _collections;

this.use = this.use.bind(this);
this.listCollections = this.listCollections.bind(this);
Expand All @@ -88,8 +63,8 @@ export class Database<
* @param schema - Schema definition
* @returns Collection instance for the schema
*/
public use<S extends AnySchema>(schema: S): Collection<S, DbRelations<TRelations>> {
return new Collection(this.db, schema, this.relations[schema.name as keyof DbRelations<TRelations>]);
public use<S extends AnySchema>(schema: S): Collection<S, TSchemas["relations"]> {
return new Collection(this.db, schema, this.relations[schema.name]);
}

/**
Expand All @@ -109,46 +84,27 @@ export class Database<
* @param schemas - Object containing schema and relation definitions
* @returns Database instance with initialized collections and relations
*/
export function createDatabase<T extends Record<string, AnySchema | Relations<any, any>>>(
db: Db,
schemas: T,
): Database<ExtractObject<T, AnySchema>, ExtractObject<T, Relations<any, any>>> {
const collections = {} as ExtractObject<T, AnySchema>;
const relations = {} as ExtractObject<T, Relations<any, any>>;

for (const [key, schema] of Object.entries(schemas)) {
if (schema instanceof Relations) {
relations[key as keyof typeof relations] = schema as (typeof relations)[keyof typeof relations];
} else {
collections[key as keyof typeof collections] = schema as (typeof collections)[keyof typeof collections];
}
}

return new Database(db, collections, relations);
export function createDatabase<T extends Schemas<any, any>>(db: Db, schemas: T): Database<T> {
return new Database(db, schemas);
}

type DbCollections<TSchemas extends Record<string, AnySchema>, TRelations extends Record<string, AnyRelations>> = {
[K in keyof TSchemas]: Collection<TSchemas[K], TRelations>;
} & {};
type DbRelations<TRelations extends Record<string, Relations<any, any>>> = {
[K in keyof TRelations as TRelations[K]["name"]]: TRelations[K]["relations"];
} & {};

/**
* Infers the input type for a collection in a database.
*
*/
export type InferInput<
TDatabase extends Database<any, any>,
TDatabase extends Database<any>,
TCollection extends keyof TDatabase["collections"],
> = InferSchemaInput<TDatabase["collections"][TCollection]["schema"]>;

/**
* Infers the output type for a collection query with projection and population options.
*
*/
export type InferOutput<
TDatabase extends Database<any, any>,
TDatabase extends Database<any>,
TCollection extends keyof TDatabase["collections"],
TOptions extends PopulationBaseOptions<
InferSchemaOutput<TDatabase["collections"][TCollection]["schema"]>,
Expand Down
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export { ObjectId } from "mongodb";
export { Collection } from "./collection/collection";
export { createClient, createDatabase, Database, type InferInput, type InferOutput } from "./database";
export { MonarchError } from "./errors";
export { createRelations, Relations, type Relation } from "./relations/relations";
export { createSchema, Schema } from "./schema/schema";
export { createSchema, defineSchemas, mergeSchemas, Schema, Schemas } from "./schema/schema";
export { type InferSchemaInput, type InferSchemaOutput } from "./schema/type-helpers";
export { virtual, type Virtual } from "./schema/virtuals";
export { toObjectId } from "./utils/objectId";
Expand Down
Loading
Loading