@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,434 +1,434 @@
1
- import { assertInputObjectType } from 'graphql'
2
-
3
- import { allowAll } from '../../access'
4
- import type {
5
- ActionAccessControlFunction,
6
- BaseItem,
7
- BaseListTypeInfo,
8
- CreateListItemAccessControl,
9
- DeleteListItemAccessControl,
10
- FieldAccessControl,
11
- FieldAccessControlFunction,
12
- FieldCreateItemAccessArgs,
13
- FieldReadItemAccessArgs,
14
- FieldUpdateItemAccessArgs,
15
- NixxieContext,
16
- ListAccessControl,
17
- ListFilterAccessControl,
18
- ListOperationAccessControl,
19
- UpdateListItemAccessControl,
20
- } from '../../types'
21
- import { coerceAndValidateForGraphQLInput } from '../coerceAndValidateForGraphQLInput'
22
- import { accessDeniedError, accessReturnError, extensionError, formatKeys } from './graphql-errors'
23
- import type { InitialisedAction, InitialisedList } from './initialise-lists'
24
- import { type InputFilter, type UniqueInputFilter, resolveUniqueWhereInput } from './where-inputs'
25
-
26
- export function cannotForItem(operation: string, list: InitialisedList) {
27
- if (operation === 'create')
28
- return `You cannot ${operation} that ${list.graphql.names.outputTypeName}`
29
- return `You cannot ${operation} that ${list.graphql.names.outputTypeName} - it may not exist`
30
- }
31
-
32
- export function cannotActionForItem(action: InitialisedAction, list: InitialisedList) {
33
- return `You cannot execute action "${action.actionKey}" for that ${list.graphql.names.outputTypeName}`
34
- }
35
-
36
- export function cannotForItemFields(
37
- operation: string,
38
- list: InitialisedList,
39
- fieldsDenied: string[]
40
- ) {
41
- return `You cannot ${operation} that ${list.graphql.names.outputTypeName} - you cannot ${operation} the fields ${formatKeys(fieldsDenied)}`
42
- }
43
-
44
- export async function getOperationFieldAccess(
45
- item: BaseItem,
46
- list: InitialisedList,
47
- fieldKey: string,
48
- context: NixxieContext,
49
- operation: 'read'
50
- ): Promise<boolean> {
51
- if (context.__internal.sudo) return true
52
-
53
- const { listKey } = list
54
- let result
55
- try {
56
- result = await list.fields[fieldKey].access.read({
57
- operation: 'read',
58
- session: context.session,
59
- listKey,
60
- fieldKey,
61
- context,
62
- item,
63
- })
64
- } catch (error: any) {
65
- throw extensionError('Access control', [
66
- { error, tag: `${list.listKey}.${fieldKey}.access.${operation}` },
67
- ])
68
- }
69
-
70
- if (typeof result !== 'boolean') {
71
- throw accessReturnError([
72
- { tag: `${listKey}.access.operation.${operation}`, returned: typeof result },
73
- ])
74
- }
75
-
76
- return result
77
- }
78
-
79
- export async function getOperationAccess(
80
- list: InitialisedList,
81
- context: NixxieContext,
82
- operation: 'query' | 'create' | 'update' | 'delete'
83
- ) {
84
- if (context.__internal.sudo) return true
85
-
86
- const { listKey } = list
87
- let result
88
- try {
89
- if (operation === 'query') {
90
- result = await list.access.operation.query({
91
- operation,
92
- session: context.session,
93
- listKey,
94
- context,
95
- })
96
- } else if (operation === 'create') {
97
- result = await list.access.operation.create({
98
- operation,
99
- session: context.session,
100
- listKey,
101
- context,
102
- })
103
- } else if (operation === 'update') {
104
- result = await list.access.operation.update({
105
- operation,
106
- session: context.session,
107
- listKey,
108
- context,
109
- })
110
- } else if (operation === 'delete') {
111
- result = await list.access.operation.delete({
112
- operation,
113
- session: context.session,
114
- listKey,
115
- context,
116
- })
117
- }
118
- } catch (error: any) {
119
- throw extensionError('Access control', [
120
- { error, tag: `${listKey}.access.operation.${operation}` },
121
- ])
122
- }
123
-
124
- if (typeof result !== 'boolean') {
125
- throw accessReturnError([
126
- { tag: `${listKey}.access.operation.${operation}`, returned: typeof result },
127
- ])
128
- }
129
-
130
- return result
131
- }
132
-
133
- export async function getAccessFilters(
134
- list: InitialisedList,
135
- context: NixxieContext,
136
- operation: keyof typeof list.access.filter
137
- ): Promise<boolean | InputFilter> {
138
- if (context.__internal.sudo) return true
139
-
140
- try {
141
- let filters
142
- if (operation === 'query') {
143
- filters = await list.access.filter.query({
144
- operation,
145
- session: context.session,
146
- listKey: list.listKey,
147
- context,
148
- })
149
- } else if (operation === 'update') {
150
- filters = await list.access.filter.update({
151
- operation,
152
- session: context.session,
153
- listKey: list.listKey,
154
- context,
155
- })
156
- } else if (operation === 'delete') {
157
- filters = await list.access.filter.delete({
158
- operation,
159
- session: context.session,
160
- listKey: list.listKey,
161
- context,
162
- })
163
- }
164
-
165
- if (typeof filters === 'boolean') return filters
166
- if (!filters) return false // shouldn't happen, but, Typescript
167
-
168
- const schema = context.sudo().graphql.schema
169
- const whereInput = assertInputObjectType(schema.getType(list.graphql.names.whereInputName))
170
- const result = coerceAndValidateForGraphQLInput(schema, whereInput, filters)
171
- if (result.kind === 'valid') return result.value
172
- throw result.error
173
- } catch (error: any) {
174
- throw extensionError('Access control', [
175
- { error, tag: `${list.listKey}.access.filter.${operation}` },
176
- ])
177
- }
178
- }
179
-
180
- export async function enforceListLevelAccessControl(
181
- context: NixxieContext,
182
- operation: 'create' | 'update' | 'delete',
183
- list: InitialisedList,
184
- inputData: Record<string, unknown>,
185
- item: BaseItem | undefined
186
- ) {
187
- if (context.__internal.sudo) return
188
-
189
- let accepted: unknown // should be boolean, but dont trust, it might accidentally be a filter
190
- try {
191
- // apply access.item.* controls
192
- if (operation === 'create') {
193
- const itemAccessControl = list.access.item[operation]
194
- accepted = await itemAccessControl({
195
- operation,
196
- session: context.session,
197
- listKey: list.listKey,
198
- context,
199
- inputData,
200
- })
201
- } else if (operation === 'update' && item !== undefined) {
202
- const itemAccessControl = list.access.item[operation]
203
- accepted = await itemAccessControl({
204
- operation,
205
- session: context.session,
206
- listKey: list.listKey,
207
- context,
208
- item,
209
- inputData,
210
- })
211
- } else if (operation === 'delete' && item !== undefined) {
212
- const itemAccessControl = list.access.item[operation]
213
- accepted = await itemAccessControl({
214
- operation,
215
- session: context.session,
216
- listKey: list.listKey,
217
- context,
218
- item,
219
- })
220
- }
221
- } catch (error: any) {
222
- throw extensionError('Access control', [
223
- { error, tag: `${list.listKey}.access.item.${operation}` },
224
- ])
225
- }
226
-
227
- // short circuit the safe path
228
- if (accepted === true) return
229
-
230
- if (typeof accepted !== 'boolean') {
231
- throw accessReturnError([
232
- {
233
- tag: `${list.listKey}.access.item.${operation}`,
234
- returned: typeof accepted,
235
- },
236
- ])
237
- }
238
-
239
- throw accessDeniedError(cannotForItem(operation, list))
240
- }
241
-
242
- export async function enforceFieldLevelAccessControl(
243
- context: NixxieContext,
244
- operation: 'create' | 'update',
245
- list: InitialisedList,
246
- inputData: Record<string, unknown>,
247
- item: BaseItem | undefined
248
- ) {
249
- if (context.__internal.sudo) return
250
-
251
- const nonBooleans: { tag: string; returned: string }[] = []
252
- const fieldsDenied: string[] = []
253
- const accessErrors: { error: Error; tag: string }[] = []
254
-
255
- await Promise.allSettled(
256
- Object.keys(inputData).map(async fieldKey => {
257
- let accepted: unknown // should be boolean, but dont trust
258
- try {
259
- // apply fields.[fieldKey].access.* controls
260
- if (operation === 'create') {
261
- const fieldAccessControl = list.fields[fieldKey].access[operation]
262
- accepted = await fieldAccessControl({
263
- operation,
264
- session: context.session,
265
- listKey: list.listKey,
266
- fieldKey,
267
- context,
268
- inputData: inputData as any, // FIXME
269
- })
270
- } else if (operation === 'update' && item !== undefined) {
271
- const fieldAccessControl = list.fields[fieldKey].access[operation]
272
- accepted = await fieldAccessControl({
273
- operation,
274
- session: context.session,
275
- listKey: list.listKey,
276
- fieldKey,
277
- context,
278
- item,
279
- inputData,
280
- })
281
- }
282
- } catch (error: any) {
283
- accessErrors.push({ error, tag: `${list.listKey}.${fieldKey}.access.${operation}` })
284
- return
285
- }
286
-
287
- // short circuit the safe path
288
- if (accepted === true) return
289
- fieldsDenied.push(fieldKey)
290
-
291
- // wrong type?
292
- if (typeof accepted !== 'boolean') {
293
- nonBooleans.push({
294
- tag: `${list.listKey}.${fieldKey}.access.${operation}`,
295
- returned: typeof accepted,
296
- })
297
- }
298
- })
299
- )
300
-
301
- if (nonBooleans.length) {
302
- throw accessReturnError(nonBooleans)
303
- }
304
-
305
- if (accessErrors.length) {
306
- throw extensionError('Access control', accessErrors)
307
- }
308
-
309
- if (fieldsDenied.length) {
310
- throw accessDeniedError(cannotForItemFields(operation, list, fieldsDenied))
311
- }
312
- }
313
-
314
- export type ResolvedFieldAccessControl = {
315
- read: FieldAccessControlFunction<FieldReadItemAccessArgs<BaseListTypeInfo>>
316
- create: FieldAccessControlFunction<FieldCreateItemAccessArgs<BaseListTypeInfo>>
317
- update: FieldAccessControlFunction<FieldUpdateItemAccessArgs<BaseListTypeInfo>>
318
- // delete: not supported
319
- }
320
-
321
- export function parseFieldAccessControl(
322
- access: FieldAccessControl<BaseListTypeInfo> | undefined
323
- ): ResolvedFieldAccessControl {
324
- if (typeof access === 'function') {
325
- return {
326
- read: access,
327
- create: access,
328
- update: access,
329
- }
330
- }
331
-
332
- return {
333
- read: access?.read ?? allowAll,
334
- create: access?.create ?? allowAll,
335
- update: access?.update ?? allowAll,
336
- }
337
- }
338
-
339
- export type ResolvedActionAccessControl = ActionAccessControlFunction<BaseListTypeInfo>
340
-
341
- export type ResolvedListAccessControl = {
342
- operation: {
343
- query: ListOperationAccessControl<'query', BaseListTypeInfo>
344
- create: ListOperationAccessControl<'create', BaseListTypeInfo>
345
- update: ListOperationAccessControl<'update', BaseListTypeInfo>
346
- delete: ListOperationAccessControl<'delete', BaseListTypeInfo>
347
- }
348
- filter: {
349
- query: ListFilterAccessControl<'query', BaseListTypeInfo>
350
- // create: not supported
351
- update: ListFilterAccessControl<'update', BaseListTypeInfo>
352
- delete: ListFilterAccessControl<'delete', BaseListTypeInfo>
353
- }
354
- item: {
355
- // query: not supported
356
- create: CreateListItemAccessControl<BaseListTypeInfo>
357
- update: UpdateListItemAccessControl<BaseListTypeInfo>
358
- delete: DeleteListItemAccessControl<BaseListTypeInfo>
359
- }
360
- }
361
-
362
- export function parseListAccessControl(
363
- access: ListAccessControl<BaseListTypeInfo>
364
- ): ResolvedListAccessControl {
365
- if (typeof access === 'function') {
366
- return {
367
- operation: {
368
- query: access,
369
- create: access,
370
- update: access,
371
- delete: access,
372
- },
373
- filter: {
374
- query: allowAll,
375
- update: allowAll,
376
- delete: allowAll,
377
- },
378
- item: {
379
- create: allowAll,
380
- update: allowAll,
381
- delete: allowAll,
382
- },
383
- }
384
- }
385
-
386
- let { operation, filter, item } = access
387
- if (typeof operation === 'function') {
388
- operation = {
389
- query: operation,
390
- create: operation,
391
- update: operation,
392
- delete: operation,
393
- }
394
- }
395
-
396
- return {
397
- operation: {
398
- query: operation.query,
399
- create: operation.create,
400
- update: operation.update,
401
- delete: operation.delete,
402
- },
403
- filter: {
404
- query: filter?.query ?? allowAll,
405
- // create: not supported
406
- update: filter?.update ?? allowAll,
407
- delete: filter?.delete ?? allowAll,
408
- },
409
- item: {
410
- // query: not supported
411
- create: item?.create ?? allowAll,
412
- update: item?.update ?? allowAll,
413
- delete: item?.delete ?? allowAll,
414
- },
415
- }
416
- }
417
-
418
- export async function checkUniqueItemExists(
419
- uniqueInput: UniqueInputFilter,
420
- foreignList: InitialisedList,
421
- context: NixxieContext,
422
- operation: string
423
- ) {
424
- // Validate and resolve the input filter
425
- const uniqueWhere = await resolveUniqueWhereInput(uniqueInput, foreignList, context)
426
-
427
- // Check whether the item exists (from this users POV).
428
- try {
429
- const item = await context.db[foreignList.listKey].findOne({ where: uniqueInput })
430
- if (item !== null) return uniqueWhere
431
- } catch (err) {}
432
-
433
- throw accessDeniedError(cannotForItem(operation, foreignList))
434
- }
1
+ import { assertInputObjectType } from 'graphql'
2
+
3
+ import { allowAll } from '../../access'
4
+ import type {
5
+ ActionAccessControlFunction,
6
+ BaseItem,
7
+ BaseCollectionTypeInfo,
8
+ CreateCollectionItemAccessControl,
9
+ DeleteCollectionItemAccessControl,
10
+ FieldAccessControl,
11
+ FieldAccessControlFunction,
12
+ FieldCreateItemAccessArgs,
13
+ FieldReadItemAccessArgs,
14
+ FieldUpdateItemAccessArgs,
15
+ NixxieContext,
16
+ CollectionAccessControl,
17
+ CollectionFilterAccessControl,
18
+ CollectionOperationAccessControl,
19
+ UpdateCollectionItemAccessControl,
20
+ } from '../../types'
21
+ import { coerceAndValidateForGraphQLInput } from '../coerceAndValidateForGraphQLInput'
22
+ import { accessDeniedError, accessReturnError, extensionError, formatKeys } from './graphql-errors'
23
+ import type { InitialisedAction, InitialisedList } from './initialise-lists'
24
+ import { type InputFilter, type UniqueInputFilter, resolveUniqueWhereInput } from './where-inputs'
25
+
26
+ export function cannotForItem(operation: string, list: InitialisedList) {
27
+ if (operation === 'create')
28
+ return `You cannot ${operation} that ${list.graphql.names.outputTypeName}`
29
+ return `You cannot ${operation} that ${list.graphql.names.outputTypeName} - it may not exist`
30
+ }
31
+
32
+ export function cannotActionForItem(action: InitialisedAction, list: InitialisedList) {
33
+ return `You cannot execute action "${action.actionKey}" for that ${list.graphql.names.outputTypeName}`
34
+ }
35
+
36
+ export function cannotForItemFields(
37
+ operation: string,
38
+ list: InitialisedList,
39
+ fieldsDenied: string[]
40
+ ) {
41
+ return `You cannot ${operation} that ${list.graphql.names.outputTypeName} - you cannot ${operation} the fields ${formatKeys(fieldsDenied)}`
42
+ }
43
+
44
+ export async function getOperationFieldAccess(
45
+ item: BaseItem,
46
+ list: InitialisedList,
47
+ fieldKey: string,
48
+ context: NixxieContext,
49
+ operation: 'read'
50
+ ): Promise<boolean> {
51
+ if (context.__internal.sudo) return true
52
+
53
+ const { listKey } = list
54
+ let result
55
+ try {
56
+ result = await list.fields[fieldKey].access.read({
57
+ operation: 'read',
58
+ session: context.session,
59
+ listKey,
60
+ fieldKey,
61
+ context,
62
+ item,
63
+ })
64
+ } catch (error: any) {
65
+ throw extensionError('Access control', [
66
+ { error, tag: `${list.listKey}.${fieldKey}.access.${operation}` },
67
+ ])
68
+ }
69
+
70
+ if (typeof result !== 'boolean') {
71
+ throw accessReturnError([
72
+ { tag: `${listKey}.access.operation.${operation}`, returned: typeof result },
73
+ ])
74
+ }
75
+
76
+ return result
77
+ }
78
+
79
+ export async function getOperationAccess(
80
+ list: InitialisedList,
81
+ context: NixxieContext,
82
+ operation: 'query' | 'create' | 'update' | 'delete'
83
+ ) {
84
+ if (context.__internal.sudo) return true
85
+
86
+ const { listKey } = list
87
+ let result
88
+ try {
89
+ if (operation === 'query') {
90
+ result = await list.access.operation.query({
91
+ operation,
92
+ session: context.session,
93
+ listKey,
94
+ context,
95
+ })
96
+ } else if (operation === 'create') {
97
+ result = await list.access.operation.create({
98
+ operation,
99
+ session: context.session,
100
+ listKey,
101
+ context,
102
+ })
103
+ } else if (operation === 'update') {
104
+ result = await list.access.operation.update({
105
+ operation,
106
+ session: context.session,
107
+ listKey,
108
+ context,
109
+ })
110
+ } else if (operation === 'delete') {
111
+ result = await list.access.operation.delete({
112
+ operation,
113
+ session: context.session,
114
+ listKey,
115
+ context,
116
+ })
117
+ }
118
+ } catch (error: any) {
119
+ throw extensionError('Access control', [
120
+ { error, tag: `${listKey}.access.operation.${operation}` },
121
+ ])
122
+ }
123
+
124
+ if (typeof result !== 'boolean') {
125
+ throw accessReturnError([
126
+ { tag: `${listKey}.access.operation.${operation}`, returned: typeof result },
127
+ ])
128
+ }
129
+
130
+ return result
131
+ }
132
+
133
+ export async function getAccessFilters(
134
+ list: InitialisedList,
135
+ context: NixxieContext,
136
+ operation: keyof typeof list.access.filter
137
+ ): Promise<boolean | InputFilter> {
138
+ if (context.__internal.sudo) return true
139
+
140
+ try {
141
+ let filters
142
+ if (operation === 'query') {
143
+ filters = await list.access.filter.query({
144
+ operation,
145
+ session: context.session,
146
+ listKey: list.listKey,
147
+ context,
148
+ })
149
+ } else if (operation === 'update') {
150
+ filters = await list.access.filter.update({
151
+ operation,
152
+ session: context.session,
153
+ listKey: list.listKey,
154
+ context,
155
+ })
156
+ } else if (operation === 'delete') {
157
+ filters = await list.access.filter.delete({
158
+ operation,
159
+ session: context.session,
160
+ listKey: list.listKey,
161
+ context,
162
+ })
163
+ }
164
+
165
+ if (typeof filters === 'boolean') return filters
166
+ if (!filters) return false // shouldn't happen, but, Typescript
167
+
168
+ const schema = context.sudo().graphql.schema
169
+ const whereInput = assertInputObjectType(schema.getType(list.graphql.names.whereInputName))
170
+ const result = coerceAndValidateForGraphQLInput(schema, whereInput, filters)
171
+ if (result.kind === 'valid') return result.value
172
+ throw result.error
173
+ } catch (error: any) {
174
+ throw extensionError('Access control', [
175
+ { error, tag: `${list.listKey}.access.filter.${operation}` },
176
+ ])
177
+ }
178
+ }
179
+
180
+ export async function enforceListLevelAccessControl(
181
+ context: NixxieContext,
182
+ operation: 'create' | 'update' | 'delete',
183
+ list: InitialisedList,
184
+ inputData: Record<string, unknown>,
185
+ item: BaseItem | undefined
186
+ ) {
187
+ if (context.__internal.sudo) return
188
+
189
+ let accepted: unknown // should be boolean, but dont trust, it might accidentally be a filter
190
+ try {
191
+ // apply access.item.* controls
192
+ if (operation === 'create') {
193
+ const itemAccessControl = list.access.item[operation]
194
+ accepted = await itemAccessControl({
195
+ operation,
196
+ session: context.session,
197
+ listKey: list.listKey,
198
+ context,
199
+ inputData,
200
+ })
201
+ } else if (operation === 'update' && item !== undefined) {
202
+ const itemAccessControl = list.access.item[operation]
203
+ accepted = await itemAccessControl({
204
+ operation,
205
+ session: context.session,
206
+ listKey: list.listKey,
207
+ context,
208
+ item,
209
+ inputData,
210
+ })
211
+ } else if (operation === 'delete' && item !== undefined) {
212
+ const itemAccessControl = list.access.item[operation]
213
+ accepted = await itemAccessControl({
214
+ operation,
215
+ session: context.session,
216
+ listKey: list.listKey,
217
+ context,
218
+ item,
219
+ })
220
+ }
221
+ } catch (error: any) {
222
+ throw extensionError('Access control', [
223
+ { error, tag: `${list.listKey}.access.item.${operation}` },
224
+ ])
225
+ }
226
+
227
+ // short circuit the safe path
228
+ if (accepted === true) return
229
+
230
+ if (typeof accepted !== 'boolean') {
231
+ throw accessReturnError([
232
+ {
233
+ tag: `${list.listKey}.access.item.${operation}`,
234
+ returned: typeof accepted,
235
+ },
236
+ ])
237
+ }
238
+
239
+ throw accessDeniedError(cannotForItem(operation, list))
240
+ }
241
+
242
+ export async function enforceFieldLevelAccessControl(
243
+ context: NixxieContext,
244
+ operation: 'create' | 'update',
245
+ list: InitialisedList,
246
+ inputData: Record<string, unknown>,
247
+ item: BaseItem | undefined
248
+ ) {
249
+ if (context.__internal.sudo) return
250
+
251
+ const nonBooleans: { tag: string; returned: string }[] = []
252
+ const fieldsDenied: string[] = []
253
+ const accessErrors: { error: Error; tag: string }[] = []
254
+
255
+ await Promise.allSettled(
256
+ Object.keys(inputData).map(async fieldKey => {
257
+ let accepted: unknown // should be boolean, but dont trust
258
+ try {
259
+ // apply fields.[fieldKey].access.* controls
260
+ if (operation === 'create') {
261
+ const fieldAccessControl = list.fields[fieldKey].access[operation]
262
+ accepted = await fieldAccessControl({
263
+ operation,
264
+ session: context.session,
265
+ listKey: list.listKey,
266
+ fieldKey,
267
+ context,
268
+ inputData: inputData as any, // FIXME
269
+ })
270
+ } else if (operation === 'update' && item !== undefined) {
271
+ const fieldAccessControl = list.fields[fieldKey].access[operation]
272
+ accepted = await fieldAccessControl({
273
+ operation,
274
+ session: context.session,
275
+ listKey: list.listKey,
276
+ fieldKey,
277
+ context,
278
+ item,
279
+ inputData,
280
+ })
281
+ }
282
+ } catch (error: any) {
283
+ accessErrors.push({ error, tag: `${list.listKey}.${fieldKey}.access.${operation}` })
284
+ return
285
+ }
286
+
287
+ // short circuit the safe path
288
+ if (accepted === true) return
289
+ fieldsDenied.push(fieldKey)
290
+
291
+ // wrong type?
292
+ if (typeof accepted !== 'boolean') {
293
+ nonBooleans.push({
294
+ tag: `${list.listKey}.${fieldKey}.access.${operation}`,
295
+ returned: typeof accepted,
296
+ })
297
+ }
298
+ })
299
+ )
300
+
301
+ if (nonBooleans.length) {
302
+ throw accessReturnError(nonBooleans)
303
+ }
304
+
305
+ if (accessErrors.length) {
306
+ throw extensionError('Access control', accessErrors)
307
+ }
308
+
309
+ if (fieldsDenied.length) {
310
+ throw accessDeniedError(cannotForItemFields(operation, list, fieldsDenied))
311
+ }
312
+ }
313
+
314
+ export type ResolvedFieldAccessControl = {
315
+ read: FieldAccessControlFunction<FieldReadItemAccessArgs<BaseCollectionTypeInfo>>
316
+ create: FieldAccessControlFunction<FieldCreateItemAccessArgs<BaseCollectionTypeInfo>>
317
+ update: FieldAccessControlFunction<FieldUpdateItemAccessArgs<BaseCollectionTypeInfo>>
318
+ // delete: not supported
319
+ }
320
+
321
+ export function parseFieldAccessControl(
322
+ access: FieldAccessControl<BaseCollectionTypeInfo> | undefined
323
+ ): ResolvedFieldAccessControl {
324
+ if (typeof access === 'function') {
325
+ return {
326
+ read: access,
327
+ create: access,
328
+ update: access,
329
+ }
330
+ }
331
+
332
+ return {
333
+ read: access?.read ?? allowAll,
334
+ create: access?.create ?? allowAll,
335
+ update: access?.update ?? allowAll,
336
+ }
337
+ }
338
+
339
+ export type ResolvedActionAccessControl = ActionAccessControlFunction<BaseCollectionTypeInfo>
340
+
341
+ export type ResolvedCollectionAccessControl = {
342
+ operation: {
343
+ query: CollectionOperationAccessControl<'query', BaseCollectionTypeInfo>
344
+ create: CollectionOperationAccessControl<'create', BaseCollectionTypeInfo>
345
+ update: CollectionOperationAccessControl<'update', BaseCollectionTypeInfo>
346
+ delete: CollectionOperationAccessControl<'delete', BaseCollectionTypeInfo>
347
+ }
348
+ filter: {
349
+ query: CollectionFilterAccessControl<'query', BaseCollectionTypeInfo>
350
+ // create: not supported
351
+ update: CollectionFilterAccessControl<'update', BaseCollectionTypeInfo>
352
+ delete: CollectionFilterAccessControl<'delete', BaseCollectionTypeInfo>
353
+ }
354
+ item: {
355
+ // query: not supported
356
+ create: CreateCollectionItemAccessControl<BaseCollectionTypeInfo>
357
+ update: UpdateCollectionItemAccessControl<BaseCollectionTypeInfo>
358
+ delete: DeleteCollectionItemAccessControl<BaseCollectionTypeInfo>
359
+ }
360
+ }
361
+
362
+ export function parseListAccessControl(
363
+ access: CollectionAccessControl<BaseCollectionTypeInfo>
364
+ ): ResolvedCollectionAccessControl {
365
+ if (typeof access === 'function') {
366
+ return {
367
+ operation: {
368
+ query: access,
369
+ create: access,
370
+ update: access,
371
+ delete: access,
372
+ },
373
+ filter: {
374
+ query: allowAll,
375
+ update: allowAll,
376
+ delete: allowAll,
377
+ },
378
+ item: {
379
+ create: allowAll,
380
+ update: allowAll,
381
+ delete: allowAll,
382
+ },
383
+ }
384
+ }
385
+
386
+ let { operation, filter, item } = access
387
+ if (typeof operation === 'function') {
388
+ operation = {
389
+ query: operation,
390
+ create: operation,
391
+ update: operation,
392
+ delete: operation,
393
+ }
394
+ }
395
+
396
+ return {
397
+ operation: {
398
+ query: operation.query,
399
+ create: operation.create,
400
+ update: operation.update,
401
+ delete: operation.delete,
402
+ },
403
+ filter: {
404
+ query: filter?.query ?? allowAll,
405
+ // create: not supported
406
+ update: filter?.update ?? allowAll,
407
+ delete: filter?.delete ?? allowAll,
408
+ },
409
+ item: {
410
+ // query: not supported
411
+ create: item?.create ?? allowAll,
412
+ update: item?.update ?? allowAll,
413
+ delete: item?.delete ?? allowAll,
414
+ },
415
+ }
416
+ }
417
+
418
+ export async function checkUniqueItemExists(
419
+ uniqueInput: UniqueInputFilter,
420
+ foreignList: InitialisedList,
421
+ context: NixxieContext,
422
+ operation: string
423
+ ) {
424
+ // Validate and resolve the input filter
425
+ const uniqueWhere = await resolveUniqueWhereInput(uniqueInput, foreignList, context)
426
+
427
+ // Check whether the item exists (from this users POV).
428
+ try {
429
+ const item = await context.db[foreignList.listKey].findOne({ where: uniqueInput })
430
+ if (item !== null) return uniqueWhere
431
+ } catch (err) {}
432
+
433
+ throw accessDeniedError(cannotForItem(operation, foreignList))
434
+ }