@contember/bindx 0.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 (234) hide show
  1. package/dist/adapter/ContemberAdapter.d.ts +40 -0
  2. package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
  3. package/dist/adapter/MockAdapter.d.ts +81 -0
  4. package/dist/adapter/MockAdapter.d.ts.map +1 -0
  5. package/dist/adapter/MockQueryEngine.d.ts +43 -0
  6. package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
  7. package/dist/adapter/index.d.ts +5 -0
  8. package/dist/adapter/index.d.ts.map +1 -0
  9. package/dist/adapter/types.d.ts +184 -0
  10. package/dist/adapter/types.d.ts.map +1 -0
  11. package/dist/brand/ComponentBrand.d.ts +25 -0
  12. package/dist/brand/ComponentBrand.d.ts.map +1 -0
  13. package/dist/contember/index.d.ts +10 -0
  14. package/dist/contember/index.d.ts.map +1 -0
  15. package/dist/core/ActionDispatcher.d.ts +64 -0
  16. package/dist/core/ActionDispatcher.d.ts.map +1 -0
  17. package/dist/core/Disposable.d.ts +57 -0
  18. package/dist/core/Disposable.d.ts.map +1 -0
  19. package/dist/core/EntityLoader.d.ts +80 -0
  20. package/dist/core/EntityLoader.d.ts.map +1 -0
  21. package/dist/core/PersistenceManager.d.ts +126 -0
  22. package/dist/core/PersistenceManager.d.ts.map +1 -0
  23. package/dist/core/SelectionResolver.d.ts +21 -0
  24. package/dist/core/SelectionResolver.d.ts.map +1 -0
  25. package/dist/core/actionClassification.d.ts +48 -0
  26. package/dist/core/actionClassification.d.ts.map +1 -0
  27. package/dist/core/actions.d.ts +301 -0
  28. package/dist/core/actions.d.ts.map +1 -0
  29. package/dist/core/index.d.ts +4 -0
  30. package/dist/core/index.d.ts.map +1 -0
  31. package/dist/dataview/filterHandlers.d.ts +18 -0
  32. package/dist/dataview/filterHandlers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +3 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/types.d.ts +164 -0
  36. package/dist/dataview/types.d.ts.map +1 -0
  37. package/dist/errors/UnfetchedFieldError.d.ts +11 -0
  38. package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
  39. package/dist/errors/errorPathResolver.d.ts +52 -0
  40. package/dist/errors/errorPathResolver.d.ts.map +1 -0
  41. package/dist/errors/index.d.ts +5 -0
  42. package/dist/errors/index.d.ts.map +1 -0
  43. package/dist/errors/pathMapper.d.ts +42 -0
  44. package/dist/errors/pathMapper.d.ts.map +1 -0
  45. package/dist/errors/types.d.ts +131 -0
  46. package/dist/errors/types.d.ts.map +1 -0
  47. package/dist/events/EventEmitter.d.ts +97 -0
  48. package/dist/events/EventEmitter.d.ts.map +1 -0
  49. package/dist/events/eventFactory.d.ts +40 -0
  50. package/dist/events/eventFactory.d.ts.map +1 -0
  51. package/dist/events/index.d.ts +15 -0
  52. package/dist/events/index.d.ts.map +1 -0
  53. package/dist/events/types.d.ts +264 -0
  54. package/dist/events/types.d.ts.map +1 -0
  55. package/dist/handles/BaseHandle.d.ts +64 -0
  56. package/dist/handles/BaseHandle.d.ts.map +1 -0
  57. package/dist/handles/EntityHandle.d.ts +208 -0
  58. package/dist/handles/EntityHandle.d.ts.map +1 -0
  59. package/dist/handles/FieldHandle.d.ts +111 -0
  60. package/dist/handles/FieldHandle.d.ts.map +1 -0
  61. package/dist/handles/HasManyListHandle.d.ts +166 -0
  62. package/dist/handles/HasManyListHandle.d.ts.map +1 -0
  63. package/dist/handles/HasOneHandle.d.ts +168 -0
  64. package/dist/handles/HasOneHandle.d.ts.map +1 -0
  65. package/dist/handles/PlaceholderHandle.d.ts +117 -0
  66. package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
  67. package/dist/handles/index.d.ts +8 -0
  68. package/dist/handles/index.d.ts.map +1 -0
  69. package/dist/handles/proxyFactory.d.ts +66 -0
  70. package/dist/handles/proxyFactory.d.ts.map +1 -0
  71. package/dist/handles/types.d.ts +430 -0
  72. package/dist/handles/types.d.ts.map +1 -0
  73. package/dist/index.d.ts +53 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/internal.d.ts +11 -0
  76. package/dist/internal.d.ts.map +1 -0
  77. package/dist/persistence/BatchPersister.d.ts +142 -0
  78. package/dist/persistence/BatchPersister.d.ts.map +1 -0
  79. package/dist/persistence/ChangeRegistry.d.ts +87 -0
  80. package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
  81. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
  82. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
  83. package/dist/persistence/MutationCollector.d.ts +96 -0
  84. package/dist/persistence/MutationCollector.d.ts.map +1 -0
  85. package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
  86. package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
  87. package/dist/persistence/index.d.ts +7 -0
  88. package/dist/persistence/index.d.ts.map +1 -0
  89. package/dist/persistence/types.d.ts +189 -0
  90. package/dist/persistence/types.d.ts.map +1 -0
  91. package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
  92. package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
  93. package/dist/roles/index.d.ts +11 -0
  94. package/dist/roles/index.d.ts.map +1 -0
  95. package/dist/roles/types.d.ts +242 -0
  96. package/dist/roles/types.d.ts.map +1 -0
  97. package/dist/schema/ContemberSchema.d.ts +98 -0
  98. package/dist/schema/ContemberSchema.d.ts.map +1 -0
  99. package/dist/schema/SchemaLoader.d.ts +28 -0
  100. package/dist/schema/SchemaLoader.d.ts.map +1 -0
  101. package/dist/schema/SchemaRegistry.d.ts +95 -0
  102. package/dist/schema/SchemaRegistry.d.ts.map +1 -0
  103. package/dist/schema/index.d.ts +6 -0
  104. package/dist/schema/index.d.ts.map +1 -0
  105. package/dist/schema/roles.d.ts +62 -0
  106. package/dist/schema/roles.d.ts.map +1 -0
  107. package/dist/schema/types.d.ts +179 -0
  108. package/dist/schema/types.d.ts.map +1 -0
  109. package/dist/selection/SelectionMetaCollector.d.ts +32 -0
  110. package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
  111. package/dist/selection/SelectionScope.d.ts +77 -0
  112. package/dist/selection/SelectionScope.d.ts.map +1 -0
  113. package/dist/selection/buildQuery.d.ts +45 -0
  114. package/dist/selection/buildQuery.d.ts.map +1 -0
  115. package/dist/selection/createFragment.d.ts +29 -0
  116. package/dist/selection/createFragment.d.ts.map +1 -0
  117. package/dist/selection/createSelectionBuilder.d.ts +18 -0
  118. package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
  119. package/dist/selection/index.d.ts +8 -0
  120. package/dist/selection/index.d.ts.map +1 -0
  121. package/dist/selection/queryTypes.d.ts +84 -0
  122. package/dist/selection/queryTypes.d.ts.map +1 -0
  123. package/dist/selection/types.d.ts +238 -0
  124. package/dist/selection/types.d.ts.map +1 -0
  125. package/dist/store/DirtyTracker.d.ts +26 -0
  126. package/dist/store/DirtyTracker.d.ts.map +1 -0
  127. package/dist/store/EntityMetaStore.d.ts +52 -0
  128. package/dist/store/EntityMetaStore.d.ts.map +1 -0
  129. package/dist/store/EntitySnapshotStore.d.ts +63 -0
  130. package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
  131. package/dist/store/ErrorStore.d.ts +82 -0
  132. package/dist/store/ErrorStore.d.ts.map +1 -0
  133. package/dist/store/RelationStore.d.ts +175 -0
  134. package/dist/store/RelationStore.d.ts.map +1 -0
  135. package/dist/store/SnapshotStore.d.ts +143 -0
  136. package/dist/store/SnapshotStore.d.ts.map +1 -0
  137. package/dist/store/SubscriptionManager.d.ts +90 -0
  138. package/dist/store/SubscriptionManager.d.ts.map +1 -0
  139. package/dist/store/TouchedStore.d.ts +24 -0
  140. package/dist/store/TouchedStore.d.ts.map +1 -0
  141. package/dist/store/entityId.d.ts +29 -0
  142. package/dist/store/entityId.d.ts.map +1 -0
  143. package/dist/store/index.d.ts +1 -0
  144. package/dist/store/index.d.ts.map +1 -0
  145. package/dist/store/snapshots.d.ts +144 -0
  146. package/dist/store/snapshots.d.ts.map +1 -0
  147. package/dist/undo/UndoManager.d.ts +120 -0
  148. package/dist/undo/UndoManager.d.ts.map +1 -0
  149. package/dist/undo/index.d.ts +3 -0
  150. package/dist/undo/index.d.ts.map +1 -0
  151. package/dist/undo/types.d.ts +74 -0
  152. package/dist/undo/types.d.ts.map +1 -0
  153. package/dist/utils/aliasGenerator.d.ts +25 -0
  154. package/dist/utils/aliasGenerator.d.ts.map +1 -0
  155. package/dist/utils/deepEqual.d.ts +6 -0
  156. package/dist/utils/deepEqual.d.ts.map +1 -0
  157. package/dist/utils/index.d.ts +3 -0
  158. package/dist/utils/index.d.ts.map +1 -0
  159. package/package.json +29 -0
  160. package/src/adapter/ContemberAdapter.ts +239 -0
  161. package/src/adapter/MockAdapter.ts +394 -0
  162. package/src/adapter/MockQueryEngine.ts +236 -0
  163. package/src/adapter/index.ts +21 -0
  164. package/src/adapter/types.ts +199 -0
  165. package/src/brand/ComponentBrand.ts +28 -0
  166. package/src/contember/index.ts +17 -0
  167. package/src/core/ActionDispatcher.ts +450 -0
  168. package/src/core/Disposable.ts +126 -0
  169. package/src/core/EntityLoader.ts +169 -0
  170. package/src/core/SelectionResolver.ts +78 -0
  171. package/src/core/actionClassification.ts +196 -0
  172. package/src/core/actions.ts +491 -0
  173. package/src/core/index.ts +20 -0
  174. package/src/dataview/filterHandlers.ts +443 -0
  175. package/src/dataview/index.ts +36 -0
  176. package/src/dataview/types.ts +220 -0
  177. package/src/errors/UnfetchedFieldError.ts +18 -0
  178. package/src/errors/errorPathResolver.ts +239 -0
  179. package/src/errors/index.ts +40 -0
  180. package/src/errors/pathMapper.ts +40 -0
  181. package/src/errors/types.ts +223 -0
  182. package/src/events/EventEmitter.ts +447 -0
  183. package/src/events/eventFactory.ts +430 -0
  184. package/src/events/index.ts +89 -0
  185. package/src/events/types.ts +374 -0
  186. package/src/handles/BaseHandle.ts +100 -0
  187. package/src/handles/EntityHandle.ts +538 -0
  188. package/src/handles/FieldHandle.ts +268 -0
  189. package/src/handles/HasManyListHandle.ts +500 -0
  190. package/src/handles/HasOneHandle.ts +500 -0
  191. package/src/handles/PlaceholderHandle.ts +321 -0
  192. package/src/handles/index.ts +37 -0
  193. package/src/handles/proxyFactory.ts +218 -0
  194. package/src/handles/types.ts +743 -0
  195. package/src/index.ts +384 -0
  196. package/src/internal.ts +19 -0
  197. package/src/persistence/BatchPersister.ts +911 -0
  198. package/src/persistence/ChangeRegistry.ts +150 -0
  199. package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
  200. package/src/persistence/MutationCollector.ts +567 -0
  201. package/src/persistence/MutationSchemaProvider.ts +33 -0
  202. package/src/persistence/index.ts +23 -0
  203. package/src/persistence/types.ts +220 -0
  204. package/src/schema/ContemberSchema.ts +132 -0
  205. package/src/schema/SchemaLoader.ts +93 -0
  206. package/src/schema/SchemaRegistry.ts +286 -0
  207. package/src/schema/index.ts +51 -0
  208. package/src/schema/roles.ts +83 -0
  209. package/src/schema/types.ts +238 -0
  210. package/src/selection/SelectionMetaCollector.ts +122 -0
  211. package/src/selection/SelectionScope.ts +208 -0
  212. package/src/selection/buildQuery.ts +134 -0
  213. package/src/selection/createFragment.ts +50 -0
  214. package/src/selection/createSelectionBuilder.ts +283 -0
  215. package/src/selection/index.ts +31 -0
  216. package/src/selection/queryTypes.ts +135 -0
  217. package/src/selection/types.ts +320 -0
  218. package/src/store/DirtyTracker.ts +117 -0
  219. package/src/store/EntityMetaStore.ts +143 -0
  220. package/src/store/EntitySnapshotStore.ts +260 -0
  221. package/src/store/ErrorStore.ts +251 -0
  222. package/src/store/RelationStore.ts +696 -0
  223. package/src/store/SnapshotStore.ts +765 -0
  224. package/src/store/SubscriptionManager.ts +241 -0
  225. package/src/store/TouchedStore.ts +42 -0
  226. package/src/store/entityId.ts +43 -0
  227. package/src/store/index.ts +2 -0
  228. package/src/store/snapshots.ts +203 -0
  229. package/src/undo/UndoManager.ts +418 -0
  230. package/src/undo/index.ts +9 -0
  231. package/src/undo/types.ts +84 -0
  232. package/src/utils/aliasGenerator.ts +56 -0
  233. package/src/utils/deepEqual.ts +33 -0
  234. package/src/utils/index.ts +2 -0
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Error thrown when accessing a field that wasn't included in the GraphQL selection.
3
+ * This helps debug field collection issues by clearly indicating which field is missing.
4
+ */
5
+ export class UnfetchedFieldError extends Error {
6
+ constructor(
7
+ public readonly entityType: string,
8
+ public readonly entityId: string,
9
+ public readonly fieldPath: string[],
10
+ ) {
11
+ const pathStr = fieldPath.join('.')
12
+ super(
13
+ `Accessing unfetched field "${pathStr}" on entity ${entityType}(${entityId}). ` +
14
+ `This field was not included in the selection.`,
15
+ )
16
+ this.name = 'UnfetchedFieldError'
17
+ }
18
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Deep error path resolution for Contember mutation errors.
3
+ *
4
+ * Walks error paths through the schema and store to resolve
5
+ * the exact target entity and field where the error occurred,
6
+ * instead of mapping everything to the root entity.
7
+ */
8
+
9
+ import type { SchemaRegistry } from '../schema/SchemaRegistry.js'
10
+ import type { SnapshotStore } from '../store/SnapshotStore.js'
11
+ import type { PathElement, ContemberMutationResult } from './pathMapper.js'
12
+ import { createServerError, type ServerError } from './types.js'
13
+
14
+ /**
15
+ * The resolved target of an error path.
16
+ */
17
+ export interface ResolvedErrorTarget {
18
+ readonly entityType: string
19
+ readonly entityId: string
20
+ readonly fieldName?: string
21
+ readonly type: 'field' | 'relation' | 'entity'
22
+ }
23
+
24
+ /**
25
+ * A fully resolved error with its target and error object.
26
+ */
27
+ export interface ResolvedError {
28
+ readonly target: ResolvedErrorTarget
29
+ readonly error: ServerError
30
+ }
31
+
32
+ /**
33
+ * Context needed for resolving error paths.
34
+ */
35
+ export interface ErrorPathContext {
36
+ readonly schema: SchemaRegistry
37
+ readonly store: SnapshotStore
38
+ }
39
+
40
+ /**
41
+ * Resolves an error path to the specific entity and field where the error occurred.
42
+ *
43
+ * Walks path elements through schema relations and store state:
44
+ * - `{field: name}` on scalar + last element → field error on current entity
45
+ * - `{field: name}` on has-one → traverse into related entity via store
46
+ * - `{field: name}` on has-many → consume next `{index, alias}` element
47
+ * - `{index, alias}` → resolve entity ID from alias or index-based lookup
48
+ *
49
+ * Falls back to last successfully resolved entity on failure.
50
+ */
51
+ export function resolveErrorPath(
52
+ path: PathElement[],
53
+ rootEntityType: string,
54
+ rootEntityId: string,
55
+ context: ErrorPathContext,
56
+ ): ResolvedErrorTarget {
57
+ if (path.length === 0) {
58
+ return { entityType: rootEntityType, entityId: rootEntityId, type: 'entity' }
59
+ }
60
+
61
+ let currentType = rootEntityType
62
+ let currentId = rootEntityId
63
+ let lastField: string | undefined
64
+
65
+ for (let i = 0; i < path.length; i++) {
66
+ const element = path[i]!
67
+
68
+ if ('field' in element) {
69
+ const fieldName = element.field
70
+ const fieldDef = context.schema.getFieldDef(currentType, fieldName)
71
+
72
+ if (!fieldDef) {
73
+ // Unknown field — return as field error on current entity (best effort)
74
+ return { entityType: currentType, entityId: currentId, fieldName, type: 'field' }
75
+ }
76
+
77
+ if (fieldDef.type === 'scalar') {
78
+ // Scalar field — this is the leaf target
79
+ return { entityType: currentType, entityId: currentId, fieldName, type: 'field' }
80
+ }
81
+
82
+ if (fieldDef.type === 'hasOne') {
83
+ const targetType = context.schema.getRelationTarget(currentType, fieldName)
84
+ if (!targetType) {
85
+ return { entityType: currentType, entityId: currentId, fieldName, type: 'relation' }
86
+ }
87
+
88
+ // Check if there are more path elements to traverse into
89
+ if (i === path.length - 1) {
90
+ // Last element — error is on the relation itself
91
+ return { entityType: currentType, entityId: currentId, fieldName, type: 'relation' }
92
+ }
93
+
94
+ // Traverse into the related entity
95
+ const relationState = context.store.getRelation(currentType, currentId, fieldName)
96
+ if (!relationState || !relationState.currentId) {
97
+ // Can't resolve further — return relation error on current entity
98
+ return { entityType: currentType, entityId: currentId, fieldName, type: 'relation' }
99
+ }
100
+
101
+ currentType = targetType
102
+ currentId = relationState.currentId
103
+ lastField = fieldName
104
+ continue
105
+ }
106
+
107
+ if (fieldDef.type === 'hasMany') {
108
+ const targetType = context.schema.getRelationTarget(currentType, fieldName)
109
+ if (!targetType) {
110
+ return { entityType: currentType, entityId: currentId, fieldName, type: 'relation' }
111
+ }
112
+
113
+ // Next element should be {index, alias}
114
+ const nextElement = path[i + 1]
115
+ if (!nextElement || !('index' in nextElement)) {
116
+ // No index element — error is on the relation itself
117
+ return { entityType: currentType, entityId: currentId, fieldName, type: 'relation' }
118
+ }
119
+
120
+ // Consume the index element
121
+ i++
122
+
123
+ // Resolve entity ID from alias or index
124
+ const resolvedId = resolveHasManyItemId(
125
+ nextElement as { index: number; alias: string | null },
126
+ currentType,
127
+ currentId,
128
+ fieldName,
129
+ context.store,
130
+ )
131
+
132
+ if (!resolvedId) {
133
+ // Can't resolve — return relation error on current entity
134
+ return { entityType: currentType, entityId: currentId, fieldName, type: 'relation' }
135
+ }
136
+
137
+ currentType = targetType
138
+ currentId = resolvedId
139
+ lastField = fieldName
140
+ continue
141
+ }
142
+ } else if ('index' in element) {
143
+ // Bare index element without preceding has-many field — shouldn't happen
144
+ // but handle gracefully by returning entity-level error
145
+ continue
146
+ }
147
+ }
148
+
149
+ // Ended traversal without hitting a leaf — return entity-level error on current entity
150
+ return { entityType: currentType, entityId: currentId, type: 'entity' }
151
+ }
152
+
153
+ /**
154
+ * Resolves a has-many item ID from an index/alias path element.
155
+ * Prefers alias (which is the entity ID) over index-based lookup.
156
+ */
157
+ function resolveHasManyItemId(
158
+ element: { index: number; alias: string | null },
159
+ parentType: string,
160
+ parentId: string,
161
+ fieldName: string,
162
+ store: SnapshotStore,
163
+ ): string | undefined {
164
+ // Alias is the entity ID — use directly
165
+ if (element.alias) {
166
+ return element.alias
167
+ }
168
+
169
+ // Fall back to index-based lookup from ordered list
170
+ const orderedIds = store.getHasManyOrderedIds(parentType, parentId, fieldName)
171
+ return orderedIds[element.index]
172
+ }
173
+
174
+ /**
175
+ * Converts error type string to a user-friendly error code.
176
+ */
177
+ function getErrorCode(errorType?: string): string | undefined {
178
+ if (!errorType) return undefined
179
+
180
+ switch (errorType) {
181
+ case 'UniqueConstraintViolation':
182
+ return 'UNIQUE_CONSTRAINT'
183
+ case 'NotNullConstraintViolation':
184
+ return 'NOT_NULL'
185
+ case 'ForeignKeyConstraintViolation':
186
+ return 'FOREIGN_KEY'
187
+ case 'NotFoundOrDenied':
188
+ return 'NOT_FOUND'
189
+ case 'NonUniqueWhereInput':
190
+ return 'NON_UNIQUE_WHERE'
191
+ case 'InvalidDataInput':
192
+ return 'INVALID_DATA'
193
+ case 'SqlError':
194
+ return 'SQL_ERROR'
195
+ default:
196
+ return undefined
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Resolves all errors from a Contember mutation result to specific entity targets.
202
+ * Processes both execution errors and validation errors.
203
+ */
204
+ export function resolveAllErrors(
205
+ result: ContemberMutationResult,
206
+ rootEntityType: string,
207
+ rootEntityId: string,
208
+ context: ErrorPathContext,
209
+ ): ResolvedError[] {
210
+ const resolved: ResolvedError[] = []
211
+
212
+ for (const error of result.errors) {
213
+ if (error.paths.length === 0) {
214
+ resolved.push({
215
+ target: { entityType: rootEntityType, entityId: rootEntityId, type: 'entity' },
216
+ error: createServerError(error.message, error.type, getErrorCode(error.type)),
217
+ })
218
+ continue
219
+ }
220
+
221
+ for (const path of error.paths) {
222
+ const target = resolveErrorPath(path, rootEntityType, rootEntityId, context)
223
+ resolved.push({
224
+ target,
225
+ error: createServerError(error.message, error.type, getErrorCode(error.type)),
226
+ })
227
+ }
228
+ }
229
+
230
+ for (const error of result.validation.errors) {
231
+ const target = resolveErrorPath(error.path, rootEntityType, rootEntityId, context)
232
+ resolved.push({
233
+ target,
234
+ error: createServerError(error.message.text, undefined, 'VALIDATION_ERROR'),
235
+ })
236
+ }
237
+
238
+ return resolved
239
+ }
@@ -0,0 +1,40 @@
1
+ export {
2
+ type ExecutionErrorType,
3
+ type ErrorCategory,
4
+ type BindxError,
5
+ type ClientError,
6
+ type ServerError,
7
+ type LoadError,
8
+ type FieldError,
9
+ type ErrorState,
10
+ type ErrorInput,
11
+ ExecutionErrorTypes,
12
+ isExecutionErrorType,
13
+ classifyError,
14
+ isRetryableCategory,
15
+ createClientError,
16
+ createServerError,
17
+ createLoadError,
18
+ isClientError,
19
+ isServerError,
20
+ isLoadError,
21
+ filterErrorsBySource,
22
+ filterStickyErrors,
23
+ } from './types.js'
24
+
25
+ export { UnfetchedFieldError } from './UnfetchedFieldError.js'
26
+
27
+ export {
28
+ type PathElement,
29
+ type ContemberMutationError,
30
+ type ContemberValidationError,
31
+ type ContemberMutationResult,
32
+ } from './pathMapper.js'
33
+
34
+ export {
35
+ type ResolvedErrorTarget,
36
+ type ResolvedError,
37
+ type ErrorPathContext,
38
+ resolveErrorPath,
39
+ resolveAllErrors,
40
+ } from './errorPathResolver.js'
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Types for Contember error paths and mutation results.
3
+ */
4
+
5
+ /**
6
+ * Contember path element - either a field name or an array index.
7
+ */
8
+ export type PathElement =
9
+ | { field: string }
10
+ | { index: number; alias: string | null }
11
+
12
+ /**
13
+ * Contember mutation error structure.
14
+ */
15
+ export interface ContemberMutationError {
16
+ readonly paths: PathElement[][]
17
+ readonly message: string
18
+ readonly type: string
19
+ }
20
+
21
+ /**
22
+ * Contember validation error structure.
23
+ */
24
+ export interface ContemberValidationError {
25
+ readonly path: PathElement[]
26
+ readonly message: { text: string }
27
+ }
28
+
29
+ /**
30
+ * Processes a Contember mutation result and extracts errors.
31
+ */
32
+ export interface ContemberMutationResult {
33
+ readonly ok: boolean
34
+ readonly errorMessage: string | null
35
+ readonly errors: ContemberMutationError[]
36
+ readonly validation: {
37
+ readonly valid: boolean
38
+ readonly errors: ContemberValidationError[]
39
+ }
40
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Error types for bindx error handling system.
3
+ *
4
+ * Supports both client-side validation errors and server-side errors
5
+ * from Contember mutations.
6
+ */
7
+
8
+ import { Result } from '@contember/schema'
9
+
10
+ /**
11
+ * Execution error types from Contember.
12
+ * Re-exported from @contember/schema to ensure type compatibility.
13
+ */
14
+ export type ExecutionErrorType = `${Result.ExecutionErrorType}`
15
+
16
+ /**
17
+ * All possible execution error type values.
18
+ * Used for runtime validation and error classification.
19
+ */
20
+ export const ExecutionErrorTypes: Record<ExecutionErrorType, ExecutionErrorType> = {
21
+ NotNullConstraintViolation: 'NotNullConstraintViolation',
22
+ UniqueConstraintViolation: 'UniqueConstraintViolation',
23
+ ForeignKeyConstraintViolation: 'ForeignKeyConstraintViolation',
24
+ NotFoundOrDenied: 'NotFoundOrDenied',
25
+ NonUniqueWhereInput: 'NonUniqueWhereInput',
26
+ InvalidDataInput: 'InvalidDataInput',
27
+ SqlError: 'SqlError',
28
+ }
29
+
30
+ /**
31
+ * Checks if a string is a valid ExecutionErrorType.
32
+ */
33
+ export function isExecutionErrorType(value: string): value is ExecutionErrorType {
34
+ return value in ExecutionErrorTypes
35
+ }
36
+
37
+ /**
38
+ * Error classification for retry logic.
39
+ */
40
+ export type ErrorCategory = 'validation' | 'constraint' | 'not_found' | 'transient' | 'unknown'
41
+
42
+ /**
43
+ * Classifies an execution error type into a category.
44
+ * - 'validation': Data validation errors (not retryable)
45
+ * - 'constraint': Database constraint violations (not retryable without data change)
46
+ * - 'not_found': Entity not found or access denied (not retryable)
47
+ * - 'transient': Temporary errors that may succeed on retry
48
+ * - 'unknown': Unclassified errors
49
+ */
50
+ export function classifyError(errorType?: string): ErrorCategory {
51
+ if (!errorType) return 'unknown'
52
+
53
+ switch (errorType) {
54
+ case 'NotNullConstraintViolation':
55
+ case 'UniqueConstraintViolation':
56
+ case 'ForeignKeyConstraintViolation':
57
+ return 'constraint'
58
+ case 'InvalidDataInput':
59
+ case 'NonUniqueWhereInput':
60
+ return 'validation'
61
+ case 'NotFoundOrDenied':
62
+ return 'not_found'
63
+ case 'SqlError':
64
+ // SQL errors could be transient (deadlock, connection issues) or permanent
65
+ return 'transient'
66
+ default:
67
+ return 'unknown'
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Checks if an error category is potentially retryable.
73
+ */
74
+ export function isRetryableCategory(category: ErrorCategory): boolean {
75
+ return category === 'transient' || category === 'unknown'
76
+ }
77
+
78
+ /**
79
+ * Base interface for all bindx errors.
80
+ */
81
+ export interface BindxError {
82
+ /** Human-readable error message */
83
+ readonly message: string
84
+ /** Optional error code for programmatic handling (e.g., 'REQUIRED', 'UNIQUE_CONSTRAINT') */
85
+ readonly code?: string
86
+ /** If true, error won't auto-clear when field value changes */
87
+ readonly sticky?: boolean
88
+ }
89
+
90
+ /**
91
+ * Client-side error - added by user code for validation.
92
+ */
93
+ export interface ClientError extends BindxError {
94
+ readonly source: 'client'
95
+ }
96
+
97
+ /**
98
+ * Server-side error - from Contember mutation response.
99
+ */
100
+ export interface ServerError extends BindxError {
101
+ readonly source: 'server'
102
+ /** Contember execution error type for database errors (string to support forward-compatible unknown types) */
103
+ readonly type?: string
104
+ /** Error category for classification */
105
+ readonly category: ErrorCategory
106
+ /** Whether this error might succeed on retry */
107
+ readonly retryable: boolean
108
+ }
109
+
110
+ /**
111
+ * Load error - from data fetching (queries).
112
+ */
113
+ export interface LoadError extends BindxError {
114
+ readonly source: 'load'
115
+ /** Error category for classification */
116
+ readonly category: ErrorCategory
117
+ /** Whether this error might succeed on retry */
118
+ readonly retryable: boolean
119
+ /** Original Error object for stack trace access */
120
+ readonly originalError?: Error
121
+ }
122
+
123
+ /**
124
+ * Union type for all error sources.
125
+ */
126
+ export type FieldError = ClientError | ServerError | LoadError
127
+
128
+ /**
129
+ * Error state stored in the SnapshotStore.
130
+ */
131
+ export interface ErrorState {
132
+ readonly errors: readonly FieldError[]
133
+ readonly version: number
134
+ }
135
+
136
+ /**
137
+ * Input type for addError - allows string shorthand or full error object.
138
+ */
139
+ export type ErrorInput = string | Omit<ClientError, 'source'>
140
+
141
+ /**
142
+ * Creates a ClientError from ErrorInput.
143
+ */
144
+ export function createClientError(input: ErrorInput): ClientError {
145
+ if (typeof input === 'string') {
146
+ return { source: 'client', message: input }
147
+ }
148
+ return { ...input, source: 'client' }
149
+ }
150
+
151
+ /**
152
+ * Creates a ServerError from Contember error data.
153
+ * Automatically computes error category and retryable flag.
154
+ */
155
+ export function createServerError(
156
+ message: string,
157
+ type?: string,
158
+ code?: string,
159
+ ): ServerError {
160
+ const category = classifyError(type)
161
+ return {
162
+ source: 'server',
163
+ message,
164
+ type,
165
+ code,
166
+ category,
167
+ retryable: isRetryableCategory(category),
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Creates a LoadError from a plain Error.
173
+ * Load errors are always classified as 'transient' and retryable.
174
+ */
175
+ export function createLoadError(error: Error): LoadError {
176
+ return {
177
+ source: 'load',
178
+ message: error.message,
179
+ category: 'transient',
180
+ retryable: true,
181
+ originalError: error,
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Checks if an error is a client error.
187
+ */
188
+ export function isClientError(error: FieldError): error is ClientError {
189
+ return error.source === 'client'
190
+ }
191
+
192
+ /**
193
+ * Checks if an error is a server error.
194
+ */
195
+ export function isServerError(error: FieldError): error is ServerError {
196
+ return error.source === 'server'
197
+ }
198
+
199
+ /**
200
+ * Checks if an error is a load error.
201
+ */
202
+ export function isLoadError(error: FieldError): error is LoadError {
203
+ return error.source === 'load'
204
+ }
205
+
206
+ /**
207
+ * Filters errors by source.
208
+ */
209
+ export function filterErrorsBySource(
210
+ errors: readonly FieldError[],
211
+ source: 'client' | 'server' | 'load',
212
+ ): FieldError[] {
213
+ return errors.filter(e => e.source === source)
214
+ }
215
+
216
+ /**
217
+ * Filters out non-sticky client errors.
218
+ * Used when auto-clearing errors on value change.
219
+ * Load and server errors are always preserved.
220
+ */
221
+ export function filterStickyErrors(errors: readonly FieldError[]): FieldError[] {
222
+ return errors.filter(e => e.source === 'server' || e.source === 'load' || e.sticky === true)
223
+ }