[{"data":1,"prerenderedAt":5647},["ShallowReactive",2],{"navigation_docs_en":3,"-en-developer-extensions-data-connectors-errors":445,"-en-developer-extensions-data-connectors-errors-surround":5642},[4,30,72,132,260,436],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":6},"Getting Started",false,"\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,25],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F2.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Quickstart","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F3.installation","i-lucide-download",{"title":21,"path":22,"stem":23,"icon":24},"Use Cases","\u002Fen\u002Fgetting-started\u002Fuse-cases","en\u002F1.getting-started\u002F4.use-cases","i-lucide-layers",{"title":26,"path":27,"stem":28,"icon":29},"Concepts","\u002Fen\u002Fgetting-started\u002Fconcepts","en\u002F1.getting-started\u002F5.concepts","i-lucide-book",{"title":26,"path":31,"stem":32,"children":33,"page":6},"\u002Fen\u002Fconcepts","en\u002F2.concepts",[34,38,43,48,53,58,63,68],{"title":35,"path":36,"stem":37,"icon":14},"Organization","\u002Fen\u002Fconcepts\u002Forganization","en\u002F2.concepts\u002F1.organization",{"title":39,"path":40,"stem":41,"icon":42},"Data Model","\u002Fen\u002Fconcepts\u002Fdata-model","en\u002F2.concepts\u002F2.data-model","i-lucide-database",{"title":44,"path":45,"stem":46,"icon":47},"Access Control","\u002Fen\u002Fconcepts\u002Faccess-permissions","en\u002F2.concepts\u002F3.access-permissions","i-lucide-user-key",{"title":49,"path":50,"stem":51,"icon":52},"Audit Logs","\u002Fen\u002Fconcepts\u002Faudit-logs","en\u002F2.concepts\u002F4.audit-logs","i-lucide-scroll-text",{"title":54,"path":55,"stem":56,"icon":57},"Introspection","\u002Fen\u002Fconcepts\u002Fintrospection","en\u002F2.concepts\u002F6.introspection","i-lucide-database-search",{"title":59,"path":60,"stem":61,"icon":62},"Query Engine","\u002Fen\u002Fconcepts\u002Fquery-engine","en\u002F2.concepts\u002F7.query-engine","i-lucide-workflow",{"title":64,"path":65,"stem":66,"icon":67},"AI","\u002Fen\u002Fconcepts\u002Fai","en\u002F2.concepts\u002F8.ai","i-lucide-sparkles",{"title":69,"path":70,"stem":71,"icon":42},"Caching","\u002Fen\u002Fconcepts\u002Fcaching","en\u002F2.concepts\u002F9.caching",{"title":73,"path":74,"stem":75,"children":76,"page":6},"Guides","\u002Fen\u002Fguides","en\u002F4.guides",[77,82,87,92,97,102,107,112],{"title":78,"path":79,"stem":80,"icon":81},"REST API Quickstart","\u002Fen\u002Fguides\u002Frest-api","en\u002F4.guides\u002F1.rest-api","i-lucide-plug",{"title":83,"path":84,"stem":85,"icon":86},"SDK Quickstart","\u002Fen\u002Fguides\u002Fsdk","en\u002F4.guides\u002F2.sdk","i-lucide-terminal",{"title":88,"path":89,"stem":90,"icon":91},"Configure SSO","\u002Fen\u002Fguides\u002Fconfigure-sso","en\u002F4.guides\u002F3.configure-sso","i-lucide-key-round",{"title":93,"path":94,"stem":95,"icon":96},"Configure MCP","\u002Fen\u002Fguides\u002Fmcp","en\u002F4.guides\u002F4.mcp","i-lucide-brain-cog",{"title":98,"path":99,"stem":100,"icon":101},"Customize Content Space","\u002Fen\u002Fguides\u002Fcustomize-content-space","en\u002F4.guides\u002F5.customize-content-space","i-lucide-columns-3-cog",{"title":103,"path":104,"stem":105,"icon":106},"Build an Application with AI Assistance","\u002Fen\u002Fguides\u002Fbuild-an-application","en\u002F4.guides\u002F6.build-an-application","i-lucide-route",{"title":108,"path":109,"stem":110,"icon":111},"Horizontal Scaling","\u002Fen\u002Fguides\u002Fhorizontal-scaling","en\u002F4.guides\u002F7.horizontal-scaling","i-lucide-network",{"title":113,"icon":114,"path":115,"stem":116,"children":117,"page":6},"Data Connectors","i-lucide-cable","\u002Fen\u002Fguides\u002Fdata-connectors","en\u002F4.guides\u002F8.data-connectors",[118,122,127],{"title":16,"path":119,"stem":120,"icon":121},"\u002Fen\u002Fguides\u002Fdata-connectors\u002Fquickstart","en\u002F4.guides\u002F8.data-connectors\u002F1.quickstart","i-lucide-rocket",{"title":123,"path":124,"stem":125,"icon":126},"Build a Stripe Connector","\u002Fen\u002Fguides\u002Fdata-connectors\u002Fstripe","en\u002F4.guides\u002F8.data-connectors\u002F2.stripe","i-lucide-hammer",{"title":128,"path":129,"stem":130,"icon":131},"Build with AI Agents","\u002Fen\u002Fguides\u002Fdata-connectors\u002Fai-agents","en\u002F4.guides\u002F8.data-connectors\u002F3.ai-agents","i-lucide-bot",{"title":133,"path":134,"stem":135,"children":136,"page":6},"Developer","\u002Fen\u002Fdeveloper","en\u002F5.developer",[137,191,215,220],{"title":138,"path":139,"stem":140,"children":141,"page":6},"Data Access","\u002Fen\u002Fdeveloper\u002Fapi","en\u002F5.developer\u002F1.api",[142,147,152,157,162,167,172,177,182,186],{"title":143,"path":144,"stem":145,"icon":146},"Overview","\u002Fen\u002Fdeveloper\u002Fapi\u002Foverview","en\u002F5.developer\u002F1.api\u002F1.overview","i-lucide-globe",{"title":148,"path":149,"stem":150,"icon":151},"Errors","\u002Fen\u002Fdeveloper\u002Fapi\u002Ferrors","en\u002F5.developer\u002F1.api\u002F10.errors","i-lucide-alert-triangle",{"title":153,"path":154,"stem":155,"icon":156},"Authentication","\u002Fen\u002Fdeveloper\u002Fapi\u002Fauthentication","en\u002F5.developer\u002F1.api\u002F2.authentication","i-lucide-lock",{"title":158,"path":159,"stem":160,"icon":161},"Reading Data","\u002Fen\u002Fdeveloper\u002Fapi\u002Freading-data","en\u002F5.developer\u002F1.api\u002F3.reading-data","i-lucide-book-open",{"title":163,"path":164,"stem":165,"icon":166},"Writing Data","\u002Fen\u002Fdeveloper\u002Fapi\u002Fwriting-data","en\u002F5.developer\u002F1.api\u002F4.writing-data","i-lucide-pencil",{"title":168,"path":169,"stem":170,"icon":171},"Filtering","\u002Fen\u002Fdeveloper\u002Fapi\u002Ffiltering","en\u002F5.developer\u002F1.api\u002F5.filtering","i-lucide-filter",{"title":173,"path":174,"stem":175,"icon":176},"Field Selection","\u002Fen\u002Fdeveloper\u002Fapi\u002Ffield-selection","en\u002F5.developer\u002F1.api\u002F6.field-selection","i-lucide-list-checks",{"title":178,"path":179,"stem":180,"icon":181},"Sorting & Pagination","\u002Fen\u002Fdeveloper\u002Fapi\u002Fsorting-pagination","en\u002F5.developer\u002F1.api\u002F7.sorting-pagination","i-lucide-arrow-up-down",{"title":183,"path":184,"stem":185,"icon":111},"Relational Data","\u002Fen\u002Fdeveloper\u002Fapi\u002Frelational-data","en\u002F5.developer\u002F1.api\u002F8.relational-data",{"title":187,"path":188,"stem":189,"icon":190},"Files & Assets","\u002Fen\u002Fdeveloper\u002Fapi\u002Ffiles","en\u002F5.developer\u002F1.api\u002F9.files","i-lucide-file-up",{"title":192,"path":193,"stem":194,"children":195,"page":6},"Client SDK","\u002Fen\u002Fdeveloper\u002Fsdk","en\u002F5.developer\u002F2.sdk",[196,200,205,210],{"title":197,"path":198,"stem":199,"icon":19},"Installation","\u002Fen\u002Fdeveloper\u002Fsdk\u002Finstallation","en\u002F5.developer\u002F2.sdk\u002F1.installation",{"title":201,"path":202,"stem":203,"icon":204},"Client Setup","\u002Fen\u002Fdeveloper\u002Fsdk\u002Fclient-setup","en\u002F5.developer\u002F2.sdk\u002F2.client-setup","i-lucide-settings",{"title":206,"path":207,"stem":208,"icon":209},"Type System","\u002Fen\u002Fdeveloper\u002Fsdk\u002Ftype-system","en\u002F5.developer\u002F2.sdk\u002F3.type-system","i-lucide-braces",{"title":211,"path":212,"stem":213,"icon":214},"Advanced","\u002Fen\u002Fdeveloper\u002Fsdk\u002Fadvanced","en\u002F5.developer\u002F2.sdk\u002F5.advanced","i-lucide-puzzle",{"title":216,"path":217,"stem":218,"icon":219},"Application Users","\u002Fen\u002Fdeveloper\u002Fapplication-users","en\u002F5.developer\u002F3.application-users","i-lucide-users",{"title":221,"path":222,"stem":223,"children":224,"icon":214},"Extensions","\u002Fen\u002Fdeveloper\u002Fextensions","en\u002F5.developer\u002F3.extensions\u002Findex",[225,226,231],{"title":143,"path":222,"stem":223,"icon":214},{"title":227,"path":228,"stem":229,"icon":230},"Install and Manage Extensions","\u002Fen\u002Fdeveloper\u002Fextensions\u002Finstall","en\u002F5.developer\u002F3.extensions\u002F1.install","i-lucide-package-plus",{"title":113,"path":232,"stem":233,"children":234,"icon":114},"\u002Fen\u002Fdeveloper\u002Fextensions\u002Fdata-connectors","en\u002F5.developer\u002F3.extensions\u002F2.data-connectors\u002Findex",[235,236,241,245,250,255],{"title":143,"path":232,"stem":233,"icon":114},{"title":237,"path":238,"stem":239,"icon":240},"How Data Connectors Run","\u002Fen\u002Fdeveloper\u002Fextensions\u002Fdata-connectors\u002Fruntime","en\u002F5.developer\u002F3.extensions\u002F2.data-connectors\u002F2.runtime","i-lucide-cpu",{"title":242,"path":243,"stem":244,"icon":42},"Data Sources","\u002Fen\u002Fdeveloper\u002Fextensions\u002Fdata-connectors\u002Fdata-sources","en\u002F5.developer\u002F3.extensions\u002F2.data-connectors\u002F3.data-sources",{"title":246,"path":247,"stem":248,"icon":249},"Map an External Data Source","\u002Fen\u002Fdeveloper\u002Fextensions\u002Fdata-connectors\u002Fdata-source-mapping","en\u002F5.developer\u002F3.extensions\u002F2.data-connectors\u002F4.data-source-mapping","i-lucide-map",{"title":251,"path":252,"stem":253,"icon":254},"Handle Errors","\u002Fen\u002Fdeveloper\u002Fextensions\u002Fdata-connectors\u002Ferrors","en\u002F5.developer\u002F3.extensions\u002F2.data-connectors\u002F5.errors","i-lucide-shield-alert",{"title":256,"path":257,"stem":258,"icon":259},"Test a Data Connector","\u002Fen\u002Fdeveloper\u002Fextensions\u002Fdata-connectors\u002Ftesting","en\u002F5.developer\u002F3.extensions\u002F2.data-connectors\u002F6.testing","i-lucide-flask-conical",{"title":261,"path":262,"stem":263,"children":264,"page":6},"Reference","\u002Fen\u002Freference","en\u002F6.reference",[265,269,274,279,295,299,304,340,369,374],{"title":266,"path":267,"stem":268,"icon":204},"Configuration","\u002Fen\u002Freference\u002Fenvironment-variables","en\u002F6.reference\u002F1.environment-variables",{"title":270,"path":271,"stem":272,"icon":273},"System Requirements","\u002Fen\u002Freference\u002Fsystem-requirements","en\u002F6.reference\u002F2.system-requirements","i-lucide-server",{"title":275,"path":276,"stem":277,"icon":278},"Licensing","\u002Fen\u002Freference\u002Flicensing","en\u002F6.reference\u002F3.licensing","i-lucide-gem",{"title":280,"path":281,"stem":282,"children":283,"icon":81},"Connectors","\u002Fen\u002Freference\u002Fconnectors","en\u002F6.reference\u002F4.connectors\u002Findex",[284,285,290],{"title":280,"path":281,"stem":282,"icon":81},{"title":286,"path":287,"stem":288,"icon":289},"PostgreSQL and Supabase","\u002Fen\u002Freference\u002Fconnectors\u002Fpostgresql-supabase","en\u002F6.reference\u002F4.connectors\u002F1.postgresql-supabase","i-simple-icons-postgresql",{"title":291,"path":292,"stem":293,"icon":294},"MySQL and MariaDB","\u002Fen\u002Freference\u002Fconnectors\u002Fmysql-mariadb","en\u002F6.reference\u002F4.connectors\u002F2.mysql-mariadb","i-simple-icons-mysql",{"title":296,"path":297,"stem":298,"icon":209},"Data Types Representation","\u002Fen\u002Freference\u002Fdata-types","en\u002F6.reference\u002F5.data-types",{"title":300,"path":301,"stem":302,"icon":303},"Permissions Reference","\u002Fen\u002Freference\u002Fpermissions","en\u002F6.reference\u002F6.permissions","i-lucide-shield",{"title":305,"path":306,"stem":307,"children":308,"page":6},"API Reference","\u002Fen\u002Freference\u002Fapi-reference","en\u002F6.reference\u002F7.api-reference",[309,313,318],{"title":310,"path":311,"stem":312,"icon":273},"System Endpoints","\u002Fen\u002Freference\u002Fapi-reference\u002Fsystem-endpoints","en\u002F6.reference\u002F7.api-reference\u002F1.system-endpoints",{"title":314,"path":315,"stem":316,"icon":317},"OpenAPI Spec","\u002Fen\u002Freference\u002Fapi-reference\u002Fopenapi-spec","en\u002F6.reference\u002F7.api-reference\u002F2.openapi-spec","i-lucide-file-code",{"title":319,"path":320,"stem":321,"children":322,"icon":324},"Schema Migration","\u002Fen\u002Freference\u002Fapi-reference\u002Fschema-migration","en\u002F6.reference\u002F7.api-reference\u002F3.schema-migration\u002Findex",[323,325,330,335],{"title":319,"path":320,"stem":321,"icon":324},"i-lucide-database-zap",{"title":326,"path":327,"stem":328,"icon":329},"Apply Migrations","\u002Fen\u002Freference\u002Fapi-reference\u002Fschema-migration\u002Fapply-migrations","en\u002F6.reference\u002F7.api-reference\u002F3.schema-migration\u002F1.apply-migrations","i-lucide-upload",{"title":331,"path":332,"stem":333,"icon":334},"Import Changes","\u002Fen\u002Freference\u002Fapi-reference\u002Fschema-migration\u002Fimport-changes","en\u002F6.reference\u002F7.api-reference\u002F3.schema-migration\u002F2.import-changes","i-lucide-refresh-cw",{"title":336,"path":337,"stem":338,"icon":339},"Operation Reference","\u002Fen\u002Freference\u002Fapi-reference\u002Fschema-migration\u002Foperation-reference","en\u002F6.reference\u002F7.api-reference\u002F3.schema-migration\u002F3.operation-reference","i-lucide-list-tree",{"title":341,"path":342,"stem":343,"children":344,"page":6},"Design Considerations","\u002Fen\u002Freference\u002Fdesign-considerations","en\u002F6.reference\u002F8.design-considerations",[345,349,354,359,364],{"title":143,"path":346,"stem":347,"icon":348},"\u002Fen\u002Freference\u002Fdesign-considerations\u002Foverview","en\u002F6.reference\u002F8.design-considerations\u002F1.overview","i-lucide-compass",{"title":350,"path":351,"stem":352,"icon":353},"Instance Configuration","\u002Fen\u002Freference\u002Fdesign-considerations\u002Finstance-configuration-related-problems","en\u002F6.reference\u002F8.design-considerations\u002F3.instance-configuration-related-problems","i-lucide-wrench",{"title":355,"path":356,"stem":357,"icon":358},"Overly Permissive Public Access","\u002Fen\u002Freference\u002Fdesign-considerations\u002Foverly-permissive","en\u002F6.reference\u002F8.design-considerations\u002F4.overly-permissive","i-lucide-shield-off",{"title":360,"path":361,"stem":362,"icon":363},"No Aggregates","\u002Fen\u002Freference\u002Fdesign-considerations\u002Fno-aggregates","en\u002F6.reference\u002F8.design-considerations\u002F5.no-aggregates","i-lucide-sigma",{"title":365,"path":366,"stem":367,"icon":368},"Cross-Data-Source Referential Integrity","\u002Fen\u002Freference\u002Fdesign-considerations\u002Fcross-data-source-referential-integrity","en\u002F6.reference\u002F8.design-considerations\u002F6.cross-data-source-referential-integrity","i-lucide-unlink",{"title":370,"path":371,"stem":372,"icon":373},"CLI","\u002Fen\u002Freference\u002Fcli","en\u002F6.reference\u002F9.cli","i-lucide-square-terminal",{"title":221,"path":375,"stem":376,"children":377,"page":6},"\u002Fen\u002Freference\u002Fextensions","en\u002F6.reference\u002F9.extensions",[378,383,386,405],{"title":379,"path":380,"stem":381,"icon":382},"Extension Config and Manifest","\u002Fen\u002Freference\u002Fextensions\u002Fmanifest","en\u002F6.reference\u002F9.extensions\u002F1.manifest","i-lucide-file-json",{"title":370,"path":384,"stem":385,"icon":86},"\u002Fen\u002Freference\u002Fextensions\u002Fcli","en\u002F6.reference\u002F9.extensions\u002F2.cli",{"title":387,"path":388,"stem":389,"children":390,"page":6},"Extension Kit","\u002Fen\u002Freference\u002Fextensions\u002Fextension-kit","en\u002F6.reference\u002F9.extensions\u002F3.extension-kit",[391,395,400],{"title":392,"path":393,"stem":394,"icon":81},"Data Connector","\u002Fen\u002Freference\u002Fextensions\u002Fextension-kit\u002Fdata-connector","en\u002F6.reference\u002F9.extensions\u002F3.extension-kit\u002F1.data-connector",{"title":396,"path":397,"stem":398,"icon":399},"Protocol","\u002Fen\u002Freference\u002Fextensions\u002Fextension-kit\u002Fprotocol","en\u002F6.reference\u002F9.extensions\u002F3.extension-kit\u002F2.protocol","i-lucide-arrow-left-right",{"title":401,"path":402,"stem":403,"icon":404},"Schema Helpers","\u002Fen\u002Freference\u002Fextensions\u002Fextension-kit\u002Fexperimental","en\u002F6.reference\u002F9.extensions\u002F3.extension-kit\u002F3.experimental","i-lucide-blocks",{"title":113,"path":406,"stem":407,"children":408,"page":6},"\u002Fen\u002Freference\u002Fextensions\u002Fdata-connectors","en\u002F6.reference\u002F9.extensions\u002F4.data-connectors",[409,413,417,421,426,431],{"title":410,"path":411,"stem":412,"icon":81},"Data Connector API","\u002Fen\u002Freference\u002Fextensions\u002Fdata-connectors\u002Fapi","en\u002F6.reference\u002F9.extensions\u002F4.data-connectors\u002F1.api",{"title":414,"path":415,"stem":416,"icon":176},"Operations and Operators","\u002Fen\u002Freference\u002Fextensions\u002Fdata-connectors\u002Foperations","en\u002F6.reference\u002F9.extensions\u002F4.data-connectors\u002F2.operations",{"title":148,"path":418,"stem":419,"icon":420},"\u002Fen\u002Freference\u002Fextensions\u002Fdata-connectors\u002Ferrors","en\u002F6.reference\u002F9.extensions\u002F4.data-connectors\u002F3.errors","i-lucide-triangle-alert",{"title":422,"path":423,"stem":424,"icon":425},"Runtime Limits","\u002Fen\u002Freference\u002Fextensions\u002Fdata-connectors\u002Flimits","en\u002F6.reference\u002F9.extensions\u002F4.data-connectors\u002F4.limits","i-lucide-gauge",{"title":427,"path":428,"stem":429,"icon":430},"Limitations","\u002Fen\u002Freference\u002Fextensions\u002Fdata-connectors\u002Flimitations","en\u002F6.reference\u002F9.extensions\u002F4.data-connectors\u002F5.limitations","i-lucide-ban",{"title":432,"path":433,"stem":434,"icon":435},"Network Permissions","\u002Fen\u002Freference\u002Fextensions\u002Fdata-connectors\u002Fpermissions","en\u002F6.reference\u002F9.extensions\u002F4.data-connectors\u002F6.permissions","i-lucide-shield-check",{"title":437,"icon":121,"path":438,"stem":439,"children":440,"page":6},"Release Notes","\u002Fen\u002Frelease-notes","en\u002F7.release-notes",[441],{"title":442,"path":443,"stem":444,"icon":121},"Changelog","\u002Fen\u002Frelease-notes\u002Fchangelog","en\u002F7.release-notes\u002F1.changelog",{"id":446,"title":447,"body":448,"description":5635,"extension":5636,"links":5637,"meta":5638,"navigation":5639,"path":252,"seo":5640,"stem":253,"__hash__":5641},"docs_en\u002Fen\u002F5.developer\u002F3.extensions\u002F2.data-connectors\u002F5.errors.md","Handle Data Connector Errors",{"type":449,"value":450,"toc":5621},"minimark",[451,454,458,476,490,494,501,523,1275,1395,1408,1412,1415,1770,1779,1794,2556,2613,2628,2633,2637,2663,2697,2948,2970,2977,2983,2987,2993,3034,3040,3044,3057,3086,3129,3139,3142,3146,3154,3221,3228,3897,3913,3921,3929,4004,4007,4011,4027,4268,4366,4379,4382,4386,4392,4412,4453,4459,4522,4525,4529,4532,4552,4732,4740,4919,4931,5478,5509,5573,5576,5582,5586,5617],[452,453],"preview",{},[455,456,143],"h2",{"id":457},"overview",[459,460,461,462,466,467,471,472,475],"p",{},"The ",[463,464,465],"strong",{},"external data source"," is the API, service, or database your data connector extension calls. When a request to it fails, the connector throws one of the kit's error classes where one fits. Engine can then tell a rate limit from a refused write or a rejected credential. Where no class fits, the connector throws a plain ",[468,469,470],"code",{},"Error",". The ",[463,473,474],{},"caller",", whoever sent the request to the Monospace API through the SDK, REST, or Studio, sees the result: an HTTP status and your error's message.",[459,477,478,479,484,485,489],{},"Work through the seven steps below. They use the error handling of the Stripe connector from ",[480,481,483],"a",{"href":482},"\u002Fguides\u002Fdata-connectors\u002Fstripe","Build a Stripe Data Connector"," as the example. You end with one client that classifies every failure, a deadline on every request, refusals that tell the caller what to change, and checks that prove each mapping. For every class, its code, and the status the caller gets, see ",[480,486,488],{"href":487},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Ferrors","Data Connector Errors",".",[455,491,493],{"id":492},"_1-map-failures-in-one-client","1. Map Failures in One Client",[459,495,496,497,500],{},"Send every request to the external data source through one function, and map failures there. The rest of the connector then deals in kit errors, and each rule of the external data source (e.g., \"only Stripe's ",[468,498,499],{},"resource_missing"," means a missing object\") lives in one place.",[459,502,503,504,507,508,511,512,515,516,519,520,522],{},"In the Stripe connector, ",[468,505,506],{},"#send"," is the only method that calls ",[468,509,510],{},"fetch",". It sets a deadline, reads the whole body inside the same ",[468,513,514],{},"try",", and maps a request that gets no answer. The snippets on this page are excerpts. A file's first excerpt includes its imports, and later excerpts of the same file continue it; ",[468,517,518],{},"\u002F\u002F …"," marks the lines an excerpt leaves out. ",[480,521,483],{"href":482}," walks through the rest of the connector.",[524,525,526],"accordion",{},[527,528,530],"accordion-item",{"label":529},"src\u002Fdata-connectors\u002Fstripe\u002Fclient.ts (#send)",[531,532,538],"pre",{"className":533,"code":534,"filename":535,"language":536,"meta":537,"style":537},"language-ts shiki shiki-themes monospace-light monospace-light monospace-dark","import {\n    AccessDenied,\n    ConnectionFailed,\n    ForeignKeyConstraintViolation,\n    NullConstraintViolation,\n    QueryRejected,\n    RateLimited,\n    UniqueConstraintViolation,\n} from '@monospace\u002Fextension-kit\u002Fdata-connector';\n\u002F\u002F Pinned next to the field paths it governs; see collections.ts.\nimport { STRIPE_API_VERSION } from '.\u002Fcollections';\n\nconst BASE_URL = 'https:\u002F\u002Fapi.stripe.com\u002Fv1\u002F';\n\u002F** Engine sets no deadline on `query` or `testConnection`, so every request carries its own. *\u002F\nconst TIMEOUT_MS = 10_000;\n\u002F** The most objects Stripe returns per list request. *\u002F\nconst PAGE_SIZE = 100;\n\u002F** Stripe ids are letters, digits, `_` and `-`. Anything else can't name an object. *\u002F\nconst STRIPE_ID = \u002F^[\\w-]+$\u002Fu;\n    \u002F\u002F …\n    \u002F** Sends one request and reads the whole body within the deadline. Throws only when no answer arrives. *\u002F\n    async #send(method: Method, path: string, form?: URLSearchParams): Promise\u003CStripeResponse> {\n        const headers: Record\u003Cstring, string> = { 'Authorization': `Bearer ${this.#apiKey}`, 'Stripe-Version': STRIPE_API_VERSION };\n        const init: RequestInit = { method, headers, signal: AbortSignal.timeout(TIMEOUT_MS) };\n\n        if (form !== undefined) {\n            headers['Content-Type'] = 'application\u002Fx-www-form-urlencoded';\n            init.body = form.toString();\n        }\n\n        let response: Response;\n        let text: string;\n\n        try {\n            response = await fetch(new URL(path, BASE_URL), init);\n            text = await response.text();\n        }\n        catch (error) {\n            const isTimeout = error instanceof Error && error.name === 'TimeoutError';\n            const reason = isTimeout ? `did not answer within ${TIMEOUT_MS \u002F 1000} seconds` : 'could not be reached';\n            \u002F\u002F Keep the rejection as the `cause`: when the data source's permissions refused the request,\n            \u002F\u002F Engine finds that there and reports a permission error instead of a failed connection.\n            throw new ConnectionFailed(`The Stripe API ${reason}.`, { cause: error });\n        }\n\n        return { status: response.status, ok: response.ok, body: parseJson(text), headers: response.headers };\n    }\n","src\u002Fdata-connectors\u002Fstripe\u002Fclient.ts","ts","",[468,539,540,553,563,571,579,587,595,603,611,627,634,655,662,680,686,701,707,722,728,758,764,770,834,881,920,925,945,957,963,969,974,985,1000,1005,1013,1053,1074,1079,1092,1130,1167,1173,1179,1211,1216,1221,1269],{"__ignoreMap":537},[541,542,545,549],"span",{"class":543,"line":544},"line",1,[541,546,548],{"class":547},"sLVBU","import",[541,550,552],{"class":551},"sTMul"," {\n",[541,554,556,560],{"class":543,"line":555},2,[541,557,559],{"class":558},"sNJ9c","    AccessDenied",[541,561,562],{"class":551},",\n",[541,564,566,569],{"class":543,"line":565},3,[541,567,568],{"class":558},"    ConnectionFailed",[541,570,562],{"class":551},[541,572,574,577],{"class":543,"line":573},4,[541,575,576],{"class":558},"    ForeignKeyConstraintViolation",[541,578,562],{"class":551},[541,580,582,585],{"class":543,"line":581},5,[541,583,584],{"class":558},"    NullConstraintViolation",[541,586,562],{"class":551},[541,588,590,593],{"class":543,"line":589},6,[541,591,592],{"class":558},"    QueryRejected",[541,594,562],{"class":551},[541,596,598,601],{"class":543,"line":597},7,[541,599,600],{"class":558},"    RateLimited",[541,602,562],{"class":551},[541,604,606,609],{"class":543,"line":605},8,[541,607,608],{"class":558},"    UniqueConstraintViolation",[541,610,562],{"class":551},[541,612,614,617,620,624],{"class":543,"line":613},9,[541,615,616],{"class":551},"} ",[541,618,619],{"class":547},"from",[541,621,623],{"class":622},"suKVh"," '@monospace\u002Fextension-kit\u002Fdata-connector'",[541,625,626],{"class":551},";\n",[541,628,630],{"class":543,"line":629},10,[541,631,633],{"class":632},"seNfP","\u002F\u002F Pinned next to the field paths it governs; see collections.ts.\n",[541,635,637,639,642,645,648,650,653],{"class":543,"line":636},11,[541,638,548],{"class":547},[541,640,641],{"class":551}," { ",[541,643,644],{"class":558},"STRIPE_API_VERSION",[541,646,647],{"class":551}," } ",[541,649,619],{"class":547},[541,651,652],{"class":622}," '.\u002Fcollections'",[541,654,626],{"class":551},[541,656,658],{"class":543,"line":657},12,[541,659,661],{"emptyLinePlaceholder":660},true,"\n",[541,663,665,668,672,675,678],{"class":543,"line":664},13,[541,666,667],{"class":547},"const",[541,669,671],{"class":670},"ssBNi"," BASE_URL",[541,673,674],{"class":547}," =",[541,676,677],{"class":622}," 'https:\u002F\u002Fapi.stripe.com\u002Fv1\u002F'",[541,679,626],{"class":551},[541,681,683],{"class":543,"line":682},14,[541,684,685],{"class":632},"\u002F** Engine sets no deadline on `query` or `testConnection`, so every request carries its own. *\u002F\n",[541,687,689,691,694,696,699],{"class":543,"line":688},15,[541,690,667],{"class":547},[541,692,693],{"class":670}," TIMEOUT_MS",[541,695,674],{"class":547},[541,697,698],{"class":670}," 10_000",[541,700,626],{"class":551},[541,702,704],{"class":543,"line":703},16,[541,705,706],{"class":632},"\u002F** The most objects Stripe returns per list request. *\u002F\n",[541,708,710,712,715,717,720],{"class":543,"line":709},17,[541,711,667],{"class":547},[541,713,714],{"class":670}," PAGE_SIZE",[541,716,674],{"class":547},[541,718,719],{"class":670}," 100",[541,721,626],{"class":551},[541,723,725],{"class":543,"line":724},18,[541,726,727],{"class":632},"\u002F** Stripe ids are letters, digits, `_` and `-`. Anything else can't name an object. *\u002F\n",[541,729,731,733,736,738,741,744,747,750,753,756],{"class":543,"line":730},19,[541,732,667],{"class":547},[541,734,735],{"class":670}," STRIPE_ID",[541,737,674],{"class":547},[541,739,740],{"class":622}," \u002F",[541,742,743],{"class":547},"^",[541,745,746],{"class":670},"[\\w-]",[541,748,749],{"class":547},"+$",[541,751,752],{"class":622},"\u002F",[541,754,755],{"class":547},"u",[541,757,626],{"class":551},[541,759,761],{"class":543,"line":760},20,[541,762,763],{"class":632},"    \u002F\u002F …\n",[541,765,767],{"class":543,"line":766},21,[541,768,769],{"class":632},"    \u002F** Sends one request and reads the whole body within the deadline. Throws only when no answer arrives. *\u002F\n",[541,771,773,776,779,782,785,788,791,794,797,800,802,805,807,810,813,816,819,823,826,829,832],{"class":543,"line":772},22,[541,774,775],{"class":558},"    async",[541,777,778],{"class":551}," #",[541,780,781],{"class":670},"send",[541,783,784],{"class":551},"(",[541,786,787],{"class":558},"method",[541,789,790],{"class":551},": ",[541,792,793],{"class":558},"Method",[541,795,796],{"class":551},", ",[541,798,799],{"class":558},"path",[541,801,790],{"class":551},[541,803,804],{"class":558},"string",[541,806,796],{"class":551},[541,808,809],{"class":558},"form",[541,811,812],{"class":547},"?:",[541,814,815],{"class":558}," URLSearchParams",[541,817,818],{"class":551},"): ",[541,820,822],{"class":821},"snHjA","Promise",[541,824,825],{"class":547},"\u003C",[541,827,828],{"class":558},"StripeResponse",[541,830,831],{"class":547},">",[541,833,552],{"class":551},[541,835,837,840,843,845,847,850,853,855,858,861,863,866,869,871,874,876,878],{"class":543,"line":836},23,[541,838,839],{"class":551},"        const headers: ",[541,841,842],{"class":558},"Record",[541,844,825],{"class":547},[541,846,804],{"class":558},[541,848,849],{"class":551},", string> = { ",[541,851,852],{"class":622},"'Authorization'",[541,854,790],{"class":551},[541,856,857],{"class":622},"`Bearer ${",[541,859,860],{"class":670},"this",[541,862,489],{"class":622},[541,864,865],{"class":558},"#apiKey",[541,867,868],{"class":622},"}`",[541,870,796],{"class":551},[541,872,873],{"class":622},"'Stripe-Version'",[541,875,790],{"class":551},[541,877,644],{"class":670},[541,879,880],{"class":551}," };\n",[541,882,884,887,890,892,894,896,898,901,904,907,909,912,914,917],{"class":543,"line":883},24,[541,885,886],{"class":551},"        const init: ",[541,888,889],{"class":558},"RequestInit",[541,891,674],{"class":547},[541,893,641],{"class":551},[541,895,787],{"class":558},[541,897,796],{"class":551},[541,899,900],{"class":558},"headers",[541,902,903],{"class":551},", signal: ",[541,905,906],{"class":558},"AbortSignal",[541,908,489],{"class":551},[541,910,911],{"class":670},"timeout",[541,913,784],{"class":551},[541,915,916],{"class":670},"TIMEOUT_MS",[541,918,919],{"class":551},") };\n",[541,921,923],{"class":543,"line":922},25,[541,924,661],{"emptyLinePlaceholder":660},[541,926,928,931,934,936,939,942],{"class":543,"line":927},26,[541,929,930],{"class":670},"        if",[541,932,933],{"class":551}," (",[541,935,809],{"class":558},[541,937,938],{"class":547}," !==",[541,940,941],{"class":670}," undefined",[541,943,944],{"class":551},") {\n",[541,946,948,951,954],{"class":543,"line":947},27,[541,949,950],{"class":551},"            headers[",[541,952,953],{"class":622},"'Content-Type'",[541,955,956],{"class":551},"] = 'application\u002Fx-www-form-urlencoded';\n",[541,958,960],{"class":543,"line":959},28,[541,961,962],{"class":551},"            init.body = form.toString();\n",[541,964,966],{"class":543,"line":965},29,[541,967,968],{"class":551},"        }\n",[541,970,972],{"class":543,"line":971},30,[541,973,661],{"emptyLinePlaceholder":660},[541,975,977,980,983],{"class":543,"line":976},31,[541,978,979],{"class":551},"        let response: ",[541,981,982],{"class":558},"Response",[541,984,626],{"class":551},[541,986,988,991,994,996,998],{"class":543,"line":987},32,[541,989,990],{"class":558},"        let",[541,992,993],{"class":558}," text",[541,995,790],{"class":551},[541,997,804],{"class":558},[541,999,626],{"class":551},[541,1001,1003],{"class":543,"line":1002},33,[541,1004,661],{"emptyLinePlaceholder":660},[541,1006,1008,1011],{"class":543,"line":1007},34,[541,1009,1010],{"class":558},"        try",[541,1012,552],{"class":551},[541,1014,1016,1019,1021,1024,1027,1029,1032,1035,1037,1039,1041,1044,1047,1050],{"class":543,"line":1015},35,[541,1017,1018],{"class":558},"            response",[541,1020,674],{"class":547},[541,1022,1023],{"class":547}," await",[541,1025,1026],{"class":670}," fetch",[541,1028,784],{"class":551},[541,1030,1031],{"class":547},"new",[541,1033,1034],{"class":670}," URL",[541,1036,784],{"class":551},[541,1038,799],{"class":558},[541,1040,796],{"class":551},[541,1042,1043],{"class":670},"BASE_URL",[541,1045,1046],{"class":551},"), ",[541,1048,1049],{"class":558},"init",[541,1051,1052],{"class":551},");\n",[541,1054,1056,1059,1061,1063,1066,1068,1071],{"class":543,"line":1055},36,[541,1057,1058],{"class":558},"            text",[541,1060,674],{"class":547},[541,1062,1023],{"class":547},[541,1064,1065],{"class":558}," response",[541,1067,489],{"class":551},[541,1069,1070],{"class":670},"text",[541,1072,1073],{"class":551},"();\n",[541,1075,1077],{"class":543,"line":1076},37,[541,1078,968],{"class":551},[541,1080,1082,1085,1087,1090],{"class":543,"line":1081},38,[541,1083,1084],{"class":670},"        catch",[541,1086,933],{"class":551},[541,1088,1089],{"class":558},"error",[541,1091,944],{"class":551},[541,1093,1095,1098,1101,1103,1106,1109,1112,1115,1117,1119,1122,1125,1128],{"class":543,"line":1094},39,[541,1096,1097],{"class":551},"            const ",[541,1099,1100],{"class":558},"isTimeout",[541,1102,674],{"class":547},[541,1104,1105],{"class":558}," error",[541,1107,1108],{"class":547}," instanceof",[541,1110,1111],{"class":670}," Error",[541,1113,1114],{"class":547}," &&",[541,1116,1105],{"class":558},[541,1118,489],{"class":551},[541,1120,1121],{"class":558},"name",[541,1123,1124],{"class":547}," ===",[541,1126,1127],{"class":622}," 'TimeoutError'",[541,1129,626],{"class":551},[541,1131,1133,1135,1138,1140,1143,1146,1149,1151,1153,1156,1159,1162,1165],{"class":543,"line":1132},40,[541,1134,1097],{"class":551},[541,1136,1137],{"class":558},"reason",[541,1139,674],{"class":547},[541,1141,1142],{"class":558}," isTimeout",[541,1144,1145],{"class":547}," ?",[541,1147,1148],{"class":622}," `did not answer within ${",[541,1150,916],{"class":670},[541,1152,740],{"class":547},[541,1154,1155],{"class":670}," 1000",[541,1157,1158],{"class":622},"} seconds`",[541,1160,1161],{"class":547}," :",[541,1163,1164],{"class":622}," 'could not be reached'",[541,1166,626],{"class":551},[541,1168,1170],{"class":543,"line":1169},41,[541,1171,1172],{"class":632},"            \u002F\u002F Keep the rejection as the `cause`: when the data source's permissions refused the request,\n",[541,1174,1176],{"class":543,"line":1175},42,[541,1177,1178],{"class":632},"            \u002F\u002F Engine finds that there and reports a permission error instead of a failed connection.\n",[541,1180,1182,1185,1188,1190,1193,1195,1198,1201,1204,1206,1208],{"class":543,"line":1181},43,[541,1183,1184],{"class":551},"            throw new ",[541,1186,1187],{"class":670},"ConnectionFailed",[541,1189,784],{"class":551},[541,1191,1192],{"class":622},"`The Stripe API ${",[541,1194,1137],{"class":558},[541,1196,1197],{"class":622},"}.`",[541,1199,1200],{"class":551},", { ",[541,1202,1203],{"class":547},"cause",[541,1205,790],{"class":551},[541,1207,1089],{"class":547},[541,1209,1210],{"class":551}," });\n",[541,1212,1214],{"class":543,"line":1213},44,[541,1215,968],{"class":551},[541,1217,1219],{"class":543,"line":1218},45,[541,1220,661],{"emptyLinePlaceholder":660},[541,1222,1224,1227,1230,1233,1235,1238,1241,1243,1245,1248,1251,1254,1256,1258,1261,1263,1265,1267],{"class":543,"line":1223},46,[541,1225,1226],{"class":558},"        return",[541,1228,1229],{"class":551}," { status: ",[541,1231,1232],{"class":558},"response",[541,1234,489],{"class":551},[541,1236,1237],{"class":558},"status",[541,1239,1240],{"class":551},", ok: ",[541,1242,1232],{"class":558},[541,1244,489],{"class":551},[541,1246,1247],{"class":558},"ok",[541,1249,1250],{"class":551},", body: ",[541,1252,1253],{"class":670},"parseJson",[541,1255,784],{"class":551},[541,1257,1070],{"class":558},[541,1259,1260],{"class":551},"), headers: ",[541,1262,1232],{"class":558},[541,1264,489],{"class":551},[541,1266,900],{"class":558},[541,1268,880],{"class":551},[541,1270,1272],{"class":543,"line":1271},47,[541,1273,1274],{"class":551},"    }\n",[1276,1277,1278,1289,1325,1339,1362,1382],"ul",{},[1279,1280,1281,1286,1287,489],"li",{},[463,1282,1283,1285],{},[468,1284,510],{}," rejects when no response arrives"," (DNS, refused connection, TLS, abort). That's ",[468,1288,1187],{},[1279,1290,1291,1296,1297,1299,1300,1303,1304,1306,1307,1309,1310,1313,1314,1317,1318,1320,1321,489],{},[463,1292,1293,1294,489],{},"Keep the rejection as ",[468,1295,1203],{}," ",[468,1298,510],{}," also rejects for a host the data source wasn't granted, with an error named ",[468,1301,1302],{},"NotCapable",". Engine looks for it anywhere in the thrown error's ",[468,1305,1203],{}," chain, so ",[468,1308,1187],{}," with ",[468,1311,1312],{},"{ cause: error }"," still reaches the caller as ",[468,1315,1316],{},"422"," \"Invalid extension permissions\", with your message (\"The Stripe API could not be reached.\") nested inside. Without ",[468,1319,1203],{},", the same denial reads as an unreachable external data source. See ",[480,1322,1324],{"href":1323},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Fpermissions#handle-a-denied-request","Handle a Denied Request",[1279,1326,1327,1332,1333,1335,1336,1338],{},[463,1328,1329,1330,489],{},"Read the body inside the same ",[468,1331,514],{}," A body read can fail or time out too. Inside the ",[468,1334,514],{},", it becomes ",[468,1337,1187],{}," instead of escaping unbranded.",[1279,1340,1341,1344,1345,1347,1348,1351,1352,1355,1356,1358,1359,489],{},[463,1342,1343],{},"Parse without throwing."," The Stripe client's ",[468,1346,1253],{}," answers ",[468,1349,1350],{},"undefined"," for a body that isn't JSON, and ",[468,1353,1354],{},"checked"," turns that into an ",[468,1357,470],{}," with a readable message, so a proxy's HTML error page never escapes as a ",[468,1360,1361],{},"SyntaxError",[1279,1363,1364,1367,1368,1371,1372,1374,1375,1378,1379,489],{},[463,1365,1366],{},"A status is not a rejection."," Check ",[468,1369,1370],{},"response.status"," yourself; ",[468,1373,510],{}," resolves for a ",[468,1376,1377],{},"429"," or a ",[468,1380,1381],{},"500",[1279,1383,1384,1387,1388,1390,1391,489],{},[463,1385,1386],{},"Throw a kit class for every failure a class describes."," An error without one reaches Engine unclassified. From a handler, it's reported as a failed query, and Engine can't tell a rate limit from a refused write. A permission denial in its ",[468,1389,1203],{}," chain still reaches the caller as one. See ",[480,1392,1394],{"href":1393},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Ferrors#know-the-defaults-for-unbranded-throws","Know the Defaults for Unbranded Throws",[459,1396,1397,1398,1400,1401,1403,1404,1407],{},"When it works, every error that leaves a handler names a kit class. The exception is a plain ",[468,1399,470],{}," you throw on purpose where no class fits (see step 2). No handler lets a ",[468,1402,510],{},", body-read or ",[468,1405,1406],{},"JSON.parse"," error escape.",[455,1409,1411],{"id":1410},"_2-pick-a-class-for-each-failure","2. Pick a Class for Each Failure",[459,1413,1414],{},"Classify by who can fix the failure, not by the status code alone. Many external data sources use one status for several meanings, so read the error body's code where the external data source has one. The Stripe and Art Institute connectors map failures this way:",[1416,1417,1418,1434],"table",{},[1419,1420,1421],"thead",{},[1422,1423,1424,1428,1431],"tr",{},[1425,1426,1427],"th",{},"Failure",[1425,1429,1430],{},"Throw",[1425,1432,1433],{},"Notes",[1435,1436,1437,1456,1482,1496,1514,1540,1560,1598,1617,1632,1648,1668,1689,1704,1719,1735,1749],"tbody",{},[1422,1438,1439,1443,1447],{},[1440,1441,1442],"td",{},"No response: DNS failure, refused connection, TLS error, aborted request",[1440,1444,1445],{},[468,1446,1187],{},[1440,1448,1449,1450,1452,1453,1455],{},"Catch around ",[468,1451,510],{},", with the rejection as ",[468,1454,1203],{},", as in step 1",[1422,1457,1458,1464,1468],{},[1440,1459,1460,1461],{},"Timeout from ",[468,1462,1463],{},"AbortSignal.timeout",[1440,1465,1466],{},[468,1467,1187],{},[1440,1469,1470,1471,1473,1474,1477,1478],{},"The abort rejects ",[468,1472,510],{},", so the same ",[468,1475,1476],{},"catch"," handles it. See ",[480,1479,1481],{"href":1480},"#_3-add-a-timeout-to-every-request","step 3",[1422,1483,1484,1489,1493],{},[1440,1485,1486],{},[468,1487,1488],{},"5xx",[1440,1490,1491],{},[468,1492,1187],{},[1440,1494,1495],{},"Treat it as an outage: a later request may work",[1422,1497,1498,1502,1507],{},[1440,1499,1500],{},[468,1501,1377],{},[1440,1503,1504],{},[468,1505,1506],{},"RateLimited",[1440,1508,1509,1510,1513],{},"Put the wait into the message, as the Stripe client's ",[468,1511,1512],{},"retryAfter"," does: \"Stripe rate limited the connector. Retry after 2 seconds.\"",[1422,1515,1516,1526,1531],{},[1440,1517,1518,1521,1522,1525],{},[468,1519,1520],{},"401"," or ",[468,1523,1524],{},"403",": the credential is rejected, or lacks access",[1440,1527,1528],{},[468,1529,1530],{},"AccessDenied",[1440,1532,1533,1534,1536,1537,1539],{},"Only someone with access to the external data source's account can fix it. If your external data source also answers a rate limit with a ",[468,1535,1524],{},", tell the two apart by what its documentation names (e.g., a header or an error code), and throw ",[468,1538,1506],{}," for the rate limit",[1422,1541,1542,1548,1553],{},[1440,1543,1544,1547],{},[468,1545,1546],{},"400",": a value the external data source refuses (e.g., an invalid email)",[1440,1549,1550],{},[468,1551,1552],{},"QueryRejected",[1440,1554,1555,1556,1559],{},"The caller can change the value. The Stripe client matches Stripe's ",[468,1557,1558],{},"type: invalid_request_error"," and passes Stripe's message on",[1422,1561,1562,1568,1591],{},[1440,1563,1564,1567],{},[468,1565,1566],{},"404"," in the external data source's own error format, for a single-item lookup",[1440,1569,1570,1571,796,1574,1577,1578,1581,1582,1585,1586,1588,1589],{},"No error where a missing item means no match. ",[468,1572,1573],{},"readOne",[468,1575,1576],{},"updateOne"," and ",[468,1579,1580],{},"deleteOne"," answer ",[468,1583,1584],{},"{ record: null }",", which the caller sees as ",[468,1587,1566],{},", and a read by id leaves the item out. Elsewhere, such as a bulk write to an item deleted meanwhile, a plain ",[468,1590,470],{},[1440,1592,1593,1594],{},"No kit class reports a missing item. See ",[480,1595,1597],{"href":1596},"#answer-keyed-misses-with-record-null","Answer Keyed Misses with record: null",[1422,1599,1600,1603,1608],{},[1440,1601,1602],{},"A collection your connector declared no longer exists in the external data source (e.g., a deleted object type)",[1440,1604,1605],{},[468,1606,1607],{},"CollectionNotFound",[1440,1609,1610,1611,1613,1614,1616],{},"The caller gets ",[468,1612,1566],{},". A collection your connector never declared gets a plain ",[468,1615,470],{}," instead. An extension update without a schema refresh can send one",[1422,1618,1619,1624,1629],{},[1440,1620,1621,1623],{},[468,1622,1566],{}," without the external data source's error format (e.g., an HTML page)",[1440,1625,1626,1627],{},"A plain ",[468,1628,470],{},[1440,1630,1631],{},"A wrong base URL or route. Treating it as \"not found\" makes a misconfigured data source look empty",[1422,1633,1634,1641,1646],{},[1440,1635,1636,1521,1638,1640],{},[468,1637,1546],{},[468,1639,1316],{},": a required value is missing",[1440,1642,1643],{},[468,1644,1645],{},"NullConstraintViolation",[1440,1647],{},[1422,1649,1650,1661,1666],{},[1440,1651,1652,796,1654,1657,1658,1660],{},[468,1653,1546],{},[468,1655,1656],{},"409",", or ",[468,1659,1316],{},": a value must be unique, or the item already exists",[1440,1662,1663],{},[468,1664,1665],{},"UniqueConstraintViolation",[1440,1667],{},[1422,1669,1670,1679,1684],{},[1440,1671,1672,796,1674,1657,1676,1678],{},[468,1673,1546],{},[468,1675,1656],{},[468,1677,1316],{},": a referenced item doesn't exist, or another item still references this one",[1440,1680,1681],{},[468,1682,1683],{},"ForeignKeyConstraintViolation",[1440,1685,1686,1687],{},"A write that names a missing item is this: the caller gets ",[468,1688,1316],{},[1422,1690,1691,1694,1698],{},[1440,1692,1693],{},"Any other failed status",[1440,1695,1626,1696],{},[468,1697,470],{},[1440,1699,1700,1701,1703],{},"The Art Institute connector answers its API's ",[468,1702,1524],{}," for a page size over 100 this way: the connector sent a request the API refuses, and the caller can't fix that",[1422,1705,1706,1712,1716],{},[1440,1707,1708,1711],{},[468,1709,1710],{},"2xx"," with a body you can't parse, of the wrong shape, or with a value of the wrong type",[1440,1713,1626,1714],{},[468,1715,470],{},[1440,1717,1718],{},"A changed API. Check the shape where you parse it, so it fails in your client instead of deep inside Engine",[1422,1720,1721,1724,1728],{},[1440,1722,1723],{},"A valid request your connector can't serve as asked",[1440,1725,1726],{},[468,1727,1552],{},[1440,1729,1730,1731],{},"See ",[480,1732,1734],{"href":1733},"#_4-refuse-requests-you-cant-serve","step 4",[1422,1736,1737,1740,1744],{},[1440,1738,1739],{},"A request outside what your connector declares",[1440,1741,1626,1742],{},[468,1743,470],{},[1440,1745,1746,1747],{},"An extension update without a schema refresh can send an undeclared collection, operation, sort or count. An undeclared filter comparison can also come from a permission rule's condition. Engine's exempt lookups on keys no declaration can carry aren't in this group: serve them. See ",[480,1748,1734],{"href":1733},[1422,1750,1751,1754,1759],{},[1440,1752,1753],{},"Missing or invalid configuration",[1440,1755,1756],{},[468,1757,1758],{},"InvalidConfiguration",[1440,1760,1761,1762,1765,1766],{},"Thrown from ",[468,1763,1764],{},"setup",". See ",[480,1767,1769],{"href":1768},"#_5-validate-configuration-in-setup","step 5",[459,1771,1772,1773,1775,1776,1778],{},"The examples throw a plain ",[468,1774,470],{}," when no class fits and nothing the caller changes fixes it, such as a changed API, which retrying doesn't fix either. Engine reports it as a failed query, ",[468,1777,1381],{}," \"Failed to query the database\".",[459,1780,1781,1783,1784,1787,1788,1577,1790,1793],{},[468,1782,1354],{}," returns the body of a successful response. ",[468,1785,1786],{},"toConnectorError"," maps every other response by Stripe's error ",[468,1789,468],{},[468,1791,1792],{},"param",", after special cases for rejected keys and rate limits, then by status:",[524,1795,1796],{},[527,1797,1799],{"label":1798},"src\u002Fdata-connectors\u002Fstripe\u002Fclient.ts (checked, toConnectorError)",[531,1800,1802],{"className":533,"code":1801,"filename":535,"language":536,"meta":537,"style":537},"\u002F** The body of a successful response; any other response becomes an error. *\u002F\nfunction checked(response: StripeResponse, method: Method, path: string): unknown {\n    if (!response.ok) throw toConnectorError(response, method, path);\n    \u002F\u002F A changed API, not something the caller can fix: a plain error, which Engine reports as a failed query.\n    if (response.body === undefined) throw new Error('Stripe returned a response that is not JSON.');\n    return response.body;\n}\n\n\u002F**\n * Maps a Stripe error to the kit error that tells Engine what went wrong. Besides the code, only the\n * message leaves the connector: Stripe's own message, or the status and the path. It never holds the\n * request log URL (which names the Stripe account), the query string, or anything about the key.\n *\u002F\nfunction toConnectorError(response: StripeResponse, method: Method, path: string): Error {\n    const detail = readError(response.body);\n    const { status } = response;\n    \u002F\u002F The path without the query string, which can hold the caller's filter values.\n    const message = detail.message ?? `Stripe answered HTTP ${status} to ${method} ${new URL(path, BASE_URL).pathname}.`;\n\n    \u002F\u002F Only someone with access to the Stripe account can fix the key. Stripe's message for a rejected\n    \u002F\u002F key echoes part of the key, so it stays out.\n    if (status === 401 || status === 403) return new AccessDenied(`Stripe rejected the API key (HTTP ${status}).`);\n    if (status === 429) return new RateLimited(`Stripe rate limited the connector.${retryAfter(response.headers)}`);\n\n    switch (detail.code) {\n        case 'resource_missing':\n            \u002F\u002F `param` names what is missing: `id` for the object in the path. On a write, any other\n            \u002F\u002F field is an object the written values refer to, such as a new price's `product`.\n            if (method === 'POST' && detail.param !== undefined && detail.param !== 'id') return new ForeignKeyConstraintViolation(message);\n            return new StripeObjectMissing(message);\n        case 'resource_already_exists':\n            return new UniqueConstraintViolation(message);\n        case 'parameter_missing':\n            return new NullConstraintViolation(message);\n    }\n\n    \u002F\u002F Stripe refuses to delete a product that still has prices with no error code, only a message.\n    if (method === 'DELETE' && detail.type === 'invalid_request_error' && \u002F\\bprices\\b\u002Fu.test(message)) {\n        return new ForeignKeyConstraintViolation(message);\n    }\n\n    \u002F\u002F Treat a server error as an outage: a later request may work.\n    if (status >= 500) return new ConnectionFailed(message);\n    \u002F\u002F Stripe answers 400 to values it refuses, such as an invalid email: the caller can change them.\n    if (status === 400 && detail.type === 'invalid_request_error') return new QueryRejected(message);\n    return new Error(message);\n}\n",[468,1803,1804,1809,1852,1891,1896,1929,1942,1947,1951,1956,1961,1966,1971,1976,2008,2031,2048,2053,2107,2111,2116,2121,2167,2210,2214,2230,2241,2246,2251,2305,2321,2330,2345,2354,2369,2373,2377,2382,2437,2451,2455,2459,2464,2493,2498,2538,2552],{"__ignoreMap":537},[541,1805,1806],{"class":543,"line":544},[541,1807,1808],{"class":632},"\u002F** The body of a successful response; any other response becomes an error. *\u002F\n",[541,1810,1811,1814,1817,1819,1822,1825,1827,1830,1833,1835,1838,1841,1844,1847,1850],{"class":543,"line":555},[541,1812,1813],{"class":547},"function",[541,1815,1816],{"class":670}," checked",[541,1818,784],{"class":551},[541,1820,1821],{"class":547},"response:",[541,1823,1824],{"class":670}," StripeResponse",[541,1826,796],{"class":551},[541,1828,1829],{"class":547},"method:",[541,1831,1832],{"class":670}," Method",[541,1834,796],{"class":551},[541,1836,1837],{"class":547},"path:",[541,1839,1840],{"class":821}," string",[541,1842,1843],{"class":551},")",[541,1845,1846],{"class":547},":",[541,1848,1849],{"class":821}," unknown",[541,1851,552],{"class":551},[541,1853,1854,1857,1859,1862,1864,1866,1868,1871,1874,1877,1879,1881,1883,1885,1887,1889],{"class":543,"line":565},[541,1855,1856],{"class":547},"    if",[541,1858,933],{"class":551},[541,1860,1861],{"class":547},"!",[541,1863,1232],{"class":558},[541,1865,489],{"class":551},[541,1867,1247],{"class":558},[541,1869,1870],{"class":551},") ",[541,1872,1873],{"class":547},"throw",[541,1875,1876],{"class":670}," toConnectorError",[541,1878,784],{"class":551},[541,1880,1232],{"class":558},[541,1882,796],{"class":551},[541,1884,787],{"class":558},[541,1886,796],{"class":551},[541,1888,799],{"class":558},[541,1890,1052],{"class":551},[541,1892,1893],{"class":543,"line":573},[541,1894,1895],{"class":632},"    \u002F\u002F A changed API, not something the caller can fix: a plain error, which Engine reports as a failed query.\n",[541,1897,1898,1900,1902,1904,1906,1909,1911,1913,1915,1917,1920,1922,1924,1927],{"class":543,"line":581},[541,1899,1856],{"class":547},[541,1901,933],{"class":551},[541,1903,1232],{"class":558},[541,1905,489],{"class":551},[541,1907,1908],{"class":558},"body",[541,1910,1124],{"class":547},[541,1912,941],{"class":670},[541,1914,1870],{"class":551},[541,1916,1873],{"class":547},[541,1918,1919],{"class":547}," new",[541,1921,1111],{"class":670},[541,1923,784],{"class":551},[541,1925,1926],{"class":622},"'Stripe returned a response that is not JSON.'",[541,1928,1052],{"class":551},[541,1930,1931,1934,1936,1938,1940],{"class":543,"line":589},[541,1932,1933],{"class":547},"    return",[541,1935,1065],{"class":558},[541,1937,489],{"class":551},[541,1939,1908],{"class":558},[541,1941,626],{"class":551},[541,1943,1944],{"class":543,"line":597},[541,1945,1946],{"class":551},"}\n",[541,1948,1949],{"class":543,"line":605},[541,1950,661],{"emptyLinePlaceholder":660},[541,1952,1953],{"class":543,"line":613},[541,1954,1955],{"class":632},"\u002F**\n",[541,1957,1958],{"class":543,"line":629},[541,1959,1960],{"class":632}," * Maps a Stripe error to the kit error that tells Engine what went wrong. Besides the code, only the\n",[541,1962,1963],{"class":543,"line":636},[541,1964,1965],{"class":632}," * message leaves the connector: Stripe's own message, or the status and the path. It never holds the\n",[541,1967,1968],{"class":543,"line":657},[541,1969,1970],{"class":632}," * request log URL (which names the Stripe account), the query string, or anything about the key.\n",[541,1972,1973],{"class":543,"line":664},[541,1974,1975],{"class":632}," *\u002F\n",[541,1977,1978,1980,1982,1984,1986,1988,1990,1992,1994,1996,1998,2000,2002,2004,2006],{"class":543,"line":682},[541,1979,1813],{"class":547},[541,1981,1876],{"class":670},[541,1983,784],{"class":551},[541,1985,1821],{"class":547},[541,1987,1824],{"class":670},[541,1989,796],{"class":551},[541,1991,1829],{"class":547},[541,1993,1832],{"class":670},[541,1995,796],{"class":551},[541,1997,1837],{"class":547},[541,1999,1840],{"class":821},[541,2001,1843],{"class":551},[541,2003,1846],{"class":547},[541,2005,1111],{"class":670},[541,2007,552],{"class":551},[541,2009,2010,2013,2016,2018,2021,2023,2025,2027,2029],{"class":543,"line":688},[541,2011,2012],{"class":547},"    const",[541,2014,2015],{"class":670}," detail",[541,2017,674],{"class":547},[541,2019,2020],{"class":670}," readError",[541,2022,784],{"class":551},[541,2024,1232],{"class":558},[541,2026,489],{"class":551},[541,2028,1908],{"class":558},[541,2030,1052],{"class":551},[541,2032,2033,2035,2037,2039,2041,2044,2046],{"class":543,"line":703},[541,2034,2012],{"class":547},[541,2036,641],{"class":551},[541,2038,1237],{"class":670},[541,2040,647],{"class":551},[541,2042,2043],{"class":547},"=",[541,2045,1065],{"class":558},[541,2047,626],{"class":551},[541,2049,2050],{"class":543,"line":709},[541,2051,2052],{"class":632},"    \u002F\u002F The path without the query string, which can hold the caller's filter values.\n",[541,2054,2055,2057,2060,2062,2064,2066,2069,2072,2075,2077,2080,2082,2085,2087,2089,2091,2093,2095,2097,2100,2103,2105],{"class":543,"line":724},[541,2056,2012],{"class":547},[541,2058,2059],{"class":670}," message",[541,2061,674],{"class":547},[541,2063,2015],{"class":558},[541,2065,489],{"class":551},[541,2067,2068],{"class":558},"message",[541,2070,2071],{"class":547}," ??",[541,2073,2074],{"class":622}," `Stripe answered HTTP ${",[541,2076,1237],{"class":558},[541,2078,2079],{"class":622},"} to ${",[541,2081,787],{"class":558},[541,2083,2084],{"class":622},"} ${",[541,2086,1031],{"class":547},[541,2088,1034],{"class":670},[541,2090,784],{"class":622},[541,2092,799],{"class":558},[541,2094,796],{"class":622},[541,2096,1043],{"class":670},[541,2098,2099],{"class":622},").",[541,2101,2102],{"class":558},"pathname",[541,2104,1197],{"class":622},[541,2106,626],{"class":551},[541,2108,2109],{"class":543,"line":730},[541,2110,661],{"emptyLinePlaceholder":660},[541,2112,2113],{"class":543,"line":760},[541,2114,2115],{"class":632},"    \u002F\u002F Only someone with access to the Stripe account can fix the key. Stripe's message for a rejected\n",[541,2117,2118],{"class":543,"line":766},[541,2119,2120],{"class":632},"    \u002F\u002F key echoes part of the key, so it stays out.\n",[541,2122,2123,2125,2127,2129,2131,2134,2137,2140,2142,2145,2147,2150,2152,2155,2157,2160,2162,2165],{"class":543,"line":772},[541,2124,1856],{"class":547},[541,2126,933],{"class":551},[541,2128,1237],{"class":558},[541,2130,1124],{"class":547},[541,2132,2133],{"class":670}," 401",[541,2135,2136],{"class":547}," ||",[541,2138,2139],{"class":558}," status",[541,2141,1124],{"class":547},[541,2143,2144],{"class":670}," 403",[541,2146,1870],{"class":551},[541,2148,2149],{"class":547},"return",[541,2151,1919],{"class":547},[541,2153,2154],{"class":670}," AccessDenied",[541,2156,784],{"class":551},[541,2158,2159],{"class":622},"`Stripe rejected the API key (HTTP ${",[541,2161,1237],{"class":558},[541,2163,2164],{"class":622},"}).`",[541,2166,1052],{"class":551},[541,2168,2169,2171,2173,2175,2177,2180,2182,2184,2186,2189,2191,2194,2196,2198,2200,2202,2204,2206,2208],{"class":543,"line":836},[541,2170,1856],{"class":547},[541,2172,933],{"class":551},[541,2174,1237],{"class":558},[541,2176,1124],{"class":547},[541,2178,2179],{"class":670}," 429",[541,2181,1870],{"class":551},[541,2183,2149],{"class":547},[541,2185,1919],{"class":547},[541,2187,2188],{"class":670}," RateLimited",[541,2190,784],{"class":551},[541,2192,2193],{"class":622},"`Stripe rate limited the connector.${",[541,2195,1512],{"class":670},[541,2197,784],{"class":622},[541,2199,1232],{"class":558},[541,2201,489],{"class":622},[541,2203,900],{"class":558},[541,2205,1843],{"class":622},[541,2207,868],{"class":622},[541,2209,1052],{"class":551},[541,2211,2212],{"class":543,"line":883},[541,2213,661],{"emptyLinePlaceholder":660},[541,2215,2216,2219,2221,2224,2226,2228],{"class":543,"line":922},[541,2217,2218],{"class":547},"    switch",[541,2220,933],{"class":551},[541,2222,2223],{"class":558},"detail",[541,2225,489],{"class":551},[541,2227,468],{"class":558},[541,2229,944],{"class":551},[541,2231,2232,2235,2238],{"class":543,"line":927},[541,2233,2234],{"class":547},"        case",[541,2236,2237],{"class":622}," 'resource_missing'",[541,2239,2240],{"class":551},":\n",[541,2242,2243],{"class":543,"line":947},[541,2244,2245],{"class":632},"            \u002F\u002F `param` names what is missing: `id` for the object in the path. On a write, any other\n",[541,2247,2248],{"class":543,"line":959},[541,2249,2250],{"class":632},"            \u002F\u002F field is an object the written values refer to, such as a new price's `product`.\n",[541,2252,2253,2256,2258,2260,2262,2265,2267,2269,2271,2273,2275,2277,2279,2281,2283,2285,2287,2290,2292,2294,2296,2299,2301,2303],{"class":543,"line":965},[541,2254,2255],{"class":547},"            if",[541,2257,933],{"class":551},[541,2259,787],{"class":558},[541,2261,1124],{"class":547},[541,2263,2264],{"class":622}," 'POST'",[541,2266,1114],{"class":547},[541,2268,2015],{"class":558},[541,2270,489],{"class":551},[541,2272,1792],{"class":558},[541,2274,938],{"class":547},[541,2276,941],{"class":670},[541,2278,1114],{"class":547},[541,2280,2015],{"class":558},[541,2282,489],{"class":551},[541,2284,1792],{"class":558},[541,2286,938],{"class":547},[541,2288,2289],{"class":622}," 'id'",[541,2291,1870],{"class":551},[541,2293,2149],{"class":547},[541,2295,1919],{"class":547},[541,2297,2298],{"class":670}," ForeignKeyConstraintViolation",[541,2300,784],{"class":551},[541,2302,2068],{"class":558},[541,2304,1052],{"class":551},[541,2306,2307,2310,2312,2315,2317,2319],{"class":543,"line":971},[541,2308,2309],{"class":547},"            return",[541,2311,1919],{"class":547},[541,2313,2314],{"class":670}," StripeObjectMissing",[541,2316,784],{"class":551},[541,2318,2068],{"class":558},[541,2320,1052],{"class":551},[541,2322,2323,2325,2328],{"class":543,"line":976},[541,2324,2234],{"class":547},[541,2326,2327],{"class":622}," 'resource_already_exists'",[541,2329,2240],{"class":551},[541,2331,2332,2334,2336,2339,2341,2343],{"class":543,"line":987},[541,2333,2309],{"class":547},[541,2335,1919],{"class":547},[541,2337,2338],{"class":670}," UniqueConstraintViolation",[541,2340,784],{"class":551},[541,2342,2068],{"class":558},[541,2344,1052],{"class":551},[541,2346,2347,2349,2352],{"class":543,"line":1002},[541,2348,2234],{"class":547},[541,2350,2351],{"class":622}," 'parameter_missing'",[541,2353,2240],{"class":551},[541,2355,2356,2358,2360,2363,2365,2367],{"class":543,"line":1007},[541,2357,2309],{"class":547},[541,2359,1919],{"class":547},[541,2361,2362],{"class":670}," NullConstraintViolation",[541,2364,784],{"class":551},[541,2366,2068],{"class":558},[541,2368,1052],{"class":551},[541,2370,2371],{"class":543,"line":1015},[541,2372,1274],{"class":551},[541,2374,2375],{"class":543,"line":1055},[541,2376,661],{"emptyLinePlaceholder":660},[541,2378,2379],{"class":543,"line":1076},[541,2380,2381],{"class":632},"    \u002F\u002F Stripe refuses to delete a product that still has prices with no error code, only a message.\n",[541,2383,2384,2386,2388,2390,2392,2395,2397,2399,2401,2404,2406,2409,2411,2413,2416,2419,2421,2423,2425,2427,2430,2432,2434],{"class":543,"line":1081},[541,2385,1856],{"class":547},[541,2387,933],{"class":551},[541,2389,787],{"class":558},[541,2391,1124],{"class":547},[541,2393,2394],{"class":622}," 'DELETE'",[541,2396,1114],{"class":547},[541,2398,2015],{"class":558},[541,2400,489],{"class":551},[541,2402,2403],{"class":558},"type",[541,2405,1124],{"class":547},[541,2407,2408],{"class":622}," 'invalid_request_error'",[541,2410,1114],{"class":547},[541,2412,740],{"class":622},[541,2414,2415],{"class":547},"\\b",[541,2417,2418],{"class":622},"prices",[541,2420,2415],{"class":547},[541,2422,752],{"class":622},[541,2424,755],{"class":547},[541,2426,489],{"class":551},[541,2428,2429],{"class":670},"test",[541,2431,784],{"class":551},[541,2433,2068],{"class":558},[541,2435,2436],{"class":551},")) {\n",[541,2438,2439,2441,2443,2445,2447,2449],{"class":543,"line":1094},[541,2440,1226],{"class":547},[541,2442,1919],{"class":547},[541,2444,2298],{"class":670},[541,2446,784],{"class":551},[541,2448,2068],{"class":558},[541,2450,1052],{"class":551},[541,2452,2453],{"class":543,"line":1132},[541,2454,1274],{"class":551},[541,2456,2457],{"class":543,"line":1169},[541,2458,661],{"emptyLinePlaceholder":660},[541,2460,2461],{"class":543,"line":1175},[541,2462,2463],{"class":632},"    \u002F\u002F Treat a server error as an outage: a later request may work.\n",[541,2465,2466,2468,2470,2472,2475,2478,2480,2482,2484,2487,2489,2491],{"class":543,"line":1181},[541,2467,1856],{"class":547},[541,2469,933],{"class":551},[541,2471,1237],{"class":558},[541,2473,2474],{"class":547}," >=",[541,2476,2477],{"class":670}," 500",[541,2479,1870],{"class":551},[541,2481,2149],{"class":547},[541,2483,1919],{"class":547},[541,2485,2486],{"class":670}," ConnectionFailed",[541,2488,784],{"class":551},[541,2490,2068],{"class":558},[541,2492,1052],{"class":551},[541,2494,2495],{"class":543,"line":1213},[541,2496,2497],{"class":632},"    \u002F\u002F Stripe answers 400 to values it refuses, such as an invalid email: the caller can change them.\n",[541,2499,2500,2502,2504,2506,2508,2511,2513,2515,2517,2519,2521,2523,2525,2527,2529,2532,2534,2536],{"class":543,"line":1218},[541,2501,1856],{"class":547},[541,2503,933],{"class":551},[541,2505,1237],{"class":558},[541,2507,1124],{"class":547},[541,2509,2510],{"class":670}," 400",[541,2512,1114],{"class":547},[541,2514,2015],{"class":558},[541,2516,489],{"class":551},[541,2518,2403],{"class":558},[541,2520,1124],{"class":547},[541,2522,2408],{"class":622},[541,2524,1870],{"class":551},[541,2526,2149],{"class":547},[541,2528,1919],{"class":547},[541,2530,2531],{"class":670}," QueryRejected",[541,2533,784],{"class":551},[541,2535,2068],{"class":558},[541,2537,1052],{"class":551},[541,2539,2540,2542,2544,2546,2548,2550],{"class":543,"line":1223},[541,2541,1933],{"class":547},[541,2543,1919],{"class":547},[541,2545,1111],{"class":670},[541,2547,784],{"class":551},[541,2549,2068],{"class":558},[541,2551,1052],{"class":551},[541,2553,2554],{"class":543,"line":1271},[541,2555,1946],{"class":551},[1276,2557,2558,2590],{},[1279,2559,2560,2565,2566,2569,2570,2573,2574,2576,2577,2580,2581,2583,2584,2587,2588,489],{},[463,2561,2562,2564],{},[468,2563,1792],{}," tells a missing object from a missing reference."," Stripe names ",[468,2567,2568],{},"id"," when the object in the path doesn't exist, and the parameter when a written value refers to a missing object (e.g., a new price's ",[468,2571,2572],{},"product","). The client maps the second to ",[468,2575,1683],{},", and the first to ",[468,2578,2579],{},"StripeObjectMissing",", its own subclass of ",[468,2582,470],{},". A keyed write that races a concurrent delete catches it and answers ",[468,2585,2586],{},"record: null","; anywhere else it reaches Engine as a plain ",[468,2589,470],{},[1279,2591,2592,2595,2596,2598,2599,2601,2602,2605,2606,1309,2608,2610,2611,489],{},[463,2593,2594],{},"Some failures only mean something for one operation."," Stripe refuses to delete a product that still has prices with a ",[468,2597,1546],{}," that has no code. The client narrows it to ",[468,2600,1683],{}," only for a ",[468,2603,2604],{},"DELETE"," whose message names prices. Every other ",[468,2607,1546],{},[468,2609,1558],{}," is a ",[468,2612,1552],{},[459,2614,2615,2616,2619,2620,2623,2624,489],{},"Unit tests that import your sources can check the class with ",[468,2617,2618],{},"instanceof",", because they share your copy of the kit. Tests of the built file read ",[468,2621,2622],{},"monospace.code"," instead. See ",[480,2625,2627],{"href":2626},"#_7-check-what-callers-see","step 7",[459,2629,2630,2632],{},[468,2631,1203],{}," stays on the JavaScript error, where Engine looks only for a permission denial. Engine receives only the code and the message, so put anything the caller needs in the message.",[2634,2635,1597],"h3",{"id":2636},"answer-keyed-misses-with-record-null",[459,2638,2639,2640,796,2642,2644,2645,2647,2648,2650,2651,2653,2654,2657,2658,1309,2661,489],{},"For ",[468,2641,1573],{},[468,2643,1576],{},", and ",[468,2646,1580],{},", answer ",[468,2649,1584],{}," when the key or the guard matches nothing, and write nothing. A miss isn't an error, so don't throw. Engine reports \"No record found\" to the caller with a ",[468,2652,1566],{},". Don't answer ",[468,2655,2656],{},"{ records: [] }",": Engine fails a keyed operation that answers ",[468,2659,2660],{},"records",[468,2662,1381],{},[459,2664,2665,2666,1347,2669,2671,2672,2674,2675,2677,2678,2681,2682,2684,2685,2688,2689,2691,2692,2694,2695,1846],{},"The Stripe client's ",[468,2667,2668],{},"retrieve",[468,2670,1350],{}," for a ",[468,2673,1566],{}," whose body carries Stripe's ",[468,2676,499],{}," code, and for a deleted object that Stripe still returns with ",[468,2679,2680],{},"deleted: true",". An id Stripe can't have answers ",[468,2683,1350],{}," without a request. The connector's ",[468,2686,2687],{},"findByKey"," also answers ",[468,2690,1350],{}," for a guard miss, and each keyed operation answers ",[468,2693,1350],{}," as ",[468,2696,2586],{},[531,2698,2700],{"className":533,"code":2699,"filename":535,"language":536,"meta":537,"style":537},"    \u002F** `undefined` when the object doesn't exist or was deleted. *\u002F\n    async retrieve(path: string, id: string): Promise\u003CStripeObject | undefined> {\n        \u002F\u002F An id Stripe can't have names no object: answer without a request.\n        if (!STRIPE_ID.test(id)) return undefined;\n\n        const response = await this.#send('GET', `${path}\u002F${id}`);\n        \u002F\u002F Only Stripe's own answer means the object doesn't exist. Any other 404, such as a proxy's\n        \u002F\u002F HTML page, is an error.\n        if (response.status === 404 && readError(response.body).code === 'resource_missing') return undefined;\n\n        const body = checked(response, 'GET', path);\n        \u002F\u002F Deleted customers and products stay retrievable, as tombstones.\n        if (isObject(body) && body.deleted === true) return undefined;\n\n        const object = parseObject(body);\n        if (object.id !== id) throw new Error(`Stripe answered a request for \\`${id}\\` with another object.`);\n        return object;\n    }\n",[468,2701,2702,2707,2748,2753,2765,2769,2808,2813,2818,2831,2835,2859,2864,2880,2884,2902,2939,2944],{"__ignoreMap":537},[541,2703,2704],{"class":543,"line":544},[541,2705,2706],{"class":632},"    \u002F** `undefined` when the object doesn't exist or was deleted. *\u002F\n",[541,2708,2709,2711,2714,2716,2718,2720,2722,2724,2726,2728,2730,2732,2734,2736,2739,2742,2744,2746],{"class":543,"line":555},[541,2710,775],{"class":558},[541,2712,2713],{"class":670}," retrieve",[541,2715,784],{"class":551},[541,2717,799],{"class":558},[541,2719,790],{"class":551},[541,2721,804],{"class":558},[541,2723,796],{"class":551},[541,2725,2568],{"class":558},[541,2727,790],{"class":551},[541,2729,804],{"class":558},[541,2731,818],{"class":551},[541,2733,822],{"class":821},[541,2735,825],{"class":547},[541,2737,2738],{"class":558},"StripeObject",[541,2740,2741],{"class":547}," |",[541,2743,941],{"class":670},[541,2745,831],{"class":547},[541,2747,552],{"class":551},[541,2749,2750],{"class":543,"line":565},[541,2751,2752],{"class":632},"        \u002F\u002F An id Stripe can't have names no object: answer without a request.\n",[541,2754,2755,2757,2760,2762],{"class":543,"line":573},[541,2756,930],{"class":670},[541,2758,2759],{"class":551}," (!STRIPE_ID.test(",[541,2761,2568],{"class":547},[541,2763,2764],{"class":551},")) return undefined;\n",[541,2766,2767],{"class":543,"line":581},[541,2768,661],{"emptyLinePlaceholder":660},[541,2770,2771,2774,2776,2778,2780,2783,2785,2787,2789,2792,2794,2797,2799,2802,2804,2806],{"class":543,"line":589},[541,2772,2773],{"class":551},"        const ",[541,2775,1232],{"class":558},[541,2777,674],{"class":547},[541,2779,1023],{"class":547},[541,2781,2782],{"class":670}," this",[541,2784,489],{"class":551},[541,2786,506],{"class":670},[541,2788,784],{"class":551},[541,2790,2791],{"class":622},"'GET'",[541,2793,796],{"class":551},[541,2795,2796],{"class":622},"`${",[541,2798,799],{"class":558},[541,2800,2801],{"class":622},"}\u002F${",[541,2803,2568],{"class":558},[541,2805,868],{"class":622},[541,2807,1052],{"class":551},[541,2809,2810],{"class":543,"line":597},[541,2811,2812],{"class":632},"        \u002F\u002F Only Stripe's own answer means the object doesn't exist. Any other 404, such as a proxy's\n",[541,2814,2815],{"class":543,"line":605},[541,2816,2817],{"class":632},"        \u002F\u002F HTML page, is an error.\n",[541,2819,2820,2822,2825,2828],{"class":543,"line":613},[541,2821,930],{"class":670},[541,2823,2824],{"class":551}," (response.status === 404 && ",[541,2826,2827],{"class":547},"readError",[541,2829,2830],{"class":551},"(response.body).code === 'resource_missing') return undefined;\n",[541,2832,2833],{"class":543,"line":629},[541,2834,661],{"emptyLinePlaceholder":660},[541,2836,2837,2839,2841,2843,2845,2847,2849,2851,2853,2855,2857],{"class":543,"line":636},[541,2838,2773],{"class":551},[541,2840,1908],{"class":558},[541,2842,674],{"class":547},[541,2844,1816],{"class":670},[541,2846,784],{"class":551},[541,2848,1232],{"class":558},[541,2850,796],{"class":551},[541,2852,2791],{"class":622},[541,2854,796],{"class":551},[541,2856,799],{"class":558},[541,2858,1052],{"class":551},[541,2860,2861],{"class":543,"line":657},[541,2862,2863],{"class":632},"        \u002F\u002F Deleted customers and products stay retrievable, as tombstones.\n",[541,2865,2866,2868,2870,2873,2875,2877],{"class":543,"line":664},[541,2867,930],{"class":670},[541,2869,933],{"class":551},[541,2871,2872],{"class":547},"isObject",[541,2874,784],{"class":551},[541,2876,1908],{"class":547},[541,2878,2879],{"class":551},") && body.deleted === true) return undefined;\n",[541,2881,2882],{"class":543,"line":682},[541,2883,661],{"emptyLinePlaceholder":660},[541,2885,2886,2888,2891,2893,2896,2898,2900],{"class":543,"line":688},[541,2887,2773],{"class":551},[541,2889,2890],{"class":558},"object",[541,2892,674],{"class":547},[541,2894,2895],{"class":670}," parseObject",[541,2897,784],{"class":551},[541,2899,1908],{"class":558},[541,2901,1052],{"class":551},[541,2903,2904,2906,2909,2911,2914,2916,2918,2921,2924,2927,2929,2932,2934,2937],{"class":543,"line":703},[541,2905,930],{"class":670},[541,2907,2908],{"class":551}," (object.id !== ",[541,2910,2568],{"class":547},[541,2912,2913],{"class":551},") throw new ",[541,2915,470],{"class":670},[541,2917,784],{"class":551},[541,2919,2920],{"class":622},"`Stripe answered a request for ",[541,2922,2923],{"class":670},"\\`",[541,2925,2926],{"class":622},"${",[541,2928,2568],{"class":558},[541,2930,2931],{"class":622},"}",[541,2933,2923],{"class":670},[541,2935,2936],{"class":622}," with another object.`",[541,2938,1052],{"class":551},[541,2940,2941],{"class":543,"line":709},[541,2942,2943],{"class":551},"        return object;\n",[541,2945,2946],{"class":543,"line":724},[541,2947,1274],{"class":551},[459,2949,2950,2951,2954,2955,2957,2958,2960,2961,2963,2964,2966,2967,2969],{},"A filter that names an item that doesn't exist is also no match, not an error. Stripe answers ",[468,2952,2953],{},"prices?product="," with a missing product as ",[468,2956,499],{}," naming the ",[468,2959,2572],{}," parameter, and the connector returns no prices for it. It does this only on the first page, and only when ",[468,2962,1792],{}," names one of the request's own list parameters; the same error on a later page throws ",[468,2965,2579],{},", a plain ",[468,2968,470],{}," to Engine.",[459,2971,2972,2973,489],{},"In the Stripe connector, the guard is a read before the write, not a conditional write. Another writer can change the item between the two requests. See ",[480,2974,2976],{"href":2975},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Flimitations#bulk-writes-are-only-as-atomic-as-the-external-data-source","Bulk Writes Are Only as Atomic as the External Data Source",[459,2978,2979,2980,2982],{},"When it works, a guard miss over REST answers ",[468,2981,1566],{}," with \"No record found\", and nothing is written.",[2634,2984,2986],{"id":2985},"retry-only-what-is-safe-to-repeat","Retry Only What Is Safe to Repeat",[459,2988,2989,2990,2992],{},"Engine doesn't retry a failed connector call. A ",[468,2991,1377],{}," from the external data source fails the caller's request unless your connector retries.",[1276,2994,2995,3001,3015],{},[1279,2996,2997,3000],{},[463,2998,2999],{},"Reads:"," retry a small, bounded number of times, and never wait longer than the caller will.",[1279,3002,3003,3006,3007,3010,3011,489],{},[463,3004,3005],{},"Writes:"," retry only when the external data source supports idempotency keys. Generate one key per item, and reuse it on every retry. A failed write can leave earlier writes applied, and Engine rolls nothing back.",[3008,3009],"br",{},"The Stripe connector creates items one after another, and runs updates and deletes by filter with at most four in flight. After the first failure it starts no new write and lets the writes in flight finish. Then it says how far it got: \"Deleted 3 of 4 products before this error: …\". See ",[480,3012,3014],{"href":3013},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Flimitations#no-transactions","No Transactions",[1279,3016,3017,3020,3021,3023,3024,3026,3027,3030,3031,3033],{},[463,3018,3019],{},"Everything else:"," throw ",[468,3022,1506],{},", and put the wait into the message. The message is all the caller gets: Engine answers ",[468,3025,1506],{}," without a ",[468,3028,3029],{},"Retry-After"," header. Both example connectors append \"Retry after N seconds.\" when ",[468,3032,3029],{}," is a whole number of seconds, and nothing for an HTTP-date.",[459,3035,1730,3036,489],{},[480,3037,3039],{"href":3038},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Flimitations#no-automatic-retries","No Automatic Retries",[455,3041,3043],{"id":3042},"_3-add-a-timeout-to-every-request","3. Add a Timeout to Every Request",[459,3045,3046,3047,796,3050,1657,3053,3056],{},"Engine sets no deadline on ",[468,3048,3049],{},"query",[468,3051,3052],{},"introspect",[468,3054,3055],{},"testConnection",". An external data source that never answers holds the caller's request open until the caller gives up. Studio's connection test, for example, stops waiting after 60 seconds.",[459,3058,3059,3060,3063,3064,3066,3067,3069,3070,3072,3073,3075,3076,3079,3080,3082,3083,3085],{},"Pass ",[468,3061,3062],{},"signal: AbortSignal.timeout(...)"," in the ",[468,3065,510],{}," options, as the Stripe client's ",[468,3068,506],{}," in step 1 does with ",[468,3071,916],{}," (10 seconds). When the deadline passes before the response arrives, ",[468,3074,510],{}," rejects with an error named ",[468,3077,3078],{},"TimeoutError",", and the ",[468,3081,1476],{}," throws ",[468,3084,1187],{},": \"The Stripe API did not answer within 10 seconds.\"",[1276,3087,3088,3114,3120],{},[1279,3089,3090,3093,3094,1521,3097,3100,3101,2694,3103,3105,3106,3108,3109,3082,3111,3113],{},[463,3091,3092],{},"The signal also covers the body."," If the deadline passes while you read the body (",[468,3095,3096],{},"response.text()",[468,3098,3099],{},"response.json()","), the read rejects. Read the body inside the same ",[468,3102,514],{},[468,3104,510],{},", as ",[468,3107,506],{}," does, or the abort escapes as an unbranded error. The ",[468,3110,1476],{},[468,3112,1187],{}," for any rejection, so a body-read timeout fails the same way.",[1279,3115,3116,3119],{},[463,3117,3118],{},"The deadline works in the sandbox."," Built with a 1 ms deadline, the Stripe connector fails the connection test with \"The Stripe API did not answer within 0.001 seconds.\"",[1279,3121,3122,3125,3126,3128],{},[463,3123,3124],{},"A per-call deadline doesn't bound the operation."," A read that pages through the external data source makes several calls, each with its own timeout, so it can run for several deadlines. To bound the whole operation, also cap its calls (e.g., with a read budget, as in ",[480,3127,1734],{"href":1733},"), or pass one signal to every call it makes.",[459,3130,1730,3131,1577,3135,489],{},[480,3132,3134],{"href":3133},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Flimits#per-request-deadline","Per-Request Deadline",[480,3136,3138],{"href":3137},"\u002Fdeveloper\u002Fextensions\u002Fdata-connectors\u002Fruntime#http-through-fetch","HTTP Through fetch",[459,3140,3141],{},"When it works, pointing the connector at an external data source that doesn't answer fails the request with the connection error after your deadline instead of hanging.",[455,3143,3145],{"id":3144},"_4-refuse-requests-you-cant-serve","4. Refuse Requests You Can't Serve",[459,3147,3148,3149,3153],{},"Engine sends only shapes your ",[480,3150,3152],{"href":3151},"\u002Fdeveloper\u002Fextensions\u002Fdata-connectors#operations-contract","operations contract"," declares, plus a few it composes on its own. Some of those requests still can't be served by a given external data source (e.g., an offset past its paging depth). Refuse them with the class that names whose request it is:",[1276,3155,3156,3173,3189,3199,3205,3211],{},[1279,3157,3158,3162,3163,3166,3167,3170,3171,489],{},[463,3159,3160],{},[468,3161,1552],{}," for a valid request the external data source can't serve as asked: beyond a page window or a read budget, without a filter the external data source requires, or with a shape Engine composes on its own that the external data source can't express (e.g., ",[468,3164,3165],{},"not"," around an update's field rules, or ",[468,3168,3169],{},"or"," across permission rules). The caller can change the request, so the message says how. Engine answers it with ",[468,3172,1316],{},[1279,3174,3175,3179,3180,3184,3185,1309,3187,1778],{},[463,3176,1626,3177],{},[468,3178,470],{}," for a request outside your declaration. That's an unknown collection or namespace, an operation, a filter field or operator, a sort, a count, a key other than the declared one, or a write. Engine checks callers' requests against the data source's stored schema, but not the filters it sends. So an undeclared comparison can reach your connector from a permission rule's condition or an ",[480,3181,3183],{"href":3182},"\u002Fdeveloper\u002Fextensions\u002Fdata-connectors\u002Fdata-sources#update-data-sources-after-an-extension-update","extension update"," that narrowed the declaration without a schema refresh. Refuse it rather than drop it: a dropped condition can widen what the caller may touch. The kit has no class for it. Engine answers the plain ",[468,3186,470],{},[468,3188,1381],{},[1279,3190,3191,3194,3195,489],{},[463,3192,3193],{},"No error"," for Engine's relation and read-back lookups on a key no declaration can carry. Engine sends them with an undeclared operator on purpose, so serve them. See ",[480,3196,3198],{"href":3197},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Fapi#handle-requests-you-didnt-declare","Handle Requests You Didn't Declare",[1279,3200,3201,3204],{},[463,3202,3203],{},"Refuse as early as you can tell."," When the request's shape decides it (an unsupported filter or sort), refuse before any request to the external data source. When only the external data source knows (how many items match), make one bounded probe, such as a count, and refuse before reading data. A read budget that runs out refuses mid-read.",[1279,3206,3207,3210],{},[463,3208,3209],{},"Write the message for the caller."," It's the only explanation they get. Name the limit and what to change.",[1279,3212,3213,3216,3217,3220],{},[463,3214,3215],{},"Never truncate instead."," Returning fewer items than the ",[468,3218,3219],{},"limit"," tells Engine there are no more items.",[459,3222,3223,3224,3227],{},"The Art Institute of Chicago search API stops at its 1,000th result. The Art Institute connector from ",[480,3225,246],{"href":3226},"\u002Fdeveloper\u002Fextensions\u002Fdata-connectors\u002Fdata-source-mapping"," translates the filter and the sort first, so an undeclared one is refused before any request. It then counts when a read reaches past result 1,000, and rejects the read only if matches exist beyond it:",[524,3229,3230],{},[527,3231,3233],{"label":3232},"src\u002Fdata-connectors\u002Fartic\u002Fsearch.ts (readMany)",[531,3234,3237],{"className":533,"code":3235,"filename":3236,"language":536,"meta":537,"style":537},"import type {\n    DeclaredOperator,\n    FilterContract,\n    FilterSerialized,\n    LogicalOperator,\n    MonospaceObject,\n    PrimitiveFieldType,\n    QueryResultFor,\n    QueryResultSerialized,\n    ReadArguments,\n    ReadManyOperationSerialized,\n    ReadOneOperationSerialized,\n    SortItemSerialized,\n    Value,\n} from '@monospace\u002Fextension-kit\u002Fdata-connector';\nimport { ConnectionFailed, QueryRejected, RateLimited } from '@monospace\u002Fextension-kit\u002Fdata-connector';\n\u002F\u002F …\nconst BASE_URL = 'https:\u002F\u002Fapi.artic.edu\u002Fapi\u002Fv1';\n\u002F** Engine sets no deadline on `query` or `testConnection`, so every request carries its own. *\u002F\nconst TIMEOUT_MS = 10_000;\n\u002F** The search API returns at most 100 items per request... *\u002F\nconst PAGE_SIZE = 100;\n\u002F** ...and refuses any request that reaches past its 1,000th result. *\u002F\nconst SEARCH_WINDOW = 1000;\n\n\u002F** A request body for the search API, which takes Elasticsearch syntax. *\u002F\ntype SearchBody = Record\u003Cstring, unknown>;\n\u002F\u002F …\n\u002F** Answers `readMany`. The response field is `records`; each entry is one item. *\u002F\nexport async function readMany(query: ReadManyOperationSerialized): Promise\u003CQueryResultSerialized> {\n    const { read, types } = declaration(query.collection);\n    \u002F\u002F Translate everything first, so an undeclared filter or sort is refused before any request.\n    const base: SearchBody = query.filter === undefined ? {} : { query: toQuery(query.filter, read.filter) };\n    const sort = toSort(query.sort ?? [], read.sort);\n\n    \u002F\u002F A count read answers the number of matches and ignores `limit` and `offset`.\n    if (query.count === true) return { totalCount: await count(query.collection, base) };\n\n    const offset = query.offset ?? 0;\n    let end = query.limit === undefined ? Infinity : offset + query.limit;\n\n    \u002F\u002F The search API refuses any request past its 1,000th result. A read that reaches past it is\n    \u002F\u002F still safe when fewer items match, so count the matches first and stop at the last one.\n    if (end > SEARCH_WINDOW) {\n        end = Math.min(end, await count(query.collection, base));\n\n        if (end > SEARCH_WINDOW) {\n            \u002F\u002F A valid read the API can't serve: the caller can change it, so the message says how.\n            throw new QueryRejected(`The Art Institute search API returns only the first ${SEARCH_WINDOW} results. Narrow the filter or read an earlier page.`);\n        }\n    }\n","src\u002Fdata-connectors\u002Fartic\u002Fsearch.ts",[468,3238,3239,3248,3255,3262,3269,3276,3283,3290,3297,3304,3311,3318,3325,3332,3339,3349,3373,3378,3391,3395,3407,3412,3424,3429,3442,3446,3451,3475,3479,3484,3521,3553,3558,3614,3648,3652,3657,3703,3707,3730,3770,3774,3779,3784,3800,3840,3844,3858,3864,3887,3892],{"__ignoreMap":537},[541,3240,3241,3243,3246],{"class":543,"line":544},[541,3242,548],{"class":547},[541,3244,3245],{"class":547}," type",[541,3247,552],{"class":551},[541,3249,3250,3253],{"class":543,"line":555},[541,3251,3252],{"class":558},"    DeclaredOperator",[541,3254,562],{"class":551},[541,3256,3257,3260],{"class":543,"line":565},[541,3258,3259],{"class":558},"    FilterContract",[541,3261,562],{"class":551},[541,3263,3264,3267],{"class":543,"line":573},[541,3265,3266],{"class":558},"    FilterSerialized",[541,3268,562],{"class":551},[541,3270,3271,3274],{"class":543,"line":581},[541,3272,3273],{"class":558},"    LogicalOperator",[541,3275,562],{"class":551},[541,3277,3278,3281],{"class":543,"line":589},[541,3279,3280],{"class":558},"    MonospaceObject",[541,3282,562],{"class":551},[541,3284,3285,3288],{"class":543,"line":597},[541,3286,3287],{"class":558},"    PrimitiveFieldType",[541,3289,562],{"class":551},[541,3291,3292,3295],{"class":543,"line":605},[541,3293,3294],{"class":558},"    QueryResultFor",[541,3296,562],{"class":551},[541,3298,3299,3302],{"class":543,"line":613},[541,3300,3301],{"class":558},"    QueryResultSerialized",[541,3303,562],{"class":551},[541,3305,3306,3309],{"class":543,"line":629},[541,3307,3308],{"class":558},"    ReadArguments",[541,3310,562],{"class":551},[541,3312,3313,3316],{"class":543,"line":636},[541,3314,3315],{"class":558},"    ReadManyOperationSerialized",[541,3317,562],{"class":551},[541,3319,3320,3323],{"class":543,"line":657},[541,3321,3322],{"class":558},"    ReadOneOperationSerialized",[541,3324,562],{"class":551},[541,3326,3327,3330],{"class":543,"line":664},[541,3328,3329],{"class":558},"    SortItemSerialized",[541,3331,562],{"class":551},[541,3333,3334,3337],{"class":543,"line":682},[541,3335,3336],{"class":558},"    Value",[541,3338,562],{"class":551},[541,3340,3341,3343,3345,3347],{"class":543,"line":688},[541,3342,616],{"class":551},[541,3344,619],{"class":547},[541,3346,623],{"class":622},[541,3348,626],{"class":551},[541,3350,3351,3353,3355,3357,3359,3361,3363,3365,3367,3369,3371],{"class":543,"line":703},[541,3352,548],{"class":547},[541,3354,641],{"class":551},[541,3356,1187],{"class":558},[541,3358,796],{"class":551},[541,3360,1552],{"class":558},[541,3362,796],{"class":551},[541,3364,1506],{"class":558},[541,3366,647],{"class":551},[541,3368,619],{"class":547},[541,3370,623],{"class":622},[541,3372,626],{"class":551},[541,3374,3375],{"class":543,"line":709},[541,3376,3377],{"class":632},"\u002F\u002F …\n",[541,3379,3380,3382,3384,3386,3389],{"class":543,"line":724},[541,3381,667],{"class":547},[541,3383,671],{"class":670},[541,3385,674],{"class":547},[541,3387,3388],{"class":622}," 'https:\u002F\u002Fapi.artic.edu\u002Fapi\u002Fv1'",[541,3390,626],{"class":551},[541,3392,3393],{"class":543,"line":730},[541,3394,685],{"class":632},[541,3396,3397,3399,3401,3403,3405],{"class":543,"line":760},[541,3398,667],{"class":547},[541,3400,693],{"class":670},[541,3402,674],{"class":547},[541,3404,698],{"class":670},[541,3406,626],{"class":551},[541,3408,3409],{"class":543,"line":766},[541,3410,3411],{"class":632},"\u002F** The search API returns at most 100 items per request... *\u002F\n",[541,3413,3414,3416,3418,3420,3422],{"class":543,"line":772},[541,3415,667],{"class":547},[541,3417,714],{"class":670},[541,3419,674],{"class":547},[541,3421,719],{"class":670},[541,3423,626],{"class":551},[541,3425,3426],{"class":543,"line":836},[541,3427,3428],{"class":632},"\u002F** ...and refuses any request that reaches past its 1,000th result. *\u002F\n",[541,3430,3431,3433,3436,3438,3440],{"class":543,"line":883},[541,3432,667],{"class":547},[541,3434,3435],{"class":670}," SEARCH_WINDOW",[541,3437,674],{"class":547},[541,3439,1155],{"class":670},[541,3441,626],{"class":551},[541,3443,3444],{"class":543,"line":922},[541,3445,661],{"emptyLinePlaceholder":660},[541,3447,3448],{"class":543,"line":927},[541,3449,3450],{"class":632},"\u002F** A request body for the search API, which takes Elasticsearch syntax. *\u002F\n",[541,3452,3453,3455,3458,3460,3463,3465,3467,3469,3472],{"class":543,"line":947},[541,3454,2403],{"class":547},[541,3456,3457],{"class":670}," SearchBody",[541,3459,674],{"class":547},[541,3461,3462],{"class":670}," Record",[541,3464,825],{"class":551},[541,3466,804],{"class":821},[541,3468,796],{"class":551},[541,3470,3471],{"class":821},"unknown",[541,3473,3474],{"class":551},">;\n",[541,3476,3477],{"class":543,"line":959},[541,3478,3377],{"class":632},[541,3480,3481],{"class":543,"line":965},[541,3482,3483],{"class":632},"\u002F** Answers `readMany`. The response field is `records`; each entry is one item. *\u002F\n",[541,3485,3486,3489,3492,3495,3498,3500,3503,3506,3508,3510,3513,3515,3518],{"class":543,"line":971},[541,3487,3488],{"class":547},"export",[541,3490,3491],{"class":547}," async",[541,3493,3494],{"class":547}," function",[541,3496,3497],{"class":670}," readMany",[541,3499,784],{"class":551},[541,3501,3502],{"class":547},"query:",[541,3504,3505],{"class":670}," ReadManyOperationSerialized",[541,3507,1843],{"class":551},[541,3509,1846],{"class":547},[541,3511,3512],{"class":670}," Promise",[541,3514,825],{"class":551},[541,3516,3517],{"class":670},"QueryResultSerialized",[541,3519,3520],{"class":551},"> {\n",[541,3522,3523,3525,3527,3530,3532,3535,3537,3539,3542,3544,3546,3548,3551],{"class":543,"line":976},[541,3524,2012],{"class":547},[541,3526,641],{"class":551},[541,3528,3529],{"class":670},"read",[541,3531,796],{"class":551},[541,3533,3534],{"class":670},"types",[541,3536,647],{"class":551},[541,3538,2043],{"class":547},[541,3540,3541],{"class":670}," declaration",[541,3543,784],{"class":551},[541,3545,3049],{"class":558},[541,3547,489],{"class":551},[541,3549,3550],{"class":558},"collection",[541,3552,1052],{"class":551},[541,3554,3555],{"class":543,"line":987},[541,3556,3557],{"class":632},"    \u002F\u002F Translate everything first, so an undeclared filter or sort is refused before any request.\n",[541,3559,3560,3562,3565,3567,3569,3571,3574,3576,3579,3581,3583,3585,3588,3590,3593,3596,3598,3600,3602,3604,3606,3608,3610,3612],{"class":543,"line":1002},[541,3561,2012],{"class":547},[541,3563,3564],{"class":670}," base",[541,3566,1846],{"class":547},[541,3568,3457],{"class":670},[541,3570,674],{"class":547},[541,3572,3573],{"class":558}," query",[541,3575,489],{"class":551},[541,3577,3578],{"class":558},"filter",[541,3580,1124],{"class":547},[541,3582,941],{"class":670},[541,3584,1145],{"class":547},[541,3586,3587],{"class":551}," {} ",[541,3589,1846],{"class":547},[541,3591,3592],{"class":551}," { query: ",[541,3594,3595],{"class":670},"toQuery",[541,3597,784],{"class":551},[541,3599,3049],{"class":558},[541,3601,489],{"class":551},[541,3603,3578],{"class":558},[541,3605,796],{"class":551},[541,3607,3529],{"class":558},[541,3609,489],{"class":551},[541,3611,3578],{"class":558},[541,3613,919],{"class":551},[541,3615,3616,3618,3621,3623,3626,3628,3630,3632,3635,3637,3640,3642,3644,3646],{"class":543,"line":1007},[541,3617,2012],{"class":547},[541,3619,3620],{"class":670}," sort",[541,3622,674],{"class":547},[541,3624,3625],{"class":670}," toSort",[541,3627,784],{"class":551},[541,3629,3049],{"class":558},[541,3631,489],{"class":551},[541,3633,3634],{"class":558},"sort",[541,3636,2071],{"class":547},[541,3638,3639],{"class":551}," [], ",[541,3641,3529],{"class":558},[541,3643,489],{"class":551},[541,3645,3634],{"class":558},[541,3647,1052],{"class":551},[541,3649,3650],{"class":543,"line":1015},[541,3651,661],{"emptyLinePlaceholder":660},[541,3653,3654],{"class":543,"line":1055},[541,3655,3656],{"class":632},"    \u002F\u002F A count read answers the number of matches and ignores `limit` and `offset`.\n",[541,3658,3659,3661,3663,3665,3667,3670,3672,3675,3677,3679,3682,3685,3688,3690,3692,3694,3696,3698,3701],{"class":543,"line":1076},[541,3660,1856],{"class":547},[541,3662,933],{"class":551},[541,3664,3049],{"class":558},[541,3666,489],{"class":551},[541,3668,3669],{"class":558},"count",[541,3671,1124],{"class":547},[541,3673,3674],{"class":670}," true",[541,3676,1870],{"class":551},[541,3678,2149],{"class":547},[541,3680,3681],{"class":551}," { totalCount: ",[541,3683,3684],{"class":547},"await",[541,3686,3687],{"class":670}," count",[541,3689,784],{"class":551},[541,3691,3049],{"class":558},[541,3693,489],{"class":551},[541,3695,3550],{"class":558},[541,3697,796],{"class":551},[541,3699,3700],{"class":558},"base",[541,3702,919],{"class":551},[541,3704,3705],{"class":543,"line":1081},[541,3706,661],{"emptyLinePlaceholder":660},[541,3708,3709,3711,3714,3716,3718,3720,3723,3725,3728],{"class":543,"line":1094},[541,3710,2012],{"class":547},[541,3712,3713],{"class":670}," offset",[541,3715,674],{"class":547},[541,3717,3573],{"class":558},[541,3719,489],{"class":551},[541,3721,3722],{"class":558},"offset",[541,3724,2071],{"class":547},[541,3726,3727],{"class":670}," 0",[541,3729,626],{"class":551},[541,3731,3732,3735,3738,3740,3742,3744,3746,3748,3750,3752,3755,3757,3759,3762,3764,3766,3768],{"class":543,"line":1132},[541,3733,3734],{"class":547},"    let",[541,3736,3737],{"class":558}," end",[541,3739,674],{"class":547},[541,3741,3573],{"class":558},[541,3743,489],{"class":551},[541,3745,3219],{"class":558},[541,3747,1124],{"class":547},[541,3749,941],{"class":670},[541,3751,1145],{"class":547},[541,3753,3754],{"class":670}," Infinity",[541,3756,1161],{"class":547},[541,3758,3713],{"class":558},[541,3760,3761],{"class":547}," +",[541,3763,3573],{"class":558},[541,3765,489],{"class":551},[541,3767,3219],{"class":558},[541,3769,626],{"class":551},[541,3771,3772],{"class":543,"line":1169},[541,3773,661],{"emptyLinePlaceholder":660},[541,3775,3776],{"class":543,"line":1175},[541,3777,3778],{"class":632},"    \u002F\u002F The search API refuses any request past its 1,000th result. A read that reaches past it is\n",[541,3780,3781],{"class":543,"line":1181},[541,3782,3783],{"class":632},"    \u002F\u002F still safe when fewer items match, so count the matches first and stop at the last one.\n",[541,3785,3786,3788,3790,3793,3796,3798],{"class":543,"line":1213},[541,3787,1856],{"class":547},[541,3789,933],{"class":551},[541,3791,3792],{"class":558},"end",[541,3794,3795],{"class":547}," >",[541,3797,3435],{"class":670},[541,3799,944],{"class":551},[541,3801,3802,3805,3807,3810,3812,3815,3817,3819,3821,3823,3825,3827,3829,3831,3833,3835,3837],{"class":543,"line":1218},[541,3803,3804],{"class":558},"        end",[541,3806,674],{"class":547},[541,3808,3809],{"class":558}," Math",[541,3811,489],{"class":551},[541,3813,3814],{"class":670},"min",[541,3816,784],{"class":551},[541,3818,3792],{"class":558},[541,3820,796],{"class":551},[541,3822,3684],{"class":547},[541,3824,3687],{"class":670},[541,3826,784],{"class":551},[541,3828,3049],{"class":558},[541,3830,489],{"class":551},[541,3832,3550],{"class":558},[541,3834,796],{"class":551},[541,3836,3700],{"class":558},[541,3838,3839],{"class":551},"));\n",[541,3841,3842],{"class":543,"line":1223},[541,3843,661],{"emptyLinePlaceholder":660},[541,3845,3846,3848,3850,3852,3854,3856],{"class":543,"line":1271},[541,3847,930],{"class":547},[541,3849,933],{"class":551},[541,3851,3792],{"class":558},[541,3853,3795],{"class":547},[541,3855,3435],{"class":670},[541,3857,944],{"class":551},[541,3859,3861],{"class":543,"line":3860},48,[541,3862,3863],{"class":632},"            \u002F\u002F A valid read the API can't serve: the caller can change it, so the message says how.\n",[541,3865,3867,3870,3872,3874,3876,3879,3882,3885],{"class":543,"line":3866},49,[541,3868,3869],{"class":547},"            throw",[541,3871,1919],{"class":547},[541,3873,2531],{"class":670},[541,3875,784],{"class":551},[541,3877,3878],{"class":622},"`The Art Institute search API returns only the first ${",[541,3880,3881],{"class":670},"SEARCH_WINDOW",[541,3883,3884],{"class":622},"} results. Narrow the filter or read an earlier page.`",[541,3886,1052],{"class":551},[541,3888,3890],{"class":543,"line":3889},50,[541,3891,968],{"class":551},[541,3893,3895],{"class":543,"line":3894},51,[541,3896,1274],{"class":551},[459,3898,3899,3900,3902,3903,3905,3906,3909,3910,3912],{},"The Stripe connector throws ",[468,3901,1552],{}," when a filter plans more than 250 Stripe requests (a list request can still take several pages; top-level ",[468,3904,2568],{}," constraints are retrieved instead and fall under the budget), when a read runs past its ",[468,3907,3908],{},"maxItemsPerQuery"," budget, and when a write reaches a data source with writes turned off. The fan-out bound and the writes switch are decided before any request. So is the budget for an ",[468,3911,2568],{}," filter, whose retrieves spend it up front. A list read spends it as items arrive, so there the budget runs out during the read.",[459,3914,3915,3916,1521,3918,3920],{},"Don't use ",[468,3917,1758],{},[468,3919,1530],{}," for these. Engine reports both as \"this data source's configuration is invalid\", which sends the caller looking in the wrong place.",[459,3922,3923,3924,1347,3926,3928],{},"Through Engine, a ",[468,3925,1552],{},[468,3927,1316],{}," with \"The data source rejected the query\" and your message nested inside:",[531,3930,3935],{"className":3931,"code":3932,"filename":3933,"language":3934,"meta":537,"style":537},"language-json shiki shiki-themes monospace-light monospace-light monospace-dark","{\n  \"message\": \"Query failed\",\n  \"source\": {\n    \"message\": \"The data source rejected the query\",\n    \"source\": {\n      \"message\": \"the extension returned an error: The Art Institute search API returns only the first 1000 results. Narrow the filter or read an earlier page.\"\n    }\n  }\n}\n","response.json","json",[468,3936,3937,3942,3954,3962,3974,3981,3991,3995,4000],{"__ignoreMap":537},[541,3938,3939],{"class":543,"line":544},[541,3940,3941],{"class":551},"{\n",[541,3943,3944,3947,3949,3952],{"class":543,"line":555},[541,3945,3946],{"class":821},"  \"message\"",[541,3948,790],{"class":551},[541,3950,3951],{"class":622},"\"Query failed\"",[541,3953,562],{"class":551},[541,3955,3956,3959],{"class":543,"line":565},[541,3957,3958],{"class":821},"  \"source\"",[541,3960,3961],{"class":551},": {\n",[541,3963,3964,3967,3969,3972],{"class":543,"line":573},[541,3965,3966],{"class":821},"    \"message\"",[541,3968,790],{"class":551},[541,3970,3971],{"class":622},"\"The data source rejected the query\"",[541,3973,562],{"class":551},[541,3975,3976,3979],{"class":543,"line":581},[541,3977,3978],{"class":821},"    \"source\"",[541,3980,3961],{"class":551},[541,3982,3983,3986,3988],{"class":543,"line":589},[541,3984,3985],{"class":821},"      \"message\"",[541,3987,790],{"class":551},[541,3989,3990],{"class":622},"\"the extension returned an error: The Art Institute search API returns only the first 1000 results. Narrow the filter or read an earlier page.\"\n",[541,3992,3993],{"class":543,"line":597},[541,3994,1274],{"class":551},[541,3996,3997],{"class":543,"line":605},[541,3998,3999],{"class":551},"  }\n",[541,4001,4002],{"class":543,"line":613},[541,4003,1946],{"class":551},[459,4005,4006],{},"When it works, the rejected request fails with your message. A shape-based refusal sends no request to the external data source, and a window refusal sends only its count request.",[455,4008,4010],{"id":4009},"_5-validate-configuration-in-setup","5. Validate Configuration in setup",[459,4012,4013,4014,4016,4017,4019,4020,4023,4024,4026],{},"Engine passes a data source's configuration to ",[468,4015,1764],{}," without checking it. Parse it there, once, and throw ",[468,4018,1758],{}," with a message that names the key and what it expects. The Stripe connector's parser starts like this; the full parser is ",[468,4021,4022],{},"src\u002Fdata-connectors\u002Fstripe\u002Fconfig.ts"," in the project that ",[480,4025,483],{"href":482}," offers as a download:",[531,4028,4030],{"className":533,"code":4029,"filename":4022,"language":536,"meta":537,"style":537},"import type { StripeClient } from '.\u002Fclient';\nimport { InvalidConfiguration } from '@monospace\u002Fextension-kit\u002Fdata-connector';\nimport { isObject } from '.\u002Fclient';\n\u002F\u002F …\n\u002F**\n * Nothing validates a connector's configuration before `setup` receives it, so this does. An\n * `InvalidConfiguration` from `setup` fails a connection test or a data source create with a 422.\n *\u002F\nexport function parseConfig(value: unknown): StripeConfig {\n    if (!isObject(value)) throw new InvalidConfiguration('Stripe configuration must be an object.');\n\n    const apiKey = value.apiKey;\n\n    if (typeof apiKey !== 'string' || !\u002F^(?:sk|rk)_(?:test|live)_\\w+$\u002Fu.test(apiKey)) {\n        throw new InvalidConfiguration('Stripe configuration `apiKey` must be a secret (`sk_`) or restricted (`rk_`) key.');\n    }\n",[468,4031,4032,4052,4068,4084,4088,4092,4097,4102,4106,4131,4163,4167,4186,4190,4248,4264],{"__ignoreMap":537},[541,4033,4034,4036,4038,4040,4043,4045,4047,4050],{"class":543,"line":544},[541,4035,548],{"class":547},[541,4037,3245],{"class":547},[541,4039,641],{"class":551},[541,4041,4042],{"class":558},"StripeClient",[541,4044,647],{"class":551},[541,4046,619],{"class":547},[541,4048,4049],{"class":622}," '.\u002Fclient'",[541,4051,626],{"class":551},[541,4053,4054,4056,4058,4060,4062,4064,4066],{"class":543,"line":555},[541,4055,548],{"class":547},[541,4057,641],{"class":551},[541,4059,1758],{"class":558},[541,4061,647],{"class":551},[541,4063,619],{"class":547},[541,4065,623],{"class":622},[541,4067,626],{"class":551},[541,4069,4070,4072,4074,4076,4078,4080,4082],{"class":543,"line":565},[541,4071,548],{"class":547},[541,4073,641],{"class":551},[541,4075,2872],{"class":558},[541,4077,647],{"class":551},[541,4079,619],{"class":547},[541,4081,4049],{"class":622},[541,4083,626],{"class":551},[541,4085,4086],{"class":543,"line":573},[541,4087,3377],{"class":632},[541,4089,4090],{"class":543,"line":581},[541,4091,1955],{"class":632},[541,4093,4094],{"class":543,"line":589},[541,4095,4096],{"class":632}," * Nothing validates a connector's configuration before `setup` receives it, so this does. An\n",[541,4098,4099],{"class":543,"line":597},[541,4100,4101],{"class":632}," * `InvalidConfiguration` from `setup` fails a connection test or a data source create with a 422.\n",[541,4103,4104],{"class":543,"line":605},[541,4105,1975],{"class":632},[541,4107,4108,4110,4112,4115,4117,4120,4122,4124,4126,4129],{"class":543,"line":613},[541,4109,3488],{"class":547},[541,4111,3494],{"class":547},[541,4113,4114],{"class":670}," parseConfig",[541,4116,784],{"class":551},[541,4118,4119],{"class":547},"value:",[541,4121,1849],{"class":821},[541,4123,1843],{"class":551},[541,4125,1846],{"class":547},[541,4127,4128],{"class":670}," StripeConfig",[541,4130,552],{"class":551},[541,4132,4133,4135,4137,4139,4141,4143,4146,4149,4151,4153,4156,4158,4161],{"class":543,"line":629},[541,4134,1856],{"class":547},[541,4136,933],{"class":551},[541,4138,1861],{"class":547},[541,4140,2872],{"class":670},[541,4142,784],{"class":551},[541,4144,4145],{"class":558},"value",[541,4147,4148],{"class":551},")) ",[541,4150,1873],{"class":547},[541,4152,1919],{"class":547},[541,4154,4155],{"class":670}," InvalidConfiguration",[541,4157,784],{"class":551},[541,4159,4160],{"class":622},"'Stripe configuration must be an object.'",[541,4162,1052],{"class":551},[541,4164,4165],{"class":543,"line":636},[541,4166,661],{"emptyLinePlaceholder":660},[541,4168,4169,4171,4174,4176,4179,4181,4184],{"class":543,"line":657},[541,4170,2012],{"class":547},[541,4172,4173],{"class":670}," apiKey",[541,4175,674],{"class":547},[541,4177,4178],{"class":558}," value",[541,4180,489],{"class":551},[541,4182,4183],{"class":558},"apiKey",[541,4185,626],{"class":551},[541,4187,4188],{"class":543,"line":664},[541,4189,661],{"emptyLinePlaceholder":660},[541,4191,4192,4194,4196,4199,4201,4203,4206,4208,4211,4213,4215,4218,4221,4224,4226,4229,4232,4234,4236,4238,4240,4242,4244,4246],{"class":543,"line":682},[541,4193,1856],{"class":547},[541,4195,933],{"class":551},[541,4197,4198],{"class":547},"typeof",[541,4200,4173],{"class":558},[541,4202,938],{"class":547},[541,4204,4205],{"class":622}," 'string'",[541,4207,2136],{"class":547},[541,4209,4210],{"class":547}," !",[541,4212,752],{"class":622},[541,4214,743],{"class":547},[541,4216,4217],{"class":622},"(?:sk",[541,4219,4220],{"class":547},"|",[541,4222,4223],{"class":622},"rk)_(?:test",[541,4225,4220],{"class":547},[541,4227,4228],{"class":622},"live)_",[541,4230,4231],{"class":670},"\\w",[541,4233,749],{"class":547},[541,4235,752],{"class":622},[541,4237,755],{"class":547},[541,4239,489],{"class":551},[541,4241,2429],{"class":670},[541,4243,784],{"class":551},[541,4245,4183],{"class":558},[541,4247,2436],{"class":551},[541,4249,4250,4253,4255,4257,4259,4262],{"class":543,"line":688},[541,4251,4252],{"class":547},"        throw",[541,4254,1919],{"class":547},[541,4256,4155],{"class":670},[541,4258,784],{"class":551},[541,4260,4261],{"class":622},"'Stripe configuration `apiKey` must be a secret (`sk_`) or restricted (`rk_`) key.'",[541,4263,1052],{"class":551},[541,4265,4266],{"class":543,"line":703},[541,4267,1274],{"class":551},[1276,4269,4270,4299,4308,4329,4351],{},[1279,4271,4272,4282,4283,4285,4286,4288,4289,4291,4292,4294,4295,489],{},[463,4273,4274,1521,4276,4278,4279,4281],{},[468,4275,1758],{},[468,4277,1530],{}," from ",[468,4280,1764],{}," stops the data source"," until the extension is reloaded, because a retry can't fix the configuration. So does a permission denial there, raw or kept as ",[468,4284,1203],{},". Another kit class, or a plain ",[468,4287,470],{},", thrown from ",[468,4290,1764],{}," fails the request with ",[468,4293,1316],{}," \"Invalid extension configuration\", and the next request tries again. See ",[480,4296,4298],{"href":4297},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Ferrors#know-what-deactivates-a-data-source","Know What Deactivates a Data Source",[1279,4300,4301,4304,4305,4307],{},[463,4302,4303],{},"Connection tests start fresh."," A test runs in a temporary connector, so after a test fails in ",[468,4306,1764],{},", fixing the configuration and testing again works without a reload.",[1279,4309,4310,4316,4317,4319,4320,4322,4323,1765,4325,489],{},[463,4311,4312,4315],{},[468,4313,4314],{},"preflight"," can refuse a configuration too."," It runs before ",[468,4318,1764],{}," to derive network permissions. When the configuration it needs is invalid (e.g., a base URL that isn't a URL), throw ",[468,4321,1758],{}," there. That stops the data source the same way. Full validation still belongs in ",[468,4324,1764],{},[480,4326,4328],{"href":4327},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Fpermissions#derive-permissions-in-preflight","Derive Permissions in preflight",[1279,4330,4331,1296,4336,4338,4339,4341,4342,4344,4345,4347,4348,4350],{},[463,4332,4333,4334,489],{},"Validate the format, then prove the credential in ",[468,4335,3055],{},[468,4337,1764],{}," can't tell a well-formed but wrong key from a real one without calling the external data source. The Stripe connector's ",[468,4340,3055],{}," makes one authenticated read, and its ",[468,4343,1520],{}," becomes ",[468,4346,1530],{},". Engine runs ",[468,4349,3055],{}," before it creates a data source or previews its schema, so a rejected credential fails both.",[1279,4352,4353,4362,4363,4365],{},[463,4354,4355,4356,4358,4359,489],{},"Throw from ",[468,4357,3055],{},", don't answer ",[468,4360,4361],{},"{ ok: false }"," Engine reports ",[468,4364,4361],{}," as a failed connection with a fixed explanation, so your reason is lost. A thrown kit error carries your message.",[459,4367,4368,4370,4371,1521,4373,4375,4376,4378],{},[468,4369,1530],{}," thrown from ",[468,4372,3055],{},[468,4374,3049],{},", like the Stripe client's ",[468,4377,1520],{}," mapping, fails only that request, reported as an invalid configuration, and leaves the data source running.",[459,4380,4381],{},"When it works, testing a data source with a malformed key fails with your message, and testing one with a well-formed but wrong key fails with the external data source's rejection.",[455,4383,4385],{"id":4384},"_6-keep-secrets-out-of-messages","6. Keep Secrets out of Messages",[459,4387,4388,4389,4391],{},"Only your error's code and message reach Engine; the ",[468,4390,1203],{}," (after the permission check), the stack and every other property are dropped. Engine maps the code to its own error, and the caller's HTTP response nests your message inside it. So the message is the one place a secret can leak, and the one place for context the caller needs.",[459,4393,4394,4395,4398,4399,4401,4402,4405,4406,4408,4409,4411],{},"Stripe's error bodies carry a ",[468,4396,4397],{},"request_log_url"," that contains the Stripe account id, and its ",[468,4400,1520],{}," message echoes the key's last characters (",[468,4403,4404],{},"sk_test_***********rong","). The Stripe client keeps both out: ",[468,4407,1786],{}," in step 2 builds every message from Stripe's ",[468,4410,2068],{}," field alone, or from the status and the path, and uses a fixed message for a rejected key.",[1276,4413,4414,4420,4426,4447],{},[1279,4415,4416,4419],{},[463,4417,4418],{},"Keep credentials out of messages."," Never interpolate a key, token, or header into a message. When the external data source's own message echoes a credential, replace it, as the Stripe client does: \"Stripe rejected the API key (HTTP 401).\"",[1279,4421,4422,4425],{},[463,4423,4424],{},"Keep the query string out of messages."," It can hold the caller's filter values. The Stripe client's fallback message names the path only: \"Stripe answered HTTP 500 to GET \u002Fv1\u002Fcustomers.\"",[1279,4427,4428,4431,4432,4434,4435,796,4437,796,4439,1577,4441,4443,4444,4446],{},[463,4429,4430],{},"Never copy a response body into a message."," Read only the fields you've reviewed. The Stripe client's ",[468,4433,2827],{}," reads Stripe's error ",[468,4436,2403],{},[468,4438,468],{},[468,4440,1792],{},[468,4442,2068],{}," to classify the error, and only ",[468,4445,2068],{}," reaches a message. The Art Institute connector names the API and the status, never the body.",[1279,4448,4449,4452],{},[463,4450,4451],{},"Keep personal data out of messages,"," such as a customer's email from an item you read.",[459,4454,4455,4456,4458],{},"With these rules, the caller's ",[468,4457,1316],{}," for a refused product delete carries Stripe's explanation, and no account id:",[531,4460,4462],{"className":3931,"code":4461,"filename":3933,"language":3934,"meta":537,"style":537},"{\n  \"message\": \"Query failed\",\n  \"source\": {\n    \"message\": \"Foreign key constraint failed\",\n    \"source\": {\n      \"message\": \"the extension returned an error: This product cannot be deleted because it has one or more user-created prices.\"\n    }\n  }\n}\n",[468,4463,4464,4468,4478,4484,4495,4501,4510,4514,4518],{"__ignoreMap":537},[541,4465,4466],{"class":543,"line":544},[541,4467,3941],{"class":551},[541,4469,4470,4472,4474,4476],{"class":543,"line":555},[541,4471,3946],{"class":821},[541,4473,790],{"class":551},[541,4475,3951],{"class":622},[541,4477,562],{"class":551},[541,4479,4480,4482],{"class":543,"line":565},[541,4481,3958],{"class":821},[541,4483,3961],{"class":551},[541,4485,4486,4488,4490,4493],{"class":543,"line":573},[541,4487,3966],{"class":821},[541,4489,790],{"class":551},[541,4491,4492],{"class":622},"\"Foreign key constraint failed\"",[541,4494,562],{"class":551},[541,4496,4497,4499],{"class":543,"line":581},[541,4498,3978],{"class":821},[541,4500,3961],{"class":551},[541,4502,4503,4505,4507],{"class":543,"line":589},[541,4504,3985],{"class":821},[541,4506,790],{"class":551},[541,4508,4509],{"class":622},"\"the extension returned an error: This product cannot be deleted because it has one or more user-created prices.\"\n",[541,4511,4512],{"class":543,"line":597},[541,4513,1274],{"class":551},[541,4515,4516],{"class":543,"line":605},[541,4517,3999],{"class":551},[541,4519,4520],{"class":543,"line":613},[541,4521,1946],{"class":551},[459,4523,4524],{},"When it works, triggering each mapped error over REST shows a response that contains nothing you wouldn't show the caller.",[455,4526,4528],{"id":4527},"_7-check-what-callers-see","7. Check What Callers See",[459,4530,4531],{},"Test each mapping at two levels: the class your connector throws, and the response Engine returns.",[459,4533,4534,4535,4538,4539,4541,4542,4545,4546,4549,4550,1846],{},"In an artifact smoke, compare ",[468,4536,4537],{},"error.monospace.code",", not ",[468,4540,2618],{},". The built file bundles its own copy of the kit, so its classes aren't the ones your test imports. The kit's ",[468,4543,4544],{},"ErrorCode"," map names each code, so never hard-code a number. The Stripe smoke reads the code through a small helper, which answers ",[468,4547,4548],{},"PLAIN_ERROR"," for a plain ",[468,4551,470],{},[531,4553,4558],{"className":4554,"code":4555,"filename":4556,"language":4557,"meta":537,"style":537},"language-js shiki shiki-themes monospace-light monospace-light monospace-dark","import { ErrorCode } from '@monospace\u002Fextension-kit\u002Fdata-connector';\n\u002F\u002F …\n\u002F** What a plain `Error` gives `codeOf`: it carries no kit code, and Engine reports it as a failed query. *\u002F\nconst PLAIN_ERROR = 'plain Error';\n\n\u002F** The kit error code the call throws, `PLAIN_ERROR`, or a failed assertion when it doesn't throw. *\u002F\nasync function codeOf(run) {\n    try {\n        await run();\n    }\n    catch (error) {\n        assert.ok(error instanceof Error, 'the connector throws an Error');\n        return error.monospace?.code ?? PLAIN_ERROR;\n    }\n\n    assert.fail('expected the connector to throw');\n}\n","scripts\u002Fsmoke.mjs","js",[468,4559,4560,4576,4580,4585,4599,4603,4608,4625,4632,4642,4646,4657,4681,4703,4707,4711,4728],{"__ignoreMap":537},[541,4561,4562,4564,4566,4568,4570,4572,4574],{"class":543,"line":544},[541,4563,548],{"class":547},[541,4565,641],{"class":551},[541,4567,4544],{"class":558},[541,4569,647],{"class":551},[541,4571,619],{"class":547},[541,4573,623],{"class":622},[541,4575,626],{"class":551},[541,4577,4578],{"class":543,"line":555},[541,4579,3377],{"class":632},[541,4581,4582],{"class":543,"line":565},[541,4583,4584],{"class":632},"\u002F** What a plain `Error` gives `codeOf`: it carries no kit code, and Engine reports it as a failed query. *\u002F\n",[541,4586,4587,4589,4592,4594,4597],{"class":543,"line":573},[541,4588,667],{"class":547},[541,4590,4591],{"class":670}," PLAIN_ERROR",[541,4593,674],{"class":547},[541,4595,4596],{"class":622}," 'plain Error'",[541,4598,626],{"class":551},[541,4600,4601],{"class":543,"line":581},[541,4602,661],{"emptyLinePlaceholder":660},[541,4604,4605],{"class":543,"line":589},[541,4606,4607],{"class":632},"\u002F** The kit error code the call throws, `PLAIN_ERROR`, or a failed assertion when it doesn't throw. *\u002F\n",[541,4609,4610,4613,4615,4618,4620,4623],{"class":543,"line":597},[541,4611,4612],{"class":547},"async",[541,4614,3494],{"class":547},[541,4616,4617],{"class":670}," codeOf",[541,4619,784],{"class":551},[541,4621,4622],{"class":547},"run",[541,4624,944],{"class":551},[541,4626,4627,4630],{"class":543,"line":605},[541,4628,4629],{"class":547},"    try",[541,4631,552],{"class":551},[541,4633,4634,4637,4640],{"class":543,"line":613},[541,4635,4636],{"class":547},"        await",[541,4638,4639],{"class":670}," run",[541,4641,1073],{"class":551},[541,4643,4644],{"class":543,"line":629},[541,4645,1274],{"class":551},[541,4647,4648,4651,4653,4655],{"class":543,"line":636},[541,4649,4650],{"class":547},"    catch",[541,4652,933],{"class":551},[541,4654,1089],{"class":558},[541,4656,944],{"class":551},[541,4658,4659,4662,4664,4666,4668,4670,4672,4674,4676,4679],{"class":543,"line":657},[541,4660,4661],{"class":558},"        assert",[541,4663,489],{"class":551},[541,4665,1247],{"class":670},[541,4667,784],{"class":551},[541,4669,1089],{"class":558},[541,4671,1108],{"class":547},[541,4673,1111],{"class":670},[541,4675,796],{"class":551},[541,4677,4678],{"class":622},"'the connector throws an Error'",[541,4680,1052],{"class":551},[541,4682,4683,4685,4687,4689,4692,4695,4697,4699,4701],{"class":543,"line":664},[541,4684,1226],{"class":547},[541,4686,1105],{"class":558},[541,4688,489],{"class":551},[541,4690,4691],{"class":558},"monospace",[541,4693,4694],{"class":551},"?.",[541,4696,468],{"class":558},[541,4698,2071],{"class":547},[541,4700,4591],{"class":670},[541,4702,626],{"class":551},[541,4704,4705],{"class":543,"line":682},[541,4706,1274],{"class":551},[541,4708,4709],{"class":543,"line":688},[541,4710,661],{"emptyLinePlaceholder":660},[541,4712,4713,4716,4718,4721,4723,4726],{"class":543,"line":703},[541,4714,4715],{"class":558},"    assert",[541,4717,489],{"class":551},[541,4719,4720],{"class":670},"fail",[541,4722,784],{"class":551},[541,4724,4725],{"class":622},"'expected the connector to throw'",[541,4727,1052],{"class":551},[541,4729,4730],{"class":543,"line":709},[541,4731,1946],{"class":551},[459,4733,4734,4735,1309,4737,4739],{},"The refusal checks use it. This check reads the code directly, because it also asserts the message: a wrong key fails ",[468,4736,3055],{},[468,4738,1530],{}," and the fixed message, and the real key passes:",[531,4741,4743],{"className":4554,"code":4742,"filename":4556,"language":4557,"meta":537,"style":537},"await check('a wrong key fails testConnection with AccessDenied, without echoing the key', async () => {\n    const wrong = await descriptor.setup({ config: { apiKey: 'sk_test_wrongwrongwrong' } });\n    const error = await wrong.testConnection().catch((thrown) => thrown);\n    assert.equal(error.monospace?.code, ErrorCode.AccessDenied);\n    assert.equal(error.message, 'Stripe rejected the API key (HTTP 401).');\n    assert.deepEqual(await connector.testConnection(), { ok: true });\n});\n",[468,4744,4745,4769,4796,4832,4863,4886,4914],{"__ignoreMap":537},[541,4746,4747,4749,4752,4754,4757,4759,4761,4764,4767],{"class":543,"line":544},[541,4748,3684],{"class":547},[541,4750,4751],{"class":670}," check",[541,4753,784],{"class":551},[541,4755,4756],{"class":622},"'a wrong key fails testConnection with AccessDenied, without echoing the key'",[541,4758,796],{"class":551},[541,4760,4612],{"class":547},[541,4762,4763],{"class":551}," () ",[541,4765,4766],{"class":547},"=>",[541,4768,552],{"class":551},[541,4770,4771,4773,4776,4778,4780,4783,4785,4787,4790,4793],{"class":543,"line":555},[541,4772,2012],{"class":547},[541,4774,4775],{"class":670}," wrong",[541,4777,674],{"class":547},[541,4779,1023],{"class":547},[541,4781,4782],{"class":558}," descriptor",[541,4784,489],{"class":551},[541,4786,1764],{"class":670},[541,4788,4789],{"class":551},"({ config: { apiKey: ",[541,4791,4792],{"class":622},"'sk_test_wrongwrongwrong'",[541,4794,4795],{"class":551}," } });\n",[541,4797,4798,4800,4802,4804,4806,4808,4810,4812,4815,4817,4820,4823,4825,4827,4830],{"class":543,"line":565},[541,4799,2012],{"class":547},[541,4801,1105],{"class":670},[541,4803,674],{"class":547},[541,4805,1023],{"class":547},[541,4807,4775],{"class":558},[541,4809,489],{"class":551},[541,4811,3055],{"class":670},[541,4813,4814],{"class":551},"().",[541,4816,1476],{"class":670},[541,4818,4819],{"class":551},"((",[541,4821,4822],{"class":547},"thrown",[541,4824,1870],{"class":551},[541,4826,4766],{"class":547},[541,4828,4829],{"class":558}," thrown",[541,4831,1052],{"class":551},[541,4833,4834,4836,4838,4841,4843,4845,4847,4849,4851,4853,4855,4857,4859,4861],{"class":543,"line":573},[541,4835,4715],{"class":558},[541,4837,489],{"class":551},[541,4839,4840],{"class":670},"equal",[541,4842,784],{"class":551},[541,4844,1089],{"class":558},[541,4846,489],{"class":551},[541,4848,4691],{"class":558},[541,4850,4694],{"class":551},[541,4852,468],{"class":558},[541,4854,796],{"class":551},[541,4856,4544],{"class":558},[541,4858,489],{"class":551},[541,4860,1530],{"class":558},[541,4862,1052],{"class":551},[541,4864,4865,4867,4869,4871,4873,4875,4877,4879,4881,4884],{"class":543,"line":581},[541,4866,4715],{"class":558},[541,4868,489],{"class":551},[541,4870,4840],{"class":670},[541,4872,784],{"class":551},[541,4874,1089],{"class":558},[541,4876,489],{"class":551},[541,4878,2068],{"class":558},[541,4880,796],{"class":551},[541,4882,4883],{"class":622},"'Stripe rejected the API key (HTTP 401).'",[541,4885,1052],{"class":551},[541,4887,4888,4890,4892,4895,4897,4899,4902,4904,4906,4909,4912],{"class":543,"line":589},[541,4889,4715],{"class":558},[541,4891,489],{"class":551},[541,4893,4894],{"class":670},"deepEqual",[541,4896,784],{"class":551},[541,4898,3684],{"class":547},[541,4900,4901],{"class":558}," connector",[541,4903,489],{"class":551},[541,4905,3055],{"class":670},[541,4907,4908],{"class":551},"(), { ok: ",[541,4910,4911],{"class":670},"true",[541,4913,1210],{"class":551},[541,4915,4916],{"class":543,"line":597},[541,4917,4918],{"class":551},"});\n",[459,4920,4921,4922,4924,4925,4927,4928,4930],{},"The unit tests cover what a live account can't produce on demand. They replace ",[468,4923,510],{}," with a fake Stripe and import the sources, so ",[468,4926,2618],{}," works there. These two check that a permission denial stays the ",[468,4929,1203],{},", and that a server error's message names the path but not the query string or the log URL:",[524,4932,4933],{},[527,4934,4936],{"label":4935},"test\u002Fconnector.test.ts (cause and server-error tests)",[531,4937,4940],{"className":533,"code":4938,"filename":4939,"language":536,"meta":537,"style":537},"\u002F\u002F Network-free tests through the connector's handlers, with `fetch` replaced by a fake Stripe.\n\u002F\u002F They cover what a live account can't produce on demand: timeouts, bad bodies, rate limits, and\n\u002F\u002F a bulk write that fails partway.\nimport type { FilterSerialized, OperationSerialized, ReadManyOperationSerialized } from '@monospace\u002Fextension-kit\u002Fdata-connector';\nimport {\n    AccessDenied,\n    ConnectionFailed,\n    ForeignKeyConstraintViolation,\n    InvalidConfiguration,\n    QueryRejected,\n    RateLimited,\n} from '@monospace\u002Fextension-kit\u002Fdata-connector';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport connector from '..\u002Fsrc\u002Fdata-connectors\u002Fstripe\u002Findex';\n\u002F\u002F …\nfunction stripeError(status: number, error: Record\u003Cstring, string>): Response {\n    return Response.json({ error: { ...error, request_log_url: 'https:\u002F\u002Fdashboard.stripe.com\u002Facct_123\u002Ftest\u002Flogs\u002Freq_1', doc_url: 'https:\u002F\u002Fdocs' } }, { status });\n}\n    \u002F\u002F …\n    it('keeps a permission denial as the cause, where Engine looks for it', async () => {\n        const denied = Object.assign(new Error('Requires net access to \"example.com\"'), { name: 'NotCapable' });\n        answer = () => Promise.reject(denied);\n        const error = await failure(query(readCustomers));\n        expect(error).toBeInstanceOf(ConnectionFailed);\n        expect(error).toHaveProperty('cause', denied);\n    });\n    \u002F\u002F …\n    it('reports a server error as ConnectionFailed, naming the path but never the query string or the log URL', async () => {\n        answer = () => stripeError(500, { type: 'api_error', code: 'server_error' });\n        const error = await failure(query({ ...readCustomers, filter: { cmp: 'equals', left: { field: 'email' }, right: { value: 'ada@x.test' } } }));\n        expect(error).toBeInstanceOf(ConnectionFailed);\n        expect(error).toHaveProperty('message', 'Stripe answered HTTP 500 to GET \u002Fv1\u002Fcustomers.');\n    });\n","test\u002Fconnector.test.ts",[468,4941,4942,4947,4952,4957,4986,4992,4998,5004,5010,5017,5023,5029,5039,5077,5091,5095,5135,5172,5176,5180,5200,5237,5262,5286,5306,5330,5335,5339,5358,5388,5432,5450,5474],{"__ignoreMap":537},[541,4943,4944],{"class":543,"line":544},[541,4945,4946],{"class":632},"\u002F\u002F Network-free tests through the connector's handlers, with `fetch` replaced by a fake Stripe.\n",[541,4948,4949],{"class":543,"line":555},[541,4950,4951],{"class":632},"\u002F\u002F They cover what a live account can't produce on demand: timeouts, bad bodies, rate limits, and\n",[541,4953,4954],{"class":543,"line":565},[541,4955,4956],{"class":632},"\u002F\u002F a bulk write that fails partway.\n",[541,4958,4959,4961,4963,4965,4968,4970,4973,4975,4978,4980,4982,4984],{"class":543,"line":573},[541,4960,548],{"class":547},[541,4962,3245],{"class":547},[541,4964,641],{"class":551},[541,4966,4967],{"class":558},"FilterSerialized",[541,4969,796],{"class":551},[541,4971,4972],{"class":558},"OperationSerialized",[541,4974,796],{"class":551},[541,4976,4977],{"class":558},"ReadManyOperationSerialized",[541,4979,647],{"class":551},[541,4981,619],{"class":547},[541,4983,623],{"class":622},[541,4985,626],{"class":551},[541,4987,4988,4990],{"class":543,"line":581},[541,4989,548],{"class":547},[541,4991,552],{"class":551},[541,4993,4994,4996],{"class":543,"line":589},[541,4995,559],{"class":558},[541,4997,562],{"class":551},[541,4999,5000,5002],{"class":543,"line":597},[541,5001,568],{"class":558},[541,5003,562],{"class":551},[541,5005,5006,5008],{"class":543,"line":605},[541,5007,576],{"class":558},[541,5009,562],{"class":551},[541,5011,5012,5015],{"class":543,"line":613},[541,5013,5014],{"class":558},"    InvalidConfiguration",[541,5016,562],{"class":551},[541,5018,5019,5021],{"class":543,"line":629},[541,5020,592],{"class":558},[541,5022,562],{"class":551},[541,5024,5025,5027],{"class":543,"line":636},[541,5026,600],{"class":558},[541,5028,562],{"class":551},[541,5030,5031,5033,5035,5037],{"class":543,"line":657},[541,5032,616],{"class":551},[541,5034,619],{"class":547},[541,5036,623],{"class":622},[541,5038,626],{"class":551},[541,5040,5041,5043,5045,5048,5050,5053,5055,5058,5060,5063,5065,5068,5070,5072,5075],{"class":543,"line":664},[541,5042,548],{"class":547},[541,5044,641],{"class":551},[541,5046,5047],{"class":558},"beforeEach",[541,5049,796],{"class":551},[541,5051,5052],{"class":558},"describe",[541,5054,796],{"class":551},[541,5056,5057],{"class":558},"expect",[541,5059,796],{"class":551},[541,5061,5062],{"class":558},"it",[541,5064,796],{"class":551},[541,5066,5067],{"class":558},"vi",[541,5069,647],{"class":551},[541,5071,619],{"class":547},[541,5073,5074],{"class":622}," 'vitest'",[541,5076,626],{"class":551},[541,5078,5079,5081,5083,5086,5089],{"class":543,"line":682},[541,5080,548],{"class":547},[541,5082,4901],{"class":558},[541,5084,5085],{"class":547}," from",[541,5087,5088],{"class":622}," '..\u002Fsrc\u002Fdata-connectors\u002Fstripe\u002Findex'",[541,5090,626],{"class":551},[541,5092,5093],{"class":543,"line":688},[541,5094,3377],{"class":632},[541,5096,5097,5099,5102,5104,5107,5110,5112,5115,5117,5119,5121,5123,5125,5128,5130,5133],{"class":543,"line":703},[541,5098,1813],{"class":547},[541,5100,5101],{"class":670}," stripeError",[541,5103,784],{"class":551},[541,5105,5106],{"class":547},"status:",[541,5108,5109],{"class":821}," number",[541,5111,796],{"class":551},[541,5113,5114],{"class":547},"error:",[541,5116,3462],{"class":670},[541,5118,825],{"class":551},[541,5120,804],{"class":821},[541,5122,796],{"class":551},[541,5124,804],{"class":821},[541,5126,5127],{"class":551},">)",[541,5129,1846],{"class":547},[541,5131,5132],{"class":670}," Response",[541,5134,552],{"class":551},[541,5136,5137,5139,5141,5143,5145,5148,5151,5153,5156,5159,5162,5165,5168,5170],{"class":543,"line":709},[541,5138,1933],{"class":547},[541,5140,5132],{"class":558},[541,5142,489],{"class":551},[541,5144,3934],{"class":670},[541,5146,5147],{"class":551},"({ error: { ",[541,5149,5150],{"class":547},"...",[541,5152,1089],{"class":558},[541,5154,5155],{"class":551},", request_log_url: ",[541,5157,5158],{"class":622},"'https:\u002F\u002Fdashboard.stripe.com\u002Facct_123\u002Ftest\u002Flogs\u002Freq_1'",[541,5160,5161],{"class":551},", doc_url: ",[541,5163,5164],{"class":622},"'https:\u002F\u002Fdocs'",[541,5166,5167],{"class":551}," } }, { ",[541,5169,1237],{"class":558},[541,5171,1210],{"class":551},[541,5173,5174],{"class":543,"line":724},[541,5175,1946],{"class":551},[541,5177,5178],{"class":543,"line":730},[541,5179,763],{"class":632},[541,5181,5182,5185,5187,5190,5192,5194,5196,5198],{"class":543,"line":760},[541,5183,5184],{"class":670},"    it",[541,5186,784],{"class":551},[541,5188,5189],{"class":622},"'keeps a permission denial as the cause, where Engine looks for it'",[541,5191,796],{"class":551},[541,5193,4612],{"class":547},[541,5195,4763],{"class":551},[541,5197,4766],{"class":547},[541,5199,552],{"class":551},[541,5201,5202,5205,5208,5210,5213,5215,5218,5220,5222,5224,5226,5229,5232,5235],{"class":543,"line":766},[541,5203,5204],{"class":547},"        const",[541,5206,5207],{"class":670}," denied",[541,5209,674],{"class":547},[541,5211,5212],{"class":558}," Object",[541,5214,489],{"class":551},[541,5216,5217],{"class":670},"assign",[541,5219,784],{"class":551},[541,5221,1031],{"class":547},[541,5223,1111],{"class":670},[541,5225,784],{"class":551},[541,5227,5228],{"class":622},"'Requires net access to \"example.com\"'",[541,5230,5231],{"class":551},"), { name: ",[541,5233,5234],{"class":622},"'NotCapable'",[541,5236,1210],{"class":551},[541,5238,5239,5242,5244,5246,5248,5250,5252,5255,5257,5260],{"class":543,"line":772},[541,5240,5241],{"class":670},"        answer",[541,5243,674],{"class":547},[541,5245,4763],{"class":551},[541,5247,4766],{"class":547},[541,5249,3512],{"class":821},[541,5251,489],{"class":551},[541,5253,5254],{"class":670},"reject",[541,5256,784],{"class":551},[541,5258,5259],{"class":558},"denied",[541,5261,1052],{"class":551},[541,5263,5264,5266,5268,5270,5272,5275,5277,5279,5281,5284],{"class":543,"line":836},[541,5265,5204],{"class":547},[541,5267,1105],{"class":670},[541,5269,674],{"class":547},[541,5271,1023],{"class":547},[541,5273,5274],{"class":670}," failure",[541,5276,784],{"class":551},[541,5278,3049],{"class":670},[541,5280,784],{"class":551},[541,5282,5283],{"class":558},"readCustomers",[541,5285,3839],{"class":551},[541,5287,5288,5291,5293,5295,5297,5300,5302,5304],{"class":543,"line":883},[541,5289,5290],{"class":670},"        expect",[541,5292,784],{"class":551},[541,5294,1089],{"class":558},[541,5296,2099],{"class":551},[541,5298,5299],{"class":670},"toBeInstanceOf",[541,5301,784],{"class":551},[541,5303,1187],{"class":558},[541,5305,1052],{"class":551},[541,5307,5308,5310,5312,5314,5316,5319,5321,5324,5326,5328],{"class":543,"line":922},[541,5309,5290],{"class":670},[541,5311,784],{"class":551},[541,5313,1089],{"class":558},[541,5315,2099],{"class":551},[541,5317,5318],{"class":670},"toHaveProperty",[541,5320,784],{"class":551},[541,5322,5323],{"class":622},"'cause'",[541,5325,796],{"class":551},[541,5327,5259],{"class":558},[541,5329,1052],{"class":551},[541,5331,5332],{"class":543,"line":927},[541,5333,5334],{"class":551},"    });\n",[541,5336,5337],{"class":543,"line":947},[541,5338,763],{"class":632},[541,5340,5341,5343,5345,5348,5350,5352,5354,5356],{"class":543,"line":959},[541,5342,5184],{"class":670},[541,5344,784],{"class":551},[541,5346,5347],{"class":622},"'reports a server error as ConnectionFailed, naming the path but never the query string or the log URL'",[541,5349,796],{"class":551},[541,5351,4612],{"class":547},[541,5353,4763],{"class":551},[541,5355,4766],{"class":547},[541,5357,552],{"class":551},[541,5359,5360,5362,5364,5366,5368,5370,5372,5374,5377,5380,5383,5386],{"class":543,"line":965},[541,5361,5241],{"class":670},[541,5363,674],{"class":547},[541,5365,4763],{"class":551},[541,5367,4766],{"class":547},[541,5369,5101],{"class":670},[541,5371,784],{"class":551},[541,5373,1381],{"class":670},[541,5375,5376],{"class":551},", { type: ",[541,5378,5379],{"class":622},"'api_error'",[541,5381,5382],{"class":551},", code: ",[541,5384,5385],{"class":622},"'server_error'",[541,5387,1210],{"class":551},[541,5389,5390,5392,5394,5396,5398,5400,5402,5404,5407,5409,5411,5414,5417,5420,5423,5426,5429],{"class":543,"line":971},[541,5391,5204],{"class":547},[541,5393,1105],{"class":670},[541,5395,674],{"class":547},[541,5397,1023],{"class":547},[541,5399,5274],{"class":670},[541,5401,784],{"class":551},[541,5403,3049],{"class":670},[541,5405,5406],{"class":551},"({ ",[541,5408,5150],{"class":547},[541,5410,5283],{"class":558},[541,5412,5413],{"class":551},", filter: { cmp: ",[541,5415,5416],{"class":622},"'equals'",[541,5418,5419],{"class":551},", left: { field: ",[541,5421,5422],{"class":622},"'email'",[541,5424,5425],{"class":551}," }, right: { value: ",[541,5427,5428],{"class":622},"'ada@x.test'",[541,5430,5431],{"class":551}," } } }));\n",[541,5433,5434,5436,5438,5440,5442,5444,5446,5448],{"class":543,"line":976},[541,5435,5290],{"class":670},[541,5437,784],{"class":551},[541,5439,1089],{"class":558},[541,5441,2099],{"class":551},[541,5443,5299],{"class":670},[541,5445,784],{"class":551},[541,5447,1187],{"class":558},[541,5449,1052],{"class":551},[541,5451,5452,5454,5456,5458,5460,5462,5464,5467,5469,5472],{"class":543,"line":987},[541,5453,5290],{"class":670},[541,5455,784],{"class":551},[541,5457,1089],{"class":558},[541,5459,2099],{"class":551},[541,5461,5318],{"class":670},[541,5463,784],{"class":551},[541,5465,5466],{"class":622},"'message'",[541,5468,796],{"class":551},[541,5470,5471],{"class":622},"'Stripe answered HTTP 500 to GET \u002Fv1\u002Fcustomers.'",[541,5473,1052],{"class":551},[541,5475,5476],{"class":543,"line":1002},[541,5477,5334],{"class":551},[459,5479,5480,5481,5484,5485,5489,5490,5492,5493,796,5496,5499,5500,5502,5503,5506,5507,489],{},"Then send the same failures through Engine. Send reads with ",[468,5482,5483],{},"Cache-Control: no-cache",", so a cached success doesn't stand in for the failing call; see ",[480,5486,5488],{"href":5487},"\u002Fconcepts\u002Fcaching#bypass-caching","Bypass Caching",". Some requests never reach your connector. Engine answers a caller's filter, sort, or operator the contract doesn't declare with a ",[468,5491,1316],{}," of its own (codes ",[468,5494,5495],{},"4003",[468,5497,5498],{},"4012","). A write to a collection without that operation gets ",[468,5501,1316],{}," code ",[468,5504,5505],{},"4010",". A filter Engine adds, such as a permission rule's condition, isn't checked: it reaches your connector even when it compares a field or uses an operator the called operation doesn't declare. See ",[480,5508,3198],{"href":3197},[459,5510,5511,5512,5514,5515,5517,5518,5520,5521,1577,5523,1581,5525,5527,5528,1577,5530,1581,5532,5534,5535,5527,5537,5539,5540,5542,5543,5545,5546,5548,5549,5551,5552,1521,5554,5556,5557,5559,5560,5563,5564,1577,5568,5572],{},"For requests that reach your connector, a keyed miss (",[468,5513,2586],{},") answers ",[468,5516,1566],{},", and for an error the caller's HTTP status depends on the class: a constraint violation, a ",[468,5519,1552],{},", a permission denial, ",[468,5522,1758],{},[468,5524,1530],{},[468,5526,1316],{},". ",[468,5529,1187],{},[468,5531,1506],{},[468,5533,1381],{},", as does a plain ",[468,5536,470],{},[468,5538,1506],{}," doesn't answer ",[468,5541,1377],{},", and carries no ",[468,5544,3029],{}," header. These are the statuses of errors your handlers throw. When ",[468,5547,1764],{}," deactivates the data source, the request that started it and every later one answer with that error's status: ",[468,5550,1316],{}," \"Invalid extension configuration\" for ",[468,5553,1758],{},[468,5555,1530],{},". A connection test runs ",[468,5558,1764],{}," in a temporary connector, so a failure there fails only the test. A ",[468,5561,5562],{},"503"," \"Data source is deactivated\" while you test a new build can come from its query capabilities. With automatic reloading on, a build that changes them deactivates existing data sources until a restart or a workspace rebuild. See ",[480,5565,5567],{"href":5566},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Foperations#when-the-capabilities-change","When the Capabilities Change",[480,5569,5571],{"href":5570},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Ferrors#know-what-the-caller-sees","Know What the Caller Sees"," for the whole mapping.",[459,5574,5575],{},"When it works, every mapped class appears in your smoke with its code, and every Engine response you trigger carries a message the caller can act on.",[459,5577,1730,5578,5581],{},[480,5579,256],{"href":5580},"\u002Fdeveloper\u002Fextensions\u002Fdata-connectors\u002Ftesting"," for the full test ladder.",[455,5583,5585],{"id":5584},"see-also","See Also",[1276,5587,5588,5593,5600,5605,5610],{},[1279,5589,5590,5592],{},[480,5591,488],{"href":487},": every error class, its code, and the status the caller gets",[1279,5594,5595,5599],{},[480,5596,5598],{"href":5597},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Flimitations#errors","Data Connector Limitations",": retries and credential checks",[1279,5601,5602,5604],{},[480,5603,246],{"href":3226},": decide what to serve and what to refuse",[1279,5606,5607,5609],{},[480,5608,256],{"href":5580},": check each mapping in an artifact smoke and through Engine",[1279,5611,5612,5616],{},[480,5613,5615],{"href":5614},"\u002Freference\u002Fextensions\u002Fdata-connectors\u002Flimits","Data Connector Runtime Limits",": timeouts and the per-request deadline",[5618,5619,5620],"style",{},"html pre.shiki code .sLVBU, html code.shiki .sLVBU{--shiki-light:#0891B2;--shiki-default:#0891B2;--shiki-dark:#22D3EE}html pre.shiki code .sTMul, html code.shiki .sTMul{--shiki-light:#1F2937;--shiki-default:#1F2937;--shiki-dark:#94A3B8}html pre.shiki code .sNJ9c, html code.shiki .sNJ9c{--shiki-light:#24292E;--shiki-default:#24292E;--shiki-dark:#E2E8F0}html pre.shiki code .suKVh, html code.shiki .suKVh{--shiki-light:#032F62;--shiki-default:#032F62;--shiki-dark:#F8FAFC}html pre.shiki code .seNfP, html code.shiki .seNfP{--shiki-light:#64748B;--shiki-default:#64748B;--shiki-dark:#94A3B8}html pre.shiki code .ssBNi, html code.shiki .ssBNi{--shiki-light:#1659E9;--shiki-default:#1659E9;--shiki-dark:#94B3F5}html pre.shiki code .snHjA, html code.shiki .snHjA{--shiki-light:#124BC4;--shiki-default:#124BC4;--shiki-dark:#5B8EF4}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":537,"searchDepth":555,"depth":555,"links":5622},[5623,5624,5625,5629,5630,5631,5632,5633,5634],{"id":457,"depth":555,"text":143},{"id":492,"depth":555,"text":493},{"id":1410,"depth":555,"text":1411,"children":5626},[5627,5628],{"id":2636,"depth":565,"text":1597},{"id":2985,"depth":565,"text":2986},{"id":3042,"depth":555,"text":3043},{"id":3144,"depth":555,"text":3145},{"id":4009,"depth":555,"text":4010},{"id":4384,"depth":555,"text":4385},{"id":4527,"depth":555,"text":4528},{"id":5584,"depth":555,"text":5585},"Map each failure of an external data source to the right error class, refuse requests you can't serve, and keep secrets out of what callers see.","md",null,{},{"title":251,"icon":254},{"title":447,"description":5635},"ytJEhrRgJQhbCIkEizb-W_QsRHkXLkwRrMIhL9P4r6k",[5643,5645],{"title":246,"path":247,"stem":248,"description":5644,"icon":249,"children":-1},"Translate an external data source's filters, paging, keys, writes and errors onto the data connector contract, with examples from a rich and a weak grammar.",{"title":256,"path":257,"stem":258,"description":5646,"icon":259,"children":-1},"Climb the test ladder from lint and unit tests to Studio, learn which bug each rung catches, and write checks that can actually fail.",1790840120552]