@01.software/sdk 0.29.0 → 0.31.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 (78) hide show
  1. package/README.md +331 -77
  2. package/dist/analytics/react.cjs +4 -1
  3. package/dist/analytics/react.cjs.map +1 -1
  4. package/dist/analytics/react.js +4 -1
  5. package/dist/analytics/react.js.map +1 -1
  6. package/dist/analytics.cjs +4 -1
  7. package/dist/analytics.cjs.map +1 -1
  8. package/dist/analytics.js +4 -1
  9. package/dist/analytics.js.map +1 -1
  10. package/dist/client.cjs +1541 -0
  11. package/dist/client.cjs.map +1 -0
  12. package/dist/client.d.cts +28 -0
  13. package/dist/client.d.ts +28 -0
  14. package/dist/client.js +1518 -0
  15. package/dist/client.js.map +1 -0
  16. package/dist/collection-client-ByzY3hWK.d.ts +218 -0
  17. package/dist/collection-client-DFXXz0vk.d.cts +218 -0
  18. package/dist/{const-DAjQYNuM.d.ts → const-AytzliEu.d.cts} +5 -7
  19. package/dist/{const-Dsixdi6z.d.cts → const-BGCP-OJL.d.ts} +5 -7
  20. package/dist/index-BGEhoDUs.d.cts +106 -0
  21. package/dist/index-BGEhoDUs.d.ts +106 -0
  22. package/dist/index.cjs +1006 -1615
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +11 -115
  25. package/dist/index.d.ts +11 -115
  26. package/dist/index.js +932 -1559
  27. package/dist/index.js.map +1 -1
  28. package/dist/metadata.cjs +91 -0
  29. package/dist/metadata.cjs.map +1 -0
  30. package/dist/metadata.d.cts +58 -0
  31. package/dist/metadata.d.ts +58 -0
  32. package/dist/metadata.js +68 -0
  33. package/dist/metadata.js.map +1 -0
  34. package/dist/{payload-types-Ci-ZA7aM.d.cts → payload-types-Wa4-eC6x.d.cts} +794 -532
  35. package/dist/{payload-types-Ci-ZA7aM.d.ts → payload-types-Wa4-eC6x.d.ts} +794 -532
  36. package/dist/query.cjs +1841 -0
  37. package/dist/query.cjs.map +1 -0
  38. package/dist/query.d.cts +244 -0
  39. package/dist/query.d.ts +244 -0
  40. package/dist/query.js +1836 -0
  41. package/dist/query.js.map +1 -0
  42. package/dist/realtime.cjs +4 -1
  43. package/dist/realtime.cjs.map +1 -1
  44. package/dist/realtime.d.cts +2 -2
  45. package/dist/realtime.d.ts +2 -2
  46. package/dist/realtime.js +4 -1
  47. package/dist/realtime.js.map +1 -1
  48. package/dist/{server-BINWywT8.d.cts → server-CrsPyqEc.d.cts} +14 -31
  49. package/dist/{server-BINWywT8.d.ts → server-CrsPyqEc.d.ts} +14 -31
  50. package/dist/server.cjs +430 -846
  51. package/dist/server.cjs.map +1 -1
  52. package/dist/server.d.cts +137 -7
  53. package/dist/server.d.ts +137 -7
  54. package/dist/server.js +430 -864
  55. package/dist/server.js.map +1 -1
  56. package/dist/{server-Cv0Q4dPQ.d.ts → types-BX2mqDf6.d.ts} +270 -743
  57. package/dist/{types-BWq_WlbB.d.ts → types-CVA10VC-.d.ts} +6 -2
  58. package/dist/{types-zKjATmDK.d.cts → types-CmLG-7RL.d.cts} +6 -2
  59. package/dist/{server-C0C8dtms.d.cts → types-DChFjQGz.d.cts} +270 -743
  60. package/dist/ui/canvas/server.cjs +7 -6
  61. package/dist/ui/canvas/server.cjs.map +1 -1
  62. package/dist/ui/canvas/server.d.cts +1 -3
  63. package/dist/ui/canvas/server.d.ts +1 -3
  64. package/dist/ui/canvas/server.js +7 -6
  65. package/dist/ui/canvas/server.js.map +1 -1
  66. package/dist/ui/canvas.cjs +11 -10
  67. package/dist/ui/canvas.cjs.map +1 -1
  68. package/dist/ui/canvas.d.cts +29 -6
  69. package/dist/ui/canvas.d.ts +29 -6
  70. package/dist/ui/canvas.js +11 -10
  71. package/dist/ui/canvas.js.map +1 -1
  72. package/dist/ui/form.d.cts +1 -1
  73. package/dist/ui/form.d.ts +1 -1
  74. package/dist/ui/video.d.cts +1 -1
  75. package/dist/ui/video.d.ts +1 -1
  76. package/dist/webhook.d.cts +3 -3
  77. package/dist/webhook.d.ts +3 -3
  78. package/package.json +84 -15
