本文へ移動
cccskills
無料GitHub で公開日本語紹介

migrate-orpc-to-effect

既存のoRPC処理を一件ずつEffectへ移行し、ルートや応答形式、認可条件を保ちながら、DB操作・エラー処理・ログ・ルーター接続を置き換えるスキル。

原文原文の説明を見る

Migrate a single oRPC procedure from classic async handler to Effect-backed implementation. Use when converting procedures one at a time to maintain incremental, reviewable changes.

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • oRPC処理を一件ずつ移行したいとき
  • DBクエリやトランザクションの移行
  • 移行時のエラー処理とログの置き換え
  • 必要なEffectサービスの有無の確認

日本語での紹介

できること

既存のoRPCのAPI処理を一件ずつ、非同期処理やエラーを組み立てるライブラリEffectを使う実装へ移行します。ルート、入出力の仕様、認可条件を保ちながら、サービスの取得、DBクエリ、トランザクション、並列処理、ログを置き換えます。新しい.effect.tsファイルの作成からルーターの差し替え、検証後の元ファイル削除まで扱います。

こんなときに便利

API全体を段階的に移行し、一件の処理ごとにコミットやPRを分けてレビューしたい開発者に向いています。元の課金ミドルウェアやエラー応答も確認するため、既存の振る舞いを保つ移行の手順として使えます。

使い方の例

  • 「このoRPC処理が移行対象になるか確認して、Effect版に置き換えて」
  • 「認可条件を保って、DB処理とエラーログをEffectへ移して」

注意点

対象の依存サービスがAppRuntimeServicesで提供されている必要があります。未対応のサービスを使う処理は対象外です。Stripeでは公開されたStripeServiceを使い、effect-program-designに従う指定があります。検証にはBunを使い、Docker環境が必要なテストはCIで実行する前提です。

この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(2)

  • SKILL.md16.6 KB
  • references/find-candidates.md2.4 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Migrate oRPC Procedure to Effect

Converts a single classic oRPC procedure to an Effect-backed implementation while preserving route shape, response contract, and authorization semantics.

When to Use

  • Converting procedures that only use services we have Effect wrappers for
  • Incremental Effect migration where you want one procedure per commit/PR
  • Procedure does not already use Effect (no from 'effect' import)

Available Effect Services

IMPORTANT: Always check the source of truth for available services:

# Check AppRuntimeServices type for available Effect services
cat apps/riptide-api/src/effects/app-runtime.ts | head -50

Only migrate procedures that use services available in appRuntime. The AppRuntimeServices type in apps/riptide-api/src/effects/app-runtime.ts is the authoritative list.

Key services to look for:

  • PostgresDb - for database access (replaces context.services.db)
  • S3 - for S3 operations (replaces context.services.s3)
  • StripeService - for Stripe billing operations (replaces eligible context.services.stripe usage)
  • Check effects/services/ directory for other available services

If a procedure uses a classic service not in AppRuntimeServices, do NOT migrate it.

Prerequisites

Before starting, verify the target procedure is eligible:

# 1. Check what services the procedure uses
rg 'services\.' <file>

# 2. Check what Effect services are available in the runtime
cat apps/riptide-api/src/effects/app-runtime.ts | grep -A20 'AppRuntimeServices'

# 3. Confirm procedure hasn't already been migrated
rg "from 'effect'" <file>
# If matches, procedure is already migrated

If the procedure uses any service from context.services that doesn't have a corresponding Effect service in AppRuntimeServices, do NOT migrate it.

Stripe procedures are eligible only when they can call the public Effect StripeService from apps/riptide-api/src/effects/services/stripe/stripe.service.ts. Do not call the Stripe TypeScript SDK directly from a migrated oRPC procedure. Load and follow the effect-program-design skill for Stripe migrations. STRIPE_SECRET_KEY may only be consumed through Config.redacted in the Stripe HTTP adapter and must never be printed, inspected, or threaded through procedure code. kv remains unavailable; there is no generic KvService for migrated procedures.

Migration Workflow

1. Identify the procedure to migrate

