@sixtyninecommerce/admin 0.11.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 (147) hide show
  1. package/README.md +1501 -23
  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/index.d.ts +15 -0
  33. package/dist/customers/index.js +4 -0
  34. package/dist/documents/emails.d.ts +150 -0
  35. package/dist/documents/emails.js +220 -0
  36. package/dist/documents/index.d.ts +13 -0
  37. package/dist/documents/index.js +2 -0
  38. package/dist/fulfillments/index.d.ts +119 -1
  39. package/dist/fulfillments/index.js +124 -4
  40. package/dist/index.d.ts +43 -23
  41. package/dist/index.js +11 -5
  42. package/dist/integrations/exact-online-types.d.ts +542 -0
  43. package/dist/integrations/exact-online.d.ts +541 -5
  44. package/dist/integrations/exact-online.js +714 -1
  45. package/dist/integrations/index.d.ts +23 -8
  46. package/dist/integrations/index.js +9 -4
  47. package/dist/integrations/pipeline-discoveries.d.ts +65 -0
  48. package/dist/integrations/pipeline-discoveries.js +65 -0
  49. package/dist/integrations/pipeline-runs.d.ts +119 -0
  50. package/dist/integrations/pipeline-runs.js +87 -0
  51. package/dist/integrations/pipelines.d.ts +73 -2
  52. package/dist/integrations/pipelines.js +36 -1
  53. package/dist/integrations/price-import.d.ts +223 -0
  54. package/dist/integrations/price-import.js +124 -0
  55. package/dist/integrations/types.d.ts +221 -3
  56. package/dist/integrations/types.js +4 -3
  57. package/dist/meta/index.d.ts +10 -0
  58. package/dist/metafields/index.d.ts +8 -1
  59. package/dist/metafields/index.js +10 -0
  60. package/dist/orders/cancel-with-money.d.ts +10 -0
  61. package/dist/orders/cancel-with-money.js +2 -0
  62. package/dist/orders/commit-edit.d.ts +16 -0
  63. package/dist/orders/commit-edit.js +14 -1
  64. package/dist/orders/deposit-policy.d.ts +3 -1
  65. package/dist/orders/index.d.ts +22 -11
  66. package/dist/orders/index.js +5 -2
  67. package/dist/orders/list.d.ts +26 -5
  68. package/dist/orders/list.js +123 -13
  69. package/dist/orders/manual-discounts.d.ts +13 -5
  70. package/dist/orders/money-view.d.ts +149 -0
  71. package/dist/orders/money-view.js +99 -0
  72. package/dist/orders/plan-edit.d.ts +34 -2
  73. package/dist/orders/plan-edit.js +110 -12
  74. package/dist/orders/surcharges.d.ts +12 -0
  75. package/dist/orders/surcharges.js +34 -9
  76. package/dist/payments/account-credit.d.ts +17 -1
  77. package/dist/payments/account-credit.js +25 -2
  78. package/dist/payments/credit-noted-fields.d.ts +8 -1
  79. package/dist/payments/credit-noted-fields.js +34 -2
  80. package/dist/payments/customer-consent.d.ts +30 -0
  81. package/dist/payments/customer-consent.js +49 -0
  82. package/dist/payments/index.d.ts +89 -14
  83. package/dist/payments/index.js +22 -2
  84. package/dist/payments/requests.d.ts +6 -3
  85. package/dist/payments/requests.js +1 -0
  86. package/dist/pickups/index.d.ts +5 -3
  87. package/dist/pickups/index.js +5 -2
  88. package/dist/platform/email-gateways.d.ts +165 -0
  89. package/dist/platform/email-gateways.js +78 -0
  90. package/dist/platform/tax.d.ts +11 -3
  91. package/dist/platform/tax.js +30 -0
  92. package/dist/pos/exchanges.d.ts +6 -1
  93. package/dist/pos/index.d.ts +229 -10
  94. package/dist/pos/index.js +113 -4
  95. package/dist/pos/offline-sales.d.ts +44 -3
  96. package/dist/pos/offline-sales.js +57 -3
  97. package/dist/pos/returns.d.ts +28 -1
  98. package/dist/pos/returns.js +36 -6
  99. package/dist/pricing/price-lists.d.ts +3 -1
  100. package/dist/purchasing/alerts.d.ts +19 -4
  101. package/dist/purchasing/alerts.js +29 -3
  102. package/dist/purchasing/backorders.d.ts +61 -0
  103. package/dist/purchasing/backorders.js +92 -0
  104. package/dist/purchasing/index.d.ts +21 -3
  105. package/dist/purchasing/index.js +8 -0
  106. package/dist/purchasing/links.d.ts +263 -0
  107. package/dist/purchasing/links.js +192 -0
  108. package/dist/purchasing/purchase-orders.d.ts +4 -1
  109. package/dist/purchasing/receipts.d.ts +396 -0
  110. package/dist/purchasing/receipts.js +287 -0
  111. package/dist/purchasing/returns.d.ts +273 -0
  112. package/dist/purchasing/returns.js +229 -0
  113. package/dist/quotes/index.d.ts +20 -3
  114. package/dist/returns/index.d.ts +62 -1
  115. package/dist/returns/index.js +61 -0
  116. package/dist/seo/config.d.ts +128 -0
  117. package/dist/seo/config.js +95 -0
  118. package/dist/seo/redirects.d.ts +125 -0
  119. package/dist/seo/redirects.js +60 -1
  120. package/dist/seo/settings.d.ts +147 -0
  121. package/dist/seo/settings.js +209 -0
  122. package/dist/shipping/index.d.ts +6 -0
  123. package/dist/shipping/index.js +2 -0
  124. package/dist/shipping/methods.d.ts +8 -1
  125. package/dist/shipping/myparcel.d.ts +415 -1
  126. package/dist/shipping/myparcel.js +291 -3
  127. package/dist/shipping/rate-tables.d.ts +149 -4
  128. package/dist/shipping/rate-tables.js +229 -23
  129. package/dist/shipping/ship-to-countries.d.ts +75 -0
  130. package/dist/shipping/ship-to-countries.js +47 -0
  131. package/dist/stock/by-location.d.ts +86 -0
  132. package/dist/stock/by-location.js +94 -0
  133. package/dist/stock/index.d.ts +8 -1
  134. package/dist/stock/index.js +4 -0
  135. package/dist/stock/settings.d.ts +106 -0
  136. package/dist/stock/settings.js +103 -0
  137. package/dist/stock/transfers.d.ts +171 -10
  138. package/dist/stock/transfers.js +82 -4
  139. package/dist/stored-value/index.d.ts +98 -7
  140. package/dist/stored-value/index.js +71 -4
  141. package/dist/tax/display-policy.d.ts +105 -0
  142. package/dist/tax/display-policy.js +84 -0
  143. package/dist/tax/index.d.ts +3 -0
  144. package/dist/tax/index.js +2 -0
  145. package/dist/tenant/email-templates.d.ts +222 -1
  146. package/dist/tenant/email-templates.js +227 -10
  147. package/package.json +2 -2
