Shipping new features to our NPM package
Tags: typescript, graphql, webdev, buildinpublic
Published: 2023-04-21 11:59:44

Earlier, we published a [Vendure plugin to NPM](https://dev.to/prasmalla/publishing-your-first-npm-package-128n) and fixed our [first bug](https://dev.to/prasmalla/debugging-nodejs-with-vscode-2ob8). Now, it is time to add new features and publish an update.

On the Todo after [extending the AdminUI in Vendure](https://dev.to/prasmalla/admin-ui-extension-in-vendure-5f1l), was the ability for sending back in stock email notifications from the admin. We have the button; let's get to the functionality! All interactions with the server need to be done via GraphQL API calls in the admin ui.

Add the function call to the button and create the `notify` method

```html
// back-in-stock-list.component.html

<td class="right align-middle">
    <button
        *ngIf="subscription.status === 'Created' && subscription.productVariant.stockOnHand > 0"
        class="icon-button"
        title="Send notification email"
        (click)="notify(subscription.id)"
    >
        <clr-icon shape="envelope"></clr-icon>
    </button>
</td>
```

```ts
// back-in-stock-list.component.ts

async notify(id: ID): Promise<void> {
    try {
        await this.dataService
            .mutate(
                gql`
                    mutation {
                        updateBackInStockSubscription(input: {id: "${id}", status: Notified}) {
                            id
                            status
                        }
                    }
                `,
            )
            .toPromise();
        this.notificationService.success('Notification email sent');
        this.refresh();
    } catch (e) {
        this.notificationService.error('Error');
    }
}
```

The hard rule is that in an admin UI extension, you _cannot_ import anything from `@vendure/core` or anything else outside the ui folder. The setup for sending off emails is event driven and came across this hard rule when importing `EventBus` with this misleading error screen, having nothing to do with other packages as shown. Removing `EventBus` made this go away


![VSCode error screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2va4chss5dh77agedavv.png)

Instead, we publish the `BackInStockEvent` from the `update` method in `BackInStockService`. Email sending from admin works! Next Todo item was limiting emails sent to amount of saleable stock with configurable options. 

[Vendure](https://www.vendure.io/) is built-on solid foundations using TypeORM with Nest.js which allows us to define database [subscribers](https://typeorm.io/listeners-and-subscribers#what-is-a-subscriber). With a subscriber, we can listen to specific entity events and take actions based on inserts, updates, deletions and more.

An important factor when working with TypeORM subscribers is that they are very low-level and require some understanding of the Vendure schema. By defining the subscriber as an injectable provider, and passing it to a Vendure plugin, you can take advantage of Nest's dependency injection inside the subscriber methods.

Under the hood, TypeORM uses the Observer design pattern to implement database subscribers. This pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. When you register a subscriber, TypeORM sets up database triggers or hooks on the entities that the subscriber is interested in.

When the trigger or hook is fired, the database notifies TypeORM of the event. TypeORM then invokes the corresponding method on the subscriber class, passing in the event data. The subscriber can then perform any custom logic it needs to in response to the event.

This approach is much more efficient than polling the database because it eliminates the need to constantly query the database for changes. Instead, the database itself is responsible for detecting events and notifying the subscribers. This leads to better performance and reduced database load.

Create a subscriber for `ProductVariant`

```ts
/**
 * @description
 * Subscribes to {@link ProductVariant} entity changes
 * and FIFO updates {@link BackInStock} to be notified
 * to the amount of saleable stock with plugin init option
 * limitEmailToStock = true or false to notify all subscribers
 *
 */
@Injectable()
@EventSubscriber()
export class ProductVariantSubscriber implements EntitySubscriberInterface<ProductVariant> {
    constructor(
        private connection: TransactionalConnection,
        private backInStockService: BackInStockService,
        private productVariantService: ProductVariantService,
        private requestContextService: RequestContextService,
    ) {
        this.connection.rawConnection.subscribers.push(this);
    }
    listenTo() {
        return ProductVariant;
    }

    // set subscriptions to be notified only on replenishment event
    async afterUpdate(event: UpdateEvent<ProductVariant>) {
        if (
            event.entity?.stockOnHand > event.databaseEntity?.stockOnHand
        ) {
            const ctx = await this.requestContextService.create({ apiType: getApiType() });
            const productVariant = await this.productVariantService.findOne(ctx, event.entity?.id);
            //! calculate saleable manually as this context is not aware of the current db transaction
            const saleableStock =
                event.entity?.stockOnHand -
                productVariant!.stockAllocated -
                productVariant!.outOfStockThreshold;

            const backInStockSubscriptions = await this.backInStockService.findActiveForProductVariant(
                ctx,
                productVariant!.id,
                {
                    take: BackInStockPlugin.options.limitEmailToStock ? saleableStock : undefined,
                    sort: {
                        createdAt: SortOrder.ASC,
                    },
                },
            );

            if (saleableStock >= 1 && backInStockSubscriptions.totalItems >= 1) {
                for (const subscription of backInStockSubscriptions.items) {
                    this.backInStockService.update(ctx, {
                        id: subscription.id,
                        status: BackInStockSubscriptionStatus.Notified,
                    });
                }
            }
        }
    }
}

@VendurePlugin({
    // .. config
    providers: [
        {
            provide: PLUGIN_INIT_OPTIONS,
            useFactory: () => BackInStockPlugin.options,
        },
        BackInStockService,
        ProductVariantSubscriber,
    ],
})

export class BackInStockPlugin {
    static options: BackInStockOptions = {
        enableEmail: true,
        limitEmailToStock: true,
    };
    static init(options: BackInStockOptions): typeof BackInStockPlugin {
        this.options = options;
        return BackInStockPlugin;
    }
    // .. ui extensions
}
```

By comparing the `stockOnHand` on the ongoing DB transaction `event.entity` and the existing value on `event.databaseEntity` we can determine replenishment event. Since the new `context` in the subscriber is not aware of the ongoing DB transaction, we need to change setting saleableStock from `ProductVariantService` to manual calculation. With the `ProductVariantSubscriber` added to the `providers` array and we are ready to test it!

Instead of publishing your changes, you can run `yarn build` and copy over the `dist` folder to the `node_modules/@callit-today/vendure-plugin-back-in-stock` in a test app. Since there is no un-publishing versions in NPM this ensures our new features work and protects against { _sometimes_ :raising_hand: } forgetting to build before publishing.

Finally, run `npm publish` and done! You can catch me on [Vendure slack](https://vendure.io/slack) for more)