diff --git a/src/main/java/com/twilio/oai/DirectoryStructureService.java b/src/main/java/com/twilio/oai/DirectoryStructureService.java index 3b9955635..99fb927a4 100644 --- a/src/main/java/com/twilio/oai/DirectoryStructureService.java +++ b/src/main/java/com/twilio/oai/DirectoryStructureService.java @@ -23,6 +23,7 @@ import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import static com.twilio.oai.common.ApplicationConstants.*; @@ -41,6 +42,7 @@ public class DirectoryStructureService { @Getter private boolean isVersionLess = false; private final Map productMap = new HashMap<>(); + private OpenAPI openAPI; private final List allModels = new ArrayList<>(); private final List dependentList = new ArrayList<>(); @@ -60,6 +62,11 @@ public static class DependentResource { private String listName; private boolean listWithPathParams; + // New fields for function overload support + private boolean hasInstanceOperation; + private String instanceParam; + private String instanceType; + public boolean getHasPathParams() { return pathParams != null && !pathParams.isEmpty(); } @@ -78,10 +85,15 @@ public static class ContextResource { } public void configure(final OpenAPI openAPI) { + this.openAPI = openAPI; final Map versionResources = getVersionResourcesMap(); Map pathsToSkipMap = new HashMap<>(); isVersionLess = additionalProperties.getOrDefault(API_VERSION, "").equals(""); + additionalProperties.put("isVersionLessSpec", isVersionLess ? "true" : "false"); + + final boolean isV1ApiSpec = ResourceCacheContext.get() != null && ResourceCacheContext.get().isV1(); + additionalProperties.put("isV1ApiSpec", isV1ApiSpec ? "true" : "false"); openAPI.getPaths().forEach(resourceTree::addResource); openAPI.getPaths().forEach((name, path) -> { @@ -104,7 +116,7 @@ public void configure(final OpenAPI openAPI) { operation.addTagsItem(tag); if (!tag.contains(PATH_SEPARATOR_PLACEHOLDER)) { - final DependentResource dependent = generateDependent(name, operation); + final DependentResource dependent = generateDependent(name, path, operation); final boolean isIgnoredOperation = Optional.ofNullable(operation.getExtensions()) .map(ext -> ext.get(IGNORE_EXTENSION_NAME)) .map(Boolean.class::cast) @@ -137,15 +149,113 @@ public void configure(final OpenAPI openAPI) { } public void addVersionResources(DependentResource dependent, Map versionResources) { + final boolean isV1ApiSpec = "true".equals(additionalProperties.get("isV1ApiSpec")); if (versionResources.containsKey(dependent.getFilename())) { DependentResource existingDependent = versionResources.get(dependent.getFilename()); - if (existingDependent.getPathParams().size() == 0) + // For v1Api specs: also replace when new entry has listWithPathParams=true and existing + // does not — handles instance path processed before list path (spec ordering issue) + if (existingDependent.getPathParams().isEmpty() + || (isV1ApiSpec && dependent.isListWithPathParams() && !existingDependent.isListWithPathParams())) { versionResources.put(dependent.getFilename(), dependent); + } } else { versionResources.put(dependent.getFilename(), dependent); } } + /** + * Enriches version resources with instance operation detection. + * For resources that have both list and instance operations, sets: + * - hasInstanceOperation: true + * - instanceParam: the instance parameter name (e.g., "summaryId") + * - instanceType: the Context type name (e.g., "ConversationSummaryContext") + */ + private void enrichVersionResourcesWithInstanceOperations(List versionResources) { + for (DependentResource resource : versionResources) { + // Find matching resource in resourceTree to check for instance operations + Optional resourceOptional = StreamSupport.stream( + resourceTree.getResources().spliterator(), false) + .filter(r -> caseResolver.filenameOperation(r.getResourceAliases().getClassName()).equals(resource.getFilename())) + .findFirst(); + + if (resourceOptional.isEmpty()) { + continue; + } + + Resource treeResource = resourceOptional.get(); + + // Check if this resource has instance operations by looking for pathType: instance + boolean hasInstanceOp = openAPI.getPaths().entrySet().stream() + .filter(entry -> { + String pathKey = entry.getKey(); + PathItem pathItem = entry.getValue(); + // Check if this path belongs to the same resource + return pathItem.readOperations().stream() + .anyMatch(op -> { + String tag = String.join(PATH_SEPARATOR_PLACEHOLDER, resourceTree.ancestors(pathKey, op)); + // Compare the tag (converted to filename format) with the resource filename + String tagFilename = caseResolver.filenameOperation(tag); + return tagFilename.equals(resource.getFilename()); + }); + }) + .anyMatch(entry -> { + PathItem pathItem = entry.getValue(); + Optional pathType = PathUtils.getTwilioExtension(pathItem, "pathType"); + return pathType.isPresent() && pathType.get().equals("instance"); + }); + + if (hasInstanceOp && resource.isListWithPathParams()) { + resource.setHasInstanceOperation(true); + + // Find the instance parameter by looking at instance paths + String instanceParam = openAPI.getPaths().entrySet().stream() + .filter(entry -> { + PathItem pathItem = entry.getValue(); + Optional pathType = PathUtils.getTwilioExtension(pathItem, "pathType"); + if (pathType.isEmpty() || !pathType.get().equals("instance")) { + return false; + } + + return pathItem.readOperations().stream() + .anyMatch(op -> { + String pathKey = entry.getKey(); + String tag = String.join(PATH_SEPARATOR_PLACEHOLDER, resourceTree.ancestors(pathKey, op)); + String tagFilename = caseResolver.filenameOperation(tag); + return tagFilename.equals(resource.getFilename()); + }); + }) + .findFirst() + .map(entry -> { + // Get the last path parameter from instance path + String path = entry.getKey(); + List allParams = Optional.ofNullable(entry.getValue().readOperations().get(0).getParameters()) + .orElse(Collections.emptyList()); + + List pathParams = allParams.stream() + .filter(p -> "path".equals(p.getIn())) + .collect(Collectors.toList()); + + // The instance param is the one not in listWithPathParams + Set listParamNames = resource.getPathParams().stream() + .map(Parameter::getName) + .collect(Collectors.toSet()); + + return pathParams.stream() + .map(Parameter::getName) + .filter(name -> !listParamNames.contains(name)) + .findFirst() + .orElse(null); + }) + .orElse(null); + + if (instanceParam != null) { + resource.setInstanceParam(caseResolver.pathOperation(instanceParam)); + resource.setInstanceType(resource.getResourceName() + "Context"); + } + } + } + } + @SuppressWarnings("unchecked") public Map getVersionResourcesMap() { return (Map) additionalProperties.computeIfAbsent(ALL_VERSION_RESOURCES, @@ -183,8 +293,12 @@ private Stream getParamStream(final Operation operation) { } public DependentResource generateDependent(final String path, final Operation operation) { + return generateDependent(path, null, operation); + } + + public DependentResource generateDependent(final String path, final PathItem pathItem, final Operation operation) { final Resource.Aliases resourceAliases = getResourceAliases(path, operation); - List params = fetchNonParentPathParams(operation); + List params = fetchNonParentPathParams(pathItem, operation); return new DependentResource.DependentResourceBuilder() .version(PathUtils.getFirstPathPart(path)) .type(resourceAliases.getClassName() + LIST_INSTANCE) @@ -225,10 +339,38 @@ public void addContextdependents(final List resourceList, final String p } private List fetchNonParentPathParams(Operation operation) { + return fetchNonParentPathParams(null, operation); + } + + private List fetchNonParentPathParams(PathItem pathItem, Operation operation) { List params = new ArrayList<>(); if (null == operation) return params; - List pathParams = Optional.ofNullable(operation.getParameters()) - .stream().flatMap(Collection::stream) + + // For v1Api specs: merge path-item level params (which may use $ref components) with + // operation-level params so nested list resources can detect their path parameters. + final boolean isV1ApiSpec = "true".equals(additionalProperties.get("isV1ApiSpec")); + List allParams = new ArrayList<>(); + if (isV1ApiSpec && pathItem != null && pathItem.getParameters() != null) { + pathItem.getParameters().stream() + .map(this::resolveParameterRef) + .filter(Objects::nonNull) + .forEach(allParams::add); + } + if (operation.getParameters() != null) { + Set operationParamNames = operation.getParameters().stream() + .map(this::resolveParameterRef) + .filter(Objects::nonNull) + .filter(p -> p.getName() != null) + .map(Parameter::getName) + .collect(Collectors.toSet()); + allParams.removeIf(p -> p.getName() != null && operationParamNames.contains(p.getName())); + operation.getParameters().stream() + .map(this::resolveParameterRef) + .filter(Objects::nonNull) + .forEach(allParams::add); + } + + List pathParams = allParams.stream() .filter(param -> Objects.nonNull(param.getIn())).filter(PathUtils::isPathParam) .collect(Collectors.toList()); params = pathParams.stream().filter(parameter -> Objects.isNull(parameter.getExtensions())) @@ -239,6 +381,26 @@ private List fetchNonParentPathParams(Operation operation) { return params; } + /** + * Resolves a $ref parameter stub to its full definition from openAPI components. + * Returns the parameter as-is if it is already fully defined (no $ref). + */ + private Parameter resolveParameterRef(Parameter param) { + if (param == null) return null; + if (param.get$ref() != null && param.getIn() == null) { + // extract component name from e.g. "#/components/parameters/StoreId" + String ref = param.get$ref(); + String componentName = ref.substring(ref.lastIndexOf('/') + 1); + if (openAPI != null + && openAPI.getComponents() != null + && openAPI.getComponents().getParameters() != null) { + return openAPI.getComponents().getParameters().get(componentName); + } + return null; + } + return param; + } + private Resource.Aliases getResourceAliases(final String path, final Operation operation) { return resourceTree .findResource(path) @@ -293,6 +455,10 @@ public List processOperations(final OperationsMap results) { .stream() .filter(resource -> resource.getVersion().equals(version)) .collect(Collectors.toList()); + + // Detect instance operations for each version resource + enrichVersionResourcesWithInstanceOperations(versionResources); + additionalProperties.put(VERSION_RESOURCES, versionResources); if (((String)additionalProperties.get(API_VERSION)).equalsIgnoreCase("v2010")) { diff --git a/src/main/java/com/twilio/oai/TwilioNodeGenerator.java b/src/main/java/com/twilio/oai/TwilioNodeGenerator.java index bb36a6b93..b92810c63 100644 --- a/src/main/java/com/twilio/oai/TwilioNodeGenerator.java +++ b/src/main/java/com/twilio/oai/TwilioNodeGenerator.java @@ -132,9 +132,17 @@ public String getHelp() { @Override public String toParamName(final String name) { - return Arrays + String paramName = Arrays .stream(twilioCodegen.toParamName(name).split("\\.")) .map(input -> StringHelper.camelize(input, true)) .collect(Collectors.joining(".")); + + // Rename 'version' path param to avoid collision with Version instance variable + // Used in generated code like: function VersionListInstance(version: V3, id: string) + if ("version".equals(paramName)) { + return "versionParam"; + } + + return paramName; } } diff --git a/src/main/java/com/twilio/oai/api/ApiResourceBuilder.java b/src/main/java/com/twilio/oai/api/ApiResourceBuilder.java index f47fbdc14..5bcc6ad31 100644 --- a/src/main/java/com/twilio/oai/api/ApiResourceBuilder.java +++ b/src/main/java/com/twilio/oai/api/ApiResourceBuilder.java @@ -235,6 +235,24 @@ protected Map mapOperation(CodegenOperation operation) { addOperationName(operation, Operation.FETCH.getValue()); } else if (StringUtils.startsWithIgnoreCase(operation.operationId, "list")) { addOperationName(operation, Operation.READ.getValue()); + // Add flag to indicate if pagination is supported (for Node.js conditional pagination) + boolean supportsPagination = hasMetaInResponse(operation); + operationMap.put("x-supports-pagination", supportsPagination); + + // Check if array items are primitives (strings, numbers) vs objects + boolean isPrimitiveArray = isRecordKeyArrayOfPrimitives(operation); + + // For non-paginated endpoints with primitive arrays: return string[] directly + boolean arrayItemsArePrimitives = !supportsPagination && isPrimitiveArray; + operationMap.put("x-array-items-are-primitives", arrayItemsArePrimitives); + + // For paginated endpoints with primitive arrays: Page exists but instances are primitives + boolean paginatedPrimitiveItems = supportsPagination && isPrimitiveArray; + operationMap.put("x-paginated-primitive-items", paginatedPrimitiveItems); + // Also add to metaAPIProperties so it's available at resource level + if (paginatedPrimitiveItems) { + metaAPIProperties.put("x-paginated-primitive-items", true); + } } else if (StringUtils.startsWithIgnoreCase(operation.operationId, "patch")) { addOperationName(operation, Operation.PATCH.getValue()); } @@ -316,6 +334,75 @@ public boolean hasPaginationOperation() { return codegenOperationList.stream().anyMatch(co -> co.operationId.toLowerCase().startsWith("list")); } + /** + * Check if any operation in this resource supports pagination (has meta in response). + * This is used for conditional Page class generation in Node.js. + * + * @return true if at least one operation has x-supports-pagination vendor extension set to true + */ + public boolean hasAnyOperationSupportingPagination() { + return codegenOperationList.stream() + .anyMatch(co -> Boolean.TRUE.equals(co.vendorExtensions.get("x-supports-pagination"))); + } + + /** + * Check if a read operation supports pagination by verifying the response schema has a 'meta' property. + * This is used to conditionally generate pagination methods (page, each, getPage) for Node.js. + * + * @param operation The CodegenOperation to check + * @return true if the operation's response schema contains a 'meta' property, false otherwise + */ + protected boolean hasMetaInResponse(CodegenOperation operation) { + // Only check for read (list) operations + if (!operation.operationId.toLowerCase().startsWith("list")) { + return false; + } + + // Check if the response schema has a 'meta' property + // The response model should contain a 'meta' field alongside the data array + Optional responseModel = getModelByClassname(operation.returnBaseType); + + if (responseModel.isPresent()) { + return responseModel.get().getAllVars().stream() + .anyMatch(var -> var.baseName.equals("meta")); + } + + return false; + } + + /** + * Check if the recordKey array contains primitive values (strings, numbers) instead of objects. + * This is needed to generate correct code for endpoints like Profile Imports that return + * arrays of ImportID strings rather than ImportInstance objects. + * + * @param operation The list operation to check + * @return true if the array items are primitives, false if they are objects + */ + protected boolean isRecordKeyArrayOfPrimitives(CodegenOperation operation) { + // Only check for read (list) operations + if (!operation.operationId.toLowerCase().startsWith("list") || recordKey == null) { + return false; + } + + // Get the response model + Optional responseModel = getModelByClassname(operation.returnBaseType); + + if (responseModel.isPresent()) { + // Find the recordKey property in the response model + Optional recordKeyProp = responseModel.get().getAllVars().stream() + .filter(var -> var.baseName.equals(recordKey)) + .findFirst(); + + if (recordKeyProp.isPresent()) { + // If complexType is null, it means the array contains primitives (strings, numbers, etc.) + // If complexType is present, the array contains objects + return recordKeyProp.get().getComplexType() == null; + } + } + + return false; + } + public String getApiName() { final List filePathArray = new ArrayList<>(Arrays.asList(codegenOperationList.get(0).baseName.split( PATH_SEPARATOR_PLACEHOLDER))); diff --git a/src/main/java/com/twilio/oai/api/ApiResources.java b/src/main/java/com/twilio/oai/api/ApiResources.java index 0b982ef67..4bd939f2d 100644 --- a/src/main/java/com/twilio/oai/api/ApiResources.java +++ b/src/main/java/com/twilio/oai/api/ApiResources.java @@ -24,6 +24,8 @@ public class ApiResources { Map metaProperties; String namespaceSubPart; boolean hasPaginationOperation; + boolean hasOperationWithPagination; + boolean hasPaginatedPrimitiveItems; Set nestedModels; private Boolean isApiV1 = null; // true or NULL @@ -38,10 +40,16 @@ public ApiResources(ApiResourceBuilder apiResourceBuilder) { requiredPathParams = new ArrayList<>(apiResourceBuilder.requiredPathParams); namespaceSubPart = apiResourceBuilder.namespaceSubPart; hasPaginationOperation = apiResourceBuilder.hasPaginationOperation(); + hasOperationWithPagination = apiResourceBuilder.hasAnyOperationSupportingPagination(); + hasPaginatedPrimitiveItems = Boolean.TRUE.equals(metaProperties.get("x-paginated-primitive-items")); nestedModels = apiResourceBuilder.nestedModels; if (ResourceCacheContext.get() != null && ResourceCacheContext.get().isV1()) { isApiV1 = true; } responseInstanceModels = apiResourceBuilder.responseInstanceModels; } + + public boolean getHasPaginatedPrimitiveItems() { + return hasPaginatedPrimitiveItems; + } } diff --git a/src/main/java/com/twilio/oai/api/FluentApiResourceBuilder.java b/src/main/java/com/twilio/oai/api/FluentApiResourceBuilder.java index 207851d52..d803fffbe 100644 --- a/src/main/java/com/twilio/oai/api/FluentApiResourceBuilder.java +++ b/src/main/java/com/twilio/oai/api/FluentApiResourceBuilder.java @@ -23,6 +23,8 @@ import org.openapitools.codegen.CodegenParameter; import org.openapitools.codegen.CodegenProperty; +import com.twilio.oai.java.cache.ResourceCacheContext; +import static com.twilio.oai.common.ApplicationConstants.IS_PARENT_PARAM_EXTENSION_NAME; import static com.twilio.oai.common.ApplicationConstants.STRING; import static com.twilio.oai.template.AbstractApiActionTemplate.API_TEMPLATE; @@ -109,6 +111,20 @@ public ApiResourceBuilder updateOperations(final Resolver code instancePathParams.add(param); } } + + // For v1Api specs only: mark params shared between list and instance paths as parent params. + // These are already provided when the list instance is constructed, so they + // should not appear in the instance context callable (e.g. get(profileId) not get(storeId, profileId)). + // Scoped to v1Api to avoid breaking existing non-v1Api SDKs. + final boolean isApiV1 = ResourceCacheContext.get() != null && ResourceCacheContext.get().isV1(); + if (isApiV1) { + final Set listParamNames = listPathParams.stream() + .map(p -> p.paramName) + .collect(Collectors.toSet()); + instancePathParams.stream() + .filter(p -> listParamNames.contains(p.paramName)) + .forEach(p -> p.vendorExtensions.put(IS_PARENT_PARAM_EXTENSION_NAME, true)); + } } return this; @@ -229,6 +245,10 @@ protected String getDataTypeName(final String dataType) { @Override public FluentApiResources build() { + // Propagate paginated primitive items flag to responseModel vendor extensions + if (Boolean.TRUE.equals(metaAPIProperties.get("x-paginated-primitive-items")) && responseModel != null) { + responseModel.vendorExtensions.put("x-paginated-primitive-items", true); + } return new FluentApiResources(this); } diff --git a/src/main/java/com/twilio/oai/api/FluentApiResources.java b/src/main/java/com/twilio/oai/api/FluentApiResources.java index a28a42ffc..f7355872b 100644 --- a/src/main/java/com/twilio/oai/api/FluentApiResources.java +++ b/src/main/java/com/twilio/oai/api/FluentApiResources.java @@ -67,6 +67,27 @@ public Collection getResponseInstanceModels() { return responseInstanceModels; } + /** + * Returns all unique vars from all response instance models combined. + * Used for generating Instance class that needs to handle all possible fields across different response types. + * Vars are deduplicated by name - if multiple models have the same var name, only the first is kept. + */ + public List getAllResponseVars() { + if (!getHasMultipleResponseModels()) { + return responseModel != null ? responseModel.getVars() : new java.util.ArrayList<>(); + } + + java.util.Map uniqueVars = new java.util.LinkedHashMap<>(); + for (org.openapitools.codegen.CodegenModel model : responseInstanceModels) { + for (org.openapitools.codegen.CodegenProperty var : model.getVars()) { + if (!uniqueVars.containsKey(var.getName())) { + uniqueVars.put(var.getName(), var); + } + } + } + return new java.util.ArrayList<>(uniqueVars.values()); + } + /** * Returns the collection of nested/complex models used in the resource. * These are models referenced within response models or parameters. diff --git a/src/main/java/com/twilio/oai/api/NodeApiResourceBuilder.java b/src/main/java/com/twilio/oai/api/NodeApiResourceBuilder.java index 5d7ddc2b4..4bc36f93a 100644 --- a/src/main/java/com/twilio/oai/api/NodeApiResourceBuilder.java +++ b/src/main/java/com/twilio/oai/api/NodeApiResourceBuilder.java @@ -8,10 +8,12 @@ import com.twilio.oai.resolver.node.NodeCodegenModelResolver; import com.twilio.oai.template.IApiActionTemplate; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -69,8 +71,16 @@ public ApiResourceBuilder updateOperations(final Resolver code co.produces = null; } } else if (co.nickname.startsWith("list")) { - addOperationName(co, "Page"); - co.returnType = apiName + "Page"; + // Check if this list operation supports pagination (has meta in response) + boolean supportsPagination = (Boolean) co.vendorExtensions.getOrDefault("x-supports-pagination", true); + if (supportsPagination) { + addOperationName(co, "Page"); + co.returnType = apiName + "Page"; + } else { + // For non-paginated list operations, return array of instances directly + addOperationName(co, "List"); + co.returnType = resourceName + "[]"; + } } else { co.returnType = resourceName; } @@ -95,6 +105,7 @@ public ApiResourceBuilder updateResponseModel(Resolver codegenP .stream() .flatMap(co -> co.responses .stream() + .filter(response -> !isApiV1 || (response.code != null && response.code.startsWith("2"))) .map(response -> response.dataType) .filter(Objects::nonNull) .flatMap(modelName -> getModel(modelName, co).stream()) @@ -179,6 +190,7 @@ public ApiResourceBuilder updateResponseModel(Resolver codegenP return this; } + @Override public ApiResourceBuilder updateModel(Resolver codegenModelResolver) { super.updateModel(codegenModelResolver); diff --git a/src/main/java/com/twilio/oai/template/AbstractApiActionTemplate.java b/src/main/java/com/twilio/oai/template/AbstractApiActionTemplate.java index 41ae9a600..710ba0668 100644 --- a/src/main/java/com/twilio/oai/template/AbstractApiActionTemplate.java +++ b/src/main/java/com/twilio/oai/template/AbstractApiActionTemplate.java @@ -220,7 +220,10 @@ public void addSupportVersion() { final String fileExtension = templateStrings.get(1); final String apiVersionClass = codegen.additionalProperties().get("apiVersionClass").toString(); - if (apiVersionClass.startsWith("V")) { + final boolean isVersionLess = "true".equals( + String.valueOf(codegen.additionalProperties().getOrDefault("isVersionLessSpec", "false"))); + + if (!isVersionLess) { codegen .supportingFiles() .add(new SupportingFile(templateName, diff --git a/src/main/resources/twilio-node/api-single.mustache b/src/main/resources/twilio-node/api-single.mustache index 15c8d44a2..f7c830df0 100644 --- a/src/main/resources/twilio-node/api-single.mustache +++ b/src/main/resources/twilio-node/api-single.mustache @@ -2,10 +2,10 @@ {{#resources}} import { inspect, InspectOptions } from "util"; -{{#hasPaginationOperation}}{{#isApiV1}}import TokenPage, { TokenPaginationPayload } from "{{apiVersionPath}}/../../base/TokenPage";{{/isApiV1}}{{^isApiV1}} +{{#hasOperationWithPagination}}{{#isApiV1}}import TokenPage, { TokenPaginationPayload } from "{{apiVersionPath}}/../../base/TokenPage";{{/isApiV1}}{{^isApiV1}} import Page, { TwilioResponsePayload } from "{{apiVersionPath}}/../../base/Page";{{/isApiV1}} import Response from "{{apiVersionPath}}/../../http/response"; -{{/hasPaginationOperation}} +{{/hasOperationWithPagination}} import {{apiVersionClass}} from "{{apiVersionPath}}/{{apiVersionClass}}"; const deserialize = require("{{apiVersionPath}}/../../base/deserialize"); const serialize = require("{{apiVersionPath}}/../../base/serialize"); @@ -122,6 +122,7 @@ export class {{apiName}}ContextImpl implements {{apiName}}Context { } {{#hasMultipleResponseModels}} {{#models}} +{{^isEnum}} /** * Nested model for {{name}} */ @@ -131,6 +132,7 @@ export interface {{name}} { {{/vars}} } +{{/isEnum}} {{/models}} {{/hasMultipleResponseModels}} @@ -213,14 +215,16 @@ export function {{apiName}}ListInstance(version: {{apiVersionClass}}{{#listPathP {{#operations}} {{^vendorExtensions.x-twilio.ignoreOperation}} {{#vendorExtensions.x-is-list-operation}} - instance.{{vendorExtensions.x-name-lower}} = function {{vendorExtensions.x-name-lower}}({{#hasParams}}params{{^hasRequiredParams}}?{{/hasRequiredParams}}: {{#bodyParam}}{{dataType}}{{/bodyParam}}{{^bodyParam}}{{vendorExtensions.x-resource-name}}{{vendorExtensions.x-name}}Options{{/bodyParam}}{{^hasRequiredParams}} | ((error: Error | null, items: {{returnType}}) => any){{/hasRequiredParams}}{{#bodyParams.0}}, headers?: any{{/bodyParams.0}},{{/hasParams}} callback?: (error: Error | null, items: {{returnType}}) => any): Promise<{{returnType}}> { -{{>operation}} + {{#vendorExtensions.x-array-items-are-primitives}}instance.{{vendorExtensions.x-name-lower}} = function {{vendorExtensions.x-name-lower}}({{#hasParams}}params{{^hasRequiredParams}}?{{/hasRequiredParams}}: {{#bodyParam}}{{dataType}}{{/bodyParam}}{{^bodyParam}}{{vendorExtensions.x-resource-name}}{{vendorExtensions.x-name}}Options{{/bodyParam}}{{^hasRequiredParams}} | ((error: Error | null, items: string[]) => any){{/hasRequiredParams}}{{#bodyParams.0}}, headers?: any{{/bodyParams.0}},{{/hasParams}} callback?: (error: Error | null, items: string[]) => any): Promise { +{{/vendorExtensions.x-array-items-are-primitives}}{{^vendorExtensions.x-array-items-are-primitives}}instance.{{vendorExtensions.x-name-lower}} = function {{vendorExtensions.x-name-lower}}({{#hasParams}}params{{^hasRequiredParams}}?{{/hasRequiredParams}}: {{#bodyParam}}{{dataType}}{{/bodyParam}}{{^bodyParam}}{{vendorExtensions.x-resource-name}}{{vendorExtensions.x-name}}Options{{/bodyParam}}{{^hasRequiredParams}} | ((error: Error | null, items: {{returnType}}) => any){{/hasRequiredParams}}{{#bodyParams.0}}, headers?: any{{/bodyParams.0}},{{/hasParams}} callback?: (error: Error | null, items: {{returnType}}) => any): Promise<{{returnType}}> { +{{/vendorExtensions.x-array-items-are-primitives}}{{>operation}} {{^vendorExtensions.x-is-read-operation}} } {{/vendorExtensions.x-is-read-operation}} - instance.{{vendorExtensions.x-name-lower}}WithHttpInfo = function {{vendorExtensions.x-name-lower}}WithHttpInfo({{#hasParams}}params{{^hasRequiredParams}}?{{/hasRequiredParams}}: {{#bodyParam}}{{dataType}}{{/bodyParam}}{{^bodyParam}}{{vendorExtensions.x-resource-name}}{{vendorExtensions.x-name}}Options{{/bodyParam}}{{^hasRequiredParams}} | ((error: Error | null, items: ApiResponse<{{returnType}}>) => any){{/hasRequiredParams}}{{#bodyParams.0}}, headers?: any{{/bodyParams.0}},{{/hasParams}} callback?: (error: Error | null, items: ApiResponse<{{returnType}}>) => any): Promise> { -{{>operationWithHttpInfo}} + {{#vendorExtensions.x-array-items-are-primitives}}instance.{{vendorExtensions.x-name-lower}}WithHttpInfo = function {{vendorExtensions.x-name-lower}}WithHttpInfo({{#hasParams}}params{{^hasRequiredParams}}?{{/hasRequiredParams}}: {{#bodyParam}}{{dataType}}{{/bodyParam}}{{^bodyParam}}{{vendorExtensions.x-resource-name}}{{vendorExtensions.x-name}}Options{{/bodyParam}}{{^hasRequiredParams}} | ((error: Error | null, items: ApiResponse) => any){{/hasRequiredParams}}{{#bodyParams.0}}, headers?: any{{/bodyParams.0}},{{/hasParams}} callback?: (error: Error | null, items: ApiResponse) => any): Promise> { +{{/vendorExtensions.x-array-items-are-primitives}}{{^vendorExtensions.x-array-items-are-primitives}}instance.{{vendorExtensions.x-name-lower}}WithHttpInfo = function {{vendorExtensions.x-name-lower}}WithHttpInfo({{#hasParams}}params{{^hasRequiredParams}}?{{/hasRequiredParams}}: {{#bodyParam}}{{dataType}}{{/bodyParam}}{{^bodyParam}}{{vendorExtensions.x-resource-name}}{{vendorExtensions.x-name}}Options{{/bodyParam}}{{^hasRequiredParams}} | ((error: Error | null, items: ApiResponse<{{returnType}}>) => any){{/hasRequiredParams}}{{#bodyParams.0}}, headers?: any{{/bodyParams.0}},{{/hasParams}} callback?: (error: Error | null, items: ApiResponse<{{returnType}}>) => any): Promise> { +{{/vendorExtensions.x-array-items-are-primitives}}{{>operationWithHttpInfo}} {{^vendorExtensions.x-is-read-operation}} } {{/vendorExtensions.x-is-read-operation}} @@ -239,5 +243,5 @@ export function {{apiName}}ListInstance(version: {{apiVersionClass}}{{#listPathP return instance; } {{^instancePath}}{{>responseModel}}{{/instancePath}} -{{#hasPaginationOperation}}{{#isApiV1}}{{>tokenPagination}}{{/isApiV1}}{{^isApiV1}}{{>pagination}}{{/isApiV1}}{{/hasPaginationOperation}} +{{#hasOperationWithPagination}}{{#isApiV1}}{{>tokenPagination}}{{/isApiV1}}{{^isApiV1}}{{>pagination}}{{/isApiV1}}{{/hasOperationWithPagination}} {{/resources}} diff --git a/src/main/resources/twilio-node/listInterfaceFunctions.mustache b/src/main/resources/twilio-node/listInterfaceFunctions.mustache index f51464aa7..775983335 100644 --- a/src/main/resources/twilio-node/listInterfaceFunctions.mustache +++ b/src/main/resources/twilio-node/listInterfaceFunctions.mustache @@ -1,3 +1,32 @@ + /** + * Lists {{instanceName}} records from the API as a list. + * + * If a function is passed as the first argument, it will be used as the callback + * function. + * + * @param { {{vendorExtensions.x-resource-name}}Options } [params] - Options for request + * @param { function } [callback] - Callback to handle list of records + */ + {{#vendorExtensions.x-array-items-are-primitives}}{{^hasRequiredParams}}list(callback?: (error: Error | null, items: string[]) => any): Promise;{{/hasRequiredParams}} + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}}list(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: string[]) => any): Promise;{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}}list(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: string[]) => any): Promise;{{/isApiV1}}{{/vendorExtensions.x-array-items-are-primitives}}{{#vendorExtensions.x-paginated-primitive-items}}{{^hasRequiredParams}}list(callback?: (error: Error | null, items: string[]) => any): Promise;{{/hasRequiredParams}} + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}}list(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: string[]) => any): Promise;{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}}list(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: string[]) => any): Promise;{{/isApiV1}}{{/vendorExtensions.x-paginated-primitive-items}}{{^vendorExtensions.x-array-items-are-primitives}}{{^vendorExtensions.x-paginated-primitive-items}}{{^hasRequiredParams}}list(callback?: (error: Error | null, items: {{instanceName}}[]) => any): Promise<{{instanceName}}[]>;{{/hasRequiredParams}} + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}}list(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: {{instanceName}}[]) => any): Promise<{{instanceName}}[]>;{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}}list(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: {{instanceName}}[]) => any): Promise<{{instanceName}}[]>;{{/isApiV1}}{{/vendorExtensions.x-paginated-primitive-items}}{{/vendorExtensions.x-array-items-are-primitives}} + /** + * Lists {{instanceName}} records from the API as a list with HTTP metadata. + * + * Returns all records along with HTTP metadata from the first page fetched. + * + * If a function is passed as the first argument, it will be used as the callback + * function. + * + * @param { {{vendorExtensions.x-resource-name}}Options } [params] - Options for request + * @param { function } [callback] - Callback to handle list of records with metadata + */ + {{#vendorExtensions.x-array-items-are-primitives}}{{^hasRequiredParams}}listWithHttpInfo(callback?: (error: Error | null, items: ApiResponse) => any): Promise>;{{/hasRequiredParams}} + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}}listWithHttpInfo(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: ApiResponse) => any): Promise>;{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}}listWithHttpInfo(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: ApiResponse) => any): Promise>;{{/isApiV1}}{{/vendorExtensions.x-array-items-are-primitives}}{{#vendorExtensions.x-paginated-primitive-items}}{{^hasRequiredParams}}listWithHttpInfo(callback?: (error: Error | null, items: ApiResponse) => any): Promise>;{{/hasRequiredParams}} + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}}listWithHttpInfo(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: ApiResponse) => any): Promise>;{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}}listWithHttpInfo(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: ApiResponse) => any): Promise>;{{/isApiV1}}{{/vendorExtensions.x-paginated-primitive-items}}{{^vendorExtensions.x-array-items-are-primitives}}{{^vendorExtensions.x-paginated-primitive-items}}{{^hasRequiredParams}}listWithHttpInfo(callback?: (error: Error | null, items: ApiResponse<{{instanceName}}[]>) => any): Promise>;{{/hasRequiredParams}} + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}}listWithHttpInfo(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: ApiResponse<{{instanceName}}[]>) => any): Promise>;{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}}listWithHttpInfo(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: ApiResponse<{{instanceName}}[]>) => any): Promise>;{{/isApiV1}}{{/vendorExtensions.x-paginated-primitive-items}}{{/vendorExtensions.x-array-items-are-primitives}} + {{#vendorExtensions.x-supports-pagination}} /** * Streams {{instanceName}} records from the API. * @@ -13,8 +42,8 @@ * @param { {{vendorExtensions.x-resource-name}}EachOptions } [params] - Options for request * @param { function } [callback] - Function to process each record */ - {{^hasRequiredParams}}each(callback?: (item: {{instanceName}}, done: (err?: Error) => void) => void): void;{{/hasRequiredParams}} - each(params: {{vendorExtensions.x-resource-name}}EachOptions, callback?: (item: {{instanceName}}, done: (err?: Error) => void) => void): void; + {{^hasRequiredParams}}each(callback?: (item: {{#vendorExtensions.x-paginated-primitive-items}}string{{/vendorExtensions.x-paginated-primitive-items}}{{^vendorExtensions.x-paginated-primitive-items}}{{instanceName}}{{/vendorExtensions.x-paginated-primitive-items}}, done: (err?: Error) => void) => void): void;{{/hasRequiredParams}} + each(params: {{vendorExtensions.x-resource-name}}EachOptions, callback?: (item: {{#vendorExtensions.x-paginated-primitive-items}}string{{/vendorExtensions.x-paginated-primitive-items}}{{^vendorExtensions.x-paginated-primitive-items}}{{instanceName}}{{/vendorExtensions.x-paginated-primitive-items}}, done: (err?: Error) => void) => void): void; /** * Streams {{instanceName}} records from the API with HTTP metadata captured per page. * @@ -30,8 +59,8 @@ * @param { {{vendorExtensions.x-resource-name}}EachOptions } [params] - Options for request * @param { function } [callback] - Function to process each record */ - {{^hasRequiredParams}}eachWithHttpInfo(callback?: (item: {{instanceName}}, done: (err?: Error) => void) => void): void;{{/hasRequiredParams}} - eachWithHttpInfo(params: {{vendorExtensions.x-resource-name}}EachOptions, callback?: (item: {{instanceName}}, done: (err?: Error) => void) => void): void; + {{^hasRequiredParams}}eachWithHttpInfo(callback?: (item: {{#vendorExtensions.x-paginated-primitive-items}}string{{/vendorExtensions.x-paginated-primitive-items}}{{^vendorExtensions.x-paginated-primitive-items}}{{instanceName}}{{/vendorExtensions.x-paginated-primitive-items}}, done: (err?: Error) => void) => void): void;{{/hasRequiredParams}} + eachWithHttpInfo(params: {{vendorExtensions.x-resource-name}}EachOptions, callback?: (item: {{#vendorExtensions.x-paginated-primitive-items}}string{{/vendorExtensions.x-paginated-primitive-items}}{{^vendorExtensions.x-paginated-primitive-items}}{{instanceName}}{{/vendorExtensions.x-paginated-primitive-items}}, done: (err?: Error) => void) => void): void; /** * Retrieve a single target page of {{instanceName}} records from the API. * @@ -50,30 +79,6 @@ * @param { function } [callback] - Callback to handle list of records with metadata */ getPageWithHttpInfo(targetUrl: string, callback?: (error: Error | null, items: ApiResponse<{{returnType}}>) => any): Promise>; - /** - * Lists {{instanceName}} records from the API as a list. - * - * If a function is passed as the first argument, it will be used as the callback - * function. - * - * @param { {{vendorExtensions.x-resource-name}}Options } [params] - Options for request - * @param { function } [callback] - Callback to handle list of records - */ - {{^hasRequiredParams}}list(callback?: (error: Error | null, items: {{instanceName}}[]) => any): Promise<{{instanceName}}[]>;{{/hasRequiredParams}} - list(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: {{instanceName}}[]) => any): Promise<{{instanceName}}[]>; - /** - * Lists {{instanceName}} records from the API as a list with HTTP metadata. - * - * Returns all records along with HTTP metadata from the first page fetched. - * - * If a function is passed as the first argument, it will be used as the callback - * function. - * - * @param { {{vendorExtensions.x-resource-name}}Options } [params] - Options for request - * @param { function } [callback] - Callback to handle list of records with metadata - */ - {{^hasRequiredParams}}listWithHttpInfo(callback?: (error: Error | null, items: ApiResponse<{{instanceName}}[]>) => any): Promise>;{{/hasRequiredParams}} - listWithHttpInfo(params: {{vendorExtensions.x-resource-name}}Options, callback?: (error: Error | null, items: ApiResponse<{{instanceName}}[]>) => any): Promise>; /** * Retrieve a single page of {{instanceName}} records from the API. * @@ -100,4 +105,5 @@ */ {{^hasRequiredParams}}pageWithHttpInfo(callback?: (error: Error | null, items: ApiResponse<{{returnType}}>) => any): Promise>;{{/hasRequiredParams}} pageWithHttpInfo(params: {{vendorExtensions.x-resource-name}}PageOptions, callback?: (error: Error | null, items: ApiResponse<{{returnType}}>) => any): Promise>; + {{/vendorExtensions.x-supports-pagination}} diff --git a/src/main/resources/twilio-node/listOperationsOptions.mustache b/src/main/resources/twilio-node/listOperationsOptions.mustache index 4f9e8e642..42d9a212e 100644 --- a/src/main/resources/twilio-node/listOperationsOptions.mustache +++ b/src/main/resources/twilio-node/listOperationsOptions.mustache @@ -1,3 +1,4 @@ +{{#isApiV1}}{{#vendorExtensions.x-supports-pagination}} /** * Options to pass to each */ @@ -13,6 +14,23 @@ export interface {{vendorExtensions.x-resource-name}}EachOptions { /** Upper limit for the number of records to return. each() guarantees never to return more than limit. Default is no limit */ limit?: number; } +{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}} +/** + * Options to pass to each + */ +export interface {{vendorExtensions.x-resource-name}}EachOptions { + {{#allParams}} + /** {{{description}}} */ + "{{paramName}}"{{^required}}?{{/required}}: {{{dataType}}}; + {{/allParams}} + /** Function to process each record. If this and a positional callback are passed, this one will be used */ + callback?: (item: {{instanceName}}, done: (err?: Error) => void) => void; + /** Function to be called upon completion of streaming */ + done?: Function; + /** Upper limit for the number of records to return. each() guarantees never to return more than limit. Default is no limit */ + limit?: number; +} +{{/isApiV1}} /** * Options to pass to list @@ -26,6 +44,17 @@ export interface {{vendorExtensions.x-resource-name}}Options { limit?: number; } +{{#isApiV1}}{{#vendorExtensions.x-supports-pagination}} +/** + * Options to pass to page + */ +export interface {{vendorExtensions.x-resource-name}}PageOptions { + {{#allParams}} + /** {{{description}}} */ + "{{paramName}}"{{^required}}?{{/required}}: {{{dataType}}}; + {{/allParams}} +} +{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}} /** * Options to pass to page */ @@ -33,9 +62,10 @@ export interface {{vendorExtensions.x-resource-name}}PageOptions { {{#allParams}} /** {{{description}}} */ "{{paramName}}"{{^required}}?{{/required}}: {{{dataType}}}; - {{/allParams}}{{^isApiV1}} + {{/allParams}} /** Page Number, this value is simply for client state */ pageNumber?: number; /** PageToken provided by the API */ - pageToken?: string;{{/isApiV1}} + pageToken?: string; } +{{/isApiV1}} diff --git a/src/main/resources/twilio-node/operation.mustache b/src/main/resources/twilio-node/operation.mustache index 65258c229..a185cac4b 100644 --- a/src/main/resources/twilio-node/operation.mustache +++ b/src/main/resources/twilio-node/operation.mustache @@ -16,9 +16,9 @@ {{^hasRequiredParams}} if (params instanceof Function) { callback = params; - params = {}; + params = {}{{^vendorExtensions.x-is-read-operation}}{{#isApiV1}}{{#bodyParam}} as Partial<{{dataType}}> as {{dataType}}{{/bodyParam}}{{^bodyParam}} as any{{/bodyParam}}{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}}; } else { - params = params || {}; + params = params || {}{{^vendorExtensions.x-is-read-operation}}{{#isApiV1}}{{#bodyParam}} as Partial<{{dataType}}> as {{dataType}}{{/bodyParam}}{{^bodyParam}} as any{{/bodyParam}}{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}}; } {{/hasRequiredParams}} @@ -53,7 +53,8 @@ {{/bodyParams}} {{/hasParams}} {{^hasParams}} - const headers: any = {}; + const headers: any = {};{{#vendorExtensions.x-is-read-operation}}{{#isApiV1}} + const data: any = {};{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}} {{#consumes}} {{#-first}} headers["Content-Type"] = "{{{mediaType}}}" @@ -70,14 +71,27 @@ const instance = this; {{/vendorExtensions.x-is-context-operation}} let operationVersion = {{#vendorExtensions.x-is-context-operation}}instance._version{{/vendorExtensions.x-is-context-operation}}{{#vendorExtensions.x-is-list-operation}}version{{/vendorExtensions.x-is-list-operation}}, - operationPromise = operationVersion.{{vendorExtensions.x-name-lower}}({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers}); + operationPromise = operationVersion.{{#vendorExtensions.x-delete-returns-model}}fetch{{/vendorExtensions.x-delete-returns-model}}{{^vendorExtensions.x-delete-returns-model}}{{#vendorExtensions.x-is-read-operation}}{{#isApiV1}}{{^vendorExtensions.x-supports-pagination}}fetch{{/vendorExtensions.x-supports-pagination}}{{#vendorExtensions.x-supports-pagination}}{{vendorExtensions.x-name-lower}}{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}}{{vendorExtensions.x-name-lower}}{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}}{{^vendorExtensions.x-is-read-operation}}{{vendorExtensions.x-name-lower}}{{/vendorExtensions.x-is-read-operation}}{{/vendorExtensions.x-delete-returns-model}}({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers}); {{^vendorExtensions.x-is-read-operation}}{{#vendorExtensions.x-delete-returns-model}} operationPromise = operationPromise.then(payload => new {{instanceName}}(operationVersion, payload{{#vendorExtensions.x-is-context-operation}}{{#instancePathParams}}, instance._solution.{{paramName}}{{/instancePathParams}}{{/vendorExtensions.x-is-context-operation}}{{#vendorExtensions.x-is-list-operation}}{{#listPathParams}}, instance._solution.{{paramName}}{{/listPathParams}}{{/vendorExtensions.x-is-list-operation}})); {{/vendorExtensions.x-delete-returns-model}}{{^vendorExtensions.x-is-delete-operation}} operationPromise = operationPromise.then(payload => new {{instanceName}}(operationVersion, payload{{#vendorExtensions.x-is-context-operation}}{{#instancePathParams}}, instance._solution.{{paramName}}{{/instancePathParams}}{{/vendorExtensions.x-is-context-operation}}{{#vendorExtensions.x-is-list-operation}}{{#listPathParams}}, instance._solution.{{paramName}}{{/listPathParams}}{{/vendorExtensions.x-is-list-operation}})); {{/vendorExtensions.x-is-delete-operation}}{{/vendorExtensions.x-is-read-operation}} {{#vendorExtensions.x-is-read-operation}} - operationPromise = operationPromise.then(payload => new {{returnType}}(operationVersion, payload,{{#isApiV1}} instance._uri, data,{{/isApiV1}} instance._solution)); + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}} + operationPromise = operationPromise.then(payload => new {{returnType}}(operationVersion, payload, instance._uri, data, instance._solution)); + {{/vendorExtensions.x-supports-pagination}}{{^vendorExtensions.x-supports-pagination}} + {{#vendorExtensions.x-array-items-are-primitives}} + // Array items are primitives (strings, numbers), return them directly + operationPromise = operationPromise.then((payload: any) => payload["{{recordKey}}"] || []); + {{/vendorExtensions.x-array-items-are-primitives}}{{^vendorExtensions.x-array-items-are-primitives}} + operationPromise = operationPromise.then((payload: any) => (payload["{{recordKey}}"] || []).map( + (item: any) => new {{instanceName}}(operationVersion, item{{#listPathParams}}, instance._solution.{{paramName}}{{/listPathParams}}) + )); + {{/vendorExtensions.x-array-items-are-primitives}} + {{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}} + operationPromise = operationPromise.then(payload => new {{returnType}}(operationVersion, payload, instance._uri, data, instance._solution)); + {{/isApiV1}} {{/vendorExtensions.x-is-read-operation}} operationPromise = instance._version.setPromiseCallback(operationPromise,callback); @@ -85,8 +99,50 @@ {{#vendorExtensions.x-is-read-operation}} } + {{#vendorExtensions.x-supports-pagination}} instance.each = instance._version.each; + {{#vendorExtensions.x-paginated-primitive-items}} + + instance.list = function list(params?: any, callback?: (error: Error | null, items: string[]) => any): Promise { + if (params instanceof Function) { + callback = params; + params = {}; + } + params = params || {}; + + let allRecords: string[] = []; + + function fetchNextPage(nextPageParams: any): Promise { + return instance.page(nextPageParams).then((page: any) => { + const payload = page['_payload']; + const records = payload && payload['{{recordKey}}'] ? payload['{{recordKey}}'] : []; + allRecords = allRecords.concat(records); + + if (params.limit && allRecords.length >= params.limit) { + allRecords = allRecords.slice(0, params.limit); + return allRecords; + } + + const nextToken = page.nextPageUrl ? new URL(page.nextPageUrl).searchParams.get('PageToken') : null; + if (nextToken) { + return fetchNextPage({ ...nextPageParams, pageToken: nextToken }); + } + return allRecords; + }); + } + + const promise = fetchNextPage(params); + return instance._version.setPromiseCallback(promise, callback); + }; + {{/vendorExtensions.x-paginated-primitive-items}}{{^vendorExtensions.x-paginated-primitive-items}} + {{/vendorExtensions.x-paginated-primitive-items}} + {{/vendorExtensions.x-supports-pagination}} + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}}{{^vendorExtensions.x-paginated-primitive-items}} + instance.list = instance._version.list; + {{/vendorExtensions.x-paginated-primitive-items}}{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}}{{^vendorExtensions.x-paginated-primitive-items}} instance.list = instance._version.list; + {{/vendorExtensions.x-paginated-primitive-items}}{{/isApiV1}} + {{#vendorExtensions.x-supports-pagination}} instance.getPage = function getPage(targetUrl: string, callback?: (error: Error | null, items: {{returnType}}) => any): Promise<{{returnType}}> { const operationPromise = instance._version._domain.twilio.request({method: "{{httpMethod}}", uri: targetUrl}); @@ -94,4 +150,5 @@ pagePromise = instance._version.setPromiseCallback(pagePromise, callback); return pagePromise; } + {{/vendorExtensions.x-supports-pagination}} {{/vendorExtensions.x-is-read-operation}} diff --git a/src/main/resources/twilio-node/operationWithHttpInfo.mustache b/src/main/resources/twilio-node/operationWithHttpInfo.mustache index 4d6ae6231..90a986e31 100644 --- a/src/main/resources/twilio-node/operationWithHttpInfo.mustache +++ b/src/main/resources/twilio-node/operationWithHttpInfo.mustache @@ -16,9 +16,9 @@ {{^hasRequiredParams}} if (params instanceof Function) { callback = params; - params = {}; + params = {}{{^vendorExtensions.x-is-read-operation}}{{#isApiV1}}{{#bodyParam}} as Partial<{{dataType}}> as {{dataType}}{{/bodyParam}}{{^bodyParam}} as any{{/bodyParam}}{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}}; } else { - params = params || {}; + params = params || {}{{^vendorExtensions.x-is-read-operation}}{{#isApiV1}}{{#bodyParam}} as Partial<{{dataType}}> as {{dataType}}{{/bodyParam}}{{^bodyParam}} as any{{/bodyParam}}{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}}; } {{/hasRequiredParams}} @@ -27,9 +27,9 @@ {{#queryParams}}{{>requestParam}}{{/queryParams}} {{#formParams}}{{>requestParam}}{{/formParams}} {{#bodyParam}}data = params{{/bodyParam}} - {{#vendorExtensions.x-is-read-operation}} + {{#vendorExtensions.x-is-read-operation}}{{^isApiV1}} if (params.pageNumber !== undefined) data["Page"] = params.pageNumber; - if (params.pageToken !== undefined) data["PageToken"] = params.pageToken; + if (params.pageToken !== undefined) data["PageToken"] = params.pageToken;{{/isApiV1}} {{/vendorExtensions.x-is-read-operation}} {{#bodyParams}}if(headers === null || headers === undefined) { @@ -53,7 +53,8 @@ {{/bodyParams}} {{/hasParams}} {{^hasParams}} - const headers: any = {}; + const headers: any = {};{{#vendorExtensions.x-is-read-operation}}{{#isApiV1}} + const data: any = {};{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}} {{#consumes}} {{#-first}} headers["Content-Type"] = "{{{mediaType}}}" @@ -79,21 +80,57 @@ })); {{/vendorExtensions.x-is-delete-operation}} {{#vendorExtensions.x-is-delete-operation}} + {{#vendorExtensions.x-delete-returns-model}} + // DELETE operation that returns a response model + let operationPromise = operationVersion.fetchWithResponseInfo<{{apiName}}Resource>({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers}).then((response) : ApiResponse<{{returnType}}> => ({ + ...response, + body: new {{instanceName}}(operationVersion, response.body{{#vendorExtensions.x-is-context-operation}}{{#instancePathParams}}, instance._solution.{{paramName}}{{/instancePathParams}}{{/vendorExtensions.x-is-context-operation}}{{#vendorExtensions.x-is-list-operation}}{{#listPathParams}}, instance._solution.{{paramName}}{{/listPathParams}}{{/vendorExtensions.x-is-list-operation}}) + })); + {{/vendorExtensions.x-delete-returns-model}} + {{^vendorExtensions.x-delete-returns-model}} // DELETE operation - returns boolean based on status code let operationPromise = operationVersion.{{vendorExtensions.x-name-lower}}WithResponseInfo({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers}).then((response) : ApiResponse<{{returnType}}> => ({ ...response, body: response.statusCode === 204 })); + {{/vendorExtensions.x-delete-returns-model}} {{/vendorExtensions.x-is-delete-operation}} {{/vendorExtensions.x-is-read-operation}} {{#vendorExtensions.x-is-read-operation}} + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}} + // For page operations, use page() directly as it already returns { statusCode, body, headers } + // IMPORTANT: Pass full response to Page constructor, not response.body + let operationPromise = operationVersion.page({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers}).then((response) : ApiResponse<{{returnType}}> => ({ + statusCode: response.statusCode, + headers: response.headers, + body: new {{returnType}}(operationVersion, response, instance._uri, data, instance._solution) + })); + {{/vendorExtensions.x-supports-pagination}}{{^vendorExtensions.x-supports-pagination}} + {{#vendorExtensions.x-array-items-are-primitives}} + // Array items are primitives (strings, numbers), return them directly + let operationPromise = operationVersion.fetchWithResponseInfo<{{apiName}}Resource>({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers}).then((response): ApiResponse => ({ + statusCode: response.statusCode, + headers: response.headers, + body: ((response.body as any)["{{recordKey}}"] || []) + })); + {{/vendorExtensions.x-array-items-are-primitives}}{{^vendorExtensions.x-array-items-are-primitives}} + let operationPromise = operationVersion.fetchWithResponseInfo<{{apiName}}Resource>({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers}).then((response): ApiResponse<{{instanceName}}[]> => ({ + statusCode: response.statusCode, + headers: response.headers, + body: ((response.body as any)["{{recordKey}}"] || []).map( + (item: any) => new {{instanceName}}(operationVersion, item{{#listPathParams}}, instance._solution.{{paramName}}{{/listPathParams}}) + ) + })); + {{/vendorExtensions.x-array-items-are-primitives}} + {{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}} // For page operations, use page() directly as it already returns { statusCode, body, headers } // IMPORTANT: Pass full response to Page constructor, not response.body let operationPromise = operationVersion.page({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers}).then((response) : ApiResponse<{{returnType}}> => ({ statusCode: response.statusCode, headers: response.headers, - body: new {{returnType}}(operationVersion, response, instance._solution) + body: new {{returnType}}(operationVersion, response, instance._uri, data, instance._solution) })); + {{/isApiV1}} {{/vendorExtensions.x-is-read-operation}} operationPromise = instance._version.setPromiseCallback(operationPromise,callback); @@ -101,10 +138,65 @@ {{#vendorExtensions.x-is-read-operation}} } + {{#vendorExtensions.x-supports-pagination}} instance.each = instance._version.each; instance.eachWithHttpInfo = instance._version.eachWithHttpInfo; + {{#vendorExtensions.x-paginated-primitive-items}} + + instance.listWithHttpInfo = function listWithHttpInfo(params?: any, callback?: (error: Error | null, items: ApiResponse) => any): Promise> { + if (params instanceof Function) { + callback = params; + params = {}; + } + params = params || {}; + + let allRecords: string[] = []; + let lastResponse: any; + + function fetchNextPage(nextPageParams: any): Promise> { + return instance.pageWithHttpInfo(nextPageParams).then((response: ApiResponse) => { + lastResponse = response; + const page = response.body; + // For primitive arrays, directly access the records array from the payload + // Access _payload using bracket notation to bypass TypeScript protected access + const payload = page['_payload']; + const records = payload && payload['{{recordKey}}'] ? payload['{{recordKey}}'] : []; + allRecords = allRecords.concat(records); + + if (params.limit && allRecords.length >= params.limit) { + allRecords = allRecords.slice(0, params.limit); + return { + statusCode: lastResponse.statusCode, + headers: lastResponse.headers, + body: allRecords + }; + } + + const nextToken = page.nextPageUrl ? new URL(page.nextPageUrl).searchParams.get('PageToken') : null; + if (nextToken) { + return fetchNextPage({ ...nextPageParams, pageToken: nextToken }); + } + return { + statusCode: lastResponse.statusCode, + headers: lastResponse.headers, + body: allRecords + }; + }); + } + + const promise = fetchNextPage(params); + return instance._version.setPromiseCallback(promise, callback); + }; + {{/vendorExtensions.x-paginated-primitive-items}} + {{/vendorExtensions.x-supports-pagination}} + {{#isApiV1}}{{#vendorExtensions.x-supports-pagination}}{{^vendorExtensions.x-paginated-primitive-items}} + instance.list = instance._version.list; + instance.listWithHttpInfo = instance._version.listWithHttpInfo; + {{/vendorExtensions.x-paginated-primitive-items}}{{/vendorExtensions.x-supports-pagination}}{{/isApiV1}}{{^isApiV1}}{{^vendorExtensions.x-paginated-primitive-items}} instance.list = instance._version.list; instance.listWithHttpInfo = instance._version.listWithHttpInfo; + {{/vendorExtensions.x-paginated-primitive-items}}{{/isApiV1}} + {{#vendorExtensions.x-supports-pagination}} instance.getPageWithHttpInfo = function getPageWithHttpInfo(targetUrl: string, callback?: (error: Error | null, items?: ApiResponse<{{returnType}}>) => any): Promise> { // Use request() directly as it already returns { statusCode, body, headers } @@ -113,9 +205,10 @@ let pagePromise = operationPromise.then((response): ApiResponse<{{returnType}}> => ({ statusCode: response.statusCode, headers: response.headers, - body: new {{returnType}}(instance._version, response, instance._solution) + body: new {{returnType}}(instance._version, response{{#isApiV1}}, instance._uri, {}{{/isApiV1}}, instance._solution) })); pagePromise = instance._version.setPromiseCallback(pagePromise, callback); return pagePromise; } + {{/vendorExtensions.x-supports-pagination}} {{/vendorExtensions.x-is-read-operation}} diff --git a/src/main/resources/twilio-node/pagination.mustache b/src/main/resources/twilio-node/pagination.mustache index 51292f1fc..ebfc84ea2 100644 --- a/src/main/resources/twilio-node/pagination.mustache +++ b/src/main/resources/twilio-node/pagination.mustache @@ -17,7 +17,11 @@ constructor(version: {{apiVersionClass}}, response: Response, solution: * * @param payload - Payload response from the API */ - getInstance(payload: {{apiName}}Resource): {{instanceName}} { + getInstance(payload: {{apiName}}Resource): {{instanceName}}{{#hasPaginatedPrimitiveItems}} | string{{/hasPaginatedPrimitiveItems}} { + {{#hasPaginatedPrimitiveItems}} + // Array items are primitives (strings, numbers), return directly + return payload as any; + {{/hasPaginatedPrimitiveItems}}{{^hasPaginatedPrimitiveItems}} return new {{instanceName}}( this._version, payload, @@ -25,6 +29,7 @@ constructor(version: {{apiVersionClass}}, response: Response, solution: this._solution.{{paramName}}, {{/listPathParams}} ); + {{/hasPaginatedPrimitiveItems}} } [inspect.custom](depth: any, options: InspectOptions) { diff --git a/src/main/resources/twilio-node/responseModel.mustache b/src/main/resources/twilio-node/responseModel.mustache index 11f2d0400..68969aac0 100644 --- a/src/main/resources/twilio-node/responseModel.mustache +++ b/src/main/resources/twilio-node/responseModel.mustache @@ -1,15 +1,17 @@ {{#responseModel}} - interface {{apiName}}Payload extends {{^hasPaginationOperation}}{{apiName}}Resource {}{{/hasPaginationOperation}}{{#hasPaginationOperation}}{{#isApiV1}}TokenPaginationPayload{{/isApiV1}}{{^isApiV1}}TwilioResponsePayload{{/isApiV1}} { - {{recordKey}}: {{apiName}}Resource[]; -}{{/hasPaginationOperation}} +{{^isApiV1}} interface {{apiName}}Payload extends {{^hasPaginationOperation}}{{apiName}}Resource {}{{/hasPaginationOperation}}{{#hasPaginationOperation}}TwilioResponsePayload { + {{recordKey}}: {{#hasPaginatedPrimitiveItems}}string[]{{/hasPaginatedPrimitiveItems}}{{^hasPaginatedPrimitiveItems}}{{apiName}}Resource[]{{/hasPaginatedPrimitiveItems}}; +}{{/hasPaginationOperation}}{{/isApiV1}}{{#isApiV1}}{{#hasOperationWithPagination}} interface {{apiName}}Payload extends TokenPaginationPayload { + {{recordKey}}: {{#hasPaginatedPrimitiveItems}}string[]{{/hasPaginatedPrimitiveItems}}{{^hasPaginatedPrimitiveItems}}{{apiName}}Resource[]{{/hasPaginatedPrimitiveItems}}; +}{{/hasOperationWithPagination}}{{/isApiV1}} {{#hasMultipleResponseModels}} {{#responseInstanceModels}} /** * Response model for {{name}} operations */ -interface {{name}}Resource { +interface {{name}}_ResponseResource { {{#vars}} {{baseName}}{{^required}}?{{/required}}: {{{baseType}}}; {{/vars}} @@ -19,7 +21,7 @@ interface {{name}}Resource { /** * Union type for all possible response models */ -type {{apiName}}Resource = {{#responseInstanceModels}}{{name}}Resource{{^-last}} | {{/-last}}{{/responseInstanceModels}}; +type {{apiName}}Resource = {{#responseInstanceModels}}{{name}}_ResponseResource{{^-last}} | {{/-last}}{{/responseInstanceModels}}; {{/hasMultipleResponseModels}} {{^hasMultipleResponseModels}} interface {{apiName}}Resource { @@ -40,8 +42,9 @@ export class {{instanceName}} { protected _context?: {{apiName}}Context; {{/instancePath}} - constructor(protected _version: {{apiVersionClass}}, payload: {{apiName}}Resource{{#instancePathParams}}, {{paramName}}{{^vendorExtensions.x-is-parent-param}}?{{/vendorExtensions.x-is-parent-param}}: {{{dataType}}}{{/instancePathParams}}) { - {{#vars}} + constructor(protected _version: {{apiVersionClass}}, {{#isApiV1}}_payload{{/isApiV1}}{{^isApiV1}}payload{{/isApiV1}}: {{apiName}}Resource{{#instancePathParams}}, {{paramName}}{{^vendorExtensions.x-is-parent-param}}?{{/vendorExtensions.x-is-parent-param}}: {{{dataType}}}{{/instancePathParams}}) { + {{#isApiV1}}{{^hasMultipleResponseModels}}const payload = _payload;{{/hasMultipleResponseModels}}{{#hasMultipleResponseModels}}const payload: any = _payload;{{/hasMultipleResponseModels}}{{/isApiV1}} + {{#allResponseVars}} {{^complexType}} this.{{name}} = {{vendorExtensions.x-deserialize}}(payload.{{baseName}}); {{/complexType}} @@ -65,21 +68,21 @@ export class {{instanceName}} { {{/isModel}} {{/isArray}} {{/complexType}} - {{/vars}} + {{/allResponseVars}} {{#instancePath}} - this._solution = { {{#instancePathParams}}{{paramName}}{{^vendorExtensions.x-is-parent-param}}: {{paramName}} || this.{{paramName}}{{#vendorExtensions.x-stringify}}.toString(){{/vendorExtensions.x-stringify}}{{/vendorExtensions.x-is-parent-param}}, {{/instancePathParams}} }; + this._solution = { {{#instancePathParams}}{{paramName}}{{^vendorExtensions.x-is-parent-param}}{{#isApiV1}}: {{paramName}}{{/isApiV1}}{{^isApiV1}}: {{paramName}} || this.{{paramName}}{{#vendorExtensions.x-stringify}}.toString(){{/vendorExtensions.x-stringify}}{{/isApiV1}}{{/vendorExtensions.x-is-parent-param}}, {{/instancePathParams}} }; {{/instancePath}} } - {{#vars}} + {{#allResponseVars}} {{#description}} /** * {{{description}}} */ {{/description}} {{name}}{{#hasMultipleResponseModels}}?{{/hasMultipleResponseModels}}: {{{baseType}}}; - {{/vars}} + {{/allResponseVars}} {{#instancePath}} private get _proxy(): {{apiName}}Context { @@ -124,9 +127,9 @@ export class {{instanceName}} { */ toJSON() { return { - {{#vars}} + {{#allResponseVars}} {{name}}: this.{{name}}, - {{/vars}} + {{/allResponseVars}} }; } diff --git a/src/main/resources/twilio-node/tokenPagination.mustache b/src/main/resources/twilio-node/tokenPagination.mustache index 7213faf84..490302604 100644 --- a/src/main/resources/twilio-node/tokenPagination.mustache +++ b/src/main/resources/twilio-node/tokenPagination.mustache @@ -1,6 +1,6 @@ {{#operations}} {{#vendorExtensions.x-is-read-operation}} -export class {{apiName}}Page extends TokenPage<{{apiVersionClass}}, {{apiName}}Payload, {{apiName}}Resource, {{instanceName}}> { +export class {{apiName}}Page extends TokenPage<{{apiVersionClass}}, {{apiName}}Payload, {{apiName}}Resource, {{#hasPaginatedPrimitiveItems}}string{{/hasPaginatedPrimitiveItems}}{{^hasPaginatedPrimitiveItems}}{{instanceName}}{{/hasPaginatedPrimitiveItems}}> { /** * Initialize the {{apiName}}Page * @@ -19,7 +19,11 @@ constructor(version: {{apiVersionClass}}, response: Response, uri: strin * * @param payload - Payload response from the API */ - getInstance(payload: {{apiName}}Resource): {{instanceName}} { + getInstance(payload: {{apiName}}Resource): {{#hasPaginatedPrimitiveItems}}string{{/hasPaginatedPrimitiveItems}}{{^hasPaginatedPrimitiveItems}}{{instanceName}}{{/hasPaginatedPrimitiveItems}} { + {{#hasPaginatedPrimitiveItems}} + // Array items are primitives (strings, numbers), return directly + return payload as any; + {{/hasPaginatedPrimitiveItems}}{{^hasPaginatedPrimitiveItems}} return new {{instanceName}}( this._version, payload, @@ -27,6 +31,7 @@ constructor(version: {{apiVersionClass}}, response: Response, uri: strin this._solution.{{paramName}}, {{/listPathParams}} ); + {{/hasPaginatedPrimitiveItems}} } [inspect.custom](depth: any, options: InspectOptions) { diff --git a/src/main/resources/twilio-node/version.mustache b/src/main/resources/twilio-node/version.mustache index c76c69398..16d17b212 100644 --- a/src/main/resources/twilio-node/version.mustache +++ b/src/main/resources/twilio-node/version.mustache @@ -2,7 +2,7 @@ import {{domainName}}Base from "../{{domainName}}Base"; import Version from "../../base/Version"; {{#versionResources}} -import { {{importName}} } from "./{{apiVersion}}/{{filename}}"; +import { {{importName}}{{#hasInstanceOperation}}, {{instanceType}}{{/hasInstanceOperation}} } from "./{{apiVersion}}/{{filename}}"; {{/versionResources}} export default class {{apiVersionClass}} extends Version { @@ -24,10 +24,26 @@ export default class {{apiVersionClass}} extends Version { {{#versionResources}} {{#listWithPathParams}} + {{#hasInstanceOperation}} + /** Accessor for {{mountName}} resource - list operations */ + {{mountName}}({{#pathParams}}{{name}}: string{{^-last}}, {{/-last}}{{/pathParams}}): {{type}}; + /** Accessor for {{mountName}} resource - instance operations */ + {{mountName}}({{#pathParams}}{{name}}: string, {{/pathParams}}{{instanceParam}}: string): {{instanceType}}; + /** Implementation */ + {{mountName}}({{#pathParams}}{{name}}: string{{^-last}}, {{/-last}}{{/pathParams}}, {{instanceParam}}?: string): {{type}} | {{instanceType}} { + const listInstance = {{className}}(this, {{#pathParams}}{{name}}{{^-last}}, {{/-last}}{{/pathParams}}); + if ({{instanceParam}} !== undefined) { + return listInstance.get({{instanceParam}}); + } + return listInstance; + } + {{/hasInstanceOperation}} + {{^hasInstanceOperation}} /** Accessor for {{mountName}} resource */ {{mountName}}({{#pathParams}}{{name}}: string{{^-last}}, {{/-last}}{{/pathParams}}): {{type}} { return {{className}}(this, {{#pathParams}}{{name}}{{^-last}}, {{/-last}}{{/pathParams}}); } + {{/hasInstanceOperation}} {{/listWithPathParams}} {{^listWithPathParams}} /** Getter for {{mountName}} resource */