|
|
|
@@ -0,0 +1,132 @@
|
|
|
|
|
// The procedure contracts of the catalog module: schemas and names, no handler and no port.
|
|
|
|
|
//
|
|
|
|
|
// The module is a client of the platform's Catalog service (B-wave contract §3.14): products and their
|
|
|
|
|
// imports live there, in the deployment's organization, and every procedure is one service call. A read
|
|
|
|
|
// reads; an import is an `effect` (it calls an external service and writes nothing of this app's own).
|
|
|
|
|
// The service's int64 fields are plain values here, so what a page loads is JSON: a revision is its digits,
|
|
|
|
|
// a count a number.
|
|
|
|
|
|
|
|
|
|
import { procedure } from "@conusai-kit/core"
|
|
|
|
|
import { z } from "zod"
|
|
|
|
|
import { CATALOG_PERMISSIONS, IMPORT_CHUNK_ROWS, PRODUCT_SEARCH_FIELDS } from "./constants.js"
|
|
|
|
|
|
|
|
|
|
const inProject = { org: z.string(), workspace: z.string(), project: z.string() }
|
|
|
|
|
const page = { after: z.string().max(64).optional(), limit: z.number().int().min(1).max(200).optional() }
|
|
|
|
|
/** What a form generates once per submission, so a retried request applies once. */
|
|
|
|
|
const requestId = z.string().uuid()
|
|
|
|
|
const productId = z.string().min(1).max(64)
|
|
|
|
|
/** A revision as its digits: the service's are 64-bit. */
|
|
|
|
|
const revision = z.string().regex(/^\d{1,19}$/)
|
|
|
|
|
|
|
|
|
|
export const productViewSchema = z.object({
|
|
|
|
|
productId: z.string(),
|
|
|
|
|
code: z.string(),
|
|
|
|
|
name: z.string(),
|
|
|
|
|
description: z.string(),
|
|
|
|
|
/** A UN/CEFACT Recommendation 20 unit code. */
|
|
|
|
|
baseUnit: z.string(),
|
|
|
|
|
status: z.enum(["active", "archived", "deleted"]),
|
|
|
|
|
groupCode: z.string().nullable(),
|
|
|
|
|
identifiers: z.array(z.object({ kind: z.string(), value: z.string() })),
|
|
|
|
|
revision,
|
|
|
|
|
})
|
|
|
|
|
export type ProductView = z.output<typeof productViewSchema>
|
|
|
|
|
|
|
|
|
|
export const importViewSchema = z.object({
|
|
|
|
|
importId: z.string(),
|
|
|
|
|
sourceName: z.string(),
|
|
|
|
|
format: z.string(),
|
|
|
|
|
status: z.string(),
|
|
|
|
|
revision,
|
|
|
|
|
created: z.number().int(),
|
|
|
|
|
updated: z.number().int(),
|
|
|
|
|
unchanged: z.number().int(),
|
|
|
|
|
refused: z.number().int(),
|
|
|
|
|
})
|
|
|
|
|
export type ImportView = z.output<typeof importViewSchema>
|
|
|
|
|
|
|
|
|
|
export const importOutcomeSchema = z.object({
|
|
|
|
|
import: importViewSchema,
|
|
|
|
|
rows: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
sourceRow: z.number().int(),
|
|
|
|
|
outcome: z.string(),
|
|
|
|
|
productId: z.string().nullable(),
|
|
|
|
|
reason: z.string().nullable(),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
export type ImportOutcomeView = z.output<typeof importOutcomeSchema>
|
|
|
|
|
|
|
|
|
|
const importRowSchema = z.object({
|
|
|
|
|
/** The line of the source file, which the service keeps as the row's provenance. */
|
|
|
|
|
sourceRow: z.number().int().min(1),
|
|
|
|
|
/** The revision the row replaces, for a product that exists. */
|
|
|
|
|
revision: revision.optional(),
|
|
|
|
|
product: z.object({
|
|
|
|
|
code: z.string().min(1).max(64),
|
|
|
|
|
name: z.string().min(1).max(200),
|
|
|
|
|
baseUnit: z.string().min(1).max(3),
|
|
|
|
|
description: z.string().max(4000).optional(),
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
export type ImportRow = z.output<typeof importRowSchema>
|
|
|
|
|
|
|
|
|
|
const productPage = z.object({ rows: z.array(productViewSchema), after: z.string().optional() })
|
|
|
|
|
|
|
|
|
|
export const productList = procedure("catalog.product.list")
|
|
|
|
|
.kind("read")
|
|
|
|
|
.scope("project")
|
|
|
|
|
.permission(CATALOG_PERMISSIONS.read)
|
|
|
|
|
.input(z.object({ ...inProject, ...page }))
|
|
|
|
|
.output(productPage)
|
|
|
|
|
.contract()
|
|
|
|
|
|
|
|
|
|
const searchField = z.enum(PRODUCT_SEARCH_FIELDS)
|
|
|
|
|
export type ProductSearchField = z.output<typeof searchField>
|
|
|
|
|
|
|
|
|
|
export const productSearch = procedure("catalog.product.search")
|
|
|
|
|
.kind("read")
|
|
|
|
|
.scope("project")
|
|
|
|
|
.permission(CATALOG_PERMISSIONS.read)
|
|
|
|
|
.input(z.object({ ...inProject, q: z.string().trim().min(1).max(200), by: searchField.optional(), ...page }))
|
|
|
|
|
.output(productPage)
|
|
|
|
|
.contract()
|
|
|
|
|
|
|
|
|
|
export const productGet = procedure("catalog.product.get")
|
|
|
|
|
.kind("read")
|
|
|
|
|
.scope("project")
|
|
|
|
|
.permission(CATALOG_PERMISSIONS.read)
|
|
|
|
|
.input(z.object({ ...inProject, productId }))
|
|
|
|
|
.output(productViewSchema)
|
|
|
|
|
.contract()
|
|
|
|
|
|
|
|
|
|
export const importList = procedure("catalog.import.list")
|
|
|
|
|
.kind("read")
|
|
|
|
|
.scope("project")
|
|
|
|
|
.permission(CATALOG_PERMISSIONS.read)
|
|
|
|
|
.input(z.object({ ...inProject, ...page }))
|
|
|
|
|
.output(z.object({ rows: z.array(importViewSchema), after: z.string().optional() }))
|
|
|
|
|
.contract()
|
|
|
|
|
|
|
|
|
|
export const importApply = procedure("catalog.import.apply")
|
|
|
|
|
.kind("effect")
|
|
|
|
|
.scope("project")
|
|
|
|
|
.permission(CATALOG_PERMISSIONS.write)
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
...inProject,
|
|
|
|
|
/** The source file's lower-case SHA-256: one file is one import, however many chunks it takes. */
|
|
|
|
|
fileHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
|
|
|
sourceName: z.string().trim().min(1).max(200),
|
|
|
|
|
format: z.enum(["csv", "xlsx", "json"]),
|
|
|
|
|
chunk: z.number().int().min(0),
|
|
|
|
|
finalChunk: z.boolean(),
|
|
|
|
|
/** The revision of the import, from the chunk before; absent on the first. */
|
|
|
|
|
revision: revision.optional(),
|
|
|
|
|
rows: z.array(importRowSchema).min(1).max(IMPORT_CHUNK_ROWS),
|
|
|
|
|
requestId,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.output(importOutcomeSchema)
|
|
|
|
|
.contract()
|