@trayio/cdk-cli 5.13.0-unstable → 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-unstable 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
@@ -5,7 +5,12 @@ export default class AddDynamicOutputSchema extends Command {
5
5
  static args: {
6
6
  operationName: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
+ private generator;
8
9
  run(): Promise<void>;
10
+ private validateParentOperation;
11
+ private detectAuthConfig;
12
+ private copyInputFromParent;
13
+ private updateParentOperationJson;
9
14
  private findSrcDirectory;
10
15
  }
11
16
  //# sourceMappingURL=add-dynamic-output-schema.d.ts.map
@@ -1 +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;AAS5C,MAAM,CAAC,OAAO,OAAO,sBAAuB,SAAQ,OAAO;IAC1D,MAAM,CAAC,WAAW,SACiD;IAEnE,MAAM,CAAC,QAAQ,WAGb;IAEF,MAAM,CAAC,IAAI;;MAOT;IAEI,GAAG;IA+PT,OAAO,CAAC,gBAAgB;CAOxB"}
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"}
@@ -28,6 +28,8 @@ const fse = __importStar(require("fs-extra"));
28
28
  const path = __importStar(require("path"));
29
29
  const StringExtensions_1 = require("@trayio/commons/string/StringExtensions");
30
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");
31
33
  class AddDynamicOutputSchema extends core_1.Command {
32
34
  static description = 'Add a dynamic output schema operation for an existing operation';
33
35
  static examples = [
@@ -41,20 +43,16 @@ class AddDynamicOutputSchema extends core_1.Command {
41
43
  description: 'Name of the parent operation to add dynamic output schema for',
42
44
  }),
43
45
  };
46
+ generator = new NodeFsGenerator_1.NodeFsGenerator();
44
47
  async run() {
45
48
  const { args } = await this.parse(AddDynamicOutputSchema);
46
49
  const { operationName } = args;
47
50
  const currentDirectory = process.cwd();
48
51
  const srcDir = this.findSrcDirectory(currentDirectory);
49
52
  // Validate parent operation exists
53
+ this.validateParentOperation(srcDir, operationName);
50
54
  const parentOperationPath = path.join(srcDir, operationName);
51
- if (!fse.existsSync(parentOperationPath)) {
52
- this.error((0, colorizeString_1.error)(`Operation '${operationName}' not found in src/ directory.\nMake sure you're in the connector root or src/ directory.`));
53
- }
54
55
  const parentOperationJsonPath = path.join(parentOperationPath, 'operation.json');
55
- if (!fse.existsSync(parentOperationJsonPath)) {
56
- this.error((0, colorizeString_1.error)(`Operation '${operationName}' exists but missing operation.json file.`));
57
- }
58
56
  this.log(`Checking for operation '${operationName}'... ✓`);
59
57
  // Check if dynamic output schema operation already exists
60
58
  const outputSchemaOpName = `${operationName}_output_schema`;
@@ -64,173 +62,95 @@ class AddDynamicOutputSchema extends core_1.Command {
64
62
  }
65
63
  // Read parent operation.json
66
64
  const parentOperationJson = await fse.readJson(parentOperationJsonPath);
67
- // Get connector name to construct auth file name (matches add-operation pattern)
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) {
68
117
  const connectorJsonPath = path.join(path.dirname(srcDir), 'connector.json');
69
- let connectorNamePascalCase = 'MyConnector';
70
- let authFileName = 'Authentication';
71
- let authType = 'NoAuth';
72
118
  try {
73
119
  const connectorJson = await fse.readJson(connectorJsonPath);
74
- connectorNamePascalCase = StringExtensions_1.StringExtensions.pascalCase(connectorJson.name);
75
- // Check for {ConnectorName}Auth.ts pattern (standard pattern)
76
- const standardAuthFile = `${connectorNamePascalCase}Auth.ts`;
77
- const standardAuthPath = path.join(srcDir, standardAuthFile);
78
- if (fse.existsSync(standardAuthPath)) {
79
- authFileName = standardAuthFile.replace('.ts', '');
80
- authType = `${connectorNamePascalCase}Auth`;
81
- }
82
- else {
83
- // Fall back to other common patterns
84
- const authFiles = ['Authentication.ts', 'Auth.ts'];
85
- const foundAuthFile = authFiles.find((file) => {
86
- const authPath = path.join(srcDir, file);
87
- return fse.existsSync(authPath);
88
- });
89
- if (foundAuthFile) {
90
- authFileName = foundAuthFile.replace('.ts', '');
91
- // Try to read and extract auth type name
92
- const authContent = await fse.readFile(path.join(srcDir, foundAuthFile), 'utf-8');
93
- const typeMatch = authContent.match(/export\s+type\s+(\w+)/);
94
- if (typeMatch) {
95
- [, authType] = typeMatch;
96
- }
97
- }
98
- }
120
+ return await auth_detector_1.AuthDetector.detectAuthInfo(srcDir, connectorJson.name);
99
121
  }
100
122
  catch (e) {
101
123
  // If we can't read connector.json, use defaults
102
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);
103
126
  }
104
- this.log(`Creating dynamic output schema operation '${outputSchemaOpName}'...`);
105
- // Create operation folder
106
- await fse.ensureDir(outputSchemaOpPath);
107
- this.log(' ✓ Created operation folder');
108
- // Generate names
109
- const parentOpNamePascalCase = StringExtensions_1.StringExtensions.pascalCase(operationName);
110
- const outputSchemaOpNamePascalCase = StringExtensions_1.StringExtensions.pascalCase(outputSchemaOpName);
111
- const parentOpNameCamelCase = operationName
112
- .split('_')
113
- .map((word, i) => i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
114
- .join('');
115
- const outputSchemaOpNameCamelCase = outputSchemaOpName
116
- .split('_')
117
- .map((word, i) => i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
118
- .join('');
119
- // 1. Generate operation.json
120
- const operationJson = {
121
- name: outputSchemaOpName,
122
- title: `${StringExtensions_1.StringExtensions.titleCase(operationName)} Output Schema`,
123
- description: `Dynamic output schema generator for ${operationName} (private operation)`,
124
- isPrivate: true,
125
- };
126
- await fse.writeJson(path.join(outputSchemaOpPath, 'operation.json'), operationJson, { spaces: 2 });
127
- this.log(' ✓ Generated operation.json');
128
- // 2. Generate input.ts (re-export from parent)
129
- const inputTsContent = `export type { ${parentOpNamePascalCase}Input as ${outputSchemaOpNamePascalCase}Input } from '../${operationName}/input';
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>;
130
140
  `;
141
+ }
131
142
  await fse.writeFile(path.join(outputSchemaOpPath, 'input.ts'), inputTsContent);
132
- this.log(' ✓ Generated input.ts');
133
- // 3. Generate output.ts
134
- const outputTsContent = `import { DynamicOutput } from '@trayio/cdk-dsl/connector/operation/OperationHandler';
135
-
136
- export type ${outputSchemaOpNamePascalCase}Output = DynamicOutput;
137
- `;
138
- await fse.writeFile(path.join(outputSchemaOpPath, 'output.ts'), outputTsContent);
139
- this.log(' ✓ Generated output.ts');
140
- // 4. Generate handler.ts from template
141
- const handlerTsContent = `import { OperationHandlerSetup } from '@trayio/cdk-dsl/connector/operation/OperationHandlerSetup';
142
- import {
143
- OperationHandlerResult,
144
- DynamicOutput,
145
- } from '@trayio/cdk-dsl/connector/operation/OperationHandler';
146
- import { JsonSchemaIntrospector } from '@trayio/commons/schema/JsonSchemaIntrospector';
147
- import { ${parentOpNamePascalCase}Input } from '../${operationName}/input';
148
- import { ${authType} } from '../${authFileName}';
149
- import { ${parentOpNameCamelCase}Handler } from '../${operationName}/handler';
150
-
151
- /**
152
- * Dynamic output schema handler for ${operationName}
153
- *
154
- * This handler generates a JSON Schema by introspecting actual API responses.
155
- * Customize the data-fetching logic below for your specific use case.
156
- */
157
- export const ${outputSchemaOpNameCamelCase}Handler =
158
- OperationHandlerSetup.configureDynamicOutputHandler<
159
- ${authType},
160
- ${parentOpNamePascalCase}Input
161
- >((handler) =>
162
- handler.usingComposite(async (ctx, input, invoke) => {
163
- try {
164
- // TODO: Customize this logic for your use case
165
-
166
- // Option 1: Invoke the main operation (ONLY for read-only GET operations!)
167
- const result = await invoke(${parentOpNameCamelCase}Handler)(input);
168
-
169
- // Option 2: Call a metadata/schema endpoint instead
170
- // const result = await invoke(getSchemaMetadataHandler)(input);
171
-
172
- // Option 3: Compute schema without API calls
173
- // const schema = computeSchemaBasedOnInput(input);
174
- // return OperationHandlerResult.success<DynamicOutput>({ output_schema: schema });
175
-
176
- if (result.isFailure) {
177
- // Return generic fallback schema on error
178
- return OperationHandlerResult.success<DynamicOutput>({
179
- output_schema: {
180
- type: 'object',
181
- additionalProperties: true,
182
- description: 'Generic schema - operation failed',
183
- },
184
- });
185
- }
186
-
187
- // Use the standard JsonSchemaIntrospector utility to convert response to JSON Schema
188
- const schema = JsonSchemaIntrospector.introspectToJsonSchema(
189
- result.value,
190
- {
191
- maxDepth: 10,
192
- includeExamples: false,
193
- strictRequired: true,
194
- }
195
- );
196
-
197
- return OperationHandlerResult.success<DynamicOutput>({
198
- output_schema: schema,
199
- });
200
- } catch (error) {
201
- // Fallback on any error
202
- return OperationHandlerResult.success<DynamicOutput>({
203
- output_schema: {
204
- type: 'object',
205
- additionalProperties: true,
206
- description: 'Fallback schema due to error',
207
- },
208
- });
209
- }
210
- })
211
- );
212
- `;
213
- await fse.writeFile(path.join(outputSchemaOpPath, 'handler.ts'), handlerTsContent);
214
- this.log(' ✓ Generated handler.ts (with JsonSchemaIntrospector template)');
215
- // 5. Update parent operation.json with isDynamicOutput: true
143
+ this.log(' ✓ Generated input.ts from parent operation');
144
+ }
145
+ async updateParentOperationJson(parentOperationJsonPath, parentOperationJson) {
216
146
  const updatedParentJson = {
217
147
  ...parentOperationJson,
218
- isDynamicOutput: true,
148
+ dynamic_output: true,
219
149
  };
220
150
  await fse.writeJson(parentOperationJsonPath, updatedParentJson, {
221
151
  spaces: 2,
222
152
  });
223
- this.log(` ✓ Updated parent operation.json with isDynamicOutput: true`);
224
- // Success message with next steps
225
- this.log('');
226
- this.log((0, colorizeString_1.success)(`Success! Created dynamic output schema operation: ${outputSchemaOpName}`));
227
- this.log('');
228
- this.log('Next steps:');
229
- this.log(` 1. Edit src/${outputSchemaOpName}/handler.ts`);
230
- this.log(' 2. Customize the data fetching logic for your use case');
231
- this.log(' 3. Run "tray-cdk connector build" to build the connector');
232
- this.log('');
233
- this.log('Note: Dynamic output is only safe for read-only GET operations!');
153
+ this.log(' ✓ Updated parent operation.json with dynamic_output: true');
234
154
  }
235
155
  findSrcDirectory(currentDirectory) {
236
156
  const { base } = path.parse(currentDirectory);
@@ -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; } });
@@ -715,5 +715,5 @@
715
715
  ]
716
716
  }
717
717
  },
718
- "version": "5.13.0-unstable"
718
+ "version": "5.14.0"
719
719
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trayio/cdk-cli",
3
- "version": "5.13.0-unstable",
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-unstable",
26
- "@trayio/cdk-build": "5.13.0-unstable",
27
- "@trayio/cdk-cli-commons": "5.13.0-unstable",
28
- "@trayio/commons": "5.13.0-unstable",
29
- "@trayio/generator": "5.13.0-unstable",
30
- "@trayio/tray-client": "5.13.0-unstable",
31
- "@trayio/tray-openapi": "5.13.0-unstable",
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"
@@ -92,6 +92,5 @@
92
92
  "devDependencies": {
93
93
  "@types/inquirer": "8.2.6",
94
94
  "oclif": "*"
95
- },
96
- "stableVersion": "0.0.0"
95
+ }
97
96
  }