@picked/client 0.0.61 → 0.0.62-next.10
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/README.md +0 -8
- package/dist/{api-CjGYS0lP.js → api-C11HFYrl.js} +1 -1
- package/dist/api-DXosW8uz.cjs +1 -0
- package/dist/cjs/form/index.js +1 -1
- package/dist/cjs/index.js +4 -4
- package/dist/cjs/plugins/create-experience/index.js +1 -1
- package/dist/cjs/plugins/dom-observer/index.js +1 -0
- package/dist/cjs/plugins/dynamic-experiences/index.js +1 -1
- package/dist/cjs/plugins/experience-browser-storage/index.js +1 -1
- package/dist/cjs/plugins/history-browser-storage/index.js +1 -1
- package/dist/cjs/plugins/responses-browser-storage/index.js +1 -1
- package/dist/cjs/plugins/single-page-app/index.js +1 -1
- package/dist/cjs/plugins/web-components/index.js +1 -1
- package/dist/cjs/server/index.js +1 -1
- package/dist/docs/actions.mdx +0 -1
- package/dist/docs/api/authentication.md +376 -0
- package/dist/docs/api/authorization-and-scopes.md +262 -0
- package/dist/docs/api/error-handling.md +211 -0
- package/dist/docs/api/index.md +172 -11
- package/dist/docs/api/pagination.md +176 -0
- package/dist/docs/image-optimisation-service.md +72 -0
- package/dist/docs/index.md +0 -2
- package/dist/docs/pickedql/data-sources.md +2 -2
- package/dist/docs/pickedql/dimensions.md +2 -0
- package/dist/docs/pickedql/schema.md +2 -2
- package/dist/docs/pickedql/virtual-functions.md +27 -0
- package/dist/docs/sdk/actions.md +0 -1
- package/dist/docs/sdk/content-security-policy.md +8 -7
- package/dist/docs/sdk/features.md +0 -1
- package/dist/docs/sdk/fields.md +2 -2
- package/dist/docs/sdk/index.md +0 -1
- package/dist/docs/sdk/plugins/create-experience.md +0 -1
- package/dist/docs/sdk/plugins/dynamic-experiences.md +2 -3
- package/dist/docs/sdk/plugins/window-object.md +0 -1
- package/dist/docs/sdk/predicates.md +0 -1
- package/dist/docs/workspaces.mdx +60 -1
- package/dist/dom-C3C6Fmwz.cjs +1 -0
- package/dist/dom-DGePXPpM.js +84 -0
- package/dist/esm/form/index.js +41 -34
- package/dist/esm/index.js +1020 -494
- package/dist/esm/plugins/create-experience/index.js +1 -1
- package/dist/esm/plugins/dom-observer/index.js +87 -0
- package/dist/esm/plugins/dynamic-experiences/index.js +133 -60
- package/dist/esm/plugins/experience-browser-storage/index.js +9 -6
- package/dist/esm/plugins/history-browser-storage/index.js +1 -1
- package/dist/esm/plugins/responses-browser-storage/index.js +1 -1
- package/dist/esm/plugins/single-page-app/index.js +1 -1
- package/dist/esm/plugins/web-components/index.js +1 -1
- package/dist/esm/server/index.js +23 -13
- package/dist/experience-instance-DImym3m_.js +419 -0
- package/dist/experience-instance-DygAdzbn.cjs +1 -0
- package/dist/rules-logic-D_CVBuv7.js +13 -0
- package/dist/rules-logic-H6P8xwHF.cjs +1 -0
- package/dist/types/client.d.ts +2 -2
- package/dist/types/entities/action/action.d.ts +6 -3
- package/dist/types/entities/action/action.d.ts.map +1 -1
- package/dist/types/entities/component/component.d.ts +39 -2
- package/dist/types/entities/component/component.d.ts.map +1 -1
- package/dist/types/entities/content/content.d.ts +37 -0
- package/dist/types/entities/content/content.d.ts.map +1 -0
- package/dist/types/entities/experience/experience.d.ts +69 -113
- package/dist/types/entities/experience/experience.d.ts.map +1 -1
- package/dist/types/entities/experience-instance/experience-instance.d.ts +17 -2
- package/dist/types/entities/experience-instance/experience-instance.d.ts.map +1 -1
- package/dist/types/entities/feature/feature.d.ts +26 -2
- package/dist/types/entities/feature/feature.d.ts.map +1 -1
- package/dist/types/entities/field/field.d.ts.map +1 -1
- package/dist/types/entities/framework/framework.d.ts +24 -3
- package/dist/types/entities/framework/framework.d.ts.map +1 -1
- package/dist/types/entities/plugin/plugin.d.ts +32 -1
- package/dist/types/entities/plugin/plugin.d.ts.map +1 -1
- package/dist/types/entities/predicate/predicate.d.ts +24 -3
- package/dist/types/entities/predicate/predicate.d.ts.map +1 -1
- package/dist/types/entities/session/session.d.ts +45 -0
- package/dist/types/entities/session/session.d.ts.map +1 -0
- package/dist/types/entities/step/step.d.ts +17 -2
- package/dist/types/entities/step/step.d.ts.map +1 -1
- package/dist/types/exports.d.ts +9 -3
- package/dist/types/exports.d.ts.map +1 -1
- package/dist/types/form/create-form.d.ts +55 -0
- package/dist/types/form/create-form.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lifecycle/lifecycle.d.ts +47 -0
- package/dist/types/lifecycle/lifecycle.d.ts.map +1 -0
- package/dist/types/lifecycle/rules-logic.d.ts +24 -0
- package/dist/types/lifecycle/rules-logic.d.ts.map +1 -0
- package/dist/types/lifecycle/state-snapshot.d.ts +37 -0
- package/dist/types/lifecycle/state-snapshot.d.ts.map +1 -0
- package/dist/types/logic-engine.d.ts +1 -1
- package/dist/types/logic-engine.d.ts.map +1 -1
- package/dist/types/picked.d.ts +141 -187
- package/dist/types/picked.d.ts.map +1 -1
- package/dist/types/plugins/dom-observer/index.d.ts +3 -0
- package/dist/types/plugins/dom-observer/index.d.ts.map +1 -0
- package/dist/types/plugins/dom-observer/plugin.d.ts +50 -0
- package/dist/types/plugins/dom-observer/plugin.d.ts.map +1 -0
- package/dist/types/plugins/dynamic-experiences/index.d.ts +2 -1
- package/dist/types/plugins/dynamic-experiences/index.d.ts.map +1 -1
- package/dist/types/plugins/dynamic-experiences/plugin.d.ts +18 -1
- package/dist/types/plugins/dynamic-experiences/plugin.d.ts.map +1 -1
- package/dist/types/plugins/dynamic-experiences/types.d.ts +63 -1
- package/dist/types/plugins/dynamic-experiences/types.d.ts.map +1 -1
- package/dist/types/plugins/experience-browser-storage/plugin.d.ts.map +1 -1
- package/dist/types/plugins/web-components/plugin.d.ts.map +1 -1
- package/dist/types/server/index.d.ts +7 -0
- package/dist/types/server/index.d.ts.map +1 -1
- package/dist/types/utils/events.d.ts +1 -1
- package/dist/types/utils/events.d.ts.map +1 -1
- package/dist/types/utils/picked-config.d.ts +12 -4
- package/dist/types/utils/picked-config.d.ts.map +1 -1
- package/dist/types/utils/rules-logic.d.ts +4 -9
- package/dist/types/utils/rules-logic.d.ts.map +1 -1
- package/package.json +12 -2
- package/dist/api-BarClNGB.cjs +0 -1
- package/dist/docs/reference/index.md +0 -11
- package/dist/docs/tooling/index.md +0 -19
- package/dist/experience-instance-DC2gKwA6.cjs +0 -1
- package/dist/experience-instance-DJaNc5Ob.js +0 -486
- package/dist/rules-logic-BTIxfrJJ.js +0 -240
- package/dist/rules-logic-DNLOv90w.cjs +0 -1
- package/dist/types/utils/state-snaphot.d.ts +0 -26
- package/dist/types/utils/state-snaphot.d.ts.map +0 -1
- package/dist/types/utils/user-session.d.ts +0 -12
- package/dist/types/utils/user-session.d.ts.map +0 -1
- /package/dist/docs/{tooling/chrome-extension.md → chrome-extension.md} +0 -0
- /package/dist/docs/{tooling/gtm-template.md → gtm-template.md} +0 -0
- /package/dist/docs/{tooling/mcp.md → mcp.md} +0 -0
- /package/dist/docs/{tooling/vite-plugin.md → vite-plugin.md} +0 -0
- /package/dist/{id-Bv44ilEo.cjs → id-Cg7fVl1O.cjs} +0 -0
- /package/dist/{id-CRqy981z.js → id-n2yrHvNo.js} +0 -0
- /package/dist/{object-C82v79xc.js → object-DvUDOytC.js} +0 -0
- /package/dist/{object-CUJOjanb.cjs → object-ThO1DEyl.cjs} +0 -0
- /package/dist/{storage-B6lQtc4-.cjs → storage-BXSlLTRa.cjs} +0 -0
- /package/dist/{storage-6Xk0uAxB.js → storage-hOHCCXvP.js} +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Error handling
|
|
6
|
+
|
|
7
|
+
The Picked API returns errors using the standard GraphQL error format. This guide covers the error response shape, all error codes, and practical strategies for handling errors.
|
|
8
|
+
|
|
9
|
+
## Error response shape
|
|
10
|
+
|
|
11
|
+
When an operation fails, the API returns an `errors` array in the response body. Each error object contains:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"data": null,
|
|
16
|
+
"errors": [
|
|
17
|
+
{
|
|
18
|
+
"message": "A human-readable description of the error",
|
|
19
|
+
"path": ["mutationName"],
|
|
20
|
+
"extensions": {
|
|
21
|
+
"code": "ERROR_CODE"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Field | Description |
|
|
29
|
+
|-------|-------------|
|
|
30
|
+
| `message` | A human-readable error description. Suitable for logging but not for programmatic handling. |
|
|
31
|
+
| `path` | The path to the field in the query that caused the error. |
|
|
32
|
+
| `extensions.code` | A machine-readable error code. Use this for programmatic error handling. |
|
|
33
|
+
|
|
34
|
+
Multiple errors may appear in a single response. The `data` field may be `null` if the entire operation failed, or it may contain partial data if only some fields failed.
|
|
35
|
+
|
|
36
|
+
## Authentication error codes
|
|
37
|
+
|
|
38
|
+
These errors relate to session creation, account management, OAuth, magic links, password operations, and MFA. They map to the `AuthErrorCode` enum in the schema.
|
|
39
|
+
|
|
40
|
+
| Code | Description |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `INVALID_CREDENTIALS` | The email/password combination is incorrect. |
|
|
43
|
+
| `EMAIL_NOT_CONFIRMED` | The account exists but the email address has not been verified. See [Authentication](/api/authentication#email-verification). |
|
|
44
|
+
| `USER_NOT_FOUND` | No account exists with the provided email address. |
|
|
45
|
+
| `USER_ALREADY_EXISTS` | An account with this email already exists. Returned during account creation. |
|
|
46
|
+
| `WEAK_PASSWORD` | The provided password does not meet strength requirements. |
|
|
47
|
+
| `INVALID_TOKEN` | The token (verification, reset, or magic link) is invalid or malformed. |
|
|
48
|
+
| `TOKEN_EXPIRED` | The token has expired. The caller should request a new one. |
|
|
49
|
+
| `MFA_CHALLENGE_FAILED` | The MFA verification code is incorrect. |
|
|
50
|
+
| `MFA_FACTOR_NOT_FOUND` | The specified MFA factor does not exist or does not belong to the caller. |
|
|
51
|
+
| `OAUTH_PROVIDER_ERROR` | An error occurred communicating with the OAuth provider (e.g., Google). |
|
|
52
|
+
| `RATE_LIMITED` | Too many requests in a short period. The caller should wait and retry. |
|
|
53
|
+
|
|
54
|
+
### Example: invalid credentials
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"data": {
|
|
59
|
+
"sessionCreate": {
|
|
60
|
+
"session": null,
|
|
61
|
+
"requiresMfa": false,
|
|
62
|
+
"mfaFactors": null
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"errors": [
|
|
66
|
+
{
|
|
67
|
+
"message": "Invalid credentials",
|
|
68
|
+
"path": ["sessionCreate"],
|
|
69
|
+
"extensions": {
|
|
70
|
+
"code": "INVALID_CREDENTIALS"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Authorization error codes
|
|
78
|
+
|
|
79
|
+
Authorization errors occur when the caller lacks the required [scope](./authorization-and-scopes.md) for an operation.
|
|
80
|
+
|
|
81
|
+
| Code | Description |
|
|
82
|
+
|------|-------------|
|
|
83
|
+
| `UNAUTHORIZED` | No valid authentication was provided. The request is missing the `Authorization` header or the `x-api-key` header, or the token/key is invalid. |
|
|
84
|
+
| `FORBIDDEN` | The caller is authenticated but does not have the required scope for the operation. |
|
|
85
|
+
|
|
86
|
+
### Example: insufficient scope
|
|
87
|
+
|
|
88
|
+
A caller with only `organization/read` attempting to delete an organization receives:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"data": {
|
|
93
|
+
"organizationDelete": null
|
|
94
|
+
},
|
|
95
|
+
"errors": [
|
|
96
|
+
{
|
|
97
|
+
"message": "Insufficient permissions",
|
|
98
|
+
"path": ["organizationDelete"],
|
|
99
|
+
"extensions": {
|
|
100
|
+
"code": "FORBIDDEN"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Organization, member, and invitation error codes
|
|
108
|
+
|
|
109
|
+
These errors relate to organization management, member operations, and invitation workflows. They map to the `ErrorCode` enum in the schema.
|
|
110
|
+
|
|
111
|
+
### Organization errors
|
|
112
|
+
|
|
113
|
+
| Code | Description |
|
|
114
|
+
|------|-------------|
|
|
115
|
+
| `ORGANIZATION_NOT_FOUND` | The specified organization does not exist or the caller does not have access to it. |
|
|
116
|
+
| `ORGANIZATION_ALREADY_EXISTS` | An organization with this name already exists. |
|
|
117
|
+
| `SLUG_ALREADY_EXISTS` | The requested slug is already in use by another organization. |
|
|
118
|
+
| `INVALID_SLUG` | The slug contains invalid characters. Slugs must be lowercase alphanumeric with hyphens. |
|
|
119
|
+
|
|
120
|
+
### Member errors
|
|
121
|
+
|
|
122
|
+
| Code | Description |
|
|
123
|
+
|------|-------------|
|
|
124
|
+
| `MEMBER_NOT_FOUND` | The specified user is not a member of the organization. |
|
|
125
|
+
| `MEMBER_ALREADY_EXISTS` | The user is already a member of the organization. |
|
|
126
|
+
| `CANNOT_REMOVE_PRIMARY_OWNER` | The primary owner of an organization cannot be removed. |
|
|
127
|
+
| `CANNOT_CHANGE_OWN_ROLE` | A member cannot change their own role. Another owner must perform this action. |
|
|
128
|
+
|
|
129
|
+
### Invitation errors
|
|
130
|
+
|
|
131
|
+
| Code | Description |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| `INVITATION_NOT_FOUND` | The specified invitation does not exist. |
|
|
134
|
+
| `INVITATION_EXPIRED` | The invitation has expired and can no longer be accepted. |
|
|
135
|
+
| `INVITATION_ALREADY_ACCEPTED` | The invitation has already been accepted. |
|
|
136
|
+
|
|
137
|
+
### Project errors
|
|
138
|
+
|
|
139
|
+
| Code | Description |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| `PROJECT_NOT_FOUND` | The specified project does not exist or the caller does not have access. |
|
|
142
|
+
|
|
143
|
+
### General errors
|
|
144
|
+
|
|
145
|
+
| Code | Description |
|
|
146
|
+
|------|-------------|
|
|
147
|
+
| `VALIDATION_ERROR` | The input failed validation. Check the `message` field for details about which field is invalid. |
|
|
148
|
+
| `INTERNAL_ERROR` | An unexpected server error occurred. Retry the request or contact support if the issue persists. |
|
|
149
|
+
|
|
150
|
+
## Handling errors in practice
|
|
151
|
+
|
|
152
|
+
### Check for errors first
|
|
153
|
+
|
|
154
|
+
Every response should be checked for the presence of an `errors` array before processing `data`:
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const response = await fetch('/graphql', {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({ query, variables }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await response.json();
|
|
167
|
+
|
|
168
|
+
if (result.errors) {
|
|
169
|
+
for (const error of result.errors) {
|
|
170
|
+
const code = error.extensions?.code;
|
|
171
|
+
|
|
172
|
+
switch (code) {
|
|
173
|
+
case 'UNAUTHORIZED':
|
|
174
|
+
// Redirect to login
|
|
175
|
+
break;
|
|
176
|
+
case 'FORBIDDEN':
|
|
177
|
+
// Show a permissions error
|
|
178
|
+
break;
|
|
179
|
+
case 'RATE_LIMITED':
|
|
180
|
+
// Wait and retry
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
// Log or display the error message
|
|
184
|
+
console.error(error.message);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Refresh expired tokens
|
|
191
|
+
|
|
192
|
+
If a request returns `UNAUTHORIZED` and the caller has a refresh token, attempt a [session refresh](/api/authentication#session-refresh) before treating it as a login failure:
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
if (code === 'UNAUTHORIZED' && refreshToken) {
|
|
196
|
+
const refreshResult = await sessionRefresh(refreshToken);
|
|
197
|
+
if (refreshResult.session) {
|
|
198
|
+
// Retry the original request with the new access token
|
|
199
|
+
} else {
|
|
200
|
+
// Redirect to login
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Handle partial data
|
|
206
|
+
|
|
207
|
+
GraphQL can return partial data alongside errors. For example, a query that fetches an organization and its members might succeed for the organization but fail for the members if the caller lacks `organization_members/read`. In this case, the response contains both `data` (with the organization) and `errors` (for the members field). Always check both.
|
|
208
|
+
|
|
209
|
+
### Retry transient failures
|
|
210
|
+
|
|
211
|
+
`INTERNAL_ERROR` and `RATE_LIMITED` are transient. For `RATE_LIMITED`, implement exponential backoff before retrying. For `INTERNAL_ERROR`, a single retry is usually sufficient.
|
package/dist/docs/api/index.md
CHANGED
|
@@ -2,31 +2,192 @@
|
|
|
2
2
|
layout: docs
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# Getting started
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
This guide walks you through your first interactions with the Picked GraphQL API, from sending a simple ping to making authenticated queries and mutations.
|
|
8
8
|
|
|
9
9
|
## Playground
|
|
10
10
|
|
|
11
|
-
Explore the API and run queries interactively
|
|
11
|
+
Explore the API and run queries interactively in the [GraphQL Playground](https://api.picked.app/api/graphql).
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## API endpoint
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
All requests go to a single endpoint:
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
|
|
18
|
+
POST https://api.picked.app/api/graphql
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Every request must use the `POST` method with a JSON body containing a `query` field and an optional `variables` field:
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"query": "query { ... }",
|
|
26
|
+
"variables": { }
|
|
27
|
+
}
|
|
28
|
+
```
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
Set the `Content-Type` header to `application/json` on every request.
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
33
|
curl -X POST https://api.picked.app/api/graphql \
|
|
29
34
|
-H "Content-Type: application/json" \
|
|
30
|
-
-
|
|
31
|
-
|
|
35
|
+
-d '{"query": "{ ping }"}'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Ping the API
|
|
39
|
+
|
|
40
|
+
Verify that the API is reachable with the `ping` query. This query does not require authentication.
|
|
41
|
+
|
|
42
|
+
```graphql
|
|
43
|
+
query {
|
|
44
|
+
ping
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Response:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"data": {
|
|
53
|
+
"ping": "pong"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If you receive this response, the API is up and ready.
|
|
59
|
+
|
|
60
|
+
## Authenticate
|
|
61
|
+
|
|
62
|
+
Most operations require an access token. You obtain one by creating a session with your email and password.
|
|
63
|
+
|
|
64
|
+
```graphql
|
|
65
|
+
mutation {
|
|
66
|
+
sessionCreate(input: {
|
|
67
|
+
email: "you@example.com"
|
|
68
|
+
password: "your-password"
|
|
69
|
+
}) {
|
|
70
|
+
session {
|
|
71
|
+
accessToken
|
|
72
|
+
refreshToken
|
|
73
|
+
expiresAt
|
|
74
|
+
}
|
|
75
|
+
requiresMfa
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A successful response returns a `Session` containing an `accessToken` and a `refreshToken`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"data": {
|
|
85
|
+
"sessionCreate": {
|
|
86
|
+
"session": {
|
|
87
|
+
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
|
|
88
|
+
"refreshToken": "v1.refresh-token...",
|
|
89
|
+
"expiresAt": "2025-01-15T12:00:00Z"
|
|
90
|
+
},
|
|
91
|
+
"requiresMfa": false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If `requiresMfa` is `true`, you need to complete the MFA challenge before receiving a session. See the [Authentication](./authentication.md) guide for details.
|
|
98
|
+
|
|
99
|
+
For server-to-server integrations, prefer [API key authentication](/api/authentication#api-key-authentication) over session tokens.
|
|
100
|
+
|
|
101
|
+
## Make an authenticated query
|
|
102
|
+
|
|
103
|
+
Pass the access token in the `Authorization` header as a Bearer token:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Now you can query resources that require authentication. For example, fetch your user profile:
|
|
110
|
+
|
|
111
|
+
```graphql
|
|
112
|
+
query {
|
|
113
|
+
user {
|
|
114
|
+
id
|
|
115
|
+
email
|
|
116
|
+
createdAt
|
|
117
|
+
organizations(first: 5) {
|
|
118
|
+
edges {
|
|
119
|
+
node {
|
|
120
|
+
id
|
|
121
|
+
name
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
32
127
|
```
|
|
128
|
+
|
|
129
|
+
## Make a mutation
|
|
130
|
+
|
|
131
|
+
Mutations follow the same pattern: send a `POST` request with the `Authorization` header. For example, create an organization:
|
|
132
|
+
|
|
133
|
+
```graphql
|
|
134
|
+
mutation {
|
|
135
|
+
organizationCreate(input: {
|
|
136
|
+
name: "My Company"
|
|
137
|
+
slug: "my-company"
|
|
138
|
+
}) {
|
|
139
|
+
organization {
|
|
140
|
+
id
|
|
141
|
+
name
|
|
142
|
+
slug
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Response:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"data": {
|
|
153
|
+
"organizationCreate": {
|
|
154
|
+
"organization": {
|
|
155
|
+
"id": "org_abc123",
|
|
156
|
+
"name": "My Company",
|
|
157
|
+
"slug": "my-company"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Every mutation accepts an `input` argument and returns a payload object. The payload contains the created or updated resource on success, or errors in the top-level `errors` array on failure.
|
|
165
|
+
|
|
166
|
+
## Error handling basics
|
|
167
|
+
|
|
168
|
+
When something goes wrong, the API returns errors in the standard GraphQL `errors` array:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"data": null,
|
|
173
|
+
"errors": [
|
|
174
|
+
{
|
|
175
|
+
"message": "Invalid credentials",
|
|
176
|
+
"extensions": {
|
|
177
|
+
"code": "INVALID_CREDENTIALS"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Check the `extensions.code` field for a machine-readable error code you can use to handle specific failures programmatically. See the [Error Handling](./error-handling.md) guide for the full list of error codes.
|
|
185
|
+
|
|
186
|
+
## Next steps
|
|
187
|
+
|
|
188
|
+
Now that you can authenticate and make requests, explore these guides to go deeper:
|
|
189
|
+
|
|
190
|
+
- [Authentication](./authentication.md) — email/password, OAuth, magic links, MFA, API keys, and session management
|
|
191
|
+
- [Authorization and Scopes](./authorization-and-scopes.md) — understand what each role can access and how scopes control permissions
|
|
192
|
+
- [Pagination](./pagination.md) — navigate large result sets with cursor-based pagination
|
|
193
|
+
- [Error Handling](./error-handling.md) — handle every error code the API can return
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Pagination
|
|
6
|
+
|
|
7
|
+
The Picked API uses Relay-style cursor-based pagination for all list fields that may return large result sets. This guide explains the connection pattern and demonstrates how to paginate through results.
|
|
8
|
+
|
|
9
|
+
## Connection pattern
|
|
10
|
+
|
|
11
|
+
Paginated fields return a **connection** object with the following shape:
|
|
12
|
+
|
|
13
|
+
```graphql
|
|
14
|
+
type ExampleConnection {
|
|
15
|
+
edges: [ExampleEdge!]!
|
|
16
|
+
pageInfo: PageInfo!
|
|
17
|
+
totalCount: Int!
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type ExampleEdge {
|
|
21
|
+
cursor: String!
|
|
22
|
+
node: Example!
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type PageInfo {
|
|
26
|
+
hasNextPage: Boolean!
|
|
27
|
+
hasPreviousPage: Boolean!
|
|
28
|
+
startCursor: String
|
|
29
|
+
endCursor: String
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Field | Description |
|
|
34
|
+
|-------|-------------|
|
|
35
|
+
| `edges` | An ordered list of edges, each containing a `cursor` and a `node` (the actual data object) |
|
|
36
|
+
| `pageInfo.hasNextPage` | `true` if more results exist after the last edge |
|
|
37
|
+
| `pageInfo.hasPreviousPage` | `true` if results exist before the first edge |
|
|
38
|
+
| `pageInfo.startCursor` | Cursor of the first edge in the current page |
|
|
39
|
+
| `pageInfo.endCursor` | Cursor of the last edge in the current page |
|
|
40
|
+
| `totalCount` | Total number of items across all pages |
|
|
41
|
+
|
|
42
|
+
## Forward pagination with `first` and `after`
|
|
43
|
+
|
|
44
|
+
To paginate forward through results, use the `first` argument to specify the page size and the `after` argument to specify the cursor to start after.
|
|
45
|
+
|
|
46
|
+
### First page
|
|
47
|
+
|
|
48
|
+
Omit the `after` argument to fetch the first page:
|
|
49
|
+
|
|
50
|
+
```graphql
|
|
51
|
+
query {
|
|
52
|
+
organizations(input: { first: 10 }) {
|
|
53
|
+
edges {
|
|
54
|
+
cursor
|
|
55
|
+
node {
|
|
56
|
+
id
|
|
57
|
+
name
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
pageInfo {
|
|
61
|
+
hasNextPage
|
|
62
|
+
endCursor
|
|
63
|
+
}
|
|
64
|
+
totalCount
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Next page
|
|
70
|
+
|
|
71
|
+
Pass the `endCursor` from the previous response as the `after` argument:
|
|
72
|
+
|
|
73
|
+
```graphql
|
|
74
|
+
query {
|
|
75
|
+
organizations(input: { first: 10, after: "cursor_from_previous_page" }) {
|
|
76
|
+
edges {
|
|
77
|
+
cursor
|
|
78
|
+
node {
|
|
79
|
+
id
|
|
80
|
+
name
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
pageInfo {
|
|
84
|
+
hasNextPage
|
|
85
|
+
endCursor
|
|
86
|
+
}
|
|
87
|
+
totalCount
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Checking for more pages
|
|
93
|
+
|
|
94
|
+
Before requesting the next page, check the `hasNextPage` field:
|
|
95
|
+
|
|
96
|
+
- `hasNextPage: true` — more results are available; pass `endCursor` to the next request's `after` argument
|
|
97
|
+
- `hasNextPage: false` — all results have been fetched; stop paginating
|
|
98
|
+
|
|
99
|
+
## Full paginated query example
|
|
100
|
+
|
|
101
|
+
The following example fetches all members of an organization, page by page:
|
|
102
|
+
|
|
103
|
+
```graphql
|
|
104
|
+
# Page 1
|
|
105
|
+
query {
|
|
106
|
+
organization(input: { slug: "my-company" }) {
|
|
107
|
+
members(first: 20) {
|
|
108
|
+
edges {
|
|
109
|
+
cursor
|
|
110
|
+
node {
|
|
111
|
+
id
|
|
112
|
+
userEmail
|
|
113
|
+
role
|
|
114
|
+
joinedAt
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
pageInfo {
|
|
118
|
+
hasNextPage
|
|
119
|
+
endCursor
|
|
120
|
+
}
|
|
121
|
+
totalCount
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If `pageInfo.hasNextPage` is `true`, fetch the next page:
|
|
128
|
+
|
|
129
|
+
```graphql
|
|
130
|
+
# Page 2
|
|
131
|
+
query {
|
|
132
|
+
organization(input: { slug: "my-company" }) {
|
|
133
|
+
members(first: 20, after: "cursor_from_page_1") {
|
|
134
|
+
edges {
|
|
135
|
+
cursor
|
|
136
|
+
node {
|
|
137
|
+
id
|
|
138
|
+
userEmail
|
|
139
|
+
role
|
|
140
|
+
joinedAt
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
pageInfo {
|
|
144
|
+
hasNextPage
|
|
145
|
+
endCursor
|
|
146
|
+
}
|
|
147
|
+
totalCount
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Continue until `hasNextPage` returns `false`.
|
|
154
|
+
|
|
155
|
+
## Paginated fields
|
|
156
|
+
|
|
157
|
+
The following fields in the schema return connection types and support `first`/`after` pagination:
|
|
158
|
+
|
|
159
|
+
| Parent Type | Field | Connection Type |
|
|
160
|
+
|------------|-------|----------------|
|
|
161
|
+
| `Query` | `organizations` | `OrganizationConnection` |
|
|
162
|
+
| `User` | `organizations` | `OrganizationConnection` |
|
|
163
|
+
| `User` | `invitations` | `ViewerInvitationConnection` |
|
|
164
|
+
| `Organization` | `members` | `OrganizationMemberConnection` |
|
|
165
|
+
| `Organization` | `invitations` | `OrganizationInvitationConnection` |
|
|
166
|
+
| `Organization` | `projects` | `ProjectConnection` |
|
|
167
|
+
| `Project` | `reports` | `ProjectReportConnection` |
|
|
168
|
+
|
|
169
|
+
All of these fields accept the same `first` and `after` arguments and return the standard `edges`, `pageInfo`, and `totalCount` structure.
|
|
170
|
+
|
|
171
|
+
## Tips
|
|
172
|
+
|
|
173
|
+
- **Default page size** — if `first` is omitted, the API applies a default limit. Explicitly setting `first` ensures predictable page sizes.
|
|
174
|
+
- **Cursors are opaque** — cursor strings should be treated as opaque tokens. Do not parse, construct, or modify them. Pass them exactly as received from the API.
|
|
175
|
+
- **Stable ordering** — results within a connection are returned in a stable order. The same cursor always refers to the same position in the result set.
|
|
176
|
+
- **Combine with other fields** — pagination arguments can be used alongside other query fields. For example, fetching an organization and its first page of members in a single request is common and efficient.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Image optimisation service
|
|
6
|
+
|
|
7
|
+
The Picked image optimisation service is a hosted endpoint at `https://img.picked-content.com`. It fetches images from allowed source domains, resizes them, re-encodes to modern formats (WebP or AVIF), and caches the result.
|
|
8
|
+
|
|
9
|
+
## Endpoint
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
GET https://img.picked-content.com/_next/image?url={source}&w={width}&q={quality}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Parameter | Description |
|
|
16
|
+
|-----------|-------------|
|
|
17
|
+
| `url` | URL-encoded URL of the source image. Must point to an allowed source domain (see below). |
|
|
18
|
+
| `w` | Target width in pixels. Must match one of the supported widths. |
|
|
19
|
+
| `q` | Target quality (`25`, `50`, `75`, or `100`). |
|
|
20
|
+
|
|
21
|
+
The service performs content negotiation: if the requesting browser advertises `image/avif` or `image/webp` in its `Accept` header, the response uses that format. Otherwise the response uses the source format.
|
|
22
|
+
|
|
23
|
+
## Allowed source domains
|
|
24
|
+
|
|
25
|
+
The service only fetches from these hostnames:
|
|
26
|
+
|
|
27
|
+
- `**.picked.app`
|
|
28
|
+
|
|
29
|
+
A request whose `url` parameter resolves to any other hostname returns `400 Bad Request`.
|
|
30
|
+
|
|
31
|
+
## Supported widths
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
640, 750, 828, 1080, 1200, 1920, 2048, 3840
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requests with any other width return `400 Bad Request`. Pick the smallest width that covers your target rendering size at the user's device pixel ratio.
|
|
38
|
+
|
|
39
|
+
## Supported qualities
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
25, 50, 75, 100
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Lower values reduce bandwidth significantly. `75` is a good default for most images; use `25` or `50` for thumbnails on slow networks.
|
|
46
|
+
|
|
47
|
+
## Responsive images
|
|
48
|
+
|
|
49
|
+
Combine the endpoint with the standard `srcset` attribute to let browsers pick the right width per viewport:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<img
|
|
53
|
+
src="https://img.picked-content.com/_next/image?url=https%3A%2F%2Fcdn.picked.app%2Fhero.png&w=1080&q=75"
|
|
54
|
+
srcset="
|
|
55
|
+
https://img.picked-content.com/_next/image?url=https%3A%2F%2Fcdn.picked.app%2Fhero.png&w=640&q=75 640w,
|
|
56
|
+
https://img.picked-content.com/_next/image?url=https%3A%2F%2Fcdn.picked.app%2Fhero.png&w=1080&q=75 1080w,
|
|
57
|
+
https://img.picked-content.com/_next/image?url=https%3A%2F%2Fcdn.picked.app%2Fhero.png&w=1920&q=75 1920w
|
|
58
|
+
"
|
|
59
|
+
sizes="(max-width: 768px) 100vw, 720px"
|
|
60
|
+
alt="..."
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Caching
|
|
65
|
+
|
|
66
|
+
Optimised responses are cached by the upstream CDN. The minimum cache TTL is 60 seconds; the actual lifetime depends on the CDN's eviction policy. If you change the source image at the same URL, expect the optimised variant to remain cached for at least that interval.
|
|
67
|
+
|
|
68
|
+
## Security
|
|
69
|
+
|
|
70
|
+
- SVG sources are not optimised. Requests for SVG return the original bytes if the source is allowed; the service never executes embedded scripts.
|
|
71
|
+
- Optimised responses carry a strict `Content-Security-Policy` (`default-src *; script-src 'none'; sandbox;`) so even a compromised source cannot run scripts in a viewing browser.
|
|
72
|
+
- The response uses `Content-Disposition: inline`, never `attachment`.
|
package/dist/docs/index.md
CHANGED
|
@@ -26,7 +26,6 @@ Add the SDK to your site and initialize the client. Copy the snippet from your p
|
|
|
26
26
|
import { createClient } from '@picked/client'
|
|
27
27
|
|
|
28
28
|
const picked = createClient({
|
|
29
|
-
organizationId: 'your-organization-id',
|
|
30
29
|
projectId: 'your-project-id',
|
|
31
30
|
plugins: [],
|
|
32
31
|
})
|
|
@@ -44,7 +43,6 @@ Register components when creating the client. Each key in the `components` objec
|
|
|
44
43
|
import { createClient } from '@picked/client'
|
|
45
44
|
|
|
46
45
|
const picked = createClient({
|
|
47
|
-
organizationId: 'your-organization-id',
|
|
48
46
|
projectId: 'your-project-id',
|
|
49
47
|
components: {
|
|
50
48
|
'welcome-survey': YourComponent,
|
|
@@ -104,8 +104,8 @@ Columns returned:
|
|
|
104
104
|
|
|
105
105
|
| Column | Type | Description |
|
|
106
106
|
|---|---|---|
|
|
107
|
-
| `source` | String | Edge start, formatted as `NN.
|
|
108
|
-
| `target` | String | Edge end, formatted as `NN.
|
|
107
|
+
| `source` | String | Edge start, formatted as `NN. <step title>` (e.g., `001. Welcome screen`). PickedQL joins `steps` to resolve the title. |
|
|
108
|
+
| `target` | String | Edge end, formatted as `NN. <step title>`. |
|
|
109
109
|
| `users` | UInt64 | Distinct `user_canonical_id` count that traversed this edge. |
|
|
110
110
|
|
|
111
111
|
:::note
|
|
@@ -26,6 +26,8 @@ The `steps`, `experiences`, and `projects` sources are reference tables sourced
|
|
|
26
26
|
| `steps`, `experiences` | `WHERE project_id = '<project_id>'` |
|
|
27
27
|
| `projects` | `WHERE id = '<project_id>'` (the row's `id` IS the project id) |
|
|
28
28
|
|
|
29
|
+
When auto-joined, these reference tables also receive a tenant equality on the JOIN's `ON` clause (e.g. `AND st.project_id = '<project_id>'`, `AND pr.id = '<project_id>'`) so ClickHouse prunes the dim scan instead of reading the full table. The predicate is added to `ON` rather than `WHERE` to preserve `LEFT JOIN` semantics.
|
|
30
|
+
|
|
29
31
|
Analyst-facing column names are prefixed (`step_title`, `project_name`, etc.) to avoid collisions across sources — for example, `steps.title` and `experiences.title` both exist, but you reference them as `step_title` and `experience_title`.
|
|
30
32
|
|
|
31
33
|
```sql
|