@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.
- package/README.md +1501 -23
- package/dist/auth/index.d.ts +7 -1
- package/dist/auth/index.js +8 -1
- package/dist/catalog/documents.d.ts +142 -0
- package/dist/catalog/documents.js +114 -0
- package/dist/catalog/index.d.ts +48 -7
- package/dist/catalog/index.js +6 -1
- package/dist/catalog/metadata.d.ts +244 -0
- package/dist/catalog/metadata.js +220 -0
- package/dist/catalog/properties.d.ts +38 -0
- package/dist/catalog/properties.js +52 -1
- package/dist/catalog/recommendations.d.ts +33 -6
- package/dist/catalog/recommendations.js +94 -2
- package/dist/catalog/reviews.d.ts +17 -4
- package/dist/catalog/reviews.js +7 -0
- package/dist/catalog/variants.d.ts +60 -0
- package/dist/catalog/variants.js +39 -0
- package/dist/catalog/visibility.d.ts +18 -2
- package/dist/client.d.ts +29 -3
- package/dist/client.js +19 -8
- package/dist/content/assets.d.ts +3 -4
- package/dist/content/forms.d.ts +332 -1
- package/dist/content/forms.js +140 -4
- package/dist/content/items.d.ts +31 -8
- package/dist/content/items.js +1 -1
- package/dist/content/menus.d.ts +254 -0
- package/dist/content/menus.js +70 -0
- package/dist/content/strings.d.ts +137 -0
- package/dist/content/strings.js +82 -0
- package/dist/customers/delivery-terms.d.ts +44 -0
- package/dist/customers/delivery-terms.js +32 -0
- package/dist/customers/index.d.ts +15 -0
- package/dist/customers/index.js +4 -0
- package/dist/documents/emails.d.ts +150 -0
- package/dist/documents/emails.js +220 -0
- package/dist/documents/index.d.ts +13 -0
- package/dist/documents/index.js +2 -0
- package/dist/fulfillments/index.d.ts +119 -1
- package/dist/fulfillments/index.js +124 -4
- package/dist/index.d.ts +43 -23
- package/dist/index.js +11 -5
- package/dist/integrations/exact-online-types.d.ts +542 -0
- package/dist/integrations/exact-online.d.ts +541 -5
- package/dist/integrations/exact-online.js +714 -1
- package/dist/integrations/index.d.ts +23 -8
- package/dist/integrations/index.js +9 -4
- package/dist/integrations/pipeline-discoveries.d.ts +65 -0
- package/dist/integrations/pipeline-discoveries.js +65 -0
- package/dist/integrations/pipeline-runs.d.ts +119 -0
- package/dist/integrations/pipeline-runs.js +87 -0
- package/dist/integrations/pipelines.d.ts +73 -2
- package/dist/integrations/pipelines.js +36 -1
- package/dist/integrations/price-import.d.ts +223 -0
- package/dist/integrations/price-import.js +124 -0
- package/dist/integrations/types.d.ts +221 -3
- package/dist/integrations/types.js +4 -3
- package/dist/meta/index.d.ts +10 -0
- package/dist/metafields/index.d.ts +8 -1
- package/dist/metafields/index.js +10 -0
- package/dist/orders/cancel-with-money.d.ts +10 -0
- package/dist/orders/cancel-with-money.js +2 -0
- package/dist/orders/commit-edit.d.ts +16 -0
- package/dist/orders/commit-edit.js +14 -1
- package/dist/orders/deposit-policy.d.ts +3 -1
- package/dist/orders/index.d.ts +22 -11
- package/dist/orders/index.js +5 -2
- package/dist/orders/list.d.ts +26 -5
- package/dist/orders/list.js +123 -13
- package/dist/orders/manual-discounts.d.ts +13 -5
- package/dist/orders/money-view.d.ts +149 -0
- package/dist/orders/money-view.js +99 -0
- package/dist/orders/plan-edit.d.ts +34 -2
- package/dist/orders/plan-edit.js +110 -12
- package/dist/orders/surcharges.d.ts +12 -0
- package/dist/orders/surcharges.js +34 -9
- package/dist/payments/account-credit.d.ts +17 -1
- package/dist/payments/account-credit.js +25 -2
- package/dist/payments/credit-noted-fields.d.ts +8 -1
- package/dist/payments/credit-noted-fields.js +34 -2
- package/dist/payments/customer-consent.d.ts +30 -0
- package/dist/payments/customer-consent.js +49 -0
- package/dist/payments/index.d.ts +89 -14
- package/dist/payments/index.js +22 -2
- package/dist/payments/requests.d.ts +6 -3
- package/dist/payments/requests.js +1 -0
- package/dist/pickups/index.d.ts +5 -3
- package/dist/pickups/index.js +5 -2
- package/dist/platform/email-gateways.d.ts +165 -0
- package/dist/platform/email-gateways.js +78 -0
- package/dist/platform/tax.d.ts +11 -3
- package/dist/platform/tax.js +30 -0
- package/dist/pos/exchanges.d.ts +6 -1
- package/dist/pos/index.d.ts +229 -10
- package/dist/pos/index.js +113 -4
- package/dist/pos/offline-sales.d.ts +44 -3
- package/dist/pos/offline-sales.js +57 -3
- package/dist/pos/returns.d.ts +28 -1
- package/dist/pos/returns.js +36 -6
- package/dist/pricing/price-lists.d.ts +3 -1
- package/dist/purchasing/alerts.d.ts +19 -4
- package/dist/purchasing/alerts.js +29 -3
- package/dist/purchasing/backorders.d.ts +61 -0
- package/dist/purchasing/backorders.js +92 -0
- package/dist/purchasing/index.d.ts +21 -3
- package/dist/purchasing/index.js +8 -0
- package/dist/purchasing/links.d.ts +263 -0
- package/dist/purchasing/links.js +192 -0
- package/dist/purchasing/purchase-orders.d.ts +4 -1
- package/dist/purchasing/receipts.d.ts +396 -0
- package/dist/purchasing/receipts.js +287 -0
- package/dist/purchasing/returns.d.ts +273 -0
- package/dist/purchasing/returns.js +229 -0
- package/dist/quotes/index.d.ts +20 -3
- package/dist/returns/index.d.ts +62 -1
- package/dist/returns/index.js +61 -0
- package/dist/seo/config.d.ts +128 -0
- package/dist/seo/config.js +95 -0
- package/dist/seo/redirects.d.ts +125 -0
- package/dist/seo/redirects.js +60 -1
- package/dist/seo/settings.d.ts +147 -0
- package/dist/seo/settings.js +209 -0
- package/dist/shipping/index.d.ts +6 -0
- package/dist/shipping/index.js +2 -0
- package/dist/shipping/methods.d.ts +8 -1
- package/dist/shipping/myparcel.d.ts +415 -1
- package/dist/shipping/myparcel.js +291 -3
- package/dist/shipping/rate-tables.d.ts +149 -4
- package/dist/shipping/rate-tables.js +229 -23
- package/dist/shipping/ship-to-countries.d.ts +75 -0
- package/dist/shipping/ship-to-countries.js +47 -0
- package/dist/stock/by-location.d.ts +86 -0
- package/dist/stock/by-location.js +94 -0
- package/dist/stock/index.d.ts +8 -1
- package/dist/stock/index.js +4 -0
- package/dist/stock/settings.d.ts +106 -0
- package/dist/stock/settings.js +103 -0
- package/dist/stock/transfers.d.ts +171 -10
- package/dist/stock/transfers.js +82 -4
- package/dist/stored-value/index.d.ts +98 -7
- package/dist/stored-value/index.js +71 -4
- package/dist/tax/display-policy.d.ts +105 -0
- package/dist/tax/display-policy.js +84 -0
- package/dist/tax/index.d.ts +3 -0
- package/dist/tax/index.js +2 -0
- package/dist/tenant/email-templates.d.ts +222 -1
- package/dist/tenant/email-templates.js +227 -10
- package/package.json +2 -2
package/dist/content/forms.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { type Page, type Transport } from '@sixtyninecommerce/core';
|
|
2
2
|
import type { Call, ListOptions } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The field types the CMS accepts on a new field (`CreateFormFieldDto.field_type`), pinned
|
|
5
|
+
* against the CMS's published `openapi.json` by `content-coverage.spec.ts`. The members that
|
|
6
|
+
* carry a field's type stay `string` so a type a newer CMS adds still reads, and so code that
|
|
7
|
+
* already passes a `string` keeps compiling; `fieldTypes()` answers each type's rules at runtime.
|
|
8
|
+
*/
|
|
9
|
+
export type FormFieldType = 'text' | 'textarea' | 'email' | 'phone' | 'number' | 'select' | 'checkbox' | 'radio' | 'date' | 'file' | 'hidden';
|
|
3
10
|
/**
|
|
4
11
|
* Everything a visitor reads is TRANSLATED, per language, in its own row:
|
|
5
12
|
* `forms_translations` and `form_fields_translations`. `name` and `slug` are
|
|
@@ -70,21 +77,40 @@ export interface Form {
|
|
|
70
77
|
/** Free-form JSONB the CMS stores verbatim — never case-converted. */
|
|
71
78
|
settings: Record<string, unknown> | null;
|
|
72
79
|
translations?: FormTranslation[];
|
|
80
|
+
/**
|
|
81
|
+
* The form's fields, REMOVED ones included (each with its `removedAt`): see
|
|
82
|
+
* {@link FormField.removedAt}. Not on `list`.
|
|
83
|
+
*/
|
|
73
84
|
fields?: FormField[];
|
|
85
|
+
/** How many submissions the form holds, deleted ones not counted. On `list` only. */
|
|
86
|
+
submissionCount?: number;
|
|
74
87
|
createdAt: string;
|
|
75
88
|
updatedAt: string;
|
|
76
89
|
[extra: string]: unknown;
|
|
77
90
|
}
|
|
78
91
|
export interface FormField {
|
|
79
92
|
id: string;
|
|
93
|
+
formId: string;
|
|
80
94
|
name: string;
|
|
95
|
+
/** The key a submission's answer for this field is stored under. */
|
|
81
96
|
slug: string;
|
|
97
|
+
/** One of {@link FormFieldType}. */
|
|
82
98
|
fieldType: string;
|
|
83
99
|
isRequired: boolean;
|
|
100
|
+
/** The field's place in the form, lowest first. */
|
|
84
101
|
position: number;
|
|
85
102
|
config: Record<string, unknown> | null;
|
|
86
103
|
validation: Record<string, unknown> | null;
|
|
87
104
|
translations?: FormFieldTranslation[];
|
|
105
|
+
/**
|
|
106
|
+
* When the field was removed from the form, or `null` for a live field. Every answer that
|
|
107
|
+
* carries a form's `fields` includes its removed ones (storefronts never see them), so the
|
|
108
|
+
* answers older submissions hold under a removed field's slug can still be labelled: filter
|
|
109
|
+
* on this to show the form a visitor fills in.
|
|
110
|
+
*/
|
|
111
|
+
removedAt: string | null;
|
|
112
|
+
createdAt: string;
|
|
113
|
+
updatedAt: string;
|
|
88
114
|
[extra: string]: unknown;
|
|
89
115
|
}
|
|
90
116
|
/**
|
|
@@ -96,14 +122,38 @@ export interface FormField {
|
|
|
96
122
|
*/
|
|
97
123
|
export interface FormFieldInput {
|
|
98
124
|
name: string;
|
|
125
|
+
/**
|
|
126
|
+
* Lowercase letters and digits in words joined by single UNDERSCORES (`first_name`), unlike
|
|
127
|
+
* a form's hyphenated slug; unique among the form's live fields. A submission sends the
|
|
128
|
+
* field's answer under it.
|
|
129
|
+
*/
|
|
99
130
|
slug: string;
|
|
131
|
+
/** One of {@link FormFieldType}. */
|
|
100
132
|
fieldType: string;
|
|
101
133
|
isRequired?: boolean;
|
|
134
|
+
/** Defaults to after the form's last live field. */
|
|
102
135
|
position?: number;
|
|
103
136
|
config?: Record<string, unknown>;
|
|
104
137
|
validation?: Record<string, unknown>;
|
|
105
138
|
translations?: FormFieldTranslationInput[];
|
|
106
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The members `fields.update` acts on: those of {@link FormFieldInput}, each optional. An absent
|
|
142
|
+
* key leaves the stored value alone; `null` clears `config` or `validation`, each of which is
|
|
143
|
+
* replaced whole, never merged. `translations` writes the languages it names and keeps the
|
|
144
|
+
* others. A new `position` is written as given and moves no other field: give each field its
|
|
145
|
+
* number to reorder a form.
|
|
146
|
+
*/
|
|
147
|
+
export interface UpdateFormFieldInput {
|
|
148
|
+
name?: string;
|
|
149
|
+
slug?: string;
|
|
150
|
+
fieldType?: string;
|
|
151
|
+
isRequired?: boolean;
|
|
152
|
+
position?: number;
|
|
153
|
+
config?: Record<string, unknown> | null;
|
|
154
|
+
validation?: Record<string, unknown> | null;
|
|
155
|
+
translations?: FormFieldTranslationInput[];
|
|
156
|
+
}
|
|
107
157
|
/**
|
|
108
158
|
* Exactly what `UpsertFormDto` whitelists. `slug` is NOT here on purpose: it
|
|
109
159
|
* comes from the URL, and sending it is a 400 under `forbidNonWhitelisted`.
|
|
@@ -123,11 +173,292 @@ export interface FormInput {
|
|
|
123
173
|
translations?: FormTranslationInput[];
|
|
124
174
|
fields?: FormFieldInput[];
|
|
125
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Exactly what `CreateFormDto` whitelists. A new form has no fields: add them with
|
|
178
|
+
* `fields.create`, or write a form and its fields in one call with `upsertBySlug`.
|
|
179
|
+
*/
|
|
180
|
+
export interface CreateFormInput {
|
|
181
|
+
name: string;
|
|
182
|
+
/**
|
|
183
|
+
* Lowercase letters and digits in words joined by single hyphens (`contact-us`); unique among
|
|
184
|
+
* the store's forms. What a storefront reads the form by.
|
|
185
|
+
*/
|
|
186
|
+
slug: string;
|
|
187
|
+
/** Comma-separated addresses each submission is e-mailed to. */
|
|
188
|
+
notificationEmails?: string | null;
|
|
189
|
+
/** Where a storefront sends the visitor after a submission. */
|
|
190
|
+
redirectUrlOnSuccess?: string | null;
|
|
191
|
+
/** What a submission does; the CMS's default is `store`. */
|
|
192
|
+
submitAction?: string;
|
|
193
|
+
/** Stored verbatim. */
|
|
194
|
+
settings?: Record<string, unknown> | null;
|
|
195
|
+
translations?: FormTranslationInput[];
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* What `UpdateFormDto` whitelists. An absent key leaves the stored value alone; `null` clears
|
|
199
|
+
* `notificationEmails`, `redirectUrlOnSuccess` and `settings`. `settings` is replaced whole,
|
|
200
|
+
* never merged: read the form, change the bag, write it back. `translations` writes the
|
|
201
|
+
* languages it names and keeps the others. A new `slug` changes what storefronts read the form
|
|
202
|
+
* by.
|
|
203
|
+
*/
|
|
204
|
+
export interface UpdateFormInput {
|
|
205
|
+
name?: string;
|
|
206
|
+
slug?: string;
|
|
207
|
+
notificationEmails?: string | null;
|
|
208
|
+
redirectUrlOnSuccess?: string | null;
|
|
209
|
+
submitAction?: string;
|
|
210
|
+
settings?: Record<string, unknown> | null;
|
|
211
|
+
translations?: FormTranslationInput[];
|
|
212
|
+
}
|
|
213
|
+
/** A form as a reference to it shows it: what a picker lists and a chosen form's label reads. */
|
|
214
|
+
export interface FormReference {
|
|
215
|
+
id: string;
|
|
216
|
+
name: string;
|
|
217
|
+
/** What a storefront reads the form by. */
|
|
218
|
+
slug: string;
|
|
219
|
+
/**
|
|
220
|
+
* How many submissions the form holds, on `references` only and only for a caller with
|
|
221
|
+
* `ReadCmsForm` (the storefront projection has no count).
|
|
222
|
+
*/
|
|
223
|
+
submissionCount?: number;
|
|
224
|
+
}
|
|
225
|
+
export interface FormListOptions extends ListOptions {
|
|
226
|
+
/** Forms whose name contains this, ignoring case. */
|
|
227
|
+
search?: string;
|
|
228
|
+
/**
|
|
229
|
+
* `name`, `created_at` or `updated_at`, with a leading `-` for descending. Anything else (and
|
|
230
|
+
* no sort) is newest first.
|
|
231
|
+
*/
|
|
232
|
+
sort?: string;
|
|
233
|
+
}
|
|
234
|
+
/** One rule a field type honours on a submission, read from `validation` first, then `config`. */
|
|
235
|
+
export interface FormFieldConstraint {
|
|
236
|
+
/** The key in the field's `validation` (or `config`): `min_length`, `pattern`, `options`, … */
|
|
237
|
+
key: string;
|
|
238
|
+
valueType: string;
|
|
239
|
+
description: string;
|
|
240
|
+
}
|
|
241
|
+
/** One field type as `GET /forms/field-types` describes it. */
|
|
242
|
+
export interface FormFieldTypeDescriptor {
|
|
243
|
+
/** One of {@link FormFieldType}. */
|
|
244
|
+
type: string;
|
|
245
|
+
label: string;
|
|
246
|
+
description: string;
|
|
247
|
+
/** The shape of the answer a submission sends for a field of this type. */
|
|
248
|
+
valueType: string;
|
|
249
|
+
constraints: FormFieldConstraint[];
|
|
250
|
+
notes?: string;
|
|
251
|
+
[extra: string]: unknown;
|
|
252
|
+
}
|
|
253
|
+
/** The shape a form slug or a field slug must have. */
|
|
254
|
+
export interface FormSlugRule {
|
|
255
|
+
/** The regular expression's source. */
|
|
256
|
+
pattern: string;
|
|
257
|
+
maxLength: number;
|
|
258
|
+
description: string;
|
|
259
|
+
validExamples: string[];
|
|
260
|
+
invalidExamples: string[];
|
|
261
|
+
[extra: string]: unknown;
|
|
262
|
+
}
|
|
263
|
+
export interface FormFieldTypeRegistry {
|
|
264
|
+
fieldTypes: FormFieldTypeDescriptor[];
|
|
265
|
+
slugRules: {
|
|
266
|
+
form: FormSlugRule;
|
|
267
|
+
field: FormSlugRule;
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* One visitor's submission of a form. PERSONAL DATA: `data` is whatever the visitor typed (a
|
|
272
|
+
* name, an address, a message), and `ipAddress` / `userAgent` identify their device.
|
|
273
|
+
*/
|
|
274
|
+
export interface FormSubmission {
|
|
275
|
+
id: string;
|
|
276
|
+
formId: string;
|
|
277
|
+
channelId: string;
|
|
278
|
+
/**
|
|
279
|
+
* The visitor's answers, keyed by field slug EXACTLY as stored (`first_name` stays
|
|
280
|
+
* `first_name`): never case-converted. Keys of removed fields stay in older submissions.
|
|
281
|
+
*/
|
|
282
|
+
data: Record<string, unknown>;
|
|
283
|
+
/** The language the visitor submitted in, when the storefront named one. */
|
|
284
|
+
localeCode: string | null;
|
|
285
|
+
ipAddress: string | null;
|
|
286
|
+
userAgent: string | null;
|
|
287
|
+
metadata: Record<string, unknown> | null;
|
|
288
|
+
createdAt: string;
|
|
289
|
+
updatedAt: string;
|
|
290
|
+
[extra: string]: unknown;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* A form's fields. Every method takes the form's id first; a form of another store, a removed
|
|
294
|
+
* form, or a field of another form is `NotFoundError` (404). A field write records the form's
|
|
295
|
+
* change exactly like a form write.
|
|
296
|
+
*/
|
|
297
|
+
export interface FormFieldsModule {
|
|
298
|
+
/**
|
|
299
|
+
* Adds a field. A slug taken by a live field of the form is `ConflictError` (409); an unknown
|
|
300
|
+
* `fieldType` or a slug of the wrong shape is `ValidationError` (`VALIDATION_FAILED`). Answers
|
|
301
|
+
* the field as stored, with its translations (a CMS before 2026-10-15 answers it without
|
|
302
|
+
* them).
|
|
303
|
+
*
|
|
304
|
+
* @effect write
|
|
305
|
+
* @permission CreateCmsForm
|
|
306
|
+
*/
|
|
307
|
+
create(formId: string, input: FormFieldInput, options?: Call): Promise<FormField>;
|
|
308
|
+
/**
|
|
309
|
+
* Writes what the input names. Answers the field with its translations.
|
|
310
|
+
*
|
|
311
|
+
* @effect write
|
|
312
|
+
* @permission UpdateCmsForm
|
|
313
|
+
*/
|
|
314
|
+
update(formId: string, fieldId: string, input: UpdateFormFieldInput, options?: Call): Promise<FormField>;
|
|
315
|
+
/**
|
|
316
|
+
* Removes the field from the form: storefronts stop rendering it and a new submission may no
|
|
317
|
+
* longer answer it, its slug is free again, and nothing in the SDK brings it back. Submissions
|
|
318
|
+
* keep what they answered under it, and the form's `fields` keep it with its `removedAt`.
|
|
319
|
+
*
|
|
320
|
+
* @effect destructive
|
|
321
|
+
* @permission DeleteCmsForm
|
|
322
|
+
*/
|
|
323
|
+
delete(formId: string, fieldId: string, options?: Call): Promise<void>;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* A form's submissions: PERSONAL DATA (see {@link FormSubmission}). A form of another store is
|
|
327
|
+
* `NotFoundError` (404).
|
|
328
|
+
*/
|
|
329
|
+
export interface FormSubmissionsModule {
|
|
330
|
+
/**
|
|
331
|
+
* The form's submissions, newest first, a page at a time, with `Page.total`.
|
|
332
|
+
*
|
|
333
|
+
* Without `after`, pages follow the CMS's cursor: a walk (`for await`) sees every submission
|
|
334
|
+
* once, even while visitors submit or others are deleted. `after: offsetCursor(n)` opens the
|
|
335
|
+
* page that starts at offset `n` (a table's page `n`), and the pages after it go by offset, so
|
|
336
|
+
* a submission that arrives or is deleted meanwhile can push a row onto the next page twice or
|
|
337
|
+
* past it. A page's `nextCursor`, passed back as `after`, continues in the page's own mode.
|
|
338
|
+
* The CMS answers at most 200 a page in cursor mode, whatever `limit` asks.
|
|
339
|
+
*
|
|
340
|
+
* An app installation below `customerData: full` is refused: `PermissionDeniedError` with
|
|
341
|
+
* code `CUSTOMER_DATA_NOT_GRANTED` (free text cannot be partly withheld). A staff member or a
|
|
342
|
+
* personal API token needs only the permission.
|
|
343
|
+
*
|
|
344
|
+
* @effect read
|
|
345
|
+
* @permission ReadCmsForm
|
|
346
|
+
*/
|
|
347
|
+
list(formId: string, options?: ListOptions): Promise<Page<FormSubmission>>;
|
|
348
|
+
/**
|
|
349
|
+
* Removes the submission from every read and from the form's `submissionCount`; nothing in
|
|
350
|
+
* the SDK brings it back. It is NOT an erasure: the CMS keeps the row, marked removed, until
|
|
351
|
+
* the store itself is purged.
|
|
352
|
+
*
|
|
353
|
+
* @effect destructive
|
|
354
|
+
* @permission DeleteCmsForm
|
|
355
|
+
*/
|
|
356
|
+
delete(formId: string, submissionId: string, options?: Call): Promise<void>;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The store's forms (a contact form, a newsletter sign-up), their fields and their submissions,
|
|
360
|
+
* through the CMS.
|
|
361
|
+
*
|
|
362
|
+
* Every method needs the permission it names (`@permission`). The form reads are public CMS
|
|
363
|
+
* routes that answer anyone without `ReadCmsForm` on the store the storefront projection, which
|
|
364
|
+
* these methods refuse with `AdminProjectionError` instead of returning half a form.
|
|
365
|
+
*
|
|
366
|
+
* Refusals are the CMS's problem+json, mapped by status:
|
|
367
|
+
* - `NotFoundError` (404, `NOT_FOUND`): no such form, field or submission in this store.
|
|
368
|
+
* - `ConflictError` (409, `CONFLICT`): the slug is taken (a form's in the store, a field's in its
|
|
369
|
+
* form).
|
|
370
|
+
* - `ValidationError` (400): `VALIDATION_FAILED` with one `details[]` entry per refused input
|
|
371
|
+
* member (a slug of the wrong shape, an unknown field type, an address that is not one in
|
|
372
|
+
* `notificationEmails`, an id that is not a UUID).
|
|
373
|
+
* - `PermissionDeniedError` (403): the credential lacks the method's permission.
|
|
374
|
+
*
|
|
375
|
+
* Every form or field write records the store's site-content change (`content.site_content_changed`
|
|
376
|
+
* with kind `form_definition`) and `form.changed`; deleting a submission records neither.
|
|
377
|
+
* `create` and `upsertBySlug` honour an `idempotencyKey` (the CMS replays the first answer);
|
|
378
|
+
* every other write sends it and it acts on nothing.
|
|
379
|
+
*/
|
|
126
380
|
export interface FormsModule {
|
|
127
|
-
|
|
381
|
+
/**
|
|
382
|
+
* The store's forms, a page at a time, newest first unless `sort` says otherwise; each with
|
|
383
|
+
* its `submissionCount`, none with its fields. Pages are offset-based (`Page.total` counts the
|
|
384
|
+
* matches), so `after: offsetCursor((n - 1) * size)` opens page `n`.
|
|
385
|
+
*
|
|
386
|
+
* @effect read
|
|
387
|
+
* @permission ReadCmsForm
|
|
388
|
+
*/
|
|
389
|
+
list(options?: FormListOptions): Promise<Page<Form>>;
|
|
390
|
+
/**
|
|
391
|
+
* One form with its translations and its fields, removed ones included.
|
|
392
|
+
*
|
|
393
|
+
* @effect read
|
|
394
|
+
* @permission ReadCmsForm
|
|
395
|
+
*/
|
|
128
396
|
get(id: string, options?: Call): Promise<Form>;
|
|
397
|
+
/**
|
|
398
|
+
* One form by its slug, as {@link FormsModule.get}.
|
|
399
|
+
*
|
|
400
|
+
* @effect read
|
|
401
|
+
* @permission ReadCmsForm
|
|
402
|
+
*/
|
|
129
403
|
getBySlug(slug: string, options?: Call): Promise<Form>;
|
|
404
|
+
/**
|
|
405
|
+
* The store's forms as references (id, name, slug), a page at a time, in `list`'s order and
|
|
406
|
+
* with its `search` and `sort`. Unlike `list` it needs no permission: it reads what a
|
|
407
|
+
* storefront reads, so a caller without `ReadCmsForm` (a content editor choosing the form a
|
|
408
|
+
* page shows) can still name and pick one.
|
|
409
|
+
*
|
|
410
|
+
* @effect read
|
|
411
|
+
*/
|
|
412
|
+
references(options?: FormListOptions): Promise<Page<FormReference>>;
|
|
413
|
+
/**
|
|
414
|
+
* One form as a reference (id, name, slug), for any caller: `NotFoundError` for a form this
|
|
415
|
+
* store does not hold.
|
|
416
|
+
*
|
|
417
|
+
* @effect read
|
|
418
|
+
*/
|
|
419
|
+
reference(id: string, options?: Call): Promise<FormReference>;
|
|
420
|
+
/**
|
|
421
|
+
* The field types a form can hold, the rules each honours, and the shapes a form slug and a
|
|
422
|
+
* field slug must have. The same for every store.
|
|
423
|
+
*
|
|
424
|
+
* @effect read
|
|
425
|
+
*/
|
|
426
|
+
fieldTypes(options?: Call): Promise<FormFieldTypeRegistry>;
|
|
427
|
+
/**
|
|
428
|
+
* Adds a form without fields. Answers it as stored, with its translations (a CMS before
|
|
429
|
+
* 2026-10-15 answers it without them).
|
|
430
|
+
*
|
|
431
|
+
* @effect write
|
|
432
|
+
* @permission CreateCmsForm
|
|
433
|
+
*/
|
|
434
|
+
create(input: CreateFormInput, options?: Call): Promise<Form>;
|
|
435
|
+
/**
|
|
436
|
+
* Writes what the input names. Answers the form as it now is, with its translations and its
|
|
437
|
+
* fields (a CMS before 2026-10-15 answers the translations as they were BEFORE this write).
|
|
438
|
+
*
|
|
439
|
+
* @effect write
|
|
440
|
+
* @permission UpdateCmsForm
|
|
441
|
+
*/
|
|
442
|
+
update(id: string, input: UpdateFormInput, options?: Call): Promise<Form>;
|
|
443
|
+
/**
|
|
444
|
+
* Creates the form when its slug is free in the store, or updates it, and upserts each listed
|
|
445
|
+
* field by slug in the same transaction; fields not listed are left alone. Creating needs
|
|
446
|
+
* `CreateCmsForm`, updating `UpdateCmsForm`.
|
|
447
|
+
*
|
|
448
|
+
* @effect write
|
|
449
|
+
* @permission CreateCmsForm
|
|
450
|
+
* @permission UpdateCmsForm
|
|
451
|
+
*/
|
|
130
452
|
upsertBySlug(slug: string, input: FormInput, options?: Call): Promise<Form>;
|
|
453
|
+
/**
|
|
454
|
+
* Removes the form: storefronts reading it get `NotFoundError`, its slug is free again, its
|
|
455
|
+
* submissions can no longer be listed, and nothing in the SDK brings it back.
|
|
456
|
+
*
|
|
457
|
+
* @effect destructive
|
|
458
|
+
* @permission DeleteCmsForm
|
|
459
|
+
*/
|
|
131
460
|
delete(id: string, options?: Call): Promise<void>;
|
|
461
|
+
fields: FormFieldsModule;
|
|
462
|
+
submissions: FormSubmissionsModule;
|
|
132
463
|
}
|
|
133
464
|
export declare function createFormsModule(transport: Transport): FormsModule;
|
package/dist/content/forms.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createPage, offsetCursor, PLATFORM_PASSTHROUGH_KEYS, readOffsetCursor, } from '@sixtyninecommerce/core';
|
|
1
|
+
import { createPage, offsetCursor, PLATFORM_PASSTHROUGH_KEYS, readOffsetCursor, snakeToCamelKeys, } from '@sixtyninecommerce/core';
|
|
2
2
|
import { AdminProjectionError } from '../errors.js';
|
|
3
3
|
const DEFAULT_PAGE_SIZE = 25;
|
|
4
|
+
const DEFAULT_SUBMISSION_PAGE_SIZE = 50;
|
|
4
5
|
/**
|
|
5
6
|
* A form carries three bags the CMS stores verbatim: `settings`, and each
|
|
6
7
|
* field's `config` and `validation` (whose constraint keys are snake_case —
|
|
@@ -9,6 +10,18 @@ const DEFAULT_PAGE_SIZE = 25;
|
|
|
9
10
|
* `Transport.rest` preserves nothing that is not named.
|
|
10
11
|
*/
|
|
11
12
|
const FORM_PASSTHROUGH = PLATFORM_PASSTHROUGH_KEYS;
|
|
13
|
+
/**
|
|
14
|
+
* A submission's answers are keyed by the merchant's field slugs (`first_name`), which the
|
|
15
|
+
* transport would rename (`firstName`) on the way in. `data` cannot join the shared set: it is
|
|
16
|
+
* also the CMS's list envelope. So the list keeps the envelope's rows verbatim and camelises
|
|
17
|
+
* each row here, its `data` excepted (`PLATFORM_PASSTHROUGH_KEYS` header in
|
|
18
|
+
* `@sixtyninecommerce/core`'s `case.ts`). Both paging modes answer the rows under `data`; the
|
|
19
|
+
* `items` copy a cursor page also carries is not read.
|
|
20
|
+
*/
|
|
21
|
+
const SUBMISSION_ENVELOPE = new Set(['data']);
|
|
22
|
+
const SUBMISSION_ROW = new Set([...PLATFORM_PASSTHROUGH_KEYS, 'data']);
|
|
23
|
+
/** The cursor-mode opener: `after` present and empty starts the CMS's keyset walk. */
|
|
24
|
+
const FIRST_CURSOR_PAGE = '';
|
|
12
25
|
/**
|
|
13
26
|
* `GET /forms*` is a PUBLIC CMS route with OPTIONAL auth: a token the CMS does
|
|
14
27
|
* not accept for this channel is not rejected — the route answers 200 with the
|
|
@@ -23,9 +36,42 @@ function assertAdminProjection(form, what) {
|
|
|
23
36
|
return form;
|
|
24
37
|
throw new AdminProjectionError(what);
|
|
25
38
|
}
|
|
39
|
+
/** The reference members: the same under the admin and the storefront projection, but the count. */
|
|
40
|
+
function toReference(form) {
|
|
41
|
+
return {
|
|
42
|
+
id: form.id,
|
|
43
|
+
name: form.name,
|
|
44
|
+
slug: form.slug,
|
|
45
|
+
...(typeof form.submissionCount === 'number' ? { submissionCount: form.submissionCount } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function formPath(id) {
|
|
49
|
+
return `/forms/${encodeURIComponent(id)}`;
|
|
50
|
+
}
|
|
51
|
+
function fieldPath(formId, fieldId) {
|
|
52
|
+
return `${formPath(formId)}/fields/${encodeURIComponent(fieldId)}`;
|
|
53
|
+
}
|
|
54
|
+
function submissionsPath(formId) {
|
|
55
|
+
return `${formPath(formId)}/submissions`;
|
|
56
|
+
}
|
|
57
|
+
/** The offset an `offsetCursor` names, or null for any other cursor (the CMS's own). */
|
|
58
|
+
function offsetOf(cursor) {
|
|
59
|
+
try {
|
|
60
|
+
return readOffsetCursor(cursor);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
26
66
|
export function createFormsModule(transport) {
|
|
27
67
|
async function listPage(offset, limit, options) {
|
|
28
|
-
const body = await transport.rest('cms', {
|
|
68
|
+
const body = await transport.rest('cms', {
|
|
69
|
+
method: 'GET',
|
|
70
|
+
path: '/forms',
|
|
71
|
+
// `offset` is always sent: it keeps the CMS out of its keyset mode, which cannot resume
|
|
72
|
+
// a `sort` and cannot open page n.
|
|
73
|
+
query: { limit, offset, search: options?.search, sort: options?.sort },
|
|
74
|
+
}, { ...options, preserveKeys: FORM_PASSTHROUGH });
|
|
29
75
|
const items = body.data ?? [];
|
|
30
76
|
// `GET /forms` is `@Public()` exactly like the two single reads, so a
|
|
31
77
|
// credential the CMS will not accept for this channel is answered 200 with
|
|
@@ -40,6 +86,73 @@ export function createFormsModule(transport) {
|
|
|
40
86
|
const hasNext = advanced < total;
|
|
41
87
|
return createPage(items, { nextCursor: hasNext ? offsetCursor(advanced) : null, hasNext, total }, (cursor) => listPage(readOffsetCursor(cursor), limit, options));
|
|
42
88
|
}
|
|
89
|
+
async function referencePage(offset, limit, options) {
|
|
90
|
+
// The bags are dropped below, so nothing in them needs preserving.
|
|
91
|
+
const body = await transport.rest('cms', {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
path: '/forms',
|
|
94
|
+
query: { limit, offset, search: options?.search, sort: options?.sort },
|
|
95
|
+
}, options);
|
|
96
|
+
const items = (body.data ?? []).map(toReference);
|
|
97
|
+
const total = body.pagination?.total ?? items.length;
|
|
98
|
+
const advanced = offset + (items.length > 0 ? items.length : limit);
|
|
99
|
+
const hasNext = advanced < total;
|
|
100
|
+
return createPage(items, { nextCursor: hasNext ? offsetCursor(advanced) : null, hasNext, total }, (cursor) => referencePage(readOffsetCursor(cursor), limit, options));
|
|
101
|
+
}
|
|
102
|
+
async function writeForm(method, path, input, options) {
|
|
103
|
+
const body = await transport.rest('cms', { method, path, body: input }, { ...options, preserveKeys: FORM_PASSTHROUGH });
|
|
104
|
+
return body.data;
|
|
105
|
+
}
|
|
106
|
+
async function submissionPage(formId, limit, position, options) {
|
|
107
|
+
const body = await transport.rest('cms', {
|
|
108
|
+
method: 'GET',
|
|
109
|
+
path: submissionsPath(formId),
|
|
110
|
+
query: 'offset' in position ? { limit, offset: position.offset } : { limit, ...position },
|
|
111
|
+
}, { ...options, preserveKeys: SUBMISSION_ENVELOPE });
|
|
112
|
+
const rows = body.data ?? [];
|
|
113
|
+
const items = rows.map((row) => snakeToCamelKeys(row, { preserveKeys: SUBMISSION_ROW }));
|
|
114
|
+
const total = body.pagination?.total;
|
|
115
|
+
if ('offset' in position) {
|
|
116
|
+
// An offset page echoes no offset: the next one is built from what THIS call asked for,
|
|
117
|
+
// advancing by the limit past an empty page so a walk always ends.
|
|
118
|
+
const advanced = position.offset + (items.length > 0 ? items.length : limit);
|
|
119
|
+
const hasNext = advanced < (total ?? items.length);
|
|
120
|
+
return createPage(items, { nextCursor: hasNext ? offsetCursor(advanced) : null, hasNext, total }, (cursor) => submissionPage(formId, limit, { offset: readOffsetCursor(cursor) }, options));
|
|
121
|
+
}
|
|
122
|
+
const nextCursor = typeof body.nextCursor === 'string' && body.nextCursor !== '' ? body.nextCursor : null;
|
|
123
|
+
const hasNext = body.hasNext === true && nextCursor !== null;
|
|
124
|
+
return createPage(items, { nextCursor: hasNext ? nextCursor : null, hasNext, total }, (cursor) => submissionPage(formId, limit, { after: cursor }, options));
|
|
125
|
+
}
|
|
126
|
+
const fields = {
|
|
127
|
+
async create(formId, input, options) {
|
|
128
|
+
const body = await transport.rest('cms', { method: 'POST', path: `${formPath(formId)}/fields`, body: input }, { ...options, preserveKeys: FORM_PASSTHROUGH });
|
|
129
|
+
return body.data;
|
|
130
|
+
},
|
|
131
|
+
async update(formId, fieldId, input, options) {
|
|
132
|
+
const body = await transport.rest('cms', { method: 'PATCH', path: fieldPath(formId, fieldId), body: input }, { ...options, preserveKeys: FORM_PASSTHROUGH });
|
|
133
|
+
return body.data;
|
|
134
|
+
},
|
|
135
|
+
async delete(formId, fieldId, options) {
|
|
136
|
+
await transport.rest('cms', { method: 'DELETE', path: fieldPath(formId, fieldId), expect: 'void' }, options);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const submissions = {
|
|
140
|
+
list(formId, options) {
|
|
141
|
+
const limit = options?.limit ?? DEFAULT_SUBMISSION_PAGE_SIZE;
|
|
142
|
+
if (options?.after === undefined) {
|
|
143
|
+
return submissionPage(formId, limit, { after: FIRST_CURSOR_PAGE }, options);
|
|
144
|
+
}
|
|
145
|
+
const offset = offsetOf(options.after);
|
|
146
|
+
return submissionPage(formId, limit, offset === null ? { after: options.after } : { offset }, options);
|
|
147
|
+
},
|
|
148
|
+
async delete(formId, submissionId, options) {
|
|
149
|
+
await transport.rest('cms', {
|
|
150
|
+
method: 'DELETE',
|
|
151
|
+
path: `${submissionsPath(formId)}/${encodeURIComponent(submissionId)}`,
|
|
152
|
+
expect: 'void',
|
|
153
|
+
}, options);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
43
156
|
return {
|
|
44
157
|
list(options) {
|
|
45
158
|
const limit = options?.limit ?? DEFAULT_PAGE_SIZE;
|
|
@@ -47,19 +160,42 @@ export function createFormsModule(transport) {
|
|
|
47
160
|
return listPage(offset, limit, options);
|
|
48
161
|
},
|
|
49
162
|
async get(id, options) {
|
|
50
|
-
const body = await transport.rest('cms', { method: 'GET', path:
|
|
163
|
+
const body = await transport.rest('cms', { method: 'GET', path: formPath(id) }, { ...options, preserveKeys: FORM_PASSTHROUGH });
|
|
51
164
|
return assertAdminProjection(body.data, `form "${id}"`);
|
|
52
165
|
},
|
|
53
166
|
async getBySlug(slug, options) {
|
|
54
167
|
const body = await transport.rest('cms', { method: 'GET', path: `/forms/by-slug/${encodeURIComponent(slug)}` }, { ...options, preserveKeys: FORM_PASSTHROUGH });
|
|
55
168
|
return assertAdminProjection(body.data, `form "${slug}"`);
|
|
56
169
|
},
|
|
170
|
+
references(options) {
|
|
171
|
+
const limit = options?.limit ?? DEFAULT_PAGE_SIZE;
|
|
172
|
+
const offset = options?.after === undefined ? 0 : readOffsetCursor(options.after);
|
|
173
|
+
return referencePage(offset, limit, options);
|
|
174
|
+
},
|
|
175
|
+
async reference(id, options) {
|
|
176
|
+
const body = await transport.rest('cms', { method: 'GET', path: formPath(id) }, options);
|
|
177
|
+
return toReference(body.data);
|
|
178
|
+
},
|
|
179
|
+
async fieldTypes(options) {
|
|
180
|
+
// Every key of this answer is the CMS's own camelCase vocabulary; the constraint keys a
|
|
181
|
+
// field's bags use (`min_length`) are VALUES here, which the conversion never touches.
|
|
182
|
+
const body = await transport.rest('cms', { method: 'GET', path: '/forms/field-types' }, options);
|
|
183
|
+
return { fieldTypes: body.data, slugRules: body.meta.slugRules };
|
|
184
|
+
},
|
|
185
|
+
create(input, options) {
|
|
186
|
+
return writeForm('POST', '/forms', input, options);
|
|
187
|
+
},
|
|
188
|
+
update(id, input, options) {
|
|
189
|
+
return writeForm('PATCH', formPath(id), input, options);
|
|
190
|
+
},
|
|
57
191
|
async upsertBySlug(slug, input, options) {
|
|
58
192
|
const body = await transport.rest('cms', { method: 'PUT', path: `/forms/by-slug/${encodeURIComponent(slug)}`, body: input }, { ...options, preserveKeys: FORM_PASSTHROUGH });
|
|
59
193
|
return body.data;
|
|
60
194
|
},
|
|
61
195
|
async delete(id, options) {
|
|
62
|
-
await transport.rest('cms', { method: 'DELETE', path:
|
|
196
|
+
await transport.rest('cms', { method: 'DELETE', path: formPath(id), expect: 'void' }, options);
|
|
63
197
|
},
|
|
198
|
+
fields,
|
|
199
|
+
submissions,
|
|
64
200
|
};
|
|
65
201
|
}
|
package/dist/content/items.d.ts
CHANGED
|
@@ -73,6 +73,29 @@ export interface ContentItemWithFields {
|
|
|
73
73
|
fieldValues: Record<string, unknown>;
|
|
74
74
|
[extra: string]: unknown;
|
|
75
75
|
}
|
|
76
|
+
/** One named item's outcome in `items.bulkSetStatus`. */
|
|
77
|
+
export interface ContentItemStatusOutcome {
|
|
78
|
+
id: string;
|
|
79
|
+
/** `OK`, or `FAILED` (a string on the wire: read anything else as not done). */
|
|
80
|
+
status: 'OK' | 'FAILED' | (string & {});
|
|
81
|
+
/**
|
|
82
|
+
* When FAILED: `VALIDATION_ERROR` (publishing an item that leaves a required field empty, the
|
|
83
|
+
* refusal `publish` answers) or `NOT_FOUND` (no live item of the type in this store).
|
|
84
|
+
*/
|
|
85
|
+
code: string | null;
|
|
86
|
+
/** When FAILED: the CMS's sentence; for an empty required field it names the fields. */
|
|
87
|
+
message: string | null;
|
|
88
|
+
}
|
|
89
|
+
/** What `items.bulkSetStatus` answers. */
|
|
90
|
+
export interface ContentItemBulkStatusResult {
|
|
91
|
+
/** How many items now have the status. */
|
|
92
|
+
updated: number;
|
|
93
|
+
/**
|
|
94
|
+
* One outcome per id named, in the order named, each id once. Null when the CMS answering
|
|
95
|
+
* predates per-item outcomes; such a CMS also publishes without the required-field check.
|
|
96
|
+
*/
|
|
97
|
+
results: ContentItemStatusOutcome[] | null;
|
|
98
|
+
}
|
|
76
99
|
export interface ContentItemListOptions extends ListOptions {
|
|
77
100
|
/**
|
|
78
101
|
* Only items in this status. Honoured for a credential holding `ReadCmsContent` on the
|
|
@@ -222,16 +245,16 @@ export interface ContentItemsModule {
|
|
|
222
245
|
*/
|
|
223
246
|
unpublish(typeId: string, itemId: string, options?: Call): Promise<ContentItemRecord>;
|
|
224
247
|
/**
|
|
225
|
-
* Sets one status on several items of the type in one transaction, and
|
|
226
|
-
*
|
|
227
|
-
*
|
|
248
|
+
* Sets one status on several items of the type in one transaction, and reports each item on its
|
|
249
|
+
* own (`results`). Publishing refuses an item that leaves a required field empty, as `publish`
|
|
250
|
+
* does: that item keeps its status (`FAILED`, `VALIDATION_ERROR`) and the others still change.
|
|
251
|
+
* An id that is not a live item of this type in this store changes nothing (`NOT_FOUND`). The
|
|
252
|
+
* call itself fails only for the whole selection (an unknown type, a malformed id or status).
|
|
228
253
|
*
|
|
229
254
|
* @effect write
|
|
230
255
|
* @permission UpdateCmsContent
|
|
231
256
|
*/
|
|
232
|
-
bulkSetStatus(typeId: string, itemIds: readonly string[], status: ContentItemStatus, options?: Call): Promise<
|
|
233
|
-
updated: number;
|
|
234
|
-
}>;
|
|
257
|
+
bulkSetStatus(typeId: string, itemIds: readonly string[], status: ContentItemStatus, options?: Call): Promise<ContentItemBulkStatusResult>;
|
|
235
258
|
/**
|
|
236
259
|
* Removes several items of the type in one transaction, and answers how many it removed. An
|
|
237
260
|
* id that is not a live item of this type is skipped; a selected item whose child is not
|
|
@@ -248,8 +271,8 @@ export interface ContentItemsModule {
|
|
|
248
271
|
* field, a menu entry or a product property stores. Any number of ids; the SDK splits them
|
|
249
272
|
* into the CMS's batches of 50. Answers the ids that resolved, in the order given, each once:
|
|
250
273
|
* an unknown id, another store's, or one the credential may not see is absent, never an
|
|
251
|
-
* error. An id that is not a UUID is
|
|
252
|
-
*
|
|
274
|
+
* error. An id that is not a UUID is absent too (a CMS before 2026-10-14 refused the whole batch
|
|
275
|
+
* with it, `ValidationError` `INVALID_DATA`).
|
|
253
276
|
*
|
|
254
277
|
* Every status for a holder of `ReadCmsContent`, `UpdateCatalog` or `UpdateProduct` on the
|
|
255
278
|
* store (a catalog writer resolves a draft its product references); published items only for
|
package/dist/content/items.js
CHANGED
|
@@ -138,7 +138,7 @@ export function createContentItemsModule(transport) {
|
|
|
138
138
|
path: `${itemsPath(typeId)}/bulk-status`,
|
|
139
139
|
body: { itemIds: [...itemIds], status },
|
|
140
140
|
}, options);
|
|
141
|
-
return { updated: body.data.updated };
|
|
141
|
+
return { updated: body.data.updated, results: body.data.results ?? null };
|
|
142
142
|
},
|
|
143
143
|
async bulkDelete(typeId, itemIds, options) {
|
|
144
144
|
const body = await transport.rest('cms', {
|