@hubspot/project-parsing-lib 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright 2019 HubSpot, Inc.
2
+ Licensed under the Apache License, Version 2.0 (the "License");
3
+ you may not use this file except in compliance with the License.
4
+ You may obtain a copy of the License at
5
+
6
+ http://www.apache.org/licenses/LICENSE-2.0
7
+
8
+ Unless required by applicable law or agreed to in writing, software
9
+ distributed under the License is distributed on an "AS IS" BASIS,
10
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ See the License for the specific language governing permissions and
12
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # project-translation-layer
2
+
3
+ ## Description
4
+ This project is a translation layer that can be used to translate a project to its immediate representation prior to being uploaded
5
+ to the project v2 service.
6
+
7
+ ## Flow Chart
8
+
9
+ ```mermaid
10
+ sequenceDiagram
11
+ autonumber
12
+
13
+ actor dev as External Developer
14
+ participant cli as HS CLI
15
+ participant ppl as Project Parsing Library
16
+ participant pv3 as Projects v3
17
+
18
+ dev ->> cli: Developer runs `hs project upload`
19
+ cli ->> cli: Loads project config
20
+ cli ->> ppl: CLI calls `translate` function with the values required from the project config
21
+ ppl ->> ppl: Walks the project directory looking for the hsmeta files
22
+ loop For each hsmeta file
23
+ ppl ->> ppl: Checks if file is valid JSON
24
+ ppl ->> ppl: Checks if the file is in a valid location
25
+ ppl ->> ppl: Generates the IR
26
+ end
27
+ ppl ->> pv3: Fetch the schemas used to validate the generated IR
28
+ loop For each hsmeta file
29
+ ppl ->> ppl: Validate the IR config block against the schema
30
+ end
31
+ alt Validation successful for all schemas
32
+ ppl ->> cli: Pass the IR back to the CLI
33
+ cli ->> cli: Zip the project contents
34
+ cli ->> pv3: Upload the project and the IR
35
+ else Validation failed for 1+ schema(s)
36
+ ppl ->> cli: Log the error and exit the upload
37
+ end
38
+ ```
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@hubspot/project-parsing-lib",
3
+ "version": "0.0.1",
4
+ "description": "Parsing library for converting projects directory structures to their intermediate representation",
5
+ "license": "Apache-2.0",
6
+ "main": "src/index.js",
7
+ "devDependencies": {
8
+ "@inquirer/prompts": "^7.1.0",
9
+ "@types/jest": "^29.5.14",
10
+ "@types/semver": "^7.5.8",
11
+ "@typescript-eslint/eslint-plugin": "^8.11.0",
12
+ "@typescript-eslint/parser": "^8.11.0",
13
+ "eslint": "^8.56.0",
14
+ "eslint-plugin-import": "^2.31.0",
15
+ "husky": "^9.1.7",
16
+ "jest": "^29.5.0",
17
+ "lint-staged": "^10.5.4",
18
+ "prettier": "^1.19.1",
19
+ "semver": "^7.6.3",
20
+ "ts-jest": "^29.2.5",
21
+ "ts-node": "^10.9.2",
22
+ "typescript": "^5.6.2"
23
+ },
24
+ "dependencies": {
25
+ "@hubspot/local-dev-lib": "^3.1.0",
26
+ "ajv": "^8.17.1",
27
+ "ajv-draft-04": "^1.0.0"
28
+ },
29
+ "scripts": {
30
+ "build": "ts-node ./scripts/build.ts",
31
+ "lint": "eslint --max-warnings=0 . && prettier ./src/** --check",
32
+ "local-dev": "yarn build && yarn link --cwd=./dist && tsc --watch --rootDir . --outdir dist",
33
+ "prettier:write": "prettier ./src/** --write",
34
+ "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ./node_modules/.bin/jest",
35
+ "release": "yarn ts-node ./scripts/release.ts release"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ }
40
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { IntermediateRepresentation, TranslationContext } from './lib/types';
2
+ export declare function translate(translationContext: TranslationContext): Promise<IntermediateRepresentation>;
3
+ export { isTranslationError } from './lib/errors';
package/src/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isTranslationError = void 0;
4
+ exports.translate = translate;
5
+ const files_1 = require("./lib/files");
6
+ const validation_1 = require("./lib/validation");
7
+ const transform_1 = require("./lib/transform");
8
+ const errors_1 = require("./lib/errors");
9
+ async function translate(translationContext) {
10
+ const metafileContents = await (0, files_1.loadHsMetaFiles)(translationContext);
11
+ if (metafileContents.length === 0) {
12
+ throw new Error('No *-hsmeta.json files found, make sure you are inside a project');
13
+ }
14
+ const transformation = (0, transform_1.transform)(metafileContents);
15
+ const intermediateRepresentation = (0, transform_1.getIntermediateRepresentation)(transformation);
16
+ const { valid, errors } = await (0, validation_1.validateIntermediateRepresentation)(intermediateRepresentation, transformation, translationContext);
17
+ if (!valid) {
18
+ throw new errors_1.TranslationError('Failed to translate project', errors);
19
+ }
20
+ return intermediateRepresentation;
21
+ }
22
+ var errors_2 = require("./lib/errors");
23
+ Object.defineProperty(exports, "isTranslationError", { enumerable: true, get: function () { return errors_2.isTranslationError; } });
@@ -0,0 +1,27 @@
1
+ export declare const AppKey = "app";
2
+ export declare const ThemeKey = "theme";
3
+ export declare const CallingKey = "calling";
4
+ export declare const CardsKey = "card";
5
+ export declare const FunctionsKey = "function";
6
+ export declare const MarketingEventsKey = "marketing-event";
7
+ export declare const MediaBridgeKey = "media-bridge";
8
+ export declare const TimelineEventsKey = "timeline-event";
9
+ export declare const VideoConferencingKey = "video-conferencing";
10
+ export declare const WebhooksKey = "webhooks";
11
+ export declare const WorkflowActionsKey = "workflow-action";
12
+ interface ComponentMetadata {
13
+ dir: string;
14
+ isToplevel?: boolean;
15
+ parentComponent?: string;
16
+ }
17
+ export declare const Components: Record<string, ComponentMetadata>;
18
+ export declare const metafileExtension = "-hsmeta.json";
19
+ export declare const allowedAppSubComponentsDirs: string[];
20
+ export declare const allowedThemeSubComponentsDirs: string[];
21
+ export declare const ProjectStructure: {
22
+ readonly app: string[];
23
+ readonly theme: string[];
24
+ };
25
+ export declare const allowedComponentDirectories: string[];
26
+ export declare const allowedSubComponentDirectories: string[];
27
+ export {};
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.allowedSubComponentDirectories = exports.allowedComponentDirectories = exports.ProjectStructure = exports.allowedThemeSubComponentsDirs = exports.allowedAppSubComponentsDirs = exports.metafileExtension = exports.Components = exports.WorkflowActionsKey = exports.WebhooksKey = exports.VideoConferencingKey = exports.TimelineEventsKey = exports.MediaBridgeKey = exports.MarketingEventsKey = exports.FunctionsKey = exports.CardsKey = exports.CallingKey = exports.ThemeKey = exports.AppKey = void 0;
27
+ // Top Level Component types
28
+ const path = __importStar(require("path"));
29
+ // Component types
30
+ exports.AppKey = 'app';
31
+ exports.ThemeKey = 'theme';
32
+ // Sub-Component types
33
+ exports.CallingKey = 'calling';
34
+ exports.CardsKey = 'card';
35
+ exports.FunctionsKey = 'function';
36
+ exports.MarketingEventsKey = 'marketing-event';
37
+ exports.MediaBridgeKey = 'media-bridge';
38
+ exports.TimelineEventsKey = 'timeline-event';
39
+ exports.VideoConferencingKey = 'video-conferencing';
40
+ exports.WebhooksKey = 'webhooks';
41
+ exports.WorkflowActionsKey = 'workflow-action';
42
+ exports.Components = {
43
+ [exports.AppKey]: {
44
+ dir: exports.AppKey,
45
+ isToplevel: true,
46
+ },
47
+ [exports.ThemeKey]: {
48
+ dir: exports.ThemeKey,
49
+ isToplevel: true,
50
+ },
51
+ [exports.CallingKey]: {
52
+ dir: exports.CallingKey,
53
+ parentComponent: exports.AppKey,
54
+ },
55
+ [exports.CardsKey]: {
56
+ dir: 'cards',
57
+ parentComponent: exports.AppKey,
58
+ },
59
+ [exports.FunctionsKey]: {
60
+ dir: 'functions',
61
+ parentComponent: exports.AppKey,
62
+ },
63
+ [exports.MarketingEventsKey]: {
64
+ dir: 'marketing-events',
65
+ parentComponent: exports.AppKey,
66
+ },
67
+ MediaBridgeKey: {
68
+ dir: exports.MediaBridgeKey,
69
+ parentComponent: exports.AppKey,
70
+ },
71
+ [exports.TimelineEventsKey]: {
72
+ dir: 'timeline-events',
73
+ parentComponent: exports.AppKey,
74
+ },
75
+ [exports.VideoConferencingKey]: {
76
+ dir: exports.VideoConferencingKey,
77
+ parentComponent: exports.AppKey,
78
+ },
79
+ [exports.WebhooksKey]: {
80
+ dir: exports.WebhooksKey,
81
+ parentComponent: exports.AppKey,
82
+ },
83
+ [exports.WorkflowActionsKey]: {
84
+ dir: 'workflow-actions',
85
+ parentComponent: exports.AppKey,
86
+ },
87
+ };
88
+ exports.metafileExtension = '-hsmeta.json';
89
+ function getSubComponentDirsForParentType(parentComponent) {
90
+ return Object.values(exports.Components).reduce((acc, item) => {
91
+ if (item.parentComponent === parentComponent) {
92
+ acc.push(item.dir);
93
+ }
94
+ return acc;
95
+ }, []);
96
+ }
97
+ exports.allowedAppSubComponentsDirs = getSubComponentDirsForParentType(exports.AppKey);
98
+ exports.allowedThemeSubComponentsDirs = getSubComponentDirsForParentType(exports.ThemeKey);
99
+ exports.ProjectStructure = {
100
+ [exports.AppKey]: exports.allowedAppSubComponentsDirs,
101
+ [exports.ThemeKey]: exports.allowedThemeSubComponentsDirs,
102
+ };
103
+ exports.allowedComponentDirectories = Object.keys(exports.ProjectStructure);
104
+ exports.allowedSubComponentDirectories = Object.entries(exports.ProjectStructure)
105
+ .map(([key, value]) => value.map((value) => path.join(key, value)))
106
+ .flat();
@@ -0,0 +1,8 @@
1
+ import { CompiledError, Transformation } from './types';
2
+ export declare class TranslationError extends Error {
3
+ private errors;
4
+ constructor(message: string, errors: CompiledError[]);
5
+ toString(): string;
6
+ }
7
+ export declare function isTranslationError(error: unknown): error is TranslationError;
8
+ export declare function compileError(validatedTransformation: Transformation): CompiledError;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TranslationError = void 0;
4
+ exports.isTranslationError = isTranslationError;
5
+ exports.compileError = compileError;
6
+ class TranslationError extends Error {
7
+ errors;
8
+ constructor(message, errors) {
9
+ super(message);
10
+ this.name = 'TranslationError';
11
+ this.errors = errors;
12
+ }
13
+ toString() {
14
+ return `${this.message}: ${this.errors.map(({ message, errors }) => `\n\n${message} \n\t- ${errors.join('\n\t- ')}`)}`;
15
+ }
16
+ }
17
+ exports.TranslationError = TranslationError;
18
+ function isTranslationError(error) {
19
+ return error instanceof TranslationError;
20
+ }
21
+ function compileError(validatedTransformation) {
22
+ const { fileParseResult } = validatedTransformation;
23
+ const { errors, file: filePath } = fileParseResult;
24
+ return {
25
+ message: `Encountered the following errors for ${filePath}:`,
26
+ errors,
27
+ };
28
+ }
@@ -0,0 +1,2 @@
1
+ import { FileParseResult, TranslationContext } from './types';
2
+ export declare function loadHsMetaFiles(translationContext: TranslationContext): Promise<FileParseResult[]>;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadHsMetaFiles = loadHsMetaFiles;
7
+ const fs_1 = require("@hubspot/local-dev-lib/fs");
8
+ const path_1 = __importDefault(require("path"));
9
+ const constants_1 = require("./constants");
10
+ const fs_2 = __importDefault(require("fs"));
11
+ async function loadHsMetaFiles(translationContext) {
12
+ const metaFiles = await locateHsMetaFiles(translationContext);
13
+ return parseHsMetaFiles(metaFiles, translationContext);
14
+ }
15
+ async function locateHsMetaFiles(translationContext) {
16
+ const { projectSourceDir } = translationContext;
17
+ return (await (0, fs_1.walk)(projectSourceDir, ['node_modules'])).reduce((metaFiles, file) => {
18
+ // Ignore the node_modules directory
19
+ if (file.includes('node_modules')) {
20
+ return metaFiles;
21
+ }
22
+ const pathRelativeToProjectSrcDir = path_1.default.relative(projectSourceDir, file);
23
+ if (!pathRelativeToProjectSrcDir.endsWith(constants_1.metafileExtension)) {
24
+ return metaFiles;
25
+ }
26
+ const { dir: metaFileDir } = path_1.default.parse(pathRelativeToProjectSrcDir);
27
+ const parent = metaFileDir.split(path_1.default.sep)[0];
28
+ if (constants_1.allowedComponentDirectories.includes(metaFileDir)) {
29
+ metaFiles.push({ file });
30
+ }
31
+ else if (constants_1.allowedSubComponentDirectories.includes(metaFileDir)) {
32
+ metaFiles.push({ file, parent });
33
+ }
34
+ else {
35
+ console.warn(`Skipping ${pathRelativeToProjectSrcDir} as it is not in a valid directory`);
36
+ }
37
+ return metaFiles;
38
+ }, []);
39
+ }
40
+ async function parseHsMetaFiles(metaFileLocations, translationContext) {
41
+ const fileLoadResults = await Promise.all(metaFileLocations.map(fileLocation => {
42
+ return loadFile(fileLocation, translationContext);
43
+ }));
44
+ return fileLoadResults.map(result => parseFile(result));
45
+ }
46
+ function loadFile(metaFileLocation, translationContext) {
47
+ const { projectSourceDir } = translationContext;
48
+ return new Promise(async (resolve) => {
49
+ const { file, parent } = metaFileLocation;
50
+ fs_2.default.readFile(file, 'utf-8', (err, content) => {
51
+ if (err) {
52
+ return resolve({
53
+ file,
54
+ errors: [err.message],
55
+ });
56
+ }
57
+ resolve({
58
+ file: path_1.default.relative(projectSourceDir, file),
59
+ content,
60
+ parent,
61
+ errors: [],
62
+ });
63
+ });
64
+ });
65
+ }
66
+ function parseFile(fileLoadResult) {
67
+ if (!fileLoadResult.content) {
68
+ // This is just to please TS, revisit this hack
69
+ return { ...fileLoadResult, content: undefined };
70
+ }
71
+ let parsedFileContents;
72
+ try {
73
+ parsedFileContents = JSON.parse(fileLoadResult.content);
74
+ }
75
+ catch (_e) {
76
+ fileLoadResult.errors?.push('Invalid JSON');
77
+ }
78
+ return { ...fileLoadResult, content: parsedFileContents };
79
+ }
@@ -0,0 +1,3 @@
1
+ import { AnySchema } from 'ajv';
2
+ import { TranslationContext } from './types';
3
+ export declare function getIntermediateRepresentationSchema(translationContext: TranslationContext): Promise<Record<string, AnySchema>>;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getIntermediateRepresentationSchema = getIntermediateRepresentationSchema;
4
+ const http_1 = require("@hubspot/local-dev-lib/http");
5
+ async function getIntermediateRepresentationSchema(translationContext) {
6
+ // This is just a placeholder that resolves the hardcoded object until we
7
+ // implement the actual logic to load the schemas from the backend
8
+ try {
9
+ const { accountId } = translationContext;
10
+ const { data } = await http_1.http.get(accountId, {
11
+ // TODO: update this to use the real platform version
12
+ url: `project-components-external/project-schemas/v3?platformVersion=testing`,
13
+ });
14
+ return data;
15
+ }
16
+ catch (e) {
17
+ throw new Error('Failed to fetch schemas', { cause: e });
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ import { FileParseResult, IntermediateRepresentation, Transformation } from './types';
2
+ export declare function transform(fileParseResults: FileParseResult[]): Transformation[];
3
+ export declare function getIntermediateRepresentation(transformations: Transformation[]): IntermediateRepresentation;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.transform = transform;
4
+ exports.getIntermediateRepresentation = getIntermediateRepresentation;
5
+ const constants_1 = require("./constants");
6
+ function calculateComponentDeps(fileValidationResult, parentComponents) {
7
+ return {
8
+ ...fileValidationResult.content?.dependencies,
9
+ // If the file has a parent, add it as a dependency
10
+ ...(fileValidationResult.parent
11
+ ? { upstreamBuildArtifact: parentComponents[fileValidationResult.parent] }
12
+ : {}),
13
+ };
14
+ }
15
+ function transform(fileParseResults) {
16
+ const parentTypes = Object.keys(constants_1.ProjectStructure);
17
+ // Compute the parent components, so we can add them as dependencies to the child components
18
+ const parentComponents = fileParseResults.reduce((acc, file) => {
19
+ if (file.content?.type && parentTypes.includes(file.content.type)) {
20
+ acc[file.content.type] = file.content.uid;
21
+ }
22
+ return acc;
23
+ }, {});
24
+ return fileParseResults.map((currentFile) => {
25
+ if (!currentFile.content) {
26
+ currentFile.errors?.push(`File content is missing for ${currentFile.file}`);
27
+ return { intermediateRepresentation: null, fileParseResult: currentFile };
28
+ }
29
+ const { config, uid } = currentFile.content;
30
+ return {
31
+ intermediateRepresentation: {
32
+ uid,
33
+ config,
34
+ componentType: currentFile.content?.type.toUpperCase() || '',
35
+ componentDeps: calculateComponentDeps(currentFile, parentComponents),
36
+ files: {},
37
+ },
38
+ fileParseResult: currentFile,
39
+ };
40
+ });
41
+ }
42
+ function getIntermediateRepresentation(transformations) {
43
+ const nodes = transformations.reduce((acc, current) => {
44
+ return {
45
+ ...acc,
46
+ [current.intermediateRepresentation
47
+ .uid]: current.intermediateRepresentation,
48
+ };
49
+ }, {});
50
+ return {
51
+ intermediateNodesIndexedByUid: nodes,
52
+ };
53
+ }
@@ -0,0 +1,42 @@
1
+ export interface Components {
2
+ uid: string;
3
+ type: string;
4
+ config: unknown;
5
+ dependencies?: Record<string, unknown>;
6
+ }
7
+ export interface FileActionResult {
8
+ file: string;
9
+ parent?: string;
10
+ errors: string[];
11
+ }
12
+ export interface FileLoadResult extends FileActionResult {
13
+ content?: string;
14
+ }
15
+ export interface FileParseResult extends FileActionResult {
16
+ content?: Components;
17
+ }
18
+ export interface IntermediateRepresentationNode {
19
+ componentType: string;
20
+ componentDeps: Record<string, string>;
21
+ uid: string;
22
+ config: unknown;
23
+ files: unknown;
24
+ }
25
+ export interface IntermediateRepresentation {
26
+ intermediateNodesIndexedByUid: {
27
+ [key: string]: IntermediateRepresentationNode;
28
+ };
29
+ }
30
+ export type Transformation = {
31
+ intermediateRepresentation?: IntermediateRepresentationNode | null;
32
+ fileParseResult: FileParseResult;
33
+ };
34
+ export type CompiledError = {
35
+ message: string;
36
+ errors: string[];
37
+ };
38
+ export interface TranslationContext {
39
+ projectSourceDir: string;
40
+ platformVersion: string;
41
+ accountId: number;
42
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
1
+ import { CompiledError, IntermediateRepresentation, Transformation, TranslationContext } from './types';
2
+ export type ValidationResults = {
3
+ valid: false;
4
+ errors: CompiledError[];
5
+ } | {
6
+ valid: true;
7
+ errors?: null;
8
+ };
9
+ export declare function validateIntermediateRepresentation(intermediateRepresentation: IntermediateRepresentation, transformation: Transformation[], translationContext: TranslationContext): Promise<ValidationResults>;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.validateIntermediateRepresentation = validateIntermediateRepresentation;
7
+ const schemas_1 = require("./schemas");
8
+ const ajv_draft_04_1 = __importDefault(require("ajv-draft-04"));
9
+ const errors_1 = require("./errors");
10
+ async function validateIntermediateRepresentation(intermediateRepresentation, transformation, translationContext) {
11
+ const schema = await (0, schemas_1.getIntermediateRepresentationSchema)(translationContext);
12
+ const results = Object.values(intermediateRepresentation.intermediateNodesIndexedByUid).map((irNode, index) => {
13
+ const ajv = new ajv_draft_04_1.default({ allErrors: true });
14
+ if (!irNode.uid) {
15
+ transformation[index].fileParseResult.errors.push(`Missing required field: 'uid'`);
16
+ }
17
+ if (!schema[irNode.componentType]) {
18
+ transformation[index].fileParseResult.errors.push(`Unsupported 'type': '${irNode.componentType.toLowerCase()}'`);
19
+ return {
20
+ valid: false,
21
+ errors: (0, errors_1.compileError)(transformation[index]),
22
+ };
23
+ }
24
+ const validate = ajv.compile(schema[irNode.componentType]);
25
+ const valid = validate(irNode.config);
26
+ if (valid) {
27
+ const { errors } = transformation[index].fileParseResult;
28
+ return errors.length === 0
29
+ ? {
30
+ valid: true,
31
+ }
32
+ : {
33
+ valid: false,
34
+ errors: (0, errors_1.compileError)(transformation[index]),
35
+ };
36
+ }
37
+ const transformationWithUpdatedErrors = hydrateValidationErrorsIntoTransformation(transformation[index], validate.errors);
38
+ return {
39
+ valid: false,
40
+ errors: (0, errors_1.compileError)(transformationWithUpdatedErrors),
41
+ };
42
+ });
43
+ const valid = results.every(result => result.valid);
44
+ if (valid) {
45
+ return {
46
+ valid,
47
+ };
48
+ }
49
+ const errors = results
50
+ .filter(item => item.errors !== undefined)
51
+ .map(result => result.errors);
52
+ return {
53
+ valid,
54
+ errors,
55
+ };
56
+ }
57
+ function hydrateValidationErrorsIntoTransformation(transformation, errors) {
58
+ if (!errors) {
59
+ return transformation;
60
+ }
61
+ errors.forEach(error => {
62
+ const { intermediateRepresentation, fileParseResult } = transformation;
63
+ fileParseResult.errors.push(generateErrorMessage(transformInstancePath(error.instancePath), error));
64
+ transformation = { intermediateRepresentation, fileParseResult };
65
+ });
66
+ return transformation;
67
+ }
68
+ function transformInstancePath(instancePath) {
69
+ return instancePath.replace(/\/nodes\//, '').split('/');
70
+ }
71
+ const missingRequiredFieldRegex = /must have required property '(.+)'/;
72
+ // TODO: This needs cleanup and better error messaging
73
+ function generateErrorMessage(path, error) {
74
+ let errorMessage = '';
75
+ const dotNotationPath = path.length === 0 || path[0] === '' ? 'Root Level' : path.join('.');
76
+ errorMessage = `Error with ${dotNotationPath}: ${error.message}`;
77
+ const params = Object.entries(error.params);
78
+ if (params.length > 0) {
79
+ const additionalContext = params
80
+ .filter(([_, value]) => value)
81
+ .map(([key, value]) => `${key}: ${value}`);
82
+ if (error.message &&
83
+ missingRequiredFieldRegex.test(error.message) &&
84
+ error.params) {
85
+ // TODO: This error message doesn't work well for nested objects, it makes it seem like the field is missing from the root object
86
+ return `Missing required field: '${error.params.missingProperty}'`;
87
+ }
88
+ errorMessage += `${additionalContext.length > 0
89
+ ? `\n\t\t- ${additionalContext.join('\n\t\t- ')}`
90
+ : ''}`;
91
+ }
92
+ return errorMessage;
93
+ }