@sixtyninecommerce/admin 0.8.0 → 0.9.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 (161) hide show
  1. package/README.md +2445 -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 +489 -0
  28. package/dist/catalog/products.js +365 -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 +274 -0
  34. package/dist/catalog/variants.js +258 -0
  35. package/dist/catalog/visibility.d.ts +57 -0
  36. package/dist/catalog/visibility.js +59 -0
  37. package/dist/client.d.ts +171 -3
  38. package/dist/client.js +81 -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 +360 -0
  66. package/dist/fulfillments/index.js +425 -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 +84 -9
  74. package/dist/index.js +16 -1
  75. package/dist/jobs/bulk-action-run.d.ts +65 -0
  76. package/dist/jobs/bulk-action-run.js +70 -0
  77. package/dist/locations/index.d.ts +6 -2
  78. package/dist/meta/index.d.ts +32 -0
  79. package/dist/orders/commit-edit.d.ts +123 -0
  80. package/dist/orders/commit-edit.js +142 -0
  81. package/dist/orders/get.d.ts +722 -0
  82. package/dist/orders/get.js +530 -0
  83. package/dist/orders/index.d.ts +105 -20
  84. package/dist/orders/index.js +44 -0
  85. package/dist/orders/line-price-preview.d.ts +297 -0
  86. package/dist/orders/line-price-preview.js +50 -0
  87. package/dist/orders/manual-discounts.d.ts +132 -0
  88. package/dist/orders/manual-discounts.js +95 -0
  89. package/dist/orders/manual-pricing.d.ts +192 -0
  90. package/dist/orders/manual-pricing.js +72 -0
  91. package/dist/orders/order-entry.d.ts +566 -0
  92. package/dist/orders/order-entry.js +129 -0
  93. package/dist/orders/parcel-charge.d.ts +45 -0
  94. package/dist/orders/parcel-charge.js +55 -0
  95. package/dist/orders/plan-edit.d.ts +489 -0
  96. package/dist/orders/plan-edit.js +93 -0
  97. package/dist/orders/status.d.ts +235 -0
  98. package/dist/orders/status.js +96 -0
  99. package/dist/orders/surcharges.d.ts +75 -2
  100. package/dist/orders/surcharges.js +110 -4
  101. package/dist/payments/account-credit.d.ts +171 -0
  102. package/dist/payments/account-credit.js +107 -0
  103. package/dist/payments/index.d.ts +234 -9
  104. package/dist/payments/index.js +90 -0
  105. package/dist/platform/catalog.d.ts +148 -0
  106. package/dist/platform/catalog.js +74 -0
  107. package/dist/platform/entitlements.d.ts +111 -0
  108. package/dist/platform/entitlements.js +86 -0
  109. package/dist/platform/legal-entities.d.ts +139 -0
  110. package/dist/platform/legal-entities.js +126 -0
  111. package/dist/platform/legal-entity-numbering.d.ts +103 -0
  112. package/dist/platform/legal-entity-numbering.js +101 -0
  113. package/dist/platform/organization-membership.d.ts +80 -0
  114. package/dist/platform/organization-membership.js +71 -0
  115. package/dist/platform/organization-settings.d.ts +33 -0
  116. package/dist/platform/organization-settings.js +42 -0
  117. package/dist/platform/organization-sharing.d.ts +172 -0
  118. package/dist/platform/organization-sharing.js +120 -0
  119. package/dist/platform/organizations.d.ts +205 -0
  120. package/dist/platform/organizations.js +180 -0
  121. package/dist/platform/provision-store.d.ts +88 -8
  122. package/dist/platform/provision-store.js +12 -0
  123. package/dist/platform/reports.d.ts +46 -0
  124. package/dist/platform/reports.js +31 -0
  125. package/dist/platform/server-metrics.d.ts +58 -0
  126. package/dist/platform/server-metrics.js +11 -0
  127. package/dist/pos/index.d.ts +567 -0
  128. package/dist/pos/index.js +205 -0
  129. package/dist/pricing/index.d.ts +405 -0
  130. package/dist/pricing/index.js +351 -0
  131. package/dist/provenance/index.d.ts +93 -0
  132. package/dist/provenance/index.js +53 -0
  133. package/dist/reports/index.d.ts +199 -0
  134. package/dist/reports/index.js +154 -0
  135. package/dist/shipping/parcel-count-suggestion.d.ts +52 -0
  136. package/dist/shipping/parcel-count-suggestion.js +73 -0
  137. package/dist/shipping/parcel-rule.d.ts +48 -0
  138. package/dist/shipping/parcel-rule.js +91 -0
  139. package/dist/stock/index.d.ts +166 -0
  140. package/dist/stock/index.js +115 -0
  141. package/dist/stock/transfers.d.ts +520 -0
  142. package/dist/stock/transfers.js +437 -0
  143. package/dist/stored-value/index.d.ts +154 -0
  144. package/dist/stored-value/index.js +130 -0
  145. package/dist/tenant/capabilities.d.ts +10 -0
  146. package/dist/tenant/entitlements.d.ts +35 -0
  147. package/dist/tenant/entitlements.js +15 -0
  148. package/dist/tenant/field-groups.d.ts +64 -0
  149. package/dist/tenant/field-groups.js +34 -0
  150. package/dist/tenant/legal-entity.d.ts +32 -0
  151. package/dist/tenant/legal-entity.js +41 -0
  152. package/dist/tenant/notifications.d.ts +148 -0
  153. package/dist/tenant/notifications.js +79 -0
  154. package/dist/tenant/retention.d.ts +10 -4
  155. package/dist/tenant/settings-sources.d.ts +103 -0
  156. package/dist/tenant/settings-sources.js +92 -0
  157. package/dist/types.d.ts +86 -11
  158. package/dist/uploads/index.d.ts +11 -7
  159. package/dist/users/index.d.ts +104 -0
  160. package/dist/users/index.js +80 -0
  161. package/package.json +3 -2
