Files
template-app/modules/archive/contract/procedures.ts
T
2026-11-14 09:56:45 +00:00

164 lines
5.4 KiB
TypeScript

// The procedure contracts of the archive module: schemas and names, no handler and no port.
//
// The module is a client of the platform's Archive service (B-wave contract §3.14): documents, their
// versions and their retention live there, in the deployment's organization, and the bytes in the
// platform's Blob store. A read reads; a change is an `effect` (it calls an external service and writes
// nothing of this app's own). A version is made from a file the project holds, so a procedure names a
// `fileId`, never a hash or a Blob reference of the caller's. The service's int64 fields are their digits
// here, so what a page loads is JSON.
import { procedure } from "@conusai-kit/core"
import { z } from "zod"
import { ARCHIVE_PERMISSIONS, RETENTION_BASES } from "./constants.js"
const inProject = { org: z.string(), workspace: z.string(), project: z.string() }
const page = { after: z.string().max(128).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 documentId = z.string().min(1).max(64)
const fileId = z.string().uuid()
/** A revision as its digits: the service's are 64-bit. */
const revision = z.string().regex(/^\d{1,19}$/)
export const documentViewSchema = z.object({
documentId: z.string(),
title: z.string(),
/** An optional UNTDID 1001 document code. */
kind: z.string().nullable(),
retentionClass: z.string(),
legalHold: z.boolean(),
status: z.enum(["active", "disposed"]),
/** Whether the document is past changing: disposed, or erased. */
disposed: z.boolean(),
revision,
createdAt: z.string(),
lastVersionAt: z.string(),
disposedAt: z.string().nullable(),
})
export type DocumentView = z.output<typeof documentViewSchema>
export const versionViewSchema = z.object({
versionId: z.string(),
sequence: z.number().int(),
sha256: z.string(),
mediaType: z.string(),
/** Bytes, as digits. */
size: z.string(),
createdAt: z.string(),
})
export type VersionView = z.output<typeof versionViewSchema>
export const retentionClassViewSchema = z.object({
code: z.string(),
keepFor: z.string(),
from: z.enum(RETENTION_BASES),
createdAt: z.string(),
})
export type RetentionClassView = z.output<typeof retentionClassViewSchema>
/** A version's immutable facts, as the use cases take them from the stored file. */
export interface VersionFacts {
readonly sha256: string
readonly mediaType: string
readonly size: number
readonly blobRef: string
}
const receipt = z.object({ documentId: z.string(), revision })
export type DocumentReceipt = z.output<typeof receipt>
const documentPage = z.object({ rows: z.array(documentViewSchema), after: z.string().optional() })
export const documentList = procedure("archive.document.list")
.kind("read")
.scope("project")
.permission(ARCHIVE_PERMISSIONS.read)
.input(z.object({ ...inProject, q: z.string().max(200).optional(), ...page }))
.output(documentPage)
.contract()
export const documentGet = procedure("archive.document.get")
.kind("read")
.scope("project")
.permission(ARCHIVE_PERMISSIONS.read)
.input(z.object({ ...inProject, documentId }))
.output(z.object({ document: documentViewSchema, versions: z.array(versionViewSchema) }))
.contract()
export const documentRegister = procedure("archive.document.register")
.kind("effect")
.scope("project")
.permission(ARCHIVE_PERMISSIONS.write)
.input(
z.object({
...inProject,
title: z.string().trim().min(1).max(200),
kind: z
.string()
.regex(/^\d{1,4}$/)
.optional(),
retentionClass: z.string().min(1).max(64),
fileId,
requestId,
}),
)
.output(receipt)
.contract()
export const versionAdd = procedure("archive.version.add")
.kind("effect")
.scope("project")
.permission(ARCHIVE_PERMISSIONS.write)
.input(z.object({ ...inProject, documentId, revision, fileId, requestId }))
.output(receipt)
.contract()
export const documentHold = procedure("archive.document.hold")
.kind("effect")
.scope("project")
.permission(ARCHIVE_PERMISSIONS.manage)
.input(z.object({ ...inProject, documentId, revision, legalHold: z.boolean(), requestId }))
.output(receipt)
.contract()
export const documentDispose = procedure("archive.document.dispose")
.kind("effect")
.scope("project")
.permission(ARCHIVE_PERMISSIONS.manage)
.input(z.object({ ...inProject, documentId, revision, requestId }))
.output(receipt)
.contract()
export const retentionList = procedure("archive.retention.list")
.kind("read")
.scope("project")
.permission(ARCHIVE_PERMISSIONS.read)
.input(z.object({ ...inProject, ...page }))
.output(z.object({ rows: z.array(retentionClassViewSchema), after: z.string().optional() }))
.contract()
export const retentionCreate = procedure("archive.retention.create")
.kind("effect")
.scope("project")
.permission(ARCHIVE_PERMISSIONS.manage)
.input(
z.object({
...inProject,
code: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
/** An ISO 8601 duration such as P10Y; the service reads it. */
keepFor: z.string().min(2).max(64),
from: z.enum(RETENTION_BASES),
requestId,
}),
)
.output(z.object({ code: z.string() }))
.contract()
export const disposalList = procedure("archive.disposal.list")
.kind("read")
.scope("project")
.permission(ARCHIVE_PERMISSIONS.read)
.input(z.object({ ...inProject, ...page }))
.output(documentPage)
.contract()