package/README.md CHANGED
@@ -42,10 +42,10 @@ export function App() {
42
42
  ```
43
43
 
44
44
  ```typescript
45
- // Main entry - browser client, query builder, hooks, utilities
45
+ // Main entry - browser client, query builder, commerce helpers, utilities
46
46
  import { createClient } from '@01.software/sdk'
47
47
 
48
- // Server-only entry - avoids importing browser Client APIs
48
+ // Server-only entry - keep Secret Key code out of browser-facing imports
49
49
  import { createServerClient } from '@01.software/sdk/server'
50
50
 
51
51
  // Webhook only - webhook handlers
@@ -67,6 +67,45 @@ import { CanvasRenderer } from '@01.software/sdk/ui/canvas'
67
67
  import { VideoPlayer } from '@01.software/sdk/ui/video'
68
68
  ```
69
69
 
70
+ The root entry keeps `createClient`, commerce helpers, collection helpers, and
71
+ types lightweight. Server, React Query, and UI features live behind explicit
72
+ sub-paths so consumers install feature peers only when they import the matching
73
+ entry.
74
+
75
+ | Import | Feature(s) | Install when used |
76
+ | ----------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
77
+ | `@01.software/sdk` | browser-safe `createClient`, commerce helpers, collection helpers, types | none |
78
+ | `@01.software/sdk/client` | browser-safe `createClient` entry | none |
79
+ | `@01.software/sdk/server` | `createServerClient`, server-only collection, commerce, and preview APIs | none; keep `secretKey` code on the server |
80
+ | `@01.software/sdk/query` | React Query hooks, cache helpers, `getQueryClient` | `@tanstack/react-query`, `react`, `react-dom` |
81
+ | `@01.software/sdk/realtime` | `RealtimeConnection`, `useRealtimeQuery` | `@tanstack/react-query`, `react`, `react-dom` |
82
+ | `@01.software/sdk/analytics/react` | `<Analytics />` | `react`, `react-dom` |
83
+ | `@01.software/sdk/ui/rich-text` | `RichTextContent`, `StyledRichTextContent` | `react`, `react-dom`, `@payloadcms/richtext-lexical` |
84
+ | `@01.software/sdk/ui/form` | `FormRenderer` | `react`, `react-dom` |
85
+ | `@01.software/sdk/ui/code-block` | `CodeBlock`, `highlight` | `react`, `react-dom`, `shiki`, `hast-util-to-jsx-runtime` |
86
+ | `@01.software/sdk/ui/canvas` | `CanvasRenderer`, `CanvasFrame`, `useCanvas`, `prefetchCanvas` | `react`, `react-dom`, `@tanstack/react-query`, `@xyflow/react`, `quickjs-emscripten`, `postcss`, `sucrase` |
87
+ | `@01.software/sdk/ui/canvas/server` | canvas server helpers | none |
88
+ | `@01.software/sdk/ui/video` | `VideoPlayer` | `react`, `react-dom`, `@mux/mux-player-react` |
89
+ | `@01.software/sdk/ui/image` | `Image` | `react`, `react-dom` |
90
+
91
+ If a feature is not listed here, it does not need a separate peer install.
92
+ For the full component-to-peer mapping, see
93
+ `packages/sdk/.claude/rules/components-reference.md`.
94
+
95
+ Migration quick reference:
96
+
97
+ - `createClient` remains available from `@01.software/sdk` and
98
+ `@01.software/sdk/client`.
99
+ - `createServerClient` must be imported from `@01.software/sdk/server`.
100
+ - React Query hooks and cache helpers must be imported from
101
+ `@01.software/sdk/query`.
102
+ - UI components must be imported from the specific `@01.software/sdk/ui/*`
103
+ sub-path and require only that row's peers.
104
+ - Console-shared pure ecommerce helpers live in private
105
+ `@01.software/contracts`. The public SDK keeps customer-facing helpers
106
+ self-contained and must not import private contracts; Console code should
107
+ import shared helpers from contracts directly.
108
+
70
109
  ## Getting Started
71
110
 
72
111
  ### Client
@@ -89,11 +128,13 @@ const { docs } = await client.collections.from('products').find({
89
128
 
90
129
  ```typescript
91
130
  import { createServerClient } from '@01.software/sdk/server'
131
+ import { createServerQueryHooks } from '@01.software/sdk/query'
92
132
 
93
133
  const server = createServerClient({
94
134
  publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY,
95
135
  secretKey: process.env.SOFTWARE_SECRET_KEY, // sk01_... opaque API key from Console
96
136
  })
137
+ const serverQuery = createServerQueryHooks(server)
97
138
 
98
139
  // Create order (server only)
99
140
  const order = await server.commerce.orders.create({
@@ -107,12 +148,29 @@ const order = await server.commerce.orders.create({
107
148
  })
108
149
 
109
150
  // SSR prefetch (server)
110
- await server.query.prefetchQuery({
151
+ await serverQuery.prefetchQuery({
111
152
  collection: 'products',
112
153
  options: { limit: 10 },
113
154
  })
114
155
  ```
115
156
 
157
+ Always import `createServerClient` from `@01.software/sdk/server` so generated
158
+ code and bundlers do not blur the Secret Key boundary.
159
+
160
+ Server-rendered preview routes can use `server.preview.detail()` with the
161
+ short-lived preview token issued by Console:
162
+
163
+ ```typescript
164
+ const preview = await server.preview.detail(
165
+ { collection: 'products', id: previewId },
166
+ { previewToken },
167
+ )
168
+ ```
169
+
170
+ For product pages, `server.commerce.product.previewDetail({ id }, {
171
+ previewToken })` returns the same shaped product detail as `detail()`, but allows
172
+ the saved draft/unpublished record addressed by the preview token.
173
+
116
174
  ## Getting product detail
117
175
 
118
176
  The recommended way to fetch a single product is the shaped helper:
@@ -124,7 +182,9 @@ const client = createClient({
124
182
  publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
125
183
  })
126
184
 
127
- const product = await client.commerce.product.detail({ slug: 'every-peach-tee' })
185
+ const product = await client.commerce.product.detail({
186
+ slug: 'every-peach-tee',
187
+ })
128
188
  if (!product) {
129
189
  return notFound() // returned null — product missing, unpublished, or not in this tenant
130
190
  }
@@ -133,14 +193,120 @@ if (!product) {
133
193
 
134
194
  `detail()` returns `ProductDetail | null`. A `null` result covers every "no result" reason: `not_found`, `not_published`, `tenant_mismatch`, `feature_disabled`. Render the same "not available" UI for all four. To recover the exact reason for triage, `404` maps to `null` rather than a thrown error — inspect `client.lastRequestId` and match against backend logs.
135
195
 
196
+ ### Product selection helpers
197
+
198
+ ```typescript
199
+ import {
200
+ buildProductHref,
201
+ buildProductOptionMatrixFromDetail,
202
+ getProductSelectionImages,
203
+ resolveProductSelectionFromMatrix,
204
+ } from '@01.software/sdk'
205
+
206
+ const matrix = buildProductOptionMatrixFromDetail(product)
207
+ const selection = resolveProductSelectionFromMatrix(
208
+ matrix,
209
+ { search: '?opt.color=black&opt.size=s' },
210
+ undefined,
211
+ { detail: product },
212
+ )
213
+
214
+ const images = getProductSelectionImages(selection) // object media only, deduped
215
+ const href = buildProductHref(product, {
216
+ optionSlug: 'color',
217
+ optionValueSlug: 'black',
218
+ })
219
+ ```
220
+
221
+ Selection media follows the resolved selection: a complete variant uses that
222
+ variant's media first; a partial option selection uses selected option-value
223
+ media first, then matching variant media, before falling back to listing or
224
+ product media. This keeps listing-card selection links and detail-page images
225
+ aligned without rebuilding media priority in storefront code.
226
+
227
+ `availableValuesByOptionSlug` / `availableValuesByOptionId` include
228
+ `availableStock`, `isUnlimited`, and `availableForSale` per value so option UIs
229
+ can render stock state without recalculating from variants.
230
+
136
231
  ### With React Query
137
232
 
138
233
  ```typescript
139
- const { data: product, isLoading } = client.query.useProductDetailBySlug(slug)
234
+ import { createQueryHooks } from '@01.software/sdk/query'
235
+
236
+ const query = createQueryHooks(client)
237
+ const { data: product, isLoading } = query.useProductDetailBySlug(slug)
140
238
  ```
141
239
 
142
240
  Cache key is `['products', 'detail', { slug }]`. Mutations on products, product-variants, product-options, product-option-values, brands, brand-logos, images, and related collections automatically invalidate this cache.
143
241
 
242
+ ### Selection URL contract
243
+
244
+ Use `createProductSelectionCodec(detail)` when product pages need to keep option
245
+ selection in the URL. Complete selections use `variant=<variantId>`; partial
246
+ selections use `opt.<optionId>=<valueId>`. Older
247
+ `opt.<optionSlug>=<valueSlug>` URLs still parse during Stage 1, but slugs are
248
+ compatibility metadata rather than canonical identity.
249
+
250
+ ```typescript
251
+ import {
252
+ createProductSelectionCodec,
253
+ resolveProductSelection,
254
+ } from '@01.software/sdk'
255
+
256
+ const codec = createProductSelectionCodec(product)
257
+ const normalizedSelection = codec.parse('?opt.option-color=color-black')
258
+ const selection = resolveProductSelection(product, normalizedSelection)
259
+ const selectionQuery = codec.stringify(normalizedSelection)
260
+ // selectionQuery === 'opt.option-color=color-black' for partial selections
261
+ // selectionQuery === 'variant=variant-black-large' once a complete variant is selected
262
+ // selection.selectedVariant, selection.price, selection.stock, selection.media
263
+ ```
264
+
265
+ Use IDs from `detail.options[].id` and `detail.options[].values[].id` when
266
+ building selection state. Slugs remain useful for display and old inbound URLs,
267
+ but new outbound URLs should use the codec output.
268
+
269
+ For listing cards, pass the listing group returned by
270
+ `buildProductListingGroupsByOption()` or the listing-groups endpoint into
271
+ `buildProductHref(product, group, { detail })`. The detail object lets the SDK
272
+ emit canonical `variant=<variantId>` or `opt.<optionId>=<valueId>` params. When
273
+ full detail is not available on a product-list page, pass the group without
274
+ `detail`; `buildProductHref()` still emits the best available selection hint and
275
+ the detail page can resolve it through `resolveProductSelection()`.
276
+
277
+ Do not use bare option query keys such as `?size=large`. The SDK rejects them
278
+ as ambiguous because product pages commonly share URLs with unrelated search,
279
+ filter, analytics, or framework parameters. Namespacing selection keys under
280
+ `opt.` lets the codec distinguish product-option state from ordinary query
281
+ parameters while still allowing unrelated parameters such as `utm_campaign` to
282
+ coexist without being interpreted as selection state.
283
+
284
+ ### Product listing card helper
285
+
286
+ `buildProductListingCard(item, options?)` turns a single
287
+ `commerce.product.listingGroups()` response item into a render-ready
288
+ `ProductListingCard`. The card carries product-level hero media
289
+ (`product.thumbnail` -> first `product.images` -> `null`), an aggregated
290
+ price range across all option-value groups, and a `swatches[]` array
291
+ derived from groups when there is more than one. Single-group products
292
+ emit `swatches: []`; storefronts that disagree can read `item.groups`
293
+ directly.
294
+
295
+ ```ts
296
+ import {
297
+ buildProductListingCard,
298
+ type ProductListingCard,
299
+ } from '@01.software/sdk'
300
+
301
+ const cards: ProductListingCard[] = response.docs.map((item) =>
302
+ buildProductListingCard(item, { basePath: '/shop' }),
303
+ )
304
+ ```
305
+
306
+ Each swatch carries a hint-only option-value href
307
+ (`?opt.<optionId>=<valueId>`); the detail page resolves it through
308
+ `resolveProductSelection(detail, { search })`.
309
+
144
310
  ## Advanced: direct Payload queries (escape hatch)
145
311
 
146
312
  Most consumers should use the helper APIs above (`commerce.product.detail`, etc.). The query builder below is the escape hatch for advanced cases the helpers do not cover: bulk operations, custom filter combinations, or fields the helper response does not expose.
@@ -171,7 +337,7 @@ await client.collections.from('products').find({
171
337
 
172
338
  ### `joins` — Payload join-field reverse-relations
173
339
 
174
- `joins` is the correct control for Payload `type: 'join'` virtual reverse-relation fields. In this platform's schema, `products.variants`, `products.options`, `products.collections`, `customers.orders`, `customers.addresses`, `posts.comments`, `article-authors.articles`, `orders.{items,transactions,fulfillments,returns}`, and similar reverse-relations are all join fields — you must use `joins` (not `depth`/`populate`) to control their pagination, sorting, filtering, and count.
340
+ `joins` is the correct control for Payload `type: 'join'` virtual reverse-relation fields. In this platform's public SDK schema, `products.variants`, `products.options`, `customers.orders`, `customers.addresses`, `posts.comments`, `article-authors.articles`, `orders.{items,transactions,fulfillments,returns}`, and similar reverse-relations are all join fields — you must use `joins` (not `depth`/`populate`) to control their pagination, sorting, filtering, and count. Internal backing joins such as product collection memberships are intentionally omitted from public SDK collection types.
175
341
 
176
342
  ```typescript
177
343
  // Canonical product detail query — variants/options are join fields on Products
@@ -244,29 +410,40 @@ export default async function ProductPage({ params }) {
244
410
  ```typescript
245
411
  const client = createClient({
246
412
  publishableKey: string, // Required
413
+ apiUrl?: string, // Optional API origin override
247
414
  })
248
415
 
249
416
  const server = createServerClient({
250
417
  publishableKey: string,
251
418
  secretKey: string, // sk01_... or pat01_...
419
+ apiUrl?: string, // Optional API origin override
252
420
  })
253
421
  ```
254
422
 
255
- | Option | Type | Description |
256
- | ---------------- | -------- | ---------------------------- |
257
- | `publishableKey` | `string` | API publishable key |
258
- | `secretKey` | `string` | API secret key (server only) |
423
+ | Option | Type | Description |
424
+ | ---------------- | -------- | ------------------------------------------------------------- |
425
+ | `publishableKey` | `string` | API publishable key |
426
+ | `secretKey` | `string` | API secret key or PAT (server only) |
427
+ | `apiUrl` | `string` | Optional API origin override for staging, preview, or proxies |
428
+
429
+ Use `apiUrl: string` when an SDK instance should target a non-default API
430
+ origin.
259
431
 
260
- API URL 환경변수로 오버라이드 가능합니다:
432
+ API URL resolution order:
261
433
 
262
- - `SOFTWARE_API_URL` (서버용) 또는 `NEXT_PUBLIC_SOFTWARE_API_URL` (브라우저용)
263
- - 미설정 시: dev 빌드(`@dev` 태그) `api-dev.01.software`, 정식 릴리즈는 `api.01.software`
434
+ 1. Explicit `apiUrl` passed to `createClient()` or `createServerClient()`
435
+ 2. `SOFTWARE_API_URL` (server) or `NEXT_PUBLIC_SOFTWARE_API_URL` (browser)
436
+ 3. Build-time default: `DEFAULT_API_URL` when injected at build time; otherwise dev-tagged SDK builds (`-dev.` versions) use `https://api.stg.01.software`, and regular releases use `https://api.01.software`
264
437
 
265
438
  ### Query Builder
266
439
 
267
440
  Access collections via `client.collections.from(slug)`.
268
441
 
269
- > **Note:** `client.collections.from()` returns a `ReadOnlyQueryBuilder` (only `find`, `findById`, `count`, `findMetadata`, `findMetadataById`). Write operations (`create`, `update`, `remove`, `updateMany`, `removeMany`) are only available on `server.collections.from()`.
442
+ > **Note:** the root `client.collections.from()` type exposes the lightweight
443
+ > read surface (`find`, `findById`, `count`). Metadata helpers live behind the
444
+ > optional `@01.software/sdk/metadata` entry, and write operations (`create`,
445
+ > `update`, `remove`, `updateMany`, `removeMany`) are only available on
446
+ > `server.collections.from()`.
270
447
 
271
448
  ```typescript
272
449
  // List query - returns PayloadFindResponse
@@ -295,11 +472,17 @@ const product = await client.collections.from('products').findById(id, {
295
472
 
296
473
  // Single item query - returns document directly
297
474
  const product = await client.collections.from('products').findById('id')
475
+ ```
476
+
477
+ Raw collection mutations are an escape hatch. For ecommerce product catalog
478
+ writes, prefer `server.commerce.product.upsert()` so options, option-values,
479
+ and variants are written through the domain transaction.
298
480
 
481
+ ```typescript
299
482
  // Create (server only) - returns PayloadMutationResponse
300
483
  const { doc, message } = await server.collections
301
- .from('products')
302
- .create({ name: 'Product' })
484
+ .from('articles')
485
+ .create({ title: 'Article' })
303
486
 
304
487
  // Create with file upload (server only) - uses multipart/form-data
305
488
  const { doc } = await server.collections
@@ -308,8 +491,8 @@ const { doc } = await server.collections
308
491
 
309
492
  // Update (server only) - returns PayloadMutationResponse
310
493
  const { doc } = await server.collections
311
- .from('products')
312
- .update('id', { name: 'Updated' })
494
+ .from('articles')
495
+ .update('id', { title: 'Updated article' })
313
496
 
314
497
  // Update with file replacement (server only)
315
498
  await server.collections
@@ -317,28 +500,26 @@ await server.collections
317
500
  .update('id', { alt: 'New alt' }, { file: newFile })
318
501
 
319
502
  // Delete (server only) - returns document directly
320
- const deletedDoc = await server.collections.from('products').remove('id')
503
+ const deletedDoc = await server.collections.from('articles').remove('id')
321
504
 
322
505
  // Count
323
506
  const { totalDocs } = await client.collections.from('products').count()
324
507
 
325
- // SEO Metadata (fetch + generate in one call, depth: 1 auto-applied)
326
- const metadata = await client.collections
327
- .from('products')
328
- .findMetadata(
329
- { where: { slug: { equals: 'my-product' } } },
330
- { siteName: 'My Store' },
331
- )
508
+ // SEO Metadata (generate from a fetched document)
509
+ import { extractSeo, generateMetadata } from '@01.software/sdk/metadata'
332
510
 
333
- const metadataById = await client.collections
334
- .from('products')
335
- .findMetadataById('id', {
336
- siteName: 'My Store',
337
- })
511
+ const { docs } = await client.collections.from('products').find({
512
+ where: { slug: { equals: 'my-product' } },
513
+ limit: 1,
514
+ depth: 1,
515
+ })
516
+ const metadata = docs[0]
517
+ ? generateMetadata(extractSeo(docs[0]), { siteName: 'My Store' })
518
+ : null
338
519
 
339
520
  // Bulk operations (server only)
340
- await server.collections.from('products').updateMany(where, data)
341
- await server.collections.from('products').removeMany(where)
521
+ await server.collections.from('articles').updateMany(where, data)
522
+ await server.collections.from('articles').removeMany(where)
342
523
  ```
343
524
 
344
525
  ### API Response Types (Payload Native)
@@ -370,95 +551,112 @@ interface PayloadMutationResponse<T> {
370
551
  // findById() / remove() returns T (document directly)
371
552
  ```
372
553
 
373
- | Operation | Response Type |
374
- | -------------------- | ------------------------------------------------------------------ |
375
- | `find()` | `PayloadFindResponse<T>` - `{ docs, totalDocs, hasNextPage, ... }` |
376
- | `findById()` | `T` - document object directly |
377
- | `create()` | `PayloadMutationResponse<T>` - `{ doc, message }` |
378
- | `update()` | `PayloadMutationResponse<T>` - `{ doc, message }` |
379
- | `remove()` | `T` - deleted document object directly |
380
- | `count()` | `{ totalDocs: number }` |
381
- | `findMetadata()` | `Metadata \| null` - Next.js Metadata (null if no match) |
382
- | `findMetadataById()` | `Metadata` - Next.js Metadata (throws on 404) |
554
+ | Operation | Response Type |
555
+ | ------------ | ------------------------------------------------------------------ |
556
+ | `find()` | `PayloadFindResponse<T>` - `{ docs, totalDocs, hasNextPage, ... }` |
557
+ | `findById()` | `T` - document object directly |
558
+ | `create()` | `PayloadMutationResponse<T>` - `{ doc, message }` |
559
+ | `update()` | `PayloadMutationResponse<T>` - `{ doc, message }` |
560
+ | `remove()` | `T` - deleted document object directly |
561
+ | `count()` | `{ totalDocs: number }` |
383
562
 
384
563
  ### React Query Hooks
385
564
 
386
- Read hooks are available on both `Client` and `ServerClient` via `client.query.*`. Mutation hooks (`useCreate`, `useUpdate`, `useRemove`) are only available on `ServerClient`.
565
+ React Query helpers are opt-in through `@01.software/sdk/query`. Install
566
+ `@tanstack/react-query` (and React peers) only when your app imports this
567
+ sub-path. Browser components should use `createQueryHooks(client)` for
568
+ browser-safe reads and customer auth hooks. Collection writes belong in trusted
569
+ server code via `createServerClient`.
387
570
 
388
571
  ```typescript
572
+ import { createQueryHooks } from '@01.software/sdk/query'
573
+
574
+ const query = createQueryHooks(client)
575
+
389
576
  // List query
390
- const { data, isLoading } = client.query.useQuery({
577
+ const { data, isLoading } = query.useQuery({
391
578
  collection: 'products',
392
579
  options: { limit: 10 },
393
580
  })
394
581
 
395
582
  // Suspense mode
396
- const { data } = client.query.useSuspenseQuery({
583
+ const { data } = query.useSuspenseQuery({
397
584
  collection: 'products',
398
585
  options: { limit: 10 },
399
586
  })
400
587
 
401
588
  // Query by ID
402
- const { data } = client.query.useQueryById({
589
+ const { data } = query.useQueryById({
403
590
  collection: 'products',
404
591
  id: 'product_id',
405
592
  })
406
593
 
407
594
  // Infinite scroll
408
- const { data, fetchNextPage, hasNextPage } = client.query.useInfiniteQuery({
595
+ const { data, fetchNextPage, hasNextPage } = query.useInfiniteQuery({
409
596
  collection: 'products',
410
597
  options: { limit: 20 },
411
598
  })
412
599
 
413
- // Mutation hooks — ServerClient only (auto-invalidate cache on success)
414
- const { mutate: create } = client.query.useCreate({ collection: 'images' })
415
- const { mutate: update } = client.query.useUpdate({ collection: 'products' })
416
- const { mutate: remove } = client.query.useRemove({ collection: 'products' })
417
-
418
- create({ data: { alt: 'Hero' }, file: imageFile, filename: 'hero.jpg' })
419
- update({ id: 'product_id', data: { title: 'Updated' } })
420
- remove('product_id')
421
-
422
600
  // SSR Prefetch
423
- await client.query.prefetchQuery({
601
+ await query.prefetchQuery({
424
602
  collection: 'products',
425
603
  options: { limit: 10 },
426
604
  })
427
- await client.query.prefetchQueryById({
605
+ await query.prefetchQueryById({
428
606
  collection: 'products',
429
607
  id: 'product_id',
430
608
  })
431
- await client.query.prefetchInfiniteQuery({
609
+ await query.prefetchInfiniteQuery({
432
610
  collection: 'products',
433
611
  pageSize: 20,
434
612
  })
435
613
 
436
614
  // Cache utilities
437
- client.query.invalidateQueries('products')
438
- client.query.getQueryData('products', 'list', options)
439
- client.query.setQueryData('products', 'detail', id, data)
615
+ query.invalidateQueries('products')
616
+ query.getQueryData('products', 'list', options)
617
+ query.setQueryData('products', 'detail', id, data)
440
618
 
441
619
  // Customer auth hooks (Client only)
442
- const { data: profile } = client.query.useCustomerMe()
443
- const { mutate: login } = client.query.useCustomerLogin()
444
- const { mutate: register } = client.query.useCustomerRegister()
445
- const { mutate: logout } = client.query.useCustomerLogout()
620
+ const { data: profile } = query.useCustomerMe()
621
+ const { mutate: login } = query.useCustomerLogin()
622
+ const { mutate: register } = query.useCustomerRegister()
623
+ const { mutate: logout } = query.useCustomerLogout()
446
624
 
447
625
  login({ email: 'user@example.com', password: 'password' })
448
626
 
449
627
  // Other customer mutations
450
- client.query.useCustomerForgotPassword()
451
- client.query.useCustomerResetPassword()
452
- client.query.useCustomerChangePassword()
628
+ query.useCustomerForgotPassword()
629
+ query.useCustomerResetPassword()
630
+ query.useCustomerChangePassword()
453
631
 
454
632
  // Customer cache utilities
455
- client.query.invalidateCustomerQueries()
456
- client.query.getCustomerData()
457
- client.query.setCustomerData(profile)
633
+ query.invalidateCustomerQueries()
634
+ query.getCustomerData()
635
+ query.setCustomerData(profile)
636
+ ```
637
+
638
+ ```typescript
639
+ // Server action / API route for collection writes
640
+ import { createServerClient } from '@01.software/sdk/server'
641
+
642
+ const server = createServerClient({
643
+ publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
644
+ secretKey: process.env.SOFTWARE_SECRET_KEY!,
645
+ })
646
+
647
+ await server.collections.from('articles').update('article_id', {
648
+ title: 'Updated article',
649
+ })
458
650
  ```
459
651
 
460
652
  ### Customer Auth
461
653
 
654
+ Customer auth methods currently cover local email/password flows: register,
655
+ login, refresh, password reset, profile read/update, and password change.
656
+ `CustomerProfile.authProvider` may contain `google`, `apple`, `kakao`, or
657
+ `naver` for accounts created through platform/provider integrations, but the
658
+ SDK does not expose social-login initiation or callback helpers yet.
659
+
462
660
  Available on Client via `client.customer.auth.*`.
463
661
 
464
662
  ```typescript
@@ -548,7 +746,63 @@ await client.commerce.shipping.calculate({ shippingPolicyId?, orderAmount, posta
548
746
 
549
747
  ### Commerce Product
550
748
 
551
- Available on both Client and ServerClient via `commerce.product.*`.
749
+ Product reads are available on both Client and ServerClient via `commerce.product.*`.
750
+ Product catalog writes are ServerClient-only.
751
+ Use `server.commerce.product.upsert()` for product catalog writes that include
752
+ options, option values, and variants. It is the tenant-admin safe path because
753
+ it applies the product/option/variant transaction that raw collection writes do
754
+ not provide.
755
+
756
+ ```typescript
757
+ const result = await server.commerce.product.upsert({
758
+ product: {
759
+ title: 'Every Peach Tee',
760
+ slug: 'every-peach-tee',
761
+ status: 'published',
762
+ },
763
+ options: [
764
+ {
765
+ title: 'Color',
766
+ slug: 'color',
767
+ values: [
768
+ { value: 'Black', slug: 'black', swatchColor: '#111111' },
769
+ { value: 'White', slug: 'white', swatchColor: '#ffffff' },
770
+ ],
771
+ },
772
+ {
773
+ title: 'Size',
774
+ slug: 'size',
775
+ values: [
776
+ { value: 'Small', slug: 's' },
777
+ { value: 'Medium', slug: 'm' },
778
+ ],
779
+ },
780
+ ],
781
+ variants: [
782
+ {
783
+ optionValues: { color: { valueSlug: 'black' }, size: { valueSlug: 's' } },
784
+ sku: 'TEE-BLK-S',
785
+ price: 29000,
786
+ stock: 10,
787
+ isActive: true,
788
+ },
789
+ {
790
+ optionValues: { color: { valueSlug: 'white' }, size: { valueSlug: 'm' } },
791
+ sku: 'TEE-WHT-M',
792
+ price: 29000,
793
+ stock: 8,
794
+ isActive: true,
795
+ },
796
+ ],
797
+ })
798
+
799
+ if (!result.ok) {
800
+ throw new Error(result.message)
801
+ }
802
+ ```
803
+
804
+ For updates to existing options or option-values, prefer `id` / `valueId` when
805
+ available so rename-safe updates do not depend on slugs.
552
806
 
553
807
  ```typescript
554
808
  // Batch stock check (point-in-time read, NOT a reservation)
@@ -643,9 +897,9 @@ Source of truth: `packages/sdk/src/core/collection/const.ts` (`COLLECTIONS`: 73)
643
897
  | Tenant | `tenants`, `tenant-metadata`, `tenant-logos` |
644
898
  | Products | `products`, `product-variants`, `product-options`, `product-option-values`, `product-categories`, `product-tags`, `product-collections`, `brands`, `brand-logos` |
645
899
  | Orders | `orders`, `order-items`, `returns`, `return-items`, `fulfillments`, `fulfillment-items`, `transactions` |
646
- | Customers | `customers`, `customer-profiles`, `customer-profile-lists`, `customer-addresses` |
900
+ | Customers | `customers`, `customer-profiles`, `customer-addresses` |
647
901
  | Carts | `carts`, `cart-items` |
648
- | Commerce | `discounts`, `shipping-policies` |
902
+ | Commerce | `discounts`, `shipping-policies`, `shipping-zones` |
649
903
  | Content | `documents`, `document-categories`, `document-types`, `articles`, `article-authors`, `article-categories`, `article-tags`, `links`, `link-categories`, `link-tags` |
650
904
  | Playlists / Tracks | `playlists`, `playlist-categories`, `playlist-tags`, `tracks`, `track-categories`, `track-tags` |
651
905
  | Galleries | `galleries`, `gallery-categories`, `gallery-tags`, `gallery-items` |
@@ -654,7 +908,7 @@ Source of truth: `packages/sdk/src/core/collection/const.ts` (`COLLECTIONS`: 73)
654
908
  | Live Streams | `live-streams` |
655
909
  | Media | `images` |
656
910
  | Forms | `forms`, `form-submissions` |
657
- | Community | `posts`, `comments`, `reactions`, `reaction-types`, `bookmarks`, `post-categories` |
911
+ | Community | `posts`, `comments`, `reactions`, `reaction-types`, `bookmarks`, `post-categories`, `customer-profile-lists` |
658
912
  | Events | `event-calendars`, `events`, `event-categories`, `event-occurrences`, `event-tags` |
659
913
 
660
914
  Server-only collections: `customer-groups`, `reports`, and `community-bans`
@@ -28,7 +28,10 @@ module.exports = __toCommonJS(react_exports);
28
28
  var import_react = require("react");
29
29
 
30
30
  // src/core/client/types.ts
31
- function resolveApiUrl() {
31
+ function resolveApiUrl(apiUrl) {
32
+ if (apiUrl) {
33
+ return apiUrl.replace(/\/$/, "");
34
+ }
32
35
  if (typeof process !== "undefined" && process.env) {
33
36
  const envUrl = process.env.SOFTWARE_API_URL || process.env.NEXT_PUBLIC_SOFTWARE_API_URL;
34
37
  if (envUrl) {