@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,178 +1,178 @@
1
- import { type CacheHint, maybeCacheControlFromInfo } from '@apollo/cache-control-types'
2
- import DataLoader from 'dataloader'
3
- import type { GraphQLResolveInfo } from 'graphql'
4
-
5
- import { g } from '../../..'
6
- import type {
7
- BaseItem,
8
- BaseListTypeInfo,
9
- FieldAccessControlFunction,
10
- FieldReadItemAccessArgs,
11
- FindManyArgsValue,
12
- NixxieContext,
13
- NextFieldType,
14
- } from '../../../types'
15
- import { getAccessFilters, getOperationAccess, getOperationFieldAccess } from '../access-control'
16
- import type { InitialisedList } from '../initialise-lists'
17
- import type { ResolvedDBField, ResolvedRelationDBField } from '../resolve-relationships'
18
- import { type IdType, getDBFieldKeyForFieldOnMultiField, weakMemoize } from '../utils'
19
- import * as queries from './resolvers'
20
- import { accessControlledFilter } from './resolvers'
21
-
22
- function getRelationVal(
23
- dbField: ResolvedRelationDBField,
24
- id: IdType,
25
- foreignList: InitialisedList,
26
- context: NixxieContext,
27
- info: GraphQLResolveInfo,
28
- fk: IdType | null | undefined
29
- ) {
30
- const oppositeDbField = foreignList.resolvedDbFields[dbField.field]
31
- if (oppositeDbField.kind !== 'relation') throw new Error('failed assert')
32
-
33
- if (dbField.mode === 'many') {
34
- const relationFilter = {
35
- [dbField.field]: oppositeDbField.mode === 'many' ? { some: { id } } : { id },
36
- }
37
- return {
38
- findMany: async (args: FindManyArgsValue) =>
39
- queries.findMany(args, foreignList, context, info, relationFilter),
40
- count: async ({ where }: { where: Record<string, unknown> }) =>
41
- queries.count({ where }, foreignList, context, info, relationFilter),
42
- }
43
- } else {
44
- return async () => {
45
- if (fk === null) {
46
- // If the foreign key is explicitly null, there's no need to anything else,
47
- // since we know the related item doesn't exist.
48
- return null
49
- }
50
- // for one-to-many relationships, the one side always owns the foreign key
51
- // so that means we have the id for the related item and we're fetching it by _its_ id.
52
- // for the a one-to-one relationship though, the id might be on the related item
53
- // so we need to fetch the related item by the id of the current item on the foreign key field
54
- const currentItemOwnsForeignKey = fk !== undefined
55
- return fetchRelatedItem(context)(foreignList)(
56
- currentItemOwnsForeignKey ? 'id' : `${dbField.field}Id`
57
- )(currentItemOwnsForeignKey ? fk : id)
58
- }
59
- }
60
- }
61
-
62
- function memoize<Arg, Return>(cb: (arg: Arg) => Return) {
63
- const cache = new Map<Arg, Return>()
64
- return (arg: Arg) => {
65
- if (!cache.has(arg)) {
66
- const result = cb(arg)
67
- cache.set(arg, result)
68
- }
69
- return cache.get(arg)!
70
- }
71
- }
72
-
73
- const fetchRelatedItem = weakMemoize((context: NixxieContext) =>
74
- weakMemoize((foreignList: InitialisedList) =>
75
- memoize((idFieldKey: string) => {
76
- const relatedItemLoader = new DataLoader(
77
- (keys: readonly IdType[]) => fetchRelatedItems(context, foreignList, idFieldKey, keys),
78
- { cache: false }
79
- )
80
- return (id: IdType) => relatedItemLoader.load(id)
81
- })
82
- )
83
- )
84
-
85
- async function fetchRelatedItems(
86
- context: NixxieContext,
87
- foreignList: InitialisedList,
88
- idFieldKey: string,
89
- toFetch: readonly IdType[]
90
- ) {
91
- const operationAccess = await getOperationAccess(foreignList, context, 'query')
92
- if (!operationAccess) {
93
- return toFetch.map(() => undefined)
94
- }
95
-
96
- const accessFilters = await getAccessFilters(foreignList, context, 'query')
97
- if (accessFilters === false) {
98
- return toFetch.map(() => undefined)
99
- }
100
-
101
- const toFetchUnique = Array.from(new Set(toFetch))
102
- const resolvedWhere = await accessControlledFilter(
103
- foreignList,
104
- context,
105
- { [idFieldKey]: { in: toFetchUnique } },
106
- accessFilters
107
- )
108
-
109
- const results = await context.prisma[foreignList.listKey].findMany({ where: resolvedWhere })
110
- const resultsById = new Map(results.map((x: any) => [x[idFieldKey], x]))
111
- return toFetch.map(id => resultsById.get(id))
112
- }
113
-
114
- function getValueForDBField(
115
- item: BaseItem,
116
- dbField: ResolvedDBField,
117
- id: IdType,
118
- fieldPath: string,
119
- context: NixxieContext,
120
- lists: Record<string, InitialisedList>,
121
- info: GraphQLResolveInfo
122
- ) {
123
- if (dbField.kind === 'multi') {
124
- return Object.fromEntries(
125
- Object.keys(dbField.fields).map(innerDBFieldKey => {
126
- const keyOnDbValue = getDBFieldKeyForFieldOnMultiField(fieldPath, innerDBFieldKey)
127
- return [innerDBFieldKey, item[keyOnDbValue] as any]
128
- })
129
- )
130
- }
131
- if (dbField.kind === 'relation') {
132
- // If we're holding a foreign key value, let's take advantage of that.
133
- let fk: IdType | undefined
134
- if (dbField.mode === 'one' && dbField.foreignIdField.kind !== 'none') {
135
- fk = item[`${fieldPath}Id`] as IdType
136
- }
137
- return getRelationVal(dbField, id, lists[dbField.list], context, info, fk)
138
- } else {
139
- return item[fieldPath] as any
140
- }
141
- }
142
-
143
- export function outputTypeField(
144
- output: NextFieldType['output'],
145
- dbField: ResolvedDBField,
146
- cacheHint: CacheHint | undefined,
147
- access: FieldAccessControlFunction<FieldReadItemAccessArgs<BaseListTypeInfo>>,
148
- listKey: string,
149
- fieldKey: string,
150
- lists: Record<string, InitialisedList>
151
- ) {
152
- const list = lists[listKey]
153
-
154
- return g.field({
155
- type: output.type,
156
- deprecationReason: output.deprecationReason,
157
- description: output.description,
158
- args: output.args,
159
- extensions: output.extensions,
160
- async resolve(item: BaseItem, args, context, info) {
161
- const id = item.id as IdType
162
- const fieldAccess = await getOperationFieldAccess(item, list, fieldKey, context, 'read')
163
- if (!fieldAccess) return null
164
-
165
- // only static cache hints are supported at the field level until a use-case makes it clear what parameters a dynamic hint would take
166
- if (cacheHint && info) {
167
- maybeCacheControlFromInfo(info)?.setCacheHint(cacheHint)
168
- }
169
-
170
- const value = getValueForDBField(item, dbField, id, fieldKey, context, lists, info)
171
- if (output.resolve) {
172
- return output.resolve({ value, item: item }, args, context, info)
173
- } else {
174
- return value
175
- }
176
- },
177
- })
178
- }
1
+ import { type CacheHint, maybeCacheControlFromInfo } from '@apollo/cache-control-types'
2
+ import DataLoader from 'dataloader'
3
+ import type { GraphQLResolveInfo } from 'graphql'
4
+
5
+ import { g } from '../../..'
6
+ import type {
7
+ BaseItem,
8
+ BaseCollectionTypeInfo,
9
+ FieldAccessControlFunction,
10
+ FieldReadItemAccessArgs,
11
+ FindManyArgsValue,
12
+ NixxieContext,
13
+ NextFieldType,
14
+ } from '../../../types'
15
+ import { getAccessFilters, getOperationAccess, getOperationFieldAccess } from '../access-control'
16
+ import type { InitialisedList } from '../initialise-lists'
17
+ import type { ResolvedDBField, ResolvedRelationDBField } from '../resolve-relationships'
18
+ import { type IdType, getDBFieldKeyForFieldOnMultiField, weakMemoize } from '../utils'
19
+ import * as queries from './resolvers'
20
+ import { accessControlledFilter } from './resolvers'
21
+
22
+ function getRelationVal(
23
+ dbField: ResolvedRelationDBField,
24
+ id: IdType,
25
+ foreignList: InitialisedList,
26
+ context: NixxieContext,
27
+ info: GraphQLResolveInfo,
28
+ fk: IdType | null | undefined
29
+ ) {
30
+ const oppositeDbField = foreignList.resolvedDbFields[dbField.field]
31
+ if (oppositeDbField.kind !== 'relation') throw new Error('failed assert')
32
+
33
+ if (dbField.mode === 'many') {
34
+ const relationFilter = {
35
+ [dbField.field]: oppositeDbField.mode === 'many' ? { some: { id } } : { id },
36
+ }
37
+ return {
38
+ findMany: async (args: FindManyArgsValue) =>
39
+ queries.findMany(args, foreignList, context, info, relationFilter),
40
+ count: async ({ where }: { where: Record<string, unknown> }) =>
41
+ queries.count({ where }, foreignList, context, info, relationFilter),
42
+ }
43
+ } else {
44
+ return async () => {
45
+ if (fk === null) {
46
+ // If the foreign key is explicitly null, there's no need to anything else,
47
+ // since we know the related item doesn't exist.
48
+ return null
49
+ }
50
+ // for one-to-many relationships, the one side always owns the foreign key
51
+ // so that means we have the id for the related item and we're fetching it by _its_ id.
52
+ // for the a one-to-one relationship though, the id might be on the related item
53
+ // so we need to fetch the related item by the id of the current item on the foreign key field
54
+ const currentItemOwnsForeignKey = fk !== undefined
55
+ return fetchRelatedItem(context)(foreignList)(
56
+ currentItemOwnsForeignKey ? 'id' : `${dbField.field}Id`
57
+ )(currentItemOwnsForeignKey ? fk : id)
58
+ }
59
+ }
60
+ }
61
+
62
+ function memoize<Arg, Return>(cb: (arg: Arg) => Return) {
63
+ const cache = new Map<Arg, Return>()
64
+ return (arg: Arg) => {
65
+ if (!cache.has(arg)) {
66
+ const result = cb(arg)
67
+ cache.set(arg, result)
68
+ }
69
+ return cache.get(arg)!
70
+ }
71
+ }
72
+
73
+ const fetchRelatedItem = weakMemoize((context: NixxieContext) =>
74
+ weakMemoize((foreignList: InitialisedList) =>
75
+ memoize((idFieldKey: string) => {
76
+ const relatedItemLoader = new DataLoader(
77
+ (keys: readonly IdType[]) => fetchRelatedItems(context, foreignList, idFieldKey, keys),
78
+ { cache: false }
79
+ )
80
+ return (id: IdType) => relatedItemLoader.load(id)
81
+ })
82
+ )
83
+ )
84
+
85
+ async function fetchRelatedItems(
86
+ context: NixxieContext,
87
+ foreignList: InitialisedList,
88
+ idFieldKey: string,
89
+ toFetch: readonly IdType[]
90
+ ) {
91
+ const operationAccess = await getOperationAccess(foreignList, context, 'query')
92
+ if (!operationAccess) {
93
+ return toFetch.map(() => undefined)
94
+ }
95
+
96
+ const accessFilters = await getAccessFilters(foreignList, context, 'query')
97
+ if (accessFilters === false) {
98
+ return toFetch.map(() => undefined)
99
+ }
100
+
101
+ const toFetchUnique = Array.from(new Set(toFetch))
102
+ const resolvedWhere = await accessControlledFilter(
103
+ foreignList,
104
+ context,
105
+ { [idFieldKey]: { in: toFetchUnique } },
106
+ accessFilters
107
+ )
108
+
109
+ const results = await context.prisma[foreignList.listKey].findMany({ where: resolvedWhere })
110
+ const resultsById = new Map(results.map((x: any) => [x[idFieldKey], x]))
111
+ return toFetch.map(id => resultsById.get(id))
112
+ }
113
+
114
+ function getValueForDBField(
115
+ item: BaseItem,
116
+ dbField: ResolvedDBField,
117
+ id: IdType,
118
+ fieldPath: string,
119
+ context: NixxieContext,
120
+ lists: Record<string, InitialisedList>,
121
+ info: GraphQLResolveInfo
122
+ ) {
123
+ if (dbField.kind === 'multi') {
124
+ return Object.fromEntries(
125
+ Object.keys(dbField.fields).map(innerDBFieldKey => {
126
+ const keyOnDbValue = getDBFieldKeyForFieldOnMultiField(fieldPath, innerDBFieldKey)
127
+ return [innerDBFieldKey, item[keyOnDbValue] as any]
128
+ })
129
+ )
130
+ }
131
+ if (dbField.kind === 'relation') {
132
+ // If we're holding a foreign key value, let's take advantage of that.
133
+ let fk: IdType | undefined
134
+ if (dbField.mode === 'one' && dbField.foreignIdField.kind !== 'none') {
135
+ fk = item[`${fieldPath}Id`] as IdType
136
+ }
137
+ return getRelationVal(dbField, id, lists[dbField.list], context, info, fk)
138
+ } else {
139
+ return item[fieldPath] as any
140
+ }
141
+ }
142
+
143
+ export function outputTypeField(
144
+ output: NextFieldType['output'],
145
+ dbField: ResolvedDBField,
146
+ cacheHint: CacheHint | undefined,
147
+ access: FieldAccessControlFunction<FieldReadItemAccessArgs<BaseCollectionTypeInfo>>,
148
+ listKey: string,
149
+ fieldKey: string,
150
+ lists: Record<string, InitialisedList>
151
+ ) {
152
+ const list = lists[listKey]
153
+
154
+ return g.field({
155
+ type: output.type,
156
+ deprecationReason: output.deprecationReason,
157
+ description: output.description,
158
+ args: output.args,
159
+ extensions: output.extensions,
160
+ async resolve(item: BaseItem, args, context, info) {
161
+ const id = item.id as IdType
162
+ const fieldAccess = await getOperationFieldAccess(item, list, fieldKey, context, 'read')
163
+ if (!fieldAccess) return null
164
+
165
+ // only static cache hints are supported at the field level until a use-case makes it clear what parameters a dynamic hint would take
166
+ if (cacheHint && info) {
167
+ maybeCacheControlFromInfo(info)?.setCacheHint(cacheHint)
168
+ }
169
+
170
+ const value = getValueForDBField(item, dbField, id, fieldKey, context, lists, info)
171
+ if (output.resolve) {
172
+ return output.resolve({ value, item: item }, args, context, info)
173
+ } else {
174
+ return value
175
+ }
176
+ },
177
+ })
178
+ }
package/src/lib/env.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type { NixxieEnvSpec, NixxieEnvVarSpec } from '../types'
2
+
3
+ function normalise(spec: NixxieEnvSpec): Record<string, NixxieEnvVarSpec> {
4
+ if (Array.isArray(spec)) {
5
+ return Object.fromEntries((spec as readonly string[]).map(name => [name, {}]))
6
+ }
7
+ return spec as Record<string, NixxieEnvVarSpec>
8
+ }
9
+
10
+ /**
11
+ * Validate `env` (default `process.env`) against a spec. Applies `default`s to the env
12
+ * object for unset variables, then throws one aggregated error listing every missing or
13
+ * malformed variable. Called automatically when the config declares `env`, and exported
14
+ * for standalone use.
15
+ */
16
+ export function validateEnv(
17
+ spec: NixxieEnvSpec,
18
+ env: Record<string, string | undefined> = process.env
19
+ ): void {
20
+ const problems: string[] = []
21
+
22
+ for (const [name, requirement] of Object.entries(normalise(spec))) {
23
+ let value = env[name]
24
+
25
+ if ((value === undefined || value === '') && requirement.default !== undefined) {
26
+ env[name] = requirement.default
27
+ value = requirement.default
28
+ }
29
+
30
+ const describe = requirement.description ? ` — ${requirement.description}` : ''
31
+
32
+ if (value === undefined || value === '') {
33
+ if (requirement.required !== false) {
34
+ problems.push(` • ${name} is not set${describe}`)
35
+ }
36
+ continue
37
+ }
38
+
39
+ if (requirement.pattern && !requirement.pattern.test(value)) {
40
+ problems.push(` • ${name} does not match ${requirement.pattern}${describe}`)
41
+ }
42
+ }
43
+
44
+ if (problems.length) {
45
+ throw new Error(
46
+ `Environment validation failed (${problems.length} problem${problems.length === 1 ? '' : 's'}):\n` +
47
+ problems.join('\n')
48
+ )
49
+ }
50
+ }
@@ -1,7 +1,7 @@
1
1
  import type { GArg, GInputObjectType, InferValueFromArg } from '../graphql-ts'
2
2
  import type { ScalarDBField } from '../types'
3
3
  import {
4
- type BaseListTypeInfo,
4
+ type BaseCollectionTypeInfo,
5
5
  type DatabaseProvider,
6
6
  type FieldTypeFunc,
7
7
  type IdFieldConfig,
@@ -140,7 +140,7 @@ function unpack(
140
140
  throw new Error(`Unknown id type ${kind}`)
141
141
  }
142
142
 
143
- export function idFieldType(config: IdFieldConfig): FieldTypeFunc<BaseListTypeInfo> {
143
+ export function idFieldType(config: IdFieldConfig): FieldTypeFunc<BaseCollectionTypeInfo> {
144
144
  const kind = config.kind
145
145
  const dbFieldOptions = unpack(config)
146
146
  const parseTypeFn = {