@sixtyninecommerce/admin 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/README.md +5772 -118
  2. package/dist/auth/index.d.ts +7 -1
  3. package/dist/auth/index.js +8 -1
  4. package/dist/catalog/assets.d.ts +120 -8
  5. package/dist/catalog/assets.js +98 -12
  6. package/dist/catalog/collections.d.ts +279 -14
  7. package/dist/catalog/collections.js +246 -15
  8. package/dist/catalog/documents.d.ts +142 -0
  9. package/dist/catalog/documents.js +114 -0
  10. package/dist/catalog/index.d.ts +54 -6
  11. package/dist/catalog/index.js +10 -1
  12. package/dist/catalog/metadata.d.ts +244 -0
  13. package/dist/catalog/metadata.js +220 -0
  14. package/dist/catalog/products.d.ts +159 -19
  15. package/dist/catalog/products.js +88 -13
  16. package/dist/catalog/properties.d.ts +140 -14
  17. package/dist/catalog/properties.js +65 -1
  18. package/dist/catalog/recommendations.d.ts +288 -0
  19. package/dist/catalog/recommendations.js +237 -0
  20. package/dist/catalog/reviews.d.ts +381 -0
  21. package/dist/catalog/reviews.js +325 -0
  22. package/dist/catalog/types.d.ts +17 -1
  23. package/dist/catalog/types.js +10 -0
  24. package/dist/catalog/variant-find.d.ts +136 -0
  25. package/dist/catalog/variant-find.js +239 -0
  26. package/dist/catalog/variants.d.ts +293 -26
  27. package/dist/catalog/variants.js +164 -7
  28. package/dist/catalog/visibility.d.ts +38 -7
  29. package/dist/catalog/visibility.js +19 -1
  30. package/dist/client.d.ts +121 -9
  31. package/dist/client.js +53 -14
  32. package/dist/content/assets.d.ts +285 -0
  33. package/dist/content/assets.js +181 -0
  34. package/dist/content/forms.d.ts +332 -1
  35. package/dist/content/forms.js +140 -4
  36. package/dist/content/items.d.ts +292 -0
  37. package/dist/content/items.js +170 -0
  38. package/dist/content/menus.d.ts +254 -0
  39. package/dist/content/menus.js +70 -0
  40. package/dist/content/strings.d.ts +137 -0
  41. package/dist/content/strings.js +82 -0
  42. package/dist/content/types.d.ts +282 -0
  43. package/dist/content/types.js +100 -0
  44. package/dist/customers/age-verification.d.ts +66 -0
  45. package/dist/customers/age-verification.js +79 -0
  46. package/dist/customers/credit.d.ts +263 -0
  47. package/dist/customers/credit.js +244 -0
  48. package/dist/customers/delivery-preferences.d.ts +120 -0
  49. package/dist/customers/delivery-preferences.js +97 -0
  50. package/dist/customers/delivery-terms.d.ts +44 -0
  51. package/dist/customers/delivery-terms.js +32 -0
  52. package/dist/customers/groups.d.ts +207 -0
  53. package/dist/customers/groups.js +438 -0
  54. package/dist/customers/index.d.ts +462 -37
  55. package/dist/customers/index.js +317 -40
  56. package/dist/customers/invitation-status.d.ts +57 -0
  57. package/dist/customers/invitation-status.js +21 -0
  58. package/dist/customers/notes.d.ts +67 -0
  59. package/dist/customers/notes.js +51 -0
  60. package/dist/customers/notification-recipients.d.ts +97 -0
  61. package/dist/customers/notification-recipients.js +84 -0
  62. package/dist/customers/saved-lists.d.ts +42 -0
  63. package/dist/customers/saved-lists.js +61 -0
  64. package/dist/customers/vat.d.ts +83 -0
  65. package/dist/customers/vat.js +46 -0
  66. package/dist/customers/wire.d.ts +20 -0
  67. package/dist/customers/wire.js +35 -0
  68. package/dist/documents/ar.d.ts +307 -0
  69. package/dist/documents/ar.js +254 -0
  70. package/dist/documents/emails.d.ts +150 -0
  71. package/dist/documents/emails.js +220 -0
  72. package/dist/documents/index.d.ts +744 -21
  73. package/dist/documents/index.js +438 -24
  74. package/dist/documents/pdf.d.ts +2 -0
  75. package/dist/documents/pdf.js +11 -0
  76. package/dist/fulfillments/create.d.ts +167 -0
  77. package/dist/fulfillments/create.js +216 -0
  78. package/dist/fulfillments/index.d.ts +163 -11
  79. package/dist/fulfillments/index.js +135 -6
  80. package/dist/fulfillments/payment-policy.d.ts +64 -0
  81. package/dist/fulfillments/payment-policy.js +140 -0
  82. package/dist/fulfillments/warehouse.d.ts +724 -0
  83. package/dist/fulfillments/warehouse.js +914 -0
  84. package/dist/geo/index.d.ts +119 -0
  85. package/dist/geo/index.js +107 -0
  86. package/dist/index.d.ts +147 -39
  87. package/dist/index.js +51 -12
  88. package/dist/integrations/afas-actions.d.ts +234 -0
  89. package/dist/integrations/afas-actions.js +310 -0
  90. package/dist/integrations/afas-types.d.ts +483 -0
  91. package/dist/integrations/afas-types.js +5 -0
  92. package/dist/integrations/afas.d.ts +234 -0
  93. package/dist/integrations/afas.js +290 -0
  94. package/dist/integrations/connections.d.ts +103 -0
  95. package/dist/integrations/connections.js +55 -0
  96. package/dist/integrations/esp.d.ts +313 -0
  97. package/dist/integrations/esp.js +169 -0
  98. package/dist/integrations/exact-online-types.d.ts +965 -0
  99. package/dist/integrations/exact-online-types.js +6 -0
  100. package/dist/integrations/exact-online.d.ts +784 -0
  101. package/dist/integrations/exact-online.js +1089 -0
  102. package/dist/integrations/index.d.ts +44 -3
  103. package/dist/integrations/index.js +23 -3
  104. package/dist/integrations/pipeline-discoveries.d.ts +65 -0
  105. package/dist/integrations/pipeline-discoveries.js +65 -0
  106. package/dist/integrations/pipeline-keys.d.ts +13 -0
  107. package/dist/integrations/pipeline-keys.js +22 -0
  108. package/dist/integrations/pipeline-runs.d.ts +119 -0
  109. package/dist/integrations/pipeline-runs.js +87 -0
  110. package/dist/integrations/pipelines.d.ts +87 -15
  111. package/dist/integrations/pipelines.js +41 -22
  112. package/dist/integrations/price-import.d.ts +223 -0
  113. package/dist/integrations/price-import.js +124 -0
  114. package/dist/integrations/types.d.ts +367 -3
  115. package/dist/integrations/types.js +4 -3
  116. package/dist/locations/index.d.ts +66 -0
  117. package/dist/locations/index.js +31 -0
  118. package/dist/meta/index.d.ts +172 -0
  119. package/dist/metafields/index.d.ts +8 -1
  120. package/dist/metafields/index.js +10 -0
  121. package/dist/orders/cancel-with-money.d.ts +237 -0
  122. package/dist/orders/cancel-with-money.js +155 -0
  123. package/dist/orders/commit-edit.d.ts +23 -1
  124. package/dist/orders/commit-edit.js +19 -2
  125. package/dist/orders/deposit-policy.d.ts +3 -1
  126. package/dist/orders/drafts.d.ts +800 -0
  127. package/dist/orders/drafts.js +771 -0
  128. package/dist/orders/fields.d.ts +245 -0
  129. package/dist/orders/fields.js +188 -0
  130. package/dist/orders/get.d.ts +29 -19
  131. package/dist/orders/get.js +17 -8
  132. package/dist/orders/index.d.ts +66 -15
  133. package/dist/orders/index.js +25 -2
  134. package/dist/orders/lifecycle.d.ts +128 -0
  135. package/dist/orders/lifecycle.js +158 -0
  136. package/dist/orders/line-price-preview.d.ts +7 -2
  137. package/dist/orders/list.d.ts +243 -0
  138. package/dist/orders/list.js +487 -0
  139. package/dist/orders/manual-discounts.d.ts +13 -5
  140. package/dist/orders/manual-pricing.d.ts +5 -0
  141. package/dist/orders/money-view.d.ts +149 -0
  142. package/dist/orders/money-view.js +99 -0
  143. package/dist/orders/order-entry.d.ts +20 -7
  144. package/dist/orders/plan-edit.d.ts +34 -2
  145. package/dist/orders/plan-edit.js +112 -11
  146. package/dist/orders/settings.d.ts +201 -0
  147. package/dist/orders/settings.js +325 -0
  148. package/dist/orders/status.d.ts +159 -13
  149. package/dist/orders/status.js +21 -0
  150. package/dist/orders/surcharges.d.ts +20 -1
  151. package/dist/orders/surcharges.js +37 -11
  152. package/dist/payments/account-credit.d.ts +46 -0
  153. package/dist/payments/account-credit.js +42 -0
  154. package/dist/payments/ar.d.ts +292 -0
  155. package/dist/payments/ar.js +223 -0
  156. package/dist/payments/connected-account.d.ts +47 -0
  157. package/dist/payments/connected-account.js +61 -0
  158. package/dist/payments/credit-noted-fields.d.ts +19 -0
  159. package/dist/payments/credit-noted-fields.js +69 -0
  160. package/dist/payments/customer-consent.d.ts +30 -0
  161. package/dist/payments/customer-consent.js +49 -0
  162. package/dist/payments/index.d.ts +340 -19
  163. package/dist/payments/index.js +151 -1
  164. package/dist/payments/methods.d.ts +303 -8
  165. package/dist/payments/methods.js +337 -7
  166. package/dist/payments/mollie-methods.d.ts +159 -0
  167. package/dist/payments/mollie-methods.js +141 -0
  168. package/dist/payments/payment-links.d.ts +180 -0
  169. package/dist/payments/payment-links.js +97 -0
  170. package/dist/payments/provider-keys.d.ts +150 -0
  171. package/dist/payments/provider-keys.js +118 -0
  172. package/dist/payments/providers.d.ts +86 -11
  173. package/dist/payments/providers.js +29 -0
  174. package/dist/payments/requests.d.ts +147 -0
  175. package/dist/payments/requests.js +93 -0
  176. package/dist/payments/store-credit-fields.d.ts +15 -0
  177. package/dist/payments/store-credit-fields.js +51 -0
  178. package/dist/pickups/index.d.ts +106 -0
  179. package/dist/pickups/index.js +108 -0
  180. package/dist/platform/channels.d.ts +132 -0
  181. package/dist/platform/channels.js +128 -0
  182. package/dist/platform/email-gateways.d.ts +165 -0
  183. package/dist/platform/email-gateways.js +78 -0
  184. package/dist/platform/geo.d.ts +123 -0
  185. package/dist/platform/geo.js +144 -0
  186. package/dist/platform/tax.d.ts +162 -0
  187. package/dist/platform/tax.js +169 -0
  188. package/dist/pos/anomalies.d.ts +158 -0
  189. package/dist/pos/anomalies.js +64 -0
  190. package/dist/pos/assisted-selling.d.ts +79 -0
  191. package/dist/pos/assisted-selling.js +38 -0
  192. package/dist/pos/exchanges.d.ts +108 -0
  193. package/dist/pos/exchanges.js +22 -0
  194. package/dist/pos/gift-cards.d.ts +135 -0
  195. package/dist/pos/gift-cards.js +47 -0
  196. package/dist/pos/index.d.ts +2370 -58
  197. package/dist/pos/index.js +854 -7
  198. package/dist/pos/no-receipt.d.ts +94 -0
  199. package/dist/pos/no-receipt.js +25 -0
  200. package/dist/pos/offline-sales.d.ts +228 -0
  201. package/dist/pos/offline-sales.js +170 -0
  202. package/dist/pos/pay-by-link.d.ts +143 -0
  203. package/dist/pos/pay-by-link.js +57 -0
  204. package/dist/pos/quick-keys.d.ts +103 -0
  205. package/dist/pos/quick-keys.js +45 -0
  206. package/dist/pos/reports.d.ts +360 -0
  207. package/dist/pos/reports.js +62 -0
  208. package/dist/pos/returns.d.ts +244 -0
  209. package/dist/pos/returns.js +83 -0
  210. package/dist/pricing/bulk-coupons.d.ts +139 -0
  211. package/dist/pricing/bulk-coupons.js +112 -0
  212. package/dist/pricing/index.d.ts +174 -4
  213. package/dist/pricing/index.js +174 -1
  214. package/dist/pricing/price-lists.d.ts +606 -0
  215. package/dist/pricing/price-lists.js +600 -0
  216. package/dist/purchasing/alerts.d.ts +104 -0
  217. package/dist/purchasing/alerts.js +88 -0
  218. package/dist/purchasing/backorders.d.ts +61 -0
  219. package/dist/purchasing/backorders.js +92 -0
  220. package/dist/purchasing/cost.d.ts +110 -0
  221. package/dist/purchasing/cost.js +86 -0
  222. package/dist/purchasing/index.d.ts +54 -0
  223. package/dist/purchasing/index.js +24 -0
  224. package/dist/purchasing/links.d.ts +263 -0
  225. package/dist/purchasing/links.js +192 -0
  226. package/dist/purchasing/purchase-orders.d.ts +551 -0
  227. package/dist/purchasing/purchase-orders.js +491 -0
  228. package/dist/purchasing/receipts.d.ts +396 -0
  229. package/dist/purchasing/receipts.js +287 -0
  230. package/dist/purchasing/replenishment.d.ts +281 -0
  231. package/dist/purchasing/replenishment.js +240 -0
  232. package/dist/purchasing/returns.d.ts +273 -0
  233. package/dist/purchasing/returns.js +229 -0
  234. package/dist/purchasing/settings.d.ts +152 -0
  235. package/dist/purchasing/settings.js +181 -0
  236. package/dist/purchasing/suppliers.d.ts +476 -0
  237. package/dist/purchasing/suppliers.js +438 -0
  238. package/dist/quotes/index.d.ts +518 -0
  239. package/dist/quotes/index.js +439 -0
  240. package/dist/reports/index.d.ts +71 -8
  241. package/dist/reports/index.js +74 -11
  242. package/dist/returns/index.d.ts +675 -21
  243. package/dist/returns/index.js +756 -11
  244. package/dist/search/index.d.ts +76 -1
  245. package/dist/search/index.js +63 -0
  246. package/dist/seo/config.d.ts +128 -0
  247. package/dist/seo/config.js +95 -0
  248. package/dist/seo/redirects.d.ts +125 -0
  249. package/dist/seo/redirects.js +60 -1
  250. package/dist/seo/settings.d.ts +147 -0
  251. package/dist/seo/settings.js +209 -0
  252. package/dist/shipping/definitions.d.ts +29 -0
  253. package/dist/shipping/definitions.js +47 -0
  254. package/dist/shipping/fulfillment-profiles.d.ts +174 -0
  255. package/dist/shipping/fulfillment-profiles.js +104 -0
  256. package/dist/shipping/hendriks.d.ts +97 -0
  257. package/dist/shipping/hendriks.js +50 -0
  258. package/dist/shipping/index.d.ts +54 -0
  259. package/dist/shipping/index.js +27 -0
  260. package/dist/shipping/methods.d.ts +205 -0
  261. package/dist/shipping/methods.js +240 -0
  262. package/dist/shipping/myparcel.d.ts +668 -0
  263. package/dist/shipping/myparcel.js +466 -0
  264. package/dist/shipping/parcel-count-suggestion.d.ts +2 -0
  265. package/dist/shipping/parcel-rule.d.ts +6 -5
  266. package/dist/shipping/parcel-rule.js +21 -23
  267. package/dist/shipping/rate-tables.d.ts +324 -0
  268. package/dist/shipping/rate-tables.js +390 -0
  269. package/dist/shipping/ship-to-countries.d.ts +75 -0
  270. package/dist/shipping/ship-to-countries.js +47 -0
  271. package/dist/stock/by-location.d.ts +86 -0
  272. package/dist/stock/by-location.js +94 -0
  273. package/dist/stock/index.d.ts +18 -4
  274. package/dist/stock/index.js +7 -0
  275. package/dist/stock/locations.d.ts +144 -0
  276. package/dist/stock/locations.js +168 -0
  277. package/dist/stock/settings.d.ts +106 -0
  278. package/dist/stock/settings.js +103 -0
  279. package/dist/stock/transfers.d.ts +386 -52
  280. package/dist/stock/transfers.js +222 -17
  281. package/dist/stored-value/index.d.ts +312 -11
  282. package/dist/stored-value/index.js +223 -4
  283. package/dist/subscriptions/index.d.ts +682 -0
  284. package/dist/subscriptions/index.js +815 -0
  285. package/dist/tax/display-policy.d.ts +105 -0
  286. package/dist/tax/display-policy.js +84 -0
  287. package/dist/tax/index.d.ts +79 -1
  288. package/dist/tax/index.js +52 -0
  289. package/dist/tenant/email-style.d.ts +102 -0
  290. package/dist/tenant/email-style.js +72 -0
  291. package/dist/tenant/email-templates.d.ts +287 -0
  292. package/dist/tenant/email-templates.js +271 -0
  293. package/dist/tenant/notifications.d.ts +23 -17
  294. package/dist/tenant/settings-sources.d.ts +1 -1
  295. package/dist/tenant/time-zone.d.ts +17 -0
  296. package/dist/tenant/time-zone.js +11 -0
  297. package/dist/types.d.ts +50 -3
  298. package/dist/users/index.d.ts +3 -0
  299. package/dist/users/index.js +2 -0
  300. package/dist/users/warehouses.d.ts +50 -0
  301. package/dist/users/warehouses.js +51 -0
  302. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import type { Transport } from '@sixtyninecommerce/core';