Read the source file and understand:

  • Which base it uses (apiBaseOrgRequired, daemonBase, apiBaseForOrganizationAdmin, etc.)
  • The route path (e.g., comments.delete, settings.get)
  • Input/output contract
  • Authorization requirements
  • Database operations (queries, transactions)
  • Error handling patterns

2. Determine the Effect base

Map the classic base to its Effect equivalent:

Classic BaseEffect Base
apiBaseOrgRequiredeffectOrpcBaseOrganizationRequired
apiBasePubliceffectOrpcBasePublic
apiBaseForOrganizationAdmineffectOrpcBaseForOrganizationAdmin
daemonBaseeffectOrpcDaemonBase

All Effect bases are exported from apps/riptide-api/src/orpc/effect-base.ts.

3. Create the Effect procedure file

Create a new file with .effect.ts suffix alongside the original:

apps/riptide-api/src/orpc/api/comments/delete.ts      # original
apps/riptide-api/src/orpc/api/comments/delete.effect.ts  # new Effect version

4. Write the Effect procedure

Follow the golden pattern structure:

import { /* drizzle tables */ } from '@humanlayer/db/tables/...'
import { /* drizzle operators */ } from 'drizzle-orm'
import { Effect } from 'effect'
import { mapPostgresErrorToResponse } from '../../../effects/services/postgres/error-mapping'
import { getTransactionId } from '../../../effects/services/postgres/helpers'
import { PostgresDb } from '../../../effects/services/postgres/postgres-db.service'
import { Sentry } from '../../../instrument'
import { effectOrpcBaseOrganizationRequired } from '../../effect-base'

export const deleteCommentEffectProcedure = effectOrpcBaseOrganizationRequired.comments.delete
  .effect(function* ({ input, context, errors }) {
    // 1. Yield services from runtime
    const postgres = yield* PostgresDb

    // 2. Extract auth context
    const { userId, organizationId } = context.auth

    // 3. Implement logic using Effect patterns
    const result = yield* postgres.transaction((tx) =>
      Effect.gen(function* () {
        // Query using tx.query() wrapper
        const [comment] = yield* tx.query(
          tx.client
            .select()
            .from(artifactComments)
            .where(eq(artifactComments.id, input.commentId))
            .limit(1)
        )

        // Fail with oRPC errors using Effect.fail
        if (!comment) {
          return yield* Effect.fail(
            errors.NOT_FOUND({ data: { message: 'Comment not found' } })
          )
        }

        // Continue with business logic...
        
        const transactionId = yield* getTransactionId(tx)
        return { success: true, transactionId }
      })
    ).pipe(
      // Log errors for observability
      Effect.tapError((e) => Effect.logError(e)),
      // Capture to Sentry
      Effect.tapError((e) => Effect.sync(() => Sentry.captureException(e))),
      // Map tagged Postgres errors to oRPC responses
      Effect.catchTags({
        PostgresDbQueryError: (error) => mapPostgresErrorToResponse(error, errors),
        PostgresDbTransactionError: (error) => mapPostgresErrorToResponse(error, errors),
      })
    )

    // 4. Log success with structured context
    yield* Effect.logInfo('Comment deleted').pipe(
      Effect.annotateLogs({ commentId: input.commentId, userId })
    )

    return result
  })

5. Key transformation patterns

Service access

Before (classic):

const { db } = context.services

After (Effect):

const postgres = yield* PostgresDb

Simple queries

Before:

const rows = await db.select().from(table).where(condition)

After:

const rows = yield* postgres.query(
  postgres.client.select().from(table).where(condition)
)

Transactions

Before:

return await db.transaction(async (tx) => {
  const [row] = await tx.select()...
  await tx.update()...
  return result
})

After:

return yield* postgres.transaction((tx) =>
  Effect.gen(function* () {
    const [row] = yield* tx.query(tx.client.select()...)
    yield* tx.query(tx.client.update()...)
    return result
  })
)

Error handling

Before:

if (!found) {
  throw errors.NOT_FOUND({ data: { message: '...' } })
}

After:

