@aloma.io/integration-sdk 3.8.64 → 3.8.67
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/build/builder/runtime-context.mjs +2 -2
- package/build/cli.mjs +19 -8
- package/build/controller/index.d.mts +3 -2
- package/build/controller/index.mjs +4 -3
- package/build/internal/dispatcher/index.d.mts +1 -4
- package/build/internal/dispatcher/index.mjs +3 -2
- package/build/openapi-to-connector.mjs +7 -2
- package/package.json +2 -2
- package/src/builder/runtime-context.mts +2 -2
- package/src/cli.mts +24 -8
- package/src/controller/index.mts +4 -3
- package/src/internal/dispatcher/index.mts +3 -2
- package/src/openapi-to-connector.mts +9 -2
- package/test/dispatcher-ctx-forwarding.test.mjs +161 -0
- package/test/openapi-ts-compilation.test.mjs +178 -0
- package/test/scenarios/complex/expected/controller.mts +14 -14
- package/test/scenarios/complex/expected/orders-resource.mts +5 -5
- package/test/scenarios/complex/expected/products-resource.mts +5 -5
- package/test/scenarios/simple/expected-controller.mts +2 -2
- package/test/verify-scenarios.mjs +4 -4
|
@@ -14,10 +14,10 @@ import { Connector } from '../internal/index.mjs';
|
|
|
14
14
|
export function buildResolvers(methods, controller) {
|
|
15
15
|
const resolvers = {};
|
|
16
16
|
methods.forEach((method) => {
|
|
17
|
-
const handler = async (args) => {
|
|
17
|
+
const handler = async (args, ctx) => {
|
|
18
18
|
if (!methods.includes(method))
|
|
19
19
|
throw new Error(`${method} not found`);
|
|
20
|
-
return controller[method](args);
|
|
20
|
+
return controller[method](args, ctx);
|
|
21
21
|
};
|
|
22
22
|
if (method.includes('.')) {
|
|
23
23
|
// Register nested tree for array-based resolution: ["crm", "contacts", "getPage"]
|
package/build/cli.mjs
CHANGED
|
@@ -392,16 +392,27 @@ program
|
|
|
392
392
|
// Create a temporary generator to generate just the exposed methods for this resource
|
|
393
393
|
const tempGenerator = new OpenAPIToConnector(spec, 'temp', generatorOptions);
|
|
394
394
|
const exposedMethods = tempGenerator.generateExposedResourceMethods(resources, resourceSpecs);
|
|
395
|
-
// Add the exposed methods to the controller
|
|
395
|
+
// Add the exposed methods to the controller, deduplicating by method name
|
|
396
396
|
if (exposedMethods.trim()) {
|
|
397
397
|
controllerContent = fs.readFileSync(controllerPath, 'utf-8');
|
|
398
|
-
//
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
398
|
+
// Extract existing method names from controller (async 'method.name' pattern)
|
|
399
|
+
const existingNames = new Set((controllerContent.match(/async\s+['"]([^'"]+)['"]\s*\(/g) || [])
|
|
400
|
+
.map(m => m.match(/['"]([^'"]+)['"]/)?.[1])
|
|
401
|
+
.filter(Boolean));
|
|
402
|
+
// Filter exposed methods to only include new ones
|
|
403
|
+
const methodBlocks = exposedMethods.split(/(?=\n async ['"])/);
|
|
404
|
+
const newMethods = methodBlocks.filter(block => {
|
|
405
|
+
const nameMatch = block.match(/async\s+['"]([^'"]+)['"]\s*\(/);
|
|
406
|
+
return !nameMatch || !existingNames.has(nameMatch[1]);
|
|
407
|
+
}).join('');
|
|
408
|
+
if (newMethods.trim()) {
|
|
409
|
+
const lastBrace = controllerContent.lastIndexOf('}');
|
|
410
|
+
if (lastBrace !== -1) {
|
|
411
|
+
const beforeBrace = controllerContent.substring(0, lastBrace);
|
|
412
|
+
const afterBrace = controllerContent.substring(lastBrace);
|
|
413
|
+
const updatedContent = `${beforeBrace}\n${newMethods}\n${afterBrace}`;
|
|
414
|
+
fs.writeFileSync(controllerPath, updatedContent);
|
|
415
|
+
}
|
|
405
416
|
}
|
|
406
417
|
}
|
|
407
418
|
console.log(`✅ Resource ${options.className} added successfully!`);
|
|
@@ -44,8 +44,9 @@ export declare abstract class AbstractController {
|
|
|
44
44
|
/**
|
|
45
45
|
* called, when the remote public method is not found on the controller
|
|
46
46
|
* @param arg
|
|
47
|
+
* @param ctx optional invocation context (taskId, namespace, sticky)
|
|
47
48
|
*/
|
|
48
|
-
protected fallback(arg: any): Promise<any>;
|
|
49
|
+
protected fallback(arg: any, ctx?: any): Promise<any>;
|
|
49
50
|
/**
|
|
50
51
|
* will be invoked, when the connector has an endpoint enabled
|
|
51
52
|
* will receive the data and can e.g. create a new task from it
|
|
@@ -134,7 +135,7 @@ export declare abstract class AbstractController {
|
|
|
134
135
|
/**
|
|
135
136
|
* @ignore
|
|
136
137
|
**/
|
|
137
|
-
__default(arg: any): Promise<any | null>;
|
|
138
|
+
__default(arg: any, ctx?: any): Promise<any | null>;
|
|
138
139
|
/**
|
|
139
140
|
* @ignore
|
|
140
141
|
**/
|
|
@@ -41,8 +41,9 @@ export class AbstractController {
|
|
|
41
41
|
/**
|
|
42
42
|
* called, when the remote public method is not found on the controller
|
|
43
43
|
* @param arg
|
|
44
|
+
* @param ctx optional invocation context (taskId, namespace, sticky)
|
|
44
45
|
*/
|
|
45
|
-
fallback(arg) {
|
|
46
|
+
fallback(arg, ctx) {
|
|
46
47
|
throw new Error('method not found');
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
@@ -136,8 +137,8 @@ export class AbstractController {
|
|
|
136
137
|
/**
|
|
137
138
|
* @ignore
|
|
138
139
|
**/
|
|
139
|
-
async __default(arg) {
|
|
140
|
-
return this.fallback(arg);
|
|
140
|
+
async __default(arg, ctx) {
|
|
141
|
+
return this.fallback(arg, ctx);
|
|
141
142
|
}
|
|
142
143
|
/**
|
|
143
144
|
* @ignore
|
|
@@ -26,10 +26,7 @@ export default class Dispatcher {
|
|
|
26
26
|
build(): {
|
|
27
27
|
introspect: () => any;
|
|
28
28
|
configSchema: () => any;
|
|
29
|
-
execute: ({ query, variables }:
|
|
30
|
-
query: any;
|
|
31
|
-
variables: any;
|
|
32
|
-
}) => Promise<any>;
|
|
29
|
+
execute: ({ query, variables, taskId, namespace, sticky }: any) => Promise<any>;
|
|
33
30
|
processPacket: (packet: any) => Promise<any>;
|
|
34
31
|
start: (arg: any) => Promise<void>;
|
|
35
32
|
};
|
|
@@ -133,7 +133,8 @@ ${arg.configurableClientScope}
|
|
|
133
133
|
}
|
|
134
134
|
return current;
|
|
135
135
|
};
|
|
136
|
-
const execute = async ({ query, variables }) => {
|
|
136
|
+
const execute = async ({ query, variables, taskId, namespace, sticky }) => {
|
|
137
|
+
const ctx = { taskId, namespace, sticky };
|
|
137
138
|
if (!Array.isArray(query))
|
|
138
139
|
query = [query];
|
|
139
140
|
query = query
|
|
@@ -143,7 +144,7 @@ ${arg.configurableClientScope}
|
|
|
143
144
|
const method = resolveMethod(query);
|
|
144
145
|
if (!method && !_resolvers.__default)
|
|
145
146
|
throw new Error(`${originalQuery} not found`);
|
|
146
|
-
return method ? method(variables) : _resolvers.__default(variables ? { ...variables, __method: originalQuery } : variables);
|
|
147
|
+
return method ? method(variables, ctx) : _resolvers.__default(variables ? { ...variables, __method: originalQuery } : variables, ctx);
|
|
147
148
|
};
|
|
148
149
|
const introspect = () => local._types;
|
|
149
150
|
const configSchema = () => local._config;
|
|
@@ -255,6 +255,8 @@ export class OpenAPIToConnector {
|
|
|
255
255
|
if (hasBody) {
|
|
256
256
|
this.addRequestBodyProperties(operation.requestBody, optionProps);
|
|
257
257
|
}
|
|
258
|
+
// Always include headers in options type (generated code references options.headers)
|
|
259
|
+
optionProps.push('headers?: Record<string, string>');
|
|
258
260
|
// Check if options parameter is required (has required query params or required body)
|
|
259
261
|
const hasRequiredNonPathParams = queryParams.some((p) => p.required) || (hasBody && operation.requestBody?.required);
|
|
260
262
|
const optionsRequired = hasRequiredNonPathParams ? '' : '?';
|
|
@@ -322,6 +324,8 @@ export class OpenAPIToConnector {
|
|
|
322
324
|
if (hasBody) {
|
|
323
325
|
this.addRequestBodyProperties(operation.requestBody, optionProps);
|
|
324
326
|
}
|
|
327
|
+
// Always include headers in options type (generated code references options.headers)
|
|
328
|
+
optionProps.push('headers?: Record<string, string>');
|
|
325
329
|
// If there are too many parameters, use simplified signature to avoid parsing issues
|
|
326
330
|
// Also check if any parameter name is too long (over 100 chars) which can cause issues
|
|
327
331
|
const hasLongParamNames = optionProps.some((prop) => prop.length > 100);
|
|
@@ -342,11 +346,12 @@ export class OpenAPIToConnector {
|
|
|
342
346
|
* Resolve a schema reference to a TypeScript type name
|
|
343
347
|
*/
|
|
344
348
|
resolveSchemaRef(ref) {
|
|
345
|
-
// Extract the component name from the reference
|
|
346
|
-
// e.g., "#/components/schemas/Company" -> "Company"
|
|
347
349
|
const parts = ref.split('/');
|
|
348
350
|
if (parts.length >= 2) {
|
|
349
351
|
const componentName = parts[parts.length - 1];
|
|
352
|
+
if (!this.spec.components?.schemas?.[componentName]) {
|
|
353
|
+
return 'any';
|
|
354
|
+
}
|
|
350
355
|
return this.sanitizeTypeName(componentName);
|
|
351
356
|
}
|
|
352
357
|
return 'any';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aloma.io/integration-sdk",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.67",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "aloma.io",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|
|
62
|
-
"url": "https://github.com/aloma-io/integration.git",
|
|
62
|
+
"url": "git+https://github.com/aloma-io/integration.git",
|
|
63
63
|
"directory": "integration-sdk"
|
|
64
64
|
},
|
|
65
65
|
"publishConfig": {
|
|
@@ -16,9 +16,9 @@ export function buildResolvers(methods: string[], controller: any): any {
|
|
|
16
16
|
const resolvers: any = {};
|
|
17
17
|
|
|
18
18
|
methods.forEach((method) => {
|
|
19
|
-
const handler = async (args: any) => {
|
|
19
|
+
const handler = async (args: any, ctx?: any) => {
|
|
20
20
|
if (!methods.includes(method)) throw new Error(`${method} not found`);
|
|
21
|
-
return controller[method](args);
|
|
21
|
+
return controller[method](args, ctx);
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
if (method.includes('.')) {
|
package/src/cli.mts
CHANGED
|
@@ -485,16 +485,32 @@ program
|
|
|
485
485
|
const tempGenerator = new OpenAPIToConnector(spec, 'temp', generatorOptions);
|
|
486
486
|
const exposedMethods = tempGenerator.generateExposedResourceMethods(resources, resourceSpecs);
|
|
487
487
|
|
|
488
|
-
// Add the exposed methods to the controller
|
|
488
|
+
// Add the exposed methods to the controller, deduplicating by method name
|
|
489
489
|
if (exposedMethods.trim()) {
|
|
490
490
|
controllerContent = fs.readFileSync(controllerPath, 'utf-8');
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
491
|
+
|
|
492
|
+
// Extract existing method names from controller (async 'method.name' pattern)
|
|
493
|
+
const existingNames = new Set(
|
|
494
|
+
(controllerContent.match(/async\s+['"]([^'"]+)['"]\s*\(/g) || [])
|
|
495
|
+
.map(m => m.match(/['"]([^'"]+)['"]/)?.[1])
|
|
496
|
+
.filter(Boolean)
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
// Filter exposed methods to only include new ones
|
|
500
|
+
const methodBlocks = exposedMethods.split(/(?=\n async ['"])/);
|
|
501
|
+
const newMethods = methodBlocks.filter(block => {
|
|
502
|
+
const nameMatch = block.match(/async\s+['"]([^'"]+)['"]\s*\(/);
|
|
503
|
+
return !nameMatch || !existingNames.has(nameMatch[1]);
|
|
504
|
+
}).join('');
|
|
505
|
+
|
|
506
|
+
if (newMethods.trim()) {
|
|
507
|
+
const lastBrace = controllerContent.lastIndexOf('}');
|
|
508
|
+
if (lastBrace !== -1) {
|
|
509
|
+
const beforeBrace = controllerContent.substring(0, lastBrace);
|
|
510
|
+
const afterBrace = controllerContent.substring(lastBrace);
|
|
511
|
+
const updatedContent = `${beforeBrace}\n${newMethods}\n${afterBrace}`;
|
|
512
|
+
fs.writeFileSync(controllerPath, updatedContent);
|
|
513
|
+
}
|
|
498
514
|
}
|
|
499
515
|
}
|
|
500
516
|
|
package/src/controller/index.mts
CHANGED
|
@@ -51,8 +51,9 @@ export abstract class AbstractController {
|
|
|
51
51
|
/**
|
|
52
52
|
* called, when the remote public method is not found on the controller
|
|
53
53
|
* @param arg
|
|
54
|
+
* @param ctx optional invocation context (taskId, namespace, sticky)
|
|
54
55
|
*/
|
|
55
|
-
protected fallback(arg: any): Promise<any> {
|
|
56
|
+
protected fallback(arg: any, ctx?: any): Promise<any> {
|
|
56
57
|
throw new Error('method not found');
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -186,8 +187,8 @@ export abstract class AbstractController {
|
|
|
186
187
|
/**
|
|
187
188
|
* @ignore
|
|
188
189
|
**/
|
|
189
|
-
async __default(arg: any): Promise<any | null> {
|
|
190
|
-
return this.fallback(arg);
|
|
190
|
+
async __default(arg: any, ctx?: any): Promise<any | null> {
|
|
191
|
+
return this.fallback(arg, ctx);
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
/**
|
|
@@ -163,7 +163,8 @@ ${arg.configurableClientScope}
|
|
|
163
163
|
return current;
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
-
const execute = async ({query, variables}) => {
|
|
166
|
+
const execute = async ({query, variables, taskId, namespace, sticky}: any) => {
|
|
167
|
+
const ctx = {taskId, namespace, sticky};
|
|
167
168
|
if (!Array.isArray(query)) query = [query];
|
|
168
169
|
|
|
169
170
|
query = query
|
|
@@ -177,7 +178,7 @@ ${arg.configurableClientScope}
|
|
|
177
178
|
const method = resolveMethod(query);
|
|
178
179
|
if (!method && !_resolvers.__default) throw new Error(`${originalQuery} not found`);
|
|
179
180
|
|
|
180
|
-
return method ? method(variables) : _resolvers.__default(variables ? {...variables, __method: originalQuery} : variables);
|
|
181
|
+
return method ? method(variables, ctx) : _resolvers.__default(variables ? {...variables, __method: originalQuery} : variables, ctx);
|
|
181
182
|
};
|
|
182
183
|
|
|
183
184
|
const introspect = () => local._types;
|
|
@@ -310,6 +310,9 @@ export class OpenAPIToConnector {
|
|
|
310
310
|
this.addRequestBodyProperties(operation.requestBody, optionProps);
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// Always include headers in options type (generated code references options.headers)
|
|
314
|
+
optionProps.push('headers?: Record<string, string>');
|
|
315
|
+
|
|
313
316
|
// Check if options parameter is required (has required query params or required body)
|
|
314
317
|
const hasRequiredNonPathParams =
|
|
315
318
|
queryParams.some((p) => p.required) || (hasBody && operation.requestBody?.required);
|
|
@@ -392,6 +395,9 @@ export class OpenAPIToConnector {
|
|
|
392
395
|
this.addRequestBodyProperties(operation.requestBody, optionProps);
|
|
393
396
|
}
|
|
394
397
|
|
|
398
|
+
// Always include headers in options type (generated code references options.headers)
|
|
399
|
+
optionProps.push('headers?: Record<string, string>');
|
|
400
|
+
|
|
395
401
|
// If there are too many parameters, use simplified signature to avoid parsing issues
|
|
396
402
|
// Also check if any parameter name is too long (over 100 chars) which can cause issues
|
|
397
403
|
const hasLongParamNames = optionProps.some((prop) => prop.length > 100);
|
|
@@ -413,11 +419,12 @@ export class OpenAPIToConnector {
|
|
|
413
419
|
* Resolve a schema reference to a TypeScript type name
|
|
414
420
|
*/
|
|
415
421
|
private resolveSchemaRef(ref: string): string {
|
|
416
|
-
// Extract the component name from the reference
|
|
417
|
-
// e.g., "#/components/schemas/Company" -> "Company"
|
|
418
422
|
const parts = ref.split('/');
|
|
419
423
|
if (parts.length >= 2) {
|
|
420
424
|
const componentName = parts[parts.length - 1];
|
|
425
|
+
if (!this.spec.components?.schemas?.[componentName]) {
|
|
426
|
+
return 'any';
|
|
427
|
+
}
|
|
421
428
|
return this.sanitizeTypeName(componentName);
|
|
422
429
|
}
|
|
423
430
|
return 'any';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Dispatcher } from '../build/internal/dispatcher/index.mjs';
|
|
2
|
+
import { buildResolvers } from '../build/builder/runtime-context.mjs';
|
|
3
|
+
|
|
4
|
+
// Simple test framework matching project convention
|
|
5
|
+
const colors = {
|
|
6
|
+
green: '\x1b[32m',
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
yellow: '\x1b[33m',
|
|
9
|
+
reset: '\x1b[0m',
|
|
10
|
+
cyan: '\x1b[36m'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let passed = 0;
|
|
14
|
+
let failed = 0;
|
|
15
|
+
|
|
16
|
+
async function test(name, fn) {
|
|
17
|
+
try {
|
|
18
|
+
console.log(`${colors.cyan}Running: ${name}${colors.reset}`);
|
|
19
|
+
await fn();
|
|
20
|
+
console.log(`${colors.green}✓ PASS: ${name}${colors.reset}\n`);
|
|
21
|
+
passed++;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.log(`${colors.red}✗ FAIL: ${name}${colors.reset}`);
|
|
24
|
+
console.log(`${colors.red} Error: ${error.message}${colors.reset}\n`);
|
|
25
|
+
failed++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assert(condition, message) {
|
|
30
|
+
if (!condition) throw new Error(message || 'Assertion failed');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assertEqual(actual, expected, message) {
|
|
34
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
35
|
+
throw new Error(message || `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Helper: build a dispatcher with given resolvers and execute a query+envelope.
|
|
40
|
+
// IMPORTANT: this test passes taskId/namespace/sticky in the envelope to verify the
|
|
41
|
+
// dispatcher destructures them and forwards them as a ctx object to handlers.
|
|
42
|
+
async function buildAndExecute(resolvers, envelope) {
|
|
43
|
+
const dispatcher = new Dispatcher();
|
|
44
|
+
dispatcher.types({ fields: {} });
|
|
45
|
+
dispatcher.resolvers(resolvers);
|
|
46
|
+
dispatcher.main(async () => {});
|
|
47
|
+
const built = dispatcher.build();
|
|
48
|
+
return built.execute(envelope);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- RED TESTS for ctx forwarding (SDK v3.8.67 additive change) ---
|
|
52
|
+
|
|
53
|
+
// Test 1: dispatcher.execute forwards ctx (taskId, namespace, sticky) to a NAMED METHOD handler
|
|
54
|
+
// as the second argument. Currently the dispatcher only passes `variables` — so the second
|
|
55
|
+
// arg is undefined and this test must FAIL (red) until the SDK is patched.
|
|
56
|
+
await test('dispatcher forwards ctx as second arg to named-method handler', async () => {
|
|
57
|
+
let receivedArgs;
|
|
58
|
+
let receivedCtx;
|
|
59
|
+
|
|
60
|
+
// Use buildResolvers so the wrapper is exercised exactly as it is in production.
|
|
61
|
+
const mockController = {
|
|
62
|
+
myMethod: async (args, ctx) => {
|
|
63
|
+
receivedArgs = args;
|
|
64
|
+
receivedCtx = ctx;
|
|
65
|
+
return { ok: true };
|
|
66
|
+
},
|
|
67
|
+
'__autocomplete': async () => ({}),
|
|
68
|
+
'__endpoint': async () => ({}),
|
|
69
|
+
'__default': async () => ({}),
|
|
70
|
+
};
|
|
71
|
+
const methods = ['myMethod', '__autocomplete', '__endpoint', '__default'];
|
|
72
|
+
const resolvers = buildResolvers(methods, mockController);
|
|
73
|
+
|
|
74
|
+
await buildAndExecute(resolvers, {
|
|
75
|
+
query: ['myMethod'],
|
|
76
|
+
variables: { foo: 'bar' },
|
|
77
|
+
taskId: 'task-123',
|
|
78
|
+
namespace: 'testing',
|
|
79
|
+
sticky: 'sticky-abc',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
assertEqual(receivedArgs, { foo: 'bar' }, 'args should be forwarded unchanged');
|
|
83
|
+
assert(receivedCtx !== undefined, 'ctx should be defined as second argument');
|
|
84
|
+
assertEqual(receivedCtx.taskId, 'task-123', `ctx.taskId should be "task-123", got ${JSON.stringify(receivedCtx)}`);
|
|
85
|
+
assertEqual(receivedCtx.namespace, 'testing', `ctx.namespace should be "testing", got ${JSON.stringify(receivedCtx)}`);
|
|
86
|
+
assertEqual(receivedCtx.sticky, 'sticky-abc', `ctx.sticky should be "sticky-abc", got ${JSON.stringify(receivedCtx)}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Test 2: dispatcher.execute forwards ctx to the __default (fallback) handler as second arg
|
|
90
|
+
// when the named method is not found. Currently the __default invocation only receives the
|
|
91
|
+
// merged variables — so ctx is undefined and this test must FAIL (red) until patched.
|
|
92
|
+
await test('dispatcher forwards ctx as second arg to __default fallback handler', async () => {
|
|
93
|
+
let receivedArg;
|
|
94
|
+
let receivedCtx;
|
|
95
|
+
|
|
96
|
+
// Resolvers map with ONLY __default — no named methods, so dispatcher falls back.
|
|
97
|
+
const resolvers = {
|
|
98
|
+
__default: async (arg, ctx) => {
|
|
99
|
+
receivedArg = arg;
|
|
100
|
+
receivedCtx = ctx;
|
|
101
|
+
return { fallback: true };
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
await buildAndExecute(resolvers, {
|
|
106
|
+
query: ['someUnknownMethod'],
|
|
107
|
+
variables: { hello: 'world' },
|
|
108
|
+
taskId: 'task-456',
|
|
109
|
+
namespace: 'testing',
|
|
110
|
+
sticky: 'sticky-def',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
assert(receivedArg !== undefined, '__default should receive arg');
|
|
114
|
+
assertEqual(receivedArg.hello, 'world', 'variables should be merged into arg');
|
|
115
|
+
assertEqual(receivedArg.__method, ['someUnknownMethod'], '__method should be in arg');
|
|
116
|
+
assert(receivedCtx !== undefined, 'ctx should be defined as second argument to __default');
|
|
117
|
+
assertEqual(receivedCtx.taskId, 'task-456', `ctx.taskId should be "task-456", got ${JSON.stringify(receivedCtx)}`);
|
|
118
|
+
assertEqual(receivedCtx.namespace, 'testing', `ctx.namespace should be "testing"`);
|
|
119
|
+
assertEqual(receivedCtx.sticky, 'sticky-def', `ctx.sticky should be "sticky-def"`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Test 3: BACKWARD COMPATIBILITY. A controller method with the old 1-arg signature
|
|
123
|
+
// `async myMethod(args)` continues to work when called via the new dispatcher path.
|
|
124
|
+
// The second argument exists but is ignored. This test MUST PASS even in the red phase —
|
|
125
|
+
// if it fails red, the test setup is broken (not the implementation).
|
|
126
|
+
await test('one-arg handler still works (backward compatibility)', async () => {
|
|
127
|
+
let receivedArgs;
|
|
128
|
+
|
|
129
|
+
const mockController = {
|
|
130
|
+
// Note: only one parameter. Second arg, if any, must be silently ignored.
|
|
131
|
+
legacyMethod: async (args) => {
|
|
132
|
+
receivedArgs = args;
|
|
133
|
+
return { legacy: true, ...args };
|
|
134
|
+
},
|
|
135
|
+
'__autocomplete': async () => ({}),
|
|
136
|
+
'__endpoint': async () => ({}),
|
|
137
|
+
'__default': async () => ({}),
|
|
138
|
+
};
|
|
139
|
+
const methods = ['legacyMethod', '__autocomplete', '__endpoint', '__default'];
|
|
140
|
+
const resolvers = buildResolvers(methods, mockController);
|
|
141
|
+
|
|
142
|
+
const result = await buildAndExecute(resolvers, {
|
|
143
|
+
query: ['legacyMethod'],
|
|
144
|
+
variables: { id: 7 },
|
|
145
|
+
taskId: 'task-789',
|
|
146
|
+
namespace: 'testing',
|
|
147
|
+
sticky: 'sticky-ghi',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
assertEqual(receivedArgs, { id: 7 }, 'legacy handler should receive args unchanged');
|
|
151
|
+
assertEqual(result, { legacy: true, id: 7 }, 'legacy handler should return correct result');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// --- Results ---
|
|
155
|
+
console.log(`\n${colors.yellow}--- Dispatcher ctx forwarding test results ---${colors.reset}`);
|
|
156
|
+
console.log(`${colors.green}Passed: ${passed}${colors.reset}`);
|
|
157
|
+
console.log(`${colors.red}Failed: ${failed}${colors.reset}`);
|
|
158
|
+
|
|
159
|
+
if (failed > 0) {
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { writeFileSync, readFileSync, mkdirSync, rmSync, existsSync } from 'fs';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const SDK_ROOT = join(__dirname, '..');
|
|
9
|
+
|
|
10
|
+
const { OpenAPIToConnector } = await import('../build/openapi-to-connector.mjs');
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
|
|
15
|
+
function test(name, fn) {
|
|
16
|
+
try { fn(); console.log(`✓ PASS: ${name}`); passed++; }
|
|
17
|
+
catch(e) { console.log(`✗ FAIL: ${name} — ${e.message}`); failed++; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- Bug A: schema type references that are not locally defined ---
|
|
21
|
+
// When a $ref points to a schema NOT present in the spec's components.schemas,
|
|
22
|
+
// resolveSchemaRef still emits the type name (e.g., PublicAssociationsForObject)
|
|
23
|
+
// in the function signature. Since no interface is generated, tsc fails with TS2304.
|
|
24
|
+
|
|
25
|
+
test('Bug A: resolveSchemaRef emits any for types not in components.schemas', () => {
|
|
26
|
+
const spec = {
|
|
27
|
+
openapi: '3.0.0',
|
|
28
|
+
info: { title: 'Test', version: '1.0' },
|
|
29
|
+
paths: {
|
|
30
|
+
'/crm/contacts/{contactId}': {
|
|
31
|
+
put: {
|
|
32
|
+
operationId: 'updateContact',
|
|
33
|
+
parameters: [{ name: 'contactId', in: 'path', required: true, schema: { type: 'string' } }],
|
|
34
|
+
requestBody: {
|
|
35
|
+
required: true,
|
|
36
|
+
content: {
|
|
37
|
+
'application/json': {
|
|
38
|
+
schema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
associations: { $ref: '#/components/schemas/PublicAssociationsForObject' },
|
|
42
|
+
properties: { type: 'object' }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
responses: { '200': { description: 'OK' } }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
// Schema is referenced but NOT defined — simulates cross-file ref or incomplete spec
|
|
53
|
+
components: { schemas: {} }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const gen = new OpenAPIToConnector(spec, 'contacts', { nestedPaths: true });
|
|
57
|
+
const resourceCode = gen.generateResourceClass('ContactsResource');
|
|
58
|
+
|
|
59
|
+
// The type name must NOT appear in generated code when no interface is emitted
|
|
60
|
+
const hasTypeRef = resourceCode.includes('PublicAssociationsForObject');
|
|
61
|
+
const hasInterface = resourceCode.includes('interface PublicAssociationsForObject');
|
|
62
|
+
|
|
63
|
+
assert(!hasTypeRef || hasInterface,
|
|
64
|
+
`Type 'PublicAssociationsForObject' referenced in function signature but not defined. ` +
|
|
65
|
+
`resolveSchemaRef must emit 'any' when the schema is not in components.schemas.`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// --- Bug B: add-resource produces duplicate functions on re-run ---
|
|
69
|
+
// The add-resource CLI command appends exposed methods to the controller
|
|
70
|
+
// without checking if they already exist. Running it twice duplicates functions.
|
|
71
|
+
|
|
72
|
+
test('Bug B: add-resource CLI deduplicates exposed methods on re-run', () => {
|
|
73
|
+
const spec = {
|
|
74
|
+
openapi: '3.0.0',
|
|
75
|
+
info: { title: 'Test', version: '1.0' },
|
|
76
|
+
paths: {
|
|
77
|
+
'/crm/contacts': {
|
|
78
|
+
get: { operationId: 'getContacts', responses: { '200': { description: 'OK' } } }
|
|
79
|
+
},
|
|
80
|
+
'/crm/contacts/{id}': {
|
|
81
|
+
get: {
|
|
82
|
+
operationId: 'getContactById',
|
|
83
|
+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
84
|
+
responses: { '200': { description: 'OK' } }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const gen = new OpenAPIToConnector(spec, 'contacts', { nestedPaths: true });
|
|
91
|
+
const resources = [{ className: 'ContactsResource', fileName: 'contacts' }];
|
|
92
|
+
const resourceSpecs = [{ fileName: 'contacts', spec }];
|
|
93
|
+
|
|
94
|
+
const exposedMethods = gen.generateExposedResourceMethods(resources, resourceSpecs);
|
|
95
|
+
|
|
96
|
+
// Simulate a controller that already contains these methods (from first add-resource run)
|
|
97
|
+
const existingController = `export default class Controller {\n${exposedMethods}\n}`;
|
|
98
|
+
|
|
99
|
+
// Simulate the fixed CLI dedup logic: extract existing names, filter new methods
|
|
100
|
+
const existingNames = new Set(
|
|
101
|
+
(existingController.match(/async\s+['"]([^'"]+)['"]\s*\(/g) || [])
|
|
102
|
+
.map(m => m.match(/['"]([^'"]+)['"]/)?.[1])
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const methodBlocks = exposedMethods.split(/(?=\n async ['"])/);
|
|
107
|
+
const newMethods = methodBlocks.filter(block => {
|
|
108
|
+
const nameMatch = block.match(/async\s+['"]([^'"]+)['"]\s*\(/);
|
|
109
|
+
return !nameMatch || !existingNames.has(nameMatch[1]);
|
|
110
|
+
}).join('');
|
|
111
|
+
|
|
112
|
+
// After dedup, no new methods should be added (all already exist)
|
|
113
|
+
const remainingDeclarations = newMethods.match(/async\s+['"]([^'"]+)['"]\s*\(/g) || [];
|
|
114
|
+
assert(remainingDeclarations.length === 0,
|
|
115
|
+
`Dedup failed: ${remainingDeclarations.length} methods would be re-added. ` +
|
|
116
|
+
`CLI must filter out methods already present in the controller.`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// --- Bug C: generated code references options.headers but type lacks headers field ---
|
|
120
|
+
// The generator emits `const { headers, ...bodyData } = options;` and
|
|
121
|
+
// `headers: options.headers` in fetchOptions, but the options type generated
|
|
122
|
+
// for the function signature does NOT include `headers?: Record<string, string>`.
|
|
123
|
+
|
|
124
|
+
test('Bug C: generated method with body includes headers in options type', () => {
|
|
125
|
+
const spec = {
|
|
126
|
+
openapi: '3.0.0',
|
|
127
|
+
info: { title: 'Test', version: '1.0' },
|
|
128
|
+
paths: {
|
|
129
|
+
'/crm/contacts': {
|
|
130
|
+
post: {
|
|
131
|
+
operationId: 'createContact',
|
|
132
|
+
requestBody: {
|
|
133
|
+
required: true,
|
|
134
|
+
content: {
|
|
135
|
+
'application/json': {
|
|
136
|
+
schema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
email: { type: 'string' },
|
|
140
|
+
firstname: { type: 'string' }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
responses: { '201': { description: 'Created' } }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const gen = new OpenAPIToConnector(spec, 'contacts', { nestedPaths: true });
|
|
153
|
+
const resourceCode = gen.generateResourceClass('ContactsResource');
|
|
154
|
+
|
|
155
|
+
// The generator emits `const { headers, ...bodyData } = options;` and
|
|
156
|
+
// `headers: options.headers` — so the options type MUST include headers
|
|
157
|
+
const usesHeaders = resourceCode.includes('options.headers') ||
|
|
158
|
+
resourceCode.includes('options?.headers') ||
|
|
159
|
+
resourceCode.includes('{ headers,') ||
|
|
160
|
+
resourceCode.includes('{ headers }');
|
|
161
|
+
|
|
162
|
+
if (usesHeaders) {
|
|
163
|
+
// Extract the function signature to check just the type definition
|
|
164
|
+
const sigMatch = resourceCode.match(/export function createContact\(this: any,([^)]+)\)/s);
|
|
165
|
+
const signature = sigMatch ? sigMatch[1] : '';
|
|
166
|
+
const hasHeadersInType = signature.includes('headers');
|
|
167
|
+
assert(hasHeadersInType,
|
|
168
|
+
`Generated code destructures/accesses 'headers' from options but the function ` +
|
|
169
|
+
`signature's options type does not include a headers field. ` +
|
|
170
|
+
`Signature: ${signature.trim()}`);
|
|
171
|
+
} else {
|
|
172
|
+
// If headers is not used, that's also a valid fix (removing the reference)
|
|
173
|
+
assert(true);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
console.log(`\n${passed} passing / ${failed} failing`);
|
|
178
|
+
if (failed > 0) process.exit(1);
|
|
@@ -58,7 +58,7 @@ export default class Controller extends AbstractController {
|
|
|
58
58
|
* - total?: number - Total number of products
|
|
59
59
|
* - hasMore?: boolean - Whether there are more products available
|
|
60
60
|
*/
|
|
61
|
-
async productsGetProducts(options?: {limit?: number, category?: string, archived?: boolean}) {
|
|
61
|
+
async productsGetProducts(options?: {limit?: number, category?: string, archived?: boolean, headers?: Record<string, string>}) {
|
|
62
62
|
return this.products.getProducts(options);
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -85,7 +85,7 @@ export default class Controller extends AbstractController {
|
|
|
85
85
|
* - createdAt?: string - Creation timestamp
|
|
86
86
|
* - updatedAt?: string - Last update timestamp
|
|
87
87
|
*/
|
|
88
|
-
async productsCreateProduct(options: {name: string /** Product name */, description: string /** Product description */, price: number /** Product price */, category: string /** Product category */, tags: string[] /** Product tags
|
|
88
|
+
async productsCreateProduct(options: {name: string /** Product name */, description: string /** Product description */, price: number /** Product price */, category: string /** Product category */, tags: string[] /** Product tags */, headers?: Record<string, string>}) {
|
|
89
89
|
return this.products.createProduct(options);
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -108,8 +108,8 @@ export default class Controller extends AbstractController {
|
|
|
108
108
|
* - createdAt?: string - Creation timestamp
|
|
109
109
|
* - updatedAt?: string - Last update timestamp
|
|
110
110
|
*/
|
|
111
|
-
async productsGetProduct(productId: string) {
|
|
112
|
-
return this.products.getProduct(productId);
|
|
111
|
+
async productsGetProduct(productId: string, options?: {headers?: Record<string, string>}) {
|
|
112
|
+
return this.products.getProduct(productId, options);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
@@ -137,7 +137,7 @@ export default class Controller extends AbstractController {
|
|
|
137
137
|
* - createdAt?: string - Creation timestamp
|
|
138
138
|
* - updatedAt?: string - Last update timestamp
|
|
139
139
|
*/
|
|
140
|
-
async productsUpdateProduct(productId: string, options: {name: string /** Product name */, description: string /** Product description */, price: number /** Product price */, category: string /** Product category */, inStock: boolean /** Whether product is in stock */, tags: string[] /** Product tags
|
|
140
|
+
async productsUpdateProduct(productId: string, options: {name: string /** Product name */, description: string /** Product description */, price: number /** Product price */, category: string /** Product category */, inStock: boolean /** Whether product is in stock */, tags: string[] /** Product tags */, headers?: Record<string, string>}) {
|
|
141
141
|
return this.products.updateProduct(productId, options);
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -149,8 +149,8 @@ export default class Controller extends AbstractController {
|
|
|
149
149
|
*
|
|
150
150
|
* @returns {Promise<any>} DELETE /products/{productId} response
|
|
151
151
|
*/
|
|
152
|
-
async productsDeleteProduct(productId: string) {
|
|
153
|
-
return this.products.deleteProduct(productId);
|
|
152
|
+
async productsDeleteProduct(productId: string, options?: {headers?: Record<string, string>}) {
|
|
153
|
+
return this.products.deleteProduct(productId, options);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/**
|
|
@@ -168,7 +168,7 @@ export default class Controller extends AbstractController {
|
|
|
168
168
|
* - total?: number - Total number of orders
|
|
169
169
|
* - hasMore?: boolean - Whether there are more orders available
|
|
170
170
|
*/
|
|
171
|
-
async ordersGetOrders(options?: {status?: string, customerId?: string, limit?: number}) {
|
|
171
|
+
async ordersGetOrders(options?: {status?: string, customerId?: string, limit?: number, headers?: Record<string, string>}) {
|
|
172
172
|
return this.orders.getOrders(options);
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -194,7 +194,7 @@ export default class Controller extends AbstractController {
|
|
|
194
194
|
* - createdAt?: string - Order creation timestamp
|
|
195
195
|
* - updatedAt?: string - Last update timestamp
|
|
196
196
|
*/
|
|
197
|
-
async ordersCreateOrder(options: {customerId: string /** Customer who is placing the order */, items: OrderItemRequest[] /** Items to order */, shippingAddress: Address, billingAddress: Address}) {
|
|
197
|
+
async ordersCreateOrder(options: {customerId: string /** Customer who is placing the order */, items: OrderItemRequest[] /** Items to order */, shippingAddress: Address, billingAddress: Address, headers?: Record<string, string>}) {
|
|
198
198
|
return this.orders.createOrder(options);
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -217,8 +217,8 @@ export default class Controller extends AbstractController {
|
|
|
217
217
|
* - createdAt?: string - Order creation timestamp
|
|
218
218
|
* - updatedAt?: string - Last update timestamp
|
|
219
219
|
*/
|
|
220
|
-
async ordersGetOrder(orderId: string) {
|
|
221
|
-
return this.orders.getOrder(orderId);
|
|
220
|
+
async ordersGetOrder(orderId: string, options?: {headers?: Record<string, string>}) {
|
|
221
|
+
return this.orders.getOrder(orderId, options);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/**
|
|
@@ -242,7 +242,7 @@ export default class Controller extends AbstractController {
|
|
|
242
242
|
* - createdAt?: string - Order creation timestamp
|
|
243
243
|
* - updatedAt?: string - Last update timestamp
|
|
244
244
|
*/
|
|
245
|
-
async ordersUpdateOrderStatus(orderId: string, options: {status: string /** New order status */, trackingNumber: string /** Tracking number (for shipped status)
|
|
245
|
+
async ordersUpdateOrderStatus(orderId: string, options: {status: string /** New order status */, trackingNumber: string /** Tracking number (for shipped status) */, headers?: Record<string, string>}) {
|
|
246
246
|
return this.orders.updateOrderStatus(orderId, options);
|
|
247
247
|
}
|
|
248
248
|
|
|
@@ -265,7 +265,7 @@ export default class Controller extends AbstractController {
|
|
|
265
265
|
* - createdAt?: string - Order creation timestamp
|
|
266
266
|
* - updatedAt?: string - Last update timestamp
|
|
267
267
|
*/
|
|
268
|
-
async ordersCancelOrder(orderId: string) {
|
|
269
|
-
return this.orders.cancelOrder(orderId);
|
|
268
|
+
async ordersCancelOrder(orderId: string, options?: {headers?: Record<string, string>}) {
|
|
269
|
+
return this.orders.cancelOrder(orderId, options);
|
|
270
270
|
}
|
|
271
271
|
}
|
|
@@ -95,7 +95,7 @@ export interface UpdateOrderStatusRequest {
|
|
|
95
95
|
* - total?: number - Total number of orders
|
|
96
96
|
* - hasMore?: boolean - Whether there are more orders available
|
|
97
97
|
*/
|
|
98
|
-
export function getOrders(this: any, options?: {status?: string, customerId?: string, limit?: number}) {
|
|
98
|
+
export function getOrders(this: any, options?: {status?: string, customerId?: string, limit?: number, headers?: Record<string, string>}) {
|
|
99
99
|
options = options || {};
|
|
100
100
|
|
|
101
101
|
const url = '/orders';
|
|
@@ -142,7 +142,7 @@ export function getOrders(this: any, options?: {status?: string, customerId?: st
|
|
|
142
142
|
* - createdAt?: string - Order creation timestamp
|
|
143
143
|
* - updatedAt?: string - Last update timestamp
|
|
144
144
|
*/
|
|
145
|
-
export function createOrder(this: any, options: {customerId: string /** Customer who is placing the order */, items: OrderItemRequest[] /** Items to order */, shippingAddress: Address, billingAddress: Address}) {
|
|
145
|
+
export function createOrder(this: any, options: {customerId: string /** Customer who is placing the order */, items: OrderItemRequest[] /** Items to order */, shippingAddress: Address, billingAddress: Address, headers?: Record<string, string>}) {
|
|
146
146
|
options = options || {};
|
|
147
147
|
|
|
148
148
|
const url = '/orders';
|
|
@@ -178,7 +178,7 @@ export function createOrder(this: any, options: {customerId: string /** Customer
|
|
|
178
178
|
* - createdAt?: string - Order creation timestamp
|
|
179
179
|
* - updatedAt?: string - Last update timestamp
|
|
180
180
|
*/
|
|
181
|
-
export function getOrder(this: any, orderId: string) {
|
|
181
|
+
export function getOrder(this: any, orderId: string, options?: {headers?: Record<string, string>}) {
|
|
182
182
|
let url = '/orders/{orderId}';
|
|
183
183
|
if (orderId) {
|
|
184
184
|
url = url.replace('{orderId}', orderId);
|
|
@@ -210,7 +210,7 @@ export function getOrder(this: any, orderId: string) {
|
|
|
210
210
|
* - createdAt?: string - Order creation timestamp
|
|
211
211
|
* - updatedAt?: string - Last update timestamp
|
|
212
212
|
*/
|
|
213
|
-
export function updateOrderStatus(this: any, orderId: string, options: {status: string /** New order status */, trackingNumber: string /** Tracking number (for shipped status
|
|
213
|
+
export function updateOrderStatus(this: any, orderId: string, options: {status: string /** New order status */, trackingNumber: string /** Tracking number (for shipped status */, headers?: Record<string, string>})) {
|
|
214
214
|
options = options || {};
|
|
215
215
|
|
|
216
216
|
// Build URL with path parameters
|
|
@@ -250,7 +250,7 @@ export function updateOrderStatus(this: any, orderId: string, options: {status:
|
|
|
250
250
|
* - createdAt?: string - Order creation timestamp
|
|
251
251
|
* - updatedAt?: string - Last update timestamp
|
|
252
252
|
*/
|
|
253
|
-
export function cancelOrder(this: any, orderId: string) {
|
|
253
|
+
export function cancelOrder(this: any, orderId: string, options?: {headers?: Record<string, string>}) {
|
|
254
254
|
let url = '/orders/{orderId}/cancel';
|
|
255
255
|
if (orderId) {
|
|
256
256
|
url = url.replace('{orderId}', orderId);
|
|
@@ -76,7 +76,7 @@ export interface UpdateProductRequest {
|
|
|
76
76
|
* - total?: number - Total number of products
|
|
77
77
|
* - hasMore?: boolean - Whether there are more products available
|
|
78
78
|
*/
|
|
79
|
-
export function getProducts(this: any, options?: {limit?: number, category?: string, archived?: boolean}) {
|
|
79
|
+
export function getProducts(this: any, options?: {limit?: number, category?: string, archived?: boolean, headers?: Record<string, string>}) {
|
|
80
80
|
options = options || {};
|
|
81
81
|
|
|
82
82
|
const url = '/products';
|
|
@@ -124,7 +124,7 @@ export function getProducts(this: any, options?: {limit?: number, category?: str
|
|
|
124
124
|
* - createdAt?: string - Creation timestamp
|
|
125
125
|
* - updatedAt?: string - Last update timestamp
|
|
126
126
|
*/
|
|
127
|
-
export function createProduct(this: any, options: {name: string /** Product name */, description: string /** Product description */, price: number /** Product price */, category: string /** Product category */, tags: string[] /** Product tags
|
|
127
|
+
export function createProduct(this: any, options: {name: string /** Product name */, description: string /** Product description */, price: number /** Product price */, category: string /** Product category */, tags: string[] /** Product tags */, headers?: Record<string, string>}) {
|
|
128
128
|
options = options || {};
|
|
129
129
|
|
|
130
130
|
const url = '/products';
|
|
@@ -160,7 +160,7 @@ export function createProduct(this: any, options: {name: string /** Product name
|
|
|
160
160
|
* - createdAt?: string - Creation timestamp
|
|
161
161
|
* - updatedAt?: string - Last update timestamp
|
|
162
162
|
*/
|
|
163
|
-
export function getProduct(this: any, productId: string) {
|
|
163
|
+
export function getProduct(this: any, productId: string, options?: {headers?: Record<string, string>}) {
|
|
164
164
|
let url = '/products/{productId}';
|
|
165
165
|
if (productId) {
|
|
166
166
|
url = url.replace('{productId}', productId);
|
|
@@ -196,7 +196,7 @@ export function getProduct(this: any, productId: string) {
|
|
|
196
196
|
* - createdAt?: string - Creation timestamp
|
|
197
197
|
* - updatedAt?: string - Last update timestamp
|
|
198
198
|
*/
|
|
199
|
-
export function updateProduct(this: any, productId: string, options: {name: string /** Product name */, description: string /** Product description */, price: number /** Product price */, category: string /** Product category */, inStock: boolean /** Whether product is in stock */, tags: string[] /** Product tags
|
|
199
|
+
export function updateProduct(this: any, productId: string, options: {name: string /** Product name */, description: string /** Product description */, price: number /** Product price */, category: string /** Product category */, inStock: boolean /** Whether product is in stock */, tags: string[] /** Product tags */, headers?: Record<string, string>}) {
|
|
200
200
|
options = options || {};
|
|
201
201
|
|
|
202
202
|
// Build URL with path parameters
|
|
@@ -225,7 +225,7 @@ export function updateProduct(this: any, productId: string, options: {name: stri
|
|
|
225
225
|
*
|
|
226
226
|
* @returns {Promise<any>} DELETE /products/{productId} response
|
|
227
227
|
*/
|
|
228
|
-
export function deleteProduct(this: any, productId: string) {
|
|
228
|
+
export function deleteProduct(this: any, productId: string, options?: {headers?: Record<string, string>}) {
|
|
229
229
|
let url = '/products/{productId}';
|
|
230
230
|
if (productId) {
|
|
231
231
|
url = url.replace('{productId}', productId);
|
|
@@ -23,7 +23,7 @@ export default class Controller extends AbstractController {
|
|
|
23
23
|
*
|
|
24
24
|
* @returns {Promise<any>} GET /products response
|
|
25
25
|
*/
|
|
26
|
-
async getProducts() {
|
|
26
|
+
async getProducts(options?: {headers?: Record<string, string>}) {
|
|
27
27
|
const url = '/products';
|
|
28
28
|
|
|
29
29
|
const fetchOptions: any = {
|
|
@@ -41,7 +41,7 @@ export default class Controller extends AbstractController {
|
|
|
41
41
|
*
|
|
42
42
|
* @returns {Promise<any>} POST /products response
|
|
43
43
|
*/
|
|
44
|
-
async createProduct(options: {body?: any}) {
|
|
44
|
+
async createProduct(options: {body?: any, headers?: Record<string, string>}) {
|
|
45
45
|
options = options || {};
|
|
46
46
|
|
|
47
47
|
const url = '/products';
|
|
@@ -86,7 +86,7 @@ test('Simple Scenario - methods without options should be clean', () => {
|
|
|
86
86
|
const generator = new OpenAPIToConnector(spec, 'simple-test');
|
|
87
87
|
const actualOutput = generator.generateController();
|
|
88
88
|
|
|
89
|
-
expect(actualOutput).toInclude('async getProducts() {');
|
|
89
|
+
expect(actualOutput).toInclude('async getProducts(options?: {headers?: Record<string, string>}) {');
|
|
90
90
|
expect(actualOutput).toInclude('async createProduct(options:');
|
|
91
91
|
});
|
|
92
92
|
|
|
@@ -173,9 +173,9 @@ test('Path parameters and options should be handled correctly', () => {
|
|
|
173
173
|
const generator = new OpenAPIToConnector(spec, 'test-shop');
|
|
174
174
|
const actualOutput = generator.generateResourceClass('OrdersResource');
|
|
175
175
|
|
|
176
|
-
expect(actualOutput).toInclude('export function getOrder(this: any, orderId: string)');
|
|
176
|
+
expect(actualOutput).toInclude('export function getOrder(this: any, orderId: string, options?: {headers?: Record<string, string>})');
|
|
177
177
|
expect(actualOutput).toInclude('export function updateOrderStatus(this: any, orderId: string, options');
|
|
178
|
-
expect(actualOutput).toInclude('export function cancelOrder(this: any, orderId: string)');
|
|
178
|
+
expect(actualOutput).toInclude('export function cancelOrder(this: any, orderId: string, options?: {headers?: Record<string, string>})');
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
// Test 8: Exposed Methods in Main Controller
|
|
@@ -279,7 +279,7 @@ test('Simple methods should not have options parameter', () => {
|
|
|
279
279
|
const generator = new OpenAPIToConnector(simpleSpec, 'test');
|
|
280
280
|
const output = generator.generateController();
|
|
281
281
|
|
|
282
|
-
expect(output).toInclude('async getSimple() {');
|
|
282
|
+
expect(output).toInclude('async getSimple(options?: {headers?: Record<string, string>}) {');
|
|
283
283
|
expect(output).toNotInclude('options = options || {}');
|
|
284
284
|
expect(output).toNotInclude('headers: options');
|
|
285
285
|
});
|