@nixxie-cms/core 1.0.2 → 1.1.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 (202) hide show
  1. package/CHANGES-1.1.md +134 -0
  2. package/context/dist/nixxie-cms-core-context.cjs.js +4 -3
  3. package/context/dist/nixxie-cms-core-context.esm.js +3 -2
  4. package/dist/declarations/src/access.d.ts +2 -2
  5. package/dist/declarations/src/access.d.ts.map +1 -1
  6. package/dist/declarations/src/admin-ui/components/Navigation.d.ts +2 -2
  7. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  8. package/dist/declarations/src/admin-ui/context.d.ts +6 -6
  9. package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
  10. package/dist/declarations/src/admin-ui/utils/Fields.d.ts +3 -3
  11. package/dist/declarations/src/admin-ui/utils/Fields.d.ts.map +1 -1
  12. package/dist/declarations/src/admin-ui/utils/filters.d.ts +5 -5
  13. package/dist/declarations/src/admin-ui/utils/filters.d.ts.map +1 -1
  14. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts +3 -3
  15. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
  16. package/dist/declarations/src/admin-ui/utils/utils.d.ts +2 -2
  17. package/dist/declarations/src/admin-ui/utils/utils.d.ts.map +1 -1
  18. package/dist/declarations/src/context.d.ts +1 -1
  19. package/dist/declarations/src/context.d.ts.map +1 -1
  20. package/dist/declarations/src/fields/types/bigInt/index.d.ts +3 -3
  21. package/dist/declarations/src/fields/types/bigInt/index.d.ts.map +1 -1
  22. package/dist/declarations/src/fields/types/bytes/index.d.ts +3 -3
  23. package/dist/declarations/src/fields/types/bytes/index.d.ts.map +1 -1
  24. package/dist/declarations/src/fields/types/calendarDay/index.d.ts +3 -3
  25. package/dist/declarations/src/fields/types/calendarDay/index.d.ts.map +1 -1
  26. package/dist/declarations/src/fields/types/checkbox/index.d.ts +3 -3
  27. package/dist/declarations/src/fields/types/checkbox/index.d.ts.map +1 -1
  28. package/dist/declarations/src/fields/types/decimal/index.d.ts +3 -3
  29. package/dist/declarations/src/fields/types/decimal/index.d.ts.map +1 -1
  30. package/dist/declarations/src/fields/types/file/index.d.ts +4 -4
  31. package/dist/declarations/src/fields/types/file/index.d.ts.map +1 -1
  32. package/dist/declarations/src/fields/types/float/index.d.ts +3 -3
  33. package/dist/declarations/src/fields/types/float/index.d.ts.map +1 -1
  34. package/dist/declarations/src/fields/types/image/index.d.ts +4 -4
  35. package/dist/declarations/src/fields/types/image/index.d.ts.map +1 -1
  36. package/dist/declarations/src/fields/types/integer/index.d.ts +3 -3
  37. package/dist/declarations/src/fields/types/integer/index.d.ts.map +1 -1
  38. package/dist/declarations/src/fields/types/json/index.d.ts +3 -3
  39. package/dist/declarations/src/fields/types/json/index.d.ts.map +1 -1
  40. package/dist/declarations/src/fields/types/multiselect/index.d.ts +3 -3
  41. package/dist/declarations/src/fields/types/multiselect/index.d.ts.map +1 -1
  42. package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
  43. package/dist/declarations/src/fields/types/password/index.d.ts +3 -3
  44. package/dist/declarations/src/fields/types/password/index.d.ts.map +1 -1
  45. package/dist/declarations/src/fields/types/relationship/index.d.ts +8 -8
  46. package/dist/declarations/src/fields/types/relationship/index.d.ts.map +1 -1
  47. package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts +3 -3
  48. package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts.map +1 -1
  49. package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts +3 -3
  50. package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts.map +1 -1
  51. package/dist/declarations/src/fields/types/relationship/views/index.d.ts +3 -3
  52. package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
  53. package/dist/declarations/src/fields/types/relationship/views/types.d.ts +3 -3
  54. package/dist/declarations/src/fields/types/relationship/views/types.d.ts.map +1 -1
  55. package/dist/declarations/src/fields/types/select/index.d.ts +3 -3
  56. package/dist/declarations/src/fields/types/select/index.d.ts.map +1 -1
  57. package/dist/declarations/src/fields/types/text/index.d.ts +3 -3
  58. package/dist/declarations/src/fields/types/text/index.d.ts.map +1 -1
  59. package/dist/declarations/src/fields/types/timestamp/index.d.ts +3 -3
  60. package/dist/declarations/src/fields/types/timestamp/index.d.ts.map +1 -1
  61. package/dist/declarations/src/fields/types/virtual/index.d.ts +7 -7
  62. package/dist/declarations/src/fields/types/virtual/index.d.ts.map +1 -1
  63. package/dist/declarations/src/helpers.d.ts +249 -13
  64. package/dist/declarations/src/helpers.d.ts.map +1 -1
  65. package/dist/declarations/src/index.d.ts +9 -4
  66. package/dist/declarations/src/index.d.ts.map +1 -1
  67. package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -1
  68. package/dist/declarations/src/lib/admin-meta.d.ts +11 -11
  69. package/dist/declarations/src/lib/admin-meta.d.ts.map +1 -1
  70. package/dist/declarations/src/lib/core/access-control.d.ts +18 -18
  71. package/dist/declarations/src/lib/core/access-control.d.ts.map +1 -1
  72. package/dist/declarations/src/lib/core/cascade.d.ts +47 -0
  73. package/dist/declarations/src/lib/core/cascade.d.ts.map +1 -0
  74. package/dist/declarations/src/lib/core/initialise-lists.d.ts +27 -24
  75. package/dist/declarations/src/lib/core/initialise-lists.d.ts.map +1 -1
  76. package/dist/declarations/src/lib/env.d.ts +9 -0
  77. package/dist/declarations/src/lib/env.d.ts.map +1 -0
  78. package/dist/declarations/src/lib/system.d.ts +1 -1
  79. package/dist/declarations/src/lib/system.d.ts.map +1 -1
  80. package/dist/declarations/src/list-features.d.ts +162 -0
  81. package/dist/declarations/src/list-features.d.ts.map +1 -0
  82. package/dist/declarations/src/schema.d.ts +24 -23
  83. package/dist/declarations/src/schema.d.ts.map +1 -1
  84. package/dist/declarations/src/session.d.ts +75 -0
  85. package/dist/declarations/src/session.d.ts.map +1 -1
  86. package/dist/declarations/src/types/admin-meta.d.ts +11 -11
  87. package/dist/declarations/src/types/admin-meta.d.ts.map +1 -1
  88. package/dist/declarations/src/types/config/access-control.d.ts +42 -42
  89. package/dist/declarations/src/types/config/access-control.d.ts.map +1 -1
  90. package/dist/declarations/src/types/config/fields.d.ts +19 -19
  91. package/dist/declarations/src/types/config/fields.d.ts.map +1 -1
  92. package/dist/declarations/src/types/config/hooks.d.ts +131 -131
  93. package/dist/declarations/src/types/config/hooks.d.ts.map +1 -1
  94. package/dist/declarations/src/types/config/index.d.ts +171 -8
  95. package/dist/declarations/src/types/config/index.d.ts.map +1 -1
  96. package/dist/declarations/src/types/config/lists.d.ts +146 -108
  97. package/dist/declarations/src/types/config/lists.d.ts.map +1 -1
  98. package/dist/declarations/src/types/context.d.ts +349 -47
  99. package/dist/declarations/src/types/context.d.ts.map +1 -1
  100. package/dist/declarations/src/types/next-fields.d.ts +28 -28
  101. package/dist/declarations/src/types/next-fields.d.ts.map +1 -1
  102. package/dist/declarations/src/types/type-info.d.ts +3 -3
  103. package/dist/declarations/src/types/type-info.d.ts.map +1 -1
  104. package/dist/{express-7559ca2d.esm.js → express-0abbce07.esm.js} +6 -6
  105. package/dist/{express-455ae20c.cjs.js → express-7ca6f76a.cjs.js} +6 -6
  106. package/dist/{index-baa799e0.esm.js → index-5d8b0b4e.esm.js} +363 -178
  107. package/dist/index-6055753b.cjs.js +393 -0
  108. package/dist/{index-89635494.cjs.js → index-ac29f382.cjs.js} +363 -180
  109. package/dist/index-f1703b7b.esm.js +386 -0
  110. package/dist/nixxie-cms-core.cjs.js +1387 -30
  111. package/dist/nixxie-cms-core.esm.js +1361 -24
  112. package/dist/{non-null-graphql-add6bb3d.cjs.js → non-null-graphql-4a44c122.cjs.js} +1 -1
  113. package/dist/{non-null-graphql-a84ed64d.esm.js → non-null-graphql-8c5feaae.esm.js} +1 -1
  114. package/dist/{resolve-hooks-165a9ce2.cjs.js → resolve-hooks-10a5f84c.cjs.js} +240 -6
  115. package/dist/{resolve-hooks-6813a045.esm.js → resolve-hooks-9e676794.esm.js} +238 -7
  116. package/dist/{system-03e49e4f.esm.js → system-4d2a2648.esm.js} +32 -7
  117. package/dist/{system-a321642d.cjs.js → system-69e1a285.cjs.js} +32 -7
  118. package/fields/dist/nixxie-cms-core-fields.cjs.js +29 -576
  119. package/fields/dist/nixxie-cms-core-fields.esm.js +18 -565
  120. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +4 -2
  121. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +4 -2
  122. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +1 -6
  123. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +1 -6
  124. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +4 -2
  125. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +4 -2
  126. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +4 -3
  127. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +4 -3
  128. package/package.json +4 -4
  129. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +4 -3
  130. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +4 -3
  131. package/scripts/dist/nixxie-cms-core-scripts.cjs.js +4 -3
  132. package/scripts/dist/nixxie-cms-core-scripts.esm.js +4 -3
  133. package/session/dist/nixxie-cms-core-session.cjs.js +286 -0
  134. package/session/dist/nixxie-cms-core-session.esm.js +279 -1
  135. package/src/access.ts +25 -25
  136. package/src/admin-ui/admin-meta-graphql.ts +5 -5
  137. package/src/admin-ui/components/CreateButtonLink.tsx +46 -46
  138. package/src/admin-ui/components/Navigation.tsx +3 -3
  139. package/src/admin-ui/context.tsx +6 -6
  140. package/src/admin-ui/utils/Fields.tsx +241 -241
  141. package/src/admin-ui/utils/actionData.ts +36 -36
  142. package/src/admin-ui/utils/filters.ts +148 -148
  143. package/src/admin-ui/utils/useCreateItem.ts +171 -171
  144. package/src/admin-ui/utils/utils.tsx +127 -127
  145. package/src/context.ts +1 -1
  146. package/src/fields/non-null-graphql.ts +115 -115
  147. package/src/fields/types/bigInt/index.ts +6 -6
  148. package/src/fields/types/bytes/index.ts +6 -6
  149. package/src/fields/types/calendarDay/index.ts +18 -19
  150. package/src/fields/types/checkbox/index.ts +6 -6
  151. package/src/fields/types/decimal/index.ts +6 -6
  152. package/src/fields/types/file/index.ts +8 -8
  153. package/src/fields/types/float/index.ts +6 -6
  154. package/src/fields/types/image/index.ts +8 -8
  155. package/src/fields/types/integer/index.ts +6 -6
  156. package/src/fields/types/json/index.ts +5 -5
  157. package/src/fields/types/multiselect/index.ts +7 -7
  158. package/src/fields/types/multiselect/views/index.tsx +149 -151
  159. package/src/fields/types/password/index.ts +6 -6
  160. package/src/fields/types/relationship/index.ts +13 -13
  161. package/src/fields/types/relationship/views/ComboboxMany.tsx +110 -110
  162. package/src/fields/types/relationship/views/ComboboxSingle.tsx +115 -115
  163. package/src/fields/types/relationship/views/ContextualActions.tsx +139 -139
  164. package/src/fields/types/relationship/views/index.tsx +492 -492
  165. package/src/fields/types/relationship/views/types.ts +46 -46
  166. package/src/fields/types/relationship/views/useApolloQuery.ts +185 -185
  167. package/src/fields/types/relationship/views/useFilter.tsx +109 -109
  168. package/src/fields/types/select/index.ts +6 -6
  169. package/src/fields/types/text/index.ts +6 -6
  170. package/src/fields/types/timestamp/index.ts +29 -22
  171. package/src/fields/types/virtual/index.ts +11 -11
  172. package/src/helpers.ts +773 -42
  173. package/src/index.ts +66 -24
  174. package/src/internal-unstable/admin-ui/pages/ItemPage/common.tsx +4 -4
  175. package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +5 -5
  176. package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +8 -8
  177. package/src/lib/admin-meta.ts +369 -369
  178. package/src/lib/context/createContext.ts +5 -0
  179. package/src/lib/core/access-control.ts +434 -434
  180. package/src/lib/core/cascade.ts +236 -0
  181. package/src/lib/core/initialise-lists.ts +49 -33
  182. package/src/lib/core/mutations/index.ts +7 -0
  183. package/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts +145 -145
  184. package/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts +71 -71
  185. package/src/lib/core/queries/output-field.ts +178 -178
  186. package/src/lib/env.ts +50 -0
  187. package/src/lib/id-field.ts +2 -2
  188. package/src/lib/system.ts +221 -207
  189. package/src/lib/typescript-schema-printer.ts +227 -227
  190. package/src/list-features.ts +476 -0
  191. package/src/schema.ts +91 -22
  192. package/src/session.ts +225 -0
  193. package/src/types/admin-meta.ts +218 -218
  194. package/src/types/config/access-control.ts +186 -186
  195. package/src/types/config/fields.ts +96 -96
  196. package/src/types/config/hooks.ts +529 -529
  197. package/src/types/config/index.ts +185 -7
  198. package/src/types/config/lists.ts +606 -565
  199. package/src/types/context.ts +426 -55
  200. package/src/types/next-fields.ts +31 -31
  201. package/src/types/type-info.ts +38 -38
  202. package/src/types/type-tests.ts +21 -21
