Feat(medusa): convert customer service to typescript (#1653)

* centralize common knowledge and checks for list-customers in repository

* convert customer service to typescript

* fix typing error in update-address

* add await

* add atomic phases

* update types for customerservice

* update api and model types

* pr feedback

* remove Promise.resolve

* typing of buildQuery

* remove atomic phase from private method
This commit is contained in:
Philip Korsholm authored and GitHub committed 2022-06-24 09:10:16 +02:00
1 parent 1585b7ae2b
commit fa7163941d
9 files changed
+237 -210

No files matched your search

@@ -53,5 +53,5 @@ export class AdminPostCustomersReq {
@IsObject() @IsObject()
@IsOptional() @IsOptional()
metadata?: object metadata?: Record<string, unknown>
} }
@@ -129,7 +129,7 @@ export class AdminPostCustomersCustomerReq {
@IsObject() @IsObject()
@IsOptional() @IsOptional()
metadata?: object metadata?: Record<string, unknown>
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@@ -64,7 +64,7 @@ export const defaultStoreCustomersRelations = [
"billing_address", "billing_address",
] ]
export const defaultStoreCustomersFields = [ export const defaultStoreCustomersFields: (keyof Customer)[] = [
"id", "id",
"email", "email",
"first_name", "first_name",
@@ -45,9 +45,9 @@ export default async (req, res) => {
"customerService" "customerService"
) as CustomerService ) as CustomerService
let customer = await customerService.updateAddress(id, address_id, validated) await customerService.updateAddress(id, address_id, validated)
customer = await customerService.retrieve(id, { const customer = await customerService.retrieve(id, {
relations: defaultStoreCustomersRelations, relations: defaultStoreCustomersRelations,
select: defaultStoreCustomersFields, select: defaultStoreCustomersFields,
}) })
@@ -93,5 +93,5 @@ export class StorePostCustomersCustomerReq {
@IsOptional() @IsOptional()
@IsObject() @IsObject()
metadata?: object metadata?: Record<string, unknown>
} }
+1 -1
View File
@@ -31,7 +31,7 @@ export class Customer extends SoftDeletableEntity {
@Index() @Index()
@Column({ nullable: true }) @Column({ nullable: true })
billing_address_id: string billing_address_id: string | null
@OneToOne(() => Address) @OneToOne(() => Address)
@JoinColumn({ name: "billing_address_id" }) @JoinColumn({ name: "billing_address_id" })
+31 -8
View File
@@ -1,23 +1,46 @@
import { EntityRepository, Repository } from "typeorm" import { Brackets, EntityRepository, ILike, Repository } from "typeorm"
import { Customer } from "../models/customer" import { Customer } from "../models/customer"
import { ExtendedFindConfig, Selector } from "../types/common"
@EntityRepository(Customer) @EntityRepository(Customer)
export class CustomerRepository extends Repository<Customer> { export class CustomerRepository extends Repository<Customer> {
async listAndCount(query, groups): Promise<[Customer[], number]> { async listAndCount(
let qb = this.createQueryBuilder("customer") query: ExtendedFindConfig<Customer, Selector<Customer>>,
.where(query.where) q: string | undefined = undefined
): Promise<[Customer[], number]> {
const groups = query.where.groups as { value: string[] }
delete query.where.groups
const qb = this.createQueryBuilder("customer")
.skip(query.skip) .skip(query.skip)
.take(query.take) .take(query.take)
if (q) {
delete query.where.email
delete query.where.first_name
delete query.where.last_name
qb.where(
new Brackets((qb) => {
qb.where({ email: ILike(`%${q}%`) })
.orWhere({ first_name: ILike(`%${q}%`) })
.orWhere({ last_name: ILike(`%${q}%`) })
})
)
}
qb.andWhere(query.where)
if (groups) { if (groups) {
qb = qb qb.leftJoinAndSelect("customer.groups", "group").andWhere(
.leftJoinAndSelect("customer.groups", "group") `group.id IN (:...ids)`,
.andWhere(`group.id IN (:...ids)`, { ids: groups.value }) { ids: groups.value }
)
} }
if (query.relations?.length) { if (query.relations?.length) {
query.relations.forEach((rel) => { query.relations.forEach((rel) => {
qb = qb.leftJoinAndSelect(`customer.${rel}`, rel) qb.leftJoinAndSelect(`customer.${rel}`, rel)
}) })
} }
@@ -1,16 +1,36 @@
import jwt from "jsonwebtoken" import jwt from "jsonwebtoken"
import _ from "lodash" import _ from "lodash"
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import Scrypt from "scrypt-kdf" import Scrypt from "scrypt-kdf"
import { Brackets, ILike } from "typeorm" import { DeepPartial, EntityManager } from "typeorm"
import { StorePostCustomersCustomerAddressesAddressReq } from "../api"
import { TransactionBaseService } from "../interfaces"
import { Address, Customer, CustomerGroup } from "../models"
import { AddressRepository } from "../repositories/address"
import { CustomerRepository } from "../repositories/customer"
import { AddressCreatePayload, FindConfig, Selector } from "../types/common"
import { CreateCustomerInput, UpdateCustomerInput } from "../types/customers"
import { buildQuery, setMetadata } from "../utils"
import { formatException } from "../utils/exception-formatter" import { formatException } from "../utils/exception-formatter"
import EventBusService from "./event-bus"
type InjectedDependencies = {
manager: EntityManager
eventBusService: EventBusService
customerRepository: typeof CustomerRepository
addressRepository: typeof AddressRepository
}
/** /**
* Provides layer to manipulate customers. * Provides layer to manipulate customers.
* @implements {BaseService}
*/ */
class CustomerService extends BaseService { class CustomerService extends TransactionBaseService<CustomerService> {
protected readonly customerRepository_: typeof CustomerRepository
protected readonly addressRepository_: typeof AddressRepository
protected readonly eventBusService_: EventBusService
protected readonly manager_: EntityManager
protected readonly transactionManager_: EntityManager | undefined
static Events = { static Events = {
PASSWORD_RESET: "customer.password_reset", PASSWORD_RESET: "customer.password_reset",
CREATED: "customer.created", CREATED: "customer.created",
@@ -22,39 +42,17 @@ class CustomerService extends BaseService {
customerRepository, customerRepository,
eventBusService, eventBusService,
addressRepository, addressRepository,
}) { }: InjectedDependencies) {
super() // eslint-disable-next-line prefer-rest-params
super(arguments[0])
/** @private @const {EntityManager} */
this.manager_ = manager this.manager_ = manager
/** @private @const {CustomerRepository} */
this.customerRepository_ = customerRepository this.customerRepository_ = customerRepository
this.eventBusService_ = eventBusService
/** @private @const {EventBus} */
this.eventBus_ = eventBusService
/** @private @const {AddressRepository} */
this.addressRepository_ = addressRepository this.addressRepository_ = addressRepository
} }
withTransaction(transactionManager) {
if (!transactionManager) {
return this
}
const cloned = new CustomerService({
manager: transactionManager,
customerRepository: this.customerRepository_,
eventBusService: this.eventBus_,
addressRepository: this.addressRepository_,
})
cloned.transactionManager_ = transactionManager
return cloned
}
/** /**
* Generate a JSON Web token, that will be sent to a customer, that wishes to * Generate a JSON Web token, that will be sent to a customer, that wishes to
* reset password. * reset password.
@@ -64,7 +62,8 @@ class CustomerService extends BaseService {
* @param {string} customerId - the customer to reset the password for * @param {string} customerId - the customer to reset the password for
* @return {string} the generated JSON web token * @return {string} the generated JSON web token
*/ */
async generateResetPasswordToken(customerId) { async generateResetPasswordToken(customerId: string): Promise<string> {
return await this.atomicPhase_(async (manager) => {
const customer = await this.retrieve(customerId, { const customer = await this.retrieve(customerId, {
select: [ select: [
"id", "id",
@@ -88,7 +87,9 @@ class CustomerService extends BaseService {
const payload = { customer_id: customer.id, exp: expiry } const payload = { customer_id: customer.id, exp: expiry }
const token = jwt.sign(payload, secret) const token = jwt.sign(payload, secret)
// Notify subscribers // Notify subscribers
this.eventBus_.emit(CustomerService.Events.PASSWORD_RESET, { this.eventBusService_
.withTransaction(manager)
.emit(CustomerService.Events.PASSWORD_RESET, {
id: customerId, id: customerId,
email: customer.email, email: customer.email,
first_name: customer.first_name, first_name: customer.first_name,
@@ -96,6 +97,7 @@ class CustomerService extends BaseService {
token, token,
}) })
return token return token
})
} }
/** /**
@@ -103,10 +105,12 @@ class CustomerService extends BaseService {
* @param {Object} config - the config object containing query settings * @param {Object} config - the config object containing query settings
* @return {Promise} the result of the find operation * @return {Promise} the result of the find operation
*/ */
async list(selector = {}, config = { relations: [], skip: 0, take: 50 }) { async list(
const customerRepo = this.manager_.getCustomRepository( selector: Selector<Customer> & { q?: string } = {},
this.customerRepository_ config: FindConfig<Customer> = { relations: [], skip: 0, take: 50 }
) ): Promise<Customer[]> {
return await this.atomicPhase_(async (manager) => {
const customerRepo = manager.getCustomRepository(this.customerRepository_)
let q let q
if ("q" in selector) { if ("q" in selector) {
@@ -114,29 +118,11 @@ class CustomerService extends BaseService {
delete selector.q delete selector.q
} }
const query = this.buildQuery_(selector, config) const query = buildQuery<Selector<Customer>, Customer>(selector, config)
if (q) { const [customers] = await customerRepo.listAndCount(query, q)
const where = query.where return customers
delete where.email
delete where.first_name
delete where.last_name
query.where = (qb) => {
qb.where(where)
qb.andWhere(
new Brackets((qb) => {
qb.where({ email: ILike(`%${q}%`) })
.orWhere({ first_name: ILike(`%${q}%`) })
.orWhere({ last_name: ILike(`%${q}%`) })
}) })
)
}
}
return customerRepo.find(query)
} }
/** /**
@@ -145,12 +131,16 @@ class CustomerService extends BaseService {
* @return {Promise} the result of the find operation * @return {Promise} the result of the find operation
*/ */
async listAndCount( async listAndCount(
selector, selector: Selector<Customer> & { q?: string },
config = { relations: [], skip: 0, take: 50, order: { created_at: "DESC" } } config: FindConfig<Customer> = {
) { relations: [],
const customerRepo = this.manager_.getCustomRepository( skip: 0,
this.customerRepository_ take: 50,
) order: { created_at: "DESC" },
}
): Promise<[Customer[], number]> {
return await this.atomicPhase_(async (manager) => {
const customerRepo = manager.getCustomRepository(this.customerRepository_)
let q let q
if ("q" in selector) { if ("q" in selector) {
@@ -158,63 +148,41 @@ class CustomerService extends BaseService {
delete selector.q delete selector.q
} }
const query = this.buildQuery_(selector, config) const query = buildQuery<Selector<Customer>, Customer>(selector, config)
const groups = query.where.groups return await customerRepo.listAndCount(query, q)
delete query.where.groups
if (q) {
const where = query.where
delete where.email
delete where.first_name
delete where.last_name
query.where = (qb) => {
qb.where(where)
qb.andWhere(
new Brackets((qb) => {
qb.where({ email: ILike(`%${q}%`) })
.orWhere({ first_name: ILike(`%${q}%`) })
.orWhere({ last_name: ILike(`%${q}%`) })
}) })
)
}
}
return await customerRepo.listAndCount(query, groups)
} }
/** /**
* Return the total number of documents in database * Return the total number of documents in database
* @return {Promise} the result of the count operation * @return {Promise} the result of the count operation
*/ */
count() { async count(): Promise<number> {
const customerRepo = this.manager_.getCustomRepository( return await this.atomicPhase_(async (manager) => {
this.customerRepository_ const customerRepo = manager.getCustomRepository(this.customerRepository_)
) return await customerRepo.count({})
return customerRepo.count({}) })
} }
/** private async retrieve_(
* Gets a customer by id. selector: Selector<Customer>,
* @param {string} customerId - the id of the customer to get. config: FindConfig<Customer> = {}
* @param {Object} config - the config object containing query settings ): Promise<Customer | never> {
* @return {Promise<Customer>} the customer document. const manager = this.transactionManager_ ?? this.manager_
*/
async retrieve(customerId, config = {}) {
const customerRepo = this.manager_.getCustomRepository(
this.customerRepository_
)
const validatedId = this.validateId_(customerId) const customerRepo = manager.getCustomRepository(this.customerRepository_)
const query = this.buildQuery_({ id: validatedId }, config)
const query = buildQuery(selector, config)
const customer = await customerRepo.findOne(query) const customer = await customerRepo.findOne(query)
if (!customer) { if (!customer) {
const selectorConstraints = Object.entries(selector)
.map((key, value) => `${key}: ${value}`)
.join(", ")
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_FOUND, MedusaError.Types.NOT_FOUND,
`Customer with ${customerId} was not found` `Customer with ${selectorConstraints} was not found`
) )
} }
@@ -227,22 +195,13 @@ class CustomerService extends BaseService {
* @param {Object} config - the config object containing query settings * @param {Object} config - the config object containing query settings
* @return {Promise<Customer>} the customer document. * @return {Promise<Customer>} the customer document.
*/ */
async retrieveByEmail(email, config = {}) { async retrieveByEmail(
const customerRepo = this.manager_.getCustomRepository( email: string,
this.customerRepository_ config: FindConfig<Customer> = {}
) ): Promise<Customer | never> {
return await this.atomicPhase_(async () => {
const query = this.buildQuery_({ email: email.toLowerCase() }, config) return await this.retrieve_({ email: email.toLowerCase() }, config)
const customer = await customerRepo.findOne(query) })
if (!customer) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Customer with email ${email} was not found`
)
}
return customer
} }
/** /**
@@ -251,22 +210,28 @@ class CustomerService extends BaseService {
* @param {Object} config - the config object containing query settings * @param {Object} config - the config object containing query settings
* @return {Promise<Customer>} the customer document. * @return {Promise<Customer>} the customer document.
*/ */
async retrieveByPhone(phone, config = {}) { async retrieveByPhone(
const customerRepo = this.manager_.getCustomRepository( phone: string,
this.customerRepository_ config: FindConfig<Customer> = {}
) ): Promise<Customer | never> {
return await this.atomicPhase_(async () => {
const query = this.buildQuery_({ phone }, config) return await this.retrieve_({ phone }, config)
const customer = await customerRepo.findOne(query) })
if (!customer) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Customer with phone ${phone} was not found`
)
} }
return customer /**
* Gets a customer by id.
* @param {string} customerId - the id of the customer to get.
* @param {Object} config - the config object containing query settings
* @return {Promise<Customer>} the customer document.
*/
async retrieve(
customerId: string,
config: FindConfig<Customer> = {}
): Promise<Customer> {
return await this.atomicPhase_(async () => {
return this.retrieve_({ id: customerId }, config)
})
} }
/** /**
@@ -274,7 +239,7 @@ class CustomerService extends BaseService {
* @param {string} password - the value to hash * @param {string} password - the value to hash
* @return {Promise<string>} hashed password * @return {Promise<string>} hashed password
*/ */
async hashPassword_(password) { async hashPassword_(password: string): Promise<string> {
const buf = await Scrypt.kdf(password, { logN: 1, r: 1, p: 1 }) const buf = await Scrypt.kdf(password, { logN: 1, r: 1, p: 1 })
return buf.toString("base64") return buf.toString("base64")
} }
@@ -287,17 +252,15 @@ class CustomerService extends BaseService {
* @param {object} customer - the customer to create * @param {object} customer - the customer to create
* @return {Promise} the result of create * @return {Promise} the result of create
*/ */
async create(customer) { async create(customer: CreateCustomerInput): Promise<Customer> {
return this.atomicPhase_(async (manager) => { return await this.atomicPhase_(async (manager) => {
const customerRepository = manager.getCustomRepository( const customerRepository = manager.getCustomRepository(
this.customerRepository_ this.customerRepository_
) )
const { email, password } = customer const { email, password } = customer
const existing = await this.retrieveByEmail(email).catch( const existing = await this.retrieveByEmail(email).catch(() => undefined)
(err) => undefined
)
if (existing && existing.has_account) { if (existing && existing.has_account) {
throw new MedusaError( throw new MedusaError(
@@ -314,7 +277,7 @@ class CustomerService extends BaseService {
const toUpdate = { ...existing, ...customer } const toUpdate = { ...existing, ...customer }
const updated = await customerRepository.save(toUpdate) const updated = await customerRepository.save(toUpdate)
await this.eventBus_ await this.eventBusService_
.withTransaction(manager) .withTransaction(manager)
.emit(CustomerService.Events.UPDATED, updated) .emit(CustomerService.Events.UPDATED, updated)
return updated return updated
@@ -328,7 +291,7 @@ class CustomerService extends BaseService {
const created = await customerRepository.create(customer) const created = await customerRepository.create(customer)
const result = await customerRepository.save(created) const result = await customerRepository.save(created)
await this.eventBus_ await this.eventBusService_
.withTransaction(manager) .withTransaction(manager)
.emit(CustomerService.Events.CREATED, result) .emit(CustomerService.Events.CREATED, result)
return result return result
@@ -343,13 +306,15 @@ class CustomerService extends BaseService {
* @param {object} update - an object with the update values. * @param {object} update - an object with the update values.
* @return {Promise} resolves to the update result. * @return {Promise} resolves to the update result.
*/ */
async update(customerId, update) { async update(
return this.atomicPhase_( customerId: string,
update: UpdateCustomerInput
): Promise<Customer> {
return await this.atomicPhase_(
async (manager) => { async (manager) => {
const customerRepository = manager.getCustomRepository( const customerRepository = manager.getCustomRepository(
this.customerRepository_ this.customerRepository_
) )
const addrRepo = manager.getCustomRepository(this.addressRepository_)
const customer = await this.retrieve(customerId) const customer = await this.retrieve(customerId)
@@ -363,13 +328,13 @@ class CustomerService extends BaseService {
} = update } = update
if (metadata) { if (metadata) {
customer.metadata = this.setMetadata_(customer, metadata) customer.metadata = setMetadata(customer, metadata)
} }
if ("billing_address_id" in update || "billing_address" in update) { if ("billing_address_id" in update || "billing_address" in update) {
const address = billing_address_id || billing_address const address = billing_address_id || billing_address
if (typeof address !== "undefined") { if (typeof address !== "undefined") {
await this.updateBillingAddress_(customer, address, addrRepo) await this.updateBillingAddress_(customer, address)
} }
} }
@@ -382,12 +347,12 @@ class CustomerService extends BaseService {
} }
if (groups) { if (groups) {
customer.groups = groups customer.groups = groups as CustomerGroup[]
} }
const updated = await customerRepository.save(customer) const updated = await customerRepository.save(customer)
await this.eventBus_ await this.eventBusService_
.withTransaction(manager) .withTransaction(manager)
.emit(CustomerService.Events.UPDATED, updated) .emit(CustomerService.Events.UPDATED, updated)
return updated return updated
@@ -405,60 +370,88 @@ class CustomerService extends BaseService {
* @param {Object} addrRepo - address repository * @param {Object} addrRepo - address repository
* @return {Promise} the result of the update operation * @return {Promise} the result of the update operation
*/ */
async updateBillingAddress_(customer, addressOrId, addrRepo) { async updateBillingAddress_(
if (addressOrId === null) { customer: Customer,
addressOrId: string | DeepPartial<Address> | undefined
): Promise<void> {
return await this.atomicPhase_(async (manager) => {
const addrRepo: AddressRepository = manager.getCustomRepository(
this.addressRepository_
)
if (addressOrId === null || addressOrId === undefined) {
customer.billing_address_id = null customer.billing_address_id = null
return return
} }
let address: DeepPartial<Address>
if (typeof addressOrId === `string`) { if (typeof addressOrId === `string`) {
addressOrId = await addrRepo.findOne({ const fetchedAddress = await addrRepo.findOne({
where: { id: addressOrId }, where: { id: addressOrId },
}) })
if (!fetchedAddress) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Address with id ${addressOrId} was not found`
)
} }
addressOrId.country_code = addressOrId.country_code.toLowerCase() address = fetchedAddress
} else {
address = addressOrId
}
if (addressOrId.id) { address.country_code = address.country_code?.toLowerCase()
customer.billing_address_id = addressOrId.id
if (typeof address?.id !== "undefined") {
customer.billing_address_id = address.id
} else { } else {
if (customer.billing_address_id) { if (customer.billing_address_id) {
const addr = await addrRepo.findOne({ const addr = await addrRepo.findOne({
where: { id: customer.billing_address_id }, where: { id: customer.billing_address_id },
}) })
await addrRepo.save({ ...addr, ...addressOrId }) await addrRepo.save({ ...addr, ...address })
} else { } else {
const created = addrRepo.create({ const created = addrRepo.create(address)
...addressOrId, const saved: Address = await addrRepo.save(created)
})
const saved = await addrRepo.save(created)
customer.billing_address = saved customer.billing_address = saved
} }
} }
})
} }
async updateAddress(customerId, addressId, address) { async updateAddress(
return this.atomicPhase_(async (manager) => { customerId: string,
addressId: string,
address: StorePostCustomersCustomerAddressesAddressReq
): Promise<Address> {
return await this.atomicPhase_(async (manager) => {
const addressRepo = manager.getCustomRepository(this.addressRepository_) const addressRepo = manager.getCustomRepository(this.addressRepository_)
address.country_code = address.country_code.toLowerCase() address.country_code = address.country_code?.toLowerCase()
const toUpdate = await addressRepo.findOne({ const toUpdate = await addressRepo.findOne({
where: { id: addressId, customer_id: customerId }, where: { id: addressId, customer_id: customerId },
}) })
if (!toUpdate) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Could not find address for customer"
)
}
for (const [key, value] of Object.entries(address)) { for (const [key, value] of Object.entries(address)) {
toUpdate[key] = value toUpdate[key] = value
} }
const result = addressRepo.save(toUpdate) return addressRepo.save(toUpdate)
return result
}) })
} }
async removeAddress(customerId, addressId) { async removeAddress(customerId: string, addressId: string): Promise<void> {
return this.atomicPhase_(async (manager) => { return await this.atomicPhase_(async (manager) => {
const addressRepo = manager.getCustomRepository(this.addressRepository_) const addressRepo = manager.getCustomRepository(this.addressRepository_)
// Should not fail, if user does not exist, since delete is idempotent // Should not fail, if user does not exist, since delete is idempotent
@@ -467,17 +460,18 @@ class CustomerService extends BaseService {
}) })
if (!address) { if (!address) {
return Promise.resolve() return
} }
await addressRepo.softRemove(address) await addressRepo.softRemove(address)
return Promise.resolve()
}) })
} }
async addAddress(customerId, address) { async addAddress(
return this.atomicPhase_(async (manager) => { customerId: string,
address: AddressCreatePayload
): Promise<Customer | Address> {
return await this.atomicPhase_(async (manager) => {
const addressRepository = manager.getCustomRepository( const addressRepository = manager.getCustomRepository(
this.addressRepository_ this.addressRepository_
) )
@@ -490,7 +484,8 @@ class CustomerService extends BaseService {
const shouldAdd = !customer.shipping_addresses.find( const shouldAdd = !customer.shipping_addresses.find(
(a) => (a) =>
a.country_code.toLowerCase() === address.country_code.toLowerCase() && a.country_code?.toLowerCase() ===
address.country_code.toLowerCase() &&
a.address_1 === address.address_1 && a.address_1 === address.address_1 &&
a.address_2 === address.address_2 && a.address_2 === address.address_2 &&
a.city === address.city && a.city === address.city &&
@@ -503,8 +498,8 @@ class CustomerService extends BaseService {
if (shouldAdd) { if (shouldAdd) {
const created = await addressRepository.create({ const created = await addressRepository.create({
customer_id: customerId,
...address, ...address,
customer_id: customerId,
}) })
const result = await addressRepository.save(created) const result = await addressRepository.save(created)
return result return result
@@ -520,37 +515,20 @@ class CustomerService extends BaseService {
* castable as an ObjectId * castable as an ObjectId
* @return {Promise} the result of the delete operation. * @return {Promise} the result of the delete operation.
*/ */
async delete(customerId) { async delete(customerId: string): Promise<Customer | void> {
return this.atomicPhase_(async (manager) => { return await this.atomicPhase_(async (manager) => {
const customerRepo = manager.getCustomRepository(this.customerRepository_) const customerRepo = manager.getCustomRepository(this.customerRepository_)
// Should not fail, if user does not exist, since delete is idempotent // Should not fail, if user does not exist, since delete is idempotent
const customer = await customerRepo.findOne({ where: { id: customerId } }) const customer = await customerRepo.findOne({ where: { id: customerId } })
if (!customer) { if (!customer) {
return Promise.resolve() return
} }
await customerRepo.softRemove(customer) return await customerRepo.softRemove(customer)
return Promise.resolve()
}) })
} }
/**
* Decorates a customer.
* @param {Customer} customer - the cart to decorate.
* @param {string[]} fields - the fields to include.
* @param {string[]} expandFields - fields to expand.
* @return {Customer} return the decorated customer.
*/
async decorate(customer, fields = [], expandFields = []) {
const requiredFields = ["_id", "metadata"]
const decorated = _.pick(customer, fields.concat(requiredFields))
const final = await this.runDecorators_(decorated)
return final
}
} }
export default CustomerService export default CustomerService
+26
View File
@@ -1,4 +1,5 @@
import { IsOptional, IsString } from "class-validator" import { IsOptional, IsString } from "class-validator"
import { AddressPayload } from "./common"
export class AdminListCustomerSelector { export class AdminListCustomerSelector {
@IsString() @IsString()
@@ -9,3 +10,28 @@ export class AdminListCustomerSelector {
@IsString({ each: true }) @IsString({ each: true })
groups?: string[] groups?: string[]
} }
export type CreateCustomerInput = {
email: string
password?: string
password_hash?: string
has_account?: boolean
first_name?: string
last_name?: string
phone?: string
metadata?: Record<string, unknown>
}
export type UpdateCustomerInput = {
password?: string
metadata?: Record<string, unknown>
billing_address?: AddressPayload | string
billing_address_id?: string
groups?: { id: string }[]
email?: string
first_name?: string
last_name?: string
phone?: string
}