@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,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
+ }