@proveanything/smartlinks 1.11.7 → 1.11.9
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 +2 -2
- package/dist/docs/app-objects.md +2 -2
- package/{docs/records-admin-pattern.md → dist/docs/app-records-pattern.md} +98 -63
- package/dist/docs/overview.md +1 -1
- package/dist/docs/ui-utils.md +3 -3
- package/docs/API_SUMMARY.md +1 -1
- package/docs/app-manifest.md +2 -2
- package/docs/app-objects.md +2 -2
- package/docs/app-records-pattern.md +340 -0
- package/docs/overview.md +1 -1
- package/docs/ui-utils.md +3 -3
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -270,7 +270,7 @@ See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-so
|
|
|
270
270
|
|
|
271
271
|
#### `records`
|
|
272
272
|
|
|
273
|
-
Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [Records
|
|
273
|
+
Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [App Records Pattern](app-records-pattern.md). Omit if the app does not use scoped records.
|
|
274
274
|
|
|
275
275
|
The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui` read this block to render the right scope tabs, rule editor, and cardinality-appropriate right pane.
|
|
276
276
|
|
|
@@ -294,7 +294,7 @@ The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils
|
|
|
294
294
|
| `scopes` | string[] | — | Allowed scope kinds in resolution order. Valid values: `"collection"`, `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"rule"`. `'rule'` is a synthetic scope holding `facetRule`-targeted records. `'collection'` replaces the legacy empty-ref catch-all — **there is no `'global'` scope**. |
|
|
295
295
|
| `defaultScope` | string | — | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
|
|
296
296
|
|
|
297
|
-
An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [records-
|
|
297
|
+
An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [app-records-pattern.md](app-records-pattern.md) for the full admin + public pattern.
|
|
298
298
|
|
|
299
299
|
#### `executor`
|
|
300
300
|
|
package/dist/docs/app-objects.md
CHANGED
|
@@ -445,7 +445,7 @@ const productComments = await app.threads.list(collectionId, appId, {
|
|
|
445
445
|
- **Usage logs** — record product usage metrics
|
|
446
446
|
- **Audit trails** — immutable logs of actions
|
|
447
447
|
- **Loyalty points** — track points earned/redeemed
|
|
448
|
-
- **Per-product / per-facet configuration** — scoped data that varies by product axis (see [records-
|
|
448
|
+
- **Per-product / per-facet configuration** — scoped data that varies by product axis (see [app-records-pattern.md](app-records-pattern.md))
|
|
449
449
|
|
|
450
450
|
### Key Features
|
|
451
451
|
|
|
@@ -526,7 +526,7 @@ proof → batch → variant → product → rule(*) → facet(*) →
|
|
|
526
526
|
- `facet(*)` — legacy single-facet anchors, walked alphabetically. Prefer `facetRule` for new work.
|
|
527
527
|
- `collection` — the top of the chain and the catch-all for any record with no anchor fields. **There is no `'global'` tier above collection.**
|
|
528
528
|
|
|
529
|
-
For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [records-
|
|
529
|
+
For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [app-records-pattern.md §2](app-records-pattern.md#2-resolution-order-one-canonical-chain) for the full guide.
|
|
530
530
|
|
|
531
531
|
### Singleton Cardinality
|
|
532
532
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SmartLinks Records
|
|
1
|
+
# SmartLinks App Records Pattern
|
|
2
2
|
|
|
3
3
|
> Canonical guide for microapps that store **per-product**, **per-facet**, **per-variant**, **per-batch**, or **rule-targeted** data.
|
|
4
4
|
>
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
>
|
|
7
7
|
> Status: **standard**. New apps MUST follow this contract; existing apps SHOULD migrate.
|
|
8
8
|
>
|
|
9
|
-
> SDK: `@proveanything/smartlinks` ≥ **1.11
|
|
9
|
+
> SDK: `@proveanything/smartlinks` ≥ **1.11**.
|
|
10
|
+
> Admin shell (React only): `@proveanything/smartlinks-utils-ui` ≥ **0.7.6** — required for the admin side if using the React shell; not needed in public widgets.
|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
|
|
@@ -22,10 +23,10 @@ Every records-based app fits into a 2×2:
|
|
|
22
23
|
That choice drives **three** things and nothing else:
|
|
23
24
|
|
|
24
25
|
1. **Manifest:** `cardinality: 'singleton' | 'collection'` and `allowFacetRules: boolean`.
|
|
25
|
-
2. **Admin
|
|
26
|
-
3. **Public
|
|
26
|
+
2. **Admin:** use `<RecordsAdminShell>` (React) or call the admin SDK functions directly. Pass `cardinality` + include `'rule'` in `scopes` if `allowFacetRules`.
|
|
27
|
+
3. **Public widget:** call `app.records.match()` (best match / singleton) or `app.records.resolveAll()` (all matches / collection). These are plain SDK calls with no framework dependency.
|
|
27
28
|
|
|
28
|
-
If you only remember one rule: **never write your own
|
|
29
|
+
If you only remember one rule: **never write your own resolution loop**. The server already walks the chain correctly — calling `match()` or `resolveAll()` is the entire public-side implementation.
|
|
29
30
|
|
|
30
31
|
---
|
|
31
32
|
|
|
@@ -115,7 +116,11 @@ Declare each record type once in `app.admin.json`. The shell and the platform re
|
|
|
115
116
|
|
|
116
117
|
---
|
|
117
118
|
|
|
118
|
-
## 4. Admin side
|
|
119
|
+
## 4. Admin side
|
|
120
|
+
|
|
121
|
+
> The admin shell and rule editor are part of `@proveanything/smartlinks-utils-ui`, which is a **React-only library**. It is only needed in admin dashboards — never import it in a public widget.
|
|
122
|
+
|
|
123
|
+
### `<RecordsAdminShell>` (React)
|
|
119
124
|
|
|
120
125
|
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
126
|
|
|
@@ -170,64 +175,78 @@ import { FacetRuleEditor } from '@proveanything/smartlinks-utils-ui/facet-rule-e
|
|
|
170
175
|
|
|
171
176
|
---
|
|
172
177
|
|
|
173
|
-
## 5. Public side
|
|
178
|
+
## 5. Public side
|
|
174
179
|
|
|
175
|
-
|
|
180
|
+
> **Do not import `@proveanything/smartlinks-utils-ui` in a public widget.** It is a React admin library. Public widgets only need `@proveanything/smartlinks`.
|
|
176
181
|
|
|
177
|
-
|
|
182
|
+
The SDK is framework-agnostic. Public widgets call two endpoints depending on cardinality:
|
|
178
183
|
|
|
179
|
-
|
|
184
|
+
| Cardinality | Call | What it does |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| **Singleton** (one answer) | `app.records.match()` | Server walks the chain, returns the best-matching record |
|
|
187
|
+
| **Collection** (all answers) | `app.records.resolveAll()` | Server walks the chain, returns every matching record |
|
|
180
188
|
|
|
181
|
-
|
|
189
|
+
Neither call requires React or any other framework — wrap them in whatever async pattern your widget uses.
|
|
190
|
+
|
|
191
|
+
> **Admin vs public — the rule is simple:**
|
|
192
|
+
>
|
|
193
|
+
> | Function | Public widget | Admin dashboard |
|
|
194
|
+
> |---|---|---|
|
|
195
|
+
> | `app.records.create(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
196
|
+
> | `app.records.list(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
197
|
+
> | `app.records.get(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
198
|
+
> | `app.records.update(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
199
|
+
> | `app.records.remove(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
200
|
+
> | `app.records.aggregate(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
201
|
+
> | `app.records.match(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
202
|
+
> | `app.records.resolveAll(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
203
|
+
> | `app.records.upsert()` | ❌ admin only — no public path | ✅ |
|
|
204
|
+
> | `app.records.bulkUpsert()` | ❌ admin only — no public path | ✅ |
|
|
205
|
+
> | `app.records.bulkDelete()` | ❌ admin only — no public path | ✅ |
|
|
206
|
+
> | `app.records.restore()` | ❌ admin only — no public path | ✅ |
|
|
207
|
+
> | `app.records.previewRule()` | ❌ admin only — no public path | ✅ |
|
|
208
|
+
|
|
209
|
+
### 5a. Singleton — `app.records.match()` (best match wins)
|
|
210
|
+
|
|
211
|
+
Use when the widget shows **one** answer for the current product (ingredients, nutrition, warranty terms, washing instructions).
|
|
212
|
+
|
|
213
|
+
```ts
|
|
182
214
|
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
215
|
|
|
198
|
-
|
|
216
|
+
const result = await SL.app.records.match(collectionId, appId, {
|
|
217
|
+
target: { productId, variantId, batchId }, // pass whatever context you have
|
|
218
|
+
strategy: 'best',
|
|
219
|
+
recordType: 'ingredients',
|
|
220
|
+
});
|
|
199
221
|
|
|
200
|
-
|
|
222
|
+
// result.best.ingredients → the single highest-specificity record
|
|
223
|
+
// result.best.ingredients.matchedAt → 'product' | 'rule' | 'facet' | 'collection' | …
|
|
224
|
+
```
|
|
201
225
|
|
|
202
|
-
|
|
226
|
+
The server walks `proof → batch → variant → product → rule → facet → collection` and returns the first match.
|
|
203
227
|
|
|
204
|
-
|
|
228
|
+
### 5b. Collection — `app.records.resolveAll()` (every match, ordered)
|
|
205
229
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
collectionId,
|
|
213
|
-
recordType: 'faq',
|
|
214
|
-
productId,
|
|
215
|
-
// sort: { kind: 'field', field: 'order', direction: 'asc' },
|
|
230
|
+
Use when the widget shows **many** answers across the chain (FAQs, recipes, care tips, SOPs).
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
234
|
+
target: { productId },
|
|
235
|
+
recordTypes: ['faq'],
|
|
216
236
|
});
|
|
217
237
|
|
|
218
|
-
//
|
|
238
|
+
// result.records → AppRecord[] sorted most-specific first
|
|
239
|
+
// each record has .matchedAt, .data, .scope
|
|
219
240
|
```
|
|
220
241
|
|
|
221
|
-
### 5c. Multi-type
|
|
242
|
+
### 5c. Multi-type — `app.records.resolveAll()` with multiple record types
|
|
222
243
|
|
|
223
|
-
When you need
|
|
224
|
-
|
|
225
|
-
```tsx
|
|
226
|
-
import { useResolveAllRecords } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
244
|
+
When you need records of several types in one call (rare; executors, SEO surfaces):
|
|
227
245
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
246
|
+
```ts
|
|
247
|
+
const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
248
|
+
target: { productId, facets: { brand: 'acme' } },
|
|
249
|
+
recordTypes: ['ingredients', 'nutrition', 'allergens'],
|
|
231
250
|
});
|
|
232
251
|
```
|
|
233
252
|
|
|
@@ -235,11 +254,15 @@ const { entries, isLoading } = useResolveAllRecords({
|
|
|
235
254
|
|
|
236
255
|
| ❌ Anti-pattern | ✅ Do this instead |
|
|
237
256
|
| ---------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
238
|
-
|
|
|
239
|
-
|
|
|
257
|
+
| Importing anything from `@proveanything/smartlinks-utils-ui` in a public widget | That package is React-only and admin-only. Public widgets only use `@proveanything/smartlinks`. |
|
|
258
|
+
| Calling `SL.app.records.list()` and filtering client-side | `app.records.match()` (singleton) or `app.records.resolveAll()` (collection). The server walks the chain. |
|
|
259
|
+
| Calling `SL.app.records.list(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
|
|
260
|
+
| Calling `SL.app.records.match(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
|
|
261
|
+
| Calling `SL.app.records.resolveAll(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
|
|
262
|
+
| Calling `upsert`, `bulkUpsert`, `bulkDelete`, or `previewRule` from widget code | Those are admin-only. Widget code reads data; it never writes records. |
|
|
263
|
+
| Walking the chain by hand with multiple `get` / `list` calls | One `match()` or `resolveAll()` call. The server handles the resolution order including rules. |
|
|
240
264
|
| Treating `facet:key:value` refs as the rule mechanism | Use `facetRule` (`{ all: [{ facetKey, anyOf: [...] }] }`). Multi-condition, scored by specificity. |
|
|
241
265
|
| 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
266
|
|
|
244
267
|
---
|
|
245
268
|
|
|
@@ -281,25 +304,37 @@ interface EditorContext<TData> {
|
|
|
281
304
|
2. **Add `cardinality` and `allowFacetRules`** to every entry under `records` in `app.admin.json`.
|
|
282
305
|
3. **Add `'rule'` (and `'collection'` if missing) to `scopes`** wherever `allowFacetRules: true`.
|
|
283
306
|
4. **Pass `cardinality`** to `<RecordsAdminShell>`.
|
|
284
|
-
5. **Replace any handwritten chain walking** with `
|
|
285
|
-
6. **Delete any code that constructs `facet:key:value` refs** for matching. Use `facetRule` via the shell or `<FacetRuleEditor
|
|
307
|
+
5. **Replace any handwritten chain walking** with `app.records.match()` (singleton) or `app.records.resolveAll()` (collection). If you are using React, the `useResolvedRecord` / `useCollectedRecords` hooks from `@proveanything/smartlinks-utils-ui` wrap these calls — but they are **admin-side React helpers**, not for public widgets.
|
|
308
|
+
6. **Delete any code that constructs `facet:key:value` refs** for matching. Use `facetRule` via the shell or `<FacetRuleEditor>` (React admin) or pass `facetRule` directly in `upsert()` calls.
|
|
286
309
|
7. **Search for the word "global"** in your code/docs and rename to "collection" — this is the most common source of confusion.
|
|
287
310
|
|
|
288
311
|
---
|
|
289
312
|
|
|
290
313
|
## 8. Where the canonical exports live
|
|
291
314
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
|
295
|
-
|
|
|
315
|
+
### Public widgets (any framework)
|
|
316
|
+
|
|
317
|
+
| Need | Import from |
|
|
318
|
+
| ---- | ----------- |
|
|
319
|
+
| Best-match resolution (singleton) | `@proveanything/smartlinks` → `SL.app.records.match()` |
|
|
320
|
+
| All-matches resolution (collection) | `@proveanything/smartlinks` → `SL.app.records.resolveAll()` |
|
|
321
|
+
| Record CRUD (public path) | `@proveanything/smartlinks` → `SL.app.records.{list, get, create, update, remove, aggregate}` |
|
|
322
|
+
|
|
323
|
+
### Admin dashboards (React)
|
|
324
|
+
|
|
325
|
+
> All of the following are from `@proveanything/smartlinks-utils-ui`, a **React-only** package. Do not use in public widgets.
|
|
326
|
+
|
|
327
|
+
| Need | Import from |
|
|
328
|
+
| ---- | ----------- |
|
|
329
|
+
| Admin shell | `@proveanything/smartlinks-utils-ui/records-admin` → `RecordsAdminShell` |
|
|
330
|
+
| Standalone rule editor | `@proveanything/smartlinks-utils-ui/facet-rule-editor` → `FacetRuleEditor` |
|
|
296
331
|
| Conditions editor (non-facet) | `@proveanything/smartlinks-utils-ui/conditions-editor` → `ConditionsEditor` |
|
|
297
|
-
| Best-match
|
|
298
|
-
| All-matches hook
|
|
299
|
-
| Multi-type
|
|
300
|
-
| Rule preview
|
|
301
|
-
|
|
|
332
|
+
| Best-match hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolvedRecord` |
|
|
333
|
+
| All-matches hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useCollectedRecords` |
|
|
334
|
+
| Multi-type hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolveAllRecords` |
|
|
335
|
+
| Rule preview hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useRulePreview` |
|
|
336
|
+
| Admin record CRUD (admin path) | `@proveanything/smartlinks` → `SL.app.records.{upsert, bulkUpsert, bulkDelete, restore, previewRule}` |
|
|
302
337
|
|
|
303
338
|
---
|
|
304
339
|
|
|
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._
|
|
340
|
+
_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._
|
package/dist/docs/overview.md
CHANGED
|
@@ -70,7 +70,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
70
70
|
| **AI Guide Template** | `docs/ai-guide-template.md` | Template for creating `public/ai-guide.md` — customise per app |
|
|
71
71
|
| **Forms** | `docs/forms.md` | Form definitions, schema-driven rendering, submission patterns |
|
|
72
72
|
| **Auth Kit** | `docs/auth-kit.md` | End-user sign-in: email/password, magic links, phone OTP, Google OAuth |
|
|
73
|
-
| **Records
|
|
73
|
+
| **App Records Pattern** | `docs/app-records-pattern.md` | Standard pattern for per-product/facet/variant/batch admin + public widget UIs |
|
|
74
74
|
| **UI Utils** | `docs/ui-utils.md` | `@proveanything/smartlinks-utils-ui` — React shells, hooks, and primitives for records-based apps |
|
|
75
75
|
|
|
76
76
|
---
|
package/dist/docs/ui-utils.md
CHANGED
|
@@ -19,7 +19,7 @@ translate SDK data into consistent admin interfaces.
|
|
|
19
19
|
|
|
20
20
|
**When do you need it?**
|
|
21
21
|
|
|
22
|
-
- You are building an admin UI for a records-based microapp (see [records-
|
|
22
|
+
- You are building an admin UI for a records-based microapp (see [app-records-pattern.md](app-records-pattern.md))
|
|
23
23
|
- You need a media asset picker, icon picker, or font picker in an admin panel
|
|
24
24
|
- You need a recursive rule/conditions editor for targeting or audience logic
|
|
25
25
|
- You want the standard inheritance/override editor for scoped records
|
|
@@ -326,7 +326,7 @@ Props:
|
|
|
326
326
|
|
|
327
327
|
Free-text facet entry is **not** supported — admins must pick from defined facets.
|
|
328
328
|
|
|
329
|
-
See [records-
|
|
329
|
+
See [app-records-pattern.md §4](app-records-pattern.md#4-admin-side----recordsadminshell-the-only-thing-you-should-be-writing) for the standalone usage example.
|
|
330
330
|
|
|
331
331
|
---
|
|
332
332
|
|
|
@@ -505,6 +505,6 @@ your microapp ← domain logic and custom forms
|
|
|
505
505
|
|
|
506
506
|
## Further reading
|
|
507
507
|
|
|
508
|
-
- [records-
|
|
508
|
+
- [app-records-pattern.md](app-records-pattern.md) — the data contract that `RecordsAdminShell` implements
|
|
509
509
|
- [building-react-components.md](building-react-components.md) — dual-mode rendering rules that apply to all components
|
|
510
510
|
- [app-manifest.md](app-manifest.md) — the `records` manifest block that drives tab generation
|
package/docs/API_SUMMARY.md
CHANGED
package/docs/app-manifest.md
CHANGED
|
@@ -270,7 +270,7 @@ See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-so
|
|
|
270
270
|
|
|
271
271
|
#### `records`
|
|
272
272
|
|
|
273
|
-
Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [Records
|
|
273
|
+
Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [App Records Pattern](app-records-pattern.md). Omit if the app does not use scoped records.
|
|
274
274
|
|
|
275
275
|
The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui` read this block to render the right scope tabs, rule editor, and cardinality-appropriate right pane.
|
|
276
276
|
|
|
@@ -294,7 +294,7 @@ The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils
|
|
|
294
294
|
| `scopes` | string[] | — | Allowed scope kinds in resolution order. Valid values: `"collection"`, `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"rule"`. `'rule'` is a synthetic scope holding `facetRule`-targeted records. `'collection'` replaces the legacy empty-ref catch-all — **there is no `'global'` scope**. |
|
|
295
295
|
| `defaultScope` | string | — | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
|
|
296
296
|
|
|
297
|
-
An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [records-
|
|
297
|
+
An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [app-records-pattern.md](app-records-pattern.md) for the full admin + public pattern.
|
|
298
298
|
|
|
299
299
|
#### `executor`
|
|
300
300
|
|
package/docs/app-objects.md
CHANGED
|
@@ -445,7 +445,7 @@ const productComments = await app.threads.list(collectionId, appId, {
|
|
|
445
445
|
- **Usage logs** — record product usage metrics
|
|
446
446
|
- **Audit trails** — immutable logs of actions
|
|
447
447
|
- **Loyalty points** — track points earned/redeemed
|
|
448
|
-
- **Per-product / per-facet configuration** — scoped data that varies by product axis (see [records-
|
|
448
|
+
- **Per-product / per-facet configuration** — scoped data that varies by product axis (see [app-records-pattern.md](app-records-pattern.md))
|
|
449
449
|
|
|
450
450
|
### Key Features
|
|
451
451
|
|
|
@@ -526,7 +526,7 @@ proof → batch → variant → product → rule(*) → facet(*) →
|
|
|
526
526
|
- `facet(*)` — legacy single-facet anchors, walked alphabetically. Prefer `facetRule` for new work.
|
|
527
527
|
- `collection` — the top of the chain and the catch-all for any record with no anchor fields. **There is no `'global'` tier above collection.**
|
|
528
528
|
|
|
529
|
-
For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [records-
|
|
529
|
+
For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [app-records-pattern.md §2](app-records-pattern.md#2-resolution-order-one-canonical-chain) for the full guide.
|
|
530
530
|
|
|
531
531
|
### Singleton Cardinality
|
|
532
532
|
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# SmartLinks App Records 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**.
|
|
10
|
+
> Admin shell (React only): `@proveanything/smartlinks-utils-ui` ≥ **0.7.6** — required for the admin side if using the React shell; not needed in public widgets.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 0. TL;DR — pick your shape, then copy the snippet
|
|
15
|
+
|
|
16
|
+
Every records-based app fits into a 2×2:
|
|
17
|
+
|
|
18
|
+
| | **Singleton** (one record per scope) | **Collection** (many records per scope) |
|
|
19
|
+
| ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------- |
|
|
20
|
+
| **Best-match (one wins)**| Ingredients, nutrition, washing instructions | _(rare — usually you want all)_ |
|
|
21
|
+
| **All matches (aggregate)** | _(rare — usually you want best)_ | FAQs, recipes, SOPs, care tips, story cards |
|
|
22
|
+
|
|
23
|
+
That choice drives **three** things and nothing else:
|
|
24
|
+
|
|
25
|
+
1. **Manifest:** `cardinality: 'singleton' | 'collection'` and `allowFacetRules: boolean`.
|
|
26
|
+
2. **Admin:** use `<RecordsAdminShell>` (React) or call the admin SDK functions directly. Pass `cardinality` + include `'rule'` in `scopes` if `allowFacetRules`.
|
|
27
|
+
3. **Public widget:** call `app.records.match()` (best match / singleton) or `app.records.resolveAll()` (all matches / collection). These are plain SDK calls with no framework dependency.
|
|
28
|
+
|
|
29
|
+
If you only remember one rule: **never write your own resolution loop**. The server already walks the chain correctly — calling `match()` or `resolveAll()` is the entire public-side implementation.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. The data model in one paragraph
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import * as SL from '@proveanything/smartlinks';
|
|
39
|
+
|
|
40
|
+
await SL.app.records.upsert(collectionId, appId, {
|
|
41
|
+
recordType: 'ingredients',
|
|
42
|
+
scope: { productId: 'prod_abc', variantId: 'var_500ml' }, // server derives the ref
|
|
43
|
+
data: { /* domain payload */ },
|
|
44
|
+
}, /* admin */ true);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or, for a rule-targeted record:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
await SL.app.records.upsert(collectionId, appId, {
|
|
51
|
+
recordType: 'ingredients',
|
|
52
|
+
facetRule: {
|
|
53
|
+
all: [
|
|
54
|
+
{ facetKey: 'brand', anyOf: ['acme'] },
|
|
55
|
+
{ facetKey: 'category', anyOf: ['bread', 'pastry'] },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
data: { /* domain payload */ },
|
|
59
|
+
}, true);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`scope` and `facetRule` are **mutually exclusive on save**.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 2. Resolution order (one canonical chain)
|
|
67
|
+
|
|
68
|
+
The server walks **most-specific → least-specific** and stops at the first match (for best-match) or collects every match (for aggregate):
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
proof → batch → variant → product → rule(*) → facet(*) → collection
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `rule(*)` — facet-rule records are scored by **specificity** (number of clauses + number of constrained values). The most specific rule wins.
|
|
75
|
+
- `facet(*)` — legacy single-facet anchors, walked deterministically (alphabetical).
|
|
76
|
+
- `collection` — the top of the chain. **There is no "global" tier above collection.** A collection-level record is the catch-all for that collection.
|
|
77
|
+
|
|
78
|
+
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"_.
|
|
79
|
+
|
|
80
|
+
> ⚠️ 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.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 3. Manifest declaration
|
|
85
|
+
|
|
86
|
+
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.
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"records": {
|
|
91
|
+
"ingredients": {
|
|
92
|
+
"label": "Ingredients",
|
|
93
|
+
"cardinality": "singleton",
|
|
94
|
+
"allowFacetRules": true,
|
|
95
|
+
"scopes": ["collection", "facet", "rule", "product", "variant", "batch"],
|
|
96
|
+
"defaultScope": "product"
|
|
97
|
+
},
|
|
98
|
+
"faq": {
|
|
99
|
+
"label": "FAQs",
|
|
100
|
+
"cardinality": "collection",
|
|
101
|
+
"allowFacetRules": true,
|
|
102
|
+
"scopes": ["collection", "rule", "product"],
|
|
103
|
+
"defaultScope": "collection"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Field | Meaning |
|
|
110
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
111
|
+
| `cardinality` | `'singleton'` (one per scope, e.g. ingredients) or `'collection'` (many per scope, e.g. FAQs). Default `'singleton'`. |
|
|
112
|
+
| `allowFacetRules` | `true` to enable the `rule` scope tab + `<FacetRuleEditor>` in the shell. Default `false`. |
|
|
113
|
+
| `scopes` | Allowed scope kinds in **resolution order**. `'rule'` is a synthetic scope that holds rule-targeted records. |
|
|
114
|
+
| `defaultScope` | Where the "Create new" button lands. |
|
|
115
|
+
| `label` | Human-readable label used in headings and toasts. |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 4. Admin side
|
|
120
|
+
|
|
121
|
+
> The admin shell and rule editor are part of `@proveanything/smartlinks-utils-ui`, which is a **React-only library**. It is only needed in admin dashboards — never import it in a public widget.
|
|
122
|
+
|
|
123
|
+
### `<RecordsAdminShell>` (React)
|
|
124
|
+
|
|
125
|
+
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`.**
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import * as SL from '@proveanything/smartlinks';
|
|
129
|
+
import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
130
|
+
|
|
131
|
+
<RecordsAdminShell<IngredientsConfig>
|
|
132
|
+
SL={SL}
|
|
133
|
+
collectionId={collectionId}
|
|
134
|
+
appId={appId}
|
|
135
|
+
recordType="ingredients"
|
|
136
|
+
label="Ingredients"
|
|
137
|
+
cardinality="singleton" // ← from manifest
|
|
138
|
+
scopes={['collection', 'facet', 'rule', 'product', 'variant', 'batch']}
|
|
139
|
+
defaultScope="product"
|
|
140
|
+
defaultData={() => emptyConfig()}
|
|
141
|
+
renderEditor={(ctx) => (
|
|
142
|
+
<IngredientsEditor
|
|
143
|
+
value={ctx.value}
|
|
144
|
+
onChange={ctx.onChange}
|
|
145
|
+
// For rule-targeted records, the shell hands you the live rule + setter:
|
|
146
|
+
facetRule={ctx.facetRule}
|
|
147
|
+
onFacetRuleChange={ctx.onFacetRuleChange}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
/>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### What the shell gives you for free
|
|
154
|
+
|
|
155
|
+
- **Scope tabs** including a **`Rule`** tab when `'rule'` is in `scopes`. Selecting it opens `<FacetRuleEditor>` above your editor — no extra wiring.
|
|
156
|
+
- **`EditorContext.facetRule` / `onFacetRuleChange`** for rule-scoped records, plus `canSave: false` until at least one clause has values (avoids server 500s).
|
|
157
|
+
- **Inheritance markers** — when editing a variant, the product baseline is shown; per-field "↩ Inherited" / "● Override" is rendered by the inheritance helpers.
|
|
158
|
+
- **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.
|
|
159
|
+
- **Telemetry** — `record.save`, `record.delete`, `scope.change`, `csv.import`, `bulk.apply`, `item.create`, etc. via `onTelemetry`.
|
|
160
|
+
|
|
161
|
+
### Standalone rule editor
|
|
162
|
+
|
|
163
|
+
If you need a rule editor outside the shell (e.g. on a settings page):
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import { FacetRuleEditor } from '@proveanything/smartlinks-utils-ui/facet-rule-editor';
|
|
167
|
+
|
|
168
|
+
<FacetRuleEditor
|
|
169
|
+
value={rule}
|
|
170
|
+
onChange={setRule}
|
|
171
|
+
collectionId={collectionId} // lazy-fetches facets via SL.facets.publicList
|
|
172
|
+
preview={rulePreview} // optional — wire from useRulePreview
|
|
173
|
+
/>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 5. Public side
|
|
179
|
+
|
|
180
|
+
> **Do not import `@proveanything/smartlinks-utils-ui` in a public widget.** It is a React admin library. Public widgets only need `@proveanything/smartlinks`.
|
|
181
|
+
|
|
182
|
+
The SDK is framework-agnostic. Public widgets call two endpoints depending on cardinality:
|
|
183
|
+
|
|
184
|
+
| Cardinality | Call | What it does |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| **Singleton** (one answer) | `app.records.match()` | Server walks the chain, returns the best-matching record |
|
|
187
|
+
| **Collection** (all answers) | `app.records.resolveAll()` | Server walks the chain, returns every matching record |
|
|
188
|
+
|
|
189
|
+
Neither call requires React or any other framework — wrap them in whatever async pattern your widget uses.
|
|
190
|
+
|
|
191
|
+
> **Admin vs public — the rule is simple:**
|
|
192
|
+
>
|
|
193
|
+
> | Function | Public widget | Admin dashboard |
|
|
194
|
+
> |---|---|---|
|
|
195
|
+
> | `app.records.create(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
196
|
+
> | `app.records.list(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
197
|
+
> | `app.records.get(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
198
|
+
> | `app.records.update(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
199
|
+
> | `app.records.remove(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
200
|
+
> | `app.records.aggregate(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
201
|
+
> | `app.records.match(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
202
|
+
> | `app.records.resolveAll(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
|
|
203
|
+
> | `app.records.upsert()` | ❌ admin only — no public path | ✅ |
|
|
204
|
+
> | `app.records.bulkUpsert()` | ❌ admin only — no public path | ✅ |
|
|
205
|
+
> | `app.records.bulkDelete()` | ❌ admin only — no public path | ✅ |
|
|
206
|
+
> | `app.records.restore()` | ❌ admin only — no public path | ✅ |
|
|
207
|
+
> | `app.records.previewRule()` | ❌ admin only — no public path | ✅ |
|
|
208
|
+
|
|
209
|
+
### 5a. Singleton — `app.records.match()` (best match wins)
|
|
210
|
+
|
|
211
|
+
Use when the widget shows **one** answer for the current product (ingredients, nutrition, warranty terms, washing instructions).
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import * as SL from '@proveanything/smartlinks';
|
|
215
|
+
|
|
216
|
+
const result = await SL.app.records.match(collectionId, appId, {
|
|
217
|
+
target: { productId, variantId, batchId }, // pass whatever context you have
|
|
218
|
+
strategy: 'best',
|
|
219
|
+
recordType: 'ingredients',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// result.best.ingredients → the single highest-specificity record
|
|
223
|
+
// result.best.ingredients.matchedAt → 'product' | 'rule' | 'facet' | 'collection' | …
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The server walks `proof → batch → variant → product → rule → facet → collection` and returns the first match.
|
|
227
|
+
|
|
228
|
+
### 5b. Collection — `app.records.resolveAll()` (every match, ordered)
|
|
229
|
+
|
|
230
|
+
Use when the widget shows **many** answers across the chain (FAQs, recipes, care tips, SOPs).
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
234
|
+
target: { productId },
|
|
235
|
+
recordTypes: ['faq'],
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// result.records → AppRecord[] sorted most-specific first
|
|
239
|
+
// each record has .matchedAt, .data, .scope
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 5c. Multi-type — `app.records.resolveAll()` with multiple record types
|
|
243
|
+
|
|
244
|
+
When you need records of several types in one call (rare; executors, SEO surfaces):
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
248
|
+
target: { productId, facets: { brand: 'acme' } },
|
|
249
|
+
recordTypes: ['ingredients', 'nutrition', 'allergens'],
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Common mistakes (do not do these)
|
|
254
|
+
|
|
255
|
+
| ❌ Anti-pattern | ✅ Do this instead |
|
|
256
|
+
| ---------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
257
|
+
| Importing anything from `@proveanything/smartlinks-utils-ui` in a public widget | That package is React-only and admin-only. Public widgets only use `@proveanything/smartlinks`. |
|
|
258
|
+
| Calling `SL.app.records.list()` and filtering client-side | `app.records.match()` (singleton) or `app.records.resolveAll()` (collection). The server walks the chain. |
|
|
259
|
+
| Calling `SL.app.records.list(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
|
|
260
|
+
| Calling `SL.app.records.match(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
|
|
261
|
+
| Calling `SL.app.records.resolveAll(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
|
|
262
|
+
| Calling `upsert`, `bulkUpsert`, `bulkDelete`, or `previewRule` from widget code | Those are admin-only. Widget code reads data; it never writes records. |
|
|
263
|
+
| Walking the chain by hand with multiple `get` / `list` calls | One `match()` or `resolveAll()` call. The server handles the resolution order including rules. |
|
|
264
|
+
| Treating `facet:key:value` refs as the rule mechanism | Use `facetRule` (`{ all: [{ facetKey, anyOf: [...] }] }`). Multi-condition, scored by specificity. |
|
|
265
|
+
| Reading `matchedAt === 'global'` | There is no `'global'`. The top of the chain is `'collection'`. |
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 6. Reference: the `EditorContext` your `renderEditor` receives
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
interface EditorContext<TData> {
|
|
273
|
+
value: TData;
|
|
274
|
+
onChange: (next: TData) => void;
|
|
275
|
+
source: 'self' | 'inherited' | 'empty';
|
|
276
|
+
recordId?: string;
|
|
277
|
+
parentValue?: TData | null;
|
|
278
|
+
scope: ParsedRef; // { kind: 'product' | 'rule' | …, productId?, … }
|
|
279
|
+
|
|
280
|
+
// Save lifecycle
|
|
281
|
+
isDirty: boolean;
|
|
282
|
+
isSaving?: boolean;
|
|
283
|
+
saveError?: unknown | null;
|
|
284
|
+
canSave?: boolean; // shell flips to false on empty rules
|
|
285
|
+
cannotSaveReason?: string;
|
|
286
|
+
save: () => Promise<void>;
|
|
287
|
+
reset: () => void;
|
|
288
|
+
|
|
289
|
+
// Deletion
|
|
290
|
+
remove: () => Promise<void>;
|
|
291
|
+
canRemove: boolean;
|
|
292
|
+
|
|
293
|
+
// Rule scope only
|
|
294
|
+
facetRule?: FacetRule | null;
|
|
295
|
+
onFacetRuleChange?: (next: FacetRule | null) => void;
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 7. Migration checklist (existing apps)
|
|
302
|
+
|
|
303
|
+
1. **Update SDKs:** `@proveanything/smartlinks@^1.11`, `@proveanything/smartlinks-utils-ui@^0.7.6`.
|
|
304
|
+
2. **Add `cardinality` and `allowFacetRules`** to every entry under `records` in `app.admin.json`.
|
|
305
|
+
3. **Add `'rule'` (and `'collection'` if missing) to `scopes`** wherever `allowFacetRules: true`.
|
|
306
|
+
4. **Pass `cardinality`** to `<RecordsAdminShell>`.
|
|
307
|
+
5. **Replace any handwritten chain walking** with `app.records.match()` (singleton) or `app.records.resolveAll()` (collection). If you are using React, the `useResolvedRecord` / `useCollectedRecords` hooks from `@proveanything/smartlinks-utils-ui` wrap these calls — but they are **admin-side React helpers**, not for public widgets.
|
|
308
|
+
6. **Delete any code that constructs `facet:key:value` refs** for matching. Use `facetRule` via the shell or `<FacetRuleEditor>` (React admin) or pass `facetRule` directly in `upsert()` calls.
|
|
309
|
+
7. **Search for the word "global"** in your code/docs and rename to "collection" — this is the most common source of confusion.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## 8. Where the canonical exports live
|
|
314
|
+
|
|
315
|
+
### Public widgets (any framework)
|
|
316
|
+
|
|
317
|
+
| Need | Import from |
|
|
318
|
+
| ---- | ----------- |
|
|
319
|
+
| Best-match resolution (singleton) | `@proveanything/smartlinks` → `SL.app.records.match()` |
|
|
320
|
+
| All-matches resolution (collection) | `@proveanything/smartlinks` → `SL.app.records.resolveAll()` |
|
|
321
|
+
| Record CRUD (public path) | `@proveanything/smartlinks` → `SL.app.records.{list, get, create, update, remove, aggregate}` |
|
|
322
|
+
|
|
323
|
+
### Admin dashboards (React)
|
|
324
|
+
|
|
325
|
+
> All of the following are from `@proveanything/smartlinks-utils-ui`, a **React-only** package. Do not use in public widgets.
|
|
326
|
+
|
|
327
|
+
| Need | Import from |
|
|
328
|
+
| ---- | ----------- |
|
|
329
|
+
| Admin shell | `@proveanything/smartlinks-utils-ui/records-admin` → `RecordsAdminShell` |
|
|
330
|
+
| Standalone rule editor | `@proveanything/smartlinks-utils-ui/facet-rule-editor` → `FacetRuleEditor` |
|
|
331
|
+
| Conditions editor (non-facet) | `@proveanything/smartlinks-utils-ui/conditions-editor` → `ConditionsEditor` |
|
|
332
|
+
| Best-match hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolvedRecord` |
|
|
333
|
+
| All-matches hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useCollectedRecords` |
|
|
334
|
+
| Multi-type hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolveAllRecords` |
|
|
335
|
+
| Rule preview hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useRulePreview` |
|
|
336
|
+
| Admin record CRUD (admin path) | `@proveanything/smartlinks` → `SL.app.records.{upsert, bulkUpsert, bulkDelete, restore, previewRule}` |
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
_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._
|
package/docs/overview.md
CHANGED
|
@@ -70,7 +70,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
70
70
|
| **AI Guide Template** | `docs/ai-guide-template.md` | Template for creating `public/ai-guide.md` — customise per app |
|
|
71
71
|
| **Forms** | `docs/forms.md` | Form definitions, schema-driven rendering, submission patterns |
|
|
72
72
|
| **Auth Kit** | `docs/auth-kit.md` | End-user sign-in: email/password, magic links, phone OTP, Google OAuth |
|
|
73
|
-
| **Records
|
|
73
|
+
| **App Records Pattern** | `docs/app-records-pattern.md` | Standard pattern for per-product/facet/variant/batch admin + public widget UIs |
|
|
74
74
|
| **UI Utils** | `docs/ui-utils.md` | `@proveanything/smartlinks-utils-ui` — React shells, hooks, and primitives for records-based apps |
|
|
75
75
|
|
|
76
76
|
---
|
package/docs/ui-utils.md
CHANGED
|
@@ -19,7 +19,7 @@ translate SDK data into consistent admin interfaces.
|
|
|
19
19
|
|
|
20
20
|
**When do you need it?**
|
|
21
21
|
|
|
22
|
-
- You are building an admin UI for a records-based microapp (see [records-
|
|
22
|
+
- You are building an admin UI for a records-based microapp (see [app-records-pattern.md](app-records-pattern.md))
|
|
23
23
|
- You need a media asset picker, icon picker, or font picker in an admin panel
|
|
24
24
|
- You need a recursive rule/conditions editor for targeting or audience logic
|
|
25
25
|
- You want the standard inheritance/override editor for scoped records
|
|
@@ -326,7 +326,7 @@ Props:
|
|
|
326
326
|
|
|
327
327
|
Free-text facet entry is **not** supported — admins must pick from defined facets.
|
|
328
328
|
|
|
329
|
-
See [records-
|
|
329
|
+
See [app-records-pattern.md §4](app-records-pattern.md#4-admin-side----recordsadminshell-the-only-thing-you-should-be-writing) for the standalone usage example.
|
|
330
330
|
|
|
331
331
|
---
|
|
332
332
|
|
|
@@ -505,6 +505,6 @@ your microapp ← domain logic and custom forms
|
|
|
505
505
|
|
|
506
506
|
## Further reading
|
|
507
507
|
|
|
508
|
-
- [records-
|
|
508
|
+
- [app-records-pattern.md](app-records-pattern.md) — the data contract that `RecordsAdminShell` implements
|
|
509
509
|
- [building-react-components.md](building-react-components.md) — dual-mode rendering rules that apply to all components
|
|
510
510
|
- [app-manifest.md](app-manifest.md) — the `records` manifest block that drives tab generation
|