Skip to content

Commit d512150

Browse files
committed
Add cni-component-manifest command
1 parent 287d36a commit d512150

12 files changed

Lines changed: 814 additions & 769 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env node
2+
require("../dist/generators/cniComponentManifest/cli.js").runMain(process);

packages/spectral/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"types": "dist/index.d.ts",
88
"homepage": "https://prismatic.io",
99
"bin": {
10-
"component-manifest": "./bin/component-manifest.js"
10+
"component-manifest": "./bin/component-manifest.js",
11+
"cni-component-manifest": "./bin/cni-component-manifest.js"
1112
},
1213
"bugs": {
1314
"url": "https://github.com/prismatic-io/spectral"
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { fetchComponentDataForManifest } from ".";
2+
import { createFlagHelpText } from "../utils/createFlagHelpText";
3+
import { getFlagsBooleanValue } from "../utils/getFlagBooleanValue";
4+
import { mkdirSync } from "node:fs";
5+
import { createActions } from "../componentManifest/createActions";
6+
import { createTriggers } from "../componentManifest/createTriggers";
7+
import { createConnections } from "../componentManifest/createConnections";
8+
import { createDataSources } from "../componentManifest/createDataSources";
9+
import path from "node:path";
10+
import { removeComponentManifest } from "../componentManifest/removeComponentManifest";
11+
import { createTemplate } from "../utils/createTemplate";
12+
13+
export const runMain = async (process: NodeJS.Process) => {
14+
const args = process.argv.slice(2);
15+
const componentKey = args[0];
16+
const cniDir = process.cwd();
17+
18+
const flags = {
19+
isPrivate: {
20+
flag: ["--private"],
21+
value: getFlagsBooleanValue({
22+
args,
23+
flags: ["--private"],
24+
}),
25+
description:
26+
"Denotes if the component is private. If not specified, the component is assumed to be public.",
27+
},
28+
dryRun: {
29+
flag: ["--dry-run", "-d"],
30+
value: getFlagsBooleanValue({
31+
args,
32+
flags: ["--dry-run", "-d"],
33+
}),
34+
description:
35+
"Perform a dry run without generating the component manifest. This provides a preview of what you could expect to happen when running the command without this flag.",
36+
},
37+
verbose: {
38+
flag: ["--verbose", "-v"],
39+
value: getFlagsBooleanValue({
40+
args,
41+
flags: ["--verbose", "-v"],
42+
}),
43+
description:
44+
"Provides more detailed or extensive information about the files being generated during the process.",
45+
},
46+
help: {
47+
flag: ["--help", "-h"],
48+
value: getFlagsBooleanValue({
49+
args,
50+
flags: ["--help", "-h"],
51+
}),
52+
description: "Show this help message.",
53+
},
54+
};
55+
56+
if (flags.help.value || !componentKey) {
57+
createFlagHelpText({
58+
command: "cni-component-manifest COMPONENT_KEY",
59+
flags,
60+
});
61+
if (!componentKey) {
62+
console.error("\nError: COMPONENT_KEY is required.");
63+
}
64+
process.exit(!componentKey ? 1 : 0);
65+
}
66+
67+
if (!cniDir) {
68+
console.error("Please run this script using npm or yarn.");
69+
process.exit(1);
70+
}
71+
72+
// Fetch component data from API
73+
const component = await fetchComponentDataForManifest({
74+
componentKey,
75+
isPrivate: flags.isPrivate.value || false,
76+
});
77+
78+
// Generate the manifest
79+
const destinationDir = path.join(process.cwd(), "src", "manifests", component.key);
80+
const templatesDir = path.join(__dirname, "..", "componentManifest", "templates");
81+
const dryRun = flags.dryRun.value;
82+
const verbose = flags.verbose.value;
83+
84+
if (verbose) {
85+
console.info(`Creating component manifest for ${component.display.label}...`);
86+
}
87+
88+
removeComponentManifest({ destinationDir, verbose });
89+
90+
// Create the directory structure
91+
mkdirSync(destinationDir, { recursive: true });
92+
93+
// Generate the source files
94+
await await createTemplate({
95+
source: path.join(templatesDir, "index.ts.ejs"),
96+
destination: path.join(destinationDir, "index.ts"),
97+
data: {
98+
component,
99+
},
100+
verbose,
101+
dryRun,
102+
});
103+
104+
await createActions({
105+
component,
106+
dryRun,
107+
verbose,
108+
sourceDir: templatesDir,
109+
destinationDir,
110+
});
111+
112+
await createTriggers({
113+
component,
114+
dryRun,
115+
verbose,
116+
sourceDir: templatesDir,
117+
destinationDir,
118+
});
119+
120+
await createConnections({
121+
component,
122+
dryRun,
123+
verbose,
124+
sourceDir: templatesDir,
125+
destinationDir,
126+
});
127+
128+
await createDataSources({
129+
component,
130+
dryRun,
131+
verbose: flags.verbose.value,
132+
sourceDir: templatesDir,
133+
destinationDir,
134+
});
135+
136+
console.info(
137+
`Component manifest created successfully for ${component.display.label} in ${destinationDir}!`,
138+
);
139+
};
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import axios from "axios";
2+
import { DataSourceType } from "../../types";
3+
import {
4+
ComponentNode,
5+
InputNode,
6+
FormattedAction,
7+
FormattedDataSource,
8+
FormattedTrigger,
9+
} from "./types";
10+
import { getPrismAccessToken } from "../utils/prism";
11+
12+
// Helper to transform input nodes from GraphQL response to the expected format
13+
function transformInputNodes(inputs: InputNode[]) {
14+
return inputs.map((node) => ({
15+
...node,
16+
collection: node.collection ? node.collection.toLowerCase() : undefined,
17+
type: node.type.toLowerCase(),
18+
}));
19+
}
20+
21+
// This function does not return a complete Component as described in ServerTypes;
22+
// it instead selectively returns only what's needed to generate a manifest.
23+
export const fetchComponentDataForManifest = async ({
24+
componentKey,
25+
isPrivate,
26+
}: { componentKey: string; isPrivate: boolean }) => {
27+
const accessToken = await getPrismAccessToken();
28+
const prismaticUrl = process.env.PRISMATIC_URL ?? "https://app.prismatic.io";
29+
30+
const query = `
31+
query componentQuery($componentKey: String!, $public: Boolean!) {
32+
components(key: $componentKey, public: $public) {
33+
nodes {
34+
id
35+
label
36+
description
37+
key
38+
actions {
39+
nodes {
40+
isDataSource
41+
isDetailDataSource
42+
dataSourceType
43+
isTrigger
44+
isCommonTrigger
45+
key
46+
label
47+
description
48+
inputs {
49+
nodes {
50+
key
51+
label
52+
type
53+
required
54+
default
55+
collection
56+
shown
57+
onPremiseControlled
58+
}
59+
}
60+
examplePayload
61+
}
62+
}
63+
connections {
64+
nodes {
65+
key
66+
label
67+
comments
68+
inputs {
69+
nodes {
70+
key
71+
label
72+
type
73+
required
74+
default
75+
collection
76+
shown
77+
onPremiseControlled
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
}
85+
`;
86+
87+
const response = await axios.post(
88+
`${prismaticUrl}/api`,
89+
{
90+
query,
91+
variables: {
92+
componentKey,
93+
public: !isPrivate,
94+
},
95+
},
96+
{
97+
headers: {
98+
Authorization: `Bearer ${accessToken}`,
99+
"Content-Type": "application/json",
100+
// @TODO: Is it OK to list spectral here? As opposed to prism
101+
"Prismatic-Client": "spectral",
102+
},
103+
},
104+
);
105+
106+
const component: ComponentNode = response.data.data.components.nodes[0];
107+
108+
if (!component) {
109+
throw new Error(
110+
`Could not find a ${
111+
isPrivate ? "private" : "public"
112+
} component with the given key: ${componentKey}`,
113+
);
114+
}
115+
116+
const actions: Record<string, FormattedAction> = {};
117+
const triggers: Record<string, FormattedTrigger> = {};
118+
const dataSources: Record<string, FormattedDataSource> = {};
119+
120+
component.actions.nodes.forEach((node) => {
121+
if (node.isTrigger) {
122+
triggers[node.key] = {
123+
key: node.key,
124+
display: {
125+
label: node.label,
126+
description: node.description,
127+
},
128+
inputs: transformInputNodes(node.inputs.nodes),
129+
};
130+
} else if (node.isDataSource) {
131+
dataSources[node.key] = {
132+
key: node.key,
133+
display: {
134+
label: node.label,
135+
description: node.description,
136+
},
137+
inputs: transformInputNodes(node.inputs.nodes),
138+
dataSourceType: (node.dataSourceType ?? "string").toLowerCase() as DataSourceType,
139+
examplePayload: node.examplePayload ?? {},
140+
};
141+
} else {
142+
actions[node.key] = {
143+
key: node.key,
144+
display: {
145+
label: node.label,
146+
description: node.description,
147+
},
148+
inputs: transformInputNodes(node.inputs.nodes),
149+
};
150+
}
151+
});
152+
153+
const connections = component.connections.nodes.map((node) => {
154+
return {
155+
key: node.key,
156+
label: node.label,
157+
comments: node.comments,
158+
inputs: transformInputNodes(node.inputs.nodes),
159+
};
160+
});
161+
162+
return {
163+
key: componentKey,
164+
public: !isPrivate,
165+
display: {
166+
label: component.label,
167+
description: component.description,
168+
},
169+
actions,
170+
triggers,
171+
dataSources,
172+
connections,
173+
};
174+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Action, Component, DataSource, Trigger } from "../../serverTypes";
2+
3+
export interface ComponentNode {
4+
id: string;
5+
label: string;
6+
description: string;
7+
key: string;
8+
actions: {
9+
nodes: ActionNode[];
10+
};
11+
connections: {
12+
nodes: ConnectionNode[];
13+
};
14+
}
15+
16+
export interface ActionNode {
17+
isDataSource: boolean;
18+
isDetailDataSource: boolean;
19+
dataSourceType: string | null;
20+
isTrigger: boolean;
21+
isCommonTrigger: boolean;
22+
key: string;
23+
label: string;
24+
description: string;
25+
inputs: {
26+
nodes: InputNode[];
27+
};
28+
examplePayload: string | null;
29+
}
30+
31+
export interface ConnectionNode {
32+
key: string;
33+
label: string;
34+
comments: string;
35+
inputs: {
36+
nodes: InputNode[];
37+
};
38+
}
39+
40+
export interface InputNode {
41+
key: string;
42+
label: string;
43+
type: string;
44+
required: boolean;
45+
default: any;
46+
collection: string;
47+
shown: boolean;
48+
onPremiseControlled: boolean;
49+
}
50+
51+
export type FormattedAction = Pick<Action, "key" | "display" | "inputs">;
52+
export type FormattedTrigger = Pick<Trigger, "key" | "display" | "inputs">;
53+
export type FormattedDataSource = Pick<
54+
DataSource,
55+
"key" | "display" | "inputs" | "dataSourceType" | "examplePayload"
56+
>;
57+
58+
export type ComponentForManifest = Pick<Component, "key" | "public" | "display" | "connections"> & {
59+
actions: Record<string, Action | FormattedAction>;
60+
triggers: Record<string, Trigger | FormattedTrigger>;
61+
dataSources: Record<string, DataSource | FormattedDataSource>;
62+
};

0 commit comments

Comments
 (0)