@mongorm/orm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ import { ObjectId } from 'mongodb';
2
+ import { z } from 'zod';
3
+
4
+ import type { SchemaShape } from '../schema/contracts.js';
5
+
6
+ /** A schema-like target resolved lazily to support circular module imports. */
7
+ export type SchemaLike = import('../schema/schema.js').Schema<SchemaShape, any, any, any>;
8
+
9
+ /** Metadata attached to a forward relation field. */
10
+ export interface RefDefinition<Target extends SchemaLike = SchemaLike> {
11
+ /** Resolve the related schema after module initialization completes. */
12
+ resolve: () => Target;
13
+ }
14
+
15
+ /** Metadata for a one-way relation between two MongoDB schemas. */
16
+ export interface SchemaRelation<
17
+ Target extends SchemaLike = SchemaLike,
18
+ LocalField extends string = string,
19
+ ForeignField extends string = string,
20
+ > {
21
+ readonly resolve: () => Target;
22
+ readonly localField: LocalField;
23
+ readonly foreignField: ForeignField;
24
+ }
25
+
26
+ /** Relation metadata attached to a schema. */
27
+ export type SchemaRelationMap = Record<string, SchemaRelation>;
28
+
29
+ /** A relation target declaration accepted by Schema.relations(). */
30
+ export type RelationInput<Target extends SchemaLike = SchemaLike> =
31
+ | (() => Target)
32
+ | {
33
+ target: () => Target;
34
+ foreignField?: string;
35
+ };
36
+
37
+ /** Extract a relation target from a relation declaration. */
38
+ export type RelationInputTarget<Input> = Input extends () => infer Target
39
+ ? Target
40
+ : Input extends { target: () => infer Target }
41
+ ? Target
42
+ : never;
43
+
44
+ /** A string identifier field carrying a typed relation target. */
45
+ export type RefField<Target extends SchemaLike = SchemaLike> = z.ZodType<ObjectId> & {
46
+ readonly __ref?: RefDefinition<Target>;
47
+ optional(): OptionalRefField<Target>;
48
+ nullable(): NullableRefField<Target>;
49
+ nullish(): NullishRefField<Target>;
50
+ };
51
+
52
+ /** An optional relation field that retains its target metadata. */
53
+ export type OptionalRefField<Target extends SchemaLike = SchemaLike> = z.ZodOptional<
54
+ RefField<Target>
55
+ > & {
56
+ readonly __ref?: RefDefinition<Target>;
57
+ };
58
+
59
+ /** A nullable relation field that retains its target metadata. */
60
+ export type NullableRefField<Target extends SchemaLike = SchemaLike> = z.ZodNullable<
61
+ RefField<Target>
62
+ > & {
63
+ readonly __ref?: RefDefinition<Target>;
64
+ };
65
+
66
+ /** An optional and nullable relation field that retains its target metadata. */
67
+ export type NullishRefField<Target extends SchemaLike = SchemaLike> = z.ZodOptional<
68
+ NullableRefField<Target>
69
+ > & {
70
+ readonly __ref?: RefDefinition<Target>;
71
+ };
72
+
73
+ type RelationDefinition<Field> = Field extends { readonly __ref?: infer Definition }
74
+ ? NonNullable<Definition>
75
+ : Field extends z.ZodOptional<infer Inner>
76
+ ? RelationDefinition<Inner>
77
+ : Field extends z.ZodNullable<infer Inner>
78
+ ? RelationDefinition<Inner>
79
+ : never;
80
+
81
+ /** The relation metadata inferred from a schema shape. */
82
+ export type RelationMap<Shape extends SchemaShape> = {
83
+ [
84
+ Key in keyof Shape as RelationDefinition<Shape[Key]> extends never ? never : Key
85
+ ]: RelationDefinition<Shape[Key]>;
86
+ };
87
+
88
+ /** Create a string ID field linked to a lazily resolved target schema. */
89
+ export const createRef = <Target extends SchemaLike>(resolve: () => Target): RefField<Target> => {
90
+ const field = z.instanceof(ObjectId) as RefField<Target>;
91
+ const createOptional = field.optional.bind(field);
92
+ const createNullable = field.nullable.bind(field);
93
+ const createNullish = field.nullish.bind(field);
94
+ Object.defineProperty(field, '__ref', {
95
+ configurable: false,
96
+ enumerable: false,
97
+ value: { resolve },
98
+ });
99
+ Object.defineProperty(field, 'optional', {
100
+ configurable: false,
101
+ enumerable: false,
102
+ value: () => attachRef(createOptional(), resolve) as OptionalRefField<Target>,
103
+ });
104
+ Object.defineProperty(field, 'nullable', {
105
+ configurable: false,
106
+ enumerable: false,
107
+ value: () => attachRef(createNullable(), resolve),
108
+ });
109
+ Object.defineProperty(field, 'nullish', {
110
+ configurable: false,
111
+ enumerable: false,
112
+ value: () => attachRef(createNullish(), resolve),
113
+ });
114
+ return field;
115
+ };
116
+
117
+ const attachRef = <Field extends z.ZodType, Target extends SchemaLike>(
118
+ field: Field,
119
+ resolve: () => Target,
120
+ ): Field & { readonly __ref?: RefDefinition<Target> } => {
121
+ Object.defineProperty(field, '__ref', {
122
+ configurable: false,
123
+ enumerable: false,
124
+ value: { resolve },
125
+ });
126
+ return field as Field & { readonly __ref?: RefDefinition<Target> };
127
+ };
128
+
129
+ /** Collect relation metadata from a schema shape without evaluating targets. */
130
+ export const collectRefs = <Shape extends SchemaShape>(shape: Shape): RelationMap<Shape> => {
131
+ return Object.fromEntries(
132
+ Object.entries(shape)
133
+ .filter(([, field]) => '__ref' in field)
134
+ .map(([name, field]) => [name, (field as RefField).__ref]),
135
+ ) as unknown as RelationMap<Shape>;
136
+ };
@@ -0,0 +1,144 @@
1
+ import type { ObjectId } from 'mongodb';
2
+
3
+ import type { PopulateSpecs } from '../query/query.js';
4
+ import type { SchemaShape } from '../schema/contracts.js';
5
+ import type { InferShape } from '../schema/inference.js';
6
+ import type { Schema, ScopeDefinitions } from '../schema/schema.js';
7
+ import type { SchemaRelation, SchemaRelationMap, SchemaLike } from './definitions.js';
8
+
9
+ type ObjectIdKeys<Shape extends SchemaShape> = {
10
+ [Key in keyof InferShape<Schema<Shape>>]-?: NonNullable<
11
+ InferShape<Schema<Shape>>[Key]
12
+ > extends ObjectId
13
+ ? Key
14
+ : never;
15
+ }[keyof InferShape<Schema<Shape>>];
16
+
17
+ export type RelationDefinitions<Registry extends Record<string, SchemaLike>> = {
18
+ [Name in keyof Registry]?: Registry[Name] extends Schema<infer Shape, any, any, any>
19
+ ? Partial<Record<Extract<ObjectIdKeys<Shape>, string>, Extract<keyof Registry, string>>>
20
+ : never;
21
+ };
22
+
23
+ type EnrichedSchema<
24
+ Registry extends Record<string, SchemaLike>,
25
+ AllDefinitions extends RelationDefinitions<Registry>,
26
+ Name,
27
+ > = Name extends keyof Registry
28
+ ? Registry[Name] extends Schema<infer Shape, infer Relations, infer Scopes, infer Options>
29
+ ? Schema<
30
+ Shape,
31
+ Relations & RelationsFor<Registry, AllDefinitions, NonNullable<AllDefinitions[Name]>>,
32
+ Scopes,
33
+ Options
34
+ >
35
+ : never
36
+ : never;
37
+
38
+ type RelationsFor<
39
+ Registry extends Record<string, SchemaLike>,
40
+ AllDefinitions extends RelationDefinitions<Registry>,
41
+ Definitions,
42
+ > = {
43
+ [Field in keyof Definitions & string]: Definitions[Field] extends keyof Registry
44
+ ? SchemaRelation<EnrichedSchema<Registry, AllDefinitions, Definitions[Field]>, Field, '_id'>
45
+ : never;
46
+ };
47
+
48
+ type RegistryWithRelations<
49
+ Registry extends Record<string, SchemaLike>,
50
+ Definitions extends RelationDefinitions<Registry>,
51
+ > = {
52
+ [Name in keyof Registry]: Registry[Name] extends Schema<
53
+ infer Shape,
54
+ infer Relations,
55
+ infer Scopes,
56
+ infer Options
57
+ >
58
+ ? Schema<
59
+ Shape,
60
+ Relations & RelationsFor<Registry, Definitions, NonNullable<Definitions[Name]>>,
61
+ Scopes,
62
+ Options
63
+ >
64
+ : Registry[Name];
65
+ };
66
+
67
+ type RelationMapOf<Value> = Value extends Schema<any, infer Relations, any, any> ? Relations : {};
68
+
69
+ export type ScopeDefinitionsBySchema<Registry extends Record<string, SchemaLike>> = {
70
+ [Name in keyof Registry]?: Record<string, PopulateSpecs<RelationMapOf<Registry[Name]>>>;
71
+ };
72
+
73
+ type RegistryWithScopes<
74
+ Registry extends Record<string, SchemaLike>,
75
+ Definitions extends ScopeDefinitionsBySchema<Registry>,
76
+ > = {
77
+ [Name in keyof Registry]: Registry[Name] extends Schema<
78
+ infer Shape,
79
+ infer Relations,
80
+ infer Scopes,
81
+ infer Options
82
+ >
83
+ ? Schema<
84
+ Shape,
85
+ Relations,
86
+ Scopes & (Definitions[Name] extends ScopeDefinitions ? Definitions[Name] : {}),
87
+ Options
88
+ >
89
+ : Registry[Name];
90
+ };
91
+
92
+ export type SchemaRegistryBuilder<Registry extends Record<string, SchemaLike>> = Registry & {
93
+ readonly __registry?: Registry;
94
+ defineRelations<const Definitions extends RelationDefinitions<Registry>>(
95
+ definitions: Definitions,
96
+ ): SchemaRegistryBuilder<RegistryWithRelations<Registry, Definitions>>;
97
+ defineScopes<const Definitions extends ScopeDefinitionsBySchema<Registry>>(
98
+ definitions: Definitions,
99
+ ): SchemaRegistryBuilder<RegistryWithScopes<Registry, Definitions>>;
100
+ };
101
+
102
+ const attachMethods = <Registry extends Record<string, SchemaLike>>(
103
+ registry: Registry,
104
+ ): SchemaRegistryBuilder<Registry> => {
105
+ const defineRelations = (definitions: RelationDefinitions<Registry>) => {
106
+ for (const [name, relations] of Object.entries(definitions)) {
107
+ const targetSchema = registry[name];
108
+ if (!targetSchema) throw new Error(`Unknown schema "${name}" in relation definitions`);
109
+ for (const [field, targetName] of Object.entries(relations ?? {})) {
110
+ const target = registry[targetName as string];
111
+ if (!target) throw new Error(`Unknown relation target "${targetName}"`);
112
+ if (!(field in targetSchema.definition.shape)) {
113
+ throw new Error(`Unknown relation field "${name}.${field}"`);
114
+ }
115
+ (targetSchema.relationMap as SchemaRelationMap)[field] = {
116
+ resolve: () => target,
117
+ localField: field,
118
+ foreignField: '_id',
119
+ };
120
+ }
121
+ }
122
+ return attachMethods(registry);
123
+ };
124
+
125
+ const defineScopes = (definitions: ScopeDefinitionsBySchema<Registry>) => {
126
+ for (const [name, scopes] of Object.entries(definitions)) {
127
+ const targetSchema = registry[name];
128
+ if (!targetSchema) throw new Error(`Unknown schema "${name}" in scope definitions`);
129
+ Object.assign(targetSchema.scopeMap, scopes);
130
+ }
131
+ return attachMethods(registry);
132
+ };
133
+
134
+ Object.defineProperties(registry, {
135
+ __registry: { configurable: false, enumerable: false, value: registry },
136
+ defineRelations: { configurable: true, enumerable: false, value: defineRelations },
137
+ defineScopes: { configurable: true, enumerable: false, value: defineScopes },
138
+ });
139
+ return registry as SchemaRegistryBuilder<Registry>;
140
+ };
141
+
142
+ export const createSchemaRegistry = <const Registry extends Record<string, SchemaLike>>(
143
+ registry: Registry,
144
+ ): SchemaRegistryBuilder<Registry> => attachMethods(registry);
@@ -0,0 +1,7 @@
1
+ import type { z } from 'zod';
2
+
3
+ /** A Zod object shape used as the source of truth for a model. */
4
+ export type SchemaShape = z.ZodRawShape;
5
+
6
+ /** The runtime Zod object generated from a schema shape. */
7
+ export type SchemaDefinition<Shape extends SchemaShape> = z.ZodObject<Shape>;
@@ -0,0 +1,22 @@
1
+ export { Schema } from './schema.js';
2
+ export { hasSoftDelete } from './schema.js';
3
+ export type {
4
+ ManagedField,
5
+ SchemaIndex,
6
+ SchemaIndexFields,
7
+ SchemaOptions,
8
+ ScopeDefinitions,
9
+ SoftDeleteEnabled,
10
+ } from './schema.js';
11
+ export type { Infer, InferInput, InferShape } from './inference.js';
12
+ export type { SchemaDefinition, SchemaShape } from './contracts.js';
13
+ export type {
14
+ RefDefinition,
15
+ RefField,
16
+ RelationInput,
17
+ RelationInputTarget,
18
+ RelationMap,
19
+ SchemaLike,
20
+ SchemaRelation,
21
+ SchemaRelationMap,
22
+ } from '../relations/definitions.js';
@@ -0,0 +1,17 @@
1
+ import type { ObjectId } from 'mongodb';
2
+ import type { z } from 'zod';
3
+
4
+ /** The raw parsed shape produced by a schema before persistence fields are added. */
5
+ export type InferShape<T> = T extends { definition: infer Definition extends z.ZodType }
6
+ ? z.infer<Definition>
7
+ : never;
8
+
9
+ /** The input type accepted by a schema before Zod defaults are applied. */
10
+ export type InferInput<T> = T extends { definition: infer Definition extends z.ZodType }
11
+ ? z.input<Definition>
12
+ : never;
13
+
14
+ /** The persisted document type inferred from a schema. */
15
+ export type Infer<T> = InferShape<T> & {
16
+ _id: ObjectId;
17
+ };
@@ -0,0 +1,110 @@
1
+ import { ObjectId } from 'mongodb';
2
+ import { z } from 'zod';
3
+
4
+ declare module 'zod' {
5
+ interface ZodType {
6
+ hidden(): this & { readonly __hidden: true };
7
+ }
8
+ }
9
+
10
+ if (!Object.prototype.hasOwnProperty.call(z.ZodType.prototype, 'hidden')) {
11
+ Object.defineProperty(z.ZodType.prototype, 'hidden', {
12
+ configurable: false,
13
+ enumerable: false,
14
+ value(this: z.ZodType) {
15
+ Object.defineProperty(this, '__hidden', {
16
+ configurable: false,
17
+ enumerable: false,
18
+ value: true,
19
+ });
20
+ return this;
21
+ },
22
+ });
23
+ }
24
+
25
+ /** A schema field that can be marked as hidden from default query results. */
26
+ export type HiddenCapable<T extends z.ZodType> = Omit<T, 'optional' | 'nullable' | 'nullish'> &
27
+ HiddenMethods<T>;
28
+
29
+ /** A schema field marked as hidden from default query results. */
30
+ export type HiddenSchema<T extends z.ZodType> = Omit<T, 'optional' | 'nullable' | 'nullish'> &
31
+ HiddenMethods<T> & {
32
+ readonly __hidden: true;
33
+ };
34
+
35
+ type HiddenMethods<T extends z.ZodType> = {
36
+ hidden(): HiddenSchema<T>;
37
+ optional(): HiddenCapable<z.ZodOptional<T>>;
38
+ nullable(): HiddenCapable<z.ZodNullable<T>>;
39
+ nullish(): HiddenCapable<z.ZodOptional<z.ZodNullable<T>>>;
40
+ };
41
+
42
+ export const withHidden = <T extends z.ZodType>(schema: T): HiddenCapable<T> => {
43
+ return new Proxy(schema, {
44
+ get(target, property, receiver) {
45
+ if (property === 'hidden') {
46
+ return () => {
47
+ Object.defineProperty(target, '__hidden', {
48
+ configurable: false,
49
+ enumerable: false,
50
+ value: true,
51
+ });
52
+ return receiver;
53
+ };
54
+ }
55
+ const value = Reflect.get(target, property, receiver);
56
+ if (typeof value !== 'function') return value;
57
+ return (...args: unknown[]) => {
58
+ const result = value.apply(target, args);
59
+ return result instanceof z.ZodType ? withHidden(result) : result;
60
+ };
61
+ },
62
+ }) as unknown as HiddenCapable<T>;
63
+ };
64
+
65
+ /** Wrap native Zod constructors so returned schemas support `.hidden()`. */
66
+ export const withZodNamespace = <T extends object>(namespace: T): T =>
67
+ new Proxy(namespace, {
68
+ get(target, property, receiver) {
69
+ const value = Reflect.get(target, property, receiver);
70
+ if (
71
+ typeof value !== 'function' ||
72
+ String(property)[0] !== String(property)[0].toLowerCase()
73
+ ) {
74
+ return value;
75
+ }
76
+ return (...args: unknown[]) => {
77
+ const result = value.apply(target, args);
78
+ return result instanceof z.ZodType ? withHidden(result) : result;
79
+ };
80
+ },
81
+ });
82
+
83
+ /** Create a string schema. */
84
+ export const string = () => withHidden(z.string());
85
+
86
+ /** Create an email schema. */
87
+ export const email = () => withHidden(z.email());
88
+
89
+ /** Create a URL schema. */
90
+ export const url = () => withHidden(z.url());
91
+
92
+ /** Create a number schema. */
93
+ export const number = () => withHidden(z.number());
94
+
95
+ /** Create a boolean schema. */
96
+ export const boolean = () => withHidden(z.boolean());
97
+
98
+ /** Create a date schema. */
99
+ export const date = () => withHidden(z.date());
100
+
101
+ /** Create a MongoDB ObjectId schema. */
102
+ export const objectId = () => withHidden(z.instanceof(ObjectId));
103
+
104
+ /** Create a nested object schema. */
105
+ export const object = <const Shape extends z.ZodRawShape>(shape: Shape) =>
106
+ withHidden(z.object(shape));
107
+
108
+ /** Create a string enum schema while preserving literal members. */
109
+ export const enumeration = <const Values extends readonly [string, ...string[]]>(values: Values) =>
110
+ withHidden(z.enum(values));
@@ -0,0 +1,234 @@
1
+ import type { IndexDescription, IndexDirection, ObjectId } from 'mongodb';
2
+ import { z } from 'zod';
3
+
4
+ import type { PopulateSpecs } from '../query/query.js';
5
+ import { collectRefs } from '../relations/definitions.js';
6
+ import type {
7
+ RelationInput,
8
+ RelationInputTarget,
9
+ RelationMap,
10
+ SchemaRelation,
11
+ SchemaRelationMap,
12
+ SchemaLike,
13
+ } from '../relations/definitions.js';
14
+ import type { SchemaDefinition, SchemaShape } from './contracts.js';
15
+ import type { InferShape } from './inference.js';
16
+
17
+ /** Built-in persistence behavior applied by a schema. */
18
+ export interface SchemaOptions {
19
+ readonly timestamps?: boolean;
20
+ readonly softdelete?: boolean;
21
+ }
22
+
23
+ export type SchemaIndexFields<Shape extends SchemaShape> = Partial<
24
+ Record<Extract<keyof Shape, string>, IndexDirection>
25
+ >;
26
+
27
+ export type SchemaIndex<Shape extends SchemaShape> = {
28
+ readonly fields: SchemaIndexFields<Shape>;
29
+ readonly options?: Omit<IndexDescription, 'key'>;
30
+ };
31
+
32
+ type TimestampShape = {
33
+ createdAt: z.ZodDefault<z.ZodDate>;
34
+ updatedAt: z.ZodDefault<z.ZodDate>;
35
+ };
36
+
37
+ type SoftDeleteShape = {
38
+ deletedAt: z.ZodDefault<z.ZodNullable<z.ZodDate>>;
39
+ };
40
+
41
+ type ManagedShape<Options extends SchemaOptions> = (Options['timestamps'] extends true
42
+ ? TimestampShape
43
+ : {}) &
44
+ (Options['softdelete'] extends true ? SoftDeleteShape : {});
45
+
46
+ export type ManagedField<Options extends SchemaOptions> =
47
+ | (Options['timestamps'] extends true ? 'createdAt' | 'updatedAt' : never)
48
+ | (Options['softdelete'] extends true ? 'deletedAt' : never);
49
+
50
+ export type SoftDeleteEnabled<Options extends SchemaOptions> = Options['softdelete'] extends true
51
+ ? true
52
+ : false;
53
+
54
+ export const hasSoftDelete = (options: SchemaOptions): boolean => options.softdelete === true;
55
+
56
+ type ObjectIdFieldKeys<Shape extends SchemaShape> = {
57
+ [Key in keyof InferShape<Schema<Shape>>]-?: NonNullable<
58
+ InferShape<Schema<Shape>>[Key]
59
+ > extends ObjectId
60
+ ? Key
61
+ : never;
62
+ }[keyof InferShape<Schema<Shape>>];
63
+
64
+ export type ScopeDefinitions = Record<string, readonly object[]>;
65
+
66
+ /** A typed, runtime-validated schema definition. */
67
+ export class Schema<
68
+ Shape extends SchemaShape,
69
+ Relations extends SchemaRelationMap = {},
70
+ Scopes extends ScopeDefinitions = {},
71
+ Options extends SchemaOptions = {},
72
+ > {
73
+ /** The underlying Zod object for advanced validation use cases. */
74
+ readonly definition: SchemaDefinition<Shape>;
75
+
76
+ /** The lazily resolved relation metadata declared by this schema. */
77
+ readonly refs: RelationMap<Shape>;
78
+
79
+ /** One-way relation metadata declared for this schema. */
80
+ readonly relationMap: Relations;
81
+
82
+ /** Named population scopes declared for this schema. */
83
+ scopeMap: Scopes;
84
+
85
+ /** Field names excluded from default query results. */
86
+ readonly hiddenFields: readonly (keyof Shape & string)[];
87
+
88
+ /** Field names declared by this schema. */
89
+ readonly fields: readonly (keyof Shape & string)[];
90
+
91
+ /** Persistence behavior enabled for this schema. */
92
+ readonly optionsConfig: Options;
93
+
94
+ /** MongoDB indexes declared for this schema. */
95
+ indexDefinitions: readonly SchemaIndex<Shape>[];
96
+
97
+ /** Preserve schema options through registry type transformations. */
98
+ declare readonly __options: Options;
99
+
100
+ /** Construct a schema from a Zod object shape. */
101
+ constructor(
102
+ shape: Shape,
103
+ relations = {} as Relations,
104
+ scopeMap = {} as Scopes,
105
+ optionsConfig = {} as Options,
106
+ indexDefinitions = [] as readonly SchemaIndex<Shape>[],
107
+ ) {
108
+ this.definition = z.object(shape);
109
+ this.refs = collectRefs(shape);
110
+ this.relationMap = relations;
111
+ this.scopeMap = scopeMap;
112
+ this.fields = Object.keys(shape) as (keyof Shape & string)[];
113
+ this.hiddenFields = this.fields.filter((field) => '__hidden' in shape[field]);
114
+ this.optionsConfig = optionsConfig;
115
+ this.indexDefinitions = indexDefinitions;
116
+ }
117
+
118
+ /** Enable managed timestamps and/or soft deletion for this schema. */
119
+ options<const Enabled extends SchemaOptions>(
120
+ options: Enabled,
121
+ ): Schema<Shape & ManagedShape<Enabled>, Relations, Scopes, Enabled> {
122
+ if (this.optionsConfig && Object.keys(this.optionsConfig).length > 0) {
123
+ throw new Error('Schema options can only be configured once');
124
+ }
125
+ if (
126
+ options.timestamps &&
127
+ ('createdAt' in this.definition.shape || 'updatedAt' in this.definition.shape)
128
+ ) {
129
+ throw new Error('Timestamp fields createdAt and updatedAt are managed by Mongorm');
130
+ }
131
+ if (options.softdelete && 'deletedAt' in this.definition.shape) {
132
+ throw new Error('The deletedAt field is managed by Mongorm');
133
+ }
134
+
135
+ const managedShape = {
136
+ ...(options.timestamps
137
+ ? {
138
+ createdAt: z.date().default(() => new Date()),
139
+ updatedAt: z.date().default(() => new Date()),
140
+ }
141
+ : {}),
142
+ ...(options.softdelete ? { deletedAt: z.date().nullable().default(null) } : {}),
143
+ } as ManagedShape<Enabled>;
144
+ const next = new Schema(
145
+ { ...this.definition.shape, ...managedShape } as Shape & ManagedShape<Enabled>,
146
+ this.relationMap,
147
+ this.scopeMap,
148
+ options,
149
+ this.indexDefinitions,
150
+ );
151
+ return next as unknown as Schema<Shape & ManagedShape<Enabled>, Relations, Scopes, Enabled>;
152
+ }
153
+
154
+ /** Declare MongoDB indexes for explicit synchronization with the database. */
155
+ indexes<const Definitions extends readonly SchemaIndex<Shape>[]>(definitions: Definitions): this {
156
+ if (definitions.some(({ fields }) => Object.keys(fields).length === 0)) {
157
+ throw new Error('Index definitions must include at least one field');
158
+ }
159
+ this.indexDefinitions = definitions;
160
+ return this;
161
+ }
162
+
163
+ /** Add one or more one-way relations without requiring circular schema declarations. */
164
+ relations<
165
+ const Definitions extends Partial<
166
+ Record<Extract<ObjectIdFieldKeys<Shape>, string>, RelationInput>
167
+ >,
168
+ >(
169
+ definitions: Definitions &
170
+ Record<Exclude<keyof Definitions, Extract<ObjectIdFieldKeys<Shape>, string>>, never>,
171
+ ): Schema<
172
+ Shape,
173
+ Relations & {
174
+ [Name in keyof Definitions]: SchemaRelation<
175
+ RelationInputTarget<NonNullable<Definitions[Name]>>,
176
+ Extract<Name, string>,
177
+ NonNullable<Definitions[Name]> extends { foreignField: infer Foreign extends string }
178
+ ? Foreign
179
+ : '_id'
180
+ >;
181
+ },
182
+ Scopes,
183
+ Options
184
+ > {
185
+ for (const [name, input] of Object.entries(definitions)) {
186
+ const definition = (typeof input === 'function' ? { target: input } : input) as {
187
+ target: () => SchemaLike;
188
+ foreignField?: string;
189
+ };
190
+ (this.relationMap as SchemaRelationMap)[name] = {
191
+ resolve: definition.target,
192
+ localField: name,
193
+ foreignField: definition.foreignField ?? '_id',
194
+ };
195
+ }
196
+ return this as unknown as Schema<
197
+ Shape,
198
+ Relations & {
199
+ [Name in keyof Definitions]: SchemaRelation<
200
+ RelationInputTarget<NonNullable<Definitions[Name]>>,
201
+ Extract<Name, string>,
202
+ NonNullable<Definitions[Name]> extends { foreignField: infer Foreign extends string }
203
+ ? Foreign
204
+ : '_id'
205
+ >;
206
+ },
207
+ Scopes,
208
+ Options
209
+ >;
210
+ }
211
+
212
+ /** Declare named, reusable population scopes. */
213
+ scopes<const Definitions extends Record<string, PopulateSpecs<Relations>>>(
214
+ definitions: Definitions,
215
+ ): Schema<Shape, Relations, Definitions, Options> {
216
+ this.scopeMap = definitions as unknown as Scopes;
217
+ return this as unknown as Schema<Shape, Relations, Definitions, Options>;
218
+ }
219
+
220
+ /** Parse unknown input and return the inferred document type. */
221
+ parse(input: unknown): InferShape<this> {
222
+ return this.definition.parse(input) as InferShape<this>;
223
+ }
224
+
225
+ /** Parse a partial document for update operations. */
226
+ parsePartial(input: unknown): Partial<InferShape<this>> {
227
+ return this.definition.partial().parse(input) as Partial<InferShape<this>>;
228
+ }
229
+
230
+ /** Parse unknown input without throwing on validation failure. */
231
+ safeParse(input: unknown): ReturnType<typeof this.definition.safeParse> {
232
+ return this.definition.safeParse(input);
233
+ }
234
+ }