🤠

zodライブラリを使ったuseCludeフックをnpmで公開してみました

2022/12/04に公開1

zodライブラリを使ったuseCludeフックをnpmで公開してみましたので、紹介です。

レポジトリはこちらになります。
https://github.com/Higashi-Kota/use-clude

npmライブラリはこちらになります。
https://www.npmjs.com/package/@nap5/use-clude

Githubのレポジトリのexample系のディレクトリにサンプル置いています。

typescriptなnextjsのサンプルも置いています。

シンプルな使い方としては以下になります。

まずはプロジェクトディレクトリを作成します。

$ cd ~/wrksp
$ mkdir -p a
$ cd a
$ yarn init -y
$ yarn add @nap5/use-clude zod
$ mkdir -p data
$ touch data/bebop.json
$ touch index.js

ファイルdata/bebop.jsonにテストデータを用意します。

[
  {
    "id": "p9z7aek9fi",
    "name": "Landon Glover",
    "age": 38,
    "blogs": []
  },
  {
    "id": "pga7t8prpl",
    "name": "Jack Jackson",
    "age": 38,
    "blogs": [
      {
        "id": "1",
        "title": "AAA"
      },
      {
        "id": "2",
        "title": "BBB"
      },
      {
        "id": "3",
        "title": "CCC"
      }
    ]
  },
  {
    "id": "tcz4kesu6p",
    "name": "Grace Dennis",
    "age": 44,
    "blogs": [
      {
        "id": "4",
        "title": "DDD"
      }
    ]
  }
]

package.jsonでモジュールハンドリングします。

{
  "type": "module",
  "dependencies": {
    "@nap5/use-clude": "^1.0.0",
    "zod": "^3.19.1"
  }
}

index.jsに実行内容を記載します。

import { z } from "zod";
import { useClude } from "@nap5/use-clude";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const data = require("./data/bebop.json");
const userShape = {
  id: z.string(),
  name: z.string(),
  age: z.number(),
  blogs: z
    .object({
      id: z.string(),
      title: z.string(),
    })
    .array(),
};
const { exclude, include } = useClude(userShape);
console.log(
  JSON.stringify(
    data.map((item) => include("blogs").parse(item)),
    null,
    2
  )
);
console.log(
  JSON.stringify(
    data.map((item) => exclude("name", "age").parse(item)),
    null,
    2
  )
);

以下が実行結果になります。

includeに指定したプロパティが包含され、excludeに指定したプロパティが除外されて結果が出力されています。

$ time node index.js
[
  {
    "blogs": []
  },
  {
    "blogs": [
      {
        "id": "1",
        "title": "AAA"
      },
      {
        "id": "2",
        "title": "BBB"
      },
      {
        "id": "3",
        "title": "CCC"
      }
    ]
  },
  {
    "blogs": [
      {
        "id": "4",
        "title": "DDD"
      }
    ]
  }
]
[
  {
    "id": "p9z7aek9fi",
    "blogs": []
  },
  {
    "id": "pga7t8prpl",
    "blogs": [
      {
        "id": "1",
        "title": "AAA"
      },
      {
        "id": "2",
        "title": "BBB"
      },
      {
        "id": "3",
        "title": "CCC"
      }
    ]
  },
  {
    "id": "tcz4kesu6p",
    "blogs": [
      {
        "id": "4",
        "title": "DDD"
      }
    ]
  }
]

real    0m0.040s
user    0m0.042s
sys     0m0.014s

このライブラリをパブリッシュする際に以下のエラーを確認できました。
が、エラー解決はできなかったものの、バンドルされたファイルは出力してくれたので、まぁよいかという感じで流れに身を任せてパブリッシュしました。

tscofing.jsonとかでリラックスできるオプションがあるか探しましたが、なかなか難しかったです。

 $ cd ~/wrksp/use-clude
 $ rm -rf dist && tsc  -p .
src/index.ts:5:17 - error TS2589: Type instantiation is excessively deep and possibly infinite.

5   type Record = z.infer<typeof record>;
                  ~~~~~~~~~~~~~~~~~~~~~~


Found 1 error.

簡単ですが、以上です。

Discussion

nap5nap5

