简体   繁体   中英

Feathers JS MongoDB Service: Dynamic Collection Names

Goal

I'm attempting to build a multi-tenant application with Feathers JS. On login, the tenant ID will be included in the request. From then on, the tenant ID will be gotten from the user field with the request parameters. Every user has this tenantId field.

In MongoDB, I have one unique collection of every data type per tenant. The collection names look like tenantId.documents and tenantId.users

Problem

The service generated via the feathers generate service CLI command looks like so:

export class Documents extends Service {
  //eslint-disable-next-line @typescript-eslint/no-unused-vars
  constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
    super(options);

    const client: Promise<Db> = app.get('mongoClient');

    client.then(db => {
      this.Model = db.collection('documents');
    });
  }
}

As you can see, the generated Service s seem to need their collection name ("documents" in this case) during instantiation. Normally, this makes sense since it saves time await ing a call to app.get("mongoClient")

However, since I need to dynamically change which collection I read from based on the User's tenantId , this won't work for me.

I implemented something like the following:

export class Documents extends Service {
  client: Promise<Db>
  //eslint-disable-next-line @typescript-eslint/no-unused-vars
  constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
    super(options);

    this.client = app.get("mongoClient");
  }

  async create(data: IDocumentData, params: Params) {
    const db: Db = await this.client;
    this.Model = db.collection(`${params.user!!.organizationId}.documents`);
    return super.create(data, params);
  }
}

The problems are these:

  1. I need to await this.client every request, even when the promise will probably already be fulfilled by the time a user actually makes a request to this service
  2. I have to implement every method of the parent Service even though I barely need to add any real functionality.

Question

What is the most feathers-y way to solve this problem?

  • I don't want to override every method that I need in every service
  • I don't see a way to handle this with middleware or hooks.
  • I also don't think it's necessary to create one service instance per tenant in my application. It seems wasteful since I don't need to make any additional external requests based on the tenant ID, I just need to change the collection

Is there a good, pretty way to do this in Feathers?

Thanks to the helpful Feather community Slack channel, I think I came across a halfway-decent solution to this specific issue. It doesn't address all of my concerns, but at least it de-clutters my code.

First, I should create a new class that extends the built in Service class that implements the feature that I want. It could look something like this:

class DynamicMongoService extends Service {
  client: Promise<Db>;
  collectionName: string;

  constructor(
    options: Partial<MongoDBServiceOptions>,
    app: Application,
    collectionName: string
  ) {
    super(options);

    this.client = app.get("mongoClient");
    this.collectionName = collectionName;
  }

  async getCollection(params: Params) {
    const db: Db = await this.client;
    this.Model = db.collection(
      `${params!!.user!!.organizationId}.${this.collectionName}`
    );
  }

  async find(params?: Params) {
    await this.getCollection(params!!);
    return super.create(params!!);
  }

  async get(id: Id, params?: Params) {
    await this.getCollection(params!!);
    return super.get(id, params);
  }

  async create(data: Partial<any> | Array<Partial<any>>, params?: Params) {
    await this.getCollection(params!!);
    return super.create(data, params);
  }

  async update(id: NullableId, data: any, params?: Params) {
    await this.getCollection(params!!);
    return super.update(id!!, data, params);
  }

  async patch(id: NullableId, data: Partial<any>, params?: Params) {
    await this.getCollection(params!!);
    return super.patch(id!!, data, params);
  }

  async remove(id: NullableId, params?: Params) {
    await this.getCollection(params!!);
    return super.patch(id!!, params!!);
  }
}

The key elements are thus:

  1. Pass collection name in the constructor
  2. Get the collection name before each method

An implementation of this service would look like this:

export class Documents extends DynamicMongoService {
  constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
    super(options, app, "documents");
  }
}

Not the best, but easy enough!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM