Skip to content

Commit

Permalink
refactor: migrate hasura module to use nestjs `ConfigurableModuleClas…
Browse files Browse the repository at this point in the history
…s` (#920)

* refactor: migrate hasura module builder api

* chore: add deprecation node
  • Loading branch information
underfisk authored Jan 5, 2025
1 parent c95fd53 commit 5b359e2
Show file tree
Hide file tree
Showing 15 changed files with 78 additions and 77 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"editor.formatOnSave": true,
"cSpell.words": ["rabbitmq", "Vitest"],
"cSpell.words": ["Hasura", "rabbitmq", "Vitest"],
"typescript.tsdk": "node_modules/typescript/lib"
}
3 changes: 1 addition & 2 deletions jest-e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
},
"moduleNameMapper": {
"^@golevelup/nestjs-rabbitmq$": "<rootDir>/packages/rabbitmq/src",
"^@golevelup/nestjs-discovery$": "<rootDir>/packages/discovery/src",
"^@golevelup/nestjs-modules$": "<rootDir>/packages/modules/src"
"^@golevelup/nestjs-discovery$": "<rootDir>/packages/discovery/src"
}
}
2 changes: 1 addition & 1 deletion packages/hasura/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ import { HasuraModule } from '@golevelup/nestjs-hasura';

