@trayio/cdk-cli 5.13.0 → 5.14.0

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/README.md CHANGED
@@ -19,7 +19,7 @@ $ npm install -g @trayio/cdk-cli
19
19
  $ tray-cdk COMMAND
20
20
  running command...
21
21
  $ tray-cdk (--version|-v)
22
- @trayio/cdk-cli/5.13.0 linux-x64 node-v18.20.8
22
+ @trayio/cdk-cli/5.14.0 linux-x64 node-v18.20.8
23
23
  $ tray-cdk --help [COMMAND]
24
24
  USAGE
25
25
  $ tray-cdk COMMAND
@@ -32,6 +32,7 @@ USAGE
32
32
  <!-- commands -->
33
33
  * [`tray-cdk .`](#tray-cdk-)
34
34
  * [`tray-cdk autocomplete [SHELL]`](#tray-cdk-autocomplete-shell)
35
+ * [`tray-cdk connector add-dynamic-output-schema OPERATIONNAME`](#tray-cdk-connector-add-dynamic-output-schema-operationname)
35
36
  * [`tray-cdk connector add-operation [OPERATIONNAME] [OPERATIONTYPE]`](#tray-cdk-connector-add-operation-operationname-operationtype)
36
37
  * [`tray-cdk connector build`](#tray-cdk-connector-build)
37
38
  * [`tray-cdk connector import [OPENAPISPEC] [CONNECTORNAME]`](#tray-cdk-connector-import-openapispec-connectorname)
@@ -96,6 +97,26 @@ EXAMPLES
96
97
 
97
98
  _See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.0.5/src/commands/autocomplete/index.ts)_
98
99
 
100
+ ## `tray-cdk connector add-dynamic-output-schema OPERATIONNAME`
101
+
102
+ Add a dynamic output schema operation for an existing operation
103
+
104
+ ```
105
+ USAGE
106
+ $ tray-cdk connector add-dynamic-output-schema OPERATIONNAME
107
+
108
+ ARGUMENTS
109
+ OPERATIONNAME Name of the parent operation to add dynamic output schema for
110
+
111
+ DESCRIPTION
112
+ Add a dynamic output schema operation for an existing operation
113
+
114
+ EXAMPLES
115
+ $ tray-cdk connector add-dynamic-output-schema get_posts
116
+
117
+ $ tray-cdk connector add-dynamic-output-schema fetch_user_data
118
+ ```
119
+
99
120
  ## `tray-cdk connector add-operation [OPERATIONNAME] [OPERATIONTYPE]`
100
121
 
101
122
  Add an operation to connector project
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AddDynamicOutputSchema extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ operationName: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ private generator;
9
+ run(): Promise<void>;
10
+ private validateParentOperation;
11
+ private detectAuthConfig;
12
+ private copyInputFromParent;
13
+ private updateParentOperationJson;
14
+ private findSrcDirectory;
15
+ }
16
+ //# sourceMappingURL=add-dynamic-output-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add-dynamic-output-schema.d.ts","sourceRoot":"","sources":["../../../src/commands/connector/add-dynamic-output-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAQ,MAAM,aAAa,CAAC;AAW5C,MAAM,CAAC,OAAO,OAAO,sBAAuB,SAAQ,OAAO;IAC1D,MAAM,CAAC,WAAW,SACiD;IAEnE,MAAM,CAAC,QAAQ,WAGb;IAEF,MAAM,CAAC,IAAI;;MAOT;IAEF,OAAO,CAAC,SAAS,CAAyB;IAEpC,GAAG;IAsGT,OAAO,CAAC,uBAAuB;YAuBjB,gBAAgB;YAehB,mBAAmB;YA8BnB,yBAAyB;IAcvC,OAAO,CAAC,gBAAgB;CAOxB"}
@@ -0,0 +1,163 @@
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
+ const core_1 = require("@oclif/core");
27
+ const fse = __importStar(require("fs-extra"));
28
+ const path = __importStar(require("path"));
29
+ const StringExtensions_1 = require("@trayio/commons/string/StringExtensions");
30
+ const colorizeString_1 = require("@trayio/cdk-cli-commons/utils/colorizeString");
31
+ const NodeFsGenerator_1 = require("@trayio/generator/generator/NodeFsGenerator");
32
+ const auth_detector_1 = require("../../lib/dynamic-output/auth-detector");
33
+ class AddDynamicOutputSchema extends core_1.Command {
34
+ static description = 'Add a dynamic output schema operation for an existing operation';
35
+ static examples = [
36
+ '<%= config.bin %> <%= command.id %> get_posts',
37
+ '<%= config.bin %> <%= command.id %> fetch_user_data',
38
+ ];
39
+ static args = {
40
+ operationName: core_1.Args.string({
41
+ name: 'operationName',
42
+ required: true,
43
+ description: 'Name of the parent operation to add dynamic output schema for',
44
+ }),
45
+ };
46
+ generator = new NodeFsGenerator_1.NodeFsGenerator();
47
+ async run() {
48
+ const { args } = await this.parse(AddDynamicOutputSchema);
49
+ const { operationName } = args;
50
+ const currentDirectory = process.cwd();
51
+ const srcDir = this.findSrcDirectory(currentDirectory);
52
+ // Validate parent operation exists
53
+ this.validateParentOperation(srcDir, operationName);
54
+ const parentOperationPath = path.join(srcDir, operationName);
55
+ const parentOperationJsonPath = path.join(parentOperationPath, 'operation.json');
56
+ this.log(`Checking for operation '${operationName}'... ✓`);
57
+ // Check if dynamic output schema operation already exists
58
+ const outputSchemaOpName = `${operationName}_output_schema`;
59
+ const outputSchemaOpPath = path.join(srcDir, outputSchemaOpName);
60
+ if (fse.existsSync(outputSchemaOpPath)) {
61
+ this.error((0, colorizeString_1.error)(`Dynamic output schema operation '${outputSchemaOpName}' already exists!`));
62
+ }
63
+ // Read parent operation.json
64
+ const parentOperationJson = await fse.readJson(parentOperationJsonPath);
65
+ // Detect auth configuration
66
+ const authInfo = await this.detectAuthConfig(srcDir);
67
+ this.log(`Creating dynamic output schema operation '${outputSchemaOpName}'...`);
68
+ // Build template values for generator
69
+ const templateValues = {
70
+ operationNameSnakeCase: operationName,
71
+ operationNameTitleCase: StringExtensions_1.StringExtensions.titleCase(operationName),
72
+ operationNamePascalCase: StringExtensions_1.StringExtensions.pascalCase(operationName),
73
+ operationNameCamelCase: StringExtensions_1.StringExtensions.camelCase(operationName),
74
+ outputSchemaOpNameSnakeCase: outputSchemaOpName,
75
+ outputSchemaOpNamePascalCase: StringExtensions_1.StringExtensions.pascalCase(outputSchemaOpName),
76
+ outputSchemaOpNameCamelCase: StringExtensions_1.StringExtensions.camelCase(outputSchemaOpName),
77
+ parentOpNameSnakeCase: operationName,
78
+ parentOpNamePascalCase: StringExtensions_1.StringExtensions.pascalCase(operationName),
79
+ parentOpNameCamelCase: StringExtensions_1.StringExtensions.camelCase(operationName),
80
+ authType: authInfo.authType,
81
+ authFileName: authInfo.authFileName,
82
+ };
83
+ // Generate files from template
84
+ const rootDir = __dirname;
85
+ const templateDir = path.join(rootDir, '..', '..', 'templates');
86
+ const templatePath = path.join(templateDir, 'dynamic-output-schema-template.zip');
87
+ await this.generator.generate(templatePath, srcDir, templateValues)();
88
+ this.log(' ✓ Generated operation files from template');
89
+ // Ensure the output schema operation directory exists
90
+ await fse.ensureDir(outputSchemaOpPath);
91
+ // Manually copy input.ts from parent operation (can't be templated)
92
+ await this.copyInputFromParent(parentOperationPath, outputSchemaOpPath, StringExtensions_1.StringExtensions.pascalCase(operationName), StringExtensions_1.StringExtensions.pascalCase(outputSchemaOpName));
93
+ // Update parent operation.json with dynamic_output: true
94
+ await this.updateParentOperationJson(parentOperationJsonPath, parentOperationJson);
95
+ // Success message with next steps
96
+ this.log('');
97
+ this.log((0, colorizeString_1.success)(`Success! Created dynamic output schema operation: ${outputSchemaOpName}`));
98
+ this.log('');
99
+ this.log('Next steps:');
100
+ this.log(` 1. Edit src/${outputSchemaOpName}/handler.ts`);
101
+ this.log(' 2. Customize the data fetching logic for your use case');
102
+ this.log(' 3. Run "tray-cdk connector build" to build the connector');
103
+ this.log('');
104
+ this.log('Note: Dynamic output is only safe for read-only GET operations!');
105
+ }
106
+ validateParentOperation(srcDir, operationName) {
107
+ const parentOperationPath = path.join(srcDir, operationName);
108
+ if (!fse.existsSync(parentOperationPath)) {
109
+ this.error((0, colorizeString_1.error)(`Operation '${operationName}' not found in src/ directory.\nMake sure you're in the connector root or src/ directory.`));
110
+ }
111
+ const parentOperationJsonPath = path.join(parentOperationPath, 'operation.json');
112
+ if (!fse.existsSync(parentOperationJsonPath)) {
113
+ this.error((0, colorizeString_1.error)(`Operation '${operationName}' exists but missing operation.json file.`));
114
+ }
115
+ }
116
+ async detectAuthConfig(srcDir) {
117
+ const connectorJsonPath = path.join(path.dirname(srcDir), 'connector.json');
118
+ try {
119
+ const connectorJson = await fse.readJson(connectorJsonPath);
120
+ return await auth_detector_1.AuthDetector.detectAuthInfo(srcDir, connectorJson.name);
121
+ }
122
+ catch (e) {
123
+ // If we can't read connector.json, use defaults
124
+ this.warn('Could not read connector.json, using default auth type. Auth import may need manual adjustment.');
125
+ return await auth_detector_1.AuthDetector.detectAuthInfo(srcDir);
126
+ }
127
+ }
128
+ async copyInputFromParent(parentOperationPath, outputSchemaOpPath, parentOpNamePascalCase, outputSchemaOpNamePascalCase) {
129
+ const parentInputPath = path.join(parentOperationPath, 'input.ts');
130
+ let inputTsContent;
131
+ if (fse.existsSync(parentInputPath)) {
132
+ // Read parent input.ts and copy it with renamed types
133
+ const parentInputContent = await fse.readFile(parentInputPath, 'utf-8');
134
+ // Replace the parent type name with the new type name
135
+ inputTsContent = parentInputContent.replace(new RegExp(`\\b${parentOpNamePascalCase}Input\\b`, 'g'), `${outputSchemaOpNamePascalCase}Input`);
136
+ }
137
+ else {
138
+ // Fallback: create a simple input type
139
+ inputTsContent = `export type ${outputSchemaOpNamePascalCase}Input = Record<string, any>;
140
+ `;
141
+ }
142
+ await fse.writeFile(path.join(outputSchemaOpPath, 'input.ts'), inputTsContent);
143
+ this.log(' ✓ Generated input.ts from parent operation');
144
+ }
145
+ async updateParentOperationJson(parentOperationJsonPath, parentOperationJson) {
146
+ const updatedParentJson = {
147
+ ...parentOperationJson,
148
+ dynamic_output: true,
149
+ };
150
+ await fse.writeJson(parentOperationJsonPath, updatedParentJson, {
151
+ spaces: 2,
152
+ });
153
+ this.log(' ✓ Updated parent operation.json with dynamic_output: true');
154
+ }
155
+ findSrcDirectory(currentDirectory) {
156
+ const { base } = path.parse(currentDirectory);
157
+ if (base === 'src') {
158
+ return currentDirectory;
159
+ }
160
+ return path.join(currentDirectory, 'src');
161
+ }
162
+ }
163
+ exports.default = AddDynamicOutputSchema;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=add-dynamic-output-schema.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add-dynamic-output-schema.unit.test.d.ts","sourceRoot":"","sources":["../../../src/commands/connector/add-dynamic-output-schema.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,335 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ const stdout_stderr_1 = require("stdout-stderr");
30
+ const fse = __importStar(require("fs-extra"));
31
+ const path = __importStar(require("path"));
32
+ const add_dynamic_output_schema_1 = __importDefault(require("./add-dynamic-output-schema"));
33
+ jest.mock('fs-extra');
34
+ describe('AddDynamicOutputSchema', () => {
35
+ const mockFse = fse;
36
+ const operationName = 'get_posts';
37
+ const outputSchemaOpName = 'get_posts_output_schema';
38
+ const srcDir = path.join(process.cwd(), 'src');
39
+ const parentOperationPath = path.join(srcDir, operationName);
40
+ const outputSchemaOpPath = path.join(srcDir, outputSchemaOpName);
41
+ beforeEach(() => {
42
+ jest.clearAllMocks();
43
+ // Default mocks for successful case
44
+ mockFse.existsSync.mockImplementation((filePath) => {
45
+ const pathStr = filePath.toString();
46
+ if (pathStr.includes(operationName) &&
47
+ pathStr.includes('operation.json')) {
48
+ return true; // Parent operation.json exists
49
+ }
50
+ if (pathStr.includes(operationName) &&
51
+ !pathStr.includes('output_schema')) {
52
+ return true; // Parent operation exists
53
+ }
54
+ if (pathStr.includes('output_schema')) {
55
+ return false; // Output schema operation doesn't exist yet
56
+ }
57
+ if (pathStr.includes('connector.json')) {
58
+ return true;
59
+ }
60
+ if (pathStr.includes('MyConnectorAuth.ts')) {
61
+ return true;
62
+ }
63
+ return false;
64
+ });
65
+ mockFse.readJson.mockImplementation(async (filePath) => {
66
+ const pathStr = filePath.toString();
67
+ if (pathStr.includes('connector.json')) {
68
+ return { name: 'my_connector' };
69
+ }
70
+ if (pathStr.includes('operation.json')) {
71
+ return {
72
+ name: operationName,
73
+ title: 'Get Posts',
74
+ description: 'Get all posts',
75
+ };
76
+ }
77
+ return {};
78
+ });
79
+ mockFse.readFile.mockImplementation(async (filePath) => {
80
+ const pathStr = filePath.toString();
81
+ if (pathStr.includes('input.ts')) {
82
+ return `export type GetPostsInput = {
83
+ limit?: number;
84
+ offset?: number;
85
+ };
86
+ `;
87
+ }
88
+ if (pathStr.includes('Auth.ts')) {
89
+ return 'export type MyConnectorAuth = { apiKey: string };';
90
+ }
91
+ return '';
92
+ });
93
+ mockFse.readdir.mockImplementation(async (dirPath) => {
94
+ const pathStr = dirPath.toString();
95
+ if (pathStr.includes('src')) {
96
+ // Mock src directory contents with auth file
97
+ return [
98
+ operationName,
99
+ 'MyConnectorAuth.ts',
100
+ 'connector.ts',
101
+ 'index.ts',
102
+ ];
103
+ }
104
+ return [];
105
+ });
106
+ mockFse.ensureDir.mockResolvedValue(undefined);
107
+ mockFse.writeJson.mockResolvedValue(undefined);
108
+ mockFse.writeFile.mockResolvedValue(undefined);
109
+ });
110
+ afterAll(() => {
111
+ jest.restoreAllMocks();
112
+ });
113
+ describe('successful operation creation', () => {
114
+ it('should create dynamic output schema operation for existing operation', async () => {
115
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
116
+ const mockGenerate = jest
117
+ .fn()
118
+ .mockImplementation(() => jest.fn().mockResolvedValue(true));
119
+ command['generator'] = {
120
+ generate: mockGenerate,
121
+ };
122
+ stdout_stderr_1.stdout.start();
123
+ await command.run();
124
+ stdout_stderr_1.stdout.stop();
125
+ // Verify success message
126
+ expect(stdout_stderr_1.stdout.output).toContain(`Success! Created dynamic output schema operation: ${outputSchemaOpName}`);
127
+ // Verify generator was called with correct template and values
128
+ expect(mockGenerate).toHaveBeenCalledWith(expect.stringContaining('dynamic-output-schema-template.zip'), expect.stringContaining('/src'), expect.objectContaining({
129
+ operationNameSnakeCase: operationName,
130
+ outputSchemaOpNameSnakeCase: outputSchemaOpName,
131
+ parentOpNameSnakeCase: operationName,
132
+ authType: 'MyConnectorAuth',
133
+ authFileName: 'MyConnectorAuth',
134
+ }));
135
+ // Verify parent operation.json was updated with dynamic_output: true
136
+ expect(mockFse.writeJson).toHaveBeenCalledWith(path.join(parentOperationPath, 'operation.json'), expect.objectContaining({
137
+ dynamic_output: true,
138
+ }), { spaces: 2 });
139
+ });
140
+ it('should generate correct input.ts by copying and renaming from parent', async () => {
141
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
142
+ const mockGenerate = jest
143
+ .fn()
144
+ .mockImplementation(() => jest.fn().mockResolvedValue(true));
145
+ command['generator'] = {
146
+ generate: mockGenerate,
147
+ };
148
+ stdout_stderr_1.stdout.start();
149
+ await command.run();
150
+ stdout_stderr_1.stdout.stop();
151
+ // Verify input.ts was written with renamed types
152
+ expect(mockFse.writeFile).toHaveBeenCalledWith(path.join(outputSchemaOpPath, 'input.ts'), expect.stringContaining('GetPostsOutputSchemaInput'));
153
+ // Verify it doesn't import from parent operation
154
+ const inputCall = mockFse.writeFile.mock.calls.find((call) => call[0].toString().includes('input.ts'));
155
+ expect(inputCall?.[1]).not.toContain(`from '../${operationName}/input'`);
156
+ expect(inputCall?.[1]).toContain('GetPostsOutputSchemaInput');
157
+ });
158
+ it('should handle input.ts fallback when parent input.ts does not exist', async () => {
159
+ mockFse.existsSync.mockImplementation((filePath) => {
160
+ const pathStr = filePath.toString();
161
+ if (pathStr.includes('input.ts')) {
162
+ return false; // Parent input.ts doesn't exist
163
+ }
164
+ if (pathStr.includes(operationName) &&
165
+ pathStr.includes('operation.json')) {
166
+ return true;
167
+ }
168
+ if (pathStr.includes(operationName) &&
169
+ !pathStr.includes('output_schema')) {
170
+ return true;
171
+ }
172
+ if (pathStr.includes('connector.json')) {
173
+ return true;
174
+ }
175
+ return false;
176
+ });
177
+ mockFse.readdir.mockResolvedValue([
178
+ operationName,
179
+ 'MyConnectorAuth.ts',
180
+ 'connector.ts',
181
+ ]);
182
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
183
+ const mockGenerate = jest
184
+ .fn()
185
+ .mockImplementation(() => jest.fn().mockResolvedValue(true));
186
+ command['generator'] = {
187
+ generate: mockGenerate,
188
+ };
189
+ stdout_stderr_1.stdout.start();
190
+ await command.run();
191
+ stdout_stderr_1.stdout.stop();
192
+ // Verify fallback input.ts was created
193
+ const inputCall = mockFse.writeFile.mock.calls.find((call) => call[0].toString().includes('input.ts'));
194
+ expect(inputCall?.[1]).toContain('Record<string, any>');
195
+ });
196
+ });
197
+ describe('error handling', () => {
198
+ it('should error when parent operation does not exist', async () => {
199
+ mockFse.existsSync.mockReturnValue(false);
200
+ mockFse.readdir.mockResolvedValue([]);
201
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
202
+ let errorThrown = false;
203
+ try {
204
+ await command.run();
205
+ }
206
+ catch (error) {
207
+ errorThrown = true;
208
+ expect(error.message).toContain(`Operation '${operationName}' not found`);
209
+ }
210
+ expect(errorThrown).toBe(true);
211
+ });
212
+ it('should error when parent operation.json is missing', async () => {
213
+ mockFse.existsSync.mockImplementation((filePath) => {
214
+ const pathStr = filePath.toString();
215
+ if (pathStr.includes('operation.json')) {
216
+ return false; // operation.json doesn't exist
217
+ }
218
+ if (pathStr.includes(operationName)) {
219
+ return true; // but operation folder exists
220
+ }
221
+ return false;
222
+ });
223
+ mockFse.readdir.mockResolvedValue([]);
224
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
225
+ let errorThrown = false;
226
+ try {
227
+ await command.run();
228
+ }
229
+ catch (error) {
230
+ errorThrown = true;
231
+ expect(error.message).toContain('missing operation.json');
232
+ }
233
+ expect(errorThrown).toBe(true);
234
+ });
235
+ it('should error when dynamic output schema operation already exists', async () => {
236
+ mockFse.existsSync.mockImplementation((filePath) =>
237
+ // Everything exists including the output schema operation
238
+ true);
239
+ mockFse.readdir.mockResolvedValue([]);
240
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
241
+ let errorThrown = false;
242
+ try {
243
+ await command.run();
244
+ }
245
+ catch (error) {
246
+ errorThrown = true;
247
+ expect(error.message).toContain('already exists');
248
+ }
249
+ expect(errorThrown).toBe(true);
250
+ });
251
+ it('should warn when connector.json cannot be read', async () => {
252
+ mockFse.readJson.mockImplementation(async (filePath) => {
253
+ const pathStr = filePath.toString();
254
+ if (pathStr.includes('connector.json')) {
255
+ throw new Error('Cannot read connector.json');
256
+ }
257
+ if (pathStr.includes('operation.json')) {
258
+ return {
259
+ name: operationName,
260
+ title: 'Get Posts',
261
+ };
262
+ }
263
+ return {};
264
+ });
265
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
266
+ stdout_stderr_1.stdout.start();
267
+ stdout_stderr_1.stderr.start();
268
+ await command.run();
269
+ stdout_stderr_1.stderr.stop();
270
+ stdout_stderr_1.stdout.stop();
271
+ // Should warn but still succeed
272
+ expect(stdout_stderr_1.stderr.output).toContain('Could not read connector.json');
273
+ expect(stdout_stderr_1.stdout.output).toContain('Success!');
274
+ });
275
+ });
276
+ describe('file detection', () => {
277
+ it('should detect standard {ConnectorName}Auth.ts pattern', async () => {
278
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
279
+ const mockGenerate = jest
280
+ .fn()
281
+ .mockImplementation(() => jest.fn().mockResolvedValue(true));
282
+ command['generator'] = {
283
+ generate: mockGenerate,
284
+ };
285
+ stdout_stderr_1.stdout.start();
286
+ await command.run();
287
+ stdout_stderr_1.stdout.stop();
288
+ // Verify auth type was detected and passed to generator
289
+ expect(mockGenerate).toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({
290
+ authType: 'MyConnectorAuth',
291
+ authFileName: 'MyConnectorAuth',
292
+ }));
293
+ });
294
+ it('should handle when in src/ directory', async () => {
295
+ // Mock process.cwd to return src directory
296
+ const originalCwd = process.cwd;
297
+ process.cwd = jest.fn().mockReturnValue(srcDir);
298
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
299
+ const mockGenerate = jest
300
+ .fn()
301
+ .mockImplementation(() => jest.fn().mockResolvedValue(true));
302
+ command['generator'] = {
303
+ generate: mockGenerate,
304
+ };
305
+ stdout_stderr_1.stdout.start();
306
+ await command.run();
307
+ stdout_stderr_1.stdout.stop();
308
+ // Verify generator was called with src directory
309
+ expect(mockGenerate).toHaveBeenCalledWith(expect.anything(), srcDir, expect.anything());
310
+ // Restore
311
+ process.cwd = originalCwd;
312
+ });
313
+ });
314
+ describe('output messages', () => {
315
+ it('should display correct success message and next steps', async () => {
316
+ const command = new add_dynamic_output_schema_1.default([operationName], {});
317
+ const mockGenerate = jest
318
+ .fn()
319
+ .mockImplementation(() => jest.fn().mockResolvedValue(true));
320
+ command['generator'] = {
321
+ generate: mockGenerate,
322
+ };
323
+ stdout_stderr_1.stdout.start();
324
+ await command.run();
325
+ stdout_stderr_1.stdout.stop();
326
+ expect(stdout_stderr_1.stdout.output).toContain('Checking for operation');
327
+ expect(stdout_stderr_1.stdout.output).toContain('Creating dynamic output schema operation');
328
+ expect(stdout_stderr_1.stdout.output).toContain('✓ Generated operation files from template');
329
+ expect(stdout_stderr_1.stdout.output).toContain('✓ Generated input.ts from parent operation');
330
+ expect(stdout_stderr_1.stdout.output).toContain('✓ Updated parent operation.json');
331
+ expect(stdout_stderr_1.stdout.output).toContain('Next steps:');
332
+ expect(stdout_stderr_1.stdout.output).toContain(`Edit src/${outputSchemaOpName}/handler.ts`);
333
+ });
334
+ });
335
+ });
@@ -0,0 +1,21 @@
1
+ export interface AuthInfo {
2
+ authFileName: string;
3
+ authType: string;
4
+ }
5
+ /**
6
+ * Detects the authentication file and type for a connector
7
+ */
8
+ export declare class AuthDetector {
9
+ /**
10
+ * Detects the auth file and extracts the auth type name
11
+ * @param srcDir - Path to the connector's src directory
12
+ * @param connectorName - Name of the connector from connector.json
13
+ * @returns AuthInfo with file name and type name
14
+ */
15
+ static detectAuthInfo(srcDir: string, connectorName?: string): Promise<AuthInfo>;
16
+ /**
17
+ * Find a file in a list using case-insensitive matching
18
+ */
19
+ private static findFileCaseInsensitive;
20
+ }
21
+ //# sourceMappingURL=auth-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-detector.d.ts","sourceRoot":"","sources":["../../../src/lib/dynamic-output/auth-detector.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,QAAQ;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,YAAY;IACxB;;;;;OAKG;WACU,cAAc,CAC1B,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,QAAQ,CAAC;IAkDpB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;CAQtC"}
@@ -0,0 +1,85 @@
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.AuthDetector = void 0;
27
+ const fse = __importStar(require("fs-extra"));
28
+ const path = __importStar(require("path"));
29
+ const StringExtensions_1 = require("@trayio/commons/string/StringExtensions");
30
+ /**
31
+ * Detects the authentication file and type for a connector
32
+ */
33
+ class AuthDetector {
34
+ /**
35
+ * Detects the auth file and extracts the auth type name
36
+ * @param srcDir - Path to the connector's src directory
37
+ * @param connectorName - Name of the connector from connector.json
38
+ * @returns AuthInfo with file name and type name
39
+ */
40
+ static async detectAuthInfo(srcDir, connectorName) {
41
+ let authFileName = 'Authentication';
42
+ let authType = 'NoAuth';
43
+ // Get all files in src directory for case-insensitive matching
44
+ const srcFiles = await fse.readdir(srcDir);
45
+ if (connectorName) {
46
+ const connectorNamePascalCase = StringExtensions_1.StringExtensions.pascalCase(connectorName);
47
+ // Check for {ConnectorName}Auth.ts pattern (standard pattern)
48
+ const standardAuthFile = `${connectorNamePascalCase}Auth.ts`;
49
+ const foundFile = this.findFileCaseInsensitive(srcFiles, standardAuthFile);
50
+ if (foundFile) {
51
+ authFileName = foundFile.replace('.ts', '');
52
+ authType = `${connectorNamePascalCase}Auth`;
53
+ return { authFileName, authType };
54
+ }
55
+ }
56
+ // Fall back to other common patterns
57
+ const authFiles = ['Authentication.ts', 'Auth.ts'];
58
+ const foundFile = authFiles
59
+ .map((pattern) => this.findFileCaseInsensitive(srcFiles, pattern))
60
+ .find((file) => file !== undefined);
61
+ if (foundFile) {
62
+ authFileName = foundFile.replace('.ts', '');
63
+ const authPath = path.join(srcDir, foundFile);
64
+ // Try to read and extract auth type name
65
+ try {
66
+ const authContent = await fse.readFile(authPath, 'utf-8');
67
+ const typeMatch = authContent.match(/export\s+type\s+(\w+)/);
68
+ if (typeMatch) {
69
+ [, authType] = typeMatch;
70
+ }
71
+ }
72
+ catch (e) {
73
+ // If we can't read the file, use the default
74
+ }
75
+ }
76
+ return { authFileName, authType };
77
+ }
78
+ /**
79
+ * Find a file in a list using case-insensitive matching
80
+ */
81
+ static findFileCaseInsensitive(files, targetFile) {
82
+ return files.find((file) => file.toLowerCase() === targetFile.toLowerCase());
83
+ }
84
+ }
85
+ exports.AuthDetector = AuthDetector;
@@ -0,0 +1,2 @@
1
+ export { AuthDetector, AuthInfo } from './auth-detector';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/dynamic-output/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthDetector = void 0;
4
+ var auth_detector_1 = require("./auth-detector");
5
+ Object.defineProperty(exports, "AuthDetector", { enumerable: true, get: function () { return auth_detector_1.AuthDetector; } });
@@ -17,6 +17,37 @@
17
17
  "index.js"
18
18
  ]
19
19
  },
20
+ "connector:add-dynamic-output-schema": {
21
+ "aliases": [],
22
+ "args": {
23
+ "operationName": {
24
+ "description": "Name of the parent operation to add dynamic output schema for",
25
+ "name": "operationName",
26
+ "required": true
27
+ }
28
+ },
29
+ "description": "Add a dynamic output schema operation for an existing operation",
30
+ "examples": [
31
+ "<%= config.bin %> <%= command.id %> get_posts",
32
+ "<%= config.bin %> <%= command.id %> fetch_user_data"
33
+ ],
34
+ "flags": {},
35
+ "hasDynamicHelp": false,
36
+ "hiddenAliases": [],
37
+ "id": "connector:add-dynamic-output-schema",
38
+ "pluginAlias": "@trayio/cdk-cli",
39
+ "pluginName": "@trayio/cdk-cli",
40
+ "pluginType": "core",
41
+ "strict": true,
42
+ "enableJsonFlag": false,
43
+ "isESM": false,
44
+ "relativePath": [
45
+ "dist",
46
+ "commands",
47
+ "connector",
48
+ "add-dynamic-output-schema.js"
49
+ ]
50
+ },
20
51
  "connector:add-operation": {
21
52
  "aliases": [],
22
53
  "args": {
@@ -684,5 +715,5 @@
684
715
  ]
685
716
  }
686
717
  },