これでいいかもです。

demo code.

https://codesandbox.io/p/sandbox/inspiring-moser-vi8vh1?file=%2FREADME.md

定義側

import { Path, getByPath } from 'dot-path-value'
import { unflatten } from 'flat'

export const echoPath = <T extends Record<string, unknown>>(data: T) => {
  return <P extends Path<T>>(path: P) => {
    return { path }
  }
}

export const peekByPath = <T extends Record<string, unknown>>(data: T) => {
  return <P extends Path<T>>(path: P) => {
    return { data, path, match: getByPath(data, path) }
  }
}

export const peekByPaths = <T extends Record<string, unknown>>(
  data: T,
  isConvolve: boolean = true
) => {
  return <P extends Path<T>>(paths: P[]) => {
    const results = paths.reduce(
      (result, path) => {
        Object.assign(result.match, { [path]: getByPath(data, path) })
        return result
      },
      { data, match: {} }
    )
    const unneatData: T = unflatten(results.match)
    const neatData = (Object.keys(unneatData) as P[]).reduce(
      (acc, props: P) => {
        if (!isConvolve) {
          acc[props] = unneatData[props]
        } else {
          if (unneatData[props] instanceof Array) {
            // convolve
            const obj: Record<string, unknown> = (
              unneatData[props] as []
            ).reduce((acc, cur) => Object.assign(acc, cur), {} as any)
            // @ts-ignore
            acc[props] = Object.keys(obj).length === 0 ? [] : [obj]
          } else {
            acc[props] = unneatData[props]
          }
        }
        return acc
      },
      {} as T
    )
    return neatData
  }
}

使用側

import { test, expect } from "vitest";

import { peekByPaths } from ".";

type Time = {
  createdAt: string;
  updatedAt: string;
};

type Blog = {
  id: string;
  title: string;
  isPublished: boolean;
} & Time;

type Author = {
  id: string;
  name: string;
  blogs: Blog[];
};

const data: Author = {
  id: "xxx",
  name: "cowboy",
  blogs: [
    {
      id: "aaa",
      title: "Holly Night",
      isPublished: false,
      createdAt: "2012/10/10 12:32",
      updatedAt: "2022/10/10 12:32",
    },
    {
      id: "bbb",
      title: "Malibu Night",
      isPublished: true,
      createdAt: "2002/10/10 12:32",
      updatedAt: "2022/10/10 12:32",
    },
    {
      id: "ccc",
      title: "Cozy Night",
      isPublished: false,
      createdAt: "2002/10/10 12:32",
      updatedAt: "2012/10/10 12:32",
    },
  ],
};

test("[convolve]peekByPaths", () => {
  expect(
    peekByPaths(
      data,
      true
    )([
      `id`,
      `name`,
      `blogs.${1}.id`,
      `blogs.${1}.title`,
      `blogs.${0}.isPublished`,
      `blogs.${2}.createdAt`,
      `blogs.${0}.updatedAt`,
    ])
  ).toStrictEqual({
    blogs: [
      {
        id: "bbb",
        isPublished: false,
        title: "Malibu Night",
        updatedAt: "2022/10/10 12:32",
        createdAt: "2002/10/10 12:32",
      },
    ],
    id: "xxx",
    name: "cowboy",
  });
});

test("[unvolve]peekByPaths", () => {
  expect(
    peekByPaths(
      data,
      false
    )([
      `id`,
      `name`,
      `blogs.${0}.id`,
      `blogs.${0}.title`,
      `blogs.${0}.isPublished`,
      `blogs.${0}.updatedAt`,
      `blogs.${1}.id`,
      `blogs.${1}.title`,
      `blogs.${1}.isPublished`,
      `blogs.${1}.updatedAt`,
    ])
  ).toStrictEqual({
    blogs: [
      {
        id: "aaa",
        isPublished: false,
        title: "Holly Night",
        updatedAt: "2022/10/10 12:32",
      },
      {
        id: "bbb",
        isPublished: true,
        title: "Malibu Night",
        updatedAt: "2022/10/10 12:32",
      },
    ],
    id: "xxx",
    name: "cowboy",
  });
});

簡単ですが、以上です。