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 <devanshusharma658@gmail.com>
Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
This commit is contained in:
DUDHAT HEMIL PRAVINKUMAR
2025-09-04 06:03:23 +00:00
committed by GitHub
co-authored by Tushar Bhatt Kartik Saini Devanshu Sharma Volnei Munhoz
parent 3f8ce8ff50
commit 4eb884e2a6
2 changed files with 127 additions and 1 deletions
+9 -1
View File
@@ -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;
+118
View File
@@ -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<typeof AppMetaSchema>;