javascriptnestjsprismaes6-class

Extending a JavaScript ES6+ class that explicitly returns an object in its constructor


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.


Solution

  • 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.