@proveanything/smartlinks 1.9.23 → 1.10.1

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.
@@ -30,7 +30,7 @@ Without a shared pattern, every app reinvents:
30
30
  - CSV import/export
31
31
  - bulk operations
32
32
 
33
- The result is drift: each app feels different and admins have to re-learn the model. This guide locks the model down at the SDK level so the matching UI primitives in `@proveanything/ui-utils` (see the [companion guide](ui-utils.md)) can stay simple.
33
+ The result is drift: each app feels different and admins have to re-learn the model. This guide locks the model down at the SDK level so the matching UI primitives in `@proveanything/smartlinks-utils-ui` (see the [companion guide](ui-utils.md)) can stay simple.
34
34
 
35
35
  ---
36
36
 
@@ -71,20 +71,35 @@ variant:<productId>:<variantId>
71
71
  batch:<productId>:<batchId>
72
72
  proof:<proofId>
73
73
  facet:<facetKey>:<valueKey>
74
- default
74
+ '' (universal / collection-wide fallback)
75
75
  ```
76
76
 
77
77
  Notes:
78
78
 
79
79
  - Variants and batches are **always nested under a product** in the SDK, so their refs include `productId`.
80
80
  - `facet:` refs are **collection-wide** — facets cross products by design.
81
- - `default` is a single record per (app, recordType) used as the global fallback.
81
+ - `''` (empty ref) is the **universal** record — one per `(app, recordType)` with no scope restrictions; the collection-wide fallback.
82
82
  - Refs are opaque to the SDK. Apps parse them. A helper module (`@proveanything/smartlinks-utils-ui/records-admin`) exports `parseRef`/`buildRef` so all apps agree on syntax. See Appendix A for the full implementation.
83
83
 
84
84
  ### Adding scopes later
85
85
 
86
86
  If a new axis appears (e.g. `region:eu`), pick a new prefix and document it. Never reuse a prefix with different semantics.
87
87
 
88
+ ### Writing records
89
+
90
+ When creating or upserting a record, send a structured **`RecordScope`** — the server derives `ref` from it:
91
+
92
+ ```ts
93
+ await app.records.upsert(collectionId, appId, {
94
+ recordType: 'nutrition',
95
+ scope: { productId: 'prod_abc', variantId: 'var_500ml' }, // server → ref: 'product:prod_abc/variant:var_500ml'
96
+ data: { calories: 260 },
97
+ });
98
+ ```
99
+
100
+ - `ref` is for **display, URL routing, and resolution output only** — never construct one to use as an upsert key.
101
+ - `customId` / `sourceSystem` are for external references (filterable via `list()`) but are **not unique** — the same external ID can exist across `recordType` values. Do not upsert on `customId` either.
102
+
88
103
  ---
89
104
 
90
105
  ## 4. Resolution order (REQUIRED)
@@ -92,7 +107,7 @@ If a new axis appears (e.g. `region:eu`), pick a new prefix and document it. Nev
92
107
  When the **public** side of an app needs "the data that applies to this proof / product / context", it walks the chain from most-specific to least-specific and returns the first match:
93
108
 
94
109
  ```
95
- proof → batch → variant → product → facet(*) → default
110
+ proof → batch → variant → product → facet(*) → universal
96
111
  ```
97
112
 
98
113
  `facet(*)` means: walk every facet attached to the product in a deterministic order (alphabetical by `facetKey`, then `valueKey`) and use the first matching facet record.
@@ -208,7 +223,7 @@ If you are using `<RecordsAdminShell>`, the bulk actions menu is included and wi
208
223
 
209
224
  ## 9. CSV import / export
210
225
 
211
- Adopt this column shape across all records-based apps:
226
+ If your app exposes CSV import/export, use this column shape so files round-trip across apps:
212
227
 
213
228
  ```
214
229
  scope,scopeRef,<field1>,<field2>,...
@@ -220,18 +235,22 @@ facet,bread_type/sourdough,240,11.0,...
220
235
 
221
236
  - `scope` is the kind; `scopeRef` is the human-readable reference (the shell maps it to the canonical `ref`).
222
237
  - Validation errors return a downloadable annotated CSV with an `error` column appended.
223
- - Round-tripping (export → reimport unchanged) MUST be a no-op.
238
+
239
+ #### If you ship CSV
240
+
241
+ - Round-tripping (export → reimport unchanged) must be a no-op.
224
242
 
225
243
  ---
226
244
 
227
245
  ## 10. Public-side hook contract
228
246
 
229
- To keep widgets consistent across apps, expose one hook per record type (implemented in `@proveanything/ui-utils`):
247
+ To keep widgets consistent across apps, expose one hook per record type (from `@proveanything/smartlinks-utils-ui`):
230
248
 
231
249
  ```ts
232
250
  import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
233
251
 
234
252
  const { data, source, isLoading } = useResolvedRecord({
253
+ SL,
235
254
  appId,
236
255
  recordType: 'nutrition',
237
256
  // any combination of these — the hook walks the chain:
@@ -239,13 +258,13 @@ const { data, source, isLoading } = useResolvedRecord({
239
258
  });
240
259
  ```