if (!found) {
  return yield* Effect.fail(
    errors.NOT_FOUND({ data: { message: '...' } })
  )
}

Parallel queries

Before:

const [a, b, c] = await Promise.all([
  db.select().from(tableA)...,
  db.select().from(tableB)...,
  db.select().from(tableC)...,
])

After:

const [a, b, c] = yield* Effect.all([
  postgres.query(postgres.client.select().from(tableA)...),
  postgres.query(postgres.client.select().from(tableB)...),
  postgres.query(postgres.client.select().from(tableC)...),
], { concurrency: 'unbounded' })

Catching and re-throwing

Before:

try {
  // logic
} catch (error) {
  if (error instanceof ORPCError) throw error
  logger.error(error, 'Failed')
  throw errors.INTERNAL_SERVER_ERROR()
}

After (use .pipe() with error handling chain):

return yield* someEffect.pipe(
  // Log errors before mapping
  Effect.tapError((e) => Effect.logError(e)),
  // Capture to Sentry
  Effect.tapError((e) => Effect.sync(() => Sentry.captureException(e))),
  // Map to oRPC errors
  Effect.mapError((error) =>
    error.retryable
      ? errors.SERVICE_UNAVAILABLE()
      : errors.INTERNAL_SERVER_ERROR()
  )
)

Effect logging

Replace classic logger calls with Effect logging:

Before:

logger.info({ taskId, userId }, 'Task created')
logger.error(error, 'Failed to create task')

After:

// Info logging with annotations
yield* Effect.logInfo('Task created').pipe(
  Effect.annotateLogs({ taskId, userId })
)

// Error logging (usually via tapError in a pipe)
Effect.tapError((e) => Effect.logError(e))

// Warning logging
yield* Effect.logWarning('Deprecated endpoint called').pipe(
  Effect.annotateLogs({ endpoint: 'old-api' })
)

Effect logs are automatically sent to Sentry via SentryEffectLogger configured in appRuntime.

6. Add billing middleware if needed

If the original procedure uses billing middleware:

Before:

export const proc = apiBaseOrgRequired.route.path
  .use(requiresBillingMiddleware)
  .handler(async ({ ... }) => { ... })

After:

export const procEffect = effectOrpcBaseOrganizationRequired.route.path
  .use(requiresBillingEffectMiddleware)
  .effect(function* ({ ... }) { ... })

7. Update the router

In apps/riptide-api/src/orpc/router.ts, swap the procedure import:

Before:

import { deleteCommentProcedure } from './api/comments/delete'
// ...
comments: {
  delete: deleteCommentProcedure,
}

After:

import { deleteCommentEffectProcedure } from './api/comments/delete.effect'
// ...
comments: {
  delete: deleteCommentEffectProcedure,
}

8. Delete the original file

Once the Effect procedure is wired in and tests pass, delete the original .ts file.

9. Validate the change

Run validation commands (typecheck and lint only - tests require Docker containers and run in CI):

# Package typecheck
bun --bun run typecheck --filter @humanlayer/riptide-api

# Linting and formatting
bun --bun run quality

Do NOT run tests locally - the riptide-api tests require Docker containers (Postgres, Electric SQL) that are set up by CI. The CI workflow will validate the migration.

Effect Service Reference

This list may be out of date. Always check the source of truth:

# Authoritative list of available services
cat apps/riptide-api/src/effects/app-runtime.ts | grep -A20 'AppRuntimeServices'

# Browse available service implementations
ls apps/riptide-api/src/effects/services/

Common services (verify availability before using):

ServiceImport PathUsage
PostgresDbeffects/services/postgres/postgres-db.serviceDatabase queries/transactions
S3effects/services/s3/s3.servicePresigned URLs
StripeServiceeffects/services/stripe/stripe.serviceStripe billing operations through the public Effect surface

Check the effects/services/ directory for other available services and their implementations.

Helper Functions

Common helpers in effects/services/postgres/helpers.ts:

  • getTransactionId(tx) - Get Postgres transaction ID
  • requireTaskReadPermission(tx, { taskId, userId, organizationId }) - Check task read access
  • requireTaskEditPermission(tx, { taskId, userId, organizationId }) - Check task edit access

