@sixtyninecommerce/admin 0.8.0 → 0.10.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 (179) hide show
  1. package/README.md +2739 -17
  2. package/dist/api-tokens/index.d.ts +15 -5
  3. package/dist/api-tokens/index.js +1 -0
  4. package/dist/apps/event-subscription.d.ts +59 -0
  5. package/dist/apps/event-subscription.js +32 -0
  6. package/dist/apps/hook-secret.d.ts +22 -0
  7. package/dist/apps/hook-secret.js +36 -0
  8. package/dist/apps/index.d.ts +54 -3
  9. package/dist/apps/index.js +17 -0
  10. package/dist/audit/index.d.ts +7 -0
  11. package/dist/auth/queries.d.ts +5 -1
  12. package/dist/auth/queries.js +6 -0
  13. package/dist/bulk/index.d.ts +323 -0
  14. package/dist/bulk/index.js +162 -0
  15. package/dist/catalog/assets.d.ts +147 -0
  16. package/dist/catalog/assets.js +164 -0
  17. package/dist/catalog/collections.d.ts +109 -0
  18. package/dist/catalog/collections.js +91 -0
  19. package/dist/catalog/facets.d.ts +103 -0
  20. package/dist/catalog/facets.js +100 -0
  21. package/dist/catalog/index.d.ts +115 -0
  22. package/dist/catalog/index.js +21 -0
  23. package/dist/catalog/options.d.ts +89 -0
  24. package/dist/catalog/options.js +88 -0
  25. package/dist/catalog/products-bulk.d.ts +21 -0
  26. package/dist/catalog/products-bulk.js +58 -0
  27. package/dist/catalog/products.d.ts +650 -0
  28. package/dist/catalog/products.js +458 -0
  29. package/dist/catalog/properties.d.ts +234 -0
  30. package/dist/catalog/properties.js +158 -0
  31. package/dist/catalog/types.d.ts +174 -0
  32. package/dist/catalog/types.js +141 -0
  33. package/dist/catalog/variants.d.ts +324 -0
  34. package/dist/catalog/variants.js +288 -0
  35. package/dist/catalog/visibility.d.ts +57 -0
  36. package/dist/catalog/visibility.js +59 -0
  37. package/dist/client.d.ts +192 -3
  38. package/dist/client.js +87 -12
  39. package/dist/compliance/index.d.ts +88 -0
  40. package/dist/compliance/index.js +88 -0
  41. package/dist/customer-data-redaction.d.ts +14 -0
  42. package/dist/customer-data-redaction.js +22 -0
  43. package/dist/customers/index.d.ts +428 -17
  44. package/dist/customers/index.js +322 -15
  45. package/dist/documents/index.d.ts +513 -16
  46. package/dist/documents/index.js +229 -3
  47. package/dist/events/index.d.ts +259 -0
  48. package/dist/events/index.js +147 -0
  49. package/dist/feeds/ai.d.ts +46 -0
  50. package/dist/feeds/ai.js +43 -0
  51. package/dist/feeds/categories.d.ts +50 -0
  52. package/dist/feeds/categories.js +56 -0
  53. package/dist/feeds/configs.d.ts +54 -0
  54. package/dist/feeds/configs.js +85 -0
  55. package/dist/feeds/identifier-fields.d.ts +25 -0
  56. package/dist/feeds/identifier-fields.js +29 -0
  57. package/dist/feeds/index.d.ts +54 -0
  58. package/dist/feeds/index.js +46 -0
  59. package/dist/feeds/preserve.d.ts +38 -0
  60. package/dist/feeds/preserve.js +74 -0
  61. package/dist/feeds/rules.d.ts +42 -0
  62. package/dist/feeds/rules.js +56 -0
  63. package/dist/feeds/types.d.ts +654 -0
  64. package/dist/feeds/types.js +135 -0
  65. package/dist/fulfillments/index.d.ts +437 -0
  66. package/dist/fulfillments/index.js +534 -0
  67. package/dist/heads/index.d.ts +60 -0
  68. package/dist/heads/index.js +45 -0
  69. package/dist/hooks/assignments.d.ts +90 -0
  70. package/dist/hooks/assignments.js +53 -0
  71. package/dist/hooks/index.d.ts +97 -0
  72. package/dist/hooks/index.js +76 -0
  73. package/dist/index.d.ts +95 -9
  74. package/dist/index.js +21 -1
  75. package/dist/integrations/index.d.ts +14 -0
  76. package/dist/integrations/index.js +9 -0
  77. package/dist/integrations/pipelines.d.ts +113 -0
  78. package/dist/integrations/pipelines.js +62 -0
  79. package/dist/integrations/types.d.ts +176 -0
  80. package/dist/integrations/types.js +8 -0
  81. package/dist/jobs/bulk-action-run.d.ts +74 -0
  82. package/dist/jobs/bulk-action-run.js +69 -0
  83. package/dist/locations/index.d.ts +6 -2
  84. package/dist/meta/index.d.ts +32 -0
  85. package/dist/orders/bulk.d.ts +75 -0
  86. package/dist/orders/bulk.js +150 -0
  87. package/dist/orders/commit-edit.d.ts +123 -0
  88. package/dist/orders/commit-edit.js +142 -0
  89. package/dist/orders/deposit-policy.d.ts +103 -0
  90. package/dist/orders/deposit-policy.js +202 -0
  91. package/dist/orders/get.d.ts +722 -0
  92. package/dist/orders/get.js +530 -0
  93. package/dist/orders/index.d.ts +186 -21
  94. package/dist/orders/index.js +57 -0
  95. package/dist/orders/line-price-preview.d.ts +297 -0
  96. package/dist/orders/line-price-preview.js +50 -0
  97. package/dist/orders/manual-discounts.d.ts +141 -0
  98. package/dist/orders/manual-discounts.js +97 -0
  99. package/dist/orders/manual-pricing.d.ts +192 -0
  100. package/dist/orders/manual-pricing.js +72 -0
  101. package/dist/orders/order-entry.d.ts +566 -0
  102. package/dist/orders/order-entry.js +129 -0
  103. package/dist/orders/parcel-charge.d.ts +45 -0
  104. package/dist/orders/parcel-charge.js +55 -0
  105. package/dist/orders/plan-edit.d.ts +489 -0
  106. package/dist/orders/plan-edit.js +93 -0
  107. package/dist/orders/status.d.ts +235 -0
  108. package/dist/orders/status.js +96 -0
  109. package/dist/orders/surcharges.d.ts +75 -2
  110. package/dist/orders/surcharges.js +110 -4
  111. package/dist/payments/account-credit.d.ts +171 -0
  112. package/dist/payments/account-credit.js +107 -0
  113. package/dist/payments/index.d.ts +241 -9
  114. package/dist/payments/index.js +92 -0
  115. package/dist/payments/methods.d.ts +37 -0
  116. package/dist/payments/methods.js +33 -0
  117. package/dist/platform/catalog.d.ts +148 -0
  118. package/dist/platform/catalog.js +74 -0
  119. package/dist/platform/entitlements.d.ts +111 -0
  120. package/dist/platform/entitlements.js +86 -0
  121. package/dist/platform/legal-entities.d.ts +139 -0
  122. package/dist/platform/legal-entities.js +126 -0
  123. package/dist/platform/legal-entity-numbering.d.ts +103 -0
  124. package/dist/platform/legal-entity-numbering.js +101 -0
  125. package/dist/platform/organization-membership.d.ts +80 -0
  126. package/dist/platform/organization-membership.js +71 -0
  127. package/dist/platform/organization-settings.d.ts +33 -0
  128. package/dist/platform/organization-settings.js +42 -0
  129. package/dist/platform/organization-sharing.d.ts +172 -0
  130. package/dist/platform/organization-sharing.js +120 -0
  131. package/dist/platform/organizations.d.ts +205 -0
  132. package/dist/platform/organizations.js +180 -0
  133. package/dist/platform/provision-store.d.ts +88 -8
  134. package/dist/platform/provision-store.js +12 -0
  135. package/dist/platform/reports.d.ts +46 -0
  136. package/dist/platform/reports.js +31 -0
  137. package/dist/platform/server-metrics.d.ts +58 -0
  138. package/dist/platform/server-metrics.js +11 -0
  139. package/dist/pos/index.d.ts +567 -0
  140. package/dist/pos/index.js +205 -0
  141. package/dist/pricing/index.d.ts +408 -0
  142. package/dist/pricing/index.js +353 -0
  143. package/dist/pricing/vouchers.d.ts +103 -0
  144. package/dist/pricing/vouchers.js +69 -0
  145. package/dist/provenance/index.d.ts +93 -0
  146. package/dist/provenance/index.js +53 -0
  147. package/dist/reports/index.d.ts +199 -0
  148. package/dist/reports/index.js +154 -0
  149. package/dist/returns/index.d.ts +225 -0
  150. package/dist/returns/index.js +160 -0
  151. package/dist/search/index.d.ts +99 -0
  152. package/dist/search/index.js +62 -0
  153. package/dist/shipping/parcel-count-suggestion.d.ts +52 -0
  154. package/dist/shipping/parcel-count-suggestion.js +73 -0
  155. package/dist/shipping/parcel-rule.d.ts +48 -0
  156. package/dist/shipping/parcel-rule.js +91 -0
  157. package/dist/stock/index.d.ts +166 -0
  158. package/dist/stock/index.js +115 -0
  159. package/dist/stock/transfers.d.ts +520 -0
  160. package/dist/stock/transfers.js +437 -0
  161. package/dist/stored-value/index.d.ts +154 -0
  162. package/dist/stored-value/index.js +130 -0
  163. package/dist/tenant/capabilities.d.ts +10 -0
  164. package/dist/tenant/entitlements.d.ts +35 -0
  165. package/dist/tenant/entitlements.js +15 -0
  166. package/dist/tenant/field-groups.d.ts +64 -0
  167. package/dist/tenant/field-groups.js +34 -0
  168. package/dist/tenant/legal-entity.d.ts +32 -0
  169. package/dist/tenant/legal-entity.js +41 -0
  170. package/dist/tenant/notifications.d.ts +148 -0
  171. package/dist/tenant/notifications.js +79 -0
  172. package/dist/tenant/retention.d.ts +10 -4
  173. package/dist/tenant/settings-sources.d.ts +103 -0
  174. package/dist/tenant/settings-sources.js +92 -0
  175. package/dist/types.d.ts +86 -11
  176. package/dist/uploads/index.d.ts +11 -7
  177. package/dist/users/index.d.ts +104 -0
  178. package/dist/users/index.js +80 -0
  179. package/package.json +3 -2
