@@ -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()
|
||||
Reference in New Issue
Block a user