@@ -0,0 +1,49 @@
|
||||
// The handlers of the module's procedures: each takes the service its kind may use and calls one use
|
||||
// case. No rule lives here.
|
||||
|
||||
import { implement } from "@conusai-kit/core"
|
||||
import { importApply, importList, productGet, productList, productSearch } from "../contract/index.js"
|
||||
import { applyImport, listImports, listProducts, searchProducts } from "../core/catalog.js"
|
||||
import { catalogServiceOf } from "./service.js"
|
||||
|
||||
export const productListImpl = implement(productList, ({ input, ctx }) =>
|
||||
listProducts(catalogServiceOf(ctx.container, ctx.kind), {
|
||||
...(input.after === undefined ? {} : { after: input.after }),
|
||||
...(input.limit === undefined ? {} : { limit: input.limit }),
|
||||
}),
|
||||
)
|
||||
|
||||
export const productSearchImpl = implement(productSearch, ({ input, ctx }) =>
|
||||
searchProducts(catalogServiceOf(ctx.container, ctx.kind), {
|
||||
q: input.q,
|
||||
...(input.by === undefined ? {} : { by: input.by }),
|
||||
...(input.after === undefined ? {} : { after: input.after }),
|
||||
...(input.limit === undefined ? {} : { limit: input.limit }),
|
||||
}),
|
||||
)
|
||||
|
||||
export const productGetImpl = implement(productGet, ({ input, ctx }) =>
|
||||
catalogServiceOf(ctx.container, ctx.kind).getProduct(input.productId),
|
||||
)
|
||||
|
||||
export const importListImpl = implement(importList, ({ input, ctx }) =>
|
||||
listImports(catalogServiceOf(ctx.container, ctx.kind), {
|
||||
...(input.after === undefined ? {} : { after: input.after }),
|
||||
...(input.limit === undefined ? {} : { limit: input.limit }),
|
||||
}),
|
||||
)
|
||||
|
||||
export const importApplyImpl = implement(importApply, ({ input, ctx }) =>
|
||||
applyImport(catalogServiceOf(ctx.container, "effect"), ctx.scope, {
|
||||
fileHash: input.fileHash,
|
||||
sourceName: input.sourceName,
|
||||
format: input.format,
|
||||
chunk: input.chunk,
|
||||
finalChunk: input.finalChunk,
|
||||
...(input.revision === undefined ? {} : { revision: input.revision }),
|
||||
rows: input.rows,
|
||||
requestId: input.requestId,
|
||||
}),
|
||||
)
|
||||
|
||||
export const CATALOG_PROCEDURES = [productListImpl, productSearchImpl, productGetImpl, importListImpl, importApplyImpl]
|
||||
@@ -0,0 +1,172 @@
|
||||
// Generated SDK adapter. Production dispatch requires a server-prepared request binding.
|
||||
|
||||
import { businessAppError, type Client, requestBusinessBinding, toBusinessError } from "@conusai-kit/business"
|
||||
import type { Container, ProcedureKind } from "@conusai-kit/core"
|
||||
import {
|
||||
type CatalogImport,
|
||||
catalogFindImports,
|
||||
catalogFindProducts,
|
||||
catalogGetProduct,
|
||||
catalogImportProducts,
|
||||
catalogSearchProducts,
|
||||
type ImportOutcome,
|
||||
type Product,
|
||||
} from "@conusai-sdk/catalog"
|
||||
import { createClient } from "@conusai-sdk/catalog/client"
|
||||
import type { ImportOutcomeView, ImportView, ProductSearchField, ProductView } from "../contract/index.js"
|
||||
import { CATALOG_SERVICE, type CatalogReads, type CatalogService, catalogReadsOf } from "../core/ports.js"
|
||||
|
||||
const productView = (product: Product): ProductView => ({
|
||||
productId: product.id,
|
||||
code: product.code,
|
||||
name: product.name,
|
||||
description: product.description ?? "",
|
||||
baseUnit: product.base_unit,
|
||||
status: product.status,
|
||||
groupCode: product.group_code ?? null,
|
||||
identifiers: (product.identifiers ?? []).map(({ kind, value }) => ({ kind, value })),
|
||||
revision: String(product.revision),
|
||||
})
|
||||
|
||||
const importView = (row: CatalogImport): ImportView => ({
|
||||
importId: row.id,
|
||||
sourceName: row.source_name,
|
||||
format: row.format,
|
||||
status: row.status,
|
||||
revision: String(row.revision),
|
||||
created: Number(row.created),
|
||||
updated: Number(row.updated),
|
||||
unchanged: Number(row.unchanged),
|
||||
refused: Number(row.refused),
|
||||
})
|
||||
|
||||
const outcomeView = (outcome: ImportOutcome): ImportOutcomeView => ({
|
||||
import: importView(outcome.import),
|
||||
rows: outcome.rows.map((row) => ({
|
||||
sourceRow: row.source_row,
|
||||
outcome: row.outcome,
|
||||
productId: row.product_id ?? null,
|
||||
reason: row.reason ?? null,
|
||||
})),
|
||||
})
|
||||
|
||||
/** What a lookup asks the service: a code prefix or an identifier. A name goes to the ranked search. */
|
||||
function searchQuery(search: { by: Exclude<ProductSearchField, "name">; q: string }) {
|
||||
switch (search.by) {
|
||||
case "code":
|
||||
return { code_prefix: search.q }
|
||||
case "gtin":
|
||||
case "erp":
|
||||
return { identifier_kind: search.by, identifier_value: search.q }
|
||||
}
|
||||
}
|
||||
|
||||
/** What a generated operation answered: its data, or the kit's app error for the refusal (`businessAppError`). */
|
||||
async function answer<T>(
|
||||
call: Promise<{ data?: T; error?: unknown; response?: Response }>,
|
||||
): Promise<Exclude<T, undefined>> {
|
||||
let result: { data?: T; error?: unknown; response?: Response }
|
||||
try {
|
||||
result = await call
|
||||
} catch (error) {
|
||||
throw businessAppError(toBusinessError(error, undefined))
|
||||
}
|
||||
if (result.error !== undefined || result.data === undefined) {
|
||||
throw businessAppError(toBusinessError(result.error, result.response))
|
||||
}
|
||||
return result.data as Exclude<T, undefined>
|
||||
}
|
||||
|
||||
/** The port over one client, in one organization. */
|
||||
export function sdkCatalogService(binding: { client: Client; orgSlug: string }): CatalogService {
|
||||
const { client } = binding
|
||||
const org = { org_slug: binding.orgSlug }
|
||||
return {
|
||||
async listProducts({ search, after, limit }) {
|
||||
const page = await answer(
|
||||
catalogFindProducts({
|
||||
client,
|
||||
path: org,
|
||||
query: {
|
||||
...(search ? searchQuery(search) : {}),
|
||||
...(after === undefined ? {} : { after }),
|
||||
limit: BigInt(limit),
|
||||
},
|
||||
}),
|
||||
)
|
||||
return { rows: page.data.map(productView), ...(page.next_after ? { after: page.next_after } : {}) }
|
||||
},
|
||||
async searchProducts({ q, after, limit }) {
|
||||
const page = await answer(
|
||||
catalogSearchProducts({
|
||||
client,
|
||||
path: org,
|
||||
query: { q, ...(after === undefined ? {} : { after }), limit: BigInt(limit) },
|
||||
}),
|
||||
)
|
||||
return {
|
||||
rows: page.data.map((hit) => productView(hit.product)),
|
||||
...(page.next_after ? { after: page.next_after } : {}),
|
||||
}
|
||||
},
|
||||
async getProduct(productId) {
|
||||
return productView(await answer(catalogGetProduct({ client, path: { ...org, product_id: productId } })))
|
||||
},
|
||||
async listImports({ after, limit }) {
|
||||
const page = await answer(
|
||||
catalogFindImports({
|
||||
client,
|
||||
path: org,
|
||||
query: { ...(after === undefined ? {} : { after }), limit: BigInt(limit) },
|
||||
}),
|
||||
)
|
||||
return { rows: page.data.map(importView), ...(page.next_after ? { after: page.next_after } : {}) }
|
||||
},
|
||||
async applyImport(input) {
|
||||
const outcome = await answer(
|
||||
catalogImportProducts({
|
||||
client,
|
||||
path: org,
|
||||
body: {
|
||||
file_hash: input.fileHash,
|
||||
source_name: input.sourceName,
|
||||
format: input.format,
|
||||
chunk: input.chunk,
|
||||
final_chunk: input.finalChunk,
|
||||
...(input.revision === undefined ? {} : { revision: BigInt(input.revision) }),
|
||||
rows: input.rows.map((row) => ({
|
||||
source_row: row.sourceRow,
|
||||
...(row.revision === undefined ? {} : { revision: BigInt(row.revision) }),
|
||||
product: {
|
||||
code: row.product.code,
|
||||
name: row.product.name,
|
||||
base_unit: row.product.baseUnit,
|
||||
...(row.product.description === undefined ? {} : { description: row.product.description }),
|
||||
},
|
||||
})),
|
||||
idempotency_key: input.key,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return outcomeView(outcome)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The service a handler of `kind` may use: the bound one (the token's view for the kind applies), or
|
||||
* the current verified request's, narrowed to its reads for a `read` or `write` handler as the token would.
|
||||
*/
|
||||
export function catalogServiceOf(container: Container, kind: "effect"): CatalogService
|
||||
export function catalogServiceOf(container: Container, kind: ProcedureKind): CatalogReads
|
||||
export function catalogServiceOf(container: Container, kind: ProcedureKind): CatalogReads {
|
||||
if (container.has(CATALOG_SERVICE)) return container.resolve(CATALOG_SERVICE) as CatalogReads
|
||||
let service: CatalogService
|
||||
try {
|
||||
const binding = requestBusinessBinding(container)
|
||||
service = sdkCatalogService({ client: binding.client(createClient), orgSlug: binding.orgSlug })
|
||||
} catch (error) {
|
||||
throw businessAppError(error)
|
||||
}
|
||||
return kind === "effect" ? service : catalogReadsOf(service)
|
||||
}
|
||||
Reference in New Issue
Block a user