From 4eb884e2a6f2b1c94a871c66f7f0253389556dc4 Mon Sep 17 00:00:00 2001 From: DUDHAT HEMIL PRAVINKUMAR <138382078+Hemil36@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:33:23 +0530 Subject: [PATCH] feat: zod schemas for app metadata in config.json (#21231) * feat: add config metadata validation and update app store metadata parsing * fix: update config metadata import to use parseconfigMetadata * Create configType.ts * fix: update app store metadata parsing to use parseconfigMetadata * feat: enhance generateFiles to handle metadata imports * Update build.ts * Updated build file --------- Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com> Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com> Co-authored-by: Devanshu Sharma Co-authored-by: Volnei Munhoz --- packages/app-store-cli/src/build.ts | 10 ++- packages/types/AppMetaSchema.ts | 118 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 packages/types/AppMetaSchema.ts diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index e52cc73439..51c3078a32 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -9,6 +9,7 @@ import prettier from "prettier"; //@ts-ignore import prettierConfig from "@calcom/config/prettier-preset"; import type { AppMeta } from "@calcom/types/App"; +import { AppMetaSchema } from "@calcom/types/AppMetaSchema"; import { APP_STORE_PATH } from "./constants"; import { getAppName } from "./utils/getAppName"; @@ -72,7 +73,14 @@ function generateFiles() { let app; if (fs.existsSync(configPath)) { - app = JSON.parse(fs.readFileSync(configPath).toString()); + try { + const rawConfig = fs.readFileSync(configPath, "utf8"); + const parsedConfig = JSON.parse(rawConfig); + app = AppMetaSchema.parse(parsedConfig); + } catch (error) { + const prefix = `Config error in ${path.join(APP_STORE_PATH, appDirs[i].path, "config.json")}`; + throw new Error(`${prefix}: ${error instanceof Error ? error.message : String(error)}`); + } } else if (fs.existsSync(metadataPath)) { // eslint-disable-next-line @typescript-eslint/no-var-requires app = require(metadataPath).metadata; diff --git a/packages/types/AppMetaSchema.ts b/packages/types/AppMetaSchema.ts new file mode 100644 index 0000000000..f6d391e8f3 --- /dev/null +++ b/packages/types/AppMetaSchema.ts @@ -0,0 +1,118 @@ +import { z } from "zod"; + +const eventLocationTypeSchema = z.object({ + type: z.string(), + label: z.string(), + messageForOrganizer: z.string().optional(), + iconUrl: z.string().optional(), + variable: z.literal("locationLink").optional(), + defaultValueVariable: z.literal("link").optional(), + attendeeInputType: z.null().optional(), + attendeeInputPlaceholder: z.null().optional(), + linkType: z.enum(["static", "dynamic"]), + urlRegExp: z.string().optional(), + organizerInputPlaceholder: z.string().optional(), + organizerInputType: z.enum(["text", "phone"]).optional(), + default: z.boolean().optional(), +}); + +const appDataSchema = z + .object({ + location: eventLocationTypeSchema.optional(), + tag: z + .object({ + scripts: z + .array( + z + .object({ + src: z.string().optional(), + content: z.string().optional(), + attrs: z.record(z.any()).optional(), + }) + .passthrough() + ) + .optional(), + }) + .passthrough() + .optional(), + }) + .passthrough() + .nullable() + .optional(); + +const paidAppDataSchema = z.object({ + priceInUsd: z.number(), + priceId: z.string().optional(), + trial: z.number().optional(), + mode: z.enum(["subscription", "one_time"]).optional(), +}); + +export const AppMetaSchema = z + .object({ + name: z.string(), + description: z.string(), + type: z.string(), + variant: z.enum([ + "calendar", + "payment", + "conferencing", + "video", + "other", + "other_calendar", + "automation", + "crm", + "analytics", + "messaging", + ]), + slug: z.string(), + categories: z.array(z.string()), + logo: z.string(), + publisher: z.string(), + url: z.string().optional(), + email: z.string(), + installed: z.boolean().optional(), + title: z.string().optional(), + category: z.string().optional(), + extendsFeature: z.enum(["EventType", "User"]).optional(), + docsUrl: z.string().optional(), + verified: z.boolean().optional(), + trending: z.boolean().optional(), + rating: z.number().optional(), + reviews: z.number().optional(), + isGlobal: z.boolean().optional(), + simplePath: z.string().optional(), + key: z.any().optional(), + feeType: z.enum(["monthly", "usage-based", "one-time", "free"]).optional(), + price: z.number().optional(), + commission: z.number().optional(), + licenseRequired: z.boolean().optional(), + teamsPlanRequired: z + .object({ + upgradeUrl: z.string(), + }) + .passthrough() + .optional(), + appData: appDataSchema, + paid: paidAppDataSchema.optional(), + dirName: z.string().optional(), + isTemplate: z.boolean().optional(), + __template: z.string().optional(), + dependencies: z.array(z.string()).optional(), + concurrentMeetings: z.boolean().optional(), + createdAt: z.string().optional(), + isOAuth: z.boolean().optional(), + isAuth: z.boolean().optional(), + delegationCredential: z + .object({ + workspacePlatformSlug: z.string(), + }) + .passthrough() + .optional(), + __createdUsingCli: z.boolean().optional(), + imageSrc: z.string().optional(), + label: z.string().optional(), + linkType: z.string().optional(), + }) + .passthrough(); + +export type AppMetaType = z.infer;