@@ -1,7 +1,13 @@
1
1
  import { type Transport } from '@sixtyninecommerce/core';
2
2
  import type { Call, Me, WhoAmI } from '../types.js';
3
3
  export interface AuthModule {
4
- /** Establishes a session. The bearer arrives on a response header, not here. */
4
+ /**
5
+ * Establishes a session. The bearer arrives on a response header, not here.
6
+ * It is stored even when a concurrent call rotated the old bearer while this
7
+ * one was in flight: a sign-in is the newest word on the session.
8
+ *
9
+ * @effect write
10
+ */
5
11
  login(username: string, password: string, options?: Call): Promise<Me>;
6
12
  /**
7
13
  * Ends the session server-side AND clears the bearer, in the caller's own
@@ -112,7 +112,14 @@ export function createAuthModule(transport, session) {
112
112
  return {
113
113
  async login(username, password, options) {
114
114
  const rotationsBefore = session?.rotations();
115
- const data = await transport.graphql({ api: 'admin', query: LOGIN_MUTATION, variables: { username, password } }, unscoped(options));
115
+ const data = await transport.graphql({
116
+ api: 'admin',
117
+ query: LOGIN_MUTATION,
118
+ variables: { username, password },
119
+ // Its bearer is adopted even if a concurrent call rotated the old
120
+ // one first; otherwise this sign-in would read as NO_SESSION_BEARER.
121
+ startsSession: true,
122
+ }, unscoped(options));
116
123
  // Vendure answers a bad password with HTTP 200 and an ErrorResult member.
117
124
  if (isErrorResult(data.login))
118
125
  throw errorFromErrorResult(data.login);
@@ -0,0 +1,142 @@
1
+ /**
2
+ * A product's documents (headless spec §9.2 `catalog`: "documents"; W10-10): the downloads the
3
+ * storefront lists on a product page (a data sheet, a manual, a certificate), each pointing at a
4
+ * file in the asset library.
5
+ *
6
+ * - **Universal or localized.** A universal document (`isLocalized: false`) has exactly ONE
7
+ * translation, with no language code, and it names the file. A localized one has one
8
+ * translation per language (each language once), and at least one of them names a file; the
9
+ * storefront shows the request's language, else the store's default, else one with a file.
10
+ * - **Who sees it** on the storefront: everyone (`PUBLIC`), signed-in customers
11
+ * (`AUTHENTICATED`), or customers in the listed groups (`CUSTOMER_GROUP`, which needs at least
12
+ * one group). The Admin API always lists every document.
13
+ * - **Order.** A new document goes after the product's last; `reorder` sets the order.
14
+ *
15
+ * Every document is the store's own: a product, document or customer group of another store is
16
+ * never answered and never written.
17
+ *
18
+ * Permissions: `list` needs `ReadCatalog`, the writes `UpdateCatalog`.
19
+ *
20
+ * Refusals, GraphQL errors mapped by `core`:
21
+ * - `USER_INPUT_ERROR` → `ValidationError` (400), in the engine's words: a product of another
22
+ * store or none ("Product … not found"), a document of another store or none
23
+ * ("ProductDocument … not found"), a translation set that breaks the rules above, a
24
+ * `CUSTOMER_GROUP` document with no group, a `reorder` naming a document that is not one of the
25
+ * product's.
26
+ * - `ENTITY_NOT_FOUND` → `NotFoundError` (404): a customer group the store does not hold.
27
+ * - `TRANSLATION_LANGUAGE_NOT_OFFERED` → `ValidationError` (400): a translation in a language the
28
+ * store does not offer (a language the document already has stays editable).
29
+ * - `FORBIDDEN` → `PermissionDeniedError` (403) without the permission.
30
+ *
31
+ * No release gate: every operation here is served by every platform release this SDK supports.
32
+ * No method deduplicates on `idempotencyKey`: repeating a `create` adds a second document.
33
+ */
34
+ import { type Transport } from '@sixtyninecommerce/core';
35
+ import type { Call } from '../types.js';
36
+ import { type LanguageCode } from './types.js';
37
+ export type ProductDocumentVisibility = 'PUBLIC' | 'AUTHENTICATED' | 'CUSTOMER_GROUP';
38
+ /** The file a document translation points at: an asset of the library. */
39
+ export interface ProductDocumentFile {
40
+ id: string;
41
+ name: string;
42
+ /** A string on the wire: a newer engine may add a type. */
43
+ type: 'IMAGE' | 'VIDEO' | 'BINARY' | (string & {});
44
+ mimeType: string;
45
+ /** Bytes. */
46
+ fileSize: number;
47
+ /** The asset's own URL (absolute or relative to the engine). */
48
+ source: string;
49
+ preview: string;
50
+ }
51
+ export interface ProductDocumentTranslation {
52
+ id: string;
53
+ /** Null on a universal document's one translation. */
54
+ languageCode: LanguageCode | null;
55
+ title: string;
56
+ description: string | null;
57
+ assetId: string | null;
58
+ /** Null when the translation names no file, or its file is gone. */
59
+ asset: ProductDocumentFile | null;
60
+ }
61
+ export interface ProductDocument {
62
+ id: string;
63
+ productId: string;
64
+ /** 0 first. */
65
+ position: number;
66
+ visibility: ProductDocumentVisibility | (string & {});
67
+ isLocalized: boolean;
68
+ translations: ProductDocumentTranslation[];
69
+ /** The groups a `CUSTOMER_GROUP` document is shown to; empty under the other two. */
70
+ customerGroups: Array<{
71
+ id: string;
72
+ name: string;
73
+ }>;
74
+ /** ISO-8601 UTC. */
75
+ createdAt: string;
76
+ /** ISO-8601 UTC. */
77
+ updatedAt: string;
78
+ }
79
+ export interface ProductDocumentTranslationInput {
80
+ /** Null (or omitted) on a universal document; required, once per language, on a localized one. */
81
+ languageCode?: LanguageCode | null;
82
+ title: string;
83
+ description?: string | null;
84
+ /** The asset (`catalog.assets`) the translation points at. */
85
+ assetId?: string | null;
86
+ }
87
+ export interface CreateProductDocumentInput {
88
+ productId: string;
89
+ visibility: ProductDocumentVisibility;
90
+ isLocalized: boolean;
91
+ /** Required (one or more) under `CUSTOMER_GROUP`; ignored under the other two. */
92
+ customerGroupIds?: string[];
93
+ translations: ProductDocumentTranslationInput[];
94
+ }
95
+ /**
96
+ * Only the fields named change. `translations`, when sent, REPLACES the document's translations;
97
+ * `customerGroupIds`, when sent, replaces its groups. Leaving `CUSTOMER_GROUP` drops its groups.
98
+ */
99
+ export interface UpdateProductDocumentInput {
100
+ visibility?: ProductDocumentVisibility;
101
+ isLocalized?: boolean;
102
+ customerGroupIds?: string[];
103
+ translations?: ProductDocumentTranslationInput[];
104
+ }
105
+ export interface ProductDocumentsModule {
106
+ /**
107
+ * The product's documents, in their order (ties: the older first). A product of another store,
108
+ * or none, answers `[]`.
109
+ *
110
+ * @effect read
111
+ */
112
+ list(productId: string, options?: Call): Promise<ProductDocument[]>;
113
+ /**
114
+ * Adds a document after the product's last.
115
+ *
116
+ * @effect write
117
+ */
118
+ create(input: CreateProductDocumentInput, options?: Call): Promise<ProductDocument>;
119
+ /** @effect write */
120
+ update(id: string, input: UpdateProductDocumentInput, options?: Call): Promise<ProductDocument>;
121
+ /**
122
+ * Deletes the document; its file stays in the asset library. A document the store does not
123
+ * have (another store's, or already deleted) is refused `PRODUCT_DOCUMENT_NOT_FOUND`
124
+ * (`NotFoundError`, 404, raised by this SDK: the engine answers it as `false`).
125
+ *
126
+ * @effect destructive
127
+ */
128
+ delete(id: string, options?: Call): Promise<void>;
129
+ /**
130
+ * Puts the named documents in this order (`0, 1, 2…`). Name the product's documents: one that
131
+ * is not the product's refuses the whole call and nothing moves; one left out keeps its position.
132
+ *
133
+ * @effect write
134
+ */
135
+ reorder(productId: string, orderedIds: string[], options?: Call): Promise<void>;
136
+ }
137
+ export declare const PRODUCT_DOCUMENTS_QUERY = "\n query CatalogProductDocuments($productId: ID!) {\n productDocuments(productId: $productId) { \n id\n productId\n position\n visibility\n isLocalized\n createdAt\n updatedAt\n translations {\n id\n languageCode\n title\n description\n assetId\n asset {\n id\n name\n type\n mimeType\n fileSize\n source\n preview\n }\n }\n customerGroups {\n id\n name\n }\n }\n }\n";
138
+ export declare const CREATE_PRODUCT_DOCUMENT_MUTATION = "\n mutation CatalogCreateProductDocument($input: CreateProductDocumentInput!) {\n createProductDocument(input: $input) { \n id\n productId\n position\n visibility\n isLocalized\n createdAt\n updatedAt\n translations {\n id\n languageCode\n title\n description\n assetId\n asset {\n id\n name\n type\n mimeType\n fileSize\n source\n preview\n }\n }\n customerGroups {\n id\n name\n }\n }\n }\n";
139
+ export declare const UPDATE_PRODUCT_DOCUMENT_MUTATION = "\n mutation CatalogUpdateProductDocument($input: UpdateProductDocumentInput!) {\n updateProductDocument(input: $input) { \n id\n productId\n position\n visibility\n isLocalized\n createdAt\n updatedAt\n translations {\n id\n languageCode\n title\n description\n assetId\n asset {\n id\n name\n type\n mimeType\n fileSize\n source\n preview\n }\n }\n customerGroups {\n id\n name\n }\n }\n }\n";
140
+ export declare const DELETE_PRODUCT_DOCUMENT_MUTATION = "\n mutation CatalogDeleteProductDocument($id: ID!) {\n deleteProductDocument(id: $id)\n }\n";
141
+ export declare const REORDER_PRODUCT_DOCUMENTS_MUTATION = "\n mutation CatalogReorderProductDocuments($productId: ID!, $orderedIds: [ID!]!) {\n reorderProductDocuments(productId: $productId, orderedIds: $orderedIds)\n }\n";
142
+ export declare function createProductDocumentsModule(transport: Transport): ProductDocumentsModule;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * A product's documents (headless spec §9.2 `catalog`: "documents"; W10-10): the downloads the
3
+ * storefront lists on a product page (a data sheet, a manual, a certificate), each pointing at a
4
+ * file in the asset library.
5
+ *
6
+ * - **Universal or localized.** A universal document (`isLocalized: false`) has exactly ONE
7
+ * translation, with no language code, and it names the file. A localized one has one
8
+ * translation per language (each language once), and at least one of them names a file; the
9
+ * storefront shows the request's language, else the store's default, else one with a file.
10
+ * - **Who sees it** on the storefront: everyone (`PUBLIC`), signed-in customers
11
+ * (`AUTHENTICATED`), or customers in the listed groups (`CUSTOMER_GROUP`, which needs at least
12
+ * one group). The Admin API always lists every document.
13
+ * - **Order.** A new document goes after the product's last; `reorder` sets the order.
14
+ *
15
+ * Every document is the store's own: a product, document or customer group of another store is
16
+ * never answered and never written.
17
+ *
18
+ * Permissions: `list` needs `ReadCatalog`, the writes `UpdateCatalog`.
19
+ *
20
+ * Refusals, GraphQL errors mapped by `core`:
21
+ * - `USER_INPUT_ERROR` → `ValidationError` (400), in the engine's words: a product of another
22
+ * store or none ("Product … not found"), a document of another store or none
23
+ * ("ProductDocument … not found"), a translation set that breaks the rules above, a
24
+ * `CUSTOMER_GROUP` document with no group, a `reorder` naming a document that is not one of the
25
+ * product's.
26
+ * - `ENTITY_NOT_FOUND` → `NotFoundError` (404): a customer group the store does not hold.
27
+ * - `TRANSLATION_LANGUAGE_NOT_OFFERED` → `ValidationError` (400): a translation in a language the
28
+ * store does not offer (a language the document already has stays editable).
29
+ * - `FORBIDDEN` → `PermissionDeniedError` (403) without the permission.
30
+ *
31
+ * No release gate: every operation here is served by every platform release this SDK supports.
32
+ * No method deduplicates on `idempotencyKey`: repeating a `create` adds a second document.
33
+ */
34
+ import { NotFoundError } from '@sixtyninecommerce/core';
35
+ import { fieldOf } from './types.js';
36
+ const DOCUMENT_FIELDS = /* GraphQL */ `
37
+ id
38
+ productId
39
+ position
40
+ visibility
41
+ isLocalized
42
+ createdAt
43
+ updatedAt
44
+ translations {
45
+ id
46
+ languageCode
47
+ title
48
+ description
49
+ assetId
50
+ asset {
51
+ id
52
+ name
53
+ type
54
+ mimeType
55
+ fileSize
56
+ source
57
+ preview
58
+ }
59
+ }
60
+ customerGroups {
61
+ id
62
+ name
63
+ }
64
+ `;
65
+ export const PRODUCT_DOCUMENTS_QUERY = /* GraphQL */ `
66
+ query CatalogProductDocuments($productId: ID!) {
67
+ productDocuments(productId: $productId) { ${DOCUMENT_FIELDS} }
68
+ }
69
+ `;
70
+ export const CREATE_PRODUCT_DOCUMENT_MUTATION = /* GraphQL */ `
71
+ mutation CatalogCreateProductDocument($input: CreateProductDocumentInput!) {
72
+ createProductDocument(input: $input) { ${DOCUMENT_FIELDS} }
73
+ }
74
+ `;
75
+ export const UPDATE_PRODUCT_DOCUMENT_MUTATION = /* GraphQL */ `
76
+ mutation CatalogUpdateProductDocument($input: UpdateProductDocumentInput!) {
77
+ updateProductDocument(input: $input) { ${DOCUMENT_FIELDS} }
78
+ }
79
+ `;
80
+ export const DELETE_PRODUCT_DOCUMENT_MUTATION = /* GraphQL */ `
81
+ mutation CatalogDeleteProductDocument($id: ID!) {
82
+ deleteProductDocument(id: $id)
83
+ }
84
+ `;
85
+ export const REORDER_PRODUCT_DOCUMENTS_MUTATION = /* GraphQL */ `
86
+ mutation CatalogReorderProductDocuments($productId: ID!, $orderedIds: [ID!]!) {
87
+ reorderProductDocuments(productId: $productId, orderedIds: $orderedIds)
88
+ }
89
+ `;
90
+ /** The engine reads the list in `position` order; keep that, with the older first on a tie. */
91
+ function byPosition(documents) {
92
+ return [...documents].sort((a, b) => a.position - b.position || a.createdAt.localeCompare(b.createdAt));
93
+ }
94
+ export function createProductDocumentsModule(transport) {
95
+ return {
96
+ list: async (productId, options) => byPosition(await fieldOf(transport, PRODUCT_DOCUMENTS_QUERY, 'productDocuments', { productId }, options)),
97
+ create: (input, options) => fieldOf(transport, CREATE_PRODUCT_DOCUMENT_MUTATION, 'createProductDocument', { input }, options),
98
+ update: (id, input, options) => fieldOf(transport, UPDATE_PRODUCT_DOCUMENT_MUTATION, 'updateProductDocument', { input: { ...input, id } }, options),
99
+ async delete(id, options) {
100
+ const deleted = await fieldOf(transport, DELETE_PRODUCT_DOCUMENT_MUTATION, 'deleteProductDocument', { id }, options);
101
+ if (!deleted) {
102
+ throw new NotFoundError({
103
+ code: 'PRODUCT_DOCUMENT_NOT_FOUND',
104
+ message: `This store has no product document ${id}.`,
105
+ status: 404,
106
+ service: 'vendure',
107
+ });
108
+ }
109
+ },
110
+ async reorder(productId, orderedIds, options) {
111
+ await fieldOf(transport, REORDER_PRODUCT_DOCUMENTS_MUTATION, 'reorderProductDocuments', { productId, orderedIds }, options);
112
+ },
113
+ };
114
+ }
@@ -2,19 +2,21 @@ import type { Transport } from '@sixtyninecommerce/core';
2
2
  import type { Call } from '../types.js';
3
3
  import { type AssetsModule } from './assets.js';
4
4
  import { type CollectionsModule } from './collections.js';
5
+ import { type ProductDocumentsModule } from './documents.js';
5
6
  import { type FacetsModule } from './facets.js';
7
+ import { type ProductMetadataModule } from './metadata.js';
6
8
  import { type OptionsModule } from './options.js';
7
9
  import { type ProductsModule } from './products.js';
8
- import { type ContentFacetSyncResult, type PropertiesModule } from './properties.js';
10
+ import { type ContentFacetSyncResult, type ContentItemFacetSyncResult, type PropertiesModule } from './properties.js';
9
11
  import { type RecommendationsModule } from './recommendations.js';
10
12
  import { type ReviewsModule } from './reviews.js';
11
13
  import { type VariantsModule } from './variants.js';
12
14
  import { type VisibilityModule } from './visibility.js';
13
15
  /**
14
16
  * The catalogue (spec §9.2 `catalog`): products, variants, options, facets,
15
- * collections, assets, storefront visibility, product properties, product
16
- * recommendations and product reviews — one resource per namespace, over the Admin API the
17
- * dashboard itself uses.
17
+ * collections, assets, storefront visibility, product documents, product SEO metadata, product
18
+ * properties, product recommendations and product reviews — one resource per namespace, over the
19
+ * Admin API the dashboard itself uses.
18
20
  *
19
21
  * **Per-resource, not per-intent.** Saving a product form is several calls
20
22
  * here (the product, its variants, its properties, its collections), and they
@@ -72,6 +74,10 @@ export interface CatalogModule {
72
74
  collections: CollectionsModule;
73
75
  assets: AssetsModule;
74
76
  visibility: VisibilityModule;
77
+ /** A product's downloads: data sheets, manuals, certificates (W10-10). */
78
+ documents: ProductDocumentsModule;
79
+ /** A product's SEO, social and AI-agent metadata, and the SEO coverage list (W10-10). */
80
+ metadata: ProductMetadataModule;
75
81
  properties: PropertiesModule;
76
82
  /** Recommendation groups, their rules and the products picked per product (W10-13). */
77
83
  recommendations: RecommendationsModule;
@@ -93,9 +99,11 @@ export interface CatalogModule {
93
99
  * anything changes. Repeating the call is safe: a label that did not change is not written
94
100
  * again, so a repeat answers the same counts and changes nothing.
95
101
  *
96
- * Nothing runs it automatically in this release: rename a content item or a product, then call
97
- * it (or press "Sync filter values") to update the filter label (ruling G17; an event-driven
98
- * update is a later phase).
102
+ * Automatic for PUBLISHED content items (headless W10-19, ruling G115): when a published item
103
+ * is edited, published or unpublished, the platform syncs the properties naming it by itself
104
+ * within about a minute ({@link CatalogModule.syncContentItemFacets}). Call this after renaming
105
+ * a DRAFT or archived item (the content system announces nothing for one until it is
106
+ * published), after renaming a product, or to sync one property at once.
99
107
  *
100
108
  * Needs `UpdateCatalog`. Platform releases before this method do not have the mutation and refuse
101
109
  * it as `ValidationError` `GRAPHQL_VALIDATION_FAILED` (400).
@@ -116,7 +124,40 @@ export interface CatalogModule {
116
124
  * - `PROPERTY_SYNC_SOURCE_UNAVAILABLE` → `BusinessRuleError` (422): a `content_item` property on
117
125
  * a platform with no content system wired.
118
126
  * - `FORBIDDEN` → `PermissionDeniedError` (403): no `UpdateCatalog`.
127
+ *
128
+ * @effect write
119
129
  */
120
130
  syncContentFacets(propertyId: string, options?: Call): Promise<ContentFacetSyncResult>;
131
+ /**
132
+ * "Sync filter values" for every filterable `content_item` property of the store whose values
133
+ * name one of `contentItemIds` (headless W10-19): each is synced as
134
+ * {@link CatalogModule.syncContentFacets} syncs it, so a renamed content item's filter label
135
+ * follows its title. The platform calls it by itself, within about a minute, for the items a
136
+ * change to a PUBLISHED item touched (published, edited while published, unpublished); call it
137
+ * yourself for items you know changed otherwise (an app importing content, a draft renamed).
138
+ *
139
+ * Per property: a property its own sync refuses is reported in `skipped` with that refusal
140
+ * (`MANAGED_BY_ORGANIZATION` for the organization workspace's property, synced in the
141
+ * workspace) and nothing of it is written; the others are synced. `changed` says whether
142
+ * anything was written. Every label is read before anything is written, and the writes are
143
+ * one transaction: a content system that cannot be read, or any failure other than a
144
+ * property's refusal, refuses the whole call and writes nothing. Repeating it is safe.
145
+ *
146
+ * At most {@link MAX_CONTENT_ITEM_FACET_SYNC_IDS} different ids per call (repeats count once);
147
+ * an id no property names syncs nothing. Needs `UpdateCatalog`. Platform releases before this
148
+ * method do not have the mutation and refuse it as `ValidationError`
149
+ * `GRAPHQL_VALIDATION_FAILED` (400).
150
+ *
151
+ * Refusals of the whole call (nothing is written):
152
+ * - `CONTENT_ITEM_IDS_INVALID` → `ValidationError` (400): a blank id, or more than 500
153
+ * different ids; checked before sending as well.
154
+ * - `ENTITY_NOT_FOUND` → `NotFoundError` (404): an id that is not a uuid.
155
+ * - `CMS_REQUEST_FAILED` → `BusinessRuleError` (422): the content system could not be read;
156
+ * try again.
157
+ * - `FORBIDDEN` → `PermissionDeniedError` (403): no `UpdateCatalog`.
158
+ *
159
+ * @effect write
160
+ */
161
+ syncContentItemFacets(contentItemIds: readonly string[], options?: Call): Promise<ContentItemFacetSyncResult>;
121
162
  }
122
163
  export declare function createCatalogModule(transport: Transport): CatalogModule;
@@ -1,9 +1,11 @@
1
1
  import { createAssetsModule } from './assets.js';
2
2
  import { createCollectionsModule } from './collections.js';
3
+ import { createProductDocumentsModule } from './documents.js';
3
4
  import { createFacetsModule } from './facets.js';
5
+ import { createProductMetadataModule } from './metadata.js';
4
6
  import { createOptionsModule } from './options.js';
5
7
  import { createProductsModule } from './products.js';
6
- import { createPropertiesModule, syncContentFacets, } from './properties.js';
8
+ import { createPropertiesModule, syncContentFacets, syncContentItemFacets, } from './properties.js';
7
9
  import { createRecommendationsModule } from './recommendations.js';
8
10
  import { createReviewsModule } from './reviews.js';
9
11
  import { createVariantsModule } from './variants.js';
@@ -17,9 +19,12 @@ export function createCatalogModule(transport) {
17
19
  collections: createCollectionsModule(transport),
18
20
  assets: createAssetsModule(transport),
19
21
  visibility: createVisibilityModule(transport),
22
+ documents: createProductDocumentsModule(transport),
23
+ metadata: createProductMetadataModule(transport),
20
24
  properties: createPropertiesModule(transport),
21
25
  recommendations: createRecommendationsModule(transport),
22
26
  reviews: createReviewsModule(transport),
23
27
  syncContentFacets: (propertyId, options) => syncContentFacets(transport, propertyId, options),
28
+ syncContentItemFacets: (contentItemIds, options) => syncContentItemFacets(transport, contentItemIds, options),
24
29
  };
25
30
  }
@@ -0,0 +1,244 @@
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 { type Page, type Transport } from '@sixtyninecommerce/core';
43
+ import type { Call, ListOptions } from '../types.js';
44
+ import { type LanguageCode } from './types.js';
45
+ /** How many products one `bulkUpsert` call may name. */
46
+ export declare const MAX_BULK_METADATA_EDITS = 200;
47
+ /** An image the metadata names (the share image, the X/Twitter override). */
48
+ export interface ProductMetadataImage {
49
+ id: string;
50
+ name: string;
51
+ preview: string;
52
+ source: string;
53
+ /** A string on the wire: a newer engine may add a type. */
54
+ type: 'IMAGE' | 'VIDEO' | 'BINARY' | (string & {});
55
+ mimeType: string;
56
+ }
57
+ /**
58
+ * One language's texts. The four JSON fields are read as the engine stores them; every write
59
+ * checks them (`breadcrumbOverride` `{ label, url }[]` of at most 5, `focusKeywords` up to 10
60
+ * distinct strings, `qaPairs` `{ question, answer }[]` of at most 20), but read them defensively.
61
+ */
62
+ export interface ProductMetadataTranslation {
63
+ id: string;
64
+ languageCode: LanguageCode;
65
+ seoTitle: string | null;
66
+ seoDescription: string | null;
67
+ canonicalUrl: string | null;
68
+ breadcrumbOverride: unknown;
69
+ focusKeywords: unknown;
70
+ socialTitle: string | null;
71
+ socialDescription: string | null;
72
+ socialImageAlt: string | null;
73
+ twitterTitleOverride: string | null;
74
+ twitterDescriptionOverride: string | null;
75
+ aiSummary: string | null;
76
+ qaPairs: unknown;
77
+ }
78
+ export interface ProductMetadata {
79
+ id: string;
80
+ productId: string;
81
+ channelId: string;
82
+ /** `index` or `noindex`. */
83
+ metaRobotsIndex: string;
84
+ /** `follow` or `nofollow`. */
85
+ metaRobotsFollow: string;
86
+ metaRobotsNoarchive: boolean;
87
+ metaRobotsNosnippet: boolean;
88
+ metaRobotsNoimageindex: boolean;
89
+ /** `none`, `standard` or `large`. */
90
+ metaRobotsMaxImagePreview: string;
91
+ metaRobotsMaxSnippet: number | null;
92
+ metaRobotsMaxVideoPreview: number | null;
93
+ /** ISO-8601 UTC instant after which search engines should drop the page; null when none. */
94
+ metaRobotsUnavailableAfter: string | null;
95
+ socialImageId: string | null;
96
+ socialImage: ProductMetadataImage | null;
97
+ twitterImageOverrideId: string | null;
98
+ twitterImageOverride: ProductMetadataImage | null;
99
+ twitterCardType: string;
100
+ socialOgType: string;
101
+ seoIncludeReviewsInJsonld: boolean;
102
+ aiSummaryAudience: string;
103
+ /** The product's own `{ [botKey]: 'allow' | 'deny' }`; null when it has none. */
104
+ aiBotOverrides: Record<string, unknown> | null;
105
+ qaEmitFaqpageJsonld: boolean;
106
+ hreflangOverrides: unknown;
107
+ /** The product's overrides on top of the store's defaults, as the storefront applies them. */
108
+ resolvedBotDirectives: Record<string, unknown>;
109
+ translations: ProductMetadataTranslation[];
110
+ }
111
+ export interface ProductMetadataTranslationInput {
112
+ languageCode: LanguageCode;
113
+ seoTitle?: string | null;
114
+ seoDescription?: string | null;
115
+ /** An absolute `https://` URL. */
116
+ canonicalUrl?: string | null;
117
+ breadcrumbOverride?: Array<{
118
+ label: string;
119
+ url: string;
120
+ }> | null;
121
+ focusKeywords?: string[] | null;
122
+ socialTitle?: string | null;
123
+ socialDescription?: string | null;
124
+ socialImageAlt?: string | null;
125
+ twitterTitleOverride?: string | null;
126
+ twitterDescriptionOverride?: string | null;
127
+ aiSummary?: string | null;
128
+ qaPairs?: Array<{
129
+ question: string;
130
+ answer: string;
131
+ }> | null;
132
+ }
133
+ /** Only the fields named change. */
134
+ export interface ProductMetadataInput {
135
+ metaRobotsIndex?: 'index' | 'noindex';
136
+ metaRobotsFollow?: 'follow' | 'nofollow';
137
+ metaRobotsNoarchive?: boolean;
138
+ metaRobotsNosnippet?: boolean;
139
+ metaRobotsNoimageindex?: boolean;
140
+ metaRobotsMaxImagePreview?: 'none' | 'standard' | 'large';
141
+ metaRobotsMaxSnippet?: number | null;
142
+ metaRobotsMaxVideoPreview?: number | null;
143
+ /**
144
+ * An ISO-8601 instant WITH an explicit offset (`Z` or `±HH:mm`). One without is refused before
145
+ * the request leaves (`INVALID_DATETIME`).
146
+ */
147
+ metaRobotsUnavailableAfter?: string | null;
148
+ socialImageId?: string | null;
149
+ twitterImageOverrideId?: string | null;
150
+ twitterCardType?: 'summary' | 'summary_large_image';
151
+ socialOgType?: 'product' | 'website';
152
+ seoIncludeReviewsInJsonld?: boolean;
153
+ aiSummaryAudience?: 'customers' | 'developers' | 'both';
154
+ aiBotOverrides?: Record<string, 'allow' | 'deny'> | null;
155
+ qaEmitFaqpageJsonld?: boolean;
156
+ hreflangOverrides?: Array<{
157
+ language: string;
158
+ country?: string;
159
+ url: string;
160
+ }> | null;
161
+ translations?: ProductMetadataTranslationInput[];
162
+ }
163
+ export type ProductMetadataCoverageFilter = 'ALL' | 'MISSING_TITLE' | 'MISSING_DESCRIPTION';
164
+ export interface ProductMetadataCoverageQuery extends ListOptions {
165
+ /** The language whose meta each row shows, and which the filter judges. */
166
+ languageCode: LanguageCode;
167
+ /** Defaults to `ALL`. A value is missing when it is null or empty. */
168
+ filter?: ProductMetadataCoverageFilter;
169
+ /** Matches the product's name. */
170
+ term?: string;
171
+ }
172
+ /** One product of the coverage list. */
173
+ export interface ProductMetadataCoverageRow {
174
+ productId: string;
175
+ /** The product's name in the request's language. */
176
+ name: string;
177
+ slug: string;
178
+ enabled: boolean;
179
+ /** The product's own meta title in the query's language; null when it has none. */
180
+ seoTitle: string | null;
181
+ /** The product's own meta description in the query's language; null when it has none. */
182
+ seoDescription: string | null;
183
+ }
184
+ export interface ProductMetadataBulkEdit {
185
+ productId: string;
186
+ input: ProductMetadataInput;
187
+ }
188
+ /** One product's outcome in a `bulkUpsert` call. */
189
+ export interface ProductMetadataBulkOutcome {
190
+ productId: string;
191
+ /** `OK`, or `FAILED` (a string on the wire: read anything else as not done). */
192
+ status: 'OK' | 'FAILED' | (string & {});
193
+ /**
194
+ * When FAILED: the single door's own code (`ENTITY_NOT_FOUND`,
195
+ * `PRODUCT_METADATA_INPUT_INVALID`, …); `INTERNAL_SERVER_ERROR` for a fault the engine logged.
196
+ */
197
+ code: string | null;
198
+ /** When FAILED: the door's sentence. */
199
+ message: string | null;
200
+ }
201
+ export interface ProductMetadataModule {
202
+ /**
203
+ * The product's metadata; null when it has none yet. `NotFoundError` (`PRODUCT_NOT_FOUND`) for
204
+ * a product the store does not hold.
205
+ *
206
+ * @effect read
207
+ */
208
+ get(productId: string, options?: Call): Promise<ProductMetadata | null>;
209
+ /**
210
+ * Creates the product's metadata or changes the fields named, and answers the whole record.
211
+ *
212
+ * @effect write
213
+ */
214
+ upsert(productId: string, input: ProductMetadataInput, options?: Call): Promise<ProductMetadata>;
215
+ /**
216
+ * Removes the product's metadata in this store: the storefront falls back to the product's own
217
+ * name and description. Resolves the same way whether or not there was any.
218
+ *
219
+ * @effect destructive
220
+ */
221
+ delete(productId: string, options?: Call): Promise<void>;
222
+ /**
223
+ * The store's products, name first, each with its own meta in `languageCode`; with a `filter`,
224
+ * only those missing a title (or a description) there. Filtered before paging: pages are full
225
+ * and `totalItems` counts the filter and the term. Never answers a deleted product.
226
+ *
227
+ * @effect read
228
+ */
229
+ coverage(query: ProductMetadataCoverageQuery): Promise<Page<ProductMetadataCoverageRow>>;
230
+ /**
231
+ * Upserts 1 to {@link MAX_BULK_METADATA_EDITS} products, each through `upsert`'s rules in a
232
+ * transaction of its own, and answers one outcome per product in the order named: a refused
233
+ * product changes nothing and the others still go through. Not keyed: a repeat writes again.
234
+ *
235
+ * @effect write
236
+ */
237
+ bulkUpsert(edits: readonly ProductMetadataBulkEdit[], options?: Call): Promise<ProductMetadataBulkOutcome[]>;
238
+ }
239
+ export declare const PRODUCT_METADATA_QUERY = "\n query CatalogProductMetadata($productId: ID!) {\n product(id: $productId) {\n id\n metadata { \n id\n productId\n channelId\n metaRobotsIndex\n metaRobotsFollow\n metaRobotsNoarchive\n metaRobotsNosnippet\n metaRobotsNoimageindex\n metaRobotsMaxImagePreview\n metaRobotsMaxSnippet\n metaRobotsMaxVideoPreview\n metaRobotsUnavailableAfter\n socialImageId\n socialImage { \n id\n name\n preview\n source\n type\n mimeType\n }\n twitterImageOverrideId\n twitterImageOverride { \n id\n name\n preview\n source\n type\n mimeType\n }\n twitterCardType\n socialOgType\n seoIncludeReviewsInJsonld\n aiSummaryAudience\n aiBotOverrides\n qaEmitFaqpageJsonld\n hreflangOverrides\n resolvedBotDirectives\n translations {\n id\n languageCode\n seoTitle\n seoDescription\n canonicalUrl\n breadcrumbOverride\n focusKeywords\n socialTitle\n socialDescription\n socialImageAlt\n twitterTitleOverride\n twitterDescriptionOverride\n aiSummary\n qaPairs\n }\n }\n }\n }\n";
240
+ export declare const UPSERT_PRODUCT_METADATA_MUTATION = "\n mutation CatalogUpsertProductMetadata($productId: ID!, $input: ProductMetadataInput!) {\n upsertProductMetadata(productId: $productId, input: $input) { \n id\n productId\n channelId\n metaRobotsIndex\n metaRobotsFollow\n metaRobotsNoarchive\n metaRobotsNosnippet\n metaRobotsNoimageindex\n metaRobotsMaxImagePreview\n metaRobotsMaxSnippet\n metaRobotsMaxVideoPreview\n metaRobotsUnavailableAfter\n socialImageId\n socialImage { \n id\n name\n preview\n source\n type\n mimeType\n }\n twitterImageOverrideId\n twitterImageOverride { \n id\n name\n preview\n source\n type\n mimeType\n }\n twitterCardType\n socialOgType\n seoIncludeReviewsInJsonld\n aiSummaryAudience\n aiBotOverrides\n qaEmitFaqpageJsonld\n hreflangOverrides\n resolvedBotDirectives\n translations {\n id\n languageCode\n seoTitle\n seoDescription\n canonicalUrl\n breadcrumbOverride\n focusKeywords\n socialTitle\n socialDescription\n socialImageAlt\n twitterTitleOverride\n twitterDescriptionOverride\n aiSummary\n qaPairs\n }\n }\n }\n";
241
+ export declare const DELETE_PRODUCT_METADATA_MUTATION = "\n mutation CatalogDeleteProductMetadata($productId: ID!) {\n deleteProductMetadata(productId: $productId) { \n result\n message\n }\n }\n";
242
+ export declare const PRODUCT_METADATA_COVERAGE_QUERY = "\n query CatalogProductMetadataCoverage(\n $languageCode: LanguageCode!\n $filter: ProductMetadataCoverageFilter\n $term: String\n $options: ProductMetadataCoverageListOptions\n ) {\n productMetadataCoverage(\n languageCode: $languageCode\n filter: $filter\n term: $term\n options: $options\n ) {\n items {\n productId\n name\n slug\n enabled\n seoTitle\n seoDescription\n }\n totalItems\n }\n }\n";
243
+ export declare const BULK_UPSERT_PRODUCT_METADATA_MUTATION = "\n mutation CatalogBulkUpsertProductMetadata($edits: [ProductMetadataBulkEdit!]!) {\n bulkUpsertProductMetadata(edits: $edits) {\n items {\n productId\n status\n code\n message\n }\n }\n }\n";
244
+ export declare function createProductMetadataModule(transport: Transport): ProductMetadataModule;