@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.
- package/dist/adapter/ContemberAdapter.d.ts +40 -0
- package/dist/adapter/ContemberAdapter.d.ts.map +1 -0
- package/dist/adapter/MockAdapter.d.ts +81 -0
- package/dist/adapter/MockAdapter.d.ts.map +1 -0
- package/dist/adapter/MockQueryEngine.d.ts +43 -0
- package/dist/adapter/MockQueryEngine.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +5 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/types.d.ts +184 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/brand/ComponentBrand.d.ts +25 -0
- package/dist/brand/ComponentBrand.d.ts.map +1 -0
- package/dist/contember/index.d.ts +10 -0
- package/dist/contember/index.d.ts.map +1 -0
- package/dist/core/ActionDispatcher.d.ts +64 -0
- package/dist/core/ActionDispatcher.d.ts.map +1 -0
- package/dist/core/Disposable.d.ts +57 -0
- package/dist/core/Disposable.d.ts.map +1 -0
- package/dist/core/EntityLoader.d.ts +80 -0
- package/dist/core/EntityLoader.d.ts.map +1 -0
- package/dist/core/PersistenceManager.d.ts +126 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -0
- package/dist/core/SelectionResolver.d.ts +21 -0
- package/dist/core/SelectionResolver.d.ts.map +1 -0
- package/dist/core/actionClassification.d.ts +48 -0
- package/dist/core/actionClassification.d.ts.map +1 -0
- package/dist/core/actions.d.ts +301 -0
- package/dist/core/actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/dataview/filterHandlers.d.ts +18 -0
- package/dist/dataview/filterHandlers.d.ts.map +1 -0
- package/dist/dataview/index.d.ts +3 -0
- package/dist/dataview/index.d.ts.map +1 -0
- package/dist/dataview/types.d.ts +164 -0
- package/dist/dataview/types.d.ts.map +1 -0
- package/dist/errors/UnfetchedFieldError.d.ts +11 -0
- package/dist/errors/UnfetchedFieldError.d.ts.map +1 -0
- package/dist/errors/errorPathResolver.d.ts +52 -0
- package/dist/errors/errorPathResolver.d.ts.map +1 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/pathMapper.d.ts +42 -0
- package/dist/errors/pathMapper.d.ts.map +1 -0
- package/dist/errors/types.d.ts +131 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/events/EventEmitter.d.ts +97 -0
- package/dist/events/EventEmitter.d.ts.map +1 -0
- package/dist/events/eventFactory.d.ts +40 -0
- package/dist/events/eventFactory.d.ts.map +1 -0
- package/dist/events/index.d.ts +15 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/types.d.ts +264 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/handles/BaseHandle.d.ts +64 -0
- package/dist/handles/BaseHandle.d.ts.map +1 -0
- package/dist/handles/EntityHandle.d.ts +208 -0
- package/dist/handles/EntityHandle.d.ts.map +1 -0
- package/dist/handles/FieldHandle.d.ts +111 -0
- package/dist/handles/FieldHandle.d.ts.map +1 -0
- package/dist/handles/HasManyListHandle.d.ts +166 -0
- package/dist/handles/HasManyListHandle.d.ts.map +1 -0
- package/dist/handles/HasOneHandle.d.ts +168 -0
- package/dist/handles/HasOneHandle.d.ts.map +1 -0
- package/dist/handles/PlaceholderHandle.d.ts +117 -0
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -0
- package/dist/handles/index.d.ts +8 -0
- package/dist/handles/index.d.ts.map +1 -0
- package/dist/handles/proxyFactory.d.ts +66 -0
- package/dist/handles/proxyFactory.d.ts.map +1 -0
- package/dist/handles/types.d.ts +430 -0
- package/dist/handles/types.d.ts.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/internal.d.ts +11 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/persistence/BatchPersister.d.ts +142 -0
- package/dist/persistence/BatchPersister.d.ts.map +1 -0
- package/dist/persistence/ChangeRegistry.d.ts +87 -0
- package/dist/persistence/ChangeRegistry.d.ts.map +1 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +16 -0
- package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -0
- package/dist/persistence/MutationCollector.d.ts +96 -0
- package/dist/persistence/MutationCollector.d.ts.map +1 -0
- package/dist/persistence/MutationSchemaProvider.d.ts +30 -0
- package/dist/persistence/MutationSchemaProvider.d.ts.map +1 -0
- package/dist/persistence/index.d.ts +7 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/types.d.ts +189 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/roles/RoleSchemaRegistry.d.ts +77 -0
- package/dist/roles/RoleSchemaRegistry.d.ts.map +1 -0
- package/dist/roles/index.d.ts +11 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/dist/roles/types.d.ts +242 -0
- package/dist/roles/types.d.ts.map +1 -0
- package/dist/schema/ContemberSchema.d.ts +98 -0
- package/dist/schema/ContemberSchema.d.ts.map +1 -0
- package/dist/schema/SchemaLoader.d.ts +28 -0
- package/dist/schema/SchemaLoader.d.ts.map +1 -0
- package/dist/schema/SchemaRegistry.d.ts +95 -0
- package/dist/schema/SchemaRegistry.d.ts.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/roles.d.ts +62 -0
- package/dist/schema/roles.d.ts.map +1 -0
- package/dist/schema/types.d.ts +179 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/selection/SelectionMetaCollector.d.ts +32 -0
- package/dist/selection/SelectionMetaCollector.d.ts.map +1 -0
- package/dist/selection/SelectionScope.d.ts +77 -0
- package/dist/selection/SelectionScope.d.ts.map +1 -0
- package/dist/selection/buildQuery.d.ts +45 -0
- package/dist/selection/buildQuery.d.ts.map +1 -0
- package/dist/selection/createFragment.d.ts +29 -0
- package/dist/selection/createFragment.d.ts.map +1 -0
- package/dist/selection/createSelectionBuilder.d.ts +18 -0
- package/dist/selection/createSelectionBuilder.d.ts.map +1 -0
- package/dist/selection/index.d.ts +8 -0
- package/dist/selection/index.d.ts.map +1 -0
- package/dist/selection/queryTypes.d.ts +84 -0
- package/dist/selection/queryTypes.d.ts.map +1 -0
- package/dist/selection/types.d.ts +238 -0
- package/dist/selection/types.d.ts.map +1 -0
- package/dist/store/DirtyTracker.d.ts +26 -0
- package/dist/store/DirtyTracker.d.ts.map +1 -0
- package/dist/store/EntityMetaStore.d.ts +52 -0
- package/dist/store/EntityMetaStore.d.ts.map +1 -0
- package/dist/store/EntitySnapshotStore.d.ts +63 -0
- package/dist/store/EntitySnapshotStore.d.ts.map +1 -0
- package/dist/store/ErrorStore.d.ts +82 -0
- package/dist/store/ErrorStore.d.ts.map +1 -0
- package/dist/store/RelationStore.d.ts +175 -0
- package/dist/store/RelationStore.d.ts.map +1 -0
- package/dist/store/SnapshotStore.d.ts +143 -0
- package/dist/store/SnapshotStore.d.ts.map +1 -0
- package/dist/store/SubscriptionManager.d.ts +90 -0
- package/dist/store/SubscriptionManager.d.ts.map +1 -0
- package/dist/store/TouchedStore.d.ts +24 -0
- package/dist/store/TouchedStore.d.ts.map +1 -0
- package/dist/store/entityId.d.ts +29 -0
- package/dist/store/entityId.d.ts.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/snapshots.d.ts +144 -0
- package/dist/store/snapshots.d.ts.map +1 -0
- package/dist/undo/UndoManager.d.ts +120 -0
- package/dist/undo/UndoManager.d.ts.map +1 -0
- package/dist/undo/index.d.ts +3 -0
- package/dist/undo/index.d.ts.map +1 -0
- package/dist/undo/types.d.ts +74 -0
- package/dist/undo/types.d.ts.map +1 -0
- package/dist/utils/aliasGenerator.d.ts +25 -0
- package/dist/utils/aliasGenerator.d.ts.map +1 -0
- package/dist/utils/deepEqual.d.ts +6 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +29 -0
- package/src/adapter/ContemberAdapter.ts +239 -0
- package/src/adapter/MockAdapter.ts +394 -0
- package/src/adapter/MockQueryEngine.ts +236 -0
- package/src/adapter/index.ts +21 -0
- package/src/adapter/types.ts +199 -0
- package/src/brand/ComponentBrand.ts +28 -0
- package/src/contember/index.ts +17 -0
- package/src/core/ActionDispatcher.ts +450 -0
- package/src/core/Disposable.ts +126 -0
- package/src/core/EntityLoader.ts +169 -0
- package/src/core/SelectionResolver.ts +78 -0
- package/src/core/actionClassification.ts +196 -0
- package/src/core/actions.ts +491 -0
- package/src/core/index.ts +20 -0
- package/src/dataview/filterHandlers.ts +443 -0
- package/src/dataview/index.ts +36 -0
- package/src/dataview/types.ts +220 -0
- package/src/errors/UnfetchedFieldError.ts +18 -0
- package/src/errors/errorPathResolver.ts +239 -0
- package/src/errors/index.ts +40 -0
- package/src/errors/pathMapper.ts +40 -0
- package/src/errors/types.ts +223 -0
- package/src/events/EventEmitter.ts +447 -0
- package/src/events/eventFactory.ts +430 -0
- package/src/events/index.ts +89 -0
- package/src/events/types.ts +374 -0
- package/src/handles/BaseHandle.ts +100 -0
- package/src/handles/EntityHandle.ts +538 -0
- package/src/handles/FieldHandle.ts +268 -0
- package/src/handles/HasManyListHandle.ts +500 -0
- package/src/handles/HasOneHandle.ts +500 -0
- package/src/handles/PlaceholderHandle.ts +321 -0
- package/src/handles/index.ts +37 -0
- package/src/handles/proxyFactory.ts +218 -0
- package/src/handles/types.ts +743 -0
- package/src/index.ts +384 -0
- package/src/internal.ts +19 -0
- package/src/persistence/BatchPersister.ts +911 -0
- package/src/persistence/ChangeRegistry.ts +150 -0
- package/src/persistence/ContemberSchemaMutationAdapter.ts +57 -0
- package/src/persistence/MutationCollector.ts +567 -0
- package/src/persistence/MutationSchemaProvider.ts +33 -0
- package/src/persistence/index.ts +23 -0
- package/src/persistence/types.ts +220 -0
- package/src/schema/ContemberSchema.ts +132 -0
- package/src/schema/SchemaLoader.ts +93 -0
- package/src/schema/SchemaRegistry.ts +286 -0
- package/src/schema/index.ts +51 -0
- package/src/schema/roles.ts +83 -0
- package/src/schema/types.ts +238 -0
- package/src/selection/SelectionMetaCollector.ts +122 -0
- package/src/selection/SelectionScope.ts +208 -0
- package/src/selection/buildQuery.ts +134 -0
- package/src/selection/createFragment.ts +50 -0
- package/src/selection/createSelectionBuilder.ts +283 -0
- package/src/selection/index.ts +31 -0
- package/src/selection/queryTypes.ts +135 -0
- package/src/selection/types.ts +320 -0
- package/src/store/DirtyTracker.ts +117 -0
- package/src/store/EntityMetaStore.ts +143 -0
- package/src/store/EntitySnapshotStore.ts +260 -0
- package/src/store/ErrorStore.ts +251 -0
- package/src/store/RelationStore.ts +696 -0
- package/src/store/SnapshotStore.ts +765 -0
- package/src/store/SubscriptionManager.ts +241 -0
- package/src/store/TouchedStore.ts +42 -0
- package/src/store/entityId.ts +43 -0
- package/src/store/index.ts +2 -0
- package/src/store/snapshots.ts +203 -0
- package/src/undo/UndoManager.ts +418 -0
- package/src/undo/index.ts +9 -0
- package/src/undo/types.ts +84 -0
- package/src/utils/aliasGenerator.ts +56 -0
- package/src/utils/deepEqual.ts +33 -0
- 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
|
+
}
|