@@ -0,0 +1,60 @@
|
||||
// Fixed server declarations. The same pure plan is used by the real core and
|
||||
// generated SDK body encoder; declarations alone grant no approval/permission.
|
||||
import { requestBusinessBinding } from "@conusai-kit/business"
|
||||
import { type Container, forbidden, type TenantScope } from "@conusai-kit/core"
|
||||
import { contactAdd, partyCreate } from "../contract/index.js"
|
||||
import { contactActionData, contactEncodedRequest, partyActionData, partyEncodedRequest } from "../core/action-plan.js"
|
||||
|
||||
import { CRM_SERVICE } from "../core/ports.js"
|
||||
|
||||
interface Context {
|
||||
readonly scope: TenantScope
|
||||
readonly container: Container
|
||||
readonly now: Date
|
||||
}
|
||||
function current(context: Context) {
|
||||
if (context.container.has(CRM_SERVICE)) throw forbidden("app action requires prepared business transport")
|
||||
if (context.scope.level !== "project" || context.scope.actor.system) throw forbidden("app action refused")
|
||||
const binding = requestBusinessBinding(context.container)
|
||||
if (binding.actorId !== context.scope.actor.userId || binding.appTenantId !== context.scope.orgId)
|
||||
throw forbidden("app action refused")
|
||||
return { binding, scope: context.scope }
|
||||
}
|
||||
function bindingFields(binding: ReturnType<typeof requestBusinessBinding>) {
|
||||
return {
|
||||
bindingId: binding.bindingId,
|
||||
environmentUuid: binding.environmentUuid,
|
||||
appTenantId: binding.appTenantId,
|
||||
actorId: binding.actorId,
|
||||
orgSlug: binding.orgSlug,
|
||||
platformProjectId: binding.platformProjectId,
|
||||
}
|
||||
}
|
||||
export const crmAppActionTargets = Object.freeze([
|
||||
{
|
||||
procedure: partyCreate,
|
||||
encode(input: unknown, context: Context) {
|
||||
const { binding, scope } = current(context)
|
||||
return {
|
||||
binding: bindingFields(binding),
|
||||
request: partyEncodedRequest(
|
||||
binding.orgSlug,
|
||||
partyActionData(scope, binding.bindingId, partyCreate.input.parse(input)),
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
procedure: contactAdd,
|
||||
encode(input: unknown, context: Context) {
|
||||
const { binding, scope } = current(context)
|
||||
return {
|
||||
binding: bindingFields(binding),
|
||||
request: contactEncodedRequest(
|
||||
binding.orgSlug,
|
||||
contactActionData(scope, contactAdd.input.parse(input), context.now),
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 { contactAdd, partyCreate, partyGet, partyList } from "../contract/index.js"
|
||||
import { addContact, createParty, getParty, listParties } from "../core/crm.js"
|
||||
import { crmServiceOf } from "./service.js"
|
||||
|
||||
export const partyListImpl = implement(partyList, ({ input, ctx }) =>
|
||||
listParties(crmServiceOf(ctx.container, ctx.kind), {
|
||||
...(input.q === undefined ? {} : { q: input.q }),
|
||||
...(input.after === undefined ? {} : { after: input.after }),
|
||||
...(input.limit === undefined ? {} : { limit: input.limit }),
|
||||
}),
|
||||
)
|
||||
|
||||
export const partyGetImpl = implement(partyGet, ({ input, ctx }) =>
|
||||
getParty(crmServiceOf(ctx.container, ctx.kind), input.partyId),
|
||||
)
|
||||
|
||||
export const partyCreateImpl = implement(partyCreate, ({ input, ctx }) =>
|
||||
createParty(crmServiceOf(ctx.container, "effect"), ctx.scope, {
|
||||
kind: input.kind,
|
||||
displayName: input.displayName,
|
||||
...(input.legalName === undefined ? {} : { legalName: input.legalName }),
|
||||
country: input.country,
|
||||
...(input.linkSelf === undefined ? {} : { linkSelf: input.linkSelf }),
|
||||
requestId: input.requestId,
|
||||
}),
|
||||
)
|
||||
|
||||
export const contactAddImpl = implement(contactAdd, ({ input, ctx }) =>
|
||||
addContact(crmServiceOf(ctx.container, "effect"), ctx.scope, {
|
||||
partyId: input.partyId,
|
||||
kind: input.kind,
|
||||
value: input.value,
|
||||
purposes: input.purposes,
|
||||
isPrimary: input.isPrimary,
|
||||
requestId: input.requestId,
|
||||
}),
|
||||
)
|
||||
|
||||
export const CRM_PROCEDURES = [partyListImpl, partyGetImpl, partyCreateImpl, contactAddImpl]
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user