The Service Layer
The service layer is the core of the application. This is where the business logic is implemented, and where the application interacts with the database. When a request comes in to the API, it gets routed to a resolver which then calls a service method to perform the required operation.
Services are classes which, in NestJS terms, are providers. They follow all the rules of NestJS providers, including dependency injection, scoping, etc.
Services are generally scoped to a specific domain or entity. For instance, in the Vendure core, there is a Product
entity,
and a corresponding ProductService
which contains all the methods for interacting with products.
Here's a simplified example of a ProductService
, including an implementation of the findOne()
method that was
used in the example in the previous section:
import { Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { ID, Product, RequestContext, TransactionalConnection, TranslatorService } from '@vendure/core';
@Injectable()
export class ProductService {
constructor(private connection: TransactionalConnection,
private translator: TranslatorService){}
/**
* @description
* Returns a Product with the given id, or undefined if not found.
*/
async findOne(ctx: RequestContext, productId: ID): Promise<Product | undefined> {
const product = await this.connection.findOneInChannel(ctx, Product, productId, ctx.channelId, {
where: {
deletedAt: IsNull(),
},
});
if (!product) {
return;
}
return this.translator.translate(product, ctx);
}
// ... other methods
findMany() {}
create() {}
update() {}
}
- The
@Injectable()
decorator is a NestJS decorator which allows the service to be injected into other services or resolvers. - The
constructor()
method is where the dependencies of the service are injected. In this case, theTransactionalConnection
is used to access the database, and theTranslatorService
is used to translate the Product entity into the current language.
Using core services
All the internal Vendure services can be used in your own plugins and scripts. They are listed in the Services API reference and
can be imported from the @vendure/core
package.
To make use of a core service in your own plugin, you need to ensure your plugin is importing the PluginCommonModule
and
then inject the desired service into your own service's constructor:
import { PluginCommonModule, VendurePlugin } from '@vendure/core';
import { MyService } from './services/my.service';
@VendurePlugin({
imports: [PluginCommonModule],
providers: [MyService],
})
export class MyPlugin {}
import { Injectable } from '@nestjs/common';
import { ProductService } from '@vendure/core';
@Injectable()
export class MyService {
constructor(private productService: ProductService) {}
// you can now use the productService methods
}
Accessing the database
One of the main responsibilities of the service layer is to interact with the database. For this, you will be using
the TransactionalConnection
class, which is a wrapper
around the TypeORM DataSource
object. The primary purpose of the TransactionalConnection
is to ensure that database operations can be performed within a transaction (which is essential for ensuring data integrity), even
across multiple services. Furthermore, it exposes some helper methods which make it easier to perform common operations.
Always pass the RequestContext
(ctx
) to the TransactionalConnection
methods. This ensures the operation occurs within
any active transaction.
There are two primary APIs for accessing data provided by TypeORM: the Find API and the QueryBuilder API.
The Find API
This API is the most convenient and type-safe way to query the database. It provides a powerful type-safe way to query including support for eager relations, pagination, sorting, filtering and more.
Here are some examples of using the Find API:
import { Injectable } from '@nestjs/common';
import { ID, RequestContext, TransactionalConnection } from '@vendure/core';
import { IsNull } from 'typeorm';
import { Item } from '../entities/item.entity';
@Injectable()
export class ItemService {
constructor(private connection: TransactionalConnection) {}
findById(ctx: RequestContext, itemId: ID): Promise<Item | null> {
return this.connection.getRepository(ctx, Item).findOne({
where: { id: itemId },
});
}
findByName(ctx: RequestContext, name: string): Promise<Item | null> {
return this.connection.getRepository(ctx, Item).findOne({
where: {
// Multiple where clauses can be specified,
// which are joined with AND
name,
deletedAt: IsNull(),
},
});
}
findWithRelations() {
return this.connection.getRepository(ctx, Item).findOne({
where: { name },
relations: {
// Join the `item.customer` relation
customer: true,
product: {
// Here we are joining a nested relation `item.product.featuredAsset`
featuredAsset: true,
},
},
});
}
findMany(ctx: RequestContext): Promise<Item[]> {
return this.connection.getRepository(ctx, Item).find({
// Pagination
skip: 0,
take: 10,
// Sorting
order: {
name: 'ASC',
},
});
}
}
Further examples can be found in the TypeORM Find Options documentation.
The QueryBuilder API
When the Find API is not sufficient, the QueryBuilder API can be used to construct more complex queries. For instance,
if you want to have a more complex WHERE
clause than can be achieved with the Find API, or if you want to perform
sub-queries, then the QueryBuilder API is the way to go.
Here are some examples of using the QueryBuilder API:
import { Injectable } from '@nestjs/common';
import { ID, RequestContext, TransactionalConnection } from '@vendure/core';
import { Brackets, IsNull } from 'typeorm';
import { Item } from '../entities/item.entity';
@Injectable()
export class ItemService {
constructor(private connection: TransactionalConnection) {}
findById(ctx: RequestContext, itemId: ID): Promise<Item | null> {
// This is simple enough that you should prefer the Find API,
// but here is how it would be done with the QueryBuilder API:
return this.connection.getRepository(ctx, Item).createQueryBuilder('item')
.where('item.id = :id', { id: itemId })
.getOne();
}
findManyWithSubquery(ctx: RequestContext, name: string) {
// Here's a more complex query that would not be possible using the Find API:
return this.connection.getRepository(ctx, Item).createQueryBuilder('item')
.where('item.name = :name', { name })
.andWhere(
new Brackets(qb1 => {
qb1.where('item.state = :state1', { state1: 'PENDING' })
.orWhere('item.state = :state2', { state2: 'RETRYING' });
}),
)
.orderBy('item.createdAt', 'ASC')
.getMany();
}
}
Further examples can be found in the TypeORM QueryBuilder documentation.