@sixtyninecommerce/admin 0.11.0 → 0.13.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 (153) hide show
  1. package/README.md +1509 -28
  2. package/dist/auth/index.d.ts +7 -1
  3. package/dist/auth/index.js +8 -1
  4. package/dist/catalog/documents.d.ts +142 -0
  5. package/dist/catalog/documents.js +114 -0
  6. package/dist/catalog/index.d.ts +48 -7
  7. package/dist/catalog/index.js +6 -1
  8. package/dist/catalog/metadata.d.ts +244 -0
  9. package/dist/catalog/metadata.js +220 -0
  10. package/dist/catalog/properties.d.ts +38 -0
  11. package/dist/catalog/properties.js +52 -1
  12. package/dist/catalog/recommendations.d.ts +33 -6
  13. package/dist/catalog/recommendations.js +94 -2
  14. package/dist/catalog/reviews.d.ts +17 -4
  15. package/dist/catalog/reviews.js +7 -0
  16. package/dist/catalog/variants.d.ts +60 -0
  17. package/dist/catalog/variants.js +39 -0
  18. package/dist/catalog/visibility.d.ts +18 -2
  19. package/dist/client.d.ts +29 -3
  20. package/dist/client.js +19 -8
  21. package/dist/content/assets.d.ts +3 -4
  22. package/dist/content/forms.d.ts +332 -1
  23. package/dist/content/forms.js +140 -4
  24. package/dist/content/items.d.ts +31 -8
  25. package/dist/content/items.js +1 -1
  26. package/dist/content/menus.d.ts +254 -0
  27. package/dist/content/menus.js +70 -0
  28. package/dist/content/strings.d.ts +137 -0
  29. package/dist/content/strings.js +82 -0
  30. package/dist/customers/delivery-terms.d.ts +44 -0
  31. package/dist/customers/delivery-terms.js +32 -0
  32. package/dist/customers/groups.d.ts +16 -5
  33. package/dist/customers/groups.js +6 -2
  34. package/dist/customers/index.d.ts +15 -0
  35. package/dist/customers/index.js +4 -0
  36. package/dist/documents/emails.d.ts +154 -0
  37. package/dist/documents/emails.js +220 -0
  38. package/dist/documents/index.d.ts +13 -0
  39. package/dist/documents/index.js +2 -0
  40. package/dist/fulfillments/create.d.ts +1 -1
  41. package/dist/fulfillments/create.js +5 -2
  42. package/dist/fulfillments/index.d.ts +119 -1
  43. package/dist/fulfillments/index.js +124 -4
  44. package/dist/fulfillments/warehouse.d.ts +29 -17
  45. package/dist/fulfillments/warehouse.js +32 -1
  46. package/dist/index.d.ts +43 -23
  47. package/dist/index.js +11 -5
  48. package/dist/integrations/exact-online-types.d.ts +542 -0
  49. package/dist/integrations/exact-online.d.ts +541 -5
  50. package/dist/integrations/exact-online.js +714 -1
  51. package/dist/integrations/index.d.ts +23 -8
  52. package/dist/integrations/index.js +9 -4
  53. package/dist/integrations/pipeline-discoveries.d.ts +65 -0
  54. package/dist/integrations/pipeline-discoveries.js +65 -0
  55. package/dist/integrations/pipeline-runs.d.ts +119 -0
  56. package/dist/integrations/pipeline-runs.js +87 -0
  57. package/dist/integrations/pipelines.d.ts +73 -2
  58. package/dist/integrations/pipelines.js +36 -1
  59. package/dist/integrations/price-import.d.ts +223 -0
  60. package/dist/integrations/price-import.js +124 -0
  61. package/dist/integrations/types.d.ts +221 -3
  62. package/dist/integrations/types.js +4 -3
  63. package/dist/meta/index.d.ts +10 -0
  64. package/dist/metafields/index.d.ts +8 -1
  65. package/dist/metafields/index.js +10 -0
  66. package/dist/orders/cancel-with-money.d.ts +10 -0
  67. package/dist/orders/cancel-with-money.js +2 -0
  68. package/dist/orders/commit-edit.d.ts +16 -0
  69. package/dist/orders/commit-edit.js +14 -1
  70. package/dist/orders/deposit-policy.d.ts +3 -1
  71. package/dist/orders/index.d.ts +22 -11
  72. package/dist/orders/index.js +5 -2
  73. package/dist/orders/list.d.ts +26 -5
  74. package/dist/orders/list.js +123 -13
  75. package/dist/orders/manual-discounts.d.ts +13 -5
  76. package/dist/orders/money-view.d.ts +149 -0
  77. package/dist/orders/money-view.js +99 -0
  78. package/dist/orders/plan-edit.d.ts +34 -2
  79. package/dist/orders/plan-edit.js +110 -12
  80. package/dist/orders/surcharges.d.ts +12 -0
  81. package/dist/orders/surcharges.js +34 -9
  82. package/dist/payments/account-credit.d.ts +17 -1
  83. package/dist/payments/account-credit.js +25 -2
  84. package/dist/payments/credit-noted-fields.d.ts +8 -1
  85. package/dist/payments/credit-noted-fields.js +34 -2
  86. package/dist/payments/customer-consent.d.ts +30 -0
  87. package/dist/payments/customer-consent.js +49 -0
  88. package/dist/payments/index.d.ts +89 -14
  89. package/dist/payments/index.js +22 -2
  90. package/dist/payments/requests.d.ts +6 -3
  91. package/dist/payments/requests.js +1 -0
  92. package/dist/pickups/index.d.ts +5 -3
  93. package/dist/pickups/index.js +5 -2
  94. package/dist/platform/email-gateways.d.ts +165 -0
  95. package/dist/platform/email-gateways.js +78 -0
  96. package/dist/platform/tax.d.ts +11 -3
  97. package/dist/platform/tax.js +30 -0
  98. package/dist/pos/exchanges.d.ts +6 -1
  99. package/dist/pos/index.d.ts +229 -10
  100. package/dist/pos/index.js +113 -4
  101. package/dist/pos/offline-sales.d.ts +44 -3
  102. package/dist/pos/offline-sales.js +57 -3
  103. package/dist/pos/returns.d.ts +28 -1
  104. package/dist/pos/returns.js +36 -6
  105. package/dist/pricing/price-lists.d.ts +3 -1
  106. package/dist/purchasing/alerts.d.ts +19 -4
  107. package/dist/purchasing/alerts.js +29 -3
  108. package/dist/purchasing/backorders.d.ts +61 -0
  109. package/dist/purchasing/backorders.js +92 -0
  110. package/dist/purchasing/index.d.ts +21 -3
  111. package/dist/purchasing/index.js +8 -0
  112. package/dist/purchasing/links.d.ts +263 -0
  113. package/dist/purchasing/links.js +192 -0
  114. package/dist/purchasing/purchase-orders.d.ts +4 -1
  115. package/dist/purchasing/receipts.d.ts +396 -0
  116. package/dist/purchasing/receipts.js +287 -0
  117. package/dist/purchasing/returns.d.ts +273 -0
  118. package/dist/purchasing/returns.js +229 -0
  119. package/dist/quotes/index.d.ts +20 -3
  120. package/dist/returns/index.d.ts +62 -1
  121. package/dist/returns/index.js +61 -0
  122. package/dist/seo/config.d.ts +128 -0
  123. package/dist/seo/config.js +95 -0
  124. package/dist/seo/redirects.d.ts +125 -0
  125. package/dist/seo/redirects.js +60 -1
  126. package/dist/seo/settings.d.ts +147 -0
  127. package/dist/seo/settings.js +209 -0
  128. package/dist/shipping/index.d.ts +6 -0
  129. package/dist/shipping/index.js +2 -0
  130. package/dist/shipping/methods.d.ts +8 -1
  131. package/dist/shipping/myparcel.d.ts +415 -1
  132. package/dist/shipping/myparcel.js +291 -3
  133. package/dist/shipping/rate-tables.d.ts +149 -4
  134. package/dist/shipping/rate-tables.js +229 -23
  135. package/dist/shipping/ship-to-countries.d.ts +75 -0
  136. package/dist/shipping/ship-to-countries.js +47 -0
  137. package/dist/stock/by-location.d.ts +86 -0
  138. package/dist/stock/by-location.js +94 -0
  139. package/dist/stock/index.d.ts +8 -1
  140. package/dist/stock/index.js +4 -0
  141. package/dist/stock/settings.d.ts +106 -0
  142. package/dist/stock/settings.js +103 -0
  143. package/dist/stock/transfers.d.ts +171 -10
  144. package/dist/stock/transfers.js +82 -4
  145. package/dist/stored-value/index.d.ts +98 -7
  146. package/dist/stored-value/index.js +71 -4
  147. package/dist/tax/display-policy.d.ts +105 -0
  148. package/dist/tax/display-policy.js +84 -0
  149. package/dist/tax/index.d.ts +3 -0
  150. package/dist/tax/index.js +2 -0
  151. package/dist/tenant/email-templates.d.ts +222 -1
  152. package/dist/tenant/email-templates.js +227 -10
  153. package/package.json +2 -2
