😱

Secret Managerからシークレットを取得しZodでValidationする関数を書く

2022/09/06に公開1

最近、Secret Managerから取得したJSONに、zodで定義したスキーマでValidationして型安全に取り扱うコードをよく書くので、関数の引数にzodで定義したスキーマを渡せばValidationしてその型を返す関数を定義して使っている。それを紹介だぜ、という小ネタです。

実装(簡易版)

エラー処理を適当に書いた簡易版です。

import {
  GetSecretValueCommand,
  SecretsManagerClient,
} from "@aws-sdk/client-secrets-manager";
import type { z, ZodType } from "zod";

export const fetchSecret = async <T extends ZodType<any, any, any>>({
  client = new SecretsManagerClient({}),
  secretId,
  schema,
}: {
  client?: SecretsManagerClient;
  secretId: string;
  schema: T;
}): Promise<z.infer<T>> => {
  const output = await client.send(
    new GetSecretValueCommand({
      SecretId: secretId,
    })
  );

  return schema.parse(JSON.parse(output.SecretString!));
};

使い方

使い方は簡単。

const SecretSchema = z.object({
  a: z.string(),
  b: z.number(),
  c: z.boolean(),
})

const secret = await fetchSecret({
  secretId: 'hoge',
  schema: SecretSchema,
})

// 型が効くぞ
secret.a;
secret.b;
secret.c;

Secrets Managerに限らず、zodで定義した型で返したい関数全てに応用できると思う。zod、最高。

おまけ: エラー処理を細かく書いた実装

最近はErrorをthrowせずに失敗時もresolveするのが好みなので、そういう実装です。

interface Success<T> {
  success: true;
  value: T;
}

interface Failure {
  success: false;
  value: {
    code: string;
    cause: unknown;
  };
}

type FetchSecretResult<T> = Success<T> | Failure;

export const fetchSecret = async <T extends ZodType<any, any, any>>({
  client = new SecretsManagerClient({}),
  secretId,
  schema,
}: {
  client?: SecretsManagerClient;
  secretId: string;
  schema: T;
}): Promise<FetchSecretResult<z.infer<T>>> => {
  let output: GetSecretValueCommandOutput;
  try {
    output = await client.send(
      new GetSecretValueCommand({
        SecretId: secretId,
      })
    );
  } catch (cause) {
    return {
      success: false,
      value: { code: "GetSecretValueCommandFailed", cause },
    };
  }

  if (!output.SecretString) {
    return {
      success: false,
      value: { code: "NoSecretString", cause: output },
    };
  }

  let json: unknown;
  try {
    json = JSON.parse(output.SecretString);
  } catch (cause) {
    return {
      success: false,
      value: { code: "JSONParseFailed", cause: output },
    };
  }

  try {
    return schema.parse(json);
  } catch (cause) {
    return {
      success: false,
      value: { code: "UnexpectedSecret", cause: output },
    };
  }
};

Discussion

nap5nap5

最近はErrorをthrowせずに

記事の内容を生かしつつ、デモをneverthrowライブラリで作ってみました。

デモコードです。

https://codesandbox.io/p/sandbox/vigilant-hawking-rvvcxu?file=%2Fsrc%2Findex.ts

import { Result } from "neverthrow";
import { match } from "ts-pattern";
import { z } from "zod";
import { CustomError, ErrorData } from "./types/error";
import { isNullOrUndefined } from "./utils";
(async () => {
  const SecretTokenSchema = z.object({
    a: z.string(),
    b: z.number(),
    c: z.boolean(),
  });
  const SecretTokenDataSchema = SecretTokenSchema.nullish();
  type SecretTokenData = z.infer<typeof SecretTokenDataSchema>;

  type ErrorMode =
    | "GetSecretValueCommandFailed"
    | "NoSecretString"
    | "JSONParseFailed"
    | "UnexpectedSecret"
    | "Cowboy";

  const mockError = <T>(errorMode?: ErrorMode) =>
    Result.fromThrowable(
      (d: T) => {
        if (isNullOrUndefined(errorMode)) {
          return d;
        }
        match(errorMode)
          .with("GetSecretValueCommandFailed", () => {
            throw new CustomError("E01", "GetSecretValueCommandFailed", {
              cause: new Error("Something went wrong..."),
            });
          })
          .with("NoSecretString", () => {
            throw new CustomError("E01", "NoSecretString", {
              cause: new Error("Something went wrong..."),
            });
          })
          .with("JSONParseFailed", () => {
            throw new CustomError("E02", "JSONParseFailed", {
              cause: new Error("Something went wrong..."),
            });
          })
          .with("UnexpectedSecret", () => {
            throw new CustomError("E99", "UnexpectedSecret", {
              cause: new Error("Something went wrong..."),
            });
          })
          .otherwise(() => {
            throw new CustomError("E99", "UnHandlingError", {
              cause: new Error("Something went wrong..."),
            });
          });
      },
      (e) => {
        return e as ErrorData;
      }
    );
  mockError<SecretTokenData>()({
    a: "Cowboy",
    b: 11,
    c: false,
  }).match(
    (data) => {
      console.log(data);
    },
    (error) => {
      console.log(error);
    }
  );
  mockError<SecretTokenData>("Cowboy")({
    a: "Cowboy",
    b: 11,
    c: false,
  }).match(
    (data) => {
      console.log(data);
    },
    (error) => {
      console.log(error);
    }
  );
})();

簡単ですが、以上です。