feat: define housekeeping foundation contracts

This commit is contained in:
Simo committed 2026-08-25 19:06:23 +02:00
1 parent 3b3d7780c9
commit 7d62a04f0e
10 files changed
+338

No files matched your search

@@ -0,0 +1,36 @@
export type CapabilityRequirement =
| { mode: "all"; slugs: readonly string[] }
| { mode: "any"; slugs: readonly string[] };
export interface HousekeepingActor {
id: number;
username: string;
rank: number;
}
export interface HousekeepingCapabilityContext {
actor: HousekeepingActor;
isSuperAdmin: boolean;
has(slug: string): boolean;
hasAny(...slugs: string[]): boolean;
hasAll(...slugs: string[]): boolean;
}
function createCapabilityRequirement(
mode: CapabilityRequirement["mode"],
slugs: readonly string[],
): CapabilityRequirement {
if (slugs.length === 0) {
throw new Error("capability requirement is empty");
}
return { mode, slugs };
}
export function anyCapability(...slugs: string[]): CapabilityRequirement {
return createCapabilityRequirement("any", slugs);
}
export function allCapabilities(...slugs: string[]): CapabilityRequirement {
return createCapabilityRequirement("all", slugs);
}
@@ -0,0 +1,18 @@
import type { HousekeepingDomainId } from "../../migration/types";
import type {
CapabilityRequirement,
HousekeepingCapabilityContext,
} from "./capability";
import type { HousekeepingResult } from "./result";
export interface HousekeepingCommand<I, O> {
id: string;
owner: HousekeepingDomainId;
risk: "safe" | "sensitive";
capability: CapabilityRequirement;
requiresReason: boolean;
execute(
context: HousekeepingCapabilityContext,
input: I,
): Promise<HousekeepingResult<O>>;
}
@@ -0,0 +1,121 @@
import { describe, expect, it } from "vitest";
import {
allCapabilities,
anyCapability,
type CapabilityRequirement,
fail,
type HousekeepingCommand,
type HousekeepingDomainManifest,
type HousekeepingInboxSource,
type HousekeepingInboxSourceResult,
type HousekeepingQuery,
type HousekeepingSearchProvider,
type HousekeepingSearchResult,
type HousekeepingWidgetDefinition,
type HousekeepingWorkItem,
ok,
} from ".";
const capability = anyCapability("admin.users.view");
const workItem = {
sourceId: "tickets",
itemId: "ticket-42",
deduplicationKey: "ticket:42",
domain: "people",
capability,
severity: "warning",
occurredAt: "2026-08-24T12:00:00.000Z",
titleKey: "pages.housekeeping.items.ticket",
context: { ticketId: "42" },
href: "/admin-next/people/tickets/42",
freshness: "fresh",
} satisfies HousekeepingWorkItem;
const searchResult = {
id: "user-42",
domain: "people",
title: "operator",
href: "/admin-next/people/users/42",
} satisfies HousekeepingSearchResult;
const searchProvider: HousekeepingSearchProvider = {
search: async () => ok([searchResult], "search-1"),
};
const inboxSourceResult = {
items: [workItem],
availability: "available",
} satisfies HousekeepingInboxSourceResult;
const inboxSource: HousekeepingInboxSource = {
getItems: async () => ok(inboxSourceResult, "inbox-1"),
};
const widget: HousekeepingWidgetDefinition = {
load: async () => ok({ count: 2 }, "widget-1"),
};
const query: HousekeepingQuery<{ id: number }, { id: number }> = {
id: "people.user",
owner: "people",
capability,
run: async (_context, input) => ok(input, "query-1"),
};
const command: HousekeepingCommand<{ id: number }, { id: number }> = {
id: "people.user.disable",
owner: "people",
risk: "sensitive",
capability,
requiresReason: true,
execute: async (_context, input) => ok(input, "command-1"),
};
const manifest: HousekeepingDomainManifest = {
id: "people",
labelKey: "pages.housekeeping.domains.people.title",
descriptionKey: "pages.housekeeping.domains.people.description",
iconId: "users",
previewHref: "/admin-next/people",
capability,
routes: [],
};
describe("housekeeping foundation contracts", () => {
it("creates typed success and error results", () => {
expect(ok({ count: 2 }, "corr-1")).toEqual({
ok: true,
data: { count: 2 },
correlationId: "corr-1",
});
expect(fail("CAPABILITY_DENIED", "corr-2")).toEqual({
ok: false,
error: { code: "CAPABILITY_DENIED" },
correlationId: "corr-2",
});
});
it("rejects empty capability requirements", () => {
expect(() => anyCapability()).toThrow("capability requirement is empty");
expect(() => allCapabilities()).toThrow("capability requirement is empty");
});
it("exports assignable type-only contracts", () => {
const requirements: readonly CapabilityRequirement[] = [
capability,
allCapabilities("admin.users.view"),
];
expect([
workItem,
searchProvider,
inboxSource,
widget,
query,
command,
manifest,
requirements,
]).toHaveLength(8);
});
});
@@ -0,0 +1,20 @@
import type { HousekeepingDomainId } from "../../migration/types";
import type { CapabilityRequirement } from "./capability";
export interface HousekeepingRouteDefinition {
id: string;
labelKey: string;
href: string;
capability: CapabilityRequirement;
matchPrefixes?: readonly string[];
}
export interface HousekeepingDomainManifest {
id: HousekeepingDomainId;
labelKey: string;
descriptionKey: string;
iconId: "inbox" | "users" | "file-text" | "gem" | "hotel" | "settings";
previewHref: `/admin-next/${HousekeepingDomainId}`;
capability: CapabilityRequirement;
routes: readonly HousekeepingRouteDefinition[];
}
@@ -0,0 +1,36 @@
import type { HousekeepingDomainId } from "../../migration/types";
import type {
CapabilityRequirement,
HousekeepingCapabilityContext,
} from "./capability";
import type { HousekeepingResult } from "./result";
export type HousekeepingWorkItemSeverity = "info" | "warning" | "critical";
export type HousekeepingWorkItemFreshness = "fresh" | "stale";
export type HousekeepingInboxSourceAvailability = "available" | "unavailable";
export interface HousekeepingWorkItem {
sourceId: string;
itemId: string;
deduplicationKey: string;
domain: HousekeepingDomainId;
capability: CapabilityRequirement;
severity: HousekeepingWorkItemSeverity;
occurredAt: string;
titleKey: string;
context?: Readonly<Record<string, unknown>>;
href: string;
freshness: HousekeepingWorkItemFreshness;
}
export interface HousekeepingInboxSourceResult {
items: readonly HousekeepingWorkItem[];
availability: HousekeepingInboxSourceAvailability;
}
export interface HousekeepingInboxSource {
getItems(
context: HousekeepingCapabilityContext,
signal: AbortSignal,
): Promise<HousekeepingResult<HousekeepingInboxSourceResult>>;
}
@@ -0,0 +1,33 @@
export {
allCapabilities,
anyCapability,
type CapabilityRequirement,
type HousekeepingActor,
type HousekeepingCapabilityContext,
} from "./capability";
export type { HousekeepingCommand } from "./command";
export type {
HousekeepingDomainManifest,
HousekeepingRouteDefinition,
} from "./domain";
export type {
HousekeepingInboxSource,
HousekeepingInboxSourceAvailability,
HousekeepingInboxSourceResult,
HousekeepingWorkItem,
HousekeepingWorkItemFreshness,
HousekeepingWorkItemSeverity,
} from "./inbox";
export type { HousekeepingQuery } from "./query";
export {
fail,
type HousekeepingErrorCode,
type HousekeepingResult,
ok,
} from "./result";
export type {
HousekeepingSearchInput,
HousekeepingSearchProvider,
HousekeepingSearchResult,
} from "./search";
export type { HousekeepingWidgetDefinition } from "./widget";
@@ -0,0 +1,16 @@
import type { HousekeepingDomainId } from "../../migration/types";
import type {
CapabilityRequirement,
HousekeepingCapabilityContext,
} from "./capability";
import type { HousekeepingResult } from "./result";
export interface HousekeepingQuery<I, O> {
id: string;
owner: HousekeepingDomainId;
capability: CapabilityRequirement;
run(
context: HousekeepingCapabilityContext,
input: I,
): Promise<HousekeepingResult<O>>;
}
@@ -0,0 +1,28 @@
export type HousekeepingErrorCode =
| "VALIDATION_FAILED"
| "AUTHENTICATION_REQUIRED"
| "CAPABILITY_DENIED"
| "NOT_FOUND"
| "CONFLICT"
| "DEPENDENCY_UNAVAILABLE"
| "PARTIAL_RESULT"
| "INTERNAL_ERROR";
export type HousekeepingResult<T> =
| { ok: true; data: T; correlationId: string }
| {
ok: false;
error: { code: HousekeepingErrorCode };
correlationId: string;
};
export function ok<T>(data: T, correlationId: string): HousekeepingResult<T> {
return { ok: true, data, correlationId };
}
export function fail(
code: HousekeepingErrorCode,
correlationId: string,
): HousekeepingResult<never> {
return { ok: false, error: { code }, correlationId };
}
@@ -0,0 +1,22 @@
import type { HousekeepingDomainId } from "../../migration/types";
import type { HousekeepingCapabilityContext } from "./capability";
import type { HousekeepingResult } from "./result";
export interface HousekeepingSearchInput {
term: string;
limit: number;
}
export interface HousekeepingSearchResult {
id: string;
domain: HousekeepingDomainId;
title: string;
href: string;
}
export interface HousekeepingSearchProvider {
search(
context: HousekeepingCapabilityContext,
input: HousekeepingSearchInput,
): Promise<HousekeepingResult<readonly HousekeepingSearchResult[]>>;
}
@@ -0,0 +1,8 @@
import type { HousekeepingCapabilityContext } from "./capability";
import type { HousekeepingResult } from "./result";
export interface HousekeepingWidgetDefinition {
load(
context: HousekeepingCapabilityContext,
): Promise<HousekeepingResult<unknown>>;
}