1
+ import { type Transport } from '@sixtyninecommerce/core';
2
2
  import type { Call } from '../types.js';
3
3
  /** What `admin.search.entities` searches. */
4
4
  export type SearchEntityType = 'PRODUCTS' | 'ORDERS' | 'CUSTOMERS' | 'CONTENT';
@@ -76,6 +76,55 @@ export interface SearchEntitiesOptions extends Call {
76
76
  export declare const SEARCH_ENTITIES_MIN_TERM_LENGTH = 2;
77
77
  export declare const SEARCH_ENTITIES_MAX_TAKE = 10;
78
78
  export declare const SEARCH_ACROSS_ENTITIES_QUERY = "\n query SearchAcrossEntities($input: CommandSearchInput!) {\n searchAcrossEntities(input: $input) {\n products {\n id\n name\n slug\n previewUrl\n }\n orders {\n id\n code\n state\n orderPlacedAt\n totalWithTax\n currencyCode\n lifecycleStatus\n }\n customers {\n id\n firstName\n lastName\n emailAddress\n company\n }\n content {\n id\n collectionId\n collectionSlug\n slug\n status\n title\n }\n skipped {\n type\n reason\n }\n }\n }\n";
79
+ /**
80
+ * The store's search indexes `search.index` reads, one per kind of record. Each holds the store's
81
+ * rows of that kind; the products index is the admin catalogue index, not the storefront's.
82
+ */
83
+ export declare const SEARCH_INDEXES: readonly ["products", "orders", "customers", "facets", "promotions", "customer_groups", "stock_locations", "collections"];
84
+ export type SearchIndex = (typeof SEARCH_INDEXES)[number];
85
+ /** The engine's bound on the clauses of one `search.index` call, `filters` and groups together. */
86
+ export declare const SEARCH_INDEX_MAX_FILTERS = 32;
87
+ export interface SearchIndexInput {
88
+ /** Full text; `''` matches every row the filters keep. */
89
+ term: string;
90
+ /**
91
+ * Clauses, all of which must hold, each one of exactly these shapes on an attribute the index
92
+ * lets you filter on: `field = "value"`, `field = true|false`, `field STARTS WITH "value"`,
93
+ * `field CONTAINS "value"`, `field IN ["a", "b"]`, `field IS EMPTY`. Quote a string value with
94
+ * {@link escapeSearchFilterValue}: `code STARTS WITH "${escapeSearchFilterValue(prefix)}"`.
95
+ */
96
+ filters?: readonly string[];
97
+ /** Groups of clauses: each group must hold, and holds when ANY of its own clauses does. */
98
+ filterGroups?: ReadonlyArray<readonly string[]>;
99
+ /** `field` ascending or `-field` descending, on a field the index sorts by; relevance otherwise. */
100
+ sort?: string;
101
+ /** Rows to skip, 0 when omitted. */
102
+ skip?: number;
103
+ /** Rows to answer, 25 when omitted. */
104
+ take?: number;
105
+ }
106
+ /** One hit: the record's id, how well it matched, and the index's copy of it. */
107
+ export interface SearchIndexHit {
108
+ id: string;
109
+ /** The search engine's ranking score, 0 to 1: higher is a better match. */
110
+ score: number;
111
+ /**
112
+ * The index's snapshot of the record, as the store may see it: written by a job after the
113
+ * record's change commits, so it can lag by seconds. Read the record itself for current values.
114
+ */
115
+ document: Record<string, unknown>;
116
+ }
117
+ export interface SearchIndexResult {
118
+ items: SearchIndexHit[];
119
+ /** Every hit the term and filters match, not only this page's. */
120
+ total: number;
121
+ }
122
+ export declare const SEARCH_ENTITIES_QUERY = "\n query SearchIndex($input: EntitySearchInput!) {\n searchEntities(input: $input) {\n items {\n id\n score\n document\n }\n totalItems\n }\n }\n";
123
+ /**
124
+ * A string value inside a `search.index` filter's double quotes: its backslashes and quotes
125
+ * escaped, so what a person typed can never close the literal and add a clause.
126
+ */
127
+ export declare function escapeSearchFilterValue(value: string): string;
79
128
  /**
80
129
  * One search across the store (spec §7 `search.entities`): products, orders, customers and
81
130
  * content items matching a term, in one call.
@@ -94,6 +143,32 @@ export declare const SEARCH_ACROSS_ENTITIES_QUERY = "\n query SearchAcrossEntit
94
143
  * outside 1..{@link SEARCH_ENTITIES_MAX_TAKE}, or an empty `types` list.
95
144
  */
96
145
  export interface SearchModule {
146
+ /** @effect read */
97
147
  entities(term: string, options?: SearchEntitiesOptions): Promise<EntitySearchResult>;
148
+ /**
149
+ * One page of one search index of the store: hits in relevance order (or `sort`'s), each with
150
+ * the index's copy of the record. Use it to find ids by text or by an indexed attribute the
151
+ * record's own list cannot filter on, then read the records themselves.
152
+ *
153
+ * - **The store is always the one you act on**: no filter reaches another store's rows.
154
+ * - **Documents are index snapshots**, updated by a job after each change: read the record for
155
+ * current values, and expect a just-written record to be missing for a few seconds.
156
+ * - Needs the read permission of the index's own list: `ReadCatalog` or `ReadProduct` for
157
+ * products; `ReadOrder` for orders; `ReadCustomer` for customers; `ReadCustomer` or
158
+ * `ReadCustomerGroup` for customer groups; `ReadCatalog`, `ReadProduct` or `ReadFacet` for
159
+ * facets; `ReadPromotion`; `ReadCatalog` or `ReadStockLocation`; `ReadCatalog` or
160
+ * `ReadCollection`. Without it: `PermissionDeniedError`.
161
+ * - An app reading customer data below `full` is refused the orders and customers indexes
162
+ * (`CUSTOMER_DATA_NOT_GRANTED`): the hits alone would say which customer matches a name.
163
+ *
164
+ * Refused: an index not in {@link SEARCH_INDEXES} (`ValidationError` `SEARCH_INPUT_INVALID`, before
165
+ * anything is asked); and by the engine with `SEARCH_INPUT_INVALID` (400) a clause on an attribute
166
+ * the index does not filter on or in another syntax, more than {@link SEARCH_INDEX_MAX_FILTERS}
167
+ * clauses, or a negative `skip`/`take`. A `sort` field the index cannot sort on is ignored.
168
+ * `SERVICE_UNAVAILABLE` (503, retryable) when the search service did not answer.
169
+ *
170
+ * @effect read
171
+ */
172
+ index(index: SearchIndex, input: SearchIndexInput, options?: Call): Promise<SearchIndexResult>;
98
173
  }
99
174
  export declare function createSearchModule(transport: Transport): SearchModule;
@@ -1,3 +1,4 @@
1
+ import { ValidationError } from '@sixtyninecommerce/core';
1
2
  /** The engine's bounds, as `searchAcrossEntities` documents them. */
2
3
  export const SEARCH_ENTITIES_MIN_TERM_LENGTH = 2;
3
4
  export const SEARCH_ENTITIES_MAX_TAKE = 10;
@@ -41,6 +42,41 @@ export const SEARCH_ACROSS_ENTITIES_QUERY = /* GraphQL */ `
41
42
  }
42
43
  }
43
44
  `;
45
+ /**
46
+ * The store's search indexes `search.index` reads, one per kind of record. Each holds the store's
47
+ * rows of that kind; the products index is the admin catalogue index, not the storefront's.
48
+ */
49
+ export const SEARCH_INDEXES = [
50
+ 'products',
51
+ 'orders',
52
+ 'customers',
53
+ 'facets',
54
+ 'promotions',
55
+ 'customer_groups',
56
+ 'stock_locations',
57
+ 'collections',
58
+ ];
59
+ /** The engine's bound on the clauses of one `search.index` call, `filters` and groups together. */
60
+ export const SEARCH_INDEX_MAX_FILTERS = 32;
61
+ export const SEARCH_ENTITIES_QUERY = /* GraphQL */ `
62
+ query SearchIndex($input: EntitySearchInput!) {
63
+ searchEntities(input: $input) {
64
+ items {
65
+ id
66
+ score
67
+ document
68
+ }
69
+ totalItems
70
+ }
71
+ }
72
+ `;
73
+ /**
74
+ * A string value inside a `search.index` filter's double quotes: its backslashes and quotes
75
+ * escaped, so what a person typed can never close the literal and add a clause.
76
+ */
77
+ export function escapeSearchFilterValue(value) {
78
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
79
+ }
44
80
  export function createSearchModule(transport) {
45
81
  return {
46
82
  async entities(term, options = {}) {
@@ -58,5 +94,32 @@ export function createSearchModule(transport) {
58
94
  }, call);
59
95
  return data.searchAcrossEntities;
60
96
  },
97
+ async index(index, input, options) {
98
+ if (!SEARCH_INDEXES.includes(index)) {
99
+ throw new ValidationError({
100
+ code: 'SEARCH_INPUT_INVALID',
101
+ message: `The search index must be one of ${SEARCH_INDEXES.join(', ')}.`,
102
+ status: 400,
103
+ service: 'vendure',
104
+ });
105
+ }
106
+ const { term, filters, filterGroups, sort, skip, take } = input;
107
+ const data = await transport.graphql({
108
+ api: 'admin',
109
+ query: SEARCH_ENTITIES_QUERY,
110
+ variables: {
111
+ input: {
112
+ term,
113
+ entityType: index,
114
+ ...(take === undefined ? {} : { take }),
115
+ ...(skip === undefined ? {} : { skip }),
116
+ ...(sort === undefined || sort === '' ? {} : { sort }),
117
+ ...(filters?.length ? { filters: [...filters] } : {}),
118
+ ...(filterGroups?.length ? { filterGroups: filterGroups.map((g) => [...g]) } : {}),
119
+ },
120
+ },
121
+ }, options);
122
+ return { items: data.searchEntities.items, total: data.searchEntities.totalItems };
123
+ },
61
124
  };
62
125
  }
