@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,500 @@
|
|
|
1
|
+
import { EntityRelatedHandle } from './BaseHandle.js'
|
|
2
|
+
import { EntityHandle } from './EntityHandle.js'
|
|
3
|
+
import type { ActionDispatcher } from '../core/ActionDispatcher.js'
|
|
4
|
+
import type { SnapshotStore } from '../store/SnapshotStore.js'
|
|
5
|
+
import type { SchemaRegistry } from '../schema/SchemaRegistry.js'
|
|
6
|
+
import type { SelectionMeta } from '../selection/types.js'
|
|
7
|
+
import {
|
|
8
|
+
addRelationError,
|
|
9
|
+
clearRelationErrors,
|
|
10
|
+
} from '../core/actions.js'
|
|
11
|
+
import { FIELD_REF_META, type HasManyRef, type FieldRefMeta, type EntityAccessor, type Unsubscribe } from './types.js'
|
|
12
|
+
import { createClientError, type ErrorInput, type FieldError } from '../errors/types.js'
|
|
13
|
+
import type {
|
|
14
|
+
EventListener,
|
|
15
|
+
Interceptor,
|
|
16
|
+
HasManyConnectedEvent,
|
|
17
|
+
HasManyDisconnectedEvent,
|
|
18
|
+
HasManyConnectingEvent,
|
|
19
|
+
HasManyDisconnectingEvent,
|
|
20
|
+
} from '../events/types.js'
|
|
21
|
+
import { createAliasProxy } from './proxyFactory.js'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* HasManyListHandle provides access to a has-many relation (list of entities).
|
|
25
|
+
* Implements HasManyRef interface for JSX compatibility.
|
|
26
|
+
*
|
|
27
|
+
* @typeParam TEntity - The full entity type of items in the relation
|
|
28
|
+
* @typeParam TSelected - The selected subset of fields (defaults to TEntity for backwards compatibility)
|
|
29
|
+
*/
|
|
30
|
+
export class HasManyListHandle<TEntity extends object = object, TSelected = TEntity> extends EntityRelatedHandle implements HasManyRef<TEntity, TSelected> {
|
|
31
|
+
private itemHandleCache = new Map<string, EntityHandle<TEntity, TSelected>>()
|
|
32
|
+
|
|
33
|
+
/** Runtime brand symbols for validation */
|
|
34
|
+
readonly __brands?: Set<symbol>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Type brand - entity name for type inference.
|
|
38
|
+
* Implements HasManyRef.__entityName.
|
|
39
|
+
*/
|
|
40
|
+
get __entityName(): string {
|
|
41
|
+
return this.itemType
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Type brand - schema for type inference.
|
|
46
|
+
* Returns undefined at runtime; schema type is compile-time only.
|
|
47
|
+
*/
|
|
48
|
+
get __schema(): undefined {
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Alias for this has-many relation.
|
|
54
|
+
* When the same field is used multiple times with different params,
|
|
55
|
+
* each instance has a unique alias to store data separately.
|
|
56
|
+
*/
|
|
57
|
+
private readonly alias: string
|
|
58
|
+
|
|
59
|
+
private constructor(
|
|
60
|
+
parentEntityType: string,
|
|
61
|
+
parentEntityId: string,
|
|
62
|
+
private readonly fieldName: string,
|
|
63
|
+
private readonly itemType: string,
|
|
64
|
+
store: SnapshotStore,
|
|
65
|
+
dispatcher: ActionDispatcher,
|
|
66
|
+
private readonly schema: SchemaRegistry,
|
|
67
|
+
brands?: Set<symbol>,
|
|
68
|
+
alias?: string,
|
|
69
|
+
private readonly selection?: SelectionMeta,
|
|
70
|
+
) {
|
|
71
|
+
super(parentEntityType, parentEntityId, store, dispatcher)
|
|
72
|
+
this.__brands = brands
|
|
73
|
+
this.alias = alias ?? fieldName
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static create<TEntity extends object = object, TSelected = TEntity>(
|
|
77
|
+
parentEntityType: string,
|
|
78
|
+
parentEntityId: string,
|
|
79
|
+
fieldName: string,
|
|
80
|
+
itemType: string,
|
|
81
|
+
store: SnapshotStore,
|
|
82
|
+
dispatcher: ActionDispatcher,
|
|
83
|
+
schema: SchemaRegistry,
|
|
84
|
+
brands?: Set<symbol>,
|
|
85
|
+
alias?: string,
|
|
86
|
+
selection?: SelectionMeta,
|
|
87
|
+
): HasManyListHandle<TEntity, TSelected> {
|
|
88
|
+
return createAliasProxy(new HasManyListHandle<TEntity, TSelected>(parentEntityType, parentEntityId, fieldName, itemType, store, dispatcher, schema, brands, alias, selection))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* JSX field reference metadata for collection phase.
|
|
93
|
+
* Implements HasManyRef interface.
|
|
94
|
+
*/
|
|
95
|
+
get [FIELD_REF_META](): FieldRefMeta {
|
|
96
|
+
return {
|
|
97
|
+
entityType: this.entityType,
|
|
98
|
+
entityId: this.entityId,
|
|
99
|
+
path: [this.fieldName],
|
|
100
|
+
fieldName: this.fieldName,
|
|
101
|
+
isArray: true,
|
|
102
|
+
isRelation: true,
|
|
103
|
+
targetType: this.itemType,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets the list of items as entity accessors with direct field access.
|
|
109
|
+
* Returns selection-aware EntityAccessors that support `item.fieldName.value`.
|
|
110
|
+
* Includes planned connections and excludes planned removals.
|
|
111
|
+
* Uses ordered IDs to preserve order including after move() operations.
|
|
112
|
+
*/
|
|
113
|
+
get items(): EntityAccessor<TEntity, TSelected>[] {
|
|
114
|
+
const data = this.getEntityData()
|
|
115
|
+
if (!data) return []
|
|
116
|
+
|
|
117
|
+
const rawData = data[this.alias] ?? data[this.fieldName]
|
|
118
|
+
const listData = this.extractItems(rawData)
|
|
119
|
+
if (!listData) return []
|
|
120
|
+
|
|
121
|
+
// Ensure snapshots exist for embedded items
|
|
122
|
+
this.ensureItemSnapshots(listData)
|
|
123
|
+
|
|
124
|
+
// Extract server IDs from embedded data
|
|
125
|
+
const serverIds = listData
|
|
126
|
+
.map((item) => item['id'] as string | undefined)
|
|
127
|
+
.filter((id): id is string => id !== undefined)
|
|
128
|
+
|
|
129
|
+
// Ensure has-many state exists with proper server IDs
|
|
130
|
+
// This is needed so that connect/disconnect operations work correctly
|
|
131
|
+
this.store.getOrCreateHasMany(
|
|
132
|
+
this.entityType,
|
|
133
|
+
this.entityId,
|
|
134
|
+
this.fieldName,
|
|
135
|
+
serverIds,
|
|
136
|
+
this.alias,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// Use ordered IDs from store (handles removals, connections, and ordering)
|
|
140
|
+
const orderedIds = this.store.getHasManyOrderedIds(
|
|
141
|
+
this.entityType,
|
|
142
|
+
this.entityId,
|
|
143
|
+
this.fieldName,
|
|
144
|
+
this.alias,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return orderedIds.map((id) => this.getItemHandle(id))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Ensures snapshots exist for embedded has-many items.
|
|
152
|
+
*/
|
|
153
|
+
private ensureItemSnapshots(listData: Array<Record<string, unknown>>): void {
|
|
154
|
+
for (const itemData of listData) {
|
|
155
|
+
const itemId = itemData['id'] as string
|
|
156
|
+
if (!itemId) continue
|
|
157
|
+
|
|
158
|
+
// Register parent-child relationship for change propagation
|
|
159
|
+
// This needs to happen even if the snapshot already exists
|
|
160
|
+
this.store.registerParentChild(this.entityType, this.entityId, this.itemType, itemId)
|
|
161
|
+
|
|
162
|
+
// Skip if snapshot already exists
|
|
163
|
+
if (this.store.hasEntity(this.itemType, itemId)) {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Create snapshot from embedded data
|
|
168
|
+
// Skip notification to avoid triggering React state updates during render
|
|
169
|
+
this.store.setEntityData(
|
|
170
|
+
this.itemType,
|
|
171
|
+
itemId,
|
|
172
|
+
itemData,
|
|
173
|
+
true, // isServerData
|
|
174
|
+
true, // skipNotify - called during render, data already exists embedded in parent
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extracts items from either flat array or Connection format (paginateRelation).
|
|
181
|
+
* Returns null if data is not in a recognized format.
|
|
182
|
+
*/
|
|
183
|
+
private extractItems(rawData: unknown): Array<Record<string, unknown>> | null {
|
|
184
|
+
// Flat array format: [{ id, name }, ...]
|
|
185
|
+
if (Array.isArray(rawData)) {
|
|
186
|
+
return rawData
|
|
187
|
+
}
|
|
188
|
+
// Connection format: { pageInfo: { totalCount }, edges: [{ node: { id, name } }] }
|
|
189
|
+
if (rawData && typeof rawData === 'object' && 'edges' in rawData) {
|
|
190
|
+
const connection = rawData as { edges: Array<{ node: Record<string, unknown> }> }
|
|
191
|
+
return connection.edges.map(edge => edge.node)
|
|
192
|
+
}
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Gets the total count from paginateRelation response.
|
|
198
|
+
* Content client attaches totalCount as a non-enumerable property on the array.
|
|
199
|
+
* Returns undefined if totalCount was not requested or not available.
|
|
200
|
+
*/
|
|
201
|
+
get totalCount(): number | undefined {
|
|
202
|
+
const data = this.getEntityData()
|
|
203
|
+
if (!data) return undefined
|
|
204
|
+
|
|
205
|
+
const rawData = data[this.alias] ?? data[this.fieldName]
|
|
206
|
+
if (Array.isArray(rawData) && 'totalCount' in rawData) {
|
|
207
|
+
return (rawData as unknown as { totalCount: number }).totalCount
|
|
208
|
+
}
|
|
209
|
+
return undefined
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Gets the number of items.
|
|
214
|
+
*/
|
|
215
|
+
get length(): number {
|
|
216
|
+
return this.items.length
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Gets a handle for a specific item.
|
|
221
|
+
* Returns selection-aware EntityAccessor that supports direct field access.
|
|
222
|
+
*/
|
|
223
|
+
getItemHandle(itemId: string): EntityAccessor<TEntity, TSelected> {
|
|
224
|
+
let handle = this.itemHandleCache.get(itemId)
|
|
225
|
+
|
|
226
|
+
if (!handle) {
|
|
227
|
+
handle = EntityHandle.create<TEntity, TSelected>(
|
|
228
|
+
itemId,
|
|
229
|
+
this.itemType,
|
|
230
|
+
this.store,
|
|
231
|
+
this.dispatcher,
|
|
232
|
+
this.schema,
|
|
233
|
+
this.__brands,
|
|
234
|
+
this.selection,
|
|
235
|
+
)
|
|
236
|
+
this.itemHandleCache.set(itemId, handle)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// EntityHandle constructor returns a Proxy that implements EntityAccessor
|
|
240
|
+
return handle as unknown as EntityAccessor<TEntity, TSelected>
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Gets an item handle by entity ID.
|
|
245
|
+
* Works for both server entities and newly created entities (via add()).
|
|
246
|
+
* Returns an EntityAccessor with direct field access.
|
|
247
|
+
*/
|
|
248
|
+
getById(id: string): EntityAccessor<TEntity, TSelected> {
|
|
249
|
+
return this.getItemHandle(id)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Checks if the list is dirty (items added/removed/moved/connected/disconnected).
|
|
254
|
+
*/
|
|
255
|
+
get isDirty(): boolean {
|
|
256
|
+
const state = this.store.getHasMany(
|
|
257
|
+
this.entityType,
|
|
258
|
+
this.entityId,
|
|
259
|
+
this.fieldName,
|
|
260
|
+
this.alias,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if (!state) return false
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
state.plannedRemovals.size > 0 ||
|
|
267
|
+
state.plannedConnections.size > 0 ||
|
|
268
|
+
state.createdEntities.size > 0 ||
|
|
269
|
+
state.orderedIds !== null
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Maps over items.
|
|
275
|
+
* Implements HasManyRef interface - returns selection-aware entity accessors with direct field access.
|
|
276
|
+
*/
|
|
277
|
+
map<R>(fn: (item: EntityAccessor<TEntity, TSelected>, index: number) => R): R[] {
|
|
278
|
+
return this.items.map((handle, index) => fn(handle, index))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Connects an existing entity to this has-many relation.
|
|
283
|
+
*/
|
|
284
|
+
connect(itemId: string): void {
|
|
285
|
+
this.assertNotDisposed()
|
|
286
|
+
this.store.planHasManyConnection(
|
|
287
|
+
this.entityType,
|
|
288
|
+
this.entityId,
|
|
289
|
+
this.fieldName,
|
|
290
|
+
itemId,
|
|
291
|
+
this.alias,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
// Register parent-child so that changes to the connected entity propagate to the parent
|
|
295
|
+
this.store.registerParentChild(this.entityType, this.entityId, this.itemType, itemId)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Disconnects an entity from this has-many relation.
|
|
300
|
+
*/
|
|
301
|
+
disconnect(itemId: string): void {
|
|
302
|
+
this.assertNotDisposed()
|
|
303
|
+
this.store.planHasManyRemoval(
|
|
304
|
+
this.entityType,
|
|
305
|
+
this.entityId,
|
|
306
|
+
this.fieldName,
|
|
307
|
+
itemId,
|
|
308
|
+
'disconnect',
|
|
309
|
+
this.alias,
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Adds a new item to the list by creating a new entity.
|
|
315
|
+
* Returns the ID of the newly created entity (temp ID).
|
|
316
|
+
* For connecting existing entities, use connect() instead.
|
|
317
|
+
*/
|
|
318
|
+
add(data?: Partial<TEntity>): string {
|
|
319
|
+
this.assertNotDisposed()
|
|
320
|
+
|
|
321
|
+
// Create new entity with temp ID
|
|
322
|
+
const tempId = this.store.createEntity(this.itemType, data as Record<string, unknown>)
|
|
323
|
+
|
|
324
|
+
// Add to has-many relation
|
|
325
|
+
this.store.addToHasMany(
|
|
326
|
+
this.entityType,
|
|
327
|
+
this.entityId,
|
|
328
|
+
this.fieldName,
|
|
329
|
+
tempId,
|
|
330
|
+
this.alias,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
// Register parent-child so that changes to the new entity propagate to the parent
|
|
334
|
+
this.store.registerParentChild(this.entityType, this.entityId, this.itemType, tempId)
|
|
335
|
+
|
|
336
|
+
return tempId
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Removes an item from the list by ID.
|
|
341
|
+
* For newly created entities (via add()), cancels the add operation.
|
|
342
|
+
* For existing server entities, plans a disconnect.
|
|
343
|
+
*/
|
|
344
|
+
remove(itemId: string): void {
|
|
345
|
+
this.assertNotDisposed()
|
|
346
|
+
|
|
347
|
+
this.store.removeFromHasMany(
|
|
348
|
+
this.entityType,
|
|
349
|
+
this.entityId,
|
|
350
|
+
this.fieldName,
|
|
351
|
+
itemId,
|
|
352
|
+
this.alias,
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Moves an item from one position to another within the list.
|
|
358
|
+
* Note: This only affects client-side order. For persistent ordering,
|
|
359
|
+
* use an 'order' field on the entity.
|
|
360
|
+
*/
|
|
361
|
+
move(fromIndex: number, toIndex: number): void {
|
|
362
|
+
this.assertNotDisposed()
|
|
363
|
+
|
|
364
|
+
this.store.moveInHasMany(
|
|
365
|
+
this.entityType,
|
|
366
|
+
this.entityId,
|
|
367
|
+
this.fieldName,
|
|
368
|
+
fromIndex,
|
|
369
|
+
toIndex,
|
|
370
|
+
this.alias,
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Resets the has-many relation to server state.
|
|
376
|
+
* Clears all planned connections and removals.
|
|
377
|
+
*/
|
|
378
|
+
reset(): void {
|
|
379
|
+
this.assertNotDisposed()
|
|
380
|
+
this.store.resetHasMany(
|
|
381
|
+
this.entityType,
|
|
382
|
+
this.entityId,
|
|
383
|
+
this.fieldName,
|
|
384
|
+
this.alias,
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Disposes the handle.
|
|
390
|
+
*/
|
|
391
|
+
override dispose(): void {
|
|
392
|
+
super.dispose()
|
|
393
|
+
|
|
394
|
+
for (const handle of this.itemHandleCache.values()) {
|
|
395
|
+
handle.dispose()
|
|
396
|
+
}
|
|
397
|
+
this.itemHandleCache.clear()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Type brand - ensures HasManyRef<Author> is not assignable to HasManyRef<Tag>.
|
|
402
|
+
* This is a phantom property that only exists in the type system.
|
|
403
|
+
*/
|
|
404
|
+
get __entityType(): TEntity {
|
|
405
|
+
return undefined as unknown as TEntity
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Gets the list of errors on this relation.
|
|
410
|
+
*/
|
|
411
|
+
get errors(): readonly FieldError[] {
|
|
412
|
+
return this.store.getRelationErrors(this.entityType, this.entityId, this.fieldName)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Checks if this relation has any errors.
|
|
417
|
+
*/
|
|
418
|
+
get hasError(): boolean {
|
|
419
|
+
return this.errors.length > 0
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Adds a client-side error to this relation.
|
|
424
|
+
*/
|
|
425
|
+
addError(error: ErrorInput): void {
|
|
426
|
+
this.assertNotDisposed()
|
|
427
|
+
this.dispatcher.dispatch(
|
|
428
|
+
addRelationError(this.entityType, this.entityId, this.fieldName, createClientError(error)),
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Clears all errors from this relation.
|
|
434
|
+
*/
|
|
435
|
+
clearErrors(): void {
|
|
436
|
+
this.assertNotDisposed()
|
|
437
|
+
this.dispatcher.dispatch(
|
|
438
|
+
clearRelationErrors(this.entityType, this.entityId, this.fieldName),
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ==================== Event Subscriptions ====================
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Subscribe to item connected events.
|
|
446
|
+
*/
|
|
447
|
+
onItemConnected(listener: EventListener<HasManyConnectedEvent>): Unsubscribe {
|
|
448
|
+
const emitter = this.dispatcher.getEventEmitter()
|
|
449
|
+
return emitter.onField(
|
|
450
|
+
'hasMany:connected',
|
|
451
|
+
this.entityType,
|
|
452
|
+
this.entityId,
|
|
453
|
+
this.fieldName,
|
|
454
|
+
listener,
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Subscribe to item disconnected events.
|
|
460
|
+
*/
|
|
461
|
+
onItemDisconnected(listener: EventListener<HasManyDisconnectedEvent>): Unsubscribe {
|
|
462
|
+
const emitter = this.dispatcher.getEventEmitter()
|
|
463
|
+
return emitter.onField(
|
|
464
|
+
'hasMany:disconnected',
|
|
465
|
+
this.entityType,
|
|
466
|
+
this.entityId,
|
|
467
|
+
this.fieldName,
|
|
468
|
+
listener,
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Intercept item connection (can cancel).
|
|
474
|
+
*/
|
|
475
|
+
interceptItemConnecting(interceptor: Interceptor<HasManyConnectingEvent>): Unsubscribe {
|
|
476
|
+
const emitter = this.dispatcher.getEventEmitter()
|
|
477
|
+
return emitter.interceptField(
|
|
478
|
+
'hasMany:connecting',
|
|
479
|
+
this.entityType,
|
|
480
|
+
this.entityId,
|
|
481
|
+
this.fieldName,
|
|
482
|
+
interceptor,
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Intercept item disconnection (can cancel).
|
|
488
|
+
*/
|
|
489
|
+
interceptItemDisconnecting(interceptor: Interceptor<HasManyDisconnectingEvent>): Unsubscribe {
|
|
490
|
+
const emitter = this.dispatcher.getEventEmitter()
|
|
491
|
+
return emitter.interceptField(
|
|
492
|
+
'hasMany:disconnecting',
|
|
493
|
+
this.entityType,
|
|
494
|
+
this.entityId,
|
|
495
|
+
this.fieldName,
|
|
496
|
+
interceptor,
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
}
|