@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
@@ -1,6 +1,18 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import * as cookie from 'cookie';
3
3
  import Iron from '@hapi/iron';
4
+ import { a as text, j as json, t as timestamp } from '../../dist/index-f1703b7b.esm.js';
5
+ import 'pluralize';
6
+ import '../../dist/next-fields-9bf04ed8.esm.js';
7
+ import 'decimal.js';
8
+ import '@graphql-ts/schema';
9
+ import '@graphql-ts/extend';
10
+ import 'graphql-upload/GraphQLUpload.js';
11
+ import 'graphql';
12
+ import '../../dist/resolve-hooks-9e676794.esm.js';
13
+ import 'node:async_hooks';
14
+ import '../../dist/non-null-graphql-8c5feaae.esm.js';
15
+ import 'node:path';
4
16
 
5
17
  // TODO: should we also accept httpOnly?
6
18
 
@@ -77,6 +89,224 @@ function statelessSessions({
77
89
  }
78
90
  };
79
91
  }
92
+ function sessionModel(context, collection) {
93
+ var _context$prisma;
94
+ const delegate = (_context$prisma = context.prisma) === null || _context$prisma === void 0 ? void 0 : _context$prisma[collection[0].toLowerCase() + collection.slice(1)];
95
+ if (!delegate) {
96
+ throw new Error(`persistentSessions: collection "${collection}" was not found in the Prisma client. ` + `Add it to your config (e.g. \`collections: { ${collection}: sessionCollection() }\`) and run a migration.`);
97
+ }
98
+ return delegate;
99
+ }
100
+
101
+ /**
102
+ * Database-backed sessions: the cookie carries only an opaque token; the session itself
103
+ * lives in a collection, so sessions survive restarts, can be listed per user, and can be
104
+ * revoked server-side (see `listSessions` / `revokeSession` / `revokeUserSessions`).
105
+ *
106
+ * Tracks `lastSeenAt` (refreshed at most once a minute), `userAgent` and `ip` per session.
107
+ * Pair with a scheduled `pruneSessions()` job to clear expired/revoked rows.
108
+ */
109
+ function persistentSessions({
110
+ collection = 'UserSession',
111
+ maxAge = 60 * 60 * 8,
112
+ // 8 hours
113
+ ...statelessSessionsOptions
114
+ } = {}) {
115
+ const stateless = statelessSessions({
116
+ ...statelessSessionsOptions,
117
+ maxAge
118
+ });
119
+ return {
120
+ async get({
121
+ context
122
+ }) {
123
+ const token = await stateless.get({
124
+ context
125
+ });
126
+ if (!token) return;
127
+ const row = await sessionModel(context, collection).findUnique({
128
+ where: {
129
+ token
130
+ }
131
+ });
132
+ if (!row || row.revokedAt) return;
133
+ if (row.expiresAt && row.expiresAt.getTime() < Date.now()) return;
134
+
135
+ // Refresh activity tracking at most once a minute; never block the request on it.
136
+ if (!row.lastSeenAt || Date.now() - row.lastSeenAt.getTime() > 60000) {
137
+ void sessionModel(context, collection).update({
138
+ where: {
139
+ token
140
+ },
141
+ data: {
142
+ lastSeenAt: new Date()
143
+ }
144
+ }).catch(() => {});
145
+ }
146
+ return row.data;
147
+ },
148
+ async start({
149
+ context,
150
+ data
151
+ }) {
152
+ var _ref, _ref2, _ref3, _forwarded$split$, _req$socket;
153
+ const token = randomBytes(24).toString('base64url'); // 192-bit
154
+ const req = context.req;
155
+ const forwarded = req === null || req === void 0 ? void 0 : req.headers['x-forwarded-for'];
156
+ await sessionModel(context, collection).create({
157
+ data: {
158
+ token,
159
+ data: data,
160
+ itemId: (data === null || data === void 0 ? void 0 : data.itemId) != null ? String(data.itemId) : null,
161
+ expiresAt: new Date(Date.now() + maxAge * 1000),
162
+ createdAt: new Date(),
163
+ lastSeenAt: new Date(),
164
+ userAgent: (_ref = req === null || req === void 0 ? void 0 : req.headers['user-agent']) !== null && _ref !== void 0 ? _ref : null,
165
+ ip: (_ref2 = (_ref3 = typeof forwarded === 'string' ? (_forwarded$split$ = forwarded.split(',')[0]) === null || _forwarded$split$ === void 0 ? void 0 : _forwarded$split$.trim() : forwarded === null || forwarded === void 0 ? void 0 : forwarded[0]) !== null && _ref3 !== void 0 ? _ref3 : req === null || req === void 0 || (_req$socket = req.socket) === null || _req$socket === void 0 ? void 0 : _req$socket.remoteAddress) !== null && _ref2 !== void 0 ? _ref2 : null
166
+ }
167
+ });
168
+ return (await stateless.start({
169
+ context,
170
+ data: token
171
+ })) || '';
172
+ },
173
+ async end({
174
+ context
175
+ }) {
176
+ const token = await stateless.get({
177
+ context
178
+ });
179
+ if (token) {
180
+ await sessionModel(context, collection).deleteMany({
181
+ where: {
182
+ token
183
+ }
184
+ });
185
+ }
186
+ await stateless.end({
187
+ context
188
+ });
189
+ }
190
+ };
191
+ }
192
+ const stripToken = row => {
193
+ var _row$itemId, _row$lastSeenAt, _row$expiresAt, _row$revokedAt, _row$userAgent, _row$ip;
194
+ return {
195
+ id: row.id,
196
+ itemId: (_row$itemId = row.itemId) !== null && _row$itemId !== void 0 ? _row$itemId : null,
197
+ createdAt: row.createdAt,
198
+ lastSeenAt: (_row$lastSeenAt = row.lastSeenAt) !== null && _row$lastSeenAt !== void 0 ? _row$lastSeenAt : null,
199
+ expiresAt: (_row$expiresAt = row.expiresAt) !== null && _row$expiresAt !== void 0 ? _row$expiresAt : null,
200
+ revokedAt: (_row$revokedAt = row.revokedAt) !== null && _row$revokedAt !== void 0 ? _row$revokedAt : null,
201
+ userAgent: (_row$userAgent = row.userAgent) !== null && _row$userAgent !== void 0 ? _row$userAgent : null,
202
+ ip: (_row$ip = row.ip) !== null && _row$ip !== void 0 ? _row$ip : null
203
+ };
204
+ };
205
+
206
+ /** Active (non-revoked, non-expired) sessions for a user, without their tokens. */
207
+ async function listSessions(context, itemId, {
208
+ collection = 'UserSession'
209
+ } = {}) {
210
+ const rows = await sessionModel(context, collection).findMany({
211
+ where: {
212
+ itemId: String(itemId),
213
+ revokedAt: null,
214
+ expiresAt: {
215
+ gte: new Date()
216
+ }
217
+ },
218
+ orderBy: {
219
+ lastSeenAt: 'desc'
220
+ }
221
+ });
222
+ return rows.map(stripToken);
223
+ }
224
+
225
+ /** Revoke a single session by its row id. The next request with its cookie is signed out. */
226
+ async function revokeSession(context, sessionId, {
227
+ collection = 'UserSession'
228
+ } = {}) {
229
+ await sessionModel(context, collection).updateMany({
230
+ where: {
231
+ id: sessionId
232
+ },
233
+ data: {
234
+ revokedAt: new Date()
235
+ }
236
+ });
237
+ }
238
+
239
+ /** Revoke every session belonging to a user (e.g. after a password change). */
240
+ async function revokeUserSessions(context, itemId, {
241
+ collection = 'UserSession'
242
+ } = {}) {
243
+ const result = await sessionModel(context, collection).updateMany({
244
+ where: {
245
+ itemId: String(itemId),
246
+ revokedAt: null
247
+ },
248
+ data: {
249
+ revokedAt: new Date()
250
+ }
251
+ });
252
+ return result.count;
253
+ }
254
+
255
+ /** Delete expired and revoked session rows. Run from a scheduled job. */
256
+ async function pruneSessions(context, {
257
+ collection = 'UserSession'
258
+ } = {}) {
259
+ const result = await sessionModel(context, collection).deleteMany({
260
+ where: {
261
+ OR: [{
262
+ expiresAt: {
263
+ lt: new Date()
264
+ }
265
+ }, {
266
+ revokedAt: {
267
+ not: null
268
+ }
269
+ }]
270
+ }
271
+ });
272
+ return result.count;
273
+ }
274
+
275
+ /**
276
+ * Sign the current user in as another user, recording who is impersonating in the
277
+ * session (`impersonatedBy`). Works with any session strategy. The caller is
278
+ * responsible for authorising the action (e.g. an admin-only check).
279
+ */
280
+ async function startImpersonation(context, itemId) {
281
+ if (!context.sessionStrategy) throw new Error('startImpersonation: no session strategy configured');
282
+ const current = context.session;
283
+ if (!(current !== null && current !== void 0 && current.itemId)) throw new Error('startImpersonation: there is no signed-in session');
284
+ if (current.impersonatedBy != null) {
285
+ throw new Error('startImpersonation: already impersonating — end the current impersonation first');
286
+ }
287
+ await context.sessionStrategy.start({
288
+ context,
289
+ data: {
290
+ itemId,
291
+ impersonatedBy: current.itemId
292
+ }
293
+ });
294
+ }
295
+
296
+ /** Return from an impersonated session to the original user's session. */
297
+ async function endImpersonation(context) {
298
+ if (!context.sessionStrategy) throw new Error('endImpersonation: no session strategy configured');
299
+ const current = context.session;
300
+ if ((current === null || current === void 0 ? void 0 : current.impersonatedBy) == null) {
301
+ throw new Error('endImpersonation: the current session is not impersonating anyone');
302
+ }
303
+ await context.sessionStrategy.start({
304
+ context,
305
+ data: {
306
+ itemId: current.impersonatedBy
307
+ }
308
+ });
309
+ }
80
310
  function storedSessions({
81
311
  store: storeFn,
82
312
  maxAge = 60 * 60 * 8,
@@ -126,4 +356,52 @@ function storedSessions({
126
356
  };
127
357
  }
128
358
 
129
- export { statelessSessions, storedSessions };
359
+ /**
360
+ * Ready-made collection definition backing `persistentSessions()`.
361
+ *
362
+ * @example
363
+ * config({
364
+ * collections: {
365
+ * UserSession: sessionCollection(),
366
+ * ...collections,
367
+ * },
368
+ * session: persistentSessions({ collection: 'UserSession' }),
369
+ * })
370
+ */
371
+ function sessionCollection() {
372
+ return {
373
+ fields: {
374
+ token: text({
375
+ validation: {
376
+ isRequired: true
377
+ },
378
+ isIndexed: 'unique'
379
+ }),
380
+ data: json(),
381
+ itemId: text({
382
+ isIndexed: true
383
+ }),
384
+ expiresAt: timestamp(),
385
+ createdAt: timestamp({
386
+ defaultValue: {
387
+ kind: 'now'
388
+ },
389
+ db: {
390
+ isNullable: false
391
+ }
392
+ }),
393
+ lastSeenAt: timestamp(),
394
+ revokedAt: timestamp(),
395
+ userAgent: text(),
396
+ ip: text()
397
+ },
398
+ graphql: {
399
+ omit: true
400
+ },
401
+ ui: {
402
+ hideNavigation: true
403
+ }
404
+ };
405
+ }
406
+
407
+ export { endImpersonation, listSessions, persistentSessions, pruneSessions, revokeSession, revokeUserSessions, sessionCollection, startImpersonation, statelessSessions, storedSessions };
package/src/access.ts CHANGED
@@ -1,25 +1,25 @@
1
- import type { MaybePromise } from './types/utils'
2
- import type { BaseListTypeInfo } from './types'
3
-
4
- export function allowAll() {
5
- return true
6
- }
7
-
8
- export function denyAll() {
9
- return false
10
- }
11
-
12
- export function unfiltered<ListTypeInfo extends BaseListTypeInfo>(): MaybePromise<
13
- boolean | ListTypeInfo['inputs']['where']
14
- > {
15
- return true
16
- }
17
-
18
- export function allOperations<F>(f: F) {
19
- return {
20
- query: f,
21
- create: f,
22
- update: f,
23
- delete: f,
24
- }
25
- }
1
+ import type { MaybePromise } from './types/utils'
2
+ import type { BaseCollectionTypeInfo } from './types'
3
+
4
+ export function allowAll() {
5
+ return true
6
+ }
7
+
8
+ export function denyAll() {
9
+ return false
10
+ }
11
+
12
+ export function unfiltered<CollectionTypeInfo extends BaseCollectionTypeInfo>(): MaybePromise<
13
+ boolean | CollectionTypeInfo['inputs']['where']
14
+ > {
15
+ return true
16
+ }
17
+
18
+ export function allOperations<F>(f: F) {
19
+ return {
20
+ query: f,
21
+ create: f,
22
+ update: f,
23
+ delete: f,
24
+ }
25
+ }
@@ -1,5 +1,5 @@
1
1
  import type { GraphQLNames, JSONValue } from '../types/utils'
2
- import type { ListMeta, FieldMeta, FieldGroupMeta } from '../types'
2
+ import type { CollectionMeta, FieldMeta, FieldGroupMeta } from '../types'
3
3
  import { gql } from './apollo'
4
4
 
5
5
  export const adminMetaQuery = gql`
@@ -142,9 +142,9 @@ export const adminMetaQuery = gql`
142
142
  export type AdminMetaQuery = {
143
143
  nixxie: {
144
144
  adminMeta: {
145
- lists: (ListMeta & {
146
- fields: ListMeta['fields'][string][]
147
- actions: ListMeta['actions']
145
+ lists: (CollectionMeta & {
146
+ fields: CollectionMeta['fields'][string][]
147
+ actions: CollectionMeta['actions']
148
148
  groups: (FieldGroupMeta & {
149
149
  fields: FieldMeta[]
150
150
  })[]
@@ -155,7 +155,7 @@ export type AdminMetaQuery = {
155
155
  pageSize: number
156
156
  initialColumns: string[]
157
157
  initialSearchFields: string[]
158
- initialSort: ListMeta['initialSort'] | null
158
+ initialSort: CollectionMeta['initialSort'] | null
159
159
  initialFilter: JSONValue
160
160
  isSingleton: boolean
161
161
 
@@ -1,46 +1,46 @@
1
- import { css } from '@keystar/ui/style'
2
-
3
- import type { ListMeta } from '../../types'
4
-
5
- export function CreateButtonLink(props: { children?: string; list: ListMeta }) {
6
- const { list, children = `New ${list.singular}` } = props
7
- return (
8
- <a
9
- href={`/${list.path}/create`}
10
- aria-label={`New ${list.singular}`}
11
- className={css({
12
- display: 'inline-flex',
13
- alignItems: 'center',
14
- gap: 6,
15
- paddingInline: '13px',
16
- paddingBlock: '7px',
17
- borderRadius: 6,
18
- border: '1px solid transparent',
19
- backgroundColor: '#111827',
20
- color: '#ffffff',
21
- fontSize: 13,
22
- fontWeight: 500,
23
- fontFamily: 'inherit',
24
- textDecoration: 'none',
25
- cursor: 'pointer',
26
- whiteSpace: 'nowrap',
27
- flexShrink: 0,
28
- letterSpacing: '-0.01em',
29
- transition: 'background 130ms',
30
-
31
- '&:hover': { backgroundColor: '#1f2937' },
32
- '&:active': { backgroundColor: '#374151' },
33
-
34
- '&:focus-visible': {
35
- outline: '2px solid #111827',
36
- outlineOffset: 2,
37
- },
38
- })}
39
- >
40
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
41
- <path d="M6 1.5v9M1.5 6h9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
42
- </svg>
43
- {children}
44
- </a>
45
- )
46
- }
1
+ import { css } from '@keystar/ui/style'
2
+
3
+ import type { CollectionMeta } from '../../types'
4
+
5
+ export function CreateButtonLink(props: { children?: string; list: CollectionMeta }) {
6
+ const { list, children = `New ${list.singular}` } = props
7
+ return (
8
+ <a
9
+ href={`/${list.path}/create`}
10
+ aria-label={`New ${list.singular}`}
11
+ className={css({
12
+ display: 'inline-flex',
13
+ alignItems: 'center',
14
+ gap: 6,
15
+ paddingInline: '13px',
16
+ paddingBlock: '7px',
17
+ borderRadius: 6,
18
+ border: '1px solid transparent',
19
+ backgroundColor: '#111827',
20
+ color: '#ffffff',
21
+ fontSize: 13,
22
+ fontWeight: 500,
23
+ fontFamily: 'inherit',
24
+ textDecoration: 'none',
25
+ cursor: 'pointer',
26
+ whiteSpace: 'nowrap',
27
+ flexShrink: 0,
28
+ letterSpacing: '-0.01em',
29
+ transition: 'background 130ms',
30
+
31
+ '&:hover': { backgroundColor: '#1f2937' },
32
+ '&:active': { backgroundColor: '#374151' },
33
+
34
+ '&:focus-visible': {
35
+ outline: '2px solid #111827',
36
+ outlineOffset: 2,
37
+ },
38
+ })}
39
+ >
40
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
41
+ <path d="M6 1.5v9M1.5 6h9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
42
+ </svg>
43
+ {children}
44
+ </a>
45
+ )
46
+ }
@@ -3,14 +3,14 @@ import { useRouter } from 'next/router'
3
3
 
4
4
  import { css } from '@keystar/ui/style'
5
5
 
6
- import type { ListMeta } from '../../types'
6
+ import type { CollectionMeta } from '../../types'
7
7
  import { useNixxie } from '../context'
8
8
 
9
9
  // ================================================================
10
10
  // Helpers
11
11
  // ================================================================
12
12
 
13
- export function getHrefFromList(list: Pick<ListMeta, 'path' | 'isSingleton'>) {
13
+ export function getHrefFromList(list: Pick<CollectionMeta, 'path' | 'isSingleton'>) {
14
14
  return `/${list.path}${list.isSingleton ? '/1' : ''}`
15
15
  }
16
16
 
@@ -237,7 +237,7 @@ function CollectionsSection({
237
237
  lists,
238
238
  onNavItemClick,
239
239
  }: {
240
- lists: ListMeta[]
240
+ lists: CollectionMeta[]
241
241
  onNavItemClick?: () => void
242
242
  }) {
243
243
  const [search, setSearch] = useState('')
@@ -10,11 +10,11 @@ import { useRouter } from '@nixxie-cms/core/admin-ui/router'
10
10
  import { snapValueToClosest } from '../internal-unstable/admin-ui/pages/ListPage/PaginationControls'
11
11
  import type {
12
12
  AdminConfig,
13
- BaseListTypeInfo,
13
+ BaseCollectionTypeInfo,
14
14
  ConditionalFilter,
15
15
  ConditionalFilterCase,
16
16
  FieldViews,
17
- ListMeta,
17
+ CollectionMeta,
18
18
  } from '../types'
19
19
  import { type AdminMetaQuery, adminMetaQuery } from './admin-meta-graphql'
20
20
  import {
@@ -32,7 +32,7 @@ type NixxieContextType = {
32
32
  apiPath: string | null
33
33
  error?: ErrorLike | null
34
34
  fieldViews: FieldViews
35
- lists: { [list: string]: ListMeta }
35
+ lists: { [list: string]: CollectionMeta }
36
36
  }
37
37
 
38
38
  const NixxieContext = createContext<NixxieContextType>({
@@ -246,10 +246,10 @@ export function useListItem(
246
246
  fieldMode: ConditionalFilter<
247
247
  'edit' | 'read' | 'hidden',
248
248
  'read' | 'hidden',
249
- BaseListTypeInfo
249
+ BaseCollectionTypeInfo
250
250
  >
251
251
  fieldPosition: 'form' | 'sidebar'
252
- isRequired: ConditionalFilterCase<BaseListTypeInfo>
252
+ isRequired: ConditionalFilterCase<BaseCollectionTypeInfo>
253
253
  } | null
254
254
  }[]
255
255
  actions: {
@@ -258,7 +258,7 @@ export function useListItem(
258
258
  actionMode: ConditionalFilter<
259
259
  'enabled' | 'disabled' | 'hidden',
260
260
  'disabled' | 'hidden',
261
- BaseListTypeInfo
261
+ BaseCollectionTypeInfo
262
262
  >
263
263
  } | null
264
264
  }[]