How to re-initialize EdgeDB database provider with updated configuration in NestJS?

118 views Asked by At

I'm developing a personal project backed by NestJS and EdgeDB. I'd like to take advantage of their new feature called 'access policies' which requires re-initializing a database client with a config that contains current user's ID. Initially I thought that I could simply call a special method which is designed to overwrite existing database client, however this approach didn't work since the injected provider is seemingly unaffected by any of the attempted mutations after the initialization. Here are code snippets to demonstrate:

Database provider

@Injectable()
export class EdgeDBService implements OnModuleInit {
  client = edgedb.createClient()

  async onModuleInit() {
    await this.client.ensureConnected()
  }

  public async setGlobals(globals: { current_user_id: string }) {
    // Attempting to tack on current user ID to all future queries
    this.client = this.client.withGlobals(globals)

    // `current_user_id` returns correct value here
    console.log(await this.client.query(`select global current_user_id;`))
  }

  public async query<Expr extends EdgeQLTypes.Expression>(expression: Expr): Promise<$infer<Expr>> {
    return await expression.run(this.client)
  }
}

Hypothetical provider for retrieving books

@Injectable()
export class PetsService {
  constructor(private edgedb: EdgeDBService) {}

  getPets() {
    // Getting nothing here.
    console.log(await this.client.query(`select global current_user_id;`))

    // This query returns nothing since `current_user_id` is not set.
    return this.edgedb.query(
      e.select(e.Pets, (item) => ({
        ...e.Pets['*'],
      }))
    )
}

So it seems like it's not really possible to simply re-write the this.client property in EdgeDBService, or am I wrong? Is it possible to alter a provider after the initialization at all, or the singleton injection scope prevents that?

Any help with figuring this out is highly appreciated

1

There are 1 answers

0
Pa Ye On BEST ANSWER

Eventually figured this out on my own going off a clue related to injection scope. By default, a single provider instance (singleton) is shared across the entire app. Attempting to mutate its properties at runtime won't reflect in the already injected instances (PetsService in the example above). Turns out that the way the provider is injected could be adjusted by specifying a different scope (docs). I went with with REQUEST as it perfectly fits my use case:

REQUEST: A new instance of the provider is created exclusively for each incoming request. The instance is garbage-collected after the request has completed processing.

Switching over to REQUEST scope allowed me to grab the request object in the constructor (docs), extract JWT, decode it and re-initialize EdgeDB client with a global variable required for access policies to work. Here's the code:

import { CONTEXT } from '@nestjs/graphql'

@Injectable({ scope: Scope.REQUEST })
export class EdgeDBService implements OnModuleInit {
  client = edgedb.createClient()

  constructor(
    @Inject(CONTEXT) private context: { req: Request },
    private moduleRef: ModuleRef
  ) {
    const token = context.req.headers['authorization']?.replace('Bearer ', '')
    if (token) {
      const authService = this.moduleRef.get(AuthService, { strict: false })
      const decoded = authService.decodeJWT(token)
      this.client = this.client.withGlobals({
        current_user_id: decoded.sub,
      })
    }
  }

  ...
}