@@ -0,0 +1,128 @@
1
+ import { type Transport } from '@sixtyninecommerce/core';
2
+ import type { SettingsSourceKind } from '../tenant/settings-sources.js';
3
+ import type { Call } from '../types.js';
4
+ /**
5
+ * How often the SEO audit (the storefront crawl included) runs. Mirrors `@IsIn` on the plugins
6
+ * service's `UpdateSeoConfigDto` and the meta catalog's `SEO_CRAWL_SCHEDULES`, pinned by
7
+ * `__tests__/seo-config.spec.ts`.
8
+ */
9
+ export declare const SEO_CRAWL_SCHEDULES: readonly ["off", "daily", "weekly"];
10
+ export type SeoCrawlSchedule = (typeof SEO_CRAWL_SCHEDULES)[number];
11
+ /**
12
+ * The ACTIVE store's SEO automation, as the plugins service applies it. The switches
13
+ * (`autoRedirectsEnabled`, `indexnowEnabled`, `cruxEnabled`, the crawl settings) are part of the
14
+ * `settings.seo` group: while the store takes the group from its organization they are the
15
+ * organization's, and `source` says so. The IndexNow key and the Search Console connection are
16
+ * always the store's own.
17
+ *
18
+ * The keys follow the service's wire names camelised (`indexnow_enabled` → `indexnowEnabled`), as
19
+ * the meta catalog's `seo` schema publishes them, so a head that renders that schema maps each
20
+ * property mechanically.
21
+ */
22
+ export interface SeoConfig {
23
+ /** Changed URLs are submitted to IndexNow (needs {@link SeoConfig.indexnowKey} to send any). */
24
+ indexnowEnabled: boolean;
25
+ /** The store's IndexNow key, served at `/<key>.txt`; null until one is generated. */
26
+ indexnowKey: string | null;
27
+ /** The Search Console property the store connected; null when none. */
28
+ gscPropertyUrl: string | null;
29
+ /** The store holds a Search Console connection. The credential itself is never answered. */
30
+ gscConnected: boolean;
31
+ /** When the connection was made (ISO 8601, UTC); null when there is none. */
32
+ gscConnectedAt: string | null;
33
+ /** Core Web Vitals (CrUX). Stored and shown; nothing on the platform reads it yet. */
34
+ cruxEnabled: boolean;
35
+ /** A string on the wire: a newer service may answer a schedule this SDK does not name yet. */
36
+ crawlSchedule: SeoCrawlSchedule | (string & {});
37
+ /** The most storefront pages one audit crawls (10 to 5,000). */
38
+ crawlMaxPages: number;
39
+ /** How fast the audit crawls, in pages per second (1 to 10). */
40
+ crawlRatePerSec: number;
41
+ /** A renamed product, collection or page keeps its old URL alive with a redirect. */
42
+ autoRedirectsEnabled: boolean;
43
+ /** `organization` while the switches above are the organization's, else `channel`. */
44
+ source: SettingsSourceKind | (string & {});
45
+ /** The organization locks `settings.seo`: {@link SeoConfigModule.set} refuses any change. */
46
+ locked: boolean;
47
+ /** The store keeps its own switches although the organization shares the group. */
48
+ overridden: boolean;
49
+ /** The organization shares the group without locking it, so the store may override it. */
50
+ inheritable: boolean;
51
+ /** The store's organization; null for a store outside any organization. */
52
+ organizationName: string | null;
53
+ }
54
+ /** The switches {@link SeoConfigModule.set} changes; a key left out keeps its value. */
55
+ export interface SeoConfigPatch {
56
+ indexnowEnabled?: boolean;
57
+ cruxEnabled?: boolean;
58
+ crawlSchedule?: SeoCrawlSchedule;
59
+ /** A whole number from 10 to 5,000. */
60
+ crawlMaxPages?: number;
61
+ /** A whole number from 1 to 10. */
62
+ crawlRatePerSec?: number;
63
+ autoRedirectsEnabled?: boolean;
64
+ }
65
+ /** What {@link SeoConfigModule.rotateIndexnowKey} answers. */
66
+ export interface IndexnowKeyRotation {
67
+ /** The new key. The store's `/<key>.txt` serves it from now on; the old key is void. */
68
+ key: string;
69
+ }
70
+ /**
71
+ * `admin.seo.config` — the ACTIVE store's SEO automation on the plugins service (Settings → SEO):
72
+ * automatic redirects, IndexNow, Core Web Vitals and the audit schedule. Needs the store's `seo`
73
+ * plugin: without it every method is `PermissionDeniedError` (403) naming the plugin.
74
+ *
75
+ * The other half of the store's SEO settings, the channel defaults (storefront URL, share and AI
76
+ * crawler defaults), is `admin.seo.settings`.
77
+ */
78
+ export interface SeoConfigModule {
79
+ /**
80
+ * The automation as the service applies it, and where its switches come from. Needs
81
+ * `ReadSettings`.
82
+ *
83
+ * @effect read
84
+ * @permission ReadSettings
85
+ */
86
+ get(options?: Call): Promise<SeoConfig>;
87
+ /**
88
+ * Change the switches `patch` names, and answer the whole configuration as it now applies.
89
+ * The next audit, URL change or IndexNow submission follows it.
90
+ *
91
+ * **Coded refusals:**
92
+ * - `SEO_CONFIG_INVALID` → `ValidationError` (400), before any request: an empty patch, a
93
+ * key that is not a switch here (the IndexNow key and Search Console have doors of their
94
+ * own), or a null (no switch can be cleared). One `details[]` entry per key, `field` naming
95
+ * it.
96
+ * - `VALIDATION_FAILED` → `ValidationError` (400), from the service: a value of the wrong type,
97
+ * a schedule outside {@link SEO_CRAWL_SCHEDULES}, or a number outside its range.
98
+ * - `MANAGED_BY_ORGANIZATION` → `BusinessRuleError` (422): a changed switch while the
99
+ * organization locks `settings.seo`.
100
+ * - `ORGANIZATION_SETTINGS_INHERITED` → `BusinessRuleError` (422): a changed switch while the
101
+ * store inherits the group; `tenant.settingsSources.override('settings.seo')` first. Sending
102
+ * back the values `get` answered is not a change and passes.
103
+ * - `ORGANIZATION_SETTINGS_SWITCH_OFF_CONFLICT` → `ConflictError` (409): in an organization's
104
+ * workspace, while the organization is switching its SEO defaults off and copying them into
105
+ * its stores. Nothing changed; try again once that finishes.
106
+ * - `PermissionDeniedError` (403): the credential lacks `UpdateSettings`.
107
+ *
108
+ * @effect write
109
+ * @permission UpdateSettings
110
+ */
111
+ set(patch: SeoConfigPatch, options?: Call): Promise<SeoConfig>;
112
+ /**
113
+ * Generate a new IndexNow key for the store and answer it. The old key is void at once: the
114
+ * store's `/<key>.txt` serves only the new one, and submissions use it from now on. The key is
115
+ * the store's own, so it is never refused for a store taking `settings.seo` from its
116
+ * organization.
117
+ *
118
+ * **Coded refusals:**
119
+ * - `ORGANIZATION_WORKSPACE_NOT_A_STORE` → `BusinessRuleError` (422): the active store is an
120
+ * organization's workspace, which has no storefront to prove.
121
+ * - `PermissionDeniedError` (403): the credential lacks `UpdateSettings`.
122
+ *
123
+ * @effect destructive
124
+ * @permission UpdateSettings
125
+ */
126
+ rotateIndexnowKey(options?: Call): Promise<IndexnowKeyRotation>;
127
+ }
128
+ export declare function createSeoConfigModule(transport: Transport): SeoConfigModule;
@@ -0,0 +1,95 @@
1
+ import { ValidationError } from '@sixtyninecommerce/core';
2
+ /**
3
+ * How often the SEO audit (the storefront crawl included) runs. Mirrors `@IsIn` on the plugins
4
+ * service's `UpdateSeoConfigDto` and the meta catalog's `SEO_CRAWL_SCHEDULES`, pinned by
5
+ * `__tests__/seo-config.spec.ts`.
6
+ */
7
+ export const SEO_CRAWL_SCHEDULES = ['off', 'daily', 'weekly'];
8
+ const INVALID = 'SEO_CONFIG_INVALID';
9
+ const PATCH_KEYS = [
10
+ 'indexnowEnabled',
11
+ 'cruxEnabled',
12
+ 'crawlSchedule',
13
+ 'crawlMaxPages',
14
+ 'crawlRatePerSec',
15
+ 'autoRedirectsEnabled',
16
+ ];
17
+ const KNOWN_KEYS = new Set(PATCH_KEYS);
18
+ /** Not a switch: answered by `get`, written through a door of its own. */
19
+ const OWN_DOORS = {
20
+ indexnowKey: 'indexnowKey is generated by rotateIndexnowKey(), never set.',
21
+ gscPropertyUrl: 'Search Console is connected through its OAuth flow, never set here.',
22
+ gscConnected: 'Search Console is connected through its OAuth flow, never set here.',
23
+ gscConnectedAt: 'Search Console is connected through its OAuth flow, never set here.',
24
+ };
25
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
26
+ function problemsOf(patch) {
27
+ const problems = [];
28
+ const add = (field, message) => problems.push({ field, code: INVALID, message });
29
+ if (!isRecord(patch)) {
30
+ add('patch', 'Pass the switches to change as an object, e.g. { autoRedirectsEnabled: false }.');
31
+ return problems;
32
+ }
33
+ const named = Object.keys(patch).filter((key) => patch[key] !== undefined);
34
+ if (named.length === 0) {
35
+ add('patch', `Name at least one switch to change: ${PATCH_KEYS.join(', ')}.`);
36
+ }
37
+ for (const key of named) {
38
+ if (!KNOWN_KEYS.has(key)) {
39
+ add(key, OWN_DOORS[key] ??
40
+ `${key} is not an SEO automation switch; they are ${PATCH_KEYS.join(', ')}.`);
41
+ }
42
+ else if (patch[key] === null) {
43
+ // The service's validation lets a null through to a NOT NULL column, whose refusal names
44
+ // no field (the meta catalog's `SEO_NULL`); nothing here has a "cleared" state.
45
+ add(key, `${key} cannot be cleared; send a value, or leave it out to keep it.`);
46
+ }
47
+ }
48
+ return problems;
49
+ }
50
+ /**
51
+ * The camelised wire body, its source fields filled in: they came with #882, and a service from
52
+ * before it leaves them out, which is the store's own settings. Every other field is the
53
+ * service's, as answered.
54
+ */
55
+ function configOf(body) {
56
+ return {
57
+ ...body,
58
+ source: body.source ?? 'channel',
59
+ locked: body.locked === true,
60
+ overridden: body.overridden === true,
61
+ inheritable: body.inheritable === true,
62
+ organizationName: body.organizationName ?? null,
63
+ };
64
+ }
65
+ export function createSeoConfigModule(transport) {
66
+ return {
67
+ async get(options) {
68
+ return configOf(await transport.rest('plugins', { method: 'GET', path: '/seo/config' }, options));
69
+ },
70
+ async set(patch, options) {
71
+ const problems = problemsOf(patch);
72
+ if (problems.length > 0) {
73
+ throw new ValidationError({
74
+ code: INVALID,
75
+ message: problems.map((problem) => problem.message).join(' '),
76
+ status: 400,
77
+ service: 'plugins',
78
+ details: problems,
79
+ });
80
+ }
81
+ // Only the named switches go out. The transport snake-cases them, and every key here
82
+ // camelises from a wire name, so the round trip is exact (`indexnowEnabled` →
83
+ // `indexnow_enabled`); a key spelt any other way would be refused whole by the service.
84
+ const body = {};
85
+ for (const key of PATCH_KEYS) {
86
+ if (patch[key] !== undefined)
87
+ body[key] = patch[key];
88
+ }
89
+ return configOf(await transport.rest('plugins', { method: 'PUT', path: '/seo/config', body }, options));
90
+ },
91
+ rotateIndexnowKey(options) {
92
+ return transport.rest('plugins', { method: 'POST', path: '/seo/config/indexnow-key/rotate' }, options);
93
+ },
94
+ };
95
+ }
@@ -18,10 +18,135 @@ export interface RedirectCreateInput {
18
18
  statusCode?: number;
19
19
  }