Error mapping in effects/services/postgres/error-mapping.ts:

  • mapPostgresErrorToResponse(error, errors) - Convert Postgres errors to oRPC errors

Review Checklist

Before committing:

  • New file uses .effect.ts suffix
  • Uses correct Effect base matching the original authorization level
  • Services obtained via yield* from runtime, not context.services
  • All queries wrapped with postgres.query() or postgres.transaction()
  • Errors returned via Effect.fail(), not throw
  • Postgres errors logged via Effect.tapError((e) => Effect.logError(e))
  • Postgres errors captured via Effect.tapError((e) => Effect.sync(() => Sentry.captureException(e)))
  • Postgres errors mapped to oRPC responses via Effect.catchTags or mapPostgresErrorToResponse
  • Success logging added via Effect.logInfo() with Effect.annotateLogs() for context
  • Billing middleware added if original had it
  • Router updated to use Effect procedure
  • Original file deleted
  • Typecheck passes for riptide-api package
  • Tests pass for riptide-api package

Anti-Patterns to Avoid

  • Accessing context.services.db directly (use PostgresDb from runtime)
  • Using throw for errors inside Effect generators (use Effect.fail)
  • Forgetting to wrap raw Drizzle queries with postgres.query()
  • Mixing async/await with Effect generators
  • Creating new runtime/layers per procedure (use shared appRuntime)
  • Migrating procedures that use services without Effect wrappers (jira, linear, kv, billingAccess)
  • Calling the Stripe TypeScript SDK directly from migrated procedures; use StripeService instead
  • Forgetting error logging with Effect.tapError before mapping errors
  • Using classic logger.info/error instead of Effect.logInfo/logError
  • Omitting Sentry.captureException in error handling chains

Example Migration

See these files for reference implementations:

  • Simple query: apps/riptide-api/src/orpc/api/hosts/alias-list.effect.ts
  • With transaction + logging: apps/riptide-api/src/orpc/api/comments/update.effect.ts
  • With billing + full error chain: apps/riptide-api/src/orpc/api/uploads/create.effect.ts
  • With S3 service: apps/riptide-api/src/orpc/api/uploads/download-url.effect.ts

Quick Reference: Error Handling Chain

The standard error handling pattern for transactions:

yield* postgres.transaction((tx) =>
  Effect.gen(function* () {
    // ... transaction logic
  })
).pipe(
  Effect.tapError((e) => Effect.logError(e)),
  Effect.tapError((e) => Effect.sync(() => Sentry.captureException(e))),
  Effect.catchTags({
    TaskNotFoundError: () => Effect.fail(errors.NOT_FOUND()),
    TaskPermissionDeniedError: () => Effect.fail(errors.FORBIDDEN()),
    PostgresDbQueryError: (error) => mapPostgresErrorToResponse(error, errors),
    PostgresDbTransactionError: (error) => mapPostgresErrorToResponse(error, errors),
  })
)

Effect Testing Patterns

Tests for Effect procedures use @effect/vitest and require Docker containers (Postgres, Electric SQL). Tests run in CI only - do not attempt to run locally without the test infrastructure.

Unit tests with it.effect

For testing Effect services in isolation:

import { describe, it } from '@effect/vitest'
import { Effect, Layer } from 'effect'
import { expect } from 'vitest'

describe('MyService', () => {
  it.effect('does something', () =>
    Effect.gen(function* () {
      const service = yield* MyService
      const result = yield* service.doSomething()
      expect(result).toBe('expected')
    }).pipe(
      Effect.provide(makeTestLayer())
    )
  )

  it.effect('handles errors', ({ expect }) =>
    Effect.gen(function* () {
      const service = yield* MyService
      const result = yield* Effect.result(service.failingOperation())
      expect(Result.isFailure(result)).toBe(true)
    }).pipe(
      Effect.provide(makeTestLayer())
    )
  )
})

Integration tests with test database

For testing Effect procedures against a real database:

