chore(medusa, interfaces): Uniformise class checks (#5869)

**What**
One problem with local development can be the dependencies management. To mitigate that, transform checks from an instance of to value check as well as harmonize utils function to be part of the class as a static method instead of separate utilities. By using this approach we are not dependent of the origin of the class and therefore it should ease the user experience.

**NOTE**
As a next step to discuss, we should probably move the interfaces from the interfaces/medusa package to the utils package. Then we can deprecate the interfaces package and remove it at a later time
This commit is contained in:
Adrien de Peretti
2023-12-19 14:26:57 +01:00
committed by GitHub
parent 496dcf10c4
commit 45996d58a2
27 changed files with 187 additions and 178 deletions

View File

@@ -0,0 +1,8 @@
---
"@medusajs/medusa": patch
"medusa-fulfillment-webshipper": patch
"medusa-interfaces": patch
"@medusajs/utils": patch
---
chore(medusa, interfaces, utils, webshiper): Uniformise class checks

View File

@@ -10,7 +10,7 @@ class WebshipperFulfillmentService extends AbstractFulfillmentService {
{ logger, totalsService, claimService, swapService, orderService },
options
) {
super()
super(...arguments)
this.options_ = options

View File

@@ -3,8 +3,15 @@ import BaseService from "./base-service"
/**
* Interface for file connectors
* @interface
* @deprecated use AbstractFileService from @medusajs/medusa instead
*/
class BaseFileService extends BaseService {
static _isFileService = true
static isFileService(obj) {
return obj?.constructor?._isFileService
}
constructor() {
super()
}

View File

@@ -5,8 +5,15 @@ import BaseService from "./base-service"
* provides the necessary methods for creating, authorizing and managing
* fulfillment orders.
* @interface
* @deprecated use AbstractFulfillmentService from @medusajs/medusa instead
*/
class BaseFulfillmentService extends BaseService {
static _isFulfillmentService = true
static isFulfillmentService(obj) {
return obj?.constructor?._isFulfillmentService
}
constructor() {
super()
}
@@ -102,7 +109,7 @@ class BaseFulfillmentService extends BaseService {
return []
}
retrieveDocuments(fulfillmentData, documentType) {
retrieveDocuments(fulfillmentData, documentType) {
throw Error("retrieveDocuments must be overridden by the child class")
}
}

View File

@@ -3,8 +3,15 @@ import BaseService from "./base-service"
/**
* Interface for Notification Providers
* @interface
* @deprecated use AbstractNotificationService from @medusajs/medusa instead
*/
class BaseNotificationService extends BaseService {
static _isNotificationService = true
static isNotificationService(obj) {
return obj?.constructor?._isNotificationService
}
constructor() {
super()
}

View File

@@ -5,6 +5,12 @@ import BaseService from "./base-service"
* @interface
*/
class BaseOauthService extends BaseService {
static _isOauthService = true
static isOauthService(obj) {
return obj?.constructor?._isOauthService
}
constructor() {
super()
}

View File

@@ -5,8 +5,15 @@ import BaseService from "./base-service"
* provides the necessary methods for creating, authorizing and managing
* payments.
* @interface
* @deprecated use AbstractPaymentProcessor from @medusajs/medusa instead
*/
class BasePaymentService extends BaseService {
static _isPaymentService = true
static isPaymentService(obj) {
return obj?.constructor?._isPaymentService
}
constructor() {
super()
}

View File

@@ -3,8 +3,15 @@ import BaseService from "./base-service"
/**
* The interface that all search services must implement.
* @interface
* @deprecated use AbstractSearchService from @medusajs/utils instead
*/
class SearchService extends BaseService {
static _isSearchService = true
static isSearchService(obj) {
return obj?.constructor?._isSearchService
}
constructor() {
super()
}

View File

@@ -33,11 +33,16 @@ export abstract class AbstractBatchJobStrategy
extends TransactionBaseService
implements IBatchJobStrategy
{
static _isBatchJobStrategy = true
static identifier: string
static batchType: string
protected abstract batchJobService_: BatchJobService
static isBatchJobStrategy(object): object is IBatchJobStrategy {
return object?.constructor?._isBatchJobStrategy
}
async prepareBatchJobForProcessing(
batchJob: CreateBatchJobInput,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -107,9 +112,3 @@ export abstract class AbstractBatchJobStrategy
})
}
}
export function isBatchJobStrategy(
object: unknown
): object is IBatchJobStrategy {
return object instanceof AbstractBatchJobStrategy
}

View File

@@ -30,16 +30,15 @@ export abstract class AbstractCartCompletionStrategy
extends TransactionBaseService
implements ICartCompletionStrategy
{
static _isCartCompletionStrategy = true
static isCartCompletionStrategy(object): object is ICartCompletionStrategy {
return object?.constructor?._isCartCompletionStrategy
}
abstract complete(
cartId: string,
idempotencyKey: IdempotencyKey,
context: RequestContext
): Promise<CartCompletionResponse>
}
export function isCartCompletionStrategy(obj: unknown): boolean {
return (
typeof (obj as AbstractCartCompletionStrategy).complete === "function" ||
obj instanceof AbstractCartCompletionStrategy
)
}

View File

@@ -55,6 +55,12 @@ export abstract class AbstractFileService
extends TransactionBaseService
implements IFileService
{
static _isFileService = true
static isFileService(object): object is AbstractFileService {
return object?.constructor?._isFileService
}
abstract upload(
fileData: Express.Multer.File
): Promise<FileServiceUploadResult>
@@ -77,7 +83,3 @@ export abstract class AbstractFileService
fileData: GetUploadedFileType
): Promise<string>
}
export const isFileService = (object: unknown): boolean => {
return object instanceof AbstractFileService
}

View File

@@ -1,6 +1,7 @@
import { MedusaContainer } from "@medusajs/types"
import { Cart, Fulfillment, LineItem, Order } from "../models"
import { CreateReturnType } from "../types/fulfillment-provider"
import { TransactionBaseService } from "./transaction-base-service"
type FulfillmentProviderData = Record<string, unknown>
type ShippingOptionData = Record<string, unknown>
@@ -44,7 +45,7 @@ type ShippingMethodData = Record<string, unknown>
*
* ---
*/
export interface FulfillmentService {
export interface FulfillmentService extends TransactionBaseService {
/**
* @ignore
*
@@ -389,7 +390,16 @@ export interface FulfillmentService {
): Promise<any>
}
export abstract class AbstractFulfillmentService implements FulfillmentService {
export abstract class AbstractFulfillmentService
extends TransactionBaseService
implements FulfillmentService
{
static _isFulfillmentService = true
static isFulfillmentService(object): boolean {
return object?.constructor?._isFulfillmentService
}
/**
* You can use the `constructor` of your fulfillment provider to access the different services in Medusa through dependency injection.
* You can also use the constructor to initialize your integration with the third-party provider. For example, if you use a client to connect to the third-party providers APIs, you can initialize it in the constructor and use it in other methods in the service.
@@ -415,7 +425,9 @@ export abstract class AbstractFulfillmentService implements FulfillmentService {
protected constructor(
protected readonly container: MedusaContainer,
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
) {}
) {
super(container, config)
}
/**
* The `FulfillmentProvider` entity has 2 properties: `identifier` and `is_installed`. The `identifier` property in the class is used when the fulfillment provider is created in the database.

View File

@@ -1,5 +1,4 @@
import { TransactionBaseService } from "./transaction-base-service"
import BaseNotificationService from "medusa-interfaces/dist/notification-service"
type ReturnedData = {
to: string
@@ -25,8 +24,13 @@ export abstract class AbstractNotificationService
extends TransactionBaseService
implements INotificationService
{
static _isNotificationService = true
static identifier: string
static isNotificationService(object): boolean {
return object?.constructor?._isNotificationService
}
getIdentifier(): string {
return (this.constructor as any).identifier
}
@@ -43,10 +47,3 @@ export abstract class AbstractNotificationService
attachmentGenerator: unknown
): Promise<ReturnedData>
}
export const isNotificationService = (obj: unknown): boolean => {
return (
obj instanceof AbstractNotificationService ||
obj instanceof BaseNotificationService
)
}

View File

@@ -676,6 +676,12 @@ export abstract class AbstractPaymentProcessor implements PaymentProcessor {
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
) {}
static _isPaymentProcessor = true
static isPaymentProcessor(object): boolean {
return object?.constructor?._isPaymentProcessor
}
/**
* The `PaymentProvider` entity has 2 properties: `id` and `is_installed`. The `identifier` property in the payment processor service is used when the payment processor is added to the database.
*
@@ -768,18 +774,6 @@ export abstract class AbstractPaymentProcessor implements PaymentProcessor {
>
}
/**
* Return if the input object is AbstractPaymentProcessor
* @param obj
*/
export function isPaymentProcessor(obj: unknown): boolean {
return obj instanceof AbstractPaymentProcessor
}
/**
* Utility function to determine if an object is a processor error
* @param obj
*/
export function isPaymentProcessorError(
obj: any
): obj is PaymentProcessorError {

View File

@@ -146,6 +146,12 @@ export abstract class AbstractPaymentService
extends TransactionBaseService
implements PaymentService
{
static _isPaymentService = true
static isPaymentService(object): boolean {
return object?.constructor?._isPaymentService
}
protected constructor(container: unknown, config?: Record<string, unknown>) {
super(container, config)
}
@@ -258,11 +264,3 @@ export abstract class AbstractPaymentService
*/
public abstract getStatus(data: Data): Promise<PaymentSessionStatus>
}
/**
* Return if the input object is one of AbstractPaymentService or PaymentService or AbstractPaymentPluginService
* @param obj
*/
export function isPaymentService(obj: unknown): boolean {
return obj instanceof AbstractPaymentService || obj instanceof PaymentService
}

View File

@@ -32,6 +32,12 @@ export abstract class AbstractPriceSelectionStrategy
extends TransactionBaseService
implements IPriceSelectionStrategy
{
static _isPriceSelectionStrategy = true
static isPriceSelectionStrategy(object): boolean {
return object?.constructor?._isPriceSelectionStrategy
}
public abstract calculateVariantPrice(
data: {
variantId: string
@@ -46,16 +52,6 @@ export abstract class AbstractPriceSelectionStrategy
}
}
export function isPriceSelectionStrategy(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any
): object is IPriceSelectionStrategy {
return (
typeof object.calculateVariantPrice === "function" &&
typeof object.withTransaction === "function"
)
}
export type PriceSelectionContext = {
cart_id?: string
customer_id?: string

View File

@@ -2,6 +2,7 @@ import { LineItem } from "../models/line-item"
import { TaxCalculationContext } from "./tax-service"
import { LineItemTaxLine } from "../models/line-item-tax-line"
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
import { TransactionBaseService } from "./transaction-base-service"
export interface ITaxCalculationStrategy {
/**
@@ -19,9 +20,22 @@ export interface ITaxCalculationStrategy {
): Promise<number>
}
export function isTaxCalculationStrategy(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any
): object is ITaxCalculationStrategy {
return typeof object.calculate === "function"
export abstract class AbstractTaxCalculationStrategy
extends TransactionBaseService
implements ITaxCalculationStrategy
{
static _isTaxCalculationStrategy = true
static isTaxCalculationStrategy(object): boolean {
return (
typeof object.calculate === "function" ||
object?.constructor?._isTaxCalculationStrategy
)
}
abstract calculate(
items: LineItem[],
taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
calculationContext: TaxCalculationContext
): Promise<number>
}

View File

@@ -1,5 +1,3 @@
import { BaseService } from "medusa-interfaces"
import { LineItem } from "../models/line-item"
import { Region } from "../models/region"
import { Address } from "../models/address"
@@ -7,6 +5,7 @@ import { ShippingMethod } from "../models/shipping-method"
import { Customer } from "../models/customer"
import { ProviderTaxLine, TaxServiceRate } from "../types/tax-service"
import { LineAllocationsMap } from "../types/totals"
import { TransactionBaseService } from "./transaction-base-service"
/**
* A shipping method and the tax rates that have been configured to apply to the
@@ -61,11 +60,16 @@ export interface ITaxService {
}
export abstract class AbstractTaxService
extends BaseService
extends TransactionBaseService
implements ITaxService
{
static _isTaxService = true
protected static identifier: string
static isTaxService(object): boolean {
return object?.constructor?._isTaxService
}
public getIdentifier(): string {
if (!(this.constructor as typeof AbstractTaxService).identifier) {
throw new Error(`Missing static property "identifier".`)

View File

@@ -14,7 +14,7 @@ export abstract class TransactionBaseService {
protected readonly __configModule__?: Record<string, unknown>,
protected readonly __moduleDeclaration__?: Record<string, unknown>
) {
this.manager_ = __container__.manager
this.manager_ = __container__?.manager
}
withTransaction(transactionManager?: EntityManager): this {

View File

@@ -9,7 +9,11 @@ import { createMedusaContainer } from "medusa-core-utils"
import { resolve } from "path"
import { DataSource, EntityManager } from "typeorm"
import Logger from "../logger"
import { MEDUSA_PROJECT_NAME, registerServices, registerStrategies } from "../plugins"
import {
MEDUSA_PROJECT_NAME,
registerServices,
registerStrategies,
} from "../plugins"
// ***** TEMPLATES *****
const buildServiceTemplate = (name: string): string => {
@@ -32,7 +36,7 @@ const buildBatchJobStrategyTemplate = (name: string, type: string): string => {
return `
import { AbstractBatchJobStrategy } from "../../../../interfaces/batch-job-strategy"
class ${name}BatchStrategy extends AbstractBatchJobStrategy{
class ${name}BatchStrategy extends AbstractBatchJobStrategy {
static identifier = '${name}-identifier';
static batchType = '${type}';
@@ -79,7 +83,8 @@ const buildPriceSelectionStrategyTemplate = (name: string): string => {
const buildTaxCalcStrategyTemplate = (name: string): string => {
return `
class ${name}TaxCalculationStrategy {
import { AbstractTaxCalculationStrategy } from "../../../../interfaces/tax-calculation-strategy"
class ${name}TaxCalculationStrategy extends AbstractTaxCalculationStrategy {
calculate(items, taxLines, calculationContext) {
throw new Error("Method not implemented.")
}

View File

@@ -1,13 +1,10 @@
import { Lifetime, LifetimeType, aliasTo, asFunction } from "awilix"
import { FulfillmentService } from "medusa-interfaces"
import { aliasTo, asFunction, Lifetime, LifetimeType } from "awilix"
import {
AbstractFulfillmentService,
AbstractPaymentProcessor,
AbstractPaymentService,
isPaymentProcessor,
isPaymentService,
} from "../../interfaces"
import { ClassConstructor, MedusaContainer } from "../../types/global"
import { PaymentService } from "medusa-interfaces"
type Context = {
container: MedusaContainer
@@ -15,44 +12,16 @@ type Context = {
registrationName: string
}
export function registerPaymentServiceFromClass(
klass: ClassConstructor<AbstractPaymentService> & {
LIFE_TIME?: LifetimeType
},
context: Context
): void {
if (!isPaymentService(klass.prototype)) {
return
}
const { container, pluginDetails, registrationName } = context
container.registerAdd(
"paymentProviders",
asFunction((cradle) => new klass(cradle, pluginDetails.options), {
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
})
)
container.register({
[registrationName]: asFunction(
(cradle) => new klass(cradle, pluginDetails.options),
{
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
}
),
[`pp_${(klass as unknown as typeof AbstractPaymentService).identifier}`]:
aliasTo(registrationName),
})
}
export function registerPaymentProcessorFromClass(
klass: ClassConstructor<AbstractPaymentProcessor> & {
LIFE_TIME?: LifetimeType
},
context: Context
): void {
if (!isPaymentProcessor(klass.prototype)) {
if (
!AbstractPaymentProcessor.isPaymentProcessor(klass.prototype) &&
!PaymentService.isPaymentService(klass.prototype)
) {
return
}
@@ -83,7 +52,7 @@ export function registerAbstractFulfillmentServiceFromClass(
},
context: Context
): void {
if (!(klass.prototype instanceof AbstractFulfillmentService)) {
if (!AbstractFulfillmentService.isFulfillmentService(klass.prototype)) {
return
}
@@ -108,34 +77,3 @@ export function registerAbstractFulfillmentServiceFromClass(
}`]: aliasTo(registrationName),
})
}
export function registerFulfillmentServiceFromClass(
klass: ClassConstructor<typeof FulfillmentService> & {
LIFE_TIME?: LifetimeType
},
context: Context
): void {
if (!(klass.prototype instanceof FulfillmentService)) {
return
}
const { container, pluginDetails, registrationName } = context
container.registerAdd(
"fulfillmentProviders",
asFunction((cradle) => new klass(cradle, pluginDetails.options), {
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
})
)
container.register({
[registrationName]: asFunction(
(cradle) => new klass(cradle, pluginDetails.options),
{
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
}
),
[`fp_${(klass as unknown as typeof FulfillmentService).identifier}`]:
aliasTo(registrationName),
})
}

View File

@@ -1,4 +1,8 @@
import { promiseAll, SearchUtils, upperCaseFirst } from "@medusajs/utils"
import {
AbstractSearchService,
promiseAll,
upperCaseFirst,
} from "@medusajs/utils"
import { aliasTo, asFunction, asValue, Lifetime } from "awilix"
import { Express } from "express"
import fs from "fs"
@@ -6,19 +10,19 @@ import { sync as existsSync } from "fs-exists-cached"
import glob from "glob"
import _ from "lodash"
import { createRequireFromPath } from "medusa-core-utils"
import { FileService, OauthService } from "medusa-interfaces"
import { OauthService } from "medusa-interfaces"
import { trackInstallation } from "medusa-telemetry"
import { EOL } from "os"
import path from "path"
import { EntitySchema } from "typeorm"
import {
AbstractBatchJobStrategy,
AbstractCartCompletionStrategy,
AbstractFileService,
AbstractNotificationService,
AbstractPriceSelectionStrategy,
AbstractTaxCalculationStrategy,
AbstractTaxService,
isBatchJobStrategy,
isCartCompletionStrategy,
isFileService,
isNotificationService,
isPriceSelectionStrategy,
isTaxCalculationStrategy,
} from "../interfaces"
import { MiddlewareService } from "../services"
import {
@@ -35,9 +39,7 @@ import { getModelExtensionsMap } from "./helpers/get-model-extension-map"
import ScheduledJobsLoader from "./helpers/jobs"
import {
registerAbstractFulfillmentServiceFromClass,
registerFulfillmentServiceFromClass,
registerPaymentProcessorFromClass,
registerPaymentServiceFromClass,
} from "./helpers/plugins"
import { RoutesLoader } from "./helpers/routing"
import { SubscriberLoader } from "./helpers/subscribers"
@@ -230,7 +232,9 @@ export function registerStrategies(
const module = require(file).default
switch (true) {
case isTaxCalculationStrategy(module.prototype): {
case AbstractTaxCalculationStrategy.isTaxCalculationStrategy(
module.prototype
): {
if (!("taxCalculationStrategy" in registeredServices)) {
container.register({
taxCalculationStrategy: asFunction(
@@ -246,7 +250,9 @@ export function registerStrategies(
break
}
case isCartCompletionStrategy(module.prototype): {
case AbstractCartCompletionStrategy.isCartCompletionStrategy(
module.prototype
): {
if (!("cartCompletionStrategy" in registeredServices)) {
container.register({
cartCompletionStrategy: asFunction(
@@ -262,7 +268,7 @@ export function registerStrategies(
break
}
case isBatchJobStrategy(module.prototype): {
case AbstractBatchJobStrategy.isBatchJobStrategy(module.prototype): {
container.registerAdd(
"batchJobStrategies",
asFunction((cradle) => new module(cradle, pluginDetails.options))
@@ -279,7 +285,9 @@ export function registerStrategies(
break
}
case isPriceSelectionStrategy(module.prototype): {
case AbstractPriceSelectionStrategy.isPriceSelectionStrategy(
module.prototype
): {
if (!("priceSelectionStrategy" in registeredServices)) {
container.register({
priceSelectionStrategy: asFunction(
@@ -441,8 +449,7 @@ async function registerApi(
}
/**
* Registers a service at the right location in our container. If the service is
* a BaseService instance it will be available directly from the container.
* Registers a service at the right location in our container.
* PaymentService instances are added to the paymentProviders array in the
* container. Names are camelCase formatted and namespaced by the folder i.e:
* services/example-payments -> examplePaymentsService
@@ -464,13 +471,10 @@ export async function registerServices(
const context = { container, pluginDetails, registrationName: name }
registerPaymentServiceFromClass(loaded, context)
registerPaymentProcessorFromClass(loaded, context)
registerFulfillmentServiceFromClass(loaded, context)
registerAbstractFulfillmentServiceFromClass(loaded, context)
if (loaded.prototype instanceof OauthService) {
if (OauthService.isOauthService(loaded.prototype)) {
const appDetails = loaded.getAppDetails(pluginDetails.options)
const oauthService =
@@ -486,7 +490,9 @@ export async function registerServices(
}
),
})
} else if (isNotificationService(loaded.prototype)) {
} else if (
AbstractNotificationService.isNotificationService(loaded.prototype)
) {
container.registerAdd(
"notificationProviders",
asFunction((cradle) => new loaded(cradle, pluginDetails.options), {
@@ -505,10 +511,7 @@ export async function registerServices(
),
[`noti_${loaded.identifier}`]: aliasTo(name),
})
} else if (
loaded.prototype instanceof FileService ||
isFileService(loaded.prototype)
) {
} else if (AbstractFileService.isFileService(loaded.prototype)) {
// Add the service directly to the container in order to make simple
// resolution if we already know which file storage provider we need to use
container.register({
@@ -520,7 +523,7 @@ export async function registerServices(
),
[`fileService`]: aliasTo(name),
})
} else if (SearchUtils.isSearchService(loaded.prototype)) {
} else if (AbstractSearchService.isSearchService(loaded.prototype)) {
// Add the service directly to the container in order to make simple
// resolution if we already know which search provider we need to use
container.register({
@@ -534,7 +537,7 @@ export async function registerServices(
})
container.register(isSearchEngineInstalledResolutionKey, asValue(true))
} else if (loaded.prototype instanceof AbstractTaxService) {
} else if (AbstractTaxService.isTaxService(loaded.prototype)) {
container.registerAdd(
"taxProviders",
asFunction((cradle) => new loaded(cradle, pluginDetails.options), {

View File

@@ -3,9 +3,9 @@ import path from "path"
import { aliasTo, asFunction } from "awilix"
import formatRegistrationName from "../utils/format-registration-name"
import { isBatchJobStrategy } from "../interfaces"
import { MedusaContainer } from "../types/global"
import { isDefined } from "medusa-core-utils"
import { AbstractBatchJobStrategy } from "../interfaces"
type LoaderOptions = {
container: MedusaContainer
@@ -49,7 +49,7 @@ export default ({ container, configModule, isTest }: LoaderOptions): void => {
const loaded = require(fn).default
const name = formatRegistrationName(fn)
if (isBatchJobStrategy(loaded.prototype)) {
if (AbstractBatchJobStrategy.isBatchJobStrategy(loaded.prototype)) {
container.registerAdd(
"batchJobStrategies",
asFunction((cradle) => new loaded(cradle, configModule))

View File

@@ -10,7 +10,7 @@ class SystemTaxService extends AbstractTaxService {
static identifier = "system"
constructor() {
super()
super({})
}
async getTaxLines(

View File

@@ -3,6 +3,12 @@ import { SearchTypes } from "@medusajs/types"
export abstract class AbstractSearchService
implements SearchTypes.ISearchService
{
static _isSearchService = true
static isSearchService(obj) {
return obj?.constructor?._isSearchService
}
abstract readonly isDefault
protected readonly options_: Record<string, unknown>

View File

@@ -1,6 +1,4 @@
export * from "./abstract-service"
export * from "./is-search-service"
export * from "./search-relations"
export * from "./index-types"
export * from "./variant-keys"

View File

@@ -1,5 +0,0 @@
import { AbstractSearchService } from "./abstract-service"
export function isSearchService(obj: unknown): boolean {
return obj instanceof AbstractSearchService
}