Improvements

This commit is contained in:
Hariom Balhara 2022-06-01 17:27:23 +05:30
parent d563343669
commit 8ecaa95dc9
28 changed files with 152 additions and 134 deletions

7
.vscode/tasks.json vendored
View File

@ -74,13 +74,6 @@
"isBackground": false,
"problemMatcher": []
},
{
"label": "AppStoreCli-build:watch",
"type": "shell",
"command": "cd packages/app-store-cli && yarn build:watch",
"isBackground": false,
"problemMatcher": []
},
{
"label": "AppStoreWatch",
"type": "shell",

View File

@ -1,29 +1,10 @@
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { deriveAppKeyFromSlug } from "@calcom/lib/deriveAppKeyFromSlug";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
function getSlugFromLegacy(legacySlug) {
const oldTypes = ["video", "other", "calendar", "web3", "payment", "messaging"];
// There can be two types of legacy slug
// - zoom_video
// - zoomvideo
// Transform `zoom_video` to `zoomvideo`;
let slug = legacySlug.split("_").join("");
// Transform zoomvideo to zoom
oldTypes.some((type) => {
const matcher = new RegExp(`(.+)${type}$`);
if (legacySlug.match(matcher)) {
slug = legacySlug.replace(matcher, "$1");
return true;
}
});
return slug;
}
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Check that user is authenticated
req.session = await getSession({ req });
@ -34,13 +15,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(404).json({ message: `API route not found` });
}
const [_appName, apiEndpoint] = args;
const appName = getSlugFromLegacy(_appName);
const [appName, apiEndpoint] = args;
try {
/* Absolute path didn't work */
const handlerMap = (await import("@calcom/app-store/apps.generated")).apiHandlers;
const handlerKey = appName as keyof typeof handlerMap;
console.log(handlerKey);
const handlerKey = deriveAppKeyFromSlug(appName, handlerMap);
const handlers = await handlerMap[handlerKey];
const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler;

View File

@ -6,23 +6,21 @@
"node": ">=10"
},
"scripts": {
"build:watch": "tsc --watch",
"build": "tsc && chmod +x dist/cli.js",
"start": "npm run build && dist/cli.js",
"pretest": "npm run build",
"test": "ava",
"cli": "chmod +x dist/cli.js && dist/cli.js"
"cli": "ts-node --transpile-only src/cli.tsx"
},
"files": [
"dist/cli.js"
],
"dependencies": {
"@calcom/lib": "*",
"ink": "^3.2.0",
"ink-text-input": "^4.0.3",
"meow": "^9.0.0",
"react": "^17.0.2"
},
"devDependencies": {
"ts-node": "^10.6.0",
"@ava/typescript": "^3.0.1",
"@types/react": "^18.0.9",
"ava": "^4.2.0",

View File

@ -15,6 +15,7 @@ Change name and description
## TODO
- Beta Release
- Print slug after creation of app. Also, mention that it would be same as dir name
- Handle legacy apps which have dirname as something else and type as something else. type is used to do lookups with key
- Add comment in config.json that this file shouldn't be modified manually.
- Install button not coming
@ -34,9 +35,16 @@ Change name and description
- Maybe get dx to run app-store:watch
- App already exists check. Ask user to run edit/regenerate command
### Why we shouldn't have appType
- App can have multiple types and thus categories is more suitable for this.
- The reason we seem to be using appType is to refer to an app uniquely but we already have app slug for that.
## Roadmap
- Allow editing and updating app from the cal app itself - including assets uploading when developing locally.
- Improvements in shared code across app
- Use baseApp/api/add.ts for all apps with configuration of credentials creation and redirection URL.
- Delete creation side effects if App creation fails - Might make debugging difficult
- This is so that web app doesn't break because of additional app folders or faulty db-seed

View File

@ -5,12 +5,20 @@ import TextInput from "ink-text-input";
import path from "path";
import React, { FC, useEffect, useRef, useState } from "react";
function sanitizeAppName(value: string): string {
return value.toLowerCase().replace(/-/g, "_");
const slugify = (str: string) => {
// It is to be a valid dir name, a valid JS variable name and a valid URL path
return str.replace(/[^a-zA-Z0-9-]/g, "_").toLowerCase();
};
function getSlugFromAppName(appName: string | null): string | null {
if (!appName) {
return appName;
}
return slugify(appName);
}
function getAppDirPath(appName: any) {
return path.join(appStoreDir, `${appName}`);
function getAppDirPath(slug: any) {
return path.join(appStoreDir, `${slug}`);
}
const appStoreDir = path.resolve(__dirname, "..", "..", "app-store");
@ -24,29 +32,30 @@ const execSync = (...args) => {
function absolutePath(appRelativePath) {
return path.join(appStoreDir, appRelativePath);
}
const updatePackageJson = ({ appName, appDirPath }) => {
const updatePackageJson = ({ slug, appDirPath }) => {
const packageJsonConfig = JSON.parse(fs.readFileSync(`${appDirPath}/package.json`).toString());
packageJsonConfig.name = `@calcom/${appName}`;
packageJsonConfig.name = `@calcom/${slug}`;
// packageJsonConfig.description = `@calcom/${appName}`;
fs.writeFileSync(`${appDirPath}/package.json`, JSON.stringify(packageJsonConfig, null, 2));
};
const BaseAppFork = {
create: function* ({ appType, appName, appTitle, publisherName, publisherEmail }) {
const appDirPath = getAppDirPath(appName);
create: function* ({ appType, appName, slug, appTitle, publisherName, publisherEmail }) {
const appDirPath = getAppDirPath(slug);
yield "Forking base app";
execSync(`mkdir -p ${appDirPath}`);
execSync(`cp -r ${absolutePath("_baseApp/*")} ${appDirPath}`);
updatePackageJson({ appName, appDirPath });
updatePackageJson({ slug, appDirPath });
let config = {
name: appName,
title: appTitle,
type: appType,
slug: appName,
imageSrc: `/api/app-store/${appName}/icon.svg`,
logo: `/api/app-store/${appName}/icon.svg`,
url: `https://cal.com/apps/${appName}`,
// @deprecated - It shouldn't exist.
type: slug,
slug: slug,
imageSrc: `/api/app-store/${slug}/icon.svg`,
logo: `/api/app-store/${slug}/icon.svg`,
url: `https://cal.com/apps/${slug}`,
variant: appType,
publisher: publisherName,
email: publisherEmail,
@ -59,22 +68,21 @@ const BaseAppFork = {
fs.writeFileSync(`${appDirPath}/config.json`, JSON.stringify(config, null, 2));
yield "Forked base app";
},
delete: function ({ appName }) {
const appDirPath = getAppDirPath(appName);
delete: function ({ slug }) {
const appDirPath = getAppDirPath(slug);
execSync(`rm -rf ${appDirPath}`);
},
};
const Seed = {
seedConfigPath: absolutePath("../prisma/seed-app-store.config.json"),
update: function ({ appName, appType, noDbUpdate }) {
update: function ({ slug, appType, noDbUpdate }) {
const seedConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString());
if (!seedConfig.find((app) => app.name === appName)) {
if (!seedConfig.find((app) => app.slug === slug)) {
seedConfig.push({
name: appName,
dirName: appName,
dirName: slug,
categories: [appType],
type: `${appName}_${appType}`,
slug: slug,
});
}
@ -83,12 +91,12 @@ const Seed = {
execSync(`cd ${workspaceDir} && yarn db-seed`);
}
},
revert: async function ({ appName, noDbUpdate }) {
revert: async function ({ slug, noDbUpdate }) {
let seedConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString());
seedConfig = seedConfig.filter((app) => app.name !== appName);
seedConfig = seedConfig.filter((app) => app.slug !== slug);
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
if (!noDbUpdate) {
execSync(`yarn workspace @calcom/prisma delete-app ${appName}`);
execSync(`yarn workspace @calcom/prisma delete-app ${slug}`);
}
},
};
@ -113,23 +121,23 @@ const CreateApp = ({ noDbUpdate }) => {
const fieldName = fields[inputIndex]?.name || "";
const fieldValue = appInputData[fieldName] || "";
const appName = appInputData["appName"];
const appType = `${appName}_${appInputData["appType"]}`;
const appType = appInputData["appType"];
const appTitle = appInputData["appTitle"];
const publisherName = appInputData["publisherName"];
const publisherEmail = appInputData["publisherEmail"];
const [result, setResult] = useState("...");
const { exit } = useApp();
const slug = getSlugFromAppName(appName);
const allFieldsFilled = inputIndex === fields.length;
useEffect(() => {
// When all fields have been filled
if (allFieldsFilled) {
const it = BaseAppFork.create({ appType, appName, appTitle, publisherName, publisherEmail });
const it = BaseAppFork.create({ appType, appName, slug, appTitle, publisherName, publisherEmail });
for (const item of it) {
setResult(item);
}
Seed.update({ appName, appType, noDbUpdate });
Seed.update({ slug, appType, noDbUpdate });
generateAppFiles();
@ -162,9 +170,6 @@ const CreateApp = ({ noDbUpdate }) => {
});
}}
onChange={(value) => {
if (value) {
value = sanitizeAppName(value);
}
setAppInputData((appInputData) => {
return {
...appInputData,
@ -177,24 +182,23 @@ const CreateApp = ({ noDbUpdate }) => {
);
};
const DeleteApp = ({ noDbUpdate, appName }) => {
appName = sanitizeAppName(appName);
BaseAppFork.delete({ appName });
Seed.revert({ appName });
const DeleteApp = ({ noDbUpdate, slug }) => {
BaseAppFork.delete({ slug });
Seed.revert({ slug });
generateAppFiles();
return <Text>Deleted App {appName}.</Text>;
return <Text>Deleted App {slug}.</Text>;
};
const App: FC<{ noDbUpdate?: boolean; command: "create" | "delete"; appName?: string }> = ({
const App: FC<{ noDbUpdate?: boolean; command: "create" | "delete"; slug?: string }> = ({
command,
noDbUpdate,
appName,
slug,
}) => {
if (command === "create") {
return <CreateApp noDbUpdate={noDbUpdate} />;
}
if (command === "delete") {
return <DeleteApp appName={appName} noDbUpdate={noDbUpdate} />;
return <DeleteApp slug={slug} noDbUpdate={noDbUpdate} />;
}
};
module.exports = App;

View File

@ -18,7 +18,7 @@ const cli = meow(
noDbUpdate: {
type: "boolean",
},
name: {
slug: {
type: "string",
},
},
@ -35,9 +35,9 @@ if (command !== "create" && command != "delete") {
cli.showHelp();
}
let appName = null;
let slug = null;
if (command === "delete") {
appName = cli.flags.name;
slug = cli.flags.slug;
}
render(<App appName={appName} command={command} noDbUpdate={cli.flags.noDbUpdate} />);
render(<App slug={slug} command={command} noDbUpdate={cli.flags.noDbUpdate} />);

View File

@ -5,7 +5,15 @@
"esModuleInterop": true,
"outDir": "dist",
"noEmitOnError": false,
"target": "ES2020"
"target": "ES2020",
"baseUrl": "."
},
"include": ["src"]
"include": [
"next-env.d.ts",
"../../packages/types/*.d.ts",
"../../packages/types/next-auth.d.ts",
"./src/**/*.ts",
"./src/**/*.tsx",
"../lib/**/*.ts"
]
}

View File

@ -28,6 +28,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const installation = await prisma.credential.create({
data: {
// TODO: Why do we need type in Credential? Why can't we simply use appId
// Using slug as type for new credentials so that we keep on using type in requests.
// `deriveAppKeyFromSlug` should be able to handle old type and new type which is equal to slug
type: slug,
key: {},
userId: req.session.user.id,

View File

@ -5,7 +5,9 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { App } from "@calcom/types/App";
function useAddAppMutation(type: App["type"], options?: Parameters<typeof useMutation>[2]) {
const appName = type.replace(/_/g, "");
// FIXME: Ensure that existing apps keep on working
// const appName = type.replace(/_/g, "");
const appName = type;
const mutation = useMutation(async () => {
const state: IntegrationOAuthCallbackState = {
returnTo: WEBAPP_URL + "/apps/installed" + location.search,

View File

@ -25,6 +25,9 @@ function getAppName(candidatePath) {
}
function generateFiles() {
let clientOutput = [`import dynamic from "next/dynamic"`];
let serverOutput = [];
fs.readdirSync(`${__dirname}`).forEach(function (dir) {
if (fs.statSync(`${__dirname}/${dir}`).isDirectory()) {
if (!getAppName(dir)) {
@ -34,9 +37,6 @@ function generateFiles() {
}
});
let clientOutput = [`import dynamic from "next/dynamic"`];
let serverOutput = [];
function forEachAppDir(callback) {
for (let i = 0; i < appDirs.length; i++) {
callback(appDirs[i]);
@ -98,12 +98,12 @@ if (isInWatchMode) {
debouncedGenerateFiles();
}
})
.on("change", (filePath) => {
if (filePath.endsWith("config.json")) {
console.log("Config file changed");
debouncedGenerateFiles();
}
})
// .on("change", (filePath) => {
// if (filePath.endsWith("config.json")) {
// console.log("Config file changed");
// debouncedGenerateFiles();
// }
// })
.on("unlinkDir", (dirPath) => {
const appName = getAppName(dirPath);
if (appName) {

View File

@ -1,6 +1,7 @@
import { useSession } from "next-auth/react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { deriveAppKeyFromSlug } from "@calcom/lib/deriveAppKeyFromSlug";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import Button from "@calcom/ui/Button";
@ -15,14 +16,8 @@ export const InstallAppButton = (
) => {
const { status } = useSession();
const { t } = useLocale();
let appName = props.type.replace(/_/g, "");
let InstallAppButtonComponent = InstallAppButtonMap[appName as keyof typeof InstallAppButtonMap];
/** So we can either call it by simple name (ex. `slack`, `giphy`) instead of
* `slackmessaging`, `giphyother` while maintaining retro-compatibility. */
if (!InstallAppButtonComponent) {
[appName] = props.type.split("_");
InstallAppButtonComponent = InstallAppButtonMap[appName as keyof typeof InstallAppButtonMap];
}
const key = deriveAppKeyFromSlug(props.type, InstallAppButtonMap);
const InstallAppButtonComponent = InstallAppButtonMap[key as keyof typeof InstallAppButtonMap];
if (!InstallAppButtonComponent) return null;
if (status === "unauthenticated")
return (

View File

@ -1,12 +0,0 @@
{
"name": "demo",
"title": "it's a demo app",
"type": "demo_other",
"slug": "demo",
"imageSrc": "/api/app-store/demo/icon.svg",
"logo": "/api/app-store/demo/icon.svg",
"url": "https://cal.com/apps/demo",
"variant": "other",
"publisher": "hariom",
"email": "hariombalhara@gmail.com"
}

View File

@ -8,6 +8,7 @@ import _package from "./package.json";
export const metadata = {
description: _package.description,
category: "other",
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
installed: true,
rating: 0,
reviews: 0,

View File

@ -4,6 +4,11 @@ import prisma from "@calcom/prisma";
import appConfig from "../config.json";
// TODO: There is a lot of code here that would be used by almost all apps
// - Login Validation
// - Looking up credential.
// - Creating credential would be specific to app, so there can be just createCredential method that app can expose
// - Redirection after successful installation can also be configured by app
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
@ -23,6 +28,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const installation = await prisma.credential.create({
data: {
// TODO: Why do we need type in Credential? Why can't we simply use appId
// Using slug as type for new credentials so that we keep on using type in requests.
// `deriveAppKeyFromSlug` should be able to handle old type and new type which is equal to slug
type: slug,
key: {},
userId: req.session.user.id,
@ -40,5 +47,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(500);
}
return res.status(200).json({ url: "/apps/zapier/setup" });
return res.status(200).json({ url: "/apps/installed" });
}

View File

@ -4,8 +4,6 @@ import useAddAppMutation from "../../_utils/useAddAppMutation";
import appConfig from "../config.json";
export default function InstallAppButton(props: InstallAppButtonProps) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
const mutation = useAddAppMutation(appConfig.slug);
return (

View File

@ -0,0 +1,12 @@
{
"name": "Demo App",
"title": "It is a demo app",
"type": "demo_app",
"slug": "demo_app",
"imageSrc": "/api/app-store/demo_app/icon.svg",
"logo": "/api/app-store/demo_app/icon.svg",
"url": "https://cal.com/apps/demo_app",
"variant": "other",
"publisher": "hariom",
"email": "har@gmail"
}

View File

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/demo",
"name": "@calcom/demo_app",
"version": "0.0.0",
"main": "./index.ts",
"description": "Your app description goes here.",

View File

@ -0,0 +1,23 @@
export function deriveAppKeyFromSlug(legacySlug, map) {
const oldTypes = ["video", "other", "calendar", "web3", "payment", "messaging"];
const handlerKey = legacySlug as keyof typeof map;
const handlers = map[handlerKey];
if (handlers) {
return handlerKey;
}
// There can be two types of legacy slug
// - zoom_video
// - zoomvideo
// Transform `zoom_video` to `zoomvideo`;
let slug = legacySlug.split("_").join("");
// Transform zoomvideo to zoom
oldTypes.some((type) => {
const matcher = new RegExp(`(.+)${type}$`);
if (legacySlug.match(matcher)) {
slug = legacySlug.replace(matcher, "$1");
return true;
}
});
return slug;
}

View File

@ -1,5 +0,0 @@
export const slugify = (str: string) => {
return str.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
};
export default slugify;

View File

@ -1,19 +1,25 @@
import prisma from ".";
require("dotenv").config({ path: "../../.env" });
// TODO: Put some restrictions here to run it on local DB only.
// Production DB currently doesn't support app deletion
async function main() {
const appName = process.argv[2];
const appId = process.argv[2];
try {
await prisma.app.delete({
where: {
slug: appName,
slug: appId,
},
});
console.log(`Deleted app from DB: '${appName}'`);
await prisma.credential.deleteMany({
where: {
appId: appId,
},
});
console.log(`Deleted app from DB: '${appId}'`);
} catch (e) {
if (e.code === "P2025") {
console.log(`App '${appName}' already deleted from DB`);
console.log(`App '${appId}' already deleted from DB`);
return;
}
throw e;

View File

@ -1,14 +1,12 @@
[
{
"name": "demo",
"dirName": "demo",
"dirName": "demo app",
"categories": ["other"],
"type": "demo_other"
"slug": "demo app"
},
{
"name": "demovideo",
"dirName": "demovideo",
"categories": ["video"],
"type": "demovideo_video"
"dirName": "demo_app",
"categories": ["other"],
"slug": "demo_app"
}
]

View File

@ -139,7 +139,7 @@ async function main() {
);
for (let i = 0; i < generatedApps.length; i++) {
const generatedApp = generatedApps[i];
await createApp(generatedApp.name, generatedApp.dirName, generatedApp.categories, generatedApp.type);
await createApp(generatedApp.slug, generatedApp.dirName, generatedApp.categories, generatedApp.type);
}
}