Search code examples
javascriptnode.jscassandranestjsscylla

NestJS - Creating dynamic module with sync and async options


Good day everyone, faced a trouble while creating my own dynamic module for NestJS & cassandra-driver.

The point is that I have two static methods:

  1. forRoot, which creates a connection and provides it to the application
  2. forFeature, which creates mappers for the provided entities. Both methods are synchronous, and forFeature depends on the creation of the client, otherwise it will throw an error.

Module code:

import { Module, DynamicModule } from '@nestjs/common';
import { Client, mapping } from 'cassandra-driver';
import { getTableName } from './scylla.decorator';
import Mapper = mapping.Mapper;
const MAPPER_PREFIX = '_MAPPER';

interface RootOptions {
  contactPoints: string[];
  keyspace: string;
  username: string;
  password: string;
  localDataCenter?: string;
}
@Module({})
export class ScyllaModule {
  private static scyllaClient: Client;
  static forRoot(options: RootOptions): DynamicModule {
    return {
      module: ScyllaModule,
      providers: [
        {
          provide: 'SCYLLA_CLIENT',
          useValue: this.createClient(options),
        },
      ],
      exports: ['SCYLLA_CLIENT'],
    };
  }
  static forFeature(
    entities: (new (...args) => any)[],
  ): DynamicModule {
    const newMappers = {};
    const providersNames = [];
    for (const entity of entities) {
      const tableName = getTableName(entity);
      if (!tableName) throw new Error('No entity name provided!');
      newMappers[tableName + MAPPER_PREFIX] = new Mapper(this.scyllaClient, {
        models: { [tableName + 'Model']: { tables: [tableName] } },
      });
      providersNames.push(tableName + MAPPER_PREFIX);
    }
    return {
      module: ScyllaModule,
      providers: Object.entries(newMappers).map(([key, value]) => {
        const mapper = value as Mapper;
        return {
          provide: key,
          useValue: mapper.forModel(key.replace(MAPPER_PREFIX, '')),
        };
      }),
      exports: providersNames,
    };
  }
  private static createClient(options: RootOptions): Client {
    const { contactPoints, keyspace, username, password, localDataCenter } =
      options;
    const client = new Client({
      contactPoints,
      localDataCenter: localDataCenter || 'datacenter1',
      keyspace,
      credentials: { username, password },
    });

    client.connect().catch((error) => {
      throw new Error(error.message);
    });

    this.scyllaClient = client;

    return client;
  }
}

Then, using this in another module like this will not cause any trouble and work as expected.

    ScyllaModule.forRoot({
      contactPoints: ['localhost'],
      keyspace: 'playground',
      username: 'cassandra',
      password: 'cassandra',
    }),
    ScyllaModule.forFeature([Test])

For the final i`ve tried to create forRootAsync method with factory, and that's why I created the question.

So, starting with async method:

static async forRootAsync(options: {
    useFactory: (...args: any[]) => Promise<RootOptions> | RootOptions;
    inject?: any[];
  }): Promise<DynamicModule> {
    return {
      module: ScyllaModule,
      providers: [
        {
          provide: 'SCYLLA_CLIENT',
          useFactory: async (...args: any[]) => {
            const clientOptions = await options.useFactory(...args);
            return this.createClient(clientOptions);
          },
          inject: options.inject || [],
        },
      ],
      exports: ['SCYLLA_CLIENT'],
    };
  }

And using in another module like that:

imports: [
    ScyllaModule.forRootAsync({
      useFactory: () => {
        const data = {
          contactPoints: ['localhost'],
          keyspace: 'playground',
          username: 'cassandra',
          password: 'cassandra',
        };
        return data;
      },
    }),
    ScyllaModule.forFeature([Test])
  ],

Causes to

throw new Error('client must be defined');
^
Error: client must be defined
at new Mapper (C:\\Users\\User\\WebstormProjects\\scyllaTest\\node_modules\\cassandra-driver\\lib\\mapping\\mapper.js:68:13)
at Function.forFeature (C:\\Users\\User\\WebstormProjects\\scyllaTest\\src\\scylla\\scylla.module.ts:57:47)

In my opinion, throuble happens on first await in forRootAsync, which prevents forFeature run without waiting for forRootAsync to be done.

Tried a lot of possibilities but still zero result. Browsing the libraries only confused me with its complex implementation. enter image description here


Solution

  • After a some research i've succeeded at this question. Here it is, .module file:

    static forRootAsync(options: {
      useFactory: (...args: any[]) => Promise<RootOptions> | RootOptions;
      inject?: any[];
    }): DynamicModule {
      return {
        module: ScyllaModule,
        providers: [
          {
            provide: 'SCYLLA_CLIENT',
            useFactory: async (...args: any[]) => {
              const clientOptions = await options.useFactory(...args);
              const newClient = this.createClient(clientOptions);
              return newClient;
            },
            inject: options.inject || [],
          },
        ],
        exports: ['SCYLLA_CLIENT'],
      };
    }
    

    Class file can create connections and etc. Mapper error solved too, beacuse useFactory waits to inject now.