@@ -0,0 +1,147 @@
1
+ import { createEventHandler, createEventsClient, createPage, readRestPage, ValidationError, } from '@sixtyninecommerce/core';
2
+ /** Most event ids one `redrive` call may name. */
3
+ export const MAX_REDRIVE_EVENT_IDS = 100;
4
+ /** The caller's test `data` is the envelope payload: sent verbatim, never case-converted. */
5
+ const SEND_TEST_PASSTHROUGH = new Set(['data']);
6
+ /** `evidence` is stored JSONB: its keys are data, not structure. */
7
+ const DELIVERY_PASSTHROUGH = new Set(['evidence']);
8
+ function requireId(id) {
9
+ if (typeof id !== 'string' || id.trim() === '') {
10
+ throw new ValidationError({
11
+ code: 'VALIDATION_ERROR',
12
+ message: 'A subscription id is required.',
13
+ status: 400,
14
+ service: 'plugins',
15
+ });
16
+ }
17
+ return encodeURIComponent(id);
18
+ }
19
+ /** Refused rather than dropped: the caller believes a retry is safe, and it is not. */
20
+ function refuseIdempotencyKey(options, message) {
21
+ if (options && 'idempotencyKey' in options && options.idempotencyKey !== undefined) {
22
+ throw new ValidationError({
23
+ code: 'IDEMPOTENCY_NOT_SUPPORTED',
24
+ message,
25
+ status: 400,
26
+ service: 'plugins',
27
+ });
28
+ }
29
+ }
30
+ export function createEventsModule(transport) {
31
+ async function list(options) {
32
+ // `?cursor=1` opts into the paged shape; without it the route answers a bare array and
33
+ // ignores `limit` (`wantsCursor`, spec §18 amendment 9).
34
+ const body = await transport.rest('plugins', {
35
+ method: 'GET',
36
+ path: '/event-subscriptions',
37
+ query: { cursor: 1, after: options?.after, limit: options?.limit },
38
+ }, options);
39
+ const { items, meta } = readRestPage(body);
40
+ return createPage(items, meta, (cursor) => list({ ...options, after: cursor }));
41
+ }
42
+ async function deliveries(id, options) {
43
+ const body = await transport.rest('plugins', {
44
+ method: 'GET',
45
+ path: `/event-subscriptions/${requireId(id)}/deliveries`,
46
+ query: { cursor: 1, after: options?.after, limit: options?.limit },
47
+ }, { ...options, preserveKeys: DELIVERY_PASSTHROUGH });
48
+ const { items, meta } = readRestPage(body);
49
+ return createPage(items, meta, (cursor) => deliveries(id, { ...options, after: cursor }));
50
+ }
51
+ const subscriptions = {
52
+ list,
53
+ deliveries,
54
+ async create(input, options) {
55
+ refuseIdempotencyKey(options, 'events.subscriptions.create takes no idempotency key: its response carries the signing secret, which the platform never stores for replay. Check list() before retrying.');
56
+ return transport.rest('plugins', { method: 'POST', path: '/event-subscriptions', body: input }, options);
57
+ },
58
+ async update(id, input, options) {
59
+ return transport.rest('plugins', { method: 'PUT', path: `/event-subscriptions/${requireId(id)}`, body: input }, options);
60
+ },
61
+ async setEnabled(id, enabled, options) {
62
+ if (typeof enabled !== 'boolean') {
63
+ throw new ValidationError({
64
+ code: 'VALIDATION_ERROR',
65
+ message: 'enabled must be true or false.',
66
+ status: 400,
67
+ service: 'plugins',
68
+ });
69
+ }
70
+ return transport.rest('plugins', {
71
+ method: 'POST',
72
+ path: `/event-subscriptions/${requireId(id)}/enabled`,
73
+ body: { enabled },
74
+ }, options);
75
+ },
76
+ async remove(id, options) {
77
+ await transport.rest('plugins', { method: 'DELETE', path: `/event-subscriptions/${requireId(id)}`, expect: 'void' }, options);
78
+ },
79
+ async sendTest(id, type, options) {
80
+ if (typeof type !== 'string' || type.trim() === '') {
81
+ throw new ValidationError({
82
+ code: 'VALIDATION_ERROR',
83
+ message: 'An event type is required, e.g. order.created.',
84
+ status: 400,
85
+ service: 'plugins',
86
+ });
87
+ }
88
+ const { data, ...call } = options ?? {};
89
+ if (data !== undefined &&
90
+ (data === null || typeof data !== 'object' || Array.isArray(data))) {
91
+ throw new ValidationError({
92
+ code: 'VALIDATION_ERROR',
93
+ message: 'data must be a JSON object: the envelope data to send.',
94
+ status: 400,
95
+ service: 'plugins',
96
+ });
97
+ }
98
+ return transport.rest('plugins', {
99
+ method: 'POST',
100
+ path: `/event-subscriptions/${requireId(id)}/test`,
101
+ body: data === undefined ? { type } : { type, data },
102
+ }, { ...call, preserveKeys: SEND_TEST_PASSTHROUGH });
103
+ },
104
+ async rotateSecret(id, options) {
105
+ refuseIdempotencyKey(options, 'events.subscriptions.rotateSecret takes no idempotency key: its response carries the new signing secret, which the platform never stores for replay.');
106
+ const { force, ...call } = options ?? {};
107
+ return transport.rest('plugins', {
108
+ method: 'POST',
109
+ path: `/event-subscriptions/${requireId(id)}/rotate-secret`,
110
+ query: { force: force === true ? true : undefined },
111
+ }, call);
112
+ },
113
+ };
114
+ async function redrives(subscriptionId, options) {
115
+ const body = await transport.rest('plugins', {
116
+ method: 'GET',
117
+ path: `/event-subscriptions/${requireId(subscriptionId)}/redrives`,
118
+ query: { after: options?.after, limit: options?.limit },
119
+ }, options);
120
+ const { items, meta } = readRestPage(body);
121
+ return createPage(items, meta, (cursor) => redrives(subscriptionId, { ...options, after: cursor }));
122
+ }
123
+ async function redrive(subscriptionId, eventIds, options) {
124
+ if (!Array.isArray(eventIds) ||
125
+ eventIds.length === 0 ||
126
+ eventIds.length > MAX_REDRIVE_EVENT_IDS) {
127
+ throw new ValidationError({
128
+ code: 'VALIDATION_ERROR',
129
+ message: `events.redrive takes 1-${String(MAX_REDRIVE_EVENT_IDS)} event ids.`,
130
+ status: 400,
131
+ service: 'plugins',
132
+ });
133
+ }
134
+ return transport.rest('plugins', {
135
+ method: 'POST',
136
+ path: `/event-subscriptions/${requireId(subscriptionId)}/redrive`,
137
+ body: { eventIds: [...eventIds] },
138
+ }, options);
139
+ }
140
+ return {
141
+ ...createEventsClient(transport),
142
+ subscribe: createEventHandler,
143
+ subscriptions,
144
+ redrive,
145
+ redrives,
146
+ };
147
+ }
@@ -0,0 +1,46 @@
1
+ import { type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call } from '../types.js';
3
+ import { type CategorySuggestEnqueueResult, type CategorySuggestStatus, type FeedAiSuggestion, type GenerateTitlePresetInput, type SuggestCategoryMappingsInput, type TitlePresetStatus } from './types.js';
4
+ /** A title preset samples the catalogue and waits on a model; 15 seconds is not enough. */
5
+ export declare const TITLE_PRESET_TIMEOUT_MS = 60000;
6
+ /**
7
+ * Whether a refusal means "this platform has no model configured" rather than
8
+ * "the model failed".
9
+ *
10
+ * The two need different handling: one is a permanent property of the
11
+ * environment (offer nothing, explain), the other is worth retrying. Both AI
12
+ * writes refuse with a 409 `AI_UNAVAILABLE`, while the two READS answer 200
13
+ * with `aiAvailable: false` — availability is a field there, never an error, so
14
+ * a page can render the disabled hint without provoking a failure state.
15
+ */
16
+ export declare function isAiUnavailable(error: unknown): boolean;
17
+ /**
18
+ * The AI helpers: categorising a channel's collections, and proposing a better
19
+ * `title` mapping for one feed.
20
+ *
21
+ * Nothing here is applied on its own. A categorisation pass writes ordinary
22
+ * `suggested` category mappings that `categoryMappings.bulkStatus` decides on;
23
+ * a title preset is stored as a PROPOSAL and only `acceptSuggestion` merges it
24
+ * into the feed's `mappingJson`, through the same compile-before-write path a
25
+ * hand-edited mapping takes.
26
+ */
27
+ export interface FeedAiModule {
28
+ /**
29
+ * Queue a categorisation pass over the channel's collections. Refuses with a
30
+ * 409 `AI_UNAVAILABLE` when no model is configured — read
31
+ * {@link FeedAiModule.categorySuggestStatus} first and offer the action only
32
+ * when `aiAvailable`.
33
+ */
34
+ suggestCategories(input?: SuggestCategoryMappingsInput, options?: Call): Promise<CategorySuggestEnqueueResult>;
35
+ /** State of the channel's latest pass. Always 200 — availability is a field, not an error. */
36
+ categorySuggestStatus(options?: Call): Promise<CategorySuggestStatus>;
37
+ /** Ask for one improved `title` mapping for a feed. Slow, and 409s when no model is configured. */
38
+ generateTitlePreset(configId: string, input?: GenerateTitlePresetInput, options?: Call): Promise<FeedAiSuggestion>;
39
+ /** A feed's latest PENDING title proposal, plus whether a new one can be asked for. */
40
+ titlePreset(configId: string, options?: Call): Promise<TitlePresetStatus>;
41
+ /** Record the decision and merge the proposal into the feed's `mappingJson`. */
42
+ acceptSuggestion(suggestionId: string, options?: Call): Promise<FeedAiSuggestion>;
43
+ /** Record the decision. Changes no configuration. */
44
+ dismissSuggestion(suggestionId: string, options?: Call): Promise<FeedAiSuggestion>;
45
+ }
46
+ export declare function createFeedAiModule(transport: Transport): FeedAiModule;
@@ -0,0 +1,43 @@
1
+ import { PlatformError } from '@sixtyninecommerce/core';
2
+ import { FEEDS_PRESERVE_KEYS, segment } from './preserve.js';
3
+ import { AI_UNAVAILABLE_CODE, } from './types.js';
4
+ const MAPPINGS = '/feeds/category-mappings';
5
+ const CONFIGS = '/feeds/configs';
6
+ const SUGGESTIONS = '/feeds/ai-suggestions';
7
+ /** A title preset samples the catalogue and waits on a model; 15 seconds is not enough. */
8
+ export const TITLE_PRESET_TIMEOUT_MS = 60_000;
9
+ /**
10
+ * Whether a refusal means "this platform has no model configured" rather than
11
+ * "the model failed".
12
+ *
13
+ * The two need different handling: one is a permanent property of the
14
+ * environment (offer nothing, explain), the other is worth retrying. Both AI
15
+ * writes refuse with a 409 `AI_UNAVAILABLE`, while the two READS answer 200
16
+ * with `aiAvailable: false` — availability is a field there, never an error, so
17
+ * a page can render the disabled hint without provoking a failure state.
18
+ */
19
+ export function isAiUnavailable(error) {
20
+ return error instanceof PlatformError && error.code === AI_UNAVAILABLE_CODE;
21
+ }
22
+ export function createFeedAiModule(transport) {
23
+ return {
24
+ suggestCategories(input = {}, options) {
25
+ return transport.rest('plugins', { method: 'POST', path: `${MAPPINGS}/suggest`, body: input }, options);
26
+ },
27
+ categorySuggestStatus(options) {
28
+ return transport.rest('plugins', { method: 'GET', path: `${MAPPINGS}/suggest/status` }, options);
29
+ },
30
+ generateTitlePreset(configId, input = {}, options) {
31
+ return transport.rest('plugins', { method: 'POST', path: `${CONFIGS}/${segment(configId)}/title-preset`, body: input }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS, timeoutMs: TITLE_PRESET_TIMEOUT_MS });
32
+ },
33
+ titlePreset(configId, options) {
34
+ return transport.rest('plugins', { method: 'GET', path: `${CONFIGS}/${segment(configId)}/title-preset` }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
35
+ },
36
+ acceptSuggestion(suggestionId, options) {
37
+ return transport.rest('plugins', { method: 'POST', path: `${SUGGESTIONS}/${segment(suggestionId)}/accept` }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
38
+ },
39
+ dismissSuggestion(suggestionId, options) {
40
+ return transport.rest('plugins', { method: 'POST', path: `${SUGGESTIONS}/${segment(suggestionId)}/dismiss` }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,50 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call, ListOptions } from '../types.js';
3
+ import type { BulkStatusUpdateResult, BulkUpdateCategoryMappingStatusInput, CreateFeedCategoryMappingInput, FeedCategoryMapping, FeedCategoryMappingSource, FeedCategoryMappingStatus, FeedCategoryMatchType, FeedTaxonomyNode, UpdateFeedCategoryMappingInput } from './types.js';
4
+ export interface ListFeedCategoryMappingsOptions extends ListOptions {
5
+ matchType?: FeedCategoryMatchType;
6
+ status?: FeedCategoryMappingStatus;
7
+ source?: FeedCategoryMappingSource;
8
+ }
9
+ export interface SearchTaxonomyOptions extends Call {
10
+ /** 1…50; omitted ⇒ 20. */
11
+ limit?: number;
12
+ }
13
+ /**
14
+ * The channel's catalogue mapped onto the Google product taxonomy.
15
+ *
16
+ * Resolution order at generation time is `collection` → `facet_value` →
17
+ * `default`, and only `approved` rows are ever applied — a `suggested` row is
18
+ * a proposal awaiting a decision, never live configuration.
19
+ *
20
+ * Every taxonomy value that comes back is RESOLVED, never echoed: a path sent
21
+ * by a client is ignored and re-derived from the id against the vendored
22
+ * taxonomy, because a stale or hand-typed path is the classic way a whole feed
23
+ * gets disapproved.
24
+ */
25
+ export interface FeedCategoryMappingsModule {
26
+ list(options?: ListFeedCategoryMappingsOptions): Promise<Page<FeedCategoryMapping>>;
27
+ get(id: string, options?: Call): Promise<FeedCategoryMapping>;
28
+ create(input: CreateFeedCategoryMappingInput, options?: Call): Promise<FeedCategoryMapping>;
29
+ update(id: string, input: UpdateFeedCategoryMappingInput, options?: Call): Promise<FeedCategoryMapping>;
30
+ delete(id: string, options?: Call): Promise<void>;
31
+ /**
32
+ * Approve or reject up to {@link MAX_BULK_STATUS_IDS} mappings in one call —
33
+ * the AI review queue's primary action. Ids belonging to another channel are
34
+ * SKIPPED, not errored, so an `updated` count lower than `ids.length` is
35
+ * information rather than a failure.
36
+ */
37
+ bulkStatus(input: BulkUpdateCategoryMappingStatusInput, options?: Call): Promise<BulkStatusUpdateResult>;
38
+ }
39
+ /**
40
+ * Ranked search over the vendored Google product taxonomy.
41
+ *
42
+ * The file is identical for every tenant, so this route takes no channel — but
43
+ * it is still entitlement-gated, so a store without the feeds plugin connected
44
+ * is refused.
45
+ */
46
+ export interface FeedTaxonomyModule {
47
+ search(query: string, options?: SearchTaxonomyOptions): Promise<FeedTaxonomyNode[]>;
48
+ }
49
+ export declare function createFeedCategoryMappingsModule(transport: Transport): FeedCategoryMappingsModule;
50
+ export declare function createFeedTaxonomyModule(transport: Transport): FeedTaxonomyModule;
@@ -0,0 +1,56 @@
1
+ import { createPage, readRestPage } from '@sixtyninecommerce/core';
2
+ import { FEEDS_PRESERVE_KEYS, feedQuery, segment } from './preserve.js';
3
+ const MAPPINGS = '/feeds/category-mappings';
4
+ const TAXONOMY_SEARCH = '/feeds/taxonomy/search';
5
+ export function createFeedCategoryMappingsModule(transport) {
6
+ async function list(options) {
7
+ const body = await transport.rest('plugins', {
8
+ method: 'GET',
9
+ path: MAPPINGS,
10
+ query: feedQuery({
11
+ // The wire spelling, written out: query keys are NOT case-converted by
12
+ // the transport (only bodies and responses are), so `matchType` would
13
+ // reach the DTO as an unknown member and be dropped — silently
14
+ // listing every mapping in the channel instead of the one kind asked
15
+ // for.
16
+ match_type: options?.matchType,
17
+ status: options?.status,
18
+ source: options?.source,
19
+ limit: options?.limit,
20
+ after: options?.after,
21
+ }),
22
+ }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
23
+ const { items, meta } = readRestPage(body);
24
+ return createPage(items, meta, (cursor) => list({ ...options, after: cursor }));
25
+ }
26
+ return {
27
+ list,
28
+ get(id, options) {
29
+ return transport.rest('plugins', { method: 'GET', path: `${MAPPINGS}/${segment(id)}` }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
30
+ },
31
+ create(input, options) {
32
+ return transport.rest('plugins', { method: 'POST', path: MAPPINGS, body: input }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
33
+ },
34
+ update(id, input, options) {
35
+ return transport.rest('plugins', { method: 'PATCH', path: `${MAPPINGS}/${segment(id)}`, body: input }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
36
+ },
37
+ async delete(id, options) {
38
+ await transport.rest('plugins', { method: 'DELETE', path: `${MAPPINGS}/${segment(id)}`, expect: 'void' }, options);
39
+ },
40
+ bulkStatus(input, options) {
41
+ return transport.rest('plugins', { method: 'POST', path: `${MAPPINGS}/bulk-status`, body: input }, options);
42
+ },
43
+ };
44
+ }
45
+ export function createFeedTaxonomyModule(transport) {
46
+ return {
47
+ async search(query, options) {
48
+ const body = await transport.rest('plugins', {
49
+ method: 'GET',
50
+ path: TAXONOMY_SEARCH,
51
+ query: feedQuery({ q: query, limit: options?.limit }),
52
+ }, options);
53
+ return body.data;
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,54 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call, ListOptions } from '../types.js';
3
+ import type { CreateFeedConfigInput, FeedConfig, FeedDestination, FeedDestinationKey, FeedItemIssue, FeedItemIssueSeverity, FeedPreview, FeedPreviewDraft, FeedRegenerateResult, FeedRun, FeedRunStatus, RotateFeedTokenResult, UpdateFeedConfigInput } from './types.js';
4
+ /**
5
+ * A preview walks the catalogue and evaluates every rule, so it is slower than
6
+ * a read by an order of magnitude — the SDK's 15-second default would time out
7
+ * a sample the service is still perfectly happy to answer.
8
+ */
9
+ export declare const FEED_PREVIEW_TIMEOUT_MS = 60000;
10
+ export interface ListFeedConfigsOptions extends ListOptions {
11
+ destination?: FeedDestinationKey;
12
+ enabled?: boolean;
13
+ }
14
+ export interface ListFeedRunsOptions extends ListOptions {
15
+ status?: FeedRunStatus;
16
+ }
17
+ export interface ListFeedIssuesOptions extends ListOptions {
18
+ severity?: FeedItemIssueSeverity;
19
+ /** One issue code, e.g. `no_category`. Free text — codes are not a closed set. */
20
+ code?: string;
21
+ }
22
+ /**
23
+ * Feed configurations, their generation runs and the item issues the last run
24
+ * produced (spec §8.14).
25
+ *
26
+ * Every method is CHANNEL-SCOPED by the client's own tenant: the service reads
27
+ * `x-channel-id` and never takes a channel argument, so a feed created here
28
+ * belongs to the store this client is bound to and no other.
29
+ */
30
+ export interface FeedConfigsModule {
31
+ /** One page of the channel's feed configurations, newest first. */
32
+ list(options?: ListFeedConfigsOptions): Promise<Page<FeedConfig>>;
33
+ get(id: string, options?: Call): Promise<FeedConfig>;
34
+ create(input: CreateFeedConfigInput, options?: Call): Promise<FeedConfig>;
35
+ update(id: string, input: UpdateFeedConfigInput, options?: Call): Promise<FeedConfig>;
36
+ delete(id: string, options?: Call): Promise<void>;
37
+ /** Mint a new token; the previous public feed URL stops resolving immediately. */
38
+ rotateToken(id: string, options?: Call): Promise<RotateFeedTokenResult>;
39
+ /** Enqueue a regeneration; the run row appears in {@link FeedConfigsModule.runs} seconds later. */
40
+ regenerate(id: string, options?: Call): Promise<FeedRegenerateResult>;
41
+ /** The destination registry and its field templates — a frozen server-side constant. */
42
+ destinations(options?: Call): Promise<FeedDestination[]>;
43
+ /**
44
+ * Evaluate a DRAFT configuration against real catalogue rows. Persists
45
+ * nothing — no config, no run, no output — which is why it takes the whole
46
+ * draft rather than an id.
47
+ */
48
+ preview(draft: FeedPreviewDraft, options?: Call): Promise<FeedPreview>;
49
+ /** Generation history for one feed, newest first. Failed runs included — that is the point. */
50
+ runs(configId: string, options?: ListFeedRunsOptions): Promise<Page<FeedRun>>;
51
+ /** Item issues from the LATEST successful run — there is no run selector. */
52
+ issues(configId: string, options?: ListFeedIssuesOptions): Promise<Page<FeedItemIssue>>;
53
+ }
54
+ export declare function createFeedConfigsModule(transport: Transport): FeedConfigsModule;
@@ -0,0 +1,85 @@
1
+ import { createPage, readRestPage } from '@sixtyninecommerce/core';
2
+ import { FEEDS_PRESERVE_KEYS, feedQuery, segment } from './preserve.js';
3
+ const CONFIGS = '/feeds/configs';
4
+ /**
5
+ * A preview walks the catalogue and evaluates every rule, so it is slower than
6
+ * a read by an order of magnitude — the SDK's 15-second default would time out
7
+ * a sample the service is still perfectly happy to answer.
8
+ */
9
+ export const FEED_PREVIEW_TIMEOUT_MS = 60_000;
10
+ export function createFeedConfigsModule(transport) {
11
+ async function list(options) {
12
+ const body = await transport.rest('plugins', {
13
+ method: 'GET',
14
+ path: CONFIGS,
15
+ query: feedQuery({
16
+ destination: options?.destination,
17
+ enabled: options?.enabled,
18
+ limit: options?.limit,
19
+ after: options?.after,
20
+ }),
21
+ }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
22
+ const { items, meta } = readRestPage(body);
23
+ return createPage(items, meta, (cursor) => list({ ...options, after: cursor }));
24
+ }
25
+ async function runs(configId, options) {
26
+ const body = await transport.rest('plugins', {
27
+ method: 'GET',
28
+ path: `${CONFIGS}/${segment(configId)}/runs`,
29
+ query: feedQuery({
30
+ status: options?.status,
31
+ limit: options?.limit,
32
+ after: options?.after,
33
+ }),
34
+ }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
35
+ const { items, meta } = readRestPage(body);
36
+ return createPage(items, meta, (cursor) => runs(configId, { ...options, after: cursor }));
37
+ }
38
+ async function issues(configId, options) {
39
+ const body = await transport.rest('plugins', {
40
+ method: 'GET',
41
+ path: `${CONFIGS}/${segment(configId)}/issues`,
42
+ query: feedQuery({
43
+ severity: options?.severity,
44
+ code: options?.code,
45
+ limit: options?.limit,
46
+ after: options?.after,
47
+ }),
48
+ }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
49
+ const { items, meta } = readRestPage(body);
50
+ return createPage(items, meta, (cursor) => issues(configId, { ...options, after: cursor }));
51
+ }
52
+ return {
53
+ list,
54
+ runs,
55
+ issues,
56
+ get(id, options) {
57
+ return transport.rest('plugins', { method: 'GET', path: `${CONFIGS}/${segment(id)}` }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
58
+ },
59
+ create(input, options) {
60
+ return transport.rest('plugins', { method: 'POST', path: CONFIGS, body: input }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
61
+ },
62
+ update(id, input, options) {
63
+ return transport.rest('plugins', { method: 'PATCH', path: `${CONFIGS}/${segment(id)}`, body: input }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
64
+ },
65
+ async delete(id, options) {
66
+ await transport.rest('plugins', { method: 'DELETE', path: `${CONFIGS}/${segment(id)}`, expect: 'void' }, options);
67
+ },
68
+ rotateToken(id, options) {
69
+ return transport.rest('plugins', { method: 'POST', path: `${CONFIGS}/${segment(id)}/rotate-token` }, options);
70
+ },
71
+ regenerate(id, options) {
72
+ return transport.rest('plugins', { method: 'POST', path: `${CONFIGS}/${segment(id)}/regenerate` }, options);
73
+ },
74
+ async destinations(options) {
75
+ // `{ data: [...] }`, not a page: the registry is a frozen constant with no
76
+ // cursor, and `readRestPage` would dress it up as a page that can never
77
+ // have a second one.
78
+ const body = await transport.rest('plugins', { method: 'GET', path: `${CONFIGS}/destinations` }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS });
79
+ return body.data;
80
+ },
81
+ preview(draft, options) {
82
+ return transport.rest('plugins', { method: 'POST', path: `${CONFIGS}/preview`, body: draft }, { ...options, preserveKeys: FEEDS_PRESERVE_KEYS, timeoutMs: FEED_PREVIEW_TIMEOUT_MS });
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,25 @@
1
+ import type { Transport } from '@sixtyninecommerce/core';
2
+ import type { Call } from '../types.js';
3
+ /**
4
+ * Whether the engine this client talks to declares the product-feed identifier
5
+ * fields — `brand` / `condition` on a product, `gtin` / `mpn` on a variant.
6
+ *
7
+ * They are declared by the product-feeds plugin, and a caller deployed ahead of
8
+ * the engine (the dashboard ships independently) that selects or writes one the
9
+ * engine does not know fails GraphQL validation for the WHOLE document. Ask
10
+ * first; select and write them only when the answer is yes.
11
+ */
12
+ export interface FeedIdentifierFieldSupport {
13
+ /** `Product.customFields.brand` AND `.condition` exist. */
14
+ product: boolean;
15
+ /** `ProductVariant.customFields.gtin` AND `.mpn` exist. */
16
+ variant: boolean;
17
+ }
18
+ export interface FeedIdentifierFieldsModule {
19
+ /**
20
+ * Introspect the engine once. A type the engine does not have at all (no
21
+ * custom fields on that entity) reads as unsupported.
22
+ */
23
+ identifierFieldSupport(options?: Call): Promise<FeedIdentifierFieldSupport>;
24
+ }
25
+ export declare function createFeedIdentifierFieldsModule(transport: Transport): FeedIdentifierFieldsModule;
@@ -0,0 +1,29 @@
1
+ const IDENTIFIER_FIELDS_QUERY = /* GraphQL */ `
2
+ query FeedIdentifierFields {
3
+ product: __type(name: "ProductCustomFields") {
4
+ fields {
5
+ name
6
+ }
7
+ }
8
+ variant: __type(name: "ProductVariantCustomFields") {
9
+ fields {
10
+ name
11
+ }
12
+ }
13
+ }
14
+ `;
15
+ function declares(type, names) {
16
+ const declared = new Set((type?.fields ?? []).map((field) => field.name));
17
+ return names.every((name) => declared.has(name));
18
+ }
19
+ export function createFeedIdentifierFieldsModule(transport) {
20
+ return {
21
+ async identifierFieldSupport(options) {
22
+ const data = await transport.graphql({ api: 'admin', query: IDENTIFIER_FIELDS_QUERY, variables: {} }, options);
23
+ return {
24
+ product: declares(data?.product, ['brand', 'condition']),
25
+ variant: declares(data?.variant, ['gtin', 'mpn']),
26
+ };
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Product feeds (spec §8.14): the merchandising surface that turns the
3
+ * catalogue into a Google, Meta, TikTok or Pinterest feed.
4
+ *
5
+ * Five groups under one noun, because they are one workflow:
6
+ *
7
+ * - **the feed itself** — `admin.feeds.list/get/create/update/delete`, plus
8
+ * `preview` (evaluate a draft, persist nothing), `regenerate`, `rotateToken`,
9
+ * `runs` and `issues`;
10
+ * - **`internalFields` / `lookupTables`** — the channel-scoped pieces a rule
11
+ * may reference (`internal:<name>`, `{ kind: 'lookup', tableName }`);
12
+ * - **`categoryMappings` / `taxonomy`** — the catalogue mapped onto the Google
13
+ * product taxonomy, and the search behind every picker;
14
+ * - **`ai`** — the two model-backed helpers: categorising collections, and
15
+ * proposing a better `title` mapping.
16
+ *
17
+ * Everything but `taxonomy` is scoped to the client's own channel through
18
+ * `x-channel-id`; no method takes a channel argument.
19
+ *
20
+ * **The rule payloads ride verbatim.** `mappingJson`, `filtersJson`,
21
+ * `settingsJson`, `rulesJson`, `entriesJson`, `perDestinationJson`,
22
+ * `payloadJson`, `statsJson` and a template's `defaultSource` are stored and
23
+ * echoed byte-for-byte, and the AST inside them is camelCase — see
24
+ * `preserve.ts` for why converting any of it corrupts a merchant's saved rules
25
+ * in a way the round trip hides.
26
+ */
27
+ import type { Transport } from '@sixtyninecommerce/core';
28
+ import { type FeedAiModule } from './ai.js';
29
+ import { type FeedCategoryMappingsModule, type FeedTaxonomyModule } from './categories.js';
30
+ import { type FeedConfigsModule } from './configs.js';
31
+ import { type FeedIdentifierFieldsModule } from './identifier-fields.js';
32
+ import { type FeedInternalFieldsModule, type FeedLookupTablesModule } from './rules.js';
33
+ export type { FeedAiModule } from './ai.js';
34
+ export { isAiUnavailable, TITLE_PRESET_TIMEOUT_MS } from './ai.js';
35
+ export type { FeedCategoryMappingsModule, FeedTaxonomyModule, ListFeedCategoryMappingsOptions, SearchTaxonomyOptions, } from './categories.js';
36
+ export type { FeedConfigsModule, ListFeedConfigsOptions, ListFeedIssuesOptions, ListFeedRunsOptions, } from './configs.js';
37
+ export { FEED_PREVIEW_TIMEOUT_MS } from './configs.js';
38
+ export type { FeedIdentifierFieldSupport, FeedIdentifierFieldsModule, } from './identifier-fields.js';
39
+ export { FEEDS_PRESERVE_KEYS } from './preserve.js';
40
+ export type { FeedInternalFieldsModule, FeedLookupTablesModule } from './rules.js';
41
+ export * from './types.js';
42
+ export interface FeedsModule extends FeedConfigsModule, FeedIdentifierFieldsModule {
43
+ /** Computed values a mapping references as `internal:<name>`. */
44
+ internalFields: FeedInternalFieldsModule;
45
+ /** Substitution tables a `lookup` value source resolves through. */
46
+ lookupTables: FeedLookupTablesModule;
47
+ /** The catalogue mapped onto the Google product taxonomy. */
48
+ categoryMappings: FeedCategoryMappingsModule;
49
+ /** Ranked search over the vendored Google taxonomy. Not channel-scoped. */
50
+ taxonomy: FeedTaxonomyModule;
51
+ /** Model-backed helpers. Availability is a field on the reads, a 409 on the writes. */
52
+ ai: FeedAiModule;
53
+ }
54
+ export declare function createFeedsModule(transport: Transport): FeedsModule;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Product feeds (spec §8.14): the merchandising surface that turns the
3
+ * catalogue into a Google, Meta, TikTok or Pinterest feed.
4
+ *
5
+ * Five groups under one noun, because they are one workflow:
6
+ *
7
+ * - **the feed itself** — `admin.feeds.list/get/create/update/delete`, plus
8
+ * `preview` (evaluate a draft, persist nothing), `regenerate`, `rotateToken`,
9
+ * `runs` and `issues`;
10
+ * - **`internalFields` / `lookupTables`** — the channel-scoped pieces a rule
11
+ * may reference (`internal:<name>`, `{ kind: 'lookup', tableName }`);
12
+ * - **`categoryMappings` / `taxonomy`** — the catalogue mapped onto the Google
13
+ * product taxonomy, and the search behind every picker;
14
+ * - **`ai`** — the two model-backed helpers: categorising collections, and
15
+ * proposing a better `title` mapping.
16
+ *
17
+ * Everything but `taxonomy` is scoped to the client's own channel through
18
+ * `x-channel-id`; no method takes a channel argument.
19
+ *
20
+ * **The rule payloads ride verbatim.** `mappingJson`, `filtersJson`,
21
+ * `settingsJson`, `rulesJson`, `entriesJson`, `perDestinationJson`,
22
+ * `payloadJson`, `statsJson` and a template's `defaultSource` are stored and
23
+ * echoed byte-for-byte, and the AST inside them is camelCase — see
24
+ * `preserve.ts` for why converting any of it corrupts a merchant's saved rules
25
+ * in a way the round trip hides.
26
+ */
27
+ import { createFeedAiModule } from './ai.js';
28
+ import { createFeedCategoryMappingsModule, createFeedTaxonomyModule, } from './categories.js';
29
+ import { createFeedConfigsModule } from './configs.js';
30
+ import { createFeedIdentifierFieldsModule, } from './identifier-fields.js';
31
+ import { createFeedInternalFieldsModule, createFeedLookupTablesModule, } from './rules.js';
32
+ export { isAiUnavailable, TITLE_PRESET_TIMEOUT_MS } from './ai.js';
33
+ export { FEED_PREVIEW_TIMEOUT_MS } from './configs.js';
34
+ export { FEEDS_PRESERVE_KEYS } from './preserve.js';
35
+ export * from './types.js';
36
+ export function createFeedsModule(transport) {
37
+ return {
38
+ ...createFeedConfigsModule(transport),
39
+ ...createFeedIdentifierFieldsModule(transport),
40
+ internalFields: createFeedInternalFieldsModule(transport),
41
+ lookupTables: createFeedLookupTablesModule(transport),
42
+ categoryMappings: createFeedCategoryMappingsModule(transport),
43
+ taxonomy: createFeedTaxonomyModule(transport),
44
+ ai: createFeedAiModule(transport),
45
+ };
46
+ }