diff --git a/.changeset/quiet-facts-kneel.md b/.changeset/quiet-facts-kneel.md new file mode 100644 index 0000000..b91a5a3 --- /dev/null +++ b/.changeset/quiet-facts-kneel.md @@ -0,0 +1,5 @@ +--- +"monarch-orm": minor +--- + +Replace `createRelations` with `schema.withRelations` and support merging multiple relations per schema diff --git a/.changeset/small-aliens-return.md b/.changeset/small-aliens-return.md new file mode 100644 index 0000000..ca28a6e --- /dev/null +++ b/.changeset/small-aliens-return.md @@ -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` diff --git a/.npmignore b/.npmignore index 3193283..ac1d2f8 100644 --- a/.npmignore +++ b/.npmignore @@ -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 diff --git a/src/collection/utils/population.ts b/src/collection/utils/population.ts index 8d61a12..39eb752 100644 --- a/src/collection/utils/population.ts +++ b/src/collection/utils/population.ts @@ -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)}`; @@ -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); // 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["$lookup"]["pipeline"] = []; @@ -91,7 +84,7 @@ export function addPopulations( ? addPopulations(populationPipeline, { population: _options.populate, relations: opts.relations, - schema: relation.target, + schema: relation.target.schema, nextVar, }) : undefined; @@ -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]; } @@ -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, }, @@ -185,12 +178,12 @@ function addPopulationPipeline( }); } - if (relation.relation === "ref") { + if (relation.relation === "many") { pipeline.push({ $lookup: { from: collectionName, let: { - [fieldVariable]: `$${relation.schemaField}`, + [fieldVariable]: `$${relation.schema.field}`, }, pipeline: [ { @@ -198,7 +191,7 @@ function addPopulationPipeline( $expr: { $and: [ { $ne: [`$$${fieldVariable}`, null] }, - { $eq: [`$${relation.targetField}`, `$$${fieldVariable}`] }, + { $eq: [`$${relation.target.field}`, `$$${fieldVariable}`] }, ], }, }, @@ -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}`], }, }, }, diff --git a/src/collection/utils/projection.ts b/src/collection/utils/projection.ts index c001641..62d8480 100644 --- a/src/collection/utils/projection.ts +++ b/src/collection/utils/projection.ts @@ -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(type: "omit" | "select", projection: BoolProjection) { @@ -34,7 +34,7 @@ export function detectProjection(projection: Projection) { export function addExtraInputsToProjection( projection: Projection, - virtuals: Record> | undefined, + virtuals: Record | undefined, populations?: Population, ): string[] | null { const { isProjected, type } = detectProjection(projection); diff --git a/src/database.ts b/src/database.ts index 53d5e9f..eb53c87 100644 --- a/src/database.ts +++ b/src/database.ts @@ -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. @@ -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 = {}, - TRelations extends Record> = {}, -> { - /** Relation definitions for each schema */ - public relations: DbRelations; - /** Collection instances for each schema */ - public collections: DbCollections>; +export class Database> { + /** Schema definitions*/ + public schemas: TSchemas["schemas"]; + /** Relation definitions*/ + public relations: TSchemas["relations"]; + /** Collection instances */ + public collections: DbCollections; /** * Creates a Database instance with collections and relations. @@ -47,36 +44,14 @@ export class Database< constructor( public db: Db, schemas: TSchemas, - relations: TRelations, ) { - const _relations = {} as DbRelations; - const _seenRelations = new Set(); - 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>; - const _seenCollection = new Set(); - 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)) { + 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); @@ -88,8 +63,8 @@ export class Database< * @param schema - Schema definition * @returns Collection instance for the schema */ - public use(schema: S): Collection> { - return new Collection(this.db, schema, this.relations[schema.name as keyof DbRelations]); + public use(schema: S): Collection { + return new Collection(this.db, schema, this.relations[schema.name]); } /** @@ -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>>( - db: Db, - schemas: T, -): Database, ExtractObject>> { - const collections = {} as ExtractObject; - const relations = {} as ExtractObject>; - - 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>(db: Db, schemas: T): Database { + return new Database(db, schemas); } type DbCollections, TRelations extends Record> = { [K in keyof TSchemas]: Collection; } & {}; -type DbRelations>> = { - [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, + TDatabase extends Database, TCollection extends keyof TDatabase["collections"], > = InferSchemaInput; /** * Infers the output type for a collection query with projection and population options. - * */ export type InferOutput< - TDatabase extends Database, + TDatabase extends Database, TCollection extends keyof TDatabase["collections"], TOptions extends PopulationBaseOptions< InferSchemaOutput, diff --git a/src/index.ts b/src/index.ts index 6c6ba69..1bd7cb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; diff --git a/src/relations/relations.ts b/src/relations/relations.ts index e60c7f3..321db37 100644 --- a/src/relations/relations.ts +++ b/src/relations/relations.ts @@ -1,139 +1,107 @@ -import type { AnySchema } from "../schema/schema"; +import { type AnySchema } from "../schema/schema"; +import type { MergeN1 } from "../utils/type-helpers"; import type { SchemaRelatableField } from "./type-helpers"; -export type AnyRelation = Relation<"one" | "many" | "ref", any, any, any, any>; +export type AnyRelation = Relation; to: RelationField }>; /** * Defines a relationship between two schemas. * */ export type Relation< - TRelation extends "one" | "many" | "ref", - TSchema extends AnySchema, - TSchemaField extends SchemaRelatableField, - TTarget extends AnySchema, - TTargetField extends SchemaRelatableField, + TRelation extends "one" | "many" | "refs", + TOptions extends { from: RelationField; to: RelationField }, > = { relation: TRelation; - schema: TSchema; - schemaField: TSchemaField; - target: TTarget; - targetField: TTargetField; + schema: TOptions["from"]; + target: TOptions["to"]; }; -export type AnyRelations = Record; - -/** - * Container for schema relationships. - * - */ -export class Relations { - /** - * Creates a Relations instance. - * - * @param name - Schema name - * @param relations - Relation definitions - */ +export class RelationField { constructor( - public name: TName, - public relations: TRelations, + public schema: TSchema, + public field: TField, ) {} } -/** - * Creates relationship definitions for a schema. - * - * Provides three relation types: - * - `one`: One-to-one relationship - * - `many`: One-to-many relationship - * - `ref`: Reference relationship - * - * @param schema - Source schema - * @param relations - Function that defines relations using relation builders - * @returns Relations instance for the schema - */ -export function createRelations>( - schema: TSchema, - relations: (relation: CreateRelation) => TRelations, -) { - return new Relations( - schema.name, - relations({ - one: (target, options) => ({ - relation: "one", - schema, - schemaField: options.field, - target, - targetField: options.references, - }), - many: (target, options) => ({ - relation: "many", - schema, - schemaField: options.field, - target, - targetField: options.references, - }), - ref: (target, options) => ({ - relation: "ref", - schema, - schemaField: options.field, - target, - targetField: options.references, - }), - }), - ); +export type SchemasRelations> = { + [K in keyof TSchemas]?: Record>; +}; + +export type AnyRelations = Record; + +export function mergeRelations< + TSchemas extends Record, + TRelations extends Record | undefined>, + T extends SchemasRelations, +>(schemas: TSchemas, relations: TRelations, fn: RelationsFn) { + const input = new Proxy({} as SchemaRelations, { + get: (_target, schemaKey: string) => { + return new Proxy({} as SchemaRelations[string], { + get: (_target, relation: "$one" | "$many" | "$refs") => { + return new Proxy({} as Record>, { + get: (_target, targetKey: string) => { + return (options: Parameters>[0]): AnyRelation => ({ + relation: relation.slice(1), + schema: new RelationField(schemas[schemaKey]!, options.from), + target: new RelationField(schemas[targetKey]!, options.to), + }); + }, + }); + }, + }); + }, + }); + const output = fn(input); + + const mergedRelations: Record = { ...relations }; + for (const [key, relations] of Object.entries(output)) { + if (key in mergedRelations) { + mergedRelations[key] = { ...mergedRelations[key], ...relations }; + } else { + mergedRelations[key] = relations; + } + } + + return mergedRelations as MergeN1; } -/** - * Relation builder interface with one, many, and ref methods. - * - */ -type CreateRelation = { - /** - * Creates a one-to-one relationship. - * - * @param target - Target schema - * @param options - Relation options - * @param options.field - Field in source schema containing the reference - * @param options.references - Field in target schema being referenced - * @returns One-to-one relation definition - */ - one: One; - /** - * Creates a one-to-many relationship. - * - * @param target - Target schema - * @param options - Relation options - * @param options.field - Field in source schema containing the reference - * @param options.references - Field in target schema being referenced - * @returns One-to-many relation definition - */ - many: Many; - /** - * Creates a reference relationship. - * - * @param target - Target schema - * @param options - Relation options - * @param options.field - Field in source schema containing the reference - * @param options.references - Field in target schema being referenced - * @returns Reference relation definition - */ - ref: Ref; +export type RelationsFn< + TSchemas extends Record, + TRelations extends Record | undefined>, +> = (s: SchemaRelations) => keyof TRelations extends keyof TSchemas ? TRelations : never; + +type SchemaRelations> = { + [SchemaKey in keyof TSchemas]: { + /** Defines a one-to-one relationship. */ + $one: SchemaOne; + /** Defines a one-to-many relationship. */ + $many: SchemaMany; + /** Defines an embedded relationship. */ + $refs: SchemaRefs; + }; +}; + +type SchemaOne> = { + [TargetK in keyof TSchemas]: One; }; -type One = RelationFactory<"one", TSchema>; -type Many = RelationFactory<"many", TSchema>; -type Ref = RelationFactory<"ref", TSchema>; +type SchemaMany> = { + [TargetK in keyof TSchemas]: Many; +}; +type SchemaRefs> = { + [TargetK in keyof TSchemas]: Refs; +}; + +type One = RelationFn<"one", TSchema, TTarget>; +type Many = RelationFn<"many", TSchema, TTarget>; +type Refs = RelationFn<"refs", TSchema, TTarget>; -type RelationFactory = < - TTarget extends AnySchema, - TSchemaField extends SchemaRelatableField, - TTargetField extends SchemaRelatableField, ->( - target: TTarget, - options: { - /** Field in source schema containing the reference */ - field: TSchemaField; - /** Field in target schema being referenced */ - references: TTargetField; - }, -) => Relation; +type RelationFn = < + const TSchemaField extends SchemaRelatableField, + const TTargetField extends SchemaRelatableField, +>(options: { + /** Local field defined in source schema */ + from: TSchemaField; + /** Foreign field defined in target schema */ + to: TTargetField; +}) => Relation; to: RelationField }>; diff --git a/src/relations/type-helpers.ts b/src/relations/type-helpers.ts index 754217c..8bd025c 100644 --- a/src/relations/type-helpers.ts +++ b/src/relations/type-helpers.ts @@ -4,13 +4,18 @@ import type { BoolProjection, WithProjection } from "../collection/types/query-o import type { AnySchema } from "../schema/schema"; import type { InferSchemaOmit, InferSchemaOutput, SchemaInputWithId } from "../schema/type-helpers"; import type { ExtractIfArray, Index, Merge, Pretty } from "../utils/type-helpers"; -import type { AnyRelation, AnyRelations, Relation } from "./relations"; +import type { AnyRelation, AnyRelations, Relation, RelationField } from "./relations"; -type ValidRelationFieldType = TRelation extends "many" +type ValidRelationFieldType = TRelation extends "refs" ? Array : string | number | ObjectId; -export type SchemaRelatableField = keyof { - [K in keyof SchemaInputWithId as NonNullable[K]> extends ValidRelationFieldType +export type SchemaRelatableField< + TRelation extends "one" | "many" | "refs" | undefined, + TSchema extends AnySchema, +> = keyof { + [K in keyof SchemaInputWithId as NonNullable< + SchemaInputWithId[K] + > extends ValidRelationFieldType ? K : never]: unknown; }; @@ -40,19 +45,23 @@ export type PopulationOptions< sort?: Sort["$sort"]; } & PopulationBaseOptions; type _RelationPopulationOptions, TRelation extends AnyRelation> = - TRelation extends Relation<"one", any, any, infer TTarget, any> - ? PopulationBaseOptions, TDbRelations, TTarget["name"]> - : TRelation extends Relation<"many", any, any, infer TTarget, any> + TRelation extends Relation<"one", { from: any; to: infer TTarget extends RelationField }> + ? PopulationBaseOptions< + InferRelationPopulation, + TDbRelations, + TTarget["schema"]["name"] + > + : TRelation extends Relation<"many", { from: any; to: infer TTarget extends RelationField }> ? PopulationOptions< ExtractIfArray>, TDbRelations, - TTarget["name"] + TTarget["schema"]["name"] > - : TRelation extends Relation<"ref", any, any, infer TTarget, any> + : TRelation extends Relation<"refs", { from: any; to: infer TTarget extends RelationField }> ? PopulationOptions< ExtractIfArray>, TDbRelations, - TTarget["name"] + TTarget["schema"]["name"] > : never; export type Population, TName extends keyof TDbRelations> = { @@ -85,25 +94,37 @@ export type InferRelationPopulation< TRelation extends AnyRelation, TPopulationOptions extends PopulationOptions | true | undefined, > = - TRelation extends Relation<"one", any, any, infer TTarget, any> + TRelation extends Relation<"one", { from: any; to: infer TTarget extends RelationField }> ? WithNestedPopulate< - WithRelationPopulation, TPopulationOptions, InferSchemaOmit>, + WithRelationPopulation< + InferSchemaOutput, + TPopulationOptions, + InferSchemaOmit + >, TDbRelations, - TTarget["name"], + TTarget["schema"]["name"], TPopulationOptions > | null - : TRelation extends Relation<"many", any, any, infer TTarget, any> + : TRelation extends Relation<"many", { from: any; to: infer TTarget extends RelationField }> ? WithNestedPopulate< - WithRelationPopulation, TPopulationOptions, InferSchemaOmit>, + WithRelationPopulation< + InferSchemaOutput, + TPopulationOptions, + InferSchemaOmit + >, TDbRelations, - TTarget["name"], + TTarget["schema"]["name"], TPopulationOptions >[] - : TRelation extends Relation<"ref", any, any, infer TTarget, any> + : TRelation extends Relation<"refs", { from: any; to: infer TTarget extends RelationField }> ? WithNestedPopulate< - WithRelationPopulation, TPopulationOptions, InferSchemaOmit>, + WithRelationPopulation< + InferSchemaOutput, + TPopulationOptions, + InferSchemaOmit + >, TDbRelations, - TTarget["name"], + TTarget["schema"]["name"], TPopulationOptions >[] : never; diff --git a/src/schema/schema.ts b/src/schema/schema.ts index cb516f4..44d51f3 100644 --- a/src/schema/schema.ts +++ b/src/schema/schema.ts @@ -1,12 +1,13 @@ import type { Projection } from "../collection/types/query-options"; import { detectProjection } from "../collection/utils/projection"; import { MonarchParseError } from "../errors"; +import { mergeRelations, type AnyRelation, type RelationsFn, type SchemasRelations } from "../relations/relations"; import { objectId } from "../types/objectId"; -import { type AnyMonarchType, MonarchType } from "../types/type"; -import type { Pretty, WithOptionalId } from "../utils/type-helpers"; +import { MonarchType, type AnyMonarchType } from "../types/type"; +import type { MergeAll, MergeN1All, Pretty, WithOptionalId } from "../utils/type-helpers"; import type { SchemaIndexes } from "./indexes"; import type { InferSchemaData, InferSchemaInput, InferSchemaOutput, InferSchemaTypes } from "./type-helpers"; -import type { SchemaVirtuals, Virtual } from "./virtuals"; +import type { AnyVirtual, SchemaVirtuals, Virtual } from "./virtuals"; type SchemaOmit> = { [K in keyof WithOptionalId]?: true; @@ -22,7 +23,7 @@ export class Schema< TName extends string, TTypes extends Record, TOmit extends SchemaOmit = {}, - TVirtuals extends Record> = {}, + TVirtuals extends Record = {}, > { /** * Creates a Schema instance. @@ -196,3 +197,63 @@ export function createSchema { return new Schema(name, types, {}); } + +export class Schemas< + TSchemas extends Record, + TRelations extends Record | undefined> = {}, +> { + constructor( + public schemas: TSchemas, + public relations: TRelations, + ) { + this.withRelations = this.withRelations.bind(this); + } + + public withRelations>(fn: RelationsFn) { + const mergedRelations = mergeRelations(this.schemas, this.relations, fn); + return new Schemas(this.schemas, mergedRelations); + } +} + +/** + * Define schemas. + */ +export function defineSchemas = {}>(schemas: TSchemas) { + const mappedSchemas: Record = {}; + + for (const schema of Object.values(schemas)) { + if (mappedSchemas[schema.name]) { + throw new Error(`Schema with name '${schema.name}' already exists.`); + } + mappedSchemas[schema.name] = schema; + } + + return new Schemas, {}>(mappedSchemas as MappedSchemas, {}); +} + +/** + * Merge multiple schema definitions into a single instance, + * merging their schemas and relations. + */ +export function mergeSchemas[]>(...schemas: T) { + let mergedSchemas: Record = {}; + const mergedRelations: Record = {}; + for (const s of schemas) { + mergedSchemas = { ...mergedSchemas, ...s.schemas }; + for (const [key, relations] of Object.entries(s.relations as Record>)) { + if (key in mergedRelations) { + mergedRelations[key] = { ...mergedRelations[key], ...relations }; + } else { + mergedRelations[key] = relations; + } + } + } + return new Schemas( + mergedSchemas as MergeAll<{ [K in keyof T]: T[K]["schemas"] }>, + mergedRelations as MergeN1All<{ [K in keyof T]: T[K]["relations"] }>, + ); +} + +type MappedSchemas> = { + [K in keyof TSchemas as TSchemas[K]["name"]]: TSchemas[K]; +} & {}; diff --git a/src/schema/virtuals.ts b/src/schema/virtuals.ts index e26fb03..fc39809 100644 --- a/src/schema/virtuals.ts +++ b/src/schema/virtuals.ts @@ -6,7 +6,7 @@ export type SchemaVirtuals< TVirtuals extends Record>, > = TVirtuals; -export type InferVirtualOutput>> = { +export type InferVirtualOutput> = { [K in keyof T]: T[K] extends Virtual ? R : never; }; @@ -14,6 +14,8 @@ type Props, P extends keyof T> = { [K in keyof T as K extends P ? K : never]: InferTypeOutput; } & {}; +export type AnyVirtual = Virtual; + /** * Defines a virtual computed field. * diff --git a/src/utils/type-helpers.ts b/src/utils/type-helpers.ts index 8397b0f..57bf58c 100644 --- a/src/utils/type-helpers.ts +++ b/src/utils/type-helpers.ts @@ -1,12 +1,20 @@ import type { ObjectId } from "mongodb"; -export type Merge = Omit & Second; export type Pretty = { [K in keyof T]: T[K] } & {}; +export type Merge = Omit & Second; +export type MergeN1 = Pretty< + Omit & { + [K in keyof Second]: K extends keyof First ? Pretty> : Second[K]; + } +>; +export type MergeAll = T extends [...infer Head, infer Tail] + ? Pretty, Tail>> + : {}; +export type MergeN1All = T extends [...infer Head, infer Tail] + ? Pretty, Tail>> + : {}; export type Index = K extends keyof T ? T[K] : never; export type ExtractIfArray = T extends (infer U)[] ? U : T; -export type ExtractObject, U> = { - [K in keyof T as T[K] extends U ? K : never]: T[K] extends U ? T[K] : never; -} & {}; export type TrueKeys = keyof { [K in keyof T as T[K] extends true ? K : never]: T[K]; }; diff --git a/tests/operators.test.ts b/tests/operators.test.ts index 1d68169..b0a06d5 100644 --- a/tests/operators.test.ts +++ b/tests/operators.test.ts @@ -1,5 +1,5 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createSchema } from "../src"; +import { createDatabase, createSchema, defineSchemas } from "../src"; import { and, eq, gt, gte, inArray, lt, lte, neq, nor, notInArray, or } from "../src/operators"; import { boolean, number, string } from "../src/types"; import { createMockDatabase, mockUsers } from "./mock"; @@ -14,9 +14,12 @@ describe("Query operators", async () => { isVerified: boolean().default(false), }); - const { collections } = createDatabase(client.db(), { - users: UserSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + users: UserSchema, + }), + ); beforeAll(async () => { await client.connect(); diff --git a/tests/query/aggregate.test.ts b/tests/query/aggregate.test.ts index 27b6e4d..8f872cd 100644 --- a/tests/query/aggregate.test.ts +++ b/tests/query/aggregate.test.ts @@ -1,5 +1,5 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createSchema } from "../../src"; +import { createDatabase, createSchema, defineSchemas } from "../../src"; import { boolean, number, string } from "../../src/types"; import { createMockDatabase, mockUsers } from "../mock"; @@ -13,9 +13,12 @@ describe("Aggregation Operations", async () => { isVerified: boolean().default(false), }); - const { collections } = createDatabase(client.db(), { - users: UserSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + users: UserSchema, + }), + ); beforeAll(async () => { await client.connect(); diff --git a/tests/query/delete.test.ts b/tests/query/delete.test.ts index cc6347d..7613625 100644 --- a/tests/query/delete.test.ts +++ b/tests/query/delete.test.ts @@ -1,5 +1,5 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createSchema } from "../../src"; +import { createDatabase, createSchema, defineSchemas } from "../../src"; import { boolean, number, string } from "../../src/types"; import { createMockDatabase, mockUsers } from "../mock"; @@ -13,9 +13,12 @@ describe("Delete Operations", async () => { isVerified: boolean().default(false), }); - const { collections } = createDatabase(client.db(), { - users: UserSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + users: UserSchema, + }), + ); beforeAll(async () => { await client.connect(); diff --git a/tests/query/insert-find.test.ts b/tests/query/insert-find.test.ts index 844118e..4698994 100644 --- a/tests/query/insert-find.test.ts +++ b/tests/query/insert-find.test.ts @@ -1,6 +1,6 @@ import { ObjectId } from "mongodb"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createSchema } from "../../src"; +import { createDatabase, createSchema, defineSchemas } from "../../src"; import { boolean, number, objectId, string } from "../../src/types"; import { createMockDatabase, mockUsers } from "../mock"; @@ -20,10 +20,13 @@ describe("Insert and Find Operations", async () => { userId: objectId(), }); - const { collections } = createDatabase(client.db(), { - users: UserSchema, - todos: TodoSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + users: UserSchema, + todos: TodoSchema, + }), + ); beforeAll(async () => { await client.connect(); diff --git a/tests/query/query-methods.test.ts b/tests/query/query-methods.test.ts index 2886321..d17c274 100644 --- a/tests/query/query-methods.test.ts +++ b/tests/query/query-methods.test.ts @@ -1,5 +1,5 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createSchema } from "../../src"; +import { createDatabase, createSchema, defineSchemas } from "../../src"; import { boolean, number, string } from "../../src/types"; import { createMockDatabase, mockUsers } from "../mock"; @@ -13,9 +13,12 @@ describe("Query Methods", async () => { isVerified: boolean().default(false), }); - const { collections } = createDatabase(client.db(), { - users: UserSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + users: UserSchema, + }), + ); beforeAll(async () => { await client.connect(); diff --git a/tests/query/update-hooks.test.ts b/tests/query/update-hooks.test.ts index 5d3b34e..9d7728f 100644 --- a/tests/query/update-hooks.test.ts +++ b/tests/query/update-hooks.test.ts @@ -1,5 +1,5 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -import { createDatabase, createSchema } from "../../src"; +import { createDatabase, createSchema, defineSchemas } from "../../src"; import { boolean, number, pipe, string, type } from "../../src/types"; import { createMockDatabase } from "../mock"; @@ -25,7 +25,8 @@ describe("Update Hooks", async () => { age: number().onUpdate(() => 100), isAdmin: boolean(), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", age: 0, @@ -59,7 +60,8 @@ describe("Update Hooks", async () => { name: string(), nonce: number().onUpdate(onUpdateTrap).transform(transformTrap), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", nonce: 0, @@ -91,7 +93,8 @@ describe("Update Hooks", async () => { .onUpdate(onUpdateTrap) .validate(() => true, ""), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", nonce: 0, @@ -119,7 +122,8 @@ describe("Update Hooks", async () => { name: string(), nonce: number().onUpdate(onUpdateTrap).optional(), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", }); @@ -146,7 +150,8 @@ describe("Update Hooks", async () => { name: string(), nonce: number().onUpdate(onUpdateTrap).nullable(), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", nonce: null, @@ -174,7 +179,8 @@ describe("Update Hooks", async () => { name: string(), nonce: number().onUpdate(onUpdateTrap).default(0), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", }); @@ -204,7 +210,8 @@ describe("Update Hooks", async () => { string(), ).onUpdate(onUpdateTrap), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", nonce: 0, @@ -233,7 +240,8 @@ describe("Update Hooks", async () => { name: string(), nonce: number().onUpdate(onUpdateTrap).transform(transformTrap), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); // Insert initial document const res = await db.collections.users.insertOne({ @@ -270,7 +278,8 @@ describe("Update Hooks", async () => { name: string(), nonce: number().transform(transformTrap).onUpdate(onUpdateTrap), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); // Insert initial document const res = await db.collections.users.insertOne({ @@ -308,7 +317,8 @@ describe("Update Hooks", async () => { name: string(), nonce: number().onUpdate(onUpdateTrap).validate(validateTrap, "nonce must be between 0 and 50"), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); // Insert initial document with valid value const res = await db.collections.users.insertOne({ @@ -346,7 +356,8 @@ describe("Update Hooks", async () => { name: string(), nonce: number().validate(validateTrap, "nonce must be between 0 and 50").onUpdate(onUpdateTrap), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); // Insert initial document with valid value const res = await db.collections.users.insertOne({ @@ -390,7 +401,8 @@ describe("Update Hooks", async () => { .onUpdate(onUpdateTrap) .validate(validateTrap, "transformed value must have length <= 2"), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); // Insert initial document const res = await db.collections.users.insertOne({ @@ -436,7 +448,8 @@ describe("Update Hooks", async () => { .transform(transformTrap) .validate(validateTrap, "transformed value must have length <= 2"), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); // Insert initial document const res = await db.collections.users.insertOne({ diff --git a/tests/query/update.test.ts b/tests/query/update.test.ts index 7eebf61..b29a4f1 100644 --- a/tests/query/update.test.ts +++ b/tests/query/update.test.ts @@ -1,6 +1,6 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createSchema } from "../../src"; -import { boolean, number, string } from "../../src/types"; +import { createDatabase, createSchema, defineSchemas } from "../../src"; +import { array, boolean, number, string } from "../../src/types"; import { createMockDatabase, mockUsers } from "../mock"; describe("Update Operations", async () => { @@ -13,9 +13,12 @@ describe("Update Operations", async () => { isVerified: boolean().default(false), }); - const { collections } = createDatabase(client.db(), { - users: UserSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + users: UserSchema, + }), + ); beforeAll(async () => { await client.connect(); @@ -103,7 +106,8 @@ describe("Update Operations", async () => { name: string(), age: number().onUpdate(() => 555), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const user = await db.collections.users.insertOne({ name: "Alice", age: 20 }); @@ -115,13 +119,45 @@ describe("Update Operations", async () => { expect(updatedUser?.age).toBe(555); }); + describe("array operators", () => { + it("should support $addToSet operator", async () => { + const schema = createSchema("posts", { + title: string(), + tags: array(string()).optional(), + }); + const schemas = defineSchemas({ posts: schema }); + const db = createDatabase(client.db(), schemas); + + const post = await db.collections.posts.insertOne({ + title: "My Post", + tags: ["javascript"], + }); + + const updated = await db.collections.posts + .findOneAndUpdate({ _id: post._id }, { $addToSet: { tags: "typescript" } }) + .options({ returnDocument: "after" }); + + expect(updated?.tags).toContain("javascript"); + expect(updated?.tags).toContain("typescript"); + expect(updated?.tags).toHaveLength(2); + + // Adding the same tag again should not duplicate + const updated2 = await db.collections.posts + .findOneAndUpdate({ _id: post._id }, { $addToSet: { tags: "typescript" } }) + .options({ returnDocument: "after" }); + + expect(updated2?.tags).toHaveLength(2); + }); + }); + describe("edge cases", () => { it("should not mutate reused update object in updateOne", async () => { const schema = createSchema("users", { name: string(), age: number().onUpdate(() => 999), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const user1 = await db.collections.users.insertOne({ name: "Alice", age: 20 }); const user2 = await db.collections.users.insertOne({ name: "Bob", age: 30 }); @@ -151,7 +187,8 @@ describe("Update Operations", async () => { name: string(), age: number().onUpdate(() => 888), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); await db.collections.users.insertOne({ name: "Alice", age: 20 }); await db.collections.users.insertOne({ name: "Bob", age: 30 }); @@ -180,7 +217,8 @@ describe("Update Operations", async () => { name: string(), age: number().onUpdate(() => 777), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const user1 = await db.collections.users.insertOne({ name: "Alice", age: 20 }); const user2 = await db.collections.users.insertOne({ name: "Bob", age: 30 }); diff --git a/tests/relations/groups.test.ts b/tests/relations/groups.test.ts new file mode 100644 index 0000000..9504d30 --- /dev/null +++ b/tests/relations/groups.test.ts @@ -0,0 +1,141 @@ +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { createDatabase, createSchema, defineSchemas, mergeSchemas } from "../../src"; +import { objectId, string } from "../../src/types"; +import { createMockDatabase } from "../mock"; + +describe("schema groups", async () => { + const { server, client } = await createMockDatabase(); + + beforeAll(async () => { + await client.connect(); + }); + + afterEach(async () => { + await client.db().dropDatabase(); + }); + + afterAll(async () => { + await client.close(); + await server.stop(); + }); + + // Group 1: User module + const UserSchema = createSchema("users", { + name: string(), + tutorId: objectId().optional(), + }); + + const userGroup = defineSchemas({ UserSchema }).withRelations((s) => ({ + users: { + tutor: s.users.$one.users({ from: "tutorId", to: "_id" }), + }, + })); + + // Group 2: Content module + const PostSchema = createSchema("posts", { + title: string(), + authorId: objectId(), + }); + + const CategorySchema = createSchema("categories", { + name: string(), + parentId: objectId().optional(), + }); + + const contentGroup = defineSchemas({ PostSchema, CategorySchema }).withRelations((s) => ({ + categories: { + parent: s.categories.$one.categories({ from: "parentId", to: "_id" }), + }, + })); + + // Merged: within-group relations only + const mergedGroups = mergeSchemas(userGroup, contentGroup); + + // Merged: with cross-group relations added after merge + const withCrossGroupRelations = mergedGroups.withRelations((s) => ({ + users: { + posts: s.users.$many.posts({ from: "_id", to: "authorId" }), + }, + posts: { + author: s.posts.$one.users({ from: "authorId", to: "_id" }), + }, + })); + + describe("merged groups — within-group relations only", () => { + it("should list all collections from both groups", () => { + const db = createDatabase(client.db(), mergedGroups); + const keys = db.listCollections(); + expect(keys).toEqual(expect.arrayContaining(["users", "posts", "categories"])); + }); + + it("should preserve user group relation (users.tutor) after merge", async () => { + const db = createDatabase(client.db(), mergedGroups); + + const tutor = await db.collections.users.insertOne({ name: "Mentor" }); + await db.collections.users.insertOne({ name: "Learner", tutorId: tutor._id }); + + const learner = await db.collections.users.findOne({ name: "Learner" }).populate({ tutor: true }); + + expect(learner?.tutor?.name).toBe("Mentor"); + }); + + it("should preserve content group relation (categories.parent) after merge", async () => { + const db = createDatabase(client.db(), mergedGroups); + + const root = await db.collections.categories.insertOne({ name: "Tech" }); + await db.collections.categories.insertOne({ name: "TypeScript", parentId: root._id }); + + const child = await db.collections.categories.findOne({ name: "TypeScript" }).populate({ parent: true }); + + expect(child?.parent?.name).toBe("Tech"); + }); + }); + + describe("merged groups — with cross-group relations defined after merge", () => { + it("should populate cross-group many relation (users.posts)", async () => { + const db = createDatabase(client.db(), withCrossGroupRelations); + + const user = await db.collections.users.insertOne({ name: "Bob" }); + await db.collections.posts.insertOne({ title: "Post 1", authorId: user._id }); + await db.collections.posts.insertOne({ title: "Post 2", authorId: user._id }); + + const populatedUser = await db.collections.users.findById(user._id).populate({ posts: true }); + + expect(populatedUser?.posts).toHaveLength(2); + expect(populatedUser?.posts?.map((p) => p.title)).toEqual(expect.arrayContaining(["Post 1", "Post 2"])); + }); + + it("should populate cross-group one relation (posts.user)", async () => { + const db = createDatabase(client.db(), withCrossGroupRelations); + + const user = await db.collections.users.insertOne({ name: "Alice" }); + await db.collections.posts.insertOne({ title: "Hello World", authorId: user._id }); + + const post = await db.collections.posts.findOne({ title: "Hello World" }).populate({ author: true }); + + expect(post?.author?.name).toBe("Alice"); + }); + + it("should still populate within-group relation (users.tutor) after adding cross-group", async () => { + const db = createDatabase(client.db(), withCrossGroupRelations); + + const tutor = await db.collections.users.insertOne({ name: "Professor" }); + await db.collections.users.insertOne({ name: "Student", tutorId: tutor._id }); + + const student = await db.collections.users.findOne({ name: "Student" }).populate({ tutor: true }); + + expect(student?.tutor?.name).toBe("Professor"); + }); + + it("should still populate within-group relation (categories.parent) after adding cross-group", async () => { + const db = createDatabase(client.db(), withCrossGroupRelations); + + const parent = await db.collections.categories.insertOne({ name: "Root" }); + await db.collections.categories.insertOne({ name: "Child", parentId: parent._id }); + + const child = await db.collections.categories.findOne({ name: "Child" }).populate({ parent: true }); + + expect(child?.parent?.name).toBe("Root"); + }); + }); +}); diff --git a/tests/relations/many.test.ts b/tests/relations/many.test.ts index ae34946..f74ba21 100644 --- a/tests/relations/many.test.ts +++ b/tests/relations/many.test.ts @@ -1,9 +1,9 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createRelations, createSchema, virtual } from "../../src"; -import { array, boolean, date, objectId, string } from "../../src/types"; +import { createDatabase, createSchema, defineSchemas } from "../../src"; +import { boolean, date, objectId, string } from "../../src/types"; import { createMockDatabase } from "../mock"; -describe("many() relation tests", async () => { +describe("many relation tests", async () => { const { server, client } = await createMockDatabase(); beforeAll(async () => { @@ -24,140 +24,183 @@ describe("many() relation tests", async () => { name: string(), isAdmin: boolean(), createdAt: date(), + tutor: objectId().optional(), }); const PostSchema = createSchema("posts", { title: string(), contents: string(), author: objectId().optional(), - contributors: array(objectId()).optional().default([]), - secret: string().default(() => "secret"), - }) - .omit({ secret: true }) - .virtuals({ - contributorsCount: virtual("contributors", ({ contributors }) => contributors?.length ?? 0), - secretSize: virtual("secret", ({ secret }) => secret?.length), - }); - - const PostSchemaRelations = createRelations(PostSchema, ({ one, many }) => ({ - author: one(UserSchema, { field: "author", references: "_id" }), - contributors: many(UserSchema, { - field: "contributors", - references: "_id", - }), - })); + }); - return createDatabase(client.db(), { - users: UserSchema, - posts: PostSchema, - PostSchemaRelations, + const BookSchema = createSchema("books", { + title: string(), + author: objectId().optional(), }); + + const schemas = defineSchemas({ UserSchema, PostSchema, BookSchema }); + const relations = schemas.withRelations((s) => ({ + users: { + tutor: s.users.$one.users({ from: "tutor", to: "_id" }), + posts: s.users.$many.posts({ from: "_id", to: "author" }), + books: s.users.$many.books({ from: "_id", to: "author" }), + }, + posts: { + author: s.posts.$one.users({ from: "author", to: "_id" }), + }, + books: { + author: s.books.$one.users({ from: "author", to: "_id" }), + }, + })); + return createDatabase(client.db(), relations); }; - it("should populate many() relation (contributors)", async () => { + it("should populate many relation (posts)", async () => { const { collections } = setupSchemasAndCollections(); const user = await collections.users.insertOne({ name: "Bob", isAdmin: false, createdAt: new Date(), + tutor: undefined, }); - const user2 = await collections.users.insertOne({ - name: "Alex", + await collections.users.insertOne({ + name: "Alexa", isAdmin: false, createdAt: new Date(), + tutor: user._id, }); await collections.posts.insertOne({ title: "Pilot", contents: "Lorem", author: user._id, - contributors: [user2._id], }); - const populatedPost = await collections.posts - .findOne({ - title: "Pilot", - }) - .populate({ contributors: true }); - expect(populatedPost?.contributors).toBeDefined(); - expect(populatedPost?.contributors).toHaveLength(1); - expect(populatedPost?.contributors[0]).toStrictEqual(user2); + await collections.posts.insertOne({ + title: "Pilot 2", + contents: "Lorem2", + author: user._id, + }); + + await collections.posts.insertOne({ + title: "No Author", + contents: "Lorem", + }); + + const populatedUsers = await collections.users.find().populate({ posts: true, tutor: true }); + + expect(populatedUsers.length).toBe(2); + expect(populatedUsers[0].posts.length).toBe(2); + expect(populatedUsers[1].posts.length).toBe(0); + expect(populatedUsers[1].tutor).toStrictEqual(user); }); - it("should populate many() relation with multiple contributors", async () => { + it("should handle multiple many relations with same field", async () => { const { collections } = setupSchemasAndCollections(); - const user1 = await collections.users.insertOne({ - name: "Bob", + const user = await collections.users.insertOne({ + name: "Test User", isAdmin: false, createdAt: new Date(), }); - const user2 = await collections.users.insertOne({ - name: "Alex", - isAdmin: false, - createdAt: new Date(), + await collections.posts.insertOne({ + title: "Post 1", + contents: "Content 1", + author: user._id, }); - const user3 = await collections.users.insertOne({ - name: "Charlie", - isAdmin: false, - createdAt: new Date(), + + await collections.books.insertOne({ + title: "Book 1", + author: user._id, }); - await collections.posts.insertOne({ - title: "Multi Author Post", - contents: "Content", - author: user1._id, - contributors: [user2._id, user3._id], - }); - - const populatedPost = await collections.posts - .findOne({ - title: "Multi Author Post", - }) - .populate({ contributors: true, author: true }); - expect(populatedPost?.author).toStrictEqual(user1); - expect(populatedPost?.contributors).toBeDefined(); - expect(populatedPost?.contributors).toHaveLength(2); - expect(populatedPost?.contributors[0]).toStrictEqual(user2); - expect(populatedPost?.contributors[1]).toStrictEqual(user3); + + const populatedUser = await collections.users.findById(user._id).populate({ posts: true, books: true }); + + expect(populatedUser).toBeTruthy(); + expect(populatedUser?.posts).toHaveLength(1); + expect(populatedUser?.books).toHaveLength(1); + expect(populatedUser?.posts?.[0]?.title).toBe("Post 1"); + expect(populatedUser?.books?.[0]?.title).toBe("Book 1"); }); - it("should access original many() field in virtuals", async () => { - const { collections } = setupSchemasAndCollections(); + it("should handle deep nested populations with many relations", async () => { + const PostSchemaWithEditor = createSchema("posts", { + title: string(), + contents: string(), + author: objectId().optional(), + editor: objectId().optional(), + }); + + const UserSchemaForEditor = createSchema("users", { + name: string(), + isAdmin: boolean(), + createdAt: date(), + }); + + const BookSchemaDeep = createSchema("books", { + title: string(), + author: objectId().optional(), + }); + + const schemas = defineSchemas({ UserSchemaForEditor, PostSchemaWithEditor, BookSchemaDeep }); + const relations = schemas.withRelations((s) => ({ + users: { + posts: s.users.$many.posts({ from: "_id", to: "author" }), + books: s.users.$many.books({ from: "_id", to: "author" }), + }, + posts: { + author: s.posts.$one.users({ from: "author", to: "_id" }), + editor: s.posts.$one.users({ from: "editor", to: "_id" }), + }, + })); + const db = createDatabase(client.db(), relations); - const user1 = await collections.users.insertOne({ - name: "Test User 1", + const user = await db.collections.users.insertOne({ + name: "Test User", isAdmin: false, createdAt: new Date(), }); - const user2 = await collections.users.insertOne({ + const user2 = await db.collections.users.insertOne({ name: "Test User 2", isAdmin: false, createdAt: new Date(), }); - await collections.posts.insertOne({ - title: "Post 6", - contents: "Content 6", - contributors: [user1._id, user2._id], - secret: "12345", + await db.collections.posts.insertOne({ + title: "Post 1", + contents: "Content 1", + author: user._id, + editor: user2._id, }); - const populatedPost = await collections.posts.find().populate({ - contributors: { - select: { name: true }, - }, + await db.collections.posts.insertOne({ + title: "Post 2", + contents: "Content 2", + author: user2._id, + editor: user2._id, }); - expect(populatedPost.length).toBe(1); - expect(populatedPost[0].contributorsCount).toBe(2); - expect(populatedPost[0].contributors.length).toBe(2); - expect(populatedPost[0].contributors[0]).toStrictEqual({ - _id: user1._id, - name: user1.name, + + await db.collections.books.insertOne({ + title: "Book 1", + author: user._id, }); - expect(populatedPost[0].contributors[1]).toStrictEqual({ - _id: user2._id, - name: user2.name, + + const populatedUser = await db.collections.users.findById(user._id).populate({ + posts: { + populate: { + editor: { + populate: { + posts: true, + }, + }, + }, + }, + books: true, }); - expect(populatedPost[0].secretSize).toBe(5); - expect(populatedPost[0]).not.toHaveProperty("secret"); + expect(populatedUser).toBeTruthy(); + expect(populatedUser?.posts).toHaveLength(1); + expect(populatedUser?.books).toHaveLength(1); + expect(populatedUser?.posts?.[0]?.title).toBe("Post 1"); + expect(populatedUser?.posts?.[0]?.editor?.posts).toHaveLength(1); + expect(populatedUser?.books?.[0]?.title).toBe("Book 1"); }); }); diff --git a/tests/relations/one.test.ts b/tests/relations/one.test.ts index 7706e63..ac50730 100644 --- a/tests/relations/one.test.ts +++ b/tests/relations/one.test.ts @@ -1,9 +1,9 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createRelations, createSchema } from "../../src"; +import { createDatabase, createSchema, defineSchemas } from "../../src"; import { array, boolean, date, objectId, string } from "../../src/types"; import { createMockDatabase } from "../mock"; -describe("one() relation tests", async () => { +describe("one relation tests", async () => { const { server, client } = await createMockDatabase(); beforeAll(async () => { @@ -35,28 +35,21 @@ describe("one() relation tests", async () => { contributors: array(objectId()).optional().default([]), }); - const UserSchemaRelations = createRelations(UserSchema, ({ one }) => ({ - tutor: one(UserSchema, { field: "tutor", references: "_id" }), + const schemas = defineSchemas({ UserSchema, PostSchema }); + const relations = schemas.withRelations((s) => ({ + users: { + tutor: s.users.$one.users({ from: "tutor", to: "_id" }), + }, + posts: { + author: s.posts.$one.users({ from: "author", to: "_id" }), + editor: s.posts.$one.users({ from: "editor", to: "_id" }), + contributors: s.posts.$refs.users({ from: "contributors", to: "_id" }), + }, })); - - const PostSchemaRelations = createRelations(PostSchema, ({ one, many }) => ({ - author: one(UserSchema, { field: "author", references: "_id" }), - editor: one(UserSchema, { field: "editor", references: "_id" }), - contributors: many(UserSchema, { - field: "contributors", - references: "_id", - }), - })); - - return createDatabase(client.db(), { - users: UserSchema, - posts: PostSchema, - UserSchemaRelations, - PostSchemaRelations, - }); + return createDatabase(client.db(), relations); }; - it("should populate one() relation (tutor)", async () => { + it("should populate one relation (tutor)", async () => { const { collections } = setupSchemasAndCollections(); const user = await collections.users.insertOne({ @@ -78,7 +71,7 @@ describe("one() relation tests", async () => { }); }); - it("should populate one() relation (author)", async () => { + it("should populate one relation (author)", async () => { const { collections } = setupSchemasAndCollections(); const user = await collections.users.insertOne({ @@ -100,7 +93,7 @@ describe("one() relation tests", async () => { expect(populatedPost?.author).toStrictEqual(user); }); - it("should support nested one() relation population", async () => { + it("should support nested one relation population", async () => { const UserSchemaWithRefs = createSchema("users", { name: string(), isAdmin: boolean(), @@ -114,21 +107,17 @@ describe("one() relation tests", async () => { author: objectId().optional(), }); - const UserRelations = createRelations(UserSchemaWithRefs, ({ one, ref }) => ({ - tutor: one(UserSchemaWithRefs, { field: "tutor", references: "_id" }), - posts: ref(PostSchemaWithRefs, { field: "_id", references: "author" }), + const schemas = defineSchemas({ UserSchemaWithRefs, PostSchemaWithRefs }); + const relations = schemas.withRelations((s) => ({ + users: { + tutor: s.users.$one.users({ from: "tutor", to: "_id" }), + posts: s.users.$many.posts({ from: "_id", to: "author" }), + }, + posts: { + author: s.posts.$one.users({ from: "author", to: "_id" }), + }, })); - - const PostRelations = createRelations(PostSchemaWithRefs, ({ one }) => ({ - author: one(UserSchemaWithRefs, { field: "author", references: "_id" }), - })); - - const db = createDatabase(client.db(), { - users: UserSchemaWithRefs, - posts: PostSchemaWithRefs, - UserRelations, - PostRelations, - }); + const db = createDatabase(client.db(), relations); // Create users with tutor relationship const tutor = await db.collections.users.insertOne({ diff --git a/tests/relations/population-options.test.ts b/tests/relations/population-options.test.ts index 404ba69..2e62357 100644 --- a/tests/relations/population-options.test.ts +++ b/tests/relations/population-options.test.ts @@ -1,5 +1,5 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createRelations, createSchema, virtual } from "../../src"; +import { createDatabase, createSchema, defineSchemas, virtual } from "../../src"; import { array, boolean, date, objectId, string } from "../../src/types"; import { createMockDatabase } from "../mock"; @@ -39,24 +39,17 @@ describe("Population Options", async () => { secretSize: virtual("secret", ({ secret }) => secret?.length), }); - const UserSchemaRelations = createRelations(UserSchema, ({ ref }) => ({ - posts: ref(PostSchema, { field: "_id", references: "author" }), - })); - - const PostSchemaRelations = createRelations(PostSchema, ({ one, many }) => ({ - author: one(UserSchema, { field: "author", references: "_id" }), - contributors: many(UserSchema, { - field: "contributors", - references: "_id", - }), + const schemas = defineSchemas({ UserSchema, PostSchema }); + const relations = schemas.withRelations((s) => ({ + users: { + posts: s.users.$many.posts({ from: "_id", to: "author" }), + }, + posts: { + author: s.posts.$one.users({ from: "author", to: "_id" }), + contributors: s.posts.$refs.users({ from: "contributors", to: "_id" }), + }, })); - - return createDatabase(client.db(), { - users: UserSchema, - posts: PostSchema, - UserSchemaRelations, - PostSchemaRelations, - }); + return createDatabase(client.db(), relations); }; it("should populate with limit and skip options", async () => { diff --git a/tests/relations/ref.test.ts b/tests/relations/ref.test.ts deleted file mode 100644 index 4201423..0000000 --- a/tests/relations/ref.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createRelations, createSchema } from "../../src"; -import { boolean, date, objectId, string } from "../../src/types"; -import { createMockDatabase } from "../mock"; - -describe("ref() relation tests", async () => { - const { server, client } = await createMockDatabase(); - - beforeAll(async () => { - await client.connect(); - }); - - afterEach(async () => { - await client.db().dropDatabase(); - }); - - afterAll(async () => { - await client.close(); - await server.stop(); - }); - - const setupSchemasAndCollections = () => { - const UserSchema = createSchema("users", { - name: string(), - isAdmin: boolean(), - createdAt: date(), - tutor: objectId().optional(), - }); - - const PostSchema = createSchema("posts", { - title: string(), - contents: string(), - author: objectId().optional(), - }); - - const BookSchema = createSchema("books", { - title: string(), - author: objectId().optional(), - }); - - const UserSchemaRelations = createRelations(UserSchema, ({ one, ref }) => ({ - tutor: one(UserSchema, { field: "tutor", references: "_id" }), - posts: ref(PostSchema, { field: "_id", references: "author" }), - books: ref(BookSchema, { field: "_id", references: "author" }), - })); - - const PostSchemaRelations = createRelations(PostSchema, ({ one }) => ({ - author: one(UserSchema, { field: "author", references: "_id" }), - })); - - const BookSchemaRelations = createRelations(BookSchema, ({ one }) => ({ - author: one(UserSchema, { field: "author", references: "_id" }), - })); - - return createDatabase(client.db(), { - users: UserSchema, - posts: PostSchema, - books: BookSchema, - UserSchemaRelations, - PostSchemaRelations, - BookSchemaRelations, - }); - }; - - it("should populate ref() relation (posts)", async () => { - const { collections } = setupSchemasAndCollections(); - - const user = await collections.users.insertOne({ - name: "Bob", - isAdmin: false, - createdAt: new Date(), - tutor: undefined, - }); - const tutoredUser = await collections.users.insertOne({ - name: "Alexa", - isAdmin: false, - createdAt: new Date(), - tutor: user._id, - }); - await collections.posts.insertOne({ - title: "Pilot", - contents: "Lorem", - author: user._id, - }); - - await collections.posts.insertOne({ - title: "Pilot 2", - contents: "Lorem2", - author: user._id, - }); - - await collections.posts.insertOne({ - title: "No Author", - contents: "Lorem", - }); - - const populatedUsers = await collections.users.find().populate({ posts: true, tutor: true }); - - expect(populatedUsers.length).toBe(2); - expect(populatedUsers[0].posts.length).toBe(2); - expect(populatedUsers[1].posts.length).toBe(0); - expect(populatedUsers[1].tutor).toStrictEqual(user); - }); - - it("should handle multiple ref() relations with same field", async () => { - const { collections } = setupSchemasAndCollections(); - - const user = await collections.users.insertOne({ - name: "Test User", - isAdmin: false, - createdAt: new Date(), - }); - await collections.posts.insertOne({ - title: "Post 1", - contents: "Content 1", - author: user._id, - }); - - await collections.books.insertOne({ - title: "Book 1", - author: user._id, - }); - - const populatedUser = await collections.users.findById(user._id).populate({ posts: true, books: true }); - - expect(populatedUser).toBeTruthy(); - expect(populatedUser?.posts).toHaveLength(1); - expect(populatedUser?.books).toHaveLength(1); - expect(populatedUser?.posts?.[0]?.title).toBe("Post 1"); - expect(populatedUser?.books?.[0]?.title).toBe("Book 1"); - }); - - it("should handle deep nested populations with ref() relations", async () => { - const PostSchemaWithEditor = createSchema("posts_deep", { - title: string(), - contents: string(), - author: objectId().optional(), - editor: objectId().optional(), - }); - - const UserSchemaForEditor = createSchema("users_deep", { - name: string(), - isAdmin: boolean(), - createdAt: date(), - }); - - const BookSchemaDeep = createSchema("books_deep", { - title: string(), - author: objectId().optional(), - }); - - const UserRelationsEditor = createRelations(UserSchemaForEditor, ({ ref }) => ({ - posts: ref(PostSchemaWithEditor, { field: "_id", references: "author" }), - books: ref(BookSchemaDeep, { field: "_id", references: "author" }), - })); - - const PostRelationsEditor = createRelations(PostSchemaWithEditor, ({ one }) => ({ - author: one(UserSchemaForEditor, { field: "author", references: "_id" }), - editor: one(UserSchemaForEditor, { field: "editor", references: "_id" }), - })); - - const db = createDatabase(client.db(), { - users: UserSchemaForEditor, - posts: PostSchemaWithEditor, - books: BookSchemaDeep, - UserRelationsEditor, - PostRelationsEditor, - }); - - const user = await db.collections.users.insertOne({ - name: "Test User", - isAdmin: false, - createdAt: new Date(), - }); - const user2 = await db.collections.users.insertOne({ - name: "Test User 2", - isAdmin: false, - createdAt: new Date(), - }); - await db.collections.posts.insertOne({ - title: "Post 1", - contents: "Content 1", - author: user._id, - editor: user2._id, - }); - - await db.collections.posts.insertOne({ - title: "Post 2", - contents: "Content 2", - author: user2._id, - editor: user2._id, - }); - - await db.collections.books.insertOne({ - title: "Book 1", - author: user._id, - }); - - const populatedUser = await db.collections.users.findById(user._id).populate({ - posts: { - populate: { - editor: { - populate: { - posts: true, - }, - }, - }, - }, - books: true, - }); - expect(populatedUser).toBeTruthy(); - expect(populatedUser?.posts).toHaveLength(1); - expect(populatedUser?.books).toHaveLength(1); - expect(populatedUser?.posts?.[0]?.title).toBe("Post 1"); - expect(populatedUser?.posts?.[0]?.editor?.posts).toHaveLength(1); - expect(populatedUser?.books?.[0]?.title).toBe("Book 1"); - }); -}); diff --git a/tests/relations/refs.test.ts b/tests/relations/refs.test.ts new file mode 100644 index 0000000..9657df9 --- /dev/null +++ b/tests/relations/refs.test.ts @@ -0,0 +1,158 @@ +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { createDatabase, createSchema, defineSchemas, virtual } from "../../src"; +import { array, boolean, date, objectId, string } from "../../src/types"; +import { createMockDatabase } from "../mock"; + +describe("refs relation tests", async () => { + const { server, client } = await createMockDatabase(); + + beforeAll(async () => { + await client.connect(); + }); + + afterEach(async () => { + await client.db().dropDatabase(); + }); + + afterAll(async () => { + await client.close(); + await server.stop(); + }); + + const setupSchemasAndCollections = () => { + const UserSchema = createSchema("users", { + name: string(), + isAdmin: boolean(), + createdAt: date(), + }); + + const PostSchema = createSchema("posts", { + title: string(), + contents: string(), + author: objectId().optional(), + contributors: array(objectId()).optional().default([]), + secret: string().default(() => "secret"), + }) + .omit({ secret: true }) + .virtuals({ + contributorsCount: virtual("contributors", ({ contributors }) => contributors?.length ?? 0), + secretSize: virtual("secret", ({ secret }) => secret?.length), + }); + + const schemas = defineSchemas({ UserSchema, PostSchema }); + const relations = schemas.withRelations((s) => ({ + posts: { + author: s.posts.$one.users({ from: "author", to: "_id" }), + contributors: s.posts.$refs.users({ from: "contributors", to: "_id" }), + }, + })); + return createDatabase(client.db(), relations); + }; + + it("should populate refs relation (contributors)", async () => { + const { collections } = setupSchemasAndCollections(); + + const user = await collections.users.insertOne({ + name: "Bob", + isAdmin: false, + createdAt: new Date(), + }); + const user2 = await collections.users.insertOne({ + name: "Alex", + isAdmin: false, + createdAt: new Date(), + }); + await collections.posts.insertOne({ + title: "Pilot", + contents: "Lorem", + author: user._id, + contributors: [user2._id], + }); + + const populatedPost = await collections.posts + .findOne({ + title: "Pilot", + }) + .populate({ contributors: true }); + expect(populatedPost?.contributors).toBeDefined(); + expect(populatedPost?.contributors).toHaveLength(1); + expect(populatedPost?.contributors[0]).toStrictEqual(user2); + }); + + it("should populate refs relation with multiple contributors", async () => { + const { collections } = setupSchemasAndCollections(); + + const user1 = await collections.users.insertOne({ + name: "Bob", + isAdmin: false, + createdAt: new Date(), + }); + const user2 = await collections.users.insertOne({ + name: "Alex", + isAdmin: false, + createdAt: new Date(), + }); + const user3 = await collections.users.insertOne({ + name: "Charlie", + isAdmin: false, + createdAt: new Date(), + }); + await collections.posts.insertOne({ + title: "Multi Author Post", + contents: "Content", + author: user1._id, + contributors: [user2._id, user3._id], + }); + + const populatedPost = await collections.posts + .findOne({ + title: "Multi Author Post", + }) + .populate({ contributors: true, author: true }); + expect(populatedPost?.author).toStrictEqual(user1); + expect(populatedPost?.contributors).toBeDefined(); + expect(populatedPost?.contributors).toHaveLength(2); + expect(populatedPost?.contributors[0]).toStrictEqual(user2); + expect(populatedPost?.contributors[1]).toStrictEqual(user3); + }); + + it("should access original refs field in virtuals", async () => { + const { collections } = setupSchemasAndCollections(); + + const user1 = await collections.users.insertOne({ + name: "Test User 1", + isAdmin: false, + createdAt: new Date(), + }); + const user2 = await collections.users.insertOne({ + name: "Test User 2", + isAdmin: false, + createdAt: new Date(), + }); + await collections.posts.insertOne({ + title: "Post 6", + contents: "Content 6", + contributors: [user1._id, user2._id], + secret: "12345", + }); + + const populatedPost = await collections.posts.find().populate({ + contributors: { + select: { name: true }, + }, + }); + expect(populatedPost.length).toBe(1); + expect(populatedPost[0].contributorsCount).toBe(2); + expect(populatedPost[0].contributors.length).toBe(2); + expect(populatedPost[0].contributors[0]).toStrictEqual({ + _id: user1._id, + name: user1.name, + }); + expect(populatedPost[0].contributors[1]).toStrictEqual({ + _id: user2._id, + name: user2.name, + }); + expect(populatedPost[0].secretSize).toBe(5); + expect(populatedPost[0]).not.toHaveProperty("secret"); + }); +}); diff --git a/tests/relations/validation.test.ts b/tests/relations/validation.test.ts index 9105b3b..64a710f 100644 --- a/tests/relations/validation.test.ts +++ b/tests/relations/validation.test.ts @@ -1,6 +1,6 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createRelations, createSchema } from "../../src"; -import { boolean, date, objectId, string } from "../../src/types"; +import { createDatabase, createSchema, defineSchemas } from "../../src"; +import { boolean, date, string } from "../../src/types"; import { createMockDatabase } from "../mock"; describe("Relation Validations", async () => { @@ -19,27 +19,6 @@ describe("Relation Validations", async () => { await server.stop(); }); - it("should throw error when relation target schema is not initialized", async () => { - const UserSchema = createSchema("users", { - name: string(), - isAdmin: boolean(), - createdAt: date(), - }); - - const UserSchemaRelations = createRelations(UserSchema, ({ ref }) => ({ - posts: ref(undefined as any, { field: "_id", references: "author" }), - })); - - const db = createDatabase(client.db(), { - users: UserSchema, - UserSchemaRelations, - }); - - await expect(async () => { - await db.collections.users.find().populate({ posts: true }); - }).rejects.toThrowError("Target schema not found for relation 'posts' in schema 'users'"); - }); - it("should throw error when schema has no relations defined", async () => { const UserSchema = createSchema("users", { name: string(), @@ -47,42 +26,15 @@ describe("Relation Validations", async () => { createdAt: date(), }); - const db = createDatabase(client.db(), { - users: UserSchema, - }); + const db = createDatabase( + client.db(), + defineSchemas({ + UserSchema, + }), + ); await expect(async () => { await db.collections.users.find().populate({ posts: true }); }).rejects.toThrowError("No relations found for schema 'users'"); }); - - it("throws error when defining relations for the same schema multiple times", () => { - const UserSchema = createSchema("users", { - name: string(), - isAdmin: boolean(), - createdAt: date(), - }); - - const PostSchema = createSchema("posts", { - title: string(), - author: objectId().optional(), - }); - - const UserRelations1 = createRelations(UserSchema, ({ ref }) => ({ - posts: ref(PostSchema, { field: "_id", references: "author" }), - })); - - const UserRelations2 = createRelations(UserSchema, ({ ref }) => ({ - books: ref(PostSchema, { field: "_id", references: "author" }), - })); - - expect(() => { - createDatabase(client.db(), { - users: UserSchema, - posts: PostSchema, - UserRelations1, - UserRelations2, - }); - }).toThrowError("Relations for schema 'users' already exists."); - }); }); diff --git a/tests/schema/schema.test.ts b/tests/schema/schema.test.ts index 24b5491..b2362c5 100644 --- a/tests/schema/schema.test.ts +++ b/tests/schema/schema.test.ts @@ -1,5 +1,5 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; -import { createDatabase, createSchema, virtual } from "../../src"; +import { createDatabase, createSchema, defineSchemas, virtual } from "../../src"; import { boolean, number, string } from "../../src/types"; import { createMockDatabase } from "../mock"; @@ -27,7 +27,8 @@ describe("Schema", async () => { }).omit({ isAdmin: true, }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", age: 0, @@ -46,7 +47,8 @@ describe("Schema", async () => { }).virtuals({ role: virtual("isAdmin", ({ isAdmin }) => (isAdmin ? "admin" : "user")), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom cruise", age: 0, @@ -75,7 +77,8 @@ describe("Schema", async () => { .virtuals({ role: virtual("isAdmin", ({ isAdmin }) => (isAdmin ? "admin" : "user")), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", age: 0, @@ -102,7 +105,8 @@ describe("Schema", async () => { .virtuals({ role: virtual("isAdmin", ({ isAdmin }) => (isAdmin !== undefined ? "known" : "unknown")), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", age: 0, @@ -143,7 +147,8 @@ describe("Schema", async () => { }).virtuals({ role: virtual("isAdmin", ({ isAdmin }) => (isAdmin ? "admin" : "user")), }); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); const res = await db.collections.users.insertOne({ name: "tom", age: 0, @@ -170,7 +175,8 @@ describe("Schema", async () => { username: unique("username"), fullname: createIndex({ firstname: 1, surname: 1 }, { unique: true }), })); - const db = createDatabase(client.db(), { users: schema }); + const schemas = defineSchemas({ users: schema }); + const db = createDatabase(client.db(), schemas); // duplicate username await db.collections.users.insertOne({ @@ -211,7 +217,8 @@ describe("Schema", async () => { name: string(), price: number(), }); - const db = createDatabase(client.db(), { products: schema }); + const schemas = defineSchemas({ products: schema }); + const db = createDatabase(client.db(), schemas); const product = await db.collections.products.insertOne({ _id: "product-123", @@ -238,7 +245,8 @@ describe("Schema", async () => { customerId: string(), total: number(), }); - const db = createDatabase(client.db(), { orders: schema }); + const schemas = defineSchemas({ orders: schema }); + const db = createDatabase(client.db(), schemas); const order = await db.collections.orders.insertOne({ _id: 12345, @@ -271,10 +279,8 @@ describe("Schema", async () => { }); expect(() => { - createDatabase(client.db(), { - users: UserSchema, - users2: AnotherUserSchema, - }); + const schemas = defineSchemas({ users: UserSchema, users2: AnotherUserSchema }); + createDatabase(client.db(), schemas); }).toThrowError("Schema with name 'users' already exists."); }); }); diff --git a/tests/types/binary.test.ts b/tests/types/binary.test.ts index b470ef3..12d5a39 100644 --- a/tests/types/binary.test.ts +++ b/tests/types/binary.test.ts @@ -1,6 +1,6 @@ import { Binary } from "mongodb"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; -import { createDatabase, createSchema, Schema } from "../../src"; +import { createDatabase, createSchema, defineSchemas, Schema } from "../../src"; import { binary } from "../../src/types"; import { createMockDatabase } from "../mock"; @@ -55,13 +55,16 @@ describe("binary", () => { await server.stop(); }); - const BsonDataSchema = createSchema("bson_data_binary", { + const BsonDataSchema = createSchema("bsonData", { binaryField: binary().optional(), }); - const { collections } = createDatabase(client.db(), { - bsonData: BsonDataSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + bsonData: BsonDataSchema, + }), + ); afterAll(async () => { await collections.bsonData.deleteMany({}); diff --git a/tests/types/decimal128.test.ts b/tests/types/decimal128.test.ts index 9bc91f5..a6bace7 100644 --- a/tests/types/decimal128.test.ts +++ b/tests/types/decimal128.test.ts @@ -1,6 +1,6 @@ import { Decimal128 } from "mongodb"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; -import { createDatabase, createSchema, Schema } from "../../src"; +import { createDatabase, createSchema, defineSchemas, Schema } from "../../src"; import { decimal128 } from "../../src/types"; import { createMockDatabase } from "../mock"; @@ -77,13 +77,16 @@ describe("decimal128", () => { await server.stop(); }); - const BsonDataSchema = createSchema("bson_data_decimal", { + const BsonDataSchema = createSchema("bsonData", { decimalField: decimal128().optional(), }); - const { collections } = createDatabase(client.db(), { - bsonData: BsonDataSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + bsonData: BsonDataSchema, + }), + ); afterAll(async () => { await collections.bsonData.deleteMany({}); diff --git a/tests/types/long.test.ts b/tests/types/long.test.ts index 8278126..5b46e92 100644 --- a/tests/types/long.test.ts +++ b/tests/types/long.test.ts @@ -1,6 +1,6 @@ import { Long } from "mongodb"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; -import { createDatabase, createSchema, Schema } from "../../src"; +import { createDatabase, createSchema, defineSchemas, Schema } from "../../src"; import { long } from "../../src/types"; import { createMockDatabase } from "../mock"; @@ -76,13 +76,16 @@ describe("long", () => { await server.stop(); }); - const BsonDataSchema = createSchema("bson_data_long", { + const BsonDataSchema = createSchema("bsonData", { longField: long().optional(), }); - const { collections } = createDatabase(client.db(), { - bsonData: BsonDataSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + bsonData: BsonDataSchema, + }), + ); afterAll(async () => { await collections.bsonData.deleteMany({}); diff --git a/tests/types/objectid.test.ts b/tests/types/objectid.test.ts index b037313..ba0af49 100644 --- a/tests/types/objectid.test.ts +++ b/tests/types/objectid.test.ts @@ -1,6 +1,6 @@ import { ObjectId } from "mongodb"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; -import { createDatabase, createSchema, Schema } from "../../src"; +import { createDatabase, createSchema, defineSchemas, Schema } from "../../src"; import { objectId } from "../../src/types"; import { createMockDatabase } from "../mock"; @@ -64,13 +64,16 @@ describe("objectId", () => { await server.stop(); }); - const TestSchema = createSchema("objectid_test", { + const TestSchema = createSchema("testData", { refId: objectId().optional(), }); - const { collections } = createDatabase(client.db(), { - testData: TestSchema, - }); + const { collections } = createDatabase( + client.db(), + defineSchemas({ + testData: TestSchema, + }), + ); afterAll(async () => { await collections.testData.deleteMany({}); diff --git a/todo.md b/todo.md index dc4070c..51e414a 100644 --- a/todo.md +++ b/todo.md @@ -47,7 +47,5 @@ Here are some features we need to implement. ### Bugs list - [] Where query argument takes anything even though the intellisense is correct -- [] Insert is not a query and does not return the model. -- [] optional() does not have any effect on the types - [] $addToSet is not working - [] findOneAndUpdate does not validate data