20
20
  export type RedirectUpdateInput = Partial<RedirectCreateInput>;
21
+ /**
22
+ * The most rows one {@link RedirectsModule.import} takes. Mirrors `ArrayMaxSize` on the plugins
23
+ * service's `ImportRedirectsDto` (`services/plugins/src/redirects/dto/import-redirects.dto.ts`),
24
+ * pinned by `__tests__/redirects.spec.ts`. Change both sides together.
25
+ */
26
+ export declare const REDIRECT_IMPORT_MAX_ROWS = 5000;
27
+ /**
28
+ * How long an import may take before the call gives up, unless the caller says otherwise: the
29
+ * service saves the rows one by one, and a full batch takes far longer than a single write.
30
+ */
31
+ export declare const REDIRECT_IMPORT_TIMEOUT_MS = 60000;
32
+ /**
33
+ * One row of {@link RedirectsModule.import}. Not checked before sending: the service judges each
34
+ * row on its own and reports a bad one in {@link RedirectImportResult.rejected}, so a malformed
35
+ * row never costs the rest of the batch.
36
+ */
37
+ export interface RedirectImportRow {
38
+ /** One exact path, e.g. `/product/old-slug`. */
39
+ oldPath: string;
40
+ /** A path, or an absolute `https://` URL. */
41
+ newPath: string;
42
+ /** 301 (the default), 302, 303, 307 or 308. */
43
+ statusCode?: number;
44
+ }
45
+ /** A row the service refused, and why. The other rows still landed. */
46
+ export interface RedirectImportRejection {
47
+ /** The row's position in the array sent, from 0. */
48
+ index: number;
49
+ /** The row's source path as sent. */
50
+ oldPath: string;
51
+ /**
52
+ * The service's sentence for the person fixing the file: a source that is not one exact path,
53
+ * a destination that is neither a path nor an `https://` URL, a status code outside the five,
54
+ * a source that redirects to itself, or a row that would close a redirect loop or a chain past
55
+ * the hop limit (with the rows already stored or with an earlier row of the same batch). There
56
+ * is no code per row yet.
57
+ */
58
+ message: string;
59
+ }
60
+ /**
61
+ * What an import did. A row whose source the store already redirects UPDATES that redirect
62
+ * (same source, so never a duplicate); any other accepted row creates one.
63
+ */
64
+ export interface RedirectImportResult {
65
+ created: number;
66
+ updated: number;
67
+ /** Empty when every row landed. */
68
+ rejected: RedirectImportRejection[];
69
+ }
70
+ /**
71
+ * `admin.seo.redirects` — the ACTIVE store's URL redirects, on the plugins service. Storefronts read
72
+ * the same rows live from the public redirect map (`@sixtyninecommerce/client/next`'s
73
+ * `createRedirectMiddleware`), so a write here changes what the store's old URLs answer within a
74
+ * few minutes, with no deployment.
75
+ *
76
+ * Every method needs `UpdateChannel` (`FORBIDDEN` → `PermissionDeniedError`, 403, otherwise). The
77
+ * writes that set up a redirect (`create`, `update`, `import`) are refused in an organization's
78
+ * workspace with `ORGANIZATION_WORKSPACE_NOT_A_STORE` (`BusinessRuleError`, 422): a workspace has
79
+ * no storefront. Removing one is never refused there.
80
+ */
21
81
  export interface RedirectsModule {
82
+ /**
83
+ * The store's redirects, newest first, a page at a time (`limit` 1 to 200; 50 by default).
84
+ * Iterate the page (`for await`) to walk every page.
85
+ *
86
+ * @effect read
87
+ * @permission UpdateChannel
88
+ */
22
89
  list(options?: ListOptions): Promise<Page<Redirect>>;
90
+ /**
91
+ * Add one redirect. The service applies the same path rules as `import`: a malformed path, a
92
+ * self-redirect or a status code outside the five is `ValidationError` (400); a source the store
93
+ * already redirects, a loop, or a chain past the service's hop limit is `ConflictError` (409).
94
+ * The idempotency key is honoured: a repeat answers the first response.
95
+ *
96
+ * @effect write
97
+ * @permission UpdateChannel
98
+ */
23
99
  create(input: RedirectCreateInput, options?: Call): Promise<Redirect>;
100
+ /**
101
+ * Change the fields `input` names; at least one. The same rules as `create`; an unknown id is
102
+ * `NotFoundError` (404).
103
+ *
104
+ * @effect write
105
+ * @permission UpdateChannel
106
+ */
24
107
  update(id: string, input: RedirectUpdateInput, options?: Call): Promise<Redirect>;
108
+ /**
109
+ * Remove one redirect: the old URL stops redirecting once the storefront's copy of the map
110
+ * expires. An unknown id is `NotFoundError` (404).
111
+ *
112
+ * @effect destructive
113
+ * @permission UpdateChannel
114
+ */
25
115
  delete(id: string, options?: Call): Promise<void>;
116
+ /**
117
+ * Add or update up to {@link REDIRECT_IMPORT_MAX_ROWS} redirects in one call, matched on the
118
+ * source path (a source the store already redirects is updated, never duplicated). Each row is
119
+ * judged on its own: a bad one is answered in `rejected` by its index and the rest still land.
120
+ * Rows are applied in order, so a later row may build on an earlier one, and a loop split
121
+ * across two rows is refused on the second.
122
+ *
123
+ * Not atomic and not deduplicated by an idempotency key, but safe to repeat: sending the same
124
+ * rows again updates them to what they already are.
125
+ *
126
+ * **Coded refusals:**
127
+ * - `REDIRECT_IMPORT_INVALID` → `ValidationError` (400), before any request: `rows` is not an
128
+ * array of 1 to {@link REDIRECT_IMPORT_MAX_ROWS} entries. Send a larger file in chunks.
129
+ * - `ORGANIZATION_WORKSPACE_NOT_A_STORE` → `BusinessRuleError` (422): the active store is an
130
+ * organization's workspace.
131
+ * - `FORBIDDEN` → `PermissionDeniedError` (403): the credential lacks `UpdateChannel`.
132
+ *
133
+ * Waits up to {@link REDIRECT_IMPORT_TIMEOUT_MS}, longer than the client's default deadline. A
134
+ * timed-out import may still have landed (the service keeps saving): list the redirects, or
135
+ * send the same rows again, which is safe.
136
+ *
137
+ * @effect write
138
+ * @permission UpdateChannel
139
+ */
140
+ import(rows: readonly RedirectImportRow[], options?: Call): Promise<RedirectImportResult>;
141
+ /**
142
+ * The store's redirects as two Nginx `map` blocks (text: `$redirect_uri` and
143
+ * `$redirect_status`, keyed on `$uri`), for a site that is not on the storefront SDK: include
144
+ * it in the server config and redirect on the mapped values. One entry per source path, those
145
+ * a storefront cannot serve included.
146
+ *
147
+ * @effect read
148
+ * @permission UpdateChannel
149
+ */
150
+ exportNginx(options?: Call): Promise<string>;
26
151
  }
