feat(link-modules,modules-sdk, utils, types, products) - Remote Link and Link modules (#4695)
What:
- Definition of all Modules links
- `link-modules` package to manage the creation of all pre-defined link or custom ones
```typescript
import { initialize as iniInventory } from "@medusajs/inventory";
import { initialize as iniProduct } from "@medusajs/product";
import {
initialize as iniLinks,
runMigrations as migrateLinks
} from "@medusajs/link-modules";
await Promise.all([iniInventory(), iniProduct()]);
await migrateLinks(); // create tables based on previous loaded modules
await iniLinks(); // load link based on previous loaded modules
await iniLinks(undefined, [
{
serviceName: "product_custom_translation_service_link",
isLink: true,
databaseConfig: {
tableName: "product_transalations",
},
alias: [
{
name: "translations",
},
],
primaryKeys: ["id", "product_id", "translation_id"],
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "product_id",
alias: "product",
},
{
serviceName: "custom_translation_service",
primaryKey: "id",
foreignKey: "translation_id",
alias: "transalation",
deleteCascade: true,
},
],
extends: [
{
serviceName: Modules.PRODUCT,
relationship: {
serviceName: "product_custom_translation_service_link",
primaryKey: "product_id",
foreignKey: "id",
alias: "translations",
isList: true,
},
},
{
serviceName: "custom_translation_service",
relationship: {
serviceName: "product_custom_translation_service_link",
primaryKey: "product_id",
foreignKey: "id",
alias: "product_link",
},
},
],
},
]); // custom links
```
Remote Link
```typescript
import { RemoteLink, Modules } from "@medusajs/modules-sdk";
// [...] initialize modules and links
const remoteLink = new RemoteLink();
// upsert the relationship
await remoteLink.create({ // one (object) or many (array)
[Modules.PRODUCT]: {
variant_id: "var_abc",
},
[Modules.INVENTORY]: {
inventory_item_id: "iitem_abc",
},
data: { // optional additional fields
required_quantity: 5
}
});
// dismiss (doesn't cascade)
await remoteLink.dismiss({ // one (object) or many (array)
[Modules.PRODUCT]: {
variant_id: "var_abc",
},
[Modules.INVENTORY]: {
inventory_item_id: "iitem_abc",
},
});
// delete
await remoteLink.delete({
// every key is a module
[Modules.PRODUCT]: {
// every key is a linkable field
variant_id: "var_abc", // single or multiple values
},
});
// restore
await remoteLink.restore({
// every key is a module
[Modules.PRODUCT]: {
// every key is a linkable field
variant_id: "var_abc", // single or multiple values
},
});
```
Co-authored-by: Riqwan Thamir <5105988+riqwan@users.noreply.github.com>
This commit is contained in:
co-authored by
Riqwan Thamir
parent
bc4c9e0d32
commit
4d16acf5f0
@@ -0,0 +1,22 @@
|
||||
import { Constructor, ILinkModule, ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { LinkModuleService } from "@services"
|
||||
|
||||
export function getModuleService(
|
||||
joinerConfig: ModuleJoinerConfig
|
||||
): Constructor<ILinkModule> {
|
||||
const joinerConfig_ = JSON.parse(JSON.stringify(joinerConfig))
|
||||
delete joinerConfig_.databaseConfig
|
||||
return class LinkService extends LinkModuleService<unknown> {
|
||||
override __joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig_ as ModuleJoinerConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getReadOnlyModuleService(joinerConfig: ModuleJoinerConfig) {
|
||||
return class ReadOnlyLinkService {
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig as ModuleJoinerConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./dynamic-service-class"
|
||||
export { default as LinkService } from "./link"
|
||||
export { default as LinkModuleService } from "./link-module-service"
|
||||
@@ -0,0 +1,301 @@
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
FindConfig,
|
||||
ILinkModule,
|
||||
InternalModuleDeclaration,
|
||||
ModuleJoinerConfig,
|
||||
RestoreReturn,
|
||||
SoftDeleteReturn,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MapToConfig,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
mapObjectTo,
|
||||
} from "@medusajs/utils"
|
||||
import { LinkService } from "@services"
|
||||
import { shouldForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
linkService: LinkService<any>
|
||||
primaryKey: string | string[]
|
||||
foreignKey: string
|
||||
extraFields: string[]
|
||||
}
|
||||
|
||||
export default class LinkModuleService<TLink> implements ILinkModule {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected readonly linkService_: LinkService<TLink>
|
||||
protected primaryKey_: string[]
|
||||
protected foreignKey_: string
|
||||
protected extraFields_: string[]
|
||||
|
||||
constructor(
|
||||
{
|
||||
baseRepository,
|
||||
linkService,
|
||||
primaryKey,
|
||||
foreignKey,
|
||||
extraFields,
|
||||
}: InjectedDependencies,
|
||||
readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
this.baseRepository_ = baseRepository
|
||||
this.linkService_ = linkService
|
||||
this.primaryKey_ = !Array.isArray(primaryKey) ? [primaryKey] : primaryKey
|
||||
this.foreignKey_ = foreignKey
|
||||
this.extraFields_ = extraFields
|
||||
}
|
||||
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return {} as ModuleJoinerConfig
|
||||
}
|
||||
|
||||
private buildData(
|
||||
primaryKeyData: string | string[],
|
||||
foreignKeyData: string,
|
||||
extra: Record<string, unknown> = {}
|
||||
) {
|
||||
if (this.primaryKey_.length > 1) {
|
||||
if (
|
||||
!Array.isArray(primaryKeyData) ||
|
||||
primaryKeyData.length !== this.primaryKey_.length
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Primary key data must be an array ${this.primaryKey_.length} values`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const pk = this.primaryKey_.join(",")
|
||||
return {
|
||||
[pk]: primaryKeyData,
|
||||
[this.foreignKey_]: foreignKeyData,
|
||||
...extra,
|
||||
}
|
||||
}
|
||||
|
||||
private isValidKeyName(name: string) {
|
||||
return this.primaryKey_.concat(this.foreignKey_).includes(name)
|
||||
}
|
||||
|
||||
private validateFields(data: any) {
|
||||
const keys = Object.keys(data)
|
||||
if (!keys.every((k) => this.isValidKeyName(k))) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Invalid field name provided. Valid field names are ${this.primaryKey_.concat(
|
||||
this.foreignKey_
|
||||
)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async retrieve(
|
||||
primaryKeyData: string | string[],
|
||||
foreignKeyData: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<unknown> {
|
||||
const filter = this.buildData(primaryKeyData, foreignKeyData)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<unknown>(filter)
|
||||
const entry = await this.linkService_.list(queryOptions, {}, sharedContext)
|
||||
|
||||
if (!entry?.length) {
|
||||
const pk = this.primaryKey_.join(",")
|
||||
const errMessage = `${pk}[${primaryKeyData}] and ${this.foreignKey_}[${foreignKeyData}]`
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Entry ${errMessage} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return entry[0]
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async list(
|
||||
filters: Record<string, unknown> = {},
|
||||
config: FindConfig<unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<unknown[]> {
|
||||
const rows = await this.linkService_.list(filters, config, sharedContext)
|
||||
|
||||
return await this.baseRepository_.serialize<object[]>(rows)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async listAndCount(
|
||||
filters: Record<string, unknown> = {},
|
||||
config: FindConfig<unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[unknown[], number]> {
|
||||
const [rows, count] = await this.linkService_.listAndCount(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return [await this.baseRepository_.serialize<object[]>(rows), count]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async create(
|
||||
primaryKeyOrBulkData:
|
||||
| string
|
||||
| string[]
|
||||
| [string | string[], string, Record<string, unknown>][],
|
||||
foreignKeyData?: string,
|
||||
extraFields?: Record<string, unknown>,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const data: unknown[] = []
|
||||
if (foreignKeyData === undefined && Array.isArray(primaryKeyOrBulkData)) {
|
||||
for (const [primaryKey, foreignKey, extra] of primaryKeyOrBulkData) {
|
||||
data.push(
|
||||
this.buildData(
|
||||
primaryKey as string | string[],
|
||||
foreignKey as string,
|
||||
extra as Record<string, unknown>
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
data.push(
|
||||
this.buildData(
|
||||
primaryKeyOrBulkData as string | string[],
|
||||
foreignKeyData!,
|
||||
extraFields
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const links = await this.linkService_.create(data, sharedContext)
|
||||
|
||||
return await this.baseRepository_.serialize<object[]>(links)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async dismiss(
|
||||
primaryKeyOrBulkData: string | string[] | [string | string[], string][],
|
||||
foreignKeyData?: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const data: unknown[] = []
|
||||
if (foreignKeyData === undefined && Array.isArray(primaryKeyOrBulkData)) {
|
||||
for (const [primaryKey, foreignKey] of primaryKeyOrBulkData) {
|
||||
data.push(this.buildData(primaryKey, foreignKey as string))
|
||||
}
|
||||
} else {
|
||||
data.push(
|
||||
this.buildData(
|
||||
primaryKeyOrBulkData as string | string[],
|
||||
foreignKeyData!
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const links = await this.linkService_.dismiss(data, sharedContext)
|
||||
|
||||
return await this.baseRepository_.serialize<object[]>(links)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async delete(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
this.validateFields(data)
|
||||
|
||||
await this.linkService_.delete(data, sharedContext)
|
||||
}
|
||||
|
||||
async softDelete(
|
||||
data: any,
|
||||
{ returnLinkableKeys }: SoftDeleteReturn = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<Record<string, unknown[]> | void> {
|
||||
this.validateFields(data)
|
||||
|
||||
let [, cascadedEntitiesMap] = await this.softDelete_(data, sharedContext)
|
||||
|
||||
const pk = this.primaryKey_.join(",")
|
||||
const entityNameToLinkableKeysMap: MapToConfig = {
|
||||
LinkModel: [
|
||||
{ mapTo: pk, valueFrom: pk },
|
||||
{ mapTo: this.foreignKey_, valueFrom: this.foreignKey_ },
|
||||
],
|
||||
}
|
||||
|
||||
let mappedCascadedEntitiesMap
|
||||
if (returnLinkableKeys) {
|
||||
// Map internal table/column names to their respective external linkable keys
|
||||
// eg: product.id = product_id, variant.id = variant_id
|
||||
mappedCascadedEntitiesMap = mapObjectTo<Record<string, string[]>>(
|
||||
cascadedEntitiesMap,
|
||||
entityNameToLinkableKeysMap,
|
||||
{
|
||||
pick: returnLinkableKeys,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
protected async softDelete_(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[string[], Record<string, string[]>]> {
|
||||
return await this.linkService_.softDelete(data, sharedContext)
|
||||
}
|
||||
|
||||
async restore(
|
||||
data: any,
|
||||
{ returnLinkableKeys }: RestoreReturn = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<Record<string, unknown[]> | void> {
|
||||
this.validateFields(data)
|
||||
|
||||
let [, cascadedEntitiesMap] = await this.restore_(data, sharedContext)
|
||||
|
||||
const pk = this.primaryKey_.join(",")
|
||||
const entityNameToLinkableKeysMap: MapToConfig = {
|
||||
LinkModel: [
|
||||
{ mapTo: pk, valueFrom: pk },
|
||||
{ mapTo: this.foreignKey_, valueFrom: this.foreignKey_ },
|
||||
],
|
||||
}
|
||||
|
||||
let mappedCascadedEntitiesMap
|
||||
if (returnLinkableKeys) {
|
||||
// Map internal table/column names to their respective external linkable keys
|
||||
// eg: product.id = product_id, variant.id = variant_id
|
||||
mappedCascadedEntitiesMap = mapObjectTo<Record<string, string[]>>(
|
||||
cascadedEntitiesMap,
|
||||
entityNameToLinkableKeysMap,
|
||||
{
|
||||
pick: returnLinkableKeys,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async restore_(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[string[], Record<string, string[]>]> {
|
||||
return await this.linkService_.restore(data, sharedContext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Context, FindConfig } from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
linkRepository: any
|
||||
}
|
||||
|
||||
export default class LinkService<TEntity> {
|
||||
protected readonly linkRepository_: any
|
||||
|
||||
constructor({ linkRepository }: InjectedDependencies) {
|
||||
this.linkRepository_ = linkRepository
|
||||
}
|
||||
|
||||
@InjectManager("linkRepository_")
|
||||
async list(
|
||||
filters: unknown = {},
|
||||
config: FindConfig<unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<unknown>(
|
||||
filters as any,
|
||||
config
|
||||
)
|
||||
return await this.linkRepository_.find(queryOptions, sharedContext)
|
||||
}
|
||||
|
||||
@InjectManager("linkRepository_")
|
||||
async listAndCount(
|
||||
filters = {},
|
||||
config: FindConfig<unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], number]> {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<unknown>(filters, config)
|
||||
return await this.linkRepository_.findAndCount(queryOptions, sharedContext)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await this.linkRepository_.create(data, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async dismiss(
|
||||
data: unknown[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const filter: any = []
|
||||
for (const pair of data) {
|
||||
filter.push({
|
||||
$and: Object.entries(pair as object).map(([key, value]) => ({
|
||||
[key]: value,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
const [rows] = await this.linkRepository_.softDelete(
|
||||
{ $or: filter },
|
||||
{
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
}
|
||||
)
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async delete(
|
||||
data: unknown,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this.linkRepository_.delete(data, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async softDelete(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[string[], Record<string, string[]>]> {
|
||||
const filter = {}
|
||||
for (const key in data) {
|
||||
filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] }
|
||||
}
|
||||
|
||||
return await this.linkRepository_.softDelete(filter, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async restore(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[string[], Record<string, string[]>]> {
|
||||
const filter = {}
|
||||
for (const key in data) {
|
||||
filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] }
|
||||
}
|
||||
|
||||
return await this.linkRepository_.restore(data, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user