@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.
Files changed (135) hide show
  1. package/README.md +0 -8
  2. package/dist/{api-CjGYS0lP.js → api-C11HFYrl.js} +1 -1
  3. package/dist/api-DXosW8uz.cjs +1 -0
  4. package/dist/cjs/form/index.js +1 -1
  5. package/dist/cjs/index.js +4 -4
  6. package/dist/cjs/plugins/create-experience/index.js +1 -1
  7. package/dist/cjs/plugins/dom-observer/index.js +1 -0
  8. package/dist/cjs/plugins/dynamic-experiences/index.js +1 -1
  9. package/dist/cjs/plugins/experience-browser-storage/index.js +1 -1
  10. package/dist/cjs/plugins/history-browser-storage/index.js +1 -1
  11. package/dist/cjs/plugins/responses-browser-storage/index.js +1 -1
  12. package/dist/cjs/plugins/single-page-app/index.js +1 -1
  13. package/dist/cjs/plugins/web-components/index.js +1 -1
  14. package/dist/cjs/server/index.js +1 -1
  15. package/dist/docs/actions.mdx +0 -1
  16. package/dist/docs/api/authentication.md +376 -0
  17. package/dist/docs/api/authorization-and-scopes.md +262 -0
  18. package/dist/docs/api/error-handling.md +211 -0
  19. package/dist/docs/api/index.md +172 -11
  20. package/dist/docs/api/pagination.md +176 -0
  21. package/dist/docs/image-optimisation-service.md +72 -0
  22. package/dist/docs/index.md +0 -2
  23. package/dist/docs/pickedql/data-sources.md +2 -2
  24. package/dist/docs/pickedql/dimensions.md +2 -0
  25. package/dist/docs/pickedql/schema.md +2 -2
  26. package/dist/docs/pickedql/virtual-functions.md +27 -0
  27. package/dist/docs/sdk/actions.md +0 -1
  28. package/dist/docs/sdk/content-security-policy.md +8 -7
  29. package/dist/docs/sdk/features.md +0 -1
  30. package/dist/docs/sdk/fields.md +2 -2
  31. package/dist/docs/sdk/index.md +0 -1
  32. package/dist/docs/sdk/plugins/create-experience.md +0 -1
  33. package/dist/docs/sdk/plugins/dynamic-experiences.md +2 -3
  34. package/dist/docs/sdk/plugins/window-object.md +0 -1
  35. package/dist/docs/sdk/predicates.md +0 -1
  36. package/dist/docs/workspaces.mdx +60 -1
  37. package/dist/dom-C3C6Fmwz.cjs +1 -0
  38. package/dist/dom-DGePXPpM.js +84 -0
  39. package/dist/esm/form/index.js +41 -34
  40. package/dist/esm/index.js +1020 -494
  41. package/dist/esm/plugins/create-experience/index.js +1 -1
  42. package/dist/esm/plugins/dom-observer/index.js +87 -0
  43. package/dist/esm/plugins/dynamic-experiences/index.js +133 -60
  44. package/dist/esm/plugins/experience-browser-storage/index.js +9 -6
  45. package/dist/esm/plugins/history-browser-storage/index.js +1 -1
  46. package/dist/esm/plugins/responses-browser-storage/index.js +1 -1
  47. package/dist/esm/plugins/single-page-app/index.js +1 -1
  48. package/dist/esm/plugins/web-components/index.js +1 -1
  49. package/dist/esm/server/index.js +23 -13
  50. package/dist/experience-instance-DImym3m_.js +419 -0
  51. package/dist/experience-instance-DygAdzbn.cjs +1 -0
  52. package/dist/rules-logic-D_CVBuv7.js +13 -0
  53. package/dist/rules-logic-H6P8xwHF.cjs +1 -0
  54. package/dist/types/client.d.ts +2 -2
  55. package/dist/types/entities/action/action.d.ts +6 -3
  56. package/dist/types/entities/action/action.d.ts.map +1 -1
  57. package/dist/types/entities/component/component.d.ts +39 -2
  58. package/dist/types/entities/component/component.d.ts.map +1 -1
  59. package/dist/types/entities/content/content.d.ts +37 -0
  60. package/dist/types/entities/content/content.d.ts.map +1 -0
  61. package/dist/types/entities/experience/experience.d.ts +69 -113
  62. package/dist/types/entities/experience/experience.d.ts.map +1 -1
  63. package/dist/types/entities/experience-instance/experience-instance.d.ts +17 -2
  64. package/dist/types/entities/experience-instance/experience-instance.d.ts.map +1 -1
  65. package/dist/types/entities/feature/feature.d.ts +26 -2
  66. package/dist/types/entities/feature/feature.d.ts.map +1 -1
  67. package/dist/types/entities/field/field.d.ts.map +1 -1
  68. package/dist/types/entities/framework/framework.d.ts +24 -3
  69. package/dist/types/entities/framework/framework.d.ts.map +1 -1
  70. package/dist/types/entities/plugin/plugin.d.ts +32 -1
  71. package/dist/types/entities/plugin/plugin.d.ts.map +1 -1
  72. package/dist/types/entities/predicate/predicate.d.ts +24 -3
  73. package/dist/types/entities/predicate/predicate.d.ts.map +1 -1
  74. package/dist/types/entities/session/session.d.ts +45 -0
  75. package/dist/types/entities/session/session.d.ts.map +1 -0
  76. package/dist/types/entities/step/step.d.ts +17 -2
  77. package/dist/types/entities/step/step.d.ts.map +1 -1
  78. package/dist/types/exports.d.ts +9 -3
  79. package/dist/types/exports.d.ts.map +1 -1
  80. package/dist/types/form/create-form.d.ts +55 -0
  81. package/dist/types/form/create-form.d.ts.map +1 -1
  82. package/dist/types/index.d.ts +3 -1
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/lifecycle/lifecycle.d.ts +47 -0
  85. package/dist/types/lifecycle/lifecycle.d.ts.map +1 -0
  86. package/dist/types/lifecycle/rules-logic.d.ts +24 -0
  87. package/dist/types/lifecycle/rules-logic.d.ts.map +1 -0
  88. package/dist/types/lifecycle/state-snapshot.d.ts +37 -0
  89. package/dist/types/lifecycle/state-snapshot.d.ts.map +1 -0
  90. package/dist/types/logic-engine.d.ts +1 -1
  91. package/dist/types/logic-engine.d.ts.map +1 -1
  92. package/dist/types/picked.d.ts +141 -187
  93. package/dist/types/picked.d.ts.map +1 -1
  94. package/dist/types/plugins/dom-observer/index.d.ts +3 -0
  95. package/dist/types/plugins/dom-observer/index.d.ts.map +1 -0
  96. package/dist/types/plugins/dom-observer/plugin.d.ts +50 -0
  97. package/dist/types/plugins/dom-observer/plugin.d.ts.map +1 -0
  98. package/dist/types/plugins/dynamic-experiences/index.d.ts +2 -1
  99. package/dist/types/plugins/dynamic-experiences/index.d.ts.map +1 -1
  100. package/dist/types/plugins/dynamic-experiences/plugin.d.ts +18 -1
  101. package/dist/types/plugins/dynamic-experiences/plugin.d.ts.map +1 -1
  102. package/dist/types/plugins/dynamic-experiences/types.d.ts +63 -1
  103. package/dist/types/plugins/dynamic-experiences/types.d.ts.map +1 -1
  104. package/dist/types/plugins/experience-browser-storage/plugin.d.ts.map +1 -1
  105. package/dist/types/plugins/web-components/plugin.d.ts.map +1 -1
  106. package/dist/types/server/index.d.ts +7 -0
  107. package/dist/types/server/index.d.ts.map +1 -1
  108. package/dist/types/utils/events.d.ts +1 -1
  109. package/dist/types/utils/events.d.ts.map +1 -1
  110. package/dist/types/utils/picked-config.d.ts +12 -4
  111. package/dist/types/utils/picked-config.d.ts.map +1 -1
  112. package/dist/types/utils/rules-logic.d.ts +4 -9
  113. package/dist/types/utils/rules-logic.d.ts.map +1 -1
  114. package/package.json +12 -2
  115. package/dist/api-BarClNGB.cjs +0 -1
  116. package/dist/docs/reference/index.md +0 -11
  117. package/dist/docs/tooling/index.md +0 -19
  118. package/dist/experience-instance-DC2gKwA6.cjs +0 -1
  119. package/dist/experience-instance-DJaNc5Ob.js +0 -486
  120. package/dist/rules-logic-BTIxfrJJ.js +0 -240
  121. package/dist/rules-logic-DNLOv90w.cjs +0 -1
  122. package/dist/types/utils/state-snaphot.d.ts +0 -26
  123. package/dist/types/utils/state-snaphot.d.ts.map +0 -1
  124. package/dist/types/utils/user-session.d.ts +0 -12
  125. package/dist/types/utils/user-session.d.ts.map +0 -1
  126. /package/dist/docs/{tooling/chrome-extension.md → chrome-extension.md} +0 -0
  127. /package/dist/docs/{tooling/gtm-template.md → gtm-template.md} +0 -0
  128. /package/dist/docs/{tooling/mcp.md → mcp.md} +0 -0
  129. /package/dist/docs/{tooling/vite-plugin.md → vite-plugin.md} +0 -0
  130. /package/dist/{id-Bv44ilEo.cjs → id-Cg7fVl1O.cjs} +0 -0
  131. /package/dist/{id-CRqy981z.js → id-n2yrHvNo.js} +0 -0
  132. /package/dist/{object-C82v79xc.js → object-DvUDOytC.js} +0 -0
  133. /package/dist/{object-CUJOjanb.cjs → object-ThO1DEyl.cjs} +0 -0
  134. /package/dist/{storage-B6lQtc4-.cjs → storage-BXSlLTRa.cjs} +0 -0
  135. /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.
