@plainbrew/microcms-api-schema-schema 0.0.0 → 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.ts +235 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +218 -0
- package/dist/schema.js.map +1 -0
- package/package.json +40 -1
- package/schema.json +800 -0
- package/src/index.ts +1 -0
- package/src/schema-examples.test.ts +32 -0
- package/src/schema.test.ts +200 -0
- package/src/schema.ts +247 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { apiSchemaSchema, ApiSchemaSchema } from "./schema.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
import { apiSchemaSchema } from "./schema.js";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const SCHEMA_EXAMPLES_DIR = join(__dirname, "../../../schema-examples");
|
|
11
|
+
|
|
12
|
+
const schemaFiles = [
|
|
13
|
+
"official-categories.json",
|
|
14
|
+
"official-news.json",
|
|
15
|
+
"official-blogs.json",
|
|
16
|
+
"official-banner.json",
|
|
17
|
+
"min-max.json",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe("schema-examples のファイルをバリデーション", () => {
|
|
21
|
+
test.each(schemaFiles)("%s", (filename) => {
|
|
22
|
+
const filePath = join(SCHEMA_EXAMPLES_DIR, filename);
|
|
23
|
+
const content = readFileSync(filePath, "utf-8");
|
|
24
|
+
const schema = JSON.parse(content);
|
|
25
|
+
|
|
26
|
+
const result = apiSchemaSchema.safeParse(schema);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
console.error(result.error);
|
|
29
|
+
}
|
|
30
|
+
expect(result.success).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { apiSchemaSchema } from "./schema.js";
|
|
3
|
+
|
|
4
|
+
describe("apiSchemaSchema", () => {
|
|
5
|
+
test("official-categories.json のスキーマをバリデーションできる", () => {
|
|
6
|
+
const schema = {
|
|
7
|
+
apiFields: [
|
|
8
|
+
{
|
|
9
|
+
fieldId: "name",
|
|
10
|
+
name: "カテゴリ名",
|
|
11
|
+
kind: "text",
|
|
12
|
+
required: false,
|
|
13
|
+
isUnique: false,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
customFields: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = apiSchemaSchema.safeParse(schema);
|
|
20
|
+
expect(result.success).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("official-news.json のスキーマをバリデーションできる", () => {
|
|
24
|
+
const schema = {
|
|
25
|
+
apiFields: [
|
|
26
|
+
{
|
|
27
|
+
fieldId: "title",
|
|
28
|
+
name: "タイトル",
|
|
29
|
+
kind: "text",
|
|
30
|
+
required: false,
|
|
31
|
+
isUnique: false,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
fieldId: "content",
|
|
35
|
+
name: "内容",
|
|
36
|
+
kind: "richEditorV2",
|
|
37
|
+
required: false,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
fieldId: "category",
|
|
41
|
+
name: "カテゴリ",
|
|
42
|
+
kind: "relation",
|
|
43
|
+
required: false,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
customFields: [],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = apiSchemaSchema.safeParse(schema);
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("official-blogs.json のスキーマをバリデーションできる", () => {
|
|
54
|
+
const schema = {
|
|
55
|
+
apiFields: [
|
|
56
|
+
{
|
|
57
|
+
fieldId: "title",
|
|
58
|
+
name: "タイトル",
|
|
59
|
+
kind: "text",
|
|
60
|
+
required: false,
|
|
61
|
+
isUnique: false,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
fieldId: "content",
|
|
65
|
+
name: "内容",
|
|
66
|
+
kind: "richEditorV2",
|
|
67
|
+
required: false,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
fieldId: "eyecatch",
|
|
71
|
+
name: "アイキャッチ",
|
|
72
|
+
kind: "media",
|
|
73
|
+
required: false,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
fieldId: "category",
|
|
77
|
+
name: "カテゴリ",
|
|
78
|
+
kind: "relation",
|
|
79
|
+
required: false,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
customFields: [],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = apiSchemaSchema.safeParse(schema);
|
|
86
|
+
expect(result.success).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("複雑なスキーマをバリデーションできる", () => {
|
|
90
|
+
const schema = {
|
|
91
|
+
apiFields: [
|
|
92
|
+
{
|
|
93
|
+
fieldId: "text_max",
|
|
94
|
+
name: "テキストフィールド 最大",
|
|
95
|
+
kind: "text",
|
|
96
|
+
description: "説明文",
|
|
97
|
+
required: true,
|
|
98
|
+
selectItems: [],
|
|
99
|
+
selectInitialValue: [],
|
|
100
|
+
multipleSelect: false,
|
|
101
|
+
textSizeLimitValidation: { textSize: { min: 1, max: 30 } },
|
|
102
|
+
patternMatchValidation: { regexp: { pattern: "\\d", flags: "gi" } },
|
|
103
|
+
isUnique: true,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
fieldId: "rich_max",
|
|
107
|
+
name: "リッチエディタ 最大",
|
|
108
|
+
kind: "richEditorV2",
|
|
109
|
+
description: "説明文",
|
|
110
|
+
required: true,
|
|
111
|
+
richEditorV2Options: ["undo", "redo", "bold", "italic", "underline", "code"],
|
|
112
|
+
richEditorV2ColorList: [
|
|
113
|
+
{ id: "KDKsIv0jXZ", value: "rgb(255, 255, 255)" },
|
|
114
|
+
{ id: "vHyfNOIY51", value: "rgb(156, 102, 102)" },
|
|
115
|
+
],
|
|
116
|
+
richEditorV2HideColorPicker: true,
|
|
117
|
+
richEditorV2FontSizeList: [{ id: "Sp9MppHN43", name: "表示名", value: "120" }],
|
|
118
|
+
customClassList: [{ id: "nL3_cmKOxf", name: "表示名", value: "class名" }],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
fieldId: "image_max",
|
|
122
|
+
name: "画像 最大",
|
|
123
|
+
kind: "media",
|
|
124
|
+
description: "説明文",
|
|
125
|
+
required: true,
|
|
126
|
+
imageSizeValidation: { imageSize: { width: 100, height: 100 } },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
fieldId: "select_max",
|
|
130
|
+
name: "セレクトフィールド 最大",
|
|
131
|
+
kind: "select",
|
|
132
|
+
required: true,
|
|
133
|
+
selectItems: [
|
|
134
|
+
{ id: "viO1_AW73E", value: "item1" },
|
|
135
|
+
{ id: "6bnahbE--K", value: "item2" },
|
|
136
|
+
],
|
|
137
|
+
selectInitialValue: ["viO1_AW73E"],
|
|
138
|
+
multipleSelect: true,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
fieldId: "relation_multi_max",
|
|
142
|
+
name: "複数参照 最大",
|
|
143
|
+
kind: "relationList",
|
|
144
|
+
description: "説明文",
|
|
145
|
+
required: true,
|
|
146
|
+
referenceDisplayItem: "eO9ST_qUoe",
|
|
147
|
+
relationListCountLimitValidation: {
|
|
148
|
+
relationListCount: { min: 1, max: 3 },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
fieldId: "number_max",
|
|
153
|
+
name: "数字 最大",
|
|
154
|
+
kind: "number",
|
|
155
|
+
description: "説明文",
|
|
156
|
+
required: true,
|
|
157
|
+
numberSizeLimitValidation: { numberSize: { min: 1, max: 300 } },
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
customFields: [],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const result = apiSchemaSchema.safeParse(schema);
|
|
164
|
+
expect(result.success).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("不正なスキーマはバリデーションエラーになる", () => {
|
|
168
|
+
const invalidSchema = {
|
|
169
|
+
apiFields: [
|
|
170
|
+
{
|
|
171
|
+
fieldId: "test",
|
|
172
|
+
// name が欠けている
|
|
173
|
+
kind: "text",
|
|
174
|
+
required: false,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
customFields: [],
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const result = apiSchemaSchema.safeParse(invalidSchema);
|
|
181
|
+
expect(result.success).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("不正な kind はバリデーションエラーになる", () => {
|
|
185
|
+
const invalidSchema = {
|
|
186
|
+
apiFields: [
|
|
187
|
+
{
|
|
188
|
+
fieldId: "test",
|
|
189
|
+
name: "テスト",
|
|
190
|
+
kind: "invalid_kind",
|
|
191
|
+
required: false,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
customFields: [],
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const result = apiSchemaSchema.safeParse(invalidSchema);
|
|
198
|
+
expect(result.success).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
});
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
|
|
3
|
+
const textSizeLimitValidationSchema = z.object({
|
|
4
|
+
textSize: z.object({
|
|
5
|
+
min: z.number().optional(),
|
|
6
|
+
max: z.number().optional(),
|
|
7
|
+
}),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const patternMatchValidationSchema = z.object({
|
|
11
|
+
regexp: z.object({
|
|
12
|
+
pattern: z.string(),
|
|
13
|
+
flags: z.string().optional(),
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const imageSizeValidationSchema = z.object({
|
|
18
|
+
imageSize: z.object({
|
|
19
|
+
width: z.number(),
|
|
20
|
+
height: z.number(),
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const numberSizeLimitValidationSchema = z.object({
|
|
25
|
+
numberSize: z.object({
|
|
26
|
+
min: z.number().optional(),
|
|
27
|
+
max: z.number().optional(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const relationListCountLimitValidationSchema = z.object({
|
|
32
|
+
relationListCount: z.object({
|
|
33
|
+
min: z.number().optional(),
|
|
34
|
+
max: z.number().optional(),
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const richEditorOptionSchema = z.enum([
|
|
39
|
+
"headerOne",
|
|
40
|
+
"headerTwo",
|
|
41
|
+
"headerThree",
|
|
42
|
+
"headerFour",
|
|
43
|
+
"headerFive",
|
|
44
|
+
"paragraph",
|
|
45
|
+
"sizeSmall",
|
|
46
|
+
"sizeNormal",
|
|
47
|
+
"sizeLarge",
|
|
48
|
+
"sizeHuge",
|
|
49
|
+
"bold",
|
|
50
|
+
"italic",
|
|
51
|
+
"underline",
|
|
52
|
+
"strike",
|
|
53
|
+
"code",
|
|
54
|
+
"background",
|
|
55
|
+
"color",
|
|
56
|
+
"align",
|
|
57
|
+
"blockquote",
|
|
58
|
+
"codeBlock",
|
|
59
|
+
"listOrdered",
|
|
60
|
+
"listBullet",
|
|
61
|
+
"indentRemove",
|
|
62
|
+
"indentAdd",
|
|
63
|
+
"scriptSub",
|
|
64
|
+
"scriptSuper",
|
|
65
|
+
"link",
|
|
66
|
+
"image",
|
|
67
|
+
"oembedly",
|
|
68
|
+
"clean",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const richEditorV2OptionSchema = z.enum([
|
|
72
|
+
"undo",
|
|
73
|
+
"redo",
|
|
74
|
+
"bold",
|
|
75
|
+
"italic",
|
|
76
|
+
"underline",
|
|
77
|
+
"strike",
|
|
78
|
+
"code",
|
|
79
|
+
"listBullet",
|
|
80
|
+
"listOrdered",
|
|
81
|
+
"link",
|
|
82
|
+
"file",
|
|
83
|
+
"image",
|
|
84
|
+
"table",
|
|
85
|
+
"horizontalRule",
|
|
86
|
+
"textAlign",
|
|
87
|
+
"oembedly",
|
|
88
|
+
"clean",
|
|
89
|
+
"color",
|
|
90
|
+
"size",
|
|
91
|
+
"customClass",
|
|
92
|
+
"headerOne",
|
|
93
|
+
"headerTwo",
|
|
94
|
+
"headerThree",
|
|
95
|
+
"headerFour",
|
|
96
|
+
"headerFive",
|
|
97
|
+
"paragraph",
|
|
98
|
+
"blockquote",
|
|
99
|
+
"codeBlock",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
const mediaListLayoutSchema = z.enum(["HORIZONTAL_SCROLL", "GRID_2", "GRID_3", "GRID_4"]);
|
|
103
|
+
|
|
104
|
+
const selectItemSchema = z.object({
|
|
105
|
+
id: z.string(),
|
|
106
|
+
value: z.string(),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const richEditorV2ColorListItemSchema = z.object({
|
|
110
|
+
id: z.string(),
|
|
111
|
+
value: z.string(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const richEditorV2FontSizeListItemSchema = z.object({
|
|
115
|
+
id: z.string(),
|
|
116
|
+
name: z.string(),
|
|
117
|
+
value: z.string(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const customClassListItemSchema = z.object({
|
|
121
|
+
id: z.string(),
|
|
122
|
+
name: z.string(),
|
|
123
|
+
value: z.string(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const baseFieldSchema = z.object({
|
|
127
|
+
fieldId: z.string(),
|
|
128
|
+
name: z.string(),
|
|
129
|
+
description: z.string().optional(),
|
|
130
|
+
required: z.boolean(),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// テキストフィールド (text)
|
|
134
|
+
const textFieldSchema = baseFieldSchema.extend({
|
|
135
|
+
kind: z.literal("text"),
|
|
136
|
+
isUnique: z.boolean().optional(),
|
|
137
|
+
textSizeLimitValidation: textSizeLimitValidationSchema.optional(),
|
|
138
|
+
patternMatchValidation: patternMatchValidationSchema.optional(),
|
|
139
|
+
selectItems: z.array(selectItemSchema).optional(),
|
|
140
|
+
selectInitialValue: z.array(z.string()).optional(),
|
|
141
|
+
multipleSelect: z.boolean().optional(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// テキストエリア (textArea)
|
|
145
|
+
const textAreaFieldSchema = baseFieldSchema.extend({
|
|
146
|
+
kind: z.literal("textArea"),
|
|
147
|
+
textSizeLimitValidation: textSizeLimitValidationSchema.optional(),
|
|
148
|
+
patternMatchValidation: patternMatchValidationSchema.optional(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// リッチエディタ (richEditorV2)
|
|
152
|
+
const richEditorV2FieldSchema = baseFieldSchema.extend({
|
|
153
|
+
kind: z.literal("richEditorV2"),
|
|
154
|
+
richEditorV2Options: z.array(richEditorV2OptionSchema).optional(),
|
|
155
|
+
richEditorV2ColorList: z.array(richEditorV2ColorListItemSchema).optional(),
|
|
156
|
+
richEditorV2HideColorPicker: z.boolean().optional(),
|
|
157
|
+
richEditorV2FontSizeList: z.array(richEditorV2FontSizeListItemSchema).optional(),
|
|
158
|
+
customClassList: z.array(customClassListItemSchema).optional(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 旧リッチエディタ (richEditor)
|
|
162
|
+
const richEditorFieldSchema = baseFieldSchema.extend({
|
|
163
|
+
kind: z.literal("richEditor"),
|
|
164
|
+
richEditorImageSize: z.boolean().optional(),
|
|
165
|
+
richEditorOptions: z.array(richEditorOptionSchema).optional(),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// 画像 (media)
|
|
169
|
+
const mediaFieldSchema = baseFieldSchema.extend({
|
|
170
|
+
kind: z.literal("media"),
|
|
171
|
+
imageSizeValidation: imageSizeValidationSchema.optional(),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// 複数画像 (mediaList)
|
|
175
|
+
const mediaListFieldSchema = baseFieldSchema.extend({
|
|
176
|
+
kind: z.literal("mediaList"),
|
|
177
|
+
imageSizeValidation: imageSizeValidationSchema.optional(),
|
|
178
|
+
mediaListLayout: mediaListLayoutSchema.optional(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 日時 (date)
|
|
182
|
+
const dateFieldSchema = baseFieldSchema.extend({
|
|
183
|
+
kind: z.literal("date"),
|
|
184
|
+
dateFormat: z.boolean().optional(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 真偽値 (boolean)
|
|
188
|
+
const booleanFieldSchema = baseFieldSchema.extend({
|
|
189
|
+
kind: z.literal("boolean"),
|
|
190
|
+
booleanInitialValue: z.boolean().optional(),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// セレクトフィールド (select)
|
|
194
|
+
const selectFieldSchema = baseFieldSchema.extend({
|
|
195
|
+
kind: z.literal("select"),
|
|
196
|
+
selectItems: z.array(selectItemSchema),
|
|
197
|
+
selectInitialValue: z.array(z.string()),
|
|
198
|
+
multipleSelect: z.boolean(),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// コンテンツ参照 (relation)
|
|
202
|
+
const relationFieldSchema = baseFieldSchema.extend({
|
|
203
|
+
kind: z.literal("relation"),
|
|
204
|
+
referenceDisplayItem: z.string().optional(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// 複数コンテンツ参照 (relationList)
|
|
208
|
+
const relationListFieldSchema = baseFieldSchema.extend({
|
|
209
|
+
kind: z.literal("relationList"),
|
|
210
|
+
referenceDisplayItem: z.string().optional(),
|
|
211
|
+
relationListCountLimitValidation: relationListCountLimitValidationSchema.optional(),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// 数字 (number)
|
|
215
|
+
const numberFieldSchema = baseFieldSchema.extend({
|
|
216
|
+
kind: z.literal("number"),
|
|
217
|
+
numberSizeLimitValidation: numberSizeLimitValidationSchema.optional(),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// 拡張フィールド (iframe)
|
|
221
|
+
const iframeFieldSchema = baseFieldSchema.extend({
|
|
222
|
+
kind: z.literal("iframe"),
|
|
223
|
+
iframeUrl: z.string(),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const apiFieldSchema = z.discriminatedUnion("kind", [
|
|
227
|
+
textFieldSchema,
|
|
228
|
+
textAreaFieldSchema,
|
|
229
|
+
richEditorFieldSchema,
|
|
230
|
+
richEditorV2FieldSchema,
|
|
231
|
+
mediaFieldSchema,
|
|
232
|
+
mediaListFieldSchema,
|
|
233
|
+
dateFieldSchema,
|
|
234
|
+
booleanFieldSchema,
|
|
235
|
+
selectFieldSchema,
|
|
236
|
+
relationFieldSchema,
|
|
237
|
+
relationListFieldSchema,
|
|
238
|
+
numberFieldSchema,
|
|
239
|
+
iframeFieldSchema,
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
export const apiSchemaSchema = z.object({
|
|
243
|
+
apiFields: z.array(apiFieldSchema),
|
|
244
|
+
customFields: z.array(z.unknown()),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
export type ApiSchemaSchema = z.infer<typeof apiSchemaSchema>;
|