Skip to content

Commit 700d44c

Browse files
authored
update the action schemas (#284)
* update the action schemas * refactor the type * order the queries * remove description * change the format * final update * self review * fix the limitation * update the date format * removed the deprecated components * update the header, body, footer * change the limitation * fix the format
1 parent f1622b3 commit 700d44c

File tree

1 file changed

+366
-16
lines changed

1 file changed

+366
-16
lines changed

src/common/schema/flexMessage.ts

Lines changed: 366 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,372 @@
11
import { z } from "zod";
22

3-
export const flexMessageSchema = z.object({
4-
type: z.literal("flex").default("flex"),
5-
altText: z
3+
const sizeSchema = z
4+
.enum(["xxs", "xs", "sm", "md", "lg", "xl", "xxl", "3xl", "4xl", "5xl"])
5+
.default("md");
6+
const imageSizeSchema = z
7+
.enum([
8+
"xxs",
9+
"xs",
10+
"sm",
11+
"md",
12+
"lg",
13+
"xl",
14+
"xxl",
15+
"3xl",
16+
"4xl",
17+
"5xl",
18+
"full",
19+
])
20+
.default("md");
21+
const spacerSizeSchema = z.enum(["xs", "sm", "md", "lg", "xl", "xxl"]);
22+
const marginSchema = z.enum(["none", "xs", "sm", "md", "lg", "xl", "xxl"]);
23+
const spacingSchema = z.enum(["none", "xs", "sm", "md", "lg", "xl", "xxl"]);
24+
const positionSchema = z.enum(["relative", "absolute"]);
25+
const alignSchema = z.enum(["start", "end", "center"]);
26+
const gravitySchema = z.enum(["top", "bottom", "center"]);
27+
const offsetSchema = z.string().regex(/^\d+px$/, "Format: '10px'");
28+
const colorSchema = z
29+
.string()
30+
.regex(/^#[0-9A-Fa-f]{6}$/, "Hex format: '#FF0000'");
31+
const flexWeightSchema = z.number();
32+
const scalingSchema = z.boolean();
33+
34+
const positionFields = {
35+
position: positionSchema.optional(),
36+
offsetTop: offsetSchema.optional(),
37+
offsetBottom: offsetSchema.optional(),
38+
offsetStart: offsetSchema.optional(),
39+
offsetEnd: offsetSchema.optional(),
40+
};
41+
42+
const layoutFields = {
43+
flex: flexWeightSchema.optional(),
44+
margin: marginSchema.optional(),
45+
...positionFields,
46+
};
47+
48+
const alignmentFields = {
49+
align: alignSchema.optional(),
50+
gravity: gravitySchema.optional(),
51+
};
52+
53+
const textStyleFields = {
54+
text: z.string().min(1).max(2000),
55+
color: colorSchema.optional(),
56+
size: sizeSchema.optional(),
57+
weight: z.enum(["regular", "bold"]).optional(),
58+
style: z.enum(["normal", "italic"]).optional(),
59+
decoration: z.enum(["none", "underline", "line-through"]).optional(),
60+
};
61+
62+
const paddingFields = {
63+
paddingAll: z
64+
.string()
65+
.regex(/^\d+px$/)
66+
.optional(),
67+
paddingTop: z
68+
.string()
69+
.regex(/^\d+px$/)
70+
.optional(),
71+
paddingBottom: z
672
.string()
7-
.describe("Alternative text shown when flex message cannot be displayed."),
8-
contents: z
9-
.object({
10-
type: z
11-
.enum(["bubble", "carousel"])
73+
.regex(/^\d+px$/)
74+
.optional(),
75+
paddingStart: z
76+
.string()
77+
.regex(/^\d+px$/)
78+
.optional(),
79+
paddingEnd: z
80+
.string()
81+
.regex(/^\d+px$/)
82+
.optional(),
83+
};
84+
85+
const flexActionSchema = z.discriminatedUnion("type", [
86+
z.object({
87+
type: z.literal("postback"),
88+
data: z.string().min(1).max(300),
89+
label: z.string().min(1).max(20),
90+
displayText: z.string().min(1).max(300).optional(),
91+
inputOption: z
92+
.enum(["closeRichMenu", "openRichMenu", "openKeyboard", "openVoice"])
93+
.optional(),
94+
fillInText: z.string().min(1).max(160).optional(),
95+
}),
96+
z.object({
97+
type: z.literal("message"),
98+
label: z.string().min(1).max(20),
99+
text: z.string().min(1).max(300),
100+
}),
101+
z.object({
102+
type: z.literal("uri"),
103+
label: z.string().min(1).max(20),
104+
uri: z
105+
.string()
106+
.describe(
107+
"LINE Custom URI or URI" +
108+
"LINE Custom URI document: https://developers.line.biz/ja/docs/messaging-api/using-line-url-scheme/",
109+
),
110+
altUri: z
111+
.object({
112+
desktop: z.string().url(),
113+
})
114+
.optional(),
115+
}),
116+
z.object({
117+
type: z.literal("datetimepicker"),
118+
label: z.string().min(1).max(20),
119+
data: z.string().min(1).max(300),
120+
mode: z.enum(["date", "time", "datetime"]),
121+
initial: z
122+
.string()
123+
.optional()
124+
.describe("Format: 2100-12-31, 23:59, 2100-12-31T23:59"),
125+
max: z
126+
.string()
127+
.optional()
128+
.describe("Format: 2100-12-31, 23:59, 2100-12-31T23:59"),
129+
min: z
130+
.string()
131+
.optional()
132+
.describe("Format: 2100-12-31, 23:59, 2100-12-31T23:59"),
133+
}),
134+
z.object({
135+
type: z.literal("camera"),
136+
label: z.string().min(1).max(20),
137+
}),
138+
z.object({
139+
type: z.literal("cameraRoll"),
140+
label: z.string().min(1).max(20),
141+
}),
142+
z.object({
143+
type: z.literal("location"),
144+
label: z.string().min(1).max(20),
145+
}),
146+
z.object({
147+
type: z.literal("richmenuswitch"),
148+
label: z.string().min(1).max(20),
149+
richMenuAliasId: z.string().min(1).max(32),
150+
data: z.string().min(1).max(300),
151+
}),
152+
z.object({
153+
type: z.literal("clipboard"),
154+
label: z.string().min(1).max(20),
155+
clipboardText: z.string().min(1).max(1000),
156+
}),
157+
]);
158+
159+
const flexSpanSchema = z.object({
160+
type: z.literal("span"),
161+
...textStyleFields,
162+
});
163+
164+
const flexComponentSchema: z.ZodType<any> = z.lazy(() =>
165+
z.discriminatedUnion("type", [
166+
z.object({
167+
type: z.literal("separator"),
168+
margin: marginSchema.optional(),
169+
color: colorSchema.optional(),
170+
}),
171+
z.object({
172+
type: z.literal("text"),
173+
contents: z.array(flexSpanSchema).optional(),
174+
adjustMode: z.enum(["shrink-to-fit"]).optional(),
175+
wrap: z.boolean().optional().default(true),
176+
lineSpacing: z.enum(["xs", "sm", "md", "lg", "xl", "xxl"]).optional(),
177+
maxLines: z.number().optional(),
178+
action: flexActionSchema.optional(),
179+
scaling: scalingSchema.optional(),
180+
...textStyleFields,
181+
...layoutFields,
182+
...alignmentFields,
183+
}),
184+
185+
z.object({
186+
type: z.literal("icon"),
187+
url: z
188+
.string()
189+
.url()
190+
.min(1)
191+
.max(2000)
192+
.refine(url => url.startsWith("https://"), "Must use HTTPS protocol"),
193+
size: sizeSchema.optional(),
194+
aspectRatio: z
195+
.string()
196+
.regex(/^\d+:\d+$/)
12197
.describe(
13-
"Type of the container. 'bubble' for single container, 'carousel' for multiple swipeable bubbles.",
198+
"Aspect ratio in '{width}:{height}' format (e.g., '1:1', '16:9'). Width and height must be 1-100000, height cannot exceed width × 3",
199+
)
200+
.optional(),
201+
scaling: scalingSchema.optional(),
202+
...layoutFields,
203+
}),
204+
z.object({
205+
type: z.literal("image"),
206+
url: z
207+
.string()
208+
.url()
209+
.min(1)
210+
.max(2000)
211+
.default(
212+
"https://developers-resource.landpress.line.me/fx/img/01_1_cafe.png",
14213
),
15-
})
16-
.passthrough()
17-
.describe(
18-
"Flexible container structure following LINE Flex Message format. For 'bubble' type, can include header, " +
19-
"hero, body, footer, and styles sections. For 'carousel' type, includes an array of bubble containers in " +
20-
"the 'contents' property.",
21-
),
214+
size: imageSizeSchema.optional(),
215+
aspectRatio: z
216+
.string()
217+
.regex(/^\d+:\d+$/)
218+
.describe(
219+
"Aspect ratio in '{width}:{height}' format (e.g., '1:1', '16:9'). Width and height must be 1-100000, height cannot exceed width × 3",
220+
)
221+
.optional(),
222+
aspectMode: z.enum(["cover", "fit"]).optional(),
223+
backgroundColor: colorSchema.optional(),
224+
animated: z.boolean().optional(),
225+
action: flexActionSchema.optional(),
226+
scaling: scalingSchema.optional(),
227+
...layoutFields,
228+
...alignmentFields,
229+
}),
230+
z.object({
231+
type: z.literal("video"),
232+
url: z
233+
.string()
234+
.url()
235+
.min(1)
236+
.max(2000)
237+
.refine(url => url.startsWith("https://"), "Must use HTTPS protocol"),
238+
previewUrl: z
239+
.string()
240+
.url()
241+
.min(1)
242+
.max(2000)
243+
.default(
244+
"https://developers-resource.landpress.line.me/fx/img/01_1_cafe.png",
245+
),
246+
altContent: flexComponentSchema,
247+
size: imageSizeSchema.optional(),
248+
aspectRatio: z
249+
.string()
250+
.regex(/^\d+:\d+$/)
251+
.describe(
252+
"Aspect ratio in '{width}:{height}' format (e.g., '1:1', '16:9'). Width and height must be 1-100000, height cannot exceed width × 3",
253+
)
254+
.optional(),
255+
action: flexActionSchema.optional(),
256+
scaling: scalingSchema.optional(),
257+
...layoutFields,
258+
...alignmentFields,
259+
}),
260+
261+
z.object({
262+
type: z.literal("button"),
263+
action: flexActionSchema,
264+
height: z.enum(["sm", "md"]).optional(),
265+
style: z.enum(["link", "primary", "secondary"]).optional(),
266+
color: colorSchema.optional(),
267+
gravity: gravitySchema.optional(),
268+
adjustMode: z.enum(["shrink-to-fit"]).optional(),
269+
scaling: scalingSchema.optional(),
270+
...layoutFields,
271+
}),
272+
273+
z.object({
274+
type: z.literal("box"),
275+
layout: z.enum(["horizontal", "vertical", "baseline"]),
276+
contents: z.array(flexComponentSchema),
277+
backgroundColor: colorSchema.optional(),
278+
borderColor: colorSchema.optional(),
279+
borderWidth: z
280+
.string()
281+
.regex(/^\d+px$/)
282+
.optional(),
283+
cornerRadius: z
284+
.string()
285+
.regex(/^\d+px$/)
286+
.optional(),
287+
spacing: spacingSchema.optional(),
288+
width: z
289+
.string()
290+
.regex(/^\d+px$/)
291+
.optional(),
292+
height: z
293+
.string()
294+
.regex(/^\d+px$/)
295+
.optional(),
296+
justifyContent: z
297+
.enum([
298+
"flex-start",
299+
"center",
300+
"flex-end",
301+
"space-between",
302+
"space-around",
303+
"space-evenly",
304+
])
305+
.optional(),
306+
alignItems: z.enum(["flex-start", "center", "flex-end"]).optional(),
307+
background: z
308+
.object({
309+
type: z.literal("linearGradient"),
310+
angle: z.string().regex(/^\d+deg$/, "Format: '90deg'"),
311+
startColor: colorSchema,
312+
endColor: colorSchema,
313+
})
314+
.optional(),
315+
action: flexActionSchema.optional(),
316+
...layoutFields,
317+
...paddingFields,
318+
}),
319+
]),
320+
);
321+
322+
const sectionStyleSchema = z.object({
323+
backgroundColor: colorSchema.optional(),
324+
separator: z.boolean().optional(),
325+
separatorColor: colorSchema.optional(),
326+
});
327+
328+
const flexBubbleStylesSchema = z.object({
329+
header: sectionStyleSchema.optional(),
330+
hero: sectionStyleSchema.optional(),
331+
body: sectionStyleSchema.optional(),
332+
footer: sectionStyleSchema.optional(),
333+
});
334+
335+
export const flexBubbleSchema = z.object({
336+
type: z.literal("bubble"),
337+
size: z
338+
.enum(["nano", "micro", "deca", "hecto", "kilo", "mega", "giga"])
339+
.optional(),
340+
direction: z.enum(["ltr", "rtl"]).optional(),
341+
header: flexComponentSchema
342+
.optional()
343+
.describe("Header must be a Box")
344+
.refine(component => component.type === "box", "Header must be a Box"),
345+
hero: flexComponentSchema.optional(),
346+
body: flexComponentSchema
347+
.optional()
348+
.describe("Body must be a Box")
349+
.refine(component => component.type === "box", "Body must be a Box"),
350+
footer: flexComponentSchema
351+
.optional()
352+
.describe("Footer must be a Box")
353+
.refine(component => component.type === "box", "Footer must be a Box"),
354+
styles: flexBubbleStylesSchema.optional(),
355+
action: flexActionSchema.optional(),
356+
});
357+
358+
const flexCarouselSchema = z.object({
359+
type: z.literal("carousel"),
360+
contents: z.array(flexBubbleSchema),
361+
});
362+
363+
const flexContainerSchema = z.discriminatedUnion("type", [
364+
flexBubbleSchema,
365+
flexCarouselSchema,
366+
]);
367+
368+
export const flexMessageSchema = z.object({
369+
type: z.literal("flex").default("flex"),
370+
altText: z.string().min(1).max(400),
371+
contents: flexContainerSchema,
22372
});

0 commit comments

Comments
 (0)