Files
template-app/modules/crm/adapters/service.ts
T
2026-11-14 09:56:45 +00:00

152 lines
5.3 KiB
TypeScript

// Generated SDK adapter. Production dispatch requires a server-prepared request binding.
import {
businessAppError,
type Client,
canonicalActionJson,
requestBusinessBinding,
requiresAppActionTransport,
toBusinessError,
} from "@conusai-kit/business"
import { type Container, forbidden, type ProcedureKind } from "@conusai-kit/core"
import {
type ContactPoint,
type ContactValue,
crmAddContactPoint,
crmCreateParty,
crmGetParty,
crmListContactPoints,
crmListParties,
type Party,
} from "@conusai-sdk/crm"
import { createClient } from "@conusai-sdk/crm/client"
import { type ContactView, type PartyView, SUBJECT_SCHEME } from "../contract/index.js"
import { contactRequestBody, partyRequestBody } from "../core/action-plan.js"
import { CRM_SERVICE, type CrmReads, type CrmService, crmReadsOf } from "../core/ports.js"
const partyView = (party: Party): PartyView => ({
partyId: party.id,
kind: party.kind,
displayName: party.display_name,
legalName: party.legal_name ?? null,
country: party.country,
status: party.status,
linked: (party.identifiers ?? []).some((identifier) => identifier.scheme === SUBJECT_SCHEME),
})
/** A contact value as a person reads it. */
function valueText(value: ContactValue): string {
switch (value.kind) {
case "postal":
return [value.value.street, value.value.postal_code, value.value.city, value.value.country]
.filter((part): part is string => typeof part === "string" && part !== "")
.join(", ")
case "peppol_endpoint":
return `${value.value.eas}:${value.value.id}`
default:
return value.value
}
}
const contactView = (contact: ContactPoint): ContactView =>
contact.fields
? {
contactId: contact.id,
kind: contact.fields.kind,
value: valueText(contact.fields),
purposes: contact.fields.purposes,
isPrimary: contact.fields.is_primary,
erased: contact.erased,
}
: // Erased: no personal value remains, only that there was a contact point.
{ contactId: contact.id, kind: "email", value: "", purposes: [], isPrimary: false, erased: true }
/** 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 of one platform project. */
export function sdkCrmService(binding: { client: Client; orgSlug: string; bindingId: string }): CrmService {
const { client } = binding
const org = { org_slug: binding.orgSlug }
return {
bindingId: binding.bindingId,
async listParties(query) {
const page = await answer(crmListParties({ client, path: org, query: { ...query, limit: BigInt(query.limit) } }))
return { rows: page.data.map(partyView), ...(page.next_after ? { after: page.next_after } : {}) }
},
async getParty(partyId) {
return partyView(await answer(crmGetParty({ client, path: { ...org, id: partyId } })))
},
async listContacts(partyId, query) {
const page = await answer(
crmListContactPoints({
client,
path: { ...org, id: partyId },
query: { ...query, limit: BigInt(query.limit) },
}),
)
return { rows: page.data.map(contactView), ...(page.next_after ? { after: page.next_after } : {}) }
},
async createParty(input) {
const party = await answer(
crmCreateParty({
client,
path: org,
body: partyRequestBody(input),
bodySerializer: canonicalActionJson,
}),
)
return partyView(party)
},
async addContact(input) {
const contact = await answer(
crmAddContactPoint({
client,
path: { ...org, id: input.partyId },
body: contactRequestBody(input),
bodySerializer: canonicalActionJson,
}),
)
return contactView(contact)
},
}
}
/**
* 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 crmServiceOf(container: Container, kind: "effect"): CrmService
export function crmServiceOf(container: Container, kind: ProcedureKind): CrmReads
export function crmServiceOf(container: Container, kind: ProcedureKind): CrmReads {
if (container.has(CRM_SERVICE)) {
if (requiresAppActionTransport(container)) throw forbidden("app action requires prepared business transport")
return container.resolve(CRM_SERVICE) as CrmReads
}
let service: CrmService
try {
const binding = requestBusinessBinding(container)
service = sdkCrmService({
client: binding.client(createClient),
orgSlug: binding.orgSlug,
bindingId: binding.bindingId,
})
} catch (error) {
throw businessAppError(error)
}
return kind === "effect" ? service : crmReadsOf(service)
}