@@ -0,0 +1,323 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { Job, JobHandle, JobsModule } from '../jobs/index.js';
3
+ import type { Call, ListOptions } from '../types.js';
4
+ import type { UploadsModule } from '../uploads/index.js';
5
+ /**
6
+ * What each line of a bulk file is.
7
+ *
8
+ * `order` carries external order entry's semantics (spec §8.1): see
9
+ * {@link BulkRow} for its row, and {@link BulkModule.export} for what an
10
+ * export of it holds. Content items are absent — they live in the CMS, which
11
+ * has no bulk writer yet.
12
+ */
13
+ export type BulkEntity = 'product' | 'variant' | 'price' | 'stock_level' | 'customer' | 'metafield' | 'order';
14
+ /**
15
+ * How an import applies each row.
16
+ *
17
+ * - `upsert` (the default) creates what is missing and updates what is there,
18
+ * so re-sending a file that was half applied CONVERGES. It is the only mode
19
+ * that is safe to repeat, which is why it is the default.
20
+ * - `create` skips a key that already exists rather than failing it.
21
+ * - `update` fails a row whose key does not exist.
22
+ *
23
+ * `price` and `stock_level` take no `create`: both belong to a variant that
24
+ * already exists, so there is nothing to create. `metafield` takes only
25
+ * `upsert`, because `setMetafield` is one by construction. `order` takes only
26
+ * `create`: an order is composed once and then moved by its own lifecycle
27
+ * (payments, park, settle), and is never RESTATED through bulk — so pass
28
+ * `mode: 'create'` explicitly, since the default is `upsert`. A mode an entity
29
+ * cannot apply is refused when you submit, not once per row.
30
+ */
31
+ export type BulkMode = 'create' | 'upsert' | 'update';
32
+ /**
33
+ * A row of a bulk file: one entity, as a JSON object, on one line.
34
+ *
35
+ * **The shapes are the SDK's own write inputs.** A `customer` row is what
36
+ * `admin.customers.create()` takes; a `metafield` row is what
37
+ * `admin.metafields.set()` takes. That is the contract of the resource, and it
38
+ * is what lets you test one row interactively and then send a million identical
39
+ * ones.
40
+ *
41
+ * The accepted fields per entity, with their keys (what a row is matched on):
42
+ *
43
+ * | entity | key | fields |
44
+ * | --- | --- | --- |
45
+ * | `product` | `slug` | `slug`, `name`, `description`, `enabled`, `facetValueIds`, `languageCode`, `customFields` |
46
+ * | `variant` | `sku` | `sku`, `productSlug` (to create), `name`, `price`, `prices`, `enabled`, `optionIds`, `facetValueIds`, `taxCategoryId`, `trackInventory`, `outOfStockThreshold`, `useGlobalOutOfStockThreshold`, `languageCode`, `customFields` |
47
+ * | `price` | `sku` | `sku`, `price`, `prices` |
48
+ * | `stock_level` | `sku` (+ location) | `sku`, `stockOnHand`, `stockLocationId` |
49
+ * | `customer` | `emailAddress` | `emailAddress`, `firstName`, `lastName`, `title`, `phoneNumber`, `customFields` |
50
+ * | `metafield` | the four together | `ownerType`, `ownerId`, `namespace`, `key`, `value` |
51
+ * | `order` | `idempotency_key` (+ `externalRef`) | `idempotency_key`, then every field of `admin.orders.createOrRecover()`'s input: `externalRef`, `customerId`, `location`, `billingAddress`, `lines` (with `kind: 'catalog' \| 'external'`), `surcharges`, `fulfilment`, `transitionTo`, `attachments` |
52
+ *
53
+ * **A `stock_level` row** writes only a location of this store: another
54
+ * store's location (or an id that does not exist) fails the row with
55
+ * `LOCATION_NOT_IN_CHANNEL`. A token or app restricted with `locationIds`
56
+ * writes only its own locations (`LOCATION_NOT_ALLOWED` for any other, dry run
57
+ * included), and its `stock_level` export holds only rows at those locations.
58
+ * The restriction is the one the credential had when the operation was
59
+ * submitted. Such a credential can read (`get`, `results`) only the
60
+ * `stock_level` operations it submitted itself.
61
+ *
62
+ * **An `order` row** is `admin.orders.createOrRecover(input, { idempotencyKey })`
63
+ * written as one object: the input's fields under their SDK names, and the key
64
+ * as `idempotency_key` (snake_case, like the rest of the bulk envelope, because
65
+ * it is the call's option rather than a field of the input). The key is
66
+ * REQUIRED and bulk never makes one up — a row without it fails. Re-sending a
67
+ * row is the entry call's own recovery: the same key and body answer `ok` with
68
+ * the id of the order the first send created (never a second order), a changed
69
+ * body under the key is `IDEMPOTENCY_CONFLICT`, and the same `externalRef` with
70
+ * a different basket is `EXTERNAL_REF_CONFLICT`. Every refusal the interactive
71
+ * call documents (`LINE_MISPRICED`, `TAX_CLASS_UNMAPPED`, …) is that row's
72
+ * `problem`, with the line's position as `details[].field` (`lines[1]`), and the
73
+ * rest of the file carries on. A row that prices a line or a surcharge itself
74
+ * needs `ImportExternalLines`, exactly as the interactive call does. A dry run
75
+ * checks the row's shape and that every variant, the `location` and the
76
+ * `customerId` exist in this store — and never calls order entry, so pricing
77
+ * and tax-class refusals only a real run can report.
78
+ *
79
+ * Two things worth knowing before you build a file:
80
+ *
81
+ * - **An unknown field is REFUSED, not ignored.** A misspelled `stockOnhand` in
82
+ * a ten-thousand-row feed fails those rows with a message naming the field,
83
+ * rather than completing green having changed nothing.
84
+ * - **Money is integer minor units**, like everywhere else on this platform:
85
+ * `1999`, never `19.99`. A fractional value is refused with that sentence.
86
+ */
87
+ export type BulkRow = Record<string, unknown>;
88
+ export type BulkRowStatus = 'ok' | 'skipped' | 'failed';
89
+ /** What happened to one row. */
90
+ export interface BulkRowResult {
91
+ /** 1-based, counting the file's NON-BLANK lines. */
92
+ row: number;
93
+ /**
94
+ * `ok` — applied (or, in a dry run, would have been). `skipped` — understood
95
+ * and deliberately not applied, which today means `create` mode meeting a key
96
+ * that already exists. `failed` — refused; `problem` says why.
97
+ */
98
+ status: BulkRowStatus;
99
+ /** The id of the entity written, when one was. */
100
+ id?: string;
101
+ /**
102
+ * RFC 9457, and the SAME document the interactive call would have answered
103
+ * with for the same input — same `code`, same `detail`. A client that already
104
+ * handles `admin.customers.create()` being refused handles this.
105
+ */
106
+ problem?: {
107
+ type: string;
108
+ title: string;
109
+ status: number;
110
+ code: string;
111
+ detail?: string;
112
+ };
113
+ [extra: string]: unknown;
114
+ }
115
+ export type BulkOperationStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled';
116
+ /** One bulk operation, and where its output is. */
117
+ export interface BulkOperation {
118
+ id: string;
119
+ operation: 'import' | 'export';
120
+ entity: BulkEntity;
121
+ mode: BulkMode | null;
122
+ dryRun: boolean;
123
+ containerId: string | null;
124
+ status: BulkOperationStatus;
125
+ /** Rows SEEN so far; the total once the operation is terminal. */
126
+ totalRows: number;
127
+ okRows: number;
128
+ skippedRows: number;
129
+ failedRows: number;
130
+ /**
131
+ * A signed link to the JSONL output — an export's rows, or an import's per-row
132
+ * results.
133
+ *
134
+ * **Minted per read and good for about fifteen minutes.** It is not stored, so
135
+ * do not persist it: read the operation again when you need a fresh one. The
136
+ * object itself lives in private storage and is not reachable any other way.
137
+ *
138
+ * For an IMPORT, check {@link resultTruncated} before treating this file as
139
+ * complete — `results(id)` is the authoritative set either way.
140
+ */
141
+ resultUrl: string | null;
142
+ /**
143
+ * The file behind {@link resultUrl} stopped SHORT of the run.
144
+ *
145
+ * The operation still succeeded and every row is still there — but in the
146
+ * TABLE, not in the file: `admin.bulk.results(id)` pages the complete set.
147
+ * It happens when the outcomes exceed the size one object may hold, which a
148
+ * file with tens of thousands of REFUSED rows reaches long before the row
149
+ * limit, because each refusal carries a problem document.
150
+ *
151
+ * Check it before you treat the downloaded file as the whole story.
152
+ */
153
+ resultTruncated: boolean;
154
+ error: {
155
+ code: string;
156
+ message: string;
157
+ } | null;
158
+ createdAt: string | null;
159
+ startedAt: string | null;
160
+ finishedAt: string | null;
161
+ [extra: string]: unknown;
162
+ }
163
+ export interface BulkImportOptions extends Call {
164
+ /** Default `upsert`. */
165
+ mode?: BulkMode;
166
+ /**
167
+ * Validate and look every row up, write NOTHING.
168
+ *
169
+ * The results say what each row WOULD have done, including "row 412's sku
170
+ * does not exist on this store" — a dry run makes the same lookups a real run
171
+ * makes. The staged file is still consumed, so a rehearsal and the real run
172
+ * mean staging the bytes twice.
173
+ */
174
+ dryRun?: boolean;
175
+ /**
176
+ * Your own handle for "these files are one logical import".
177
+ *
178
+ * It GROUPS; it does not coordinate. The operations sharing one still succeed
179
+ * and fail independently, because a multi-file import whose parts had to agree
180
+ * would need a transaction across a million rows.
181
+ */
182
+ containerId?: string;
183
+ /** Makes a retried submit return the SAME operation instead of a second one. */
184
+ idempotencyKey?: string;
185
+ /**
186
+ * Your own handle for the STAGING call, when this SDK has to stage the bytes.
187
+ *
188
+ * Staging is the call that hands out a signed URL, and a dropped response on
189
+ * it is the ordinary failure: without a token every retry would reserve
190
+ * another place and leave an object nobody ever claims. One is generated when
191
+ * you do not give one, which is right for a single attempt and wrong for a
192
+ * retry you drive yourself.
193
+ */
194
+ clientToken?: string;
195
+ /** A label for a human reading the upload list. Default `bulk.jsonl`. */
196
+ filename?: string;
197
+ }
198
+ export interface BulkExportOptions extends Call {
199
+ containerId?: string;
200
+ idempotencyKey?: string;
201
+ }
202
+ export interface BulkResultsFilter extends ListOptions {
203
+ }
204
+ /**
205
+ * What you can hand `admin.bulk.import` as the file.
206
+ *
207
+ * - **bytes** (`Uint8Array`, `ArrayBuffer`, `Blob`) — staged and PUT for you;
208
+ * - a **`ReadableStream`** — collected, then staged. A presigned PUT signs one
209
+ * exact length, so the bytes have to be counted before they are sent; there
210
+ * is no way to stage a stream of unknown size;
211
+ * - a **staged upload id** you staged yourself with
212
+ * `admin.uploads.stage({ purpose: 'bulk', … })`, which is how to send a file
213
+ * this process never holds whole.
214
+ *
215
+ * **A filesystem PATH is deliberately not one of them.** This package is
216
+ * isomorphic and depends on `@sixtyninecommerce/core` and nothing else, so it
217
+ * cannot reach `node:fs` — a dynamic import of it would break every browser
218
+ * bundle and every external install. In Node it is one line at the call site,
219
+ * and it is YOUR `readFile`, not a hidden one:
220
+ *
221
+ * ```ts
222
+ * const bytes = await readFile('./prices.jsonl');
223
+ * ```
224
+ */
225
+ export type BulkFile = {
226
+ stagedUploadId: string;
227
+ } | Uint8Array | ArrayBuffer | Blob | ReadableStream<Uint8Array>;
228
+ /**
229
+ * JSONL bulk import and export (spec §6.11, §15.22).
230
+ *
231
+ * **The shape of it.** One line per entity, of the same shapes the SDK's own
232
+ * writes take. One operation becomes one job; a malformed row fails THAT row
233
+ * and never the job; the per-row outcomes are a page you can walk, and the
234
+ * whole thing is also a JSONL file behind a signed link.
235
+ *
236
+ * ```ts
237
+ * const entity = 'price' as const;
238
+ * const bytes = await readFile('./prices.jsonl');
239
+ * const handle = await admin.bulk.import(entity, bytes, {
240
+ * mode: 'update',
241
+ * idempotencyKey: crypto.randomUUID(),
242
+ * });
243
+ * const result = await handle.wait();
244
+ * if (result.failedRows > 0) {
245
+ * for await (const row of await admin.bulk.results(result.id)) {
246
+ * if (row.status === 'failed') console.error(row.row, row.problem?.detail);
247
+ * }
248
+ * }
249
+ * ```
250
+ *
251
+ * **Field groups.** While an app or API token owns a field group on the store, a row that would
252
+ * change that group's data fails with `problem.code` `FIELD_GROUP_LOCKED` (the detail names the
253
+ * group and its owner), on every entity, unless the owner itself submitted the import. An
254
+ * unchanged value passes. A row that creates a record is never refused for it.
255
+ *
256
+ * **Feed identifiers.** A `product` row whose `customFields.brand`, or a `variant` row whose
257
+ * `customFields.gtin`, is NEW or CHANGED and breaks the feed rule (a brand over 70 characters, a
258
+ * GTIN that is not 8, 12, 13 or 14 digits, both after trimming) fails with `problem.code`
259
+ * `FEED_IDENTIFIER_INVALID` (status 400), creates included; the other rows carry on. A value the
260
+ * record already holds passes, so an edited export sent back is never refused for a value it did
261
+ * not change. A `dryRun` does not reach this rule: it stops before the write.
262
+ *
263
+ * **It does not spend your rate limit.** Submitting costs one request like any
264
+ * other write; the rows themselves are applied by the platform's own worker, so
265
+ * a million of them spend nothing of your budget (§15.22). Nothing is bypassed
266
+ * either — the same hooks, provenance and validation run as for an interactive
267
+ * write.
268
+ *
269
+ * **Rehearse first.** `dryRun` runs every check and every lookup and writes
270
+ * nothing. It is the cheapest way to find out that a column in a supplier's
271
+ * spreadsheet is in the wrong units.
272
+ *
273
+ * **An app installation's `customerData` level applies.** An export an app
274
+ * submits comes back as that app reads the Admin API: at `none` a customer
275
+ * row's `emailAddress`, `firstName` and `lastName` are `""`, and any field its
276
+ * level withholds is blank. A `customer` import, and a `customer` export
277
+ * filtered by `emails`, need `identity` (each row is matched by its email, so
278
+ * the outcome says whether an address is a customer); below it they are
279
+ * refused with `PermissionDeniedError` code `CUSTOMER_DATA_NOT_GRANTED`. An
280
+ * operation is readable (`get`, `results`) only at or above the level it was
281
+ * submitted at. A human's and a personal token's operations are unaffected.
282
+ */
283
+ export interface BulkModule {
284
+ /**
285
+ * Import a file of rows. Answers a handle whose `wait()` resolves with the
286
+ * finished operation, whatever the ending was.
287
+ */
288
+ import(entity: BulkEntity, file: BulkFile, options?: BulkImportOptions): Promise<JobHandle<BulkOperation>>;
289
+ /**
290
+ * Export rows to JSONL. The output is exactly what `import` accepts, so an
291
+ * export can be edited and sent back — except for `order`, below.
292
+ *
293
+ * `filter` narrows it, and an UNRECOGNISED key is refused rather than ignored
294
+ * — a filter nothing honours would turn an intended narrowing into a
295
+ * whole-store export that completes green. Common keys: `updated_after` (an
296
+ * ISO-8601 instant WITH an offset), and a key list per entity (`slugs`,
297
+ * `skus`, `emails`, and `ids` for `order`). `metafield` requires `owner_type`
298
+ * and `owner_ids`.
299
+ *
300
+ * **An `order` export is a record of the entry, not an input to re-import**
301
+ * (orders are not restated). One row per order — a shopper's open cart is not
302
+ * an order and is left out — with `id`, `code`, `externalRef`, `state`,
303
+ * `placedAt`, `customerId` and `currencyCode`; `lines` in the order the entry
304
+ * request sent them, each with its `id` and `position`: an externally priced
305
+ * line as `kind: 'external'` with its `label`, `taxClass`, and the `unitPrice`
306
+ * and `priceBasis` it was STATED in (the pair the platform holds fixed), a
307
+ * catalog line as `kind: 'catalog'` with its `productVariantId` and `sku`, and
308
+ * on every line `charged` — what the order computed, in both bases: `unitPrice`/`linePrice` before discounts, `proratedLinePrice(WithTax)` after line and order-level promotions (the figure the lines' sum to `subTotal` on). Then the
309
+ * `surcharges`, the order's totals, `billableTotalWithTax` (what the invoice
310
+ * charges, goodwill rounding folded in) beside the raw `totalWithTax`, and
311
+ * `payment` — `covered`, `settled`, `authorized`, `refunded`, `outstanding` at
312
+ * the moment of export. Money is integer minor units of `currencyCode`.
313
+ * Customer identity is the id only: no name, email or address leaves through
314
+ * an export, the same rule the `order.*` events keep.
315
+ */
316
+ export(entity: BulkEntity, filter?: Record<string, unknown>, options?: BulkExportOptions): Promise<JobHandle<BulkOperation>>;
317
+ /** One operation, with a freshly signed `resultUrl`. */
318
+ get(id: string, options?: Call): Promise<BulkOperation>;
319
+ /** The per-row outcomes, in ROW order. Iterate the page to walk them all. */
320
+ results(id: string, filter?: BulkResultsFilter): Promise<Page<BulkRowResult>>;
321
+ }
322
+ export declare function createBulkModule(transport: Transport, uploads: UploadsModule, jobs: JobsModule): BulkModule;
323
+ export type { Job, JobHandle };
@@ -0,0 +1,162 @@
1
+ import { createPage, readRestPage } from '@sixtyninecommerce/core';
2
+ /** The type a bulk file is staged as. */
3
+ const JSONL_CONTENT_TYPE = 'application/x-ndjson';
4
+ export function createBulkModule(transport, uploads, jobs) {
5
+ async function get(id, options) {
6
+ return transport.rest('plugins', { method: 'GET', path: `/bulk/${encodeURIComponent(id)}` }, options);
7
+ }
8
+ /**
9
+ * Wait for the JOB, then read the OPERATION.
10
+ *
11
+ * Two reads rather than one, because they answer different questions and the
12
+ * job resource is the one that knows when to stop polling. The operation is
13
+ * then read once, at the end, which is also when its `resultUrl` is worth
14
+ * having — a link minted while the run was going would have expired by the
15
+ * time the file existed.
16
+ */
17
+ function handleFor(jobId, options) {
18
+ return {
19
+ runId: jobId,
20
+ async wait(waitOptions) {
21
+ await jobs.wait(jobId, { ...options, ...waitOptions });
22
+ return get(jobId, options);
23
+ },
24
+ };
25
+ }
26
+ async function resultsPage(id, filter) {
27
+ const body = await transport.rest('plugins', {
28
+ method: 'GET',
29
+ path: `/bulk/${encodeURIComponent(id)}/results`,
30
+ query: { after: filter?.after, limit: filter?.limit },
31
+ }, filter);
32
+ const { items, meta } = readRestPage(body);
33
+ return createPage(items, meta, (cursor) => resultsPage(id, { ...filter, after: cursor }));
34
+ }
35
+ return {
36
+ async import(entity, file, options) {
37
+ const stagedUploadId = await resolveStagedUpload(uploads, file, options);
38
+ // `jobId`, not `job_id`. The transport camelises every response key
39
+ // (`snakeToCamelKeys`, on by default), so the controller's `{ job_id }`
40
+ // arrives as `{ jobId }` — destructuring the wire spelling yields
41
+ // `undefined` and every handle then polls `/jobs/undefined`.
42
+ const { jobId } = await transport.rest('plugins', {
43
+ method: 'POST',
44
+ path: '/bulk/imports',
45
+ body: {
46
+ entity,
47
+ ...(options?.mode === undefined ? {} : { mode: options.mode }),
48
+ ...(options?.dryRun === undefined ? {} : { dry_run: options.dryRun }),
49
+ staged_upload_id: stagedUploadId,
50
+ ...(options?.containerId === undefined ? {} : { container_id: options.containerId }),
51
+ },
52
+ },
53
+ // `idempotencyKey` rides on the OPTIONS, which is where the transport
54
+ // reads it to compose the `Idempotency-Key` header. Putting it in the
55
+ // call would have been silently ignored — the header would never be
56
+ // sent and a retried submit would start a second operation.
57
+ options);
58
+ return handleFor(jobId, options);
59
+ },
60
+ async export(entity, filter, options) {
61
+ const { jobId } = await transport.rest('plugins', {
62
+ method: 'POST',
63
+ path: '/bulk/exports',
64
+ body: {
65
+ entity,
66
+ ...(filter === undefined ? {} : { filter }),
67
+ ...(options?.containerId === undefined ? {} : { container_id: options.containerId }),
68
+ },
69
+ }, {
70
+ ...options,
71
+ // The filter is the CALLER's own vocabulary — `updated_after`,
72
+ // `owner_type` — and the transport camelises every key unless told
73
+ // not to. Without this, `updated_after` would arrive as `updatedAfter`
74
+ // and be refused as an unknown filter key, which is the one mistake
75
+ // that is impossible to debug from the outside.
76
+ preserveKeys: FILTER_PASSTHROUGH,
77
+ });
78
+ return handleFor(jobId, options);
79
+ },
80
+ get,
81
+ results: resultsPage,
82
+ };
83
+ }
84
+ /**
85
+ * `filter` keys travel EXACTLY as the caller spelled them.
86
+ *
87
+ * The service refuses an unrecognised filter key on purpose, so a transport
88
+ * that camelised `updated_after` into `updatedAfter` would turn every filtered
89
+ * export into a 400 naming a key the caller never typed.
90
+ */
91
+ const FILTER_PASSTHROUGH = new Set(['filter']);
92
+ /**
93
+ * Turn whatever the caller gave us into a staged upload id.
94
+ *
95
+ * The bytes are COUNTED before they are staged, because a presigned PUT signs
96
+ * one exact `Content-Length` and there is no way to sign an upper bound. That
97
+ * is also why a `ReadableStream` is collected rather than piped: the size has
98
+ * to be known first. A caller whose file is too large to hold should stage it
99
+ * themselves and pass the id.
100
+ */
101
+ async function resolveStagedUpload(uploads, file, options) {
102
+ if (typeof file === 'object' && file !== null && 'stagedUploadId' in file) {
103
+ return file.stagedUploadId;
104
+ }
105
+ const bytes = await readBytes(file);
106
+ const ticket = await uploads.stage({
107
+ purpose: 'bulk',
108
+ mimeType: JSONL_CONTENT_TYPE,
109
+ size: bytes.byteLength,
110
+ // A token of the caller's, when they gave one, so a dropped response on
111
+ // the staging call does not strand an object nobody will claim.
112
+ clientToken: options?.clientToken ?? randomToken(),
113
+ // A label for a human reading the upload list; it never reaches the
114
+ // object's location, which the platform composes from the store and an
115
+ // id it minted.
116
+ filename: options?.filename ?? 'bulk.jsonl',
117
+ },
118
+ // The CALLER's options, forwarded. Staging is a request like any other, and
119
+ // dropping them here sends it to the client's DEFAULT tenant while the
120
+ // submit that follows names the one the caller asked for — so the bytes
121
+ // land under the wrong store's prefix and the import then answers
122
+ // `UPLOAD_NOT_FOUND`, after they have already been uploaded. `signal` goes
123
+ // the same way: without it an abort cannot cancel the upload leg, which is
124
+ // the long one.
125
+ options);
126
+ await uploads.put(ticket, bytes);
127
+ return ticket.id;
128
+ }
129
+ async function readBytes(file) {
130
+ if (file instanceof Uint8Array)
131
+ return file;
132
+ if (file instanceof ArrayBuffer)
133
+ return new Uint8Array(file);
134
+ if (typeof Blob !== 'undefined' && file instanceof Blob) {
135
+ return new Uint8Array(await file.arrayBuffer());
136
+ }
137
+ return collectStream(file);
138
+ }
139
+ async function collectStream(stream) {
140
+ const chunks = [];
141
+ let total = 0;
142
+ const reader = stream.getReader();
143
+ for (;;) {
144
+ const { done, value } = await reader.read();
145
+ if (done)
146
+ break;
147
+ if (value) {
148
+ chunks.push(value);
149
+ total += value.byteLength;
150
+ }
151
+ }
152
+ const out = new Uint8Array(total);
153
+ let at = 0;
154
+ for (const chunk of chunks) {
155
+ out.set(chunk, at);
156
+ at += chunk.byteLength;
157
+ }
158
+ return out;
159
+ }
160
+ function randomToken() {
161
+ return globalThis.crypto.randomUUID();
162
+ }
@@ -0,0 +1,147 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call, ListOptions } from '../types.js';
3
+ import { type CatalogueOwnership } from './types.js';
4
+ export interface Asset {
5
+ id: string;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ name: string;
9
+ type: 'IMAGE' | 'VIDEO' | 'BINARY';
10
+ mimeType: string;
11
+ fileSize: number;
12
+ width: number;
13
+ height: number;
14
+ source: string;
15
+ preview: string;
16
+ /** Fractions of width and height (0–1); null when never set. */
17
+ focalPoint: {
18
+ x: number;
19
+ y: number;
20
+ } | null;
21
+ tags: string[];
22
+ /** The store's own asset, or its organization's; see {@link CatalogueOwnership}. */
23
+ catalogueOwnership: CatalogueOwnership;
24
+ }
25
+ export interface CreateAssetInput {
26
+ file: Blob;
27
+ /**
28
+ * The name the asset is stored and listed under. Required unless `file` is a
29
+ * `File` (which carries its own): a bare Blob would otherwise be stored as
30
+ * "blob".
31
+ */
32
+ fileName?: string;
33
+ tags?: string[];
34
+ }
35
+ export interface UpdateAssetInput {
36
+ id: string;
37
+ name?: string;
38
+ focalPoint?: {
39
+ x: number;
40
+ y: number;
41
+ } | null;
42
+ /** Replaces the tag list. */
43
+ tags?: string[];
44
+ }
45
+ /** One language's description of an image: its alt text in that language. */
46
+ export interface AssetDescription {
47
+ /** A language the store offers (`en`, `nl`, `pt_BR`). */
48
+ languageCode: string;
49
+ description: string;
50
+ }
51
+ /** What {@link AssetDescriptionsModule.set} saved. */
52
+ export interface AssetDescriptionsResult {
53
+ assetId: string;
54
+ /** The image's descriptions in the store, ordered by language. */
55
+ descriptions: AssetDescription[];
56
+ /** The image's alt text: the store's default-language description, or null when it has none. */
57
+ altText: string | null;
58
+ }
59
+ /**
60
+ * An image's descriptions in the client's store: what a storefront shows as its alt text, per
61
+ * language (headless W9-38). The content system keeps them per store; the engine keeps one alt
62
+ * text per image (`customFields.altText`), the store's default-language description.
63
+ */
64
+ export interface AssetDescriptionsModule {
65
+ /**
66
+ * The image's descriptions in the client's store, ordered by language; `[]` when it has none.
67
+ * The content system's public read: it can also hold a language the store no longer offers,
68
+ * which `set` refuses, so leave such an entry out when sending the set back.
69
+ */
70
+ get(assetId: string, options?: Call): Promise<AssetDescription[]>;
71
+ /**
72
+ * Save the image's descriptions in the client's store AND its alt text, as one change.
73
+ * `descriptions` is the WHOLE set, one entry per language the store offers: a language left
74
+ * out is removed, and leaving out the store's default language clears the alt text. The alt
75
+ * text is one value wherever the image is used: another store holding it reads the new one.
76
+ *
77
+ * Both change or neither does. Repeating a call is safe: both are set to its input again,
78
+ * whatever an earlier attempt left, so a call that failed with `outcomeUnknown` is settled by
79
+ * sending it again. No idempotency key is needed (and none is acted on).
80
+ *
81
+ * Refused, nothing written: `CMS_PERMISSION_NOT_ALLOWED` (403, the caller holds no
82
+ * UpdateCmsAsset in the store), `ENTITY_NOT_FOUND` (404, an image this store does not hold),
83
+ * `MANAGED_BY_ORGANIZATION` (422, an image of the store's organization: change it in the
84
+ * workspace), `SHARED_ASSET_NOT_ALLOWED` (403, an image another store holds that the caller
85
+ * cannot change), `ASSET_DESCRIPTIONS_INVALID` (400, a language the store does not offer or
86
+ * named twice, a blank text, a text over 500 characters; `details[]` points at each),
87
+ * `FORBIDDEN` (403, neither UpdateCatalog nor UpdateAsset). `CMS_REQUEST_FAILED` (422): the
88
+ * content system did not save; nothing changed, or, with `outcomeUnknown`, the descriptions may
89
+ * have been saved while the alt text was not: send the same call again.
90
+ *
91
+ * Requires a platform release that serves `setAssetDescriptions`.
92
+ */
93
+ set(assetId: string, descriptions: readonly AssetDescription[], options?: Call): Promise<AssetDescriptionsResult>;
94
+ }
95
+ export interface AssetListOptions extends ListOptions {
96
+ /** Case-insensitive substring of the name. */
97
+ name?: string;
98
+ /** Assets carrying ALL of these tags. */
99
+ tags?: string[];
100
+ }
101
+ /**
102
+ * The asset library. Assign an asset to a product, variant or collection with
103
+ * `assetIds` / `featuredAssetId` on that entity's own write.
104
+ *
105
+ * `create` uploads through the Admin API (GraphQL multipart), and is never
106
+ * retried by the SDK — a retry would upload the file twice. A file type the
107
+ * store does not permit is refused with `MIME_TYPE_ERROR`
108
+ * (`BusinessRuleError`): "The MIME type "…" is not permitted." For large files,
109
+ * `uploads` (spec §6.11) stages them directly to storage instead.
110
+ *
111
+ * A refused delete — "The selected Asset is featured by N Products" — is a
112
+ * `ConflictError` (`ASSET_NOT_DELETED`) unless `force` is set.
113
+ */
114
+ export interface AssetsModule {
115
+ /** An image's descriptions (its alt text per language) in the store. */
116
+ descriptions: AssetDescriptionsModule;
117
+ list(options?: AssetListOptions): Promise<Page<Asset>>;
118
+ get(id: string, options?: Call): Promise<Asset | null>;
119
+ create(input: CreateAssetInput, options?: Call): Promise<Asset>;
120
+ update(input: UpdateAssetInput, options?: Call): Promise<Asset>;
121
+ delete(id: string, options?: Call & {
122
+ force?: boolean;
123
+ }): Promise<void>;
124
+ }
125
+ /**
126
+ * An asset's own fields, without `catalogueOwnership`: what a read outside the catalogue (the
127
+ * settings images) selects, so it keeps working against an engine older than the field.
128
+ */
129
+ export declare const ASSET_FILE_FIELDS = "\n id\n createdAt\n updatedAt\n name\n type\n mimeType\n fileSize\n width\n height\n source\n preview\n focalPoint {\n x\n y\n }\n tags {\n value\n }\n";
130
+ export declare const ASSET_FIELDS = "\n \n id\n createdAt\n updatedAt\n name\n type\n mimeType\n fileSize\n width\n height\n source\n preview\n focalPoint {\n x\n y\n }\n tags {\n value\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n";
131
+ export declare const ASSETS_QUERY = "\n query CatalogAssets($options: AssetListOptions) {\n assets(options: $options) {\n items { \n \n id\n createdAt\n updatedAt\n name\n type\n mimeType\n fileSize\n width\n height\n source\n preview\n focalPoint {\n x\n y\n }\n tags {\n value\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n totalItems\n }\n }\n";
132
+ export declare const ASSET_QUERY = "\n query CatalogAsset($id: ID!) {\n asset(id: $id) { \n \n id\n createdAt\n updatedAt\n name\n type\n mimeType\n fileSize\n width\n height\n source\n preview\n focalPoint {\n x\n y\n }\n tags {\n value\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n }\n";
133
+ export declare const CREATE_ASSETS_MUTATION = "\n mutation CatalogCreateAssets($input: [CreateAssetInput!]!) {\n createAssets(input: $input) {\n ... on Asset { \n \n id\n createdAt\n updatedAt\n name\n type\n mimeType\n fileSize\n width\n height\n source\n preview\n focalPoint {\n x\n y\n }\n tags {\n value\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n ... on ErrorResult {\n errorCode\n message\n }\n }\n }\n";
134
+ export declare const UPDATE_ASSET_MUTATION = "\n mutation CatalogUpdateAsset($input: UpdateAssetInput!) {\n updateAsset(input: $input) { \n \n id\n createdAt\n updatedAt\n name\n type\n mimeType\n fileSize\n width\n height\n source\n preview\n focalPoint {\n x\n y\n }\n tags {\n value\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n }\n";
135
+ export declare const DELETE_ASSET_MUTATION = "\n mutation CatalogDeleteAsset($input: DeleteAssetInput!) {\n deleteAsset(input: $input) { \n result\n message\n }\n }\n";
136
+ export declare const SET_ASSET_DESCRIPTIONS_MUTATION = "\n mutation CatalogSetAssetDescriptions(\n $assetId: ID!\n $descriptions: [AssetDescriptionInput!]!\n ) {\n setAssetDescriptions(assetId: $assetId, descriptions: $descriptions) {\n assetId\n descriptions {\n languageCode\n description\n }\n altText\n }\n }\n";
137
+ /** An asset as {@link ASSET_FILE_FIELDS} reads it: no `catalogueOwnership`. */
138
+ export type AssetFile = Omit<Asset, 'catalogueOwnership'>;
139
+ /** An asset as the Admin API answers it: tags as objects. */
140
+ export type WireOf<T extends AssetFile> = Omit<T, 'tags'> & {
141
+ tags: Array<{
142
+ value: string;
143
+ }>;
144
+ };
145
+ export declare function toAsset(wire: WireOf<Asset>): Asset;
146
+ export declare function toAsset(wire: WireOf<AssetFile>): AssetFile;
147
+ export declare function createAssetsModule(transport: Transport): AssetsModule;