feat(core-flows,modules-sdk,types,medusa,link-modules): adds variant <> inventory item link endpoints (#7576)
what: - adds variant inventory link management endpoints: ``` Link inventory item to variant POST /products/:id/variants/:vid/inventory-items Update variant's inventory item link POST /products/:id/variants/:vid/inventory-items/:iid Unlink variant's inventory item DELETE /products/:id/variants/:vid/inventory-items/:iid ``` - a batch endpoint that does the above 3 across variants ``` POST /products/:id/variants/inventory-items ```
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
export * from "./steps/remove-remote-links"
|
||||
export * from "./steps/use-remote-query"
|
||||
export * from "./steps/create-remote-links"
|
||||
export * from "./steps/dismiss-remote-links"
|
||||
export * from "./steps/remove-remote-links"
|
||||
export * from "./steps/use-remote-query"
|
||||
export * from "./workflows/batch-links"
|
||||
export * from "./workflows/create-links"
|
||||
export * from "./workflows/dismiss-links"
|
||||
export * from "./workflows/update-links"
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { LinkDefinition, RemoteLink } from "@medusajs/modules-sdk"
|
||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
|
||||
type CreateRemoteLinksStepInput = LinkDefinition[]
|
||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||
|
||||
export const createLinksStepId = "create-remote-links"
|
||||
export const createRemoteLinkStep = createStep(
|
||||
createLinksStepId,
|
||||
async (data: CreateRemoteLinksStepInput, { container }) => {
|
||||
async (data: LinkDefinition[], { container }) => {
|
||||
const link = container.resolve<RemoteLink>(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
|
||||
type DismissRemoteLinksStepInput = LinkDefinition | LinkDefinition[]
|
||||
|
||||
// TODO: add ability for this step to restore links from only foreign keys
|
||||
export const dismissRemoteLinkStepId = "dismiss-remote-links"
|
||||
export const dismissRemoteLinkStep = createStep(
|
||||
dismissRemoteLinkStepId,
|
||||
@@ -18,18 +19,27 @@ export const dismissRemoteLinkStep = createStep(
|
||||
const link = container.resolve<RemoteLink>(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
|
||||
// Our current revert strategy for dismissed links are to recreate it again.
|
||||
// This works when its just the primary keys, but when you have additional data
|
||||
// in the links, we need to preserve them in order to recreate the links accurately.
|
||||
const dataBeforeDismiss = (await link.list(data, {
|
||||
asLinkDefinition: true,
|
||||
})) as LinkDefinition[]
|
||||
|
||||
await link.dismiss(entries)
|
||||
|
||||
return new StepResponse(entries, entries)
|
||||
return new StepResponse(entries, dataBeforeDismiss)
|
||||
},
|
||||
async (dismissdLinks, { container }) => {
|
||||
if (!dismissdLinks) {
|
||||
async (dataBeforeDismiss, { container }) => {
|
||||
if (!dataBeforeDismiss?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const link = container.resolve<RemoteLink>(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
await link.create(dismissdLinks)
|
||||
|
||||
await link.create(dataBeforeDismiss)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { LinkDefinition, RemoteLink } from "@medusajs/modules-sdk"
|
||||
import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||
|
||||
export const updateRemoteLinksStepId = "update-remote-links-step"
|
||||
export const updateRemoteLinksStep = createStep(
|
||||
updateRemoteLinksStepId,
|
||||
async (data: LinkDefinition[], { container }) => {
|
||||
if (!data.length) {
|
||||
return new StepResponse([], [])
|
||||
}
|
||||
|
||||
const link = container.resolve<RemoteLink>(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
|
||||
// Fetch all existing links and throw an error if any weren't found
|
||||
const dataBeforeUpdate = (await link.list(data, {
|
||||
asLinkDefinition: true,
|
||||
})) as LinkDefinition[]
|
||||
|
||||
const unequal = dataBeforeUpdate.length !== data.length
|
||||
|
||||
if (unequal) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find all existing links from data`
|
||||
)
|
||||
}
|
||||
|
||||
// link.create here performs an upsert. By performing validation above, we can ensure
|
||||
// that this method will always perform an update in these cases
|
||||
await link.create(data)
|
||||
|
||||
return new StepResponse(data, dataBeforeUpdate)
|
||||
},
|
||||
async (dataBeforeUpdate, { container }) => {
|
||||
if (!dataBeforeUpdate?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const link = container.resolve<RemoteLink>(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
|
||||
await link.create(dataBeforeUpdate)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
import { LinkDefinition } from "@medusajs/modules-sdk"
|
||||
import { BatchWorkflowInput } from "@medusajs/types"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { createRemoteLinkStep } from "../steps/create-remote-links"
|
||||
import { dismissRemoteLinkStep } from "../steps/dismiss-remote-links"
|
||||
import { updateRemoteLinksStep } from "../steps/update-remote-links"
|
||||
|
||||
export const batchLinksWorkflowId = "batch-links"
|
||||
export const batchLinksWorkflow = createWorkflow(
|
||||
batchLinksWorkflowId,
|
||||
(
|
||||
input: WorkflowData<
|
||||
BatchWorkflowInput<LinkDefinition, LinkDefinition, LinkDefinition>
|
||||
>
|
||||
) => {
|
||||
const [created, updated, deleted] = parallelize(
|
||||
createRemoteLinkStep(input.create || []),
|
||||
updateRemoteLinksStep(input.update || []),
|
||||
dismissRemoteLinkStep(input.delete || [])
|
||||
)
|
||||
|
||||
return {
|
||||
created,
|
||||
updated,
|
||||
deleted,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LinkDefinition } from "@medusajs/modules-sdk"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { createRemoteLinkStep } from "../steps/create-remote-links"
|
||||
|
||||
export const createLinksWorkflowId = "create-link"
|
||||
export const createLinksWorkflow = createWorkflow(
|
||||
createLinksWorkflowId,
|
||||
(input: WorkflowData<LinkDefinition[]>) => {
|
||||
return createRemoteLinkStep(input)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LinkDefinition } from "@medusajs/modules-sdk"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { dismissRemoteLinkStep } from "../steps/dismiss-remote-links"
|
||||
|
||||
export const dismissLinksWorkflowId = "dismiss-link"
|
||||
export const dismissLinksWorkflow = createWorkflow(
|
||||
dismissLinksWorkflowId,
|
||||
(input: WorkflowData<LinkDefinition[]>) => {
|
||||
return dismissRemoteLinkStep(input)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LinkDefinition } from "@medusajs/modules-sdk"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { updateRemoteLinksStep } from "../steps/update-remote-links"
|
||||
|
||||
export const updateLinksWorkflowId = "update-link"
|
||||
export const updateLinksWorkflow = createWorkflow(
|
||||
updateLinksWorkflowId,
|
||||
(input: WorkflowData<LinkDefinition[]>) => {
|
||||
return updateRemoteLinksStep(input)
|
||||
}
|
||||
)
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { isObject, promiseAll, toPascalCase } from "@medusajs/utils"
|
||||
import { Modules } from "./definitions"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
import { convertRecordsToLinkDefinition } from "./utils/convert-data-to-link-definition"
|
||||
import { linkingErrorMessage } from "./utils/linking-error"
|
||||
|
||||
export type DeleteEntityInput = {
|
||||
@@ -16,7 +17,8 @@ export type RestoreEntityInput = DeleteEntityInput
|
||||
|
||||
export type LinkDefinition = {
|
||||
[moduleName: string]: {
|
||||
[fieldName: string]: string
|
||||
// TODO: changing this to any temporarily as the "data" attribute is not being picked up correctly
|
||||
[fieldName: string]: any
|
||||
}
|
||||
} & {
|
||||
data?: Record<string, unknown>
|
||||
@@ -41,6 +43,14 @@ type CascadeError = {
|
||||
error: Error
|
||||
}
|
||||
|
||||
type LinkDataConfig = {
|
||||
moduleA: string
|
||||
moduleB: string
|
||||
primaryKeys: string[]
|
||||
moduleAKey: string
|
||||
moduleBKey: string
|
||||
}
|
||||
|
||||
export class RemoteLink {
|
||||
private modulesMap: Map<string, LoadedLinkModule> = new Map()
|
||||
private relationsPairs: Map<string, LoadedLinkModule> = new Map()
|
||||
@@ -325,6 +335,48 @@ export class RemoteLink {
|
||||
return [errors.length ? errors : null, result]
|
||||
}
|
||||
|
||||
private getLinkModuleOrThrow(link: LinkDefinition): LoadedLinkModule {
|
||||
const mods = Object.keys(link).filter((attr) => attr !== "data")
|
||||
|
||||
if (mods.length > 2) {
|
||||
throw new Error(`Only two modules can be linked.`)
|
||||
}
|
||||
|
||||
const { moduleA, moduleB, moduleAKey, moduleBKey } =
|
||||
this.getLinkDataConfig(link)
|
||||
const service = this.getLinkModule(moduleA, moduleAKey, moduleB, moduleBKey)
|
||||
|
||||
if (!service) {
|
||||
throw new Error(
|
||||
linkingErrorMessage({
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey,
|
||||
type: "link",
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
private getLinkDataConfig(link: LinkDefinition): LinkDataConfig {
|
||||
const moduleNames = Object.keys(link).filter((attr) => attr !== "data")
|
||||
const [moduleA, moduleB] = moduleNames
|
||||
const primaryKeys = Object.keys(link[moduleA])
|
||||
const moduleAKey = primaryKeys.join(",")
|
||||
const moduleBKey = Object.keys(link[moduleB]).join(",")
|
||||
|
||||
return {
|
||||
moduleA,
|
||||
moduleB,
|
||||
primaryKeys,
|
||||
moduleAKey,
|
||||
moduleBKey,
|
||||
}
|
||||
}
|
||||
|
||||
async create(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
||||
const allLinks = Array.isArray(link) ? link : [link]
|
||||
const serviceLinks = new Map<
|
||||
@@ -332,114 +384,72 @@ export class RemoteLink {
|
||||
[string | string[], string, Record<string, unknown>?][]
|
||||
>()
|
||||
|
||||
for (const rel of allLinks) {
|
||||
const extraFields = rel.data
|
||||
delete rel.data
|
||||
for (const link of allLinks) {
|
||||
const service = this.getLinkModuleOrThrow(link)
|
||||
const { moduleA, moduleB, moduleBKey, primaryKeys } =
|
||||
this.getLinkDataConfig(link)
|
||||
|
||||
const mods = Object.keys(rel)
|
||||
if (mods.length > 2) {
|
||||
throw new Error(`Only two modules can be linked.`)
|
||||
}
|
||||
|
||||
const [moduleA, moduleB] = mods
|
||||
const pk = Object.keys(rel[moduleA])
|
||||
const moduleAKey = pk.join(",")
|
||||
const moduleBKey = Object.keys(rel[moduleB]).join(",")
|
||||
|
||||
const service = this.getLinkModule(
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey
|
||||
)
|
||||
|
||||
if (!service) {
|
||||
throw new Error(
|
||||
linkingErrorMessage({
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey,
|
||||
type: "link",
|
||||
})
|
||||
)
|
||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
||||
if (!serviceLinks.has(service.__definition.key)) {
|
||||
serviceLinks.set(service.__definition.key, [])
|
||||
}
|
||||
|
||||
const pkValue =
|
||||
pk.length === 1 ? rel[moduleA][pk[0]] : pk.map((k) => rel[moduleA][k])
|
||||
primaryKeys.length === 1
|
||||
? link[moduleA][primaryKeys[0]]
|
||||
: primaryKeys.map((k) => link[moduleA][k])
|
||||
|
||||
const fields: unknown[] = [pkValue, rel[moduleB][moduleBKey]]
|
||||
if (isObject(extraFields)) {
|
||||
fields.push(extraFields)
|
||||
const fields: unknown[] = [pkValue, link[moduleB][moduleBKey]]
|
||||
|
||||
if (isObject(link.data)) {
|
||||
fields.push(link.data)
|
||||
}
|
||||
|
||||
serviceLinks.get(service.__definition.key)?.push(fields as any)
|
||||
}
|
||||
|
||||
const promises: Promise<unknown[]>[] = []
|
||||
|
||||
for (const [serviceName, links] of serviceLinks) {
|
||||
const service = this.modulesMap.get(serviceName)!
|
||||
|
||||
promises.push(service.create(links))
|
||||
}
|
||||
|
||||
const created = await promiseAll(promises)
|
||||
return created.flat()
|
||||
return (await promiseAll(promises)).flat()
|
||||
}
|
||||
|
||||
async dismiss(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
||||
const allLinks = Array.isArray(link) ? link : [link]
|
||||
const serviceLinks = new Map<string, [string | string[], string][]>()
|
||||
|
||||
for (const rel of allLinks) {
|
||||
const mods = Object.keys(rel)
|
||||
if (mods.length > 2) {
|
||||
throw new Error(`Only two modules can be linked.`)
|
||||
}
|
||||
for (const link of allLinks) {
|
||||
const service = this.getLinkModuleOrThrow(link)
|
||||
const { moduleA, moduleB, moduleBKey, primaryKeys } =
|
||||
this.getLinkDataConfig(link)
|
||||
|
||||
const [moduleA, moduleB] = mods
|
||||
const pk = Object.keys(rel[moduleA])
|
||||
const moduleAKey = pk.join(",")
|
||||
const moduleBKey = Object.keys(rel[moduleB]).join(",")
|
||||
|
||||
const service = this.getLinkModule(
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey
|
||||
)
|
||||
|
||||
if (!service) {
|
||||
throw new Error(
|
||||
linkingErrorMessage({
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey,
|
||||
type: "dismiss",
|
||||
})
|
||||
)
|
||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
||||
if (!serviceLinks.has(service.__definition.key)) {
|
||||
serviceLinks.set(service.__definition.key, [])
|
||||
}
|
||||
|
||||
const pkValue =
|
||||
pk.length === 1 ? rel[moduleA][pk[0]] : pk.map((k) => rel[moduleA][k])
|
||||
primaryKeys.length === 1
|
||||
? link[moduleA][primaryKeys[0]]
|
||||
: primaryKeys.map((k) => link[moduleA][k])
|
||||
|
||||
serviceLinks
|
||||
.get(service.__definition.key)
|
||||
?.push([pkValue, rel[moduleB][moduleBKey]])
|
||||
?.push([pkValue, link[moduleB][moduleBKey]] as any)
|
||||
}
|
||||
|
||||
const promises: Promise<unknown[]>[] = []
|
||||
|
||||
for (const [serviceName, links] of serviceLinks) {
|
||||
const service = this.modulesMap.get(serviceName)!
|
||||
|
||||
promises.push(service.dismiss(links))
|
||||
}
|
||||
|
||||
const created = await promiseAll(promises)
|
||||
return created.flat()
|
||||
return (await promiseAll(promises)).flat()
|
||||
}
|
||||
|
||||
async delete(
|
||||
@@ -453,4 +463,45 @@ export class RemoteLink {
|
||||
): Promise<[CascadeError[] | null, RestoredIds]> {
|
||||
return await this.executeCascade(removedServices, "restore")
|
||||
}
|
||||
|
||||
async list(
|
||||
link: LinkDefinition | LinkDefinition[],
|
||||
options?: { asLinkDefinition?: boolean }
|
||||
): Promise<(object | LinkDefinition)[]> {
|
||||
const allLinks = Array.isArray(link) ? link : [link]
|
||||
const serviceLinks = new Map<string, object[]>()
|
||||
|
||||
for (const link of allLinks) {
|
||||
const service = this.getLinkModuleOrThrow(link)
|
||||
const { moduleA, moduleB, moduleBKey, primaryKeys } =
|
||||
this.getLinkDataConfig(link)
|
||||
|
||||
if (!serviceLinks.has(service.__definition.key)) {
|
||||
serviceLinks.set(service.__definition.key, [])
|
||||
}
|
||||
|
||||
serviceLinks.get(service.__definition.key)?.push({
|
||||
...link[moduleA],
|
||||
...link[moduleB],
|
||||
})
|
||||
}
|
||||
|
||||
const promises: Promise<object[]>[] = []
|
||||
|
||||
for (const [serviceName, filters] of serviceLinks) {
|
||||
const service = this.modulesMap.get(serviceName)!
|
||||
|
||||
promises.push(
|
||||
service
|
||||
.list({ $or: filters })
|
||||
.then((links: any[]) =>
|
||||
options?.asLinkDefinition
|
||||
? convertRecordsToLinkDefinition(links, service)
|
||||
: links
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (await promiseAll(promises)).flat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { LoadedModule } from "@medusajs/types"
|
||||
import { isPresent } from "@medusajs/utils"
|
||||
import { LinkDefinition } from "../remote-link"
|
||||
|
||||
export const convertRecordsToLinkDefinition = (
|
||||
links: object[],
|
||||
service: LoadedModule
|
||||
): LinkDefinition[] => {
|
||||
const linkRelations = service.__joinerConfig.relationships || []
|
||||
const linkDataFields = service.__joinerConfig.extraDataFields || []
|
||||
|
||||
const results: LinkDefinition[] = []
|
||||
|
||||
for (const link of links) {
|
||||
const result: LinkDefinition = {}
|
||||
|
||||
for (const relation of linkRelations) {
|
||||
result[relation.serviceName] = {
|
||||
[relation.foreignKey]: link[relation.foreignKey],
|
||||
}
|
||||
}
|
||||
|
||||
const data: LinkDefinition["data"] = {}
|
||||
|
||||
for (const dataField of linkDataFields) {
|
||||
data[dataField] = link[dataField]
|
||||
}
|
||||
|
||||
if (isPresent(data)) {
|
||||
result.data = data
|
||||
}
|
||||
|
||||
results.push(result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -9,10 +9,10 @@ export type LinkWorkflowInput = {
|
||||
remove?: string[]
|
||||
}
|
||||
|
||||
export type BatchMethodRequest<TCreate, TUpdate> = {
|
||||
export type BatchMethodRequest<TCreate, TUpdate, TDelete = string> = {
|
||||
create?: TCreate[]
|
||||
update?: TUpdate[]
|
||||
delete?: string[]
|
||||
delete?: TDelete[]
|
||||
}
|
||||
|
||||
export type BatchMethodResponse<T> = {
|
||||
@@ -25,9 +25,10 @@ export type BatchMethodResponse<T> = {
|
||||
}
|
||||
}
|
||||
|
||||
export type BatchWorkflowInput<TCreate, TUpdate> = BatchMethodRequest<
|
||||
export type BatchWorkflowInput<
|
||||
TCreate,
|
||||
TUpdate
|
||||
>
|
||||
TUpdate,
|
||||
TDelete = string
|
||||
> = BatchMethodRequest<TCreate, TUpdate, TDelete>
|
||||
|
||||
export type BatchWorkflowOutput<T> = BatchMethodResponse<T>
|
||||
|
||||
@@ -128,6 +128,29 @@ export type ModulesResponse = {
|
||||
resolution: string | false
|
||||
}[]
|
||||
|
||||
type ExtraFieldType =
|
||||
| "date"
|
||||
| "time"
|
||||
| "datetime"
|
||||
| "bigint"
|
||||
| "blob"
|
||||
| "uint8array"
|
||||
| "array"
|
||||
| "enumArray"
|
||||
| "enum"
|
||||
| "json"
|
||||
| "integer"
|
||||
| "smallint"
|
||||
| "tinyint"
|
||||
| "mediumint"
|
||||
| "float"
|
||||
| "double"
|
||||
| "boolean"
|
||||
| "decimal"
|
||||
| "string"
|
||||
| "uuid"
|
||||
| "text"
|
||||
|
||||
export type ModuleJoinerConfig = Omit<
|
||||
JoinerServiceConfig,
|
||||
"serviceName" | "primaryKeys" | "relationships" | "extends"
|
||||
@@ -164,6 +187,11 @@ export type ModuleJoinerConfig = Omit<
|
||||
* If true it expands a RemoteQuery property but doesn't create a pivot table
|
||||
*/
|
||||
isReadOnlyLink?: boolean
|
||||
/**
|
||||
* Fields that will be part of the link record aside from the primary keys that can be updated
|
||||
* If not explicitly defined, this array will be populated by databaseConfig.extraFields
|
||||
*/
|
||||
extraDataFields?: string[]
|
||||
databaseConfig?: {
|
||||
/**
|
||||
* Name of the pivot table. If not provided it is auto generated
|
||||
@@ -176,28 +204,7 @@ export type ModuleJoinerConfig = Omit<
|
||||
extraFields?: Record<
|
||||
string,
|
||||
{
|
||||
type:
|
||||
| "date"
|
||||
| "time"
|
||||
| "datetime"
|
||||
| "bigint"
|
||||
| "blob"
|
||||
| "uint8array"
|
||||
| "array"
|
||||
| "enumArray"
|
||||
| "enum"
|
||||
| "json"
|
||||
| "integer"
|
||||
| "smallint"
|
||||
| "tinyint"
|
||||
| "mediumint"
|
||||
| "float"
|
||||
| "double"
|
||||
| "boolean"
|
||||
| "decimal"
|
||||
| "string"
|
||||
| "uuid"
|
||||
| "text"
|
||||
type: ExtraFieldType
|
||||
defaultValue?: string
|
||||
nullable?: boolean
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user