import { call } from '@orpc/server'
import { beforeAll, afterAll, describe, test, expect } from 'vitest'
import { myEffectProcedure as liveProcedure } from '../../src/orpc/api/my/procedure.effect'
import { bindProcedureToRuntime, createRuntimeWithTestPostgresLayer } from '../utils/effect-procedure-test-runtime'
import { createTestDb } from '../utils/test-db'

describe('my effect procedure', async () => {
  let db: ReturnType<typeof drizzle>
  let effectRuntime: ReturnType<typeof createRuntimeWithTestPostgresLayer>
  let myProcedure: typeof liveProcedure

  beforeAll(async () => {
    const testDb = await createTestDb('my_procedure_test')
    db = testDb.db
    effectRuntime = createRuntimeWithTestPostgresLayer(db)
    myProcedure = bindProcedureToRuntime(liveProcedure, effectRuntime)
  })

  afterAll(async () => {
    await effectRuntime?.dispose()
  })

  test('does the thing', async () => {
    const result = await call(myProcedure, { input }, { context: makeContext() })
    expect(result).toEqual({ expected: 'value' })
  })
})

See apps/riptide-api/test/procedures/list-effect-procedures.test.ts for a complete example.

Important Notes

  • Do NOT modify this skill file - it is maintained separately
  • Do NOT run tests locally - they require Docker infrastructure set up by CI
  • Do NOT migrate procedures using unavailable services - check the availability table first
  • Always validate with typecheck and quality before committing

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

agent-browser

無料概要あり

Webページの移動やフォーム入力、クリックをエージェントに任せ、情報抽出や画面保存、操作前後の比較まで行います。ブラウザ作業の自動化やWebアプリの確認に使えるスキル。

  • フォーム入力やクリックを自動化したいとき
  • Webページのテキスト抽出
  • ページのスクリーンショット・PDF保存
humanlayer/humanlayer1.2万2026年10月23日 更新

asd-ste100

無料概要あり

曖昧で読みにくい英文を、語の意味をそろえた短い能動態の文へ書き直すスキル。指示文やツール説明を見直し、修正理由と変更前後を表で確認できます。

  • 別のエージェントへの英文指示の明確化
  • 英語のツール説明やエラー文の簡素化
  • 英語を母語としない読者向けの文章整理
humanlayer/humanlayer1.2万2026年10月23日 更新

better-github-skill

無料概要あり

GitHubのPRの状態、レビューの会話、CI失敗のログを専用スクリプトで収集し、変更の確認状況や未解決の指摘、失敗したジョブとステップを調べるスキル。

  • PRの状態をまとめて確認したいとき
  • 未解決のレビューの会話の確認
  • CIの失敗ジョブとステップの調査
humanlayer/humanlayer1.2万2026年10月23日 更新

bump-version

無料概要あり

リリース時に対象パッケージの基準バージョンを更新し、関連ファイルの同期と生成結果を検証して、コミットからPRブランチへのプッシュまで進めるスキル。

  • リリース用のバージョンを更新したいとき
  • 関連ファイルのバージョン表記の同期
  • プッシュ前の生成バージョン検証
humanlayer/humanlayer1.2万2026年10月23日 更新

calldiff

無料概要あり

Git のコミット間や作業中のコードで、関数の呼び出し構造の違いを確認します。呼び出しツリーの表示や、指定した関数へ至る経路の調査にも使えます。

  • 変更前後の関数呼び出しを比較したいとき
  • ブランチ間の呼び出し構造レビュー
  • 起点から目的の関数への経路調査
humanlayer/humanlayer1.2万2026年10月23日 更新

chrome-devtools-cli

無料概要あり

ターミナルからブラウザのクリックや入力、ページ移動を自動化し、通信やエラー、表示速度も調べられる、Chrome DevToolsのCLI操作を支援するスキル。

  • フォーム入力やページ移動の自動化
  • ページや要素の画面撮影
  • 通信内容やコンソールエラーの調査
humanlayer/humanlayer1.2万2026年10月23日 更新

humanlayer のスキルをすべて見る

このスキルの問題を報告する