agentsclimarketplace

Workflow repository

Skill marzun9620/agent_skills/workflow/skills/workflow-repository

Repository (Port + Infrastructure) implementation guide. Define the Port as a Context.Tag and implement with Drizzle ORM via Layer.effect. Integration tests use a real DB. Triggers: repository creation, Drizzle implementation, port definition, DB operation, infrastructure implementation, Layer.effect, findById, database.From its SKILL.md

Install
npx -y skills add marzun9620/agent_skills --skill workflow-repository

Assembled 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

8.6 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it

Repository 実装手順

ADR-0004 / ADR-0005 準拠。Port → Infrastructure → Test の順で実装。

Import ルール(重要)

全ての cross-directory import は barrel(index.js)経由 でなければならない(ADR-0004 §7):

// ✅ 正しい
import { EntityRepository } from "~/usecase/ports/index.js";
import { DrizzleClient } from "~/infrastructure/db/index.js";
import { AppConfig } from "~/packages/config/index.js";

// ❌ 禁止 — 直接ファイル import
import { EntityRepository } from "~/usecase/ports/entityRepository.js";
import { AppConfig } from "~/packages/config/appConfig.js";

同一ディレクトリ内(./)の import は barrel 不要。

1. Port 定義

ファイル: apps/datahub/src/usecase/ports/{entity}Repository.ts

import { Context, Effect } from "effect";
import { EntityId, Entity, EntityNotFoundError } from "~/domain/{module}/index.js";
import { RepositoryError } from "./errors.js";

export class EntityRepository extends Context.Tag("EntityRepository")<
  EntityRepository,
  {
    readonly findById: (id: EntityId) => Effect.Effect<Entity, EntityNotFoundError | RepositoryError>;
    readonly save: (entity: Entity) => Effect.Effect<void, RepositoryError>;
  }
>() {}
  • Context.Tag(name)<Tag, Interface>() パターン
  • E channel に全エラー型を明示
  • RepositoryErrorusecase/ports/errors.ts に既存
  • save の戻り値は Effect<void, RepositoryError> — Entity を返さない。保存操作は副作用のみ。Effect.asVoid でラップすること

Port の barrel export: usecase/ports/index.ts に追加。

2. Mapper 定義

ファイル: apps/datahub/src/infrastructure/db/mappers/{entity}Mapper.ts

import { Entity, EntityId } from "~/domain/{module}/index.js";
import { type entitiesInHw } from "~/infrastructure/db/index.js";

type EntityRow = typeof entitiesInHw.$inferSelect;

export const toDomain = (row: EntityRow): Entity =>
  new Entity({
    id: EntityId.make(row.publicId),
    name: row.name,
    status: row.status,
  });

export const toRow = (entity: Entity): EntityInsertRow => ({
  publicId: entity.id,
  name: entity.name,
  status: entity.status,
});
  • 関数名は toDomain / toRow(エンティティ名をプレフィクスにしない)
  • barrel export: infrastructure/db/mappers/index.ts に追加(ただし mapper は同一パッケージ内の直接 import も可)

3. Infrastructure 実装

ファイル: apps/datahub/src/infrastructure/repository/{entity}RepositoryLive.ts

import { Effect, Layer } from "effect";
import { eq } from "drizzle-orm";
import { EntityRepository, RepositoryError } from "~/usecase/ports/index.js";
import { DrizzleClient, entitiesInHw } from "~/infrastructure/db/index.js";
import { EntityNotFoundError } from "~/domain/{module}/index.js";
import { toDomain, toRow } from "~/infrastructure/db/mappers/{entity}Mapper.js";
import { dbRetryPolicy, withRetryLogging } from "~/packages/resilience/index.js";

export const EntityRepositoryLive = Layer.effect(
  EntityRepository,
  Effect.gen(function* () {
    const { db } = yield* DrizzleClient;
    return {
      findById: (id) =>
        withRetryLogging(
          Effect.tryPromise({
            try: () => db.query.entitiesInHw.findFirst({ where: eq(entitiesInHw.publicId, id) }),
            catch: (cause) => new RepositoryError({ cause }),
          }).pipe(
            Effect.flatMap((row) =>
              row
                ? Effect.succeed(toDomain(row))
                : Effect.fail(new EntityNotFoundError({ entityId: id })),
            ),
            Effect.withLogSpan("EntityRepository.findById"),
          ),
          dbRetryPolicy,
          "EntityRepository.findById",
        ),

      save: (entity) =>
        withRetryLogging(
          Effect.tryPromise({
            try: () =>
              db.insert(entitiesInHw).values(toRow(entity)).onConflictDoUpdate({
                target: entitiesInHw.publicId,
                set: toRow(entity),
              }),
            catch: (cause) => new RepositoryError({ cause }),
          }).pipe(
            Effect.asVoid,
            Effect.withLogSpan("EntityRepository.save"),
          ),
          dbRetryPolicy,
          "EntityRepository.save",
        ),
    };
  }),
);

