This is page 35 of 61. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .DS_Store ├── .github │ ├── dependabot.yml │ ├── instructions │ │ ├── mcp-node-tests.instructions.md │ │ └── mcp-yml-tests.instructions.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bug_fix.md │ │ ├── documentation.md │ │ └── new_tool.md │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── deploy-pages.yml │ ├── publish.yml │ └── update-docs.yml ├── .gitignore ├── .husky │ └── pre-commit ├── aegis.config.docs-only.json ├── aegis.config.json ├── aegis.config.with-dw.json ├── AGENTS.md ├── ai-instructions │ ├── claude-desktop │ │ └── claude_custom_instructions.md │ ├── cursor │ │ └── .cursor │ │ └── rules │ │ ├── debugging-workflows.mdc │ │ ├── hooks-development.mdc │ │ ├── isml-templates.mdc │ │ ├── job-framework.mdc │ │ ├── performance-optimization.mdc │ │ ├── scapi-endpoints.mdc │ │ ├── security-patterns.mdc │ │ ├── sfcc-development.mdc │ │ ├── sfra-controllers.mdc │ │ ├── sfra-models.mdc │ │ ├── system-objects.mdc │ │ └── testing-patterns.mdc │ └── github-copilot │ └── copilot-instructions.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── docs │ ├── best-practices │ │ ├── cartridge_creation.md │ │ ├── isml_templates.md │ │ ├── job_framework.md │ │ ├── localserviceregistry.md │ │ ├── ocapi_hooks.md │ │ ├── performance.md │ │ ├── scapi_custom_endpoint.md │ │ ├── scapi_hooks.md │ │ ├── security.md │ │ ├── sfra_client_side_js.md │ │ ├── sfra_controllers.md │ │ ├── sfra_models.md │ │ └── sfra_scss.md │ ├── dw_campaign │ │ ├── ABTest.md │ │ ├── ABTestMgr.md │ │ ├── ABTestSegment.md │ │ ├── AmountDiscount.md │ │ ├── ApproachingDiscount.md │ │ ├── BonusChoiceDiscount.md │ │ ├── BonusDiscount.md │ │ ├── Campaign.md │ │ ├── CampaignMgr.md │ │ ├── CampaignStatusCodes.md │ │ ├── Coupon.md │ │ ├── CouponMgr.md │ │ ├── CouponRedemption.md │ │ ├── CouponStatusCodes.md │ │ ├── Discount.md │ │ ├── DiscountPlan.md │ │ ├── FixedPriceDiscount.md │ │ ├── FixedPriceShippingDiscount.md │ │ ├── FreeDiscount.md │ │ ├── FreeShippingDiscount.md │ │ ├── PercentageDiscount.md │ │ ├── PercentageOptionDiscount.md │ │ ├── PriceBookPriceDiscount.md │ │ ├── Promotion.md │ │ ├── PromotionMgr.md │ │ ├── PromotionPlan.md │ │ ├── SlotContent.md │ │ ├── SourceCodeGroup.md │ │ ├── SourceCodeInfo.md │ │ ├── SourceCodeStatusCodes.md │ │ └── TotalFixedPriceDiscount.md │ ├── dw_catalog │ │ ├── Catalog.md │ │ ├── CatalogMgr.md │ │ ├── Category.md │ │ ├── CategoryAssignment.md │ │ ├── CategoryLink.md │ │ ├── PriceBook.md │ │ ├── PriceBookMgr.md │ │ ├── Product.md │ │ ├── ProductActiveData.md │ │ ├── ProductAttributeModel.md │ │ ├── ProductAvailabilityLevels.md │ │ ├── ProductAvailabilityModel.md │ │ ├── ProductInventoryList.md │ │ ├── ProductInventoryMgr.md │ │ ├── ProductInventoryRecord.md │ │ ├── ProductLink.md │ │ ├── ProductMgr.md │ │ ├── ProductOption.md │ │ ├── ProductOptionModel.md │ │ ├── ProductOptionValue.md │ │ ├── ProductPriceInfo.md │ │ ├── ProductPriceModel.md │ │ ├── ProductPriceTable.md │ │ ├── ProductSearchHit.md │ │ ├── ProductSearchModel.md │ │ ├── ProductSearchRefinementDefinition.md │ │ ├── ProductSearchRefinements.md │ │ ├── ProductSearchRefinementValue.md │ │ ├── ProductVariationAttribute.md │ │ ├── ProductVariationAttributeValue.md │ │ ├── ProductVariationModel.md │ │ ├── Recommendation.md │ │ ├── SearchModel.md │ │ ├── SearchRefinementDefinition.md │ │ ├── SearchRefinements.md │ │ ├── SearchRefinementValue.md │ │ ├── SortingOption.md │ │ ├── SortingRule.md │ │ ├── Store.md │ │ ├── StoreGroup.md │ │ ├── StoreInventoryFilter.md │ │ ├── StoreInventoryFilterValue.md │ │ ├── StoreMgr.md │ │ ├── Variant.md │ │ └── VariationGroup.md │ ├── dw_content │ │ ├── Content.md │ │ ├── ContentMgr.md │ │ ├── ContentSearchModel.md │ │ ├── ContentSearchRefinementDefinition.md │ │ ├── ContentSearchRefinements.md │ │ ├── ContentSearchRefinementValue.md │ │ ├── Folder.md │ │ ├── Library.md │ │ ├── MarkupText.md │ │ └── MediaFile.md │ ├── dw_crypto │ │ ├── CertificateRef.md │ │ ├── CertificateUtils.md │ │ ├── Cipher.md │ │ ├── Encoding.md │ │ ├── JWE.md │ │ ├── JWEHeader.md │ │ ├── JWS.md │ │ ├── JWSHeader.md │ │ ├── KeyRef.md │ │ ├── Mac.md │ │ ├── MessageDigest.md │ │ ├── SecureRandom.md │ │ ├── Signature.md │ │ ├── WeakCipher.md │ │ ├── WeakMac.md │ │ ├── WeakMessageDigest.md │ │ ├── WeakSignature.md │ │ └── X509Certificate.md │ ├── dw_customer │ │ ├── AddressBook.md │ │ ├── AgentUserMgr.md │ │ ├── AgentUserStatusCodes.md │ │ ├── AuthenticationStatus.md │ │ ├── Credentials.md │ │ ├── Customer.md │ │ ├── CustomerActiveData.md │ │ ├── CustomerAddress.md │ │ ├── CustomerCDPData.md │ │ ├── CustomerContextMgr.md │ │ ├── CustomerGroup.md │ │ ├── CustomerList.md │ │ ├── CustomerMgr.md │ │ ├── CustomerPasswordConstraints.md │ │ ├── CustomerPaymentInstrument.md │ │ ├── CustomerStatusCodes.md │ │ ├── EncryptedObject.md │ │ ├── ExternalProfile.md │ │ ├── OrderHistory.md │ │ ├── ProductList.md │ │ ├── ProductListItem.md │ │ ├── ProductListItemPurchase.md │ │ ├── ProductListMgr.md │ │ ├── ProductListRegistrant.md │ │ ├── Profile.md │ │ └── Wallet.md │ ├── dw_extensions.applepay │ │ ├── ApplePayHookResult.md │ │ └── ApplePayHooks.md │ ├── dw_extensions.facebook │ │ ├── FacebookFeedHooks.md │ │ └── FacebookProduct.md │ ├── dw_extensions.paymentrequest │ │ ├── PaymentRequestHookResult.md │ │ └── PaymentRequestHooks.md │ ├── dw_extensions.payments │ │ ├── SalesforceBancontactPaymentDetails.md │ │ ├── SalesforceCardPaymentDetails.md │ │ ├── SalesforceEpsPaymentDetails.md │ │ ├── SalesforceIdealPaymentDetails.md │ │ ├── SalesforceKlarnaPaymentDetails.md │ │ ├── SalesforcePaymentDetails.md │ │ ├── SalesforcePaymentIntent.md │ │ ├── SalesforcePaymentMethod.md │ │ ├── SalesforcePaymentRequest.md │ │ ├── SalesforcePaymentsHooks.md │ │ ├── SalesforcePaymentsMgr.md │ │ ├── SalesforcePaymentsSiteConfiguration.md │ │ ├── SalesforcePayPalOrder.md │ │ ├── SalesforcePayPalOrderAddress.md │ │ ├── SalesforcePayPalOrderPayer.md │ │ ├── SalesforcePayPalPaymentDetails.md │ │ ├── SalesforceSepaDebitPaymentDetails.md │ │ └── SalesforceVenmoPaymentDetails.md │ ├── dw_extensions.pinterest │ │ ├── PinterestAvailability.md │ │ ├── PinterestFeedHooks.md │ │ ├── PinterestOrder.md │ │ ├── PinterestOrderHooks.md │ │ └── PinterestProduct.md │ ├── dw_io │ │ ├── CSVStreamReader.md │ │ ├── CSVStreamWriter.md │ │ ├── File.md │ │ ├── FileReader.md │ │ ├── FileWriter.md │ │ ├── InputStream.md │ │ ├── OutputStream.md │ │ ├── PrintWriter.md │ │ ├── RandomAccessFileReader.md │ │ ├── Reader.md │ │ ├── StringWriter.md │ │ ├── Writer.md │ │ ├── XMLIndentingStreamWriter.md │ │ ├── XMLStreamConstants.md │ │ ├── XMLStreamReader.md │ │ └── XMLStreamWriter.md │ ├── dw_job │ │ ├── JobExecution.md │ │ └── JobStepExecution.md │ ├── dw_net │ │ ├── FTPClient.md │ │ ├── FTPFileInfo.md │ │ ├── HTTPClient.md │ │ ├── HTTPRequestPart.md │ │ ├── Mail.md │ │ ├── SFTPClient.md │ │ ├── SFTPFileInfo.md │ │ ├── WebDAVClient.md │ │ └── WebDAVFileInfo.md │ ├── dw_object │ │ ├── ActiveData.md │ │ ├── CustomAttributes.md │ │ ├── CustomObject.md │ │ ├── CustomObjectMgr.md │ │ ├── Extensible.md │ │ ├── ExtensibleObject.md │ │ ├── Note.md │ │ ├── ObjectAttributeDefinition.md │ │ ├── ObjectAttributeGroup.md │ │ ├── ObjectAttributeValueDefinition.md │ │ ├── ObjectTypeDefinition.md │ │ ├── PersistentObject.md │ │ ├── SimpleExtensible.md │ │ └── SystemObjectMgr.md │ ├── dw_order │ │ ├── AbstractItem.md │ │ ├── AbstractItemCtnr.md │ │ ├── Appeasement.md │ │ ├── AppeasementItem.md │ │ ├── Basket.md │ │ ├── BasketMgr.md │ │ ├── BonusDiscountLineItem.md │ │ ├── CouponLineItem.md │ │ ├── CreateAgentBasketLimitExceededException.md │ │ ├── CreateBasketFromOrderException.md │ │ ├── CreateCouponLineItemException.md │ │ ├── CreateOrderException.md │ │ ├── CreateTemporaryBasketLimitExceededException.md │ │ ├── GiftCertificate.md │ │ ├── GiftCertificateLineItem.md │ │ ├── GiftCertificateMgr.md │ │ ├── GiftCertificateStatusCodes.md │ │ ├── Invoice.md │ │ ├── InvoiceItem.md │ │ ├── LineItem.md │ │ ├── LineItemCtnr.md │ │ ├── Order.md │ │ ├── OrderAddress.md │ │ ├── OrderItem.md │ │ ├── OrderMgr.md │ │ ├── OrderPaymentInstrument.md │ │ ├── OrderProcessStatusCodes.md │ │ ├── PaymentCard.md │ │ ├── PaymentInstrument.md │ │ ├── PaymentMethod.md │ │ ├── PaymentMgr.md │ │ ├── PaymentProcessor.md │ │ ├── PaymentStatusCodes.md │ │ ├── PaymentTransaction.md │ │ ├── PriceAdjustment.md │ │ ├── PriceAdjustmentLimitTypes.md │ │ ├── ProductLineItem.md │ │ ├── ProductShippingCost.md │ │ ├── ProductShippingLineItem.md │ │ ├── ProductShippingModel.md │ │ ├── Return.md │ │ ├── ReturnCase.md │ │ ├── ReturnCaseItem.md │ │ ├── ReturnItem.md │ │ ├── Shipment.md │ │ ├── ShipmentShippingCost.md │ │ ├── ShipmentShippingModel.md │ │ ├── ShippingLineItem.md │ │ ├── ShippingLocation.md │ │ ├── ShippingMethod.md │ │ ├── ShippingMgr.md │ │ ├── ShippingOrder.md │ │ ├── ShippingOrderItem.md │ │ ├── SumItem.md │ │ ├── TaxGroup.md │ │ ├── TaxItem.md │ │ ├── TaxMgr.md │ │ ├── TrackingInfo.md │ │ └── TrackingRef.md │ ├── dw_order.hooks │ │ ├── CalculateHooks.md │ │ ├── OrderHooks.md │ │ ├── PaymentHooks.md │ │ ├── ReturnHooks.md │ │ └── ShippingOrderHooks.md │ ├── dw_rpc │ │ ├── SOAPUtil.md │ │ ├── Stub.md │ │ └── WebReference.md │ ├── dw_suggest │ │ ├── BrandSuggestions.md │ │ ├── CategorySuggestions.md │ │ ├── ContentSuggestions.md │ │ ├── CustomSuggestions.md │ │ ├── ProductSuggestions.md │ │ ├── SearchPhraseSuggestions.md │ │ ├── SuggestedCategory.md │ │ ├── SuggestedContent.md │ │ ├── SuggestedPhrase.md │ │ ├── SuggestedProduct.md │ │ ├── SuggestedTerm.md │ │ ├── SuggestedTerms.md │ │ ├── Suggestions.md │ │ └── SuggestModel.md │ ├── dw_svc │ │ ├── FTPService.md │ │ ├── FTPServiceDefinition.md │ │ ├── HTTPFormService.md │ │ ├── HTTPFormServiceDefinition.md │ │ ├── HTTPService.md │ │ ├── HTTPServiceDefinition.md │ │ ├── LocalServiceRegistry.md │ │ ├── Result.md │ │ ├── Service.md │ │ ├── ServiceCallback.md │ │ ├── ServiceConfig.md │ │ ├── ServiceCredential.md │ │ ├── ServiceDefinition.md │ │ ├── ServiceProfile.md │ │ ├── ServiceRegistry.md │ │ ├── SOAPService.md │ │ └── SOAPServiceDefinition.md │ ├── dw_system │ │ ├── AgentUserStatusCodes.md │ │ ├── Cache.md │ │ ├── CacheMgr.md │ │ ├── HookMgr.md │ │ ├── InternalObject.md │ │ ├── JobProcessMonitor.md │ │ ├── Log.md │ │ ├── Logger.md │ │ ├── LogNDC.md │ │ ├── OrganizationPreferences.md │ │ ├── Pipeline.md │ │ ├── PipelineDictionary.md │ │ ├── RemoteInclude.md │ │ ├── Request.md │ │ ├── RequestHooks.md │ │ ├── Response.md │ │ ├── RESTErrorResponse.md │ │ ├── RESTResponseMgr.md │ │ ├── RESTSuccessResponse.md │ │ ├── SearchStatus.md │ │ ├── Session.md │ │ ├── Site.md │ │ ├── SitePreferences.md │ │ ├── Status.md │ │ ├── StatusItem.md │ │ ├── System.md │ │ └── Transaction.md │ ├── dw_util │ │ ├── ArrayList.md │ │ ├── Assert.md │ │ ├── BigInteger.md │ │ ├── Bytes.md │ │ ├── Calendar.md │ │ ├── Collection.md │ │ ├── Currency.md │ │ ├── DateUtils.md │ │ ├── Decimal.md │ │ ├── FilteringCollection.md │ │ ├── Geolocation.md │ │ ├── HashMap.md │ │ ├── HashSet.md │ │ ├── Iterator.md │ │ ├── LinkedHashMap.md │ │ ├── LinkedHashSet.md │ │ ├── List.md │ │ ├── Locale.md │ │ ├── Map.md │ │ ├── MapEntry.md │ │ ├── MappingKey.md │ │ ├── MappingMgr.md │ │ ├── PropertyComparator.md │ │ ├── SecureEncoder.md │ │ ├── SecureFilter.md │ │ ├── SeekableIterator.md │ │ ├── Set.md │ │ ├── SortedMap.md │ │ ├── SortedSet.md │ │ ├── StringUtils.md │ │ ├── Template.md │ │ └── UUIDUtils.md │ ├── dw_value │ │ ├── EnumValue.md │ │ ├── MimeEncodedText.md │ │ ├── Money.md │ │ └── Quantity.md │ ├── dw_web │ │ ├── ClickStream.md │ │ ├── ClickStreamEntry.md │ │ ├── Cookie.md │ │ ├── Cookies.md │ │ ├── CSRFProtection.md │ │ ├── Form.md │ │ ├── FormAction.md │ │ ├── FormElement.md │ │ ├── FormElementValidationResult.md │ │ ├── FormField.md │ │ ├── FormFieldOption.md │ │ ├── FormFieldOptions.md │ │ ├── FormGroup.md │ │ ├── FormList.md │ │ ├── FormListItem.md │ │ ├── Forms.md │ │ ├── HttpParameter.md │ │ ├── HttpParameterMap.md │ │ ├── LoopIterator.md │ │ ├── PageMetaData.md │ │ ├── PageMetaTag.md │ │ ├── PagingModel.md │ │ ├── Resource.md │ │ ├── URL.md │ │ ├── URLAction.md │ │ ├── URLParameter.md │ │ ├── URLRedirect.md │ │ ├── URLRedirectMgr.md │ │ └── URLUtils.md │ ├── sfra │ │ ├── account.md │ │ ├── address.md │ │ ├── billing.md │ │ ├── cart.md │ │ ├── categories.md │ │ ├── content.md │ │ ├── locale.md │ │ ├── order.md │ │ ├── payment.md │ │ ├── price-default.md │ │ ├── price-range.md │ │ ├── price-tiered.md │ │ ├── product-bundle.md │ │ ├── product-full.md │ │ ├── product-line-items.md │ │ ├── product-search.md │ │ ├── product-tile.md │ │ ├── querystring.md │ │ ├── render.md │ │ ├── request.md │ │ ├── response.md │ │ ├── server.md │ │ ├── shipping.md │ │ ├── store.md │ │ ├── stores.md │ │ └── totals.md │ └── TopLevel │ ├── APIException.md │ ├── arguments.md │ ├── Array.md │ ├── ArrayBuffer.md │ ├── BigInt.md │ ├── Boolean.md │ ├── ConversionError.md │ ├── DataView.md │ ├── Date.md │ ├── Error.md │ ├── ES6Iterator.md │ ├── EvalError.md │ ├── Fault.md │ ├── Float32Array.md │ ├── Float64Array.md │ ├── Function.md │ ├── Generator.md │ ├── global.md │ ├── Int16Array.md │ ├── Int32Array.md │ ├── Int8Array.md │ ├── InternalError.md │ ├── IOError.md │ ├── Iterable.md │ ├── Iterator.md │ ├── JSON.md │ ├── Map.md │ ├── Math.md │ ├── Module.md │ ├── Namespace.md │ ├── Number.md │ ├── Object.md │ ├── QName.md │ ├── RangeError.md │ ├── ReferenceError.md │ ├── RegExp.md │ ├── Set.md │ ├── StopIteration.md │ ├── String.md │ ├── Symbol.md │ ├── SyntaxError.md │ ├── SystemError.md │ ├── TypeError.md │ ├── Uint16Array.md │ ├── Uint32Array.md │ ├── Uint8Array.md │ ├── Uint8ClampedArray.md │ ├── URIError.md │ ├── WeakMap.md │ ├── WeakSet.md │ ├── XML.md │ ├── XMLList.md │ └── XMLStreamError.md ├── docs-site │ ├── .gitignore │ ├── App.tsx │ ├── components │ │ ├── Badge.tsx │ │ ├── BreadcrumbSchema.tsx │ │ ├── CodeBlock.tsx │ │ ├── Collapsible.tsx │ │ ├── ConfigBuilder.tsx │ │ ├── ConfigHero.tsx │ │ ├── ConfigModeTabs.tsx │ │ ├── icons.tsx │ │ ├── Layout.tsx │ │ ├── LightCodeContainer.tsx │ │ ├── NewcomerCTA.tsx │ │ ├── NextStepsStrip.tsx │ │ ├── OnThisPage.tsx │ │ ├── Search.tsx │ │ ├── SEO.tsx │ │ ├── Sidebar.tsx │ │ ├── StructuredData.tsx │ │ ├── ToolCard.tsx │ │ ├── ToolFilters.tsx │ │ ├── Typography.tsx │ │ └── VersionBadge.tsx │ ├── constants.tsx │ ├── index.html │ ├── main.tsx │ ├── metadata.json │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── AIInterfacesPage.tsx │ │ ├── ConfigurationPage.tsx │ │ ├── DevelopmentPage.tsx │ │ ├── ExamplesPage.tsx │ │ ├── FeaturesPage.tsx │ │ ├── HomePage.tsx │ │ ├── SecurityPage.tsx │ │ ├── ToolsPage.tsx │ │ └── TroubleshootingPage.tsx │ ├── postcss.config.js │ ├── public │ │ ├── .well-known │ │ │ └── security.txt │ │ ├── 404.html │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── explain-product-pricing-methods-no-mcp.png │ │ ├── explain-product-pricing-methods.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── llms.txt │ │ ├── robots.txt │ │ ├── site.webmanifest │ │ └── sitemap.xml │ ├── README.md │ ├── scripts │ │ ├── generate-search-index.js │ │ ├── generate-sitemap.js │ │ └── search-dev.js │ ├── src │ │ └── styles │ │ ├── input.css │ │ └── prism-theme.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types.ts │ ├── utils │ │ ├── search.ts │ │ └── toolsData.ts │ └── vite.config.ts ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── convert-docs.js ├── SECURITY.md ├── server.json ├── src │ ├── clients │ │ ├── base │ │ │ ├── http-client.ts │ │ │ ├── oauth-token.ts │ │ │ └── ocapi-auth-client.ts │ │ ├── best-practices-client.ts │ │ ├── cartridge-generation-client.ts │ │ ├── docs │ │ │ ├── class-content-parser.ts │ │ │ ├── class-name-resolver.ts │ │ │ ├── documentation-scanner.ts │ │ │ ├── index.ts │ │ │ └── referenced-types-extractor.ts │ │ ├── docs-client.ts │ │ ├── log-client.ts │ │ ├── logs │ │ │ ├── index.ts │ │ │ ├── log-analyzer.ts │ │ │ ├── log-client.ts │ │ │ ├── log-constants.ts │ │ │ ├── log-file-discovery.ts │ │ │ ├── log-file-reader.ts │ │ │ ├── log-formatter.ts │ │ │ ├── log-processor.ts │ │ │ ├── log-types.ts │ │ │ └── webdav-client-manager.ts │ │ ├── ocapi │ │ │ ├── code-versions-client.ts │ │ │ ├── site-preferences-client.ts │ │ │ └── system-objects-client.ts │ │ ├── ocapi-client.ts │ │ └── sfra-client.ts │ ├── config │ │ ├── configuration-factory.ts │ │ └── dw-json-loader.ts │ ├── core │ │ ├── handlers │ │ │ ├── abstract-log-tool-handler.ts │ │ │ ├── base-handler.ts │ │ │ ├── best-practices-handler.ts │ │ │ ├── cartridge-handler.ts │ │ │ ├── client-factory.ts │ │ │ ├── code-version-handler.ts │ │ │ ├── docs-handler.ts │ │ │ ├── job-log-handler.ts │ │ │ ├── job-log-tool-config.ts │ │ │ ├── log-handler.ts │ │ │ ├── log-tool-config.ts │ │ │ ├── sfra-handler.ts │ │ │ ├── system-object-handler.ts │ │ │ └── validation-helpers.ts │ │ ├── server.ts │ │ └── tool-definitions.ts │ ├── index.ts │ ├── main.ts │ ├── services │ │ ├── file-system-service.ts │ │ ├── index.ts │ │ └── path-service.ts │ ├── tool-configs │ │ ├── best-practices-tool-config.ts │ │ ├── cartridge-tool-config.ts │ │ ├── code-version-tool-config.ts │ │ ├── docs-tool-config.ts │ │ ├── job-log-tool-config.ts │ │ ├── log-tool-config.ts │ │ ├── sfra-tool-config.ts │ │ └── system-object-tool-config.ts │ ├── types │ │ └── types.ts │ └── utils │ ├── cache.ts │ ├── job-log-tool-config.ts │ ├── job-log-utils.ts │ ├── log-cache.ts │ ├── log-tool-config.ts │ ├── log-tool-constants.ts │ ├── log-tool-utils.ts │ ├── logger.ts │ ├── ocapi-url-builder.ts │ ├── path-resolver.ts │ ├── query-builder.ts │ ├── utils.ts │ └── validator.ts ├── tests │ ├── __mocks__ │ │ ├── docs-client.ts │ │ ├── src │ │ │ └── clients │ │ │ └── base │ │ │ └── http-client.js │ │ └── webdav.js │ ├── base-handler.test.ts │ ├── base-http-client.test.ts │ ├── best-practices-handler.test.ts │ ├── cache.test.ts │ ├── cartridge-handler.test.ts │ ├── class-content-parser.test.ts │ ├── class-name-resolver.test.ts │ ├── client-factory.test.ts │ ├── code-version-handler.test.ts │ ├── code-versions-client.test.ts │ ├── config.test.ts │ ├── configuration-factory.test.ts │ ├── docs-handler.test.ts │ ├── documentation-scanner.test.ts │ ├── file-system-service.test.ts │ ├── job-log-handler.test.ts │ ├── job-log-utils.test.ts │ ├── log-client.test.ts │ ├── log-handler.test.ts │ ├── log-processor.test.ts │ ├── logger.test.ts │ ├── mcp │ │ ├── AGENTS.md │ │ ├── node │ │ │ ├── activate-code-version-advanced.full-mode.programmatic.test.js │ │ │ ├── code-versions.full-mode.programmatic.test.js │ │ │ ├── generate-cartridge-structure.docs-only.programmatic.test.js │ │ │ ├── get-available-best-practice-guides.docs-only.programmatic.test.js │ │ │ ├── get-available-sfra-documents.programmatic.test.js │ │ │ ├── get-best-practice-guide.docs-only.programmatic.test.js │ │ │ ├── get-hook-reference.docs-only.programmatic.test.js │ │ │ ├── get-job-execution-summary.full-mode.programmatic.test.js │ │ │ ├── get-job-log-entries.full-mode.programmatic.test.js │ │ │ ├── get-latest-debug.full-mode.programmatic.test.js │ │ │ ├── get-latest-error.full-mode.programmatic.test.js │ │ │ ├── get-latest-info.full-mode.programmatic.test.js │ │ │ ├── get-latest-job-log-files.full-mode.programmatic.test.js │ │ │ ├── get-latest-warn.full-mode.programmatic.test.js │ │ │ ├── get-log-file-contents.full-mode.programmatic.test.js │ │ │ ├── get-sfcc-class-documentation.docs-only.programmatic.test.js │ │ │ ├── get-sfcc-class-info.docs-only.programmatic.test.js │ │ │ ├── get-sfra-categories.docs-only.programmatic.test.js │ │ │ ├── get-sfra-document.programmatic.test.js │ │ │ ├── get-sfra-documents-by-category.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definition.full-mode.programmatic.test.js │ │ │ ├── get-system-object-definitions.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definitions.full-mode.programmatic.test.js │ │ │ ├── list-log-files.full-mode.programmatic.test.js │ │ │ ├── list-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-best-practices.docs-only.programmatic.test.js │ │ │ ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-job-logs-by-name.full-mode.programmatic.test.js │ │ │ ├── search-job-logs.full-mode.programmatic.test.js │ │ │ ├── search-logs.full-mode.programmatic.test.js │ │ │ ├── search-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-sfcc-methods.docs-only.programmatic.test.js │ │ │ ├── search-sfra-documentation.docs-only.programmatic.test.js │ │ │ ├── search-site-preferences.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-groups.full-mode.programmatic.test.js │ │ │ ├── summarize-logs.full-mode.programmatic.test.js │ │ │ ├── tools.docs-only.programmatic.test.js │ │ │ └── tools.full-mode.programmatic.test.js │ │ ├── README.md │ │ ├── test-fixtures │ │ │ └── dw.json │ │ └── yaml │ │ ├── activate-code-version.docs-only.test.mcp.yml │ │ ├── activate-code-version.full-mode.test.mcp.yml │ │ ├── get_latest_error.test.mcp.yml │ │ ├── get-available-best-practice-guides.docs-only.test.mcp.yml │ │ ├── get-available-best-practice-guides.full-mode.test.mcp.yml │ │ ├── get-available-sfra-documents.docs-only.test.mcp.yml │ │ ├── get-available-sfra-documents.full-mode.test.mcp.yml │ │ ├── get-best-practice-guide.docs-only.test.mcp.yml │ │ ├── get-best-practice-guide.full-mode.test.mcp.yml │ │ ├── get-code-versions.docs-only.test.mcp.yml │ │ ├── get-code-versions.full-mode.test.mcp.yml │ │ ├── get-hook-reference.docs-only.test.mcp.yml │ │ ├── get-hook-reference.full-mode.test.mcp.yml │ │ ├── get-job-execution-summary.full-mode.test.mcp.yml │ │ ├── get-job-log-entries.full-mode.test.mcp.yml │ │ ├── get-latest-debug.full-mode.test.mcp.yml │ │ ├── get-latest-error.full-mode.test.mcp.yml │ │ ├── get-latest-info.full-mode.test.mcp.yml │ │ ├── get-latest-job-log-files.full-mode.test.mcp.yml │ │ ├── get-latest-warn.full-mode.test.mcp.yml │ │ ├── get-log-file-contents.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-documentation.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-documentation.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-info.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-info.full-mode.test.mcp.yml │ │ ├── get-sfra-categories.docs-only.test.mcp.yml │ │ ├── get-sfra-categories.full-mode.test.mcp.yml │ │ ├── get-sfra-document.docs-only.test.mcp.yml │ │ ├── get-sfra-document.full-mode.test.mcp.yml │ │ ├── get-sfra-documents-by-category.docs-only.test.mcp.yml │ │ ├── get-sfra-documents-by-category.full-mode.test.mcp.yml │ │ ├── get-system-object-definition.docs-only.test.mcp.yml │ │ ├── get-system-object-definition.full-mode.test.mcp.yml │ │ ├── get-system-object-definitions.docs-only.test.mcp.yml │ │ ├── get-system-object-definitions.full-mode.test.mcp.yml │ │ ├── list-log-files.full-mode.test.mcp.yml │ │ ├── list-sfcc-classes.docs-only.test.mcp.yml │ │ ├── list-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-best-practices.docs-only.test.mcp.yml │ │ ├── search-best-practices.full-mode.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.test.mcp.yml │ │ ├── search-job-logs-by-name.full-mode.test.mcp.yml │ │ ├── search-job-logs.full-mode.test.mcp.yml │ │ ├── search-logs.full-mode.test.mcp.yml │ │ ├── search-sfcc-classes.docs-only.test.mcp.yml │ │ ├── search-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-sfcc-methods.docs-only.test.mcp.yml │ │ ├── search-sfcc-methods.full-mode.test.mcp.yml │ │ ├── search-sfra-documentation.docs-only.test.mcp.yml │ │ ├── search-sfra-documentation.full-mode.test.mcp.yml │ │ ├── search-site-preferences.docs-only.test.mcp.yml │ │ ├── search-site-preferences.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-groups.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-groups.full-mode.test.mcp.yml │ │ ├── summarize-logs.full-mode.test.mcp.yml │ │ ├── tools.docs-only.test.mcp.yml │ │ └── tools.full-mode.test.mcp.yml │ ├── oauth-token.test.ts │ ├── ocapi-auth-client.test.ts │ ├── ocapi-client.test.ts │ ├── path-service.test.ts │ ├── query-builder.test.ts │ ├── referenced-types-extractor.test.ts │ ├── servers │ │ ├── sfcc-mock-server │ │ │ ├── mock-data │ │ │ │ └── ocapi │ │ │ │ ├── code-versions.json │ │ │ │ ├── custom-object-attributes-customapi.json │ │ │ │ ├── custom-object-attributes-globalsettings.json │ │ │ │ ├── custom-object-attributes-versionhistory.json │ │ │ │ ├── site-preferences-ccv.json │ │ │ │ ├── site-preferences-fastforward.json │ │ │ │ ├── site-preferences-sfra.json │ │ │ │ ├── site-preferences-storefront.json │ │ │ │ ├── site-preferences-system.json │ │ │ │ ├── system-object-attribute-groups-campaign.json │ │ │ │ ├── system-object-attribute-groups-category.json │ │ │ │ ├── system-object-attribute-groups-order.json │ │ │ │ ├── system-object-attribute-groups-product.json │ │ │ │ ├── system-object-attribute-groups-sitepreferences.json │ │ │ │ ├── system-object-attributes-customeraddress.json │ │ │ │ ├── system-object-attributes-product-expanded.json │ │ │ │ ├── system-object-attributes-product.json │ │ │ │ ├── system-object-definition-category.json │ │ │ │ ├── system-object-definition-customer.json │ │ │ │ ├── system-object-definition-customeraddress.json │ │ │ │ ├── system-object-definition-order.json │ │ │ │ ├── system-object-definition-product.json │ │ │ │ ├── system-object-definitions-old.json │ │ │ │ └── system-object-definitions.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ ├── scripts │ │ │ │ └── setup-logs.js │ │ │ ├── server.js │ │ │ └── src │ │ │ ├── app.js │ │ │ ├── config │ │ │ │ └── server-config.js │ │ │ ├── middleware │ │ │ │ ├── auth.js │ │ │ │ ├── cors.js │ │ │ │ └── logging.js │ │ │ ├── routes │ │ │ │ ├── ocapi │ │ │ │ │ ├── code-versions-handler.js │ │ │ │ │ ├── oauth-handler.js │ │ │ │ │ ├── ocapi-error-utils.js │ │ │ │ │ ├── ocapi-utils.js │ │ │ │ │ ├── site-preferences-handler.js │ │ │ │ │ └── system-objects-handler.js │ │ │ │ ├── ocapi.js │ │ │ │ └── webdav.js │ │ │ └── utils │ │ │ ├── mock-data-loader.js │ │ │ └── webdav-xml.js │ │ └── sfcc-mock-server-manager.ts │ ├── sfcc-mock-server.test.ts │ ├── site-preferences-client.test.ts │ ├── system-objects-client.test.ts │ ├── utils.test.ts │ ├── validation-helpers.test.ts │ └── validator.test.ts ├── tsconfig.json └── tsconfig.test.json ``` # Files -------------------------------------------------------------------------------- /docs/dw_util/StringUtils.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.util 2 | 3 | # Class StringUtils 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.util.StringUtils 9 | 10 | ## Description 11 | 12 | String utility class. 13 | 14 | ## Constants 15 | 16 | ### ENCODE_TYPE_HTML 17 | 18 | **Type:** Number = 0 19 | 20 | String encoding type HTML. 21 | 22 | ### ENCODE_TYPE_WML 23 | 24 | **Type:** Number = 2 25 | 26 | String encoding type WML. 27 | 28 | ### ENCODE_TYPE_XML 29 | 30 | **Type:** Number = 1 31 | 32 | String encoding type XML. 33 | 34 | ### TRUNCATE_CHAR 35 | 36 | **Type:** String = "char" 37 | 38 | String truncate mode 'char'. Truncate string to the nearest character. Default mode if no truncate mode is specified. 39 | 40 | ### TRUNCATE_SENTENCE 41 | 42 | **Type:** String = "sentence" 43 | 44 | String truncate mode 'sentence'. Truncate string to the nearest sentence. 45 | 46 | ### TRUNCATE_WORD 47 | 48 | **Type:** String = "word" 49 | 50 | String truncate mode 'word'. Truncate string to the nearest word. 51 | 52 | ## Properties 53 | 54 | ## Constructor Summary 55 | 56 | ## Method Summary 57 | 58 | ### decodeBase64 59 | 60 | **Signature:** `static decodeBase64(base64 : String) : String` 61 | 62 | Interprets a Base64 encoded string as byte stream of an UTF-8 encoded string. 63 | 64 | ### decodeBase64 65 | 66 | **Signature:** `static decodeBase64(base64 : String, characterEncoding : String) : String` 67 | 68 | Interprets a Base64 encoded string as the byte stream representation of a string. 69 | 70 | ### decodeString 71 | 72 | **Signature:** `static decodeString(str : String, type : Number) : String` 73 | 74 | Convert a given syntax-safe string to a string according to the selected character entity encoding type. 75 | 76 | ### encodeBase64 77 | 78 | **Signature:** `static encodeBase64(str : String) : String` 79 | 80 | Encodes the byte representation of the given string as Base64. 81 | 82 | ### encodeBase64 83 | 84 | **Signature:** `static encodeBase64(str : String, characterEncoding : String) : String` 85 | 86 | Encodes the byte representation of the given string as Base64. 87 | 88 | ### encodeString 89 | 90 | **Signature:** `static encodeString(str : String, type : Number) : String` 91 | 92 | Convert a given string to a syntax-safe string according to the selected character entity encoding type. 93 | 94 | ### format 95 | 96 | **Signature:** `static format(format : String, args : Object...) : String` 97 | 98 | Returns a formatted string using the specified format and arguments. 99 | 100 | ### formatCalendar 101 | 102 | **Signature:** `static formatCalendar(calendar : Calendar) : String` 103 | 104 | Formats a Calendar object with Calendar.INPUT_DATE_TIME_PATTERN format of the current request locale, for example "MM/dd/yyyy h:mm a" for the locale en_US. 105 | 106 | ### formatCalendar 107 | 108 | **Signature:** `static formatCalendar(calendar : Calendar, format : String) : String` 109 | 110 | Formats a Calendar object with the provided date format. 111 | 112 | ### formatCalendar 113 | 114 | **Signature:** `static formatCalendar(calendar : Calendar, locale : String, pattern : Number) : String` 115 | 116 | Formats a Calendar object with the date format defined by the provided locale and Calendar pattern. 117 | 118 | ### formatDate 119 | 120 | **Signature:** `static formatDate(date : Date) : String` 121 | 122 | Formats a date with the default date format of the current site. 123 | 124 | ### formatDate 125 | 126 | **Signature:** `static formatDate(date : Date, format : String) : String` 127 | 128 | Formats a date with the provided date format. 129 | 130 | ### formatDate 131 | 132 | **Signature:** `static formatDate(date : Date, format : String, locale : String) : String` 133 | 134 | Formats a date with the provided date format in specified locale. 135 | 136 | ### formatInteger 137 | 138 | **Signature:** `static formatInteger(number : Number) : String` 139 | 140 | Returns a formatted integer number using the default integer format of the current site. 141 | 142 | ### formatMoney 143 | 144 | **Signature:** `static formatMoney(money : Money) : String` 145 | 146 | Formats a Money Object with the default money format of the current request locale. 147 | 148 | ### formatNumber 149 | 150 | **Signature:** `static formatNumber(number : Number) : String` 151 | 152 | Returns a formatted number using the default number format of the current site. 153 | 154 | ### formatNumber 155 | 156 | **Signature:** `static formatNumber(number : Number, format : String) : String` 157 | 158 | Returns a formatted string using the specified number and format. 159 | 160 | ### formatNumber 161 | 162 | **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String` 163 | 164 | Returns a formatted number as a string using the specified number format in specified locale. 165 | 166 | ### formatNumber 167 | 168 | **Signature:** `static formatNumber(number : Number, format : String) : String` 169 | 170 | Returns a formatted string using the specified number and format. 171 | 172 | ### formatNumber 173 | 174 | **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String` 175 | 176 | Returns a formatted number as a string using the specified number format in specified locale. 177 | 178 | ### garble 179 | 180 | **Signature:** `static garble(str : String, replaceChar : String, suffixLength : Number) : String` 181 | 182 | Return a string in which specified number of characters in the suffix is not changed and the rest of the characters replaced with specified character. 183 | 184 | ### ltrim 185 | 186 | **Signature:** `static ltrim(str : String) : String` 187 | 188 | Returns the string with leading white space removed. 189 | 190 | ### pad 191 | 192 | **Signature:** `static pad(str : String, width : Number) : String` 193 | 194 | This method provides cell padding functionality to the template. 195 | 196 | ### rtrim 197 | 198 | **Signature:** `static rtrim(str : String) : String` 199 | 200 | Returns the string with trailing white space removed. 201 | 202 | ### stringToHtml 203 | 204 | **Signature:** `static stringToHtml(str : String) : String` 205 | 206 | Convert a given string to an HTML-safe string. 207 | 208 | ### stringToWml 209 | 210 | **Signature:** `static stringToWml(str : String) : String` 211 | 212 | Converts a given string to a WML-safe string. 213 | 214 | ### stringToXml 215 | 216 | **Signature:** `static stringToXml(str : String) : String` 217 | 218 | Converts a given string to a XML-safe string. 219 | 220 | ### trim 221 | 222 | **Signature:** `static trim(str : String) : String` 223 | 224 | Returns the string with leading and trailing white space removed. 225 | 226 | ### truncate 227 | 228 | **Signature:** `static truncate(str : String, maxLength : Number, mode : String, suffix : String) : String` 229 | 230 | Truncate the string to the specified length using specified truncate mode. 231 | 232 | ## Method Detail 233 | 234 | ## Method Details 235 | 236 | ### decodeBase64 237 | 238 | **Signature:** `static decodeBase64(base64 : String) : String` 239 | 240 | **Description:** Interprets a Base64 encoded string as byte stream of an UTF-8 encoded string. The method throws an IllegalArgumentException in case the encoding failed because of a mismatch between the input string and the character encoding. 241 | 242 | **Parameters:** 243 | 244 | - `base64`: the Base64 encoded string - should not be empty or null. 245 | 246 | **Returns:** 247 | 248 | the decoded string. 249 | 250 | --- 251 | 252 | ### decodeBase64 253 | 254 | **Signature:** `static decodeBase64(base64 : String, characterEncoding : String) : String` 255 | 256 | **Description:** Interprets a Base64 encoded string as the byte stream representation of a string. The given character encoding is used for decoding the byte stream into the character representation. The method throws an IllegalArgumentException in case the encoding failed because of a mismatch between the input String and the character encoding. 257 | 258 | **Parameters:** 259 | 260 | - `base64`: the Base64 encoded string - should not be empty or null. 261 | - `characterEncoding`: the character encoding to read the input string - should not be empty or null. 262 | 263 | **Returns:** 264 | 265 | the decoded string. 266 | 267 | --- 268 | 269 | ### decodeString 270 | 271 | **Signature:** `static decodeString(str : String, type : Number) : String` 272 | 273 | **Description:** Convert a given syntax-safe string to a string according to the selected character entity encoding type. 274 | 275 | **Parameters:** 276 | 277 | - `str`: String to be decoded 278 | - `type`: decode type 279 | 280 | **Returns:** 281 | 282 | decoded string 283 | 284 | --- 285 | 286 | ### encodeBase64 287 | 288 | **Signature:** `static encodeBase64(str : String) : String` 289 | 290 | **Description:** Encodes the byte representation of the given string as Base64. The string is converted into the byte representation with UTF-8 encoding. The method throws an IllegalArgumentException in case the encoding failed because of a mismatch between the input string and the character encoding. 291 | 292 | **Parameters:** 293 | 294 | - `str`: the string to encode - should not be empty or null. 295 | 296 | **Returns:** 297 | 298 | the encoded string. 299 | 300 | --- 301 | 302 | ### encodeBase64 303 | 304 | **Signature:** `static encodeBase64(str : String, characterEncoding : String) : String` 305 | 306 | **Description:** Encodes the byte representation of the given string as Base64. The string is converted into the byte representation using the given character encoding. The method throws an IllegalArgumentException in case the encoding failed because of a mismatch between the input string and the character encoding. 307 | 308 | **Parameters:** 309 | 310 | - `str`: the string to encode - should not be empty or null. 311 | - `characterEncoding`: the character encoding to read the input string - should not be empty or null. 312 | 313 | **Returns:** 314 | 315 | the encoded string. 316 | 317 | --- 318 | 319 | ### encodeString 320 | 321 | **Signature:** `static encodeString(str : String, type : Number) : String` 322 | 323 | **Description:** Convert a given string to a syntax-safe string according to the selected character entity encoding type. 324 | 325 | **Parameters:** 326 | 327 | - `str`: String to be encoded 328 | - `type`: encode type 329 | 330 | **Returns:** 331 | 332 | encoded string 333 | 334 | --- 335 | 336 | ### format 337 | 338 | **Signature:** `static format(format : String, args : Object...) : String` 339 | 340 | **Description:** Returns a formatted string using the specified format and arguments. The formatting string is a Java MessageFormat expression, e.g. format( "Message: {0}, {1}", "test", 10 ) would result in "Message: test, 10". If a Collection is passed as the only argument, the elements of this collection are used as arguments for the formatting. 341 | 342 | **Parameters:** 343 | 344 | - `format`: Java like formatting string. 345 | - `args`: optional list of arguments or a collection, which are included into the result string 346 | 347 | **Returns:** 348 | 349 | the formatted result string. 350 | 351 | --- 352 | 353 | ### formatCalendar 354 | 355 | **Signature:** `static formatCalendar(calendar : Calendar) : String` 356 | 357 | **Description:** Formats a Calendar object with Calendar.INPUT_DATE_TIME_PATTERN format of the current request locale, for example "MM/dd/yyyy h:mm a" for the locale en_US. The used time zone is the time zone of the calendar object. 358 | 359 | **Parameters:** 360 | 361 | - `calendar`: the calendar object. 362 | 363 | **Returns:** 364 | 365 | a string representation of the formatted calendar object. 366 | 367 | --- 368 | 369 | ### formatCalendar 370 | 371 | **Signature:** `static formatCalendar(calendar : Calendar, format : String) : String` 372 | 373 | **Description:** Formats a Calendar object with the provided date format. The format is a Java date format, like "yyy-MM-dd". The used time zone is the time zone of the calendar object. 374 | 375 | **Parameters:** 376 | 377 | - `calendar`: the calendar object to be printed 378 | - `format`: the format to use. 379 | 380 | **Returns:** 381 | 382 | a string representation of the formatted calendar object. 383 | 384 | --- 385 | 386 | ### formatCalendar 387 | 388 | **Signature:** `static formatCalendar(calendar : Calendar, locale : String, pattern : Number) : String` 389 | 390 | **Description:** Formats a Calendar object with the date format defined by the provided locale and Calendar pattern. The locale can be for instance the request.getLocale(). The used time zone is the time zone of the calendar object. 391 | 392 | **Parameters:** 393 | 394 | - `calendar`: the calendar object to be printed 395 | - `locale`: the locale, which defines the date format to be used 396 | - `pattern`: the pattern is one of a calendar pattern e.g. SHORT_DATE_PATTERN as defined in the regional settings for the locale 397 | 398 | **Returns:** 399 | 400 | a string representation of the formatted calendar object. 401 | 402 | --- 403 | 404 | ### formatDate 405 | 406 | **Signature:** `static formatDate(date : Date) : String` 407 | 408 | **Description:** Formats a date with the default date format of the current site. 409 | 410 | **Deprecated:** 411 | 412 | Use formatCalendar(Calendar, String) instead. 413 | 414 | **Parameters:** 415 | 416 | - `date`: the date to format. 417 | 418 | **Returns:** 419 | 420 | a string representation of the formatted date. 421 | 422 | --- 423 | 424 | ### formatDate 425 | 426 | **Signature:** `static formatDate(date : Date, format : String) : String` 427 | 428 | **Description:** Formats a date with the provided date format. The format is the Java date format, like "yyyy-MM-DD". The locale of the calling context request is used in formatting. 429 | 430 | **Deprecated:** 431 | 432 | Use formatCalendar(Calendar, String) instead. 433 | 434 | **Parameters:** 435 | 436 | - `date`: the date to format. 437 | - `format`: the format to use. 438 | 439 | **Returns:** 440 | 441 | a string representation of the formatted date. 442 | 443 | --- 444 | 445 | ### formatDate 446 | 447 | **Signature:** `static formatDate(date : Date, format : String, locale : String) : String` 448 | 449 | **Description:** Formats a date with the provided date format in specified locale. The format is Java date format, like "yyyy-MM-DD". 450 | 451 | **Deprecated:** 452 | 453 | Use formatCalendar(Calendar, String) instead. 454 | 455 | **Parameters:** 456 | 457 | - `date`: the date to format. 458 | - `format`: the format to use. 459 | - `locale`: the locale to use. 460 | 461 | **Returns:** 462 | 463 | a string representation of the formatted date. 464 | 465 | --- 466 | 467 | ### formatInteger 468 | 469 | **Signature:** `static formatInteger(number : Number) : String` 470 | 471 | **Description:** Returns a formatted integer number using the default integer format of the current site. The method can be also called to format a floating number as integer. 472 | 473 | **Parameters:** 474 | 475 | - `number`: the number to format. 476 | 477 | **Returns:** 478 | 479 | a formatted an integer number with the default integer format of the current site. 480 | 481 | --- 482 | 483 | ### formatMoney 484 | 485 | **Signature:** `static formatMoney(money : Money) : String` 486 | 487 | **Description:** Formats a Money Object with the default money format of the current request locale. 488 | 489 | **Parameters:** 490 | 491 | - `money`: The Money instance that should be formatted. 492 | 493 | **Returns:** 494 | 495 | The formatted String representation of the passed money. In case of an error the string 'N/A' is returned. 496 | 497 | --- 498 | 499 | ### formatNumber 500 | 501 | **Signature:** `static formatNumber(number : Number) : String` 502 | 503 | **Description:** Returns a formatted number using the default number format of the current site. Decimal and grouping separators are used as specified in the locales regional settings. 504 | 505 | **Parameters:** 506 | 507 | - `number`: the number to format. 508 | 509 | **Returns:** 510 | 511 | a formatted number using the default number format of the current site. 512 | 513 | --- 514 | 515 | ### formatNumber 516 | 517 | **Signature:** `static formatNumber(number : Number, format : String) : String` 518 | 519 | **Description:** Returns a formatted string using the specified number and format. The format is Java number format, like "#,###.00". To format as an integer number provide "0" as format string. The locale of the calling context request is used in formatting. 520 | 521 | **API Versioned:** 522 | 523 | No longer available as of version 18.10. 524 | 525 | **Parameters:** 526 | 527 | - `number`: the number to format. 528 | - `format`: the format to use. 529 | 530 | **Returns:** 531 | 532 | a formatted string using the specified number and format. 533 | 534 | --- 535 | 536 | ### formatNumber 537 | 538 | **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String` 539 | 540 | **Description:** Returns a formatted number as a string using the specified number format in specified locale. The format is Java number format, like "#,###.00". To format as an integer number provide "0" as format string. 541 | 542 | **API Versioned:** 543 | 544 | No longer available as of version 18.10. 545 | 546 | **Parameters:** 547 | 548 | - `number`: the number to format. 549 | - `format`: the format to use. 550 | - `locale`: the locale to use. 551 | 552 | **Returns:** 553 | 554 | a formatted number as a string using the specified number format in specified locale. 555 | 556 | --- 557 | 558 | ### formatNumber 559 | 560 | **Signature:** `static formatNumber(number : Number, format : String) : String` 561 | 562 | **Description:** Returns a formatted string using the specified number and format. The format is Java number format, like "#,###.00". To format as an integer number provide "0" as format string. The locale of the calling context request is used in formatting. Decimal and grouping separators are used as specified in the locales regional settings (when configured, otherwise a fallback to the internal configuration is done). 563 | 564 | **API Versioned:** 565 | 566 | From version 18.10. In prior versions this method did fall back to Java formatting rules, instead of using the definitions in regional settings. 567 | 568 | **Parameters:** 569 | 570 | - `number`: the number to format. 571 | - `format`: the format to use. 572 | 573 | **Returns:** 574 | 575 | a formatted string using the specified number and format. 576 | 577 | --- 578 | 579 | ### formatNumber 580 | 581 | **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String` 582 | 583 | **Description:** Returns a formatted number as a string using the specified number format in specified locale. The format is Java number format, like "#,###.00". To format as an integer number provide "0" as format string. Decimal and grouping separators are used as specified in the locales regional settings (when configured, otherwise a fallback to the internal configuration is done). 584 | 585 | **API Versioned:** 586 | 587 | From version 18.10. In prior versions this method did fall back to Java formatting rules, instead of using the definitions in regional settings. 588 | 589 | **Parameters:** 590 | 591 | - `number`: the number to format. 592 | - `format`: the format to use. 593 | - `locale`: the locale to use. 594 | 595 | **Returns:** 596 | 597 | a formatted number as a string using the specified number format in specified locale. 598 | 599 | --- 600 | 601 | ### garble 602 | 603 | **Signature:** `static garble(str : String, replaceChar : String, suffixLength : Number) : String` 604 | 605 | **Description:** Return a string in which specified number of characters in the suffix is not changed and the rest of the characters replaced with specified character. 606 | 607 | **Parameters:** 608 | 609 | - `str`: String to garble 610 | - `replaceChar`: character to use as a replacement 611 | - `suffixLength`: length of the suffix 612 | 613 | **Returns:** 614 | 615 | the garbled string. 616 | 617 | --- 618 | 619 | ### ltrim 620 | 621 | **Signature:** `static ltrim(str : String) : String` 622 | 623 | **Description:** Returns the string with leading white space removed. 624 | 625 | **Parameters:** 626 | 627 | - `str`: the String to remove characters from. 628 | 629 | **Returns:** 630 | 631 | the string with leading white space removed. 632 | 633 | --- 634 | 635 | ### pad 636 | 637 | **Signature:** `static pad(str : String, width : Number) : String` 638 | 639 | **Description:** This method provides cell padding functionality to the template. 640 | 641 | **Parameters:** 642 | 643 | - `str`: the string to process 644 | - `width`: The absolute value of this number defines the width of the cell. A possitive number forces left, a negative number right alignment. A '0' doesn't change the string. 645 | 646 | **Returns:** 647 | 648 | the processed string. 649 | 650 | --- 651 | 652 | ### rtrim 653 | 654 | **Signature:** `static rtrim(str : String) : String` 655 | 656 | **Description:** Returns the string with trailing white space removed. 657 | 658 | **Parameters:** 659 | 660 | - `str`: the String to remove characters from. 661 | 662 | **Returns:** 663 | 664 | the string with trailing white space removed. 665 | 666 | --- 667 | 668 | ### stringToHtml 669 | 670 | **Signature:** `static stringToHtml(str : String) : String` 671 | 672 | **Description:** Convert a given string to an HTML-safe string. This method substitutes characters that conflict with HTML syntax (<,>,&,") and characters that are beyond the ASCII chart (Unicode 160-255) to HTML 3.2 named character entities. 673 | 674 | **Parameters:** 675 | 676 | - `str`: String to be converted. 677 | 678 | **Returns:** 679 | 680 | converted string. 681 | 682 | --- 683 | 684 | ### stringToWml 685 | 686 | **Signature:** `static stringToWml(str : String) : String` 687 | 688 | **Description:** Converts a given string to a WML-safe string. This method substitutes characters that conflict with WML syntax (<,>,&,',"$) to WML named character entities. 689 | 690 | **Deprecated:** 691 | 692 | Don't use this method anymore 693 | 694 | **Parameters:** 695 | 696 | - `str`: String to be converted. 697 | 698 | **Returns:** 699 | 700 | the converted string. 701 | 702 | --- 703 | 704 | ### stringToXml 705 | 706 | **Signature:** `static stringToXml(str : String) : String` 707 | 708 | **Description:** Converts a given string to a XML-safe string. This method substitutes characters that conflict with XML syntax (<,>,&,',") to XML named character entities. 709 | 710 | **Parameters:** 711 | 712 | - `str`: String to be converted. 713 | 714 | **Returns:** 715 | 716 | the converted string. 717 | 718 | --- 719 | 720 | ### trim 721 | 722 | **Signature:** `static trim(str : String) : String` 723 | 724 | **Description:** Returns the string with leading and trailing white space removed. 725 | 726 | **Parameters:** 727 | 728 | - `str`: the string to trim. 729 | 730 | **Returns:** 731 | 732 | the string with leading and trailing white space removed. 733 | 734 | --- 735 | 736 | ### truncate 737 | 738 | **Signature:** `static truncate(str : String, maxLength : Number, mode : String, suffix : String) : String` 739 | 740 | **Description:** Truncate the string to the specified length using specified truncate mode. Optionally, append suffix to truncated string. 741 | 742 | **Parameters:** 743 | 744 | - `str`: string to truncate 745 | - `maxLength`: maximum length of the truncated string, not including suffix 746 | - `mode`: truncate mode (TRUNCATE_CHAR, TRUNCATE_WORD, TRUNCATE_SENTENCE), if null TRUNCATE_CHAR is assumed 747 | - `suffix`: suffix append to the truncated string 748 | 749 | **Returns:** 750 | 751 | the truncated string. 752 | 753 | --- ``` -------------------------------------------------------------------------------- /docs/best-practices/performance.md: -------------------------------------------------------------------------------- ```markdown 1 | # Salesforce B2C Commerce Cloud: Performance Best Practices 2 | 3 | This document outlines key performance optimization strategies for Salesforce B2C Commerce Cloud, focusing on caching and efficient data retrieval. 4 | 5 | --- 6 | 7 | ## Performance and Stability Coding Standards 8 | 9 | Ecommerce applications built on Salesforce B2C Commerce can run fast and perform reliably. Use B2C Commerce within its capabilities and ensure that your customizations follow coding best practices. Identify permissible designs that ensure the scalability and robustness of your customizations. 10 | 11 | ### Data Transfer Volume 12 | 13 | B2C Commerce imposes limits on incoming and outgoing network traffic for B2C Commerce instances and the Content Delivery Network. 14 | 15 | These limits are relative to the Gross Merchandise Value (GMV) and are defined in the Main Subscription Agreement (MSA). Data transfers within the limits are at no additional charge (are included in the subscription fee). There is a fee for data transfers exceeding the limits. 16 | 17 | ### Storefront Development for Performance and Stability 18 | 19 | When developing your storefront, consider storefront development best practices: 20 | 21 | #### Search and Product Processing 22 | 23 | - **Don't post-process product or content search results.** Search results can be large sets. Instead, all search criteria must go into the query for efficiency execution. Don't post-process with custom code. 24 | 25 | - **Don't iterate over variations of a base product on a search result page** (or any page where multiple base products appear). This approach can significantly increase the number of touched business objects. Instead, use native Salesforce B2C Commerce features: 26 | - Use pipelet Search with input parameter `OrderableProductsOnly` to deal with variation product availability 27 | - Use `dw.catalog.ProductSearchHit.getRepresentedVariationValues()` to determine available variation values 28 | - Use `dw.catalog.ProductSearchHit.minPrice` or `Product.priceMode.minPrice` to determine price ranges 29 | 30 | - **Break search results into pages** before processing or displaying in the storefront (pipelet Paging, class PagingModel). Limit the maximum page size, for example, a maximum of 120 products per page, especially if the "View All" functionality is provided. 31 | 32 | #### External System Integration 33 | 34 | - **Don't trigger live calls to external systems on frequently visited pages** (homepage, category, search result pages, and product pages). Where live calls are needed, specify a low timeout value (for example, 1 second). A B2C Commerce application server thread waiting for a response from an external system can't serve other requests. Many threads waiting for responses can make the entire cluster unresponsive. 35 | 36 | #### Long-Running Operations 37 | 38 | - **Don't execute any long running operations in a storefront controller or pipeline** (for example, import or export). Instead, use "jobs" for all long running tasks. The web tier closes browser connections after 5 minutes. The controller or pipeline could still be running at this time. 39 | 40 | #### Concurrency and Data Integrity 41 | 42 | - **Avoid concurrent changes to the same object.** Storefront controllers and pipelines should only: 43 | - Read shared data (for example, catalogs and prices) 44 | - Read or write customer-specific data (for example, customer profiles, shopping carts or orders) 45 | 46 | - The inventory framework is designed to support concurrent change (for example, two customers buying the same product at the same time or a customer buying a product while the inventory import is running). 47 | 48 | - The storefront controller or pipeline marks the order with `EXPORT_STATUS_READY` as the last step in order creation. Then order processing jobs can start modifying the order object. 49 | 50 | - Concurrent requests for the same session are serialized at the application server. Concurrent Script API controller or pipeline requests can lead to Optimistic Locking exceptions. 51 | 52 | #### Transaction Management 53 | 54 | - **Limit transaction size.** The system is designed to deal with transactions with up to 1,000 modified business objects. A storefront controller or pipeline shouldn't even come close to this number. 55 | 56 | #### Critical Page Performance 57 | 58 | - **Make sure that the most visited pages are cacheable and well performing.** These controllers and pages are usually: 59 | - Category page or search result pages (Search-Show) 60 | - Product detail pages (Product-Show) 61 | - Home pages (Default-Start, Home-Show) 62 | - Cart Page (Cart-Show) 63 | - Checkout pages 64 | 65 | - **Limit expensive (> 10 ms) custom server logic** on OnSession and OnRequest controllers. 66 | 67 | ### Use Index-Friendly APIs 68 | 69 | Replace database intensive or inefficient APIs with appropriate index-friendly APIs. Check code for database intensive APIs in most-visited pages: 70 | 71 | #### Avoid These Database-Intensive APIs: 72 | - `Category.getProducts()` 73 | - `Category.getOnlineProducts()` 74 | - `Category.getProductAssignments()` 75 | - `Category.getOnlineCategoryAssignments()` 76 | - `ProductMgr.queryAllSiteProducts()` 77 | - `Product.getPriceModel()` 78 | - `Product.getVariants()` 79 | - `Product.getVariationModel()` 80 | 81 | #### Use These Index-Friendly APIs Instead: 82 | - `ProductSearchModel.search()` 83 | - `ProductSearchModel.orderableProductsOnly(true)` 84 | - `ProductSearchModel.getRefinements()` 85 | - `ProductSearchRefinements.getNextLevelRefinementValues()` 86 | - `ProductSearchModel.getProductSearchHits()` 87 | - `ProductSearchHit.getMinPrice()` 88 | - `ProductSearchHit.getMaxPrice()` 89 | - `ProductSearchHit.getRepresentedProductIDs()` 90 | - `ProductSearchHit.getRepresentedVariationValues(attribute)` 91 | 92 | ### Additional Performance Requirements 93 | 94 | - **Ensure all direct 3rd-party HTTP calls are migrated to Web Service Framework** 95 | - **Ensure no Enforced quota violations** are reported in STAGING and PRODUCTION, and that Quota Dashboard alerts have been subscribed by all site admins 96 | - **Ensure there isn't unnecessary creation** of custom Session objects, productlist objects, or cookies 97 | - **Ensure a WishList isn't created for every anonymous user** (e.g., created at the end of every product item add to cart calls) 98 | - **Ensure Custom Object volume is kept in check** with purge jobs 99 | 100 | #### OCAPI Specific Requirements: 101 | - **Ensure Shop API GET requests are limited to smaller blocks of data.** Instead of 200 products payload, retrieve 100 or 50 102 | - **Ensure there's no OCAPI request of persistent objects within a hook customization** such as `ProductMgr.getProduct()` or `product.getVariations()` 103 | 104 | #### SFRA Specific Requirements: 105 | - **Ensure SFRA templates don't include multi-part, embedded, or nested forms.** We don't recommend them as a best practice 106 | - **Ensure that controllers don't call each other,** because controller functionality should be self-contained to avoid circular dependencies 107 | - **Ensure no calling pipelets from within a controller.** It's allowed while there are still pipelets that don't have equivalent B2C Commerce script methods, but won't be supported in future 108 | 109 | ### Job Development for Performance and Stability 110 | 111 | To optimize job performance, follow the job development standards: 112 | 113 | #### Import and Data Processing 114 | 115 | - **To modify objects in Salesforce B2C Commerce, use standard imports instead of customizations.** In jobs, use B2C Commerce Job Steps for imports. 116 | 117 | - **B2C Commerce standard imports are designed to process arbitrary feed sizes.** Changes are committed to the database on a per business object basis. If related changes must be committed in a single transaction, enclose the import pipelets in an explicit controller or pipeline transaction. Choose this approach only as an exception. 118 | 119 | - **The transaction size is limited to 1,000 modified business objects.** Ensure that this limit isn't exceeded. B2C Commerce does not enforce this limit today, but might in the future. 120 | 121 | #### Data Quality and Validation 122 | 123 | - **Don't implement data validation jobs on B2C Commerce** (for example, products with no names or $0 prices). Instead, ensure that the feeds into B2C Commerce are of high quality, and don't include products with incomplete attribution or are marked offline. You can manually review catalog data on a staging instance. 124 | 125 | #### Memory Management 126 | 127 | When processing large data sets, pay attention to the memory footprint: 128 | 129 | - **Design loop logic so that memory consumption doesn't increase with result set size** 130 | - **Keep only currently processed objects in memory,** and do not retain references to that object (so that the object can be freed from memory). Specifically, don't perform sorting or other types of collections in memory 131 | - **Stream data to file regularly** (do not build large structures in memory) 132 | - **Read feeds record by record** (do not read an entire file into memory) 133 | - **If you must create multiple feeds,** query the objects once and write records to all feeds as you iterate over the results. This approach saves time because the objects must be created in memory only once 134 | 135 | #### Concurrency and Resource Management 136 | 137 | - **Avoid concurrent changes to the same object.** Use the locking framework to ensure exclusive access. Specify named resources for job schedules. 138 | 139 | - **Keep application server utilization by jobs to a minimum.** Calculate the job load factor: total number of seconds of job execution time on an instance (Staging or Production) on a day divided by 86,400 (number of seconds in a day). Try to keep the job load factor below 0.20. 140 | 141 | #### Recovery and Reliability 142 | 143 | - **Pay attention to recovery in solution design.** A job might end abnormally, for example, server restart or application server failure. The job can be resumed or restarted. Design the job so that it recovers gracefully. It must be possible to repeat a job step that was aborted. 144 | 145 | - **Don't start many jobs at the same time.** Instead, disperse job start times to balance the job load. 146 | 147 | --- 148 | 149 | ## 1. Page Caching 150 | 151 | The web-server page cache is the most critical performance feature for server-rendered storefronts. The goal is to serve fully rendered HTML from this cache to avoid hitting the application server. 152 | 153 | ### Controller-Driven Caching (Best Practice) 154 | 155 | Control caching within your controller using the response object. This is superior to the legacy `<iscache>` tag. 156 | 157 | `response.setExpires(milliseconds)`: Sets a cache duration for the entire page response. 158 | 159 | **Example:** 160 | 161 | ```javascript 162 | // cartridge/controllers/Product.js 163 | var server = require('server'); 164 | 165 | server.get('Show', function (req, res, next) { 166 | // Cache for 24 hours 167 | var oneDay = 24 * 60 * 60 * 1000; 168 | response.setExpires(Date.now() + oneDay); 169 | 170 | res.render('product/productDetails'); 171 | next(); 172 | }); 173 | ``` 174 | 175 | ### Remote Includes for Dynamic Content 176 | 177 | Use remote includes (`<isinclude url="..." />`) to assemble pages from components with different cache policies. A long-cached main page can include a dynamic, non-cached header with user-specific info. [1, 2] 178 | 179 | **Anti-Pattern:** Avoid creating remote includes with unique URL parameters for each item in a list (e.g., `&position=1`, `&position=2`). This creates an N+1 request problem at the HTTP level and defeats the cache. [1, 3] 180 | 181 | ### Cache Key Strategy 182 | 183 | The cache key is the full URL. To maximize the cache hit ratio: 184 | 185 | - **Ignore Volatile Parameters:** Use Business Manager (`Administration > Sites > Feature Switches`) to ignore marketing parameters (e.g., `utm_source`, `utm_campaign`) when generating the cache key. [4, 5] 186 | - **Personalized Caching:** Use `response.setVaryBy('price_promotion')` to create separate cache entries for users with different prices or promotions. Use this carefully, as it can fragment the cache. [4, 5] 187 | 188 | --- 189 | 190 | ## 2. Custom Caches (`CacheMgr`) 191 | 192 | Use custom caches for application-level data caching within dynamic requests (e.g., cart, checkout) where page caching isn't possible. 193 | 194 | **Use Cases:** 195 | - Caching expensive calculations (e.g., iterating variants to check for a sale). 196 | - Caching responses from external services (e.g., inventory, ratings). 197 | 198 | ### Implementation 199 | 200 | 1. **Define in `caches.json`:** 201 | ```json 202 | { 203 | "caches": [ 204 | { 205 | "name": "ExternalAPICache", 206 | "ttl": 300 207 | } 208 | ] 209 | } 210 | ``` 211 | 212 | 2. **Register in `package.json`:** 213 | ```json 214 | { 215 | "caches": "./cartridge/scripts/caches.json" 216 | } 217 | ``` 218 | 219 | ### The "Get-or-Load" Pattern (Required) 220 | 221 | Always use the atomic `cache.get(key, loader)` method to prevent a "thundering herd" problem on cache misses. [6, 9] 222 | 223 | **Example:** 224 | 225 | ```javascript 226 | var CacheMgr = require('dw/system/CacheMgr'); 227 | var MyHTTPService = require('~/cartridge/scripts/services/myHTTPService'); 228 | 229 | function getExternalData() { 230 | var apiCache = CacheMgr.getCache('ExternalAPICache'); 231 | var cacheKey = 'myExternalData'; 232 | 233 | // get() executes the loader function ONLY on a cache miss. 234 | var data = apiCache.get(cacheKey, function () { 235 | // This expensive call only runs if data is not in cache. 236 | var result = MyHTTPService.getService().call(); 237 | return result.ok ? JSON.parse(result.object.text) : null; 238 | }); 239 | 240 | return data; 241 | } 242 | ``` 243 | 244 | **Key Limitation:** Custom caches are local to each application server pod and are not a distributed, instance-wide cache. Data stored on one pod is not visible to others. 245 | 246 | ## 3. ProductSearchModel vs. ProductMgr 247 | 248 | This is a critical performance distinction. 249 | 250 | - **ProductSearchModel (PSM):** Queries the fast, optimized Search Index. Use for any list of products (PLPs, search results, filtering). 251 | - **ProductMgr:** Queries the live Database. Use only to get a single, known product by its ID (e.g., on a PDP). 252 | 253 | ### The N+1 Anti-Pattern (CRITICAL) 254 | 255 | **NEVER** use `ProductMgr.getProduct()` inside a loop over ProductSearchModel results. This causes one fast index query followed by N slow database queries, which will crash a site under load. 256 | 257 | **Incorrect (Anti-Pattern):** 258 | 259 | ```javascript 260 | // In a PLP template, looping over search results 261 | var psm = new ProductSearchModel(); 262 | //... configure psm... 263 | psm.search(); 264 | var hits = psm.getProductSearchHits(); 265 | while (hits.hasNext()) { 266 | var hit = hits.next(); 267 | // ANTI-PATTERN: Calling ProductMgr in a loop! 268 | var product = ProductMgr.getProduct(hit.getProductID()); 269 | //... do something with the full product object... 270 | } 271 | ``` 272 | 273 | **Correct:** 274 | 275 | Use the ProductSearchHit object directly. It contains all necessary data from the index for display on a listing page. If data is missing, add it to the search index configuration. 276 | 277 | ```javascript 278 | // In a PLP template, looping over search results 279 | var psm = new ProductSearchModel(); 280 | //... configure psm... 281 | psm.search(); 282 | var hits = psm.getProductSearchHits(); 283 | while (hits.hasNext()) { 284 | var hit = hits.next(); 285 | // CORRECT: Use the hit object directly for name, price, etc. 286 | var minPrice = hit.getMinPrice(); 287 | var variationValues = hit.getRepresentedVariationValues('color'); 288 | //... render tile using data from 'hit'... 289 | } 290 | ``` 291 | 292 | ## 4. Caching in OCAPI/SCAPI Hooks 293 | 294 | The caching models for OCAPI and SCAPI are fundamentally different. 295 | 296 | - **OCAPI:** The hook runs before the response is cached. The modified response is what gets stored in OCAPI's application-tier cache. 297 | - **SCAPI:** The web-tier cache is checked before the hook runs. The hook only executes on a cache miss. The original, unmodified response from the platform is what gets cached. 298 | 299 | ### SCAPI Web-Tier Cache Fundamentals 300 | 301 | One of the Commerce Cloud application layer components performs web-tier caching for SCAPI **GET** requests across multiple API families. This cache lives on the server side and is applied only after a request reaches the platform. Any additional caching layers you add (CDN, browser, SPA state) operate independently—you could have a cache miss on the web tier but a hit in your edge cache, and vice versa. Plan your caching strategy with this multi-layer reality in mind. 302 | 303 | ### Personalized Cache Keys 304 | 305 | When personalization is enabled for a SCAPI resource, the cache key includes the following in addition to the URL string: 306 | 307 | - Active promotions 308 | - Active product sorting rules 309 | - Applicable price books 310 | - Active AB test groups 311 | 312 | The platform keeps separate cache entries for each combination. For example, if shopper A qualifies for promotion X and shopper B qualifies for promotion Y, the same product URL produces two cache entries. This segmentation can be powerful but also multiplies cache storage. Use personalization only when you have well-sized groups and a clear business reason. 313 | 314 | By default, product requests that expand prices or promotions—and product search requests with the `prices` expand—are already personalized. Calling `response.setVaryBy('price_promotion')` in a script reinforces that behavior. Note that `price_promotion` is the only supported value; other strings have no effect. 315 | 316 | ### Script-Level Cache Controls 317 | 318 | Use the Script API to adjust cache policies dynamically: 319 | 320 | - `dw.system.Response#setExpires(milliseconds)`: Sets an explicit expiration timestamp. The value must be at least 1,000 ms in the future and no more than 86,400,000 ms (24 hours). 321 | - `dw.system.Response#setVaryBy('price_promotion')`: Opts into personalized caching for price- or promotion-sensitive responses. 322 | 323 | ```javascript 324 | exports.modifyGETResponse = function (scriptCategory, categoryWO) { 325 | // Cache for one hour instead of the default TTL 326 | response.setExpires(Date.now() + 3_600_000); 327 | 328 | // Optional: personalize by price & promotion eligibility 329 | response.setVaryBy('price_promotion'); 330 | 331 | return new Status(Status.OK); 332 | }; 333 | ``` 334 | 335 | ### Best Practices 336 | 337 | - **OCAPI Hook:** Your modifications will be cached. Keep the logic simple and avoid slow calls like `ProductMgr.getProduct()`. 338 | - **SCAPI Hook:** To cache a modification, you must create a unique cache key by adding a custom query parameter to the URL. 339 | 340 | **Example (SCAPI):** 341 | 342 | ```javascript 343 | // Client makes a call with a custom parameter 344 | // GET /shopper-products/v1/.../products/my-prod?c_view=light 345 | 346 | // SCAPI Hook Script (product.js) 347 | exports.modifyResponse = function (product, productResponse) { 348 | // Logic is conditional on the custom parameter 349 | if (request.httpParameters.c_view === 'light') { 350 | delete productResponse.long_description; 351 | } 352 | }; 353 | ``` 354 | 355 | This creates a separate, cacheable version of the response for the `c_view=light` URL. 356 | 357 | **SCAPI expand Parameter:** The cache TTL for a SCAPI response is determined by the lowest TTL of all requested expand parameters. Avoid requesting volatile data (like availability, 60s TTL) alongside stable data (like images, 24hr TTL). 358 | 359 | ## 5. Caching in Custom SCAPI Endpoints 360 | 361 | You can build your own REST endpoints that integrate with SCAPI's caching, security, and other framework features. 362 | 363 | ### Implementation 364 | 365 | Custom endpoints can leverage the same powerful web-tier page cache. Enable it by calling `response.setExpires()` in your implementation script. 366 | 367 | **Example:** 368 | 369 | ```javascript 370 | // cartridge/rest-apis/my-api/v1/script.js 371 | var RESTResponseMgr = require('dw/system/RESTResponseMgr'); 372 | 373 | exports.getLoyaltyInfo = function (params) { 374 | var loyaltyData = { id: params.c_customer_id, points: 1234 }; 375 | 376 | // Cache this custom API response for 5 minutes 377 | response.setExpires(Date.now() + (5 * 60 * 1000)); 378 | 379 | RESTResponseMgr.createSuccess(loyaltyData).render(); 380 | }; 381 | exports.getLoyaltyInfo.public = true; 382 | ``` 383 | 384 | ### Two-Tier Caching Pattern (for External Services) 385 | 386 | For maximum resilience and performance when calling external systems, combine both cache layers: 387 | 388 | 1. **Tier 1 (Application Cache):** Use CacheMgr with the "get-or-load" pattern to cache the raw data from the external service. This acts as a buffer if the service is slow or down. 389 | 390 | 2. **Tier 2 (Web-Tier Cache):** In your custom endpoint script, after getting data from the Tier 1 cache, format the final JSON response and set a web-tier cache policy on it using `response.setExpires()`. 391 | 392 | This pattern ensures that most requests are served instantly from the web-tier, and even on a miss, the data is likely served from the fast application-tier cache, minimizing slow calls to the external dependency. 393 | ``` -------------------------------------------------------------------------------- /docs/dw_order/BasketMgr.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.order 2 | 3 | # Class BasketMgr 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.order.BasketMgr 9 | 10 | ## Description 11 | 12 | Provides static helper methods for managing baskets. 13 | 14 | ## Properties 15 | 16 | ### baskets 17 | 18 | **Type:** List (Read Only) 19 | 20 | Retrieve all open baskets for the logged in customer including the temporary baskets. 21 | 22 | 23 | Restricted to agent scenario use cases: The returned list contains all agent baskets created with 24 | createAgentBasket() and the current storefront basket which can also be retrieved with 25 | getCurrentBasket(). This method will result in an exception if called by a user without permission 26 | Create_Order_On_Behalf_Of or if no customer is logged in the session. 27 | 28 | 29 | Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore. 30 | 31 | ### currentBasket 32 | 33 | **Type:** Basket (Read Only) 34 | 35 | This method returns the current valid basket of the session customer or null if no current valid 36 | basket exists. 37 | 38 | The methods getCurrentBasket() and getCurrentOrNewBasket() work based on the selected basket 39 | persistence, which can be configured in the Business Manager site preferences / baskets section. A basket is 40 | valid for the configured basket lifetime. 41 | 42 | 43 | In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also 44 | utilizing PWA Kit or other custom headless solution for another part of the same site), this method must 45 | NOT be used. Instead, retrieve baskets via GET baskets/{basketId} or 46 | GET customers/{customerId}/baskets. Do not use getCurrentOrNewBasket() for basket creation 47 | in any scenario. 48 | 49 | 50 | The current basket, if one exists, is usually updated by the method. In particular the last-modified date is 51 | updated. No update is done when method getCurrentBasket() is used within a read-only hook 52 | implementation (such as a beforeGet or a modifyResponse hook). The lifetime of a basket can be extended 53 | in 2 ways: 54 | 55 | The basket is modified in some way, e.g. a product is added resulting in the basket total being newly 56 | calculated. This results in the basket lifetime being reset. 57 | The basket has not been modified for 60 minutes, then using this method to access the basket will also reset 58 | the basket lifetime. 59 | 60 | 61 | 62 | What happens when a customer logs in? Personal data held inside the basket such as addresses, email addresses and 63 | payment settings is associated with the customer to whom the basket belongs. If the basket being updated belongs 64 | to a different customer this data is removed. This happens when a guest customer that has a basket logs in and 65 | hence identifies as a registered customer. In this case the basket which was previously created by the guest 66 | customer gets transferred to the (now logged in) registered customer. Should the registered customer already have 67 | a basket, this basket is effectively invalidated, but made available using getStoredBasket() allowing 68 | the script to merge content from it if desired. 69 | 70 | 71 | What happens when a customer logs out or when the customer session times out? After the customer logs out, a 72 | basket belonging to the registered customer (now logged out) is stored (where applicable) and this method 73 | returns null. Personal data is also cleared when the session times out for a guest customer. 74 | 75 | 76 | The following personal data is cleared: 77 | 78 | product line items that were added from a wish list 79 | shipping method 80 | coupon line items 81 | gift certificate line items 82 | billing and shipping addresses 83 | payment instruments 84 | buyer email 85 | 86 | If the session currency no longer matches the basket currency, the basket currency should be updated with 87 | Basket.updateCurrency(). 88 | 89 | 90 | Typical usage: 91 | 92 | var basket : Basket = BasketMgr.getCurrentBasket(); 93 | if (basket) { 94 | // do something with basket 95 | } 96 | 97 | 98 | 99 | Constraints: 100 | 101 | The method only accesses the basket for the session customer, an exception is thrown when the session 102 | customer is null. 103 | Method getCurrentOrNewBasket() only creates a basket when method getCurrentBasket() returns 104 | null. 105 | 106 | ### currentOrNewBasket 107 | 108 | **Type:** Basket (Read Only) 109 | 110 | This method returns the current valid basket of the session customer or creates a new one if no current valid 111 | basket exists. See getCurrentBasket() for more details. 112 | 113 | 114 | In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also 115 | utilizing PWA Kit or other custom headless solution for another part of the same site), this method must 116 | NOT be used. For these scenarios, create baskets via POST baskets REST calls. 117 | 118 | ### storedBasket 119 | 120 | **Type:** Basket (Read Only) 121 | 122 | This method returns the stored basket of the session customer or null if none is found. A stored 123 | basket is returned in the following situation: 124 | 125 | During one visit, a customer-Q logs in and creates a basket-A by adding products to it. 126 | In a later visit, a second basket-B is created for a guest customer who then logs in as customer-Q. 127 | 128 | In this case basket-B is reassigned to customer-Q and basket-A is accessible as the stored basket using this method. 129 | Now it is possible to merge the information from the stored basket to the active basket. 130 | 131 | A stored basket will exist only if the corresponding setting is selected in the Business Manager site 132 | preferences' baskets section. A basket is valid for the configured basket lifetime. 133 | 134 | Typical usage: 135 | 136 | var currentBasket : Basket = BasketMgr.getCurrentOrNewBasket(); 137 | var storedBasket : Basket = BasketMgr.getStoredBasket(); 138 | if (storedBasket) { 139 | // transfer all the data needed from the stored to the active basket 140 | } 141 | 142 | ### temporaryBaskets 143 | 144 | **Type:** List (Read Only) 145 | 146 | Retrieve all open temporary baskets for the logged in customer. 147 | 148 | Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore. 149 | 150 | ## Constructor Summary 151 | 152 | ## Method Summary 153 | 154 | ### createAgentBasket 155 | 156 | **Signature:** `static createAgentBasket() : Basket` 157 | 158 | Creates a new agent basket for the current session customer. 159 | 160 | ### createBasketFromOrder 161 | 162 | **Signature:** `static createBasketFromOrder(order : Order) : Basket` 163 | 164 | Creates a Basket from an existing Order for the purposes of changing an Order. 165 | 166 | ### createTemporaryBasket 167 | 168 | **Signature:** `static createTemporaryBasket() : Basket` 169 | 170 | Creates a new temporary basket for the current session customer. 171 | 172 | ### deleteBasket 173 | 174 | **Signature:** `static deleteBasket(basket : Basket) : void` 175 | 176 | Remove a customer basket including a temporary basket. 177 | 178 | ### deleteTemporaryBasket 179 | 180 | **Signature:** `static deleteTemporaryBasket(basket : Basket) : void` 181 | 182 | Remove a customer temporary basket. 183 | 184 | ### getBasket 185 | 186 | **Signature:** `static getBasket(uuid : String) : Basket` 187 | 188 | This method returns a valid basket of the session customer or null if none is found. 189 | 190 | ### getBaskets 191 | 192 | **Signature:** `static getBaskets() : List` 193 | 194 | Retrieve all open baskets for the logged in customer including the temporary baskets. 195 | 196 | ### getCurrentBasket 197 | 198 | **Signature:** `static getCurrentBasket() : Basket` 199 | 200 | This method returns the current valid basket of the session customer or null if no current valid basket exists. 201 | 202 | ### getCurrentOrNewBasket 203 | 204 | **Signature:** `static getCurrentOrNewBasket() : Basket` 205 | 206 | This method returns the current valid basket of the session customer or creates a new one if no current valid basket exists. 207 | 208 | ### getStoredBasket 209 | 210 | **Signature:** `static getStoredBasket() : Basket` 211 | 212 | This method returns the stored basket of the session customer or null if none is found. 213 | 214 | ### getTemporaryBasket 215 | 216 | **Signature:** `static getTemporaryBasket(uuid : String) : Basket` 217 | 218 | This method returns a valid temporary basket of the session customer or null if none is found. 219 | 220 | ### getTemporaryBaskets 221 | 222 | **Signature:** `static getTemporaryBaskets() : List` 223 | 224 | Retrieve all open temporary baskets for the logged in customer. 225 | 226 | ## Method Detail 227 | 228 | ## Method Details 229 | 230 | ### createAgentBasket 231 | 232 | **Signature:** `static createAgentBasket() : Basket` 233 | 234 | **Description:** Creates a new agent basket for the current session customer. By default only 4 open agent baskets are allowed per customer. If this is exceeded a CreateAgentBasketLimitExceededException will be thrown. This method will result in an exception if called by a user without permission Create_Order_On_Behalf_Of or if no customer is logged in the session. 235 | 236 | **Returns:** 237 | 238 | the newly created basket for the customer which is logged in 239 | 240 | **Throws:** 241 | 242 | CreateAgentBasketLimitExceededException - indicates that no agent basket could be created because the agent basket limit is already exceeded 243 | 244 | --- 245 | 246 | ### createBasketFromOrder 247 | 248 | **Signature:** `static createBasketFromOrder(order : Order) : Basket` 249 | 250 | **Description:** Creates a Basket from an existing Order for the purposes of changing an Order. When an Order is later created from the Basket, the original Order is changed to status Order.ORDER_STATUS_REPLACED. Restricted to agent scenario use cases. In case a storefront customer is using it the created storefront basket cannot be retrieved via getCurrentBasket() (ScriptAPI), GET /baskets/<basketid> (REST APIs) or DELETE /baskets/<basketid> (REST APIs) or GetBasket (Pipelet) or Basket-related CSC Operations from BM (these also use OCAPI REST API). Baskets containing an "orderNumberBeingEdited" are explicitly excluded from the list of baskets that can be retrieved. Responsible for this behavior (this kind of basket cannot be used as general purpose shopping baskets) - see Basket.getOrderNoBeingEdited() / Basket.getOrderBeingEdited(). In case a Business Manager user is logged in into the session the basket will be marked as an agent basket. See Basket.isAgentBasket(). Any inventory reservation associated with the order will be canceled either early when Basket.reserveInventory() is called for the new basket or (later) when a new replacement order is created from the basket. Consider reserving the basket following its creation. The method only succeeds for an Order without gift certificates, status is not cancelled, was not previously replaced and was not previously exported. Failures are indicated by throwing an APIException of type CreateBasketFromOrderException which provides one of these errorCodes: Code OrderProcessStatusCodes.ORDER_CONTAINS_GC - the Order contains a gift certificate and cannot be replaced. Code OrderProcessStatusCodes.ORDER_ALREADY_REPLACED - the Order was already replaced. Code OrderProcessStatusCodes.ORDER_ALREADY_CANCELLED - the Order was cancelled. Code OrderProcessStatusCodes.ORDER_ALREADY_EXPORTED - the Order has already been exported. Usage: var order : Order; // known try { var basket : Basket = BasketMgr.createBasketFromOrder(order); } catch (e) { if (e instanceof APIException && e.type === 'CreateBasketFromOrderException') { // handle e.errorCode } } 251 | 252 | **Parameters:** 253 | 254 | - `order`: Order to create a Basket for 255 | 256 | **Returns:** 257 | 258 | a new Basket 259 | 260 | **See Also:** 261 | 262 | AgentUserMgr.loginAgentUser(String, String) 263 | AgentUserMgr.loginOnBehalfOfCustomer(Customer) 264 | 265 | **Throws:** 266 | 267 | - `CreateBasketFromOrderException`: indicates the Order is in an invalid state. 268 | 269 | --- 270 | 271 | ### createTemporaryBasket 272 | 273 | **Signature:** `static createTemporaryBasket() : Basket` 274 | 275 | **Description:** Creates a new temporary basket for the current session customer. Temporary baskets are separate from shopper storefront and agent baskets, and are intended for use to perform calculations or create an order without disturbing a shopper's open storefront basket. Temporary baskets are automatically deleted after a time duration of 15 minutes. By default only 4 open temporary baskets are allowed per customer. If this is exceeded a CreateTemporaryBasketLimitExceededException will be thrown. 276 | 277 | **Returns:** 278 | 279 | the newly created basket for the current session customer 280 | 281 | --- 282 | 283 | ### deleteBasket 284 | 285 | **Signature:** `static deleteBasket(basket : Basket) : void` 286 | 287 | **Description:** Remove a customer basket including a temporary basket. This method will result in an exception if called by a user without permission Create_Order_On_Behalf_Of or if no customer is logged in the session. 288 | 289 | **Parameters:** 290 | 291 | - `basket`: the basket to be removed 292 | 293 | **See Also:** 294 | 295 | AgentUserMgr.loginAgentUser(String, String) 296 | AgentUserMgr.loginOnBehalfOfCustomer(Customer) 297 | 298 | --- 299 | 300 | ### deleteTemporaryBasket 301 | 302 | **Signature:** `static deleteTemporaryBasket(basket : Basket) : void` 303 | 304 | **Description:** Remove a customer temporary basket. 305 | 306 | **Parameters:** 307 | 308 | - `basket`: the temporary basket to be removed 309 | 310 | --- 311 | 312 | ### getBasket 313 | 314 | **Signature:** `static getBasket(uuid : String) : Basket` 315 | 316 | **Description:** This method returns a valid basket of the session customer or null if none is found. This method can also be used to get a temporary basket for the session customer. If the basket does not belong to the session customer, the method returns null. If the registered customer is not logged in, the method returns null. Restricted to agent scenario use cases: This method will result in an exception if called by a user without permission Create_Order_On_Behalf_Of or if no customer is logged in the session. The basket, if accessible, is usually updated in the same way as getCurrentBasket(). If the session currency no longer matches the basket currency, the basket currency should be updated with Basket.updateCurrency(). 317 | 318 | **Parameters:** 319 | 320 | - `uuid`: the id of the requested basket. 321 | 322 | **Returns:** 323 | 324 | the basket or null 325 | 326 | --- 327 | 328 | ### getBaskets 329 | 330 | **Signature:** `static getBaskets() : List` 331 | 332 | **Description:** Retrieve all open baskets for the logged in customer including the temporary baskets. Restricted to agent scenario use cases: The returned list contains all agent baskets created with createAgentBasket() and the current storefront basket which can also be retrieved with getCurrentBasket(). This method will result in an exception if called by a user without permission Create_Order_On_Behalf_Of or if no customer is logged in the session. Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore. 333 | 334 | **Returns:** 335 | 336 | all open baskets 337 | 338 | --- 339 | 340 | ### getCurrentBasket 341 | 342 | **Signature:** `static getCurrentBasket() : Basket` 343 | 344 | **Description:** This method returns the current valid basket of the session customer or null if no current valid basket exists. The methods getCurrentBasket() and getCurrentOrNewBasket() work based on the selected basket persistence, which can be configured in the Business Manager site preferences / baskets section. A basket is valid for the configured basket lifetime. In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also utilizing PWA Kit or other custom headless solution for another part of the same site), this method must NOT be used. Instead, retrieve baskets via GET baskets/{basketId} or GET customers/{customerId}/baskets. Do not use getCurrentOrNewBasket() for basket creation in any scenario. The current basket, if one exists, is usually updated by the method. In particular the last-modified date is updated. No update is done when method getCurrentBasket() is used within a read-only hook implementation (such as a beforeGet or a modifyResponse hook). The lifetime of a basket can be extended in 2 ways: The basket is modified in some way, e.g. a product is added resulting in the basket total being newly calculated. This results in the basket lifetime being reset. The basket has not been modified for 60 minutes, then using this method to access the basket will also reset the basket lifetime. What happens when a customer logs in? Personal data held inside the basket such as addresses, email addresses and payment settings is associated with the customer to whom the basket belongs. If the basket being updated belongs to a different customer this data is removed. This happens when a guest customer that has a basket logs in and hence identifies as a registered customer. In this case the basket which was previously created by the guest customer gets transferred to the (now logged in) registered customer. Should the registered customer already have a basket, this basket is effectively invalidated, but made available using getStoredBasket() allowing the script to merge content from it if desired. What happens when a customer logs out or when the customer session times out? After the customer logs out, a basket belonging to the registered customer (now logged out) is stored (where applicable) and this method returns null. Personal data is also cleared when the session times out for a guest customer. The following personal data is cleared: product line items that were added from a wish list shipping method coupon line items gift certificate line items billing and shipping addresses payment instruments buyer email If the session currency no longer matches the basket currency, the basket currency should be updated with Basket.updateCurrency(). Typical usage: var basket : Basket = BasketMgr.getCurrentBasket(); if (basket) { // do something with basket } Constraints: The method only accesses the basket for the session customer, an exception is thrown when the session customer is null. Method getCurrentOrNewBasket() only creates a basket when method getCurrentBasket() returns null. 345 | 346 | **Returns:** 347 | 348 | the current basket or null if no valid current basket exists. 349 | 350 | --- 351 | 352 | ### getCurrentOrNewBasket 353 | 354 | **Signature:** `static getCurrentOrNewBasket() : Basket` 355 | 356 | **Description:** This method returns the current valid basket of the session customer or creates a new one if no current valid basket exists. See getCurrentBasket() for more details. In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also utilizing PWA Kit or other custom headless solution for another part of the same site), this method must NOT be used. For these scenarios, create baskets via POST baskets REST calls. 357 | 358 | **Returns:** 359 | 360 | the basket, existing or newly created 361 | 362 | --- 363 | 364 | ### getStoredBasket 365 | 366 | **Signature:** `static getStoredBasket() : Basket` 367 | 368 | **Description:** This method returns the stored basket of the session customer or null if none is found. A stored basket is returned in the following situation: During one visit, a customer-Q logs in and creates a basket-A by adding products to it. In a later visit, a second basket-B is created for a guest customer who then logs in as customer-Q. In this case basket-B is reassigned to customer-Q and basket-A is accessible as the stored basket using this method. Now it is possible to merge the information from the stored basket to the active basket. A stored basket will exist only if the corresponding setting is selected in the Business Manager site preferences' baskets section. A basket is valid for the configured basket lifetime. Typical usage: var currentBasket : Basket = BasketMgr.getCurrentOrNewBasket(); var storedBasket : Basket = BasketMgr.getStoredBasket(); if (storedBasket) { // transfer all the data needed from the stored to the active basket } 369 | 370 | **Returns:** 371 | 372 | the stored basket or null if no valid stored basket exists. 373 | 374 | --- 375 | 376 | ### getTemporaryBasket 377 | 378 | **Signature:** `static getTemporaryBasket(uuid : String) : Basket` 379 | 380 | **Description:** This method returns a valid temporary basket of the session customer or null if none is found. If the basket does not belong to the session customer, the method returns null. If the basket is not a temporary basket, the method returns null. The basket, if accessible, is usually updated in the same way as getCurrentBasket(). If the session currency no longer matches the basket currency, the basket currency should be updated with Basket.updateCurrency(). 381 | 382 | **Parameters:** 383 | 384 | - `uuid`: the id of the requested temporary basket. 385 | 386 | **Returns:** 387 | 388 | the temporary basket or null 389 | 390 | --- 391 | 392 | ### getTemporaryBaskets 393 | 394 | **Signature:** `static getTemporaryBaskets() : List` 395 | 396 | **Description:** Retrieve all open temporary baskets for the logged in customer. Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore. 397 | 398 | **Returns:** 399 | 400 | all open temporary baskets 401 | 402 | --- ``` -------------------------------------------------------------------------------- /tests/mcp/node/search-system-object-attribute-groups.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Node.js programmatic tests for search_system_object_attribute_groups MCP tool (Full Mode) 3 | * 4 | * These tests provide comprehensive validation of the tool's functionality in full mode 5 | * with real SFCC OCAPI integration, including complex query scenarios, error handling, 6 | * performance validation, and integration testing. 7 | * 8 | * Test Categories: 9 | * 1. Basic Functionality Tests 10 | * 2. Complex Query Scenarios 11 | * 3. Pagination and Sorting Tests 12 | * 4. Error Handling and Edge Cases 13 | * 5. Performance and Resource Management 14 | * 6. Integration and Data Consistency Tests 15 | * 7. Authentication and Security Tests 16 | */ 17 | 18 | import { strictEqual, ok } from 'assert'; 19 | import { describe, it, before, after, beforeEach } from 'node:test'; 20 | import { connect } from 'mcp-aegis'; 21 | 22 | describe('search_system_object_attribute_groups - Full Mode Comprehensive Tests', () => { 23 | let client; 24 | const configPath = './aegis.config.with-dw.json'; 25 | 26 | before(async () => { 27 | client = await connect(configPath); 28 | }); 29 | 30 | after(async () => { 31 | if (client?.connected) { 32 | await client.disconnect(); 33 | } 34 | }); 35 | 36 | beforeEach(() => { 37 | // CRITICAL: Clear all buffers to prevent leaking into next tests 38 | if (client?.clearAllBuffers) { 39 | client.clearAllBuffers(); 40 | } 41 | }); 42 | 43 | // Enhanced helper functions for complex validations 44 | function assertValidMCPResponse(result) { 45 | ok(result.content, 'Should have content'); 46 | ok(Array.isArray(result.content), 'Content should be array'); 47 | strictEqual(typeof result.isError, 'boolean', 'isError should be boolean'); 48 | } 49 | 50 | function getTextContent(result) { 51 | assertValidMCPResponse(result); 52 | const textContent = result.content.find(c => c.type === 'text'); 53 | ok(textContent, 'Should have text content'); 54 | return textContent.text; 55 | } 56 | 57 | async function callTool(params) { 58 | const result = await client.callTool('search_system_object_attribute_groups', params); 59 | assertValidMCPResponse(result); 60 | return result; 61 | } 62 | 63 | describe('1. Basic Functionality Tests', () => { 64 | it('should be available in full mode', async () => { 65 | const tools = await client.listTools(); 66 | const tool = tools.find(t => t.name === 'search_system_object_attribute_groups'); 67 | ok(tool, 'Tool should be available in full mode'); 68 | strictEqual(tool.name, 'search_system_object_attribute_groups'); 69 | ok(tool.description.includes('Search attribute groups'), 'Tool should have proper description'); 70 | }); 71 | 72 | it('should return attribute groups for Product object type', async () => { 73 | const result = await callTool({ 74 | objectType: 'Product', 75 | searchRequest: { 76 | query: { match_all_query: {} }, 77 | count: 10 78 | } 79 | }); 80 | 81 | const text = getTextContent(result); 82 | ok(text.includes('object_attribute_group'), 'Should mention object_attribute_group in response'); 83 | }); 84 | 85 | it('should return attribute groups for SitePreferences object type', async () => { 86 | const result = await callTool({ 87 | objectType: 'SitePreferences', 88 | searchRequest: { 89 | query: { match_all_query: {} }, 90 | count: 5 91 | } 92 | }); 93 | 94 | const text = getTextContent(result); 95 | ok(text.includes('SitePreferences'), 'Should mention SitePreferences'); 96 | }); 97 | 98 | it('should handle Customer object type', async () => { 99 | const result = await callTool({ 100 | objectType: 'Customer', 101 | searchRequest: { 102 | query: { match_all_query: {} }, 103 | count: 5 104 | } 105 | }); 106 | 107 | const text = getTextContent(result); 108 | ok(text.length > 0, 'Should have some response content'); 109 | }); 110 | }); 111 | 112 | describe('2. Complex Query Scenarios', () => { 113 | it('should handle text_query for searching by display name', async () => { 114 | const result = await callTool({ 115 | objectType: 'Product', 116 | searchRequest: { 117 | query: { 118 | text_query: { 119 | fields: ['display_name', 'description'], 120 | search_phrase: 'product' 121 | } 122 | }, 123 | count: 5 124 | } 125 | }); 126 | 127 | const text = getTextContent(result); 128 | ok(text.length > 0, 'Should have response content'); 129 | }); 130 | 131 | it('should handle term_query for exact field matching', async () => { 132 | const result = await callTool({ 133 | objectType: 'Product', 134 | searchRequest: { 135 | query: { 136 | term_query: { 137 | fields: ['internal'], 138 | operator: 'is', 139 | values: ['false'] 140 | } 141 | }, 142 | count: 10 143 | } 144 | }); 145 | 146 | const text = getTextContent(result); 147 | ok(text.length > 0, 'Should have response content'); 148 | }); 149 | 150 | it('should handle complex bool_query with multiple conditions', async () => { 151 | const result = await callTool({ 152 | objectType: 'Product', 153 | searchRequest: { 154 | query: { 155 | bool_query: { 156 | must: [ 157 | { 158 | term_query: { 159 | fields: ['internal'], 160 | operator: 'is', 161 | values: ['false'] 162 | } 163 | } 164 | ], 165 | must_not: [ 166 | { 167 | text_query: { 168 | fields: ['id'], 169 | search_phrase: 'system' 170 | } 171 | } 172 | ] 173 | } 174 | }, 175 | count: 5 176 | } 177 | }); 178 | 179 | const text = getTextContent(result); 180 | ok(text.length > 0, 'Should have response content'); 181 | }); 182 | }); 183 | 184 | describe('3. Pagination and Sorting Tests', () => { 185 | it('should handle pagination with start and count parameters', async () => { 186 | const result1 = await callTool({ 187 | objectType: 'Product', 188 | searchRequest: { 189 | query: { match_all_query: {} }, 190 | start: 0, 191 | count: 2 192 | } 193 | }); 194 | 195 | const result2 = await callTool({ 196 | objectType: 'Product', 197 | searchRequest: { 198 | query: { match_all_query: {} }, 199 | start: 2, 200 | count: 2 201 | } 202 | }); 203 | 204 | const text1 = getTextContent(result1); 205 | const text2 = getTextContent(result2); 206 | 207 | // Pages should be different (unless there are very few groups) 208 | if (text1.includes('attribute groups') && text2.includes('attribute groups')) { 209 | // Both pages have data, they might be different or the same if limited data 210 | ok(true, 'Pagination works correctly'); 211 | } 212 | }); 213 | 214 | it('should handle sorting by different fields', async () => { 215 | const ascResult = await callTool({ 216 | objectType: 'Product', 217 | searchRequest: { 218 | query: { match_all_query: {} }, 219 | sorts: [{ field: 'id', sort_order: 'asc' }], 220 | count: 5 221 | } 222 | }); 223 | 224 | const descResult = await callTool({ 225 | objectType: 'Product', 226 | searchRequest: { 227 | query: { match_all_query: {} }, 228 | sorts: [{ field: 'id', sort_order: 'desc' }], 229 | count: 5 230 | } 231 | }); 232 | 233 | const ascText = getTextContent(ascResult); 234 | const descText = getTextContent(descResult); 235 | 236 | // Both should have content 237 | ok(ascText.length > 0, 'Ascending sort should return data'); 238 | ok(descText.length > 0, 'Descending sort should return data'); 239 | }); 240 | 241 | it('should handle multiple sort criteria', async () => { 242 | const result = await callTool({ 243 | objectType: 'Product', 244 | searchRequest: { 245 | query: { match_all_query: {} }, 246 | sorts: [ 247 | { field: 'internal', sort_order: 'asc' }, 248 | { field: 'position', sort_order: 'desc' } 249 | ], 250 | count: 5 251 | } 252 | }); 253 | 254 | const text = getTextContent(result); 255 | ok(text.length > 0, 'Should return data'); 256 | }); 257 | }); 258 | 259 | describe('4. Error Handling and Edge Cases', () => { 260 | it('should handle invalid object type gracefully', async () => { 261 | const result = await callTool({ 262 | objectType: 'InvalidObjectType', 263 | searchRequest: { 264 | query: { match_all_query: {} } 265 | } 266 | }); 267 | 268 | const text = getTextContent(result).toLowerCase(); 269 | ok( 270 | text.includes('error') || text.includes('not found') || text.includes('no') || 271 | text.includes('invalid') || text.includes('empty') || text.includes('0'), 272 | 'Should handle invalid object type appropriately' 273 | ); 274 | }); 275 | 276 | it('should handle missing required parameters', async () => { 277 | try { 278 | await client.callTool('search_system_object_attribute_groups', { 279 | searchRequest: { 280 | query: { match_all_query: {} } 281 | } 282 | // Missing objectType 283 | }); 284 | ok(false, 'Should have thrown an error for missing objectType'); 285 | } catch (error) { 286 | ok(error.message.includes('objectType') || error.message.includes('required'), 287 | 'Error should mention missing objectType'); 288 | } 289 | }); 290 | 291 | it('should handle malformed query structures', async () => { 292 | const result = await callTool({ 293 | objectType: 'Product', 294 | searchRequest: { 295 | query: { 296 | text_query: { 297 | // Missing required fields 298 | search_phrase: 'test' 299 | } 300 | } 301 | } 302 | }); 303 | 304 | const text = getTextContent(result); 305 | ok(text.length > 0, 'Should have some response'); 306 | }); 307 | 308 | it('should handle empty search results', async () => { 309 | const result = await callTool({ 310 | objectType: 'Product', 311 | searchRequest: { 312 | query: { 313 | text_query: { 314 | fields: ['id'], 315 | search_phrase: 'zzz_nonexistent_group_name_xyz' 316 | } 317 | } 318 | } 319 | }); 320 | 321 | const text = getTextContent(result).toLowerCase(); 322 | ok( 323 | text.includes('no') || text.includes('empty') || text.includes('not found') || 324 | text.includes('0') || text.includes('none'), 325 | 'Should indicate no results found' 326 | ); 327 | }); 328 | 329 | it('should handle very large count parameters', async () => { 330 | const result = await callTool({ 331 | objectType: 'Product', 332 | searchRequest: { 333 | query: { match_all_query: {} }, 334 | count: 1000 // Very large count 335 | } 336 | }); 337 | 338 | const text = getTextContent(result); 339 | ok(text.length > 0, 'Should return data'); 340 | }); 341 | }); 342 | 343 | describe('5. Performance and Resource Management Tests', () => { 344 | it('should respond within reasonable time for simple queries', async () => { 345 | const startTime = Date.now(); 346 | 347 | const result = await callTool({ 348 | objectType: 'Product', 349 | searchRequest: { 350 | query: { match_all_query: {} }, 351 | count: 5 352 | } 353 | }); 354 | 355 | const duration = Date.now() - startTime; 356 | 357 | ok(result.content, 'Should return result'); 358 | ok(duration < 5000, `Simple query should complete within 5 seconds, took ${duration}ms`); 359 | }); 360 | 361 | it('should handle concurrent requests efficiently', async () => { 362 | const startTime = Date.now(); 363 | 364 | const promises = Array.from({ length: 3 }, (_, i) => 365 | callTool({ 366 | objectType: 'Product', 367 | searchRequest: { 368 | query: { match_all_query: {} }, 369 | start: i * 2, 370 | count: 2 371 | } 372 | }) 373 | ); 374 | 375 | const results = await Promise.all(promises); 376 | const duration = Date.now() - startTime; 377 | 378 | ok(results.length === 3, 'Should handle all concurrent requests'); 379 | results.forEach((result, index) => { 380 | ok(result.content, `Request ${index} should have content`); 381 | }); 382 | ok(duration < 10000, `Concurrent requests should complete within 10 seconds, took ${duration}ms`); 383 | }); 384 | 385 | it('should handle memory efficiently with large result sets', async () => { 386 | const result = await callTool({ 387 | objectType: 'Product', 388 | searchRequest: { 389 | query: { match_all_query: {} }, 390 | count: 200 // Large result set 391 | } 392 | }); 393 | 394 | const text = getTextContent(result); 395 | ok(text.length > 0, 'Should have response content'); 396 | ok(text.length < 1000000, 'Response should be reasonable in size (< 1MB)'); 397 | }); 398 | }); 399 | 400 | describe('6. Integration and Data Consistency Tests', () => { 401 | it('should return consistent results for repeated identical queries', async () => { 402 | const queryParams = { 403 | objectType: 'Product', 404 | searchRequest: { 405 | query: { match_all_query: {} }, 406 | count: 5, 407 | sorts: [{ field: 'id', sort_order: 'asc' }] 408 | } 409 | }; 410 | 411 | const result1 = await callTool(queryParams); 412 | const result2 = await callTool(queryParams); 413 | 414 | const text1 = getTextContent(result1); 415 | const text2 = getTextContent(result2); 416 | 417 | // Results should be identical for same query 418 | strictEqual(text1, text2, 'Repeated identical queries should return same results'); 419 | }); 420 | 421 | it('should validate select parameter functionality', async () => { 422 | const fullResult = await callTool({ 423 | objectType: 'Product', 424 | searchRequest: { 425 | query: { match_all_query: {} }, 426 | count: 3, 427 | select: '(**)' 428 | } 429 | }); 430 | 431 | const limitedResult = await callTool({ 432 | objectType: 'Product', 433 | searchRequest: { 434 | query: { match_all_query: {} }, 435 | count: 3, 436 | select: '(data.(id))' 437 | } 438 | }); 439 | 440 | const fullText = getTextContent(fullResult); 441 | const limitedText = getTextContent(limitedResult); 442 | 443 | // Both should have content but limited might be shorter 444 | ok(fullText.length > 0, 'Full select should return data'); 445 | ok(limitedText.length > 0, 'Limited select should return data'); 446 | }); 447 | 448 | it('should handle cross-object type comparisons', async () => { 449 | const productResult = await callTool({ 450 | objectType: 'Product', 451 | searchRequest: { 452 | query: { match_all_query: {} }, 453 | count: 3 454 | } 455 | }); 456 | 457 | const sitePrefsResult = await callTool({ 458 | objectType: 'SitePreferences', 459 | searchRequest: { 460 | query: { match_all_query: {} }, 461 | count: 3 462 | } 463 | }); 464 | 465 | const productText = getTextContent(productResult); 466 | const sitePrefsText = getTextContent(sitePrefsResult); 467 | 468 | // Results should be different (different object types should have different groups) 469 | ok(productText.includes('Product') || productText.includes('attribute'), 470 | 'Product result should be relevant to products'); 471 | ok(sitePrefsText.includes('SitePreferences') || sitePrefsText.includes('attribute'), 472 | 'SitePreferences result should be relevant to site preferences'); 473 | }); 474 | }); 475 | 476 | describe('7. Authentication and Security Tests', () => { 477 | it('should require valid OCAPI credentials', async () => { 478 | // This test verifies that the tool properly uses authentication 479 | // The fact that we can call the tool successfully means auth is working 480 | const result = await callTool({ 481 | objectType: 'Product', 482 | searchRequest: { 483 | query: { match_all_query: {} }, 484 | count: 1 485 | } 486 | }); 487 | 488 | const text = getTextContent(result).toLowerCase(); 489 | 490 | // Should not indicate authentication errors 491 | ok(!text.includes('unauthorized') && !text.includes('authentication'), 492 | 'Should not have authentication errors with valid credentials'); 493 | }); 494 | 495 | it('should handle input sanitization properly', async () => { 496 | // Test with potentially problematic input characters 497 | const result = await callTool({ 498 | objectType: 'Product', 499 | searchRequest: { 500 | query: { 501 | text_query: { 502 | fields: ['id'], 503 | search_phrase: '<script>alert("test")</script>' 504 | } 505 | }, 506 | count: 1 507 | } 508 | }); 509 | 510 | const text = getTextContent(result); 511 | 512 | // The query echo should be properly escaped (showing backslashes for quotes) 513 | // and should not contain executable script content in the actual data hits 514 | ok(text.includes('\\"test\\"'), 'Should properly escape quotes in query echo'); 515 | 516 | // Check that the hits section doesn't contain the script content 517 | const jsonResponse = JSON.parse(text); 518 | if (jsonResponse.hits && jsonResponse.hits.length > 0) { 519 | const hitsText = JSON.stringify(jsonResponse.hits); 520 | ok(!hitsText.includes('<script>'), 'Actual data hits should not contain script content'); 521 | } 522 | }); 523 | 524 | it('should respect OCAPI rate limiting and security constraints', async () => { 525 | // Make multiple rapid requests to test rate limiting handling 526 | const promises = Array.from({ length: 5 }, () => 527 | callTool({ 528 | objectType: 'Product', 529 | searchRequest: { 530 | query: { match_all_query: {} }, 531 | count: 1 532 | } 533 | }) 534 | ); 535 | 536 | try { 537 | const results = await Promise.all(promises); 538 | 539 | // All requests should succeed or fail gracefully 540 | results.forEach((result, index) => { 541 | ok(result.content, `Request ${index} should have content or proper error handling`); 542 | }); 543 | } catch (error) { 544 | // If rate limited, should fail gracefully 545 | ok(error.message.includes('rate') || error.message.includes('limit') || 546 | error.message.includes('too many'), 547 | 'Rate limiting should be handled gracefully'); 548 | } 549 | }); 550 | }); 551 | 552 | describe('8. Edge Case and Robustness Tests', () => { 553 | it('should handle extremely specific search criteria', async () => { 554 | const result = await callTool({ 555 | objectType: 'Product', 556 | searchRequest: { 557 | query: { 558 | bool_query: { 559 | must: [ 560 | { 561 | term_query: { 562 | fields: ['internal'], 563 | operator: 'is', 564 | values: ['false'] 565 | } 566 | }, 567 | { 568 | text_query: { 569 | fields: ['display_name'], 570 | search_phrase: 'custom' 571 | } 572 | } 573 | ] 574 | } 575 | }, 576 | count: 1 577 | } 578 | }); 579 | 580 | const text = getTextContent(result); 581 | ok(text.length > 0, 'Should have response content'); 582 | }); 583 | 584 | it('should handle boundary values for pagination', async () => { 585 | // Test with start = 0 586 | const zeroStart = await callTool({ 587 | objectType: 'Product', 588 | searchRequest: { 589 | query: { match_all_query: {} }, 590 | start: 0, 591 | count: 1 592 | } 593 | }); 594 | 595 | // Test with count = 1 596 | const minCount = await callTool({ 597 | objectType: 'Product', 598 | searchRequest: { 599 | query: { match_all_query: {} }, 600 | start: 0, 601 | count: 1 602 | } 603 | }); 604 | 605 | ok(zeroStart.content, 'Should handle start=0'); 606 | ok(minCount.content, 'Should handle count=1'); 607 | }); 608 | 609 | it('should maintain consistent response format across different scenarios', async () => { 610 | const scenarios = [ 611 | { 612 | name: 'match_all', 613 | params: { 614 | objectType: 'Product', 615 | searchRequest: { query: { match_all_query: {} }, count: 2 } 616 | } 617 | }, 618 | { 619 | name: 'text_search', 620 | params: { 621 | objectType: 'Product', 622 | searchRequest: { 623 | query: { 624 | text_query: { 625 | fields: ['id'], 626 | search_phrase: 'product' 627 | } 628 | }, 629 | count: 2 630 | } 631 | } 632 | }, 633 | { 634 | name: 'with_sorting', 635 | params: { 636 | objectType: 'Product', 637 | searchRequest: { 638 | query: { match_all_query: {} }, 639 | sorts: [{ field: 'id', sort_order: 'asc' }], 640 | count: 2 641 | } 642 | } 643 | } 644 | ]; 645 | 646 | for (const scenario of scenarios) { 647 | const result = await callTool(scenario.params); 648 | 649 | assertValidMCPResponse(result); 650 | 651 | const textContent = result.content.find(c => c.type === 'text'); 652 | ok(textContent, `${scenario.name} should have text content`); 653 | ok(typeof textContent.text === 'string', `${scenario.name} should have string text`); 654 | } 655 | }); 656 | }); 657 | }); ``` -------------------------------------------------------------------------------- /tests/referenced-types-extractor.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ReferencedTypesExtractor } from '../src/clients/docs/referenced-types-extractor.js'; 2 | 3 | describe('ReferencedTypesExtractor', () => { 4 | describe('extractReferencedTypes', () => { 5 | it('should extract types from property definitions', () => { 6 | const content = ` 7 | # Class Product 8 | 9 | ## Properties 10 | 11 | ### price 12 | **Type:** Money 13 | 14 | The price of the product. 15 | 16 | ### category 17 | **Type:** Category 18 | 19 | The product category. 20 | `; 21 | 22 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 23 | 24 | expect(result).toContain('Money'); 25 | expect(result).toContain('Category'); 26 | expect(result).toHaveLength(2); 27 | }); 28 | 29 | it('should extract return types from method signatures', () => { 30 | const content = ` 31 | # Class Product 32 | 33 | ## Methods 34 | 35 | ### getPrice(): Money 36 | Returns the product price. 37 | 38 | ### getCategory(): Category 39 | Returns the product category. 40 | 41 | ### getName(): String 42 | Returns the product name. 43 | `; 44 | 45 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 46 | 47 | expect(result).toContain('Money'); 48 | expect(result).toContain('Category'); 49 | expect(result).toContain('String'); 50 | expect(result).toHaveLength(3); 51 | }); 52 | 53 | it('should extract parameter types from method signatures', () => { 54 | const content = ` 55 | # Class Product 56 | 57 | ## Methods 58 | 59 | ### setPrice(price: Money): void 60 | Sets the product price. 61 | 62 | ### addToCategory(category: Category, primary: Boolean): void 63 | Adds product to category. 64 | 65 | ### updateInventory(record: InventoryRecord, quantity: Number): Boolean 66 | Updates inventory record. 67 | `; 68 | 69 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 70 | 71 | expect(result).toContain('Money'); 72 | expect(result).toContain('Category'); 73 | expect(result).toContain('Boolean'); 74 | expect(result).toContain('InventoryRecord'); 75 | expect(result).toContain('Number'); 76 | expect(result).toHaveLength(5); 77 | }); 78 | 79 | it('should extract types with dots (fully qualified names)', () => { 80 | const content = ` 81 | # Class Product 82 | 83 | ## Properties 84 | 85 | ### site 86 | **Type:** dw.system.Site 87 | 88 | The site this product belongs to. 89 | 90 | ## Methods 91 | 92 | ### getCustomer(): dw.customer.Customer 93 | Returns the customer. 94 | 95 | ### processOrder(order: dw.order.Order): dw.system.Status 96 | Processes an order. 97 | `; 98 | 99 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 100 | 101 | expect(result).toContain('dw.system.Site'); 102 | expect(result).toContain('dw.customer.Customer'); 103 | expect(result).toContain('dw.order.Order'); 104 | expect(result).toContain('dw.system.Status'); 105 | expect(result).toHaveLength(4); 106 | }); 107 | 108 | it('should handle complex method signatures with multiple parameters', () => { 109 | const content = ` 110 | # Class OrderProcessor 111 | 112 | ## Methods 113 | 114 | ### processPayment(order: Order, payment: PaymentInstrument, amount: Money): PaymentStatus 115 | Processes payment for an order. 116 | 117 | ### createShipment(order: Order, items: Collection, address: OrderAddress): Shipment 118 | Creates a shipment. 119 | `; 120 | 121 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 122 | 123 | expect(result).toContain('Order'); 124 | expect(result).toContain('PaymentInstrument'); 125 | expect(result).toContain('Money'); 126 | expect(result).toContain('PaymentStatus'); 127 | expect(result).toContain('Collection'); 128 | expect(result).toContain('OrderAddress'); 129 | expect(result).toContain('Shipment'); 130 | expect(result).toHaveLength(7); 131 | }); 132 | 133 | it('should ignore primitive types and lowercase types', () => { 134 | const content = ` 135 | # Class Product 136 | 137 | ## Properties 138 | 139 | ### name 140 | **Type:** string 141 | 142 | Product name. 143 | 144 | ### active 145 | **Type:** boolean 146 | 147 | Whether product is active. 148 | 149 | ### count 150 | **Type:** number 151 | 152 | Product count. 153 | 154 | ### data 155 | **Type:** object 156 | 157 | Product data. 158 | 159 | ## Methods 160 | 161 | ### isValid(): boolean 162 | Returns validity. 163 | 164 | ### process(data: object, flag: boolean): string 165 | Processes data. 166 | `; 167 | 168 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 169 | 170 | // Should not include primitive types (string, boolean, number, object) 171 | expect(result).not.toContain('string'); 172 | expect(result).not.toContain('boolean'); 173 | expect(result).not.toContain('number'); 174 | expect(result).not.toContain('object'); 175 | expect(result).toHaveLength(0); 176 | }); 177 | 178 | it('should handle mixed case and special formatting', () => { 179 | const content = ` 180 | # Class ProductManager 181 | 182 | ## Properties 183 | 184 | ### defaultCategory 185 | **Type:** Category 186 | 187 | Default category with extra spaces. 188 | 189 | ### primarySite 190 | **Type:**dw.system.Site 191 | 192 | Site without space after colon. 193 | 194 | ## Methods 195 | 196 | ### getCollection(): Collection 197 | Returns collection with extra spaces. 198 | 199 | ### updateStatus( status: ProductStatus ): Boolean 200 | Method with spaced parameters. 201 | `; 202 | 203 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 204 | 205 | expect(result).toContain('Category'); 206 | expect(result).toContain('dw.system.Site'); 207 | expect(result).toContain('Collection'); 208 | expect(result).toContain('ProductStatus'); 209 | expect(result).toContain('Boolean'); 210 | expect(result).toHaveLength(5); 211 | }); 212 | 213 | it('should handle empty content', () => { 214 | const result = ReferencedTypesExtractor.extractReferencedTypes(''); 215 | expect(result).toEqual([]); 216 | }); 217 | 218 | it('should handle content with no type references', () => { 219 | const content = ` 220 | # Class Product 221 | 222 | This is a product class without any type references. 223 | 224 | It has some text but no property types or method signatures. 225 | `; 226 | 227 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 228 | expect(result).toEqual([]); 229 | }); 230 | 231 | it('should handle malformed content gracefully', () => { 232 | const content = ` 233 | # Class Product 234 | 235 | **Type:** 236 | 237 | ### method(): 238 | Returns something. 239 | 240 | ### invalid(: Missing): 241 | Malformed signature. 242 | `; 243 | 244 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 245 | // The regex will match "Missing" from the parameter, which starts with uppercase 246 | expect(result).toContain('Missing'); 247 | expect(result).toHaveLength(1); 248 | }); 249 | 250 | it('should deduplicate repeated type references', () => { 251 | const content = ` 252 | # Class Product 253 | 254 | ## Properties 255 | 256 | ### price 257 | **Type:** Money 258 | 259 | ### discountPrice 260 | **Type:** Money 261 | 262 | ## Methods 263 | 264 | ### getPrice(): Money 265 | Returns price. 266 | 267 | ### setPrice(amount: Money): void 268 | Sets price. 269 | `; 270 | 271 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 272 | 273 | expect(result).toContain('Money'); 274 | expect(result.filter(type => type === 'Money')).toHaveLength(1); 275 | expect(result).toHaveLength(1); 276 | }); 277 | }); 278 | 279 | describe('isSFCCType', () => { 280 | it('should identify uppercase types as SFCC types', () => { 281 | // Access private method through any for testing 282 | const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; 283 | 284 | expect(isSFCCType('Product')).toBe(true); 285 | expect(isSFCCType('Money')).toBe(true); 286 | expect(isSFCCType('Category')).toBe(true); 287 | expect(isSFCCType('Boolean')).toBe(true); 288 | expect(isSFCCType('String')).toBe(true); 289 | expect(isSFCCType('Number')).toBe(true); 290 | }); 291 | 292 | it('should identify dotted types as SFCC types', () => { 293 | const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; 294 | 295 | expect(isSFCCType('dw.catalog.Product')).toBe(true); 296 | expect(isSFCCType('dw.system.Site')).toBe(true); 297 | expect(isSFCCType('dw.customer.Customer')).toBe(true); 298 | expect(isSFCCType('dw.order.Order')).toBe(true); 299 | }); 300 | 301 | it('should reject lowercase primitive types', () => { 302 | const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; 303 | 304 | expect(isSFCCType('string')).toBe(false); 305 | expect(isSFCCType('boolean')).toBe(false); 306 | expect(isSFCCType('number')).toBe(false); 307 | expect(isSFCCType('object')).toBe(false); 308 | expect(isSFCCType('function')).toBe(false); 309 | expect(isSFCCType('undefined')).toBe(false); 310 | expect(isSFCCType('null')).toBe(false); 311 | }); 312 | 313 | it('should reject types starting with lowercase', () => { 314 | const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; 315 | 316 | expect(isSFCCType('productType')).toBe(false); 317 | expect(isSFCCType('categoryId')).toBe(false); 318 | expect(isSFCCType('data')).toBe(false); 319 | expect(isSFCCType('value')).toBe(false); 320 | }); 321 | 322 | it('should handle edge cases', () => { 323 | const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; 324 | 325 | expect(isSFCCType('')).toBe(false); 326 | expect(isSFCCType('A')).toBe(true); 327 | expect(isSFCCType('a')).toBe(false); 328 | expect(isSFCCType('1Product')).toBe(false); // starts with number 329 | expect(isSFCCType('_Product')).toBe(false); // starts with underscore 330 | }); 331 | }); 332 | 333 | describe('filterCircularReferences', () => { 334 | it('should filter out exact class name matches', () => { 335 | const referencedTypes = ['Product', 'Category', 'Money', 'Product']; 336 | const currentClassName = 'Product'; 337 | 338 | const result = ReferencedTypesExtractor.filterCircularReferences( 339 | referencedTypes, 340 | currentClassName, 341 | ); 342 | 343 | expect(result).toContain('Category'); 344 | expect(result).toContain('Money'); 345 | expect(result).not.toContain('Product'); 346 | expect(result).toHaveLength(2); 347 | }); 348 | 349 | it('should filter out fully qualified class name matches', () => { 350 | const referencedTypes = [ 351 | 'dw.catalog.Product', 352 | 'dw.system.Site', 353 | 'Category', 354 | 'Money', 355 | ]; 356 | const currentClassName = 'Product'; 357 | 358 | const result = ReferencedTypesExtractor.filterCircularReferences( 359 | referencedTypes, 360 | currentClassName, 361 | ); 362 | 363 | expect(result).toContain('dw.system.Site'); 364 | expect(result).toContain('Category'); 365 | expect(result).toContain('Money'); 366 | expect(result).not.toContain('dw.catalog.Product'); 367 | expect(result).toHaveLength(3); 368 | }); 369 | 370 | it('should preserve types that do not create circular references', () => { 371 | const referencedTypes = [ 372 | 'Category', 373 | 'Money', 374 | 'ProductVariant', // Different from Product 375 | 'dw.system.Site', 376 | 'Boolean', 377 | ]; 378 | const currentClassName = 'Product'; 379 | 380 | const result = ReferencedTypesExtractor.filterCircularReferences( 381 | referencedTypes, 382 | currentClassName, 383 | ); 384 | 385 | expect(result).toEqual([ 386 | 'Category', 387 | 'Money', 388 | 'ProductVariant', 389 | 'dw.system.Site', 390 | 'Boolean', 391 | ]); 392 | expect(result).toHaveLength(5); 393 | }); 394 | 395 | it('should handle empty input arrays', () => { 396 | const result = ReferencedTypesExtractor.filterCircularReferences([], 'Product'); 397 | expect(result).toEqual([]); 398 | }); 399 | 400 | it('should handle case where all types are circular references', () => { 401 | const referencedTypes = [ 402 | 'Product', 403 | 'dw.catalog.Product', 404 | 'some.namespace.Product', 405 | ]; 406 | const currentClassName = 'Product'; 407 | 408 | const result = ReferencedTypesExtractor.filterCircularReferences( 409 | referencedTypes, 410 | currentClassName, 411 | ); 412 | 413 | expect(result).toEqual([]); 414 | }); 415 | 416 | it('should be case sensitive', () => { 417 | const referencedTypes = ['product', 'PRODUCT', 'Product']; 418 | const currentClassName = 'Product'; 419 | 420 | const result = ReferencedTypesExtractor.filterCircularReferences( 421 | referencedTypes, 422 | currentClassName, 423 | ); 424 | 425 | expect(result).toContain('product'); 426 | expect(result).toContain('PRODUCT'); 427 | expect(result).not.toContain('Product'); 428 | expect(result).toHaveLength(2); 429 | }); 430 | 431 | it('should handle complex namespace scenarios', () => { 432 | const referencedTypes = [ 433 | 'dw.catalog.Category', 434 | 'dw.order.OrderItem', // Different class 435 | 'com.custom.Product', // Different namespace but same class 436 | 'Product', // Exact match 437 | ]; 438 | const currentClassName = 'Product'; 439 | 440 | const result = ReferencedTypesExtractor.filterCircularReferences( 441 | referencedTypes, 442 | currentClassName, 443 | ); 444 | 445 | expect(result).toContain('dw.catalog.Category'); 446 | expect(result).toContain('dw.order.OrderItem'); 447 | expect(result).not.toContain('com.custom.Product'); // Should be filtered 448 | expect(result).not.toContain('Product'); // Should be filtered 449 | expect(result).toHaveLength(2); 450 | }); 451 | }); 452 | 453 | describe('extractFilteredReferencedTypes', () => { 454 | it('should extract and filter types in one operation', () => { 455 | const content = ` 456 | # Class Product 457 | 458 | ## Properties 459 | 460 | ### price 461 | **Type:** Money 462 | 463 | ### category 464 | **Type:** Category 465 | 466 | ### relatedProduct 467 | **Type:** Product 468 | 469 | ## Methods 470 | 471 | ### getPrice(): Money 472 | Returns price. 473 | 474 | ### getRelatedProduct(): Product 475 | Returns related product. 476 | 477 | ### createProduct(name: String): Product 478 | Creates a new product. 479 | `; 480 | 481 | const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( 482 | content, 483 | 'Product', 484 | ); 485 | 486 | expect(result).toContain('Money'); 487 | expect(result).toContain('Category'); 488 | expect(result).toContain('String'); 489 | expect(result).not.toContain('Product'); // Should be filtered out 490 | expect(result).toHaveLength(3); 491 | }); 492 | 493 | it('should handle complex scenarios with multiple filtering needs', () => { 494 | const content = ` 495 | # Class OrderProcessor 496 | 497 | ## Properties 498 | 499 | ### defaultOrder 500 | **Type:** dw.order.Order 501 | 502 | ### processor 503 | **Type:** OrderProcessor 504 | 505 | ## Methods 506 | 507 | ### processOrder(order: dw.order.Order): OrderProcessor 508 | Processes an order. 509 | 510 | ### createProcessor(): OrderProcessor 511 | Creates processor instance. 512 | 513 | ### validatePayment(payment: PaymentInstrument): Boolean 514 | Validates payment. 515 | `; 516 | 517 | const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( 518 | content, 519 | 'OrderProcessor', 520 | ); 521 | 522 | expect(result).toContain('dw.order.Order'); 523 | expect(result).toContain('PaymentInstrument'); 524 | expect(result).toContain('Boolean'); 525 | expect(result).not.toContain('OrderProcessor'); // Should be filtered out 526 | expect(result).toHaveLength(3); 527 | }); 528 | 529 | it('should work with empty content', () => { 530 | const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( 531 | '', 532 | 'Product', 533 | ); 534 | expect(result).toEqual([]); 535 | }); 536 | 537 | it('should work when no types need filtering', () => { 538 | const content = ` 539 | # Class Product 540 | 541 | ## Properties 542 | 543 | ### price 544 | **Type:** Money 545 | 546 | ### category 547 | **Type:** Category 548 | 549 | ## Methods 550 | 551 | ### getInventory(): InventoryRecord 552 | Returns inventory. 553 | `; 554 | 555 | const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( 556 | content, 557 | 'Product', 558 | ); 559 | 560 | expect(result).toContain('Money'); 561 | expect(result).toContain('Category'); 562 | expect(result).toContain('InventoryRecord'); 563 | expect(result).toHaveLength(3); 564 | }); 565 | 566 | it('should work when all types need filtering', () => { 567 | const content = ` 568 | # Class Product 569 | 570 | ## Properties 571 | 572 | ### self 573 | **Type:** Product 574 | 575 | ## Methods 576 | 577 | ### getSelf(): Product 578 | Returns self. 579 | 580 | ### createProduct(): dw.catalog.Product 581 | Creates product. 582 | `; 583 | 584 | const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( 585 | content, 586 | 'Product', 587 | ); 588 | 589 | expect(result).toEqual([]); 590 | }); 591 | }); 592 | 593 | describe('integration tests', () => { 594 | it('should handle realistic SFCC class documentation', () => { 595 | const content = ` 596 | # Class dw.catalog.Product 597 | 598 | The Product class represents a product in the catalog. 599 | 600 | ## Properties 601 | 602 | ### ID 603 | **Type:** String 604 | 605 | The product ID. 606 | 607 | ### name 608 | **Type:** String 609 | 610 | The product name. 611 | 612 | ### primaryCategory 613 | **Type:** Category 614 | 615 | The primary category for this product. 616 | 617 | ### priceModel 618 | **Type:** ProductPriceModel 619 | 620 | The price model for this product. 621 | 622 | ### availabilityModel 623 | **Type:** ProductAvailabilityModel 624 | 625 | The availability model for this product. 626 | 627 | ## Methods 628 | 629 | ### getID(): String 630 | Returns the product ID. 631 | 632 | ### getName(): String 633 | Returns the product name. 634 | 635 | ### getPrimaryCategory(): Category 636 | Returns the primary category. 637 | 638 | ### getPriceModel(): ProductPriceModel 639 | Returns the price model. 640 | 641 | ### getAvailabilityModel(): ProductAvailabilityModel 642 | Returns the availability model. 643 | 644 | ### setName(name: String): void 645 | Sets the product name. 646 | 647 | ### assignToCategory(category: Category, primary: Boolean): void 648 | Assigns the product to a category. 649 | 650 | ### getVariationModel(): ProductVariationModel 651 | Returns the variation model. 652 | 653 | ### isVariant(): Boolean 654 | Returns true if this is a variant product. 655 | 656 | ### getMasterProduct(): Product 657 | Returns the master product if this is a variant. 658 | 659 | ### getVariants(): Collection 660 | Returns all variants of this master product. 661 | `; 662 | 663 | const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( 664 | content, 665 | 'Product', 666 | ); 667 | 668 | expect(result).toContain('String'); 669 | expect(result).toContain('Category'); 670 | expect(result).toContain('ProductPriceModel'); 671 | expect(result).toContain('ProductAvailabilityModel'); 672 | expect(result).toContain('Boolean'); 673 | expect(result).toContain('ProductVariationModel'); 674 | expect(result).toContain('Collection'); 675 | expect(result).not.toContain('Product'); // Should be filtered out 676 | expect(result).toHaveLength(7); 677 | }); 678 | 679 | it('should handle documentation with inheritance information', () => { 680 | const content = ` 681 | # Class ProductVariant extends Product 682 | 683 | A product variant inherits from Product. 684 | 685 | ## Properties 686 | 687 | ### masterProduct 688 | **Type:** Product 689 | 690 | The master product. 691 | 692 | ### variationAttributes 693 | **Type:** Map 694 | 695 | The variation attributes. 696 | 697 | ## Methods 698 | 699 | ### getMasterProduct(): Product 700 | Returns the master product. 701 | 702 | ### getVariationValue(attribute: ProductAttribute): ProductAttributeValue 703 | Returns variation value. 704 | 705 | ### isOnline(): Boolean 706 | Checks if variant is online. 707 | `; 708 | 709 | const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( 710 | content, 711 | 'ProductVariant', 712 | ); 713 | 714 | expect(result).toContain('Map'); 715 | expect(result).toContain('ProductAttribute'); 716 | expect(result).toContain('ProductAttributeValue'); 717 | expect(result).toContain('Boolean'); 718 | expect(result).toContain('Product'); // Will be included since it's extracted from content 719 | expect(result).not.toContain('ProductVariant'); // Should be filtered as self-reference 720 | expect(result).toHaveLength(5); 721 | }); 722 | }); 723 | 724 | describe('error handling and edge cases', () => { 725 | it('should handle null input gracefully', () => { 726 | expect(() => { 727 | ReferencedTypesExtractor.extractReferencedTypes(null as any); 728 | }).toThrow(); 729 | }); 730 | 731 | it('should handle undefined input gracefully', () => { 732 | expect(() => { 733 | ReferencedTypesExtractor.extractReferencedTypes(undefined as any); 734 | }).toThrow(); 735 | }); 736 | 737 | it('should handle very large content efficiently', () => { 738 | const largeContent = ` 739 | # Class Product 740 | 741 | ## Properties 742 | 743 | ### prop1 744 | **Type:** Type1 745 | 746 | `.repeat(1000); 747 | 748 | const result = ReferencedTypesExtractor.extractReferencedTypes(largeContent); 749 | expect(result).toContain('Type1'); 750 | expect(result).toHaveLength(1); 751 | }); 752 | 753 | it('should handle content with special characters', () => { 754 | const content = ` 755 | # Class Product 756 | 757 | ## Properties 758 | 759 | ### price€ 760 | **Type:** Money€ 761 | 762 | European price. 763 | 764 | ### nameÜnicode 765 | **Type:** String 766 | 767 | Unicode name. 768 | 769 | ## Methods 770 | 771 | ### getPrice€(): Money€ 772 | Returns European price. 773 | `; 774 | 775 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 776 | 777 | // The current regex pattern [A-Za-z0-9.] doesn't include Unicode characters 778 | // So Money€ will be extracted as just "Money" 779 | expect(result).toContain('Money'); // Money€ gets truncated to Money 780 | expect(result).toContain('String'); 781 | expect(result).toHaveLength(2); 782 | }); 783 | 784 | it('should handle malformed markdown gracefully', () => { 785 | const content = ` 786 | # Class Product 787 | 788 | **Type:** Category 789 | This is not a proper property definition. 790 | 791 | ### method(: Money 792 | Malformed method signature. 793 | 794 | **Type:** 795 | Empty type. 796 | 797 | ### valid 798 | **Type:** ValidType 799 | 800 | This should work. 801 | `; 802 | 803 | const result = ReferencedTypesExtractor.extractReferencedTypes(content); 804 | // The extractor will find: 805 | // - "Category" from "**Type:** Category" (matches property pattern) 806 | // - "Money" from "method(: Money" (matches return type pattern) 807 | // - "ValidType" from the proper property definition 808 | expect(result).toContain('Category'); 809 | expect(result).toContain('Money'); 810 | expect(result).toContain('ValidType'); 811 | expect(result).toHaveLength(3); 812 | }); 813 | }); 814 | }); 815 | ``` -------------------------------------------------------------------------------- /tests/mcp/node/search-sfcc-methods.docs-only.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Programmatic tests for search_sfcc_methods tool 3 | * 4 | * These tests provide advanced verification capabilities beyond YAML pattern matching, 5 | * including performance monitoring, dynamic validation, error categorization, 6 | * comprehensive response structure analysis, and method signature validation. 7 | * 8 | * Response format discovered via aegis query: 9 | * - Success: { content: [{ type: "text", text: "[{\"className\": \"...\", \"method\": {...}}, ...]" }] } 10 | * - Empty: { content: [{ type: "text", text: "[]" }] } 11 | * - Error: { content: [{ type: "text", text: "Error: ..." }], isError: true } 12 | * 13 | * Method object structure: 14 | * { 15 | * "className": "dw_util.Calendar", 16 | * "method": { 17 | * "name": "get", 18 | * "signature": "get(field : Number) : Number", 19 | * "description": "Returns the value of the given calendar field." 20 | * } 21 | * } 22 | */ 23 | 24 | import { test, describe, before, after, beforeEach } from 'node:test'; 25 | import { strict as assert } from 'node:assert'; 26 | import { connect } from 'mcp-aegis'; 27 | 28 | /** 29 | * Performance monitoring utility class 30 | */ 31 | 32 | /** 33 | * Method signature analysis utility class 34 | */ 35 | class MethodSignatureAnalyzer { 36 | constructor() { 37 | this.patterns = { 38 | staticMethod: /^static\s+/, 39 | returnType: /:\s*([A-Za-z0-9_[\]]+)\s*$/, 40 | parameters: /\(([^)]*)\)/, 41 | methodName: /^(?:static\s+)?([A-Za-z_][A-Za-z0-9_]*)/ 42 | }; 43 | } 44 | 45 | analyzeSignature(signature) { 46 | return { 47 | isStatic: this.patterns.staticMethod.test(signature), 48 | returnType: this.extractReturnType(signature), 49 | parameters: this.extractParameters(signature), 50 | methodName: this.extractMethodName(signature), 51 | isValid: this.validateSignature(signature) 52 | }; 53 | } 54 | 55 | extractReturnType(signature) { 56 | const match = signature.match(this.patterns.returnType); 57 | return match ? match[1] : null; 58 | } 59 | 60 | extractParameters(signature) { 61 | const match = signature.match(this.patterns.parameters); 62 | if (!match || !match[1].trim()) return []; 63 | 64 | return match[1].split(',').map(param => { 65 | const parts = param.trim().split(/\s*:\s*/); 66 | return { 67 | name: parts[0]?.trim() || '', 68 | type: parts[1]?.trim() || '' 69 | }; 70 | }); 71 | } 72 | 73 | extractMethodName(signature) { 74 | const match = signature.match(this.patterns.methodName); 75 | return match ? match[1] : null; 76 | } 77 | 78 | validateSignature(signature) { 79 | // Basic validation - should have method name, parentheses, and return type 80 | return signature.includes('(') && 81 | signature.includes(')') && 82 | signature.includes(':') && 83 | signature.trim().length > 0; 84 | } 85 | 86 | categorizeMethod(methodData) { 87 | const { signature } = methodData.method; 88 | const analysis = this.analyzeSignature(signature); 89 | 90 | const categories = []; 91 | 92 | if (analysis.isStatic) categories.push('static'); 93 | if (analysis.methodName?.startsWith('get')) categories.push('getter'); 94 | if (analysis.methodName?.startsWith('set')) categories.push('setter'); 95 | if (analysis.methodName?.startsWith('is') || analysis.methodName?.startsWith('has')) categories.push('boolean'); 96 | if (analysis.returnType === 'void') categories.push('void'); 97 | if (analysis.parameters.length === 0) categories.push('parameterless'); 98 | if (analysis.parameters.length > 3) categories.push('complex'); 99 | 100 | return categories; 101 | } 102 | } 103 | 104 | describe('search_sfcc_methods Programmatic Tests', () => { 105 | let client; 106 | const signatureAnalyzer = new MethodSignatureAnalyzer(); 107 | 108 | before(async () => { 109 | client = await connect('./aegis.config.docs-only.json'); 110 | }); 111 | 112 | after(async () => { 113 | if (client?.connected) { 114 | await client.disconnect(); 115 | } 116 | }); 117 | 118 | beforeEach(() => { 119 | // CRITICAL: Clear all buffers to prevent test interference 120 | client.clearAllBuffers(); // Recommended - comprehensive protection 121 | }); 122 | 123 | describe('Protocol Compliance', () => { 124 | test('should be properly connected to MCP server', async () => { 125 | assert.ok(client.connected, 'Client should be connected'); 126 | }); 127 | 128 | test('should have search_sfcc_methods tool available', async () => { 129 | const tools = await client.listTools(); 130 | const searchTool = tools.find(tool => tool.name === 'search_sfcc_methods'); 131 | 132 | assert.ok(searchTool, 'search_sfcc_methods tool should be available'); 133 | assert.equal(searchTool.name, 'search_sfcc_methods'); 134 | assert.ok(searchTool.description, 'Tool should have description'); 135 | assert.ok(searchTool.inputSchema, 'Tool should have input schema'); 136 | assert.equal(searchTool.inputSchema.type, 'object'); 137 | assert.ok(searchTool.inputSchema.properties.methodName, 'Tool should require methodName parameter'); 138 | }); 139 | }); 140 | 141 | describe('Response Structure Validation', () => { 142 | test('should return properly structured MCP response for valid method search', async () => { 143 | const result = await client.callTool('search_sfcc_methods', { methodName: 'get' }); 144 | 145 | // Validate MCP response structure 146 | assertValidMCPResponse(result); 147 | assert.equal(result.isError, false, 'Should not be an error response'); 148 | assert.equal(result.content.length, 1, 'Should have exactly one content item'); 149 | assert.equal(result.content[0].type, 'text', 'Content should be text type'); 150 | 151 | // Validate JSON array structure 152 | const methodArray = parseMethodArray(result.content[0].text); 153 | assert.ok(Array.isArray(methodArray), 'Response should contain valid JSON array'); 154 | assert.ok(methodArray.length > 0, 'Should return at least one method for get query'); 155 | 156 | // Validate method object structure 157 | methodArray.forEach(methodData => { 158 | assert.equal(typeof methodData, 'object', 'Each method should be an object'); 159 | assert.ok(methodData.className, 'Method should have className property'); 160 | assert.ok(methodData.method, 'Method should have method property'); 161 | assert.equal(typeof methodData.className, 'string', 'className should be string'); 162 | assert.equal(typeof methodData.method, 'object', 'method should be object'); 163 | 164 | // Validate method object properties 165 | assert.ok(methodData.method.name, 'Method should have name'); 166 | assert.ok(methodData.method.signature, 'Method should have signature'); 167 | assert.ok(methodData.method.description, 'Method should have description'); 168 | assert.equal(typeof methodData.method.name, 'string', 'Method name should be string'); 169 | assert.equal(typeof methodData.method.signature, 'string', 'Method signature should be string'); 170 | assert.equal(typeof methodData.method.description, 'string', 'Method description should be string'); 171 | }); 172 | 173 | // Performance validation (lenient for CI environments) 174 | }); 175 | 176 | test('should return empty array for no matches', async () => { 177 | const result = await client.callTool( 178 | 'search_sfcc_methods', { methodName: 'zzznothingfound' } 179 | ); 180 | 181 | assertValidMCPResponse(result); 182 | assert.equal(result.isError, false, 'Should not be an error response'); 183 | 184 | const methodArray = parseMethodArray(result.content[0].text); 185 | assert.ok(Array.isArray(methodArray), 'Response should be valid JSON array'); 186 | assert.equal(methodArray.length, 0, 'Should return empty array for no matches'); 187 | 188 | // Performance should be reasonable for no results (lenient for CI) 189 | }); 190 | 191 | test('should return error response for invalid parameters', async () => { 192 | const result = await client.callTool('search_sfcc_methods', { methodName: '' } 193 | ); 194 | 195 | assertValidMCPResponse(result); 196 | assert.equal(result.isError, true, 'Should be an error response'); 197 | assert.ok(result.content[0].text.includes('Error:'), 'Should contain error message'); 198 | assert.ok(result.content[0].text.includes('non-empty string'), 'Should specify validation requirement'); 199 | 200 | // Error responses should be reasonably fast (CI-friendly) 201 | }); 202 | }); 203 | 204 | describe('Method Search Functionality', () => { 205 | const commonMethodQueries = [ 206 | { query: 'get', expectedMin: 50, category: 'getter' }, 207 | { query: 'set', expectedMin: 10, category: 'setter' }, 208 | { query: 'create', expectedMin: 5, category: 'factory' }, 209 | { query: 'toString', expectedMin: 20, category: 'conversion' }, 210 | { query: 'getValue', expectedMin: 10, category: 'accessor' }, 211 | { query: 'getName', expectedMin: 5, category: 'accessor' } 212 | ]; 213 | 214 | commonMethodQueries.forEach(({ query, expectedMin, category }) => { 215 | test(`should find relevant methods for ${category} query: "${query}"`, async () => { 216 | const result = await client.callTool( 217 | 'search_sfcc_methods', { methodName: query } 218 | ); 219 | 220 | assertValidMCPResponse(result); 221 | assert.equal(result.isError, false, 'Should not be an error'); 222 | 223 | const methodArray = parseMethodArray(result.content[0].text); 224 | assert.ok(methodArray.length >= expectedMin, 225 | `Should find at least ${expectedMin} methods for "${query}", found ${methodArray.length}`); 226 | 227 | // Validate relevance - all method names should contain the query term 228 | methodArray.forEach(methodData => { 229 | const methodName = methodData.method.name.toLowerCase(); 230 | const lowerQuery = query.toLowerCase(); 231 | assert.ok(methodName.includes(lowerQuery), 232 | `Method "${methodData.method.name}" should contain query term "${query}"`); 233 | }); 234 | 235 | // Validate class name format 236 | methodArray.forEach(methodData => { 237 | assert.ok( 238 | methodData.className.startsWith('dw_') || 239 | methodData.className.startsWith('TopLevel.') || 240 | methodData.className.startsWith('best-practices.') || 241 | methodData.className.startsWith('sfra.'), 242 | `Class name "${methodData.className}" should start with recognized namespace` 243 | ); 244 | }); 245 | }); 246 | }); 247 | }); 248 | 249 | describe('Method Signature Analysis', () => { 250 | test('should return valid method signatures for all results', async () => { 251 | const result = await client.callTool('search_sfcc_methods', { methodName: 'get' }); 252 | 253 | assertValidMCPResponse(result); 254 | const methodArray = parseMethodArray(result.content[0].text); 255 | 256 | methodArray.slice(0, 20).forEach(methodData => { // Test first 20 for performance 257 | const analysis = signatureAnalyzer.analyzeSignature(methodData.method.signature); 258 | 259 | assert.ok(analysis.isValid, 260 | `Method signature "${methodData.method.signature}" should be valid`); 261 | assert.ok(analysis.methodName, 262 | `Should extract method name from "${methodData.method.signature}"`); 263 | assert.ok(analysis.returnType, 264 | `Should extract return type from "${methodData.method.signature}"`); 265 | 266 | // Method name in signature should match the method name property 267 | assert.equal(analysis.methodName, methodData.method.name, 268 | 'Method name in signature should match method.name property'); 269 | 270 | // Signature should be properly formatted 271 | assert.ok(methodData.method.signature.includes('('), 'Signature should contain opening parenthesis'); 272 | assert.ok(methodData.method.signature.includes(')'), 'Signature should contain closing parenthesis'); 273 | assert.ok(methodData.method.signature.includes(':'), 'Signature should contain return type separator'); 274 | }); 275 | }); 276 | 277 | test('should categorize methods correctly by signature patterns', async () => { 278 | const result = await client.callTool('search_sfcc_methods', { methodName: 'get' }); 279 | 280 | assertValidMCPResponse(result); 281 | const methodArray = parseMethodArray(result.content[0].text); 282 | 283 | const categoryCounts = { 284 | static: 0, 285 | getter: 0, 286 | parameterless: 0, 287 | complex: 0 288 | }; 289 | 290 | methodArray.slice(0, 50).forEach(methodData => { 291 | const categories = signatureAnalyzer.categorizeMethod(methodData); 292 | categories.forEach(category => { 293 | if (Object.prototype.hasOwnProperty.call(categoryCounts, category)) { 294 | categoryCounts[category]++; 295 | } 296 | }); 297 | }); 298 | 299 | // Should find a good mix of method types 300 | assert.ok(categoryCounts.getter > 10, 'Should find plenty of getter methods'); 301 | // Static methods might not be common in 'get' search, so make this more flexible 302 | assert.ok(categoryCounts.static >= 0, 'Static method count should be non-negative'); 303 | assert.ok(categoryCounts.parameterless > 5, 'Should find parameterless methods'); 304 | 305 | assert.ok(Object.keys(categoryCounts).length > 0, 'Should have categorized methods'); 306 | }); 307 | }); 308 | 309 | describe('Edge Case Validation', () => { 310 | const edgeCases = [ 311 | { methodName: 'A', description: 'single character' }, 312 | { methodName: 'get', description: 'common method prefix' }, 313 | { methodName: 'GET', description: 'uppercase query' }, 314 | { methodName: 'getValue', description: 'compound method name' }, 315 | { methodName: '123', description: 'numeric query' }, 316 | { methodName: 'xyz_nonexistent_method', description: 'clearly non-existent method' } 317 | ]; 318 | 319 | edgeCases.forEach(({ methodName, description }) => { 320 | test(`should handle ${description} query: "${methodName}"`, async () => { 321 | const result = await client.callTool( 322 | 'search_sfcc_methods', { methodName } 323 | ); 324 | 325 | assertValidMCPResponse(result); 326 | assert.equal(result.isError, false, 'Should not be an error for valid string'); 327 | 328 | const methodArray = parseMethodArray(result.content[0].text); 329 | assert.ok(Array.isArray(methodArray), 'Should return valid array'); 330 | 331 | // All results should have valid structure 332 | methodArray.forEach(methodData => { 333 | assert.ok(methodData.className, 'Should have className'); 334 | assert.ok(methodData.method, 'Should have method object'); 335 | assert.ok(methodData.method.name, 'Should have method name'); 336 | assert.ok(methodData.method.signature, 'Should have method signature'); 337 | assert.ok(methodData.method.description, 'Should have method description'); 338 | 339 | // Method name should contain the query (case insensitive) 340 | const methodNameLower = methodData.method.name.toLowerCase(); 341 | const queryLower = methodName.toLowerCase(); 342 | assert.ok(methodNameLower.includes(queryLower), 343 | `Method "${methodData.method.name}" should contain query "${methodName}"`); 344 | }); 345 | 346 | // Performance should be reasonable for CI environments 347 | }); 348 | }); 349 | }); 350 | 351 | describe('Error Handling Validation', () => { 352 | const errorCases = [ 353 | { args: { methodName: '' }, description: 'empty method name' }, 354 | { args: {}, description: 'missing methodName parameter' }, 355 | { args: { methodName: ' ' }, description: 'whitespace-only method name' }, 356 | { args: { methodName: null }, description: 'null method name' }, 357 | { args: { methodName: 123 }, description: 'non-string method name (number)' }, 358 | { args: { methodName: true }, description: 'non-string method name (boolean)' }, 359 | { args: { methodName: [] }, description: 'non-string method name (array)' }, 360 | { args: { methodName: {} }, description: 'non-string method name (object)' } 361 | ]; 362 | 363 | errorCases.forEach(({ args, description }) => { 364 | test(`should return error for ${description}`, async () => { 365 | const result = await client.callTool( 366 | 'search_sfcc_methods', args 367 | ); 368 | 369 | assertValidMCPResponse(result); 370 | assert.equal(result.isError, true, 'Should be an error response'); 371 | assert.ok(result.content[0].text.includes('Error'), 'Should contain error message'); 372 | 373 | // Categorize error type 374 | const errorType = categorizeError(result.content[0].text); 375 | assert.ok(['validation', 'not_found', 'unknown'].includes(errorType), 376 | `Error should be categorized (got: ${errorType})`); 377 | 378 | // Error responses should be reasonably fast (CI-friendly) 379 | }); 380 | }); 381 | }); 382 | 383 | describe('Consistency and Reliability', () => { 384 | test('should return consistent results across multiple calls', async () => { 385 | const methodName = 'getValue'; 386 | const results = await Promise.all([ 387 | client.callTool('search_sfcc_methods', { methodName }), 388 | client.callTool('search_sfcc_methods', { methodName }), 389 | client.callTool('search_sfcc_methods', { methodName }) 390 | ]); 391 | 392 | // All results should be successful 393 | results.forEach(result => { 394 | assertValidMCPResponse(result); 395 | assert.equal(result.isError, false, 'Should not be error'); 396 | }); 397 | 398 | // Parse arrays for comparison 399 | const arrays = results.map(result => parseMethodArray(result.content[0].text)); 400 | 401 | // All arrays should be identical 402 | assert.deepEqual(arrays[0], arrays[1], 'Results should be consistent across calls'); 403 | assert.deepEqual(arrays[1], arrays[2], 'Results should be consistent across calls'); 404 | }); 405 | 406 | test('should validate method data integrity and format', async () => { 407 | const result = await client.callTool('search_sfcc_methods', { methodName: 'toString' }); 408 | 409 | assertValidMCPResponse(result); 410 | const methodArray = parseMethodArray(result.content[0].text); 411 | 412 | methodArray.forEach(methodData => { 413 | // Validate method data integrity 414 | assert.ok(methodData.className.length > 3, 415 | `Class name "${methodData.className}" should be reasonable length`); 416 | assert.ok(methodData.className.length < 100, 417 | `Class name "${methodData.className}" should not be excessively long`); 418 | 419 | // Validate class name format 420 | assert.match(methodData.className, /^(dw_|TopLevel\.|best-practices\.|sfra\.)[a-zA-Z0-9_./-]+$/, 421 | `Class name "${methodData.className}" should follow valid pattern`); 422 | 423 | // Validate method name format 424 | assert.match(methodData.method.name, /^[a-zA-Z_][a-zA-Z0-9_]*$/, 425 | `Method name "${methodData.method.name}" should follow valid identifier pattern`); 426 | 427 | // Should not contain HTML or special characters 428 | assert.ok(!methodData.method.name.includes('<'), 'Method name should not contain HTML'); 429 | assert.ok(!methodData.method.name.includes('>'), 'Method name should not contain HTML'); 430 | 431 | // Description should be reasonable length 432 | assert.ok(methodData.method.description.length > 5, 'Description should not be too short'); 433 | assert.ok(methodData.method.description.length < 2000, 'Description should not be excessively long'); 434 | 435 | // Signature should contain the method name 436 | assert.ok(methodData.method.signature.includes(methodData.method.name), 437 | 'Signature should contain the method name'); 438 | }); 439 | }); 440 | }); 441 | }); 442 | 443 | // Helper functions 444 | 445 | /** 446 | * Validates that a response follows proper MCP structure 447 | */ 448 | function assertValidMCPResponse(result) { 449 | assert.ok(result.content, 'Response should have content property'); 450 | assert.ok(Array.isArray(result.content), 'Content should be an array'); 451 | assert.ok(result.content.length > 0, 'Content array should not be empty'); 452 | assert.equal(result.content[0].type, 'text', 'First content item should be text type'); 453 | assert.equal(typeof result.content[0].text, 'string', 'Text content should be a string'); 454 | 455 | // isError property should always be present and boolean 456 | assert.ok(Object.prototype.hasOwnProperty.call(result, 'isError'), 'isError property should always be present'); 457 | assert.equal(typeof result.isError, 'boolean', 'isError should be a boolean'); 458 | } 459 | 460 | /** 461 | * Parses the method array from the response text 462 | */ 463 | function parseMethodArray(text) { 464 | try { 465 | return JSON.parse(text); 466 | } catch { 467 | throw new Error(`Failed to parse method array from response: ${text}`); 468 | } 469 | } 470 | 471 | /** 472 | * Categorizes error messages by type 473 | */ 474 | function categorizeError(errorText) { 475 | const errorPatterns = [ 476 | { type: 'validation', keywords: ['required', 'invalid', 'missing', 'non-empty', 'string'] }, 477 | { type: 'not_found', keywords: ['not found', 'does not exist'] }, 478 | { type: 'permission', keywords: ['permission', 'unauthorized', 'forbidden'] }, 479 | { type: 'network', keywords: ['connection', 'timeout', 'unreachable'] } 480 | ]; 481 | 482 | const lowerText = errorText.toLowerCase(); 483 | for (const pattern of errorPatterns) { 484 | if (pattern.keywords.some(keyword => lowerText.includes(keyword))) { 485 | return pattern.type; 486 | } 487 | } 488 | return 'unknown'; 489 | } 490 | ``` -------------------------------------------------------------------------------- /tests/mcp/node/search-job-logs.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { test, describe, before, after, beforeEach } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import { connect } from 'mcp-aegis'; 4 | 5 | describe('search_job_logs - Optimized Programmatic Tests', () => { 6 | let client; 7 | let discoveredJobNames = []; 8 | let discoveredPatterns = []; 9 | 10 | before(async () => { 11 | client = await connect('./aegis.config.with-dw.json'); 12 | 13 | // Discover available job names and patterns for advanced testing 14 | await discoverJobNames(); 15 | await discoverCommonPatterns(); 16 | }); 17 | 18 | after(async () => { 19 | if (client?.connected) { 20 | await client.disconnect(); 21 | } 22 | }); 23 | 24 | beforeEach(() => { 25 | // CRITICAL: Clear all buffers to prevent leaking into next tests 26 | client.clearAllBuffers(); // Recommended - comprehensive protection 27 | }); 28 | 29 | // Helper functions for dynamic discovery and complex validation 30 | async function discoverJobNames() { 31 | try { 32 | const result = await client.callTool('search_job_logs', { 33 | pattern: 'Executing', 34 | limit: 50 35 | }); 36 | 37 | if (!result.isError && result.content?.[0]?.text) { 38 | const text = parseResponseText(result.content[0].text); 39 | // Extract job names from the beginning of each log entry (first bracket pair) 40 | const jobMatches = text.match(/^\[([A-Za-z][A-Za-z0-9_-]*)\]/gm) || []; 41 | discoveredJobNames = [...new Set(jobMatches.map(match => match.slice(1, -1)))]; 42 | } 43 | } catch (error) { 44 | console.warn('Could not discover job names:', error.message); 45 | } 46 | } 47 | 48 | async function discoverCommonPatterns() { 49 | const commonTerms = ['INFO', 'ERROR', 'step', 'completed', 'job', 'Executing']; 50 | 51 | for (const term of commonTerms) { 52 | try { 53 | const result = await client.callTool('search_job_logs', { 54 | pattern: term, 55 | limit: 1 56 | }); 57 | 58 | if (!result.isError && result.content?.[0]?.text) { 59 | const text = parseResponseText(result.content[0].text); 60 | if (!text.includes('No matches found')) { 61 | discoveredPatterns.push(term); 62 | } 63 | } 64 | } catch (error) { 65 | console.warn(`Error testing pattern "${term}":`, error.message); 66 | } 67 | } 68 | } 69 | 70 | // Helper functions for complex validations 71 | function assertValidMCPResponse(result) { 72 | assert.ok(result.content, 'Should have content'); 73 | assert.ok(Array.isArray(result.content), 'Content should be array'); 74 | assert.equal(typeof result.isError, 'boolean', 'isError should be boolean'); 75 | } 76 | 77 | function parseResponseText(text) { 78 | // The response may come wrapped in quotes, so parse if needed 79 | return text.startsWith('"') && text.endsWith('"') 80 | ? JSON.parse(text) 81 | : text; 82 | } 83 | 84 | function assertSearchResultsFormat(result, pattern, expectedJobName = null) { 85 | assertValidMCPResponse(result); 86 | assert.equal(result.isError, false, 'Should not be an error response'); 87 | assert.equal(result.content[0].type, 'text'); 88 | 89 | const text = parseResponseText(result.content[0].text); 90 | 91 | if (text.includes('No matches found')) { 92 | // Valid empty result case 93 | assert.ok(text.includes(`No matches found for "${pattern}"`), 94 | `Should indicate no matches for pattern "${pattern}"`); 95 | return { matchCount: 0, entries: [] }; 96 | } 97 | 98 | // Should contain found matches header 99 | if (expectedJobName) { 100 | assert.ok(text.includes(`Found`) && text.includes(`matches for "${pattern}"`), 101 | `Should contain found matches header for pattern "${pattern}"`); 102 | assert.ok(text.includes(`job: ${expectedJobName}`) || text.includes(`in job: ${expectedJobName}`), 103 | `Should indicate filtering by job "${expectedJobName}"`); 104 | } else { 105 | assert.ok(text.includes(`Found`) && text.includes(`matches for "${pattern}"`), 106 | `Should contain found matches header for pattern "${pattern}"`); 107 | } 108 | 109 | // Extract job log entries 110 | const entries = extractJobLogEntries(text); 111 | 112 | // Validate each entry has proper structure and contains pattern 113 | for (const entry of entries) { 114 | // Each entry should have job name in brackets 115 | assert.ok(/^\[[\w\-_]+\]/.test(entry.trim()), 116 | `Entry should start with job name in brackets: "${entry.substring(0, 50)}..."`); 117 | 118 | // Should contain timestamp in GMT format 119 | assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(entry), 120 | `Entry should contain GMT timestamp: "${entry.substring(0, 100)}..."`); 121 | 122 | // Should contain the search pattern (case-insensitive check) 123 | assert.ok(entry.toLowerCase().includes(pattern.toLowerCase()), 124 | `Entry should contain pattern "${pattern}": "${entry.substring(0, 100)}..."`); 125 | 126 | // If filtering by job name, all entries should be from that job 127 | if (expectedJobName) { 128 | assert.ok(entry.includes(`[${expectedJobName}]`), 129 | `Entry should be from job "${expectedJobName}": "${entry.substring(0, 50)}..."`); 130 | } 131 | } 132 | 133 | // Extract match count from header 134 | const matchCountMatch = text.match(/Found (\d+) matches/); 135 | const matchCount = matchCountMatch ? parseInt(matchCountMatch[1]) : entries.length; 136 | 137 | return { matchCount, entries }; 138 | } 139 | 140 | function extractJobLogEntries(text) { 141 | // Split by double newlines and filter for entries that look like job logs 142 | const lines = text.split(/\n\n+/); 143 | return lines.filter(line => 144 | line.trim() && 145 | /^\[[\w\-_]+\]/.test(line.trim()) && 146 | /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(line) 147 | ); 148 | } 149 | 150 | function assertTimestampFormat(entries) { 151 | for (const entry of entries) { 152 | // Should contain valid timestamp format 153 | const timestampMatch = entry.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT)/); 154 | assert.ok(timestampMatch, `Entry should contain valid timestamp: "${entry.substring(0, 100)}..."`); 155 | 156 | // Validate timestamp is parseable 157 | const timestampStr = timestampMatch[1].replace(' GMT', 'Z'); 158 | const date = new Date(timestampStr); 159 | assert.ok(!isNaN(date.getTime()), `Timestamp should be valid date: "${timestampMatch[1]}"`); 160 | } 161 | } 162 | 163 | // === Dynamic Discovery and Validation Tests === 164 | describe('Dynamic Discovery and Validation', () => { 165 | test('should dynamically validate discovered job names', async () => { 166 | if (discoveredJobNames.length === 0) { 167 | console.warn('No job names discovered - skipping dynamic validation'); 168 | return; 169 | } 170 | 171 | // Test a sample of discovered job names (not all to avoid excessive testing) 172 | const sampleJobs = discoveredJobNames.slice(0, Math.min(3, discoveredJobNames.length)); 173 | 174 | for (const jobName of sampleJobs) { 175 | const result = await client.callTool('search_job_logs', { 176 | pattern: 'Executing', 177 | jobName: jobName, 178 | limit: 1 179 | }); 180 | 181 | assertValidMCPResponse(result); 182 | assert.equal(result.isError, false, `Job "${jobName}" should be searchable`); 183 | 184 | const searchResults = assertSearchResultsFormat(result, 'Executing', jobName); 185 | 186 | // If we found entries, they should all be from the specified job 187 | if (searchResults.entries.length > 0) { 188 | for (const entry of searchResults.entries) { 189 | assert.ok(entry.includes(`[${jobName}]`), 190 | `Entry should be from job "${jobName}": "${entry.substring(0, 50)}..."`); 191 | } 192 | } 193 | } 194 | }); 195 | 196 | test('should dynamically test discovered patterns with complex validation', async () => { 197 | if (discoveredPatterns.length === 0) { 198 | console.warn('No patterns discovered - skipping dynamic pattern testing'); 199 | return; 200 | } 201 | 202 | for (const pattern of discoveredPatterns) { 203 | const result = await client.callTool('search_job_logs', { 204 | pattern: pattern, 205 | limit: 3 206 | }); 207 | 208 | assertValidMCPResponse(result); 209 | assert.equal(result.isError, false, `Pattern "${pattern}" should be searchable`); 210 | 211 | const searchResults = assertSearchResultsFormat(result, pattern); 212 | 213 | if (searchResults.entries.length > 0) { 214 | // Validate complex content structure for each discovered pattern 215 | assertTimestampFormat(searchResults.entries); 216 | 217 | // Validate pattern appears in content with case-insensitive search 218 | for (const entry of searchResults.entries) { 219 | assert.ok(entry.toLowerCase().includes(pattern.toLowerCase()), 220 | `Entry should contain pattern "${pattern}" (case-insensitive): "${entry.substring(0, 100)}..."`); 221 | } 222 | } 223 | } 224 | }); 225 | 226 | test('should validate cross-job pattern distribution', async () => { 227 | if (discoveredJobNames.length < 2 || discoveredPatterns.length === 0) { 228 | console.warn('Insufficient data for cross-job validation - skipping'); 229 | return; 230 | } 231 | 232 | // Test if common patterns appear across multiple jobs 233 | const commonPattern = discoveredPatterns[0]; // Use first discovered pattern 234 | const jobResults = new Map(); 235 | 236 | // Search each job for the common pattern 237 | for (const jobName of discoveredJobNames.slice(0, 3)) { 238 | const result = await client.callTool('search_job_logs', { 239 | pattern: commonPattern, 240 | jobName: jobName, 241 | limit: 1 242 | }); 243 | 244 | assertValidMCPResponse(result); 245 | const searchResults = assertSearchResultsFormat(result, commonPattern, jobName); 246 | jobResults.set(jobName, searchResults.matchCount); 247 | } 248 | 249 | // Should be able to search across multiple jobs successfully 250 | assert.ok(jobResults.size > 0, 'Should be able to search multiple jobs'); 251 | }); 252 | }); 253 | 254 | // === Complex Content and Format Validation === 255 | describe('Complex Content Validation', () => { 256 | test('should validate comprehensive job log entry structure', async () => { 257 | const result = await client.callTool('search_job_logs', { 258 | pattern: 'step', 259 | limit: 5 260 | }); 261 | 262 | assertValidMCPResponse(result); 263 | const searchResults = assertSearchResultsFormat(result, 'step'); 264 | 265 | if (searchResults.entries.length > 0) { 266 | for (const entry of searchResults.entries) { 267 | // Complex structural validation 268 | assert.ok(/^\[[\w\-_]+\]/.test(entry.trim()), 269 | `Entry should start with job name: "${entry.substring(0, 50)}..."`); 270 | 271 | // Validate timestamp format and parseability 272 | const timestampMatch = entry.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT)/); 273 | assert.ok(timestampMatch, `Entry should contain timestamp: "${entry.substring(0, 100)}..."`); 274 | 275 | const timestampStr = timestampMatch[1].replace(' GMT', 'Z'); 276 | const date = new Date(timestampStr); 277 | assert.ok(!isNaN(date.getTime()), `Timestamp should be parseable: "${timestampMatch[1]}"`); 278 | 279 | // Validate log level presence 280 | assert.ok(/\s(INFO|ERROR|WARN|DEBUG)\s/.test(entry), 281 | `Entry should contain log level: "${entry.substring(0, 100)}..."`); 282 | 283 | // Validate thread information 284 | assert.ok(/Thread|SystemJobThread/.test(entry), 285 | `Entry should contain thread info: "${entry.substring(0, 100)}..."`); 286 | } 287 | } 288 | }); 289 | 290 | test('should maintain consistent format across different search parameters', async () => { 291 | const testCombinations = [ 292 | { pattern: 'INFO', level: 'info' }, 293 | { pattern: 'step', limit: 2 }, 294 | { pattern: 'completed', jobName: discoveredJobNames[0] || 'ImportCatalog' } 295 | ]; 296 | 297 | for (const combo of testCombinations) { 298 | const result = await client.callTool('search_job_logs', combo); 299 | 300 | assertValidMCPResponse(result); 301 | assert.equal(result.isError, false, `Combination ${JSON.stringify(combo)} should succeed`); 302 | 303 | const searchResults = assertSearchResultsFormat(result, combo.pattern, combo.jobName); 304 | 305 | if (searchResults.entries.length > 0) { 306 | // Validate consistent structure regardless of search parameters 307 | for (const entry of searchResults.entries) { 308 | assert.ok(/^\[[\w\-_]+\]/.test(entry.trim()), 309 | `Consistent format for combo ${JSON.stringify(combo)}: "${entry.substring(0, 50)}..."`); 310 | assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(entry), 311 | `Consistent timestamp for combo ${JSON.stringify(combo)}: "${entry.substring(0, 100)}..."`); 312 | } 313 | } 314 | } 315 | }); 316 | }); 317 | 318 | // === Advanced Multi-Step Scenarios === 319 | describe('Advanced Multi-Step Scenarios', () => { 320 | test('should support complex search refinement workflows', async () => { 321 | // Step 1: Broad search to find available data 322 | const broadResult = await client.callTool('search_job_logs', { 323 | pattern: 'job', 324 | limit: 10 325 | }); 326 | 327 | assertValidMCPResponse(broadResult); 328 | const broadSearch = assertSearchResultsFormat(broadResult, 'job'); 329 | 330 | if (broadSearch.entries.length > 0) { 331 | // Step 2: Extract job name from first result for targeted search 332 | const firstEntry = broadSearch.entries[0]; 333 | const jobMatch = firstEntry.match(/^\[([^\]]+)\]/); 334 | 335 | if (jobMatch) { 336 | const extractedJobName = jobMatch[1]; 337 | 338 | // Step 3: Refined search using extracted job name 339 | const refinedResult = await client.callTool('search_job_logs', { 340 | pattern: 'step', 341 | jobName: extractedJobName, 342 | limit: 3 343 | }); 344 | 345 | assertValidMCPResponse(refinedResult); 346 | const refinedSearch = assertSearchResultsFormat(refinedResult, 'step', extractedJobName); 347 | 348 | // Step 4: Validate refinement worked correctly 349 | if (refinedSearch.entries.length > 0) { 350 | for (const entry of refinedSearch.entries) { 351 | assert.ok(entry.includes(`[${extractedJobName}]`), 352 | `Refined search should only return entries from "${extractedJobName}"`); 353 | assert.ok(entry.toLowerCase().includes('step'), 354 | `Refined search should contain "step" pattern`); 355 | } 356 | } 357 | } 358 | } 359 | }); 360 | 361 | test('should handle progressive pattern narrowing', async () => { 362 | // Progressive narrowing from broad to specific patterns 363 | const progressivePatterns = ['job', 'step', 'Executing step']; 364 | const results = []; 365 | 366 | for (const pattern of progressivePatterns) { 367 | const result = await client.callTool('search_job_logs', { 368 | pattern: pattern, 369 | limit: 5 370 | }); 371 | 372 | assertValidMCPResponse(result); 373 | const searchResults = assertSearchResultsFormat(result, pattern); 374 | results.push({ pattern, matchCount: searchResults.matchCount }); 375 | } 376 | 377 | // Each pattern should be valid (no errors) 378 | assert.equal(results.length, progressivePatterns.length, 379 | 'All progressive patterns should execute successfully'); 380 | 381 | // More specific patterns should not return more results than broader ones 382 | if (results[0].matchCount > 0 && results[1].matchCount > 0) { 383 | assert.ok(results[1].matchCount <= results[0].matchCount, 384 | 'More specific patterns should not exceed broader pattern results'); 385 | } 386 | }); 387 | }); 388 | 389 | // === Performance and Reliability Monitoring === 390 | describe('Performance and Reliability', () => { 391 | test('should handle sequential search operations reliably', async () => { 392 | const searchOperations = [ 393 | { pattern: 'INFO', limit: 3 }, 394 | { pattern: 'step', limit: 2 }, 395 | { pattern: 'completed', limit: 1 } 396 | ]; 397 | 398 | const results = []; 399 | 400 | // Execute operations sequentially to test reliability 401 | for (const operation of searchOperations) { 402 | const result = await client.callTool('search_job_logs', operation); 403 | 404 | assertValidMCPResponse(result); 405 | assert.equal(result.isError, false, 406 | `Operation ${JSON.stringify(operation)} should succeed`); 407 | 408 | results.push({ 409 | operation, 410 | success: !result.isError, 411 | hasContent: result.content && result.content.length > 0 412 | }); 413 | } 414 | 415 | // All operations should succeed 416 | const successfulOps = results.filter(r => r.success).length; 417 | assert.equal(successfulOps, searchOperations.length, 418 | 'All sequential operations should succeed'); 419 | 420 | // All operations should return content 421 | const opsWithContent = results.filter(r => r.hasContent).length; 422 | assert.equal(opsWithContent, searchOperations.length, 423 | 'All operations should return content'); 424 | }); 425 | 426 | test('should provide consistent results for repeated searches', async () => { 427 | const testPattern = 'INFO'; 428 | const repeatCount = 3; 429 | const results = []; 430 | 431 | // Perform same search multiple times 432 | for (let i = 0; i < repeatCount; i++) { 433 | const result = await client.callTool('search_job_logs', { 434 | pattern: testPattern, 435 | limit: 2 436 | }); 437 | 438 | assertValidMCPResponse(result); 439 | assert.equal(result.isError, false, `Iteration ${i + 1} should succeed`); 440 | 441 | const text = parseResponseText(result.content[0].text); 442 | const matchCountMatch = text.match(/Found (\d+) matches/); 443 | const matchCount = matchCountMatch ? parseInt(matchCountMatch[1]) : 0; 444 | 445 | results.push({ iteration: i + 1, matchCount, hasMatches: matchCount > 0 }); 446 | } 447 | 448 | // Results should be consistent across iterations 449 | const uniqueMatchCounts = [...new Set(results.map(r => r.matchCount))]; 450 | assert.equal(uniqueMatchCounts.length, 1, 451 | `Match counts should be consistent across iterations: ${results.map(r => r.matchCount).join(', ')}`); 452 | }); 453 | }); 454 | 455 | // === Comprehensive Error Scenario Testing === 456 | describe('Complex Error Scenarios', () => { 457 | test('should categorize and handle different error types systematically', async () => { 458 | const errorScenarios = [ 459 | { 460 | name: 'validation_error', 461 | params: { pattern: '' }, 462 | expectedKeywords: ['pattern', 'non-empty', 'string'] 463 | }, 464 | { 465 | name: 'type_error', 466 | params: { pattern: 'INFO', limit: 'invalid' }, 467 | expectedKeywords: ['Invalid limit', 'number'] 468 | }, 469 | { 470 | name: 'constraint_error', 471 | params: { pattern: 'INFO', limit: -1 }, 472 | expectedKeywords: ['Invalid limit', 'Must be between'] 473 | } 474 | ]; 475 | 476 | for (const scenario of errorScenarios) { 477 | const result = await client.callTool('search_job_logs', scenario.params); 478 | 479 | assertValidMCPResponse(result); 480 | assert.equal(result.isError, true, 481 | `Scenario "${scenario.name}" should be an error`); 482 | 483 | const errorText = result.content[0].text.toLowerCase(); 484 | 485 | // Check if error contains expected keywords 486 | const hasExpectedKeywords = scenario.expectedKeywords.some(keyword => 487 | errorText.includes(keyword.toLowerCase()) 488 | ); 489 | 490 | assert.ok(hasExpectedKeywords, 491 | `Error for "${scenario.name}" should contain keywords: ${scenario.expectedKeywords.join(', ')}. Got: "${result.content[0].text}"`); 492 | } 493 | }); 494 | 495 | test('should handle edge cases with complex validation logic', async () => { 496 | const edgeCases = [ 497 | { name: 'special_characters', pattern: '[test]', expectSuccess: true }, 498 | { name: 'unicode_characters', pattern: 'тест', expectSuccess: true }, 499 | { name: 'very_long_pattern', pattern: 'a'.repeat(500), expectSuccess: true }, 500 | { name: 'pattern_with_quotes', pattern: '"quoted"', expectSuccess: true } 501 | ]; 502 | 503 | for (const edgeCase of edgeCases) { 504 | const result = await client.callTool('search_job_logs', { 505 | pattern: edgeCase.pattern, 506 | limit: 1 507 | }); 508 | 509 | assertValidMCPResponse(result); 510 | 511 | if (edgeCase.expectSuccess) { 512 | assert.equal(result.isError, false, 513 | `Edge case "${edgeCase.name}" should succeed`); 514 | 515 | // Should handle gracefully even if no matches found 516 | const searchResults = assertSearchResultsFormat(result, edgeCase.pattern); 517 | assert.ok(searchResults.matchCount >= 0, 518 | `Edge case "${edgeCase.name}" should return valid match count`); 519 | } else { 520 | assert.equal(result.isError, true, 521 | `Edge case "${edgeCase.name}" should fail as expected`); 522 | } 523 | } 524 | }); 525 | }); 526 | }); ```