@proveanything/smartlinks 1.11.10 → 1.11.11
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
CHANGED
|
@@ -32,7 +32,7 @@ If you only remember one rule: **never write your own resolution loop**. The ser
|
|
|
32
32
|
|
|
33
33
|
## 1. The data model in one paragraph
|
|
34
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.
|
|
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). Records also carry a `status` (`'active'` | `'draft'` | `'archived'`) and optional `startsAt` / `expiresAt` timestamps. 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
36
|
|
|
37
37
|
```ts
|
|
38
38
|
import * as SL from '@proveanything/smartlinks';
|
|
@@ -219,11 +219,12 @@ const result = await SL.app.records.match(collectionId, appId, {
|
|
|
219
219
|
recordType: 'ingredients',
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
-
// result.
|
|
223
|
-
// result.
|
|
222
|
+
// result.data[0] → the single highest-specificity MatchEntry (when strategy: 'best')
|
|
223
|
+
// result.data[0].matchedAt → 'product' | 'rule' | 'facet' | 'collection' | …
|
|
224
|
+
// result.data[0].data → your record payload
|
|
224
225
|
```
|
|
225
226
|
|
|
226
|
-
The server walks `proof → batch → variant → product → rule → facet → collection` and returns the first match.
|
|
227
|
+
The server walks `proof → batch → variant → product → rule → facet → collection` and returns the first match. `result.data` will have at most one entry when `strategy: 'best'`.
|
|
227
228
|
|
|
228
229
|
### 5b. Collection — `app.records.resolveAll()` (every match, ordered)
|
|
229
230
|
|
|
@@ -231,25 +232,69 @@ Use when the widget shows **many** answers across the chain (FAQs, recipes, care
|
|
|
231
232
|
|
|
232
233
|
```ts
|
|
233
234
|
const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
context: { productId }, // note: resolveAll uses 'context', not 'target'
|
|
236
|
+
recordType: 'faq', // singular — omit to return all record types
|
|
236
237
|
});
|
|
237
238
|
|
|
238
|
-
// result.records →
|
|
239
|
-
// each record
|
|
239
|
+
// result.records → ResolveAllEntry[] sorted most-specific first
|
|
240
|
+
// each entry: { record: AppRecord, matchedAt, specificity, matchedRule? }
|
|
240
241
|
```
|
|
241
242
|
|
|
242
|
-
### 5c. Multi-type — `app.records.resolveAll()`
|
|
243
|
+
### 5c. Multi-type — `app.records.resolveAll()` without a recordType filter
|
|
243
244
|
|
|
244
|
-
When you need records of
|
|
245
|
+
When you need records of all types for a context in one call (rare; executors, SEO surfaces), omit `recordType`:
|
|
245
246
|
|
|
246
247
|
```ts
|
|
247
248
|
const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
context: {
|
|
250
|
+
productId,
|
|
251
|
+
facets: { brand: ['acme'] }, // include facets to match rule records
|
|
252
|
+
},
|
|
253
|
+
// no recordType → returns all declared types
|
|
250
254
|
});
|
|
255
|
+
|
|
256
|
+
// result.records → one ResolveAllEntry per matched record, all types interleaved
|
|
257
|
+
// filter client-side by entry.record.recordType if you need to separate them
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 5d. Status filtering and the draft → active lifecycle
|
|
261
|
+
|
|
262
|
+
Every record has a `status` field with three canonical values:
|
|
263
|
+
|
|
264
|
+
| Value | Meaning | Returned to public/owner callers? |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| `active` | Live and current | ✅ Yes |
|
|
267
|
+
| `draft` | Being prepared, not yet published | ❌ No |
|
|
268
|
+
| `archived` | Previously live, retained for history | ❌ No |
|
|
269
|
+
|
|
270
|
+
**Enforcement:** `match()`, `resolveAll()`, and `GET /records` (query) now only return `status: "active"` records to public and owner callers. Admin callers receive all statuses as before; use explicit `status` filters (`status=draft`, `status=archived`) to narrow results.
|
|
271
|
+
|
|
272
|
+
**`active` is the default** when no `status` is supplied on creation, so existing records and simple creation flows are unaffected.
|
|
273
|
+
|
|
274
|
+
**Draft → publish workflow:** create the record with `status: 'draft'` so it is invisible to public widgets, then update it to `status: 'active'` when ready to publish.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
// Create a record that is not yet publicly visible
|
|
278
|
+
await SL.app.records.upsert(collectionId, appId, {
|
|
279
|
+
recordType: 'ingredients',
|
|
280
|
+
scope: { productId },
|
|
281
|
+
data: { /* draft payload */ },
|
|
282
|
+
status: 'draft',
|
|
283
|
+
}, /* admin */ true);
|
|
284
|
+
|
|
285
|
+
// Publish it
|
|
286
|
+
await SL.app.records.upsert(collectionId, appId, {
|
|
287
|
+
recordType: 'ingredients',
|
|
288
|
+
scope: { productId },
|
|
289
|
+
data: { /* final payload */ },
|
|
290
|
+
status: 'active',
|
|
291
|
+
}, true);
|
|
251
292
|
```
|
|
252
293
|
|
|
294
|
+
**Composes with `startsAt` / `expiresAt`:** a record must satisfy **both** the status check and the time window to be returned to public callers. A record that is `active` but whose `startsAt` is in the future, or whose `expiresAt` has passed, is excluded.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
253
298
|
### Common mistakes (do not do these)
|
|
254
299
|
|
|
255
300
|
| ❌ Anti-pattern | ✅ Do this instead |
|
|
@@ -263,6 +308,8 @@ const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
|
263
308
|
| Walking the chain by hand with multiple `get` / `list` calls | One `match()` or `resolveAll()` call. The server handles the resolution order including rules. |
|
|
264
309
|
| Treating `facet:key:value` refs as the rule mechanism | Use `facetRule` (`{ all: [{ facetKey, anyOf: [...] }] }`). Multi-condition, scored by specificity. |
|
|
265
310
|
| Reading `matchedAt === 'global'` | There is no `'global'`. The top of the chain is `'collection'`. |
|
|
311
|
+
| Expecting `draft` or `archived` records to appear in public widget results | Public/owner callers only receive `status: "active"` records from `match()`, `resolveAll()`, and `GET /records`. Use admin calls to query by other statuses. |
|
|
312
|
+
| Setting `status: 'active'` and wondering why a record is still hidden | Check `startsAt` / `expiresAt` — a record must satisfy both the status check and the time window. |
|
|
266
313
|
|
|
267
314
|
---
|
|
268
315
|
|
|
@@ -307,6 +354,7 @@ interface EditorContext<TData> {
|
|
|
307
354
|
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
355
|
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
356
|
7. **Search for the word "global"** in your code/docs and rename to "collection" — this is the most common source of confusion.
|
|
357
|
+
8. **Audit records that should not be public yet:** any record that previously relied on obscurity (e.g. not linked in the widget, no active product) is now filtered by `status`. Set `status: 'draft'` on records that are not ready and `status: 'active'` when publishing. Records without an explicit status were created as `active` and are unaffected.
|
|
310
358
|
|
|
311
359
|
---
|
|
312
360
|
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -32,7 +32,7 @@ If you only remember one rule: **never write your own resolution loop**. The ser
|
|
|
32
32
|
|
|
33
33
|
## 1. The data model in one paragraph
|
|
34
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.
|
|
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). Records also carry a `status` (`'active'` | `'draft'` | `'archived'`) and optional `startsAt` / `expiresAt` timestamps. 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
36
|
|
|
37
37
|
```ts
|
|
38
38
|
import * as SL from '@proveanything/smartlinks';
|
|
@@ -219,11 +219,12 @@ const result = await SL.app.records.match(collectionId, appId, {
|
|
|
219
219
|
recordType: 'ingredients',
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
-
// result.
|
|
223
|
-
// result.
|
|
222
|
+
// result.data[0] → the single highest-specificity MatchEntry (when strategy: 'best')
|
|
223
|
+
// result.data[0].matchedAt → 'product' | 'rule' | 'facet' | 'collection' | …
|
|
224
|
+
// result.data[0].data → your record payload
|
|
224
225
|
```
|
|
225
226
|
|
|
226
|
-
The server walks `proof → batch → variant → product → rule → facet → collection` and returns the first match.
|
|
227
|
+
The server walks `proof → batch → variant → product → rule → facet → collection` and returns the first match. `result.data` will have at most one entry when `strategy: 'best'`.
|
|
227
228
|
|
|
228
229
|
### 5b. Collection — `app.records.resolveAll()` (every match, ordered)
|
|
229
230
|
|
|
@@ -231,25 +232,69 @@ Use when the widget shows **many** answers across the chain (FAQs, recipes, care
|
|
|
231
232
|
|
|
232
233
|
```ts
|
|
233
234
|
const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
context: { productId }, // note: resolveAll uses 'context', not 'target'
|
|
236
|
+
recordType: 'faq', // singular — omit to return all record types
|
|
236
237
|
});
|
|
237
238
|
|
|
238
|
-
// result.records →
|
|
239
|
-
// each record
|
|
239
|
+
// result.records → ResolveAllEntry[] sorted most-specific first
|
|
240
|
+
// each entry: { record: AppRecord, matchedAt, specificity, matchedRule? }
|
|
240
241
|
```
|
|
241
242
|
|
|
242
|
-
### 5c. Multi-type — `app.records.resolveAll()`
|
|
243
|
+
### 5c. Multi-type — `app.records.resolveAll()` without a recordType filter
|
|
243
244
|
|
|
244
|
-
When you need records of
|
|
245
|
+
When you need records of all types for a context in one call (rare; executors, SEO surfaces), omit `recordType`:
|
|
245
246
|
|
|
246
247
|
```ts
|
|
247
248
|
const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
context: {
|
|
250
|
+
productId,
|
|
251
|
+
facets: { brand: ['acme'] }, // include facets to match rule records
|
|
252
|
+
},
|
|
253
|
+
// no recordType → returns all declared types
|
|
250
254
|
});
|
|
255
|
+
|
|
256
|
+
// result.records → one ResolveAllEntry per matched record, all types interleaved
|
|
257
|
+
// filter client-side by entry.record.recordType if you need to separate them
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 5d. Status filtering and the draft → active lifecycle
|
|
261
|
+
|
|
262
|
+
Every record has a `status` field with three canonical values:
|
|
263
|
+
|
|
264
|
+
| Value | Meaning | Returned to public/owner callers? |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| `active` | Live and current | ✅ Yes |
|
|
267
|
+
| `draft` | Being prepared, not yet published | ❌ No |
|
|
268
|
+
| `archived` | Previously live, retained for history | ❌ No |
|
|
269
|
+
|
|
270
|
+
**Enforcement:** `match()`, `resolveAll()`, and `GET /records` (query) now only return `status: "active"` records to public and owner callers. Admin callers receive all statuses as before; use explicit `status` filters (`status=draft`, `status=archived`) to narrow results.
|
|
271
|
+
|
|
272
|
+
**`active` is the default** when no `status` is supplied on creation, so existing records and simple creation flows are unaffected.
|
|
273
|
+
|
|
274
|
+
**Draft → publish workflow:** create the record with `status: 'draft'` so it is invisible to public widgets, then update it to `status: 'active'` when ready to publish.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
// Create a record that is not yet publicly visible
|
|
278
|
+
await SL.app.records.upsert(collectionId, appId, {
|
|
279
|
+
recordType: 'ingredients',
|
|
280
|
+
scope: { productId },
|
|
281
|
+
data: { /* draft payload */ },
|
|
282
|
+
status: 'draft',
|
|
283
|
+
}, /* admin */ true);
|
|
284
|
+
|
|
285
|
+
// Publish it
|
|
286
|
+
await SL.app.records.upsert(collectionId, appId, {
|
|
287
|
+
recordType: 'ingredients',
|
|
288
|
+
scope: { productId },
|
|
289
|
+
data: { /* final payload */ },
|
|
290
|
+
status: 'active',
|
|
291
|
+
}, true);
|
|
251
292
|
```
|
|
252
293
|
|
|
294
|
+
**Composes with `startsAt` / `expiresAt`:** a record must satisfy **both** the status check and the time window to be returned to public callers. A record that is `active` but whose `startsAt` is in the future, or whose `expiresAt` has passed, is excluded.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
253
298
|
### Common mistakes (do not do these)
|
|
254
299
|
|
|
255
300
|
| ❌ Anti-pattern | ✅ Do this instead |
|
|
@@ -263,6 +308,8 @@ const result = await SL.app.records.resolveAll(collectionId, appId, {
|
|
|
263
308
|
| Walking the chain by hand with multiple `get` / `list` calls | One `match()` or `resolveAll()` call. The server handles the resolution order including rules. |
|
|
264
309
|
| Treating `facet:key:value` refs as the rule mechanism | Use `facetRule` (`{ all: [{ facetKey, anyOf: [...] }] }`). Multi-condition, scored by specificity. |
|
|
265
310
|
| Reading `matchedAt === 'global'` | There is no `'global'`. The top of the chain is `'collection'`. |
|
|
311
|
+
| Expecting `draft` or `archived` records to appear in public widget results | Public/owner callers only receive `status: "active"` records from `match()`, `resolveAll()`, and `GET /records`. Use admin calls to query by other statuses. |
|
|
312
|
+
| Setting `status: 'active'` and wondering why a record is still hidden | Check `startsAt` / `expiresAt` — a record must satisfy both the status check and the time window. |
|
|
266
313
|
|
|
267
314
|
---
|
|
268
315
|
|
|
@@ -307,6 +354,7 @@ interface EditorContext<TData> {
|
|
|
307
354
|
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
355
|
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
356
|
7. **Search for the word "global"** in your code/docs and rename to "collection" — this is the most common source of confusion.
|
|
357
|
+
8. **Audit records that should not be public yet:** any record that previously relied on obscurity (e.g. not linked in the widget, no active product) is now filtered by `status`. Set `status: 'draft'` on records that are not ready and `status: 'active'` when publishing. Records without an explicit status were created as `active` and are unaffected.
|
|
310
358
|
|
|
311
359
|
---
|
|
312
360
|
|