@@ -0,0 +1,220 @@
1
+ /**
2
+ * A product's SEO, social and AI-agent metadata (headless spec §9.2 `catalog`: "metadata";
3
+ * W10-10): what the storefront puts in a product page's `<head>`, its share cards and its
4
+ * structured data. One record per product and store; its texts are per language.
5
+ *
6
+ * - **Partial writes.** `upsert` changes only the fields it names. A translation is written per
7
+ * language: a language not sent keeps its values, and a field not sent in a sent language keeps
8
+ * its value (send `null` to clear one).
9
+ * - **Coverage.** `coverage` lists the store's products with their own meta title and description
10
+ * in one language, optionally only those missing one, filtered by the engine BEFORE the page is
11
+ * cut: every page is full and `totalItems` counts the filter.
12
+ * - **Bulk.** `bulkUpsert` writes many products, each through `upsert`'s rules in a transaction
13
+ * of its own and reported on its own (ruling G117).
14
+ *
15
+ * Every record is the store's own: a product of another store is never answered and never
16
+ * written.
17
+ *
18
+ * Permissions: `get` and `coverage` need `ReadCatalog`, the writes `UpdateCatalog`.
19
+ *
20
+ * Refusals, GraphQL errors mapped by `core`:
21
+ * - `PRODUCT_METADATA_INPUT_INVALID` → `ValidationError` (400): an input that breaks a rule (a
22
+ * length, an allowed value, an `https://` URL, an instant without an offset, a language sent
23
+ * twice). `details` names each failure (`field` an RFC 6901 pointer such as
24
+ * `/input/translations/0/seoTitle`, `code` the rule such as `INVALID_FIELD_LENGTH`). Nothing is
25
+ * written.
26
+ * - `ENTITY_NOT_FOUND` → `NotFoundError` (404): `upsert` of a product the store does not hold.
27
+ * - `TRANSLATION_LANGUAGE_NOT_OFFERED` → `ValidationError` (400): a new translation in a language
28
+ * the store does not offer.
29
+ * - `BULK_ACTION_INPUT_INVALID` → `ValidationError` (400): a `bulkUpsert` of no product, more than
30
+ * {@link MAX_BULK_METADATA_EDITS} or one product twice, refused before anything is sent.
31
+ * - `INVALID_DATETIME` → `ValidationError` (400): `metaRobotsUnavailableAfter` without an offset,
32
+ * refused before anything is sent.
33
+ * - `FORBIDDEN` → `PermissionDeniedError` (403) without the permission.
34
+ *
35
+ * Release: `coverage` and `bulkUpsert` need a platform release with `productMetadataCoverage` and
36
+ * `bulkUpsertProductMetadata` (deploy vendure-server first: an older one refuses the document as
37
+ * `GRAPHQL_VALIDATION_FAILED`); on such a release `upsert` also answers a validation failure as an
38
+ * uncoded `INTERNAL_SERVER_ERROR` and writes a row for a product the store does not hold. `get`
39
+ * and `delete` are served by every release this SDK supports. No method deduplicates on
40
+ * `idempotencyKey`; every write is naturally repeatable.
41
+ */
42
+ import { NotFoundError, UpstreamError, ValidationError, } from '@sixtyninecommerce/core';
43
+ import { assertDeleted, assertInstant, DELETION_FIELDS, fieldOf, vendureListPage, } from './types.js';
44
+ /** How many products one `bulkUpsert` call may name. */
45
+ export const MAX_BULK_METADATA_EDITS = 200;
46
+ const IMAGE_FIELDS = /* GraphQL */ `
47
+ id
48
+ name
49
+ preview
50
+ source
51
+ type
52
+ mimeType
53
+ `;
54
+ const METADATA_FIELDS = /* GraphQL */ `
55
+ id
56
+ productId
57
+ channelId
58
+ metaRobotsIndex
59
+ metaRobotsFollow
60
+ metaRobotsNoarchive
61
+ metaRobotsNosnippet
62
+ metaRobotsNoimageindex
63
+ metaRobotsMaxImagePreview
64
+ metaRobotsMaxSnippet
65
+ metaRobotsMaxVideoPreview
66
+ metaRobotsUnavailableAfter
67
+ socialImageId
68
+ socialImage { ${IMAGE_FIELDS} }
69
+ twitterImageOverrideId
70
+ twitterImageOverride { ${IMAGE_FIELDS} }
71
+ twitterCardType
72
+ socialOgType
73
+ seoIncludeReviewsInJsonld
74
+ aiSummaryAudience
75
+ aiBotOverrides
76
+ qaEmitFaqpageJsonld
77
+ hreflangOverrides
78
+ resolvedBotDirectives
79
+ translations {
80
+ id
81
+ languageCode
82
+ seoTitle
83
+ seoDescription
84
+ canonicalUrl
85
+ breadcrumbOverride
86
+ focusKeywords
87
+ socialTitle
88
+ socialDescription
89
+ socialImageAlt
90
+ twitterTitleOverride
91
+ twitterDescriptionOverride
92
+ aiSummary
93
+ qaPairs
94
+ }
95
+ `;
96
+ export const PRODUCT_METADATA_QUERY = /* GraphQL */ `
97
+ query CatalogProductMetadata($productId: ID!) {
98
+ product(id: $productId) {
99
+ id
100
+ metadata { ${METADATA_FIELDS} }
101
+ }
102
+ }
103
+ `;
104
+ export const UPSERT_PRODUCT_METADATA_MUTATION = /* GraphQL */ `
105
+ mutation CatalogUpsertProductMetadata($productId: ID!, $input: ProductMetadataInput!) {
106
+ upsertProductMetadata(productId: $productId, input: $input) { ${METADATA_FIELDS} }
107
+ }
108
+ `;
109
+ export const DELETE_PRODUCT_METADATA_MUTATION = /* GraphQL */ `
110
+ mutation CatalogDeleteProductMetadata($productId: ID!) {
111
+ deleteProductMetadata(productId: $productId) { ${DELETION_FIELDS} }
112
+ }
113
+ `;
114
+ export const PRODUCT_METADATA_COVERAGE_QUERY = /* GraphQL */ `
115
+ query CatalogProductMetadataCoverage(
116
+ $languageCode: LanguageCode!
117
+ $filter: ProductMetadataCoverageFilter
118
+ $term: String
119
+ $options: ProductMetadataCoverageListOptions
120
+ ) {
121
+ productMetadataCoverage(
122
+ languageCode: $languageCode
123
+ filter: $filter
124
+ term: $term
125
+ options: $options
126
+ ) {
127
+ items {
128
+ productId
129
+ name
130
+ slug
131
+ enabled
132
+ seoTitle
133
+ seoDescription
134
+ }
135
+ totalItems
136
+ }
137
+ }
138
+ `;
139
+ export const BULK_UPSERT_PRODUCT_METADATA_MUTATION = /* GraphQL */ `
140
+ mutation CatalogBulkUpsertProductMetadata($edits: [ProductMetadataBulkEdit!]!) {
141
+ bulkUpsertProductMetadata(edits: $edits) {
142
+ items {
143
+ productId
144
+ status
145
+ code
146
+ message
147
+ }
148
+ }
149
+ }
150
+ `;
151
+ function bulkInvalid(message) {
152
+ return new ValidationError({
153
+ code: 'BULK_ACTION_INPUT_INVALID',
154
+ message,
155
+ status: 400,
156
+ service: 'vendure',
157
+ });
158
+ }
159
+ /** The edits as sent, refused before the request leaves when the engine would refuse them. */
160
+ function bulkEdits(edits) {
161
+ if (!Array.isArray(edits))
162
+ throw bulkInvalid('edits must be a list.');
163
+ if (edits.length === 0)
164
+ throw bulkInvalid('Name at least one product.');
165
+ if (edits.length > MAX_BULK_METADATA_EDITS) {
166
+ throw bulkInvalid(`A bulk upsert takes at most ${String(MAX_BULK_METADATA_EDITS)} products; this one names ${String(edits.length)}.`);
167
+ }
168
+ const ids = edits.map((edit) => String(edit.productId).toLowerCase());
169
+ if (new Set(ids).size !== ids.length)
170
+ throw bulkInvalid('Name each product once.');
171
+ for (const edit of edits) {
172
+ assertInstant('metaRobotsUnavailableAfter', edit.input.metaRobotsUnavailableAfter);
173
+ }
174
+ return edits.map((edit) => ({ productId: edit.productId, input: edit.input }));
175
+ }
176
+ export function createProductMetadataModule(transport) {
177
+ return {
178
+ async get(productId, options) {
179
+ const product = await fieldOf(transport, PRODUCT_METADATA_QUERY, 'product', { productId }, options);
180
+ if (!product) {
181
+ throw new NotFoundError({
182
+ code: 'PRODUCT_NOT_FOUND',
183
+ message: `This store has no product ${productId}.`,
184
+ status: 404,
185
+ service: 'vendure',
186
+ });
187
+ }
188
+ return product.metadata;
189
+ },
190
+ async upsert(productId, input, options) {
191
+ assertInstant('metaRobotsUnavailableAfter', input.metaRobotsUnavailableAfter);
192
+ return fieldOf(transport, UPSERT_PRODUCT_METADATA_MUTATION, 'upsertProductMetadata', { productId, input }, options);
193
+ },
194
+ async delete(productId, options) {
195
+ const response = await fieldOf(transport, DELETE_PRODUCT_METADATA_MUTATION, 'deleteProductMetadata', { productId }, options);
196
+ assertDeleted('PRODUCT_METADATA_NOT_DELETED', [response]);
197
+ },
198
+ async coverage(query) {
199
+ const { languageCode, filter, term, ...options } = query;
200
+ return vendureListPage(transport, PRODUCT_METADATA_COVERAGE_QUERY, 'productMetadataCoverage', (skip, take) => ({
201
+ languageCode,
202
+ filter: filter ?? 'ALL',
203
+ ...(term === undefined || term.trim() === '' ? {} : { term }),
204
+ options: { skip, take },
205
+ }), options);
206
+ },
207
+ async bulkUpsert(edits, options) {
208
+ const data = await fieldOf(transport, BULK_UPSERT_PRODUCT_METADATA_MUTATION, 'bulkUpsertProductMetadata', { edits: bulkEdits(edits) }, options);
209
+ if (data === null) {
210
+ throw new UpstreamError({
211
+ code: 'UNEXPECTED_RESPONSE',
212
+ message: 'The engine answered bulkUpsertProductMetadata with nothing.',
213
+ status: 502,
214
+ service: 'vendure',
215
+ });
216
+ }
217
+ return data.items;
218
+ },
219
+ };
220
+ }
@@ -319,4 +319,42 @@ export declare const SET_PRODUCT_PROPERTY_VALUES_MUTATION = "\n mutation Catalo
319
319
  export declare const SYNC_PROPERTY_FACETS_MUTATION = "\n mutation CatalogSyncPropertyFacets($propertyId: ID!) {\n syncPropertyFacets(propertyId: $propertyId) {\n propertyId\n items\n unresolved\n fieldFacets\n }\n }\n";