重要ルール:

  • Repository メソッドは Layer 内で定義する(standalone 関数禁止)
  • saveEffect.asVoid で void に変換(Entity を返さない)
  • 全メソッドに withRetryLogging + dbRetryPolicy を適用
  • 全メソッドに Effect.withLogSpan("{ClassName}.{methodName}") を付与
  • barrel export: infrastructure/repository/index.ts に追加

4. DI 登録

ファイル: apps/datahub/src/di/appLayer.ts

InfraLayerLayer.mergeAllEntityRepositoryLive を追加。 詳細は di-composition skill を参照。

5. Integration Test

ファイル: apps/datahub/tests/infrastructure/repository/{entity}Repository.test.ts

import { randomUUID } from "node:crypto";
import { it } from "@effect/vitest";
import { Effect, Either, ManagedRuntime, Schema } from "effect";
import { afterAll, beforeEach, describe, expect } from "vitest";
import { AppLayer } from "~/di/index.js";
import { Entity, EntityId, EntityNotFoundError } from "~/domain/{module}/index.js";
import { EntityRepository } from "~/usecase/ports/index.js";
import { EntityFactory } from "../../factories/entityFactory.js";
import { getDb } from "../../helpers/db.js";
import { truncateAll } from "../../helpers/truncate.js";

const runtime = ManagedRuntime.make(AppLayer);

beforeEach(async () => { await truncateAll(); });
afterAll(async () => { await runtime.dispose(); });

// --- findById ---

type FindByIdCase = {
  scenario: string;
  arrange: () => Promise<{ entityId: EntityId }>;
  assert: (result: Either.Either<unknown, unknown>) => void;
};

const findByIdCases: FindByIdCase[] = [
  {
    scenario: "returns entity when it exists",
    arrange: async () => {
      const db = getDb();
      const entity = await EntityFactory.create(db);
      return { entityId: EntityId.make(entity.publicId) };
    },
    assert: (result) => {
      expect(Either.isRight(result)).toEqual(true);
    },
  },
  {
    scenario: "returns EntityNotFoundError when entity does not exist",
    arrange: async () => ({ entityId: EntityId.make(randomUUID()) }),
    assert: (result) => {
      expect(Either.isLeft(result)).toEqual(true);
      if (Either.isLeft(result)) {
        expect(Schema.is(EntityNotFoundError)(result.left)).toEqual(true);
      }
    },
  },
];

describe("EntityRepositoryLive", () => {
  it.each(findByIdCases)("findById $scenario", async ({ arrange, assert }) => {
    const { entityId } = await arrange();
    const result = await runtime.runPromise(
      Effect.either(
        EntityRepository.pipe(Effect.flatMap((repo) => repo.findById(entityId))),
      ),
    );
    assert(result);
  });
});

テスト踏襲ルール

実装前に、plan の Ref test に指定された既存テストファイルを読むこと:

  • 基本: tests/infrastructure/repository/userRepository.test.ts を参考にする
  • 構造を合わせる: type 定義(FindByIdCase 等)、table-driven(.each())、AAA パターン
  • arrange で Factory + real DB、assert で Either チェック
  • runtime.runPromise(Effect.either(...)) パターンを踏襲
  • expect(result).toEqual(expected) を使う(toBe ではない)
  • エラー型チェックは Schema.is(ErrorClass)(result.left) を使う

チェックリスト

  • Port は Context.Tag で定義
  • save の戻り値は Effect<void, RepositoryError> — Entity を返さない
  • Infrastructure は Layer.effect 内で定義(standalone 関数禁止)
  • Effect.tryPromise で DB 操作をラップ
  • RepositoryError で catch(GatewayError ではない)
  • withRetryLogging + dbRetryPolicy を全メソッドに適用
  • Effect.withLogSpan を全メソッドに付与
  • Mapper の関数名は toDomain / toRow
  • 全ての cross-directory import が barrel(index.js)経由
  • DI 層(appLayer.ts)に登録済み
  • Integration test が real DB で動作
  • barrel export が存在(ports/index.ts, repository/index.ts)

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.