Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(providers): add ability to register tax and fulfillment providers using medusa-extender annotations #146

Merged
merged 6 commits into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion integration-tests/tests/fixtures/loaders.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
import { MedusaMiddleware, MedusaRequest, Middleware, Module, Router, Service } from 'medusa-extender';
import {
MedusaMiddleware,
MedusaRequest,
Middleware,
Module,
Router,
Service,
TaxProvider,
FulfillmentProvider,
} from 'medusa-extender';
import { default as MedusaCartService } from '@medusajs/medusa/dist/services/cart';
import { Response, Request, NextFunction } from 'express';
import { Cart } from '@medusajs/medusa/dist/models/cart';
import {
AbstractTaxService,
ItemTaxCalculationLine,
ShippingTaxCalculationLine,
TaxCalculationContext,
} from '@medusajs/medusa';
import { ProviderTaxLine } from '@medusajs/medusa/dist/types/tax-service';
import { FulfillmentService } from 'medusa-interfaces';

@Service()
export class TestService {
static resolutionKey = 'testService';
}

@TaxProvider()
export class TestTaxService extends AbstractTaxService {
static identifier = 'TestTax';
async getTaxLines(
itemLines: ItemTaxCalculationLine[],
shippingLines: ShippingTaxCalculationLine[],
context: TaxCalculationContext
): Promise<ProviderTaxLine[]> {
return [];
}
}

@FulfillmentProvider()
export class TestFulfillmentService extends FulfillmentService {
static identifier = 'TestFulfillment';
}

@Service({ override: MedusaCartService })
export class CartService extends MedusaCartService {
static resolutionKey = 'cartService';
Expand Down Expand Up @@ -214,6 +248,8 @@ CustomTopTestPathMiddleware.prototype.consume = jest
StoreTestPathMiddleware,
CustomTopRouter,
CustomTopTestPathMiddleware,
TestTaxService,
TestFulfillmentService,
],
})
export class TestModule {}
46 changes: 46 additions & 0 deletions integration-tests/tests/loaders.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
StoreTestPathMiddleware,
TestModule,
TestService,
TestTaxService,
TestFulfillmentService,
} from './fixtures/loaders';
import { Context } from '../utils/types';
import { makeRequest } from '../utils/request';
Expand Down Expand Up @@ -211,4 +213,48 @@ describe('Loaders', () => {
expect(CustomTopTestPathMiddleware.prototype.consume).toHaveBeenCalled();
});
});