@@ -1,369 +1,369 @@
1
- import path from 'node:path'
2
-
3
- import type {
4
- BaseFieldTypeInfo,
5
- BaseItem,
6
- BaseListTypeInfo,
7
- ConditionalFilter,
8
- ConditionalFilterCase,
9
- NixxieConfig,
10
- NixxieContext,
11
- ListSortDescriptor,
12
- MaybeBooleanItemFunctionWithFilter,
13
- MaybeFieldFunction,
14
- MaybeItemActionFunctionWithFilter,
15
- MaybeItemFieldFunction,
16
- MaybeItemFieldFunctionWithFilter,
17
- MaybePromise,
18
- MaybeSessionFunction,
19
- } from '../types'
20
- import type { ActionMeta, FieldMeta, ListMeta } from '../types/admin-meta'
21
- import type { GraphQLNames, JSONValue } from '../types/utils'
22
- import type { InitialisedList } from './core/initialise-lists'
23
-
24
- type EmptyResolver<Return> = (args: {}, context: NixxieContext) => MaybePromise<Return>
25
-
26
- type FieldMetaSource_ = {
27
- listKey: string
28
- fieldKey: string
29
- isOrderable: EmptyResolver<boolean>
30
- isFilterable: EmptyResolver<boolean>
31
-
32
- createView: {
33
- fieldMode: EmptyResolver<ConditionalFilter<'edit' | 'hidden', 'hidden', BaseListTypeInfo>>
34
- isRequired: EmptyResolver<ConditionalFilterCase<BaseListTypeInfo>>
35
- }
36
- itemView: {
37
- fieldMode: MaybeItemFieldFunctionWithFilter<
38
- 'edit' | 'read' | 'hidden',
39
- 'read' | 'hidden',
40
- BaseListTypeInfo,
41
- BaseFieldTypeInfo
42
- >
43
- fieldPosition: MaybeItemFieldFunction<'form' | 'sidebar', BaseListTypeInfo, BaseFieldTypeInfo>
44
- isRequired: MaybeBooleanItemFunctionWithFilter<BaseListTypeInfo, BaseFieldTypeInfo>
45
- }
46
- listView: {
47
- fieldMode: EmptyResolver<'read' | 'hidden'>
48
- }
49
- item: BaseItem | null
50
- itemField: BaseItem[string] | null
51
- }
52
- export type FieldMetaSource = FieldMetaSource_ &
53
- Omit<FieldMeta, keyof FieldMetaSource_ | 'controller' | 'views'>
54
-
55
- type ActionMetaSource_ = {
56
- listKey: string
57
- itemView: Omit<ActionMeta['itemView'], 'actionMode'> & {
58
- actionMode: MaybeItemActionFunctionWithFilter<
59
- 'enabled' | 'disabled' | 'hidden',
60
- 'disabled' | 'hidden',
61
- BaseListTypeInfo
62
- >
63
- }
64
- listView: {
65
- actionMode: EmptyResolver<
66
- ConditionalFilter<'enabled' | 'disabled' | 'hidden', 'disabled' | 'hidden', BaseListTypeInfo>
67
- >
68
- }
69
- item: BaseItem | null
70
- }
71
- export type ActionMetaSource = ActionMetaSource_ & Omit<ActionMeta, keyof ActionMetaSource_>
72
-
73
- type ListMetaSource_ = {
74
- fields: FieldMetaSource[]
75
- fieldsByKey: Record<string, FieldMetaSource>
76
- groups: {
77
- label: string
78
- description: string
79
- fields: FieldMetaSource[]
80
- }[]
81
- actions: ActionMetaSource[]
82
- graphql: { names: GraphQLNames }
83
- pageSize: number
84
- initialColumns: string[]
85
- initialFilter: EmptyResolver<JSONValue>
86
- initialSearchFields: string[]
87
- initialSort: ListSortDescriptor<string> | null
88
- isSingleton: boolean
89
-
90
- hideNavigation: EmptyResolver<boolean>
91
- hideCreate: EmptyResolver<boolean>
92
- hideDelete: EmptyResolver<boolean>
93
- item: BaseItem | null
94
- }
95
- export type ListMetaSource = ListMetaSource_ & Omit<ListMeta, keyof ListMetaSource_>
96
-
97
- export type AdminMetaSource = {
98
- lists: ListMetaSource[]
99
- listsByKey: Record<string, ListMetaSource>
100
- views: string[]
101
- isAccessAllowed: (context: NixxieContext) => MaybePromise<boolean>
102
- }
103
-
104
- export function createAdminMeta(
105
- config: NixxieConfig,
106
- initialisedLists: Record<string, InitialisedList>
107
- ) {
108
- const { lists } = config
109
- const adminMetaRoot: AdminMetaSource = {
110
- listsByKey: {},
111
- lists: [],
112
- views: [],
113
- isAccessAllowed: config.ui?.isAccessAllowed,
114
- }
115
-
116
- const omittedLists: string[] = []
117
-
118
- for (const [listKey, list] of Object.entries(initialisedLists)) {
119
- const listConfig = lists[listKey]
120
-
121
- // TODO: is this reasonable?
122
- if (list.graphql.isEnabled.query === false) {
123
- omittedLists.push(listKey)
124
- continue
125
- }
126
-
127
- let initialColumns: string[]
128
- if (listConfig.ui?.listView?.initialColumns) {
129
- // if they've asked for a particular thing, give them that thing
130
- initialColumns = listConfig.ui.listView.initialColumns as string[]
131
- } else {
132
- // otherwise, we'll start with the labelfield on the left and then add
133
- // 2 more fields to the right of that. We don't include the 'id' field
134
- // unless it happened to be the labelField
135
- initialColumns = [
136
- list.ui.labelField,
137
- ...Object.keys(list.fields)
138
- .filter(fieldKey => list.fields[fieldKey].graphql.isEnabled.read)
139
- .filter(fieldKey => fieldKey !== list.ui.labelField)
140
- .filter(fieldKey => fieldKey !== 'id'),
141
- ].slice(0, 3)
142
- }
143
-
144
- let initialSearchFields = listConfig.ui?.searchFields?.concat()
145
- if (!initialSearchFields) {
146
- initialSearchFields = [...list.ui.triviallySearchableFields]
147
- }
148
-
149
- const maximumPageSize = Math.min(
150
- listConfig.ui?.listView?.pageSize ?? 50,
151
- (list.graphql.types.findManyArgs.take.defaultValue ?? Infinity) as number
152
- )
153
-
154
- adminMetaRoot.listsByKey[listKey] = {
155
- key: listKey,
156
- path: list.ui.labels.path,
157
-
158
- label: list.ui.labels.label,
159
- singular: list.ui.labels.singular,
160
- plural: list.ui.labels.plural,
161
-
162
- labelField: list.ui.labelField,
163
- fields: [],
164
- fieldsByKey: {},
165
- groups: [],
166
- actions: [],
167
-
168
- graphql: {
169
- names: list.graphql.names,
170
- },
171
-
172
- pageSize: maximumPageSize,
173
- initialColumns,
174
- initialSearchFields,
175
- initialSort:
176
- (listConfig.ui?.listView?.initialSort as ListSortDescriptor<string> | undefined) ?? null,
177
- initialFilter: normalizeMaybeSessionFunction(listConfig.ui?.listView?.initialFilter ?? {}),
178
- isSingleton: list.isSingleton,
179
-
180
- hideNavigation: normalizeMaybeSessionFunction(listConfig.ui?.hideNavigation ?? false),
181
- hideCreate: normalizeMaybeSessionFunction(
182
- listConfig.ui?.hideCreate ?? !list.graphql.isEnabled.create
183
- ),
184
- hideDelete: normalizeMaybeSessionFunction(
185
- listConfig.ui?.hideDelete ?? !list.graphql.isEnabled.delete
186
- ),
187
-
188
- item: null, // part of resolver
189
- } satisfies ListMetaSource
190
-
191
- adminMetaRoot.lists.push(adminMetaRoot.listsByKey[listKey])
192
- }
193
-
194
- let uniqueViewCount = -1
195
- const stringViewsToIndex: Record<string, number> = {}
196
- function getViewId(view: string) {
197
- if (stringViewsToIndex[view] !== undefined) return stringViewsToIndex[view]
198
-
199
- uniqueViewCount++
200
- stringViewsToIndex[view] = uniqueViewCount
201
- adminMetaRoot.views.push(view)
202
- return uniqueViewCount
203
- }
204
-
205
- for (const [listKey, list] of Object.entries(initialisedLists)) {
206
- if (omittedLists.includes(listKey)) continue
207
-
208
- const listMeta = adminMetaRoot.listsByKey[listKey]
209
-
210
- // populate .fields
211
- for (const [fieldKey, field] of Object.entries(list.fields)) {
212
- // if the field is a relationship field and is related to an omitted list, skip.
213
- if (field.dbField.kind === 'relation' && omittedLists.includes(field.dbField.list)) continue
214
- if (Object.values(field.graphql.isEnabled).every(x => x === false)) continue
215
- assertValidView(
216
- field.views,
217
- `The \`views\` on the implementation of the field type at lists.${listKey}.fields.${fieldKey}`
218
- )
219
-
220
- const baseOrderFilterArgs = { fieldKey, listKey: list.listKey }
221
- const isNonNull = (['read', 'create', 'update'] as const).filter(
222
- operation => field.graphql.isNonNull[operation]
223
- )
224
- const fieldMeta = {
225
- // FieldMeta
226
- key: fieldKey,
227
- label: field.ui.label,
228
- description: field.ui.description,
229
- fieldMeta: null,
230
- viewsIndex: getViewId(field.views),
231
- customViewsIndex:
232
- field.ui.views === null
233
- ? null
234
- : (assertValidView(field.views, `lists.${listKey}.fields.${fieldKey}.ui.views`),
235
- getViewId(field.ui.views)),
236
- search: list.ui.searchableFields.get(fieldKey) ?? null,
237
-
238
- // FieldMetaSource_
239
- listKey: listKey,
240
- fieldKey: fieldKey,
241
- isFilterable: normalizeIsOrderFilter(
242
- field.input?.where ? field.graphql.isEnabled.filter : false,
243
- baseOrderFilterArgs
244
- ),
245
- isOrderable: normalizeIsOrderFilter(
246
- field.input?.orderBy ? field.graphql.isEnabled.orderBy : false,
247
- baseOrderFilterArgs
248
- ),
249
-
250
- isNonNull,
251
- createView: {
252
- fieldMode: normalizeMaybeSessionFunction(field.ui.createView.fieldMode),
253
- isRequired: normalizeMaybeSessionFunction(field.ui.createView.isRequired ?? false),
254
- },
255
- itemView: {
256
- fieldMode: field.ui.itemView.fieldMode,
257
- fieldPosition: field.ui.itemView.fieldPosition,
258
- isRequired: field.ui.itemView.isRequired,
259
- },
260
- listView: {
261
- fieldMode: normalizeMaybeSessionFunction(field.ui.listView.fieldMode),
262
- },
263
-
264
- item: null, // part of resolver
265
- itemField: null, // part of resolver
266
- } satisfies FieldMetaSource
267
-
268
- listMeta.fields.push(fieldMeta)
269
- listMeta.fieldsByKey[fieldKey] = fieldMeta
270
- }
271
-
272
- // populate .actions
273
- for (const action of list.actions) {
274
- listMeta.actions.push({
275
- // ActionMeta
276
- key: action.actionKey,
277
- label: action.ui.label,
278
- icon: action.ui.icon,
279
- messages: {
280
- ...action.ui.messages,
281
- },
282
- graphql: {
283
- arguments: action.graphql.arguments,
284
- names: action.graphql.names,
285
- },
286
-
287
- // ActionMetaSource_
288
- listKey,
289
- itemView: {
290
- ...action.ui.itemView,
291
- },
292
- listView: {
293
- actionMode: normalizeMaybeSessionFunction(action.ui.listView.actionMode),
294
- },
295
- item: null, // part of resolver
296
- })
297
- }
298
-
299
- // populate .groups
300
- for (const group of list.groups) {
301
- listMeta.groups.push({
302
- label: group.label,
303
- description: group.description,
304
- fields: group.fields.map(
305
- fieldKey => adminMetaRoot.listsByKey[listKey].fieldsByKey[fieldKey]
306
- ),
307
- })
308
- }
309
- }
310
-
311
- // we do this seperately to the above so that fields can check other fields to validate their config or etc.
312
- // (ofc they won't necessarily be able to see other field's fieldMeta)
313
- for (const [key, list] of Object.entries(initialisedLists)) {
314
- if (list.graphql.isEnabled.query === false) continue
315
- for (const fieldMetaSource of adminMetaRoot.listsByKey[key].fields) {
316
- // if the field is a relationship field and is related to an omitted list, skip.
317
- const dbField = list.fields[fieldMetaSource.fieldKey].dbField
318
- if (dbField.kind === 'relation' && omittedLists.includes(dbField.list)) continue
319
-
320
- currentAdminMeta = adminMetaRoot
321
- try {
322
- fieldMetaSource.fieldMeta = list.fields[fieldMetaSource.fieldKey].getAdminMeta?.() ?? null
323
- } finally {
324
- currentAdminMeta = undefined
325
- }
326
- }
327
- }
328
-
329
- return adminMetaRoot
330
- }
331
-
332
- let currentAdminMeta: undefined | AdminMetaSource
333
-
334
- export function getAdminMetaForRelationshipField() {
335
- if (currentAdminMeta) return currentAdminMeta
336
- throw new Error('unexpected call to getAdminMetaInRelationshipField')
337
- }
338
-
339
- function assertValidView(view: string, location: string) {
340
- if (view.includes('\\')) {
341
- throw new Error(
342
- `${location} contains a backslash, which is invalid. You need to use a module path that is resolved from where 'nixxie start' is run (see https://github.com/nixxiecms/nixxie/pull/7805)`
343
- )
344
- }
345
-
346
- if (path.isAbsolute(view)) {
347
- throw new Error(
348
- `${location} is an absolute path, which is invalid. You need to use a module path that is resolved from where 'nixxie start' is run (see https://github.com/nixxiecms/nixxie/pull/7805)`
349
- )
350
- }
351
- }
352
-
353
- function normalizeMaybeSessionFunction<Return extends string | boolean | object | null | number>(
354
- input: MaybeSessionFunction<Return, BaseListTypeInfo>
355
- ): EmptyResolver<Return> {
356
- if (typeof input !== 'function') return () => input
357
- return (_, context) => input({ context, session: context.session })
358
- }
359
-
360
- function normalizeIsOrderFilter(
361
- input: MaybeFieldFunction<BaseListTypeInfo>,
362
- baseOrderFilterArgs: {
363
- listKey: string
364
- fieldKey: string
365
- }
366
- ): EmptyResolver<boolean> {
367
- if (typeof input !== 'function') return () => input
368
- return (_, context) => input({ context, session: context.session, ...baseOrderFilterArgs })
369
- }
1
+ import path from 'node:path'
2
+
3
+ import type {
4
+ BaseFieldTypeInfo,
5
+ BaseItem,
6
+ BaseCollectionTypeInfo,
7
+ ConditionalFilter,
8
+ ConditionalFilterCase,
9
+ NixxieConfig,
10
+ NixxieContext,
11
+ CollectionSortDescriptor,
12
+ MaybeBooleanItemFunctionWithFilter,
13
+ MaybeFieldFunction,
14
+ MaybeItemActionFunctionWithFilter,
15
+ MaybeItemFieldFunction,
16
+ MaybeItemFieldFunctionWithFilter,
17
+ MaybePromise,
18
+ MaybeSessionFunction,
19
+ } from '../types'
20
+ import type { ActionMeta, FieldMeta, CollectionMeta } from '../types/admin-meta'
21
+ import type { GraphQLNames, JSONValue } from '../types/utils'
22
+ import type { InitialisedList } from './core/initialise-lists'
23
+
24
+ type EmptyResolver<Return> = (args: {}, context: NixxieContext) => MaybePromise<Return>
25
+
26
+ type FieldMetaSource_ = {
27
+ listKey: string
28
+ fieldKey: string
29
+ isOrderable: EmptyResolver<boolean>
30
+ isFilterable: EmptyResolver<boolean>
31
+
32
+ createView: {
33
+ fieldMode: EmptyResolver<ConditionalFilter<'edit' | 'hidden', 'hidden', BaseCollectionTypeInfo>>
34
+ isRequired: EmptyResolver<ConditionalFilterCase<BaseCollectionTypeInfo>>
35
+ }
36
+ itemView: {
37
+ fieldMode: MaybeItemFieldFunctionWithFilter<
38
+ 'edit' | 'read' | 'hidden',
39
+ 'read' | 'hidden',
40
+ BaseCollectionTypeInfo,
41
+ BaseFieldTypeInfo
42
+ >
43
+ fieldPosition: MaybeItemFieldFunction<'form' | 'sidebar', BaseCollectionTypeInfo, BaseFieldTypeInfo>
44
+ isRequired: MaybeBooleanItemFunctionWithFilter<BaseCollectionTypeInfo, BaseFieldTypeInfo>
45
+ }
46
+ listView: {
47
+ fieldMode: EmptyResolver<'read' | 'hidden'>
48
+ }
49
+ item: BaseItem | null
50
+ itemField: BaseItem[string] | null
51
+ }
52
+ export type FieldMetaSource = FieldMetaSource_ &
53
+ Omit<FieldMeta, keyof FieldMetaSource_ | 'controller' | 'views'>
54
+
55
+ type ActionMetaSource_ = {
56
+ listKey: string
57
+ itemView: Omit<ActionMeta['itemView'], 'actionMode'> & {
58
+ actionMode: MaybeItemActionFunctionWithFilter<
59
+ 'enabled' | 'disabled' | 'hidden',
60
+ 'disabled' | 'hidden',
61
+ BaseCollectionTypeInfo
62
+ >
63
+ }
64
+ listView: {
65
+ actionMode: EmptyResolver<
66
+ ConditionalFilter<'enabled' | 'disabled' | 'hidden', 'disabled' | 'hidden', BaseCollectionTypeInfo>
67
+ >
68
+ }
69
+ item: BaseItem | null
70
+ }
71
+ export type ActionMetaSource = ActionMetaSource_ & Omit<ActionMeta, keyof ActionMetaSource_>
72
+
73
+ type ListMetaSource_ = {
74
+ fields: FieldMetaSource[]
75
+ fieldsByKey: Record<string, FieldMetaSource>
76
+ groups: {
77
+ label: string
78
+ description: string
79
+ fields: FieldMetaSource[]
80
+ }[]
81
+ actions: ActionMetaSource[]
82
+ graphql: { names: GraphQLNames }
83
+ pageSize: number
84
+ initialColumns: string[]
85
+ initialFilter: EmptyResolver<JSONValue>
86
+ initialSearchFields: string[]
87
+ initialSort: CollectionSortDescriptor<string> | null
88
+ isSingleton: boolean
89
+
90
+ hideNavigation: EmptyResolver<boolean>
91
+ hideCreate: EmptyResolver<boolean>
92
+ hideDelete: EmptyResolver<boolean>
93
+ item: BaseItem | null
94
+ }
95
+ export type ListMetaSource = ListMetaSource_ & Omit<CollectionMeta, keyof ListMetaSource_>
96
+
97
+ export type AdminMetaSource = {
98
+ lists: ListMetaSource[]
99
+ listsByKey: Record<string, ListMetaSource>
100
+ views: string[]
101
+ isAccessAllowed: (context: NixxieContext) => MaybePromise<boolean>
102
+ }
103
+
104
+ export function createAdminMeta(
105
+ config: NixxieConfig,
106
+ initialisedLists: Record<string, InitialisedList>
107
+ ) {
108
+ const { lists } = config
109
+ const adminMetaRoot: AdminMetaSource = {
110
+ listsByKey: {},
111
+ lists: [],
112
+ views: [],
113
+ isAccessAllowed: config.ui?.isAccessAllowed,
114
+ }
115
+
116
+ const omittedLists: string[] = []
117
+
118
+ for (const [listKey, list] of Object.entries(initialisedLists)) {
119
+ const listConfig = lists[listKey]
120
+
121
+ // TODO: is this reasonable?
122
+ if (list.graphql.isEnabled.query === false) {
123
+ omittedLists.push(listKey)
124
+ continue
125
+ }
126
+
127
+ let initialColumns: string[]
128
+ if (listConfig.ui?.listView?.initialColumns) {
129
+ // if they've asked for a particular thing, give them that thing
130
+ initialColumns = listConfig.ui.listView.initialColumns as string[]
131
+ } else {
132
+ // otherwise, we'll start with the labelfield on the left and then add
133
+ // 2 more fields to the right of that. We don't include the 'id' field
134
+ // unless it happened to be the labelField
135
+ initialColumns = [
136
+ list.ui.labelField,
137
+ ...Object.keys(list.fields)
138
+ .filter(fieldKey => list.fields[fieldKey].graphql.isEnabled.read)
139
+ .filter(fieldKey => fieldKey !== list.ui.labelField)
140
+ .filter(fieldKey => fieldKey !== 'id'),
141
+ ].slice(0, 3)
142
+ }
143
+
144
+ let initialSearchFields = listConfig.ui?.searchFields?.concat()
145
+ if (!initialSearchFields) {
146
+ initialSearchFields = [...list.ui.triviallySearchableFields]
147
+ }
148
+
149
+ const maximumPageSize = Math.min(
150
+ listConfig.ui?.listView?.pageSize ?? 50,
151
+ (list.graphql.types.findManyArgs.take.defaultValue ?? Infinity) as number
152
+ )
153
+
154
+ adminMetaRoot.listsByKey[listKey] = {
155
+ key: listKey,
156
+ path: list.ui.labels.path,
157
+
158
+ label: list.ui.labels.label,
159
+ singular: list.ui.labels.singular,
160
+ plural: list.ui.labels.plural,
161
+
162
+ labelField: list.ui.labelField,
163
+ fields: [],
164
+ fieldsByKey: {},
165
+ groups: [],
166
+ actions: [],
167
+
168
+ graphql: {
169
+ names: list.graphql.names,
170
+ },
171
+
172
+ pageSize: maximumPageSize,
173
+ initialColumns,
174
+ initialSearchFields,
175
+ initialSort:
176
+ (listConfig.ui?.listView?.initialSort as CollectionSortDescriptor<string> | undefined) ?? null,
177
+ initialFilter: normalizeMaybeSessionFunction(listConfig.ui?.listView?.initialFilter ?? {}),
178
+ isSingleton: list.isSingleton,
179
+
180
+ hideNavigation: normalizeMaybeSessionFunction(listConfig.ui?.hideNavigation ?? false),
181
+ hideCreate: normalizeMaybeSessionFunction(
182
+ listConfig.ui?.hideCreate ?? !list.graphql.isEnabled.create
183
+ ),
184
+ hideDelete: normalizeMaybeSessionFunction(
185
+ listConfig.ui?.hideDelete ?? !list.graphql.isEnabled.delete
186
+ ),
187
+
188
+ item: null, // part of resolver
189
+ } satisfies ListMetaSource
190
+
191
+ adminMetaRoot.lists.push(adminMetaRoot.listsByKey[listKey])
192
+ }
193
+
194
+ let uniqueViewCount = -1
195
+ const stringViewsToIndex: Record<string, number> = {}
196
+ function getViewId(view: string) {
197
+ if (stringViewsToIndex[view] !== undefined) return stringViewsToIndex[view]
198
+
199
+ uniqueViewCount++
200
+ stringViewsToIndex[view] = uniqueViewCount
201
+ adminMetaRoot.views.push(view)
202
+ return uniqueViewCount
203
+ }
204
+
205
+ for (const [listKey, list] of Object.entries(initialisedLists)) {
206
+ if (omittedLists.includes(listKey)) continue
207
+
208
+ const listMeta = adminMetaRoot.listsByKey[listKey]
209
+
210
+ // populate .fields
211
+ for (const [fieldKey, field] of Object.entries(list.fields)) {
212
+ // if the field is a relationship field and is related to an omitted list, skip.
213
+ if (field.dbField.kind === 'relation' && omittedLists.includes(field.dbField.list)) continue
214
+ if (Object.values(field.graphql.isEnabled).every(x => x === false)) continue
215
+ assertValidView(
216
+ field.views,
217
+ `The \`views\` on the implementation of the field type at lists.${listKey}.fields.${fieldKey}`
218
+ )
219
+
220
+ const baseOrderFilterArgs = { fieldKey, listKey: list.listKey }
221
+ const isNonNull = (['read', 'create', 'update'] as const).filter(
222
+ operation => field.graphql.isNonNull[operation]
223
+ )
224
+ const fieldMeta = {
225
+ // FieldMeta
226
+ key: fieldKey,
227
+ label: field.ui.label,
228
+ description: field.ui.description,
229
+ fieldMeta: null,
230
+ viewsIndex: getViewId(field.views),
231
+ customViewsIndex:
232
+ field.ui.views === null
233
+ ? null
234
+ : (assertValidView(field.views, `lists.${listKey}.fields.${fieldKey}.ui.views`),
235
+ getViewId(field.ui.views)),
236
+ search: list.ui.searchableFields.get(fieldKey) ?? null,
237
+
238
+ // FieldMetaSource_
239
+ listKey: listKey,
240
+ fieldKey: fieldKey,
241
+ isFilterable: normalizeIsOrderFilter(
242
+ field.input?.where ? field.graphql.isEnabled.filter : false,
243
+ baseOrderFilterArgs
244
+ ),
245
+ isOrderable: normalizeIsOrderFilter(
246
+ field.input?.orderBy ? field.graphql.isEnabled.orderBy : false,
247
+ baseOrderFilterArgs
248
+ ),
249
+
250
+ isNonNull,
251
+ createView: {
252
+ fieldMode: normalizeMaybeSessionFunction(field.ui.createView.fieldMode),
253
+ isRequired: normalizeMaybeSessionFunction(field.ui.createView.isRequired ?? false),
254
+ },
255
+ itemView: {
256
+ fieldMode: field.ui.itemView.fieldMode,
257
+ fieldPosition: field.ui.itemView.fieldPosition,
258
+ isRequired: field.ui.itemView.isRequired,
259
+ },
260
+ listView: {
261
+ fieldMode: normalizeMaybeSessionFunction(field.ui.listView.fieldMode),
262
+ },
263
+
264
+ item: null, // part of resolver
265
+ itemField: null, // part of resolver
266
+ } satisfies FieldMetaSource
267
+
268
+ listMeta.fields.push(fieldMeta)
269
+ listMeta.fieldsByKey[fieldKey] = fieldMeta
270
+ }
271
+
272
+ // populate .actions
273
+ for (const action of list.actions) {
274
+ listMeta.actions.push({
275
+ // ActionMeta
276
+ key: action.actionKey,
277
+ label: action.ui.label,
278
+ icon: action.ui.icon,
279
+ messages: {
280
+ ...action.ui.messages,
281
+ },
282
+ graphql: {
283
+ arguments: action.graphql.arguments,
284
+ names: action.graphql.names,
285
+ },
286
+
287
+ // ActionMetaSource_
288
+ listKey,
289
+ itemView: {
290
+ ...action.ui.itemView,
291
+ },
292
+ listView: {
293
+ actionMode: normalizeMaybeSessionFunction(action.ui.listView.actionMode),
294
+ },
295
+ item: null, // part of resolver
296
+ })
297
+ }
298
+
299
+ // populate .groups
300
+ for (const group of list.groups) {
301
+ listMeta.groups.push({
302
+ label: group.label,
303
+ description: group.description,
304
+ fields: group.fields.map(
305
+ fieldKey => adminMetaRoot.listsByKey[listKey].fieldsByKey[fieldKey]
306
+ ),
307
+ })
308
+ }
309
+ }
310
+
311
+ // we do this seperately to the above so that fields can check other fields to validate their config or etc.
312
+ // (ofc they won't necessarily be able to see other field's fieldMeta)
313
+ for (const [key, list] of Object.entries(initialisedLists)) {
314
+ if (list.graphql.isEnabled.query === false) continue
315
+ for (const fieldMetaSource of adminMetaRoot.listsByKey[key].fields) {
316
+ // if the field is a relationship field and is related to an omitted list, skip.
317
+ const dbField = list.fields[fieldMetaSource.fieldKey].dbField
318
+ if (dbField.kind === 'relation' && omittedLists.includes(dbField.list)) continue
319
+
320
+ currentAdminMeta = adminMetaRoot
321
+ try {
322
+ fieldMetaSource.fieldMeta = list.fields[fieldMetaSource.fieldKey].getAdminMeta?.() ?? null
323
+ } finally {
324
+ currentAdminMeta = undefined
325
+ }
326
+ }
327
+ }
328
+
329
+ return adminMetaRoot
330
+ }
331
+
332
+ let currentAdminMeta: undefined | AdminMetaSource
333
+
334
+ export function getAdminMetaForRelationshipField() {
335
+ if (currentAdminMeta) return currentAdminMeta
336
+ throw new Error('unexpected call to getAdminMetaInRelationshipField')
337
+ }
338
+
339
+ function assertValidView(view: string, location: string) {
340
+ if (view.includes('\\')) {
341
+ throw new Error(
342
+ `${location} contains a backslash, which is invalid. You need to use a module path that is resolved from where 'nixxie start' is run (see https://github.com/nixxiecms/nixxie/pull/7805)`
343
+ )
344
+ }
345
+
346
+ if (path.isAbsolute(view)) {
347
+ throw new Error(
348
+ `${location} is an absolute path, which is invalid. You need to use a module path that is resolved from where 'nixxie start' is run (see https://github.com/nixxiecms/nixxie/pull/7805)`
349
+ )
350
+ }
351
+ }
352
+
353
+ function normalizeMaybeSessionFunction<Return extends string | boolean | object | null | number>(
354
+ input: MaybeSessionFunction<Return, BaseCollectionTypeInfo>
355
+ ): EmptyResolver<Return> {
356
+ if (typeof input !== 'function') return () => input
357
+ return (_, context) => input({ context, session: context.session })
358
+ }
359
+
360
+ function normalizeIsOrderFilter(
361
+ input: MaybeFieldFunction<BaseCollectionTypeInfo>,
362
+ baseOrderFilterArgs: {
363
+ listKey: string
364
+ fieldKey: string
365
+ }
366
+ ): EmptyResolver<boolean> {
367
+ if (typeof input !== 'function') return () => input
368
+ return (_, context) => input({ context, session: context.session, ...baseOrderFilterArgs })
369
+ }