Announcing Vendure v1.0.0-beta
June 07, 2023
Video sections:
From here to v1.0
The -beta.1
suffix means that this is not yet the final, stable Vendure v1.0.0 release. There are still some outstanding issues to be handled, and rough edges to smooth out. However, there are no more breaking changes planned between now and v1.0.0. Over the coming weeks we'll be releasing a series of -beta.n
releases which will include these changes. Just before the final release we'll release a -rc.1
("release candidate 1"), and if all is well then that version will then become the final v1.0.0 release.
Future releases during this phase will not be accompanied by blog posts, just changelog entries and announcement via the usual channels (Slack, Twitter).
To recap:
1.0.0-beta.1
<- we are here1.0.0-beta.2
...
1.0.0-beta.n
(as needed)1.0.0-rc.1
1.0.0-rc.n
(if needed)1.0.0
<- goal: 1 - 2 months from now
With that established, let's take a look at some of the new features in this release!
Major improvement of worker architecture
Vendure uses the concept of a "worker" process to process long-running tasks in the background (a pattern known as a job queue or task queue). Previously, the worker process was implemented as a NestJS microservice. Jobs would be added to a queue in the database, and then the server would typically send those jobs to the worker over the network via TCP using the WorkerService
. This arrangement had a number of issues:
The need for network connectivity between the server and worker made deployment more complex
It was difficult to successfully scale the worker to multiple instances
The TCP connection was not robust, leading to the possibility of job loss
Creating plugins which made use of the worker was overly complex and verbose
This release introduces a more efficient and stable architecture which addresses these issues. We have switched from the NestJS microservice-based architecture to a much simpler pure job queue solution, which essentially combines the roles of the JobQueueService
with the WorkerService
, so that any job added to a JobQueue will by default run on the worker.
The advantages of this approach are:
It is now trivial to spin up as many parallel worker processes as you need (see the demo in the release video)
Less plugin code is needed to run tasks on the worker
More performant job queue strategies can result in much reduced load on the database (see chart below)
The internals are simpler - no network layer is needed to run the worker, meaning we no longer have the overhead of sending & receiving network requests to send jobs to the worker
This change involved removing a bunch of code and APIs which are made obsolete, as can be seen when we look at the number of new lines vs deleted lines when this change was merged in:
$ git diff --shortstat 2693174b dac1aac3
99 files changed, 786 insertions(+), 2265 deletions(-)
This change is breaking, and the amount of work you'll need to do to migrate depends on how much you made use of the worker in your custom plugins. See the migration guide at the end for full details.
A huge thanks is due to community member Fred Cox for his outstanding work on this (PR #663 & PR #771).
relation
custom field type
Custom fields enable the extension of built-in models with data specific to your business use-case. Up until now, only simple data types (string, booleans, etc.) have been supported. With this release, you can now define custom relations between entities in just a single line of configuration!
For example, here's how you can define a relation between Customer
and Asset
to allow your customers to upload an avatar image:
const config: VendureConfig = {
// ...
customFields: {
Customer: [
{ name: 'avatar', type: 'relation', entity: Asset },
]
}
}
This then allows you to query the avatar via the GraphQL API like this:
query {
customer(id: 1) {
id
firstName
lastName
customFields {
avatar {
id
name
preview
}
}
}
}
See the newly-expanded custom fields documentation for more details.
Payment process improvements
A number of improvements have been made to the way payment methods are defined and handled:
Just like the Order & Fulfillment processes, the Payments process can now be customized, allowing you to define your own states and transitions in the payment process. See the Payment Integrations guide for an example.
PaymentMethods are now channel-aware, so if you make use of multiple Channels, you can now limit PaymentMethods to specific Channels.
You can now define a PaymentMethodEligibilityChecker which is used to determine whether a particular PaymentMethod is eligible to be used against the current active Order. This works in much the same way as the existing checkers for ShippingMethods, and accordingly introduces a new query on the Shop API:
type Query { eligiblePaymentMethods: [PaymentMethodQuote!]! }
Displaying stock level in the Shop API
Up until now, it has not been possible to view stock levels via the Shop API without creating a custom plugin. The reason for this was that by default, we did not want to expose potentially sensitive stock information publicly. However, some kind of indication of whether an item is in or out of stock is an extremely common requirement, so with this release we are introducing a ProductVariant.stockLevel
field and the accompanying StockDisplayStrategy.
TheDefaultStockDisplayStrategy will return either 'IN_STOCK'
, 'OUT_OF_STOCK'
or 'LOW_STOCK'
, but you can provide your own strategy to expose even more granular data if required (e.g. "Only 2 items left") by setting the catalogOptions.stockDisplayStrategy
config option.
Other notable improvements
Assets, Facets and FacetValues are now channel-aware, meaning they can be assigned to specific Channels in a multi-Channel configuration.
The
Administrator
andChannel
entities now support custom fields.The new ChangedPriceHandlingStrategy allows you full control over how you handle changes to the price of items which are already in an active Order.
The new OrderPlacedStrategy enables even more control over custom Order processes, allowing you to define the exact point at which the Order is considered "placed" (i.e. Customer has checked out, Order no longer active).
The TaxCategory entity has a new
isDefault
property, which is used to decide on the default TaxCategory to use when creating new ProductVariants.Assets can now be tagged. Tags are simple text labels which can be used to classify and group Assets. For example, if you are creating Customer avatars, you can tag them with an "avatar" tag, which allows you to easily filter for only avatar assets.
Performance when dealing with very large orders (OrderLines with quantity of > 100) has been massively improved (#705)
All major dependencies (NestJS, TypeORM, Graphql-js, Apollo Server, Angular) have been updated to the latest versions
See all changes in the v1.0.0-beta.1 Changelog
Contributor acknowledgements
The following community members contributed to this release. Thank you for your support and participation in making this the best version of Vendure!
Rohan Rajpal added support for custom fields on the Channel entity (#670)
Fred Cox did incredible work on the worker improvements (#663 & #771)
Thomas Blommaert improved performance of large order modifications plus other fixes (#705, #713, #714)
William Milne added support for custom generators & senders in the EmailPlugin (#707)
Hendrik Depauw made fixes to SessionCacheStrategy and the auth resolvers (#731, #748)
Karel Van De Winkel fixed product deletion in the Elasticsearch plugin (#743)
Jean Carlos Farias fixed our Brazilian Portuguese Admin UI translations (#725)
BREAKING CHANGES / Migration Guide
🚧 Read this section carefully!
For general instructions on upgrading, please see the new Updating Vendure guide.
Database migration
Create a complete backup of your database schema and data.
Generate a migration script as described in the Migrations guide.
You must now modify the generated migration script in accordance with the examples given in this Vendure 1.0.0-beta.1 Migration gist. This will involve moving around the generated queries and adding calls to the provided utility functions which will deal with the more complex data migrations.
Example migration script (if using postgres, the syntax will be slightly different, but the sequence of commands should match this example)
Migration utility functions (copy the contents of this file into the parent directory of the
/migrations
folder)
IMPORTANT test the migration first on data you are prepared to lose to ensure that it works as expected. Do not run on production data without testing. For production data, make a full backup first!
Update worker-related code
The
index-worker.ts
file needs to be updated:bootstrapWorker(config) + .then(worker => worker.startJobQueue()) .catch((err: any) => { console.log(err); process.exit(1); });
The
VendureConfig.workerOptions
config object has been removed. Since Vendure no longer uses TCP to connect to the Worker, these options are not needed. If you currently rely on theWorkerOptions.runInMainProcess
setting, this can be replaced bythis solution.If you have any custom plugins which use the worker (making use of the
WorkerService
and controllers with@MessagePattern()
decorator) then you'll need to make some substantial changes: Specialized controllers are no longer needed, and theworkers: []
array of the VendurePlugin metadata has been removed. Instead, the work previously performed in the controller can now be placed in a regular service.- @Controller() - class OrderProcessingController { - @MessagePattern(ProcessOrderMessage.pattern) - async processOrder(orderId: ProcessOrderMessage['data']) { - // long-running async work - } - } class OrderProcessingService { private jobQueue: JobQueue<ID>; constructor( - private workerService: WorkerService, private jobQueueService: JobQueueService, ) { this.jobQueue = this.jobQueueService.createQueue({ name: ['process-order-analytics'], - concurrency: 1, process: async job => { - this.workerService - .send(new ProcessOrderMessage(job.data.id)).subscribe(); + return this.processOrder(job.data.id); } } addOrderJobToQueue(order: Order) { return this.jobQueue.add(order.id); } + async processOrder(orderId: ID) { + // long-running async work + } } @VendurePlugin({ imports: [PluginCommonModule], providers: [OrderProcessingService], - workers: [OrderProcessingController], }) export class OrderAnalyticsPlugin {}
Update EmailPlugin, AssetServerPlugin config
The EmailPlugin & AssetServerPlugin now run directly as part of the main Vendure server process. This means that you do not need to specify a port
for them to run on. These plugins also require an explicit route
to be specified:
plugins: [
DefaultJobQueuePlugin,
AssetServerPlugin.init({
route: 'vendure-assets',
assetUploadDir: path.join(__dirname, '../static/assets'),
- port: 3001,
}),
EmailPlugin.init({
+ route: 'mailbox',
handlers: defaultEmailHandlers,
templatePath: path.join(__dirname, '../static/email/templates'),
outputPath: path.join(__dirname, '../static/email/output'),
- mailboxPort: 3003,
devMode: true,
globalTemplateVars: {
// ...
},
}),
AdminUiPlugin.init({
port: 3002,
+ route: 'admin',
}),
],
Replace Vendure-specific plugin lifecycle hooks
If your plugins make use of the Vendure-specific lifecycle hooks, you will need to replace them with the standard NestJS hooks:
Vendure hook | Replace with -------------|-------------- beforeVendureBootstrap | configure beforeVendureWorkerBootstrap | configure onVendureBootstrap | onApplicationBootstrap onVendureWorkerBootstrap | onApplicationBootstrap onVendureClose | onModuleDestroy onVendureWorkerClose | onModuleDestroy
Update e2e tests
If you have e2e test which rely on adding payments to an Order, you will need to update the InitialData object which you pass to the TestServer.init()
method. This is because Vendure no longer automatically creates a PaymentMethod for each PaymentMethodHandler passed into the config. Instead, you need to specify the creation of a new PaymentMethod via the InitialData:
describe('my plugin', () => {
const { server, adminClient, shopClient } = createTestEnvironment({
...testConfig,
plugins: [MyPlugin],
paymentOptions: {
paymentMethodHandlers: [testPaymentMethod],
},
});
beforeAll(async () => {
await server.init({
- initialData,
+ initialData: {
+ ...initialData,
+ paymentMethods: [
+ {
+ name: testPaymentMethod.code,
+ handler: {
+ code: testPaymentMethod.code,
+ arguments: [],
+ },
+ },
+ ],
+ },
productsCsvPath: PRODUCTS_CSV_PATH,
customerCount: 1,
});
await adminClient.asSuperAdmin();
}, TEST_SETUP_TIMEOUT_MS);
// ...
}
Create your first commerce experience with Vendure in less than 2 minutes
Vendure is a registered trademark. Our trademark policy ensures that our brand and products are protected. Feel free to reach out if you have any questions about our trademarks.
Documentation
Newsletter
Get the latest product news and announcements delivered directly to your inbox.