This is page 38 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 -------------------------------------------------------------------------------- /.github/instructions/mcp-yml-tests.instructions.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | applyTo: "**/*.test.mcp.yml" 3 | --- 4 | 5 | # MCP Aegis - YAML Testing Guide for AI Agents 6 | 7 | **Target**: AI assistants generating declarative YAML test files for Model Context Protocol servers. 8 | 9 | **Core Purpose**: Test MCP servers with human-readable YAML files using 35+ advanced pattern matching capabilities including string patterns, numeric comparisons, date validation, array operations, field extraction, cross-field validation, and pattern negation. 10 | 11 | ## 🆕 New Features: Pipe-Separated Parameters & Enhanced Testing 12 | 13 | MCP Aegis now supports **CLI-friendly pipe-separated parameter format** alongside traditional JSON: 14 | 15 | ```bash 16 | # 🆕 Pipe format (CLI-friendly): key:value|other:123 17 | npx aegis query calculator 'operation:add|a:5|b:3' --config "config.json" 18 | 19 | # Traditional JSON (still supported): {"key":"value","other":123} 20 | npx aegis query calculator '{"operation": "add", "a": 5, "b": 3}' --config "config.json" 21 | ``` 22 | 23 | **Benefits**: ✅ No shell escaping ✅ Readable syntax ✅ Type inference ✅ Nested objects via dot notation ✅ Backward compatible 24 | 25 | ## 🥇 GOLDEN RULE: Always Discover Response Formats First 26 | 27 | **CRITICAL**: Before writing ANY YAML test, you MUST use `aegis query` to discover actual response formats for both success and failure scenarios. Never assume response structure. 28 | 29 | ### Test Development Workflow (Discovery-First) 30 | 31 | **Step 1: Discovery Commands (Mandatory)** 32 | ```bash 33 | # Test successful execution 34 | npx aegis query [tool_name] '[valid_params]' --config "config.json" 35 | 36 | # Test failure scenarios 37 | npx aegis query [tool_name] '[invalid_params]' --config "config.json" 38 | npx aegis query [tool_name] '' --config "config.json" # Empty/missing params 39 | ``` 40 | 41 | **Step 2: Document Findings & Write Tests** 42 | ```yaml 43 | # Document discovery results as comments 44 | # Discovery: npx aegis query search_sfcc_classes 'query:catalog' 45 | # Success: ["dw.catalog.Product", "dw.catalog.Catalog"] (simple array) 46 | # Empty: [] (empty array) 47 | # Error: {"content": [{"type": "text", "text": "Error: ..."}], "isError": true} 48 | 49 | - it: "should return class array" 50 | expect: 51 | response: 52 | result: 53 | text: "match:regex:\\[[\\s\\S]*\\]" # Based on actual format 54 | text: "match:contains:dw.catalog" # Contains expected content 55 | ``` 56 | 57 | **Step 3: Common Discovery Patterns** 58 | ```bash 59 | # Examples with expected patterns: 60 | npx aegis query list_tools --config "config.json" 61 | # → ["tool1", "tool2"] → YAML: text: "match:regex:\\[[\\s\\S]*\\]" 62 | 63 | npx aegis query search_nothing 'query:zzznothingfound' --config "config.json" 64 | # → [] → YAML: text: "match:regex:^\\[\\s*\\]$" 65 | 66 | npx aegis query invalid_tool 'bad:params' --config "config.json" 67 | # → {"content": [...], "isError": true} → YAML: text: "match:contains:Error" 68 | ``` 69 | 70 | ## Configuration & Basic Usage 71 | 72 | ### Required Configuration (`*.config.json`) 73 | ```json 74 | { 75 | "name": "My MCP Server", // Human-readable name for reports 76 | "command": "node", // Executable (node, python, ./binary) 77 | "args": ["./server.js"], // Arguments array 78 | "cwd": "./optional/directory", // Working directory (optional) 79 | "env": {"CUSTOM_VAR": "value"}, // Environment variables (optional) 80 | "startupTimeout": 5000, // Max startup wait (ms, default: 10000) 81 | "readyPattern": "Server ready" // Stderr regex for ready state (optional) 82 | } 83 | ``` 84 | 85 | ### Common Configurations 86 | ```json 87 | // Node.js Server 88 | {"name": "Node.js MCP", "command": "node", "args": ["./dist/index.js"], "startupTimeout": 5000} 89 | 90 | // Python Server 91 | {"name": "Python MCP", "command": "python", "args": ["-m", "my_mcp_server"], "env": {"PYTHONPATH": "./src"}} 92 | 93 | // Development Server 94 | {"name": "Dev Server", "command": "npm", "args": ["run", "dev"], "startupTimeout": 15000} 95 | ``` 96 | ### Basic Test Structure (`*.test.mcp.yml`) 97 | ```yaml 98 | description: "Test suite description" 99 | tests: 100 | - it: "Test description" 101 | request: 102 | jsonrpc: "2.0" 103 | id: "unique-id" 104 | method: "tools/list|tools/call" 105 | params: {} # or tool call params 106 | expect: 107 | response: 108 | jsonrpc: "2.0" 109 | id: "unique-id" 110 | result: {} # expected response 111 | stderr: "toBeEmpty" # optional 112 | ``` 113 | 114 | ### Execute Tests 115 | ```bash 116 | # 🥇 GOLDEN RULE: ALWAYS discover response formats first! 117 | # Before writing any test, run these discovery commands: 118 | npx aegis query [tool_name] '[success_params]' --config "config.json" 119 | npx aegis query [tool_name] '[failure_params]' --config "config.json" 120 | 121 | # Then run your tests based on discovered formats 122 | npx aegis "tests/**/*.test.mcp.yml" --config "config.json" 123 | npx aegis "tests/*.yml" --config "config.json" --verbose --filter "tools" 124 | ``` 125 | 126 | ## Parameter Formats: Pipe vs JSON 127 | 128 | ### Pipe Format (Recommended for CLI) 129 | ```bash 130 | # Simple: No parameters 131 | npx aegis query get_code_versions --config "config.json" 132 | 133 | # Simple: key:value|other:123 134 | npx aegis query read_file 'path:test.txt' --config "config.json" 135 | 136 | # Nested via dot notation: config.host:localhost|config.port:8080 137 | npx aegis query api_client 'config.host:localhost|config.port:8080|timeout:30' --config "config.json" 138 | 139 | # Method syntax: name:tool|arguments.key:value 140 | npx aegis query --method tools/call --params 'name:read_file|arguments.path:test.txt' --config "config.json" 141 | ``` 142 | 143 | ### JSON Format (Complex Structures) 144 | ```bash 145 | npx aegis query complex_tool '{"config": {"host": "localhost"}, "data": [1,2,3]}' --config "config.json" 146 | ``` 147 | 148 | **Note**: For tools with no parameters, omit arguments entirely rather than using `'{}'` - use `npx aegis query tool_name --config "config.json"` instead of `npx aegis query tool_name '{}' --config "config.json"`. 149 | 150 | ## Complete Pattern Matching Reference (35+) 151 | 152 | ### Core Patterns 153 | ```yaml 154 | # 1. DEEP EQUALITY (default) - Exact match 155 | result: {tools: [{name: "read_file", description: "Reads a file"}]} 156 | 157 | # 2. TYPE VALIDATION 158 | result: 159 | tools: "match:type:array" 160 | count: "match:type:number" 161 | name: "match:type:string" 162 | 163 | # 3. STRING PATTERNS 164 | result: 165 | text: "match:contains:substring" 166 | name: "match:startsWith:prefix" 167 | file: "match:endsWith:.txt" 168 | pattern: "match:regex:\\d{4}-\\d{2}-\\d{2}" # YAML: escape backslashes 169 | 170 | # String length validation 171 | title: "match:stringLength:10" # Exactly 10 characters 172 | description: "match:stringLengthGreaterThan:5" # More than 5 chars 173 | summary: "match:stringLengthLessThan:100" # Less than 100 chars 174 | content: "match:stringLengthBetween:10:200" # Between 10-200 chars 175 | error: "match:stringEmpty" # Must be empty 176 | text: "match:stringNotEmpty" # Must not be empty 177 | 178 | # 4. ARRAY PATTERNS 179 | result: 180 | tools: "match:arrayLength:3" 181 | data: "match:arrayContains:value" 182 | tools: "match:arrayContains:name:read_file" # Object field matching 183 | tools: 184 | match:arrayElements: # All elements must match 185 | name: "match:type:string" 186 | description: "match:contains:tool" 187 | 188 | # 5. FIELD EXTRACTION (dot notation) 189 | result: 190 | match:extractField: "tools.*.name" # Extract all tool names 191 | value: ["read_file", "write_file"] 192 | 193 | # 6. NUMERIC COMPARISONS 194 | result: 195 | count: "match:greaterThan:5" 196 | price: "match:lessThanOrEqual:100.50" 197 | amount: "match:greaterThanOrEqual:0" 198 | score: "match:between:0:100" 199 | exact: "match:equals:42" 200 | not_equal: "match:notEquals:0" 201 | approximate: "match:approximately:3.14159:0.001" # tolerance 202 | decimal: "match:decimalPlaces:2" # exactly 2 decimal places 203 | multiple: "match:multipleOf:5" # divisible by 5 204 | 205 | # 7. DATE/TIMESTAMP PATTERNS 206 | result: 207 | createdAt: "match:dateValid" 208 | publishDate: "match:dateAfter:2023-01-01" 209 | expireDate: "match:dateBefore:2025-01-01" 210 | eventDate: "match:dateBetween:2023-01-01:2024-12-31" 211 | lastUpdate: "match:dateAge:1d" # within last day 212 | 213 | # 8. CROSS-FIELD VALIDATION 214 | result: 215 | "match:crossField": "price > minPrice" # Field comparison 216 | "match:crossField": "endDate >= startDate" 217 | 218 | # 9. PATTERN NEGATION (prefix with "not:") 219 | result: 220 | tools: "match:not:arrayLength:0" # NOT empty 221 | text: "match:not:contains:error" # NOT containing error 222 | 223 | # 10. PARTIAL MATCHING 224 | result: 225 | match:partial: # Only check specified fields 226 | tools: 227 | - name: "read_file" 228 | description: "match:contains:Reads" 229 | 230 | # 11. COMBINED PATTERNS - arrayElements + partial (POWERFUL!) 231 | result: 232 | tools: 233 | match:arrayElements: # Apply to ALL array elements 234 | match:partial: # But only validate specified fields 235 | name: "match:regex:^[a-z_]+$" 236 | description: "match:contains:tool" 237 | # Ignores any other fields like inputSchema, etc. 238 | 239 | # 12. ADVANCED PATTERNS 240 | result: 241 | email: "match:regex:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}" 242 | name: "match:equalsIgnoreCase:Hello World" 243 | text: "match:containsIgnoreCase:ERROR" 244 | value: "match:range:0:100" # Between 0-100 inclusive 245 | score: "match:between:60:90" # Between 60-90 exclusive 246 | match:extractField: "tools.*.inputSchema.properties.*.type" 247 | value: ["string", "number", "object"] 248 | ``` 249 | 250 | ## YAML Syntax Rules & Real-World Examples 251 | 252 | ### ❌ Common Errors to Avoid 253 | ```yaml 254 | # WRONG - Duplicate YAML keys (overwrites previous) 255 | result: 256 | tools: "match:arrayLength:1" 257 | tools: ["read_file"] # OVERWRITES above line! 258 | 259 | # WRONG - Invalid escaping in regex 260 | result: 261 | text: "match:regex:\d+" # Missing double backslash 262 | 263 | # WRONG - Mixing patterns in same object 264 | result: 265 | tools: "match:arrayLength:1" 266 | match:extractField: "tools.*.name" # Can't mix in same object 267 | ``` 268 | 269 | ### ✅ Correct Best Practices 270 | ```yaml 271 | # CORRECT - Separate pattern validations into different tests 272 | - it: "should have exactly one tool" 273 | expect: 274 | response: 275 | result: 276 | tools: "match:arrayLength:1" 277 | 278 | - it: "should extract correct tool name" 279 | expect: 280 | response: 281 | result: 282 | match:extractField: "tools.*.name" 283 | value: ["read_file"] 284 | 285 | # CORRECT - Proper regex escaping in YAML 286 | result: 287 | text: "match:regex:\\d{4}-\\d{2}-\\d{2}" # Double backslashes 288 | 289 | # CORRECT - Flexible arrayElements with partial matching 290 | result: 291 | tools: 292 | match:arrayElements: 293 | match:partial: # 🔥 Only validate what matters, ignore extra fields 294 | name: "match:regex:^[a-z][a-z0-9_]*$" 295 | description: "match:regex:.{10,}" 296 | ``` 297 | 298 | ### Real-World Test Examples 299 | 300 | #### Tool Discovery Test 301 | ```yaml 302 | - it: "should list available tools with correct structure" 303 | request: 304 | jsonrpc: "2.0" 305 | id: "list-1" 306 | method: "tools/list" 307 | params: {} 308 | expect: 309 | response: 310 | jsonrpc: "2.0" 311 | id: "list-1" 312 | result: 313 | tools: 314 | match:arrayElements: 315 | match:partial: # Flexible - recommended approach 316 | name: "match:regex:^[a-z][a-z0-9_]*$" # snake_case 317 | description: "match:regex:.{10,}" # min 10 chars 318 | ``` 319 | 320 | #### Tool Execution Test 321 | ```yaml 322 | - it: "should execute tool successfully" 323 | request: 324 | jsonrpc: "2.0" 325 | id: "exec-1" 326 | method: "tools/call" 327 | params: 328 | name: "read_file" 329 | arguments: 330 | path: "test.txt" 331 | expect: 332 | response: 333 | jsonrpc: "2.0" 334 | id: "exec-1" 335 | result: 336 | content: 337 | - type: "text" 338 | text: "match:contains:expected content" 339 | isError: false 340 | stderr: "toBeEmpty" 341 | ``` 342 | 343 | #### Error Handling Test 344 | ```yaml 345 | - it: "should handle invalid file gracefully" 346 | request: 347 | jsonrpc: "2.0" 348 | id: "error-1" 349 | method: "tools/call" 350 | params: 351 | name: "read_file" 352 | arguments: 353 | path: "nonexistent.txt" 354 | expect: 355 | response: 356 | jsonrpc: "2.0" 357 | id: "error-1" 358 | result: 359 | content: 360 | - type: "text" 361 | text: "match:contains:not found" 362 | isError: true 363 | ``` 364 | 365 | #### Performance Testing with SLA Validation 366 | ```yaml 367 | - it: "should meet performance SLA" 368 | request: 369 | jsonrpc: "2.0" 370 | id: "perf-1" 371 | method: "tools/call" 372 | params: 373 | name: "search_tools" 374 | arguments: 375 | category: "documentation" 376 | expect: 377 | response: 378 | result: 379 | tools: 380 | match:arrayElements: 381 | match:partial: 382 | name: "match:regex:^[a-z][a-z0-9_]*$" 383 | description: "match:regex:.{10,}" 384 | count: "match:type:number" 385 | performance: 386 | maxResponseTime: "1500ms" # SLA requirement 387 | stderr: "toBeEmpty" 388 | ``` 389 | 390 | ## CLI Commands & Interactive Testing 391 | 392 | ### Test Execution 393 | ```bash 394 | # Basic testing 395 | npx aegis "tests/**/*.test.mcp.yml" --config "config.json" 396 | 397 | # Debug modes & filtering 398 | npx aegis "tests/*.yml" --config "config.json" --verbose --debug --timing 399 | npx aegis "tests/*.yml" --config "config.json" --errors-only --filter "tools" 400 | npx aegis "tests/*.yml" --config "config.json" --filter "should validate" --json 401 | ``` 402 | 403 | ### Interactive Tool Testing (Dual Format Support) 404 | ```bash 405 | # List all tools 406 | npx aegis query --config "config.json" 407 | 408 | # Pipe format (recommended) - ALWAYS test success AND failure 409 | npx aegis query read_file 'path:test.txt' --config "config.json" # Success case 410 | npx aegis query read_file 'path:nonexistent.txt' --config "config.json" # Failure case 411 | 412 | # JSON format for complex structures 413 | npx aegis query complex_tool '{"config": {"host": "localhost"}, "data": [1,2,3]}' --config "config.json" 414 | 415 | # Discovery examples for edge cases 416 | npx aegis query search_tool 'query:' --config "config.json" # Empty input 417 | npx aegis query search_tool 'query:zzznothingfound' --config "config.json" # No results 418 | ``` 419 | 420 | ### Performance Testing Guidelines 421 | | Operation Type | Recommended Timeout | Use Case | 422 | |----------------|-------------------|----------| 423 | | Tool Listing | `200-500ms` | Metadata operations | 424 | | Simple File Ops | `1000ms` | Basic I/O | 425 | | Complex Operations | `2000ms` | Search, computation | 426 | | Error Responses | `800ms` | Should be faster than success | 427 | | Heavy Operations | `5000ms` | Database, large files | 428 | 429 | **Always use `--timing` flag** to see actual response times and adjust expectations. 430 | 431 | ## Pattern Selection Guide & Advanced Combinations 432 | 433 | ### When to Use Each Pattern 434 | - **Deep Equality**: Exact value matching (default) 435 | - **Type Validation**: Verify data types (`match:type:`) 436 | - **String Patterns**: Text validation (`contains`, `startsWith`, `endsWith`, `regex`, `stringLength`) 437 | - **Array Patterns**: Array validation (`arrayLength`, `arrayContains`, `arrayElements`) 438 | - **Field Extraction**: Extract nested values (`match:extractField`) 439 | - **Numeric**: Math comparisons (`greaterThan`, `approximately`, `decimalPlaces`) 440 | - **Date/Time**: Date validation (`dateValid`, `dateAfter`, `dateAge`) 441 | - **Cross-Field**: Compare fields (`match:crossField`) 442 | - **Negation**: Exclude patterns (`match:not:*`) 443 | - **Partial**: Subset validation (`match:partial`) 444 | - **🔥 Combined arrayElements + partial**: Validate specific fields across ALL array elements while ignoring others - extremely powerful for flexible schema validation! 445 | 446 | ### Multi-Step Validation Pattern (Recommended) 447 | ```yaml 448 | # Test 1: Basic structure 449 | - it: "should return array of tools" 450 | expect: 451 | response: 452 | result: 453 | tools: "match:type:array" 454 | 455 | # Test 2: Array length 456 | - it: "should have expected number of tools" 457 | expect: 458 | response: 459 | result: 460 | tools: "match:arrayLength:3" 461 | 462 | # Test 3: Extract and validate specific fields 463 | - it: "should have correct tool names" 464 | expect: 465 | response: 466 | result: 467 | match:extractField: "tools.*.name" 468 | value: ["read_file", "write_file", "list_files"] 469 | ``` 470 | 471 | ### Comprehensive Tool Validation (Best Practice) 472 | ```yaml 473 | - it: "should validate tools with flexible schema handling" 474 | expect: 475 | response: 476 | result: 477 | tools: 478 | match:arrayElements: 479 | match:partial: # 🔥 RECOMMENDED: Combines power with flexibility 480 | name: "match:regex:^[a-z][a-z0-9_]*$" # snake_case names 481 | description: "match:regex:.{10,200}" # 10-200 chars 482 | inputSchema: 483 | type: "object" 484 | properties: "match:type:object" 485 | ``` 486 | 487 | ## MCP Protocol Basics 488 | 489 | ### Standard JSON-RPC 2.0 Methods 490 | 491 | #### Initialize Request (Required for handshake) 492 | ```yaml 493 | request: 494 | jsonrpc: "2.0" 495 | id: "init-1" 496 | method: "initialize" 497 | params: 498 | protocolVersion: "2025-06-18" 499 | capabilities: {"tools": {}} 500 | clientInfo: {"name": "MCP Aegis", "version": "1.0.0"} 501 | ``` 502 | 503 | #### Tools List Request 504 | ```yaml 505 | request: 506 | jsonrpc: "2.0" 507 | id: "list-1" 508 | method: "tools/list" 509 | params: {} 510 | ``` 511 | 512 | #### Tool Call Request 513 | ```yaml 514 | request: 515 | jsonrpc: "2.0" 516 | id: "call-1" 517 | method: "tools/call" 518 | params: 519 | name: "tool_name" 520 | arguments: 521 | key: "value" 522 | ``` 523 | 524 | ### Standard Response Structure 525 | ```yaml 526 | expect: 527 | response: 528 | jsonrpc: "2.0" 529 | id: "matching-request-id" 530 | result: # For successful responses 531 | # Response data 532 | # OR for errors: 533 | error: 534 | code: -32601 # Standard JSON-RPC error codes 535 | message: "Method not found" 536 | stderr: "toBeEmpty" # Optional stderr validation 537 | ``` 538 | 539 | ## MCP Protocol Basics & Troubleshooting 540 | 541 | ### Standard JSON-RPC 2.0 Methods 542 | ```yaml 543 | # Initialize Request (Required for handshake) 544 | request: 545 | jsonrpc: "2.0" 546 | id: "init-1" 547 | method: "initialize" 548 | params: 549 | protocolVersion: "2025-06-18" 550 | capabilities: {"tools": {}} 551 | clientInfo: {"name": "MCP Aegis", "version": "1.0.0"} 552 | 553 | # Tools List Request 554 | request: 555 | jsonrpc: "2.0" 556 | id: "list-1" 557 | method: "tools/list" 558 | params: {} 559 | 560 | # Tool Call Request 561 | request: 562 | jsonrpc: "2.0" 563 | id: "call-1" 564 | method: "tools/call" 565 | params: 566 | name: "tool_name" 567 | arguments: 568 | key: "value" 569 | 570 | # Standard Response Structure 571 | expect: 572 | response: 573 | jsonrpc: "2.0" 574 | id: "matching-request-id" 575 | result: {} # For successful responses 576 | # OR for errors: 577 | error: 578 | code: -32601 # Standard JSON-RPC error codes 579 | message: "Method not found" 580 | stderr: "toBeEmpty" # Optional stderr validation 581 | ``` 582 | 583 | ## Quick Debugging Workflow 584 | 585 | ### 🥇 Discovery-First Development (Essential) 586 | 587 | **Before ANY test writing, run discovery commands:** 588 | ```bash 589 | # Success case 590 | npx aegis query [tool_name] '[valid_params]' --config "config.json" 591 | # Failure case 592 | npx aegis query [tool_name] '[invalid_params]' --config "config.json" 593 | ``` 594 | 595 | ### Common Issues & Solutions 596 | 597 | #### Server Won't Start 598 | ```bash 599 | npx aegis "test.yml" --config "config.json" --debug # Check startup issues 600 | # Increase startupTimeout if server is slow 601 | # Verify command/args in config are correct 602 | ``` 603 | 604 | #### Pattern Not Matching 605 | ```yaml 606 | # ❌ WRONG - Assuming structure without discovery 607 | - it: "should return tools object" 608 | expect: 609 | response: 610 | result: 611 | tools: "match:arrayLength:3" # Assumes 'tools' field exists 612 | count: "match:type:number" # Assumes 'count' field exists 613 | 614 | # ✅ CORRECT - Based on actual npx aegis query discovery 615 | # First run: npx aegis query list_tools --config "config.json" 616 | # Result: ["tool1", "tool2", "tool3"] # Simple array, no wrapper object! 617 | - it: "should return tools array" 618 | expect: 619 | response: 620 | result: 621 | text: "match:regex:\\[[\\s\\S]*\\]" # Match JSON array format 622 | text: "match:contains:tool1" # Check content exists 623 | ``` 624 | 625 | #### Regex Patterns Failing 626 | ```yaml 627 | # YAML requires double escaping backslashes 628 | text: "match:regex:\\d+" # ✅ Correct 629 | text: "match:regex:\d+" # ❌ Wrong 630 | ``` 631 | 632 | #### Array Pattern Issues 633 | ```yaml 634 | # ❌ Duplicate YAML keys (overwrites previous) 635 | result: 636 | tools: "match:arrayLength:1" 637 | tools: ["exact_tool"] # OVERWRITES above! 638 | 639 | # ✅ Use flexible arrayElements with partial matching instead 640 | result: 641 | tools: 642 | match:arrayElements: 643 | match:partial: # Only validate what you care about 644 | name: "match:type:string" 645 | description: "match:type:string" 646 | ``` 647 | 648 | ### 2. Test Filtering for Focus Development 649 | ```bash 650 | # Filter by suite/test description (case-insensitive) 651 | npx aegis "tests/**/*.yml" --config "config.json" --filter "Tools validation" 652 | npx aegis "tests/**/*.yml" --config "config.json" --filter "should handle errors" 653 | 654 | # Use regex patterns for advanced filtering 655 | npx aegis "tests/**/*.yml" --config "config.json" --filter "/should (read|write|validate)/" 656 | 657 | # Combine with debugging options 658 | npx aegis "tests/**/*.yml" --config "config.json" --filter "tools" --errors-only --timing 659 | ``` 660 | 661 | ### 3. Pipe Format Quick Reference 662 | ```bash 663 | # Basic syntax: key:value|other:123 664 | 'path:test.txt|encoding:utf8' 665 | 666 | # Nested objects: config.host:localhost|config.port:8080 667 | 'database.host:localhost|database.port:5432|cache.enabled:true' 668 | 669 | # Auto data types: text:hello|count:42|enabled:true|data:null 670 | 'operation:add|a:5|b:3|precise:true' 671 | 672 | # JSON values within pipe: simple:value|complex:{"nested":"object"} 673 | 'metadata:{"version":"1.0"}|tags:["test","demo"]|count:5' 674 | ``` 675 | 676 | ### 3. Pipe Format Quick Reference 677 | ```bash 678 | # Basic: key:value|other:123 679 | 'path:test.txt|encoding:utf8' 680 | 681 | # Discovery examples: 682 | 'query:catalog|limit:10' # Success case 683 | 'query:zzznothingfound' # No results 684 | 'query:' # Validation error 685 | 'fileName:nonexistent.txt' # File not found 686 | ``` 687 | 688 | ### 4. Performance Testing Guidelines 689 | - **Tool Listing**: 200-500ms (metadata - should be fast) 690 | - **Simple File Ops**: 1000ms (basic I/O operations) 691 | - **Complex Operations**: 2000ms (search, computation, API calls) 692 | - **Error Responses**: 800ms (often faster than successful operations) 693 | - **Heavy Operations**: 5000ms (database queries, large file processing) 694 | 695 | Use `--timing` flag to see actual response times and adjust expectations based on your environment. 696 | 697 | ## Complete Pattern Reference (35+ Patterns) 698 | 699 | ```yaml 700 | # STRING PATTERNS 701 | "match:contains:substring" 702 | "match:startsWith:prefix" 703 | "match:endsWith:suffix" 704 | "match:containsIgnoreCase:TEXT" 705 | "match:equalsIgnoreCase:value" 706 | "match:regex:pattern" 707 | "match:stringLength:10" 708 | "match:stringLengthGreaterThan:5" 709 | "match:stringLengthLessThan:100" 710 | "match:stringLengthBetween:10:200" 711 | "match:stringEmpty|stringNotEmpty" 712 | 713 | # TYPE & STRUCTURE 714 | "match:type:string|number|boolean|object|array" 715 | "match:exists" 716 | "match:length:5" 717 | 718 | # NUMERIC PATTERNS 719 | "match:greaterThan:10" 720 | "match:lessThanOrEqual:50" 721 | "match:between:10:90" 722 | "match:equals:42" 723 | "match:notEquals:0" 724 | "match:approximately:3.14:0.01" 725 | "match:multipleOf:5" 726 | "match:decimalPlaces:2" 727 | 728 | # ARRAY PATTERNS 729 | "match:arrayLength:3" 730 | "match:arrayContains:value" 731 | match:arrayElements: 732 | field: "match:type:string" 733 | 734 | # DATE PATTERNS 735 | "match:dateValid" 736 | "match:dateAfter:2023-01-01" 737 | "match:dateBetween:2023-01-01:2024-12-31" 738 | "match:dateAge:1d" 739 | 740 | # ADVANCED PATTERNS 741 | "match:crossField": "field1 > field2" 742 | "match:not:pattern" # Negate any pattern 743 | match:extractField: "path.*.field" 744 | match:partial: # Check subset of fields 745 | match:arrayElements: # Validate ALL array elements 746 | match:partial: # 🔥 POWERFUL COMBO 747 | field: "pattern" 748 | ``` 749 | 750 | ### Installation & Getting Started 751 | ```bash 752 | npm install -g mcp-aegis 753 | npx aegis init # Create sample config and tests 754 | npx aegis "tests/*.yml" --config "config.json" 755 | ``` 756 | ``` -------------------------------------------------------------------------------- /docs/dw_customer/ProductList.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.customer 2 | 3 | # Class ProductList 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.object.PersistentObject 9 | - dw.object.ExtensibleObject 10 | - dw.customer.ProductList 11 | 12 | ## Description 13 | 14 | Represents a list of products (and optionally a gift certificate) that is typically maintained by a customer. This class can be used to implement a number of different storefront features, e.g. shopping list, wish list and gift registry. A product list is always owned by a customer. The owner can be anonymous or a registered customer. The owner can be the person for which items from that list will be purchased (wish list). Or it can be a person who maintains the list, for example a gift registry, on behalf of the bridal couple. Each product list can have a registrant and a co-registrant. A registrant is typically associated with an event related product list such as a gift registry. It holds information about a person associated with the event such as a bride or groom. A shipping address can be associated with this product list to ship the items, e.g. to an event location. A post-event shipping address can be associated to ship items to which could not be delivered on event date. The product list can also hold information about the event date and event location. 15 | 16 | ## Constants 17 | 18 | ### EXPORT_STATUS_EXPORTED 19 | 20 | **Type:** Number = 1 21 | 22 | Constant for when Export Status is Exported 23 | 24 | ### EXPORT_STATUS_NOTEXPORTED 25 | 26 | **Type:** Number = 0 27 | 28 | Constant for when Export Status is Not Exported 29 | 30 | ### TYPE_CUSTOM_1 31 | 32 | **Type:** Number = 100 33 | 34 | Constant representing a custom list type attribute. 35 | 36 | ### TYPE_CUSTOM_2 37 | 38 | **Type:** Number = 101 39 | 40 | Constant representing a custom list type attribute. 41 | 42 | ### TYPE_CUSTOM_3 43 | 44 | **Type:** Number = 102 45 | 46 | Constant representing a custom list type attribute. 47 | 48 | ### TYPE_GIFT_REGISTRY 49 | 50 | **Type:** Number = 11 51 | 52 | Constant representing the gift registry type attribute. 53 | 54 | ### TYPE_SHOPPING_LIST 55 | 56 | **Type:** Number = 12 57 | 58 | Constant representing the shopping list type attribute. 59 | 60 | ### TYPE_WISH_LIST 61 | 62 | **Type:** Number = 10 63 | 64 | Constant representing the wish list registry type attribute. 65 | 66 | ## Properties 67 | 68 | ### anonymous 69 | 70 | **Type:** boolean (Read Only) 71 | 72 | Returns true if this product list is owned by an anonymous customer. 73 | 74 | ### coRegistrant 75 | 76 | **Type:** ProductListRegistrant (Read Only) 77 | 78 | The ProductListRegistrant assigned to the coRegistrant attribute or null 79 | if this list has no co-registrant. 80 | 81 | ### currentShippingAddress 82 | 83 | **Type:** CustomerAddress (Read Only) 84 | 85 | This is a helper method typically used with an event related list. 86 | It provides the appropriate shipping address based on the eventDate. 87 | If the current date is after the eventDate, then the postEventShippingAddress 88 | is returned, otherwise the shippingAddress is returned. If the eventDate 89 | is null, then null is returned. 90 | 91 | ### description 92 | 93 | **Type:** String 94 | 95 | A description text that, for example, explains the purpose of this product list. 96 | 97 | ### eventCity 98 | 99 | **Type:** String 100 | 101 | For event related uses (e.g. gift registry), this holds the event city. 102 | 103 | ### eventCountry 104 | 105 | **Type:** String 106 | 107 | For event related uses (e.g. gift registry), this holds the event country. 108 | 109 | ### eventDate 110 | 111 | **Type:** Date 112 | 113 | For event related uses (e.g. gift registry), this holds the date 114 | of the event. 115 | 116 | ### eventState 117 | 118 | **Type:** String 119 | 120 | For event related uses (e.g. gift registry), this holds the event state. 121 | 122 | ### eventType 123 | 124 | **Type:** String 125 | 126 | For event related uses (e.g. gift registry), this holds the type 127 | of event, e.g. Wedding, Baby Shower. 128 | 129 | ### exportStatus 130 | 131 | **Type:** EnumValue (Read Only) 132 | 133 | The export status of the product list. 134 | Possible values are: EXPORT_STATUS_NOTEXPORTED, 135 | EXPORT_STATUS_EXPORTED. 136 | 137 | ### giftCertificateItem 138 | 139 | **Type:** ProductListItem (Read Only) 140 | 141 | The item in the list that represents a gift certificate. 142 | 143 | ### ID 144 | 145 | **Type:** String (Read Only) 146 | 147 | The unique system generated ID of the object. 148 | 149 | ### items 150 | 151 | **Type:** Collection (Read Only) 152 | 153 | A collection containing all items in the list. 154 | 155 | ### lastExportTime 156 | 157 | **Type:** Date (Read Only) 158 | 159 | The date where this product list has been exported successfully 160 | the last time. 161 | 162 | ### name 163 | 164 | **Type:** String 165 | 166 | The name of this product list given by its owner. 167 | 168 | ### owner 169 | 170 | **Type:** Customer (Read Only) 171 | 172 | The customer that created and owns the product list. 173 | 174 | ### postEventShippingAddress 175 | 176 | **Type:** CustomerAddress 177 | 178 | The shipping address for purchases made after the event date. 179 | 180 | ### productItems 181 | 182 | **Type:** Collection (Read Only) 183 | 184 | A collection containing all items in the list that reference products. 185 | 186 | ### public 187 | 188 | **Type:** boolean 189 | 190 | A flag, typically used to determine if the object is searchable 191 | by other customers. 192 | 193 | ### publicItems 194 | 195 | **Type:** Collection (Read Only) 196 | 197 | A collection containing all items in the list that are flagged as public. 198 | 199 | ### purchases 200 | 201 | **Type:** Collection (Read Only) 202 | 203 | The aggregated purchases from all the individual items. 204 | 205 | ### registrant 206 | 207 | **Type:** ProductListRegistrant (Read Only) 208 | 209 | The ProductListRegistrant assigned to the registrant attribute or null 210 | if this list has no registrant. 211 | 212 | ### shippingAddress 213 | 214 | **Type:** CustomerAddress 215 | 216 | Return the address that should be used as the shipping address for purchases 217 | made from the list. 218 | 219 | ### type 220 | 221 | **Type:** Number (Read Only) 222 | 223 | An int representing the type of object (e.g. wish list, 224 | gift registry). This is set at object creation time. 225 | 226 | ## Constructor Summary 227 | 228 | ## Method Summary 229 | 230 | ### createCoRegistrant 231 | 232 | **Signature:** `createCoRegistrant() : ProductListRegistrant` 233 | 234 | Create a ProductListRegistrant and assign it to the coRegistrant attribute of the list. 235 | 236 | ### createGiftCertificateItem 237 | 238 | **Signature:** `createGiftCertificateItem() : ProductListItem` 239 | 240 | Create an item in the list that represents a gift certificate. 241 | 242 | ### createProductItem 243 | 244 | **Signature:** `createProductItem(product : Product) : ProductListItem` 245 | 246 | Create an item in the list that references the specified product. 247 | 248 | ### createRegistrant 249 | 250 | **Signature:** `createRegistrant() : ProductListRegistrant` 251 | 252 | Create a ProductListRegistrant and assign it to the registrant attribute of the list. 253 | 254 | ### getCoRegistrant 255 | 256 | **Signature:** `getCoRegistrant() : ProductListRegistrant` 257 | 258 | Returns the ProductListRegistrant assigned to the coRegistrant attribute or null if this list has no co-registrant. 259 | 260 | ### getCurrentShippingAddress 261 | 262 | **Signature:** `getCurrentShippingAddress() : CustomerAddress` 263 | 264 | This is a helper method typically used with an event related list. 265 | 266 | ### getDescription 267 | 268 | **Signature:** `getDescription() : String` 269 | 270 | Returns a description text that, for example, explains the purpose of this product list. 271 | 272 | ### getEventCity 273 | 274 | **Signature:** `getEventCity() : String` 275 | 276 | For event related uses (e.g. 277 | 278 | ### getEventCountry 279 | 280 | **Signature:** `getEventCountry() : String` 281 | 282 | For event related uses (e.g. 283 | 284 | ### getEventDate 285 | 286 | **Signature:** `getEventDate() : Date` 287 | 288 | For event related uses (e.g. 289 | 290 | ### getEventState 291 | 292 | **Signature:** `getEventState() : String` 293 | 294 | For event related uses (e.g. 295 | 296 | ### getEventType 297 | 298 | **Signature:** `getEventType() : String` 299 | 300 | For event related uses (e.g. 301 | 302 | ### getExportStatus 303 | 304 | **Signature:** `getExportStatus() : EnumValue` 305 | 306 | Returns the export status of the product list. Possible values are: EXPORT_STATUS_NOTEXPORTED, EXPORT_STATUS_EXPORTED. 307 | 308 | ### getGiftCertificateItem 309 | 310 | **Signature:** `getGiftCertificateItem() : ProductListItem` 311 | 312 | Returns the item in the list that represents a gift certificate. 313 | 314 | ### getID 315 | 316 | **Signature:** `getID() : String` 317 | 318 | Returns the unique system generated ID of the object. 319 | 320 | ### getItem 321 | 322 | **Signature:** `getItem(ID : String) : ProductListItem` 323 | 324 | Returns the item from the list that has the specified ID. 325 | 326 | ### getItems 327 | 328 | **Signature:** `getItems() : Collection` 329 | 330 | Returns a collection containing all items in the list. 331 | 332 | ### getLastExportTime 333 | 334 | **Signature:** `getLastExportTime() : Date` 335 | 336 | Returns the date where this product list has been exported successfully the last time. 337 | 338 | ### getName 339 | 340 | **Signature:** `getName() : String` 341 | 342 | Returns the name of this product list given by its owner. 343 | 344 | ### getOwner 345 | 346 | **Signature:** `getOwner() : Customer` 347 | 348 | Returns the customer that created and owns the product list. 349 | 350 | ### getPostEventShippingAddress 351 | 352 | **Signature:** `getPostEventShippingAddress() : CustomerAddress` 353 | 354 | Returns the shipping address for purchases made after the event date. 355 | 356 | ### getProductItems 357 | 358 | **Signature:** `getProductItems() : Collection` 359 | 360 | Returns a collection containing all items in the list that reference products. 361 | 362 | ### getPublicItems 363 | 364 | **Signature:** `getPublicItems() : Collection` 365 | 366 | Returns a collection containing all items in the list that are flagged as public. 367 | 368 | ### getPurchases 369 | 370 | **Signature:** `getPurchases() : Collection` 371 | 372 | Returns the aggregated purchases from all the individual items. 373 | 374 | ### getRegistrant 375 | 376 | **Signature:** `getRegistrant() : ProductListRegistrant` 377 | 378 | Returns the ProductListRegistrant assigned to the registrant attribute or null if this list has no registrant. 379 | 380 | ### getShippingAddress 381 | 382 | **Signature:** `getShippingAddress() : CustomerAddress` 383 | 384 | Return the address that should be used as the shipping address for purchases made from the list. 385 | 386 | ### getType 387 | 388 | **Signature:** `getType() : Number` 389 | 390 | Returns an int representing the type of object (e.g. 391 | 392 | ### isAnonymous 393 | 394 | **Signature:** `isAnonymous() : boolean` 395 | 396 | Returns true if this product list is owned by an anonymous customer. 397 | 398 | ### isPublic 399 | 400 | **Signature:** `isPublic() : boolean` 401 | 402 | A flag, typically used to determine if the object is searchable by other customers. 403 | 404 | ### removeCoRegistrant 405 | 406 | **Signature:** `removeCoRegistrant() : void` 407 | 408 | Removes the ProductListRegistrant assigned to the coRegistrant attribute. 409 | 410 | ### removeItem 411 | 412 | **Signature:** `removeItem(item : ProductListItem) : void` 413 | 414 | Removes the specified item from the list. 415 | 416 | ### removeRegistrant 417 | 418 | **Signature:** `removeRegistrant() : void` 419 | 420 | Removes the ProductListRegistrant assigned to the registrant attribute. 421 | 422 | ### setDescription 423 | 424 | **Signature:** `setDescription(description : String) : void` 425 | 426 | Set the description of this product list. 427 | 428 | ### setEventCity 429 | 430 | **Signature:** `setEventCity(eventCity : String) : void` 431 | 432 | Set the event city to which this product list is related. 433 | 434 | ### setEventCountry 435 | 436 | **Signature:** `setEventCountry(eventCountry : String) : void` 437 | 438 | Set the event country to which this product list is related. 439 | 440 | ### setEventDate 441 | 442 | **Signature:** `setEventDate(eventDate : Date) : void` 443 | 444 | Set the date of the event to which this product list is related. 445 | 446 | ### setEventState 447 | 448 | **Signature:** `setEventState(eventState : String) : void` 449 | 450 | Set the event state to which this product list is related. 451 | 452 | ### setEventType 453 | 454 | **Signature:** `setEventType(eventType : String) : void` 455 | 456 | Set the event type for which this product list was created by the owner. 457 | 458 | ### setName 459 | 460 | **Signature:** `setName(name : String) : void` 461 | 462 | Set the name of this product list. 463 | 464 | ### setPostEventShippingAddress 465 | 466 | **Signature:** `setPostEventShippingAddress(address : CustomerAddress) : void` 467 | 468 | This is typically used by an event related list (e.g. 469 | 470 | ### setPublic 471 | 472 | **Signature:** `setPublic(flag : boolean) : void` 473 | 474 | Makes this product list visible to other customers or hides it. 475 | 476 | ### setShippingAddress 477 | 478 | **Signature:** `setShippingAddress(address : CustomerAddress) : void` 479 | 480 | Associate an address, used as the shipping address for purchases made from the list. 481 | 482 | ## Method Detail 483 | 484 | ## Method Details 485 | 486 | ### createCoRegistrant 487 | 488 | **Signature:** `createCoRegistrant() : ProductListRegistrant` 489 | 490 | **Description:** Create a ProductListRegistrant and assign it to the coRegistrant attribute of the list. An exception is thrown if the list already has a coRegistrant assigned to it. 491 | 492 | **Returns:** 493 | 494 | the created ProductListRegistrant instance. 495 | 496 | **Throws:** 497 | 498 | CreateException - if one already exists 499 | 500 | --- 501 | 502 | ### createGiftCertificateItem 503 | 504 | **Signature:** `createGiftCertificateItem() : ProductListItem` 505 | 506 | **Description:** Create an item in the list that represents a gift certificate. A list may only contain a single gift certificate, so an exception is thrown if one already exists in the list. 507 | 508 | **Returns:** 509 | 510 | the created item. 511 | 512 | **Throws:** 513 | 514 | CreateException - if a gift certificate item already exists in the list. 515 | 516 | --- 517 | 518 | ### createProductItem 519 | 520 | **Signature:** `createProductItem(product : Product) : ProductListItem` 521 | 522 | **Description:** Create an item in the list that references the specified product. 523 | 524 | **Parameters:** 525 | 526 | - `product`: the product to use to create the list item. 527 | 528 | **Returns:** 529 | 530 | the created item. 531 | 532 | --- 533 | 534 | ### createRegistrant 535 | 536 | **Signature:** `createRegistrant() : ProductListRegistrant` 537 | 538 | **Description:** Create a ProductListRegistrant and assign it to the registrant attribute of the list. An exception is thrown if the list already has a registrant assigned to it. 539 | 540 | **Returns:** 541 | 542 | the created ProductListRegistrant instance. 543 | 544 | **Throws:** 545 | 546 | CreateException - if one already exists 547 | 548 | --- 549 | 550 | ### getCoRegistrant 551 | 552 | **Signature:** `getCoRegistrant() : ProductListRegistrant` 553 | 554 | **Description:** Returns the ProductListRegistrant assigned to the coRegistrant attribute or null if this list has no co-registrant. 555 | 556 | **Returns:** 557 | 558 | the ProductListRegistrant assigned to the coRegistrant attribute or null if this list has no co-registrant. 559 | 560 | --- 561 | 562 | ### getCurrentShippingAddress 563 | 564 | **Signature:** `getCurrentShippingAddress() : CustomerAddress` 565 | 566 | **Description:** This is a helper method typically used with an event related list. It provides the appropriate shipping address based on the eventDate. If the current date is after the eventDate, then the postEventShippingAddress is returned, otherwise the shippingAddress is returned. If the eventDate is null, then null is returned. 567 | 568 | **Returns:** 569 | 570 | the appropriate address, as described above. 571 | 572 | --- 573 | 574 | ### getDescription 575 | 576 | **Signature:** `getDescription() : String` 577 | 578 | **Description:** Returns a description text that, for example, explains the purpose of this product list. 579 | 580 | **Returns:** 581 | 582 | a description text explaining the purpose of this product list. Returns an empty string if the description is not set. 583 | 584 | --- 585 | 586 | ### getEventCity 587 | 588 | **Signature:** `getEventCity() : String` 589 | 590 | **Description:** For event related uses (e.g. gift registry), this holds the event city. 591 | 592 | **Returns:** 593 | 594 | the event city. The event city or an empty string if no event city is set. 595 | 596 | --- 597 | 598 | ### getEventCountry 599 | 600 | **Signature:** `getEventCountry() : String` 601 | 602 | **Description:** For event related uses (e.g. gift registry), this holds the event country. 603 | 604 | **Returns:** 605 | 606 | the event country. The event country or an empty string if no event country is set. 607 | 608 | --- 609 | 610 | ### getEventDate 611 | 612 | **Signature:** `getEventDate() : Date` 613 | 614 | **Description:** For event related uses (e.g. gift registry), this holds the date of the event. 615 | 616 | **Returns:** 617 | 618 | the date of the event. 619 | 620 | --- 621 | 622 | ### getEventState 623 | 624 | **Signature:** `getEventState() : String` 625 | 626 | **Description:** For event related uses (e.g. gift registry), this holds the event state. 627 | 628 | **Returns:** 629 | 630 | the event state. The event state or an empty string if no event state is set. 631 | 632 | --- 633 | 634 | ### getEventType 635 | 636 | **Signature:** `getEventType() : String` 637 | 638 | **Description:** For event related uses (e.g. gift registry), this holds the type of event, e.g. Wedding, Baby Shower. 639 | 640 | **Returns:** 641 | 642 | the type of event. Returns an empty string, if not set. 643 | 644 | --- 645 | 646 | ### getExportStatus 647 | 648 | **Signature:** `getExportStatus() : EnumValue` 649 | 650 | **Description:** Returns the export status of the product list. Possible values are: EXPORT_STATUS_NOTEXPORTED, EXPORT_STATUS_EXPORTED. 651 | 652 | **Returns:** 653 | 654 | Product list export status 655 | 656 | --- 657 | 658 | ### getGiftCertificateItem 659 | 660 | **Signature:** `getGiftCertificateItem() : ProductListItem` 661 | 662 | **Description:** Returns the item in the list that represents a gift certificate. 663 | 664 | **Returns:** 665 | 666 | the gift certificate item, or null if it doesn't exist. 667 | 668 | --- 669 | 670 | ### getID 671 | 672 | **Signature:** `getID() : String` 673 | 674 | **Description:** Returns the unique system generated ID of the object. 675 | 676 | **Returns:** 677 | 678 | the ID of object. 679 | 680 | --- 681 | 682 | ### getItem 683 | 684 | **Signature:** `getItem(ID : String) : ProductListItem` 685 | 686 | **Description:** Returns the item from the list that has the specified ID. 687 | 688 | **Parameters:** 689 | 690 | - `ID`: the product list item identifier. 691 | 692 | **Returns:** 693 | 694 | the specified item, or null if it's not found in the list. 695 | 696 | --- 697 | 698 | ### getItems 699 | 700 | **Signature:** `getItems() : Collection` 701 | 702 | **Description:** Returns a collection containing all items in the list. 703 | 704 | **Returns:** 705 | 706 | all items. 707 | 708 | --- 709 | 710 | ### getLastExportTime 711 | 712 | **Signature:** `getLastExportTime() : Date` 713 | 714 | **Description:** Returns the date where this product list has been exported successfully the last time. 715 | 716 | **Returns:** 717 | 718 | The time of the last successful export or null if this product list was not exported yet. 719 | 720 | --- 721 | 722 | ### getName 723 | 724 | **Signature:** `getName() : String` 725 | 726 | **Description:** Returns the name of this product list given by its owner. 727 | 728 | **Returns:** 729 | 730 | the name of this product list. Returns an empty string if the name is not set. 731 | 732 | --- 733 | 734 | ### getOwner 735 | 736 | **Signature:** `getOwner() : Customer` 737 | 738 | **Description:** Returns the customer that created and owns the product list. 739 | 740 | **Returns:** 741 | 742 | Owning customer 743 | 744 | --- 745 | 746 | ### getPostEventShippingAddress 747 | 748 | **Signature:** `getPostEventShippingAddress() : CustomerAddress` 749 | 750 | **Description:** Returns the shipping address for purchases made after the event date. 751 | 752 | **Returns:** 753 | 754 | the shipping address for purchases made after the event date. Returns null if no post-event shipping address is associated. 755 | 756 | --- 757 | 758 | ### getProductItems 759 | 760 | **Signature:** `getProductItems() : Collection` 761 | 762 | **Description:** Returns a collection containing all items in the list that reference products. 763 | 764 | **Returns:** 765 | 766 | all product items. 767 | 768 | --- 769 | 770 | ### getPublicItems 771 | 772 | **Signature:** `getPublicItems() : Collection` 773 | 774 | **Description:** Returns a collection containing all items in the list that are flagged as public. 775 | 776 | **Returns:** 777 | 778 | all public items. 779 | 780 | --- 781 | 782 | ### getPurchases 783 | 784 | **Signature:** `getPurchases() : Collection` 785 | 786 | **Description:** Returns the aggregated purchases from all the individual items. 787 | 788 | **Returns:** 789 | 790 | purchases 791 | 792 | --- 793 | 794 | ### getRegistrant 795 | 796 | **Signature:** `getRegistrant() : ProductListRegistrant` 797 | 798 | **Description:** Returns the ProductListRegistrant assigned to the registrant attribute or null if this list has no registrant. 799 | 800 | **Returns:** 801 | 802 | the ProductListRegistrant assigned to the registrant attribute or null if this list has no registrant. 803 | 804 | --- 805 | 806 | ### getShippingAddress 807 | 808 | **Signature:** `getShippingAddress() : CustomerAddress` 809 | 810 | **Description:** Return the address that should be used as the shipping address for purchases made from the list. 811 | 812 | **Returns:** 813 | 814 | the shipping address. The shipping address of this list or null if no address is associated. 815 | 816 | --- 817 | 818 | ### getType 819 | 820 | **Signature:** `getType() : Number` 821 | 822 | **Description:** Returns an int representing the type of object (e.g. wish list, gift registry). This is set at object creation time. 823 | 824 | **Returns:** 825 | 826 | the type of object. 827 | 828 | --- 829 | 830 | ### isAnonymous 831 | 832 | **Signature:** `isAnonymous() : boolean` 833 | 834 | **Description:** Returns true if this product list is owned by an anonymous customer. 835 | 836 | **Returns:** 837 | 838 | true if the owner of this product list is anonymous, false otherwise. 839 | 840 | --- 841 | 842 | ### isPublic 843 | 844 | **Signature:** `isPublic() : boolean` 845 | 846 | **Description:** A flag, typically used to determine if the object is searchable by other customers. 847 | 848 | **Returns:** 849 | 850 | true if the product list is public. False otherwise. 851 | 852 | --- 853 | 854 | ### removeCoRegistrant 855 | 856 | **Signature:** `removeCoRegistrant() : void` 857 | 858 | **Description:** Removes the ProductListRegistrant assigned to the coRegistrant attribute. 859 | 860 | --- 861 | 862 | ### removeItem 863 | 864 | **Signature:** `removeItem(item : ProductListItem) : void` 865 | 866 | **Description:** Removes the specified item from the list. This will also cause all purchase information associated with that item to be removed. 867 | 868 | **Parameters:** 869 | 870 | - `item`: The item to remove. 871 | 872 | --- 873 | 874 | ### removeRegistrant 875 | 876 | **Signature:** `removeRegistrant() : void` 877 | 878 | **Description:** Removes the ProductListRegistrant assigned to the registrant attribute. 879 | 880 | --- 881 | 882 | ### setDescription 883 | 884 | **Signature:** `setDescription(description : String) : void` 885 | 886 | **Description:** Set the description of this product list. 887 | 888 | **Parameters:** 889 | 890 | - `description`: The description of this product list. The description can have up to 256 characters, longer descriptions get truncated. If an empty string is provided, the description gets set to null. 891 | 892 | --- 893 | 894 | ### setEventCity 895 | 896 | **Signature:** `setEventCity(eventCity : String) : void` 897 | 898 | **Description:** Set the event city to which this product list is related. 899 | 900 | **Parameters:** 901 | 902 | - `eventCity`: The event city can have up to 256 characters, longer event city get truncated. If an empty string is provided, the event city gets set to null. 903 | 904 | --- 905 | 906 | ### setEventCountry 907 | 908 | **Signature:** `setEventCountry(eventCountry : String) : void` 909 | 910 | **Description:** Set the event country to which this product list is related. 911 | 912 | **Parameters:** 913 | 914 | - `eventCountry`: The event country can have up to 256 characters, longer event country get truncated. If an empty string is provided, the event country gets set to null. 915 | 916 | --- 917 | 918 | ### setEventDate 919 | 920 | **Signature:** `setEventDate(eventDate : Date) : void` 921 | 922 | **Description:** Set the date of the event to which this product list is related. 923 | 924 | **Parameters:** 925 | 926 | - `eventDate`: The event date or null if no event date should be available. 927 | 928 | --- 929 | 930 | ### setEventState 931 | 932 | **Signature:** `setEventState(eventState : String) : void` 933 | 934 | **Description:** Set the event state to which this product list is related. 935 | 936 | **Parameters:** 937 | 938 | - `eventState`: The event state can have up to 256 characters, longer event state get truncated. If an empty string is provided, the event state gets set to null. 939 | 940 | --- 941 | 942 | ### setEventType 943 | 944 | **Signature:** `setEventType(eventType : String) : void` 945 | 946 | **Description:** Set the event type for which this product list was created by the owner. 947 | 948 | **Parameters:** 949 | 950 | - `eventType`: The event type can have up to 256 characters, longer event type get truncated. If an empty string is provided, the event type gets set to null. 951 | 952 | --- 953 | 954 | ### setName 955 | 956 | **Signature:** `setName(name : String) : void` 957 | 958 | **Description:** Set the name of this product list. 959 | 960 | **Parameters:** 961 | 962 | - `name`: The name of this product list. The name can have up to 256 characters, longer names get truncated. If an empty string is provided, the name gets set to null. 963 | 964 | --- 965 | 966 | ### setPostEventShippingAddress 967 | 968 | **Signature:** `setPostEventShippingAddress(address : CustomerAddress) : void` 969 | 970 | **Description:** This is typically used by an event related list (e.g. gift registry) to specify a shipping address for purchases made after the event date. 971 | 972 | **Parameters:** 973 | 974 | - `address`: The shipping address. 975 | 976 | --- 977 | 978 | ### setPublic 979 | 980 | **Signature:** `setPublic(flag : boolean) : void` 981 | 982 | **Description:** Makes this product list visible to other customers or hides it. 983 | 984 | **Parameters:** 985 | 986 | - `flag`: If true, this product list becomes visible to other customers. If false, this product list can only be seen and searched by its owner. 987 | 988 | --- 989 | 990 | ### setShippingAddress 991 | 992 | **Signature:** `setShippingAddress(address : CustomerAddress) : void` 993 | 994 | **Description:** Associate an address, used as the shipping address for purchases made from the list. 995 | 996 | **Parameters:** 997 | 998 | - `address`: The shipping address. 999 | 1000 | --- ``` -------------------------------------------------------------------------------- /docs/TopLevel/Date.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: TopLevel 2 | 3 | # Class Date 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - Date 9 | 10 | ## Description 11 | 12 | A Date object contains a number indicating a particular instant in time to within a millisecond. The number may also be NaN, indicating that the Date object does not represent a specific instant of time. 13 | 14 | ## Constructor Summary 15 | 16 | Date() Constructs the Date instance using the current date and time. 17 | 18 | Date(millis : Number) Constructs the Date instance using the specified milliseconds. 19 | 20 | Date(year : Number, month : Number, args : Number...) Constructs the Date instance using the specified year and month. 21 | 22 | Date(dateString : String) Constructs the Date instance by parsing the specified String. 23 | 24 | ## Method Summary 25 | 26 | ### getDate 27 | 28 | **Signature:** `getDate() : Number` 29 | 30 | Returns the day of the month where the value is a Number from 1 to 31. 31 | 32 | ### getDay 33 | 34 | **Signature:** `getDay() : Number` 35 | 36 | Returns the day of the week where the value is a Number from 0 to 6. 37 | 38 | ### getFullYear 39 | 40 | **Signature:** `getFullYear() : Number` 41 | 42 | Returns the year of the Date in four-digit format. 43 | 44 | ### getHours 45 | 46 | **Signature:** `getHours() : Number` 47 | 48 | Return the hours field of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). 49 | 50 | ### getMilliseconds 51 | 52 | **Signature:** `getMilliseconds() : Number` 53 | 54 | Returns the milliseconds field of the Date. 55 | 56 | ### getMinutes 57 | 58 | **Signature:** `getMinutes() : Number` 59 | 60 | Return the minutes field of the Date where the value is a Number from 0 to 59. 61 | 62 | ### getMonth 63 | 64 | **Signature:** `getMonth() : Number` 65 | 66 | Returns the month of the year as a value between 0 and 11. 67 | 68 | ### getSeconds 69 | 70 | **Signature:** `getSeconds() : Number` 71 | 72 | Return the seconds field of the Date where the value is a Number from 0 to 59. 73 | 74 | ### getTime 75 | 76 | **Signature:** `getTime() : Number` 77 | 78 | Returns the internal, millisecond representation of the Date object. 79 | 80 | ### getTimezoneOffset 81 | 82 | **Signature:** `getTimezoneOffset() : Number` 83 | 84 | Returns the difference between local time and Greenwich Mean Time (GMT) in minutes. 85 | 86 | ### getUTCDate 87 | 88 | **Signature:** `getUTCDate() : Number` 89 | 90 | Returns the day of the month where the value is a Number from 1 to 31 when date is expressed in universal time. 91 | 92 | ### getUTCDay 93 | 94 | **Signature:** `getUTCDay() : Number` 95 | 96 | Returns the day of the week where the value is a Number from 0 to 6 when date is expressed in universal time. 97 | 98 | ### getUTCFullYear 99 | 100 | **Signature:** `getUTCFullYear() : Number` 101 | 102 | Returns the year when the Date is expressed in universal time. 103 | 104 | ### getUTCHours 105 | 106 | **Signature:** `getUTCHours() : Number` 107 | 108 | Return the hours field, expressed in universal time, of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). 109 | 110 | ### getUTCMilliseconds 111 | 112 | **Signature:** `getUTCMilliseconds() : Number` 113 | 114 | Returns the milliseconds field, expressed in universal time, of the Date. 115 | 116 | ### getUTCMinutes 117 | 118 | **Signature:** `getUTCMinutes() : Number` 119 | 120 | Return the minutes field, expressed in universal time, of the Date where the value is a Number from 0 to 59. 121 | 122 | ### getUTCMonth 123 | 124 | **Signature:** `getUTCMonth() : Number` 125 | 126 | Returns the month of the year that results when the Date is expressed in universal time. 127 | 128 | ### getUTCSeconds 129 | 130 | **Signature:** `getUTCSeconds() : Number` 131 | 132 | Return the seconds field, expressed in universal time, of the Date where the value is a Number from 0 to 59. 133 | 134 | ### now 135 | 136 | **Signature:** `static now() : Number` 137 | 138 | Returns the number of milliseconds since midnight of January 1, 1970 up until now. 139 | 140 | ### parse 141 | 142 | **Signature:** `static parse(dateString : String) : Number` 143 | 144 | Takes a date string and returns the number of milliseconds since midnight of January 1, 1970. 145 | 146 | ### setDate 147 | 148 | **Signature:** `setDate(date : Number) : Number` 149 | 150 | Sets the day of the month where the value is a Number from 1 to 31. 151 | 152 | ### setFullYear 153 | 154 | **Signature:** `setFullYear(year : Number, args : Number...) : Number` 155 | 156 | Sets the full year of Date where the value must be a four-digit Number. 157 | 158 | ### setHours 159 | 160 | **Signature:** `setHours(hours : Number, args : Number...) : Number` 161 | 162 | Sets the hours field of this Date instance. 163 | 164 | ### setMilliseconds 165 | 166 | **Signature:** `setMilliseconds(millis : Number) : Number` 167 | 168 | Sets the milliseconds field of this Date instance. 169 | 170 | ### setMinutes 171 | 172 | **Signature:** `setMinutes(minutes : Number, args : Number...) : Number` 173 | 174 | Sets the minutes field of this Date instance. 175 | 176 | ### setMonth 177 | 178 | **Signature:** `setMonth(month : Number, date : Number...) : Number` 179 | 180 | Sets the month of the year where the value is a Number from 0 to 11. 181 | 182 | ### setSeconds 183 | 184 | **Signature:** `setSeconds(seconds : Number, millis : Number...) : Number` 185 | 186 | Sets the seconds field of this Date instance. 187 | 188 | ### setTime 189 | 190 | **Signature:** `setTime(millis : Number) : Number` 191 | 192 | Sets the number of milliseconds between the desired date and time and January 1, 1970. 193 | 194 | ### setUTCDate 195 | 196 | **Signature:** `setUTCDate(date : Number) : Number` 197 | 198 | Sets the day of the month, expressed in universal time, where the value is a Number from 1 to 31. 199 | 200 | ### setUTCFullYear 201 | 202 | **Signature:** `setUTCFullYear(year : Number, args : Number...) : Number` 203 | 204 | Sets the full year, expressed in universal time, of Date where the value must be a four-digit Number. 205 | 206 | ### setUTCHours 207 | 208 | **Signature:** `setUTCHours(hours : Number, args : Number...) : Number` 209 | 210 | Sets the hours field, expressed in universal time, of this Date instance. 211 | 212 | ### setUTCMilliseconds 213 | 214 | **Signature:** `setUTCMilliseconds(millis : Number) : Number` 215 | 216 | Sets the milliseconds field, expressed in universal time, of this Date instance. 217 | 218 | ### setUTCMinutes 219 | 220 | **Signature:** `setUTCMinutes(minutes : Number, args : Number...) : Number` 221 | 222 | Sets the minutes field, expressed in universal time, of this Date instance. 223 | 224 | ### setUTCMonth 225 | 226 | **Signature:** `setUTCMonth(month : Number, date : Number...) : Number` 227 | 228 | Sets the month of the year, expressed in universal time, where the value is a Number from 0 to 11. 229 | 230 | ### setUTCSeconds 231 | 232 | **Signature:** `setUTCSeconds(seconds : Number, millis : Number...) : Number` 233 | 234 | Sets the seconds field, expressed in universal time, of this Date instance. 235 | 236 | ### toDateString 237 | 238 | **Signature:** `toDateString() : String` 239 | 240 | Returns the Date as a String value where the value represents the date portion of the Date in the default locale (en_US). 241 | 242 | ### toISOString 243 | 244 | **Signature:** `toISOString() : String` 245 | 246 | This function returns a string value represent the instance in time represented by this Date object. 247 | 248 | ### toJSON 249 | 250 | **Signature:** `toJSON(key : String) : Object` 251 | 252 | This function returns the same string as Date.prototype.toISOString(). 253 | 254 | ### toLocaleDateString 255 | 256 | **Signature:** `toLocaleDateString() : String` 257 | 258 | Returns the Date as a String value where the value represents the date portion of the Date in the default locale (en_US). 259 | 260 | ### toLocaleString 261 | 262 | **Signature:** `toLocaleString() : String` 263 | 264 | Returns the Date as a String using the default locale (en_US). 265 | 266 | ### toLocaleTimeString 267 | 268 | **Signature:** `toLocaleTimeString() : String` 269 | 270 | Returns the Date as a String value where the value represents the time portion of the Date in the default locale (en_US). 271 | 272 | ### toTimeString 273 | 274 | **Signature:** `toTimeString() : String` 275 | 276 | Returns the Date as a String value where the value represents the time portion of the Date in the default locale (en_US). 277 | 278 | ### toUTCString 279 | 280 | **Signature:** `toUTCString() : String` 281 | 282 | Returns a String representation of this Date, expressed in universal time. 283 | 284 | ### UTC 285 | 286 | **Signature:** `static UTC(year : Number, month : Number, args : Number...) : Number` 287 | 288 | Returns the number of milliseconds since midnight of January 1, 1970 according to universal time. 289 | 290 | ### valueOf 291 | 292 | **Signature:** `valueOf() : Object` 293 | 294 | Returns the value of this Date represented in milliseconds. 295 | 296 | ## Constructor Detail 297 | 298 | ## Method Detail 299 | 300 | ## Method Details 301 | 302 | ### getDate 303 | 304 | **Signature:** `getDate() : Number` 305 | 306 | **Description:** Returns the day of the month where the value is a Number from 1 to 31. 307 | 308 | **Returns:** 309 | 310 | the day of the month where the value is a Number from 1 to 31. 311 | 312 | --- 313 | 314 | ### getDay 315 | 316 | **Signature:** `getDay() : Number` 317 | 318 | **Description:** Returns the day of the week where the value is a Number from 0 to 6. 319 | 320 | **Returns:** 321 | 322 | the day of the month where the value is a Number from 0 to 6. 323 | 324 | --- 325 | 326 | ### getFullYear 327 | 328 | **Signature:** `getFullYear() : Number` 329 | 330 | **Description:** Returns the year of the Date in four-digit format. 331 | 332 | **Returns:** 333 | 334 | the year of the Date in four-digit format. 335 | 336 | --- 337 | 338 | ### getHours 339 | 340 | **Signature:** `getHours() : Number` 341 | 342 | **Description:** Return the hours field of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). 343 | 344 | **Returns:** 345 | 346 | the hours field of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). 347 | 348 | --- 349 | 350 | ### getMilliseconds 351 | 352 | **Signature:** `getMilliseconds() : Number` 353 | 354 | **Description:** Returns the milliseconds field of the Date. 355 | 356 | **Returns:** 357 | 358 | the milliseconds field of the Date. 359 | 360 | --- 361 | 362 | ### getMinutes 363 | 364 | **Signature:** `getMinutes() : Number` 365 | 366 | **Description:** Return the minutes field of the Date where the value is a Number from 0 to 59. 367 | 368 | **Returns:** 369 | 370 | the minutes field of the Date where the value is a Number from 0 to 59. 371 | 372 | --- 373 | 374 | ### getMonth 375 | 376 | **Signature:** `getMonth() : Number` 377 | 378 | **Description:** Returns the month of the year as a value between 0 and 11. 379 | 380 | **Returns:** 381 | 382 | the month of the year as a value between 0 and 11. 383 | 384 | --- 385 | 386 | ### getSeconds 387 | 388 | **Signature:** `getSeconds() : Number` 389 | 390 | **Description:** Return the seconds field of the Date where the value is a Number from 0 to 59. 391 | 392 | **Returns:** 393 | 394 | the seconds field of the Date where the value is a Number from 0 to 59. 395 | 396 | --- 397 | 398 | ### getTime 399 | 400 | **Signature:** `getTime() : Number` 401 | 402 | **Description:** Returns the internal, millisecond representation of the Date object. This value is independent of time zone. 403 | 404 | **Returns:** 405 | 406 | the internal, millisecond representation of the Date object. 407 | 408 | --- 409 | 410 | ### getTimezoneOffset 411 | 412 | **Signature:** `getTimezoneOffset() : Number` 413 | 414 | **Description:** Returns the difference between local time and Greenwich Mean Time (GMT) in minutes. 415 | 416 | **Returns:** 417 | 418 | the difference between local time and Greenwich Mean Time (GMT) in minutes. 419 | 420 | --- 421 | 422 | ### getUTCDate 423 | 424 | **Signature:** `getUTCDate() : Number` 425 | 426 | **Description:** Returns the day of the month where the value is a Number from 1 to 31 when date is expressed in universal time. 427 | 428 | **Returns:** 429 | 430 | the day of the month where the value is a Number from 1 to 31 when date is expressed in universal time. 431 | 432 | --- 433 | 434 | ### getUTCDay 435 | 436 | **Signature:** `getUTCDay() : Number` 437 | 438 | **Description:** Returns the day of the week where the value is a Number from 0 to 6 when date is expressed in universal time. 439 | 440 | **Returns:** 441 | 442 | the day of the week where the value is a Number from 0 to 6 when date is expressed in universal time. 443 | 444 | --- 445 | 446 | ### getUTCFullYear 447 | 448 | **Signature:** `getUTCFullYear() : Number` 449 | 450 | **Description:** Returns the year when the Date is expressed in universal time. The return value is a four-digit format. 451 | 452 | **Returns:** 453 | 454 | the year of the Date in four-digit form. 455 | 456 | --- 457 | 458 | ### getUTCHours 459 | 460 | **Signature:** `getUTCHours() : Number` 461 | 462 | **Description:** Return the hours field, expressed in universal time, of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). 463 | 464 | **Returns:** 465 | 466 | the hours field, expressed in universal time, of the Date where the value is a Number from 0 (midnight) to 23 (11 PM). 467 | 468 | --- 469 | 470 | ### getUTCMilliseconds 471 | 472 | **Signature:** `getUTCMilliseconds() : Number` 473 | 474 | **Description:** Returns the milliseconds field, expressed in universal time, of the Date. 475 | 476 | **Returns:** 477 | 478 | the milliseconds field, expressed in universal time, of the Date. 479 | 480 | --- 481 | 482 | ### getUTCMinutes 483 | 484 | **Signature:** `getUTCMinutes() : Number` 485 | 486 | **Description:** Return the minutes field, expressed in universal time, of the Date where the value is a Number from 0 to 59. 487 | 488 | **Returns:** 489 | 490 | the minutes field, expressed in universal time, of the Date where the value is a Number from 0 to 59. 491 | 492 | --- 493 | 494 | ### getUTCMonth 495 | 496 | **Signature:** `getUTCMonth() : Number` 497 | 498 | **Description:** Returns the month of the year that results when the Date is expressed in universal time. The return value is a Number betwee 0 and 11. 499 | 500 | **Returns:** 501 | 502 | the month of the year as a value between 0 and 11. 503 | 504 | --- 505 | 506 | ### getUTCSeconds 507 | 508 | **Signature:** `getUTCSeconds() : Number` 509 | 510 | **Description:** Return the seconds field, expressed in universal time, of the Date where the value is a Number from 0 to 59. 511 | 512 | **Returns:** 513 | 514 | the seconds field, expressed in universal time, of the Date where the value is a Number from 0 to 59. 515 | 516 | --- 517 | 518 | ### now 519 | 520 | **Signature:** `static now() : Number` 521 | 522 | **Description:** Returns the number of milliseconds since midnight of January 1, 1970 up until now. 523 | 524 | **Returns:** 525 | 526 | the number of milliseconds since midnight of January 1, 1970. 527 | 528 | --- 529 | 530 | ### parse 531 | 532 | **Signature:** `static parse(dateString : String) : Number` 533 | 534 | **Description:** Takes a date string and returns the number of milliseconds since midnight of January 1, 1970. Supports: RFC2822 date strings strings matching the exact ISO 8601 format 'YYYY-MM-DDTHH:mm:ss.sssZ' 535 | 536 | **Parameters:** 537 | 538 | - `dateString`: represents a Date in a valid date format. 539 | 540 | **Returns:** 541 | 542 | the number of milliseconds since midnight of January 1, 1970 or NaN if no date could be recognized. 543 | 544 | --- 545 | 546 | ### setDate 547 | 548 | **Signature:** `setDate(date : Number) : Number` 549 | 550 | **Description:** Sets the day of the month where the value is a Number from 1 to 31. 551 | 552 | **Parameters:** 553 | 554 | - `date`: the day of the month. 555 | 556 | **Returns:** 557 | 558 | the millisecond representation of the adjusted date. 559 | 560 | --- 561 | 562 | ### setFullYear 563 | 564 | **Signature:** `setFullYear(year : Number, args : Number...) : Number` 565 | 566 | **Description:** Sets the full year of Date where the value must be a four-digit Number. Optionally, you can set the month and date. 567 | 568 | **Parameters:** 569 | 570 | - `year`: the year as a four-digit Number. 571 | - `args`: the month and day of the month. 572 | 573 | **Returns:** 574 | 575 | the millisecond representation of the adjusted date. 576 | 577 | --- 578 | 579 | ### setHours 580 | 581 | **Signature:** `setHours(hours : Number, args : Number...) : Number` 582 | 583 | **Description:** Sets the hours field of this Date instance. The minutes value should be a Number from 0 to 23. Optionally, hours, seconds and milliseconds can also be provided. 584 | 585 | **Parameters:** 586 | 587 | - `hours`: the minutes field of this Date instance. 588 | - `args`: the hours, seconds and milliseconds values for this Date instance. 589 | 590 | **Returns:** 591 | 592 | the millisecond representation of the adjusted date. 593 | 594 | --- 595 | 596 | ### setMilliseconds 597 | 598 | **Signature:** `setMilliseconds(millis : Number) : Number` 599 | 600 | **Description:** Sets the milliseconds field of this Date instance. 601 | 602 | **Parameters:** 603 | 604 | - `millis`: the milliseconds field of this Date instance. 605 | 606 | **Returns:** 607 | 608 | the millisecond representation of the adjusted date. 609 | 610 | --- 611 | 612 | ### setMinutes 613 | 614 | **Signature:** `setMinutes(minutes : Number, args : Number...) : Number` 615 | 616 | **Description:** Sets the minutes field of this Date instance. The minutes value should be a Number from 0 to 59. Optionally, seconds and milliseconds can also be provided. 617 | 618 | **Parameters:** 619 | 620 | - `minutes`: the minutes field of this Date instance. 621 | - `args`: the seconds and milliseconds value for this Date instance. 622 | 623 | **Returns:** 624 | 625 | the millisecond representation of the adjusted date. 626 | 627 | --- 628 | 629 | ### setMonth 630 | 631 | **Signature:** `setMonth(month : Number, date : Number...) : Number` 632 | 633 | **Description:** Sets the month of the year where the value is a Number from 0 to 11. Optionally, you can set the day of the month. 634 | 635 | **Parameters:** 636 | 637 | - `month`: the month of the year. 638 | - `date`: the day of the month. 639 | 640 | **Returns:** 641 | 642 | the millisecond representation of the adjusted date. 643 | 644 | --- 645 | 646 | ### setSeconds 647 | 648 | **Signature:** `setSeconds(seconds : Number, millis : Number...) : Number` 649 | 650 | **Description:** Sets the seconds field of this Date instance. The seconds value should be a Number from 0 to 59. Optionally, milliseconds can also be provided. 651 | 652 | **Parameters:** 653 | 654 | - `seconds`: the seconds field of this Date instance. 655 | - `millis`: the milliseconds field of this Date instance. 656 | 657 | **Returns:** 658 | 659 | the millisecond representation of the adjusted date. 660 | 661 | --- 662 | 663 | ### setTime 664 | 665 | **Signature:** `setTime(millis : Number) : Number` 666 | 667 | **Description:** Sets the number of milliseconds between the desired date and time and January 1, 1970. 668 | 669 | **Parameters:** 670 | 671 | - `millis`: the number of milliseconds between the desired date and time and January 1, 1970. 672 | 673 | **Returns:** 674 | 675 | the millisecond representation of the adjusted date. 676 | 677 | --- 678 | 679 | ### setUTCDate 680 | 681 | **Signature:** `setUTCDate(date : Number) : Number` 682 | 683 | **Description:** Sets the day of the month, expressed in universal time, where the value is a Number from 1 to 31. 684 | 685 | **Parameters:** 686 | 687 | - `date`: the day of the month, expressed in universal time. 688 | 689 | **Returns:** 690 | 691 | the millisecond representation of the adjusted date. 692 | 693 | --- 694 | 695 | ### setUTCFullYear 696 | 697 | **Signature:** `setUTCFullYear(year : Number, args : Number...) : Number` 698 | 699 | **Description:** Sets the full year, expressed in universal time, of Date where the value must be a four-digit Number. Optionally, you can set the month and date. 700 | 701 | **Parameters:** 702 | 703 | - `year`: the year as a four-digit Number, expressed in universal time. 704 | - `args`: the month and day of the month. 705 | 706 | **Returns:** 707 | 708 | the millisecond representation of the adjusted date. 709 | 710 | --- 711 | 712 | ### setUTCHours 713 | 714 | **Signature:** `setUTCHours(hours : Number, args : Number...) : Number` 715 | 716 | **Description:** Sets the hours field, expressed in universal time, of this Date instance. The minutes value should be a Number from 0 to 23. Optionally, seconds and milliseconds can also be provided. 717 | 718 | **Parameters:** 719 | 720 | - `hours`: the minutes field, expressed in universal time, of this Date instance. 721 | - `args`: the seconds and milliseconds value, expressed in universal time, for this Date instance. 722 | 723 | **Returns:** 724 | 725 | the millisecond representation of the adjusted date. 726 | 727 | --- 728 | 729 | ### setUTCMilliseconds 730 | 731 | **Signature:** `setUTCMilliseconds(millis : Number) : Number` 732 | 733 | **Description:** Sets the milliseconds field, expressed in universal time, of this Date instance. 734 | 735 | **Parameters:** 736 | 737 | - `millis`: the milliseconds field, expressed in universal time, of this Date instance. 738 | 739 | **Returns:** 740 | 741 | the millisecond representation of the adjusted date. 742 | 743 | --- 744 | 745 | ### setUTCMinutes 746 | 747 | **Signature:** `setUTCMinutes(minutes : Number, args : Number...) : Number` 748 | 749 | **Description:** Sets the minutes field, expressed in universal time, of this Date instance. The minutes value should be a Number from 0 to 59. Optionally, seconds and milliseconds can also be provided. 750 | 751 | **Parameters:** 752 | 753 | - `minutes`: the minutes field, expressed in universal time, of this Date instance. 754 | - `args`: the seconds and milliseconds values, expressed in universal time, for this Date instance. 755 | 756 | **Returns:** 757 | 758 | the millisecond representation of the adjusted date. 759 | 760 | --- 761 | 762 | ### setUTCMonth 763 | 764 | **Signature:** `setUTCMonth(month : Number, date : Number...) : Number` 765 | 766 | **Description:** Sets the month of the year, expressed in universal time, where the value is a Number from 0 to 11. Optionally, you can set the day of the month. 767 | 768 | **Parameters:** 769 | 770 | - `month`: the month of the year, expressed in universal time. 771 | - `date`: the day of the month. 772 | 773 | **Returns:** 774 | 775 | the millisecond representation of the adjusted date. 776 | 777 | --- 778 | 779 | ### setUTCSeconds 780 | 781 | **Signature:** `setUTCSeconds(seconds : Number, millis : Number...) : Number` 782 | 783 | **Description:** Sets the seconds field, expressed in universal time, of this Date instance. The seconds value should be a Number from 0 to 59. Optionally, milliseconds can also be provided. 784 | 785 | **Parameters:** 786 | 787 | - `seconds`: the seconds field, expressed in universal time, of this Date instance. 788 | - `millis`: the milliseconds field, expressed in universal time, of this Date instance. 789 | 790 | **Returns:** 791 | 792 | the millisecond representation of the adjusted date. 793 | 794 | --- 795 | 796 | ### toDateString 797 | 798 | **Signature:** `toDateString() : String` 799 | 800 | **Description:** Returns the Date as a String value where the value represents the date portion of the Date in the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. 801 | 802 | **Returns:** 803 | 804 | the Date as a String value. 805 | 806 | --- 807 | 808 | ### toISOString 809 | 810 | **Signature:** `toISOString() : String` 811 | 812 | **Description:** This function returns a string value represent the instance in time represented by this Date object. The date is formatted with the Simplified ISO 8601 format as follows: YYYY-MM-DDTHH:mm:ss.sssTZ. The time zone is always UTC, denoted by the suffix Z. 813 | 814 | **Returns:** 815 | 816 | string representation of this date 817 | 818 | --- 819 | 820 | ### toJSON 821 | 822 | **Signature:** `toJSON(key : String) : Object` 823 | 824 | **Description:** This function returns the same string as Date.prototype.toISOString(). The method is called when a Date object is stringified. 825 | 826 | **Parameters:** 827 | 828 | - `key`: the name of the key, which is stringified 829 | 830 | **Returns:** 831 | 832 | JSON string representation of this date 833 | 834 | --- 835 | 836 | ### toLocaleDateString 837 | 838 | **Signature:** `toLocaleDateString() : String` 839 | 840 | **Description:** Returns the Date as a String value where the value represents the date portion of the Date in the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. 841 | 842 | **Returns:** 843 | 844 | returns the date portion of the Date as a String. 845 | 846 | --- 847 | 848 | ### toLocaleString 849 | 850 | **Signature:** `toLocaleString() : String` 851 | 852 | **Description:** Returns the Date as a String using the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. 853 | 854 | **Returns:** 855 | 856 | the Date as a String using the default locale en_US 857 | 858 | --- 859 | 860 | ### toLocaleTimeString 861 | 862 | **Signature:** `toLocaleTimeString() : String` 863 | 864 | **Description:** Returns the Date as a String value where the value represents the time portion of the Date in the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. 865 | 866 | **Returns:** 867 | 868 | returns the time time's portion of the Date as a String. 869 | 870 | --- 871 | 872 | ### toTimeString 873 | 874 | **Signature:** `toTimeString() : String` 875 | 876 | **Description:** Returns the Date as a String value where the value represents the time portion of the Date in the default locale (en_US). To format a calendar object in an alternate format use the dw.util.StringUtils.formatCalendar() functions instead. 877 | 878 | **Returns:** 879 | 880 | the Date's time. 881 | 882 | --- 883 | 884 | ### toUTCString 885 | 886 | **Signature:** `toUTCString() : String` 887 | 888 | **Description:** Returns a String representation of this Date, expressed in universal time. 889 | 890 | **Returns:** 891 | 892 | a String representation of this Date, expressed in universal time. 893 | 894 | --- 895 | 896 | ### UTC 897 | 898 | **Signature:** `static UTC(year : Number, month : Number, args : Number...) : Number` 899 | 900 | **Description:** Returns the number of milliseconds since midnight of January 1, 1970 according to universal time. Optionally, you can pass up to five additional arguments representing date, hours, minutes, seconds, and milliseconds. 901 | 902 | **Parameters:** 903 | 904 | - `year`: a number representing the year. 905 | - `month`: a number representing the month. 906 | - `args`: a set of numbers representing the date, hours, minutes, seconds, and milliseconds. 907 | 908 | **Returns:** 909 | 910 | the number of milliseconds since midnight of January 1, 1970 according to universal time. 911 | 912 | --- 913 | 914 | ### valueOf 915 | 916 | **Signature:** `valueOf() : Object` 917 | 918 | **Description:** Returns the value of this Date represented in milliseconds. 919 | 920 | **Returns:** 921 | 922 | the value of this Date represented in milliseconds. 923 | 924 | --- ``` -------------------------------------------------------------------------------- /tests/oauth-token.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { TokenManager } from '../src/clients/base/oauth-token.js'; 2 | import { OAuthTokenResponse } from '../src/types/types'; 3 | 4 | describe('TokenManager', () => { 5 | let tokenManager: TokenManager; 6 | const testHostname = 'test-instance.demandware.net'; 7 | const testClientId = 'test-client-id'; 8 | const testClientId2 = 'test-client-id-2'; 9 | const testHostname2 = 'test-instance-2.demandware.net'; 10 | 11 | beforeEach(() => { 12 | // Get a fresh instance and clear all tokens 13 | tokenManager = TokenManager.getInstance(); 14 | tokenManager.clearAllTokens(); 15 | }); 16 | 17 | afterEach(() => { 18 | // Clean up after each test 19 | tokenManager.clearAllTokens(); 20 | }); 21 | 22 | describe('Singleton pattern', () => { 23 | it('should return the same instance when called multiple times', () => { 24 | const instance1 = TokenManager.getInstance(); 25 | const instance2 = TokenManager.getInstance(); 26 | const instance3 = TokenManager.getInstance(); 27 | 28 | expect(instance1).toBe(instance2); 29 | expect(instance2).toBe(instance3); 30 | expect(instance1).toBe(tokenManager); 31 | }); 32 | 33 | it('should maintain state across getInstance calls', () => { 34 | const tokenResponse: OAuthTokenResponse = { 35 | access_token: 'test-token', 36 | token_type: 'bearer', 37 | expires_in: 3600, 38 | }; 39 | 40 | const instance1 = TokenManager.getInstance(); 41 | instance1.storeToken(testHostname, testClientId, tokenResponse); 42 | 43 | const instance2 = TokenManager.getInstance(); 44 | expect(instance2.getValidToken(testHostname, testClientId)).toBe('test-token'); 45 | }); 46 | }); 47 | 48 | describe('storeToken()', () => { 49 | it('should store a token correctly', () => { 50 | const tokenResponse: OAuthTokenResponse = { 51 | access_token: 'stored-token', 52 | token_type: 'bearer', 53 | expires_in: 3600, 54 | }; 55 | 56 | tokenManager.storeToken(testHostname, testClientId, tokenResponse); 57 | 58 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('stored-token'); 59 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); 60 | }); 61 | 62 | it('should calculate expiration time correctly', () => { 63 | const tokenResponse: OAuthTokenResponse = { 64 | access_token: 'expiration-test-token', 65 | token_type: 'bearer', 66 | expires_in: 3600, // 1 hour 67 | }; 68 | 69 | const beforeStore = Date.now(); 70 | tokenManager.storeToken(testHostname, testClientId, tokenResponse); 71 | const afterStore = Date.now(); 72 | 73 | const expiration = tokenManager.getTokenExpiration(testHostname, testClientId); 74 | expect(expiration).toBeInstanceOf(Date); 75 | 76 | if (expiration) { 77 | const expirationTime = expiration.getTime(); 78 | const expectedMinExpiration = beforeStore + (3600 * 1000); 79 | const expectedMaxExpiration = afterStore + (3600 * 1000); 80 | 81 | expect(expirationTime).toBeGreaterThanOrEqual(expectedMinExpiration); 82 | expect(expirationTime).toBeLessThanOrEqual(expectedMaxExpiration); 83 | } 84 | }); 85 | 86 | it('should overwrite existing tokens for the same hostname/clientId', () => { 87 | const firstToken: OAuthTokenResponse = { 88 | access_token: 'first-token', 89 | token_type: 'bearer', 90 | expires_in: 3600, 91 | }; 92 | 93 | const secondToken: OAuthTokenResponse = { 94 | access_token: 'second-token', 95 | token_type: 'bearer', 96 | expires_in: 7200, 97 | }; 98 | 99 | tokenManager.storeToken(testHostname, testClientId, firstToken); 100 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('first-token'); 101 | 102 | tokenManager.storeToken(testHostname, testClientId, secondToken); 103 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('second-token'); 104 | }); 105 | 106 | it('should store different tokens for different hostname/clientId combinations', () => { 107 | const token1: OAuthTokenResponse = { 108 | access_token: 'token-1', 109 | token_type: 'bearer', 110 | expires_in: 3600, 111 | }; 112 | 113 | const token2: OAuthTokenResponse = { 114 | access_token: 'token-2', 115 | token_type: 'bearer', 116 | expires_in: 3600, 117 | }; 118 | 119 | const token3: OAuthTokenResponse = { 120 | access_token: 'token-3', 121 | token_type: 'bearer', 122 | expires_in: 3600, 123 | }; 124 | 125 | // Same hostname, different clientId 126 | tokenManager.storeToken(testHostname, testClientId, token1); 127 | tokenManager.storeToken(testHostname, testClientId2, token2); 128 | 129 | // Different hostname, same clientId 130 | tokenManager.storeToken(testHostname2, testClientId, token3); 131 | 132 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('token-1'); 133 | expect(tokenManager.getValidToken(testHostname, testClientId2)).toBe('token-2'); 134 | expect(tokenManager.getValidToken(testHostname2, testClientId)).toBe('token-3'); 135 | }); 136 | }); 137 | 138 | describe('getValidToken()', () => { 139 | it('should return null for non-existent tokens', () => { 140 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); 141 | expect(tokenManager.getValidToken('nonexistent.host', 'nonexistent-client')).toBeNull(); 142 | }); 143 | 144 | it('should return valid tokens', () => { 145 | const tokenResponse: OAuthTokenResponse = { 146 | access_token: 'valid-token', 147 | token_type: 'bearer', 148 | expires_in: 3600, 149 | }; 150 | 151 | tokenManager.storeToken(testHostname, testClientId, tokenResponse); 152 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('valid-token'); 153 | }); 154 | 155 | it('should return null for expired tokens', async () => { 156 | const shortLivedToken: OAuthTokenResponse = { 157 | access_token: 'short-lived-token', 158 | token_type: 'bearer', 159 | expires_in: 0.001, // Very short expiration (1ms) 160 | }; 161 | 162 | tokenManager.storeToken(testHostname, testClientId, shortLivedToken); 163 | 164 | // Wait for token to expire 165 | await new Promise(resolve => setTimeout(resolve, 100)); 166 | 167 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); 168 | }); 169 | 170 | it('should return null for tokens that expire within 60-second buffer', () => { 171 | // Create a token that expires in 30 seconds (within the 60-second buffer) 172 | const soonToExpireToken: OAuthTokenResponse = { 173 | access_token: 'soon-to-expire-token', 174 | token_type: 'bearer', 175 | expires_in: 30, // 30 seconds 176 | }; 177 | 178 | tokenManager.storeToken(testHostname, testClientId, soonToExpireToken); 179 | 180 | // Should return null because it's within the 60-second buffer 181 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); 182 | }); 183 | 184 | it('should return tokens that expire beyond the 60-second buffer', () => { 185 | // Create a token that expires in 120 seconds (beyond the 60-second buffer) 186 | const validToken: OAuthTokenResponse = { 187 | access_token: 'valid-token-beyond-buffer', 188 | token_type: 'bearer', 189 | expires_in: 120, // 2 minutes 190 | }; 191 | 192 | tokenManager.storeToken(testHostname, testClientId, validToken); 193 | 194 | // Should return the token because it expires beyond the 60-second buffer 195 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('valid-token-beyond-buffer'); 196 | }); 197 | }); 198 | 199 | describe('isTokenValid()', () => { 200 | it('should return false for non-existent tokens', () => { 201 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); 202 | expect(tokenManager.isTokenValid('nonexistent.host', 'nonexistent-client')).toBe(false); 203 | }); 204 | 205 | it('should return true for valid tokens', () => { 206 | const tokenResponse: OAuthTokenResponse = { 207 | access_token: 'valid-token', 208 | token_type: 'bearer', 209 | expires_in: 3600, 210 | }; 211 | 212 | tokenManager.storeToken(testHostname, testClientId, tokenResponse); 213 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); 214 | }); 215 | 216 | it('should return false for expired tokens', async () => { 217 | const shortLivedToken: OAuthTokenResponse = { 218 | access_token: 'expired-token', 219 | token_type: 'bearer', 220 | expires_in: 0.001, // Very short expiration 221 | }; 222 | 223 | tokenManager.storeToken(testHostname, testClientId, shortLivedToken); 224 | 225 | // Wait for expiration 226 | await new Promise(resolve => setTimeout(resolve, 100)); 227 | 228 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); 229 | }); 230 | 231 | it('should return false for tokens within 60-second expiration buffer', () => { 232 | const soonToExpireToken: OAuthTokenResponse = { 233 | access_token: 'buffer-test-token', 234 | token_type: 'bearer', 235 | expires_in: 30, // 30 seconds - within buffer 236 | }; 237 | 238 | tokenManager.storeToken(testHostname, testClientId, soonToExpireToken); 239 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); 240 | }); 241 | 242 | it('should return true for tokens beyond 60-second expiration buffer', () => { 243 | const validToken: OAuthTokenResponse = { 244 | access_token: 'buffer-safe-token', 245 | token_type: 'bearer', 246 | expires_in: 120, // 2 minutes - beyond buffer 247 | }; 248 | 249 | tokenManager.storeToken(testHostname, testClientId, validToken); 250 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); 251 | }); 252 | 253 | it('should handle edge case of exactly 60-second expiration', () => { 254 | const exactBufferToken: OAuthTokenResponse = { 255 | access_token: 'exact-buffer-token', 256 | token_type: 'bearer', 257 | expires_in: 60, // Exactly 60 seconds 258 | }; 259 | 260 | tokenManager.storeToken(testHostname, testClientId, exactBufferToken); 261 | // Should be false because 60 seconds is not > 60 seconds (buffer) 262 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); 263 | }); 264 | 265 | it('should handle edge case just beyond 60-second buffer', () => { 266 | const justBeyondBufferToken: OAuthTokenResponse = { 267 | access_token: 'just-beyond-buffer-token', 268 | token_type: 'bearer', 269 | expires_in: 61, // 61 seconds - just beyond buffer 270 | }; 271 | 272 | tokenManager.storeToken(testHostname, testClientId, justBeyondBufferToken); 273 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); 274 | }); 275 | }); 276 | 277 | describe('clearToken()', () => { 278 | it('should remove specific tokens', () => { 279 | const tokenResponse: OAuthTokenResponse = { 280 | access_token: 'to-be-cleared', 281 | token_type: 'bearer', 282 | expires_in: 3600, 283 | }; 284 | 285 | tokenManager.storeToken(testHostname, testClientId, tokenResponse); 286 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('to-be-cleared'); 287 | 288 | tokenManager.clearToken(testHostname, testClientId); 289 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); 290 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); 291 | }); 292 | 293 | it('should not affect other tokens when clearing specific token', () => { 294 | const token1: OAuthTokenResponse = { 295 | access_token: 'token-1', 296 | token_type: 'bearer', 297 | expires_in: 3600, 298 | }; 299 | 300 | const token2: OAuthTokenResponse = { 301 | access_token: 'token-2', 302 | token_type: 'bearer', 303 | expires_in: 3600, 304 | }; 305 | 306 | tokenManager.storeToken(testHostname, testClientId, token1); 307 | tokenManager.storeToken(testHostname, testClientId2, token2); 308 | 309 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('token-1'); 310 | expect(tokenManager.getValidToken(testHostname, testClientId2)).toBe('token-2'); 311 | 312 | tokenManager.clearToken(testHostname, testClientId); 313 | 314 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); 315 | expect(tokenManager.getValidToken(testHostname, testClientId2)).toBe('token-2'); 316 | }); 317 | 318 | it('should handle clearing non-existent tokens gracefully', () => { 319 | // Should not throw when clearing non-existent token 320 | expect(() => { 321 | tokenManager.clearToken(testHostname, testClientId); 322 | }).not.toThrow(); 323 | 324 | expect(() => { 325 | tokenManager.clearToken('nonexistent.host', 'nonexistent-client'); 326 | }).not.toThrow(); 327 | }); 328 | }); 329 | 330 | describe('clearAllTokens()', () => { 331 | it('should remove all stored tokens', () => { 332 | const token1: OAuthTokenResponse = { 333 | access_token: 'token-1', 334 | token_type: 'bearer', 335 | expires_in: 3600, 336 | }; 337 | 338 | const token2: OAuthTokenResponse = { 339 | access_token: 'token-2', 340 | token_type: 'bearer', 341 | expires_in: 3600, 342 | }; 343 | 344 | const token3: OAuthTokenResponse = { 345 | access_token: 'token-3', 346 | token_type: 'bearer', 347 | expires_in: 3600, 348 | }; 349 | 350 | tokenManager.storeToken(testHostname, testClientId, token1); 351 | tokenManager.storeToken(testHostname, testClientId2, token2); 352 | tokenManager.storeToken(testHostname2, testClientId, token3); 353 | 354 | // Verify all tokens are stored 355 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('token-1'); 356 | expect(tokenManager.getValidToken(testHostname, testClientId2)).toBe('token-2'); 357 | expect(tokenManager.getValidToken(testHostname2, testClientId)).toBe('token-3'); 358 | 359 | tokenManager.clearAllTokens(); 360 | 361 | // Verify all tokens are cleared 362 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); 363 | expect(tokenManager.getValidToken(testHostname, testClientId2)).toBeNull(); 364 | expect(tokenManager.getValidToken(testHostname2, testClientId)).toBeNull(); 365 | }); 366 | 367 | it('should handle clearing when no tokens exist', () => { 368 | expect(() => { 369 | tokenManager.clearAllTokens(); 370 | }).not.toThrow(); 371 | 372 | // Should still work normally after clearing empty storage 373 | const tokenResponse: OAuthTokenResponse = { 374 | access_token: 'after-clear-all', 375 | token_type: 'bearer', 376 | expires_in: 3600, 377 | }; 378 | 379 | tokenManager.storeToken(testHostname, testClientId, tokenResponse); 380 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('after-clear-all'); 381 | }); 382 | }); 383 | 384 | describe('getTokenExpiration()', () => { 385 | it('should return null for non-existent tokens', () => { 386 | expect(tokenManager.getTokenExpiration(testHostname, testClientId)).toBeNull(); 387 | expect(tokenManager.getTokenExpiration('nonexistent.host', 'nonexistent-client')).toBeNull(); 388 | }); 389 | 390 | it('should return correct expiration date for existing tokens', () => { 391 | const tokenResponse: OAuthTokenResponse = { 392 | access_token: 'expiration-date-token', 393 | token_type: 'bearer', 394 | expires_in: 3600, // 1 hour 395 | }; 396 | 397 | const beforeStore = Date.now(); 398 | tokenManager.storeToken(testHostname, testClientId, tokenResponse); 399 | const afterStore = Date.now(); 400 | 401 | const expiration = tokenManager.getTokenExpiration(testHostname, testClientId); 402 | 403 | expect(expiration).toBeInstanceOf(Date); 404 | if (expiration) { 405 | const expirationTime = expiration.getTime(); 406 | const expectedMinExpiration = beforeStore + (3600 * 1000); 407 | const expectedMaxExpiration = afterStore + (3600 * 1000); 408 | 409 | expect(expirationTime).toBeGreaterThanOrEqual(expectedMinExpiration); 410 | expect(expirationTime).toBeLessThanOrEqual(expectedMaxExpiration); 411 | } 412 | }); 413 | 414 | it('should return different expiration times for different tokens', () => { 415 | const shortToken: OAuthTokenResponse = { 416 | access_token: 'short-token', 417 | token_type: 'bearer', 418 | expires_in: 1800, // 30 minutes 419 | }; 420 | 421 | const longToken: OAuthTokenResponse = { 422 | access_token: 'long-token', 423 | token_type: 'bearer', 424 | expires_in: 7200, // 2 hours 425 | }; 426 | 427 | tokenManager.storeToken(testHostname, testClientId, shortToken); 428 | tokenManager.storeToken(testHostname, testClientId2, longToken); 429 | 430 | const shortExpiration = tokenManager.getTokenExpiration(testHostname, testClientId); 431 | const longExpiration = tokenManager.getTokenExpiration(testHostname, testClientId2); 432 | 433 | expect(shortExpiration).toBeInstanceOf(Date); 434 | expect(longExpiration).toBeInstanceOf(Date); 435 | 436 | if (shortExpiration && longExpiration) { 437 | expect(longExpiration.getTime()).toBeGreaterThan(shortExpiration.getTime()); 438 | } 439 | }); 440 | 441 | it('should return null after token is cleared', () => { 442 | const tokenResponse: OAuthTokenResponse = { 443 | access_token: 'cleared-token', 444 | token_type: 'bearer', 445 | expires_in: 3600, 446 | }; 447 | 448 | tokenManager.storeToken(testHostname, testClientId, tokenResponse); 449 | expect(tokenManager.getTokenExpiration(testHostname, testClientId)).toBeInstanceOf(Date); 450 | 451 | tokenManager.clearToken(testHostname, testClientId); 452 | expect(tokenManager.getTokenExpiration(testHostname, testClientId)).toBeNull(); 453 | }); 454 | }); 455 | 456 | describe('Token key generation', () => { 457 | it('should create unique keys for different hostname/clientId combinations', () => { 458 | // We can't directly test the private getTokenKey method, but we can test its behavior 459 | const token1: OAuthTokenResponse = { 460 | access_token: 'unique-test-1', 461 | token_type: 'bearer', 462 | expires_in: 3600, 463 | }; 464 | 465 | const token2: OAuthTokenResponse = { 466 | access_token: 'unique-test-2', 467 | token_type: 'bearer', 468 | expires_in: 3600, 469 | }; 470 | 471 | // Store tokens with similar but different keys 472 | tokenManager.storeToken('host1.com', 'client1', token1); 473 | tokenManager.storeToken('host1.com', 'client2', token2); 474 | 475 | expect(tokenManager.getValidToken('host1.com', 'client1')).toBe('unique-test-1'); 476 | expect(tokenManager.getValidToken('host1.com', 'client2')).toBe('unique-test-2'); 477 | 478 | // Different hostname, same client 479 | tokenManager.storeToken('host2.com', 'client1', token1); 480 | expect(tokenManager.getValidToken('host2.com', 'client1')).toBe('unique-test-1'); 481 | expect(tokenManager.getValidToken('host1.com', 'client1')).toBe('unique-test-1'); // Should still exist 482 | }); 483 | 484 | it('should handle special characters in hostname and clientId', () => { 485 | const tokenResponse: OAuthTokenResponse = { 486 | access_token: 'special-chars-token', 487 | token_type: 'bearer', 488 | expires_in: 3600, 489 | }; 490 | 491 | const specialHostname = 'test-instance_with.special-chars.demandware.net'; 492 | const specialClientId = 'client-id_with.special:chars'; 493 | 494 | tokenManager.storeToken(specialHostname, specialClientId, tokenResponse); 495 | expect(tokenManager.getValidToken(specialHostname, specialClientId)).toBe('special-chars-token'); 496 | expect(tokenManager.isTokenValid(specialHostname, specialClientId)).toBe(true); 497 | }); 498 | }); 499 | 500 | describe('Edge cases and error handling', () => { 501 | it('should handle zero expiration time', () => { 502 | const zeroExpirationToken: OAuthTokenResponse = { 503 | access_token: 'zero-expiration-token', 504 | token_type: 'bearer', 505 | expires_in: 0, 506 | }; 507 | 508 | tokenManager.storeToken(testHostname, testClientId, zeroExpirationToken); 509 | 510 | // Should be invalid immediately due to buffer 511 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); 512 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); 513 | }); 514 | 515 | it('should handle negative expiration time', () => { 516 | const negativeExpirationToken: OAuthTokenResponse = { 517 | access_token: 'negative-expiration-token', 518 | token_type: 'bearer', 519 | expires_in: -100, 520 | }; 521 | 522 | tokenManager.storeToken(testHostname, testClientId, negativeExpirationToken); 523 | 524 | // Should be invalid immediately 525 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); 526 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBeNull(); 527 | }); 528 | 529 | it('should handle very large expiration times', () => { 530 | const largeExpirationToken: OAuthTokenResponse = { 531 | access_token: 'large-expiration-token', 532 | token_type: 'bearer', 533 | expires_in: 86400 * 365, // 1 year in seconds 534 | }; 535 | 536 | tokenManager.storeToken(testHostname, testClientId, largeExpirationToken); 537 | 538 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); 539 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('large-expiration-token'); 540 | 541 | const expiration = tokenManager.getTokenExpiration(testHostname, testClientId); 542 | expect(expiration).toBeInstanceOf(Date); 543 | }); 544 | 545 | it('should handle empty strings for hostname and clientId', () => { 546 | const tokenResponse: OAuthTokenResponse = { 547 | access_token: 'empty-string-token', 548 | token_type: 'bearer', 549 | expires_in: 3600, 550 | }; 551 | 552 | // Should not throw errors with empty strings 553 | expect(() => { 554 | tokenManager.storeToken('', '', tokenResponse); 555 | }).not.toThrow(); 556 | 557 | expect(() => { 558 | tokenManager.getValidToken('', ''); 559 | }).not.toThrow(); 560 | 561 | expect(() => { 562 | tokenManager.isTokenValid('', ''); 563 | }).not.toThrow(); 564 | 565 | expect(() => { 566 | tokenManager.clearToken('', ''); 567 | }).not.toThrow(); 568 | 569 | expect(() => { 570 | tokenManager.getTokenExpiration('', ''); 571 | }).not.toThrow(); 572 | }); 573 | 574 | it('should handle fractional expiration times', () => { 575 | const fractionalExpirationToken: OAuthTokenResponse = { 576 | access_token: 'fractional-expiration-token', 577 | token_type: 'bearer', 578 | expires_in: 3600.5, // 1 hour and 30 minutes 579 | }; 580 | 581 | tokenManager.storeToken(testHostname, testClientId, fractionalExpirationToken); 582 | 583 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); 584 | expect(tokenManager.getValidToken(testHostname, testClientId)).toBe('fractional-expiration-token'); 585 | }); 586 | }); 587 | 588 | describe('Concurrency and state consistency', () => { 589 | it('should maintain consistent state with rapid operations', () => { 590 | const tokenResponse: OAuthTokenResponse = { 591 | access_token: 'rapid-ops-token', 592 | token_type: 'bearer', 593 | expires_in: 3600, 594 | }; 595 | 596 | // Rapid store/clear/check operations 597 | for (let i = 0; i < 100; i++) { 598 | tokenManager.storeToken(testHostname, testClientId, tokenResponse); 599 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(true); 600 | tokenManager.clearToken(testHostname, testClientId); 601 | expect(tokenManager.isTokenValid(testHostname, testClientId)).toBe(false); 602 | } 603 | }); 604 | 605 | it('should handle multiple token storage operations correctly', () => { 606 | const baseToken: OAuthTokenResponse = { 607 | access_token: '', 608 | token_type: 'bearer', 609 | expires_in: 3600, 610 | }; 611 | 612 | // Store multiple tokens rapidly 613 | for (let i = 0; i < 50; i++) { 614 | const token = { ...baseToken, access_token: `token-${i}` }; 615 | tokenManager.storeToken(testHostname, `client-${i}`, token); 616 | } 617 | 618 | // Verify all tokens are stored correctly 619 | for (let i = 0; i < 50; i++) { 620 | expect(tokenManager.getValidToken(testHostname, `client-${i}`)).toBe(`token-${i}`); 621 | } 622 | 623 | // Clear all and verify 624 | tokenManager.clearAllTokens(); 625 | for (let i = 0; i < 50; i++) { 626 | expect(tokenManager.getValidToken(testHostname, `client-${i}`)).toBeNull(); 627 | } 628 | }); 629 | }); 630 | }); 631 | ``` -------------------------------------------------------------------------------- /scripts/convert-docs.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | import axios from 'axios'; 4 | import * as cheerio from 'cheerio'; 5 | import fs from 'fs/promises'; 6 | import path from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | const BASE_URL = 'https://salesforcecommercecloud.github.io/b2c-dev-doc/docs/current/scriptapi/html/api'; 13 | const OUTPUT_DIR = path.join(__dirname, '..', 'docs'); 14 | 15 | // Configuration options 16 | const CONFIG = { 17 | // Limit number of classes per package (0 = no limit) 18 | maxClassesPerPackage: 0, 19 | // Limit number of packages (0 = no limit) 20 | maxPackages: 0, 21 | // Enable debug logging 22 | debug: false, 23 | // Rate limiting settings 24 | rateLimit: { 25 | // Delay between requests in milliseconds 26 | requestDelay: 1000, 27 | // Maximum requests per minute 28 | maxRequestsPerMinute: 30, 29 | // Delay between processing packages in milliseconds 30 | packageDelay: 2000, 31 | // Random jitter to add (0-1 multiplier) 32 | jitter: 0.3 33 | } 34 | }; 35 | 36 | // Override config from command line args 37 | if (process.argv.includes('--test')) { 38 | CONFIG.maxClassesPerPackage = 3; 39 | CONFIG.maxPackages = 2; 40 | CONFIG.debug = true; 41 | } 42 | 43 | if (process.argv.includes('--limit')) { 44 | const limitIndex = process.argv.indexOf('--limit'); 45 | if (limitIndex >= 0 && process.argv[limitIndex + 1]) { 46 | CONFIG.maxClassesPerPackage = parseInt(process.argv[limitIndex + 1]) || 5; 47 | } 48 | } 49 | 50 | if (process.argv.includes('--fast')) { 51 | CONFIG.rateLimit.requestDelay = 500; 52 | CONFIG.rateLimit.maxRequestsPerMinute = 45; 53 | CONFIG.rateLimit.packageDelay = 1000; 54 | console.log('Using fast mode (less conservative rate limiting)'); 55 | } 56 | 57 | if (process.argv.includes('--slow')) { 58 | CONFIG.rateLimit.requestDelay = 2000; 59 | CONFIG.rateLimit.maxRequestsPerMinute = 15; 60 | CONFIG.rateLimit.packageDelay = 5000; 61 | console.log('Using slow mode (very conservative rate limiting)'); 62 | } 63 | 64 | // Ensure output directory exists 65 | async function ensureDir(dir) { 66 | try { 67 | await fs.access(dir); 68 | } catch { 69 | await fs.mkdir(dir, { recursive: true }); 70 | } 71 | } 72 | 73 | // Fetch HTML content from URL with rate limiting 74 | async function fetchHTML(url) { 75 | // Apply rate limiting before making request 76 | await rateLimiter.waitForRateLimit(); 77 | 78 | try { 79 | console.log(`Fetching: ${url}`); 80 | 81 | const response = await axios.get(url, { 82 | headers: { 83 | 'User-Agent': rateLimiter.getRandomUserAgent(), 84 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 85 | 'Accept-Language': 'en-US,en;q=0.5', 86 | 'Accept-Encoding': 'gzip, deflate, br', 87 | 'Connection': 'keep-alive', 88 | 'Upgrade-Insecure-Requests': '1', 89 | 'Sec-Fetch-Dest': 'document', 90 | 'Sec-Fetch-Mode': 'navigate', 91 | 'Sec-Fetch-Site': 'none', 92 | 'Cache-Control': 'max-age=0' 93 | }, 94 | timeout: 30000, // 30 second timeout 95 | validateStatus: function (status) { 96 | return status >= 200 && status < 300; // Accept only 2xx status codes 97 | } 98 | }); 99 | 100 | return response.data; 101 | } catch (error) { 102 | if (error.response && error.response.status === 429) { 103 | console.warn(`Rate limited by server. Waiting 60 seconds before retry...`); 104 | // eslint-disable-next-line no-undef 105 | await new Promise(resolve => setTimeout(resolve, 60000)); 106 | // Retry once after rate limit 107 | try { 108 | const retryResponse = await axios.get(url, { 109 | headers: { 110 | 'User-Agent': rateLimiter.getRandomUserAgent(), 111 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 112 | 'Accept-Language': 'en-US,en;q=0.5', 113 | 'Accept-Encoding': 'gzip, deflate, br', 114 | 'Connection': 'keep-alive', 115 | 'Upgrade-Insecure-Requests': '1', 116 | 'Cache-Control': 'max-age=0' 117 | }, 118 | timeout: 30000 119 | }); 120 | return retryResponse.data; 121 | } catch (retryError) { 122 | console.error(`Retry failed for ${url}:`, retryError.message); 123 | return null; 124 | } 125 | } 126 | 127 | console.error(`Error fetching ${url}:`, error.message); 128 | return null; 129 | } 130 | } 131 | 132 | // Helper function to clean and normalize text 133 | function cleanText(text) { 134 | return text 135 | .replace(/\s+/g, ' ') // Replace multiple whitespace with single space 136 | .replace(/\n\s*\n/g, '\n') // Remove empty lines 137 | .replace(/\t/g, ' ') // Replace tabs with spaces 138 | .trim(); 139 | } 140 | 141 | // Helper function to extract method signature cleanly 142 | function cleanMethodSignature(htmlContent) { 143 | const $ = cheerio.load(`<div>${htmlContent}</div>`); 144 | 145 | // Extract static keyword if present 146 | let signature = ''; 147 | if (htmlContent.includes('static')) { 148 | signature += 'static '; 149 | } 150 | 151 | // Extract method name from emphasis span 152 | const methodName = $('.emphasis').text().trim(); 153 | if (methodName) { 154 | signature += methodName; 155 | } 156 | 157 | // Get the full text and clean it up 158 | const fullText = $.text().replace(/\s+/g, ' ').trim(); 159 | 160 | // Extract parameters and return type using regex 161 | const methodPattern = /(\w+)\s*\(\s*([^)]*)\s*\)\s*:\s*(.+?)$/; 162 | const match = fullText.match(methodPattern); 163 | 164 | if (match) { 165 | const params = match[2].trim(); 166 | const returnType = match[3].trim(); 167 | 168 | // Clean up parameters - remove excessive whitespace and normalize format 169 | let cleanParams = ''; 170 | if (params) { 171 | cleanParams = params 172 | .replace(/\s*:\s*/g, ' : ') // Normalize colons 173 | .replace(/,\s+/g, ', ') // Normalize commas 174 | .replace(/\s+/g, ' ') // Single spaces 175 | .trim(); 176 | } 177 | 178 | signature += `(${cleanParams}) : ${returnType}`; 179 | } else { 180 | // Fallback - try to extract just the method call part 181 | const simpleMatch = fullText.match(/(\w+\s*\([^)]*\))/); 182 | if (simpleMatch) { 183 | const methodCall = simpleMatch[1].replace(/\s+/g, ' ').trim(); 184 | signature += methodCall; 185 | } 186 | } 187 | 188 | return signature.replace(/\s+/g, ' ').trim(); 189 | } 190 | 191 | // Convert HTML to Markdown 192 | function htmlToMarkdown(html) { 193 | const $ = cheerio.load(html); 194 | 195 | // Remove script tags and other unwanted elements 196 | $('script, style, .banner, .site-footer, header').remove(); 197 | 198 | let markdown = ''; 199 | 200 | // Find the main class content 201 | const classDiv = $('[id^="class_"]').first(); 202 | 203 | if (classDiv.length === 0) { 204 | console.warn('No class content found'); 205 | return ''; 206 | } 207 | 208 | // Extract package name 209 | const packageName = classDiv.find('.packageName').text().trim(); 210 | if (packageName) { 211 | markdown += `## Package: ${packageName}\n\n`; 212 | } 213 | 214 | // Extract class name 215 | const className = cleanText(classDiv.find('.className').text()); 216 | if (className) { 217 | markdown += `# ${className}\n\n`; 218 | } 219 | 220 | // Extract inheritance hierarchy 221 | const hierarchy = classDiv.find('.hierarchy'); 222 | if (hierarchy.length > 0) { 223 | markdown += '## Inheritance Hierarchy\n\n'; 224 | hierarchy.find('div').each((i, div) => { 225 | const $div = $(div); 226 | const text = cleanText($div.text()); 227 | if (text) { 228 | // Calculate indentation based on left position 229 | const leftStyle = $div.attr('style') || ''; 230 | const leftMatch = leftStyle.match(/left:\s*(\d+)%/); 231 | const level = leftMatch ? Math.floor(parseInt(leftMatch[1]) / 3) : 0; 232 | const indent = ' '.repeat(level); 233 | markdown += `${indent}- ${text}\n`; 234 | } 235 | }); 236 | markdown += '\n'; 237 | } 238 | 239 | // Extract class description 240 | const description = cleanText(classDiv.find('.classSummary .description').text()); 241 | if (description) { 242 | markdown += '## Description\n\n'; 243 | markdown += `${description}\n\n`; 244 | } 245 | 246 | // Process sections (Properties, Methods, etc.) 247 | classDiv.find('.section').each((i, section) => { 248 | const $section = $(section); 249 | const header = cleanText($section.find('.header').first().text()); 250 | 251 | if (!header) return; 252 | 253 | markdown += `## ${header}\n\n`; 254 | 255 | // Handle different section types 256 | if (header === 'Constants') { 257 | $section.find('.summaryItem').each((j, item) => { 258 | const $item = $(item); 259 | 260 | // Extract constant name from the text content before the colon 261 | const spanContent = $item.find('span').first(); 262 | const fullText = spanContent.text(); 263 | 264 | // Parse the constant line: "CONSTANT_NAME : Type = value" 265 | const constMatch = fullText.match(/^([A-Z_][A-Z0-9_]*)\s*:/); 266 | const constName = constMatch ? constMatch[1].trim() : ''; 267 | 268 | // Extract type - look for the type link after the colon 269 | const typeLink = spanContent.find('a span').first(); 270 | const constType = typeLink.text().trim(); 271 | 272 | // Extract value if present (after the = sign) 273 | const valueMatch = fullText.match(/=\s*([^]+?)(?=\s|$)/); 274 | const constValue = valueMatch ? valueMatch[1].trim() : ''; 275 | 276 | const desc = cleanText($item.find('.description').text()); 277 | 278 | if (constName) { 279 | markdown += `### ${constName}\n\n`; 280 | if (constType) { 281 | let typeInfo = `**Type:** ${constType}`; 282 | if (constValue) { 283 | typeInfo += ` = ${constValue}`; 284 | } 285 | markdown += `${typeInfo}\n\n`; 286 | } 287 | if (desc) { 288 | markdown += `${desc}\n\n`; 289 | } 290 | } 291 | }); 292 | } else if (header === 'Properties') { 293 | $section.find('.summaryItem').each((j, item) => { 294 | const $item = $(item); 295 | 296 | // For properties, parse the span content more carefully 297 | const spanContent = $item.find('span').first(); 298 | const fullText = spanContent.text(); 299 | 300 | // Extract property name (text before the first colon) 301 | const nameMatch = fullText.match(/^([^\s:]+)\s*:/); 302 | const propName = nameMatch ? nameMatch[1].trim() : ''; 303 | 304 | // Extract type - look for linked type names 305 | const typeLinks = spanContent.find('a'); 306 | let propType = ''; 307 | typeLinks.each((idx, link) => { 308 | const $link = $(link); 309 | const linkText = $link.text().trim(); 310 | // Skip if it's an anchor link (starts with #) 311 | if (!$link.attr('href')?.startsWith('#') && linkText) { 312 | propType = linkText; 313 | return false; // break the loop 314 | } 315 | }); 316 | 317 | // If no type found in links, try to extract from text pattern 318 | if (!propType) { 319 | const typeMatch = fullText.match(/:\s*([A-Za-z][A-Za-z0-9]*)/); 320 | if (typeMatch) { 321 | propType = typeMatch[1]; 322 | } 323 | } 324 | 325 | // Check for modifiers 326 | const isStatic = fullText.includes('static'); 327 | const isReadOnly = fullText.includes('(Read Only)'); 328 | 329 | const desc = $item.find('.description').text().trim(); 330 | 331 | if (propName) { 332 | markdown += `### ${propName}\n\n`; 333 | if (propType) { 334 | let typeInfo = `**Type:** ${propType}`; 335 | if (isStatic) typeInfo += ' (Static)'; 336 | if (isReadOnly) typeInfo += ' (Read Only)'; 337 | markdown += `${typeInfo}\n\n`; 338 | } 339 | if (desc) { 340 | markdown += `${desc}\n\n`; 341 | } 342 | } 343 | }); 344 | } else if (header === 'Method Summary' || header.includes('Method')) { 345 | $section.find('.summaryItem').each((j, item) => { 346 | const $item = $(item); 347 | 348 | // Extract method name from emphasis link 349 | const methodLink = $item.find('.emphasis a').first(); 350 | const methodName = methodLink.text().trim(); 351 | 352 | // Get clean signature 353 | const signature = cleanMethodSignature($item.find('span').first().html()); 354 | 355 | const desc = cleanText($item.find('.description').text()); 356 | 357 | if (methodName && !desc.startsWith('This class does not have')) { 358 | markdown += `### ${methodName}\n\n`; 359 | if (signature) { 360 | markdown += `**Signature:** \`${signature}\`\n\n`; 361 | } 362 | if (desc) { 363 | markdown += `${desc}\n\n`; 364 | } 365 | } 366 | }); 367 | } else if (header === 'Constructor Summary') { 368 | $section.find('.summaryItem').each((j, item) => { 369 | const $item = $(item); 370 | const constructorText = cleanText($item.text()); 371 | if (constructorText && !constructorText.includes('This class does not have')) { 372 | markdown += `${constructorText}\n\n`; 373 | } 374 | }); 375 | } else { 376 | // Handle inherited methods and other sections 377 | const content = cleanText($section.find('.summaryItem').text()); 378 | if (content && !content.includes('This class does not have')) { 379 | markdown += `${content}\n\n`; 380 | } 381 | } 382 | }); 383 | 384 | // Process detailed method descriptions 385 | const methodDetails = classDiv.find('.section').filter((i, el) => { 386 | return $(el).find('.header').text().trim() === 'Method Detail'; 387 | }); 388 | 389 | if (methodDetails.length > 0) { 390 | markdown += '## Method Details\n\n'; 391 | 392 | methodDetails.find('.detailItem').each((i, item) => { 393 | const $item = $(item); 394 | 395 | const methodName = cleanText($item.find('.detailName').text()); 396 | const signature = cleanText($item.find('.detailSignature').text()); 397 | const description = cleanText($item.find('.description').first().text()); 398 | 399 | if (methodName) { 400 | markdown += `### ${methodName}\n\n`; 401 | 402 | if (signature) { 403 | markdown += `**Signature:** \`${signature}\`\n\n`; 404 | } 405 | 406 | if (description) { 407 | markdown += `**Description:** ${description}\n\n`; 408 | } 409 | 410 | // Process parameters 411 | $item.find('.parameters').each((j, param) => { 412 | const $param = $(param); 413 | const title = cleanText($param.find('.parameterTitle').text()); 414 | 415 | if (title) { 416 | markdown += `**${title}**\n\n`; 417 | 418 | $param.find('.parameterDetail').each((k, detail) => { 419 | const $detail = $(detail); 420 | const paramName = cleanText($detail.find('.parameterName').text()); 421 | const paramDesc = cleanText($detail.find('.parameterDesc').text()); 422 | 423 | if (paramName && paramDesc) { 424 | markdown += `- \`${paramName}\`: ${paramDesc}\n`; 425 | } else { 426 | const detailText = cleanText($detail.text()); 427 | if (detailText) { 428 | markdown += `${detailText}\n`; 429 | } 430 | } 431 | }); 432 | 433 | markdown += '\n'; 434 | } 435 | }); 436 | 437 | markdown += '---\n\n'; 438 | } 439 | }); 440 | } 441 | 442 | // Clean up the final markdown 443 | return markdown 444 | .replace(/\n{3,}/g, '\n\n') // Replace 3+ newlines with 2 445 | .trim(); 446 | } 447 | 448 | // Rate limiting utilities 449 | class RateLimiter { 450 | constructor(config) { 451 | this.config = config; 452 | this.requestTimes = []; 453 | this.userAgents = [ 454 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 455 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15', 456 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 457 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0' 458 | ]; 459 | this.currentUserAgentIndex = 0; 460 | } 461 | 462 | async waitForRateLimit() { 463 | const now = Date.now(); 464 | 465 | // Remove requests older than 1 minute 466 | this.requestTimes = this.requestTimes.filter(time => now - time < 60000); 467 | 468 | // Check if we're at the rate limit 469 | if (this.requestTimes.length >= this.config.maxRequestsPerMinute) { 470 | const oldestRequest = Math.min(...this.requestTimes); 471 | const waitTime = 60000 - (now - oldestRequest) + 1000; // Add 1 second buffer 472 | 473 | if (waitTime > 0) { 474 | console.log(`Rate limit reached. Waiting ${Math.ceil(waitTime / 1000)} seconds...`); 475 | // eslint-disable-next-line no-undef 476 | await new Promise(resolve => setTimeout(resolve, waitTime)); 477 | } 478 | } 479 | 480 | // Apply base delay with jitter 481 | const baseDelay = this.config.requestDelay; 482 | const jitter = Math.random() * this.config.jitter * baseDelay; 483 | const totalDelay = baseDelay + jitter; 484 | 485 | if (CONFIG.debug) { 486 | console.log(`Applying delay: ${Math.ceil(totalDelay)}ms`); 487 | } 488 | 489 | // eslint-disable-next-line no-undef 490 | await new Promise(resolve => setTimeout(resolve, totalDelay)); 491 | 492 | // Record this request 493 | this.requestTimes.push(Date.now()); 494 | } 495 | 496 | getRandomUserAgent() { 497 | const userAgent = this.userAgents[this.currentUserAgentIndex]; 498 | this.currentUserAgentIndex = (this.currentUserAgentIndex + 1) % this.userAgents.length; 499 | return userAgent; 500 | } 501 | } 502 | 503 | // Initialize rate limiter 504 | const rateLimiter = new RateLimiter(CONFIG.rateLimit); 505 | 506 | // Get all packages from overview page 507 | async function getPackages() { 508 | const overviewHTML = await fetchHTML(`${BASE_URL}/overview.html`); 509 | if (!overviewHTML) return []; 510 | 511 | const $ = cheerio.load(overviewHTML); 512 | const packages = []; 513 | 514 | $('.summaryItem a').each((i, element) => { 515 | const href = $(element).attr('href'); 516 | const name = $(element).text().trim(); 517 | if (href && href.startsWith('package_')) { 518 | packages.push({ 519 | name, 520 | href, 521 | id: href.replace('.html', '').replace('package_', '') 522 | }); 523 | } 524 | }); 525 | 526 | return packages; 527 | } 528 | 529 | // Get all classes from a package page 530 | async function getClassesFromPackage(packageHref) { 531 | const packageHTML = await fetchHTML(`${BASE_URL}/${packageHref}`); 532 | if (!packageHTML) return []; 533 | 534 | const $ = cheerio.load(packageHTML); 535 | const classes = []; 536 | 537 | $('.classesName a').each((i, element) => { 538 | const href = $(element).attr('href'); 539 | const name = $(element).text().trim(); 540 | if (href && href.startsWith('class_')) { 541 | classes.push({ 542 | name, 543 | href, 544 | id: href.replace('.html', '').replace('class_', '') 545 | }); 546 | } 547 | }); 548 | 549 | return classes; 550 | } 551 | 552 | // Convert a single class to markdown 553 | async function convertClass(classInfo, packageName) { 554 | const classHTML = await fetchHTML(`${BASE_URL}/${classInfo.href}`); 555 | if (!classHTML) return; 556 | 557 | const markdown = htmlToMarkdown(classHTML); 558 | if (!markdown.trim()) { 559 | console.warn(`No content generated for ${classInfo.name}`); 560 | return; 561 | } 562 | 563 | // Create package directory 564 | const packageDir = path.join(OUTPUT_DIR, packageName.replace('.', '_')); 565 | await ensureDir(packageDir); 566 | 567 | // Write markdown file 568 | const filename = `${classInfo.name}.md`; 569 | const filepath = path.join(packageDir, filename); 570 | 571 | await fs.writeFile(filepath, markdown, 'utf8'); 572 | console.log(`✓ Converted: ${packageName}.${classInfo.name} -> ${filepath}`); 573 | } 574 | 575 | // Main function 576 | async function main() { 577 | console.log('Starting SFCC documentation conversion...'); 578 | 579 | if (CONFIG.maxPackages > 0 || CONFIG.maxClassesPerPackage > 0) { 580 | console.log('Running in limited mode:'); 581 | if (CONFIG.maxPackages > 0) console.log(`- Max packages: ${CONFIG.maxPackages}`); 582 | if (CONFIG.maxClassesPerPackage > 0) console.log(`- Max classes per package: ${CONFIG.maxClassesPerPackage}`); 583 | } 584 | 585 | await ensureDir(OUTPUT_DIR); 586 | 587 | // Get all packages 588 | const packages = await getPackages(); 589 | console.log(`Found ${packages.length} packages`); 590 | 591 | // Limit packages if configured 592 | const packageLimit = CONFIG.maxPackages > 0 ? CONFIG.maxPackages : packages.length; 593 | const packagesToProcess = packages.slice(0, packageLimit); 594 | 595 | for (const pkg of packagesToProcess) { 596 | console.log(`\nProcessing package: ${pkg.name}`); 597 | 598 | // Get classes in this package 599 | const classes = await getClassesFromPackage(pkg.href); 600 | console.log(`Found ${classes.length} classes in ${pkg.name}`); 601 | 602 | // Limit classes if configured 603 | const classLimit = CONFIG.maxClassesPerPackage > 0 ? CONFIG.maxClassesPerPackage : classes.length; 604 | const classesToProcess = classes.slice(0, classLimit); 605 | 606 | if (classLimit < classes.length) { 607 | console.log(`Processing first ${classLimit} classes only...`); 608 | } 609 | 610 | // Convert each class 611 | for (const cls of classesToProcess) { 612 | await convertClass(cls, pkg.name); 613 | } 614 | 615 | // Delay between processing packages 616 | if (pkg !== packagesToProcess[packagesToProcess.length - 1]) { 617 | const packageDelay = CONFIG.rateLimit.packageDelay; 618 | const jitter = Math.random() * CONFIG.rateLimit.jitter * packageDelay; 619 | console.log(`Waiting ${Math.ceil((packageDelay + jitter) / 1000)}s before next package...`); 620 | // eslint-disable-next-line no-undef 621 | await new Promise(resolve => setTimeout(resolve, packageDelay + jitter)); 622 | } 623 | } 624 | 625 | console.log('\n✓ Documentation conversion completed!'); 626 | } 627 | 628 | // Handle command line execution 629 | if (import.meta.url === `file://${process.argv[1]}`) { 630 | main().catch(console.error); 631 | } 632 | 633 | export { main, convertClass, getPackages, getClassesFromPackage }; 634 | ```