@Module({
imports: [
HasuraModule.forRoot(HasuraModule, {
HasuraModule.forRoot({
webhookConfig: {
secretFactory: secret,
secretHeader: secretHeader,
Expand Down
2 changes: 0 additions & 2 deletions packages/hasura/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@
"access": "public"
},
"dependencies": {
"@golevelup/nestjs-common": "workspace:^",
"@golevelup/nestjs-discovery": "workspace:^",
"@golevelup/nestjs-modules": "workspace:^",
"@hasura/metadata": "^1.0.2",
"js-yaml": "^4.1.0",
"zod": "^3.24.1"
Expand Down
7 changes: 7 additions & 0 deletions packages/hasura/src/hasura-module-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { HasuraModuleConfig } from './hasura.interfaces';

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<HasuraModuleConfig>()
.setClassMethodName('forRoot')
.build();
3 changes: 1 addition & 2 deletions packages/hasura/src/hasura.constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export const HASURA_EVENT_HANDLER = Symbol('HASURA_EVENT_HANDLER');
export const HASURA_MODULE_CONFIG = Symbol('HASURA_MODULE_CONFIG');
export const HASURA_SCHEDULED_EVENT_HANDLER = Symbol(
'HASURA_SCHEDULED_EVENT_HANDLER'
'HASURA_SCHEDULED_EVENT_HANDLER',
);
13 changes: 6 additions & 7 deletions packages/hasura/src/hasura.decorators.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { makeInjectableDecorator } from '@golevelup/nestjs-common';
import { applyDecorators, SetMetadata } from '@nestjs/common';
import { applyDecorators, Inject, SetMetadata } from '@nestjs/common';
import { MODULE_OPTIONS_TOKEN } from './hasura-module-definition';
import {
HASURA_EVENT_HANDLER,
HASURA_MODULE_CONFIG,
HASURA_SCHEDULED_EVENT_HANDLER,
} from './hasura.constants';
import {
Expand All @@ -14,16 +13,16 @@ import {
export const HasuraEventHandler = (config: HasuraEventHandlerConfig) =>
applyDecorators(SetMetadata(HASURA_EVENT_HANDLER, config));

export const InjectHasuraConfig = makeInjectableDecorator(HASURA_MODULE_CONFIG);
export const InjectHasuraConfig = () => Inject(MODULE_OPTIONS_TOKEN);

export const TrackedHasuraEventHandler = (
config: TrackedHasuraEventHandlerConfig
config: TrackedHasuraEventHandlerConfig,
) => applyDecorators(SetMetadata(HASURA_EVENT_HANDLER, config));

export const TrackedHasuraScheduledEventHandler = (
config: TrackedHasuraScheduledEventHandlerConfig
config: TrackedHasuraScheduledEventHandlerConfig,
) =>
applyDecorators(
SetMetadata(HASURA_SCHEDULED_EVENT_HANDLER, config),
SetMetadata(HASURA_EVENT_HANDLER, { triggerName: config.name })
SetMetadata(HASURA_EVENT_HANDLER, { triggerName: config.name }),
);
6 changes: 3 additions & 3 deletions packages/hasura/src/hasura.event-handler.guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Request } from 'express';
import type { Request } from 'express';
import { Observable } from 'rxjs';
import { InjectHasuraConfig } from './hasura.decorators';
import { HasuraModuleConfig } from './hasura.interfaces';
Expand All @@ -9,7 +9,7 @@ export class HasuraEventHandlerHeaderGuard implements CanActivate {
private readonly apiSecret: string;
constructor(
@InjectHasuraConfig()
private readonly hasuraConfig: HasuraModuleConfig
private readonly hasuraConfig: HasuraModuleConfig,
) {
this.apiSecret =
typeof hasuraConfig.webhookConfig.secretFactory === 'function'
Expand All @@ -18,7 +18,7 @@ export class HasuraEventHandlerHeaderGuard implements CanActivate {
}

canActivate(
context: ExecutionContext
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();

Expand Down
2 changes: 1 addition & 1 deletion packages/hasura/src/hasura.event-handler.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { HasuraEvent } from './hasura.interfaces';
@Injectable()
export class EventHandlerService {
public handleEvent(evt: HasuraEvent): any {
// The implementation for this method is overriden by the containing module
// The implementation for this method is overridden by the containing module
console.log(evt);
}
}
52 changes: 25 additions & 27 deletions packages/hasura/src/hasura.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { DiscoveryModule, DiscoveryService } from '@golevelup/nestjs-discovery';
import { createConfigurableDynamicRootModule } from '@golevelup/nestjs-modules';
import {
BadRequestException,
Logger,
Expand All @@ -9,9 +8,12 @@ import {
import { PATH_METADATA } from '@nestjs/common/constants';
import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator';
import { flatten, groupBy } from 'lodash';
import {
ConfigurableModuleClass,
MODULE_OPTIONS_TOKEN,
} from './hasura-module-definition';
import {
HASURA_EVENT_HANDLER,
HASURA_MODULE_CONFIG,
HASURA_SCHEDULED_EVENT_HANDLER,
} from './hasura.constants';
import { InjectHasuraConfig } from './hasura.decorators';
Expand Down Expand Up @@ -44,34 +46,30 @@ function isHasuraScheduledEventPayload(

@Module({
imports: [DiscoveryModule],
providers: [
{
provide: Symbol('CONTROLLER_HACK'),
inject: [MODULE_OPTIONS_TOKEN],
useFactory: (config: HasuraModuleConfig) => {
const controllerPrefix = config.controllerPrefix || 'hasura';

Reflect.defineMetadata(
PATH_METADATA,
controllerPrefix,
EventHandlerController,
);
config.decorators?.forEach((deco) => {
deco(EventHandlerController);
});
},
},
EventHandlerService,
HasuraEventHandlerHeaderGuard,
],
controllers: [EventHandlerController],
})
export class HasuraModule
extends createConfigurableDynamicRootModule<HasuraModule, HasuraModuleConfig>(
HASURA_MODULE_CONFIG,
{
providers: [
{
provide: Symbol('CONTROLLER_HACK'),
useFactory: (config: HasuraModuleConfig) => {
const controllerPrefix = config.controllerPrefix || 'hasura';

Reflect.defineMetadata(
PATH_METADATA,
controllerPrefix,
EventHandlerController,
);
config.decorators?.forEach((deco) => {
deco(EventHandlerController);
});
},
inject: [HASURA_MODULE_CONFIG],
},
EventHandlerService,
HasuraEventHandlerHeaderGuard,
],
},
)
extends ConfigurableModuleClass
implements OnModuleInit
{
private readonly logger = new Logger(HasuraModule.name);
Expand Down
18 changes: 9 additions & 9 deletions packages/hasura/src/tests/hasura.event-handling.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ describe.each(cases)(
beforeEach(async () => {
const moduleImport =
moduleType === 'forRootAsync'
? HasuraModule.forRootAsync(HasuraModule, {
? HasuraModule.forRootAsync({
useFactory: () => moduleConfig,
})
: HasuraModule.forRoot(HasuraModule, moduleConfig);
: HasuraModule.forRoot(moduleConfig);

const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [moduleImport],
Expand Down Expand Up @@ -169,8 +169,8 @@ describe.each(cases)(
expect(scheduledEventHandler).toHaveBeenCalledTimes(1);
expect(scheduledEventHandler).toHaveBeenCalledWith(
expect.objectContaining(
pick(scheduledOneOffEventPayload, ['comment', 'payload'])
)
pick(scheduledOneOffEventPayload, ['comment', 'payload']),
),
);
});

Expand All @@ -184,18 +184,18 @@ describe.each(cases)(
expect(scheduledEventHandler).toHaveBeenCalledTimes(1);
expect(scheduledEventHandler).toHaveBeenCalledWith(
expect.objectContaining(
pick(scheduledEventPayload, ['name', 'payload'])
)
pick(scheduledEventPayload, ['name', 'payload']),
),
);
});
}
},
);

describe('HasuraModule with Custom Decorator', () => {
it('should call the decorator and set metadata for the controller', async () => {
await Test.createTestingModule({
imports: [
HasuraModule.forRoot(HasuraModule, {
HasuraModule.forRoot({
decorators: [SetMetadata()],
webhookConfig: {
secretHeader,
Expand All @@ -206,7 +206,7 @@ describe('HasuraModule with Custom Decorator', () => {
}).compile();
const controllerMeta = Reflect.getMetadata(
'TEST:METADATA',
EventHandlerController
EventHandlerController,
);
expect(controllerMeta).toBe('metadata');
});
Expand Down
4 changes: 2 additions & 2 deletions packages/hasura/src/tests/hasura.metadata.spec-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ export const yamlFileToJson = (filePath: string) => {
};

export const getVersionedMetadataPathAndConfig = (
v: string
v: string,
): [string, HasuraModuleConfig] => {
const version = metadataVersion.parse(v);
const metadataPath = path.join(
__dirname,
`../../test/__fixtures__/hasura/${version}/metadata`
`../../test/__fixtures__/hasura/${version}/metadata`,
);

const { managedMetaDataConfig } = baseConfig;
Expand Down
6 changes: 3 additions & 3 deletions packages/hasura/src/tests/hasura.metadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('Hasura Metadata', () => {

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [HasuraModule.forRoot(HasuraModule, moduleConfig)],
imports: [HasuraModule.forRoot(moduleConfig)],
providers: [TestEventHandlerService],
}).compile();

Expand All @@ -70,7 +70,7 @@ describe('Hasura Metadata', () => {

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [HasuraModule.forRoot(HasuraModule, moduleConfig)],
imports: [HasuraModule.forRoot(moduleConfig)],
providers: [TestEventHandlerService],
}).compile();

Expand All @@ -97,7 +97,7 @@ describe('Hasura Metadata', () => {

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [HasuraModule.forRoot(HasuraModule, moduleConfig)],
imports: [HasuraModule.forRoot(moduleConfig)],
providers: [TestEventHandlerService],
}).compile();

Expand Down
29 changes: 18 additions & 11 deletions packages/modules/src/dynamicModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type AsyncModuleConfig<T> = Pick<ModuleMetadata, 'imports' | 'exports'> &

export function createModuleConfigProvider<T>(
provide: InjectionToken,
options: AsyncModuleConfig<T>
options: AsyncModuleConfig<T>,
): Provider[] {
if ('useFactory' in options) {
return [
Expand All @@ -40,7 +40,7 @@ export function createModuleConfigProvider<T>(
}

const optionsProviderGenerator = (
inject: InjectionToken | OptionalFactoryDependency
inject: InjectionToken | OptionalFactoryDependency,
): Provider => ({
provide,
useFactory: async (moduleConfigFactory: ModuleConfigFactory<T>) => {
Expand All @@ -62,7 +62,7 @@ export function createModuleConfigProvider<T>(
return [
optionsProviderGenerator(
options.useExisting.provide ??
options.useExisting.value.constructor.name
options.useExisting.value.constructor.name,
),
{
provide:
Expand All @@ -85,15 +85,22 @@ export interface IConfigurableDynamicRootModule<T, U> {

forRootAsync(
moduleCtor: Type<T>,
asyncModuleConfig: AsyncModuleConfig<U>
asyncModuleConfig: AsyncModuleConfig<U>,
): DynamicModule;

externallyConfigured(
moduleCtor: Type<T>,
wait: number
wait: number,
): Promise<DynamicModule>;
}

/**
*
* @deprecated Please use Nestjs Configurable Module {@see https://docs.nestjs.com/fundamentals/dynamic-modules#configurable-module-builder}
* @param moduleConfigToken
* @param moduleProperties
* @returns
*/
export function createConfigurableDynamicRootModule<T, U>(
moduleConfigToken: InjectionToken,
moduleProperties: Partial<
Expand All @@ -102,14 +109,14 @@ export function createConfigurableDynamicRootModule<T, U>(
imports: [],
exports: [],
providers: [],
}
},
) {
abstract class DynamicRootModule {
static moduleSubject = new Subject<DynamicModule>();

static forRootAsync(
moduleCtor: Type<T>,
asyncModuleConfig: AsyncModuleConfig<U>
asyncModuleConfig: AsyncModuleConfig<U>,
): DynamicModule {
const dynamicModule = {
module: moduleCtor,
Expand Down Expand Up @@ -154,19 +161,19 @@ export function createConfigurableDynamicRootModule<T, U>(

static async externallyConfigured(
moduleCtor: Type<T>,
wait: number
wait: number,
): Promise<DynamicModule> {
const timeout$ = interval(wait).pipe(
first(),
map(() => {
throw new Error(
`Expected ${moduleCtor.name} to be configured by at last one Module but it was not configured within ${wait}ms`
`Expected ${moduleCtor.name} to be configured by at last one Module but it was not configured within ${wait}ms`,
);
})
}),
);

return lastValueFrom(
race(timeout$, DynamicRootModule.moduleSubject.pipe(first()))
race(timeout$, DynamicRootModule.moduleSubject.pipe(first())),
);
}
}
Expand Down
6 changes: 0 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 5b359e2

Please sign in to comment.