@proveanything/smartlinks 1.9.20 → 1.9.21
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/dist/docs/API_SUMMARY.md +3 -1
- package/dist/docs/app-manifest.md +38 -1
- package/dist/docs/auth-kit.md +162 -0
- package/dist/docs/forms.md +113 -0
- package/dist/docs/overview.md +4 -0
- package/dist/docs/records-admin-pattern.md +324 -0
- package/dist/docs/ui-utils.md +127 -0
- package/dist/openapi.yaml +6 -0
- package/dist/types/collection.d.ts +2 -0
- package/docs/API_SUMMARY.md +3 -1
- package/docs/app-manifest.md +38 -1
- package/docs/auth-kit.md +162 -0
- package/docs/forms.md +113 -0
- package/docs/overview.md +4 -0
- package/docs/records-admin-pattern.md +324 -0
- package/docs/ui-utils.md +127 -0
- package/openapi.yaml +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# SmartLinks Records-Based Admin Pattern
|
|
2
|
+
|
|
3
|
+
> Canonical guide for building admin UIs in microapps that store **per-product**, **per-facet**, **per-variant** or **per-batch** data.
|
|
4
|
+
>
|
|
5
|
+
> Audience: microapp developers (nutrition, allergy, ingredients, cooking-guide, warranty, provenance, …).
|
|
6
|
+
>
|
|
7
|
+
> Status: **standard** — new admin apps in this category MUST follow this contract. Existing apps SHOULD migrate.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Why a shared pattern?
|
|
12
|
+
|
|
13
|
+
Many SmartLinks microapps store **structured data that varies along one or more product axes**:
|
|
14
|
+
|
|
15
|
+
| App | Varies by |
|
|
16
|
+
|------------------|------------------------------------------------|
|
|
17
|
+
| Nutrition | product, facet (bread type, region), batch |
|
|
18
|
+
| Allergy | product, facet (recipe family) |
|
|
19
|
+
| Ingredients | product, variant (size), batch (production run)|
|
|
20
|
+
| Cooking guide | product, facet (cut of meat) |
|
|
21
|
+
| Warranty | product, variant, batch |
|
|
22
|
+
| Provenance | batch |
|
|
23
|
+
|
|
24
|
+
Without a shared pattern, every app reinvents:
|
|
25
|
+
|
|
26
|
+
- the left-rail "browse + select" list
|
|
27
|
+
- the per-axis precedence rules
|
|
28
|
+
- the inheritance/override UI
|
|
29
|
+
- the "no data yet" empty state
|
|
30
|
+
- CSV import/export
|
|
31
|
+
- bulk operations
|
|
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.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Storage model: `app.records`
|
|
38
|
+
|
|
39
|
+
**Do not** stuff per-axis data into `appConfiguration.products[productId]`. Use `app.records` (recordType-keyed) so that data is queryable, paginatable and survives schema changes.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { app } from '@proveanything/smartlinks';
|
|
43
|
+
|
|
44
|
+
await app.records.create(collectionId, appId, {
|
|
45
|
+
recordType: 'nutrition', // app-defined, stable
|
|
46
|
+
ref: 'product:prod_abc', // see §3
|
|
47
|
+
data: { /* domain payload */ },
|
|
48
|
+
}, /* admin= */ true);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Each record has:
|
|
52
|
+
|
|
53
|
+
| Field | Purpose |
|
|
54
|
+
|--------------|-------------------------------------------------------------|
|
|
55
|
+
| `recordType` | Namespaces records inside the app. One app may have several (`nutrition`, `cooking_steps`). |
|
|
56
|
+
| `ref` | Encodes the **scope** of the record. See §3. |
|
|
57
|
+
| `data` | The domain payload. Free-form per app. |
|
|
58
|
+
| `meta` | Reserved for system fields (timestamps, author). |
|
|
59
|
+
|
|
60
|
+
> **Rule:** `(appId, recordType, ref)` is unique. Treat it as the natural key.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 3. The `ref` convention (REQUIRED)
|
|
65
|
+
|
|
66
|
+
`ref` is a colon-delimited string that encodes which scope a record applies to. Standardising it is what makes the shared admin shell possible.
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
product:<productId>
|
|
70
|
+
variant:<productId>:<variantId>
|
|
71
|
+
batch:<productId>:<batchId>
|
|
72
|
+
proof:<proofId>
|
|
73
|
+
facet:<facetKey>:<valueKey>
|
|
74
|
+
default
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Notes:
|
|
78
|
+
|
|
79
|
+
- Variants and batches are **always nested under a product** in the SDK, so their refs include `productId`.
|
|
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.
|
|
82
|
+
- Refs are opaque to the SDK. Apps parse them. A helper module (`@proveanything/ui-utils/records`) exports `parseRef`/`buildRef` so all apps agree on syntax. See Appendix A for the full implementation.
|
|
83
|
+
|
|
84
|
+
### Adding scopes later
|
|
85
|
+
|
|
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
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 4. Resolution order (REQUIRED)
|
|
91
|
+
|
|
92
|
+
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
|
+
|
|
94
|
+
```
|
|
95
|
+
proof → batch → variant → product → facet(*) → default
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`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.
|
|
99
|
+
|
|
100
|
+
> **Rule:** Resolution is **first-match-wins, not merge**. If you need field-level merging, build it on top with explicit `inheritsFrom` markers in the payload — but the default for shared infra is whole-record replacement, because it is far easier to reason about.
|
|
101
|
+
|
|
102
|
+
The canonical resolver lives in `@proveanything/ui-utils/records` so every app behaves identically:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { resolveRecord } from '@proveanything/ui-utils/records';
|
|
106
|
+
|
|
107
|
+
const resolved = await resolveRecord({
|
|
108
|
+
appId,
|
|
109
|
+
recordType: 'nutrition',
|
|
110
|
+
scope: { collectionId, productId, variantId, batchId, proofId },
|
|
111
|
+
});
|
|
112
|
+
// → { record, source: 'variant' | 'product' | 'facet' | … } | null
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Apps that only support a subset of scopes pass `supportedScopes: ['product', 'facet']` and the resolver skips the rest.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. Scope capabilities (declared per app)
|
|
120
|
+
|
|
121
|
+
An app declares which scopes it accepts records for in `app.manifest.json`. This drives the admin UI and avoids dead tabs:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
// app.manifest.json (extension)
|
|
125
|
+
{
|
|
126
|
+
"records": {
|
|
127
|
+
"nutrition": {
|
|
128
|
+
"scopes": ["product", "facet", "batch"],
|
|
129
|
+
"defaultScope": "facet",
|
|
130
|
+
"label": "Nutrition info"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Field | Meaning |
|
|
137
|
+
|----------------|----------------------------------------------------------------|
|
|
138
|
+
| `scopes` | Allowed scope kinds, in **resolution order**. |
|
|
139
|
+
| `defaultScope` | Where the "Create new" button lands in the admin shell. |
|
|
140
|
+
| `label` | Human-readable label for the record type (used in headings). |
|
|
141
|
+
|
|
142
|
+
The shared admin shell reads this manifest entry and renders only the relevant tabs. Apps don't hard-code tab lists.
|
|
143
|
+
|
|
144
|
+
See [app-manifest.md](app-manifest.md) for the full schema reference.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 6. Discovering whether variants / batches are in use
|
|
149
|
+
|
|
150
|
+
The SDK does **not** expose a `collection.variantsEnabled` flag, by design. Whether a product has variants/batches is an **emergent** property: it has them iff someone created some.
|
|
151
|
+
|
|
152
|
+
The standard probe pattern:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { variant, batch } from '@proveanything/smartlinks';
|
|
156
|
+
|
|
157
|
+
const [variants, batches] = await Promise.all([
|
|
158
|
+
variant.list(collectionId, productId).catch(() => []),
|
|
159
|
+
batch.list(collectionId, productId).catch(() => []),
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
// scopeConfig is the parsed manifest records entry for this recordType
|
|
163
|
+
// e.g. manifest.records?.nutrition → { scopes: ['product', 'facet', 'batch'], ... }
|
|
164
|
+
const showVariantTab = variants.length > 0 || scopeConfig?.scopes.includes('variant') === true;
|
|
165
|
+
const showBatchTab = batches.length > 0 || scopeConfig?.scopes.includes('batch') === true;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Rules:
|
|
169
|
+
|
|
170
|
+
1. If the app **declares** support for the scope, always offer "Add variant" / "Add batch" affordances even when the list is empty.
|
|
171
|
+
2. If the app does **not** declare support, hide the tab entirely even if some exist (another app created them).
|
|
172
|
+
3. Cache list results per `(collectionId, productId)` with a short TTL — the shell will hit them often.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 7. Inheritance & overrides
|
|
177
|
+
|
|
178
|
+
Because resolution is first-match-wins, the admin UI must make inheritance **visible**:
|
|
179
|
+
|
|
180
|
+
- When editing a **variant** record, show the **product** record as the inherited baseline.
|
|
181
|
+
- Each field in the editor displays a small **↩ "Inherited"** marker when its value matches the parent and **● "Override"** when it differs.
|
|
182
|
+
- A row-level "Reset to inherited" action removes the override (deletes the record at the current scope if all overrides are reset).
|
|
183
|
+
|
|
184
|
+
Apps don't have to implement this themselves — the `<RecordEditor>` primitive in `@proveanything/ui-utils` does it given the resolved parent payload.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 8. Bulk operations
|
|
189
|
+
|
|
190
|
+
Standard verbs every shell should expose:
|
|
191
|
+
|
|
192
|
+
| Verb | Behaviour |
|
|
193
|
+
|-------------------|----------------------------------------------------------------------|
|
|
194
|
+
| **Apply to many** | Take the current record's payload, write it to N selected products / variants. |
|
|
195
|
+
| **Copy from** | Pick a source scope, copy its payload to the current scope. |
|
|
196
|
+
| **Clear** | Delete records at the current scope (children unaffected). |
|
|
197
|
+
|
|
198
|
+
The SDK does not currently expose a bulk write endpoint for records. Implement these by fanning out individual `app.records.create` / `app.records.update` calls with bounded concurrency (e.g. 10 in-flight at a time):
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { app } from '@proveanything/smartlinks';
|
|
202
|
+
|
|
203
|
+
// fan-out with concurrency cap
|
|
204
|
+
const chunks = chunkArray(targetRefs, 10);
|
|
205
|
+
for (const chunk of chunks) {
|
|
206
|
+
await Promise.all(
|
|
207
|
+
chunk.map((ref) =>
|
|
208
|
+
app.records.update(collectionId, appId, refToRecordId(ref), { data: payload }, true)
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
If you are writing to **hundreds** of records at once, **flag it** to the platform team — a batch endpoint should be added rather than saturating the API from the browser.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 9. CSV import / export
|
|
219
|
+
|
|
220
|
+
Adopt this column shape across all records-based apps:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
scope,scopeRef,<field1>,<field2>,...
|
|
224
|
+
product,prod_abc,250,12.5,...
|
|
225
|
+
variant,prod_abc/var_500ml,260,12.5,...
|
|
226
|
+
batch,prod_abc/B-2024-03,255,12.5,...
|
|
227
|
+
facet,bread_type/sourdough,240,11.0,...
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `scope` is the kind; `scopeRef` is the human-readable reference (the shell maps it to the canonical `ref`).
|
|
231
|
+
- Validation errors return a downloadable annotated CSV with an `error` column appended.
|
|
232
|
+
- Round-tripping (export → reimport unchanged) MUST be a no-op.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 10. Public-side hook contract
|
|
237
|
+
|
|
238
|
+
To keep widgets consistent across apps, expose one hook per record type (implemented in `@proveanything/ui-utils`):
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { useResolvedRecord } from '@proveanything/ui-utils/records';
|
|
242
|
+
|
|
243
|
+
const { data, source, isLoading } = useResolvedRecord({
|
|
244
|
+
appId,
|
|
245
|
+
recordType: 'nutrition',
|
|
246
|
+
// any combination of these — the hook walks the chain:
|
|
247
|
+
collectionId, productId, variantId, batchId, proofId,
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`source` is one of `'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'default' | null`. UI can show a badge ("Showing batch-specific values") when useful.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 11. Telemetry
|
|
256
|
+
|
|
257
|
+
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.
|
|
258
|
+
|
|
259
|
+
| Event | Props |
|
|
260
|
+
|------------------------|------------------------------------------------------|
|
|
261
|
+
| `record.opened` | `appId, recordType, ref, source` |
|
|
262
|
+
| `record.saved` | `appId, recordType, ref, fieldsChanged` |
|
|
263
|
+
| `record.deleted` | `appId, recordType, ref` |
|
|
264
|
+
| `record.bulkApplied` | `appId, recordType, sourceRef, targetCount` |
|
|
265
|
+
| `record.imported` | `appId, recordType, rows, errors` |
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 12. Required reading for app authors
|
|
270
|
+
|
|
271
|
+
1. This document.
|
|
272
|
+
2. The companion **[UI utils guide](ui-utils.md)** — explains the React primitives that implement this pattern.
|
|
273
|
+
3. [PRODUCT_FACETS_SDK.md](PRODUCT_FACETS_SDK.md) — facet model.
|
|
274
|
+
4. [app-data-storage.md](app-data-storage.md) — `app.records` surface.
|
|
275
|
+
|
|
276
|
+
## 13. Migration checklist for existing apps
|
|
277
|
+
|
|
278
|
+
- [ ] Stop writing per-product data into `appConfiguration`.
|
|
279
|
+
- [ ] Move to `app.records` with `recordType` + `ref`.
|
|
280
|
+
- [ ] Adopt the `ref` syntax in §3.
|
|
281
|
+
- [ ] Add a `records` block to `app.manifest.json`.
|
|
282
|
+
- [ ] Replace bespoke admin browser with `<RecordsAdminShell>` from `@proveanything/ui-utils`.
|
|
283
|
+
- [ ] Replace bespoke public hook with `useResolvedRecord`.
|
|
284
|
+
- [ ] Remove any "is variants enabled?" config — probe instead (§6).
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Appendix A — `ref` parser reference
|
|
289
|
+
|
|
290
|
+
This is the canonical implementation. Copy it into your app or import from `@proveanything/ui-utils/records`.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
type ParsedRef =
|
|
294
|
+
| { kind: 'default' }
|
|
295
|
+
| { kind: 'product'; productId: string }
|
|
296
|
+
| { kind: 'variant'; productId: string; variantId: string }
|
|
297
|
+
| { kind: 'batch'; productId: string; batchId: string }
|
|
298
|
+
| { kind: 'proof'; proofId: string }
|
|
299
|
+
| { kind: 'facet'; facetKey: string; valueKey: string };
|
|
300
|
+
|
|
301
|
+
export const buildRef = (p: ParsedRef): string => {
|
|
302
|
+
switch (p.kind) {
|
|
303
|
+
case 'default': return 'default';
|
|
304
|
+
case 'product': return `product:${p.productId}`;
|
|
305
|
+
case 'variant': return `variant:${p.productId}:${p.variantId}`;
|
|
306
|
+
case 'batch': return `batch:${p.productId}:${p.batchId}`;
|
|
307
|
+
case 'proof': return `proof:${p.proofId}`;
|
|
308
|
+
case 'facet': return `facet:${p.facetKey}:${p.valueKey}`;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const parseRef = (ref: string): ParsedRef | null => {
|
|
313
|
+
if (ref === 'default') return { kind: 'default' };
|
|
314
|
+
const [head, ...rest] = ref.split(':');
|
|
315
|
+
switch (head) {
|
|
316
|
+
case 'product': return rest.length === 1 ? { kind: 'product', productId: rest[0] } : null;
|
|
317
|
+
case 'variant': return rest.length === 2 ? { kind: 'variant', productId: rest[0], variantId: rest[1] } : null;
|
|
318
|
+
case 'batch': return rest.length === 2 ? { kind: 'batch', productId: rest[0], batchId: rest[1] } : null;
|
|
319
|
+
case 'proof': return rest.length === 1 ? { kind: 'proof', proofId: rest[0] } : null;
|
|
320
|
+
case 'facet': return rest.length >= 2 ? { kind: 'facet', facetKey: rest[0], valueKey: rest.slice(1).join(':') } : null;
|
|
321
|
+
default: return null;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
```
|
package/docs/ui-utils.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# SmartLinks UI Utils (`@proveanything/ui-utils`)
|
|
2
|
+
|
|
3
|
+
> Companion module to the SmartLinks SDK. Provides React primitives, hooks, and admin shells for building consistent microapp UIs.
|
|
4
|
+
>
|
|
5
|
+
> Package: `@proveanything/ui-utils`
|
|
6
|
+
> Install: `npm install @proveanything/ui-utils`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What is this module for?
|
|
11
|
+
|
|
12
|
+
`@proveanything/ui-utils` sits on top of `@proveanything/smartlinks`. The core SDK handles data — authentication, records, configurations, interactions. This module handles **UI** — the shared React components, hooks, and shell primitives that translate SDK data into consistent admin and public interfaces.
|
|
13
|
+
|
|
14
|
+
**When do you need it?**
|
|
15
|
+
|
|
16
|
+
- You are building an admin UI for a records-based microapp (see [records-admin-pattern.md](records-admin-pattern.md))
|
|
17
|
+
- You want the standard inheritance/override editor for scoped records
|
|
18
|
+
- You need the `useResolvedRecord` hook on the public widget side
|
|
19
|
+
- You are using `parseRef` / `buildRef` for the standard `ref` convention
|
|
20
|
+
|
|
21
|
+
You do **not** need it for apps that only use `appConfiguration`, basic widgets without scoped data, or executor bundles.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Key exports
|
|
26
|
+
|
|
27
|
+
### Records admin shell
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { RecordsAdminShell } from '@proveanything/ui-utils';
|
|
31
|
+
|
|
32
|
+
// Drop-in admin interface for any records-based app.
|
|
33
|
+
// Reads the app's manifest `records` block to determine tabs and scopes.
|
|
34
|
+
<RecordsAdminShell
|
|
35
|
+
collectionId={collectionId}
|
|
36
|
+
appId={appId}
|
|
37
|
+
recordType="nutrition"
|
|
38
|
+
renderForm={(record, parentRecord) => <NutritionForm record={record} parent={parentRecord} />}
|
|
39
|
+
/>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`RecordsAdminShell` handles: left-rail product/scope browsing, variant/batch tab discovery, the "New record" button, inheritance markers, bulk operations, CSV import/export, and telemetry events. Your app only provides the domain form.
|
|
43
|
+
|
|
44
|
+
### Record editor primitive
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { RecordEditor } from '@proveanything/ui-utils';
|
|
48
|
+
|
|
49
|
+
// Lower-level editor with inheritance diffing. Use when you need
|
|
50
|
+
// custom layout but still want the inherited/override markers.
|
|
51
|
+
<RecordEditor
|
|
52
|
+
record={variantRecord}
|
|
53
|
+
parentRecord={productRecord}
|
|
54
|
+
fields={nutritionFields}
|
|
55
|
+
onSave={(data) => app.records.update(collectionId, appId, record.id, { data }, true)}
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `useResolvedRecord` hook
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { useResolvedRecord } from '@proveanything/ui-utils/records';
|
|
63
|
+
|
|
64
|
+
const { data, source, isLoading } = useResolvedRecord({
|
|
65
|
+
appId,
|
|
66
|
+
recordType: 'nutrition',
|
|
67
|
+
collectionId,
|
|
68
|
+
productId,
|
|
69
|
+
variantId, // optional
|
|
70
|
+
batchId, // optional
|
|
71
|
+
proofId, // optional
|
|
72
|
+
});
|
|
73
|
+
// source: 'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'default' | null
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Walks the resolution chain defined in [records-admin-pattern.md §4](records-admin-pattern.md#4-resolution-order-required) and returns the first matching record plus its source scope.
|
|
77
|
+
|
|
78
|
+
### `resolveRecord` function
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { resolveRecord } from '@proveanything/ui-utils/records';
|
|
82
|
+
|
|
83
|
+
// Async, non-hook version for executors and server-side logic.
|
|
84
|
+
const resolved = await resolveRecord({
|
|
85
|
+
appId,
|
|
86
|
+
recordType: 'nutrition',
|
|
87
|
+
scope: { collectionId, productId, variantId, batchId, proofId },
|
|
88
|
+
supportedScopes: ['product', 'facet'], // optional — skip unsupported scopes
|
|
89
|
+
});
|
|
90
|
+
// → { record: AppRecord, source: string } | null
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `parseRef` / `buildRef` utilities
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { parseRef, buildRef } from '@proveanything/ui-utils/records';
|
|
97
|
+
|
|
98
|
+
const parsed = parseRef('variant:prod_abc:var_500ml');
|
|
99
|
+
// → { kind: 'variant', productId: 'prod_abc', variantId: 'var_500ml' }
|
|
100
|
+
|
|
101
|
+
const ref = buildRef({ kind: 'facet', facetKey: 'bread_type', valueKey: 'sourdough' });
|
|
102
|
+
// → 'facet:bread_type:sourdough'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
See [records-admin-pattern.md Appendix A](records-admin-pattern.md#appendix-a--ref-parser-reference) for the full `ParsedRef` type and standalone implementation.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Relationship to the core SDK
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
@proveanything/smartlinks ← data layer (records, config, interactions, …)
|
|
113
|
+
↑
|
|
114
|
+
@proveanything/ui-utils ← UI layer (components, hooks, admin shells)
|
|
115
|
+
↑
|
|
116
|
+
your microapp ← domain logic and custom forms
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`ui-utils` imports from `@proveanything/smartlinks` internally. You should install both packages, but only import directly from each for their respective concerns — don't reach into `ui-utils` for SDK data primitives.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Further reading
|
|
124
|
+
|
|
125
|
+
- [records-admin-pattern.md](records-admin-pattern.md) — the data contract that `RecordsAdminShell` implements
|
|
126
|
+
- [building-react-components.md](building-react-components.md) — dual-mode rendering rules that apply to all ui-utils components
|
|
127
|
+
- [app-manifest.md](app-manifest.md) — the `records` manifest block that drives tab generation
|
package/openapi.yaml
CHANGED
|
@@ -17134,6 +17134,10 @@ components:
|
|
|
17134
17134
|
type: string
|
|
17135
17135
|
allowAutoGenerateClaims:
|
|
17136
17136
|
type: boolean
|
|
17137
|
+
variants:
|
|
17138
|
+
type: boolean
|
|
17139
|
+
batches:
|
|
17140
|
+
type: boolean
|
|
17137
17141
|
defaultAuthKitId:
|
|
17138
17142
|
type: string
|
|
17139
17143
|
required:
|
|
@@ -17159,6 +17163,8 @@ components:
|
|
|
17159
17163
|
- supported
|
|
17160
17164
|
- roles
|
|
17161
17165
|
- shortId
|
|
17166
|
+
- variants
|
|
17167
|
+
- batches
|
|
17162
17168
|
- defaultAuthKitId
|
|
17163
17169
|
AppConfig:
|
|
17164
17170
|
type: object
|