@proveanything/smartlinks 1.9.20 → 1.9.22
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 +3 -1
- package/dist/docs/app-manifest.md +38 -1
- package/dist/docs/auth-kit.md +162 -0
- package/dist/docs/forms.md +113 -0
- package/dist/docs/overview.md +4 -0
- package/dist/docs/records-admin-pattern.md +315 -0
- package/dist/docs/ui-utils.md +294 -0
- package/dist/openapi.yaml +6 -0
- package/dist/types/collection.d.ts +2 -0
- package/docs/API_SUMMARY.md +3 -1
- package/docs/app-manifest.md +38 -1
- package/docs/auth-kit.md +162 -0
- package/docs/forms.md +113 -0
- package/docs/overview.md +4 -0
- package/docs/records-admin-pattern.md +315 -0
- package/docs/ui-utils.md +294 -0
- package/openapi.yaml +6 -0
- 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.9.
|
|
3
|
+
Version: 1.9.22 | Generated: 2026-04-25T11:11:00.344Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -2994,6 +2994,8 @@ interface Collection {
|
|
|
2994
2994
|
secondaryColor?: string
|
|
2995
2995
|
portalUrl?: string // URL for the collection's portal (if applicable)
|
|
2996
2996
|
allowAutoGenerateClaims?: boolean
|
|
2997
|
+
variants: boolean // does this collection support variants?
|
|
2998
|
+
batches: boolean // does this collection support batches?
|
|
2997
2999
|
defaultAuthKitId: string // default auth kit for this collection, used for auth
|
|
2998
3000
|
}
|
|
2999
3001
|
```
|
|
@@ -95,7 +95,20 @@ The manifest is loaded automatically by the platform for every collection page.
|
|
|
95
95
|
{ "title": "Home", "path": "/" },
|
|
96
96
|
{ "title": "Gallery", "path": "/gallery" },
|
|
97
97
|
{ "title": "Settings", "path": "/settings", "params": { "tab": "advanced" } }
|
|
98
|
-
]
|
|
98
|
+
],
|
|
99
|
+
|
|
100
|
+
"records": {
|
|
101
|
+
"nutrition": {
|
|
102
|
+
"scopes": ["product", "facet", "batch"],
|
|
103
|
+
"defaultScope": "facet",
|
|
104
|
+
"label": "Nutrition info"
|
|
105
|
+
},
|
|
106
|
+
"cooking_steps": {
|
|
107
|
+
"scopes": ["product", "facet"],
|
|
108
|
+
"defaultScope": "product",
|
|
109
|
+
"label": "Cooking steps"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
99
112
|
}
|
|
100
113
|
```
|
|
101
114
|
|
|
@@ -188,6 +201,30 @@ See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-so
|
|
|
188
201
|
| `path` | string | ❌ | Hash route within the app (defaults to `"/"` if omitted) |
|
|
189
202
|
| `params` | object | ❌ | App-specific query params appended to the URL — do **not** include platform params (`collectionId`, `productId`, etc.) |
|
|
190
203
|
|
|
204
|
+
#### `records`
|
|
205
|
+
|
|
206
|
+
Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [Records-Based Admin Pattern](records-admin-pattern.md). Omit if the app does not use scoped records.
|
|
207
|
+
|
|
208
|
+
The platform and the `<RecordsAdminShell>` from `@proveanything/ui-utils` read this block to render only the relevant tabs and affordances.
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
"records": {
|
|
212
|
+
"<recordType>": {
|
|
213
|
+
"scopes": ["product", "facet", "batch"],
|
|
214
|
+
"defaultScope": "facet",
|
|
215
|
+
"label": "Human-readable label"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Field | Type | Required | Description |
|
|
221
|
+
|----------------|----------|----------|-------------|
|
|
222
|
+
| `scopes` | string[] | ✅ | Allowed scope kinds in resolution order. Valid values: `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"default"`. |
|
|
223
|
+
| `defaultScope` | string | ✅ | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
|
|
224
|
+
| `label` | string | ✅ | Human-readable label for the record type, used in headings and tabs. |
|
|
225
|
+
|
|
226
|
+
An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`).
|
|
227
|
+
|
|
191
228
|
#### `executor`
|
|
192
229
|
|
|
193
230
|
Declares the executor bundle — a standalone JS library for programmatic configuration, server-side SEO, and LLM content generation. Omit if the app has no executor.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# SmartLinks Auth Kit (`@proveanything/smartlinks` — `authKit` namespace)
|
|
2
|
+
|
|
3
|
+
> End-user authentication flows for SmartLinks microapps. Covers email/password, magic links, phone OTP, Google OAuth, profile management, and password/email change flows.
|
|
4
|
+
>
|
|
5
|
+
> **This is part of the core SDK** — no separate install required. Import from `@proveanything/smartlinks`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What is Auth Kit for?
|
|
10
|
+
|
|
11
|
+
Auth Kit is the **end-user identity layer** for microapps that need users to sign in. It is distinct from the admin/platform authentication (Bearer tokens) used to call admin endpoints.
|
|
12
|
+
|
|
13
|
+
Use Auth Kit when:
|
|
14
|
+
- Your app has a login/register screen for end users (not collection admins)
|
|
15
|
+
- You need to gate features behind a verified user identity
|
|
16
|
+
- You want to store user-specific data with the `userAppData` API (see [app-data-storage.md](app-data-storage.md))
|
|
17
|
+
|
|
18
|
+
Do **not** use Auth Kit for:
|
|
19
|
+
- Admin-side API calls (those use Bearer tokens set by the platform shell)
|
|
20
|
+
- Claiming proofs (see proof claiming methods)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Setup: creating an Auth Kit client
|
|
25
|
+
|
|
26
|
+
Each app requires an Auth Kit configuration, created by a collection admin:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// Admin setup (one-time, done in admin console)
|
|
30
|
+
import { authKit } from '@proveanything/smartlinks';
|
|
31
|
+
|
|
32
|
+
// Returns an authKitId to store in your app config
|
|
33
|
+
const config = await authKit.create(collectionId, {
|
|
34
|
+
name: 'My App Auth',
|
|
35
|
+
loginMethods: ['email', 'google', 'magic_link'],
|
|
36
|
+
redirectUrl: 'https://myapp.example.com/auth/callback',
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Key flows
|
|
43
|
+
|
|
44
|
+
### Email / password
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { authKit } from '@proveanything/smartlinks';
|
|
48
|
+
|
|
49
|
+
// Register
|
|
50
|
+
const session = await authKit.register(clientId, {
|
|
51
|
+
email: 'user@example.com',
|
|
52
|
+
password: 'securePassword123',
|
|
53
|
+
displayName: 'Alice',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Login
|
|
57
|
+
const session = await authKit.login(clientId, 'user@example.com', 'securePassword123');
|
|
58
|
+
|
|
59
|
+
// session.token — store this; pass to initializeApi for subsequent calls
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Magic link (passwordless email)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
await authKit.sendMagicLink(clientId, {
|
|
66
|
+
email: 'user@example.com',
|
|
67
|
+
redirectUrl: 'https://myapp.example.com/auth/verify',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// On callback page, extract token from URL and verify
|
|
71
|
+
const session = await authKit.verifyMagicLink(clientId, tokenFromUrl);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Phone OTP
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
await authKit.sendPhoneCode(clientId, '+61400000000');
|
|
78
|
+
const session = await authKit.verifyPhoneCode(clientId, '+61400000000', '123456');
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Google OAuth
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// After Google sign-in, pass the id_token to Auth Kit
|
|
85
|
+
const session = await authKit.googleLogin(clientId, googleIdToken);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Profile management
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { authKit } from '@proveanything/smartlinks';
|
|
94
|
+
|
|
95
|
+
// Get current user's profile
|
|
96
|
+
const profile = await authKit.getProfile(clientId);
|
|
97
|
+
|
|
98
|
+
// Update profile
|
|
99
|
+
await authKit.updateProfile(clientId, { displayName: 'Alice B.', avatarUrl: '...' });
|
|
100
|
+
|
|
101
|
+
// Change password
|
|
102
|
+
await authKit.changePassword(clientId, 'currentPass', 'newPass');
|
|
103
|
+
|
|
104
|
+
// Change email (triggers verification)
|
|
105
|
+
await authKit.changeEmail(clientId, 'newemail@example.com', 'password', redirectUrl);
|
|
106
|
+
|
|
107
|
+
// Delete account
|
|
108
|
+
await authKit.deleteAccount(clientId, 'password', 'DELETE');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Email verification
|
|
114
|
+
|
|
115
|
+
Auth Kit can send and verify email addresses after registration:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
await authKit.sendEmailVerification(clientId, {
|
|
119
|
+
userId,
|
|
120
|
+
email: 'user@example.com',
|
|
121
|
+
redirectUrl: 'https://myapp.example.com/auth/verified',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// On callback page
|
|
125
|
+
await authKit.verifyEmail(clientId, tokenFromUrl);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Password reset
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
await authKit.requestPasswordReset(clientId, {
|
|
134
|
+
email: 'user@example.com',
|
|
135
|
+
redirectUrl: 'https://myapp.example.com/auth/reset',
|
|
136
|
+
clientName: 'My App',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// On reset page — verify token is still valid before showing the form
|
|
140
|
+
await authKit.verifyResetToken(clientId, tokenFromUrl);
|
|
141
|
+
|
|
142
|
+
// Complete reset
|
|
143
|
+
await authKit.completePasswordReset(clientId, tokenFromUrl, 'newSecurePassword');
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Relationship to other parts of the SDK
|
|
149
|
+
|
|
150
|
+
| Concern | Where it lives |
|
|
151
|
+
|---------|---------------|
|
|
152
|
+
| End-user sign-in / register | `authKit` namespace (this doc) |
|
|
153
|
+
| Admin Bearer token auth | Platform shell — not set by your app |
|
|
154
|
+
| Per-user data storage | `userAppData` namespace — see [app-data-storage.md](app-data-storage.md) |
|
|
155
|
+
| User identity in analytics | `userId` field on `analytics` and `interactions` calls |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Further reading
|
|
160
|
+
|
|
161
|
+
- [app-data-storage.md](app-data-storage.md) — storing user-specific data after login
|
|
162
|
+
- [app-manifest.md](app-manifest.md) — `app.admin.json` setup questions for configuring `clientId`
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# SmartLinks Forms
|
|
2
|
+
|
|
3
|
+
> Platform-managed form definitions and submissions. Covers the `form` data API (core SDK) and the `@proveanything/ui-forms` React package for schema-driven form UIs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Two layers
|
|
8
|
+
|
|
9
|
+
| Layer | Package | What it does |
|
|
10
|
+
|-------|---------|--------------|
|
|
11
|
+
| **Forms data API** | `@proveanything/smartlinks` (`form` namespace) | CRUD for form definitions; stores submission schema |
|
|
12
|
+
| **Forms UI** | `@proveanything/ui-forms` | React components that render a form definition, validate input, and submit |
|
|
13
|
+
|
|
14
|
+
You can use the data API without the UI package (e.g. in an executor), but the UI package requires both.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Forms data API (`form` namespace)
|
|
19
|
+
|
|
20
|
+
The `form` namespace in the core SDK manages **form definitions** — the schema that describes fields, validation rules, and submission behaviour. This is an admin-side API.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { form } from '@proveanything/smartlinks';
|
|
24
|
+
|
|
25
|
+
// List all forms in a collection
|
|
26
|
+
const forms = await form.list(collectionId, /* admin= */ true);
|
|
27
|
+
|
|
28
|
+
// Get a specific form
|
|
29
|
+
const myForm = await form.get(collectionId, formId);
|
|
30
|
+
|
|
31
|
+
// Create a form (admin)
|
|
32
|
+
const created = await form.create(collectionId, {
|
|
33
|
+
name: 'Warranty Registration',
|
|
34
|
+
fields: [
|
|
35
|
+
{ id: 'name', type: 'text', label: 'Full name', required: true },
|
|
36
|
+
{ id: 'email', type: 'email', label: 'Email address', required: true },
|
|
37
|
+
{ id: 'serial', type: 'text', label: 'Serial number', required: true },
|
|
38
|
+
{ id: 'receipt', type: 'file', label: 'Proof of purchase' },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Update a form (admin)
|
|
43
|
+
await form.update(collectionId, formId, { name: 'Warranty Registration v2' });
|
|
44
|
+
|
|
45
|
+
// Delete a form (admin)
|
|
46
|
+
await form.remove(collectionId, formId);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Form definitions are stored per-collection and referenced by `formId`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Forms UI (`@proveanything/ui-forms`)
|
|
54
|
+
|
|
55
|
+
`@proveanything/ui-forms` provides React components that consume a form definition from the API and render a complete, accessible, validated form.
|
|
56
|
+
|
|
57
|
+
Install: `npm install @proveanything/ui-forms`
|
|
58
|
+
|
|
59
|
+
### Rendering a form
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { SmartForm } from '@proveanything/ui-forms';
|
|
63
|
+
|
|
64
|
+
// Fetches the form definition and renders it
|
|
65
|
+
<SmartForm
|
|
66
|
+
collectionId={collectionId}
|
|
67
|
+
formId={formId}
|
|
68
|
+
onSubmit={async (values) => {
|
|
69
|
+
// handle submission — e.g. create an app.records entry or send via comms
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Headless usage
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { useFormDefinition, FormRenderer } from '@proveanything/ui-forms';
|
|
78
|
+
|
|
79
|
+
const { fields, isLoading } = useFormDefinition(collectionId, formId);
|
|
80
|
+
|
|
81
|
+
// Render fields yourself using the definition
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Common patterns
|
|
87
|
+
|
|
88
|
+
### Warranty / registration forms
|
|
89
|
+
|
|
90
|
+
Define the form in `app.admin.json` setup or via the admin API. On the public widget, render with `<SmartForm>` and on submit create an `app.records` entry:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
onSubmit={async (values) => {
|
|
94
|
+
await app.records.create(collectionId, appId, {
|
|
95
|
+
recordType: 'warranty_registration',
|
|
96
|
+
ref: `proof:${proofId}`,
|
|
97
|
+
data: values,
|
|
98
|
+
});
|
|
99
|
+
}}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Competition / feedback forms
|
|
103
|
+
|
|
104
|
+
Same pattern — use `<SmartForm>` for capture and write to `app.records` or trigger an `interactions.appendEvent` on submit.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Further reading
|
|
109
|
+
|
|
110
|
+
- [app-objects.md](app-objects.md) — `app.records` for storing submissions
|
|
111
|
+
- [app-data-storage.md](app-data-storage.md) — choosing the right storage model
|
|
112
|
+
- [interactions.md](interactions.md) — firing events on form submission
|
|
113
|
+
- [comms.md](comms.md) — sending confirmation emails after submission
|
package/dist/docs/overview.md
CHANGED
|
@@ -64,6 +64,10 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
64
64
|
| **Liquid Templates** | `docs/liquid-templates.md` | Dynamic content rendering with LiquidJS |
|
|
65
65
|
| **Iframe Responder** | `docs/iframe-responder.md` | Iframe communication and proxy mode internals |
|
|
66
66
|
| **AI Guide Template** | `docs/ai-guide-template.md` | Template for creating `public/ai-guide.md` — customise per app |
|
|
67
|
+
| **Forms** | `docs/forms.md` | Form definitions, schema-driven rendering, submission patterns |
|
|
68
|
+
| **Auth Kit** | `docs/auth-kit.md` | End-user sign-in: email/password, magic links, phone OTP, Google OAuth |
|
|
69
|
+
| **Records Admin Pattern** | `docs/records-admin-pattern.md` | Standard pattern for per-product/facet/variant/batch admin UIs |
|
|
70
|
+
| **UI Utils** | `docs/ui-utils.md` | `@proveanything/smartlinks-utils-ui` — React shells, hooks, and primitives for records-based apps |
|
|
67
71
|
|
|
68
72
|
---
|
|
69
73
|
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# SmartLinks Records-Based Admin Pattern
|
|
2
|
+
|
|
3
|
+
> Canonical guide for building admin UIs in microapps that store **per-product**, **per-facet**, **per-variant** or **per-batch** data.
|
|
4
|
+
>
|
|
5
|
+
> Audience: microapp developers (nutrition, allergy, ingredients, cooking-guide, warranty, provenance, …).
|
|
6
|
+
>
|
|
7
|
+
> Status: **standard** — new admin apps in this category MUST follow this contract. Existing apps SHOULD migrate.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Why a shared pattern?
|
|
12
|
+
|
|
13
|
+
Many SmartLinks microapps store **structured data that varies along one or more product axes**:
|
|
14
|
+
|
|
15
|
+
| App | Varies by |
|
|
16
|
+
|------------------|------------------------------------------------|
|
|
17
|
+
| Nutrition | product, facet (bread type, region), batch |
|
|
18
|
+
| Allergy | product, facet (recipe family) |
|
|
19
|
+
| Ingredients | product, variant (size), batch (production run)|
|
|
20
|
+
| Cooking guide | product, facet (cut of meat) |
|
|
21
|
+
| Warranty | product, variant, batch |
|
|
22
|
+
| Provenance | batch |
|
|
23
|
+
|
|
24
|
+
Without a shared pattern, every app reinvents:
|
|
25
|
+
|
|
26
|
+
- the left-rail "browse + select" list
|
|
27
|
+
- the per-axis precedence rules
|
|
28
|
+
- the inheritance/override UI
|
|
29
|
+
- the "no data yet" empty state
|
|
30
|
+
- CSV import/export
|
|
31
|
+
- bulk operations
|
|
32
|
+
|
|
33
|
+
The result is drift: each app feels different and admins have to re-learn the model. This guide locks the model down at the SDK level so the matching UI primitives in `@proveanything/ui-utils` (see the [companion guide](ui-utils.md)) can stay simple.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Storage model: `app.records`
|
|
38
|
+
|
|
39
|
+
**Do not** stuff per-axis data into `appConfiguration.products[productId]`. Use `app.records` (recordType-keyed) so that data is queryable, paginatable and survives schema changes.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { app } from '@proveanything/smartlinks';
|
|
43
|
+
|
|
44
|
+
await app.records.create(collectionId, appId, {
|
|
45
|
+
recordType: 'nutrition', // app-defined, stable
|
|
46
|
+
ref: 'product:prod_abc', // see §3
|
|
47
|
+
data: { /* domain payload */ },
|
|
48
|
+
}, /* admin= */ true);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Each record has:
|
|
52
|
+
|
|
53
|
+
| Field | Purpose |
|
|
54
|
+
|--------------|-------------------------------------------------------------|
|
|
55
|
+
| `recordType` | Namespaces records inside the app. One app may have several (`nutrition`, `cooking_steps`). |
|
|
56
|
+
| `ref` | Encodes the **scope** of the record. See §3. |
|
|
57
|
+
| `data` | The domain payload. Free-form per app. |
|
|
58
|
+
| `meta` | Reserved for system fields (timestamps, author). |
|
|
59
|
+
|
|
60
|
+
> **Rule:** `(appId, recordType, ref)` is unique. Treat it as the natural key.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 3. The `ref` convention (REQUIRED)
|
|
65
|
+
|
|
66
|
+
`ref` is a colon-delimited string that encodes which scope a record applies to. Standardising it is what makes the shared admin shell possible.
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
product:<productId>
|
|
70
|
+
variant:<productId>:<variantId>
|
|
71
|
+
batch:<productId>:<batchId>
|
|
72
|
+
proof:<proofId>
|
|
73
|
+
facet:<facetKey>:<valueKey>
|
|
74
|
+
default
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Notes:
|
|
78
|
+
|
|
79
|
+
- Variants and batches are **always nested under a product** in the SDK, so their refs include `productId`.
|
|
80
|
+
- `facet:` refs are **collection-wide** — facets cross products by design.
|
|
81
|
+
- `default` is a single record per (app, recordType) used as the global fallback.
|
|
82
|
+
- Refs are opaque to the SDK. Apps parse them. A helper module (`@proveanything/smartlinks-utils-ui/records-admin`) exports `parseRef`/`buildRef` so all apps agree on syntax. See Appendix A for the full implementation.
|
|
83
|
+
|
|
84
|
+
### Adding scopes later
|
|
85
|
+
|
|
86
|
+
If a new axis appears (e.g. `region:eu`), pick a new prefix and document it. Never reuse a prefix with different semantics.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 4. Resolution order (REQUIRED)
|
|
91
|
+
|
|
92
|
+
When the **public** side of an app needs "the data that applies to this proof / product / context", it walks the chain from most-specific to least-specific and returns the first match:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
proof → batch → variant → product → facet(*) → default
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`facet(*)` means: walk every facet attached to the product in a deterministic order (alphabetical by `facetKey`, then `valueKey`) and use the first matching facet record.
|
|
99
|
+
|
|
100
|
+
> **Rule:** Resolution is **first-match-wins, not merge**. If you need field-level merging, build it on top with explicit `inheritsFrom` markers in the payload — but the default for shared infra is whole-record replacement, because it is far easier to reason about.
|
|
101
|
+
|
|
102
|
+
The canonical resolver lives in `@proveanything/smartlinks-utils-ui/records-admin` so every app behaves identically:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { resolveRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
106
|
+
|
|
107
|
+
const resolved = await resolveRecord({
|
|
108
|
+
appId,
|
|
109
|
+
recordType: 'nutrition',
|
|
110
|
+
scope: { collectionId, productId, variantId, batchId, proofId },
|
|
111
|
+
});
|
|
112
|
+
// → { record, source: 'variant' | 'product' | 'facet' | … } | null
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Apps that only support a subset of scopes pass `supportedScopes: ['product', 'facet']` and the resolver skips the rest.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. Scope capabilities (declared per app)
|
|
120
|
+
|
|
121
|
+
An app declares which scopes it accepts records for in `app.manifest.json`. This drives the admin UI and avoids dead tabs:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
// app.manifest.json (extension)
|
|
125
|
+
{
|
|
126
|
+
"records": {
|
|
127
|
+
"nutrition": {
|
|
128
|
+
"scopes": ["product", "facet", "batch"],
|
|
129
|
+
"defaultScope": "facet",
|
|
130
|
+
"label": "Nutrition info"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Field | Meaning |
|
|
137
|
+
|----------------|----------------------------------------------------------------|
|
|
138
|
+
| `scopes` | Allowed scope kinds, in **resolution order**. |
|
|
139
|
+
| `defaultScope` | Where the "Create new" button lands in the admin shell. |
|
|
140
|
+
| `label` | Human-readable label for the record type (used in headings). |
|
|
141
|
+
|
|
142
|
+
The shared admin shell reads this manifest entry and renders only the relevant tabs. Apps don't hard-code tab lists.
|
|
143
|
+
|
|
144
|
+
See [app-manifest.md](app-manifest.md) for the full schema reference.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 6. Discovering whether variants / batches are in use
|
|
149
|
+
|
|
150
|
+
The `Collection` object exposes top-level `variants: boolean` and `batches: boolean` flags that indicate whether the collection has these features enabled. Read them directly rather than probing by listing:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { appConfiguration } from '@proveanything/smartlinks';
|
|
154
|
+
|
|
155
|
+
const collection = await appConfiguration.getCollection(collectionId);
|
|
156
|
+
|
|
157
|
+
const showVariantTab = collection.variants && scopeConfig?.scopes.includes('variant') === true;
|
|
158
|
+
const showBatchTab = collection.batches && scopeConfig?.scopes.includes('batch') === true;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`scopeConfig` is the parsed manifest `records` entry for this `recordType` — e.g. `manifest.records?.nutrition`.
|
|
162
|
+
|
|
163
|
+
Rules:
|
|
164
|
+
|
|
165
|
+
1. If the collection has the feature **and** the app **declares** support for the scope, show the tab and offer "Add variant" / "Add batch" affordances.
|
|
166
|
+
2. If the app does **not** declare support, hide the tab entirely even if `collection.variants` is true (another app created them).
|
|
167
|
+
3. If the collection does not have the feature enabled, hide the tab even if the app declares support.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 7. Inheritance & overrides
|
|
172
|
+
|
|
173
|
+
Because resolution is first-match-wins, the admin UI must make inheritance **visible**:
|
|
174
|
+
|
|
175
|
+
- When editing a **variant** record, show the **product** record as the inherited baseline.
|
|
176
|
+
- Each field in the editor displays a small **↩ "Inherited"** marker when its value matches the parent and **● "Override"** when it differs.
|
|
177
|
+
- A row-level "Reset to inherited" action removes the override (deletes the record at the current scope if all overrides are reset).
|
|
178
|
+
|
|
179
|
+
Apps don't have to implement this themselves — the `<RecordEditor>` primitive in `@proveanything/smartlinks-utils-ui` does it given the resolved parent payload.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 8. Bulk operations
|
|
184
|
+
|
|
185
|
+
Standard verbs every shell should expose:
|
|
186
|
+
|
|
187
|
+
| Verb | Behaviour |
|
|
188
|
+
|-------------------|----------------------------------------------------------------------|
|
|
189
|
+
| **Apply to many** | Take the current record's payload, write it to N selected products / variants. |
|
|
190
|
+
| **Copy from** | Pick a source scope, copy its payload to the current scope. |
|
|
191
|
+
| **Clear** | Delete records at the current scope (children unaffected). |
|
|
192
|
+
|
|
193
|
+
Use the `bulkUpsert` / `bulkDelete` helpers from `@proveanything/smartlinks-utils-ui/records-admin`, which handle batching and error collection for you:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { bulkUpsert, bulkDelete } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
197
|
+
|
|
198
|
+
// Apply current payload to many refs
|
|
199
|
+
await bulkUpsert({ SL, collectionId, appId, recordType, refs: targetRefs, data: payload });
|
|
200
|
+
|
|
201
|
+
// Clear records at selected refs
|
|
202
|
+
await bulkDelete({ SL, collectionId, appId, recordType, refs: targetRefs });
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
If you are using `<RecordsAdminShell>`, the bulk actions menu is included and wired automatically via the `onTelemetry` hook — you don't call these directly unless you're building a custom shell.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 9. CSV import / export
|
|
210
|
+
|
|
211
|
+
Adopt this column shape across all records-based apps:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
scope,scopeRef,<field1>,<field2>,...
|
|
215
|
+
product,prod_abc,250,12.5,...
|
|
216
|
+
variant,prod_abc/var_500ml,260,12.5,...
|
|
217
|
+
batch,prod_abc/B-2024-03,255,12.5,...
|
|
218
|
+
facet,bread_type/sourdough,240,11.0,...
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- `scope` is the kind; `scopeRef` is the human-readable reference (the shell maps it to the canonical `ref`).
|
|
222
|
+
- Validation errors return a downloadable annotated CSV with an `error` column appended.
|
|
223
|
+
- Round-tripping (export → reimport unchanged) MUST be a no-op.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 10. Public-side hook contract
|
|
228
|
+
|
|
229
|
+
To keep widgets consistent across apps, expose one hook per record type (implemented in `@proveanything/ui-utils`):
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
|
|
233
|
+
|
|
234
|
+
const { data, source, isLoading } = useResolvedRecord({
|
|
235
|
+
appId,
|
|
236
|
+
recordType: 'nutrition',
|
|
237
|
+
// any combination of these — the hook walks the chain:
|
|
238
|
+
collectionId, productId, variantId, batchId, proofId,
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`source` is one of `'proof' | 'batch' | 'variant' | 'product' | 'facet' | 'default' | null`. UI can show a badge ("Showing batch-specific values") when useful.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 11. Telemetry
|
|
247
|
+
|
|
248
|
+
All admin shells emit these events. The `<RecordsAdminShell>` from `@proveanything/ui-utils` wires them automatically using `interactions.appendEvent` from the SDK — apps don't call this themselves.
|
|
249
|
+
|
|
250
|
+
| Event | Props |
|
|
251
|
+
|------------------------|------------------------------------------------------|
|
|
252
|
+
| `record.opened` | `appId, recordType, ref, source` |
|
|
253
|
+
| `record.saved` | `appId, recordType, ref, fieldsChanged` |
|
|
254
|
+
| `record.deleted` | `appId, recordType, ref` |
|
|
255
|
+
| `record.bulkApplied` | `appId, recordType, sourceRef, targetCount` |
|
|
256
|
+
| `record.imported` | `appId, recordType, rows, errors` |
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 12. Required reading for app authors
|
|
261
|
+
|
|
262
|
+
1. This document.
|
|
263
|
+
2. The companion **[UI utils guide](ui-utils.md)** — explains the React primitives (`<RecordsAdminShell>`, `useResolvedRecord`, etc.) that implement this pattern.
|
|
264
|
+
3. [PRODUCT_FACETS_SDK.md](PRODUCT_FACETS_SDK.md) — facet model.
|
|
265
|
+
4. [app-data-storage.md](app-data-storage.md) — `app.records` surface.
|
|
266
|
+
|
|
267
|
+
## 13. Migration checklist for existing apps
|
|
268
|
+
|
|
269
|
+
- [ ] Stop writing per-product data into `appConfiguration`.
|
|
270
|
+
- [ ] Move to `app.records` with `recordType` + `ref`.
|
|
271
|
+
- [ ] Adopt the `ref` syntax in §3.
|
|
272
|
+
- [ ] Add a `records` block to `app.manifest.json`.
|
|
273
|
+
- [ ] Replace bespoke admin browser with `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui`.
|
|
274
|
+
- [ ] Replace bespoke public hook with `useResolvedRecord`.
|
|
275
|
+
- [ ] Remove any "is variants enabled?" config — use `collection.variants` / `collection.batches` flags instead (§6).
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Appendix A — `ref` parser reference
|
|
280
|
+
|
|
281
|
+
This is the canonical implementation. Copy it into your app or import from `@proveanything/ui-utils/records`.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
type ParsedRef =
|
|
285
|
+
| { kind: 'default' }
|
|
286
|
+
| { kind: 'product'; productId: string }
|
|
287
|
+
| { kind: 'variant'; productId: string; variantId: string }
|
|
288
|
+
| { kind: 'batch'; productId: string; batchId: string }
|
|
289
|
+
| { kind: 'proof'; proofId: string }
|
|
290
|
+
| { kind: 'facet'; facetKey: string; valueKey: string };
|
|
291
|
+
|
|
292
|
+
export const buildRef = (p: ParsedRef): string => {
|
|
293
|
+
switch (p.kind) {
|
|
294
|
+
case 'default': return 'default';
|
|
295
|
+
case 'product': return `product:${p.productId}`;
|
|
296
|
+
case 'variant': return `variant:${p.productId}:${p.variantId}`;
|
|
297
|
+
case 'batch': return `batch:${p.productId}:${p.batchId}`;
|
|
298
|
+
case 'proof': return `proof:${p.proofId}`;
|
|
299
|
+
case 'facet': return `facet:${p.facetKey}:${p.valueKey}`;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export const parseRef = (ref: string): ParsedRef | null => {
|
|
304
|
+
if (ref === 'default') return { kind: 'default' };
|
|
305
|
+
const [head, ...rest] = ref.split(':');
|
|
306
|
+
switch (head) {
|
|
307
|
+
case 'product': return rest.length === 1 ? { kind: 'product', productId: rest[0] } : null;
|
|
308
|
+
case 'variant': return rest.length === 2 ? { kind: 'variant', productId: rest[0], variantId: rest[1] } : null;
|
|
309
|
+
case 'batch': return rest.length === 2 ? { kind: 'batch', productId: rest[0], batchId: rest[1] } : null;
|
|
310
|
+
case 'proof': return rest.length === 1 ? { kind: 'proof', proofId: rest[0] } : null;
|
|
311
|
+
case 'facet': return rest.length >= 2 ? { kind: 'facet', facetKey: rest[0], valueKey: rest.slice(1).join(':') } : null;
|
|
312
|
+
default: return null;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
```
|