@sixtyninecommerce/admin 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/README.md +5772 -118
  2. package/dist/auth/index.d.ts +7 -1
  3. package/dist/auth/index.js +8 -1
  4. package/dist/catalog/assets.d.ts +120 -8
  5. package/dist/catalog/assets.js +98 -12
  6. package/dist/catalog/collections.d.ts +279 -14
  7. package/dist/catalog/collections.js +246 -15
  8. package/dist/catalog/documents.d.ts +142 -0
  9. package/dist/catalog/documents.js +114 -0
  10. package/dist/catalog/index.d.ts +54 -6
  11. package/dist/catalog/index.js +10 -1
  12. package/dist/catalog/metadata.d.ts +244 -0
  13. package/dist/catalog/metadata.js +220 -0
  14. package/dist/catalog/products.d.ts +159 -19
  15. package/dist/catalog/products.js +88 -13
  16. package/dist/catalog/properties.d.ts +140 -14
  17. package/dist/catalog/properties.js +65 -1
  18. package/dist/catalog/recommendations.d.ts +288 -0
  19. package/dist/catalog/recommendations.js +237 -0
  20. package/dist/catalog/reviews.d.ts +381 -0
  21. package/dist/catalog/reviews.js +325 -0
  22. package/dist/catalog/types.d.ts +17 -1
  23. package/dist/catalog/types.js +10 -0
  24. package/dist/catalog/variant-find.d.ts +136 -0
  25. package/dist/catalog/variant-find.js +239 -0
  26. package/dist/catalog/variants.d.ts +293 -26
  27. package/dist/catalog/variants.js +164 -7
  28. package/dist/catalog/visibility.d.ts +38 -7
  29. package/dist/catalog/visibility.js +19 -1
  30. package/dist/client.d.ts +121 -9
  31. package/dist/client.js +53 -14
  32. package/dist/content/assets.d.ts +285 -0
  33. package/dist/content/assets.js +181 -0
  34. package/dist/content/forms.d.ts +332 -1
  35. package/dist/content/forms.js +140 -4
  36. package/dist/content/items.d.ts +292 -0
  37. package/dist/content/items.js +170 -0
  38. package/dist/content/menus.d.ts +254 -0
  39. package/dist/content/menus.js +70 -0
  40. package/dist/content/strings.d.ts +137 -0
  41. package/dist/content/strings.js +82 -0
  42. package/dist/content/types.d.ts +282 -0
  43. package/dist/content/types.js +100 -0
  44. package/dist/customers/age-verification.d.ts +66 -0
  45. package/dist/customers/age-verification.js +79 -0
  46. package/dist/customers/credit.d.ts +263 -0
  47. package/dist/customers/credit.js +244 -0
  48. package/dist/customers/delivery-preferences.d.ts +120 -0
  49. package/dist/customers/delivery-preferences.js +97 -0
  50. package/dist/customers/delivery-terms.d.ts +44 -0
  51. package/dist/customers/delivery-terms.js +32 -0
  52. package/dist/customers/groups.d.ts +207 -0
  53. package/dist/customers/groups.js +438 -0
  54. package/dist/customers/index.d.ts +462 -37
  55. package/dist/customers/index.js +317 -40
  56. package/dist/customers/invitation-status.d.ts +57 -0
  57. package/dist/customers/invitation-status.js +21 -0
  58. package/dist/customers/notes.d.ts +67 -0
  59. package/dist/customers/notes.js +51 -0
  60. package/dist/customers/notification-recipients.d.ts +97 -0
  61. package/dist/customers/notification-recipients.js +84 -0
  62. package/dist/customers/saved-lists.d.ts +42 -0
  63. package/dist/customers/saved-lists.js +61 -0
  64. package/dist/customers/vat.d.ts +83 -0
  65. package/dist/customers/vat.js +46 -0
  66. package/dist/customers/wire.d.ts +20 -0
  67. package/dist/customers/wire.js +35 -0
  68. package/dist/documents/ar.d.ts +307 -0
  69. package/dist/documents/ar.js +254 -0
  70. package/dist/documents/emails.d.ts +150 -0
  71. package/dist/documents/emails.js +220 -0
  72. package/dist/documents/index.d.ts +744 -21
  73. package/dist/documents/index.js +438 -24
  74. package/dist/documents/pdf.d.ts +2 -0
  75. package/dist/documents/pdf.js +11 -0
  76. package/dist/fulfillments/create.d.ts +167 -0
  77. package/dist/fulfillments/create.js +216 -0
  78. package/dist/fulfillments/index.d.ts +163 -11
  79. package/dist/fulfillments/index.js +135 -6
  80. package/dist/fulfillments/payment-policy.d.ts +64 -0
  81. package/dist/fulfillments/payment-policy.js +140 -0
  82. package/dist/fulfillments/warehouse.d.ts +724 -0
  83. package/dist/fulfillments/warehouse.js +914 -0
  84. package/dist/geo/index.d.ts +119 -0
  85. package/dist/geo/index.js +107 -0
  86. package/dist/index.d.ts +147 -39
  87. package/dist/index.js +51 -12
  88. package/dist/integrations/afas-actions.d.ts +234 -0
  89. package/dist/integrations/afas-actions.js +310 -0
  90. package/dist/integrations/afas-types.d.ts +483 -0
  91. package/dist/integrations/afas-types.js +5 -0
  92. package/dist/integrations/afas.d.ts +234 -0
  93. package/dist/integrations/afas.js +290 -0
  94. package/dist/integrations/connections.d.ts +103 -0
  95. package/dist/integrations/connections.js +55 -0
  96. package/dist/integrations/esp.d.ts +313 -0
  97. package/dist/integrations/esp.js +169 -0
  98. package/dist/integrations/exact-online-types.d.ts +965 -0
  99. package/dist/integrations/exact-online-types.js +6 -0
  100. package/dist/integrations/exact-online.d.ts +784 -0
  101. package/dist/integrations/exact-online.js +1089 -0
  102. package/dist/integrations/index.d.ts +44 -3
  103. package/dist/integrations/index.js +23 -3
  104. package/dist/integrations/pipeline-discoveries.d.ts +65 -0
  105. package/dist/integrations/pipeline-discoveries.js +65 -0
  106. package/dist/integrations/pipeline-keys.d.ts +13 -0
  107. package/dist/integrations/pipeline-keys.js +22 -0
  108. package/dist/integrations/pipeline-runs.d.ts +119 -0
  109. package/dist/integrations/pipeline-runs.js +87 -0
  110. package/dist/integrations/pipelines.d.ts +87 -15
  111. package/dist/integrations/pipelines.js +41 -22
  112. package/dist/integrations/price-import.d.ts +223 -0
  113. package/dist/integrations/price-import.js +124 -0
  114. package/dist/integrations/types.d.ts +367 -3
  115. package/dist/integrations/types.js +4 -3
  116. package/dist/locations/index.d.ts +66 -0
  117. package/dist/locations/index.js +31 -0
  118. package/dist/meta/index.d.ts +172 -0
  119. package/dist/metafields/index.d.ts +8 -1
  120. package/dist/metafields/index.js +10 -0
  121. package/dist/orders/cancel-with-money.d.ts +237 -0
  122. package/dist/orders/cancel-with-money.js +155 -0
  123. package/dist/orders/commit-edit.d.ts +23 -1
  124. package/dist/orders/commit-edit.js +19 -2
  125. package/dist/orders/deposit-policy.d.ts +3 -1
  126. package/dist/orders/drafts.d.ts +800 -0
  127. package/dist/orders/drafts.js +771 -0
  128. package/dist/orders/fields.d.ts +245 -0
  129. package/dist/orders/fields.js +188 -0
  130. package/dist/orders/get.d.ts +29 -19
  131. package/dist/orders/get.js +17 -8
  132. package/dist/orders/index.d.ts +66 -15
  133. package/dist/orders/index.js +25 -2
  134. package/dist/orders/lifecycle.d.ts +128 -0
  135. package/dist/orders/lifecycle.js +158 -0
  136. package/dist/orders/line-price-preview.d.ts +7 -2
  137. package/dist/orders/list.d.ts +243 -0
  138. package/dist/orders/list.js +487 -0
  139. package/dist/orders/manual-discounts.d.ts +13 -5
  140. package/dist/orders/manual-pricing.d.ts +5 -0
  141. package/dist/orders/money-view.d.ts +149 -0
  142. package/dist/orders/money-view.js +99 -0
  143. package/dist/orders/order-entry.d.ts +20 -7
  144. package/dist/orders/plan-edit.d.ts +34 -2
  145. package/dist/orders/plan-edit.js +112 -11
  146. package/dist/orders/settings.d.ts +201 -0
  147. package/dist/orders/settings.js +325 -0
  148. package/dist/orders/status.d.ts +159 -13
  149. package/dist/orders/status.js +21 -0
  150. package/dist/orders/surcharges.d.ts +20 -1
  151. package/dist/orders/surcharges.js +37 -11
  152. package/dist/payments/account-credit.d.ts +46 -0
  153. package/dist/payments/account-credit.js +42 -0
  154. package/dist/payments/ar.d.ts +292 -0
  155. package/dist/payments/ar.js +223 -0
  156. package/dist/payments/connected-account.d.ts +47 -0
  157. package/dist/payments/connected-account.js +61 -0
  158. package/dist/payments/credit-noted-fields.d.ts +19 -0
  159. package/dist/payments/credit-noted-fields.js +69 -0
  160. package/dist/payments/customer-consent.d.ts +30 -0
  161. package/dist/payments/customer-consent.js +49 -0
  162. package/dist/payments/index.d.ts +340 -19
  163. package/dist/payments/index.js +151 -1
  164. package/dist/payments/methods.d.ts +303 -8
  165. package/dist/payments/methods.js +337 -7
  166. package/dist/payments/mollie-methods.d.ts +159 -0
  167. package/dist/payments/mollie-methods.js +141 -0
  168. package/dist/payments/payment-links.d.ts +180 -0
  169. package/dist/payments/payment-links.js +97 -0
  170. package/dist/payments/provider-keys.d.ts +150 -0
  171. package/dist/payments/provider-keys.js +118 -0
  172. package/dist/payments/providers.d.ts +86 -11
  173. package/dist/payments/providers.js +29 -0
  174. package/dist/payments/requests.d.ts +147 -0
  175. package/dist/payments/requests.js +93 -0
  176. package/dist/payments/store-credit-fields.d.ts +15 -0
  177. package/dist/payments/store-credit-fields.js +51 -0
  178. package/dist/pickups/index.d.ts +106 -0
  179. package/dist/pickups/index.js +108 -0
  180. package/dist/platform/channels.d.ts +132 -0
  181. package/dist/platform/channels.js +128 -0
  182. package/dist/platform/email-gateways.d.ts +165 -0
  183. package/dist/platform/email-gateways.js +78 -0
  184. package/dist/platform/geo.d.ts +123 -0
  185. package/dist/platform/geo.js +144 -0
  186. package/dist/platform/tax.d.ts +162 -0
  187. package/dist/platform/tax.js +169 -0
  188. package/dist/pos/anomalies.d.ts +158 -0
  189. package/dist/pos/anomalies.js +64 -0
  190. package/dist/pos/assisted-selling.d.ts +79 -0
  191. package/dist/pos/assisted-selling.js +38 -0
  192. package/dist/pos/exchanges.d.ts +108 -0
  193. package/dist/pos/exchanges.js +22 -0
  194. package/dist/pos/gift-cards.d.ts +135 -0
  195. package/dist/pos/gift-cards.js +47 -0
  196. package/dist/pos/index.d.ts +2370 -58
  197. package/dist/pos/index.js +854 -7
  198. package/dist/pos/no-receipt.d.ts +94 -0
  199. package/dist/pos/no-receipt.js +25 -0
  200. package/dist/pos/offline-sales.d.ts +228 -0
  201. package/dist/pos/offline-sales.js +170 -0
  202. package/dist/pos/pay-by-link.d.ts +143 -0
  203. package/dist/pos/pay-by-link.js +57 -0
  204. package/dist/pos/quick-keys.d.ts +103 -0
  205. package/dist/pos/quick-keys.js +45 -0
  206. package/dist/pos/reports.d.ts +360 -0
  207. package/dist/pos/reports.js +62 -0
  208. package/dist/pos/returns.d.ts +244 -0
  209. package/dist/pos/returns.js +83 -0
  210. package/dist/pricing/bulk-coupons.d.ts +139 -0
  211. package/dist/pricing/bulk-coupons.js +112 -0
  212. package/dist/pricing/index.d.ts +174 -4
  213. package/dist/pricing/index.js +174 -1
  214. package/dist/pricing/price-lists.d.ts +606 -0
  215. package/dist/pricing/price-lists.js +600 -0
  216. package/dist/purchasing/alerts.d.ts +104 -0
  217. package/dist/purchasing/alerts.js +88 -0
  218. package/dist/purchasing/backorders.d.ts +61 -0
  219. package/dist/purchasing/backorders.js +92 -0
  220. package/dist/purchasing/cost.d.ts +110 -0
  221. package/dist/purchasing/cost.js +86 -0
  222. package/dist/purchasing/index.d.ts +54 -0
  223. package/dist/purchasing/index.js +24 -0
  224. package/dist/purchasing/links.d.ts +263 -0
  225. package/dist/purchasing/links.js +192 -0
  226. package/dist/purchasing/purchase-orders.d.ts +551 -0
  227. package/dist/purchasing/purchase-orders.js +491 -0
  228. package/dist/purchasing/receipts.d.ts +396 -0
  229. package/dist/purchasing/receipts.js +287 -0
  230. package/dist/purchasing/replenishment.d.ts +281 -0
  231. package/dist/purchasing/replenishment.js +240 -0
  232. package/dist/purchasing/returns.d.ts +273 -0
  233. package/dist/purchasing/returns.js +229 -0
  234. package/dist/purchasing/settings.d.ts +152 -0
  235. package/dist/purchasing/settings.js +181 -0
  236. package/dist/purchasing/suppliers.d.ts +476 -0
  237. package/dist/purchasing/suppliers.js +438 -0
  238. package/dist/quotes/index.d.ts +518 -0
  239. package/dist/quotes/index.js +439 -0
  240. package/dist/reports/index.d.ts +71 -8
  241. package/dist/reports/index.js +74 -11
  242. package/dist/returns/index.d.ts +675 -21
  243. package/dist/returns/index.js +756 -11
  244. package/dist/search/index.d.ts +76 -1
  245. package/dist/search/index.js +63 -0
  246. package/dist/seo/config.d.ts +128 -0
  247. package/dist/seo/config.js +95 -0
  248. package/dist/seo/redirects.d.ts +125 -0
  249. package/dist/seo/redirects.js +60 -1
  250. package/dist/seo/settings.d.ts +147 -0
  251. package/dist/seo/settings.js +209 -0
  252. package/dist/shipping/definitions.d.ts +29 -0
  253. package/dist/shipping/definitions.js +47 -0
  254. package/dist/shipping/fulfillment-profiles.d.ts +174 -0
  255. package/dist/shipping/fulfillment-profiles.js +104 -0
  256. package/dist/shipping/hendriks.d.ts +97 -0
  257. package/dist/shipping/hendriks.js +50 -0
  258. package/dist/shipping/index.d.ts +54 -0
  259. package/dist/shipping/index.js +27 -0
  260. package/dist/shipping/methods.d.ts +205 -0
  261. package/dist/shipping/methods.js +240 -0
  262. package/dist/shipping/myparcel.d.ts +668 -0
  263. package/dist/shipping/myparcel.js +466 -0
  264. package/dist/shipping/parcel-count-suggestion.d.ts +2 -0
  265. package/dist/shipping/parcel-rule.d.ts +6 -5
  266. package/dist/shipping/parcel-rule.js +21 -23
  267. package/dist/shipping/rate-tables.d.ts +324 -0
  268. package/dist/shipping/rate-tables.js +390 -0
  269. package/dist/shipping/ship-to-countries.d.ts +75 -0
  270. package/dist/shipping/ship-to-countries.js +47 -0
  271. package/dist/stock/by-location.d.ts +86 -0
  272. package/dist/stock/by-location.js +94 -0
  273. package/dist/stock/index.d.ts +18 -4
  274. package/dist/stock/index.js +7 -0
  275. package/dist/stock/locations.d.ts +144 -0
  276. package/dist/stock/locations.js +168 -0
  277. package/dist/stock/settings.d.ts +106 -0
  278. package/dist/stock/settings.js +103 -0
  279. package/dist/stock/transfers.d.ts +386 -52
  280. package/dist/stock/transfers.js +222 -17
  281. package/dist/stored-value/index.d.ts +312 -11
  282. package/dist/stored-value/index.js +223 -4
  283. package/dist/subscriptions/index.d.ts +682 -0
  284. package/dist/subscriptions/index.js +815 -0
  285. package/dist/tax/display-policy.d.ts +105 -0
  286. package/dist/tax/display-policy.js +84 -0
  287. package/dist/tax/index.d.ts +79 -1
  288. package/dist/tax/index.js +52 -0
  289. package/dist/tenant/email-style.d.ts +102 -0
  290. package/dist/tenant/email-style.js +72 -0
  291. package/dist/tenant/email-templates.d.ts +287 -0
  292. package/dist/tenant/email-templates.js +271 -0
  293. package/dist/tenant/notifications.d.ts +23 -17
  294. package/dist/tenant/settings-sources.d.ts +1 -1
  295. package/dist/tenant/time-zone.d.ts +17 -0
  296. package/dist/tenant/time-zone.js +11 -0
  297. package/dist/types.d.ts +50 -3
  298. package/dist/users/index.d.ts +3 -0
  299. package/dist/users/index.js +2 -0
  300. package/dist/users/warehouses.d.ts +50 -0
  301. package/dist/users/warehouses.js +51 -0
  302. package/package.json +2 -2
@@ -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
- list(options?: ListOptions): Promise<Page<Form>>;
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;
@@ -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', { method: 'GET', path: '/forms', query: { limit, offset } }, { ...options, preserveKeys: FORM_PASSTHROUGH });
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: `/forms/${encodeURIComponent(id)}` }, { ...options, preserveKeys: FORM_PASSTHROUGH });
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: `/forms/${encodeURIComponent(id)}`, expect: 'void' }, options);
196
+ await transport.rest('cms', { method: 'DELETE', path: formPath(id), expect: 'void' }, options);
63
197
  },
198
+ fields,
199
+ submissions,
64
200
  };
65
201
  }