@proveanything/smartlinks 1.11.0 → 1.11.2
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 +1 -1
- package/dist/docs/app-manifest.md +30 -22
- package/dist/docs/app-objects.md +17 -4
- package/dist/docs/mobile-admin-container.md +83 -20
- package/dist/docs/records-admin-pattern.md +305 -334
- package/dist/docs/ui-utils.md +257 -41
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mobile-admin/errors.d.ts +65 -0
- package/dist/mobile-admin/errors.js +72 -0
- package/dist/mobile-admin/types.d.ts +199 -0
- package/dist/mobile-admin/types.js +1 -0
- package/docs/API_SUMMARY.md +1 -1
- package/docs/app-manifest.md +30 -22
- package/docs/app-objects.md +17 -4
- package/docs/mobile-admin-container.md +83 -20
- package/docs/records-admin-pattern.md +305 -334
- package/docs/ui-utils.md +257 -41
- package/package.json +1 -1
- package/docs/scanner-container.md +0 -556
|
@@ -1,334 +1,305 @@
|
|
|
1
|
-
# SmartLinks Records
|
|
2
|
-
|
|
3
|
-
> Canonical guide for
|
|
4
|
-
>
|
|
5
|
-
> Audience: microapp developers (nutrition, allergy,
|
|
6
|
-
>
|
|
7
|
-
> Status: **standard
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
| Ingredients
|
|
20
|
-
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
variant
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
facet
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
| { kind: 'variant'; productId: string; variantId: string }
|
|
307
|
-
| { kind: 'batch'; productId: string; batchId: string }
|
|
308
|
-
| { kind: 'proof'; proofId: string }
|
|
309
|
-
| { kind: 'facet'; facetKey: string; valueKey: string };
|
|
310
|
-
|
|
311
|
-
export const buildRef = (p: ParsedRef): string => {
|
|
312
|
-
switch (p.kind) {
|
|
313
|
-
case 'universal': return '';
|
|
314
|
-
case 'product': return `product:${p.productId}`;
|
|
315
|
-
case 'variant': return `variant:${p.productId}:${p.variantId}`;
|
|
316
|
-
case 'batch': return `batch:${p.productId}:${p.batchId}`;
|
|
317
|
-
case 'proof': return `proof:${p.proofId}`;
|
|
318
|
-
case 'facet': return `facet:${p.facetKey}:${p.valueKey}`;
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
export const parseRef = (ref: string): ParsedRef | null => {
|
|
323
|
-
if (!ref) return { kind: 'universal' };
|
|
324
|
-
const [head, ...rest] = ref.split(':');
|
|
325
|
-
switch (head) {
|
|
326
|
-
case 'product': return rest.length === 1 ? { kind: 'product', productId: rest[0] } : null;
|
|
327
|
-
case 'variant': return rest.length === 2 ? { kind: 'variant', productId: rest[0], variantId: rest[1] } : null;
|
|
328
|
-
case 'batch': return rest.length === 2 ? { kind: 'batch', productId: rest[0], batchId: rest[1] } : null;
|
|
329
|
-
case 'proof': return rest.length === 1 ? { kind: 'proof', proofId: rest[0] } : null;
|
|
330
|
-
case 'facet': return rest.length >= 2 ? { kind: 'facet', facetKey: rest[0], valueKey: rest.slice(1).join(':') } : null;
|
|
331
|
-
default: return null;
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
```
|
|
1
|
+
# SmartLinks Records — Admin & Public Pattern
|
|
2
|
+
|
|
3
|
+
> Canonical guide for microapps that store **per-product**, **per-facet**, **per-variant**, **per-batch**, or **rule-targeted** data.
|
|
4
|
+
>
|
|
5
|
+
> Audience: microapp developers (ingredients, nutrition, allergy, FAQs, recipes, warranty, provenance, …).
|
|
6
|
+
>
|
|
7
|
+
> Status: **standard**. New apps MUST follow this contract; existing apps SHOULD migrate.
|
|
8
|
+
>
|
|
9
|
+
> SDK: `@proveanything/smartlinks` ≥ **1.11**, `@proveanything/smartlinks-utils-ui` ≥ **0.7.6**.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 0. TL;DR — pick your shape, then copy the snippet
|
|
14
|
+
|
|
15
|
+
Every records-based app fits into a 2×2:
|
|
16
|
+
|
|
17
|
+
| | **Singleton** (one record per scope) | **Collection** (many records per scope) |
|
|
18
|
+
| ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------- |
|
|
19
|
+
| **Best-match (one wins)**| Ingredients, nutrition, washing instructions | _(rare — usually you want all)_ |
|
|
20
|
+
| **All matches (aggregate)** | _(rare — usually you want best)_ | FAQs, recipes, SOPs, care tips, story cards |
|
|
21
|
+
|
|
22
|
+
That choice drives **three** things and nothing else:
|
|
23
|
+
|
|
24
|
+
1. **Manifest:** `cardinality: 'singleton' | 'collection'` and `allowFacetRules: boolean`.
|
|
25
|
+
2. **Admin (`<RecordsAdminShell>`):** pass `cardinality` + include `'rule'` in `scopes` if `allowFacetRules`.
|
|
26
|
+
3. **Public hook:** `useResolvedRecord` (best match) **or** `useCollectedRecords` / `useResolveAllRecords` (all matches).
|
|
27
|
+
|
|
28
|
+
If you only remember one rule: **never write your own resolver**. The shell, the hooks, and the SDK already agree on resolution order. Reimplementing it is how apps drift.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. The data model in one paragraph
|
|
33
|
+
|
|
34
|
+
A microapp owns a typed **records table** keyed by `(appId, recordType, id)`. Each `AppRecord` carries a `data` payload plus **either** a structured `scope` (anchored to a node in the chain) **or** a `facetRule` (matches products dynamically by their facets). The server resolves which record(s) apply to a given product context. There is no "global"; the top of the chain is **collection** — anything not explicitly scoped further applies to the whole collection.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import * as SL from '@proveanything/smartlinks';
|
|
38
|
+
|
|
39
|
+
await SL.app.records.upsert(collectionId, appId, {
|
|
40
|
+
recordType: 'ingredients',
|
|
41
|
+
scope: { productId: 'prod_abc', variantId: 'var_500ml' }, // server derives the ref
|
|
42
|
+
data: { /* domain payload */ },
|
|
43
|
+
}, /* admin */ true);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or, for a rule-targeted record:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
await SL.app.records.upsert(collectionId, appId, {
|
|
50
|
+
recordType: 'ingredients',
|
|
51
|
+
facetRule: {
|
|
52
|
+
all: [
|
|
53
|
+
{ facetKey: 'brand', anyOf: ['acme'] },
|
|
54
|
+
{ facetKey: 'category', anyOf: ['bread', 'pastry'] },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
data: { /* domain payload */ },
|
|
58
|
+
}, true);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`scope` and `facetRule` are **mutually exclusive on save**.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 2. Resolution order (one canonical chain)
|
|
66
|
+
|
|
67
|
+
The server walks **most-specific → least-specific** and stops at the first match (for best-match) or collects every match (for aggregate):
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
proof → batch → variant → product → rule(*) → facet(*) → collection
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `rule(*)` — facet-rule records are scored by **specificity** (number of clauses + number of constrained values). The most specific rule wins.
|
|
74
|
+
- `facet(*)` — legacy single-facet anchors, walked deterministically (alphabetical).
|
|
75
|
+
- `collection` — the top of the chain. **There is no "global" tier above collection.** A collection-level record is the catch-all for that collection.
|
|
76
|
+
|
|
77
|
+
The resolved value comes back tagged with `matchedAt: 'product' | 'rule' | 'facet' | …` so the UI can say things like _"Matched by rule: brand=Acme AND category=bread"_.
|
|
78
|
+
|
|
79
|
+
> ⚠️ Legacy `scope.facets[]` (colon-delimited single-facet refs) is deprecated and removed in SDK 1.12. Use `facetRule` for everything that isn't a one-off facet pin.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 3. Manifest declaration
|
|
84
|
+
|
|
85
|
+
Declare each record type once in `app.admin.json`. The shell and the platform read this to render the right scope tabs and disable the wrong affordances.
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"records": {
|
|
90
|
+
"ingredients": {
|
|
91
|
+
"label": "Ingredients",
|
|
92
|
+
"cardinality": "singleton",
|
|
93
|
+
"allowFacetRules": true,
|
|
94
|
+
"scopes": ["collection", "facet", "rule", "product", "variant", "batch"],
|
|
95
|
+
"defaultScope": "product"
|
|
96
|
+
},
|
|
97
|
+
"faq": {
|
|
98
|
+
"label": "FAQs",
|
|
99
|
+
"cardinality": "collection",
|
|
100
|
+
"allowFacetRules": true,
|
|
101
|
+
"scopes": ["collection", "rule", "product"],
|
|
102
|
+
"defaultScope": "collection"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| Field | Meaning |
|
|
109
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
110
|
+
| `cardinality` | `'singleton'` (one per scope, e.g. ingredients) or `'collection'` (many per scope, e.g. FAQs). Default `'singleton'`. |
|
|
111
|
+
| `allowFacetRules` | `true` to enable the `rule` scope tab + `<FacetRuleEditor>` in the shell. Default `false`. |
|
|
112
|
+
| `scopes` | Allowed scope kinds in **resolution order**. `'rule'` is a synthetic scope that holds rule-targeted records. |
|
|
113
|
+
| `defaultScope` | Where the "Create new" button lands. |
|
|
114
|
+
| `label` | Human-readable label used in headings and toasts. |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 4. Admin side — `<RecordsAdminShell>` (the only thing you should be writing)
|
|
119
|
+
|
|
120
|
+
The shell owns: scope tabs, browser pane, rule editor, save/discard, dirty navigation, inheritance markers, deletion, CSV, bulk apply, deep linking. **You only supply the editor for one record's `data`.**
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import * as SL from '@proveanything/smartlinks';
|
|
124
|
+
import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
125
|
+
|
|
126
|
+
<RecordsAdminShell<IngredientsConfig>
|
|
127
|
+
SL={SL}
|
|
128
|
+
collectionId={collectionId}
|
|
129
|
+
appId={appId}
|
|
130
|
+
recordType="ingredients"
|
|
131
|
+
label="Ingredients"
|
|
132
|
+
cardinality="singleton" // ← from manifest
|
|
133
|
+
scopes={['collection', 'facet', 'rule', 'product', 'variant', 'batch']}
|
|
134
|
+
defaultScope="product"
|
|
135
|
+
defaultData={() => emptyConfig()}
|
|
136
|
+
renderEditor={(ctx) => (
|
|
137
|
+
<IngredientsEditor
|
|
138
|
+
value={ctx.value}
|
|
139
|
+
onChange={ctx.onChange}
|
|
140
|
+
// For rule-targeted records, the shell hands you the live rule + setter:
|
|
141
|
+
facetRule={ctx.facetRule}
|
|
142
|
+
onFacetRuleChange={ctx.onFacetRuleChange}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### What the shell gives you for free
|
|
149
|
+
|
|
150
|
+
- **Scope tabs** including a **`Rule`** tab when `'rule'` is in `scopes`. Selecting it opens `<FacetRuleEditor>` above your editor — no extra wiring.
|
|
151
|
+
- **`EditorContext.facetRule` / `onFacetRuleChange`** for rule-scoped records, plus `canSave: false` until at least one clause has values (avoids server 500s).
|
|
152
|
+
- **Inheritance markers** — when editing a variant, the product baseline is shown; per-field "↩ Inherited" / "● Override" is rendered by the inheritance helpers.
|
|
153
|
+
- **Collection cardinality flow** — set `cardinality="collection"` and the shell turns the right pane into a list of items (table / cards / gallery) with `+ New` and per-item nav.
|
|
154
|
+
- **Telemetry** — `record.save`, `record.delete`, `scope.change`, `csv.import`, `bulk.apply`, `item.create`, etc. via `onTelemetry`.
|
|
155
|
+
|
|
156
|
+
### Standalone rule editor
|
|
157
|
+
|
|
158
|
+
If you need a rule editor outside the shell (e.g. on a settings page):
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import { FacetRuleEditor } from '@proveanything/smartlinks-utils-ui/facet-rule-editor';
|
|
162
|
+
|
|
163
|
+
<FacetRuleEditor
|
|
164
|
+
value={rule}
|
|
165
|
+
onChange={setRule}
|
|
166
|
+
collectionId={collectionId} // lazy-fetches facets via SL.facets.publicList
|
|
167
|
+
preview={rulePreview} // optional — wire from useRulePreview
|
|
168
|
+
/>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 5. Public side — pick the right hook (this is where apps go wrong)
|
|
174
|
+
|
|
175
|
+
There is exactly **one decision** to make on the public side, and it follows from the manifest's `cardinality`:
|
|
176
|
+
|
|
177
|
+
### 5a. Singleton → `useResolvedRecord` (best match wins)
|
|
178
|
+
|
|
179
|
+
Use this when the app shows **one** answer for the current product (ingredients, nutrition, washing instructions, warranty terms).
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import * as SL from '@proveanything/smartlinks';
|
|
183
|
+
import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
184
|
+
|
|
185
|
+
const { data, source, sourceRef, matchedAt, matchedRule, isLoading } =
|
|
186
|
+
useResolvedRecord<IngredientsConfig>({
|
|
187
|
+
SL,
|
|
188
|
+
appId,
|
|
189
|
+
collectionId,
|
|
190
|
+
recordType: 'ingredients',
|
|
191
|
+
productId,
|
|
192
|
+
variantId, // optional
|
|
193
|
+
batchId, // optional
|
|
194
|
+
proofId, // optional
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The resolver walks `proof → batch → variant → product → rule → facet → collection` and returns the **first match**, plus `matchedAt` so you can render _"From the product record"_ vs _"Matched by rule"_ badges if useful.
|
|
199
|
+
|
|
200
|
+
> Wrap this once in your app (e.g. `useResolvedIngredientSet`) so the rest of the codebase reads `{ data, isLoading }` and never sees the resolver.
|
|
201
|
+
|
|
202
|
+
### 5b. Collection → `useCollectedRecords` (every match, ordered)
|
|
203
|
+
|
|
204
|
+
Use this when the app shows **many** answers aggregated across the chain (FAQs, recipes, SOPs, care tips). Most-specific first by default; pass `sort` to override.
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
import { useCollectedRecords } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
208
|
+
|
|
209
|
+
const { items, isLoading } = useCollectedRecords<FaqEntry>({
|
|
210
|
+
SL,
|
|
211
|
+
appId,
|
|
212
|
+
collectionId,
|
|
213
|
+
recordType: 'faq',
|
|
214
|
+
productId,
|
|
215
|
+
// sort: { kind: 'field', field: 'order', direction: 'asc' },
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// items: CollectedRecord<FaqEntry>[] — each has { data, scope, ref, depth }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 5c. Multi-type aggregate → `useResolveAllRecords`
|
|
222
|
+
|
|
223
|
+
When you need every record of every type that applies to a context (rare; mostly executors and SEO surfaces).
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
import { useResolveAllRecords } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
227
|
+
|
|
228
|
+
const { entries, isLoading } = useResolveAllRecords({
|
|
229
|
+
SL, collectionId, appId,
|
|
230
|
+
context: { productId, facets: { brand: 'acme', category: ['bread'] } },
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Common mistakes (do not do these)
|
|
235
|
+
|
|
236
|
+
| ❌ Anti-pattern | ✅ Do this instead |
|
|
237
|
+
| ---------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
238
|
+
| Calling `SL.app.records.list()` and filtering client-side | `useResolvedRecord` (singleton) or `useCollectedRecords` (collection). The server already knows the chain. |
|
|
239
|
+
| Walking the chain by hand with multiple `getConfig` calls | One hook call. The resolver is tested, cached, and includes rules. |
|
|
240
|
+
| Treating `facet:key:value` refs as the rule mechanism | Use `facetRule` (`{ all: [{ facetKey, anyOf: [...] }] }`). Multi-condition, scored by specificity. |
|
|
241
|
+
| Reading `matchedAt === 'global'` | There is no `'global'`. The top of the chain is `'collection'`. |
|
|
242
|
+
| Building your own `<FacetRuleEditor>` | Use the one from `@proveanything/smartlinks-utils-ui/facet-rule-editor`. |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 6. Reference: the `EditorContext` your `renderEditor` receives
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
interface EditorContext<TData> {
|
|
250
|
+
value: TData;
|
|
251
|
+
onChange: (next: TData) => void;
|
|
252
|
+
source: 'self' | 'inherited' | 'empty';
|
|
253
|
+
recordId?: string;
|
|
254
|
+
parentValue?: TData | null;
|
|
255
|
+
scope: ParsedRef; // { kind: 'product' | 'rule' | …, productId?, … }
|
|
256
|
+
|
|
257
|
+
// Save lifecycle
|
|
258
|
+
isDirty: boolean;
|
|
259
|
+
isSaving?: boolean;
|
|
260
|
+
saveError?: unknown | null;
|
|
261
|
+
canSave?: boolean; // shell flips to false on empty rules
|
|
262
|
+
cannotSaveReason?: string;
|
|
263
|
+
save: () => Promise<void>;
|
|
264
|
+
reset: () => void;
|
|
265
|
+
|
|
266
|
+
// Deletion
|
|
267
|
+
remove: () => Promise<void>;
|
|
268
|
+
canRemove: boolean;
|
|
269
|
+
|
|
270
|
+
// Rule scope only
|
|
271
|
+
facetRule?: FacetRule | null;
|
|
272
|
+
onFacetRuleChange?: (next: FacetRule | null) => void;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 7. Migration checklist (existing apps)
|
|
279
|
+
|
|
280
|
+
1. **Update SDKs:** `@proveanything/smartlinks@^1.11`, `@proveanything/smartlinks-utils-ui@^0.7.6`.
|
|
281
|
+
2. **Add `cardinality` and `allowFacetRules`** to every entry under `records` in `app.admin.json`.
|
|
282
|
+
3. **Add `'rule'` (and `'collection'` if missing) to `scopes`** wherever `allowFacetRules: true`.
|
|
283
|
+
4. **Pass `cardinality`** to `<RecordsAdminShell>`.
|
|
284
|
+
5. **Replace any handwritten chain walking** with `useResolvedRecord` (singleton) or `useCollectedRecords` (collection).
|
|
285
|
+
6. **Delete any code that constructs `facet:key:value` refs** for matching. Use `facetRule` via the shell or `<FacetRuleEditor>`.
|
|
286
|
+
7. **Search for the word "global"** in your code/docs and rename to "collection" — this is the most common source of confusion.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 8. Where the canonical exports live
|
|
291
|
+
|
|
292
|
+
| Need | Import from |
|
|
293
|
+
| ----------------------------- | ---------------------------------------------------------------------- |
|
|
294
|
+
| Admin shell | `@proveanything/smartlinks-utils-ui/records-admin` → `RecordsAdminShell` |
|
|
295
|
+
| Standalone rule editor | `@proveanything/smartlinks-utils-ui/facet-rule-editor` → `FacetRuleEditor` |
|
|
296
|
+
| Conditions editor (non-facet) | `@proveanything/smartlinks-utils-ui/conditions-editor` → `ConditionsEditor` |
|
|
297
|
+
| Best-match resolver hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolvedRecord` |
|
|
298
|
+
| All-matches hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useCollectedRecords` |
|
|
299
|
+
| Multi-type aggregate hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolveAllRecords` |
|
|
300
|
+
| Rule preview ("matches N") | `@proveanything/smartlinks-utils-ui/records-admin` → `useRulePreview` |
|
|
301
|
+
| Server-side record CRUD | `@proveanything/smartlinks` → `SL.app.records.{upsert, list, remove, match, resolveAll}` |
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
_End of doc. If anything below the SDK contradicts this file, this file wins — open a PR against the SDK to bring the two back into sync._
|