-
Notifications
You must be signed in to change notification settings - Fork 0
microCMS API スキーマバリデーションパッケージの実装 #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
57b1880
eb6cfa0
c84d217
60dd648
04271e8
6d2fcb6
86bfc84
543e409
25ed77a
5f3e498
8e3a094
55f120e
58eeb3c
ac49f23
82ff054
db7ae12
aefb637
d778f66
19a2b1e
63e82e6
703ce0a
bfbb03e
2d40a30
0da1815
3167022
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| /schema.json | ||
| dist |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # microcms-api-schema-schema | ||
|
|
||
| microCMS の API スキーマを定義するためのスキーマライブラリ | ||
|
|
||
| ## 概要 | ||
|
|
||
| このパッケージは、microCMS の API スキーマを TypeScript で型安全に定義し、Zod によるランタイムバリデーションを提供します。 | ||
|
|
||
| ## インストール | ||
|
|
||
| ```bash | ||
| pnpm add microcms-api-schema-schema | ||
| ``` | ||
|
|
||
| ## 開発 | ||
|
|
||
| ```bash | ||
| # ビルド | ||
| pnpm build | ||
|
|
||
| # 開発モード(watch) | ||
| pnpm dev | ||
|
|
||
| # テスト | ||
| pnpm test | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,49 @@ | ||
| { | ||
| "name": "@plainbrew/microcms-api-schema-schema", | ||
| "version": "0.0.0", | ||
| "version": "0.0.1-alpha.0", | ||
| "description": "", | ||
| "license": "MIT", | ||
| "files": [ | ||
| "dist", | ||
| "schema.json", | ||
| "src" | ||
| ], | ||
| "type": "module", | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.js" | ||
| }, | ||
| "./schema.json": "./schema.json" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "scripts": { | ||
| "dev": "tsc --watch", | ||
| "build": "pnpm build:tsc && pnpm build:schema", | ||
| "build:tsc": "tsc", | ||
| "build:schema": "node scripts/generate-json-schema.ts", | ||
| "type-check": "tsc --noEmit", | ||
| "test": "vitest run", | ||
| "test:watch": "vitest", | ||
| "prepare": "pnpm run build" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^24.10.4", | ||
| "typescript": "^5.7.3", | ||
| "vitest": "^4.0.16", | ||
| "zod": "^4.2.1" | ||
| }, | ||
| "peerDependencies": { | ||
| "zod": "^3.25.0 || ^4.0.0" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "zod": { | ||
| "optional": true | ||
| } | ||
| } | ||
|
Comment on lines
+40
to
47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify zod import paths in schema.ts and related files
echo "=== Searching for zod imports ==="
rg -n "from ['\"]zod" packages/microcms-api-schema-schema/src/
echo ""
echo "=== Searching for zod require statements ==="
rg -n "require\(['\"]zod" packages/microcms-api-schema-schema/src/Repository: plainbrew/microcms-schema Length of output: 229 peerDependenciesとインポートパスの不一致を修正してください。
以下のいずれかの対応が必要です:
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { writeFileSync, mkdirSync } from "node:fs"; | ||
| import { dirname, join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { z } from "zod"; | ||
| import { apiSchemaSchema } from "../src/schema.ts"; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const distDir = join(__dirname, "../"); | ||
|
|
||
| mkdirSync(distDir, { recursive: true }); | ||
|
|
||
| writeFileSync( | ||
| join(distDir, "schema.json"), | ||
| JSON.stringify(z.toJSONSchema(apiSchemaSchema), null, 2), | ||
| ); | ||
|
|
||
| console.log("Generated schema.json"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { apiSchemaSchema, ApiSchemaSchema } from "./schema.js"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { readFileSync } from "node:fs"; | ||
| import { dirname, join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { describe, expect, test } from "vitest"; | ||
| import { apiSchemaSchema } from "./schema.js"; | ||
|
|
||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = dirname(__filename); | ||
|
|
||
| const SCHEMA_EXAMPLES_DIR = join(__dirname, "../../../schema-examples"); | ||
|
|
||
| const schemaFiles = [ | ||
| "official-categories.json", | ||
| "official-news.json", | ||
| "official-blogs.json", | ||
| "official-banner.json", | ||
| "min-max.json", | ||
| ]; | ||
|
|
||
| describe("schema-examples のファイルをバリデーション", () => { | ||
| test.each(schemaFiles)("%s", (filename) => { | ||
| const filePath = join(SCHEMA_EXAMPLES_DIR, filename); | ||
| const content = readFileSync(filePath, "utf-8"); | ||
| const schema = JSON.parse(content); | ||
|
|
||
| const result = apiSchemaSchema.safeParse(schema); | ||
| if (!result.success) { | ||
| console.error(result.error); | ||
| } | ||
| expect(result.success).toBe(true); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| import { describe, expect, test } from "vitest"; | ||
| import { apiSchemaSchema } from "./schema.js"; | ||
|
|
||
| describe("apiSchemaSchema", () => { | ||
| test("official-categories.json のスキーマをバリデーションできる", () => { | ||
| const schema = { | ||
| apiFields: [ | ||
| { | ||
| fieldId: "name", | ||
| name: "カテゴリ名", | ||
| kind: "text", | ||
| required: false, | ||
| isUnique: false, | ||
| }, | ||
| ], | ||
| customFields: [], | ||
| }; | ||
|
|
||
| const result = apiSchemaSchema.safeParse(schema); | ||
| expect(result.success).toBe(true); | ||
| }); | ||
|
|
||
| test("official-news.json のスキーマをバリデーションできる", () => { | ||
| const schema = { | ||
| apiFields: [ | ||
| { | ||
| fieldId: "title", | ||
| name: "タイトル", | ||
| kind: "text", | ||
| required: false, | ||
| isUnique: false, | ||
| }, | ||
| { | ||
| fieldId: "content", | ||
| name: "内容", | ||
| kind: "richEditorV2", | ||
| required: false, | ||
| }, | ||
| { | ||
| fieldId: "category", | ||
| name: "カテゴリ", | ||
| kind: "relation", | ||
| required: false, | ||
| }, | ||
| ], | ||
| customFields: [], | ||
| }; | ||
|
|
||
| const result = apiSchemaSchema.safeParse(schema); | ||
| expect(result.success).toBe(true); | ||
| }); | ||
|
|
||
| test("official-blogs.json のスキーマをバリデーションできる", () => { | ||
| const schema = { | ||
| apiFields: [ | ||
| { | ||
| fieldId: "title", | ||
| name: "タイトル", | ||
| kind: "text", | ||
| required: false, | ||
| isUnique: false, | ||
| }, | ||
| { | ||
| fieldId: "content", | ||
| name: "内容", | ||
| kind: "richEditorV2", | ||
| required: false, | ||
| }, | ||
| { | ||
| fieldId: "eyecatch", | ||
| name: "アイキャッチ", | ||
| kind: "media", | ||
| required: false, | ||
| }, | ||
| { | ||
| fieldId: "category", | ||
| name: "カテゴリ", | ||
| kind: "relation", | ||
| required: false, | ||
| }, | ||
| ], | ||
| customFields: [], | ||
| }; | ||
|
|
||
| const result = apiSchemaSchema.safeParse(schema); | ||
| expect(result.success).toBe(true); | ||
| }); | ||
|
|
||
| test("複雑なスキーマをバリデーションできる", () => { | ||
| const schema = { | ||
| apiFields: [ | ||
| { | ||
| fieldId: "text_max", | ||
| name: "テキストフィールド 最大", | ||
| kind: "text", | ||
| description: "説明文", | ||
| required: true, | ||
| selectItems: [], | ||
| selectInitialValue: [], | ||
| multipleSelect: false, | ||
| textSizeLimitValidation: { textSize: { min: 1, max: 30 } }, | ||
| patternMatchValidation: { regexp: { pattern: "\\d", flags: "gi" } }, | ||
| isUnique: true, | ||
| }, | ||
| { | ||
| fieldId: "rich_max", | ||
| name: "リッチエディタ 最大", | ||
| kind: "richEditorV2", | ||
| description: "説明文", | ||
| required: true, | ||
| richEditorV2Options: ["undo", "redo", "bold", "italic", "underline", "code"], | ||
| richEditorV2ColorList: [ | ||
| { id: "KDKsIv0jXZ", value: "rgb(255, 255, 255)" }, | ||
| { id: "vHyfNOIY51", value: "rgb(156, 102, 102)" }, | ||
| ], | ||
| richEditorV2HideColorPicker: true, | ||
| richEditorV2FontSizeList: [{ id: "Sp9MppHN43", name: "表示名", value: "120" }], | ||
| customClassList: [{ id: "nL3_cmKOxf", name: "表示名", value: "class名" }], | ||
| }, | ||
| { | ||
| fieldId: "image_max", | ||
| name: "画像 最大", | ||
| kind: "media", | ||
| description: "説明文", | ||
| required: true, | ||
| imageSizeValidation: { imageSize: { width: 100, height: 100 } }, | ||
| }, | ||
| { | ||
| fieldId: "select_max", | ||
| name: "セレクトフィールド 最大", | ||
| kind: "select", | ||
| required: true, | ||
| selectItems: [ | ||
| { id: "viO1_AW73E", value: "item1" }, | ||
| { id: "6bnahbE--K", value: "item2" }, | ||
| ], | ||
| selectInitialValue: ["viO1_AW73E"], | ||
| multipleSelect: true, | ||
| }, | ||
| { | ||
| fieldId: "relation_multi_max", | ||
| name: "複数参照 最大", | ||
| kind: "relationList", | ||
| description: "説明文", | ||
| required: true, | ||
| referenceDisplayItem: "eO9ST_qUoe", | ||
| relationListCountLimitValidation: { | ||
| relationListCount: { min: 1, max: 3 }, | ||
| }, | ||
| }, | ||
| { | ||
| fieldId: "number_max", | ||
| name: "数字 最大", | ||
| kind: "number", | ||
| description: "説明文", | ||
| required: true, | ||
| numberSizeLimitValidation: { numberSize: { min: 1, max: 300 } }, | ||
| }, | ||
| ], | ||
| customFields: [], | ||
| }; | ||
|
|
||
| const result = apiSchemaSchema.safeParse(schema); | ||
| expect(result.success).toBe(true); | ||
| }); | ||
|
|
||
| test("不正なスキーマはバリデーションエラーになる", () => { | ||
| const invalidSchema = { | ||
| apiFields: [ | ||
| { | ||
| fieldId: "test", | ||
| // name が欠けている | ||
| kind: "text", | ||
| required: false, | ||
| }, | ||
| ], | ||
| customFields: [], | ||
| }; | ||
|
|
||
| const result = apiSchemaSchema.safeParse(invalidSchema); | ||
| expect(result.success).toBe(false); | ||
| }); | ||
|
|
||
| test("不正な kind はバリデーションエラーになる", () => { | ||
| const invalidSchema = { | ||
| apiFields: [ | ||
| { | ||
| fieldId: "test", | ||
| name: "テスト", | ||
| kind: "invalid_kind", | ||
| required: false, | ||
| }, | ||
| ], | ||
| customFields: [], | ||
| }; | ||
|
|
||
| const result = apiSchemaSchema.safeParse(invalidSchema); | ||
| expect(result.success).toBe(false); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.