@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.
- package/README.md +5772 -118
- package/dist/auth/index.d.ts +7 -1
- package/dist/auth/index.js +8 -1
- package/dist/catalog/assets.d.ts +120 -8
- package/dist/catalog/assets.js +98 -12
- package/dist/catalog/collections.d.ts +279 -14
- package/dist/catalog/collections.js +246 -15
- package/dist/catalog/documents.d.ts +142 -0
- package/dist/catalog/documents.js +114 -0
- package/dist/catalog/index.d.ts +54 -6
- package/dist/catalog/index.js +10 -1
- package/dist/catalog/metadata.d.ts +244 -0
- package/dist/catalog/metadata.js +220 -0
- package/dist/catalog/products.d.ts +159 -19
- package/dist/catalog/products.js +88 -13
- package/dist/catalog/properties.d.ts +140 -14
- package/dist/catalog/properties.js +65 -1
- package/dist/catalog/recommendations.d.ts +288 -0
- package/dist/catalog/recommendations.js +237 -0
- package/dist/catalog/reviews.d.ts +381 -0
- package/dist/catalog/reviews.js +325 -0
- package/dist/catalog/types.d.ts +17 -1
- package/dist/catalog/types.js +10 -0
- package/dist/catalog/variant-find.d.ts +136 -0
- package/dist/catalog/variant-find.js +239 -0
- package/dist/catalog/variants.d.ts +293 -26
- package/dist/catalog/variants.js +164 -7
- package/dist/catalog/visibility.d.ts +38 -7
- package/dist/catalog/visibility.js +19 -1
- package/dist/client.d.ts +121 -9
- package/dist/client.js +53 -14
- package/dist/content/assets.d.ts +285 -0
- package/dist/content/assets.js +181 -0
- package/dist/content/forms.d.ts +332 -1
- package/dist/content/forms.js +140 -4
- package/dist/content/items.d.ts +292 -0
- package/dist/content/items.js +170 -0
- package/dist/content/menus.d.ts +254 -0
- package/dist/content/menus.js +70 -0
- package/dist/content/strings.d.ts +137 -0
- package/dist/content/strings.js +82 -0
- package/dist/content/types.d.ts +282 -0
- package/dist/content/types.js +100 -0
- package/dist/customers/age-verification.d.ts +66 -0
- package/dist/customers/age-verification.js +79 -0
- package/dist/customers/credit.d.ts +263 -0
- package/dist/customers/credit.js +244 -0
- package/dist/customers/delivery-preferences.d.ts +120 -0
- package/dist/customers/delivery-preferences.js +97 -0
- package/dist/customers/delivery-terms.d.ts +44 -0
- package/dist/customers/delivery-terms.js +32 -0
- package/dist/customers/groups.d.ts +207 -0
- package/dist/customers/groups.js +438 -0
- package/dist/customers/index.d.ts +462 -37
- package/dist/customers/index.js +317 -40
- package/dist/customers/invitation-status.d.ts +57 -0
- package/dist/customers/invitation-status.js +21 -0
- package/dist/customers/notes.d.ts +67 -0
- package/dist/customers/notes.js +51 -0
- package/dist/customers/notification-recipients.d.ts +97 -0
- package/dist/customers/notification-recipients.js +84 -0
- package/dist/customers/saved-lists.d.ts +42 -0
- package/dist/customers/saved-lists.js +61 -0
- package/dist/customers/vat.d.ts +83 -0
- package/dist/customers/vat.js +46 -0
- package/dist/customers/wire.d.ts +20 -0
- package/dist/customers/wire.js +35 -0
- package/dist/documents/ar.d.ts +307 -0
- package/dist/documents/ar.js +254 -0
- package/dist/documents/emails.d.ts +150 -0
- package/dist/documents/emails.js +220 -0
- package/dist/documents/index.d.ts +744 -21
- package/dist/documents/index.js +438 -24
- package/dist/documents/pdf.d.ts +2 -0
- package/dist/documents/pdf.js +11 -0
- package/dist/fulfillments/create.d.ts +167 -0
- package/dist/fulfillments/create.js +216 -0
- package/dist/fulfillments/index.d.ts +163 -11
- package/dist/fulfillments/index.js +135 -6
- package/dist/fulfillments/payment-policy.d.ts +64 -0
- package/dist/fulfillments/payment-policy.js +140 -0
- package/dist/fulfillments/warehouse.d.ts +724 -0
- package/dist/fulfillments/warehouse.js +914 -0
- package/dist/geo/index.d.ts +119 -0
- package/dist/geo/index.js +107 -0
- package/dist/index.d.ts +147 -39
- package/dist/index.js +51 -12
- package/dist/integrations/afas-actions.d.ts +234 -0
- package/dist/integrations/afas-actions.js +310 -0
- package/dist/integrations/afas-types.d.ts +483 -0
- package/dist/integrations/afas-types.js +5 -0
- package/dist/integrations/afas.d.ts +234 -0
- package/dist/integrations/afas.js +290 -0
- package/dist/integrations/connections.d.ts +103 -0
- package/dist/integrations/connections.js +55 -0
- package/dist/integrations/esp.d.ts +313 -0
- package/dist/integrations/esp.js +169 -0
- package/dist/integrations/exact-online-types.d.ts +965 -0
- package/dist/integrations/exact-online-types.js +6 -0
- package/dist/integrations/exact-online.d.ts +784 -0
- package/dist/integrations/exact-online.js +1089 -0
- package/dist/integrations/index.d.ts +44 -3
- package/dist/integrations/index.js +23 -3
- package/dist/integrations/pipeline-discoveries.d.ts +65 -0
- package/dist/integrations/pipeline-discoveries.js +65 -0
- package/dist/integrations/pipeline-keys.d.ts +13 -0
- package/dist/integrations/pipeline-keys.js +22 -0
- package/dist/integrations/pipeline-runs.d.ts +119 -0
- package/dist/integrations/pipeline-runs.js +87 -0
- package/dist/integrations/pipelines.d.ts +87 -15
- package/dist/integrations/pipelines.js +41 -22
- package/dist/integrations/price-import.d.ts +223 -0
- package/dist/integrations/price-import.js +124 -0
- package/dist/integrations/types.d.ts +367 -3
- package/dist/integrations/types.js +4 -3
- package/dist/locations/index.d.ts +66 -0
- package/dist/locations/index.js +31 -0
- package/dist/meta/index.d.ts +172 -0
- package/dist/metafields/index.d.ts +8 -1
- package/dist/metafields/index.js +10 -0
- package/dist/orders/cancel-with-money.d.ts +237 -0
- package/dist/orders/cancel-with-money.js +155 -0
- package/dist/orders/commit-edit.d.ts +23 -1
- package/dist/orders/commit-edit.js +19 -2
- package/dist/orders/deposit-policy.d.ts +3 -1
- package/dist/orders/drafts.d.ts +800 -0
- package/dist/orders/drafts.js +771 -0
- package/dist/orders/fields.d.ts +245 -0
- package/dist/orders/fields.js +188 -0
- package/dist/orders/get.d.ts +29 -19
- package/dist/orders/get.js +17 -8
- package/dist/orders/index.d.ts +66 -15
- package/dist/orders/index.js +25 -2
- package/dist/orders/lifecycle.d.ts +128 -0
- package/dist/orders/lifecycle.js +158 -0
- package/dist/orders/line-price-preview.d.ts +7 -2
- package/dist/orders/list.d.ts +243 -0
- package/dist/orders/list.js +487 -0
- package/dist/orders/manual-discounts.d.ts +13 -5
- package/dist/orders/manual-pricing.d.ts +5 -0
- package/dist/orders/money-view.d.ts +149 -0
- package/dist/orders/money-view.js +99 -0
- package/dist/orders/order-entry.d.ts +20 -7
- package/dist/orders/plan-edit.d.ts +34 -2
- package/dist/orders/plan-edit.js +112 -11
- package/dist/orders/settings.d.ts +201 -0
- package/dist/orders/settings.js +325 -0
- package/dist/orders/status.d.ts +159 -13
- package/dist/orders/status.js +21 -0
- package/dist/orders/surcharges.d.ts +20 -1
- package/dist/orders/surcharges.js +37 -11
- package/dist/payments/account-credit.d.ts +46 -0
- package/dist/payments/account-credit.js +42 -0
- package/dist/payments/ar.d.ts +292 -0
- package/dist/payments/ar.js +223 -0
- package/dist/payments/connected-account.d.ts +47 -0
- package/dist/payments/connected-account.js +61 -0
- package/dist/payments/credit-noted-fields.d.ts +19 -0
- package/dist/payments/credit-noted-fields.js +69 -0
- package/dist/payments/customer-consent.d.ts +30 -0
- package/dist/payments/customer-consent.js +49 -0
- package/dist/payments/index.d.ts +340 -19
- package/dist/payments/index.js +151 -1
- package/dist/payments/methods.d.ts +303 -8
- package/dist/payments/methods.js +337 -7
- package/dist/payments/mollie-methods.d.ts +159 -0
- package/dist/payments/mollie-methods.js +141 -0
- package/dist/payments/payment-links.d.ts +180 -0
- package/dist/payments/payment-links.js +97 -0
- package/dist/payments/provider-keys.d.ts +150 -0
- package/dist/payments/provider-keys.js +118 -0
- package/dist/payments/providers.d.ts +86 -11
- package/dist/payments/providers.js +29 -0
- package/dist/payments/requests.d.ts +147 -0
- package/dist/payments/requests.js +93 -0
- package/dist/payments/store-credit-fields.d.ts +15 -0
- package/dist/payments/store-credit-fields.js +51 -0
- package/dist/pickups/index.d.ts +106 -0
- package/dist/pickups/index.js +108 -0
- package/dist/platform/channels.d.ts +132 -0
- package/dist/platform/channels.js +128 -0
- package/dist/platform/email-gateways.d.ts +165 -0
- package/dist/platform/email-gateways.js +78 -0
- package/dist/platform/geo.d.ts +123 -0
- package/dist/platform/geo.js +144 -0
- package/dist/platform/tax.d.ts +162 -0
- package/dist/platform/tax.js +169 -0
- package/dist/pos/anomalies.d.ts +158 -0
- package/dist/pos/anomalies.js +64 -0
- package/dist/pos/assisted-selling.d.ts +79 -0
- package/dist/pos/assisted-selling.js +38 -0
- package/dist/pos/exchanges.d.ts +108 -0
- package/dist/pos/exchanges.js +22 -0
- package/dist/pos/gift-cards.d.ts +135 -0
- package/dist/pos/gift-cards.js +47 -0
- package/dist/pos/index.d.ts +2370 -58
- package/dist/pos/index.js +854 -7
- package/dist/pos/no-receipt.d.ts +94 -0
- package/dist/pos/no-receipt.js +25 -0
- package/dist/pos/offline-sales.d.ts +228 -0
- package/dist/pos/offline-sales.js +170 -0
- package/dist/pos/pay-by-link.d.ts +143 -0
- package/dist/pos/pay-by-link.js +57 -0
- package/dist/pos/quick-keys.d.ts +103 -0
- package/dist/pos/quick-keys.js +45 -0
- package/dist/pos/reports.d.ts +360 -0
- package/dist/pos/reports.js +62 -0
- package/dist/pos/returns.d.ts +244 -0
- package/dist/pos/returns.js +83 -0
- package/dist/pricing/bulk-coupons.d.ts +139 -0
- package/dist/pricing/bulk-coupons.js +112 -0
- package/dist/pricing/index.d.ts +174 -4
- package/dist/pricing/index.js +174 -1
- package/dist/pricing/price-lists.d.ts +606 -0
- package/dist/pricing/price-lists.js +600 -0
- package/dist/purchasing/alerts.d.ts +104 -0
- package/dist/purchasing/alerts.js +88 -0
- package/dist/purchasing/backorders.d.ts +61 -0
- package/dist/purchasing/backorders.js +92 -0
- package/dist/purchasing/cost.d.ts +110 -0
- package/dist/purchasing/cost.js +86 -0
- package/dist/purchasing/index.d.ts +54 -0
- package/dist/purchasing/index.js +24 -0
- package/dist/purchasing/links.d.ts +263 -0
- package/dist/purchasing/links.js +192 -0
- package/dist/purchasing/purchase-orders.d.ts +551 -0
- package/dist/purchasing/purchase-orders.js +491 -0
- package/dist/purchasing/receipts.d.ts +396 -0
- package/dist/purchasing/receipts.js +287 -0
- package/dist/purchasing/replenishment.d.ts +281 -0
- package/dist/purchasing/replenishment.js +240 -0
- package/dist/purchasing/returns.d.ts +273 -0
- package/dist/purchasing/returns.js +229 -0
- package/dist/purchasing/settings.d.ts +152 -0
- package/dist/purchasing/settings.js +181 -0
- package/dist/purchasing/suppliers.d.ts +476 -0
- package/dist/purchasing/suppliers.js +438 -0
- package/dist/quotes/index.d.ts +518 -0
- package/dist/quotes/index.js +439 -0
- package/dist/reports/index.d.ts +71 -8
- package/dist/reports/index.js +74 -11
- package/dist/returns/index.d.ts +675 -21
- package/dist/returns/index.js +756 -11
- package/dist/search/index.d.ts +76 -1
- package/dist/search/index.js +63 -0
- package/dist/seo/config.d.ts +128 -0
- package/dist/seo/config.js +95 -0
- package/dist/seo/redirects.d.ts +125 -0
- package/dist/seo/redirects.js +60 -1
- package/dist/seo/settings.d.ts +147 -0
- package/dist/seo/settings.js +209 -0
- package/dist/shipping/definitions.d.ts +29 -0
- package/dist/shipping/definitions.js +47 -0
- package/dist/shipping/fulfillment-profiles.d.ts +174 -0
- package/dist/shipping/fulfillment-profiles.js +104 -0
- package/dist/shipping/hendriks.d.ts +97 -0
- package/dist/shipping/hendriks.js +50 -0
- package/dist/shipping/index.d.ts +54 -0
- package/dist/shipping/index.js +27 -0
- package/dist/shipping/methods.d.ts +205 -0
- package/dist/shipping/methods.js +240 -0
- package/dist/shipping/myparcel.d.ts +668 -0
- package/dist/shipping/myparcel.js +466 -0
- package/dist/shipping/parcel-count-suggestion.d.ts +2 -0
- package/dist/shipping/parcel-rule.d.ts +6 -5
- package/dist/shipping/parcel-rule.js +21 -23
- package/dist/shipping/rate-tables.d.ts +324 -0
- package/dist/shipping/rate-tables.js +390 -0
- package/dist/shipping/ship-to-countries.d.ts +75 -0
- package/dist/shipping/ship-to-countries.js +47 -0
- package/dist/stock/by-location.d.ts +86 -0
- package/dist/stock/by-location.js +94 -0
- package/dist/stock/index.d.ts +18 -4
- package/dist/stock/index.js +7 -0
- package/dist/stock/locations.d.ts +144 -0
- package/dist/stock/locations.js +168 -0
- package/dist/stock/settings.d.ts +106 -0
- package/dist/stock/settings.js +103 -0
- package/dist/stock/transfers.d.ts +386 -52
- package/dist/stock/transfers.js +222 -17
- package/dist/stored-value/index.d.ts +312 -11
- package/dist/stored-value/index.js +223 -4
- package/dist/subscriptions/index.d.ts +682 -0
- package/dist/subscriptions/index.js +815 -0
- package/dist/tax/display-policy.d.ts +105 -0
- package/dist/tax/display-policy.js +84 -0
- package/dist/tax/index.d.ts +79 -1
- package/dist/tax/index.js +52 -0
- package/dist/tenant/email-style.d.ts +102 -0
- package/dist/tenant/email-style.js +72 -0
- package/dist/tenant/email-templates.d.ts +287 -0
- package/dist/tenant/email-templates.js +271 -0
- package/dist/tenant/notifications.d.ts +23 -17
- package/dist/tenant/settings-sources.d.ts +1 -1
- package/dist/tenant/time-zone.d.ts +17 -0
- package/dist/tenant/time-zone.js +11 -0
- package/dist/types.d.ts +50 -3
- package/dist/users/index.d.ts +3 -0
- package/dist/users/index.js +2 -0
- package/dist/users/warehouses.d.ts +50 -0
- package/dist/users/warehouses.js +51 -0
- package/package.json +2 -2
package/dist/search/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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;
|
package/dist/search/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/seo/redirects.d.ts
CHANGED
|
@@ -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;
|
package/dist/seo/redirects.js
CHANGED
|
@@ -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
|
}
|