27
152
  export declare function createRedirectsModule(transport: Transport): RedirectsModule;
@@ -1,4 +1,25 @@
1
- import { createPage, readRestPage } from '@sixtyninecommerce/core';
1
+ import { createPage, readRestPage, ValidationError, } from '@sixtyninecommerce/core';
2
+ /**
3
+ * The most rows one {@link RedirectsModule.import} takes. Mirrors `ArrayMaxSize` on the plugins
4
+ * service's `ImportRedirectsDto` (`services/plugins/src/redirects/dto/import-redirects.dto.ts`),
5
+ * pinned by `__tests__/redirects.spec.ts`. Change both sides together.
6
+ */
7
+ export const REDIRECT_IMPORT_MAX_ROWS = 5000;
8
+ /**
9
+ * How long an import may take before the call gives up, unless the caller says otherwise: the
10
+ * service saves the rows one by one, and a full batch takes far longer than a single write.
11
+ */
12
+ export const REDIRECT_IMPORT_TIMEOUT_MS = 60_000;
13
+ const IMPORT_INVALID = 'REDIRECT_IMPORT_INVALID';
14
+ /** The import's answer, read defensively: an older service may leave `rejected` out. */
15
+ function importResultOf(body) {
16
+ const record = (typeof body === 'object' && body !== null ? body : {});
17
+ const count = (value) => (typeof value === 'number' ? value : 0);
18
+ const rejected = Array.isArray(record.rejected)
19
+ ? record.rejected
20
+ : [];
21
+ return { created: count(record.created), updated: count(record.updated), rejected };
22
+ }
2
23
  export function createRedirectsModule(transport) {
3
24
  async function list(options) {
4
25
  // `GET /redirects` answers with a bare array unless the request OPTS IN.
@@ -33,5 +54,43 @@ export function createRedirectsModule(transport) {
33
54
  // discards the body rather than pretending it is a Redirect.
34
55
  await transport.rest('plugins', { method: 'DELETE', path: `/redirects/${encodeURIComponent(id)}`, expect: 'void' }, options);
35
56
  },
57
+ async import(rows, options) {
58
+ if (!Array.isArray(rows) || rows.length === 0 || rows.length > REDIRECT_IMPORT_MAX_ROWS) {
59
+ throw new ValidationError({
60
+ code: IMPORT_INVALID,
61
+ message: `Send 1 to ${String(REDIRECT_IMPORT_MAX_ROWS)} redirects per import; split a larger file into several calls.`,
62
+ status: 400,
63
+ service: 'plugins',
64
+ details: [
65
+ {
66
+ field: 'rows',
67
+ code: IMPORT_INVALID,
68
+ message: Array.isArray(rows)
69
+ ? `${String(rows.length)} rows sent.`
70
+ : 'rows is not an array.',
71
+ },
72
+ ],
73
+ });
74
+ }
75
+ const body = await transport.rest('plugins', {
76
+ method: 'POST',
77
+ path: '/redirects/import',
78
+ // Only the three row fields go out: the service's DTO refuses any other key, which
79
+ // would fail the whole batch rather than the one row.
80
+ body: {
81
+ redirects: rows.map((row) => ({
82
+ oldPath: row.oldPath,
83
+ newPath: row.newPath,
84
+ ...(row.statusCode === undefined ? {} : { statusCode: row.statusCode }),
85
+ })),
86
+ },
87
+ }, { timeoutMs: REDIRECT_IMPORT_TIMEOUT_MS, ...options });
88
+ return importResultOf(body);
89
+ },
90
+ async exportNginx(options) {
91
+ // A text body, never JSON: `blob` keeps the transport from parsing or case-converting it.
92
+ const answer = await transport.rest('plugins', { method: 'GET', path: '/redirects/nginx', expect: 'blob' }, options);
93
+ return answer.blob.text();
94
+ },
36
95
  };
37
96
  }