687
- "version": "5.13.0"
718
+ "version": "5.14.0"
688
719
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trayio/cdk-cli",
3
- "version": "5.13.0",
3
+ "version": "5.14.0",
4
4
  "description": "A CLI for connector development",
5
5
  "exports": {
6
6
  "./*": "./dist/*.js"
@@ -22,13 +22,13 @@
22
22
  "@oclif/plugin-version": "2.0.11",
23
23
  "@oclif/plugin-warn-if-update-available": "^3.1.4",
24
24
  "@oclif/test": "3.1.12",
25
- "@trayio/axios": "5.13.0",
26
- "@trayio/cdk-build": "5.13.0",
27
- "@trayio/cdk-cli-commons": "5.13.0",
28
- "@trayio/commons": "5.13.0",
29
- "@trayio/generator": "5.13.0",
30
- "@trayio/tray-client": "5.13.0",
31
- "@trayio/tray-openapi": "5.13.0",
25
+ "@trayio/axios": "5.14.0",
26
+ "@trayio/cdk-build": "5.14.0",
27
+ "@trayio/cdk-cli-commons": "5.14.0",
28
+ "@trayio/commons": "5.14.0",
29
+ "@trayio/generator": "5.14.0",
30
+ "@trayio/tray-client": "5.14.0",
31
+ "@trayio/tray-openapi": "5.14.0",
32
32
  "chalk": "4.1.2",
33
33
  "dotenv": "^16.0.0",
34
34
  "inquirer": "8.2.6"