Earlier, we published a Vendure plugin to NPM and fixed our first bug. Now, it is time to add new features and publish an update.
On the Todo after extending the AdminUI in Vendure, 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
// 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>
// 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
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 is built-on solid foundations using TypeORM with Nest.js which allows us to define database subscribers. 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
/**
* @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 🙋 } forgetting to build before publishing.
Finally, run npm publish
and done! You can catch me on Vendure slack for more)
Top comments (0)