241
260
 
242
- `source` is one of `'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'default' | null`. UI can show a badge ("Showing batch-specific values") when useful.
261
+ `source` is one of `'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'universal' | null`. UI can show a badge ("Showing batch-specific values") when useful.
243
262
 
244
263
  ---
245
264
 
246
265
  ## 11. Telemetry
247
266
 
248
- All admin shells emit these events. The `<RecordsAdminShell>` from `@proveanything/ui-utils` wires them automatically using `interactions.appendEvent` from the SDK — apps don't call this themselves.
267
+ All admin shells emit these events. The `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui` wires them automatically using `interactions.appendEvent` from the SDK — apps don't call this themselves.
249
268
 
250
269
  | Event | Props |
251
270
  |------------------------|------------------------------------------------------|
@@ -278,11 +297,11 @@ All admin shells emit these events. The `<RecordsAdminShell>` from `@proveanythi
278
297
 
279
298
  ## Appendix A — `ref` parser reference
280
299
 
281
- This is the canonical implementation. Copy it into your app or import from `@proveanything/ui-utils/records`.
300
+ This is the canonical implementation. Copy it into your app or import from `@proveanything/smartlinks-utils-ui/records-admin`.
282
301
 
283
302
  ```ts
284
303
  type ParsedRef =
285
- | { kind: 'default' }
304
+ | { kind: 'universal' }
286
305
  | { kind: 'product'; productId: string }
287
306
  | { kind: 'variant'; productId: string; variantId: string }
288
307
  | { kind: 'batch'; productId: string; batchId: string }
@@ -291,7 +310,7 @@ type ParsedRef =
291
310
 
292
311
  export const buildRef = (p: ParsedRef): string => {
293
312
  switch (p.kind) {
294
- case 'default': return 'default';
313
+ case 'universal': return '';
295
314
  case 'product': return `product:${p.productId}`;
296
315
  case 'variant': return `variant:${p.productId}:${p.variantId}`;
297
316
  case 'batch': return `batch:${p.productId}:${p.batchId}`;
@@ -301,7 +320,7 @@ export const buildRef = (p: ParsedRef): string => {
301
320
  };
302
321
 
303
322
  export const parseRef = (ref: string): ParsedRef | null => {
304
- if (ref === 'default') return { kind: 'default' };
323
+ if (!ref) return { kind: 'universal' };
305
324
  const [head, ...rest] = ref.split(':');
306
325
  switch (head) {
307
326
  case 'product': return rest.length === 1 ? { kind: 'product', productId: rest[0] } : null;
package/docs/ui-utils.md CHANGED
@@ -82,7 +82,7 @@ import * as SL from '@proveanything/smartlinks';
82
82
  scopes={['facet', 'product', 'variant', 'batch']}
83
83
  contextScope={{ productId, variantId, batchId }} // from iframe URL — optional
84
84
  defaultData={() => ({})}
85
- csvSchema={{ columns: [/* ... */] }}
85
+ csvSchema={{ columns: [/* ... */] }} // optional — omit to disable CSV import/export
86
86
  renderEditor={(ctx) => <NutritionForm ctx={ctx} />}
87
87
  renderPreview={({ resolved }) => <pre>{JSON.stringify(resolved, null, 2)}</pre>}
88
88
  />
@@ -97,7 +97,7 @@ import * as SL from '@proveanything/smartlinks';
97
97
  - **Collection-aware tabs**: calls `collection.get` and hides Variants / Batches tabs unless `collection.variants` / `collection.batches` are true — no flicker
98
98
  - **Server-side pagination** via `useInfiniteQuery` — handles thousands of products with a "Load more" button
99
99
  - **Context-aware**: pass `contextScope` from your iframe URL (`productId` / `variantId` / `batchId`) and the browser is constrained to that subtree with the right tab auto-selected
100
- - **CSV import / export** with a schema you define; failed rows come back as an annotated CSV
100
+ - **CSV import / export** (optional provide `csvSchema` to enable); failed rows come back as an annotated CSV
101
101
  - **Bulk actions menu** (apply-to-many, copy-from, clear) via `bulkUpsert` / `bulkDelete`
102
102
  - **Telemetry hook** (`onTelemetry`) emits typed events for save, delete, scope change, CSV import/export, bulk apply
103
103
  - **i18n strings** fully overridable
@@ -136,7 +136,7 @@ const { data, source, isLoading } = useResolvedRecord({
136
136
  batchId, // optional
137
137
  proofId, // optional
138
138
  });
139
- // source: 'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'default' | null
139
+ // source: 'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'universal' | null
140
140
  ```
141
141
 
142
142
  Walks the resolution chain defined in [records-admin-pattern.md §4](records-admin-pattern.md#4-resolution-order-required). Use this on the **public widget side** to read the correct value for a given context.