I'm having issue with extending a Javascript ES6+ class that returns an object from the constructor.
Take for example:
class Base {
constructor() {
return {name: "base"}
}
}
class Extension extends Base {
constructor() {
super()
}
saySomething() {
console.log("What do you want me to say?")
}
}
Creating a new instance of Extension
class and calling saySomething
fails with an error, because for whatever reason, which I think is related to base class returning a value in its constructor, the method saySomething
does not seem to exist (in fact nothing other than just what belongs to base class) on Extension
class prototype.
The major problem that has led to this base problem was me trying to create an extended PrismaClient
as seen here, with client extension in NestJS, this certain way which results in other methods on the PrismaService
class to not exist on the prototype chain.
function PrismaExtended() {
type Params = ConstructorParameters<typeof PrismaClient<Prisma.PrismaClientOptions, Prisma.LogLevel>>;
const client = (...args: Params) =>
new PrismaClient<Prisma.PrismaClientOptions, Prisma.LogLevel>(...args).$extends({
result: {
$allModels: {
toJSON: {
compute(data: BaseEntity) {
return <T extends typeof data = typeof data>() => {
return Object.assign({}, data, {
sn: typeof data.sn === 'bigint' ? data.sn.toString() : data.sn,
}) as unknown as Omit<T, 'sn'> & { sn: string };
};
},
},
},
},
});
class PrismaExtended {
constructor(...args: Params) {
return client(...args);
}
}
// Object.setPrototypeOf(PrismaExtended, PrismaClient.prototype);
return PrismaExtended as new (...args: Params) => ReturnType<typeof client>;
}
@Injectable()
export class PrismaService extends PrismaExtended() implements OnModuleInit, OnModuleDestroy {
private retries: number = 0;
constructor(
private readonly logger: AppLogger,
configService: ConfigService,
) {
super({
log: pickFrom(configService, 'app.status') !== NodeEnv.PROD ? ['query'] : undefined,
transactionOptions: { timeout: 60_000 },
});
}
/**
* Check if an error is as a result of a precodition necessary to run an
* operation not being matched.
*
* @param e The error to check
* @returns boolean
*/
isUnmatchedPrecondition(e: unknown): e is PrismaClientKnownRequestError {
return e instanceof PrismaClientKnownRequestError && e.code === PrismaErrorCode.UNMATCHED_PRECONDITION;
}
}
Here the method, isUnmatchedPrecondition
, and any other method added don't exist at runtime.
I'm now able to get this to work by using Proxy
.
Using double Proxies, one for intercepting object construction when the new keyword is used on PrismaService
, and because the constructed object from the Proxy
which was supposed to inherit from PrismaClient
ideally, has no methods or properties of the "supposed base class" (because no inheritance of PrismaClient
occured), which has already been constructed outside of the child class constructor context, I've had to keep a reference of the "supposed base class" and maintain a proxy to the constructed child class such that when any method or property is accessed on the child class instance and it's not found, it is redirected to the supposed base class whose instance I hold a reference to in the outermost proxy at the point where getClient
was invoked.
function PrismaExtended() {
type Params = ConstructorParameters<typeof PrismaClient<Prisma.PrismaClientOptions, Prisma.LogLevel>>;
const getClient = (...args: Params) => {
return new PrismaClient<Prisma.PrismaClientOptions, Prisma.LogLevel>(...args).$extends({
result: {
$allModels: {
toJSON: {
compute(data: BaseEntity) {
return <T extends typeof data = typeof data>() => {
return Object.assign({}, data, {
sn: typeof data.sn === 'bigint' ? data.sn.toString() : data.sn,
}) as unknown as Omit<T, 'sn'> & { sn: string };
};
},
},
},
},
});
};
return new Proxy(class {}, {
construct(target, args, newTarget) {
const client = getClient(...args);
const constructed = Reflect.construct(target, args, newTarget);
return new Proxy(constructed, {
get(target, key) {
if (key in target) return target[key];
return client[key];
},
});
},
}) as unknown as new (...args: Params) => ReturnType<typeof getClient>;
}
For now, this seems to work fine and I've not recorded any "catch" so far. But, hey, if you do notice something I'm missing, any pitfalls whatsoe'er, about this solution, do let me know.
Thanks and thanks to everyone that contributed to helping me out.