-
-
Notifications
You must be signed in to change notification settings - Fork 135
Does typegoose support TypeScript's mixins (multiple inheritence)? #936
Replies: 1 comment · 5 replies
-
i have personally not used mixins and they are not tested in typegoose, but from what i read i dont see a reason on why they shouldnt work, you will just need to in addition to the normal properties also copy / merge the reflection metadata, as that is how the decorators work. btw, running |
Beta Was this translation helpful? Give feedback.
All reactions
-
I have created a reproducible example that doesn't work, what am I missing? /* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable jsdoc/require-jsdoc */
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import {
getModelForClass, modelOptions, prop, type Ref,
} from '@typegoose/typegoose';
import 'reflect-metadata'; // eslint-disable-line import/no-unassigned-import
class Father {
@prop({required: true})
fatherId!: string;
}
class Mother {
@prop({required: true})
motherId!: string;
}
class FatherReference {
@prop({
ref: () => Father, localField: 'fatherId', foreignField: 'fatherId', justOne: true,
})
father?: Ref<Father>;
}
class MotherReference {
@prop({
ref: () => Mother, localField: 'motherId', foreignField: 'motherId', justOne: true,
})
mother?: Ref<Mother>;
}
@modelOptions({schemaOptions: {collection: 'children'}})
class Children {
@prop({required: true, type: () => String})
name!: string;
}
function applyMixins(target: any, sources: any) {
for (const baseCtor of sources) {
for (const name of Object.getOwnPropertyNames(baseCtor.prototype)) {
const descriptor = Object.getOwnPropertyDescriptor(baseCtor.prototype, name);
if (!descriptor) {
continue;
}
Object.defineProperty(target.prototype, name, descriptor);
const metadataKeys = Reflect.getMetadataKeys(baseCtor.prototype, name);
for (const key of metadataKeys) {
const metadataValue = Reflect.getMetadata(key, baseCtor.prototype, name);
Reflect.defineMetadata(key, metadataValue, target.prototype, name);
}
}
}
}
interface Children extends FatherReference, MotherReference {}
applyMixins(Children, [FatherReference, MotherReference]);
const ChildrenModel = getModelForClass(Children);
console.debug(ChildrenModel.schema.paths); When I run this I get this printed: {
name: SchemaString {
enumValues: [],
regExp: null,
path: 'name',
instance: 'String',
validators: [ [Object] ],
getters: [],
setters: [],
_presplitPath: [ 'name' ],
options: SchemaStringOptions { required: true, type: [Function: String] },
_index: null,
isRequired: true,
requiredValidator: [Function (anonymous)],
originalRequiredValue: true,
[Symbol(mongoose#schemaType)]: true
},
_id: SchemaObjectId {
path: '_id',
instance: 'ObjectId',
validators: [],
getters: [],
setters: [ [Function: resetId] ],
_presplitPath: [ '_id' ],
options: SchemaObjectIdOptions { auto: true, type: 'ObjectId' },
_index: null,
defaultValue: [Function: defaultId] { '$runBeforeSetters': true },
[Symbol(mongoose#schemaType)]: true
},
__v: SchemaNumber {
path: '__v',
instance: 'Number',
validators: [],
getters: [],
setters: [],
_presplitPath: [ '__v' ],
options: SchemaNumberOptions { type: [Function: Number] },
_index: null,
[Symbol(mongoose#schemaType)]: true
}
} The properties |
Beta Was this translation helpful? Give feedback.
All reactions
-
I managed to get one step closer by attempting to write my own (horrendous) import 'reflect-metadata'; // eslint-disable-line import/no-unassigned-import
type Constructor = new (...arguments: any[]) => any;
function applyMixins(target: Constructor, constructors: Constructor[]) {
const decorators = {};
for (const constructor of [...constructors, target]) {
const instance = new constructor();
for (const property of Object.getOwnPropertyNames(instance)) {
const descriptor = Object.getOwnPropertyDescriptor(instance, property)!;
Object.defineProperty(target.prototype, property, descriptor);
target.prototype[property] = instance[property];
}
for (const key of Reflect.getMetadataKeys(instance)) {
if (!Object.hasOwn(decorators, key)) {
(decorators as any)[key] = new Map<string, any>();
}
const metadata: Map<string, any> = Reflect.getMetadata(key, instance);
for (const [name, value] of metadata) {
(decorators as any)[key].set(name, value);
}
}
}
for (const [key, value] of Object.entries(decorators)) {
Reflect.defineMetadata(key, value, target.prototype);
}
return target;
} The metadata is definitely present in the resulting class, and non |
Beta Was this translation helpful? Give feedback.
All reactions
-
as you likely had figured out, the script you provided there always overwrote the previously set value, so only the latest applies
i dont quite understand what you mean with that, from what i can tell in the (second) code, you still overwrite the previous properties whole, you will need to special-case typegoose's properties key and merge the inner maps if you want both options. |
Beta Was this translation helpful? Give feedback.
All reactions
-
I still can't make it work :/ This is my current (reproducible) attempt: /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable import/no-unassigned-import */
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable jsdoc/require-jsdoc */
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import 'reflect-metadata';
import {
getModelForClass, modelOptions, prop, type Ref,
} from '@typegoose/typegoose';
class Father {
@prop({required: true})
fatherId!: string;
}
class Mother {
@prop({required: true})
motherId!: string;
}
class FatherReference {
@prop({
ref: () => Father, localField: 'fatherId', foreignField: 'fatherId', justOne: true,
})
father?: Ref<Father>;
}
class MotherReference {
@prop({
ref: () => Mother, localField: 'motherId', foreignField: 'motherId', justOne: true,
})
mother?: Ref<Mother>;
}
@modelOptions({schemaOptions: {collection: 'children'}})
class Children {
@prop({required: true, type: () => String})
name!: string;
}
function applyMixins(target: any, sources: any[]): void {
for (const source of sources) {
for (const name of Object.getOwnPropertyNames(source.prototype)) {
const descriptor = Object.getOwnPropertyDescriptor(source.prototype, name);
if (descriptor) {
Object.defineProperty(target.prototype, name, descriptor);
}
// Copy and merge metadata, with special handling for Typegoose's @prop metadata
const keys = Reflect.getMetadataKeys(source.prototype, name);
for (const key of keys) {
const sourceMetadata = Reflect.getMetadata(key, source.prototype, name);
if (Array.isArray(sourceMetadata)) {
// For array-based metadata, concatenate arrays
const existingMetadata = Reflect.getMetadata(key, target.prototype, name) || [];
Reflect.defineMetadata(key, existingMetadata.concat(sourceMetadata), target.prototype, name);
} else {
// For object-based metadata, merge objects
const existingMetadata = Reflect.getMetadata(key, target.prototype, name) || {};
Reflect.defineMetadata(key, {...existingMetadata, ...sourceMetadata}, target.prototype, name);
}
}
}
}
}
interface Children extends FatherReference, MotherReference {}
applyMixins(Children, [FatherReference, MotherReference]);
const ChildrenModel = getModelForClass(Children);
console.debug(ChildrenModel.schema.paths); Running this code prints: {
name: SchemaString {
enumValues: [],
regExp: null,
path: 'name',
instance: 'String',
validators: [ [Object] ],
getters: [],
setters: [],
_presplitPath: [ 'name' ],
options: SchemaStringOptions { required: true, type: [Function: String] },
_index: null,
isRequired: true,
requiredValidator: [Function (anonymous)],
originalRequiredValue: true,
[Symbol(mongoose#schemaType)]: true
},
_id: SchemaObjectId {
path: '_id',
instance: 'ObjectId',
validators: [],
getters: [],
setters: [ [Function: resetId] ],
_presplitPath: [ '_id' ],
options: SchemaObjectIdOptions { auto: true, type: 'ObjectId' },
_index: null,
defaultValue: [Function: defaultId] { '$runBeforeSetters': true },
[Symbol(mongoose#schemaType)]: true
},
__v: SchemaNumber {
path: '__v',
instance: 'Number',
validators: [],
getters: [],
setters: [],
_presplitPath: [ '__v' ],
options: SchemaNumberOptions { type: [Function: Number] },
_index: null,
[Symbol(mongoose#schemaType)]: true
}
} which is missing the Any chance you could help out with the |
Beta Was this translation helpful? Give feedback.
All reactions
-
logging the paths of a schema will never make the properties of your source classes to show as they only define virtuals, you would need to check aside from the above, your script makes the assumption typegoose defines its here is a example of only translating // NodeJS: 21.6.2
// MongoDB: 5.0 (Docker)
// Typescript 5.3.3
import { Ref, getModelForClass, modelOptions, prop, setLogLevel } from '@typegoose/typegoose'; // @typegoose/[email protected]
import { DecoratorKeys } from '@typegoose/typegoose/lib/internal/constants';
import { DecoratedPropertyMetadataMap } from '@typegoose/typegoose/lib/types';
// import * as mongoose from 'mongoose'; // [email protected]
setLogLevel('DEBUG');
class Father {
@prop({ required: true })
fatherId!: string;
}
class Mother {
@prop({ required: true })
motherId!: string;
}
class FatherReference {
@prop({
ref: () => Father,
localField: 'fatherId',
foreignField: 'fatherId',
justOne: true,
})
father?: Ref<Father>;
}
class MotherReference {
@prop({
ref: () => Mother,
localField: 'motherId',
foreignField: 'motherId',
justOne: true,
})
mother?: Ref<Mother>;
}
@modelOptions({ schemaOptions: { collection: 'children' } })
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class Children {
@prop({ required: true, type: () => String })
name!: string;
}
function applyMixins(target: any, sources: any[]): void {
for (const source of sources) {
for (const name of Object.getOwnPropertyNames(source.prototype)) {
// dont copy over the constructor, as that would change the "target" class itself to be that of "source"
// and so later apply reflect metadata on "source" instead of "target"
if (name === 'constructor') {
continue;
}
const descriptor = Object.getOwnPropertyDescriptor(source.prototype, name);
if (descriptor) {
Object.defineProperty(target.prototype, name, descriptor);
}
}
// Copy and merge metadata, with special handling for Typegoose's @prop metadata
const keys = Reflect.getMetadataKeys(source.prototype) as string[];
for (const key of keys) {
if (key == DecoratorKeys.PropCache) {
// no mapping needed, as the above gurantees this exists
const source_metadata: DecoratedPropertyMetadataMap = Reflect.getMetadata(DecoratorKeys.PropCache, source.prototype);
const target_map: DecoratedPropertyMetadataMap = Reflect.getOwnMetadata(DecoratorKeys.PropCache, target.prototype) ?? new Map();
for (const [source_key, source_value] of source_metadata) {
// modify target as the references are not chainable anymore
const clone = {
...source_value,
target: target.prototype,
};
// TODO: maybe dont overwrite, but merge options instead?
// TODO: maybe the mixins should not overwrite existing keys on this class
target_map.set(source_key, clone);
// also define "emitDecoratorMetadata" decorator key, needs to be done here as class keys dont exist on the constructor / prototype and so would need to be figured out
// there also does not exist any way to get all property keys, at least i am not aware of a way
const type_value = Reflect.getMetadata(DecoratorKeys.Type, source.prototype, source_key);
if (type_value !== undefined) {
Reflect.defineMetadata(DecoratorKeys.Type, type_value, target.prototype, source_key);
}
}
Reflect.defineMetadata(DecoratorKeys.PropCache, target_map, target.prototype);
}
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
interface Children extends FatherReference, MotherReference {}
applyMixins(Children, [FatherReference, MotherReference]);
const ChildrenModel = getModelForClass(Children);
console.debug(ChildrenModel.schema); logs: Schema {
paths: {
name: SchemaString {
enumValues: [],
regExp: null,
path: 'name',
instance: 'String',
validators: [Array],
getters: [],
setters: [],
_presplitPath: [Array],
options: [SchemaStringOptions],
_index: null,
isRequired: true,
requiredValidator: [Function (anonymous)],
originalRequiredValue: true,
[Symbol(mongoose#schemaType)]: true
},
_id: SchemaObjectId {
path: '_id',
instance: 'ObjectId',
validators: [],
getters: [],
setters: [Array],
_presplitPath: [Array],
options: [SchemaObjectIdOptions],
_index: null,
defaultValue: [Function],
[Symbol(mongoose#schemaType)]: true
},
__v: SchemaNumber {
path: '__v',
instance: 'Number',
validators: [],
getters: [],
setters: [],
_presplitPath: [Array],
options: [SchemaNumberOptions],
_index: null,
[Symbol(mongoose#schemaType)]: true
}
},
virtuals: {
father: VirtualType {
path: 'father',
getters: [],
setters: [Array],
options: [VirtualOptions]
},
mother: VirtualType {
path: 'mother',
getters: [],
setters: [Array],
options: [VirtualOptions]
},
id: VirtualType {
path: 'id',
getters: [Array],
setters: [],
options: {}
}
},
// ...
} |
Beta Was this translation helpful? Give feedback.
All reactions
This discussion was converted from issue #935 on May 04, 2024 10:39.
-
I have failed to find any info about it online, and have also failed getting a working example.
Is TypeScript's Mixin supported?
Can I create small helper classes like:
Then when I create a class for a collection, which has both
fatherId
andmotherId
, I could use mixins to inherit both of them like:And then be able to do:
Is it possible?
Beta Was this translation helpful? Give feedback.
All reactions