320
320
  /** `catalog.syncContentFacets`; its contract is on {@link CatalogModule.syncContentFacets}. */
321
321
  export declare function syncContentFacets(transport: Transport, propertyId: string, options?: Call): Promise<ContentFacetSyncResult>;
322
+ /**
323
+ * The most different content item ids one {@link CatalogModule.syncContentItemFacets} call takes
324
+ * (the engine's own bound); repeats count once.
325
+ */
326
+ export declare const MAX_CONTENT_ITEM_FACET_SYNC_IDS = 500;
327
+ /** A property {@link CatalogModule.syncContentItemFacets} did not sync, and why. */
328
+ export interface ContentItemFacetSyncSkip {
329
+ propertyId: string;
330
+ /**
331
+ * The refusal the property's own sync gave: `MANAGED_BY_ORGANIZATION` for a property of the
332
+ * store's organization workspace (it is synced in the workspace), or any refusal
333
+ * {@link CatalogModule.syncContentFacets} documents.
334
+ */
335
+ code: string;
336
+ /** The refusal's sentence, as the engine words it. */
337
+ message: string;
338
+ }
339
+ /** What {@link CatalogModule.syncContentItemFacets} did. */
340
+ export interface ContentItemFacetSyncResult {
341
+ /**
342
+ * Each filterable `content_item` property of the store whose values name at least one of the
343
+ * items, synced as {@link CatalogModule.syncContentFacets} syncs it.
344
+ */
345
+ synced: ContentFacetSyncResult[];
346
+ /** Each such property that was refused; nothing of it was written, the others were synced. */
347
+ skipped: ContentItemFacetSyncSkip[];
348
+ /**
349
+ * Whether the call wrote anything: a filter value created, renamed or removed, or a product's
350
+ * filter assignments changed. False on a repeat that found everything in line.
351
+ */
352
+ changed: boolean;
353
+ }
354
+ export declare const SYNC_CONTENT_ITEM_FACETS_MUTATION = "\n mutation CatalogSyncContentItemFacets($contentItemIds: [ID!]!) {\n syncContentItemFacets(contentItemIds: $contentItemIds) {\n synced {\n propertyId\n items\n unresolved\n fieldFacets\n }\n skipped {\n propertyId\n code\n message\n }\n changed\n }\n }\n";
355
+ /**
356
+ * `catalog.syncContentItemFacets`; its contract is on
357
+ * {@link CatalogModule.syncContentItemFacets}. The engine's own id checks run before sending.
358
+ */
359
+ export declare function syncContentItemFacets(transport: Transport, contentItemIds: readonly string[], options?: Call): Promise<ContentItemFacetSyncResult>;
322
360
  export declare function createPropertiesModule(transport: Transport): PropertiesModule;