@@ -2,31 +2,192 @@
2
2
  layout: docs
3
3
  ---
4
4
 
5
- # API
5
+ # Getting started
6
6
 
7
- The Picked API is a GraphQL API that provides programmatic access to all platform features.
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 using the [GraphQL Playground](https://api.picked.app/api/graphql).
11
+ Explore the API and run queries interactively in the [GraphQL Playground](https://api.picked.app/api/graphql).
12
12
 
13
- ## Authentication
13
+ ## API endpoint
14
14
 
15
- Authenticate requests by including an API key in the `Authorization` header.
15
+ All requests go to a single endpoint:
16
16
 
17
17
  ```
18
- Authorization: Bearer your-api-key
18
+ POST https://api.picked.app/api/graphql
19
19
  ```
20
20
 
21
- See [API Keys](/api-keys) for how to create and manage keys.
21
+ Every request must use the `POST` method with a JSON body containing a `query` field and an optional `variables` field:
22
22
 
23
- ## Making requests
23
+ ```json
24
+ {
25
+ "query": "query { ... }",
26
+ "variables": { }
27
+ }
28
+ ```
24
29
 
25
- Send a POST request to the GraphQL endpoint with your query in the request body.
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
- -H "Authorization: Bearer your-api-key" \
31
- -d '{"query": "{ viewer { id email } }"}'
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`.
@@ -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. step_id` (e.g., `01. welcome`). |
108
- | `target` | String | Edge end, formatted as `NN. step_id`. |
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