This is page 37 of 61. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .DS_Store ├── .github │ ├── dependabot.yml │ ├── instructions │ │ ├── mcp-node-tests.instructions.md │ │ └── mcp-yml-tests.instructions.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bug_fix.md │ │ ├── documentation.md │ │ └── new_tool.md │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── deploy-pages.yml │ ├── publish.yml │ └── update-docs.yml ├── .gitignore ├── .husky │ └── pre-commit ├── aegis.config.docs-only.json ├── aegis.config.json ├── aegis.config.with-dw.json ├── AGENTS.md ├── ai-instructions │ ├── claude-desktop │ │ └── claude_custom_instructions.md │ ├── cursor │ │ └── .cursor │ │ └── rules │ │ ├── debugging-workflows.mdc │ │ ├── hooks-development.mdc │ │ ├── isml-templates.mdc │ │ ├── job-framework.mdc │ │ ├── performance-optimization.mdc │ │ ├── scapi-endpoints.mdc │ │ ├── security-patterns.mdc │ │ ├── sfcc-development.mdc │ │ ├── sfra-controllers.mdc │ │ ├── sfra-models.mdc │ │ ├── system-objects.mdc │ │ └── testing-patterns.mdc │ └── github-copilot │ └── copilot-instructions.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── docs │ ├── best-practices │ │ ├── cartridge_creation.md │ │ ├── isml_templates.md │ │ ├── job_framework.md │ │ ├── localserviceregistry.md │ │ ├── ocapi_hooks.md │ │ ├── performance.md │ │ ├── scapi_custom_endpoint.md │ │ ├── scapi_hooks.md │ │ ├── security.md │ │ ├── sfra_client_side_js.md │ │ ├── sfra_controllers.md │ │ ├── sfra_models.md │ │ └── sfra_scss.md │ ├── dw_campaign │ │ ├── ABTest.md │ │ ├── ABTestMgr.md │ │ ├── ABTestSegment.md │ │ ├── AmountDiscount.md │ │ ├── ApproachingDiscount.md │ │ ├── BonusChoiceDiscount.md │ │ ├── BonusDiscount.md │ │ ├── Campaign.md │ │ ├── CampaignMgr.md │ │ ├── CampaignStatusCodes.md │ │ ├── Coupon.md │ │ ├── CouponMgr.md │ │ ├── CouponRedemption.md │ │ ├── CouponStatusCodes.md │ │ ├── Discount.md │ │ ├── DiscountPlan.md │ │ ├── FixedPriceDiscount.md │ │ ├── FixedPriceShippingDiscount.md │ │ ├── FreeDiscount.md │ │ ├── FreeShippingDiscount.md │ │ ├── PercentageDiscount.md │ │ ├── PercentageOptionDiscount.md │ │ ├── PriceBookPriceDiscount.md │ │ ├── Promotion.md │ │ ├── PromotionMgr.md │ │ ├── PromotionPlan.md │ │ ├── SlotContent.md │ │ ├── SourceCodeGroup.md │ │ ├── SourceCodeInfo.md │ │ ├── SourceCodeStatusCodes.md │ │ └── TotalFixedPriceDiscount.md │ ├── dw_catalog │ │ ├── Catalog.md │ │ ├── CatalogMgr.md │ │ ├── Category.md │ │ ├── CategoryAssignment.md │ │ ├── CategoryLink.md │ │ ├── PriceBook.md │ │ ├── PriceBookMgr.md │ │ ├── Product.md │ │ ├── ProductActiveData.md │ │ ├── ProductAttributeModel.md │ │ ├── ProductAvailabilityLevels.md │ │ ├── ProductAvailabilityModel.md │ │ ├── ProductInventoryList.md │ │ ├── ProductInventoryMgr.md │ │ ├── ProductInventoryRecord.md │ │ ├── ProductLink.md │ │ ├── ProductMgr.md │ │ ├── ProductOption.md │ │ ├── ProductOptionModel.md │ │ ├── ProductOptionValue.md │ │ ├── ProductPriceInfo.md │ │ ├── ProductPriceModel.md │ │ ├── ProductPriceTable.md │ │ ├── ProductSearchHit.md │ │ ├── ProductSearchModel.md │ │ ├── ProductSearchRefinementDefinition.md │ │ ├── ProductSearchRefinements.md │ │ ├── ProductSearchRefinementValue.md │ │ ├── ProductVariationAttribute.md │ │ ├── ProductVariationAttributeValue.md │ │ ├── ProductVariationModel.md │ │ ├── Recommendation.md │ │ ├── SearchModel.md │ │ ├── SearchRefinementDefinition.md │ │ ├── SearchRefinements.md │ │ ├── SearchRefinementValue.md │ │ ├── SortingOption.md │ │ ├── SortingRule.md │ │ ├── Store.md │ │ ├── StoreGroup.md │ │ ├── StoreInventoryFilter.md │ │ ├── StoreInventoryFilterValue.md │ │ ├── StoreMgr.md │ │ ├── Variant.md │ │ └── VariationGroup.md │ ├── dw_content │ │ ├── Content.md │ │ ├── ContentMgr.md │ │ ├── ContentSearchModel.md │ │ ├── ContentSearchRefinementDefinition.md │ │ ├── ContentSearchRefinements.md │ │ ├── ContentSearchRefinementValue.md │ │ ├── Folder.md │ │ ├── Library.md │ │ ├── MarkupText.md │ │ └── MediaFile.md │ ├── dw_crypto │ │ ├── CertificateRef.md │ │ ├── CertificateUtils.md │ │ ├── Cipher.md │ │ ├── Encoding.md │ │ ├── JWE.md │ │ ├── JWEHeader.md │ │ ├── JWS.md │ │ ├── JWSHeader.md │ │ ├── KeyRef.md │ │ ├── Mac.md │ │ ├── MessageDigest.md │ │ ├── SecureRandom.md │ │ ├── Signature.md │ │ ├── WeakCipher.md │ │ ├── WeakMac.md │ │ ├── WeakMessageDigest.md │ │ ├── WeakSignature.md │ │ └── X509Certificate.md │ ├── dw_customer │ │ ├── AddressBook.md │ │ ├── AgentUserMgr.md │ │ ├── AgentUserStatusCodes.md │ │ ├── AuthenticationStatus.md │ │ ├── Credentials.md │ │ ├── Customer.md │ │ ├── CustomerActiveData.md │ │ ├── CustomerAddress.md │ │ ├── CustomerCDPData.md │ │ ├── CustomerContextMgr.md │ │ ├── CustomerGroup.md │ │ ├── CustomerList.md │ │ ├── CustomerMgr.md │ │ ├── CustomerPasswordConstraints.md │ │ ├── CustomerPaymentInstrument.md │ │ ├── CustomerStatusCodes.md │ │ ├── EncryptedObject.md │ │ ├── ExternalProfile.md │ │ ├── OrderHistory.md │ │ ├── ProductList.md │ │ ├── ProductListItem.md │ │ ├── ProductListItemPurchase.md │ │ ├── ProductListMgr.md │ │ ├── ProductListRegistrant.md │ │ ├── Profile.md │ │ └── Wallet.md │ ├── dw_extensions.applepay │ │ ├── ApplePayHookResult.md │ │ └── ApplePayHooks.md │ ├── dw_extensions.facebook │ │ ├── FacebookFeedHooks.md │ │ └── FacebookProduct.md │ ├── dw_extensions.paymentrequest │ │ ├── PaymentRequestHookResult.md │ │ └── PaymentRequestHooks.md │ ├── dw_extensions.payments │ │ ├── SalesforceBancontactPaymentDetails.md │ │ ├── SalesforceCardPaymentDetails.md │ │ ├── SalesforceEpsPaymentDetails.md │ │ ├── SalesforceIdealPaymentDetails.md │ │ ├── SalesforceKlarnaPaymentDetails.md │ │ ├── SalesforcePaymentDetails.md │ │ ├── SalesforcePaymentIntent.md │ │ ├── SalesforcePaymentMethod.md │ │ ├── SalesforcePaymentRequest.md │ │ ├── SalesforcePaymentsHooks.md │ │ ├── SalesforcePaymentsMgr.md │ │ ├── SalesforcePaymentsSiteConfiguration.md │ │ ├── SalesforcePayPalOrder.md │ │ ├── SalesforcePayPalOrderAddress.md │ │ ├── SalesforcePayPalOrderPayer.md │ │ ├── SalesforcePayPalPaymentDetails.md │ │ ├── SalesforceSepaDebitPaymentDetails.md │ │ └── SalesforceVenmoPaymentDetails.md │ ├── dw_extensions.pinterest │ │ ├── PinterestAvailability.md │ │ ├── PinterestFeedHooks.md │ │ ├── PinterestOrder.md │ │ ├── PinterestOrderHooks.md │ │ └── PinterestProduct.md │ ├── dw_io │ │ ├── CSVStreamReader.md │ │ ├── CSVStreamWriter.md │ │ ├── File.md │ │ ├── FileReader.md │ │ ├── FileWriter.md │ │ ├── InputStream.md │ │ ├── OutputStream.md │ │ ├── PrintWriter.md │ │ ├── RandomAccessFileReader.md │ │ ├── Reader.md │ │ ├── StringWriter.md │ │ ├── Writer.md │ │ ├── XMLIndentingStreamWriter.md │ │ ├── XMLStreamConstants.md │ │ ├── XMLStreamReader.md │ │ └── XMLStreamWriter.md │ ├── dw_job │ │ ├── JobExecution.md │ │ └── JobStepExecution.md │ ├── dw_net │ │ ├── FTPClient.md │ │ ├── FTPFileInfo.md │ │ ├── HTTPClient.md │ │ ├── HTTPRequestPart.md │ │ ├── Mail.md │ │ ├── SFTPClient.md │ │ ├── SFTPFileInfo.md │ │ ├── WebDAVClient.md │ │ └── WebDAVFileInfo.md │ ├── dw_object │ │ ├── ActiveData.md │ │ ├── CustomAttributes.md │ │ ├── CustomObject.md │ │ ├── CustomObjectMgr.md │ │ ├── Extensible.md │ │ ├── ExtensibleObject.md │ │ ├── Note.md │ │ ├── ObjectAttributeDefinition.md │ │ ├── ObjectAttributeGroup.md │ │ ├── ObjectAttributeValueDefinition.md │ │ ├── ObjectTypeDefinition.md │ │ ├── PersistentObject.md │ │ ├── SimpleExtensible.md │ │ └── SystemObjectMgr.md │ ├── dw_order │ │ ├── AbstractItem.md │ │ ├── AbstractItemCtnr.md │ │ ├── Appeasement.md │ │ ├── AppeasementItem.md │ │ ├── Basket.md │ │ ├── BasketMgr.md │ │ ├── BonusDiscountLineItem.md │ │ ├── CouponLineItem.md │ │ ├── CreateAgentBasketLimitExceededException.md │ │ ├── CreateBasketFromOrderException.md │ │ ├── CreateCouponLineItemException.md │ │ ├── CreateOrderException.md │ │ ├── CreateTemporaryBasketLimitExceededException.md │ │ ├── GiftCertificate.md │ │ ├── GiftCertificateLineItem.md │ │ ├── GiftCertificateMgr.md │ │ ├── GiftCertificateStatusCodes.md │ │ ├── Invoice.md │ │ ├── InvoiceItem.md │ │ ├── LineItem.md │ │ ├── LineItemCtnr.md │ │ ├── Order.md │ │ ├── OrderAddress.md │ │ ├── OrderItem.md │ │ ├── OrderMgr.md │ │ ├── OrderPaymentInstrument.md │ │ ├── OrderProcessStatusCodes.md │ │ ├── PaymentCard.md │ │ ├── PaymentInstrument.md │ │ ├── PaymentMethod.md │ │ ├── PaymentMgr.md │ │ ├── PaymentProcessor.md │ │ ├── PaymentStatusCodes.md │ │ ├── PaymentTransaction.md │ │ ├── PriceAdjustment.md │ │ ├── PriceAdjustmentLimitTypes.md │ │ ├── ProductLineItem.md │ │ ├── ProductShippingCost.md │ │ ├── ProductShippingLineItem.md │ │ ├── ProductShippingModel.md │ │ ├── Return.md │ │ ├── ReturnCase.md │ │ ├── ReturnCaseItem.md │ │ ├── ReturnItem.md │ │ ├── Shipment.md │ │ ├── ShipmentShippingCost.md │ │ ├── ShipmentShippingModel.md │ │ ├── ShippingLineItem.md │ │ ├── ShippingLocation.md │ │ ├── ShippingMethod.md │ │ ├── ShippingMgr.md │ │ ├── ShippingOrder.md │ │ ├── ShippingOrderItem.md │ │ ├── SumItem.md │ │ ├── TaxGroup.md │ │ ├── TaxItem.md │ │ ├── TaxMgr.md │ │ ├── TrackingInfo.md │ │ └── TrackingRef.md │ ├── dw_order.hooks │ │ ├── CalculateHooks.md │ │ ├── OrderHooks.md │ │ ├── PaymentHooks.md │ │ ├── ReturnHooks.md │ │ └── ShippingOrderHooks.md │ ├── dw_rpc │ │ ├── SOAPUtil.md │ │ ├── Stub.md │ │ └── WebReference.md │ ├── dw_suggest │ │ ├── BrandSuggestions.md │ │ ├── CategorySuggestions.md │ │ ├── ContentSuggestions.md │ │ ├── CustomSuggestions.md │ │ ├── ProductSuggestions.md │ │ ├── SearchPhraseSuggestions.md │ │ ├── SuggestedCategory.md │ │ ├── SuggestedContent.md │ │ ├── SuggestedPhrase.md │ │ ├── SuggestedProduct.md │ │ ├── SuggestedTerm.md │ │ ├── SuggestedTerms.md │ │ ├── Suggestions.md │ │ └── SuggestModel.md │ ├── dw_svc │ │ ├── FTPService.md │ │ ├── FTPServiceDefinition.md │ │ ├── HTTPFormService.md │ │ ├── HTTPFormServiceDefinition.md │ │ ├── HTTPService.md │ │ ├── HTTPServiceDefinition.md │ │ ├── LocalServiceRegistry.md │ │ ├── Result.md │ │ ├── Service.md │ │ ├── ServiceCallback.md │ │ ├── ServiceConfig.md │ │ ├── ServiceCredential.md │ │ ├── ServiceDefinition.md │ │ ├── ServiceProfile.md │ │ ├── ServiceRegistry.md │ │ ├── SOAPService.md │ │ └── SOAPServiceDefinition.md │ ├── dw_system │ │ ├── AgentUserStatusCodes.md │ │ ├── Cache.md │ │ ├── CacheMgr.md │ │ ├── HookMgr.md │ │ ├── InternalObject.md │ │ ├── JobProcessMonitor.md │ │ ├── Log.md │ │ ├── Logger.md │ │ ├── LogNDC.md │ │ ├── OrganizationPreferences.md │ │ ├── Pipeline.md │ │ ├── PipelineDictionary.md │ │ ├── RemoteInclude.md │ │ ├── Request.md │ │ ├── RequestHooks.md │ │ ├── Response.md │ │ ├── RESTErrorResponse.md │ │ ├── RESTResponseMgr.md │ │ ├── RESTSuccessResponse.md │ │ ├── SearchStatus.md │ │ ├── Session.md │ │ ├── Site.md │ │ ├── SitePreferences.md │ │ ├── Status.md │ │ ├── StatusItem.md │ │ ├── System.md │ │ └── Transaction.md │ ├── dw_util │ │ ├── ArrayList.md │ │ ├── Assert.md │ │ ├── BigInteger.md │ │ ├── Bytes.md │ │ ├── Calendar.md │ │ ├── Collection.md │ │ ├── Currency.md │ │ ├── DateUtils.md │ │ ├── Decimal.md │ │ ├── FilteringCollection.md │ │ ├── Geolocation.md │ │ ├── HashMap.md │ │ ├── HashSet.md │ │ ├── Iterator.md │ │ ├── LinkedHashMap.md │ │ ├── LinkedHashSet.md │ │ ├── List.md │ │ ├── Locale.md │ │ ├── Map.md │ │ ├── MapEntry.md │ │ ├── MappingKey.md │ │ ├── MappingMgr.md │ │ ├── PropertyComparator.md │ │ ├── SecureEncoder.md │ │ ├── SecureFilter.md │ │ ├── SeekableIterator.md │ │ ├── Set.md │ │ ├── SortedMap.md │ │ ├── SortedSet.md │ │ ├── StringUtils.md │ │ ├── Template.md │ │ └── UUIDUtils.md │ ├── dw_value │ │ ├── EnumValue.md │ │ ├── MimeEncodedText.md │ │ ├── Money.md │ │ └── Quantity.md │ ├── dw_web │ │ ├── ClickStream.md │ │ ├── ClickStreamEntry.md │ │ ├── Cookie.md │ │ ├── Cookies.md │ │ ├── CSRFProtection.md │ │ ├── Form.md │ │ ├── FormAction.md │ │ ├── FormElement.md │ │ ├── FormElementValidationResult.md │ │ ├── FormField.md │ │ ├── FormFieldOption.md │ │ ├── FormFieldOptions.md │ │ ├── FormGroup.md │ │ ├── FormList.md │ │ ├── FormListItem.md │ │ ├── Forms.md │ │ ├── HttpParameter.md │ │ ├── HttpParameterMap.md │ │ ├── LoopIterator.md │ │ ├── PageMetaData.md │ │ ├── PageMetaTag.md │ │ ├── PagingModel.md │ │ ├── Resource.md │ │ ├── URL.md │ │ ├── URLAction.md │ │ ├── URLParameter.md │ │ ├── URLRedirect.md │ │ ├── URLRedirectMgr.md │ │ └── URLUtils.md │ ├── sfra │ │ ├── account.md │ │ ├── address.md │ │ ├── billing.md │ │ ├── cart.md │ │ ├── categories.md │ │ ├── content.md │ │ ├── locale.md │ │ ├── order.md │ │ ├── payment.md │ │ ├── price-default.md │ │ ├── price-range.md │ │ ├── price-tiered.md │ │ ├── product-bundle.md │ │ ├── product-full.md │ │ ├── product-line-items.md │ │ ├── product-search.md │ │ ├── product-tile.md │ │ ├── querystring.md │ │ ├── render.md │ │ ├── request.md │ │ ├── response.md │ │ ├── server.md │ │ ├── shipping.md │ │ ├── store.md │ │ ├── stores.md │ │ └── totals.md │ └── TopLevel │ ├── APIException.md │ ├── arguments.md │ ├── Array.md │ ├── ArrayBuffer.md │ ├── BigInt.md │ ├── Boolean.md │ ├── ConversionError.md │ ├── DataView.md │ ├── Date.md │ ├── Error.md │ ├── ES6Iterator.md │ ├── EvalError.md │ ├── Fault.md │ ├── Float32Array.md │ ├── Float64Array.md │ ├── Function.md │ ├── Generator.md │ ├── global.md │ ├── Int16Array.md │ ├── Int32Array.md │ ├── Int8Array.md │ ├── InternalError.md │ ├── IOError.md │ ├── Iterable.md │ ├── Iterator.md │ ├── JSON.md │ ├── Map.md │ ├── Math.md │ ├── Module.md │ ├── Namespace.md │ ├── Number.md │ ├── Object.md │ ├── QName.md │ ├── RangeError.md │ ├── ReferenceError.md │ ├── RegExp.md │ ├── Set.md │ ├── StopIteration.md │ ├── String.md │ ├── Symbol.md │ ├── SyntaxError.md │ ├── SystemError.md │ ├── TypeError.md │ ├── Uint16Array.md │ ├── Uint32Array.md │ ├── Uint8Array.md │ ├── Uint8ClampedArray.md │ ├── URIError.md │ ├── WeakMap.md │ ├── WeakSet.md │ ├── XML.md │ ├── XMLList.md │ └── XMLStreamError.md ├── docs-site │ ├── .gitignore │ ├── App.tsx │ ├── components │ │ ├── Badge.tsx │ │ ├── BreadcrumbSchema.tsx │ │ ├── CodeBlock.tsx │ │ ├── Collapsible.tsx │ │ ├── ConfigBuilder.tsx │ │ ├── ConfigHero.tsx │ │ ├── ConfigModeTabs.tsx │ │ ├── icons.tsx │ │ ├── Layout.tsx │ │ ├── LightCodeContainer.tsx │ │ ├── NewcomerCTA.tsx │ │ ├── NextStepsStrip.tsx │ │ ├── OnThisPage.tsx │ │ ├── Search.tsx │ │ ├── SEO.tsx │ │ ├── Sidebar.tsx │ │ ├── StructuredData.tsx │ │ ├── ToolCard.tsx │ │ ├── ToolFilters.tsx │ │ ├── Typography.tsx │ │ └── VersionBadge.tsx │ ├── constants.tsx │ ├── index.html │ ├── main.tsx │ ├── metadata.json │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── AIInterfacesPage.tsx │ │ ├── ConfigurationPage.tsx │ │ ├── DevelopmentPage.tsx │ │ ├── ExamplesPage.tsx │ │ ├── FeaturesPage.tsx │ │ ├── HomePage.tsx │ │ ├── SecurityPage.tsx │ │ ├── ToolsPage.tsx │ │ └── TroubleshootingPage.tsx │ ├── postcss.config.js │ ├── public │ │ ├── .well-known │ │ │ └── security.txt │ │ ├── 404.html │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── explain-product-pricing-methods-no-mcp.png │ │ ├── explain-product-pricing-methods.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── llms.txt │ │ ├── robots.txt │ │ ├── site.webmanifest │ │ └── sitemap.xml │ ├── README.md │ ├── scripts │ │ ├── generate-search-index.js │ │ ├── generate-sitemap.js │ │ └── search-dev.js │ ├── src │ │ └── styles │ │ ├── input.css │ │ └── prism-theme.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types.ts │ ├── utils │ │ ├── search.ts │ │ └── toolsData.ts │ └── vite.config.ts ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── convert-docs.js ├── SECURITY.md ├── server.json ├── src │ ├── clients │ │ ├── base │ │ │ ├── http-client.ts │ │ │ ├── oauth-token.ts │ │ │ └── ocapi-auth-client.ts │ │ ├── best-practices-client.ts │ │ ├── cartridge-generation-client.ts │ │ ├── docs │ │ │ ├── class-content-parser.ts │ │ │ ├── class-name-resolver.ts │ │ │ ├── documentation-scanner.ts │ │ │ ├── index.ts │ │ │ └── referenced-types-extractor.ts │ │ ├── docs-client.ts │ │ ├── log-client.ts │ │ ├── logs │ │ │ ├── index.ts │ │ │ ├── log-analyzer.ts │ │ │ ├── log-client.ts │ │ │ ├── log-constants.ts │ │ │ ├── log-file-discovery.ts │ │ │ ├── log-file-reader.ts │ │ │ ├── log-formatter.ts │ │ │ ├── log-processor.ts │ │ │ ├── log-types.ts │ │ │ └── webdav-client-manager.ts │ │ ├── ocapi │ │ │ ├── code-versions-client.ts │ │ │ ├── site-preferences-client.ts │ │ │ └── system-objects-client.ts │ │ ├── ocapi-client.ts │ │ └── sfra-client.ts │ ├── config │ │ ├── configuration-factory.ts │ │ └── dw-json-loader.ts │ ├── core │ │ ├── handlers │ │ │ ├── abstract-log-tool-handler.ts │ │ │ ├── base-handler.ts │ │ │ ├── best-practices-handler.ts │ │ │ ├── cartridge-handler.ts │ │ │ ├── client-factory.ts │ │ │ ├── code-version-handler.ts │ │ │ ├── docs-handler.ts │ │ │ ├── job-log-handler.ts │ │ │ ├── job-log-tool-config.ts │ │ │ ├── log-handler.ts │ │ │ ├── log-tool-config.ts │ │ │ ├── sfra-handler.ts │ │ │ ├── system-object-handler.ts │ │ │ └── validation-helpers.ts │ │ ├── server.ts │ │ └── tool-definitions.ts │ ├── index.ts │ ├── main.ts │ ├── services │ │ ├── file-system-service.ts │ │ ├── index.ts │ │ └── path-service.ts │ ├── tool-configs │ │ ├── best-practices-tool-config.ts │ │ ├── cartridge-tool-config.ts │ │ ├── code-version-tool-config.ts │ │ ├── docs-tool-config.ts │ │ ├── job-log-tool-config.ts │ │ ├── log-tool-config.ts │ │ ├── sfra-tool-config.ts │ │ └── system-object-tool-config.ts │ ├── types │ │ └── types.ts │ └── utils │ ├── cache.ts │ ├── job-log-tool-config.ts │ ├── job-log-utils.ts │ ├── log-cache.ts │ ├── log-tool-config.ts │ ├── log-tool-constants.ts │ ├── log-tool-utils.ts │ ├── logger.ts │ ├── ocapi-url-builder.ts │ ├── path-resolver.ts │ ├── query-builder.ts │ ├── utils.ts │ └── validator.ts ├── tests │ ├── __mocks__ │ │ ├── docs-client.ts │ │ ├── src │ │ │ └── clients │ │ │ └── base │ │ │ └── http-client.js │ │ └── webdav.js │ ├── base-handler.test.ts │ ├── base-http-client.test.ts │ ├── best-practices-handler.test.ts │ ├── cache.test.ts │ ├── cartridge-handler.test.ts │ ├── class-content-parser.test.ts │ ├── class-name-resolver.test.ts │ ├── client-factory.test.ts │ ├── code-version-handler.test.ts │ ├── code-versions-client.test.ts │ ├── config.test.ts │ ├── configuration-factory.test.ts │ ├── docs-handler.test.ts │ ├── documentation-scanner.test.ts │ ├── file-system-service.test.ts │ ├── job-log-handler.test.ts │ ├── job-log-utils.test.ts │ ├── log-client.test.ts │ ├── log-handler.test.ts │ ├── log-processor.test.ts │ ├── logger.test.ts │ ├── mcp │ │ ├── AGENTS.md │ │ ├── node │ │ │ ├── activate-code-version-advanced.full-mode.programmatic.test.js │ │ │ ├── code-versions.full-mode.programmatic.test.js │ │ │ ├── generate-cartridge-structure.docs-only.programmatic.test.js │ │ │ ├── get-available-best-practice-guides.docs-only.programmatic.test.js │ │ │ ├── get-available-sfra-documents.programmatic.test.js │ │ │ ├── get-best-practice-guide.docs-only.programmatic.test.js │ │ │ ├── get-hook-reference.docs-only.programmatic.test.js │ │ │ ├── get-job-execution-summary.full-mode.programmatic.test.js │ │ │ ├── get-job-log-entries.full-mode.programmatic.test.js │ │ │ ├── get-latest-debug.full-mode.programmatic.test.js │ │ │ ├── get-latest-error.full-mode.programmatic.test.js │ │ │ ├── get-latest-info.full-mode.programmatic.test.js │ │ │ ├── get-latest-job-log-files.full-mode.programmatic.test.js │ │ │ ├── get-latest-warn.full-mode.programmatic.test.js │ │ │ ├── get-log-file-contents.full-mode.programmatic.test.js │ │ │ ├── get-sfcc-class-documentation.docs-only.programmatic.test.js │ │ │ ├── get-sfcc-class-info.docs-only.programmatic.test.js │ │ │ ├── get-sfra-categories.docs-only.programmatic.test.js │ │ │ ├── get-sfra-document.programmatic.test.js │ │ │ ├── get-sfra-documents-by-category.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definition.full-mode.programmatic.test.js │ │ │ ├── get-system-object-definitions.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definitions.full-mode.programmatic.test.js │ │ │ ├── list-log-files.full-mode.programmatic.test.js │ │ │ ├── list-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-best-practices.docs-only.programmatic.test.js │ │ │ ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-job-logs-by-name.full-mode.programmatic.test.js │ │ │ ├── search-job-logs.full-mode.programmatic.test.js │ │ │ ├── search-logs.full-mode.programmatic.test.js │ │ │ ├── search-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-sfcc-methods.docs-only.programmatic.test.js │ │ │ ├── search-sfra-documentation.docs-only.programmatic.test.js │ │ │ ├── search-site-preferences.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-groups.full-mode.programmatic.test.js │ │ │ ├── summarize-logs.full-mode.programmatic.test.js │ │ │ ├── tools.docs-only.programmatic.test.js │ │ │ └── tools.full-mode.programmatic.test.js │ │ ├── README.md │ │ ├── test-fixtures │ │ │ └── dw.json │ │ └── yaml │ │ ├── activate-code-version.docs-only.test.mcp.yml │ │ ├── activate-code-version.full-mode.test.mcp.yml │ │ ├── get_latest_error.test.mcp.yml │ │ ├── get-available-best-practice-guides.docs-only.test.mcp.yml │ │ ├── get-available-best-practice-guides.full-mode.test.mcp.yml │ │ ├── get-available-sfra-documents.docs-only.test.mcp.yml │ │ ├── get-available-sfra-documents.full-mode.test.mcp.yml │ │ ├── get-best-practice-guide.docs-only.test.mcp.yml │ │ ├── get-best-practice-guide.full-mode.test.mcp.yml │ │ ├── get-code-versions.docs-only.test.mcp.yml │ │ ├── get-code-versions.full-mode.test.mcp.yml │ │ ├── get-hook-reference.docs-only.test.mcp.yml │ │ ├── get-hook-reference.full-mode.test.mcp.yml │ │ ├── get-job-execution-summary.full-mode.test.mcp.yml │ │ ├── get-job-log-entries.full-mode.test.mcp.yml │ │ ├── get-latest-debug.full-mode.test.mcp.yml │ │ ├── get-latest-error.full-mode.test.mcp.yml │ │ ├── get-latest-info.full-mode.test.mcp.yml │ │ ├── get-latest-job-log-files.full-mode.test.mcp.yml │ │ ├── get-latest-warn.full-mode.test.mcp.yml │ │ ├── get-log-file-contents.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-documentation.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-documentation.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-info.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-info.full-mode.test.mcp.yml │ │ ├── get-sfra-categories.docs-only.test.mcp.yml │ │ ├── get-sfra-categories.full-mode.test.mcp.yml │ │ ├── get-sfra-document.docs-only.test.mcp.yml │ │ ├── get-sfra-document.full-mode.test.mcp.yml │ │ ├── get-sfra-documents-by-category.docs-only.test.mcp.yml │ │ ├── get-sfra-documents-by-category.full-mode.test.mcp.yml │ │ ├── get-system-object-definition.docs-only.test.mcp.yml │ │ ├── get-system-object-definition.full-mode.test.mcp.yml │ │ ├── get-system-object-definitions.docs-only.test.mcp.yml │ │ ├── get-system-object-definitions.full-mode.test.mcp.yml │ │ ├── list-log-files.full-mode.test.mcp.yml │ │ ├── list-sfcc-classes.docs-only.test.mcp.yml │ │ ├── list-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-best-practices.docs-only.test.mcp.yml │ │ ├── search-best-practices.full-mode.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.test.mcp.yml │ │ ├── search-job-logs-by-name.full-mode.test.mcp.yml │ │ ├── search-job-logs.full-mode.test.mcp.yml │ │ ├── search-logs.full-mode.test.mcp.yml │ │ ├── search-sfcc-classes.docs-only.test.mcp.yml │ │ ├── search-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-sfcc-methods.docs-only.test.mcp.yml │ │ ├── search-sfcc-methods.full-mode.test.mcp.yml │ │ ├── search-sfra-documentation.docs-only.test.mcp.yml │ │ ├── search-sfra-documentation.full-mode.test.mcp.yml │ │ ├── search-site-preferences.docs-only.test.mcp.yml │ │ ├── search-site-preferences.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-groups.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-groups.full-mode.test.mcp.yml │ │ ├── summarize-logs.full-mode.test.mcp.yml │ │ ├── tools.docs-only.test.mcp.yml │ │ └── tools.full-mode.test.mcp.yml │ ├── oauth-token.test.ts │ ├── ocapi-auth-client.test.ts │ ├── ocapi-client.test.ts │ ├── path-service.test.ts │ ├── query-builder.test.ts │ ├── referenced-types-extractor.test.ts │ ├── servers │ │ ├── sfcc-mock-server │ │ │ ├── mock-data │ │ │ │ └── ocapi │ │ │ │ ├── code-versions.json │ │ │ │ ├── custom-object-attributes-customapi.json │ │ │ │ ├── custom-object-attributes-globalsettings.json │ │ │ │ ├── custom-object-attributes-versionhistory.json │ │ │ │ ├── site-preferences-ccv.json │ │ │ │ ├── site-preferences-fastforward.json │ │ │ │ ├── site-preferences-sfra.json │ │ │ │ ├── site-preferences-storefront.json │ │ │ │ ├── site-preferences-system.json │ │ │ │ ├── system-object-attribute-groups-campaign.json │ │ │ │ ├── system-object-attribute-groups-category.json │ │ │ │ ├── system-object-attribute-groups-order.json │ │ │ │ ├── system-object-attribute-groups-product.json │ │ │ │ ├── system-object-attribute-groups-sitepreferences.json │ │ │ │ ├── system-object-attributes-customeraddress.json │ │ │ │ ├── system-object-attributes-product-expanded.json │ │ │ │ ├── system-object-attributes-product.json │ │ │ │ ├── system-object-definition-category.json │ │ │ │ ├── system-object-definition-customer.json │ │ │ │ ├── system-object-definition-customeraddress.json │ │ │ │ ├── system-object-definition-order.json │ │ │ │ ├── system-object-definition-product.json │ │ │ │ ├── system-object-definitions-old.json │ │ │ │ └── system-object-definitions.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ ├── scripts │ │ │ │ └── setup-logs.js │ │ │ ├── server.js │ │ │ └── src │ │ │ ├── app.js │ │ │ ├── config │ │ │ │ └── server-config.js │ │ │ ├── middleware │ │ │ │ ├── auth.js │ │ │ │ ├── cors.js │ │ │ │ └── logging.js │ │ │ ├── routes │ │ │ │ ├── ocapi │ │ │ │ │ ├── code-versions-handler.js │ │ │ │ │ ├── oauth-handler.js │ │ │ │ │ ├── ocapi-error-utils.js │ │ │ │ │ ├── ocapi-utils.js │ │ │ │ │ ├── site-preferences-handler.js │ │ │ │ │ └── system-objects-handler.js │ │ │ │ ├── ocapi.js │ │ │ │ └── webdav.js │ │ │ └── utils │ │ │ ├── mock-data-loader.js │ │ │ └── webdav-xml.js │ │ └── sfcc-mock-server-manager.ts │ ├── sfcc-mock-server.test.ts │ ├── site-preferences-client.test.ts │ ├── system-objects-client.test.ts │ ├── utils.test.ts │ ├── validation-helpers.test.ts │ └── validator.test.ts ├── tsconfig.json └── tsconfig.test.json ``` # Files -------------------------------------------------------------------------------- /docs/dw_util/Calendar.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.util 2 | 3 | # Class Calendar 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.util.Calendar 9 | 10 | ## Description 11 | 12 | Represents a Calendar and is based on the java.util.Calendar class. Refer to the java.util.Calendar documentation for more information. IMPORTANT NOTE: Please use the StringUtils.formatCalendar(Calendar) functions to convert a Calendar object into a String. 13 | 14 | ## Constants 15 | 16 | ### AM_PM 17 | 18 | **Type:** Number = 9 19 | 20 | Indicates whether the HOUR is before or after noon. 21 | 22 | ### APRIL 23 | 24 | **Type:** Number = 3 25 | 26 | Value for the month of year field representing April. 27 | 28 | ### AUGUST 29 | 30 | **Type:** Number = 7 31 | 32 | Value for the month of year field representing August. 33 | 34 | ### DATE 35 | 36 | **Type:** Number = 5 37 | 38 | Represents a date. 39 | 40 | ### DAY_OF_MONTH 41 | 42 | **Type:** Number = 5 43 | 44 | Represents a day of the month. 45 | 46 | ### DAY_OF_WEEK 47 | 48 | **Type:** Number = 7 49 | 50 | Represents a day of the week. 51 | 52 | ### DAY_OF_WEEK_IN_MONTH 53 | 54 | **Type:** Number = 8 55 | 56 | Represents a day of the week in a month. 57 | 58 | ### DAY_OF_YEAR 59 | 60 | **Type:** Number = 6 61 | 62 | Represents a day of the year. 63 | 64 | ### DECEMBER 65 | 66 | **Type:** Number = 11 67 | 68 | Value for the month of year field representing December. 69 | 70 | ### DST_OFFSET 71 | 72 | **Type:** Number = 16 73 | 74 | Indicates the daylight savings offset in milliseconds. 75 | 76 | ### ERA 77 | 78 | **Type:** Number = 0 79 | 80 | Indicates the era such as 'AD' or 'BC' in the Julian calendar. 81 | 82 | ### FEBRUARY 83 | 84 | **Type:** Number = 1 85 | 86 | Value for the month of year field representing February. 87 | 88 | ### FRIDAY 89 | 90 | **Type:** Number = 6 91 | 92 | Value for the day of the week field representing Friday. 93 | 94 | ### HOUR 95 | 96 | **Type:** Number = 10 97 | 98 | Represents an hour. 99 | 100 | ### HOUR_OF_DAY 101 | 102 | **Type:** Number = 11 103 | 104 | Represents an hour of the day. 105 | 106 | ### INPUT_DATE_PATTERN 107 | 108 | **Type:** Number = 3 109 | 110 | The input date pattern, for instance MM/dd/yyyy 111 | 112 | ### INPUT_DATE_TIME_PATTERN 113 | 114 | **Type:** Number = 5 115 | 116 | The input date time pattern, for instance MM/dd/yyyy h:mm a 117 | 118 | ### INPUT_TIME_PATTERN 119 | 120 | **Type:** Number = 4 121 | 122 | The input time pattern, for instance h:mm a 123 | 124 | ### JANUARY 125 | 126 | **Type:** Number = 0 127 | 128 | Value for the month of year field representing January. 129 | 130 | ### JULY 131 | 132 | **Type:** Number = 6 133 | 134 | Value for the month of year field representing July. 135 | 136 | ### JUNE 137 | 138 | **Type:** Number = 5 139 | 140 | Value for the month of year field representing June. 141 | 142 | ### LONG_DATE_PATTERN 143 | 144 | **Type:** Number = 1 145 | 146 | The long date pattern, for instance MMM/d/yyyy 147 | 148 | ### MARCH 149 | 150 | **Type:** Number = 2 151 | 152 | Value for the month of year field representing March. 153 | 154 | ### MAY 155 | 156 | **Type:** Number = 4 157 | 158 | Value for the month of year field representing May. 159 | 160 | ### MILLISECOND 161 | 162 | **Type:** Number = 14 163 | 164 | Represents a millisecond. 165 | 166 | ### MINUTE 167 | 168 | **Type:** Number = 12 169 | 170 | Represents a minute. 171 | 172 | ### MONDAY 173 | 174 | **Type:** Number = 2 175 | 176 | Value for the day of the week field representing Monday. 177 | 178 | ### MONTH 179 | 180 | **Type:** Number = 2 181 | 182 | Represents a month where the first month of the year is 0. 183 | 184 | ### NOVEMBER 185 | 186 | **Type:** Number = 10 187 | 188 | Value for the month of year field representing November. 189 | 190 | ### OCTOBER 191 | 192 | **Type:** Number = 9 193 | 194 | Value for the month of year field representing October. 195 | 196 | ### SATURDAY 197 | 198 | **Type:** Number = 7 199 | 200 | Value for the day of the week field representing Saturday. 201 | 202 | ### SECOND 203 | 204 | **Type:** Number = 13 205 | 206 | Represents a second. 207 | 208 | ### SEPTEMBER 209 | 210 | **Type:** Number = 8 211 | 212 | Value for the month of year field representing September. 213 | 214 | ### SHORT_DATE_PATTERN 215 | 216 | **Type:** Number = 0 217 | 218 | The short date pattern, for instance M/d/yy 219 | 220 | ### SUNDAY 221 | 222 | **Type:** Number = 1 223 | 224 | Value for the day of the week field representing Sunday. 225 | 226 | ### THURSDAY 227 | 228 | **Type:** Number = 5 229 | 230 | Value for the day of the week field representing Thursday. 231 | 232 | ### TIME_PATTERN 233 | 234 | **Type:** Number = 2 235 | 236 | The time pattern, for instance h:mm:ss a 237 | 238 | ### TUESDAY 239 | 240 | **Type:** Number = 3 241 | 242 | Value for the day of the week field representing Tuesday. 243 | 244 | ### WEDNESDAY 245 | 246 | **Type:** Number = 4 247 | 248 | Value for the day of the week field representing Wednesday. 249 | 250 | ### WEEK_OF_MONTH 251 | 252 | **Type:** Number = 4 253 | 254 | Represents a week of the month. 255 | 256 | ### WEEK_OF_YEAR 257 | 258 | **Type:** Number = 3 259 | 260 | Represents a week in the year. 261 | 262 | ### YEAR 263 | 264 | **Type:** Number = 1 265 | 266 | Represents a year. 267 | 268 | ### ZONE_OFFSET 269 | 270 | **Type:** Number = 15 271 | 272 | Indicates the raw offset from GMT in milliseconds. 273 | 274 | ## Properties 275 | 276 | ### firstDayOfWeek 277 | 278 | **Type:** Number 279 | 280 | The first day of the week base on locale context. For example, in the US 281 | the first day of the week is SUNDAY. However, in France the 282 | first day of the week is MONDAY. 283 | 284 | ### time 285 | 286 | **Type:** Date 287 | 288 | The current time stamp of this calendar. This method 289 | is also used to convert a Calendar into a Date. 290 | 291 | WARNING: Keep in mind that the returned Date object's time is always 292 | interpreted in the time zone GMT. This means time zone information 293 | set at the calendar object will not be honored and gets lost. 294 | 295 | ### timeZone 296 | 297 | **Type:** String 298 | 299 | The current time zone of this calendar. 300 | 301 | ## Constructor Summary 302 | 303 | Calendar() Creates a new Calendar object that is set to the current time. 304 | 305 | Calendar(date : Date) Creates a new Calendar object for the given Date object. 306 | 307 | ## Method Summary 308 | 309 | ### add 310 | 311 | **Signature:** `add(field : Number, value : Number) : void` 312 | 313 | Adds or subtracts the specified amount of time to the given calendar field, based on the calendar's rules. 314 | 315 | ### after 316 | 317 | **Signature:** `after(obj : Object) : boolean` 318 | 319 | Indicates if this Calendar represents a time after the time represented by the specified Object. 320 | 321 | ### before 322 | 323 | **Signature:** `before(obj : Object) : boolean` 324 | 325 | Indicates if this Calendar represents a time before the time represented by the specified Object. 326 | 327 | ### clear 328 | 329 | **Signature:** `clear() : void` 330 | 331 | Sets all the calendar field values and the time value (millisecond offset from the Epoch) of this Calendar undefined. 332 | 333 | ### clear 334 | 335 | **Signature:** `clear(field : Number) : void` 336 | 337 | Sets the given calendar field value and the time value (millisecond offset from the Epoch) of this Calendar undefined. 338 | 339 | ### compareTo 340 | 341 | **Signature:** `compareTo(anotherCalendar : Calendar) : Number` 342 | 343 | Compares the time values (millisecond offsets from the Epoch) represented by two Calendar objects. 344 | 345 | ### equals 346 | 347 | **Signature:** `equals(other : Object) : boolean` 348 | 349 | Compares two calendar values whether they are equivalent. 350 | 351 | ### get 352 | 353 | **Signature:** `get(field : Number) : Number` 354 | 355 | Returns the value of the given calendar field. 356 | 357 | ### getActualMaximum 358 | 359 | **Signature:** `getActualMaximum(field : Number) : Number` 360 | 361 | Returns the maximum value that the specified calendar field could have. 362 | 363 | ### getActualMinimum 364 | 365 | **Signature:** `getActualMinimum(field : Number) : Number` 366 | 367 | Returns the minimum value that the specified calendar field could have. 368 | 369 | ### getFirstDayOfWeek 370 | 371 | **Signature:** `getFirstDayOfWeek() : Number` 372 | 373 | Returns the first day of the week base on locale context. 374 | 375 | ### getMaximum 376 | 377 | **Signature:** `getMaximum(field : Number) : Number` 378 | 379 | Returns the maximum value for the given calendar field. 380 | 381 | ### getMinimum 382 | 383 | **Signature:** `getMinimum(field : Number) : Number` 384 | 385 | Returns the minimum value for the given calendar field. 386 | 387 | ### getTime 388 | 389 | **Signature:** `getTime() : Date` 390 | 391 | Returns the current time stamp of this calendar. 392 | 393 | ### getTimeZone 394 | 395 | **Signature:** `getTimeZone() : String` 396 | 397 | Returns the current time zone of this calendar. 398 | 399 | ### hashCode 400 | 401 | **Signature:** `hashCode() : Number` 402 | 403 | Calculates the hash code for a calendar; 404 | 405 | ### isLeapYear 406 | 407 | **Signature:** `isLeapYear(year : Number) : boolean` 408 | 409 | Indicates if the specified year is a leap year. 410 | 411 | ### isSameDay 412 | 413 | **Signature:** `isSameDay(other : Calendar) : boolean` 414 | 415 | Checks, whether two calendar dates fall on the same day. 416 | 417 | ### isSameDayByTimestamp 418 | 419 | **Signature:** `isSameDayByTimestamp(other : Calendar) : boolean` 420 | 421 | Checks, whether two calendar dates fall on the same day. 422 | 423 | ### isSet 424 | 425 | **Signature:** `isSet(field : Number) : boolean` 426 | 427 | Indicates if the field is set. 428 | 429 | ### parseByFormat 430 | 431 | **Signature:** `parseByFormat(timeString : String, format : String) : void` 432 | 433 | Parses the string according to the date and time format pattern and set the time at this calendar object. 434 | 435 | ### parseByLocale 436 | 437 | **Signature:** `parseByLocale(timeString : String, locale : String, pattern : Number) : void` 438 | 439 | Parses the string according the date format pattern of the given locale. 440 | 441 | ### roll 442 | 443 | **Signature:** `roll(field : Number, up : boolean) : void` 444 | 445 | Rolls the specified field up or down one value. 446 | 447 | ### roll 448 | 449 | **Signature:** `roll(field : Number, amount : Number) : void` 450 | 451 | Rolls the specified field using the specified value. 452 | 453 | ### set 454 | 455 | **Signature:** `set(field : Number, value : Number) : void` 456 | 457 | Sets the given calendar field to the given value. 458 | 459 | ### set 460 | 461 | **Signature:** `set(year : Number, month : Number, date : Number) : void` 462 | 463 | Sets the values for the calendar fields YEAR, MONTH, and DAY_OF_MONTH. 464 | 465 | ### set 466 | 467 | **Signature:** `set(year : Number, month : Number, date : Number, hourOfDay : Number, minute : Number) : void` 468 | 469 | Sets the values for the calendar fields YEAR, MONTH, DAY_OF_MONTH, HOUR_OF_DAY, and MINUTE. 470 | 471 | ### set 472 | 473 | **Signature:** `set(year : Number, month : Number, date : Number, hourOfDay : Number, minute : Number, second : Number) : void` 474 | 475 | Sets the values for the calendar fields YEAR, MONTH, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE and SECOND. 476 | 477 | ### setFirstDayOfWeek 478 | 479 | **Signature:** `setFirstDayOfWeek(value : Number) : void` 480 | 481 | Sets what the first day of the week is. 482 | 483 | ### setTime 484 | 485 | **Signature:** `setTime(date : Date) : void` 486 | 487 | Sets the current time stamp of this calendar. WARNING: Keep in mind that the set Date object's time is always interpreted in the time zone GMT. 488 | 489 | ### setTimeZone 490 | 491 | **Signature:** `setTimeZone(timeZone : String) : void` 492 | 493 | Sets the current time zone of this calendar. WARNING: Keep in mind that the time stamp represented by the calendar is always interpreted in the time zone GMT. 494 | 495 | ## Constructor Detail 496 | 497 | ## Method Detail 498 | 499 | ## Method Details 500 | 501 | ### add 502 | 503 | **Signature:** `add(field : Number, value : Number) : void` 504 | 505 | **Description:** Adds or subtracts the specified amount of time to the given calendar field, based on the calendar's rules. 506 | 507 | **Parameters:** 508 | 509 | - `field`: the calendar field. 510 | - `value`: the amount of date or time to be added to the field 511 | 512 | --- 513 | 514 | ### after 515 | 516 | **Signature:** `after(obj : Object) : boolean` 517 | 518 | **Description:** Indicates if this Calendar represents a time after the time represented by the specified Object. 519 | 520 | **Parameters:** 521 | 522 | - `obj`: the object to test. 523 | 524 | **Returns:** 525 | 526 | true if this Calendar represents a time after the time represented by the specified Object, false otherwise. 527 | 528 | --- 529 | 530 | ### before 531 | 532 | **Signature:** `before(obj : Object) : boolean` 533 | 534 | **Description:** Indicates if this Calendar represents a time before the time represented by the specified Object. 535 | 536 | **Parameters:** 537 | 538 | - `obj`: the object to test. 539 | 540 | **Returns:** 541 | 542 | true if this Calendar represents a time before the time represented by the specified Object, false otherwise. 543 | 544 | --- 545 | 546 | ### clear 547 | 548 | **Signature:** `clear() : void` 549 | 550 | **Description:** Sets all the calendar field values and the time value (millisecond offset from the Epoch) of this Calendar undefined. 551 | 552 | --- 553 | 554 | ### clear 555 | 556 | **Signature:** `clear(field : Number) : void` 557 | 558 | **Description:** Sets the given calendar field value and the time value (millisecond offset from the Epoch) of this Calendar undefined. 559 | 560 | **Parameters:** 561 | 562 | - `field`: the calendar field to be cleared. 563 | 564 | --- 565 | 566 | ### compareTo 567 | 568 | **Signature:** `compareTo(anotherCalendar : Calendar) : Number` 569 | 570 | **Description:** Compares the time values (millisecond offsets from the Epoch) represented by two Calendar objects. 571 | 572 | **Parameters:** 573 | 574 | - `anotherCalendar`: the Calendar to be compared. 575 | 576 | **Returns:** 577 | 578 | the value 0 if the time represented by the argument is equal to the time represented by this Calendar; a value less than 0 if the time of this Calendar is before the time represented by the argument; and a value greater than 0 if the time of this Calendar is after the time represented by the argument. 579 | 580 | --- 581 | 582 | ### equals 583 | 584 | **Signature:** `equals(other : Object) : boolean` 585 | 586 | **Description:** Compares two calendar values whether they are equivalent. 587 | 588 | **Parameters:** 589 | 590 | - `other`: the object to compare against this calendar. 591 | 592 | --- 593 | 594 | ### get 595 | 596 | **Signature:** `get(field : Number) : Number` 597 | 598 | **Description:** Returns the value of the given calendar field. 599 | 600 | **Parameters:** 601 | 602 | - `field`: the calendar field to retrieve. 603 | 604 | **Returns:** 605 | 606 | the value for the given calendar field. 607 | 608 | --- 609 | 610 | ### getActualMaximum 611 | 612 | **Signature:** `getActualMaximum(field : Number) : Number` 613 | 614 | **Description:** Returns the maximum value that the specified calendar field could have. 615 | 616 | **Parameters:** 617 | 618 | - `field`: the calendar field. 619 | 620 | **Returns:** 621 | 622 | the maximum value that the specified calendar field could have. 623 | 624 | --- 625 | 626 | ### getActualMinimum 627 | 628 | **Signature:** `getActualMinimum(field : Number) : Number` 629 | 630 | **Description:** Returns the minimum value that the specified calendar field could have. 631 | 632 | **Parameters:** 633 | 634 | - `field`: the calendar field. 635 | 636 | **Returns:** 637 | 638 | the minimum value that the specified calendar field could have. 639 | 640 | --- 641 | 642 | ### getFirstDayOfWeek 643 | 644 | **Signature:** `getFirstDayOfWeek() : Number` 645 | 646 | **Description:** Returns the first day of the week base on locale context. For example, in the US the first day of the week is SUNDAY. However, in France the first day of the week is MONDAY. 647 | 648 | **Returns:** 649 | 650 | the first day of the week base on locale context. For example, in the US the first day of the week is SUNDAY. However, in France the first day of the week is MONDAY. 651 | 652 | --- 653 | 654 | ### getMaximum 655 | 656 | **Signature:** `getMaximum(field : Number) : Number` 657 | 658 | **Description:** Returns the maximum value for the given calendar field. 659 | 660 | **Parameters:** 661 | 662 | - `field`: the calendar field. 663 | 664 | **Returns:** 665 | 666 | the maximum value for the given calendar field. 667 | 668 | --- 669 | 670 | ### getMinimum 671 | 672 | **Signature:** `getMinimum(field : Number) : Number` 673 | 674 | **Description:** Returns the minimum value for the given calendar field. 675 | 676 | **Parameters:** 677 | 678 | - `field`: the calendar field. 679 | 680 | **Returns:** 681 | 682 | the minimum value for the given calendar field. 683 | 684 | --- 685 | 686 | ### getTime 687 | 688 | **Signature:** `getTime() : Date` 689 | 690 | **Description:** Returns the current time stamp of this calendar. This method is also used to convert a Calendar into a Date. WARNING: Keep in mind that the returned Date object's time is always interpreted in the time zone GMT. This means time zone information set at the calendar object will not be honored and gets lost. 691 | 692 | **Returns:** 693 | 694 | the current time stamp of this calendar as a Date. 695 | 696 | --- 697 | 698 | ### getTimeZone 699 | 700 | **Signature:** `getTimeZone() : String` 701 | 702 | **Description:** Returns the current time zone of this calendar. 703 | 704 | **Returns:** 705 | 706 | the current time zone of this calendar. 707 | 708 | --- 709 | 710 | ### hashCode 711 | 712 | **Signature:** `hashCode() : Number` 713 | 714 | **Description:** Calculates the hash code for a calendar; 715 | 716 | --- 717 | 718 | ### isLeapYear 719 | 720 | **Signature:** `isLeapYear(year : Number) : boolean` 721 | 722 | **Description:** Indicates if the specified year is a leap year. 723 | 724 | **Parameters:** 725 | 726 | - `year`: the year to test. 727 | 728 | **Returns:** 729 | 730 | true if the specified year is a leap year. 731 | 732 | --- 733 | 734 | ### isSameDay 735 | 736 | **Signature:** `isSameDay(other : Calendar) : boolean` 737 | 738 | **Description:** Checks, whether two calendar dates fall on the same day. The method performs comparison based on both calendar's field values by honoring the defined time zones. Examples: new Calendar( new Date( "2002/02/28 13:45" ).isSameDay( new Calendar( new Date( "2002/02/28 06:01" ) ) ); would return true. new Calendar( new Date( "2002/02/28 13:45" ).isSameDay( new Calendar( new Date( "2002/02/12 13:45" ) ) ); would return false. new Calendar( new Date( "2002/02/28 13:45" ).isSameDay( new Calendar( new Date( "1970/02/28 13:45" ) ) ); would return false. var cal1 = new Calendar( new Date( "2002/02/28 02:00" ); cal1.setTimeZone( "Etc/GMT+1" ); var cal2 = new Calendar( new Date( "2002/02/28 00:00" ); cal2.setTimeZone( "Etc/GMT+1" ); cal1.isSameDay( cal2 ); would return false since the time zone is applied first which results in comparing 2002/02/28 01:00 for cal1 with 2002/02/27 23:00 for cal2. 739 | 740 | **Parameters:** 741 | 742 | - `other`: the calendar to compare against this calendar. 743 | 744 | --- 745 | 746 | ### isSameDayByTimestamp 747 | 748 | **Signature:** `isSameDayByTimestamp(other : Calendar) : boolean` 749 | 750 | **Description:** Checks, whether two calendar dates fall on the same day. The method performs comparison based on both calendar's time stamps by ignoring any defined time zones. Examples: new Calendar( new Date( "2002/02/28 13:45" ).isSameDayByTimestamp( new Calendar( new Date( "2002/02/28 06:01" ) ) ); would return true. new Calendar( new Date( "2002/02/28 13:45" ).isSameDayByTimestamp( new Calendar( new Date( "2002/02/12 13:45" ) ) ); would return false. new Calendar( new Date( "2002/02/28 13:45" ).isSameDayByTimestamp( new Calendar( new Date( "1970/02/28 13:45" ) ) ); would return false. var cal1 = new Calendar( new Date( "2002/02/28 02:00" ); cal1.setTimeZone( "Etc/GMT+1" ); var cal2 = new Calendar( new Date( "2002/02/28 00:00" ); cal2.setTimeZone( "Etc/GMT+1" ); cal1.isSameDayByTimestamp( cal2 ); would return true since the time zone is not applied first which results in comparing 2002/02/28 02:00 for cal1 with 2002/02/28 00:00 for cal2. 751 | 752 | **Parameters:** 753 | 754 | - `other`: the calendar to compare against this calendar. 755 | 756 | --- 757 | 758 | ### isSet 759 | 760 | **Signature:** `isSet(field : Number) : boolean` 761 | 762 | **Description:** Indicates if the field is set. 763 | 764 | **Parameters:** 765 | 766 | - `field`: the field to test. 767 | 768 | **Returns:** 769 | 770 | true if the field is set, false otherwise. 771 | 772 | --- 773 | 774 | ### parseByFormat 775 | 776 | **Signature:** `parseByFormat(timeString : String, format : String) : void` 777 | 778 | **Description:** Parses the string according to the date and time format pattern and set the time at this calendar object. For the specification of the date and time format pattern see the javadoc of the JDK class java.text.SimpleDateFormat. If a time zone is included in the format string, this time zone is used to interpet the time. Otherwise the currently set calendar time zone is used to parse the given time string. 779 | 780 | **Parameters:** 781 | 782 | - `timeString`: the time string to parsed 783 | - `format`: the time format string 784 | 785 | --- 786 | 787 | ### parseByLocale 788 | 789 | **Signature:** `parseByLocale(timeString : String, locale : String, pattern : Number) : void` 790 | 791 | **Description:** Parses the string according the date format pattern of the given locale. If the locale name is invalid, an exception is thrown. The currently set calendar time zone is used to parse the given time string. 792 | 793 | **Parameters:** 794 | 795 | - `timeString`: the time string to parsed 796 | - `locale`: the locale id, which defines the date format pattern 797 | - `pattern`: the pattern is one of calendar pattern e.g. SHORT_DATE_PATTERN as defined in the regional settings for the locale 798 | 799 | --- 800 | 801 | ### roll 802 | 803 | **Signature:** `roll(field : Number, up : boolean) : void` 804 | 805 | **Description:** Rolls the specified field up or down one value. 806 | 807 | **Parameters:** 808 | 809 | - `field`: the field to roll. 810 | - `up`: if true rolls the field up, if false rolls the field down. 811 | 812 | --- 813 | 814 | ### roll 815 | 816 | **Signature:** `roll(field : Number, amount : Number) : void` 817 | 818 | **Description:** Rolls the specified field using the specified value. 819 | 820 | **Parameters:** 821 | 822 | - `field`: the field to roll. 823 | - `amount`: the amount to roll the field. 824 | 825 | --- 826 | 827 | ### set 828 | 829 | **Signature:** `set(field : Number, value : Number) : void` 830 | 831 | **Description:** Sets the given calendar field to the given value. 832 | 833 | **Parameters:** 834 | 835 | - `field`: the calendar field to set. 836 | - `value`: the value to set in the field. 837 | 838 | --- 839 | 840 | ### set 841 | 842 | **Signature:** `set(year : Number, month : Number, date : Number) : void` 843 | 844 | **Description:** Sets the values for the calendar fields YEAR, MONTH, and DAY_OF_MONTH. 845 | 846 | **Parameters:** 847 | 848 | - `year`: the value for year. 849 | - `month`: the value for month. 850 | - `date`: the value for date. 851 | 852 | --- 853 | 854 | ### set 855 | 856 | **Signature:** `set(year : Number, month : Number, date : Number, hourOfDay : Number, minute : Number) : void` 857 | 858 | **Description:** Sets the values for the calendar fields YEAR, MONTH, DAY_OF_MONTH, HOUR_OF_DAY, and MINUTE. 859 | 860 | **Parameters:** 861 | 862 | - `year`: the value for year. 863 | - `month`: the value for month. 864 | - `date`: the value for date. 865 | - `hourOfDay`: the value for hour of day. 866 | - `minute`: the value for minute. 867 | 868 | --- 869 | 870 | ### set 871 | 872 | **Signature:** `set(year : Number, month : Number, date : Number, hourOfDay : Number, minute : Number, second : Number) : void` 873 | 874 | **Description:** Sets the values for the calendar fields YEAR, MONTH, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE and SECOND. 875 | 876 | **Parameters:** 877 | 878 | - `year`: the value for year. 879 | - `month`: the value for month. 880 | - `date`: the value for date. 881 | - `hourOfDay`: the value for hour of day. 882 | - `minute`: the value for minute. 883 | - `second`: the value for second. 884 | 885 | --- 886 | 887 | ### setFirstDayOfWeek 888 | 889 | **Signature:** `setFirstDayOfWeek(value : Number) : void` 890 | 891 | **Description:** Sets what the first day of the week is. 892 | 893 | **Parameters:** 894 | 895 | - `value`: the day to set as the first day of the week. 896 | 897 | --- 898 | 899 | ### setTime 900 | 901 | **Signature:** `setTime(date : Date) : void` 902 | 903 | **Description:** Sets the current time stamp of this calendar. WARNING: Keep in mind that the set Date object's time is always interpreted in the time zone GMT. This means that time zone information at the calendar object needs to be set separately by using the setTimeZone(String) method. 904 | 905 | **Parameters:** 906 | 907 | - `date`: the current time stamp of this calendar. 908 | 909 | --- 910 | 911 | ### setTimeZone 912 | 913 | **Signature:** `setTimeZone(timeZone : String) : void` 914 | 915 | **Description:** Sets the current time zone of this calendar. WARNING: Keep in mind that the time stamp represented by the calendar is always interpreted in the time zone GMT. Changing the time zone will not change the calendar's time stamp. 916 | 917 | **Parameters:** 918 | 919 | - `timeZone`: the current time zone value to set. 920 | 921 | --- ``` -------------------------------------------------------------------------------- /docs/dw_system/Request.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.system 2 | 3 | # Class Request 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.system.Request 9 | 10 | ## Description 11 | 12 | Represents a request in Commerce Cloud Digital. Each pipeline dictionary contains a CurrentRequest object, which is of type dw.system.Request. Most requests are HTTP requests, so you can use this object to get information about the HTTP request, such as the HTTP headers. You can also get a list of cookies, if any, associated with the request. If the request is issued from a job, the request is not an HTTP request, so HTTP-related methods return null. 13 | 14 | ## Properties 15 | 16 | ### clientId 17 | 18 | **Type:** String (Read Only) 19 | 20 | The client id of the current SCAPI or OCAPI request. If the request is not a SCAPI request or not an 21 | OCAPI request 'null' is returned. For client ids owned by Commerce Cloud Digital an alias is returned. 22 | 23 | ### custom 24 | 25 | **Type:** CustomAttributes (Read Only) 26 | 27 | All of the custom attributes associated with the request. The attributes are stored for the life time of 28 | the request. 29 | 30 | ### geolocation 31 | 32 | **Type:** Geolocation 33 | 34 | The physical location for the current request, if available. The 35 | location is calculated based on the IP address of the request. Note, if 36 | the geolocation tracking feature is not enabled, this method always 37 | returns null. 38 | 39 | ### httpCookies 40 | 41 | **Type:** Cookies (Read Only) 42 | 43 | The Cookies object, which can be used to read cookies sent by the client. Use the method 44 | Response.addHttpCookie() to add a cookie to the outgoing response. 45 | 46 | ### httpHeaders 47 | 48 | **Type:** Map (Read Only) 49 | 50 | A Map containing all HTTP header values. 51 | 52 | ### httpHost 53 | 54 | **Type:** String (Read Only) 55 | 56 | The host name or null if there is no host name. 57 | 58 | ### httpLocale 59 | 60 | **Type:** String (Read Only) 61 | 62 | The locale or null if there is no associated locale. 63 | 64 | ### httpMethod 65 | 66 | **Type:** String (Read Only) 67 | 68 | The name of the HTTP method with which this request was made, for example, GET, POST, or PUT. 69 | 70 | ### httpParameterMap 71 | 72 | **Type:** HttpParameterMap (Read Only) 73 | 74 | The parameter map that contains the HTTP parameters for the current request. 75 | 76 | ### httpParameters 77 | 78 | **Type:** Map (Read Only) 79 | 80 | A Map containing the raw HTTP parameters sent to the server. The Map contains name/value pairs. Each name 81 | is a String and each value is a String array. 82 | 83 | ### httpPath 84 | 85 | **Type:** String (Read Only) 86 | 87 | The path. 88 | 89 | ### httpProtocol 90 | 91 | **Type:** String (Read Only) 92 | 93 | The HTTP protocol used for this request. Possible values are "http" or "https". If the current activity 94 | is not related to an HTTP request, for example, when the request is part of a job, this method returns null. 95 | 96 | ### httpQueryString 97 | 98 | **Type:** String (Read Only) 99 | 100 | The query string or null if there is no query string. 101 | 102 | ### httpReferer 103 | 104 | **Type:** String (Read Only) 105 | 106 | The referer or null if there is no referer. 107 | 108 | ### httpRemoteAddress 109 | 110 | **Type:** String (Read Only) 111 | 112 | The remote address or null if no remote address is found. 113 | 114 | ### httpRequest 115 | 116 | **Type:** boolean (Read Only) 117 | 118 | Identifies if this request is an HTTP request. The method returns true, if the current processing is related to a 119 | HTTP request. 120 | 121 | ### httpSecure 122 | 123 | **Type:** boolean (Read Only) 124 | 125 | Returns whether the HTTP communication is secure, which basically means that the communication happens via https. 126 | If the current activity is not related to an HTTP request the method returns false. 127 | 128 | ### httpURL 129 | 130 | **Type:** URL (Read Only) 131 | 132 | The complete URL of the request which was received at the server. 133 | This URL does not include SEO optimizations. 134 | 135 | ### httpUserAgent 136 | 137 | **Type:** String (Read Only) 138 | 139 | The HTTP user agent or null if there is no user agent. 140 | 141 | ### includeRequest 142 | 143 | **Type:** boolean (Read Only) 144 | 145 | Returns true if the request represents a request for a remote include, false if it is a top-level request. 146 | 147 | ### locale 148 | 149 | **Type:** String 150 | 151 | The locale of the current request. This locale is set by the system based on the information in the URL. 152 | It may be different from the locale returned by getHttpLocale(), which is the preferred locale sent by the user agent. 153 | 154 | ### ocapiVersion 155 | 156 | **Type:** String (Read Only) 157 | 158 | The OCAPI version of the current request. If this is not 159 | an OCAPI request, 'null' is returned. 160 | 161 | ### pageMetaData 162 | 163 | **Type:** PageMetaData (Read Only) 164 | 165 | The page meta data that are associated with the current request. 166 | 167 | ### requestID 168 | 169 | **Type:** String (Read Only) 170 | 171 | The unique identifier of the current request. The unique id is helpful for debugging purpose, e.g. relate 172 | debug messages to a particular request. 173 | 174 | ### SCAPI 175 | 176 | **Type:** boolean (Read Only) 177 | 178 | Returns whether the request originated in SCAPI. 179 | 180 | ### SCAPIPathParameters 181 | 182 | **Type:** Map (Read Only) 183 | 184 | A map containing all path parameters of current SCAPI request in the following way: 185 | 186 | keys: path parameter names from path pattern 187 | values: corresponding path parameter values from current request 188 | 189 | 190 | Returns null if isSCAPI() returns false i.e. if the request is not a SCAPI request. 191 | 192 | 193 | For example: 194 | 195 | Current request: /product/shopper-products/v1/organizations/sfcc_org/products/apple-ipod-shuffle 196 | Path pattern: /product/shopper-products/v1/organizations/{organizationId}/products/{id} 197 | Result: Map with 2 key:value pairs: organizationId:sfcc_org and id:apple-ipod-shuffle. 198 | 199 | ### SCAPIPathPattern 200 | 201 | **Type:** String (Read Only) 202 | 203 | The SCAPI path pattern in the following way: 204 | 205 | 206 | The first three segments /api-family/api-name/version with concrete values. 207 | The /organizations part with the path parameter name organizationId in curly brackets. 208 | The actual resource path additional path parameter names in curly brackets. 209 | 210 | 211 | Returns null if isSCAPI() returns false i.e. if the request is not a SCAPI request. 212 | 213 | 214 | For example, in the context of a request to get a single product from shopper-products API, this method would 215 | return /product/shopper-products/v1/organizations/{organizationId}/products/{id} 216 | 217 | ### session 218 | 219 | **Type:** Session (Read Only) 220 | 221 | The session associated with this request. 222 | 223 | ### triggeredForm 224 | 225 | **Type:** Form (Read Only) 226 | 227 | The form that was submitted by the client if the request represents a form submission. 228 | 229 | ### triggeredFormAction 230 | 231 | **Type:** FormAction (Read Only) 232 | 233 | The form action that was triggered by the client if the request represents a form submission. 234 | 235 | ## Constructor Summary 236 | 237 | ## Method Summary 238 | 239 | ### addHttpCookie 240 | 241 | **Signature:** `addHttpCookie(cookie : Cookie) : void` 242 | 243 | Adds the specified cookie to the outgoing response. 244 | 245 | ### getClientId 246 | 247 | **Signature:** `getClientId() : String` 248 | 249 | Returns the client id of the current SCAPI or OCAPI request. 250 | 251 | ### getCustom 252 | 253 | **Signature:** `getCustom() : CustomAttributes` 254 | 255 | Returns all of the custom attributes associated with the request. 256 | 257 | ### getGeolocation 258 | 259 | **Signature:** `getGeolocation() : Geolocation` 260 | 261 | Returns the physical location for the current request, if available. 262 | 263 | ### getHttpCookies 264 | 265 | **Signature:** `getHttpCookies() : Cookies` 266 | 267 | Returns the Cookies object, which can be used to read cookies sent by the client. 268 | 269 | ### getHttpHeaders 270 | 271 | **Signature:** `getHttpHeaders() : Map` 272 | 273 | Returns a Map containing all HTTP header values. 274 | 275 | ### getHttpHost 276 | 277 | **Signature:** `getHttpHost() : String` 278 | 279 | Returns the host name or null if there is no host name. 280 | 281 | ### getHttpLocale 282 | 283 | **Signature:** `getHttpLocale() : String` 284 | 285 | Returns the locale or null if there is no associated locale. 286 | 287 | ### getHttpMethod 288 | 289 | **Signature:** `getHttpMethod() : String` 290 | 291 | Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT. 292 | 293 | ### getHttpParameterMap 294 | 295 | **Signature:** `getHttpParameterMap() : HttpParameterMap` 296 | 297 | Returns the parameter map that contains the HTTP parameters for the current request. 298 | 299 | ### getHttpParameters 300 | 301 | **Signature:** `getHttpParameters() : Map` 302 | 303 | Returns a Map containing the raw HTTP parameters sent to the server. 304 | 305 | ### getHttpPath 306 | 307 | **Signature:** `getHttpPath() : String` 308 | 309 | Returns the path. 310 | 311 | ### getHttpProtocol 312 | 313 | **Signature:** `getHttpProtocol() : String` 314 | 315 | Returns the HTTP protocol used for this request. 316 | 317 | ### getHttpQueryString 318 | 319 | **Signature:** `getHttpQueryString() : String` 320 | 321 | Returns the query string or null if there is no query string. 322 | 323 | ### getHttpReferer 324 | 325 | **Signature:** `getHttpReferer() : String` 326 | 327 | Returns the referer or null if there is no referer. 328 | 329 | ### getHttpRemoteAddress 330 | 331 | **Signature:** `getHttpRemoteAddress() : String` 332 | 333 | Returns the remote address or null if no remote address is found. 334 | 335 | ### getHttpURL 336 | 337 | **Signature:** `getHttpURL() : URL` 338 | 339 | Returns the complete URL of the request which was received at the server. 340 | 341 | ### getHttpUserAgent 342 | 343 | **Signature:** `getHttpUserAgent() : String` 344 | 345 | Returns the HTTP user agent or null if there is no user agent. 346 | 347 | ### getLocale 348 | 349 | **Signature:** `getLocale() : String` 350 | 351 | Returns the locale of the current request. 352 | 353 | ### getOcapiVersion 354 | 355 | **Signature:** `getOcapiVersion() : String` 356 | 357 | Returns the OCAPI version of the current request. 358 | 359 | ### getPageMetaData 360 | 361 | **Signature:** `getPageMetaData() : PageMetaData` 362 | 363 | Returns the page meta data that are associated with the current request. 364 | 365 | ### getRequestID 366 | 367 | **Signature:** `getRequestID() : String` 368 | 369 | Returns the unique identifier of the current request. 370 | 371 | ### getSCAPIPathParameters 372 | 373 | **Signature:** `getSCAPIPathParameters() : Map` 374 | 375 | Returns a map containing all path parameters of current SCAPI request in the following way: keys: path parameter names from path pattern values: corresponding path parameter values from current request Returns null if isSCAPI() returns false i.e. 376 | 377 | ### getSCAPIPathPattern 378 | 379 | **Signature:** `getSCAPIPathPattern() : String` 380 | 381 | Returns the SCAPI path pattern in the following way: The first three segments /api-family/api-name/version with concrete values. The /organizations part with the path parameter name organizationId in curly brackets. The actual resource path additional path parameter names in curly brackets. Returns null if isSCAPI() returns false i.e. 382 | 383 | ### getSession 384 | 385 | **Signature:** `getSession() : Session` 386 | 387 | Returns the session associated with this request. 388 | 389 | ### getTriggeredForm 390 | 391 | **Signature:** `getTriggeredForm() : Form` 392 | 393 | Returns the form that was submitted by the client if the request represents a form submission. 394 | 395 | ### getTriggeredFormAction 396 | 397 | **Signature:** `getTriggeredFormAction() : FormAction` 398 | 399 | Returns the form action that was triggered by the client if the request represents a form submission. 400 | 401 | ### isHttpRequest 402 | 403 | **Signature:** `isHttpRequest() : boolean` 404 | 405 | Identifies if this request is an HTTP request. 406 | 407 | ### isHttpSecure 408 | 409 | **Signature:** `isHttpSecure() : boolean` 410 | 411 | Returns whether the HTTP communication is secure, which basically means that the communication happens via https. 412 | 413 | ### isIncludeRequest 414 | 415 | **Signature:** `isIncludeRequest() : boolean` 416 | 417 | Returns true if the request represents a request for a remote include, false if it is a top-level request. 418 | 419 | ### isSCAPI 420 | 421 | **Signature:** `isSCAPI() : boolean` 422 | 423 | Returns whether the request originated in SCAPI. 424 | 425 | ### setGeolocation 426 | 427 | **Signature:** `setGeolocation(geoLocation : Geolocation) : void` 428 | 429 | Sets the physical location for the current request and remembers the new value for the duration of the user session. 430 | 431 | ### setLocale 432 | 433 | **Signature:** `setLocale(localeID : String) : boolean` 434 | 435 | Sets the given locale for the request. 436 | 437 | ## Method Detail 438 | 439 | ## Method Details 440 | 441 | ### addHttpCookie 442 | 443 | **Signature:** `addHttpCookie(cookie : Cookie) : void` 444 | 445 | **Description:** Adds the specified cookie to the outgoing response. This method can be called multiple times to set more than one cookie. If a cookie with the same cookie name, domain and path is set multiple times for the same response, only the last set cookie with this name is send to the client. This method can be used to set, update or delete cookies at the client. If the cookie doesn't exist at the client, it is set initially. If a cookie with the same name, domain and path already exists at the client, it is updated. A cookie can be deleted at the client by submitting a cookie with the maxAge attribute set to 0 (see Cookie.setMaxAge() for more information). Example, how a cookie can be deleted at the client: var cookie : Cookie = new Cookie("SomeName", "Simple Value"); cookie.setMaxAge(0); request.addHttpCookie(cookie); 446 | 447 | **Deprecated:** 448 | 449 | Use Response.addHttpCookie(Cookie) instead. 450 | 451 | **Parameters:** 452 | 453 | - `cookie`: a Cookie object 454 | 455 | --- 456 | 457 | ### getClientId 458 | 459 | **Signature:** `getClientId() : String` 460 | 461 | **Description:** Returns the client id of the current SCAPI or OCAPI request. If the request is not a SCAPI request or not an OCAPI request 'null' is returned. For client ids owned by Commerce Cloud Digital an alias is returned. 462 | 463 | **Returns:** 464 | 465 | a client id or alias in case of an OCAPI request, otherwise null. 466 | 467 | --- 468 | 469 | ### getCustom 470 | 471 | **Signature:** `getCustom() : CustomAttributes` 472 | 473 | **Description:** Returns all of the custom attributes associated with the request. The attributes are stored for the life time of the request. 474 | 475 | **Returns:** 476 | 477 | all of the custom attributes associated with the request. 478 | 479 | --- 480 | 481 | ### getGeolocation 482 | 483 | **Signature:** `getGeolocation() : Geolocation` 484 | 485 | **Description:** Returns the physical location for the current request, if available. The location is calculated based on the IP address of the request. Note, if the geolocation tracking feature is not enabled, this method always returns null. 486 | 487 | **Returns:** 488 | 489 | The geolocation of the current request, or null if this is not available. 490 | 491 | --- 492 | 493 | ### getHttpCookies 494 | 495 | **Signature:** `getHttpCookies() : Cookies` 496 | 497 | **Description:** Returns the Cookies object, which can be used to read cookies sent by the client. Use the method Response.addHttpCookie() to add a cookie to the outgoing response. 498 | 499 | **Returns:** 500 | 501 | Cookies object or null if this is not an HTTP request 502 | 503 | --- 504 | 505 | ### getHttpHeaders 506 | 507 | **Signature:** `getHttpHeaders() : Map` 508 | 509 | **Description:** Returns a Map containing all HTTP header values. 510 | 511 | **Returns:** 512 | 513 | a Map containing all HTTP header values. 514 | 515 | --- 516 | 517 | ### getHttpHost 518 | 519 | **Signature:** `getHttpHost() : String` 520 | 521 | **Description:** Returns the host name or null if there is no host name. 522 | 523 | **Returns:** 524 | 525 | the host name or null if there is no host name. 526 | 527 | --- 528 | 529 | ### getHttpLocale 530 | 531 | **Signature:** `getHttpLocale() : String` 532 | 533 | **Description:** Returns the locale or null if there is no associated locale. 534 | 535 | **Returns:** 536 | 537 | the locale or null. 538 | 539 | --- 540 | 541 | ### getHttpMethod 542 | 543 | **Signature:** `getHttpMethod() : String` 544 | 545 | **Description:** Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT. 546 | 547 | **Returns:** 548 | 549 | the HTTP method 550 | 551 | --- 552 | 553 | ### getHttpParameterMap 554 | 555 | **Signature:** `getHttpParameterMap() : HttpParameterMap` 556 | 557 | **Description:** Returns the parameter map that contains the HTTP parameters for the current request. 558 | 559 | **Returns:** 560 | 561 | the HTTP parameter map 562 | 563 | --- 564 | 565 | ### getHttpParameters 566 | 567 | **Signature:** `getHttpParameters() : Map` 568 | 569 | **Description:** Returns a Map containing the raw HTTP parameters sent to the server. The Map contains name/value pairs. Each name is a String and each value is a String array. 570 | 571 | **Returns:** 572 | 573 | a Map containing all the raw HTTP parameters send to the server. 574 | 575 | --- 576 | 577 | ### getHttpPath 578 | 579 | **Signature:** `getHttpPath() : String` 580 | 581 | **Description:** Returns the path. 582 | 583 | **Returns:** 584 | 585 | the path or null. 586 | 587 | --- 588 | 589 | ### getHttpProtocol 590 | 591 | **Signature:** `getHttpProtocol() : String` 592 | 593 | **Description:** Returns the HTTP protocol used for this request. Possible values are "http" or "https". If the current activity is not related to an HTTP request, for example, when the request is part of a job, this method returns null. 594 | 595 | **Returns:** 596 | 597 | "http", "https" or null 598 | 599 | --- 600 | 601 | ### getHttpQueryString 602 | 603 | **Signature:** `getHttpQueryString() : String` 604 | 605 | **Description:** Returns the query string or null if there is no query string. 606 | 607 | **Returns:** 608 | 609 | the query string or null. 610 | 611 | --- 612 | 613 | ### getHttpReferer 614 | 615 | **Signature:** `getHttpReferer() : String` 616 | 617 | **Description:** Returns the referer or null if there is no referer. 618 | 619 | **Returns:** 620 | 621 | the referer or null if there is no referer. 622 | 623 | --- 624 | 625 | ### getHttpRemoteAddress 626 | 627 | **Signature:** `getHttpRemoteAddress() : String` 628 | 629 | **Description:** Returns the remote address or null if no remote address is found. 630 | 631 | **Returns:** 632 | 633 | the remote address or null if no remote address is found. 634 | 635 | --- 636 | 637 | ### getHttpURL 638 | 639 | **Signature:** `getHttpURL() : URL` 640 | 641 | **Description:** Returns the complete URL of the request which was received at the server. This URL does not include SEO optimizations. 642 | 643 | **Returns:** 644 | 645 | the URL as URL object 646 | 647 | --- 648 | 649 | ### getHttpUserAgent 650 | 651 | **Signature:** `getHttpUserAgent() : String` 652 | 653 | **Description:** Returns the HTTP user agent or null if there is no user agent. 654 | 655 | **Returns:** 656 | 657 | the HTTP user agent or null if there is no user agent. 658 | 659 | --- 660 | 661 | ### getLocale 662 | 663 | **Signature:** `getLocale() : String` 664 | 665 | **Description:** Returns the locale of the current request. This locale is set by the system based on the information in the URL. It may be different from the locale returned by getHttpLocale(), which is the preferred locale sent by the user agent. 666 | 667 | **Returns:** 668 | 669 | the locale of the current request, like 'en_US' 670 | 671 | --- 672 | 673 | ### getOcapiVersion 674 | 675 | **Signature:** `getOcapiVersion() : String` 676 | 677 | **Description:** Returns the OCAPI version of the current request. If this is not an OCAPI request, 'null' is returned. 678 | 679 | **Returns:** 680 | 681 | OCAPI version of the current request 682 | 683 | --- 684 | 685 | ### getPageMetaData 686 | 687 | **Signature:** `getPageMetaData() : PageMetaData` 688 | 689 | **Description:** Returns the page meta data that are associated with the current request. 690 | 691 | **Returns:** 692 | 693 | the page meta data object 694 | 695 | --- 696 | 697 | ### getRequestID 698 | 699 | **Signature:** `getRequestID() : String` 700 | 701 | **Description:** Returns the unique identifier of the current request. The unique id is helpful for debugging purpose, e.g. relate debug messages to a particular request. 702 | 703 | **Returns:** 704 | 705 | the unique identifier of the current request. 706 | 707 | --- 708 | 709 | ### getSCAPIPathParameters 710 | 711 | **Signature:** `getSCAPIPathParameters() : Map` 712 | 713 | **Description:** Returns a map containing all path parameters of current SCAPI request in the following way: keys: path parameter names from path pattern values: corresponding path parameter values from current request Returns null if isSCAPI() returns false i.e. if the request is not a SCAPI request. For example: Current request: /product/shopper-products/v1/organizations/sfcc_org/products/apple-ipod-shuffle Path pattern: /product/shopper-products/v1/organizations/{organizationId}/products/{id} Result: Map with 2 key:value pairs: organizationId:sfcc_org and id:apple-ipod-shuffle. 714 | 715 | **Returns:** 716 | 717 | the path parameter map or null 718 | 719 | --- 720 | 721 | ### getSCAPIPathPattern 722 | 723 | **Signature:** `getSCAPIPathPattern() : String` 724 | 725 | **Description:** Returns the SCAPI path pattern in the following way: The first three segments /api-family/api-name/version with concrete values. The /organizations part with the path parameter name organizationId in curly brackets. The actual resource path additional path parameter names in curly brackets. Returns null if isSCAPI() returns false i.e. if the request is not a SCAPI request. For example, in the context of a request to get a single product from shopper-products API, this method would return /product/shopper-products/v1/organizations/{organizationId}/products/{id} 726 | 727 | **Returns:** 728 | 729 | the path pattern or null. 730 | 731 | --- 732 | 733 | ### getSession 734 | 735 | **Signature:** `getSession() : Session` 736 | 737 | **Description:** Returns the session associated with this request. 738 | 739 | **Returns:** 740 | 741 | the session associated with this request. 742 | 743 | --- 744 | 745 | ### getTriggeredForm 746 | 747 | **Signature:** `getTriggeredForm() : Form` 748 | 749 | **Description:** Returns the form that was submitted by the client if the request represents a form submission. 750 | 751 | **Returns:** 752 | 753 | the form which was triggered 754 | 755 | --- 756 | 757 | ### getTriggeredFormAction 758 | 759 | **Signature:** `getTriggeredFormAction() : FormAction` 760 | 761 | **Description:** Returns the form action that was triggered by the client if the request represents a form submission. 762 | 763 | **Returns:** 764 | 765 | the action of the form that was triggered 766 | 767 | --- 768 | 769 | ### isHttpRequest 770 | 771 | **Signature:** `isHttpRequest() : boolean` 772 | 773 | **Description:** Identifies if this request is an HTTP request. The method returns true, if the current processing is related to a HTTP request. 774 | 775 | **Deprecated:** 776 | 777 | Effectively always returns true. 778 | 779 | **Returns:** 780 | 781 | true if the current processing is related to a HTTP request, false otherwise. 782 | 783 | --- 784 | 785 | ### isHttpSecure 786 | 787 | **Signature:** `isHttpSecure() : boolean` 788 | 789 | **Description:** Returns whether the HTTP communication is secure, which basically means that the communication happens via https. If the current activity is not related to an HTTP request the method returns false. 790 | 791 | --- 792 | 793 | ### isIncludeRequest 794 | 795 | **Signature:** `isIncludeRequest() : boolean` 796 | 797 | **Description:** Returns true if the request represents a request for a remote include, false if it is a top-level request. 798 | 799 | --- 800 | 801 | ### isSCAPI 802 | 803 | **Signature:** `isSCAPI() : boolean` 804 | 805 | **Description:** Returns whether the request originated in SCAPI. 806 | 807 | **Returns:** 808 | 809 | true or false. 810 | 811 | --- 812 | 813 | ### setGeolocation 814 | 815 | **Signature:** `setGeolocation(geoLocation : Geolocation) : void` 816 | 817 | **Description:** Sets the physical location for the current request and remembers the new value for the duration of the user session. So any subsequent calls to getGeolocation() will return this value 818 | 819 | **Parameters:** 820 | 821 | - `geoLocation`: the geolocation object to use 822 | 823 | --- 824 | 825 | ### setLocale 826 | 827 | **Signature:** `setLocale(localeID : String) : boolean` 828 | 829 | **Description:** Sets the given locale for the request. The locale is only set if it is valid, if it is active and if it is allowed for the current site. 830 | 831 | **Parameters:** 832 | 833 | - `localeID`: the locale ID to be set, like 'en_US' 834 | 835 | **Returns:** 836 | 837 | true, if the locale was successfully set, false otherwise 838 | 839 | --- ``` -------------------------------------------------------------------------------- /tests/mcp/node/get-sfra-documents-by-category.docs-only.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * ================================================================================== 3 | * SFCC MCP Server - get_sfra_documents_by_category Tool Node.js Programmatic Tests 4 | * Comprehensive testing with dynamic validation logic and advanced test scenarios 5 | * 6 | * Tool: get_sfra_documents_by_category 7 | * Purpose: Get SFRA documents filtered by category (core, product, order, customer, pricing, store, other) 8 | * Parameters: category (required) - Category to filter by 9 | * 10 | * Quick Test Commands: 11 | * node --test tests/mcp/node/get-sfra-documents-by-category.docs-only.programmatic.test.js 12 | * npm run test:mcp:node -- --grep "get_sfra_documents_by_category" 13 | * ================================================================================== 14 | */ 15 | 16 | import { test, describe, before, after, beforeEach } from 'node:test'; 17 | import { strict as assert } from 'node:assert'; 18 | import { connect } from 'mcp-aegis'; 19 | 20 | describe('SFCC MCP Server - get_sfra_documents_by_category Tool Programmatic Tests', () => { 21 | let client; 22 | 23 | before(async () => { 24 | client = await connect('./aegis.config.docs-only.json'); 25 | }); 26 | 27 | after(async () => { 28 | if (client?.connected) { 29 | await client.disconnect(); 30 | } 31 | }); 32 | 33 | beforeEach(() => { 34 | // CRITICAL: Clear all buffers to prevent test interference 35 | client.clearAllBuffers(); 36 | }); 37 | 38 | // ================================================================================== 39 | // SUCCESSFUL OPERATIONS - VALID CATEGORIES 40 | // ================================================================================== 41 | 42 | describe('Valid Category Operations', () => { 43 | test('should retrieve core SFRA documents with proper structure', async () => { 44 | const result = await client.callTool('get_sfra_documents_by_category', { 45 | category: 'core' 46 | }); 47 | 48 | // Validate MCP response structure 49 | assert.ok(result.content, 'Should have content'); 50 | assert.ok(Array.isArray(result.content), 'Content should be array'); 51 | assert.equal(result.content.length, 1, 'Should have one content item'); 52 | assert.equal(result.content[0].type, 'text', 'Content type should be text'); 53 | assert.equal(result.isError, false, 'Should not be error'); 54 | 55 | // Parse and validate JSON structure 56 | const jsonText = result.content[0].text; 57 | assert.ok(jsonText, 'Should have text content'); 58 | 59 | const documents = JSON.parse(jsonText); 60 | assert.ok(Array.isArray(documents), 'Should parse to array'); 61 | assert.ok(documents.length > 0, 'Should have documents'); 62 | }); 63 | 64 | test('should return expected core documents with correct fields', async () => { 65 | const result = await client.callTool('get_sfra_documents_by_category', { 66 | category: 'core' 67 | }); 68 | 69 | const documents = JSON.parse(result.content[0].text); 70 | 71 | // Validate document count and names 72 | assert.equal(documents.length, 5, 'Core should have 5 documents'); 73 | 74 | const expectedNames = ['querystring', 'render', 'request', 'response', 'server']; 75 | const actualNames = documents.map(doc => doc.name).sort(); 76 | assert.deepEqual(actualNames, expectedNames, 'Should have expected core document names'); 77 | 78 | // Validate document structure 79 | documents.forEach((doc, index) => { 80 | assert.ok(doc.name, `Document ${index} should have name`); 81 | assert.ok(doc.title, `Document ${index} should have title`); 82 | assert.ok(doc.description !== undefined, `Document ${index} should have description`); 83 | assert.ok(doc.type, `Document ${index} should have type`); 84 | assert.equal(doc.category, 'core', `Document ${index} should have core category`); 85 | assert.ok(doc.filename, `Document ${index} should have filename`); 86 | assert.ok(doc.filename.endsWith('.md'), `Document ${index} filename should be markdown`); 87 | }); 88 | }); 89 | 90 | test('should return product category documents with model types', async () => { 91 | const result = await client.callTool('get_sfra_documents_by_category', { 92 | category: 'product' 93 | }); 94 | 95 | assert.equal(result.isError, false, 'Should not be error'); 96 | 97 | const documents = JSON.parse(result.content[0].text); 98 | assert.ok(Array.isArray(documents), 'Should return array'); 99 | assert.ok(documents.length > 0, 'Should have product documents'); 100 | 101 | // Validate product-specific content 102 | const hasProductFull = documents.some(doc => doc.name === 'product-full'); 103 | const hasProductTile = documents.some(doc => doc.name === 'product-tile'); 104 | assert.ok(hasProductFull, 'Should contain product-full document'); 105 | assert.ok(hasProductTile, 'Should contain product-tile document'); 106 | 107 | // Validate all documents have product category 108 | documents.forEach(doc => { 109 | assert.equal(doc.category, 'product', 'All documents should have product category'); 110 | }); 111 | }); 112 | 113 | test('should handle all valid categories', async () => { 114 | const validCategories = ['core', 'product', 'order', 'customer', 'pricing', 'store', 'other']; 115 | 116 | for (const category of validCategories) { 117 | const result = await client.callTool('get_sfra_documents_by_category', { 118 | category: category 119 | }); 120 | 121 | assert.equal(result.isError, false, `Category ${category} should not error`); 122 | assert.ok(result.content, `Category ${category} should have content`); 123 | 124 | const documents = JSON.parse(result.content[0].text); 125 | assert.ok(Array.isArray(documents), `Category ${category} should return array`); 126 | 127 | // If documents exist, they should have the correct category 128 | documents.forEach(doc => { 129 | assert.equal(doc.category, category, `Document should have ${category} category`); 130 | }); 131 | } 132 | }); 133 | }); 134 | 135 | // ================================================================================== 136 | // EDGE CASES AND ERROR HANDLING 137 | // ================================================================================== 138 | 139 | describe('Edge Cases and Error Handling', () => { 140 | test('should handle invalid category gracefully', async () => { 141 | const result = await client.callTool('get_sfra_documents_by_category', { 142 | category: 'invalid_category_xyz' 143 | }); 144 | 145 | assert.equal(result.isError, false, 'Invalid category should not be error'); 146 | 147 | const documents = JSON.parse(result.content[0].text); 148 | assert.ok(Array.isArray(documents), 'Should return array'); 149 | assert.equal(documents.length, 0, 'Should return empty array for invalid category'); 150 | }); 151 | 152 | test('should require category parameter', async () => { 153 | const result = await client.callTool('get_sfra_documents_by_category', {}); 154 | 155 | assert.equal(result.isError, true, 'Missing category should be error'); 156 | assert.ok(result.content[0].text.includes('category must be a non-empty string'), 157 | 'Should have specific error message'); 158 | }); 159 | 160 | test('should handle empty category string', async () => { 161 | const result = await client.callTool('get_sfra_documents_by_category', { 162 | category: '' 163 | }); 164 | 165 | assert.equal(result.isError, true, 'Empty category should be error'); 166 | assert.ok(result.content[0].text.includes('Error'), 'Should contain error message'); 167 | }); 168 | 169 | test('should handle null category parameter', async () => { 170 | const result = await client.callTool('get_sfra_documents_by_category', { 171 | category: null 172 | }); 173 | 174 | assert.equal(result.isError, true, 'Null category should be error'); 175 | assert.ok(result.content[0].text.includes('Error'), 'Should contain error message'); 176 | }); 177 | 178 | test('should be case sensitive for categories', async () => { 179 | const testCases = ['CORE', 'Core', 'PRODUCT', 'Product']; 180 | 181 | for (const category of testCases) { 182 | const result = await client.callTool('get_sfra_documents_by_category', { 183 | category: category 184 | }); 185 | 186 | assert.equal(result.isError, false, `Category ${category} should not error`); 187 | 188 | const documents = JSON.parse(result.content[0].text); 189 | assert.equal(documents.length, 0, `Case-sensitive ${category} should return empty array`); 190 | } 191 | }); 192 | }); 193 | 194 | // ================================================================================== 195 | // DATA VALIDATION AND STRUCTURE TESTING 196 | // ================================================================================== 197 | 198 | describe('Data Validation and Structure', () => { 199 | test('should validate document field types and values', async () => { 200 | const result = await client.callTool('get_sfra_documents_by_category', { 201 | category: 'core' 202 | }); 203 | 204 | const documents = JSON.parse(result.content[0].text); 205 | 206 | documents.forEach((doc, index) => { 207 | // Field type validation 208 | assert.equal(typeof doc.name, 'string', `Document ${index} name should be string`); 209 | assert.equal(typeof doc.title, 'string', `Document ${index} title should be string`); 210 | assert.equal(typeof doc.description, 'string', `Document ${index} description should be string`); 211 | assert.equal(typeof doc.type, 'string', `Document ${index} type should be string`); 212 | assert.equal(typeof doc.category, 'string', `Document ${index} category should be string`); 213 | assert.equal(typeof doc.filename, 'string', `Document ${index} filename should be string`); 214 | 215 | // Field value validation 216 | assert.ok(doc.name.length > 0, `Document ${index} name should not be empty`); 217 | assert.ok(doc.title.length > 0, `Document ${index} title should not be empty`); 218 | assert.ok(['class', 'module', 'model'].includes(doc.type), 219 | `Document ${index} type should be valid: ${doc.type}`); 220 | assert.ok(doc.filename.endsWith('.md'), 221 | `Document ${index} filename should end with .md: ${doc.filename}`); 222 | }); 223 | }); 224 | 225 | test('should maintain consistent document structure across categories', async () => { 226 | const categories = ['core', 'product']; 227 | const allDocuments = []; 228 | 229 | for (const category of categories) { 230 | const result = await client.callTool('get_sfra_documents_by_category', { 231 | category: category 232 | }); 233 | 234 | const documents = JSON.parse(result.content[0].text); 235 | allDocuments.push(...documents); 236 | } 237 | 238 | // Validate all documents have the same structure 239 | const requiredFields = ['name', 'title', 'description', 'type', 'category', 'filename']; 240 | 241 | allDocuments.forEach((doc, index) => { 242 | requiredFields.forEach(field => { 243 | assert.ok(Object.prototype.hasOwnProperty.call(doc, field), 244 | `Document ${index} should have ${field} field`); 245 | }); 246 | 247 | // No extra fields beyond expected ones 248 | const docFields = Object.keys(doc); 249 | assert.equal(docFields.length, requiredFields.length, 250 | `Document ${index} should have exactly ${requiredFields.length} fields`); 251 | }); 252 | }); 253 | 254 | test('should return documents sorted alphabetically by name', async () => { 255 | const result = await client.callTool('get_sfra_documents_by_category', { 256 | category: 'core' 257 | }); 258 | 259 | const documents = JSON.parse(result.content[0].text); 260 | const names = documents.map(doc => doc.name); 261 | const sortedNames = [...names].sort(); 262 | 263 | assert.deepEqual(names, sortedNames, 'Documents should be sorted alphabetically by name'); 264 | }); 265 | }); 266 | 267 | // ================================================================================== 268 | // DYNAMIC VALIDATION AND BUSINESS LOGIC 269 | // ================================================================================== 270 | 271 | describe('Dynamic Validation and Business Logic', () => { 272 | test('should validate document naming conventions', async () => { 273 | const categories = ['core', 'product']; 274 | 275 | for (const category of categories) { 276 | const result = await client.callTool('get_sfra_documents_by_category', { 277 | category: category 278 | }); 279 | 280 | const documents = JSON.parse(result.content[0].text); 281 | 282 | documents.forEach(doc => { 283 | // Name should be lowercase with hyphens 284 | assert.ok(/^[a-z][a-z0-9-]*$/.test(doc.name), 285 | `Document name should follow convention: ${doc.name}`); 286 | 287 | // Filename should match name + .md 288 | assert.equal(doc.filename, `${doc.name}.md`, 289 | `Filename should match name: ${doc.filename}`); 290 | }); 291 | } 292 | }); 293 | 294 | test('should validate type consistency by category', async () => { 295 | // Core category should have classes and modules 296 | const coreResult = await client.callTool('get_sfra_documents_by_category', { 297 | category: 'core' 298 | }); 299 | 300 | const coreDocuments = JSON.parse(coreResult.content[0].text); 301 | const coreTypes = [...new Set(coreDocuments.map(doc => doc.type))]; 302 | assert.ok(coreTypes.includes('class'), 'Core should have class types'); 303 | assert.ok(coreTypes.includes('module'), 'Core should have module types'); 304 | 305 | // Product category should primarily have models 306 | const productResult = await client.callTool('get_sfra_documents_by_category', { 307 | category: 'product' 308 | }); 309 | 310 | const productDocuments = JSON.parse(productResult.content[0].text); 311 | if (productDocuments.length > 0) { 312 | // Product documents should primarily be models 313 | const modelCount = productDocuments.filter(doc => doc.type === 'model').length; 314 | const totalCount = productDocuments.length; 315 | assert.ok(modelCount / totalCount > 0.5, 'Product category should be primarily models'); 316 | } 317 | }); 318 | 319 | test('should validate unique document names within category', async () => { 320 | const result = await client.callTool('get_sfra_documents_by_category', { 321 | category: 'core' 322 | }); 323 | 324 | const documents = JSON.parse(result.content[0].text); 325 | const names = documents.map(doc => doc.name); 326 | const uniqueNames = [...new Set(names)]; 327 | 328 | assert.equal(names.length, uniqueNames.length, 329 | 'All document names within category should be unique'); 330 | }); 331 | 332 | test('should handle empty categories appropriately', async () => { 333 | // Test categories that might be empty 334 | const possiblyEmptyCategories = ['other']; 335 | 336 | for (const category of possiblyEmptyCategories) { 337 | const result = await client.callTool('get_sfra_documents_by_category', { 338 | category: category 339 | }); 340 | 341 | assert.equal(result.isError, false, `Category ${category} should not error`); 342 | 343 | const documents = JSON.parse(result.content[0].text); 344 | assert.ok(Array.isArray(documents), `Category ${category} should return array`); 345 | // Empty is acceptable for some categories 346 | } 347 | }); 348 | }); 349 | 350 | // ================================================================================== 351 | // INTEGRATION AND WORKFLOW TESTING 352 | // ================================================================================== 353 | 354 | describe('Integration and Workflow Testing', () => { 355 | test('should support category discovery workflow', async () => { 356 | // Step 1: Get available categories by testing each one 357 | const allCategories = ['core', 'product', 'order', 'customer', 'pricing', 'store', 'other']; 358 | const availableCategories = []; 359 | 360 | for (const category of allCategories) { 361 | const result = await client.callTool('get_sfra_documents_by_category', { 362 | category: category 363 | }); 364 | 365 | const documents = JSON.parse(result.content[0].text); 366 | if (documents.length > 0) { 367 | availableCategories.push(category); 368 | } 369 | } 370 | 371 | assert.ok(availableCategories.length >= 2, 'Should have at least 2 non-empty categories'); 372 | assert.ok(availableCategories.includes('core'), 'Core should be available'); 373 | 374 | // Step 2: Validate each available category has consistent structure 375 | for (const category of availableCategories) { 376 | const result = await client.callTool('get_sfra_documents_by_category', { 377 | category: category 378 | }); 379 | 380 | const documents = JSON.parse(result.content[0].text); 381 | assert.ok(documents.every(doc => doc.category === category), 382 | `All documents in ${category} should have correct category`); 383 | } 384 | }); 385 | 386 | test('should support document exploration workflow', async () => { 387 | // Step 1: Get core documents 388 | const coreResult = await client.callTool('get_sfra_documents_by_category', { 389 | category: 'core' 390 | }); 391 | 392 | const coreDocuments = JSON.parse(coreResult.content[0].text); 393 | 394 | // Step 2: Find specific document types 395 | const serverDoc = coreDocuments.find(doc => doc.name === 'server'); 396 | assert.ok(serverDoc, 'Should find server document'); 397 | assert.equal(serverDoc.type, 'class', 'Server should be a class'); 398 | 399 | const renderDoc = coreDocuments.find(doc => doc.name === 'render'); 400 | assert.ok(renderDoc, 'Should find render document'); 401 | assert.equal(renderDoc.type, 'module', 'Render should be a module'); 402 | 403 | // Step 3: Validate document relationships 404 | assert.ok(serverDoc.filename !== renderDoc.filename, 405 | 'Different documents should have different filenames'); 406 | }); 407 | 408 | test('should validate cross-category document consistency', async () => { 409 | const allDocuments = []; 410 | const allCategories = ['core', 'product', 'order', 'customer', 'pricing', 'store', 'other']; 411 | 412 | // Collect all documents across categories 413 | for (const category of allCategories) { 414 | const result = await client.callTool('get_sfra_documents_by_category', { 415 | category: category 416 | }); 417 | 418 | const documents = JSON.parse(result.content[0].text); 419 | allDocuments.push(...documents); 420 | } 421 | 422 | // Validate no duplicate document names across categories 423 | const allNames = allDocuments.map(doc => doc.name); 424 | const uniqueNames = [...new Set(allNames)]; 425 | assert.equal(allNames.length, uniqueNames.length, 426 | 'Document names should be unique across all categories'); 427 | 428 | // Validate filename consistency 429 | allDocuments.forEach(doc => { 430 | assert.equal(doc.filename, `${doc.name}.md`, 431 | `Document ${doc.name} should have consistent filename`); 432 | }); 433 | }); 434 | }); 435 | 436 | // ================================================================================== 437 | // PERFORMANCE AND RELIABILITY TESTING 438 | // ================================================================================== 439 | 440 | describe('Performance and Reliability', () => { 441 | test('should handle repeated requests consistently', async () => { 442 | const requestCount = 5; 443 | const results = []; 444 | 445 | // Make multiple identical requests 446 | for (let i = 0; i < requestCount; i++) { 447 | const result = await client.callTool('get_sfra_documents_by_category', { 448 | category: 'core' 449 | }); 450 | results.push(result); 451 | } 452 | 453 | // Validate all responses are identical 454 | const firstResponse = results[0].content[0].text; 455 | results.forEach((result, index) => { 456 | assert.equal(result.isError, false, `Request ${index} should not error`); 457 | assert.equal(result.content[0].text, firstResponse, 458 | `Request ${index} should return identical response`); 459 | }); 460 | }); 461 | 462 | test('should handle concurrent category requests sequentially', async () => { 463 | // Note: Not using Promise.all due to MCP single-process limitations 464 | const categories = ['core', 'product']; 465 | const results = []; 466 | 467 | // Sequential requests to avoid concurrency issues 468 | for (const category of categories) { 469 | const result = await client.callTool('get_sfra_documents_by_category', { 470 | category: category 471 | }); 472 | results.push({ category, result }); 473 | } 474 | 475 | // Validate all requests succeeded 476 | results.forEach(({ category, result }) => { 477 | assert.equal(result.isError, false, `Category ${category} should not error`); 478 | 479 | const documents = JSON.parse(result.content[0].text); 480 | assert.ok(Array.isArray(documents), `Category ${category} should return array`); 481 | }); 482 | }); 483 | 484 | test('should maintain performance under different input sizes', async () => { 485 | const categories = ['core', 'product', 'invalid_long_category_name_that_should_not_exist']; 486 | 487 | for (const category of categories) { 488 | const startTime = process.hrtime.bigint(); 489 | 490 | const result = await client.callTool('get_sfra_documents_by_category', { 491 | category: category 492 | }); 493 | 494 | const endTime = process.hrtime.bigint(); 495 | const duration = Number(endTime - startTime) / 1000000; // Convert to milliseconds 496 | 497 | // Functional validation (performance is environment-dependent) 498 | assert.ok(result.content, `Category ${category} should return content`); 499 | assert.ok(duration < 5000, `Category ${category} should complete within 5 seconds`); 500 | } 501 | }); 502 | }); 503 | }); 504 | 505 | // ================================================================================== 506 | // HELPER FUNCTIONS 507 | // ================================================================================== 508 | 509 | // Helper functions removed to avoid unused function linting errors. 510 | // Validation logic is implemented inline within tests for better maintainability. 511 | ``` -------------------------------------------------------------------------------- /tests/mcp/node/search-site-preferences.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { test, describe, before, after, beforeEach } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import { connect } from 'mcp-aegis'; 4 | 5 | describe('search_site_preferences tool - Full Mode Programmatic Tests', () => { 6 | let client; 7 | 8 | before(async () => { 9 | client = await connect('./aegis.config.with-dw.json'); 10 | }); 11 | 12 | after(async () => { 13 | if (client?.connected) { 14 | await client.disconnect(); 15 | } 16 | }); 17 | 18 | beforeEach(() => { 19 | // CRITICAL: Clear all buffers to prevent leaking into next tests 20 | client.clearAllBuffers(); 21 | }); 22 | 23 | describe('Complex Query Structure Validation', () => { 24 | test('should validate complex boolean query with multiple conditions', async () => { 25 | const complexQuery = { 26 | groupId: 'Storefront', 27 | instanceType: 'sandbox', 28 | searchRequest: { 29 | query: { 30 | bool_query: { 31 | must: [ 32 | { 33 | text_query: { 34 | fields: ['id', 'display_name'], 35 | search_phrase: 'cart' 36 | } 37 | } 38 | ], 39 | should: [ 40 | { 41 | term_query: { 42 | fields: ['value_type'], 43 | operator: 'is', 44 | values: ['boolean'] 45 | } 46 | } 47 | ] 48 | } 49 | }, 50 | count: 5, 51 | start: 0 52 | } 53 | }; 54 | 55 | const result = await client.callTool('search_site_preferences', complexQuery); 56 | 57 | assert.equal(result.isError, false, 'Complex boolean query should succeed'); 58 | assert.ok(result.content, 'Should have content'); 59 | assert.equal(result.content[0].type, 'text', 'Content should be text type'); 60 | 61 | const responseData = JSON.parse(result.content[0].text); 62 | assert.ok(responseData.hits, 'Response should have hits array'); 63 | assert.ok(responseData.query, 'Response should echo query'); 64 | assert.equal(responseData.query.bool_query.must.length, 1, 'Should preserve bool_query structure'); 65 | assert.equal(responseData.query.bool_query.should.length, 1, 'Should preserve should conditions'); 66 | }); 67 | 68 | test('should handle nested boolean queries with must_not conditions', async () => { 69 | const nestedQuery = { 70 | groupId: 'System', 71 | instanceType: 'sandbox', 72 | searchRequest: { 73 | query: { 74 | bool_query: { 75 | must: [ 76 | { 77 | match_all_query: {} 78 | } 79 | ], 80 | must_not: [ 81 | { 82 | term_query: { 83 | fields: ['value_type'], 84 | operator: 'is', 85 | values: ['password'] 86 | } 87 | } 88 | ] 89 | } 90 | }, 91 | count: 10 92 | } 93 | }; 94 | 95 | const result = await client.callTool('search_site_preferences', nestedQuery); 96 | 97 | assert.equal(result.isError, false, 'Nested boolean query should succeed'); 98 | 99 | const responseData = JSON.parse(result.content[0].text); 100 | assert.ok(responseData.hits, 'Should have hits'); 101 | assert.ok(responseData.query.bool_query.must_not, 'Should preserve must_not conditions'); 102 | 103 | // Verify no password type preferences are returned 104 | const passwordPrefs = responseData.hits.filter(hit => 105 | hit.attribute_definition?.value_type === 'password' 106 | ); 107 | assert.equal(passwordPrefs.length, 0, 'Should exclude password type preferences'); 108 | }); 109 | }); 110 | 111 | describe('Response Structure Deep Validation', () => { 112 | test('should validate complete response structure with all fields', async () => { 113 | const result = await client.callTool('search_site_preferences', { 114 | groupId: 'Storefront', 115 | instanceType: 'sandbox', 116 | searchRequest: { 117 | query: { match_all_query: {} }, 118 | count: 3, 119 | start: 0 120 | }, 121 | options: { 122 | expand: 'value', 123 | maskPasswords: false 124 | } 125 | }); 126 | 127 | assert.equal(result.isError, false, 'Request should succeed'); 128 | 129 | const responseData = JSON.parse(result.content[0].text); 130 | 131 | // Validate top-level structure 132 | assert.ok(responseData._type, 'Should have _type field'); 133 | assert.equal(responseData._type, 'preference_value_search_result', 'Should have correct type'); 134 | assert.ok(Array.isArray(responseData.hits), 'Hits should be array'); 135 | assert.ok(typeof responseData.start === 'number', 'Start should be number'); 136 | assert.ok(typeof responseData.count === 'number', 'Count should be number'); 137 | assert.ok(typeof responseData.total === 'number', 'Total should be number'); 138 | assert.ok(responseData.query, 'Should have query echo'); 139 | 140 | // Validate hit structure if any hits exist 141 | if (responseData.hits.length > 0) { 142 | const hit = responseData.hits[0]; 143 | assert.ok(hit.attribute_definition, 'Hit should have attribute_definition'); 144 | assert.ok(hit.site_values, 'Hit should have site_values'); 145 | 146 | // Validate attribute definition structure 147 | const attrDef = hit.attribute_definition; 148 | assert.ok(typeof attrDef.id === 'string', 'Attribute definition should have id'); 149 | assert.ok(typeof attrDef.display_name === 'object', 'Display name should be object'); 150 | assert.ok(typeof attrDef.value_type === 'string', 'Should have value_type'); 151 | 152 | // Validate site values structure 153 | const siteValues = hit.site_values; 154 | assert.ok(typeof siteValues === 'object', 'Site values should be object'); 155 | assert.ok(siteValues !== null, 'Site values should not be null'); 156 | 157 | if (Object.keys(siteValues).length > 0) { 158 | const firstSiteId = Object.keys(siteValues)[0]; 159 | assert.ok(typeof firstSiteId === 'string', 'Site ID should be string'); 160 | assert.ok(Object.prototype.hasOwnProperty.call(siteValues, firstSiteId), 'Site values should have site ID property'); 161 | } 162 | } 163 | }); 164 | 165 | test('should validate pagination metadata consistency', async () => { 166 | // Get total count first with reasonable count 167 | const countResult = await client.callTool('search_site_preferences', { 168 | groupId: 'Storefront', 169 | instanceType: 'sandbox', 170 | searchRequest: { 171 | query: { match_all_query: {} }, 172 | count: 50, // Reasonable count instead of 1000 173 | start: 0 174 | } 175 | }); 176 | 177 | assert.equal(countResult.isError, false, 'Count request should succeed'); 178 | const countData = JSON.parse(countResult.content[0].text); 179 | const totalPreferences = countData.total; 180 | 181 | if (totalPreferences > 5) { 182 | // Test pagination with smaller pages 183 | const pageSize = 3; 184 | const page1Result = await client.callTool('search_site_preferences', { 185 | groupId: 'Storefront', 186 | instanceType: 'sandbox', 187 | searchRequest: { 188 | query: { match_all_query: {} }, 189 | count: pageSize, 190 | start: 0 191 | } 192 | }); 193 | 194 | const page2Result = await client.callTool('search_site_preferences', { 195 | groupId: 'Storefront', 196 | instanceType: 'sandbox', 197 | searchRequest: { 198 | query: { match_all_query: {} }, 199 | count: pageSize, 200 | start: pageSize 201 | } 202 | }); 203 | 204 | assert.equal(page1Result.isError, false, 'Page 1 should succeed'); 205 | assert.equal(page2Result.isError, false, 'Page 2 should succeed'); 206 | 207 | const page1Data = JSON.parse(page1Result.content[0].text); 208 | const page2Data = JSON.parse(page2Result.content[0].text); 209 | 210 | // Validate pagination metadata 211 | assert.equal(page1Data.start, 0, 'Page 1 start should be 0'); 212 | assert.equal(page1Data.count, pageSize, 'Page 1 count should match request'); 213 | assert.equal(page2Data.start, pageSize, 'Page 2 start should be offset'); 214 | assert.equal(page2Data.count, pageSize, 'Page 2 count should match request'); 215 | 216 | // Both pages should report same total 217 | assert.equal(page1Data.total, page2Data.total, 'Total should be consistent across pages'); 218 | 219 | // Validate no duplicate preferences between pages 220 | const page1Ids = page1Data.hits.map(hit => hit.attribute_definition.id); 221 | const page2Ids = page2Data.hits.map(hit => hit.attribute_definition.id); 222 | const intersection = page1Ids.filter(id => page2Ids.includes(id)); 223 | assert.equal(intersection.length, 0, 'Pages should not have duplicate preferences'); 224 | } 225 | }); 226 | }); 227 | 228 | describe('Advanced Query Scenarios', () => { 229 | test('should handle multiple preference groups sequentially', async () => { 230 | const preferenceGroups = ['Storefront', 'System', 'SFRA']; 231 | const results = new Map(); 232 | 233 | // Test each group sequentially (no concurrent requests) 234 | for (const groupId of preferenceGroups) { 235 | const result = await client.callTool('search_site_preferences', { 236 | groupId, 237 | instanceType: 'sandbox', 238 | searchRequest: { 239 | query: { match_all_query: {} }, 240 | count: 5 241 | } 242 | }); 243 | 244 | assert.equal(result.isError, false, `Group ${groupId} should be accessible`); 245 | 246 | const responseData = JSON.parse(result.content[0].text); 247 | results.set(groupId, responseData); 248 | 249 | // Validate group-specific response 250 | assert.ok(responseData.hits, `Group ${groupId} should have hits`); 251 | assert.ok(responseData.total >= 0, `Group ${groupId} should have total count`); 252 | } 253 | 254 | // Validate that different groups return different preferences 255 | const storefrontIds = results.get('Storefront').hits.map(hit => hit.attribute_definition.id); 256 | const systemIds = results.get('System').hits.map(hit => hit.attribute_definition.id); 257 | 258 | if (storefrontIds.length > 0 && systemIds.length > 0) { 259 | const overlap = storefrontIds.filter(id => systemIds.includes(id)); 260 | assert.equal(overlap.length, 0, 'Different groups should have different preferences'); 261 | } 262 | }); 263 | 264 | test('should validate search with sorting and field selection', async () => { 265 | const result = await client.callTool('search_site_preferences', { 266 | groupId: 'Storefront', 267 | instanceType: 'sandbox', 268 | searchRequest: { 269 | query: { match_all_query: {} }, 270 | count: 10, 271 | start: 0, 272 | select: '(*)', 273 | sorts: [ 274 | { 275 | field: 'id', 276 | sort_order: 'asc' 277 | } 278 | ] 279 | } 280 | }); 281 | 282 | assert.equal(result.isError, false, 'Sorted query should succeed'); 283 | 284 | const responseData = JSON.parse(result.content[0].text); 285 | assert.ok(responseData.hits, 'Should have hits'); 286 | 287 | // Note: Sorting validation removed as mock server doesn't implement actual sorting 288 | // This test now focuses on validating that sorting parameters are accepted 289 | 290 | // Validate that select parameter is echoed in response 291 | assert.ok(responseData.select, 'Response should echo select parameter'); 292 | assert.equal(responseData.select, '(*)', 'Select parameter should be preserved as (*)'); 293 | }); 294 | }); 295 | 296 | describe('Error Handling and Edge Cases', () => { 297 | test('should handle query timeout gracefully', async () => { 298 | // Test with a very complex query that might timeout 299 | const complexTimeoutQuery = { 300 | groupId: 'Storefront', 301 | instanceType: 'sandbox', 302 | searchRequest: { 303 | query: { 304 | bool_query: { 305 | must: [ 306 | { 307 | text_query: { 308 | fields: ['id', 'display_name', 'description'], 309 | search_phrase: 'a' // Very broad search 310 | } 311 | } 312 | ], 313 | should: Array.from({ length: 10 }, (_, i) => ({ 314 | term_query: { 315 | fields: ['value_type'], 316 | operator: 'is', 317 | values: [`type_${i}`] 318 | } 319 | })) 320 | } 321 | }, 322 | count: 1000, // Large count 323 | start: 0 324 | } 325 | }; 326 | 327 | const result = await client.callTool('search_site_preferences', complexTimeoutQuery); 328 | 329 | // Should either succeed or fail gracefully with timeout error 330 | if (result.isError) { 331 | assert.ok( 332 | result.content[0].text.includes('timeout') || 333 | result.content[0].text.includes('error') || 334 | result.content[0].text.includes('invalid'), 335 | 'Timeout should produce meaningful error message' 336 | ); 337 | } else { 338 | // If it succeeds, validate response structure 339 | const responseData = JSON.parse(result.content[0].text); 340 | assert.ok(responseData._type, 'Should have valid response structure even for complex queries'); 341 | } 342 | }); 343 | 344 | test('should validate parameter combinations and constraints', async () => { 345 | const testCases = [ 346 | { 347 | name: 'negative start parameter', 348 | params: { 349 | groupId: 'Storefront', 350 | instanceType: 'sandbox', 351 | searchRequest: { 352 | query: { match_all_query: {} }, 353 | start: -1 354 | } 355 | }, 356 | shouldSucceed: false 357 | } 358 | ]; 359 | 360 | for (const testCase of testCases) { 361 | const result = await client.callTool('search_site_preferences', testCase.params); 362 | 363 | if (testCase.shouldSucceed) { 364 | assert.equal(result.isError, false, `${testCase.name} should succeed`); 365 | } else { 366 | assert.equal(result.isError, true, `${testCase.name} should fail with validation error`); 367 | assert.ok( 368 | result.content[0].text.includes('error') || 369 | result.content[0].text.includes('Error') || 370 | result.content[0].text.includes('invalid') || 371 | result.content[0].text.includes('required') || 372 | result.content[0].text.includes('must be'), 373 | `${testCase.name} should provide meaningful error message. Got: ${result.content[0].text}` 374 | ); 375 | } 376 | } 377 | }); 378 | }); 379 | 380 | describe('Data Consistency and Business Logic', () => { 381 | test('should validate preference value types and constraints', async () => { 382 | const result = await client.callTool('search_site_preferences', { 383 | groupId: 'Storefront', 384 | instanceType: 'sandbox', 385 | searchRequest: { 386 | query: { match_all_query: {} }, 387 | count: 20 388 | } 389 | }); 390 | 391 | assert.equal(result.isError, false, 'Query should succeed'); 392 | 393 | const responseData = JSON.parse(result.content[0].text); 394 | 395 | if (responseData.hits.length > 0) { 396 | const validValueTypes = ['string', 'boolean', 'int', 'double', 'password', 'email', 'text', 'html', 'date', 'enum_of_string', 'set_of_string']; 397 | 398 | responseData.hits.forEach((hit, index) => { 399 | const attrDef = hit.attribute_definition; 400 | 401 | // Validate required fields 402 | assert.ok(attrDef.id, `Hit ${index} should have attribute id`); 403 | assert.ok(attrDef.value_type, `Hit ${index} should have value_type`); 404 | assert.ok(validValueTypes.includes(attrDef.value_type), 405 | `Hit ${index} should have valid value_type: ${attrDef.value_type}`); 406 | 407 | // Validate display_name structure 408 | if (attrDef.display_name) { 409 | assert.ok(typeof attrDef.display_name === 'object', 410 | `Hit ${index} display_name should be object`); 411 | } 412 | 413 | // Validate site_values structure 414 | assert.ok(typeof hit.site_values === 'object', 415 | `Hit ${index} site_values should be object`); 416 | assert.ok(hit.site_values !== null, 417 | `Hit ${index} site_values should not be null`); 418 | 419 | const siteValueEntries = Object.entries(hit.site_values); 420 | siteValueEntries.forEach(([siteId, siteValue], siteIndex) => { 421 | assert.ok(typeof siteId === 'string', 422 | `Hit ${index}, site entry ${siteIndex} should have string site ID`); 423 | 424 | // Validate value based on type (skip null values) 425 | if (siteValue !== null) { 426 | if (attrDef.value_type === 'boolean') { 427 | assert.ok(typeof siteValue === 'boolean', 428 | `Hit ${index}, site ${siteId} should have boolean value for boolean type`); 429 | } 430 | 431 | if (attrDef.value_type === 'int') { 432 | assert.ok(Number.isInteger(siteValue), 433 | `Hit ${index}, site ${siteId} should have integer value for int type`); 434 | } 435 | } 436 | }); 437 | }); 438 | } 439 | }); 440 | 441 | test('should validate query result consistency across calls', async () => { 442 | const queryParams = { 443 | groupId: 'Storefront', 444 | instanceType: 'sandbox', 445 | searchRequest: { 446 | query: { 447 | text_query: { 448 | fields: ['id', 'display_name'], 449 | search_phrase: 'cart' 450 | } 451 | }, 452 | count: 10 453 | } 454 | }; 455 | 456 | // Execute same query multiple times 457 | const results = []; 458 | for (let i = 0; i < 3; i++) { 459 | const result = await client.callTool('search_site_preferences', queryParams); 460 | assert.equal(result.isError, false, `Call ${i + 1} should succeed`); 461 | 462 | const responseData = JSON.parse(result.content[0].text); 463 | results.push(responseData); 464 | } 465 | 466 | // Validate consistency across calls 467 | const firstResult = results[0]; 468 | for (let i = 1; i < results.length; i++) { 469 | const currentResult = results[i]; 470 | 471 | assert.equal(currentResult.total, firstResult.total, 472 | `Call ${i + 1} total should match first call`); 473 | assert.equal(currentResult.hits.length, firstResult.hits.length, 474 | `Call ${i + 1} hits count should match first call`); 475 | 476 | // Compare preference IDs (order should be consistent) 477 | const firstIds = firstResult.hits.map(hit => hit.attribute_definition.id); 478 | const currentIds = currentResult.hits.map(hit => hit.attribute_definition.id); 479 | assert.deepEqual(currentIds, firstIds, 480 | `Call ${i + 1} should return same preferences in same order`); 481 | } 482 | }); 483 | }); 484 | 485 | describe('Performance and Scalability Validation', () => { 486 | test('should handle large result sets efficiently', async () => { 487 | const largeQueryResult = await client.callTool('search_site_preferences', { 488 | groupId: 'Storefront', 489 | instanceType: 'sandbox', 490 | searchRequest: { 491 | query: { match_all_query: {} }, 492 | count: 50 // Reduced from 200 to avoid server issues 493 | } 494 | }); 495 | 496 | assert.equal(largeQueryResult.isError, false, 'Large query should succeed'); 497 | 498 | const responseData = JSON.parse(largeQueryResult.content[0].text); 499 | 500 | // Validate response structure is maintained for large results 501 | assert.ok(responseData._type, 'Large response should have type'); 502 | assert.ok(Array.isArray(responseData.hits), 'Large response should have hits array'); 503 | assert.ok(typeof responseData.total === 'number', 'Large response should have total'); 504 | 505 | // Validate all hits have required structure 506 | responseData.hits.forEach((hit, index) => { 507 | assert.ok(hit.attribute_definition, `Hit ${index} should have attribute_definition`); 508 | assert.ok(hit.site_values, `Hit ${index} should have site_values`); 509 | assert.ok(hit.attribute_definition.id, `Hit ${index} should have id`); 510 | }); 511 | }); 512 | 513 | test('should validate functional reliability over multiple operations', async () => { 514 | const operations = [ 515 | { groupId: 'Storefront', query: { match_all_query: {} } }, 516 | { groupId: 'System', query: { text_query: { fields: ['id'], search_phrase: 'test' } } }, 517 | { groupId: 'SFRA', query: { match_all_query: {} } }, 518 | { groupId: 'Storefront', query: { term_query: { fields: ['value_type'], operator: 'is', values: ['boolean'] } } } 519 | ]; 520 | 521 | const results = []; 522 | 523 | for (const operation of operations) { 524 | const result = await client.callTool('search_site_preferences', { 525 | groupId: operation.groupId, 526 | instanceType: 'sandbox', 527 | searchRequest: { 528 | query: operation.query, 529 | count: 10 530 | } 531 | }); 532 | 533 | results.push({ 534 | groupId: operation.groupId, 535 | success: !result.isError, 536 | hasContent: result.content && result.content.length > 0, 537 | responseValid: !result.isError && result.content[0].text.startsWith('{') 538 | }); 539 | } 540 | 541 | // Calculate reliability metrics 542 | const successfulOperations = results.filter(r => r.success).length; 543 | const validResponses = results.filter(r => r.responseValid).length; 544 | 545 | assert.ok(successfulOperations >= operations.length * 0.8, 546 | `At least 80% of operations should succeed (${successfulOperations}/${operations.length})`); 547 | assert.ok(validResponses >= operations.length * 0.8, 548 | `At least 80% of responses should be valid JSON (${validResponses}/${operations.length})`); 549 | 550 | // Log reliability stats for monitoring 551 | console.log(`\nReliability Stats:`); 552 | console.log(`- Successful operations: ${successfulOperations}/${operations.length} (${(successfulOperations/operations.length*100).toFixed(1)}%)`); 553 | console.log(`- Valid responses: ${validResponses}/${operations.length} (${(validResponses/operations.length*100).toFixed(1)}%)`); 554 | }); 555 | }); 556 | }); ``` -------------------------------------------------------------------------------- /docs-site/pages/SecurityPage.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import SEO from '../components/SEO'; 4 | import BreadcrumbSchema from '../components/BreadcrumbSchema'; 5 | import StructuredData from '../components/StructuredData'; 6 | import { H1, PageSubtitle, H2, H3 } from '../components/Typography'; 7 | import { InlineCode } from '../components/CodeBlock'; 8 | import { SITE_DATES } from '../constants'; 9 | 10 | // Small utility card 11 | const Pill: React.FC<React.PropsWithChildren<{ color?: string }>> = ({ children, color = 'from-blue-600 to-purple-600' }) => ( 12 | <div className={`inline-flex items-center gap-2 bg-gradient-to-r ${color} text-white px-4 py-2 rounded-full text-sm font-medium`}>{children}</div> 13 | ); 14 | 15 | const Bullet: React.FC<React.PropsWithChildren<{ icon?: string; className?: string }>> = ({ children, icon = '✔', className = '' }) => ( 16 | <li className={`flex items-start gap-2 text-sm text-gray-700 ${className}`}> 17 | <span className="mt-0.5 text-green-600 flex-shrink-0">{icon}</span> 18 | <span>{children}</span> 19 | </li> 20 | ); 21 | 22 | const SectionShell: React.FC<React.PropsWithChildren<{ gradient?: string; className?: string; border?: string }>> = ({ children, gradient = 'from-blue-50 via-indigo-50 to-purple-50', className = '', border = 'border-blue-100' }) => ( 23 | <div className={`mb-20 last:mb-0 bg-gradient-to-r ${gradient} rounded-2xl p-8 shadow-xl ${border} border ${className}`}>{children}</div> 24 | ); 25 | 26 | // Structured feature list rows for mode comparison 27 | const ModeFeatureList: React.FC<{ color: 'green' | 'blue'; items: Array<{ icon: string; label: string; detail: string }> }> = ({ color, items }) => { 28 | const colorMap = { 29 | green: { 30 | badge: 'bg-green-100 text-green-800 border-green-200', 31 | icon: 'text-green-600', 32 | label: 'text-green-900', 33 | detail: 'text-green-700' 34 | }, 35 | blue: { 36 | badge: 'bg-blue-100 text-blue-800 border-blue-200', 37 | icon: 'text-blue-600', 38 | label: 'text-blue-900', 39 | detail: 'text-blue-700' 40 | } 41 | } as const; 42 | const c = colorMap[color]; 43 | return ( 44 | <ul className="list-none p-0 m-0 space-y-3"> 45 | {items.map(item => ( 46 | <li key={item.label} className="group"> 47 | <div className={`flex items-start gap-3 rounded-xl border ${c.badge} bg-white/70 backdrop-blur-sm px-3 py-2 hover:shadow-sm transition`}> 48 | <span className={`text-base leading-none mt-0.5 ${c.icon}`}>{item.icon}</span> 49 | <div className="flex-1 min-w-0"> 50 | <p className={`m-0 text-sm font-medium ${c.label}`}>{item.label}</p> 51 | <p className={`m-0 text-[11px] leading-snug ${c.detail}`}>{item.detail}</p> 52 | </div> 53 | </div> 54 | </li> 55 | ))} 56 | </ul> 57 | ); 58 | }; 59 | 60 | const SecurityPage: React.FC = () => { 61 | const securityStructuredData = { 62 | "@context": "https://schema.org", 63 | "@type": "TechArticle", 64 | "headline": "Security & Privacy - SFCC Development MCP Server", 65 | "description": "Security guidelines and privacy considerations for SFCC Development MCP Server. Credential protection, threat mitigations, data handling and secure usage checklist.", 66 | "author": { 67 | "@type": "Person", 68 | "name": "Thomas Theunen" 69 | }, 70 | "publisher": { 71 | "@type": "Person", 72 | "name": "Thomas Theunen" 73 | }, 74 | "datePublished": SITE_DATES.PUBLISHED, 75 | "dateModified": SITE_DATES.MODIFIED, 76 | "url": "https://sfcc-mcp-dev.rhino-inquisitor.com/security/", 77 | "mainEntity": { 78 | "@type": "Guide", 79 | "name": "SFCC MCP Security Guide" 80 | } 81 | }; 82 | 83 | return ( 84 | <div className="max-w-6xl mx-auto px-6 py-10"> 85 | <SEO 86 | title="Security & Privacy" 87 | description="Security guidelines and privacy considerations for SFCC Development MCP Server. Credential protection, threat mitigations, data handling and secure usage checklist." 88 | keywords="SFCC MCP security, Commerce Cloud security, MCP server privacy, SFCC credential protection, development security, API security, local development security, SFCC authentication security" 89 | canonical="/security/" 90 | ogType="article" 91 | /> 92 | <BreadcrumbSchema items={[ 93 | { name: "Home", url: "/" }, 94 | { name: "Security", url: "/security/" } 95 | ]} /> 96 | <StructuredData structuredData={securityStructuredData} /> 97 | 98 | {/* Hero */} 99 | <header className="text-center mb-16"> 100 | <Pill>Security & Privacy</Pill> 101 | <H1 id="security-guidelines" className="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-gray-900 via-blue-900 to-purple-900 bg-clip-text text-transparent mt-6 mb-6">Built-In Guardrails – You Add Discipline</H1> 102 | <PageSubtitle className="text-xl md:text-2xl text-gray-600 max-w-4xl mx-auto leading-relaxed"> 103 | Opinionated local-only design: minimal credential footprint, scoped API access, defensive parsing. Use this page as a <strong>practical hardening checklist</strong>, not a marketing overview. 104 | </PageSubtitle> 105 | <p className="mt-4 text-[11px] uppercase tracking-wide text-gray-400">Surface: <strong>36+ specialized tools</strong> (docs, best practices, SFRA, cartridge gen, runtime logs, job logs, system & custom objects, site preferences, code versions)</p> 106 | </header> 107 | 108 | {/* Quick Essentials */} 109 | <div className="grid md:grid-cols-3 gap-6 mb-20"> 110 | {[ 111 | { title: 'Local Only', desc: 'Never deploy to shared or production infra. No multi-user isolation layer exists.' }, 112 | { title: 'Least Privilege', desc: 'Grant only OCAPI resources you actively need.' }, 113 | { title: 'No Persistent Secrets', desc: 'Credentials live in memory during execution; you own filesystem storage strategy.' } 114 | ].map(card => ( 115 | <div key={card.title} className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm"> 116 | <h3 className="font-semibold text-gray-900 mb-2 text-lg">{card.title}</h3> 117 | <p className="text-sm text-gray-600 leading-relaxed">{card.desc}</p> 118 | </div> 119 | ))} 120 | </div> 121 | 122 | {/* Mode Comparison */} 123 | <SectionShell> 124 | <div className="text-center mb-10"> 125 | <H2 id="modes" className="text-3xl font-bold mb-3">🔐 Modes & Security Characteristics</H2> 126 | <p className="text-gray-700 max-w-3xl mx-auto text-lg"> 127 | Both modes are designed for <strong>local single‑developer use</strong>. Docs mode has a <em>zero credential surface</em>; Full Mode’s profile is essentially the same as any normal SFCC development workflow using a <InlineCode>dw.json</InlineCode> for OCAPI + WebDAV access. Choose based on capability needs, not fear—just scope credentials sensibly. 128 | </p> 129 | </div> 130 | <div className="grid lg:grid-cols-2 gap-8"> 131 | {/* Docs Mode Card */} 132 | <div className="rounded-2xl bg-green-50 border border-green-200 p-6 flex flex-col"> 133 | <h3 className="text-xl font-semibold text-green-800 mb-4 flex items-center gap-2"> 134 | <span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-green-600 text-white text-sm shadow">D</span> 135 | Docs Mode (Default) 136 | </h3> 137 | <ModeFeatureList 138 | color="green" 139 | items={[ 140 | { icon: '❇', label: 'No credentials required', detail: 'Pure static: zero auth surface' }, 141 | { icon: '📄', label: 'Static content only', detail: 'Docs, guides, cartridge scaffolding' }, 142 | { icon: '🧱', label: 'No outbound authenticated calls', detail: 'Nothing to leak or revoke' }, 143 | { icon: '🧪', label: 'Safe capability exploration', detail: 'Great for AI tool schema discovery' }, 144 | { icon: '�', label: 'Instant reversible baseline', detail: 'Add credentials later without refactor' } 145 | ]} 146 | /> 147 | <div className="mt-5 text-[11px] text-green-700 font-medium bg-white/60 rounded-md px-3 py-2 border border-green-200"> 148 | Baseline mode: zero credential management, ideal first run. 149 | </div> 150 | </div> 151 | {/* Full Mode Card */} 152 | <div className="rounded-2xl bg-blue-50 border border-blue-200 p-6 flex flex-col"> 153 | <h3 className="text-xl font-semibold text-blue-800 mb-4 flex items-center gap-2"> 154 | <span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-blue-600 text-white text-sm shadow">F</span> 155 | Full Mode (<InlineCode>--dw-json</InlineCode>) 156 | </h3> 157 | <ModeFeatureList 158 | color="blue" 159 | items={[ 160 | { icon: '🔑', label: 'Credential parity', detail: 'Same auth data you already use locally' }, 161 | { icon: '🪵', label: 'Runtime + job logs', detail: 'Tail, search, summarize – read-only WebDAV access' }, 162 | { icon: '🧭', label: 'System & custom object metadata', detail: 'OCAPI Data API – attribute & group definitions' }, 163 | { icon: '⚙️', label: 'Site preference discovery', detail: 'Group-scoped search with masked password values' }, 164 | { icon: '🚦', label: 'Explicit code version activation', detail: 'Never automatic; requires targeted command' }, 165 | { icon: '🪄', label: 'Cartridge generation + docs', detail: 'Same as docs mode plus live capabilities' } 166 | ]} 167 | /> 168 | <div className="mt-5 text-[11px] text-blue-700 font-medium bg-white/60 rounded-md px-3 py-2 border border-blue-200"> 169 | Comparable risk to normal SFCC dev with <InlineCode>dw.json</InlineCode>; treat scope hygiene the same. 170 | </div> 171 | </div> 172 | </div> 173 | <div className="mt-8 text-xs text-gray-600 text-center">Switch between modes freely: omit <InlineCode>--dw-json</InlineCode> to return to a zero‑credential baseline.</div> 174 | </SectionShell> 175 | {/* Inline component definitions for mode feature rows */} 176 | {/* Keeping them near usage for maintainability; extract later if reused */} 177 | {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */} 178 | 179 | {/* Hardening Checklist */} 180 | <SectionShell gradient="from-gray-50 via-slate-50 to-blue-50" border="border-gray-200"> 181 | <div className="text-center mb-8"> 182 | <H2 id="checklist" className="text-3xl font-bold mb-3">📋 Baseline Hardening Checklist</H2> 183 | <p className="text-gray-700 max-w-2xl mx-auto">Perform these once per environment. Keep it lightweight; delete unused credentials.</p> 184 | </div> 185 | <ol className="grid md:grid-cols-2 gap-6 counter-reset list-none pl-0"> 186 | {[ 187 | 'Confirm sandbox hostname (never production domain).', 188 | 'Add dw.json + *.dw.json to .gitignore and verify not tracked.', 189 | 'chmod 600 dw.json (owner read/write only).', 190 | 'Remove unused OAuth fields if only using logs.', 191 | 'Grant only required OCAPI resources (add incrementally).', 192 | 'Mask secrets with environment overrides in CI contexts.', 193 | 'Run docs mode first; validate tool set boundaries.', 194 | 'Rotate client secret + password on schedule (quarterly baseline).' 195 | ].map(item => ( 196 | <li key={item} className="relative pl-10 text-sm text-gray-700 leading-relaxed"> 197 | <span className="absolute left-0 top-0 w-7 h-7 rounded-full bg-gradient-to-br from-blue-600 to-purple-600 text-white flex items-center justify-center text-xs font-semibold shadow">{String(([ 198 | 'Confirm sandbox hostname (never production domain).', 199 | 'Add dw.json + *.dw.json to .gitignore and verify not tracked.', 200 | 'chmod 600 dw.json (owner read/write only).', 201 | 'Remove unused OAuth fields if only using logs.', 202 | 'Grant only required OCAPI resources (add incrementally).', 203 | 'Mask secrets with environment overrides in CI contexts.', 204 | 'Run docs mode first; validate tool set boundaries.', 205 | 'Rotate client secret + password on schedule (quarterly baseline).' 206 | ].indexOf(item) + 1))}</span> 207 | {item} 208 | </li> 209 | ))} 210 | </ol> 211 | </SectionShell> 212 | 213 | {/* Credential Handling */} 214 | <SectionShell gradient="from-emerald-50 via-teal-50 to-cyan-50" border="border-emerald-200"> 215 | <div className="grid md:grid-cols-3 gap-8 mb-6 items-start"> 216 | <div className="md:col-span-3 max-w-2xl"> 217 | <H2 id="credentials" className="text-2xl font-bold mb-2">🛡️ Credential Handling</H2> 218 | <p className="text-sm text-gray-700 leading-relaxed">You retain full control of persistence. The server reads your <InlineCode>dw.json</InlineCode> once, hydrates in-memory configuration, and performs authenticated calls. No outbound exfiltration logic exists.</p> 219 | </div> 220 | </div> 221 | <div className="grid md:grid-cols-3 gap-6"> 222 | <div className="rounded-xl bg-white border border-gray-200 p-5"> 223 | <h3 className="font-semibold text-sm mb-2">Minimize Scope</h3> 224 | <ul className="text-xs space-y-1 text-gray-600 list-disc pl-4"> 225 | <li>Start w/ no Data API resources</li> 226 | <li>Add system objects only when needed</li> 227 | <li>Remove stale resources quarterly</li> 228 | </ul> 229 | </div> 230 | <div className="rounded-xl bg-white border border-gray-200 p-5"> 231 | <h3 className="font-semibold text-sm mb-2">Protect Files</h3> 232 | <ul className="text-xs space-y-1 text-gray-600 list-disc pl-4"> 233 | <li><InlineCode>chmod 600 dw.json</InlineCode></li> 234 | <li>Avoid shared directories (Sync/Drive)</li> 235 | <li>Do not email secrets</li> 236 | </ul> 237 | </div> 238 | <div className="rounded-xl bg-white border border-gray-200 p-5"> 239 | <h3 className="font-semibold text-sm mb-2">Rotate & Audit</h3> 240 | <ul className="text-xs space-y-1 text-gray-600 list-disc pl-4"> 241 | <li>Quarterly secret rotation baseline</li> 242 | <li>Remove orphaned API clients</li> 243 | <li>Track creation dates (label names)</li> 244 | </ul> 245 | </div> 246 | </div> 247 | </SectionShell> 248 | 249 | {/* Threat Model & Mitigations */} 250 | <SectionShell gradient="from-red-50 via-rose-50 to-orange-50" border="border-red-200"> 251 | <div className="text-center mb-10"> 252 | <H2 id="threat-model" className="text-3xl font-bold mb-3">🧪 Practical Threat Model (Local Context)</H2> 253 | <p className="text-gray-700 max-w-3xl mx-auto text-lg">In a single‑developer local setup the incremental risk introduced by Full Mode is roughly equivalent to any normal use of <InlineCode>dw.json</InlineCode>. Core concerns remain <strong>credential scope creep</strong>, <strong>accidental sharing of log snippets containing business data</strong>, and <strong>copying sensitive preference values externally</strong>. Below: built‑in mitigations vs. your ongoing hygiene tasks.</p> 254 | </div> 255 | <div className="grid md:grid-cols-2 gap-8"> 256 | <div className="rounded-xl bg-white p-6 border border-gray-200"> 257 | <h3 className="font-semibold mb-3 text-gray-900">Mitigated In Design</h3> 258 | <ul className="space-y-2 text-sm"> 259 | <Bullet>Path traversal (validated absolute paths)</Bullet> 260 | <Bullet>Parameter schema/type validation</Bullet> 261 | <Bullet>Read-only log operations (no writes)</Bullet> 262 | <Bullet>Scoped tool registration (no dynamic eval)</Bullet> 263 | <Bullet>Token refresh w/ expiration handling</Bullet> 264 | <Bullet>Memory-only caching (no disk persistence)</Bullet> 265 | </ul> 266 | </div> 267 | <div className="rounded-xl bg-white p-6 border border-gray-200"> 268 | <h3 className="font-semibold mb-3 text-gray-900">Your Responsibilities</h3> 269 | <ul className="space-y-2 text-sm"> 270 | <Bullet icon="⚠">Do not run on shared multi-user servers</Bullet> 271 | <Bullet icon="⚠">Keep secrets out of version control</Bullet> 272 | <Bullet icon="⚠">Avoid copying raw logs with PII into tickets</Bullet> 273 | <Bullet icon="⚠">Limit OCAPI resources to active feature work</Bullet> 274 | <Bullet icon="⚠">Rotate credentials + revoke unused clients</Bullet> 275 | <Bullet icon="⚠">Disable debug once diagnosing finished</Bullet> 276 | </ul> 277 | </div> 278 | </div> 279 | <div className="mt-8 grid md:grid-cols-3 gap-6 text-xs"> 280 | <div className="bg-green-50 border border-green-200 rounded-lg p-4"> 281 | <p className="font-semibold text-green-800 mb-1">Docs Mode</p> 282 | <p className="text-green-700 leading-snug">Static reference + generation only. Zero credential or data surface.</p> 283 | </div> 284 | <div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> 285 | <p className="font-semibold text-blue-800 mb-1">Full Mode (Scoped)</p> 286 | <p className="text-blue-700 leading-snug">Typical local dev parity: targeted OCAPI, selective log tailing, metadata queries.</p> 287 | </div> 288 | <div className="bg-amber-50 border border-amber-200 rounded-lg p-4"> 289 | <p className="font-semibold text-amber-800 mb-1">Broad Scope (Review)</p> 290 | <p className="text-amber-700 leading-snug">Wide OCAPI grants + continuous debug + indiscriminate log sharing. Still local—but audit and prune.</p> 291 | </div> 292 | </div> 293 | </SectionShell> 294 | 295 | {/* Data Protection */} 296 | <SectionShell gradient="from-yellow-50 via-amber-50 to-orange-50" border="border-amber-200"> 297 | <div className="grid md:grid-cols-3 gap-8 mb-6 items-start"> 298 | <div className="md:col-span-2 max-w-2xl"> 299 | <H2 id="data-protection" className="text-2xl font-bold mb-2">💾 Data Handling & Privacy</H2> 300 | <p className="text-sm text-gray-700 leading-relaxed">Runtime data (logs, attribute listings, preference search results) is streamed, parsed, optionally cached in memory, and discarded on process exit.</p> 301 | </div> 302 | <div className="md:col-span-1 rounded-lg border border-yellow-300 bg-white p-5 text-[13px] text-yellow-800 shadow-sm min-w-[260px]"> 303 | <p className="font-semibold mb-2 tracking-tight">Design Principles</p> 304 | <ul className="list-disc pl-4 space-y-1.5"> 305 | <li>No silent disk writes</li> 306 | <li>Bounded tail reads (~200KB)</li> 307 | <li>Optional debug noise suppression</li> 308 | </ul> 309 | </div> 310 | </div> 311 | <div className="grid md:grid-cols-3 gap-6"> 312 | {[ 313 | { title: 'Log Processing', items: ['Tail/range reads only (≈200KB)', 'Pattern search constrained by limit', 'Analyzer strips obvious secret tokens'] }, 314 | { title: 'Preference Values', items: ['Password types masked (no bypass)', 'Group-limited search scope', 'No storage of raw values'] }, 315 | { title: 'System & Custom Objects', items: ['Metadata only (ids, flags, counts)', 'No record-level PII retrieval', 'You control query breadth'] } 316 | ].map(card => ( 317 | <div key={card.title} className="rounded-xl bg-white border border-gray-200 p-5"> 318 | <h3 className="font-semibold text-sm mb-2">{card.title}</h3> 319 | <ul className="text-xs space-y-1 text-gray-600 list-disc pl-4"> 320 | {card.items.map(i => <li key={i}>{i}</li>)} 321 | </ul> 322 | </div> 323 | ))} 324 | </div> 325 | </SectionShell> 326 | 327 | {/* Reporting */} 328 | <SectionShell gradient="from-slate-50 via-gray-50 to-blue-50" border="border-gray-200"> 329 | <div className="text-center mb-8"> 330 | <H2 id="reporting" className="text-3xl font-bold mb-3">🔍 Responsible Disclosure</H2> 331 | <p className="text-gray-700 max-w-2xl mx-auto">Found a vulnerability? Help strengthen the ecosystem—avoid public zero-days.</p> 332 | </div> 333 | <ol className="list-decimal pl-6 space-y-3 text-sm text-gray-700 max-w-3xl mx-auto"> 334 | <li><strong>Do NOT</strong> open a public GitHub issue containing exploit details.</li> 335 | <li>Email maintainers with: version, environment, reproduction steps, impact summary.</li> 336 | <li>Suggest a remediation direction if obvious (helps triage).</li> 337 | <li>Allow a reasonable patch window before disclosure.</li> 338 | <li>Re-test once patch is published; confirm mitigation completeness.</li> 339 | </ol> 340 | </SectionShell> 341 | 342 | {/* Final CTA */} 343 | <section className="mt-24 text-center" aria-labelledby="next-steps-security"> 344 | <H2 id="next-steps-security" className="text-3xl font-bold mb-4">🔗 Next Steps</H2> 345 | <p className="text-sm md:text-base text-gray-600 max-w-2xl mx-auto mb-8">Keep momentum: refine configuration or explore advanced tooling now that baseline security posture is set.</p> 346 | <div className="flex flex-col sm:flex-row gap-4 justify-center mb-10"> 347 | <NavLink 348 | to="/configuration/" 349 | className="group bg-gradient-to-r from-blue-600 to-purple-600 text-white px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 no-underline hover:no-underline focus:no-underline" 350 | > 351 | Configuration Guide 352 | <span className="ml-2 group-hover:translate-x-1 inline-block transition-transform">→</span> 353 | </NavLink> 354 | <NavLink 355 | to="/features/" 356 | className="border-2 border-gray-300 text-gray-700 px-8 py-4 rounded-xl font-semibold text-lg hover:border-blue-500 hover:text-blue-600 transition-all duration-300 no-underline hover:no-underline focus:no-underline" 357 | > 358 | Explore Features 359 | </NavLink> 360 | <NavLink 361 | to="/examples/" 362 | className="border-2 border-gray-300 text-gray-700 px-8 py-4 rounded-xl font-semibold text-lg hover:border-blue-500 hover:text-blue-600 transition-all duration-300 no-underline hover:no-underline focus:no-underline" 363 | > 364 | See Examples 365 | </NavLink> 366 | </div> 367 | </section> 368 | </div> 369 | ); 370 | }; 371 | 372 | export default SecurityPage; 373 | ``` -------------------------------------------------------------------------------- /tests/servers/sfcc-mock-server/src/routes/ocapi/system-objects-handler.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * System Objects Handler 3 | * 4 | * Handles system object definitions, attribute definitions, attribute groups, 5 | * and custom object definitions for OCAPI endpoints. 6 | */ 7 | 8 | const express = require('express'); 9 | const OCAPIUtils = require('./ocapi-utils'); 10 | const OCAPIErrorUtils = require('./ocapi-error-utils'); 11 | 12 | class SystemObjectsHandler { 13 | constructor(config, dataLoader) { 14 | this.config = config; 15 | this.ocapiConfig = config.getOcapiConfig(); 16 | this.dataLoader = dataLoader; 17 | this.router = express.Router(); 18 | this.setupRoutes(); 19 | } 20 | 21 | setupRoutes() { 22 | // System Object Definitions - List all 23 | this.router.get(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definitions`, 24 | this.handleGetSystemObjectDefinitions.bind(this) 25 | ); 26 | 27 | // System Object Definition - Get specific 28 | this.router.get(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definitions/:objectType`, 29 | this.handleGetSystemObjectDefinition.bind(this) 30 | ); 31 | 32 | // System Object Definition Search 33 | this.router.post(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definition_search`, 34 | this.handleSearchSystemObjectDefinitions.bind(this) 35 | ); 36 | 37 | // System Object Attribute Definition Search 38 | this.router.post(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definitions/:objectType/attribute_definition_search`, 39 | this.handleSearchSystemObjectAttributeDefinitions.bind(this) 40 | ); 41 | 42 | // System Object Attribute Group Search 43 | this.router.post(`/s/-/dw/data/${this.ocapiConfig.version}/system_object_definitions/:objectType/attribute_group_search`, 44 | this.handleSearchSystemObjectAttributeGroups.bind(this) 45 | ); 46 | 47 | // Custom Object Attribute Definition Search 48 | this.router.post(`/s/-/dw/data/${this.ocapiConfig.version}/custom_object_definitions/:objectType/attribute_definition_search`, 49 | this.handleSearchCustomObjectAttributeDefinitions.bind(this) 50 | ); 51 | } 52 | 53 | /** 54 | * Handle GET system object definitions 55 | */ 56 | async handleGetSystemObjectDefinitions(req, res) { 57 | const { start = 0, count = 200, select = '(**)' } = req.query; 58 | 59 | let mockData = this.dataLoader.loadOcapiData('system-object-definitions.json'); 60 | 61 | if (!mockData) { 62 | // Fallback mock data with proper SFCC format 63 | mockData = { 64 | "_v": "24.4", 65 | "_type": "system_object_definitions", 66 | "count": 0, 67 | "data": [], 68 | "next": null, 69 | "previous": null, 70 | "start": 0, 71 | "total": 0 72 | }; 73 | } 74 | 75 | // Fix total count to match actual data length 76 | mockData.total = mockData.data.length; 77 | 78 | // Extract data-specific select pattern if present 79 | let dataSelectPattern = select; 80 | if (select && select.includes('data.(')) { 81 | const dataMatch = select.match(/data\.\(([^)]*)\)/); 82 | if (dataMatch) { 83 | dataSelectPattern = `(${dataMatch[1]})`; 84 | } 85 | } 86 | 87 | // Apply select parameter to modify object structure 88 | let processedData = mockData.data.map(obj => OCAPIUtils.applySelectParameter(obj, dataSelectPattern)); 89 | 90 | // Apply pagination 91 | const startInt = parseInt(start); 92 | const countInt = parseInt(count); 93 | const paginatedData = processedData.slice(startInt, startInt + countInt); 94 | 95 | // Calculate pagination URLs 96 | const { nextUrl, previousUrl } = OCAPIUtils.generatePaginationUrls( 97 | req, startInt, countInt, mockData.total, select 98 | ); 99 | 100 | const fullResponse = { 101 | "_v": mockData._v, 102 | "_type": mockData._type, 103 | "count": paginatedData.length, 104 | "data": paginatedData, 105 | "next": nextUrl, 106 | "previous": previousUrl, 107 | "start": startInt, 108 | "total": mockData.total 109 | }; 110 | 111 | // Add select field to response if provided (like real API) 112 | if (select && select !== '(**)') { 113 | fullResponse.select = select; 114 | } 115 | 116 | // Apply root-level select parameter to the entire response 117 | const response = OCAPIUtils.applyRootSelectParameter(fullResponse, select); 118 | 119 | res.json(response); 120 | } 121 | 122 | /** 123 | * Handle GET specific system object definition 124 | */ 125 | async handleGetSystemObjectDefinition(req, res) { 126 | const { objectType } = req.params; 127 | 128 | // Try to load specific object definition 129 | let mockData = this.dataLoader.loadOcapiData(`system-object-definition-${objectType.toLowerCase()}.json`); 130 | 131 | if (!mockData) { 132 | // Create a basic fallback 133 | mockData = { 134 | _type: 'object_type_definition', 135 | object_type: objectType, 136 | display_name: { default: objectType }, 137 | description: { default: `SFCC ${objectType} object` }, 138 | attribute_definition_count: 0, 139 | attribute_group_count: 0, 140 | key_attribute_id: 'id', 141 | content_object: false, 142 | queryable: true, 143 | read_only: false, 144 | creation_date: new Date().toISOString(), 145 | last_modified: new Date().toISOString() 146 | }; 147 | } 148 | 149 | res.json(mockData); 150 | } 151 | 152 | /** 153 | * Handle system object definition search 154 | */ 155 | async handleSearchSystemObjectDefinitions(req, res) { 156 | const searchRequest = req.body; 157 | 158 | // Load base data 159 | let mockData = this.dataLoader.loadOcapiData('system-object-definitions.json'); 160 | if (!mockData) { 161 | mockData = this.getDefaultSystemObjectDefinitions(); 162 | } 163 | 164 | // Simple search implementation (in real SFCC this would be more complex) 165 | let results = mockData.hits; 166 | 167 | // Apply basic filtering if search criteria provided 168 | results = OCAPIUtils.applyTextSearch(results, searchRequest.query); 169 | 170 | // Apply pagination 171 | const start = searchRequest.start || 0; 172 | const count = searchRequest.count || 200; 173 | const paginatedResults = results.slice(start, start + count); 174 | 175 | res.json({ 176 | count: paginatedResults.length, 177 | total: results.length, 178 | start, 179 | hits: paginatedResults 180 | }); 181 | } 182 | 183 | /** 184 | * Handle system object attribute definition search 185 | */ 186 | async handleSearchSystemObjectAttributeDefinitions(req, res) { 187 | const { objectType } = req.params; 188 | const searchRequest = req.body; 189 | 190 | // Try to load specific attribute definitions based on select parameter 191 | const isExpandedRequest = searchRequest.select === "(**)"; 192 | const mockDataFile = isExpandedRequest 193 | ? `system-object-attributes-${objectType.toLowerCase()}-expanded.json` 194 | : `system-object-attributes-${objectType.toLowerCase()}.json`; 195 | 196 | let mockData = this.dataLoader.loadOcapiData(mockDataFile); 197 | 198 | // Fallback to basic data if expanded data doesn't exist 199 | if (!mockData && isExpandedRequest) { 200 | mockData = this.dataLoader.loadOcapiData(`system-object-attributes-${objectType.toLowerCase()}.json`); 201 | } 202 | 203 | if (!mockData) { 204 | // Create fallback data with realistic SFCC format 205 | mockData = { 206 | "_v": "23.2", 207 | "_type": "object_attribute_definition_search_result", 208 | "count": 0, 209 | "hits": [], 210 | "query": searchRequest.query || {"match_all_query": {}}, 211 | "start": 0, 212 | "total": 0 213 | }; 214 | } 215 | 216 | // Apply search and pagination 217 | let results = mockData.hits || []; 218 | 219 | // Apply text search if provided 220 | results = OCAPIUtils.applyTextSearch(results, searchRequest.query); 221 | 222 | // Handle select parameter for expanded data 223 | if (isExpandedRequest && mockData.expandedData) { 224 | results = results.map(item => { 225 | const expandedItem = mockData.expandedData[item.id]; 226 | return expandedItem || item; 227 | }); 228 | } 229 | 230 | // Store total filtered results count 231 | const totalFiltered = results.length; 232 | 233 | // Apply pagination 234 | const start = searchRequest.start || 0; 235 | const count = searchRequest.count || 200; 236 | const paginatedResults = results.slice(start, start + count); 237 | 238 | // Build query response with proper _type fields to match real API 239 | const queryResponse = OCAPIUtils.buildQueryResponse(searchRequest.query); 240 | 241 | // Build response in SFCC format 242 | const response = { 243 | "_v": mockData._v || "23.2", 244 | "_type": "object_attribute_definition_search_result", 245 | "count": paginatedResults.length, 246 | "hits": paginatedResults, 247 | "query": queryResponse, 248 | "start": start, 249 | "total": totalFiltered 250 | }; 251 | 252 | // Add select parameter to response if it was provided 253 | if (searchRequest.select) { 254 | response.select = searchRequest.select; 255 | } 256 | 257 | // Add next page link if there are more results 258 | if (start + count < totalFiltered) { 259 | response.next = { 260 | "_type": "result_page", 261 | "count": Math.min(count, totalFiltered - (start + count)), 262 | "start": start + count 263 | }; 264 | } 265 | 266 | res.json(response); 267 | } 268 | 269 | /** 270 | * Handle system object attribute group search 271 | */ 272 | async handleSearchSystemObjectAttributeGroups(req, res) { 273 | try { 274 | const { objectType } = req.params; 275 | const searchRequest = req.body; 276 | 277 | // 1. Validate object type 278 | const objectTypeError = OCAPIErrorUtils.validateObjectType(objectType); 279 | if (objectTypeError) { 280 | return OCAPIErrorUtils.sendErrorResponse(res, objectTypeError); 281 | } 282 | 283 | // 2. Check for object types that don't support attribute groups (based on real API testing) 284 | const unsupportedObjectTypes = ['Customer', 'Site', 'Inventory']; 285 | if (unsupportedObjectTypes.includes(objectType)) { 286 | const notFoundError = OCAPIErrorUtils.createObjectTypeNotFound(objectType); 287 | return OCAPIErrorUtils.sendErrorResponse(res, notFoundError); 288 | } 289 | 290 | // 3. Validate search request structure 291 | const searchRequestError = OCAPIErrorUtils.validateSearchRequest(searchRequest); 292 | if (searchRequestError) { 293 | return OCAPIErrorUtils.sendErrorResponse(res, searchRequestError); 294 | } 295 | 296 | // 4. Validate pagination parameters 297 | const paginationError = OCAPIErrorUtils.validatePagination(searchRequest.start, searchRequest.count); 298 | if (paginationError) { 299 | return OCAPIErrorUtils.sendErrorResponse(res, paginationError); 300 | } 301 | 302 | // 5. Validate specific query types 303 | if (searchRequest.query.text_query) { 304 | const textQueryError = OCAPIErrorUtils.validateTextQuery(searchRequest.query.text_query); 305 | if (textQueryError) { 306 | return OCAPIErrorUtils.sendErrorResponse(res, textQueryError); 307 | } 308 | } 309 | 310 | if (searchRequest.query.term_query) { 311 | const termQueryError = OCAPIErrorUtils.validateTermQuery(searchRequest.query.term_query); 312 | if (termQueryError) { 313 | return OCAPIErrorUtils.sendErrorResponse(res, termQueryError); 314 | } 315 | } 316 | 317 | // 6. Simulate occasional server errors (1% chance) - only if randomErrors is enabled 318 | if (this.config.features.randomErrors && Math.random() < 0.01) { 319 | const serverError = OCAPIErrorUtils.createInternalServerError( 320 | "Service temporarily unavailable. Please try again later." 321 | ); 322 | return OCAPIErrorUtils.sendErrorResponse(res, serverError); 323 | } 324 | 325 | // Try to load specific attribute groups with correct naming 326 | let mockData = this.dataLoader.loadOcapiData(`system-object-attribute-groups-${objectType.toLowerCase()}.json`); 327 | 328 | if (!mockData) { 329 | // Create fallback data with proper SFCC format (matching real API structure) 330 | mockData = { 331 | "_v": "23.2", 332 | "_type": "object_attribute_group_search_result", 333 | "count": 0, 334 | "hits": [], 335 | "query": {"match_all_query": {"_type": "match_all_query"}}, 336 | "start": 0, 337 | "total": 0 338 | }; 339 | } 340 | 341 | // Apply search and pagination 342 | let results = mockData.hits || []; 343 | 344 | // Apply pagination 345 | const start = searchRequest.start || 0; 346 | const count = searchRequest.count || 200; 347 | const paginatedResults = results.slice(start, start + count); 348 | 349 | // Build query response to match real API 350 | const queryResponse = OCAPIUtils.buildQueryResponse(searchRequest.query); 351 | 352 | // Return response matching real SFCC API format 353 | const response = { 354 | "_v": mockData._v || "23.2", 355 | "_type": "object_attribute_group_search_result", 356 | "count": paginatedResults.length, 357 | "hits": paginatedResults, 358 | "query": queryResponse, 359 | "start": start, 360 | "total": mockData.total || results.length 361 | }; 362 | 363 | res.json(response); 364 | 365 | } catch (error) { 366 | // Handle unexpected errors 367 | console.error('Unexpected error in handleSearchSystemObjectAttributeGroups:', error); 368 | const serverError = OCAPIErrorUtils.createInternalServerError( 369 | "An unexpected error occurred while processing the request." 370 | ); 371 | return OCAPIErrorUtils.sendErrorResponse(res, serverError); 372 | } 373 | } 374 | 375 | /** 376 | * Handle custom object attribute definition search 377 | */ 378 | async handleSearchCustomObjectAttributeDefinitions(req, res) { 379 | const { objectType } = req.params; 380 | const searchRequest = req.body; 381 | 382 | // Validate object type parameter 383 | if (!objectType || objectType.trim() === '') { 384 | const error = OCAPIErrorUtils.createInvalidRequest( 385 | "objectType must be a non-empty string", 386 | "objectType" 387 | ); 388 | return OCAPIErrorUtils.sendErrorResponse(res, error); 389 | } 390 | 391 | // Validate search request structure 392 | const validationError = this.validateSearchRequest(searchRequest); 393 | if (validationError) { 394 | return OCAPIErrorUtils.sendErrorResponse(res, validationError); 395 | } 396 | 397 | // Define known custom object types (case-insensitive) 398 | const knownCustomObjects = ['customapi', 'versionhistory', 'globalsettings']; 399 | const objectTypeLower = objectType.toLowerCase(); 400 | 401 | // Try to load specific custom object attribute definitions 402 | let mockData = this.dataLoader.loadOcapiData(`custom-object-attributes-${objectTypeLower}.json`); 403 | 404 | if (!mockData) { 405 | // Check if this is a known custom object type with no data vs unknown object type 406 | if (!knownCustomObjects.includes(objectTypeLower)) { 407 | // Return 404 for unknown custom object types (like real SFCC API) 408 | const error = OCAPIErrorUtils.createObjectTypeNotFound(objectType); 409 | return OCAPIErrorUtils.sendErrorResponse(res, error); 410 | } 411 | 412 | // Create fallback data with proper SFCC format for known objects with no data 413 | mockData = { 414 | "_v": "23.2", 415 | "_type": "object_attribute_definition_search_result", 416 | "count": 0, 417 | "hits": [], 418 | "query": searchRequest.query || {"match_all_query": {}}, 419 | "start": 0, 420 | "total": 0 421 | }; 422 | } 423 | 424 | // Apply search and pagination 425 | let results = mockData.hits || []; 426 | 427 | // Apply pagination 428 | const start = searchRequest.start || 0; 429 | const count = searchRequest.count || 200; 430 | const paginatedResults = results.slice(start, start + count); 431 | 432 | // Build query response to match real API 433 | const queryResponse = OCAPIUtils.buildQueryResponse(searchRequest.query); 434 | 435 | res.json({ 436 | "_v": mockData._v, 437 | "_type": mockData._type, 438 | "count": paginatedResults.length, 439 | "hits": paginatedResults, 440 | "query": queryResponse, 441 | "start": start, 442 | "total": mockData.total 443 | }); 444 | } 445 | 446 | /** 447 | * Get default system object definitions for fallback 448 | */ 449 | getDefaultSystemObjectDefinitions() { 450 | return { 451 | count: 25, 452 | hits: [ 453 | { 454 | _type: 'object_type_definition', 455 | object_type: 'Product', 456 | display_name: { default: 'Product' }, 457 | description: { default: 'SFCC Product object' }, 458 | attribute_definition_count: 45, 459 | attribute_group_count: 8, 460 | key_attribute_id: 'id', 461 | content_object: false, 462 | queryable: true, 463 | read_only: false, 464 | creation_date: '2021-01-01T00:00:00.000Z', 465 | last_modified: '2024-01-01T00:00:00.000Z' 466 | }, 467 | { 468 | _type: 'object_type_definition', 469 | object_type: 'Customer', 470 | display_name: { default: 'Customer' }, 471 | description: { default: 'SFCC Customer object' }, 472 | attribute_definition_count: 32, 473 | attribute_group_count: 6, 474 | key_attribute_id: 'customer_no', 475 | content_object: false, 476 | queryable: true, 477 | read_only: false, 478 | creation_date: '2021-01-01T00:00:00.000Z', 479 | last_modified: '2024-01-01T00:00:00.000Z' 480 | }, 481 | { 482 | _type: 'object_type_definition', 483 | object_type: 'Order', 484 | display_name: { default: 'Order' }, 485 | description: { default: 'SFCC Order object' }, 486 | attribute_definition_count: 28, 487 | attribute_group_count: 5, 488 | key_attribute_id: 'order_no', 489 | content_object: false, 490 | queryable: true, 491 | read_only: false, 492 | creation_date: '2021-01-01T00:00:00.000Z', 493 | last_modified: '2024-01-01T00:00:00.000Z' 494 | } 495 | ] 496 | }; 497 | } 498 | 499 | /** 500 | * Validate search request structure and parameters 501 | */ 502 | validateSearchRequest(searchRequest) { 503 | // Must have a query property 504 | if (!searchRequest.query) { 505 | return OCAPIErrorUtils.createPropertyConstraintViolation("$.query", "search_request"); 506 | } 507 | 508 | // Validate count parameter 509 | if (searchRequest.count !== undefined) { 510 | if (typeof searchRequest.count !== 'number' || searchRequest.count < 0) { 511 | return OCAPIErrorUtils.createInvalidRequest("count must be a positive number", "count"); 512 | } 513 | } 514 | 515 | // Validate start parameter 516 | if (searchRequest.start !== undefined) { 517 | if (typeof searchRequest.start !== 'number' || searchRequest.start < 0) { 518 | return OCAPIErrorUtils.createInvalidRequest("start must be a positive number", "start"); 519 | } 520 | } 521 | 522 | // Validate query object 523 | const query = searchRequest.query; 524 | const queryTypes = ['text_query', 'term_query', 'filtered_query', 'bool_query', 'match_all_query']; 525 | const hasValidQueryType = queryTypes.some(type => query[type]); 526 | 527 | if (!hasValidQueryType) { 528 | return OCAPIErrorUtils.createInvalidRequest( 529 | "Search query must contain at least one of: text_query, term_query, filtered_query, bool_query, match_all_query" 530 | ); 531 | } 532 | 533 | // Validate text_query 534 | if (query.text_query) { 535 | const textQuery = query.text_query; 536 | if (!textQuery.fields || !Array.isArray(textQuery.fields) || textQuery.fields.length === 0) { 537 | return OCAPIErrorUtils.createInvalidRequest("text_query.fields must be a non-empty array"); 538 | } 539 | if (!textQuery.search_phrase || typeof textQuery.search_phrase !== 'string' || textQuery.search_phrase.trim() === '') { 540 | return OCAPIErrorUtils.createInvalidRequest("text_query.search_phrase must be a non-empty string"); 541 | } 542 | } 543 | 544 | // Validate term_query 545 | if (query.term_query) { 546 | const termQuery = query.term_query; 547 | if (!termQuery.fields || !Array.isArray(termQuery.fields) || termQuery.fields.length === 0) { 548 | return OCAPIErrorUtils.createInvalidRequest("term_query.fields must be a non-empty array"); 549 | } 550 | if (!termQuery.values || !Array.isArray(termQuery.values) || termQuery.values.length === 0) { 551 | return OCAPIErrorUtils.createInvalidRequest("term_query.values must be a non-empty array"); 552 | } 553 | if (termQuery.operator) { 554 | const validOperators = ['is', 'one_of', 'not_one_of', 'is_null', 'is_not_null']; 555 | if (!validOperators.includes(termQuery.operator)) { 556 | return OCAPIErrorUtils.createEnumConstraintViolation(termQuery.operator); 557 | } 558 | } 559 | } 560 | 561 | return null; // No validation errors 562 | } 563 | 564 | /** 565 | * Get the configured router 566 | */ 567 | getRouter() { 568 | return this.router; 569 | } 570 | } 571 | 572 | module.exports = SystemObjectsHandler; ```