elysiajs / elysia

Ergonomic Framework for Humans
https://elysiajs.com
MIT License
10.58k stars 225 forks source link

[BUG] Dependency Injection of states does not work for objects #165

Closed dazraf closed 1 year ago

dazraf commented 1 year ago

Hello! :wave: :cat: I came across Elysia this weekend and rather excitedly started writing some demo code. Great work - it's very fast and easy to use! However, I came across a bug with dependency injection, as documented here. I've created this "reproducer" example code. Thank you for looking at this issue!

import Elysia from 'elysia'
import cors from '@elysiajs/cors'
import swagger from '@elysiajs/swagger'

// this is reductive reproducer to demonstrate the issue that I've found
// following the dependency injection pattern described in the documentation

/**
 * this is a 'service' that will be saved as a state in the store
 * saving this only in the top level router 
 */
class FooService {
    bar(): string {
        return 'world'
    }
}

/**
 * here's a router that assigns some state
 * this is attempting to following the dependency injection pattern 
 * as described in the docs
 */
const fooServiceState = new Elysia({ name: 'fooServiceState' })
    .state('fooService', new FooService()) // including our FooService
    .state('dummy', 'data')

/**
 * this is sub router for the / path
 * it expects to be able to use the fooService
 * but ...
 */
const helloRouter = new Elysia({ name: 'helloRouter' })
    .use(fooServiceState) // we 'inject' the type for the FooService
    .get('/', ({ store: { fooService } }) => { // we're using the fooService in the store

        // .. fooService is an empty object! 
        console.log(fooService) // <- this outputs an empty object

        // the following line throws an exception causing this response
        // status: 500
        // response body: fooService.bar is not a function. (In 'fooService.bar()', 'fooService.bar' is undefined)
        return 'hello' + fooService.bar()
    })

/**
 * top level router 
 * please note that if itry to add the FooService with a direct call to `state(...)` it works fine.
 */
const app = new Elysia({ name: "app" })
    .use(cors())
    .use(swagger())
    .use(fooServiceState) // we inject the service here
    .onBeforeHandle(route => console.log(route))
    .use(helloRouter)
    .listen({ port: 3000 })

console.log(`🦊 Elysia started: http://${app.server?.hostname}:${app.server?.port}`)
ChristoferMendes commented 1 year ago

Hello! Same error here ;/

I managed to bypass this with an arrow function:

.state("service", () => {
    return new ResponsibilityDayService(new PrismaClient());
  })
  .get(
    "/user/:id/is-responsible",
    async ({ params, store: { service } }) => {
      const { id: userId } = params;

      return service().isResponsible(Number(userId));
    }
  )
HKGx commented 1 year ago

It's a duplicate of #143

dazraf commented 1 year ago

Thank you so so very much @ChristoferMendes and @HKGx!! I was just debugging it and I saw the problem appear in mergician just as @HKGx documented a few hours ago. @ChristoferMendes I will try the arrow function. I want to see if the code caches the result of the function or if it calls it mutiple times. Btw, I've also debugged Mergician and found that this function gets called to get the object keys - the implementation fails to collect functions on the object [EDIT: class instance]. I think there's a better way to do this ...

function getObjectKeys(obj, includeProto = false) {
    if (includeProto) {
        const keys = [];
        for (const key in obj) {
            keys.push(key);
        }
        return keys;
    } else {
        return Object.keys(obj);
    }
}
HKGx commented 1 year ago

@dazraf A good writeup, I noticed the same thing.

dazraf commented 1 year ago

Closed as it's a duplicate of #143. Btw, @ChristoferMendes I tried your arrow function suggestion. Worked great, thank you. Only thing was that every call to the handler created a new service. I made a lazy evaluation like this:

function lazy<T>(fn: () => T): () => T {
    let obj: T
    return () => {
        if (!obj) {
            obj = fn()
        }
        return obj
    }
}

and used it like this:

    .state('fooService', lazy(() => new FooService())) 

This way, the construction of my service only happens once.