@nixxie-cms/core 1.0.3 → 2.0.0

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 (203) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/CHANGES-1.1.md +134 -0
  3. package/context/dist/nixxie-cms-core-context.cjs.js +4 -3
  4. package/context/dist/nixxie-cms-core-context.esm.js +3 -2
  5. package/dist/declarations/src/access.d.ts +2 -2
  6. package/dist/declarations/src/access.d.ts.map +1 -1
  7. package/dist/declarations/src/admin-ui/components/Navigation.d.ts +2 -2
  8. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  9. package/dist/declarations/src/admin-ui/context.d.ts +6 -6
  10. package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
  11. package/dist/declarations/src/admin-ui/utils/Fields.d.ts +3 -3
  12. package/dist/declarations/src/admin-ui/utils/Fields.d.ts.map +1 -1
  13. package/dist/declarations/src/admin-ui/utils/filters.d.ts +5 -5
  14. package/dist/declarations/src/admin-ui/utils/filters.d.ts.map +1 -1
  15. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts +3 -3
  16. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
  17. package/dist/declarations/src/admin-ui/utils/utils.d.ts +2 -2
  18. package/dist/declarations/src/admin-ui/utils/utils.d.ts.map +1 -1
  19. package/dist/declarations/src/context.d.ts +1 -1
  20. package/dist/declarations/src/context.d.ts.map +1 -1
  21. package/dist/declarations/src/fields/types/bigInt/index.d.ts +3 -3
  22. package/dist/declarations/src/fields/types/bigInt/index.d.ts.map +1 -1
  23. package/dist/declarations/src/fields/types/bytes/index.d.ts +3 -3
  24. package/dist/declarations/src/fields/types/bytes/index.d.ts.map +1 -1
  25. package/dist/declarations/src/fields/types/calendarDay/index.d.ts +3 -3
  26. package/dist/declarations/src/fields/types/calendarDay/index.d.ts.map +1 -1
  27. package/dist/declarations/src/fields/types/checkbox/index.d.ts +3 -3
  28. package/dist/declarations/src/fields/types/checkbox/index.d.ts.map +1 -1
  29. package/dist/declarations/src/fields/types/decimal/index.d.ts +3 -3
  30. package/dist/declarations/src/fields/types/decimal/index.d.ts.map +1 -1
  31. package/dist/declarations/src/fields/types/file/index.d.ts +4 -4
  32. package/dist/declarations/src/fields/types/file/index.d.ts.map +1 -1
  33. package/dist/declarations/src/fields/types/float/index.d.ts +3 -3
  34. package/dist/declarations/src/fields/types/float/index.d.ts.map +1 -1
  35. package/dist/declarations/src/fields/types/image/index.d.ts +4 -4
  36. package/dist/declarations/src/fields/types/image/index.d.ts.map +1 -1
  37. package/dist/declarations/src/fields/types/integer/index.d.ts +3 -3
  38. package/dist/declarations/src/fields/types/integer/index.d.ts.map +1 -1
  39. package/dist/declarations/src/fields/types/json/index.d.ts +3 -3
  40. package/dist/declarations/src/fields/types/json/index.d.ts.map +1 -1
  41. package/dist/declarations/src/fields/types/multiselect/index.d.ts +3 -3
  42. package/dist/declarations/src/fields/types/multiselect/index.d.ts.map +1 -1
  43. package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
  44. package/dist/declarations/src/fields/types/password/index.d.ts +3 -3
  45. package/dist/declarations/src/fields/types/password/index.d.ts.map +1 -1
  46. package/dist/declarations/src/fields/types/relationship/index.d.ts +8 -8
  47. package/dist/declarations/src/fields/types/relationship/index.d.ts.map +1 -1
  48. package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts +3 -3
  49. package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts.map +1 -1
  50. package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts +3 -3
  51. package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts.map +1 -1
  52. package/dist/declarations/src/fields/types/relationship/views/index.d.ts +3 -3
  53. package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
  54. package/dist/declarations/src/fields/types/relationship/views/types.d.ts +3 -3
  55. package/dist/declarations/src/fields/types/relationship/views/types.d.ts.map +1 -1
  56. package/dist/declarations/src/fields/types/select/index.d.ts +3 -3
  57. package/dist/declarations/src/fields/types/select/index.d.ts.map +1 -1
  58. package/dist/declarations/src/fields/types/text/index.d.ts +3 -3
  59. package/dist/declarations/src/fields/types/text/index.d.ts.map +1 -1
  60. package/dist/declarations/src/fields/types/timestamp/index.d.ts +3 -3
  61. package/dist/declarations/src/fields/types/timestamp/index.d.ts.map +1 -1
  62. package/dist/declarations/src/fields/types/virtual/index.d.ts +7 -7
  63. package/dist/declarations/src/fields/types/virtual/index.d.ts.map +1 -1
  64. package/dist/declarations/src/helpers.d.ts +249 -13
  65. package/dist/declarations/src/helpers.d.ts.map +1 -1
  66. package/dist/declarations/src/index.d.ts +9 -4
  67. package/dist/declarations/src/index.d.ts.map +1 -1
  68. package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -1
  69. package/dist/declarations/src/lib/admin-meta.d.ts +11 -11
  70. package/dist/declarations/src/lib/admin-meta.d.ts.map +1 -1
  71. package/dist/declarations/src/lib/core/access-control.d.ts +18 -18
  72. package/dist/declarations/src/lib/core/access-control.d.ts.map +1 -1
  73. package/dist/declarations/src/lib/core/cascade.d.ts +47 -0
  74. package/dist/declarations/src/lib/core/cascade.d.ts.map +1 -0
  75. package/dist/declarations/src/lib/core/initialise-lists.d.ts +27 -24
  76. package/dist/declarations/src/lib/core/initialise-lists.d.ts.map +1 -1
  77. package/dist/declarations/src/lib/env.d.ts +9 -0
  78. package/dist/declarations/src/lib/env.d.ts.map +1 -0
  79. package/dist/declarations/src/lib/system.d.ts +1 -1
  80. package/dist/declarations/src/lib/system.d.ts.map +1 -1
  81. package/dist/declarations/src/list-features.d.ts +162 -0
  82. package/dist/declarations/src/list-features.d.ts.map +1 -0
  83. package/dist/declarations/src/schema.d.ts +24 -23
  84. package/dist/declarations/src/schema.d.ts.map +1 -1
  85. package/dist/declarations/src/session.d.ts +75 -0
  86. package/dist/declarations/src/session.d.ts.map +1 -1
  87. package/dist/declarations/src/types/admin-meta.d.ts +11 -11
  88. package/dist/declarations/src/types/admin-meta.d.ts.map +1 -1
  89. package/dist/declarations/src/types/config/access-control.d.ts +42 -42
  90. package/dist/declarations/src/types/config/access-control.d.ts.map +1 -1
  91. package/dist/declarations/src/types/config/fields.d.ts +19 -19
  92. package/dist/declarations/src/types/config/fields.d.ts.map +1 -1
  93. package/dist/declarations/src/types/config/hooks.d.ts +131 -131
  94. package/dist/declarations/src/types/config/hooks.d.ts.map +1 -1
  95. package/dist/declarations/src/types/config/index.d.ts +190 -8
  96. package/dist/declarations/src/types/config/index.d.ts.map +1 -1
  97. package/dist/declarations/src/types/config/lists.d.ts +146 -108
  98. package/dist/declarations/src/types/config/lists.d.ts.map +1 -1
  99. package/dist/declarations/src/types/context.d.ts +507 -47
  100. package/dist/declarations/src/types/context.d.ts.map +1 -1
  101. package/dist/declarations/src/types/next-fields.d.ts +28 -28
  102. package/dist/declarations/src/types/next-fields.d.ts.map +1 -1
  103. package/dist/declarations/src/types/type-info.d.ts +3 -3
  104. package/dist/declarations/src/types/type-info.d.ts.map +1 -1
  105. package/dist/{express-455ae20c.cjs.js → express-84d534c2.cjs.js} +6 -6
  106. package/dist/{express-7559ca2d.esm.js → express-d0a4ce99.esm.js} +6 -6
  107. package/dist/{index-15c8f81e.esm.js → index-5d8b0b4e.esm.js} +363 -183
  108. package/dist/index-6055753b.cjs.js +393 -0
  109. package/dist/{index-42045902.cjs.js → index-ac29f382.cjs.js} +363 -185
  110. package/dist/index-f1703b7b.esm.js +386 -0
  111. package/dist/nixxie-cms-core.cjs.js +1388 -30
  112. package/dist/nixxie-cms-core.esm.js +1362 -24
  113. package/dist/{non-null-graphql-add6bb3d.cjs.js → non-null-graphql-4a44c122.cjs.js} +1 -1
  114. package/dist/{non-null-graphql-a84ed64d.esm.js → non-null-graphql-8c5feaae.esm.js} +1 -1
  115. package/dist/{resolve-hooks-165a9ce2.cjs.js → resolve-hooks-10a5f84c.cjs.js} +240 -6
  116. package/dist/{resolve-hooks-6813a045.esm.js → resolve-hooks-9e676794.esm.js} +238 -7
  117. package/dist/{system-a321642d.cjs.js → system-6b37a5f8.cjs.js} +33 -7
  118. package/dist/{system-03e49e4f.esm.js → system-e591d821.esm.js} +33 -7
  119. package/fields/dist/nixxie-cms-core-fields.cjs.js +29 -576
  120. package/fields/dist/nixxie-cms-core-fields.esm.js +18 -565
  121. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +4 -2
  122. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +4 -2
  123. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +1 -6
  124. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +1 -6
  125. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +4 -2
  126. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +4 -2
  127. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +4 -3
  128. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +4 -3
  129. package/package.json +4 -4
  130. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +4 -3
  131. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +4 -3
  132. package/scripts/dist/nixxie-cms-core-scripts.cjs.js +4 -3
  133. package/scripts/dist/nixxie-cms-core-scripts.esm.js +4 -3
  134. package/session/dist/nixxie-cms-core-session.cjs.js +286 -0
  135. package/session/dist/nixxie-cms-core-session.esm.js +279 -1
  136. package/src/access.ts +25 -25
  137. package/src/admin-ui/admin-meta-graphql.ts +5 -5
  138. package/src/admin-ui/components/CreateButtonLink.tsx +46 -46
  139. package/src/admin-ui/components/Navigation.tsx +3 -3
  140. package/src/admin-ui/context.tsx +6 -6
  141. package/src/admin-ui/utils/Fields.tsx +241 -241
  142. package/src/admin-ui/utils/actionData.ts +36 -36
  143. package/src/admin-ui/utils/filters.ts +148 -148
  144. package/src/admin-ui/utils/useCreateItem.ts +171 -171
  145. package/src/admin-ui/utils/utils.tsx +127 -127
  146. package/src/context.ts +1 -1
  147. package/src/fields/non-null-graphql.ts +115 -115
  148. package/src/fields/types/bigInt/index.ts +6 -6
  149. package/src/fields/types/bytes/index.ts +6 -6
  150. package/src/fields/types/calendarDay/index.ts +18 -19
  151. package/src/fields/types/checkbox/index.ts +6 -6
  152. package/src/fields/types/decimal/index.ts +6 -6
  153. package/src/fields/types/file/index.ts +8 -8
  154. package/src/fields/types/float/index.ts +6 -6
  155. package/src/fields/types/image/index.ts +8 -8
  156. package/src/fields/types/integer/index.ts +6 -6
  157. package/src/fields/types/json/index.ts +5 -5
  158. package/src/fields/types/multiselect/index.ts +7 -7
  159. package/src/fields/types/multiselect/views/index.tsx +149 -151
  160. package/src/fields/types/password/index.ts +6 -6
  161. package/src/fields/types/relationship/index.ts +13 -13
  162. package/src/fields/types/relationship/views/ComboboxMany.tsx +110 -110
  163. package/src/fields/types/relationship/views/ComboboxSingle.tsx +115 -115
  164. package/src/fields/types/relationship/views/ContextualActions.tsx +139 -139
  165. package/src/fields/types/relationship/views/index.tsx +492 -492
  166. package/src/fields/types/relationship/views/types.ts +46 -46
  167. package/src/fields/types/relationship/views/useApolloQuery.ts +185 -185
  168. package/src/fields/types/relationship/views/useFilter.tsx +109 -109
  169. package/src/fields/types/select/index.ts +6 -6
  170. package/src/fields/types/text/index.ts +6 -6
  171. package/src/fields/types/timestamp/index.ts +23 -21
  172. package/src/fields/types/virtual/index.ts +11 -11
  173. package/src/helpers.ts +773 -42
  174. package/src/index.ts +66 -24
  175. package/src/internal-unstable/admin-ui/pages/ItemPage/common.tsx +4 -4
  176. package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +5 -5
  177. package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +8 -8
  178. package/src/lib/admin-meta.ts +369 -369
  179. package/src/lib/context/createContext.ts +6 -0
  180. package/src/lib/core/access-control.ts +434 -434
  181. package/src/lib/core/cascade.ts +236 -0
  182. package/src/lib/core/initialise-lists.ts +49 -33
  183. package/src/lib/core/mutations/index.ts +7 -0
  184. package/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts +145 -145
  185. package/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts +71 -71
  186. package/src/lib/core/queries/output-field.ts +178 -178
  187. package/src/lib/env.ts +50 -0
  188. package/src/lib/id-field.ts +2 -2
  189. package/src/lib/system.ts +221 -207
  190. package/src/lib/typescript-schema-printer.ts +227 -227
  191. package/src/list-features.ts +476 -0
  192. package/src/schema.ts +92 -22
  193. package/src/session.ts +225 -0
  194. package/src/types/admin-meta.ts +218 -218
  195. package/src/types/config/access-control.ts +186 -186
  196. package/src/types/config/fields.ts +96 -96
  197. package/src/types/config/hooks.ts +529 -529
  198. package/src/types/config/index.ts +206 -7
  199. package/src/types/config/lists.ts +606 -565
  200. package/src/types/context.ts +592 -55
  201. package/src/types/next-fields.ts +31 -31
  202. package/src/types/type-info.ts +38 -38
  203. package/src/types/type-tests.ts +21 -21
