Workflow gateway
Skill marzun9620/agent_skills/workflow/skills/workflow-gateway
Gateway (external API client) implementation guide. Define the Port as a Context.Tag and implement with Layer.effect in Infrastructure. Wrap errors in GatewayError and apply externalApiRetryPolicy. Triggers: gateway creation, external API, Capsule, TableCheck, HTTP client, external service integration, API call, webhook.From its SKILL.md
npx -y skills add marzun9620/agent_skills --skill workflow-gatewayAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.9 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it
Gateway 実装手順
ADR-0004 / ADR-0006 準拠。外部サービス(Capsule CRM, TableCheck 等)への呼び出しを担当。
Repository と同じ Context.Tag + Layer.effect パターンだが、エラー型と retry policy が異なる。
Import ルール(重要)
全ての cross-directory import は barrel(index.js)経由 でなければならない(ADR-0004 §7):
// ✅ 正しい
import { GatewayError } from "~/usecase/ports/index.js";
import { AppConfig } from "~/packages/config/index.js";
import { externalApiRetryPolicy, withRetryLogging } from "~/packages/resilience/index.js";
// ❌ 禁止
import { AppConfig } from "~/packages/config/appConfig.js";
import { GatewayError } from "~/usecase/ports/errors.js";
Repository との違い
| Repository | Gateway | |
|---|---|---|
| エラー型 | RepositoryError | GatewayError |
| Retry | dbRetryPolicy (3回, 100ms) | externalApiRetryPolicy (2回, 500ms) |
| I/O | Drizzle ORM → PostgreSQL | HTTP client → 外部 API |
| Mapper | DB row → Domain entity | JSON response → Domain entity |
| テスト | Real DB (Testcontainers) | Mock HTTP (msw 等) or contract test |
1. Port 定義
ファイル: apps/datahub/src/usecase/ports/{service}Gateway.ts
import { Context, Effect } from "effect";
import { GatewayError } from "./errors.js";
import { Customer } from "~/domain/customer/index.js";
export class CapsuleCrmGateway extends Context.Tag("CapsuleCrmGateway")<
CapsuleCrmGateway,
{
readonly searchParties: (query: string) => Effect.Effect<ReadonlyArray<Customer>, GatewayError>;
readonly updateParty: (id: string, data: CustomerUpdate) => Effect.Effect<void, GatewayError>;
}
>() {}
- エラー型は
GatewayError(usecase/ports/errors.tsに既存) RepositoryErrorではなくGatewayErrorを使う
Port の barrel export: usecase/ports/index.ts に追加。
2. Infrastructure 実装
ファイル: apps/datahub/src/infrastructure/gateway/{service}/{service}GatewayLive.ts
import { Effect, Layer } from "effect";
import { CapsuleCrmGateway } from "~/usecase/ports/index.js";
import { GatewayError } from "~/usecase/ports/index.js";
import { AppConfig } from "~/packages/config/index.js";
import { withRetryLogging, externalApiRetryPolicy } from "~/packages/resilience/index.js";
import { toDomain } from "./mappers/partyMapper.js";
export const CapsuleCrmGatewayLive = Layer.effect(
CapsuleCrmGateway,
Effect.gen(function* () {
const config = yield* AppConfig;
const apiKey = config.capsuleApiKey;
return {
searchParties: (query) =>
withRetryLogging(
Effect.tryPromise({
try: () =>
fetch(`https://api.capsulecrm.com/api/v2/parties/search?q=${encodeURIComponent(query)}`, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json()),
catch: (cause) => new GatewayError({ cause }),
}).pipe(
Effect.map((response) => response.parties.map(toDomain)),
Effect.withLogSpan("CapsuleCrmGateway.searchParties"),
),
externalApiRetryPolicy,
"CapsuleCrmGateway.searchParties",
),
};
}),
);
重要な違い(Repository との比較):
Effect.tryPromiseの catch でGatewayError(RepositoryErrorではない)externalApiRetryPolicy(2回, 500ms —dbRetryPolicyの3回, 100ms ではない)AppConfigから API キーを取得- PII を含む外部レスポンスのログ出力禁止(ADR-0006)
3. Mapper 定義
ファイル: apps/datahub/src/infrastructure/gateway/{service}/mappers/{entity}Mapper.ts
import { Customer, CustomerId } from "~/domain/customer/index.js";
type CapsuleParty = {
id: number;
firstName: string;
lastName: string;
// ...
};
export const toDomain = (party: CapsuleParty): Customer =>
new Customer({
id: CustomerId.make(String(party.id)),
name: `${party.firstName} ${party.lastName}`,
// ...
});
- 外部 API のレスポンス型 → Domain Entity への変換
- PII フィールドをログに出さないこと
4. DI 登録
ファイル: apps/datahub/src/di/appLayer.ts
InfraLayer の Layer.mergeAll に CapsuleCrmGatewayLive を追加。
5. ディレクトリ構造
infrastructure/gateway/
├── capsuleCrm/
│ ├── index.ts # barrel export
│ ├── capsuleCrmGatewayLive.ts # Layer.effect 実装
│ └── mappers/
│ └── partyMapper.ts # 外部 JSON → Domain
└── tableCheck/
├── index.ts
├── tableCheckGatewayLive.ts
└── mappers/
└── bookingMapper.ts
テスト踏襲ルール
- Gateway のテストは外部 API をモックする(msw 等)
- Usecase テストでは
Layer.succeed(GatewayTag, { method: mockImpl })で mock Effect.either+ table-driven + AAA パターンを踏襲- 外部 API のタイムアウト・レート制限エラーのケースも含める
チェックリスト
- Port は
Context.Tagで定義 - エラーは
GatewayError(RepositoryErrorではない) -
externalApiRetryPolicyを適用 -
withRetryLoggingでラップ -
Effect.withLogSpanでスパン付与 - PII を含むデータをログに出力していない(ADR-0006)
- AppConfig から API キーを取得(ハードコード禁止)
- DI 層(
appLayer.ts)に登録済み - barrel export が存在
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.