Skip to content

Commit 4c5e2fb

Browse files
support PBB app feature types in manifest and prompts (#165)
* support PBB app feature types in manifest and prompts Fetch active types from /apps_ms/public/platform-building-blocks-schemas at runtime and merge with the static AppFeatureType enum so manifests can declare PBB-only types (incl. action-family blocks) without enum maintenance. Falls back to enum-only when the endpoint is unreachable. Co-authored-by: Cursor <cursoragent@cursor.com> * bump version --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b8790f0 commit 4c5e2fb

11 files changed

Lines changed: 119 additions & 15 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mondaycom/apps-cli",
3-
"version": "4.10.5",
3+
"version": "4.10.6",
44
"description": "A cli tool to manage apps (and monday-code projects) in monday.com",
55
"author": "monday.com Apps Team",
66
"type": "module",

src/commands/app-features/create.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { Flags } from '@oclif/core';
22

33
import { AuthenticatedCommand } from 'commands-base/authenticated-command';
44
import { APP_ID_TO_ENTER, APP_VERSION_ID_TO_ENTER } from 'consts/messages';
5+
import { getValidAppFeatureTypes } from 'services/app-feature-types-service';
56
import { createAppFeature } from 'services/app-features-service';
67
import { DynamicChoicesService } from 'services/dynamic-choices-service';
8+
import { pbbSchemaManager } from 'services/pbb-schema-manager';
79
import { PromptService } from 'services/prompt-service';
810
import { AppFeatureType } from 'types/services/app-features-service';
911
import logger from 'utils/logger';
@@ -67,8 +69,9 @@ export default class Create extends AuthenticatedCommand {
6769
appFeatureName = await PromptService.promptInput('Please enter feature name');
6870
}
6971

72+
await pbbSchemaManager.initialize();
7073
if (
71-
!(Object.values(AppFeatureType) as string[]).includes(appFeatureType) ||
74+
!getValidAppFeatureTypes().includes(appFeatureType) ||
7275
appFeatureType === (AppFeatureType.AppFeatureOauth as string)
7376
) {
7477
logger.error(`Invalid feature type`);

src/commands/app/deploy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export default class AppDeploy extends AuthenticatedCommand {
6464
let { appId, appVersionId } = flags;
6565
const region = getRegionFromString(flags?.region);
6666
const manifestFileDir = directoryPath || getCurrentWorkingDirectory();
67-
const manifestFileData = readManifestFile(manifestFileDir);
67+
const manifestFileData = await readManifestFile(manifestFileDir);
6868
appId = appId || manifestFileData.app.id;
6969

7070
appVersionId = await this.getAppVersionId(appVersionId, appId, force);

src/consts/urls.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,7 @@ export const makeAppManifestExportableUrl = (appId: AppId): string => {
146146
export const getDeploymentSecurityScanUrl = (appVersionId: number): string => {
147147
return `${appVersionIdBaseUrl(appVersionId)}/deployments/security-scan`;
148148
};
149+
150+
export const platformBuildingBlocksSchemasUrl = (): string => {
151+
return `/apps_ms/public/platform-building-blocks-schemas`;
152+
};

src/services/__tests__/manifest-service.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ import { readManifestFile } from 'services/manifest-service';
55

66
describe('ManifestService', () => {
77
describe('readManifestFile', () => {
8-
it('should read manifest file', () => {
9-
const manifest = readManifestFile('src/services/__tests__/mocks/', 'manifest-mock.yml');
8+
it('should read manifest file', async () => {
9+
const manifest = await readManifestFile('src/services/__tests__/mocks/', 'manifest-mock.yml');
1010
expect(manifest?.app?.id).toEqual(undefined);
1111
expect(manifest?.app?.hosting?.cdn?.path).toEqual('./build');
1212
expect(manifest?.app?.hosting?.server?.path).toEqual('./server');
1313
expect(manifest?.version).toEqual('1.0.0');
1414
});
1515

16-
it('should raise an error if manifest file is not valid', () => {
17-
expect(() => readManifestFile('src/services/__tests__/mocks/', 'invalid-manifest-mock.yml')).toThrowError();
16+
it('should raise an error if manifest file is not valid', async () => {
17+
await expect(readManifestFile('src/services/__tests__/mocks/', 'invalid-manifest-mock.yml')).rejects.toThrow();
1818
});
1919
});
2020
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { pbbSchemaManager } from 'services/pbb-schema-manager';
2+
import { AppFeatureType } from 'types/services/app-features-service';
3+
4+
export const getValidAppFeatureTypes = (): string[] => {
5+
const enumTypes = Object.values(AppFeatureType) as string[];
6+
const pbbTypes = pbbSchemaManager.getActiveTypeNames();
7+
return [...new Set([...enumTypes, ...pbbTypes])];
8+
};

src/services/apps-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const cloneAppTemplateAndLoadManifest = async (
7373
};
7474

7575
await cloneFolderFromGitRepo(ctx.githubUrl, ctx.folder, ctx.branch, ctx.targetPath, output);
76-
const manifestData = readManifestFile(ctx.targetPath);
76+
const manifestData = await readManifestFile(ctx.targetPath);
7777
ctx.appName = ctx.appName || manifestData.app.name;
7878
ctx.features = manifestData.app.features;
7979
};

src/services/dynamic-choices-service.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { APP_TEMPLATES_CONFIG } from 'consts/app-templates-config';
22
import { APP_VERSION_STATUS } from 'consts/app-versions';
33
import { listAppBuilds } from 'services/app-builds-service';
4+
import { getValidAppFeatureTypes } from 'services/app-feature-types-service';
45
import { listAppFeaturesByAppVersionId } from 'services/app-features-service';
56
import { defaultVersionByAppId, listAppVersionsByAppId } from 'services/app-versions-service';
67
import { listApps } from 'services/apps-service';
8+
import { pbbSchemaManager } from 'services/pbb-schema-manager';
79
import { PromptService } from 'services/prompt-service';
810
import { LIVE_VERSION_ERROR_LOG } from 'src/consts/messages';
911
import { AppId } from 'src/types/general';
@@ -77,11 +79,13 @@ export const DynamicChoicesService = {
7779
return { appId: chosenAppId, appVersionId };
7880
},
7981

80-
async chooseAppFeatureType(excludeTypes?: AppFeatureType[]) {
81-
const featureTypes = Object.values(AppFeatureType);
82-
const featureTypeChoicesMap: Record<string, AppFeatureType> = {};
82+
async chooseAppFeatureType(excludeTypes?: Array<AppFeatureType | string>) {
83+
await pbbSchemaManager.initialize();
84+
const featureTypes = getValidAppFeatureTypes();
85+
const excluded = new Set<string>(excludeTypes);
86+
const featureTypeChoicesMap: Record<string, string> = {};
8387
for (const featureType of featureTypes) {
84-
if (excludeTypes?.includes(featureType)) continue;
88+
if (excluded.has(featureType)) continue;
8589
featureTypeChoicesMap[featureType] = featureType;
8690
}
8791

src/services/manifest-service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { join } from 'node:path';
44
import { load } from 'js-yaml';
55

66
import { BadConfigError } from 'errors/bad-config-error';
7+
import { pbbSchemaManager } from 'services/pbb-schema-manager';
78
import { ManifestFileSchema } from 'services/schemas/manifest-service-schemas';
89
import { BUILD_TYPES, BUILD_TYPES_MANIFEST_FORMAT } from 'types/services/app-features-service';
910
import logger from 'utils/logger';
@@ -16,14 +17,15 @@ const checkConfigExists = (directoryPath: string, fileName = MANIFEST_FILE_NAME)
1617
return existsSync(filePath);
1718
};
1819

19-
export const readManifestFile = (directoryPath: string, fileName = MANIFEST_FILE_NAME) => {
20+
export const readManifestFile = async (directoryPath: string, fileName = MANIFEST_FILE_NAME) => {
2021
if (!checkConfigExists(directoryPath, fileName)) {
2122
throw new BadConfigError(`the file: ${fileName} is not found in ${directoryPath}`);
2223
}
2324

2425
const filePath = join(directoryPath, fileName);
2526
const stringifiedData = readFileSync(filePath, { encoding: ENCODING });
2627
const data = load(stringifiedData);
28+
await pbbSchemaManager.initialize();
2729
try {
2830
return ManifestFileSchema.parse(data);
2931
} catch (error) {

src/services/pbb-schema-manager.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import crypto from 'node:crypto';
2+
import https from 'node:https';
3+
4+
import axios from 'axios';
5+
6+
import { platformBuildingBlocksSchemasUrl } from 'consts/urls';
7+
import logger from 'utils/logger';
8+
import { appsUrlBuilder } from 'utils/urls-builder';
9+
10+
type PbbSchemaEntry = {
11+
name?: string;
12+
status?: string;
13+
};
14+
15+
const TWO_HOURS_IN_MS = 1000 * 60 * 60 * 2;
16+
const FETCH_TIMEOUT_IN_MS = 5000;
17+
18+
class PbbSchemaManager {
19+
private activeTypeNames: string[] = [];
20+
private lastFetchedAt?: number;
21+
private fetchPromise?: Promise<void>;
22+
23+
public async initialize(): Promise<void> {
24+
if (this.fetchPromise) {
25+
return this.fetchPromise;
26+
}
27+
28+
if (this.isInitialized()) {
29+
return;
30+
}
31+
32+
this.fetchPromise = this.fetchSchemas();
33+
try {
34+
await this.fetchPromise;
35+
} finally {
36+
this.fetchPromise = undefined;
37+
}
38+
}
39+
40+
public getActiveTypeNames(): string[] {
41+
return [...this.activeTypeNames];
42+
}
43+
44+
public isInitialized(): boolean {
45+
return (
46+
this.activeTypeNames.length > 0 &&
47+
this.lastFetchedAt !== undefined &&
48+
this.lastFetchedAt + TWO_HOURS_IN_MS > Date.now()
49+
);
50+
}
51+
52+
private async fetchSchemas(): Promise<void> {
53+
try {
54+
const url = appsUrlBuilder(platformBuildingBlocksSchemasUrl());
55+
const httpsAgent = new https.Agent({
56+
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
57+
rejectUnauthorized: false,
58+
});
59+
60+
const response = await axios.get<PbbSchemaEntry[]>(url, {
61+
timeout: FETCH_TIMEOUT_IN_MS,
62+
headers: { Accept: 'application/json' },
63+
httpsAgent,
64+
});
65+
66+
const schemas = Array.isArray(response.data) ? response.data : [];
67+
this.activeTypeNames = schemas
68+
.filter(schema => schema.status === 'ACTIVE' && typeof schema.name === 'string' && schema.name.length > 0)
69+
.map(schema => schema.name as string);
70+
this.lastFetchedAt = Date.now();
71+
} catch (error) {
72+
logger.debug(error, 'pbb-schema-manager');
73+
}
74+
}
75+
}
76+
77+
export const pbbSchemaManager = new PbbSchemaManager();

0 commit comments

Comments
 (0)