@proveanything/smartlinks 1.15.3 → 1.15.5
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/api/appConfiguration.d.ts +90 -1
- package/dist/api/appConfiguration.js +117 -0
- package/dist/api/collection.d.ts +6 -2
- package/dist/api/collection.js +6 -2
- package/dist/cache.d.ts +19 -0
- package/dist/cache.js +25 -0
- package/dist/docs/API_SUMMARY.md +157 -11
- package/dist/docs/app-data-storage.md +2 -0
- package/dist/docs/appConfig.md +483 -0
- package/dist/docs/overview.md +20 -0
- package/dist/docs/proof-product-data-scoping.md +459 -0
- package/dist/openapi.yaml +222 -4
- package/dist/types/appConfiguration.d.ts +83 -0
- package/dist/types/collection.d.ts +11 -2
- package/dist/types/product.d.ts +37 -0
- package/dist/types/proof.d.ts +39 -3
- package/docs/API_SUMMARY.md +157 -11
- package/docs/app-data-storage.md +2 -0
- package/docs/appConfig.md +483 -0
- package/docs/overview.md +20 -0
- package/docs/proof-product-data-scoping.md +459 -0
- package/openapi.yaml +222 -4
- package/package.json +1 -1
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# Data Scoping for Products and Proofs
|
|
2
|
+
|
|
3
|
+
Canonical specification for how custom data is stored, who can read it, and who can write it — on both **products** and **proofs**.
|
|
4
|
+
|
|
5
|
+
Applies to the SmartLinks SDK, admin apps, public apps, and any consumer of the platform API.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Design principles
|
|
10
|
+
|
|
11
|
+
1. **Fixed named fields at the root; flexible data in JSON bags.** The root of a product or proof is reserved for typed, first-class columns (name, image, gtin, ownerId, claimable, …). Everything custom lives inside a JSON bag.
|
|
12
|
+
2. **Every bag is defined by *who can write and read* it, and lives at the top level.** Write authority and read visibility are hard, server-enforced boundaries. Rather than hide them under sub-keys, each combination gets its own top-level bag. This matches the existing `product.admin` convention and keeps read-time filtering trivial: the server either returns a bag or it doesn't.
|
|
13
|
+
3. **Public is the default; admin is opt-in.** Anything in `data` is world-readable. Anything private to the business goes in `admin`, which the server never returns to a non-admin caller.
|
|
14
|
+
4. **Same shape everywhere.** Products and proofs share the same rules for the business-writable bags (`data`, `admin`). Proofs add a third top-level bag — `values` — for owner-writable and per-user data, because proofs have an owner and products don't.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The schema
|
|
19
|
+
|
|
20
|
+
### Product
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
product.<rootColumns> // typed columns: name, description, image, gtin, …
|
|
24
|
+
|
|
25
|
+
product.data = { … } // PUBLIC → read: everyone, write: business
|
|
26
|
+
product.admin = { … } // BUSINESS-ONLY → read: business, write: business
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Proof
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
proof.<rootColumns> // typed columns: id, productId, ownerId, claimable, …
|
|
33
|
+
|
|
34
|
+
proof.data = { … } // PUBLIC → read: everyone, write: business
|
|
35
|
+
proof.admin = { … } // BUSINESS-ONLY → read: business, write: business
|
|
36
|
+
|
|
37
|
+
proof.values = { // consumer bag — owner + business-writable
|
|
38
|
+
<anyKey>: …, // PUBLIC → read: everyone,
|
|
39
|
+
// write: business + current owner
|
|
40
|
+
owner: { … }, // OWNER-SCOPED → read: business + current owner,
|
|
41
|
+
// write: business + current owner,
|
|
42
|
+
// transfers with ownership
|
|
43
|
+
personal: { // PER-USER → read/write: only the specific user,
|
|
44
|
+
[userId]: { … }, // not visible to the next owner
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Products intentionally have no `values` bag — there is no owner to write it. If a product ever needs owner-like data, that data belongs on the proof.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Reserved sub-key names
|
|
54
|
+
|
|
55
|
+
Inside `proof.values`, these keys are reserved and MUST NOT be used as free-form public field names:
|
|
56
|
+
|
|
57
|
+
| Bag | Reserved keys |
|
|
58
|
+
| ------------- | --------------------------- |
|
|
59
|
+
| `proof.values`| `owner`, `personal` |
|
|
60
|
+
|
|
61
|
+
`data` and `admin` bags have no reserved sub-keys — everything inside them is flat.
|
|
62
|
+
|
|
63
|
+
The field-config editor and SDK write helpers MUST reject attempts to create top-level public fields in `proof.values` named `owner` or `personal`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Who reads what (visibility matrix)
|
|
68
|
+
|
|
69
|
+
| Location | Public / Anonymous | Authenticated (non-owner) | Current Owner | Business Admin |
|
|
70
|
+
| --------------------------------- | :-----------------: | :------------------------: | :------------: | :--------------: |
|
|
71
|
+
| `product.<root>` | ✅ | ✅ | ✅ | ✅ |
|
|
72
|
+
| `product.data.*` | ✅ | ✅ | ✅ | ✅ |
|
|
73
|
+
| `product.admin.*` | ❌ | ❌ | ❌ | ✅ |
|
|
74
|
+
| `proof.<root>` | ✅ | ✅ | ✅ | ✅ |
|
|
75
|
+
| `proof.data.*` | ✅ | ✅ | ✅ | ✅ |
|
|
76
|
+
| `proof.admin.*` | ❌ | ❌ | ❌ | ✅ |
|
|
77
|
+
| `proof.values.<publicKey>` | ✅ | ✅ | ✅ | ✅ |
|
|
78
|
+
| `proof.values.owner.*` | ❌ | ❌ | ✅ | ✅ |
|
|
79
|
+
| `proof.values.personal[me].*` | ❌ | ✅ (own slot only) | ✅ (own slot) | ❌ (see note) |
|
|
80
|
+
| `proof.values.personal[other].*` | ❌ | ❌ | ❌ | ❌ (see note) |
|
|
81
|
+
|
|
82
|
+
> **Note on `personal`:** the default rule is that `personal` slots are readable *only* by the user whose `userId` matches the slot key — not even business admins. If the platform ever needs an admin-visible variant, it should be a separate mechanism, not a relaxation of this rule.
|
|
83
|
+
|
|
84
|
+
## Who writes what (authority matrix)
|
|
85
|
+
|
|
86
|
+
| Location | Public / Anonymous | Authenticated (non-owner) | Current Owner | Business Admin |
|
|
87
|
+
| --------------------------------- | :-----------------: | :------------------------: | :------------: | :--------------: |
|
|
88
|
+
| `product.<root>` | ❌ | ❌ | ❌ | ✅ |
|
|
89
|
+
| `product.data.*` | ❌ | ❌ | ❌ | ✅ |
|
|
90
|
+
| `product.admin.*` | ❌ | ❌ | ❌ | ✅ |
|
|
91
|
+
| `proof.<root>` (spec) | ❌ | ❌ | ❌ | ✅ |
|
|
92
|
+
| `proof.data.*` | ❌ | ❌ | ❌ | ✅ |
|
|
93
|
+
| `proof.admin.*` | ❌ | ❌ | ❌ | ✅ |
|
|
94
|
+
| `proof.values.<publicKey>` | ❌ | ❌ | ✅ | ✅ |
|
|
95
|
+
| `proof.values.owner.*` | ❌ | ❌ | ✅ | ✅ |
|
|
96
|
+
| `proof.values.personal[me].*` | ❌ | ✅ (own slot only) | ✅ (own slot) | ❌ |
|
|
97
|
+
|
|
98
|
+
All read and write rules are **server-enforced**. Clients MUST NOT rely on UI hiding alone.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Worked examples
|
|
103
|
+
|
|
104
|
+
### A bicycle
|
|
105
|
+
|
|
106
|
+
```jsonc
|
|
107
|
+
// product (the model)
|
|
108
|
+
{
|
|
109
|
+
"id": "prod_road_2024",
|
|
110
|
+
"name": "Aero Road 2024",
|
|
111
|
+
"image": "…",
|
|
112
|
+
"data": {
|
|
113
|
+
"frameMaterial": "carbon", // public
|
|
114
|
+
"wheelSize": "700c", // public
|
|
115
|
+
"purchaseLink": "https://…" // public
|
|
116
|
+
},
|
|
117
|
+
"admin": {
|
|
118
|
+
"internalSku": "AR24-CARB-M", // business-only
|
|
119
|
+
"unitCost": 1420, // business-only
|
|
120
|
+
"manufacturingSite": "Taichung" // business-only
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// proof (a specific bike)
|
|
125
|
+
{
|
|
126
|
+
"id": "proof_abc123",
|
|
127
|
+
"productId": "prod_road_2024",
|
|
128
|
+
"ownerId": "user_42",
|
|
129
|
+
"claimable": false,
|
|
130
|
+
"data": {
|
|
131
|
+
"serial": "SN-000123", // public spec, business-set
|
|
132
|
+
"colour": "matte black", // public spec, business-set
|
|
133
|
+
"year": 2024 // public spec, business-set
|
|
134
|
+
},
|
|
135
|
+
"admin": {
|
|
136
|
+
"warrantyExpiry": "2027-06-01", // business-only
|
|
137
|
+
"batchId": "B-2024-Q2-17" // business-only
|
|
138
|
+
},
|
|
139
|
+
"values": {
|
|
140
|
+
"nickname": "Midnight", // public, owner-editable
|
|
141
|
+
"displayNote": "For sale — DM me", // public, owner-editable
|
|
142
|
+
"owner": {
|
|
143
|
+
"purchasedAt": "Halfords Oxford", // business + current owner, transfers
|
|
144
|
+
"serviceHistory": [ /* … */ ] // with ownership
|
|
145
|
+
},
|
|
146
|
+
"personal": {
|
|
147
|
+
"user_42": {
|
|
148
|
+
"combinationLock": "17-04-88", // only user_42 ever sees this
|
|
149
|
+
"privateNotes": "…"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Liquid template ergonomics
|
|
157
|
+
|
|
158
|
+
```liquid
|
|
159
|
+
{{ product.name }} {# root column #}
|
|
160
|
+
{{ product.data.purchaseLink }} {# public #}
|
|
161
|
+
{{ product.admin.internalSku }} {# admin surfaces only #}
|
|
162
|
+
|
|
163
|
+
{{ proof.data.colour }} {# public spec #}
|
|
164
|
+
{{ proof.admin.warrantyExpiry }} {# admin surfaces only #}
|
|
165
|
+
{{ proof.values.nickname }} {# public, owner-editable #}
|
|
166
|
+
{{ proof.values.owner.purchasedAt }} {# owner-scoped #}
|
|
167
|
+
{{ proof.values.personal[currentUserId].combinationLock }}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Legacy compatibility
|
|
173
|
+
|
|
174
|
+
The new shape is **strictly additive**. No existing data needs to move.
|
|
175
|
+
|
|
176
|
+
- **`product.data.<key>`** — legacy flat keys continue to mean "public". No migration.
|
|
177
|
+
- **`product.admin.<key>`** — already exists today; formalised here as the canonical private-business bag.
|
|
178
|
+
- **`proof.values.<key>`** — legacy flat keys continue to mean "public, owner-editable". No migration.
|
|
179
|
+
- **`proof.data`** — new bucket, added alongside the existing `proof.values`, for public business-owned spec that isn't consumer-editable.
|
|
180
|
+
- **`proof.admin`** — new bucket, mirroring `product.admin`.
|
|
181
|
+
- **`owner` / `personal`** — reserved going forward inside `proof.values`. Any pre-existing custom field literally named `owner` or `personal` should be renamed on a case-by-case basis. New writes MUST reject these as free-form key names.
|
|
182
|
+
|
|
183
|
+
Legacy apps that only read the flat public shape continue to work unchanged. New apps that need scoped data opt in by reading and writing the new bags and reserved sub-keys.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## SDK guidance
|
|
188
|
+
|
|
189
|
+
- **Reads** return whatever bags the caller is entitled to. Callers MUST NOT assume `admin`, `values.owner`, or `values.personal` are present.
|
|
190
|
+
- **Writes** to `admin` bags, `proof.data`, and `proof.values.owner` require the appropriate role; the server rejects unauthorised writes with a permission error rather than silently dropping keys.
|
|
191
|
+
- **Field-config editors** (custom-fields UIs on collection settings) should:
|
|
192
|
+
- Offer a **scope** dropdown per field. Products: `Public` (→ `product.data`), `Admin` (→ `product.admin`). Proofs: `Public` (→ `proof.values.<key>` — owner-editable), `Owner` (→ `proof.values.owner`), `Personal` (→ `proof.values.personal[userId]`), `Admin` (→ `proof.admin`).
|
|
193
|
+
- Reject reserved names (`owner`, `personal`) as free-form keys in `proof.values`.
|
|
194
|
+
- Persist enough metadata for renderers to know which bag a field lives in and to gate its display by the current caller's role.
|
|
195
|
+
- **Server** performs both read-scoping (strip bags the caller can't see) and write authorisation (reject writes the caller isn't allowed to make). The client is a convenience layer, never the source of truth for either.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Summary
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
product.data = { … } // public business
|
|
203
|
+
product.admin = { … } // admin-only business
|
|
204
|
+
|
|
205
|
+
proof.data = { … } // public business (spec)
|
|
206
|
+
proof.admin = { … } // admin-only business
|
|
207
|
+
proof.values = { …public, owner?, personal? } // consumer
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Reserved sub-keys: **`owner`** and **`personal`** inside `proof.values`. Everything else in every bag is a free-form key.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Field-definition schemas (collection settings)
|
|
215
|
+
|
|
216
|
+
The custom-field definitions that drive the editors above are stored on the
|
|
217
|
+
**collection** in named settings groups. Both are read/written via the standard
|
|
218
|
+
settings API:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// Read (admin: true required for admin-scoped fields to be returned)
|
|
222
|
+
const productFields = await SL.collection.getSettings(collectionId, 'productFields', true);
|
|
223
|
+
const proofFields = await SL.collection.getSettings(collectionId, 'proofFields', true);
|
|
224
|
+
|
|
225
|
+
// Write
|
|
226
|
+
await SL.collection.updateSettings(collectionId, 'productFields', productFieldsConfig);
|
|
227
|
+
await SL.collection.updateSettings(collectionId, 'proofFields', proofFieldsConfig);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Third-party apps (CRM, warranty, provenance, etc.) that render a product or
|
|
231
|
+
proof for a user should fetch these once per collection to know which
|
|
232
|
+
configurable fields exist, their labels, types, and — crucially — which bag to
|
|
233
|
+
read the value from.
|
|
234
|
+
|
|
235
|
+
### Settings group names
|
|
236
|
+
|
|
237
|
+
| Group key | Shape | Applies to |
|
|
238
|
+
| ----------------- | --------------------- | ---------------------------------------------------------------- |
|
|
239
|
+
| `productFields` | `ProductFieldsConfig`| Custom fields on `product.data` / `product.admin` |
|
|
240
|
+
| `proofFields` | `ProofFieldsConfig` | Custom fields on `proof.values` / `proof.data` / `proof.admin`|
|
|
241
|
+
|
|
242
|
+
### Shared field-def shape
|
|
243
|
+
|
|
244
|
+
Product and proof field defs share the same base shape. The only difference is
|
|
245
|
+
the allowed `scope` values.
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
type FieldType =
|
|
249
|
+
| 'text'
|
|
250
|
+
| 'textarea'
|
|
251
|
+
| 'number'
|
|
252
|
+
| 'date'
|
|
253
|
+
| 'select'
|
|
254
|
+
| 'switch'
|
|
255
|
+
| 'url';
|
|
256
|
+
|
|
257
|
+
interface FieldOption {
|
|
258
|
+
value: string;
|
|
259
|
+
label: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface FieldDef {
|
|
263
|
+
/** Stable key. Written into the resolved bag at this name. */
|
|
264
|
+
key: string;
|
|
265
|
+
/** Human label for editors and read-only renderers. */
|
|
266
|
+
label: string;
|
|
267
|
+
type: FieldType;
|
|
268
|
+
required?: boolean;
|
|
269
|
+
placeholder?: string;
|
|
270
|
+
help?: string;
|
|
271
|
+
/** Required when type === 'select'. */
|
|
272
|
+
options?: FieldOption[];
|
|
273
|
+
/** Default value applied when creating a new record. */
|
|
274
|
+
defaultValue?: unknown;
|
|
275
|
+
/** Show as a column in the default items table. */
|
|
276
|
+
showInTable?: boolean;
|
|
277
|
+
/** Show on the inline detail / edit view. */
|
|
278
|
+
showInDetail?: boolean;
|
|
279
|
+
/**
|
|
280
|
+
* Storage scope. When omitted the field is legacy public and written flat
|
|
281
|
+
* (product.data.<key> or proof.values.<key>). New fields opt into a scope.
|
|
282
|
+
*/
|
|
283
|
+
scope?: FieldScope;
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `productFields` — `ProductFieldsConfig`
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
type ProductFieldScope = 'public' | 'admin';
|
|
291
|
+
|
|
292
|
+
type ProductFieldDef = FieldDef & { scope?: ProductFieldScope };
|
|
293
|
+
|
|
294
|
+
interface ProductFieldsConfig {
|
|
295
|
+
fields: ProductFieldDef[];
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Value resolution** for a given `ProductFieldDef` `f`:
|
|
300
|
+
|
|
301
|
+
| `f.scope` | Read from |
|
|
302
|
+
| ---------------------- | ---------------------------------------------- |
|
|
303
|
+
| `'public'` / omitted | `product.data[f.key]` |
|
|
304
|
+
| `'admin'` | `product.admin?.[f.key]` (admin only) |
|
|
305
|
+
|
|
306
|
+
### `proofFields` — `ProofFieldsConfig`
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
type ProofFieldScope = 'public' | 'owner' | 'personal' | 'admin';
|
|
310
|
+
|
|
311
|
+
type ProofFieldDef = FieldDef & { scope?: ProofFieldScope };
|
|
312
|
+
|
|
313
|
+
interface ProofFieldsConfig {
|
|
314
|
+
fields: ProofFieldDef[];
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Value resolution** for a given `ProofFieldDef` `f`, current user id `uid`:
|
|
319
|
+
|
|
320
|
+
| `f.scope` | Read from |
|
|
321
|
+
| ---------------------- | -------------------------------------------------- |
|
|
322
|
+
| `'public'` / omitted | `proof.values[f.key]` |
|
|
323
|
+
| `'owner'` | `proof.values.owner?.[f.key]` |
|
|
324
|
+
| `'personal'` | `proof.values.personal?.[uid]?.[f.key]` |
|
|
325
|
+
| `'admin'` | `proof.admin?.[f.key]` |
|
|
326
|
+
|
|
327
|
+
If the caller isn't entitled to the scope, the server strips the bag; the
|
|
328
|
+
field def still exists but the value comes back `undefined`. Clients should
|
|
329
|
+
treat that as "not visible to me" rather than "unset".
|
|
330
|
+
|
|
331
|
+
> **Note:** there is currently no `scope` value that maps to `proof.data`
|
|
332
|
+
> (public business spec). If/when the editor UX wants to distinguish
|
|
333
|
+
> "public, business-only-writable" from "public, owner-editable" (the current
|
|
334
|
+
> unscoped `proof.values.<key>`), add a `'spec'` scope that resolves to
|
|
335
|
+
> `proof.data[f.key]`. Until then, treat `proof.data` as hand-authored /
|
|
336
|
+
> server-set and out of scope for the collection-settings field editor.
|
|
337
|
+
|
|
338
|
+
### JSON Schema — `productFields`
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
343
|
+
"$id": "https://smartlinks.app/schemas/collection-settings/productFields.json",
|
|
344
|
+
"title": "Collection Settings: productFields",
|
|
345
|
+
"type": "object",
|
|
346
|
+
"additionalProperties": false,
|
|
347
|
+
"required": ["fields"],
|
|
348
|
+
"properties": {
|
|
349
|
+
"fields": {
|
|
350
|
+
"type": "array",
|
|
351
|
+
"items": { "$ref": "#/$defs/productFieldDef" }
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
"$defs": {
|
|
355
|
+
"productFieldDef": {
|
|
356
|
+
"type": "object",
|
|
357
|
+
"additionalProperties": false,
|
|
358
|
+
"required": ["key", "label", "type"],
|
|
359
|
+
"properties": {
|
|
360
|
+
"key": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
|
|
361
|
+
"label": { "type": "string" },
|
|
362
|
+
"type": { "enum": ["text","textarea","number","date","select","switch","url"] },
|
|
363
|
+
"required": { "type": "boolean" },
|
|
364
|
+
"placeholder": { "type": "string" },
|
|
365
|
+
"help": { "type": "string" },
|
|
366
|
+
"options": {
|
|
367
|
+
"type": "array",
|
|
368
|
+
"items": {
|
|
369
|
+
"type": "object",
|
|
370
|
+
"additionalProperties": false,
|
|
371
|
+
"required": ["value", "label"],
|
|
372
|
+
"properties": {
|
|
373
|
+
"value": { "type": "string" },
|
|
374
|
+
"label": { "type": "string" }
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
"defaultValue": {},
|
|
379
|
+
"showInTable": { "type": "boolean" },
|
|
380
|
+
"showInDetail": { "type": "boolean" },
|
|
381
|
+
"scope": { "enum": ["public", "admin"] }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### JSON Schema — `proofFields`
|
|
389
|
+
|
|
390
|
+
```json
|
|
391
|
+
{
|
|
392
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
393
|
+
"$id": "https://smartlinks.app/schemas/collection-settings/proofFields.json",
|
|
394
|
+
"title": "Collection Settings: proofFields",
|
|
395
|
+
"type": "object",
|
|
396
|
+
"additionalProperties": false,
|
|
397
|
+
"required": ["fields"],
|
|
398
|
+
"properties": {
|
|
399
|
+
"fields": {
|
|
400
|
+
"type": "array",
|
|
401
|
+
"items": { "$ref": "#/$defs/proofFieldDef" }
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
"$defs": {
|
|
405
|
+
"proofFieldDef": {
|
|
406
|
+
"type": "object",
|
|
407
|
+
"additionalProperties": false,
|
|
408
|
+
"required": ["key", "label", "type"],
|
|
409
|
+
"properties": {
|
|
410
|
+
"key": { "type": "string", "pattern": "^(?!owner$|personal$)[A-Za-z_][A-Za-z0-9_]*$" },
|
|
411
|
+
"label": { "type": "string" },
|
|
412
|
+
"type": { "enum": ["text","textarea","number","date","select","switch","url"] },
|
|
413
|
+
"required": { "type": "boolean" },
|
|
414
|
+
"placeholder": { "type": "string" },
|
|
415
|
+
"help": { "type": "string" },
|
|
416
|
+
"options": {
|
|
417
|
+
"type": "array",
|
|
418
|
+
"items": {
|
|
419
|
+
"type": "object",
|
|
420
|
+
"additionalProperties": false,
|
|
421
|
+
"required": ["value", "label"],
|
|
422
|
+
"properties": {
|
|
423
|
+
"value": { "type": "string" },
|
|
424
|
+
"label": { "type": "string" }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
"defaultValue": {},
|
|
429
|
+
"showInTable": { "type": "boolean" },
|
|
430
|
+
"showInDetail": { "type": "boolean" },
|
|
431
|
+
"scope": { "enum": ["public", "owner", "personal", "admin"] }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Worked example — CRM app resolving a proof
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
const { fields } = await SL.collection.getSettings(collectionId, 'proofFields');
|
|
442
|
+
const uid = (await SL.auth.getAccount())?.id;
|
|
443
|
+
|
|
444
|
+
for (const f of fields) {
|
|
445
|
+
const value =
|
|
446
|
+
!f.scope || f.scope === 'public' ? proof.values?.[f.key] :
|
|
447
|
+
f.scope === 'owner' ? proof.values?.owner?.[f.key] :
|
|
448
|
+
f.scope === 'personal' ? proof.values?.personal?.[uid!]?.[f.key] :
|
|
449
|
+
f.scope === 'admin' ? proof.admin?.[f.key] :
|
|
450
|
+
undefined;
|
|
451
|
+
|
|
452
|
+
render({ label: f.label, type: f.type, value });
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
The same loop, swapping `proofFields` → `productFields` and the resolution
|
|
457
|
+
table for the product one (`public` → `product.data`, `admin` → `product.admin`),
|
|
458
|
+
gives a CRM app a complete configurable read of a product. No app-specific
|
|
459
|
+
knowledge required beyond "fetch the field defs from the collection".
|