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:
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>
|
||||||
}
|
}
|
||||||
@@ -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" })
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+173
-195
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in new issue
Block a user