@proveanything/smartlinks 1.10.1 → 1.10.3
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 +87 -79
- package/dist/docs/app-objects.md +87 -49
- package/dist/openapi.yaml +120 -65
- package/dist/types/appObjects.d.ts +113 -112
- package/docs/API_SUMMARY.md +87 -79
- package/docs/app-objects.md +87 -49
- package/openapi.yaml +120 -65
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.10.
|
|
3
|
+
Version: 1.10.3 | Generated: 2026-04-27T10:04:40.477Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -1901,27 +1901,6 @@ interface FacetRule {
|
|
|
1901
1901
|
}
|
|
1902
1902
|
```
|
|
1903
1903
|
|
|
1904
|
-
**ScopeFacetClause** (interface)
|
|
1905
|
-
```typescript
|
|
1906
|
-
interface ScopeFacetClause {
|
|
1907
|
-
key: string
|
|
1908
|
-
valueKeys: string[]
|
|
1909
|
-
}
|
|
1910
|
-
```
|
|
1911
|
-
|
|
1912
|
-
**RecordScope** (interface)
|
|
1913
|
-
```typescript
|
|
1914
|
-
interface RecordScope {
|
|
1915
|
-
productId?: string
|
|
1916
|
-
variantId?: string
|
|
1917
|
-
proofId?: string
|
|
1918
|
-
batchId?: string
|
|
1919
|
-
* Arbitrary facet clauses.
|
|
1920
|
-
* Clauses are ANDed together; valueKeys within a clause are ORed.
|
|
1921
|
-
facets?: ScopeFacetClause[]
|
|
1922
|
-
}
|
|
1923
|
-
```
|
|
1924
|
-
|
|
1925
1904
|
**RecordTarget** (interface)
|
|
1926
1905
|
```typescript
|
|
1927
1906
|
interface RecordTarget {
|
|
@@ -1929,8 +1908,10 @@ interface RecordTarget {
|
|
|
1929
1908
|
variantId?: string
|
|
1930
1909
|
proofId?: string
|
|
1931
1910
|
batchId?: string
|
|
1932
|
-
* Facet
|
|
1933
|
-
*
|
|
1911
|
+
* Facet assignments for the product (e.g. `{ brand: ['samsung'], type: ['tv'] }`).
|
|
1912
|
+
* Used exclusively to match FacetRule records via GIN-indexed containment check.
|
|
1913
|
+
* Does NOT filter legacy scope.facets arrays (that system is removed in SDK 1.12).
|
|
1914
|
+
* Omit to exclude rule records from results.
|
|
1934
1915
|
facets?: Record<string, string[]>
|
|
1935
1916
|
}
|
|
1936
1917
|
```
|
|
@@ -1940,12 +1921,15 @@ interface RecordTarget {
|
|
|
1940
1921
|
interface BulkUpsertItem {
|
|
1941
1922
|
ref: string
|
|
1942
1923
|
recordType?: string
|
|
1943
|
-
|
|
1944
|
-
|
|
1924
|
+
productId?: string | null
|
|
1925
|
+
variantId?: string | null
|
|
1926
|
+
batchId?: string | null
|
|
1927
|
+
proofId?: string | null
|
|
1928
|
+
customId?: string | null
|
|
1929
|
+
sourceSystem?: string | null
|
|
1945
1930
|
startsAt?: string | null
|
|
1946
1931
|
expiresAt?: string | null
|
|
1947
1932
|
status?: string | null
|
|
1948
|
-
scope?: RecordScope
|
|
1949
1933
|
data?: Record<string, unknown> | null
|
|
1950
1934
|
metadata?: Record<string, unknown> | null
|
|
1951
1935
|
facetRule?: FacetRule | null
|
|
@@ -1972,26 +1956,12 @@ interface BulkDeleteResult {
|
|
|
1972
1956
|
}
|
|
1973
1957
|
```
|
|
1974
1958
|
|
|
1975
|
-
**MatchEntry** (interface)
|
|
1976
|
-
```typescript
|
|
1977
|
-
interface MatchEntry {
|
|
1978
|
-
record: AppRecord
|
|
1979
|
-
matchedAt: MatchedAt
|
|
1980
|
-
matchedRule?: FacetRule
|
|
1981
|
-
* Number of clauses in the rule that fired.
|
|
1982
|
-
* Present only when matchedAt === 'rule'.
|
|
1983
|
-
matchedClauseCount?: number
|
|
1984
|
-
specificity: number
|
|
1985
|
-
}
|
|
1986
|
-
```
|
|
1987
|
-
|
|
1988
1959
|
**MatchResult** (interface)
|
|
1989
1960
|
```typescript
|
|
1990
1961
|
interface MatchResult {
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
best?: Record<string, MatchEntry>
|
|
1962
|
+
data: MatchEntry[]
|
|
1963
|
+
total: number
|
|
1964
|
+
strategy: 'all' | 'best'
|
|
1995
1965
|
}
|
|
1996
1966
|
```
|
|
1997
1967
|
|
|
@@ -2000,12 +1970,15 @@ interface MatchResult {
|
|
|
2000
1970
|
interface UpsertRecordInput {
|
|
2001
1971
|
ref: string
|
|
2002
1972
|
recordType?: string
|
|
2003
|
-
|
|
2004
|
-
|
|
1973
|
+
productId?: string | null
|
|
1974
|
+
variantId?: string | null
|
|
1975
|
+
batchId?: string | null
|
|
1976
|
+
proofId?: string | null
|
|
1977
|
+
customId?: string | null
|
|
1978
|
+
sourceSystem?: string | null
|
|
2005
1979
|
startsAt?: string | null
|
|
2006
1980
|
expiresAt?: string | null
|
|
2007
1981
|
status?: string | null
|
|
2008
|
-
scope?: RecordScope
|
|
2009
1982
|
data?: Record<string, unknown> | null
|
|
2010
1983
|
metadata?: Record<string, unknown> | null
|
|
2011
1984
|
facetRule?: FacetRule | null
|
|
@@ -2037,10 +2010,15 @@ interface AppRecord {
|
|
|
2037
2010
|
visibility: Visibility
|
|
2038
2011
|
recordType: string | null
|
|
2039
2012
|
ref: string | null
|
|
2013
|
+
scopeType: string | null
|
|
2014
|
+
scopeId: string | null
|
|
2040
2015
|
customId: string | null
|
|
2016
|
+
customIdNormalized: string | null
|
|
2041
2017
|
sourceSystem: string | null
|
|
2042
2018
|
status: string | null
|
|
2043
2019
|
productId: string | null
|
|
2020
|
+
variantId: string | null
|
|
2021
|
+
batchId: string | null
|
|
2044
2022
|
proofId: string | null
|
|
2045
2023
|
contactId: string | null
|
|
2046
2024
|
authorId: string | null
|
|
@@ -2052,16 +2030,13 @@ interface AppRecord {
|
|
|
2052
2030
|
startsAt: string | null
|
|
2053
2031
|
expiresAt: string | null
|
|
2054
2032
|
deletedAt: string | null // admin only
|
|
2055
|
-
*
|
|
2056
|
-
*
|
|
2057
|
-
scope: RecordScope
|
|
2058
|
-
* Numeric specificity score computed from scope.
|
|
2059
|
-
* Higher = more specific. 0 = universal scope.
|
|
2033
|
+
* Numeric specificity score. Server-computed from anchor IDs and facetRule.
|
|
2034
|
+
* Higher = more specific. 0 = universal (no anchors, no rule).
|
|
2060
2035
|
specificity: number
|
|
2061
2036
|
* Facet rule for rule records (ref starts with "rule:").
|
|
2062
|
-
* null on all other record types. Mutually exclusive with
|
|
2063
|
-
* SDK 1.10.
|
|
2037
|
+
* null on all other record types. Mutually exclusive with anchor IDs.
|
|
2064
2038
|
facetRule: FacetRule | null
|
|
2039
|
+
singletonKey: string | null
|
|
2065
2040
|
data: Record<string, unknown>
|
|
2066
2041
|
owner: Record<string, unknown>
|
|
2067
2042
|
admin: Record<string, unknown> // admin only
|
|
@@ -2072,25 +2047,31 @@ interface AppRecord {
|
|
|
2072
2047
|
**CreateRecordInput** (interface)
|
|
2073
2048
|
```typescript
|
|
2074
2049
|
interface CreateRecordInput {
|
|
2075
|
-
recordType
|
|
2076
|
-
visibility?: Visibility
|
|
2077
|
-
ref?: string
|
|
2078
|
-
status?: string
|
|
2079
|
-
productId?: string
|
|
2080
|
-
|
|
2050
|
+
recordType?: string
|
|
2051
|
+
visibility?: Visibility
|
|
2052
|
+
ref?: string
|
|
2053
|
+
status?: string
|
|
2054
|
+
productId?: string | null
|
|
2055
|
+
variantId?: string | null
|
|
2056
|
+
batchId?: string | null
|
|
2057
|
+
proofId?: string | null
|
|
2081
2058
|
contactId?: string
|
|
2082
2059
|
authorId?: string
|
|
2083
2060
|
authorType?: string
|
|
2084
2061
|
parentType?: string
|
|
2085
2062
|
parentId?: string
|
|
2086
|
-
startsAt?: string
|
|
2087
|
-
expiresAt?: string
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2063
|
+
startsAt?: string | null
|
|
2064
|
+
expiresAt?: string | null
|
|
2065
|
+
scopeType?: string | null
|
|
2066
|
+
scopeId?: string | null
|
|
2067
|
+
customId?: string | null
|
|
2068
|
+
sourceSystem?: string | null
|
|
2069
|
+
* Opt-in singleton cardinality. When set, the server upserts rather than
|
|
2070
|
+
* inserting a duplicate. Values: 'collection' | 'product' | 'variant' | 'batch' | 'proof'
|
|
2071
|
+
singletonPer?: string
|
|
2091
2072
|
data?: Record<string, unknown>
|
|
2092
2073
|
owner?: Record<string, unknown>
|
|
2093
|
-
admin?: Record<string, unknown>
|
|
2074
|
+
admin?: Record<string, unknown>
|
|
2094
2075
|
metadata?: Record<string, unknown>
|
|
2095
2076
|
facetRule?: FacetRule | null
|
|
2096
2077
|
}
|
|
@@ -2106,11 +2087,16 @@ interface UpdateRecordInput {
|
|
|
2106
2087
|
visibility?: Visibility
|
|
2107
2088
|
ref?: string
|
|
2108
2089
|
recordType?: string
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2090
|
+
productId?: string | null
|
|
2091
|
+
variantId?: string | null
|
|
2092
|
+
batchId?: string | null
|
|
2093
|
+
proofId?: string | null
|
|
2094
|
+
startsAt?: string | null
|
|
2095
|
+
expiresAt?: string | null
|
|
2096
|
+
scopeType?: string | null
|
|
2097
|
+
scopeId?: string | null
|
|
2098
|
+
customId?: string | null
|
|
2099
|
+
sourceSystem?: string | null
|
|
2114
2100
|
metadata?: Record<string, unknown>
|
|
2115
2101
|
facetRule?: FacetRule | null
|
|
2116
2102
|
}
|
|
@@ -2141,12 +2127,32 @@ interface ResolveAllParams {
|
|
|
2141
2127
|
**ResolveAllResult** (interface)
|
|
2142
2128
|
```typescript
|
|
2143
2129
|
interface ResolveAllResult {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2130
|
+
records: ResolveAllEntry[]
|
|
2131
|
+
total: number
|
|
2132
|
+
context: ResolveAllContext
|
|
2133
|
+
truncated: boolean
|
|
2134
|
+
}
|
|
2135
|
+
```
|
|
2136
|
+
|
|
2137
|
+
**ResolveAllEntry** (interface)
|
|
2138
|
+
```typescript
|
|
2139
|
+
interface ResolveAllEntry {
|
|
2140
|
+
record: AppRecord
|
|
2141
|
+
matchedAt: MatchedAt
|
|
2142
|
+
specificity: number
|
|
2143
|
+
matchedRule?: FacetRule
|
|
2144
|
+
matchedClauseCount?: number
|
|
2145
|
+
}
|
|
2146
|
+
```
|
|
2147
|
+
|
|
2148
|
+
**ResolveAllContext** (interface)
|
|
2149
|
+
```typescript
|
|
2150
|
+
interface ResolveAllContext {
|
|
2151
|
+
productId?: string
|
|
2152
|
+
variantId?: string
|
|
2153
|
+
batchId?: string
|
|
2154
|
+
proofId?: string
|
|
2155
|
+
facets?: Record<string, string[]>
|
|
2150
2156
|
}
|
|
2151
2157
|
```
|
|
2152
2158
|
|
|
@@ -2154,6 +2160,7 @@ interface ResolveAllResult {
|
|
|
2154
2160
|
```typescript
|
|
2155
2161
|
interface PreviewRuleParams {
|
|
2156
2162
|
facetRule: FacetRule
|
|
2163
|
+
recordType?: string
|
|
2157
2164
|
limit?: number
|
|
2158
2165
|
}
|
|
2159
2166
|
```
|
|
@@ -2161,8 +2168,9 @@ interface PreviewRuleParams {
|
|
|
2161
2168
|
**PreviewRuleResult** (interface)
|
|
2162
2169
|
```typescript
|
|
2163
2170
|
interface PreviewRuleResult {
|
|
2164
|
-
|
|
2165
|
-
|
|
2171
|
+
matchingProducts: Array<{ productId: string; name?: string; facets: Record<string, string[]> }>
|
|
2172
|
+
total: number
|
|
2173
|
+
rule: FacetRule
|
|
2166
2174
|
}
|
|
2167
2175
|
```
|
|
2168
2176
|
|
package/dist/docs/app-objects.md
CHANGED
|
@@ -432,7 +432,7 @@ const productComments = await app.threads.list(collectionId, appId, {
|
|
|
432
432
|
|
|
433
433
|
## Records
|
|
434
434
|
|
|
435
|
-
**Records** are the most flexible object type — use them for structured data with time-based lifecycles, hierarchies, or custom schemas. Records also support **structured
|
|
435
|
+
**Records** are the most flexible object type — use them for structured data with time-based lifecycles, hierarchies, or custom schemas. Records also support **structured targeting** — each record can declare which products, variants, batches, or proofs it applies to (via anchor fields), or which product attributes match (via `facetRule`), and the platform can match records against a runtime context.
|
|
436
436
|
|
|
437
437
|
### When to Use Records
|
|
438
438
|
|
|
@@ -450,64 +450,102 @@ const productComments = await app.threads.list(collectionId, appId, {
|
|
|
450
450
|
|
|
451
451
|
- **Record types** — `recordType` field for categorization (required)
|
|
452
452
|
- **Time windows** — `startsAt` and `expiresAt` for time-based data
|
|
453
|
-
- **
|
|
454
|
-
- **
|
|
453
|
+
- **Anchor fields** — `productId`, `variantId`, `batchId`, `proofId` restrict which context the record applies to
|
|
454
|
+
- **Facet rules** — `facetRule` matches records to products based on attribute values
|
|
455
|
+
- **Specificity scoring** — `specificity` enables "best match" resolution across multiple targeted records
|
|
455
456
|
- **Parent linking** — attach to products, proofs, contacts, etc.
|
|
456
457
|
- **Author tracking** — `authorId` + `authorType`
|
|
457
458
|
- **Status lifecycle** — custom statuses (default `'active'`)
|
|
458
|
-
- **References** — optional `ref` field for external IDs; auto-derived from
|
|
459
|
+
- **References** — optional `ref` field for external IDs; auto-derived from anchor fields if omitted
|
|
459
460
|
|
|
460
|
-
###
|
|
461
|
+
### Targeted Records
|
|
461
462
|
|
|
462
|
-
|
|
463
|
+
Records use flat anchor fields to declare what context they apply to. A record with no anchor fields is universal — it applies everywhere. Populated anchor fields restrict the context to a specific product, variant, batch, or proof.
|
|
463
464
|
|
|
464
465
|
```typescript
|
|
465
466
|
import { app } from '@proveanything/smartlinks';
|
|
466
467
|
|
|
467
|
-
// A nutrition record
|
|
468
|
+
// A nutrition record anchored to a specific product
|
|
468
469
|
await app.records.create(collectionId, appId, {
|
|
469
470
|
recordType: 'nutrition',
|
|
470
|
-
|
|
471
|
+
productId: 'prod_abc',
|
|
471
472
|
data: { calories: 250, protein: 12.5 },
|
|
472
473
|
}, true);
|
|
473
474
|
|
|
474
475
|
// A nutrition record for a specific variant, overriding the product-level record
|
|
475
476
|
await app.records.create(collectionId, appId, {
|
|
476
477
|
recordType: 'nutrition',
|
|
477
|
-
|
|
478
|
+
productId: 'prod_abc',
|
|
479
|
+
variantId: 'var_500ml',
|
|
478
480
|
data: { calories: 260, protein: 12.5 },
|
|
479
481
|
}, true);
|
|
480
482
|
|
|
481
|
-
// A record
|
|
483
|
+
// A record matching products by facet rule (applies to all products with tier=gold)
|
|
482
484
|
await app.records.create(collectionId, appId, {
|
|
483
485
|
recordType: 'loyalty_promo',
|
|
484
|
-
|
|
485
|
-
|
|
486
|
+
facetRule: {
|
|
487
|
+
all: [{ facetKey: 'tier', anyOf: ['gold', 'platinum'] }],
|
|
486
488
|
},
|
|
487
489
|
data: { discountPercent: 15 },
|
|
488
490
|
}, true);
|
|
489
491
|
```
|
|
490
492
|
|
|
491
|
-
The `ref` field is derived automatically
|
|
493
|
+
The `ref` field is derived automatically from anchor fields when omitted:
|
|
492
494
|
|
|
493
495
|
```
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
496
|
+
productId: 'prod_abc' → ref: 'product:prod_abc'
|
|
497
|
+
productId: 'prod_abc', variantId: 'var_x' → ref: 'product:prod_abc/variant:var_x'
|
|
498
|
+
(no anchor fields) → ref: '' (universal)
|
|
499
|
+
facetRule: { ... } → ref: 'rule:<ulid>'
|
|
497
500
|
```
|
|
498
501
|
|
|
499
502
|
#### Specificity scores
|
|
500
503
|
|
|
501
504
|
When multiple scoped records match a context, they are ordered by `specificity`. Higher = more specific:
|
|
502
505
|
|
|
503
|
-
|
|
|
504
|
-
|
|
506
|
+
| Field / element | Points |
|
|
507
|
+
|-----------------|--------|
|
|
505
508
|
| `proofId` | +1000 |
|
|
506
509
|
| `batchId` | +500 |
|
|
507
510
|
| `variantId` | +250 |
|
|
508
511
|
| `productId` | +100 |
|
|
509
|
-
| Per
|
|
510
|
-
| Per
|
|
512
|
+
| Per `facetRule` clause | +50 |
|
|
513
|
+
| Per `anyOf` value | +1 |
|
|
514
|
+
| No anchors / no rule | 0 |
|
|
515
|
+
|
|
516
|
+
### Singleton Cardinality
|
|
517
|
+
|
|
518
|
+
By default, `create` always inserts a new row — calling it twice produces two records with identical anchor fields. **Singleton cardinality** changes that: pass `singletonPer` on creation and the server will **upsert** instead, ensuring at most one record of a given `recordType` exists per scope boundary.
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
// Ensure only one active registration record per product per contact
|
|
522
|
+
await app.records.create(collectionId, appId, {
|
|
523
|
+
recordType: 'product_registration',
|
|
524
|
+
visibility: 'owner',
|
|
525
|
+
contactId: user.contactId,
|
|
526
|
+
productId: product.id,
|
|
527
|
+
singletonPer: 'product', // one per (appId + recordType + contactId + productId)
|
|
528
|
+
data: { registeredAt: new Date().toISOString() },
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
`singletonPer` values and the scope they enforce:
|
|
533
|
+
|
|
534
|
+
| Value | De-duplicates across |
|
|
535
|
+
|-------|---------------------|
|
|
536
|
+
| `'collection'` | entire app (one record of this type per app) |
|
|
537
|
+
| `'product'` | `productId` |
|
|
538
|
+
| `'variant'` | `variantId` |
|
|
539
|
+
| `'batch'` | `batchId` |
|
|
540
|
+
| `'proof'` | `proofId` |
|
|
541
|
+
|
|
542
|
+
The server assigns a **`singletonKey`** to each record that is governed by this rule — an opaque, stable string that acts as the upsert key. If a record with the same key already exists the server updates it in place (clearing `deletedAt` if it was soft-deleted) and returns the existing `id`. `singletonKey` is read-only and exposed on `AppRecord` for debugging but has no meaning to clients.
|
|
543
|
+
|
|
544
|
+
**When to use `singletonPer`:**
|
|
545
|
+
- One loyalty card per contact per product
|
|
546
|
+
- One registration per proof scan
|
|
547
|
+
- One active subscription record per variant
|
|
548
|
+
- Any "find-or-create" pattern where calling `create` twice should be idempotent
|
|
511
549
|
|
|
512
550
|
### Matching Records Against a Context
|
|
513
551
|
|
|
@@ -515,22 +553,22 @@ Use `app.records.match()` to find records whose scope is satisfied by a runtime
|
|
|
515
553
|
|
|
516
554
|
```typescript
|
|
517
555
|
// Find all nutrition records that apply for this product + facet context
|
|
518
|
-
const {
|
|
556
|
+
const { data } = await app.records.match(collectionId, appId, {
|
|
519
557
|
target: {
|
|
520
558
|
productId: 'prod_abc',
|
|
521
559
|
facets: { tier: ['gold'] },
|
|
522
560
|
},
|
|
523
561
|
recordType: 'nutrition',
|
|
524
562
|
}, true);
|
|
525
|
-
//
|
|
526
|
-
// each
|
|
563
|
+
// data is ordered by specificity descending — most specific first
|
|
564
|
+
// each entry carries a `matchedAt` field indicating which dimension matched
|
|
527
565
|
|
|
528
|
-
//
|
|
529
|
-
const { best } = await app.records.match(collectionId, appId, {
|
|
566
|
+
// Use strategy: 'best' to get only the single winner per recordType
|
|
567
|
+
const { data: best } = await app.records.match(collectionId, appId, {
|
|
530
568
|
target: { productId: 'prod_abc', variantId: 'var_500ml' },
|
|
531
569
|
strategy: 'best',
|
|
532
570
|
}, true);
|
|
533
|
-
// best
|
|
571
|
+
// best[0] → the highest-specificity record for this context
|
|
534
572
|
```
|
|
535
573
|
|
|
536
574
|
Facet matching rules:
|
|
@@ -540,18 +578,18 @@ Facet matching rules:
|
|
|
540
578
|
|
|
541
579
|
#### `matchedAt` — match attribution
|
|
542
580
|
|
|
543
|
-
Every record in the response includes a `matchedAt` field indicating **which
|
|
581
|
+
Every record in the response includes a `matchedAt` field indicating **which matching dimension caused the match**. Use it to render attribution labels:
|
|
544
582
|
|
|
545
583
|
```typescript
|
|
546
|
-
const {
|
|
584
|
+
const { data } = await app.records.match(collectionId, appId, { target, recordType: 'nutrition' }, true);
|
|
547
585
|
|
|
548
|
-
for (const entry of
|
|
586
|
+
for (const entry of data) {
|
|
549
587
|
switch (entry.matchedAt) {
|
|
588
|
+
case 'rule': /* "Matches rule" */ break;
|
|
550
589
|
case 'proof': /* "Scan-specific" */ break;
|
|
551
590
|
case 'batch': /* "Batch-specific" */ break;
|
|
552
591
|
case 'variant': /* "Size-specific" */ break;
|
|
553
592
|
case 'product': /* "Inherited from product" */ break;
|
|
554
|
-
case 'rule': /* "Matches rule" */ break;
|
|
555
593
|
case 'facet': /* "Tier-specific" */ break;
|
|
556
594
|
case 'collection': /* "Collection default" */ break;
|
|
557
595
|
case 'universal': /* "Default" */ break;
|
|
@@ -559,7 +597,7 @@ for (const entry of records) {
|
|
|
559
597
|
}
|
|
560
598
|
```
|
|
561
599
|
|
|
562
|
-
Precedence follows: `proof > batch > variant > product >
|
|
600
|
+
Precedence follows: `rule > proof > batch > variant > product > facet > collection > universal`.
|
|
563
601
|
|
|
564
602
|
#### React — `useResolvedRecord`
|
|
565
603
|
|
|
@@ -583,19 +621,19 @@ await app.records.create(collectionId, appId, {
|
|
|
583
621
|
}, true);
|
|
584
622
|
```
|
|
585
623
|
|
|
586
|
-
`facetRule`
|
|
624
|
+
`facetRule` and anchor fields (`productId`, `variantId`, etc.) are mutually exclusive. A record uses either anchor-based targeting or a facetRule, never both. The server assigns `ref: 'rule:<ulid>'` automatically.
|
|
587
625
|
|
|
588
626
|
Specificity for rule records: `Σ (50 + clause.anyOf.length)` across all clauses. A 2-clause rule with 1 value each scores `(50+1)+(50+1) = 102`, which ranks above a plain product-scoped record (100) in `resolveAll()` results.
|
|
589
627
|
|
|
590
628
|
Use `records.previewRule()` to see which products a rule would match before creating it:
|
|
591
629
|
|
|
592
630
|
```typescript
|
|
593
|
-
const {
|
|
631
|
+
const { matchingProducts, total } = await app.records.previewRule(collectionId, appId, {
|
|
594
632
|
facetRule: {
|
|
595
633
|
all: [{ facetKey: 'brand', anyOf: ['samsung'] }],
|
|
596
634
|
},
|
|
597
635
|
});
|
|
598
|
-
//
|
|
636
|
+
// total: 42, matchingProducts: [{ productId: 'prod_001', facets: {...} }, ...]
|
|
599
637
|
```
|
|
600
638
|
|
|
601
639
|
### Resolve All
|
|
@@ -604,7 +642,7 @@ Use `app.records.resolveAll()` to fetch **every applicable record for a product
|
|
|
604
642
|
|
|
605
643
|
```typescript
|
|
606
644
|
// All records that apply to this product context (admin)
|
|
607
|
-
const { records, truncated } = await app.records.resolveAll(collectionId, appId, {
|
|
645
|
+
const { records, total, truncated } = await app.records.resolveAll(collectionId, appId, {
|
|
608
646
|
context: {
|
|
609
647
|
productId: 'prod_001',
|
|
610
648
|
facets: { brand: 'samsung', type: 'tv' },
|
|
@@ -641,7 +679,7 @@ Create-or-update a record by `ref` in a single call:
|
|
|
641
679
|
const { created } = await app.records.upsert(collectionId, appId, {
|
|
642
680
|
ref: 'product:prod_abc',
|
|
643
681
|
recordType: 'nutrition',
|
|
644
|
-
|
|
682
|
+
productId: 'prod_abc',
|
|
645
683
|
data: { calories: 250, protein: 12.5 },
|
|
646
684
|
});
|
|
647
685
|
// created: true if new, false if updated
|
|
@@ -654,8 +692,8 @@ Upsert or delete large sets of records efficiently:
|
|
|
654
692
|
```typescript
|
|
655
693
|
// Bulk upsert up to 500 records in one transaction
|
|
656
694
|
const result = await app.records.bulkUpsert(collectionId, appId, [
|
|
657
|
-
{ ref: 'product:prod_abc', recordType: 'nutrition',
|
|
658
|
-
{ ref: 'product:prod_xyz', recordType: 'nutrition',
|
|
695
|
+
{ ref: 'product:prod_abc', recordType: 'nutrition', productId: 'prod_abc', data: { calories: 250 } },
|
|
696
|
+
{ ref: 'product:prod_xyz', recordType: 'nutrition', productId: 'prod_xyz', data: { calories: 180 } },
|
|
659
697
|
]);
|
|
660
698
|
// result: { saved: 2, failed: 0, results: [...] }
|
|
661
699
|
|
|
@@ -665,7 +703,7 @@ await app.records.bulkDelete(collectionId, appId, {
|
|
|
665
703
|
recordType: 'nutrition',
|
|
666
704
|
});
|
|
667
705
|
|
|
668
|
-
// Bulk delete by
|
|
706
|
+
// Bulk delete by anchor (all records under a product)
|
|
669
707
|
await app.records.bulkDelete(collectionId, appId, {
|
|
670
708
|
scope: { productId: 'prod_abc' },
|
|
671
709
|
});
|
|
@@ -719,7 +757,7 @@ await app.records.upsert(collectionId, appId, {
|
|
|
719
757
|
customId: slug,
|
|
720
758
|
sourceSystem: 'contentful',
|
|
721
759
|
recordType: 'content_page',
|
|
722
|
-
|
|
760
|
+
productId,
|
|
723
761
|
data: { title, body },
|
|
724
762
|
});
|
|
725
763
|
// upsert finds-or-creates by ref deterministically,
|
|
@@ -754,21 +792,21 @@ await app.records.aggregate(collectionId, appId, {
|
|
|
754
792
|
|
|
755
793
|
### Canonical Ref Format
|
|
756
794
|
|
|
757
|
-
The `ref` field is **server-derived** when
|
|
795
|
+
The `ref` field is **server-derived** when anchor fields are provided and `ref` is omitted. Clients should never construct ref strings manually. The authoritative grammar is slash-joined:
|
|
758
796
|
|
|
759
797
|
```
|
|
760
|
-
[
|
|
798
|
+
[product:{productId}/][variant:{variantId}/][batch:{batchId}/][proof:{proofId}]
|
|
761
799
|
```
|
|
762
800
|
|
|
763
801
|
Examples:
|
|
764
802
|
|
|
765
|
-
|
|
|
766
|
-
|
|
767
|
-
| `
|
|
768
|
-
| `
|
|
769
|
-
| `
|
|
770
|
-
| `
|
|
771
|
-
|
|
|
803
|
+
| Anchor fields | Derived ref |
|
|
804
|
+
|---------------|-------------|
|
|
805
|
+
| `productId: 'prod_abc'` | `product:prod_abc` |
|
|
806
|
+
| `productId: 'prod_abc', variantId: 'var_500ml'` | `product:prod_abc/variant:var_500ml` |
|
|
807
|
+
| `batchId: 'batch_q1'` | `batch:batch_q1` |
|
|
808
|
+
| `facetRule: { ... }` | `rule:<ulid>` |
|
|
809
|
+
| *(no anchor fields)* | `''` (universal) |
|
|
772
810
|
|
|
773
811
|
`parseRef` / `buildRef` in `data/refs.ts` should be used for **display and URL round-tripping only**, never as upsert keys. For ETL use cases, set an explicit `ref` using a stable external key (see [External ID / ETL Workflow](#external-id--etl-workflow)).
|
|
774
812
|
|