@@ -1,4 +1,4 @@
1
- import { createPage } from '@sixtyninecommerce/core';
1
+ import { createPage, ValidationError } from '@sixtyninecommerce/core';
2
2
  import { assertDeleted, DELETION_FIELDS, fieldOf, } from './types.js';
3
3
  const PROPERTY_FIELDS = /* GraphQL */ `
4
4
  id
@@ -134,6 +134,57 @@ export const SYNC_PROPERTY_FACETS_MUTATION = /* GraphQL */ `
134
134
  export function syncContentFacets(transport, propertyId, options) {
135
135
  return fieldOf(transport, SYNC_PROPERTY_FACETS_MUTATION, 'syncPropertyFacets', { propertyId }, options);
136
136
  }
137
+ /**
138
+ * The most different content item ids one {@link CatalogModule.syncContentItemFacets} call takes
139
+ * (the engine's own bound); repeats count once.
140
+ */
141
+ export const MAX_CONTENT_ITEM_FACET_SYNC_IDS = 500;
142
+ export const SYNC_CONTENT_ITEM_FACETS_MUTATION = /* GraphQL */ `
143
+ mutation CatalogSyncContentItemFacets($contentItemIds: [ID!]!) {
144
+ syncContentItemFacets(contentItemIds: $contentItemIds) {
145
+ synced {
146
+ propertyId
147
+ items
148
+ unresolved
149
+ fieldFacets
150
+ }
151
+ skipped {
152
+ propertyId
153
+ code
154
+ message
155
+ }
156
+ changed
157
+ }
158
+ }
159
+ `;
160
+ function refuseContentItemIds(message) {
161
+ return new ValidationError({
162
+ code: 'CONTENT_ITEM_IDS_INVALID',
163
+ message: `${message} Nothing was sent.`,
164
+ status: 400,
165
+ service: 'vendure',
166
+ });
167
+ }
168
+ /**
169
+ * `catalog.syncContentItemFacets`; its contract is on
170
+ * {@link CatalogModule.syncContentItemFacets}. The engine's own id checks run before sending.
171
+ */
172
+ export function syncContentItemFacets(transport, contentItemIds, options) {
173
+ if (!Array.isArray(contentItemIds)) {
174
+ return Promise.reject(refuseContentItemIds('Send the content item ids as a list.'));
175
+ }
176
+ const distinct = new Set();
177
+ for (const id of contentItemIds) {
178
+ if (typeof id !== 'string' || id.trim() === '') {
179
+ return Promise.reject(refuseContentItemIds('A content item id is blank.'));
180
+ }
181
+ distinct.add(id.trim().toLowerCase());
182
+ }
183
+ if (distinct.size > MAX_CONTENT_ITEM_FACET_SYNC_IDS) {
184
+ return Promise.reject(refuseContentItemIds(`Send at most ${String(MAX_CONTENT_ITEM_FACET_SYNC_IDS)} different content item ids per call; this one named ${String(distinct.size)}. Split them into several calls.`));
185
+ }
186
+ return fieldOf(transport, SYNC_CONTENT_ITEM_FACETS_MUTATION, 'syncContentItemFacets', { contentItemIds: [...contentItemIds] }, options);
187
+ }
137
188
  export function createPropertiesModule(transport) {
138
189
  const call = (document, field, variables, options) => fieldOf(transport, document, field, variables, options);
139
190
  const deleted = async (document, field, code, id, options) => {
@@ -30,6 +30,11 @@
30
30
  * not hold, each named in the message and in `details` (`field` `/targetProductIds/<index>`).
31
31
  * - `RECOMMENDATION_GROUP_SLUG_TAKEN` → `ConflictError` (409): another of the store's groups holds
32
32
  * the slug, also when two writes race for one free slug.
33
+ * - `RECOMMENDATION_MAX_ITEMS_INVALID` → `ValidationError` (400): a group's `maxItems` that is not
34
+ * a whole number from 1 to 50 ({@link RECOMMENDATION_MAX_ITEMS_RANGE}), on `groups.create` and
35
+ * on a `groups.update` that sends it, `details[0].field` `/input/maxItems`. This SDK refuses it
36
+ * before anything is sent, with the same code and detail, so an older platform release (which
37
+ * stores any value) never receives one.
33
38
  * - `USER_INPUT_ERROR` → `ValidationError` (400), in the engine's words: a rule that does not fit
34
39
  * its group or its type (a `MANUAL` group, a `sourceId` missing or sent where none goes), picks
35
40
  * in a `DYNAMIC` group.
@@ -47,8 +52,17 @@
47
52
  * No release gate: every operation here is served by every platform release this SDK supports.
48
53
  * No method deduplicates on `idempotencyKey`: repeating a `create` makes a second row.
49
54
  */
50
- import type { Transport } from '@sixtyninecommerce/core';
55
+ import { type Transport } from '@sixtyninecommerce/core';
51
56
  import type { Call } from '../types.js';
57
+ /**
58
+ * The fewest and the most products a group may show (`maxItems`), both included. The platform
59
+ * refuses any other value with `RECOMMENDATION_MAX_ITEMS_INVALID`, and so does this SDK before
60
+ * sending.
61
+ */
62
+ export declare const RECOMMENDATION_MAX_ITEMS_RANGE: {
63
+ readonly min: 1;
64
+ readonly max: 50;
65
+ };
52
66
  /** What a `DYNAMIC` group's entry draws its products from. */
53
67
  export type RecommendationSourceType = 'PRODUCT' | 'COLLECTION' | 'FACET_VALUE' | 'SAME_COLLECTION' | 'SAME_PROPERTIES';
54
68
  /** `DYNAMIC`: rules (entries). `MANUAL`: products picked per viewed product (`manual.set`). */
@@ -82,7 +96,10 @@ export interface RecommendationGroupSummary {
82
96
  /** Unique within the store; the storefront asks for one group by it. */
83
97
  slug: string;
84
98
  description: string | null;
85
- /** The most products the storefront shows for this group. */
99
+ /**
100
+ * The most products the storefront shows for this group. Written as 1 to 50; a group stored
101
+ * outside that range before the platform enforced it keeps its value until `maxItems` is sent.
102
+ */
86
103
  maxItems: number;
87
104
  position: number;
88
105
  enabled: boolean;
@@ -107,7 +124,10 @@ export interface CreateRecommendationGroupInput {
107
124
  /** Defaults to the name, lower-cased, every run of other characters than `a-z0-9` a `-`. */
108
125
  slug?: string;
109
126
  description?: string;
110
- /** Defaults to 10. The engine sets no bounds. */
127
+ /**
128
+ * Defaults to 10. A whole number from 1 to 50 ({@link RECOMMENDATION_MAX_ITEMS_RANGE}); any
129
+ * other is refused `RECOMMENDATION_MAX_ITEMS_INVALID` before it is sent.
130
+ */
111
131
  maxItems?: number;
112
132
  /** Defaults to after the store's last group. */
113
133
  position?: number;
@@ -121,6 +141,11 @@ export interface UpdateRecommendationGroupInput {
121
141
  name?: string;
122
142
  slug?: string;
123
143
  description?: string | null;
144
+ /**
145
+ * A whole number from 1 to 50, else refused `RECOMMENDATION_MAX_ITEMS_INVALID` before it is
146
+ * sent. Left out, the stored value stays (and is not judged, so a group stored outside the
147
+ * range before the rule can still be renamed, moved or disabled).
148
+ */
124
149
  maxItems?: number;
125
150
  position?: number;
126
151
  enabled?: boolean;
@@ -155,14 +180,16 @@ export interface RecommendationsModule {
155
180
  */
156
181
  get(id: string, options?: Call): Promise<RecommendationGroup | null>;
157
182
  /**
158
- * Refused (`RECOMMENDATION_GROUP_SLUG_TAKEN`) when the store already has a group with that
159
- * slug, the defaulted one included.
183
+ * Refused: a `maxItems` outside 1–50 (`RECOMMENDATION_MAX_ITEMS_INVALID`, before anything is
184
+ * sent); a slug the store's other group already holds, the defaulted one included
185
+ * (`RECOMMENDATION_GROUP_SLUG_TAKEN`).
160
186
  *
161
187
  * @effect write
162
188
  */
163
189
  create(input: CreateRecommendationGroupInput, options?: Call): Promise<RecommendationGroup>;
164
190
  /**
165
- * Refused: a group the store does not have (`ENTITY_NOT_FOUND`), a slug another of its groups
191
+ * Refused: a `maxItems` sent outside 1–50 (`RECOMMENDATION_MAX_ITEMS_INVALID`, before anything
192
+ * is sent); a group the store does not have (`ENTITY_NOT_FOUND`); a slug another of its groups
166
193
  * holds (`RECOMMENDATION_GROUP_SLUG_TAKEN`).
167
194
  *
168
195
  * @effect write
@@ -1,4 +1,65 @@
1
+ /**
2
+ * Product recommendations (headless spec §9.2 `catalog`: "recommendations"; W10-13): the store's
3
+ * recommendation groups ("Customers also bought", "Complete the look"), what each one draws its
4
+ * products from, and the products hand-picked per product for a manual group.
5
+ *
6
+ * - **A `DYNAMIC` group's entries are its rules**, read in `position` order: one named product
7
+ * (`PRODUCT`), the products of a collection (`COLLECTION`) or with a facet value
8
+ * (`FACET_VALUE`), or the viewed product's own collections (`SAME_COLLECTION`) or properties
9
+ * (`SAME_PROPERTIES`). The storefront fills the group up to `maxItems` from the first entry on,
10
+ * never with the viewed product, a disabled product or one the store hides from the shopper.
11
+ * - **A `MANUAL` group has no entries**: its products are picked per viewed product
12
+ * (`manual.set`), and the storefront shows them in the order picked. A group's mode is chosen
13
+ * when it is created and never changes.
14
+ * - **A disabled group** (`enabled: false`) is kept but never shown on the storefront.
15
+ * - **Positions** are integers; a group or entry created without one goes after the last.
16
+ * `reorder` rewrites them as `0, 1, 2…` in the order given.
17
+ *
18
+ * Permissions: reads need `ReadCatalog`, writes `UpdateCatalog`. Every row is the store's own: a
19
+ * group or entry of another store is never answered and never changed (a read answers `null` or
20
+ * leaves it out; a write refuses it as unknown), and a rule only ever draws from the store's own
21
+ * products, collections and facet values.
22
+ *
23
+ * Refusals, GraphQL errors mapped by `core`. An unknown id and another store's get the same answer,
24
+ * so none tells whether another store holds the row:
25
+ * - `ENTITY_NOT_FOUND` → `NotFoundError` (404): a group or entry the store does not have, and the
26
+ * viewed product of `manual.set` when the store does not hold it.
27
+ * - `RECOMMENDATION_SOURCE_NOT_FOUND` → `NotFoundError` (404): a rule's `sourceId` that is not a
28
+ * product, collection or facet value of the store (`entries.create`, `entries.update`).
29
+ * - `RECOMMENDATION_TARGET_NOT_FOUND` → `NotFoundError` (404): `manual.set` picks the store does
30
+ * not hold, each named in the message and in `details` (`field` `/targetProductIds/<index>`).
31
+ * - `RECOMMENDATION_GROUP_SLUG_TAKEN` → `ConflictError` (409): another of the store's groups holds
32
+ * the slug, also when two writes race for one free slug.
33
+ * - `RECOMMENDATION_MAX_ITEMS_INVALID` → `ValidationError` (400): a group's `maxItems` that is not
34
+ * a whole number from 1 to 50 ({@link RECOMMENDATION_MAX_ITEMS_RANGE}), on `groups.create` and
35
+ * on a `groups.update` that sends it, `details[0].field` `/input/maxItems`. This SDK refuses it
36
+ * before anything is sent, with the same code and detail, so an older platform release (which
37
+ * stores any value) never receives one.
38
+ * - `USER_INPUT_ERROR` → `ValidationError` (400), in the engine's words: a rule that does not fit
39
+ * its group or its type (a `MANUAL` group, a `sourceId` missing or sent where none goes), picks
40
+ * in a `DYNAMIC` group.
41
+ * - `FORBIDDEN` → `PermissionDeniedError` (403) without the permission.
42
+ *
43
+ * An id that is not a uuid (any id argument, a `sourceId` and each pick included) is refused
44
+ * before all of these with core's `ENTITY_NOT_FOUND` (`NotFoundError`, 404, no `details`), so
45
+ * branch on the class for not-found and on the code only to tell which row is missing.
46
+ *
47
+ * A platform release older than these codes answers each not-found and a taken slug as
48
+ * `USER_INPUT_ERROR` (400) in its own words ("Recommendation group not found"), a slug lost in a
49
+ * race as `DUPLICATE_RECORD`, and takes another store's collection or facet value as a rule's
50
+ * source.
51
+ *
52
+ * No release gate: every operation here is served by every platform release this SDK supports.
53
+ * No method deduplicates on `idempotencyKey`: repeating a `create` makes a second row.
54
+ */
55
+ import { ValidationError } from '@sixtyninecommerce/core';
1
56
  import { assertDeleted, DELETION_FIELDS, fieldOf } from './types.js';
57
+ /**
58
+ * The fewest and the most products a group may show (`maxItems`), both included. The platform
59
+ * refuses any other value with `RECOMMENDATION_MAX_ITEMS_INVALID`, and so does this SDK before
60
+ * sending.
61
+ */
62
+ export const RECOMMENDATION_MAX_ITEMS_RANGE = { min: 1, max: 50 };
2
63
  /** The source types whose entry names a row (`sourceId`); the others must name none. */
3
64
  export const RECOMMENDATION_SOURCE_TYPES_WITH_ID = [
4
65
  'PRODUCT',
@@ -103,6 +164,31 @@ export const SET_PRODUCT_MANUAL_RECOMMENDATIONS_MUTATION = /* GraphQL */ `
103
164
  ) { ${GROUP_SUMMARY_FIELDS} }
104
165
  }
105
166
  `;
167
+ /**
168
+ * The platform's `maxItems` rule, checked before sending: a value present and not null must be a
169
+ * whole number in {@link RECOMMENDATION_MAX_ITEMS_RANGE}. Same code, message and detail as the
170
+ * engine's refusal, so a caller handles one shape whichever side refused.
171
+ */
172
+ function assertMaxItems(input) {
173
+ const maxItems = input?.maxItems;
174
+ if (maxItems == null)
175
+ return;
176
+ const { min, max } = RECOMMENDATION_MAX_ITEMS_RANGE;
177
+ if (typeof maxItems === 'number' &&
178
+ Number.isInteger(maxItems) &&
179
+ maxItems >= min &&
180
+ maxItems <= max) {
181
+ return;
182
+ }
183
+ const message = `Max items must be a whole number from ${min} to ${max}; got ${JSON.stringify(maxItems)}`;
184
+ throw new ValidationError({
185
+ code: 'RECOMMENDATION_MAX_ITEMS_INVALID',
186
+ message,
187
+ status: 400,
188
+ service: 'vendure',
189
+ details: [{ field: '/input/maxItems', code: 'OUT_OF_RANGE', message }],
190
+ });
191
+ }
106
192
  /** Entries in `position` order, the older first on a tie, as the engine's own entry reads sort. */
107
193
  function byPosition(entries) {
108
194
  return [...entries].sort((a, b) => a.position - b.position || a.createdAt.localeCompare(b.createdAt));
@@ -120,8 +206,14 @@ export function createRecommendationsModule(transport) {
120
206
  const group = await call(RECOMMENDATION_GROUP_QUERY, 'recommendationGroup', { id }, options);
121
207
  return group === null ? null : withSortedEntries(group);
122
208
  },
123
- create: async (input, options) => withSortedEntries(await call(CREATE_RECOMMENDATION_GROUP_MUTATION, 'createRecommendationGroup', { input }, options)),
124
- update: async (id, input, options) => withSortedEntries(await call(UPDATE_RECOMMENDATION_GROUP_MUTATION, 'updateRecommendationGroup', { id, input }, options)),
209
+ create: async (input, options) => {
210
+ assertMaxItems(input);
211
+ return withSortedEntries(await call(CREATE_RECOMMENDATION_GROUP_MUTATION, 'createRecommendationGroup', { input }, options));
212
+ },
213
+ update: async (id, input, options) => {
214
+ assertMaxItems(input);
215
+ return withSortedEntries(await call(UPDATE_RECOMMENDATION_GROUP_MUTATION, 'updateRecommendationGroup', { id, input }, options));
216
+ },
125
217
  async delete(id, options) {
126
218
  const response = await call(DELETE_RECOMMENDATION_GROUP_MUTATION, 'deleteRecommendationGroup', { id }, options);
127
219
  assertDeleted('RECOMMENDATION_GROUP_NOT_DELETED', [response]);
@@ -21,6 +21,13 @@
21
21
  * `CUSTOMER_DATA_NOT_GRANTED` (`PermissionDeniedError`, 403). The answers of the writes are
22
22
  * blanked the same way.
23
23
  *
24
+ * Publishing needs `full` (user decision O6): approving a review (`moderate`, or `bulkModerate`
25
+ * with `APPROVED`) and `reply.set` publish under the store's name next to text a caller below
26
+ * `full` reads blanked, so an app installation below it (or a request narrowed below it) is
27
+ * refused them with `CUSTOMER_DATA_NOT_GRANTED` (`PermissionDeniedError`, 403; `bulkModerate`
28
+ * reports it on each review). It may still reject, flag or re-open a review and remove the
29
+ * store's reply. A staff session and a personal API token act at `full`.
30
+ *
24
31
  * Permissions: reads need `ReadCatalog`, writes `UpdateCatalog`. Every row is the store's own: a
25
32
  * review or attribute of another store is never answered and never changed.
26
33
  *
@@ -266,7 +273,9 @@ export interface ReviewsModule {
266
273
  * Give one review a status: `APPROVED` publishes it on the storefront; `REJECTED`, `FLAGGED`
267
274
  * and `PENDING` take it off. Recorded in its moderation log with `reason`. The product's
268
275
  * average rating follows within seconds. Refused (`ENTITY_NOT_FOUND`) for a review the store
269
- * does not have.
276
+ * does not have. `APPROVED` is refused `CUSTOMER_DATA_NOT_GRANTED` (`PermissionDeniedError`) to
277
+ * a caller below `customerData: full`, whatever the review; the other statuses are open at every
278
+ * level.
270
279
  *
271
280
  * @effect write
272
281
  */
@@ -277,7 +286,9 @@ export interface ReviewsModule {
277
286
  * (`ENTITY_NOT_FOUND`) changes nothing, and the others still go through. A selection of no
278
287
  * review, more than {@link MAX_BULK_REVIEWS} or one twice is refused
279
288
  * `BULK_ACTION_INPUT_INVALID` (400) before anything is sent. Not keyed: a repeat moderates
280
- * again (and logs again). Needs a platform release with `moderateReviews`.
289
+ * again (and logs again). Needs a platform release with `moderateReviews`. `APPROVED` below
290
+ * `customerData: full` reports every review `FAILED` with code `CUSTOMER_DATA_NOT_GRANTED` and
291
+ * changes none; the call itself succeeds.
281
292
  *
282
293
  * @effect write
283
294
  */
@@ -287,13 +298,15 @@ export interface ReviewsModule {
287
298
  /**
288
299
  * Set (or replace) the store's reply, trimmed; shown under the review on the storefront once
289
300
  * it is approved. Over 2,000 characters is refused (`USER_INPUT_ERROR`). An empty reply is
290
- * stored as one: use `remove` to take a reply away.
301
+ * stored as one: use `remove` to take a reply away. Refused `CUSTOMER_DATA_NOT_GRANTED`
302
+ * (`PermissionDeniedError`) below `customerData: full`: the reply is published under the
303
+ * store's name.
291
304
  *
292
305
  * @effect write
293
306
  */
294
307
  set(reviewId: string, reply: string, options?: Call): Promise<ProductReviewSummary>;
295
308
  /**
296
- * Take the store's reply away.
309
+ * Take the store's reply away. Open at every customer-data level: it publishes nothing.
297
310
  *
298
311
  * @effect destructive
299
312
  */
@@ -21,6 +21,13 @@
21
21
  * `CUSTOMER_DATA_NOT_GRANTED` (`PermissionDeniedError`, 403). The answers of the writes are
22
22
  * blanked the same way.
23
23
  *
24
+ * Publishing needs `full` (user decision O6): approving a review (`moderate`, or `bulkModerate`
25
+ * with `APPROVED`) and `reply.set` publish under the store's name next to text a caller below
26
+ * `full` reads blanked, so an app installation below it (or a request narrowed below it) is
27
+ * refused them with `CUSTOMER_DATA_NOT_GRANTED` (`PermissionDeniedError`, 403; `bulkModerate`
28
+ * reports it on each review). It may still reject, flag or re-open a review and remove the
29
+ * store's reply. A staff session and a personal API token act at `full`.
30
+ *
24
31
  * Permissions: reads need `ReadCatalog`, writes `UpdateCatalog`. Every row is the store's own: a
25
32
  * review or attribute of another store is never answered and never changed.
26
33
  *