redis / redis-om-node

Object mapping, and more, for Redis and Node.js. Written in TypeScript.
MIT License
1.18k stars 80 forks source link

Best way to add custom functionality to repositories? #204

Closed humblemodulo closed 1 year ago

humblemodulo commented 1 year ago

Following similarly to issue #28 , I'm curious, with Entity no longer being a thing (per the CHANGELOG )what is the best way to embed custom functionality? Simply, extend the Schema or Repository somehow? Or is this capability simply lost? 🤔

guyroyse commented 1 year ago

You could certainly subclass Repository and override .fetch and .save to annotate and deannotate the fetched and saved JSON. Just two functions.

Although a wise person once told me to favor composition over inheritance. So maybe wrap Repository instead.

humblemodulo commented 1 year ago

Although a wise person once told me to favor composition over inheritance. So maybe wrap Repository instead.

Yeah, I'm actually considering going down the Proxy path, just so the primary functionality remains "first class" without having to duplicate/override. We'll see.

Appreciate the input. Closing, but feel free to append other thoughts.

guyroyse commented 1 year ago

I'm curious to see what you come up with. Mind sharing once you do?

humblemodulo commented 1 year ago

Hrm, so not looking good. Attempting to extend the Repository class results in errors like Property 'makeKeys' of exported class expression may not be private or protected. and the project refuses to build.

Using Proxy results in Cannot read private member #schema from an object whose class did not declare it at Proxy.search when I try to make use of it.

I could wrap each instance in an external service, but that really kneecaps me when it comes to centralizing functionality between instances and sharing between projects.

I'll noodle on it some more . . . 🤔

guyroyse commented 1 year ago

I would have thought extending would have worked. Can you not extend classes with private properties in TypeScript? Wonder if I change it to the #makeKeys syntax that JavaScript supports if it would work?

guyroyse commented 1 year ago

Tried out a simple example and it worked on my machine! ;)

class Foo {
  getHelper() {
    return this.helper()
  }

  private helper() {
    return 'helper'
  }
}

class Bar extends Foo {
  getHelper() {
    return super.getHelper()
  }
}
guyroyse commented 1 year ago

If you can find a change to Redis OM that makes this possible, I will give you Internet points. ;)

guyroyse commented 1 year ago

I was able to get a simple example to work:

import { createClient } from 'redis'
import { Entity, Schema, Repository } from 'redis-om'

export const redis = createClient()
await redis.connect()

export const schema = new Schema('bigfoot', {
  id: { type: 'string' },
  title: { type: 'text' },
  description: { type: 'text' },
  date: { type: 'date' },
  classification: { type: 'string' },
  county: { type: 'string' },
  state: { type: 'string' },
  latlng: { type: 'point' },
  temperatures: { type: 'number[]' }
})

class BigfootRepository extends Repository {
  constructor() {
    super(schema, redis)
  }

  async save(entity: Entity): Promise<Entity>
  async save(id: string, entity: Entity): Promise<Entity>
  async save(entityOrId: Entity | string, maybeEntity?: Entity): Promise<Entity> {
    if (typeof entityOrId === 'string' && maybeEntity) {
      return super.save(entityOrId, maybeEntity)
    } else if (typeof entityOrId === 'object') {
      return super.save(entityOrId)
    }
    throw "Some error"
  }

}

export const repository = new BigfootRepository()
await repository.createIndex()

Admittedly, it's not doing much, but I was able to override the .save function. The if block is to make typescript happy. You could just call save with the two args and be done with it. ;)

humblemodulo commented 1 year ago

Yeah, I tried something similar. The problem comes in situations where I don't want to limit the functionality of the repository.

Basically, I want to forward 90% of all function calls to super (without explicitly re-declaring every method), and override/append just a few.

For example, I'd like to add a final, post-fluent/builder method like returnResultsWithIds that maps the result of the current repo.search().where()...returnResultsWithIds() with EntityId.

Normally, I'd simply create a wrapper/service in whatever framework I'm currently using (e.g. Nest, Next, Nuxt), but in this case, we'll be using the repositories across multiple frameworks, so it would be nice to have certain bits of functionality in the core repo object.

Obviously in this specific case I could just create a single "wrapping" function that handles it, but that's the type of functionality I'm looking to add to all repos -- without breaking the rest of the functionality.