feat: add removable sample vertical contract fixture
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { toResourceViewModel } from "../../application/view-models/resource-view-model.js";
|
||||
import { queryKeys } from "../../contracts/query-keys.js";
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* http: { execute(operationId: string, input?: Record<string, unknown>): Promise<
|
||||
* {ok: true, value: unknown} | {ok: false, error: import("../../contracts/errors.js").ApiFailure}
|
||||
* > },
|
||||
* cache: import("../../application/ports/query-cache-port.js").QueryCachePort
|
||||
* }} ports
|
||||
*/
|
||||
export function createSampleFacade(ports) {
|
||||
return Object.freeze({
|
||||
async listResources(filters = {}) {
|
||||
const key = queryKeys.resource.list(filters);
|
||||
const result = await ports.http.execute("LIST_SAMPLE_RESOURCES", {
|
||||
routeId: "SAMPLE_RESOURCE_LIST",
|
||||
});
|
||||
if (!result.ok) return result;
|
||||
|
||||
const models =
|
||||
/** @type {Array<import("../../domain/models/resource.js").Resource>} */ (
|
||||
result.value
|
||||
);
|
||||
const cached = ports.cache.write(key, models);
|
||||
if (!cached.ok) return cached;
|
||||
return {
|
||||
ok: /** @type {true} */ (true),
|
||||
value: models.map((model) => toResourceViewModel(model)),
|
||||
};
|
||||
},
|
||||
|
||||
/** @param {{ name: string }} command */
|
||||
async createResource(command) {
|
||||
const result = await ports.http.execute("CREATE_SAMPLE_RESOURCE", {
|
||||
body: command,
|
||||
routeId: "SAMPLE_RESOURCE_LIST",
|
||||
});
|
||||
if (!result.ok) return result;
|
||||
|
||||
const invalidated = await ports.cache.invalidate(queryKeys.resource.all());
|
||||
if (!invalidated.ok) return invalidated;
|
||||
return {
|
||||
ok: /** @type {true} */ (true),
|
||||
value: toResourceViewModel(
|
||||
/** @type {import("../../domain/models/resource.js").Resource} */ (
|
||||
result.value
|
||||
),
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { deriveAsyncState } from "../../application/view-models/async-state.js";
|
||||
import { AsyncSurface } from "../../presentation/components/async-surface.jsx";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* loading: boolean,
|
||||
* resources?: Array<{resourceId: string, title: string, createdAtLabel: string | null}>,
|
||||
* failure?: import("../../contracts/errors.js").ApiFailure
|
||||
* }} SamplePageState
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* facade: ReturnType<typeof import("./sample-facade.js").createSampleFacade>
|
||||
* }} props
|
||||
*/
|
||||
export function SampleResourcePage({ facade }) {
|
||||
const [result, setResult] = useState(
|
||||
/** @type {SamplePageState} */ ({
|
||||
loading: true,
|
||||
resources: undefined,
|
||||
failure: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
void facade.listResources().then((outcome) => {
|
||||
if (!active) return;
|
||||
setResult(
|
||||
outcome.ok
|
||||
? { loading: false, resources: outcome.value, failure: undefined }
|
||||
: { loading: false, resources: undefined, failure: outcome.error },
|
||||
);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [facade]);
|
||||
|
||||
const state = deriveAsyncState({
|
||||
isInitialLoading: result.loading,
|
||||
data: result.resources,
|
||||
failure: result.failure,
|
||||
});
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>샘플 리소스</h1>
|
||||
<AsyncSurface state={state}>
|
||||
<ul>
|
||||
{(result.resources ?? []).map((resource) => (
|
||||
<li key={resource.resourceId}>{resource.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AsyncSurface>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user