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:
Carlos R. L. Rodrigues
2023-08-30 14:31:32 +00:00
committed by GitHub
co-authored by Riqwan Thamir
parent bc4c9e0d32
commit 4d16acf5f0
97 changed files with 3540 additions and 424 deletions
@@ -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)
}
}
+117
View File
@@ -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,
})
}
}