package/src/session.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
  import * as cookie from 'cookie'
3
3
  import Iron from '@hapi/iron'
4
+ import { json } from './fields/types/json'
5
+ import { text } from './fields/types/text'
6
+ import { timestamp } from './fields/types/timestamp'
4
7
  import type { NixxieContext, SessionStrategy, SessionStoreFunction } from '../types'
5
8
 
6
9
  // TODO: should we also accept httpOnly?
@@ -135,6 +138,194 @@ export function statelessSessions<Session>({
135
138
  } satisfies SessionStrategy<Session, any>
136
139
  }
137
140
 
141
+ type PersistentSessionsOptions = StatelessSessionsOptions & {
142
+ /**
143
+ * Key of the collection that stores sessions (see `sessionCollection()`).
144
+ * @default 'UserSession'
145
+ */
146
+ collection?: string
147
+ }
148
+
149
+ function sessionModel(context: NixxieContext, collection: string): any {
150
+ const delegate = (context.prisma as any)?.[collection[0].toLowerCase() + collection.slice(1)]
151
+ if (!delegate) {
152
+ throw new Error(
153
+ `persistentSessions: collection "${collection}" was not found in the Prisma client. ` +
154
+ `Add it to your config (e.g. \`collections: { ${collection}: sessionCollection() }\`) and run a migration.`
155
+ )
156
+ }
157
+ return delegate
158
+ }
159
+
160
+ /**
161
+ * Database-backed sessions: the cookie carries only an opaque token; the session itself
162
+ * lives in a collection, so sessions survive restarts, can be listed per user, and can be
163
+ * revoked server-side (see `listSessions` / `revokeSession` / `revokeUserSessions`).
164
+ *
165
+ * Tracks `lastSeenAt` (refreshed at most once a minute), `userAgent` and `ip` per session.
166
+ * Pair with a scheduled `pruneSessions()` job to clear expired/revoked rows.
167
+ */
168
+ export function persistentSessions<Session>({
169
+ collection = 'UserSession',
170
+ maxAge = 60 * 60 * 8, // 8 hours
171
+ ...statelessSessionsOptions
172
+ }: PersistentSessionsOptions = {}) {
173
+ const stateless = statelessSessions<string>({ ...statelessSessionsOptions, maxAge })
174
+
175
+ return {
176
+ async get({ context }) {
177
+ const token = await stateless.get({ context })
178
+ if (!token) return
179
+
180
+ const row = await sessionModel(context, collection).findUnique({ where: { token } })
181
+ if (!row || row.revokedAt) return
182
+ if (row.expiresAt && row.expiresAt.getTime() < Date.now()) return
183
+
184
+ // Refresh activity tracking at most once a minute; never block the request on it.
185
+ if (!row.lastSeenAt || Date.now() - row.lastSeenAt.getTime() > 60_000) {
186
+ void sessionModel(context, collection)
187
+ .update({ where: { token }, data: { lastSeenAt: new Date() } })
188
+ .catch(() => {})
189
+ }
190
+
191
+ return row.data as Session
192
+ },
193
+ async start({ context, data }) {
194
+ const token = randomBytes(24).toString('base64url') // 192-bit
195
+ const req = context.req
196
+ const forwarded = req?.headers['x-forwarded-for']
197
+ await sessionModel(context, collection).create({
198
+ data: {
199
+ token,
200
+ data: data as any,
201
+ itemId: (data as any)?.itemId != null ? String((data as any).itemId) : null,
202
+ expiresAt: new Date(Date.now() + maxAge * 1000),
203
+ createdAt: new Date(),
204
+ lastSeenAt: new Date(),
205
+ userAgent: (req?.headers['user-agent'] as string | undefined) ?? null,
206
+ ip:
207
+ (typeof forwarded === 'string' ? forwarded.split(',')[0]?.trim() : forwarded?.[0]) ??
208
+ req?.socket?.remoteAddress ??
209
+ null,
210
+ },
211
+ })
212
+ return (await stateless.start({ context, data: token })) || ''
213
+ },
214
+ async end({ context }) {
215
+ const token = await stateless.get({ context })
216
+ if (token) {
217
+ await sessionModel(context, collection).deleteMany({ where: { token } })
218
+ }
219
+ await stateless.end({ context })
220
+ },
221
+ } satisfies SessionStrategy<Session, any>
222
+ }
223
+
224
+ export type SessionInfo = {
225
+ id: string
226
+ itemId: string | null
227
+ createdAt: Date
228
+ lastSeenAt: Date | null
229
+ expiresAt: Date | null
230
+ revokedAt: Date | null
231
+ userAgent: string | null
232
+ ip: string | null
233
+ }
234
+
235
+ const stripToken = (row: any): SessionInfo => ({
236
+ id: row.id,
237
+ itemId: row.itemId ?? null,
238
+ createdAt: row.createdAt,
239
+ lastSeenAt: row.lastSeenAt ?? null,
240
+ expiresAt: row.expiresAt ?? null,
241
+ revokedAt: row.revokedAt ?? null,
242
+ userAgent: row.userAgent ?? null,
243
+ ip: row.ip ?? null,
244
+ })
245
+
246
+ /** Active (non-revoked, non-expired) sessions for a user, without their tokens. */
247
+ export async function listSessions(
248
+ context: NixxieContext,
249
+ itemId: string,
250
+ { collection = 'UserSession' }: { collection?: string } = {}
251
+ ): Promise<SessionInfo[]> {
252
+ const rows = await sessionModel(context, collection).findMany({
253
+ where: { itemId: String(itemId), revokedAt: null, expiresAt: { gte: new Date() } },
254
+ orderBy: { lastSeenAt: 'desc' },
255
+ })
256
+ return rows.map(stripToken)
257
+ }
258
+
259
+ /** Revoke a single session by its row id. The next request with its cookie is signed out. */
260
+ export async function revokeSession(
261
+ context: NixxieContext,
262
+ sessionId: string,
263
+ { collection = 'UserSession' }: { collection?: string } = {}
264
+ ): Promise<void> {
265
+ await sessionModel(context, collection).updateMany({
266
+ where: { id: sessionId },
267
+ data: { revokedAt: new Date() },
268
+ })
269
+ }
270
+
271
+ /** Revoke every session belonging to a user (e.g. after a password change). */
272
+ export async function revokeUserSessions(
273
+ context: NixxieContext,
274
+ itemId: string,
275
+ { collection = 'UserSession' }: { collection?: string } = {}
276
+ ): Promise<number> {
277
+ const result = await sessionModel(context, collection).updateMany({
278
+ where: { itemId: String(itemId), revokedAt: null },
279
+ data: { revokedAt: new Date() },
280
+ })
281
+ return result.count
282
+ }
283
+
284
+ /** Delete expired and revoked session rows. Run from a scheduled job. */
285
+ export async function pruneSessions(
286
+ context: NixxieContext,
287
+ { collection = 'UserSession' }: { collection?: string } = {}
288
+ ): Promise<number> {
289
+ const result = await sessionModel(context, collection).deleteMany({
290
+ where: { OR: [{ expiresAt: { lt: new Date() } }, { revokedAt: { not: null } }] },
291
+ })
292
+ return result.count
293
+ }
294
+
295
+ /**
296
+ * Sign the current user in as another user, recording who is impersonating in the
297
+ * session (`impersonatedBy`). Works with any session strategy. The caller is
298
+ * responsible for authorising the action (e.g. an admin-only check).
299
+ */
300
+ export async function startImpersonation(
301
+ context: NixxieContext,
302
+ itemId: string | number
303
+ ): Promise<void> {
304
+ if (!context.sessionStrategy) throw new Error('startImpersonation: no session strategy configured')
305
+ const current = context.session as { itemId?: string | number; impersonatedBy?: unknown } | undefined
306
+ if (!current?.itemId) throw new Error('startImpersonation: there is no signed-in session')
307
+ if (current.impersonatedBy != null) {
308
+ throw new Error('startImpersonation: already impersonating — end the current impersonation first')
309
+ }
310
+ await context.sessionStrategy.start({
311
+ context,
312
+ data: { itemId, impersonatedBy: current.itemId } as any,
313
+ })
314
+ }
315
+
316
+ /** Return from an impersonated session to the original user's session. */
317
+ export async function endImpersonation(context: NixxieContext): Promise<void> {
318
+ if (!context.sessionStrategy) throw new Error('endImpersonation: no session strategy configured')
319
+ const current = context.session as { impersonatedBy?: string | number } | undefined
320
+ if (current?.impersonatedBy == null) {
321
+ throw new Error('endImpersonation: the current session is not impersonating anyone')
322
+ }
323
+ await context.sessionStrategy.start({
324
+ context,
325
+ data: { itemId: current.impersonatedBy } as any,
326
+ })
327
+ }
328
+
138
329
  export function storedSessions<Session>({
139
330
  store: storeFn,
140
331
  maxAge = 60 * 60 * 8, // 8 hours
@@ -166,3 +357,37 @@ export function storedSessions<Session>({
166
357
  },
167
358
  } satisfies SessionStrategy<Session, any>
168
359
  }
360
+
361
+ /**
362
+ * Ready-made collection definition backing `persistentSessions()`.
363
+ *
364
+ * @example
365
+ * config({
366
+ * collections: {
367
+ * UserSession: sessionCollection(),
368
+ * ...collections,
369
+ * },
370
+ * session: persistentSessions({ collection: 'UserSession' }),
371
+ * })
372
+ */
373
+ export function sessionCollection(): any {
374
+ return {
375
+ fields: {
376
+ token: text({ validation: { isRequired: true }, isIndexed: 'unique' }),
377
+ data: json(),
378
+ itemId: text({ isIndexed: true }),
379
+ expiresAt: timestamp(),
380
+ createdAt: timestamp({ defaultValue: { kind: 'now' }, db: { isNullable: false } }),
381
+ lastSeenAt: timestamp(),
382
+ revokedAt: timestamp(),
383
+ userAgent: text(),
384
+ ip: text(),
385
+ },
386
+ graphql: {
387
+ omit: true,
388
+ },
389
+ ui: {
390
+ hideNavigation: true,
391
+ },
392
+ }
393
+ }
@@ -1,218 +1,218 @@
1
- import type { allIcons as KeystarIcons } from '@keystar/ui/icon/all'
2
- import type { ReactElement } from 'react'
3
-
4
- import type { ConditionalFilter, ConditionalFilterCase, ListSortDescriptor } from './config'
5
- import type { BaseListTypeInfo } from './type-info'
6
- import type { GraphQLNames, JSONValue } from './utils'
7
-
8
- export type NavigationProps = {
9
- lists: ListMeta[]
10
- }
11
-
12
- export type AdminConfig = {
13
- components?: {
14
- Logo?: (props: object) => ReactElement
15
- Navigation?: (props: NavigationProps) => ReactElement
16
- }
17
- }
18
-
19
- export type FieldControllerConfig<FieldMeta extends JSONValue | undefined = undefined> = {
20
- listKey: string
21
- fieldKey: string
22
-
23
- label: string
24
- description: string
25
- customViews: Record<string, any>
26
- fieldMeta: FieldMeta
27
- }
28
-
29
- type FilterTypeDeclaration<Value extends JSONValue> = {
30
- readonly label: string
31
- readonly initialValue: Value
32
- }
33
-
34
- export type FilterTypeToFormat<Value extends JSONValue> = {
35
- readonly type: string
36
- readonly label: string
37
- readonly value: Value
38
- }
39
-
40
- export type FieldController<
41
- FormState,
42
- FilterValue extends JSONValue = never,
43
- GraphQLFilterValue = never,
44
- > = {
45
- fieldKey: string
46
-
47
- label: string
48
- description: string
49
-
50
- defaultValue: FormState
51
- deserialize: (item: any) => FormState // TODO: unknown
52
- serialize: (formState: FormState) => any // TODO: unknown
53
- validate?: (formState: FormState, opts: { isRequired: boolean }) => boolean
54
-
55
- graphqlSelection: string
56
- filter?: {
57
- types: Record<string, FilterTypeDeclaration<FilterValue>>
58
- parseGraphQL(value: GraphQLFilterValue & {}): { type: string; value: FilterValue }[]
59
- graphql(type: { type: string; value: FilterValue }): Record<string, any>
60
- Label(type: FilterTypeToFormat<FilterValue>): string | ReactElement | null
61
- Filter(props: {
62
- autoFocus?: boolean
63
- forceValidation?: boolean
64
- context: 'add' | 'edit'
65
- onChange(value: FilterValue): void
66
- type: string
67
- // TODO: could be derived `filter.types[type].label`?
68
- typeLabel?: string
69
- value: FilterValue
70
- }): ReactElement | null
71
- }
72
- }
73
-
74
- // TODO: duplicate, reference core/src/lib/admin-meta.ts
75
- export type FieldMeta = {
76
- key: string
77
- label: string
78
- description: string
79
- fieldMeta: JSONValue | null
80
- viewsIndex: number
81
- customViewsIndex: number | null
82
- views: FieldViews[number]
83
- controller: FieldController<unknown, JSONValue>
84
- isFilterable: boolean
85
- isOrderable: boolean
86
-
87
- search: 'default' | 'insensitive' | null
88
- isNonNull: ('read' | 'create' | 'update')[]
89
- createView: {
90
- fieldMode: ConditionalFilter<'edit' | 'hidden', 'hidden', BaseListTypeInfo>
91
- isRequired: ConditionalFilterCase<BaseListTypeInfo>
92
- }
93
- itemView: {
94
- fieldMode: ConditionalFilter<'edit' | 'read' | 'hidden', 'read' | 'hidden', BaseListTypeInfo>
95
- fieldPosition: 'form' | 'sidebar'
96
- isRequired: ConditionalFilterCase<BaseListTypeInfo>
97
- }
98
- listView: {
99
- fieldMode: 'read' | 'hidden'
100
- }
101
- }
102
-
103
- export type FieldGroupMeta = {
104
- label: string
105
- description: string
106
- fields: FieldMeta[]
107
- }
108
-
109
- export type ActionMeta = {
110
- key: string
111
- graphql: {
112
- arguments: readonly { name: string; type: string; source: { itemField: string } | null }[]
113
- names: {
114
- one: string
115
- many: string
116
- }
117
- }
118
-
119
- label: string
120
- icon: keyof typeof KeystarIcons | null
121
- messages: {
122
- promptTitle: string
123
- promptTitleMany: string
124
- prompt: string
125
- promptMany: string
126
- promptConfirmLabel: string
127
- promptConfirmLabelMany: string
128
- fail: string
129
- failMany: string
130
- success: string
131
- successMany: string
132
- }
133
- itemView: {
134
- actionMode: ConditionalFilter<
135
- 'enabled' | 'disabled' | 'hidden',
136
- 'disabled' | 'hidden',
137
- BaseListTypeInfo
138
- >
139
- navigation: 'follow' | 'refetch' | 'return'
140
- hidePrompt: boolean
141
- hideToast: boolean
142
- }
143
- listView: {
144
- actionMode: ConditionalFilter<
145
- 'enabled' | 'disabled' | 'hidden',
146
- 'disabled' | 'hidden',
147
- BaseListTypeInfo
148
- >
149
- }
150
- }
151
-
152
- export type ListMeta = {
153
- key: string
154
- label: string
155
- singular: string
156
- plural: string
157
- path: string
158
-
159
- labelField: string
160
- fields: { [key: string]: FieldMeta }
161
- groups: FieldGroupMeta[]
162
- actions: ActionMeta[]
163
- graphql: {
164
- names: GraphQLNames
165
- }
166
-
167
- pageSize: number
168
- initialColumns: string[]
169
- initialSearchFields: string[]
170
- initialSort: ListSortDescriptor<string>
171
- initialFilter: JSONValue
172
- isSingleton: boolean
173
-
174
- hideNavigation: boolean
175
- hideCreate: boolean
176
- hideDelete: boolean
177
- }
178
-
179
- export type Item = {
180
- [key: string]: unknown
181
- }
182
-
183
- export type FieldProps<FieldControllerFn extends (...args: any) => FieldController<any, any>> = {
184
- autoFocus?: boolean
185
- field: ReturnType<FieldControllerFn>
186
- isRequired: boolean
187
- /**
188
- * Will be true when the user has clicked submit and
189
- * the validate function on the field controller has returned false
190
- */
191
- forceValidation?: boolean
192
- onChange?(value: ReturnType<ReturnType<FieldControllerFn>['deserialize']>): void
193
- value: ReturnType<ReturnType<FieldControllerFn>['deserialize']>
194
- itemValue: Item
195
- }
196
-
197
- export type FieldViews = Record<
198
- string,
199
- {
200
- Field: (props: FieldProps<any>) => ReactElement | null
201
- Cell: CellComponent
202
- controller: (args: FieldControllerConfig<any>) => FieldController<unknown, JSONValue>
203
- allowedExportsOnCustomViews?: string[]
204
- }
205
- >
206
-
207
- export type CellComponent<
208
- FieldControllerFn extends (...args: any) => FieldController<any, any> = () => FieldController<
209
- any,
210
- any
211
- >,
212
- > = {
213
- (props: {
214
- value: any // TODO: T
215
- field: ReturnType<FieldControllerFn>
216
- item: Record<string, unknown>
217
- }): ReactElement | null
218
- }
1
+ import type { allIcons as KeystarIcons } from '@keystar/ui/icon/all'
2
+ import type { ReactElement } from 'react'
3
+
4
+ import type { ConditionalFilter, ConditionalFilterCase, CollectionSortDescriptor } from './config'
5
+ import type { BaseCollectionTypeInfo } from './type-info'
6
+ import type { GraphQLNames, JSONValue } from './utils'
7
+
8
+ export type NavigationProps = {
9
+ lists: CollectionMeta[]
10
+ }
11
+
12
+ export type AdminConfig = {
13
+ components?: {
14
+ Logo?: (props: object) => ReactElement
15
+ Navigation?: (props: NavigationProps) => ReactElement
16
+ }
17
+ }
18
+
19
+ export type FieldControllerConfig<FieldMeta extends JSONValue | undefined = undefined> = {
20
+ listKey: string
21
+ fieldKey: string
22
+
23
+ label: string
24
+ description: string
25
+ customViews: Record<string, any>
26
+ fieldMeta: FieldMeta
27
+ }
28
+
29
+ type FilterTypeDeclaration<Value extends JSONValue> = {
30
+ readonly label: string
31
+ readonly initialValue: Value
32
+ }
33
+
34
+ export type FilterTypeToFormat<Value extends JSONValue> = {
35
+ readonly type: string
36
+ readonly label: string
37
+ readonly value: Value
38
+ }
39
+
40
+ export type FieldController<
41
+ FormState,
42
+ FilterValue extends JSONValue = never,
43
+ GraphQLFilterValue = never,
44
+ > = {
45
+ fieldKey: string
46
+
47
+ label: string
48
+ description: string
49
+
50
+ defaultValue: FormState
51
+ deserialize: (item: any) => FormState // TODO: unknown
52
+ serialize: (formState: FormState) => any // TODO: unknown
53
+ validate?: (formState: FormState, opts: { isRequired: boolean }) => boolean
54
+
55
+ graphqlSelection: string
56
+ filter?: {
57
+ types: Record<string, FilterTypeDeclaration<FilterValue>>
58
+ parseGraphQL(value: GraphQLFilterValue & {}): { type: string; value: FilterValue }[]
59
+ graphql(type: { type: string; value: FilterValue }): Record<string, any>
60
+ Label(type: FilterTypeToFormat<FilterValue>): string | ReactElement | null
61
+ Filter(props: {
62
+ autoFocus?: boolean
63
+ forceValidation?: boolean
64
+ context: 'add' | 'edit'
65
+ onChange(value: FilterValue): void
66
+ type: string
67
+ // TODO: could be derived `filter.types[type].label`?
68
+ typeLabel?: string
69
+ value: FilterValue
70
+ }): ReactElement | null
71
+ }
72
+ }
73
+
74
+ // TODO: duplicate, reference core/src/lib/admin-meta.ts
75
+ export type FieldMeta = {
76
+ key: string
77
+ label: string
78
+ description: string
79
+ fieldMeta: JSONValue | null
80
+ viewsIndex: number
81
+ customViewsIndex: number | null
82
+ views: FieldViews[number]
83
+ controller: FieldController<unknown, JSONValue>
84
+ isFilterable: boolean
85
+ isOrderable: boolean
86
+
87
+ search: 'default' | 'insensitive' | null
88
+ isNonNull: ('read' | 'create' | 'update')[]
89
+ createView: {
90
+ fieldMode: ConditionalFilter<'edit' | 'hidden', 'hidden', BaseCollectionTypeInfo>
91
+ isRequired: ConditionalFilterCase<BaseCollectionTypeInfo>
92
+ }
93
+ itemView: {
94
+ fieldMode: ConditionalFilter<'edit' | 'read' | 'hidden', 'read' | 'hidden', BaseCollectionTypeInfo>
95
+ fieldPosition: 'form' | 'sidebar'
96
+ isRequired: ConditionalFilterCase<BaseCollectionTypeInfo>
97
+ }
98
+ listView: {
99
+ fieldMode: 'read' | 'hidden'
100
+ }
101
+ }
102
+
103
+ export type FieldGroupMeta = {
104
+ label: string
105
+ description: string
106
+ fields: FieldMeta[]
107
+ }
108
+
109
+ export type ActionMeta = {
110
+ key: string
111
+ graphql: {
112
+ arguments: readonly { name: string; type: string; source: { itemField: string } | null }[]
113
+ names: {
114
+ one: string
115
+ many: string
116
+ }
117
+ }
118
+
119
+ label: string
120
+ icon: keyof typeof KeystarIcons | null
121
+ messages: {
122
+ promptTitle: string
123
+ promptTitleMany: string
124
+ prompt: string
125
+ promptMany: string
126
+ promptConfirmLabel: string
127
+ promptConfirmLabelMany: string
128
+ fail: string
129
+ failMany: string
130
+ success: string
131
+ successMany: string
132
+ }
133
+ itemView: {
134
+ actionMode: ConditionalFilter<
135
+ 'enabled' | 'disabled' | 'hidden',
136
+ 'disabled' | 'hidden',
137
+ BaseCollectionTypeInfo
138
+ >
139
+ navigation: 'follow' | 'refetch' | 'return'
140
+ hidePrompt: boolean
141
+ hideToast: boolean
142
+ }
143
+ listView: {
144
+ actionMode: ConditionalFilter<
145
+ 'enabled' | 'disabled' | 'hidden',
146
+ 'disabled' | 'hidden',
147
+ BaseCollectionTypeInfo
148
+ >
149
+ }
150
+ }
151
+
152
+ export type CollectionMeta = {
153
+ key: string
154
+ label: string
155
+ singular: string
156
+ plural: string
157
+ path: string
158
+
159
+ labelField: string
160
+ fields: { [key: string]: FieldMeta }
161
+ groups: FieldGroupMeta[]
162
+ actions: ActionMeta[]
163
+ graphql: {
164
+ names: GraphQLNames
165
+ }
166
+
167
+ pageSize: number
168
+ initialColumns: string[]
169
+ initialSearchFields: string[]
170
+ initialSort: CollectionSortDescriptor<string>
171
+ initialFilter: JSONValue
172
+ isSingleton: boolean
173
+
174
+ hideNavigation: boolean
175
+ hideCreate: boolean
176
+ hideDelete: boolean
177
+ }
178
+
179
+ export type Item = {
180
+ [key: string]: unknown
181
+ }
182
+
183
+ export type FieldProps<FieldControllerFn extends (...args: any) => FieldController<any, any>> = {
184
+ autoFocus?: boolean
185
+ field: ReturnType<FieldControllerFn>
186
+ isRequired: boolean
187
+ /**
188
+ * Will be true when the user has clicked submit and
189
+ * the validate function on the field controller has returned false
190
+ */
191
+ forceValidation?: boolean
192
+ onChange?(value: ReturnType<ReturnType<FieldControllerFn>['deserialize']>): void
193
+ value: ReturnType<ReturnType<FieldControllerFn>['deserialize']>
194
+ itemValue: Item
195
+ }
196
+
197
+ export type FieldViews = Record<
198
+ string,
199
+ {
200
+ Field: (props: FieldProps<any>) => ReactElement | null
201
+ Cell: CellComponent
202
+ controller: (args: FieldControllerConfig<any>) => FieldController<unknown, JSONValue>
203
+ allowedExportsOnCustomViews?: string[]
204
+ }
205
+ >
206
+
207
+ export type CellComponent<
208
+ FieldControllerFn extends (...args: any) => FieldController<any, any> = () => FieldController<
209
+ any,
210
+ any
211
+ >,
212
+ > = {
213
+ (props: {
214
+ value: any // TODO: T
215
+ field: ReturnType<FieldControllerFn>
216
+ item: Record<string, unknown>
217
+ }): ReactElement | null
218
+ }