@contember/bindx 0.1.51 → 0.1.53
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/core/ActionDispatcher.d.ts.map +1 -1
- package/dist/core/EntityLoader.d.ts +7 -0
- package/dist/core/EntityLoader.d.ts.map +1 -1
- package/dist/core/actions.d.ts +1 -2
- package/dist/core/actions.d.ts.map +1 -1
- package/dist/dataview/types.d.ts +6 -6
- package/dist/dataview/types.d.ts.map +1 -1
- package/dist/handles/EntityHandle.d.ts.map +1 -1
- package/dist/handles/HasManyListHandle.d.ts +8 -0
- package/dist/handles/HasManyListHandle.d.ts.map +1 -1
- package/dist/handles/HasOneHandle.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/BatchPersister.d.ts.map +1 -1
- package/dist/store/HasManyStore.d.ts +94 -78
- package/dist/store/HasManyStore.d.ts.map +1 -1
- package/dist/store/RelationStore.d.ts +21 -12
- package/dist/store/RelationStore.d.ts.map +1 -1
- package/dist/store/ResponseOccurrenceIndex.d.ts +65 -0
- package/dist/store/ResponseOccurrenceIndex.d.ts.map +1 -0
- package/dist/store/SnapshotStore.d.ts +53 -15
- package/dist/store/SnapshotStore.d.ts.map +1 -1
- package/dist/store/SubscriptionManager.d.ts +19 -4
- package/dist/store/SubscriptionManager.d.ts.map +1 -1
- package/dist/store/hasManyState.d.ts +164 -0
- package/dist/store/hasManyState.d.ts.map +1 -0
- package/dist/store/relationKey.d.ts +11 -0
- package/dist/store/relationKey.d.ts.map +1 -1
- package/dist/undo/UndoJournal.d.ts.map +1 -1
- package/dist/undo/rekeyJournalEntry.d.ts +4 -4
- package/dist/undo/rekeyJournalEntry.d.ts.map +1 -1
- package/dist/utils/aliasGenerator.d.ts +1 -1
- package/dist/utils/aliasGenerator.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/core/ActionDispatcher.ts +0 -1
- package/src/core/EntityLoader.ts +13 -6
- package/src/core/actions.ts +1 -3
- package/src/dataview/types.ts +6 -6
- package/src/handles/EntityHandle.ts +11 -0
- package/src/handles/HasManyListHandle.ts +15 -17
- package/src/handles/HasOneHandle.ts +4 -2
- package/src/index.ts +6 -0
- package/src/persistence/BatchPersister.ts +15 -11
- package/src/store/HasManyStore.ts +312 -488
- package/src/store/RelationStore.ts +60 -24
- package/src/store/ResponseOccurrenceIndex.ts +224 -0
- package/src/store/SnapshotStore.ts +183 -109
- package/src/store/SubscriptionManager.ts +116 -58
- package/src/store/hasManyState.ts +408 -0
- package/src/store/relationKey.ts +17 -0
- package/src/undo/UndoJournal.ts +3 -1
- package/src/undo/UndoManager.ts +1 -1
- package/src/undo/rekeyJournalEntry.ts +31 -43
- package/src/utils/aliasGenerator.ts +1 -1
|
@@ -5,27 +5,33 @@ import {
|
|
|
5
5
|
type SentHasOneTransition,
|
|
6
6
|
type StoredRelationState,
|
|
7
7
|
} from './HasOneStore.js'
|
|
8
|
+
import { HasManyStore } from './HasManyStore.js'
|
|
8
9
|
import {
|
|
9
|
-
HasManyStore,
|
|
10
|
-
computeDefaultOrderedIds,
|
|
11
10
|
type HasManyAdditionKind,
|
|
11
|
+
type HasManyRelationProjection,
|
|
12
12
|
type HasManyRemovalType,
|
|
13
|
+
type HasManyViewMembership,
|
|
14
|
+
type HasManyViewProjection,
|
|
13
15
|
type SentHasManyDelta,
|
|
14
16
|
type StoredHasManyState,
|
|
15
|
-
} from './
|
|
17
|
+
} from './hasManyState.js'
|
|
16
18
|
|
|
17
19
|
// Re-exported so existing imports from './RelationStore.js' keep resolving.
|
|
18
20
|
export type { StoredRelationState } from './HasOneStore.js'
|
|
19
21
|
export type { SentHasOneTransition } from './HasOneStore.js'
|
|
20
22
|
export type {
|
|
21
23
|
HasManyAdditionKind,
|
|
24
|
+
HasManyRelationProjection,
|
|
22
25
|
HasManyRemovalType,
|
|
26
|
+
HasManyView,
|
|
27
|
+
HasManyViewMembership,
|
|
28
|
+
HasManyViewProjection,
|
|
29
|
+
PlannedHasManyAddition,
|
|
23
30
|
SentHasManyAddition,
|
|
24
31
|
SentHasManyDelta,
|
|
25
32
|
SentHasManyRemoval,
|
|
26
33
|
StoredHasManyState,
|
|
27
|
-
} from './
|
|
28
|
-
export { computeDefaultOrderedIds } from './HasManyStore.js'
|
|
34
|
+
} from './hasManyState.js'
|
|
29
35
|
|
|
30
36
|
export type RelationReconciliationResult = 'applied' | 'conflict'
|
|
31
37
|
|
|
@@ -103,28 +109,58 @@ export class RelationStore implements Rekeyable {
|
|
|
103
109
|
|
|
104
110
|
// ==================== Has-Many Relations ====================
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
declareHasManyViewMembership(
|
|
113
|
+
entityType: string,
|
|
114
|
+
fieldName: string,
|
|
115
|
+
alias: string,
|
|
116
|
+
membership: HasManyViewMembership,
|
|
117
|
+
): void {
|
|
118
|
+
this.hasMany.declareViewMembership(entityType, fieldName, alias, membership)
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
|
|
121
|
+
getOrCreateHasMany(key: string, alias: string, serverIds?: string[]): void {
|
|
122
|
+
this.hasMany.getOrCreateHasMany(key, alias, serverIds)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The raw stored state, views and all. Named apart from the projections above it:
|
|
127
|
+
* {@link SnapshotStore.getHasMany} hands out a relation projection with no views,
|
|
128
|
+
* and the undo / export paths that need the real thing must not get that by mistake.
|
|
129
|
+
*/
|
|
130
|
+
getHasManyState(key: string): StoredHasManyState | undefined {
|
|
111
131
|
return this.hasMany.getHasMany(key)
|
|
112
132
|
}
|
|
113
133
|
|
|
114
|
-
|
|
115
|
-
this.hasMany.
|
|
134
|
+
getHasManyRelation(key: string): HasManyRelationProjection | undefined {
|
|
135
|
+
return this.hasMany.getRelationProjection(key)
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
|
|
119
|
-
this.hasMany.
|
|
138
|
+
getHasManyView(key: string, alias: string): HasManyViewProjection {
|
|
139
|
+
return this.hasMany.getViewProjection(key, alias)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
hasPendingHasManyWrites(key: string): boolean {
|
|
143
|
+
return this.hasMany.hasPendingWrites(key)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
hasExplicitHasManyOrder(key: string, alias: string): boolean {
|
|
147
|
+
return this.hasMany.hasExplicitOrder(key, alias)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
collectHasManyViewServerIds(key: string): ReadonlyMap<string, ReadonlySet<string>> {
|
|
151
|
+
return this.hasMany.collectViewServerIds(key)
|
|
120
152
|
}
|
|
121
153
|
|
|
122
|
-
|
|
123
|
-
this.hasMany.
|
|
154
|
+
setHasManyServerIds(key: string, alias: string, serverIds: string[]): void {
|
|
155
|
+
this.hasMany.setHasManyServerIds(key, alias, serverIds)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
planHasManyRemoval(key: string, itemId: string, type: HasManyRemovalType): void {
|
|
159
|
+
this.hasMany.planHasManyRemoval(key, itemId, type)
|
|
124
160
|
}
|
|
125
161
|
|
|
126
|
-
|
|
127
|
-
this.hasMany.
|
|
162
|
+
planHasManyConnection(key: string, alias: string, itemId: string): void {
|
|
163
|
+
this.hasMany.planHasManyConnection(key, alias, itemId)
|
|
128
164
|
}
|
|
129
165
|
|
|
130
166
|
reconcileSentHasMany(
|
|
@@ -138,24 +174,24 @@ export class RelationStore implements Rekeyable {
|
|
|
138
174
|
this.hasMany.resetHasMany(key)
|
|
139
175
|
}
|
|
140
176
|
|
|
141
|
-
addToHasMany(key: string, itemId: string): void {
|
|
142
|
-
this.hasMany.addToHasMany(key, itemId)
|
|
177
|
+
addToHasMany(key: string, alias: string, itemId: string): void {
|
|
178
|
+
this.hasMany.addToHasMany(key, alias, itemId)
|
|
143
179
|
}
|
|
144
180
|
|
|
145
|
-
connectExistingToHasMany(key: string, itemId: string): void {
|
|
146
|
-
this.hasMany.connectExistingToHasMany(key, itemId)
|
|
181
|
+
connectExistingToHasMany(key: string, alias: string, itemId: string): void {
|
|
182
|
+
this.hasMany.connectExistingToHasMany(key, alias, itemId)
|
|
147
183
|
}
|
|
148
184
|
|
|
149
185
|
removeFromHasMany(key: string, itemId: string, removalType: HasManyRemovalType): boolean {
|
|
150
186
|
return this.hasMany.removeFromHasMany(key, itemId, removalType)
|
|
151
187
|
}
|
|
152
188
|
|
|
153
|
-
moveInHasMany(key: string, fromIndex: number, toIndex: number): void {
|
|
154
|
-
this.hasMany.moveInHasMany(key, fromIndex, toIndex)
|
|
189
|
+
moveInHasMany(key: string, alias: string, fromIndex: number, toIndex: number): void {
|
|
190
|
+
this.hasMany.moveInHasMany(key, alias, fromIndex, toIndex)
|
|
155
191
|
}
|
|
156
192
|
|
|
157
|
-
getHasManyOrderedIds(key: string): string[] {
|
|
158
|
-
return this.hasMany.getHasManyOrderedIds(key)
|
|
193
|
+
getHasManyOrderedIds(key: string, alias: string): string[] {
|
|
194
|
+
return this.hasMany.getHasManyOrderedIds(key, alias)
|
|
159
195
|
}
|
|
160
196
|
|
|
161
197
|
/**
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { SelectionMeta } from '../selection/types.js'
|
|
2
|
+
|
|
3
|
+
/** The schema knowledge the index needs: where a relation points and whether it is a list. */
|
|
4
|
+
export interface RelationTargetResolver {
|
|
5
|
+
getRelationTarget(entityType: string, fieldName: string): string | undefined
|
|
6
|
+
isHasMany(entityType: string, fieldName: string): boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type EntityRecord = Record<string, unknown>
|
|
10
|
+
|
|
11
|
+
interface PendingVisit {
|
|
12
|
+
readonly value: unknown
|
|
13
|
+
readonly entityType: string
|
|
14
|
+
readonly selection: SelectionMeta
|
|
15
|
+
readonly isList: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Unites the occurrences of one entity within a single server response.
|
|
20
|
+
*
|
|
21
|
+
* One query can reach the same entity through several paths, each with its own
|
|
22
|
+
* sub-selection (`article.attachments` next to `article.sections.article.attachments`).
|
|
23
|
+
* Every occurrence is later written into the entity's snapshot, so without this
|
|
24
|
+
* the narrower one could replace fields the wider one carried. Occurrences of
|
|
25
|
+
* one response are equally fresh, so they are merged without any recency rule:
|
|
26
|
+
* each maps to the union of all of them, keyed by entity type and id. A scalar two
|
|
27
|
+
* occurrences disagree on (which one server read should not produce) takes the
|
|
28
|
+
* value of the occurrence visited last. The response is walked breadth first,
|
|
29
|
+
* relations in selection order and list items in list order, so the outcome
|
|
30
|
+
* follows from the response and the selection alone.
|
|
31
|
+
*
|
|
32
|
+
* The union is shallow. A relation value in it is one occurrence's raw value, and
|
|
33
|
+
* the related entities inside it resolve to their own unions when they are
|
|
34
|
+
* written, so relations inside relations unite level by level without rewriting
|
|
35
|
+
* the response.
|
|
36
|
+
*
|
|
37
|
+
* Reads never merge across responses: a later read replaces what an earlier one
|
|
38
|
+
* stored, as before.
|
|
39
|
+
*/
|
|
40
|
+
export class ResponseOccurrenceIndex {
|
|
41
|
+
private readonly unions = new WeakMap<object, Readonly<EntityRecord>>()
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Indexes a response read for `entityType` with `selection`. The type of every
|
|
45
|
+
* nested occurrence comes from the schema through the selection's field names;
|
|
46
|
+
* an occurrence whose type cannot be resolved is left out rather than matched
|
|
47
|
+
* by id alone.
|
|
48
|
+
*
|
|
49
|
+
* Occurrences of one type at one selection node carry the same fields, so
|
|
50
|
+
* their union changes nothing. Only types the selection reaches at two or more
|
|
51
|
+
* nodes are collected, only the relations leading to them are walked, and a
|
|
52
|
+
* selection with none skips the walk entirely (see {@link planOccurrenceWalk}).
|
|
53
|
+
*/
|
|
54
|
+
index(entityType: string, data: unknown, selection: SelectionMeta, schema: RelationTargetResolver): void {
|
|
55
|
+
const plan = planOccurrenceWalk(entityType, selection, schema)
|
|
56
|
+
if (plan.repeatedTypes.size === 0) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
const root: PendingVisit = { value: data, entityType, selection, isList: Array.isArray(data) }
|
|
60
|
+
const occurrencesByType = collectOccurrences(root, plan, schema)
|
|
61
|
+
for (const occurrencesById of occurrencesByType.values()) {
|
|
62
|
+
for (const occurrences of occurrencesById.values()) {
|
|
63
|
+
if (Array.isArray(occurrences)) {
|
|
64
|
+
this.unite(occurrences)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private unite(occurrences: readonly EntityRecord[]): void {
|
|
71
|
+
const union: EntityRecord = {}
|
|
72
|
+
for (const occurrence of occurrences) {
|
|
73
|
+
Object.assign(union, occurrence)
|
|
74
|
+
}
|
|
75
|
+
Object.freeze(union)
|
|
76
|
+
for (const occurrence of occurrences) {
|
|
77
|
+
this.unions.set(occurrence, union)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The union of every occurrence of this entity in its response, or the occurrence itself when it had no sibling. */
|
|
82
|
+
resolve(occurrence: EntityRecord): EntityRecord {
|
|
83
|
+
return this.unions.get(occurrence) ?? occurrence
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Occurrences by entity type and id. A lone occurrence is kept as is; an array only appears once an id repeats. */
|
|
88
|
+
type OccurrencesByType = Map<string, Map<unknown, EntityRecord | EntityRecord[]>>
|
|
89
|
+
|
|
90
|
+
function collectOccurrences(root: PendingVisit, plan: OccurrenceWalkPlan, schema: RelationTargetResolver): OccurrencesByType {
|
|
91
|
+
const occurrencesByType: OccurrencesByType = new Map()
|
|
92
|
+
const pending: PendingVisit[] = [root]
|
|
93
|
+
for (let next = 0; next < pending.length; next++) {
|
|
94
|
+
const visit = pending[next]!
|
|
95
|
+
for (const entity of entitiesOf(visit.value, visit.isList)) {
|
|
96
|
+
if (plan.repeatedTypes.has(visit.entityType)) {
|
|
97
|
+
addOccurrence(occurrencesByType, visit.entityType, entity)
|
|
98
|
+
}
|
|
99
|
+
for (const fieldMeta of visit.selection.fields.values()) {
|
|
100
|
+
if (!fieldMeta.nested || !plan.nodesToWalk.has(fieldMeta.nested) || entity[fieldMeta.alias] == null) continue
|
|
101
|
+
const targetType = schema.getRelationTarget(visit.entityType, fieldMeta.fieldName)
|
|
102
|
+
if (targetType === undefined) continue
|
|
103
|
+
pending.push({
|
|
104
|
+
value: entity[fieldMeta.alias],
|
|
105
|
+
entityType: targetType,
|
|
106
|
+
selection: fieldMeta.nested,
|
|
107
|
+
isList: schema.isHasMany(visit.entityType, fieldMeta.fieldName),
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return occurrencesByType
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** What walking a response needs from its selection. */
|
|
116
|
+
export interface OccurrenceWalkPlan {
|
|
117
|
+
/** Entity types the selection reaches at two or more nodes, such as `article` and `article.sections.article`. Only these are united. */
|
|
118
|
+
readonly repeatedTypes: ReadonlySet<string>
|
|
119
|
+
/** Selection nodes on a path to a repeated type. The walk descends into no other. */
|
|
120
|
+
readonly nodesToWalk: ReadonlySet<SelectionMeta>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Plans the walk of one response. It depends only on the schema, the selection
|
|
125
|
+
* and its root type, and costs one pass over the selection tree, not over the
|
|
126
|
+
* response. It is computed per response rather than cached, because a selection
|
|
127
|
+
* can still grow after its first fetch (`mergeSelections` extends nested
|
|
128
|
+
* selections in place, and a fragment's selection is shared by every place
|
|
129
|
+
* that uses it).
|
|
130
|
+
*/
|
|
131
|
+
export function planOccurrenceWalk(entityType: string, selection: SelectionMeta, schema: RelationTargetResolver): OccurrenceWalkPlan {
|
|
132
|
+
const nodes = listSelectionNodes(entityType, selection, schema)
|
|
133
|
+
const repeatedTypes = findRepeatedTypes(nodes)
|
|
134
|
+
return { repeatedTypes, nodesToWalk: findNodesToWalk(nodes, repeatedTypes) }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface SelectionNode {
|
|
138
|
+
readonly entityType: string
|
|
139
|
+
readonly selection: SelectionMeta
|
|
140
|
+
readonly parent: number | null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Every position in the selection tree, breadth first, each with the index of its parent. */
|
|
144
|
+
function listSelectionNodes(entityType: string, selection: SelectionMeta, schema: RelationTargetResolver): readonly SelectionNode[] {
|
|
145
|
+
const nodes: SelectionNode[] = [{ entityType, selection, parent: null }]
|
|
146
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
147
|
+
const node = nodes[index]!
|
|
148
|
+
for (const fieldMeta of node.selection.fields.values()) {
|
|
149
|
+
if (!fieldMeta.isRelation || !fieldMeta.nested) continue
|
|
150
|
+
const targetType = schema.getRelationTarget(node.entityType, fieldMeta.fieldName)
|
|
151
|
+
if (targetType !== undefined) {
|
|
152
|
+
nodes.push({ entityType: targetType, selection: fieldMeta.nested, parent: index })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return nodes
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function findRepeatedTypes(nodes: readonly SelectionNode[]): ReadonlySet<string> {
|
|
160
|
+
const seenTypes = new Set<string>()
|
|
161
|
+
const repeatedTypes = new Set<string>()
|
|
162
|
+
for (const node of nodes) {
|
|
163
|
+
if (seenTypes.has(node.entityType)) {
|
|
164
|
+
repeatedTypes.add(node.entityType)
|
|
165
|
+
}
|
|
166
|
+
seenTypes.add(node.entityType)
|
|
167
|
+
}
|
|
168
|
+
return repeatedTypes
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The selections on a path from the root to a repeated type. The climb tracks
|
|
173
|
+
* tree positions, not selection objects: a reused fragment is one selection
|
|
174
|
+
* object at several positions, and each position has ancestors of its own.
|
|
175
|
+
*/
|
|
176
|
+
function findNodesToWalk(nodes: readonly SelectionNode[], repeatedTypes: ReadonlySet<string>): ReadonlySet<SelectionMeta> {
|
|
177
|
+
const nodesToWalk = new Set<SelectionMeta>()
|
|
178
|
+
const climbed = new Set<number>()
|
|
179
|
+
nodes.forEach((node, index) => {
|
|
180
|
+
if (!repeatedTypes.has(node.entityType)) return
|
|
181
|
+
for (let position: number | null = index; position !== null && !climbed.has(position); position = nodes[position]!.parent) {
|
|
182
|
+
climbed.add(position)
|
|
183
|
+
nodesToWalk.add(nodes[position]!.selection)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
return nodesToWalk
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function addOccurrence(occurrencesByType: OccurrencesByType, entityType: string, entity: EntityRecord): void {
|
|
190
|
+
const id = entity['id']
|
|
191
|
+
if (typeof id !== 'string' && typeof id !== 'number') return
|
|
192
|
+
let occurrencesById = occurrencesByType.get(entityType)
|
|
193
|
+
if (!occurrencesById) {
|
|
194
|
+
occurrencesById = new Map()
|
|
195
|
+
occurrencesByType.set(entityType, occurrencesById)
|
|
196
|
+
}
|
|
197
|
+
const known = occurrencesById.get(id)
|
|
198
|
+
if (known === undefined) {
|
|
199
|
+
occurrencesById.set(id, entity)
|
|
200
|
+
} else if (Array.isArray(known)) {
|
|
201
|
+
known.push(entity)
|
|
202
|
+
} else if (known !== entity) {
|
|
203
|
+
occurrencesById.set(id, [known, entity])
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The entity records a relation value holds: a has-one object, or a has-many array or connection (`{ edges: [{ node }] }`). */
|
|
208
|
+
function entitiesOf(value: unknown, isList: boolean): readonly EntityRecord[] {
|
|
209
|
+
if (Array.isArray(value)) {
|
|
210
|
+
return value.filter(isRecord)
|
|
211
|
+
}
|
|
212
|
+
if (!isRecord(value)) {
|
|
213
|
+
return []
|
|
214
|
+
}
|
|
215
|
+
if (!isList) {
|
|
216
|
+
return [value]
|
|
217
|
+
}
|
|
218
|
+
const edges = value['edges']
|
|
219
|
+
return Array.isArray(edges) ? edges.map(edge => (isRecord(edge) ? edge['node'] : undefined)).filter(isRecord) : []
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isRecord(value: unknown): value is EntityRecord {
|
|
223
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
224
|
+
}
|