@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.
- package/CHANGELOG.md +36 -0
- package/CHANGES-1.1.md +134 -0
- package/context/dist/nixxie-cms-core-context.cjs.js +4 -3
- package/context/dist/nixxie-cms-core-context.esm.js +3 -2
- package/dist/declarations/src/access.d.ts +2 -2
- package/dist/declarations/src/access.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/components/Navigation.d.ts +2 -2
- package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/context.d.ts +6 -6
- package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/Fields.d.ts +3 -3
- package/dist/declarations/src/admin-ui/utils/Fields.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/filters.d.ts +5 -5
- package/dist/declarations/src/admin-ui/utils/filters.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts +3 -3
- package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/utils.d.ts +2 -2
- package/dist/declarations/src/admin-ui/utils/utils.d.ts.map +1 -1
- package/dist/declarations/src/context.d.ts +1 -1
- package/dist/declarations/src/context.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/bigInt/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/bigInt/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/bytes/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/bytes/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/calendarDay/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/calendarDay/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/checkbox/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/checkbox/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/decimal/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/decimal/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/file/index.d.ts +4 -4
- package/dist/declarations/src/fields/types/file/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/float/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/float/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/image/index.d.ts +4 -4
- package/dist/declarations/src/fields/types/image/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/integer/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/integer/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/json/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/json/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/multiselect/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/multiselect/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/password/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/password/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/index.d.ts +8 -8
- package/dist/declarations/src/fields/types/relationship/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/types.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/types.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/select/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/select/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/text/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/text/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/timestamp/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/timestamp/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/virtual/index.d.ts +7 -7
- package/dist/declarations/src/fields/types/virtual/index.d.ts.map +1 -1
- package/dist/declarations/src/helpers.d.ts +249 -13
- package/dist/declarations/src/helpers.d.ts.map +1 -1
- package/dist/declarations/src/index.d.ts +9 -4
- package/dist/declarations/src/index.d.ts.map +1 -1
- package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -1
- package/dist/declarations/src/lib/admin-meta.d.ts +11 -11
- package/dist/declarations/src/lib/admin-meta.d.ts.map +1 -1
- package/dist/declarations/src/lib/core/access-control.d.ts +18 -18
- package/dist/declarations/src/lib/core/access-control.d.ts.map +1 -1
- package/dist/declarations/src/lib/core/cascade.d.ts +47 -0
- package/dist/declarations/src/lib/core/cascade.d.ts.map +1 -0
- package/dist/declarations/src/lib/core/initialise-lists.d.ts +27 -24
- package/dist/declarations/src/lib/core/initialise-lists.d.ts.map +1 -1
- package/dist/declarations/src/lib/env.d.ts +9 -0
- package/dist/declarations/src/lib/env.d.ts.map +1 -0
- package/dist/declarations/src/lib/system.d.ts +1 -1
- package/dist/declarations/src/lib/system.d.ts.map +1 -1
- package/dist/declarations/src/list-features.d.ts +162 -0
- package/dist/declarations/src/list-features.d.ts.map +1 -0
- package/dist/declarations/src/schema.d.ts +24 -23
- package/dist/declarations/src/schema.d.ts.map +1 -1
- package/dist/declarations/src/session.d.ts +75 -0
- package/dist/declarations/src/session.d.ts.map +1 -1
- package/dist/declarations/src/types/admin-meta.d.ts +11 -11
- package/dist/declarations/src/types/admin-meta.d.ts.map +1 -1
- package/dist/declarations/src/types/config/access-control.d.ts +42 -42
- package/dist/declarations/src/types/config/access-control.d.ts.map +1 -1
- package/dist/declarations/src/types/config/fields.d.ts +19 -19
- package/dist/declarations/src/types/config/fields.d.ts.map +1 -1
- package/dist/declarations/src/types/config/hooks.d.ts +131 -131
- package/dist/declarations/src/types/config/hooks.d.ts.map +1 -1
- package/dist/declarations/src/types/config/index.d.ts +190 -8
- package/dist/declarations/src/types/config/index.d.ts.map +1 -1
- package/dist/declarations/src/types/config/lists.d.ts +146 -108
- package/dist/declarations/src/types/config/lists.d.ts.map +1 -1
- package/dist/declarations/src/types/context.d.ts +507 -47
- package/dist/declarations/src/types/context.d.ts.map +1 -1
- package/dist/declarations/src/types/next-fields.d.ts +28 -28
- package/dist/declarations/src/types/next-fields.d.ts.map +1 -1
- package/dist/declarations/src/types/type-info.d.ts +3 -3
- package/dist/declarations/src/types/type-info.d.ts.map +1 -1
- package/dist/{express-455ae20c.cjs.js → express-84d534c2.cjs.js} +6 -6
- package/dist/{express-7559ca2d.esm.js → express-d0a4ce99.esm.js} +6 -6
- package/dist/{index-15c8f81e.esm.js → index-5d8b0b4e.esm.js} +363 -183
- package/dist/index-6055753b.cjs.js +393 -0
- package/dist/{index-42045902.cjs.js → index-ac29f382.cjs.js} +363 -185
- package/dist/index-f1703b7b.esm.js +386 -0
- package/dist/nixxie-cms-core.cjs.js +1388 -30
- package/dist/nixxie-cms-core.esm.js +1362 -24
- package/dist/{non-null-graphql-add6bb3d.cjs.js → non-null-graphql-4a44c122.cjs.js} +1 -1
- package/dist/{non-null-graphql-a84ed64d.esm.js → non-null-graphql-8c5feaae.esm.js} +1 -1
- package/dist/{resolve-hooks-165a9ce2.cjs.js → resolve-hooks-10a5f84c.cjs.js} +240 -6
- package/dist/{resolve-hooks-6813a045.esm.js → resolve-hooks-9e676794.esm.js} +238 -7
- package/dist/{system-a321642d.cjs.js → system-6b37a5f8.cjs.js} +33 -7
- package/dist/{system-03e49e4f.esm.js → system-e591d821.esm.js} +33 -7
- package/fields/dist/nixxie-cms-core-fields.cjs.js +29 -576
- package/fields/dist/nixxie-cms-core-fields.esm.js +18 -565
- package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +4 -2
- package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +4 -2
- package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +1 -6
- package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +1 -6
- package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +4 -2
- package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +4 -2
- package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +4 -3
- package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +4 -3
- package/package.json +4 -4
- package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +4 -3
- package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +4 -3
- package/scripts/dist/nixxie-cms-core-scripts.cjs.js +4 -3
- package/scripts/dist/nixxie-cms-core-scripts.esm.js +4 -3
- package/session/dist/nixxie-cms-core-session.cjs.js +286 -0
- package/session/dist/nixxie-cms-core-session.esm.js +279 -1
- package/src/access.ts +25 -25
- package/src/admin-ui/admin-meta-graphql.ts +5 -5
- package/src/admin-ui/components/CreateButtonLink.tsx +46 -46
- package/src/admin-ui/components/Navigation.tsx +3 -3
- package/src/admin-ui/context.tsx +6 -6
- package/src/admin-ui/utils/Fields.tsx +241 -241
- package/src/admin-ui/utils/actionData.ts +36 -36
- package/src/admin-ui/utils/filters.ts +148 -148
- package/src/admin-ui/utils/useCreateItem.ts +171 -171
- package/src/admin-ui/utils/utils.tsx +127 -127
- package/src/context.ts +1 -1
- package/src/fields/non-null-graphql.ts +115 -115
- package/src/fields/types/bigInt/index.ts +6 -6
- package/src/fields/types/bytes/index.ts +6 -6
- package/src/fields/types/calendarDay/index.ts +18 -19
- package/src/fields/types/checkbox/index.ts +6 -6
- package/src/fields/types/decimal/index.ts +6 -6
- package/src/fields/types/file/index.ts +8 -8
- package/src/fields/types/float/index.ts +6 -6
- package/src/fields/types/image/index.ts +8 -8
- package/src/fields/types/integer/index.ts +6 -6
- package/src/fields/types/json/index.ts +5 -5
- package/src/fields/types/multiselect/index.ts +7 -7
- package/src/fields/types/multiselect/views/index.tsx +149 -151
- package/src/fields/types/password/index.ts +6 -6
- package/src/fields/types/relationship/index.ts +13 -13
- package/src/fields/types/relationship/views/ComboboxMany.tsx +110 -110
- package/src/fields/types/relationship/views/ComboboxSingle.tsx +115 -115
- package/src/fields/types/relationship/views/ContextualActions.tsx +139 -139
- package/src/fields/types/relationship/views/index.tsx +492 -492
- package/src/fields/types/relationship/views/types.ts +46 -46
- package/src/fields/types/relationship/views/useApolloQuery.ts +185 -185
- package/src/fields/types/relationship/views/useFilter.tsx +109 -109
- package/src/fields/types/select/index.ts +6 -6
- package/src/fields/types/text/index.ts +6 -6
- package/src/fields/types/timestamp/index.ts +23 -21
- package/src/fields/types/virtual/index.ts +11 -11
- package/src/helpers.ts +773 -42
- package/src/index.ts +66 -24
- package/src/internal-unstable/admin-ui/pages/ItemPage/common.tsx +4 -4
- package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +5 -5
- package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +8 -8
- package/src/lib/admin-meta.ts +369 -369
- package/src/lib/context/createContext.ts +6 -0
- package/src/lib/core/access-control.ts +434 -434
- package/src/lib/core/cascade.ts +236 -0
- package/src/lib/core/initialise-lists.ts +49 -33
- package/src/lib/core/mutations/index.ts +7 -0
- package/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts +145 -145
- package/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts +71 -71
- package/src/lib/core/queries/output-field.ts +178 -178
- package/src/lib/env.ts +50 -0
- package/src/lib/id-field.ts +2 -2
- package/src/lib/system.ts +221 -207
- package/src/lib/typescript-schema-printer.ts +227 -227
- package/src/list-features.ts +476 -0
- package/src/schema.ts +92 -22
- package/src/session.ts +225 -0
- package/src/types/admin-meta.ts +218 -218
- package/src/types/config/access-control.ts +186 -186
- package/src/types/config/fields.ts +96 -96
- package/src/types/config/hooks.ts +529 -529
- package/src/types/config/index.ts +206 -7
- package/src/types/config/lists.ts +606 -565
- package/src/types/context.ts +592 -55
- package/src/types/next-fields.ts +31 -31
- package/src/types/type-info.ts +38 -38
- 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
|
+
}
|
package/src/types/admin-meta.ts
CHANGED
|
@@ -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,
|
|
5
|
-
import type {
|
|
6
|
-
import type { GraphQLNames, JSONValue } from './utils'
|
|
7
|
-
|
|
8
|
-
export type NavigationProps = {
|
|
9
|
-
lists:
|
|
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',
|
|
91
|
-
isRequired: ConditionalFilterCase<
|
|
92
|
-
}
|
|
93
|
-
itemView: {
|
|
94
|
-
fieldMode: ConditionalFilter<'edit' | 'read' | 'hidden', 'read' | 'hidden',
|
|
95
|
-
fieldPosition: 'form' | 'sidebar'
|
|
96
|
-
isRequired: ConditionalFilterCase<
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
>
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export type
|
|
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:
|
|
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
|
+
}
|