Files
template-app/modules/account/core/export.ts
T

47 lines
2.2 KiB
TypeScript

// "Export my data": every installed module's share of the account, composed into one document.
//
// Each module that keeps something about the person contributes an export to kit-tenancy's
// `tenancy.account` point -- this module's own rows among them -- and every one of them is a section.
// A module that fails is a `failed` section and the export is `complete: false`, naming it; a module
// that can erase but not export is `not_exported`. Nothing is left out without saying so (joint
// contract §12).
import { AppError, notFound } from "@conusai-kit/core"
import { ACCOUNT_EXPORT_FORMAT } from "../contract/constants.js"
import type { AccountExport, ExportSection } from "../contract/procedures.js"
import type { AccountDeps } from "./ports.js"
/** The problem code of a refusal, and whether trying again later may help. Never its detail. */
export function refusalOf(error: unknown): { code: string; retryable: boolean } {
if (error instanceof AppError) return { code: error.code, retryable: error.status >= 500 || error.status === 429 }
return { code: "unavailable", retryable: true }
}
export async function exportAccount(deps: AccountDeps, userId: string): Promise<AccountExport> {
if ((await deps.store.emailOf(userId)) === null) throw notFound("no such user")
const sections = await Promise.all(
deps.contributions.map(async (contribution): Promise<[string, ExportSection]> => {
if (!contribution.export) return [contribution.key, { status: "not_exported" }]
try {
const data = await contribution.export({
subject: userId,
container: deps.container,
...(deps.signal ? { signal: deps.signal } : {}),
})
return [contribution.key, { status: "ok", data }]
} catch (error) {
return [contribution.key, { status: "failed", ...refusalOf(error) }]
}
}),
)
const failed = sections.filter(([, section]) => section.status === "failed").map(([key]) => key)
return {
format: ACCOUNT_EXPORT_FORMAT,
exportedAt: new Date(deps.now()).toISOString(),
subject: userId,
complete: sections.every(([, section]) => section.status === "ok"),
failed,
sections: Object.fromEntries(sections),
}
}