@nixxie-cms/core 1.0.3 → 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-15c8f81e.esm.js → index-5d8b0b4e.esm.js} +363 -183
  107. package/dist/index-6055753b.cjs.js +393 -0
  108. package/dist/{index-42045902.cjs.js → index-ac29f382.cjs.js} +363 -185
  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 +23 -21
  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,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
+ }