describe('plugins loader', () => {
it('should load tax providers using name', () => {
const provider = context.container.resolve<TestTaxService>('testTaxService');
expect(provider).toBeTruthy();
expect(provider).toBeInstanceOf(TestTaxService);
});

it('should load into the tax providers array', () => {
const taxProviders = context.container.resolve<any[]>('taxProviders');
const provider = taxProviders.find((t) => t instanceof TestTaxService);

expect(provider).toBeTruthy();
expect(provider).toBeInstanceOf(TestTaxService);
});

it('should load tax provider with as tp_${identifier}', () => {
const provider = context.container.resolve<TestTaxService>('tp_TestTax');

expect(provider).toBeTruthy();
expect(provider).toBeInstanceOf(TestTaxService);
});

it('should load fulfillment provider using name', () => {
const provider = context.container.resolve<TestFulfillmentService>('testFulfillmentService');
expect(provider).toBeTruthy();
expect(provider).toBeInstanceOf(TestFulfillmentService);
});

it('should load fulfillment provider into the fulfillment providers array', () => {
const providers = context.container.resolve<any[]>('fulfillmentProviders');
const provider = providers.find((t) => t instanceof TestFulfillmentService);

expect(provider).toBeTruthy();
expect(provider).toBeInstanceOf(TestFulfillmentService);
});

it('should load fulfillment provider with as fp_${identifier}', () => {
const provider = context.container.resolve<TestFulfillmentService>('fp_TestFulfillment');

expect(provider).toBeTruthy();
expect(provider).toBeInstanceOf(TestFulfillmentService);
});
});
});
4 changes: 2 additions & 2 deletions src/Medusa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
modulesLoader,
overrideEntitiesLoader,
overrideRepositoriesLoader,
pluginsLoadersAndListeners,
pluginsLoadersProvidersAndListeners,
servicesLoader,
storeApiLoader,
subscribersLoader,
Expand Down Expand Up @@ -78,7 +78,7 @@ export class Medusa {
moduleComponentsOptions.get('repository') ?? [],
moduleComponentsOptions.get('migration') ?? []
);
await pluginsLoadersAndListeners(this.#express);
await pluginsLoadersProvidersAndListeners(this.#express, moduleComponentsOptions.get('provider') ?? []);
await servicesLoader(moduleComponentsOptions.get('service') ?? []);
await subscribersLoader(moduleComponentsOptions.get('subscriber') ?? []);

Expand Down
9 changes: 8 additions & 1 deletion src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface Type<T = unknown> extends Function {

export type Constructor<T> = new (...args: unknown[]) => T;

export type InjectableProviderSubTypes = 'tax' | 'fulfillment';

/**
* Components that does not required any other options that Type.
*/
Expand All @@ -23,7 +25,8 @@ export type InjectableComponentTypes =
| 'migration'
| 'router'
| 'middleware'
| 'subscriber';
| 'subscriber'
| 'provider';

/**
* Defines the injection options for entities.
Expand All @@ -36,6 +39,8 @@ export type EntityInjectableOptions<TOverride = unknown> = {
override?: Type<TOverride>;
};

export type ProviderInjectableOptions = { subtype?: InjectableProviderSubTypes };

/**
* Defines the injection options for service.
*/
Expand Down Expand Up @@ -134,6 +139,8 @@ export type GetInjectableOption<TComponentType extends InjectableComponentTypes
? ValidatorInjectionOptions
: TComponentType extends Extract<InjectableComponentTypes, 'subscriber'>
? SubscriberInjectionOptions
: TComponentType extends Extract<InjectableComponentTypes, 'provider'>
? ProviderInjectableOptions
: never) & {
type: InjectableComponentTypes;
metatype: TComponentType extends 'middleware' ? Type<MedusaMiddleware> : Type;
Expand Down
15 changes: 15 additions & 0 deletions src/decorators/helpers/build-provider-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'reflect-metadata';
import {
INJECTABLE_OPTIONS_KEY,
InjectableOptions,
InjectableProviderSubTypes,
InjectableComponentTypes,
} from '../../core';

export function buildProviderDecorator<T>(
options: InjectableOptions<T> & { type: InjectableComponentTypes; subtype: InjectableProviderSubTypes }
): ClassDecorator {
return <T>(target: T) => {
Reflect.defineMetadata(INJECTABLE_OPTIONS_KEY, options, target);
};
}
1 change: 1 addition & 0 deletions src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './module.decorator';
export * from './components.decorator';
export * from './onMedusaEntityEvent.decorator';
export * from './providers.decorator';
22 changes: 22 additions & 0 deletions src/decorators/providers.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ProviderInjectableOptions } from '../core';
import { buildProviderDecorator } from './helpers/build-provider-decorator';

export type AllowedProviderInjectableOptions = Omit<ProviderInjectableOptions, 'subtype'>;

/**
* Mark a class as a tax provider to be used by the loader.
* @param options
* @constructor
*/
export function TaxProvider(options: AllowedProviderInjectableOptions = {}): ClassDecorator {
return buildProviderDecorator({ type: 'provider', subtype: 'tax', ...options });
}

/**
* Mark a class as a fulfillment provider to be used by the loader.
* @param options
* @constructor
*/
export function FulfillmentProvider(options: AllowedProviderInjectableOptions = {}): ClassDecorator {
return buildProviderDecorator({ type: 'provider', subtype: 'fulfillment', ...options });
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export {
Router,
Validator,
Subscriber,
TaxProvider,
FulfillmentProvider,
} from './decorators';

export { MonitoringOptions } from './modules/monitoring';
Expand Down
13 changes: 10 additions & 3 deletions src/loaders/plugins.loader.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { Express, NextFunction, Response } from 'express';
import { MedusaAuthenticatedRequest, MedusaContainer, MedusaRequest } from './';
import { customEventEmitter } from '../core';
import { customEventEmitter, GetInjectableOptions } from '../core';
import { registerProviders } from './providers.loader';

/**
* @internal
* Register all listeners before the plugins are loaded to be sure that the scope middleware has already been created.
* Also register all providers that are typically created in plugins.
* @param app Express app
*/
export async function pluginsLoadersAndListeners(app: Express): Promise<void> {
export async function pluginsLoadersProvidersAndListeners(
app: Express,
providers: GetInjectableOptions<'provider'>
): Promise<void> {
const pluginLoader = await import('@medusajs/medusa/dist/loaders/plugins');
const originalPluginLoader = pluginLoader.default;
pluginLoader.default = async (cradle: {
app: Express;
rootDirectory: string;
container: MedusaContainer;
activityId: string;
configModule: Record<string, unknown>;
}) => {
app.use(
async (
Expand All @@ -28,6 +34,7 @@ export async function pluginsLoadersAndListeners(app: Express): Promise<void> {
}
);

return originalPluginLoader(cradle as any);
await originalPluginLoader(cradle as any);
await registerProviders(cradle.container, providers, cradle.configModule);
};
}
78 changes: 78 additions & 0 deletions src/loaders/providers.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { aliasTo, asFunction } from 'awilix';
import { GetInjectableOption, GetInjectableOptions, MedusaContainer } from '.';
import { Logger, lowerCaseFirst } from '../core';

const logger = Logger.contextualize('ProvidersLoader');

export function registerProviders(
container: MedusaContainer,
providers: GetInjectableOptions<'provider'>,
configModule: Record<string, unknown>
): void {
for (const providerOptions of providers) {
registerProvider(container, providerOptions, configModule);
}

logger.log(`${providers.length} providers registered`);
}

export function registerProvider(
container: MedusaContainer,
options: GetInjectableOption<'provider'>,
configModule: Record<string, unknown>
): void {
const { subtype } = options;
if (subtype === 'tax') return registerTaxProvider(container, options, configModule);
if (subtype === 'fulfillment') return registerFulfillmentProvider(container, options, configModule);
}

export function registerTaxProvider(
container: MedusaContainer,
options: GetInjectableOption<'provider'>,
configModule: Record<string, unknown>
): void {
const { metatype } = options;
const name = lowerCaseFirst(metatype.name);
const identifier = ensureIdentifier(metatype);

console.log('container', container);

container.registerAdd(
'taxProviders',
asFunction((cradle) => new metatype(cradle, configModule.options))
);

container.register({
[name]: asFunction((cradle) => new metatype(cradle, configModule.options)).singleton(),
[`tp_${identifier}`]: aliasTo(name),
});

logger.log(`Tax Provider loaded - ${name}`);
}

export function registerFulfillmentProvider(
container: MedusaContainer,
options: GetInjectableOption<'provider'>,
configModule: Record<string, unknown>
): void {
const { metatype } = options;
const name = lowerCaseFirst(metatype.name);
const identifier = ensureIdentifier(metatype);

container.registerAdd(
'fulfillmentProviders',
asFunction((cradle) => new metatype(cradle, configModule.options))
);

container.register({
[name]: asFunction((cradle) => new metatype(cradle, configModule.options)).singleton(),
[`fp_${identifier}`]: aliasTo(name),
});

logger.log(`Fulfillment Provider loaded - ${name}`);
}

function ensureIdentifier(metatype: any): string {
if (!metatype.identifier) throw new Error(`${metatype.name} is missing a static identifier property`);
return metatype.identifier;
}
18 changes: 2 additions & 16 deletions src/loaders/tests/entities.loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { asValue, createContainer } from 'awilix';
import { Entity as MedusaEntity, Module } from '../../decorators';
import { MedusaContainer, metadataReader } from '../../core';
import { Entity } from 'typeorm';
import { newContainer } from './utils/new-container';

@MedusaEntity({ override: MedusaUser })
@Entity()
Expand All @@ -26,22 +27,7 @@ class Another {}
class AnotherModule {}

describe('Entities loader', () => {
const container = createContainer() as MedusaContainer;
container.registerAdd = function (name, registration) {
const storeKey = name + '_STORE';

if (this.registrations[storeKey] === undefined) {
this.register(storeKey, asValue([]));
}
const store = this.resolve(storeKey);

if (this.registrations[name] === undefined) {
this.register(name, asArray(store));
}
store.unshift(registration);

return this;
}.bind(container);
const container = newContainer();

describe('overriddenEntitiesLoader', () => {
it(' should override MedusaUser with User', async () => {
Expand Down
Loading