@apollo-annotation/shared 0.1.18 → 0.1.19
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/package.json +13 -7
- package/src/Changes/AddAssemblyAndFeaturesFromFileChange.ts +7 -4
- package/src/Changes/AddAssemblyFromFileChange.ts +3 -2
- package/src/Changes/AddFeatureChange.ts +2 -3
- package/src/Changes/AddFeaturesFromFileChange.ts +8 -3
- package/src/Changes/AddRefSeqAliasesChange.ts +88 -0
- package/src/Changes/DeleteAssemblyChange.ts +1 -1
- package/src/Changes/DeleteFeatureChange.ts +1 -1
- package/src/Changes/FeatureAttributeChange.ts +1 -1
- package/src/Changes/FromFileBaseChange.ts +221 -0
- package/src/Changes/ImportJBrowseConfigChange.ts +116 -0
- package/src/Changes/LocationEndChange.ts +25 -18
- package/src/Changes/LocationStartChange.ts +25 -18
- package/src/Changes/TypeChange.ts +1 -1
- package/src/Changes/index.ts +6 -6
- package/src/Checks/CDSCheck.ts +251 -249
- package/src/Common/jwtPayload.ts +1 -1
- package/src/GFF3/gff3ToAnnotationFeature.test.ts +49 -0
- package/src/GFF3/gff3ToAnnotationFeature.ts +257 -0
- package/src/GFF3/gffReservedKeys.ts +61 -0
- package/src/GFF3/index.ts +2 -0
- package/src/Operations/GetFeaturesOperation.ts +2 -2
- package/src/Validations/ParentChildValidation.ts +5 -5
- package/src/index.ts +1 -0
- package/src/util.ts +15 -12
- package/tsconfig.json +4 -4
- package/src/Changes/DiscontinuousLocationEndChange.ts +0 -182
- package/src/Changes/DiscontinuousLocationStartChange.ts +0 -182
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
|
|
2
|
+
import { GFF3Feature, GFF3FeatureLineWithRefs } from '@gmod/gff'
|
|
3
|
+
import { doesIntersect2 } from '@jbrowse/core/util'
|
|
4
|
+
import ObjectID from 'bson-objectid'
|
|
5
|
+
|
|
6
|
+
import { gffToInternal, isGFFReservedAttribute } from './gffReservedKeys'
|
|
7
|
+
|
|
8
|
+
export function gff3ToAnnotationFeature(
|
|
9
|
+
gff3Feature: GFF3Feature,
|
|
10
|
+
refSeq?: string,
|
|
11
|
+
featureIds?: string[],
|
|
12
|
+
): AnnotationFeatureSnapshot {
|
|
13
|
+
const [firstFeature] = gff3Feature
|
|
14
|
+
const { end, seq_id: refName, start, strand, type } = firstFeature
|
|
15
|
+
if (!refName) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`feature does not have seq_id: ${JSON.stringify(firstFeature)}`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
if (!type) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`feature does not have type: ${JSON.stringify(firstFeature)}`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
if (start === null) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`feature does not have start: ${JSON.stringify(firstFeature)}`,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
if (end === null) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`feature does not have end: ${JSON.stringify(firstFeature)}`,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [min, max] = getFeatureMinMax(gff3Feature)
|
|
37
|
+
|
|
38
|
+
const convertedChildren = convertChildren(gff3Feature, refSeq, featureIds)
|
|
39
|
+
|
|
40
|
+
const convertedAttributes = convertFeatureAttributes(gff3Feature)
|
|
41
|
+
|
|
42
|
+
const feature: AnnotationFeatureSnapshot = {
|
|
43
|
+
_id: new ObjectID().toHexString(),
|
|
44
|
+
refSeq: refSeq ?? refName,
|
|
45
|
+
type,
|
|
46
|
+
min,
|
|
47
|
+
max,
|
|
48
|
+
}
|
|
49
|
+
if (strand) {
|
|
50
|
+
if (strand === '+') {
|
|
51
|
+
feature.strand = 1
|
|
52
|
+
} else if (strand === '-') {
|
|
53
|
+
feature.strand = -1
|
|
54
|
+
} else {
|
|
55
|
+
throw new Error(`Unknown strand: "${strand}"`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (convertedChildren) {
|
|
59
|
+
feature.children = convertedChildren
|
|
60
|
+
}
|
|
61
|
+
if (convertedAttributes) {
|
|
62
|
+
feature.attributes = convertedAttributes
|
|
63
|
+
}
|
|
64
|
+
if (featureIds) {
|
|
65
|
+
featureIds.push(feature._id)
|
|
66
|
+
}
|
|
67
|
+
return feature
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getFeatureMinMax(gff3Feature: GFF3Feature): [number, number] {
|
|
71
|
+
if (gff3Feature.length > 1 && !gff3Feature.every((f) => f.type === 'CDS')) {
|
|
72
|
+
throw new Error('GFF3 features has multiple locations but is not a CDS')
|
|
73
|
+
}
|
|
74
|
+
const mins = gff3Feature.map((f) => f.start).filter((m) => m !== null)
|
|
75
|
+
const maxes = gff3Feature.map((f) => f.end).filter((m) => m !== null)
|
|
76
|
+
const min = Math.min(...mins)
|
|
77
|
+
const max = Math.max(...maxes)
|
|
78
|
+
return [min - 1, max]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function convertFeatureAttributes(
|
|
82
|
+
gff3Feature: GFF3Feature,
|
|
83
|
+
): Record<string, string[]> | undefined {
|
|
84
|
+
const convertedAttributes: Record<string, string[]> = {}
|
|
85
|
+
const scores = gff3Feature
|
|
86
|
+
.map((f) => f.score)
|
|
87
|
+
.filter((score) => score !== null)
|
|
88
|
+
const sources = gff3Feature
|
|
89
|
+
.map((f) => f.source)
|
|
90
|
+
.filter((source) => source !== null)
|
|
91
|
+
const attributesCollections = gff3Feature
|
|
92
|
+
.map((f) => f.attributes)
|
|
93
|
+
.filter((attributes) => attributes !== null)
|
|
94
|
+
if (scores.length > 0) {
|
|
95
|
+
let [score] = scores
|
|
96
|
+
if (scores.length > 1) {
|
|
97
|
+
const scoresSum = scores.reduce(
|
|
98
|
+
(accumulator, currentValue) => accumulator + currentValue,
|
|
99
|
+
0,
|
|
100
|
+
)
|
|
101
|
+
// Average score
|
|
102
|
+
score = scoresSum / scores.length
|
|
103
|
+
}
|
|
104
|
+
convertedAttributes.gff_score = [String(score)]
|
|
105
|
+
}
|
|
106
|
+
if (sources.length > 0) {
|
|
107
|
+
let [source] = sources
|
|
108
|
+
if (sources.length > 1) {
|
|
109
|
+
const sourceSet = new Set(...sources)
|
|
110
|
+
source = [...sourceSet].join(',')
|
|
111
|
+
}
|
|
112
|
+
convertedAttributes.gff_source = [source]
|
|
113
|
+
}
|
|
114
|
+
if (attributesCollections.length > 0) {
|
|
115
|
+
const newAttributes: Record<string, string[] | undefined> = {}
|
|
116
|
+
for (const attributesCollection of attributesCollections) {
|
|
117
|
+
for (const [key, val] of Object.entries(attributesCollection)) {
|
|
118
|
+
if (!val || key === 'Parent') {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
const newKey = isGFFReservedAttribute(key) ? gffToInternal[key] : key
|
|
122
|
+
const existingVal = newAttributes[newKey]
|
|
123
|
+
if (existingVal) {
|
|
124
|
+
const valSet = new Set(...existingVal, ...val)
|
|
125
|
+
convertedAttributes[newKey] = [...valSet]
|
|
126
|
+
} else {
|
|
127
|
+
convertedAttributes[newKey] = val
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (Object.keys(convertedAttributes).length > 0) {
|
|
133
|
+
return convertedAttributes
|
|
134
|
+
}
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function convertChildren(
|
|
139
|
+
gff3Feature: GFF3Feature,
|
|
140
|
+
refSeq?: string,
|
|
141
|
+
featureIds?: string[],
|
|
142
|
+
): Record<string, AnnotationFeatureSnapshot> | undefined {
|
|
143
|
+
const convertedChildren: Record<string, AnnotationFeatureSnapshot> = {}
|
|
144
|
+
const locationsWithChildren = gff3Feature.filter(
|
|
145
|
+
(feature) => feature.child_features.length > 0,
|
|
146
|
+
)
|
|
147
|
+
if (locationsWithChildren.length > 1) {
|
|
148
|
+
throw new Error('Features with multiple locations may not have children')
|
|
149
|
+
}
|
|
150
|
+
if (locationsWithChildren.length === 0) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
const [firstFeature] = locationsWithChildren
|
|
154
|
+
const { child_features: childFeatures } = firstFeature
|
|
155
|
+
|
|
156
|
+
const cdsFeatures: GFF3Feature[] = []
|
|
157
|
+
for (const childFeature of childFeatures) {
|
|
158
|
+
const [firstChildFeatureLocation] = childFeature
|
|
159
|
+
if (
|
|
160
|
+
firstChildFeatureLocation.type === 'three_prime_UTR' ||
|
|
161
|
+
firstChildFeatureLocation.type === 'five_prime_UTR'
|
|
162
|
+
) {
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
if (firstChildFeatureLocation.type === 'CDS') {
|
|
166
|
+
cdsFeatures.push(childFeature)
|
|
167
|
+
} else {
|
|
168
|
+
const child = gff3ToAnnotationFeature(childFeature, refSeq, featureIds)
|
|
169
|
+
convertedChildren[child._id] = child
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const processedCDS =
|
|
173
|
+
cdsFeatures.length > 0 ? processCDS(cdsFeatures, refSeq, featureIds) : []
|
|
174
|
+
for (const cds of processedCDS) {
|
|
175
|
+
convertedChildren[cds._id] = cds
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (Object.keys(convertedChildren).length > 0) {
|
|
179
|
+
return convertedChildren
|
|
180
|
+
}
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* If a GFF3 file has CDS features that either (1) don't have an ID or (2) have
|
|
186
|
+
* different IDs for each CDS, we have to do a bit of guessing about how they
|
|
187
|
+
* should be represented in our internal structure
|
|
188
|
+
* @param cdsFeatures -
|
|
189
|
+
*/
|
|
190
|
+
function processCDS(
|
|
191
|
+
cdsFeatures: GFF3Feature[],
|
|
192
|
+
refSeq?: string,
|
|
193
|
+
featureIds?: string[],
|
|
194
|
+
): AnnotationFeatureSnapshot[] {
|
|
195
|
+
const locationCounts = cdsFeatures.map((cds) => cds.length)
|
|
196
|
+
// If any CDS have multiple locations, assume it really is multiple CDS
|
|
197
|
+
// (e.g. the mRNA has multiple alternative translational start sites)
|
|
198
|
+
// and process normally.
|
|
199
|
+
if (locationCounts.some((count) => count > 1)) {
|
|
200
|
+
return cdsFeatures.map((cds) =>
|
|
201
|
+
gff3ToAnnotationFeature(cds, refSeq, featureIds),
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
// If all CDS have a single location, we guess that this GFF3 represented CDS
|
|
205
|
+
// as multiple features instead of a single feature with multiple locations.
|
|
206
|
+
// To figure out if it's actually representing one vs. multiple CDS features,
|
|
207
|
+
// first check to see if any of the CDS overlap
|
|
208
|
+
const sortedCDSLocations = cdsFeatures
|
|
209
|
+
.map((cds) => cds[0])
|
|
210
|
+
.filter((cds) => cds.start !== null && cds.end !== null)
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
212
|
+
.sort((cdsA, cdsB) => cdsA.start! - cdsB.start!)
|
|
213
|
+
const overlapping = sortedCDSLocations.some((loc, idx) => {
|
|
214
|
+
const nextLoc = sortedCDSLocations.at(idx + 1)
|
|
215
|
+
if (!nextLoc) {
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
219
|
+
return doesIntersect2(loc.start!, loc.end!, nextLoc.start!, nextLoc.end!)
|
|
220
|
+
})
|
|
221
|
+
// If no overlaps, assume it's a single CDS feature
|
|
222
|
+
if (!overlapping) {
|
|
223
|
+
return [gff3ToAnnotationFeature(sortedCDSLocations, refSeq, featureIds)]
|
|
224
|
+
}
|
|
225
|
+
// Some CDS locations overlap, the best we can do is use the original order to
|
|
226
|
+
// guess how to group the locations into features
|
|
227
|
+
const cdsLocations = cdsFeatures.map((cds) => cds[0])
|
|
228
|
+
const groupedLocations: GFF3FeatureLineWithRefs[][] = []
|
|
229
|
+
for (const location of cdsLocations) {
|
|
230
|
+
const lastGroup = groupedLocations.at(-1)
|
|
231
|
+
if (!lastGroup) {
|
|
232
|
+
groupedLocations.push([location])
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
const lastGroupLastLocation = lastGroup.at(-1)
|
|
236
|
+
if (!lastGroupLastLocation) {
|
|
237
|
+
throw new Error('Got group with no locations')
|
|
238
|
+
}
|
|
239
|
+
if (
|
|
240
|
+
doesIntersect2(
|
|
241
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
242
|
+
lastGroupLastLocation.start!,
|
|
243
|
+
lastGroupLastLocation.end!,
|
|
244
|
+
location.start!,
|
|
245
|
+
location.end!,
|
|
246
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
247
|
+
)
|
|
248
|
+
) {
|
|
249
|
+
groupedLocations.push([location])
|
|
250
|
+
} else {
|
|
251
|
+
lastGroup.push(location)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return groupedLocations.map((group) =>
|
|
255
|
+
gff3ToAnnotationFeature(group, refSeq, featureIds),
|
|
256
|
+
)
|
|
257
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type GFFReservedAttribute =
|
|
2
|
+
| 'ID'
|
|
3
|
+
| 'Name'
|
|
4
|
+
| 'Alias'
|
|
5
|
+
| 'Parent'
|
|
6
|
+
| 'Target'
|
|
7
|
+
| 'Gap'
|
|
8
|
+
| 'Derives_from'
|
|
9
|
+
| 'Note'
|
|
10
|
+
| 'Dbxref'
|
|
11
|
+
| 'Ontology_term'
|
|
12
|
+
| 'Is_circular'
|
|
13
|
+
|
|
14
|
+
export type GFFInternalAttribute =
|
|
15
|
+
| 'gff_id'
|
|
16
|
+
| 'gff_name'
|
|
17
|
+
| 'gff_alias'
|
|
18
|
+
| 'gff_parent'
|
|
19
|
+
| 'gff_target'
|
|
20
|
+
| 'gff_gap'
|
|
21
|
+
| 'gff_derives_from'
|
|
22
|
+
| 'gff_note'
|
|
23
|
+
| 'gff_dbxref'
|
|
24
|
+
| 'gff_ontology_term'
|
|
25
|
+
| 'gff_is_circular'
|
|
26
|
+
|
|
27
|
+
export const gffToInternal: Record<GFFReservedAttribute, GFFInternalAttribute> =
|
|
28
|
+
{
|
|
29
|
+
ID: 'gff_id',
|
|
30
|
+
Name: 'gff_name',
|
|
31
|
+
Alias: 'gff_alias',
|
|
32
|
+
Parent: 'gff_parent',
|
|
33
|
+
Target: 'gff_target',
|
|
34
|
+
Gap: 'gff_gap',
|
|
35
|
+
Derives_from: 'gff_derives_from',
|
|
36
|
+
Note: 'gff_note',
|
|
37
|
+
Dbxref: 'gff_dbxref',
|
|
38
|
+
Ontology_term: 'gff_ontology_term',
|
|
39
|
+
Is_circular: 'gff_is_circular',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isGFFReservedAttribute(
|
|
43
|
+
attribute: string,
|
|
44
|
+
): attribute is GFFReservedAttribute {
|
|
45
|
+
return attribute in gffToInternal
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const internalToGFF: Record<GFFInternalAttribute, GFFReservedAttribute> =
|
|
49
|
+
{
|
|
50
|
+
gff_id: 'ID',
|
|
51
|
+
gff_name: 'Name',
|
|
52
|
+
gff_alias: 'Alias',
|
|
53
|
+
gff_parent: 'Parent',
|
|
54
|
+
gff_target: 'Target',
|
|
55
|
+
gff_gap: 'Gap',
|
|
56
|
+
gff_derives_from: 'Derives_from',
|
|
57
|
+
gff_note: 'Note',
|
|
58
|
+
gff_dbxref: 'Dbxref',
|
|
59
|
+
gff_ontology_term: 'Ontology_term',
|
|
60
|
+
gff_is_circular: 'Is_circular',
|
|
61
|
+
}
|
|
@@ -45,8 +45,8 @@ export class GetFeaturesOperation extends Operation {
|
|
|
45
45
|
return backend.featureModel
|
|
46
46
|
.find({
|
|
47
47
|
refSeq: this.refSeq,
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
min: { $lte: this.end },
|
|
49
|
+
max: { $gte: this.start },
|
|
50
50
|
status: 0,
|
|
51
51
|
})
|
|
52
52
|
.exec()
|
|
@@ -72,11 +72,11 @@ export class ParentChildValidation extends Validation {
|
|
|
72
72
|
}
|
|
73
73
|
for (const [, childFeature] of feature.children || new Map()) {
|
|
74
74
|
if (
|
|
75
|
-
feature.
|
|
76
|
-
feature.
|
|
77
|
-
childFeature.
|
|
78
|
-
childFeature.
|
|
79
|
-
(childFeature.
|
|
75
|
+
feature.min !== null &&
|
|
76
|
+
feature.max !== null &&
|
|
77
|
+
childFeature.min !== null &&
|
|
78
|
+
childFeature.max !== null &&
|
|
79
|
+
(childFeature.max > feature.max || childFeature.min < feature.min)
|
|
80
80
|
) {
|
|
81
81
|
throw new Error(
|
|
82
82
|
`Feature "${childFeature._id}" exceeds the bounds of its parent, "${feature._id}"`,
|
package/src/index.ts
CHANGED
package/src/util.ts
CHANGED
|
@@ -7,9 +7,10 @@ export function makeGFF3Feature(
|
|
|
7
7
|
parentId?: string,
|
|
8
8
|
refSeqNames?: Record<string, string | undefined>,
|
|
9
9
|
): GFF3Feature {
|
|
10
|
-
const locations = feature.
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const locations = [{ start: feature.min, end: feature.max }]
|
|
11
|
+
// const locations = feature.discontinuousLocations?.length
|
|
12
|
+
// ? feature.discontinuousLocations
|
|
13
|
+
// : [{ start: feature.start, end: feature.end, phase: feature.phase }]
|
|
13
14
|
const attributes: Record<string, string[] | undefined> = JSON.parse(
|
|
14
15
|
JSON.stringify(feature.attributes),
|
|
15
16
|
)
|
|
@@ -76,16 +77,18 @@ export function makeGFF3Feature(
|
|
|
76
77
|
seq_id: refSeqNames ? refSeqNames[feature.refSeq] ?? null : feature.refSeq,
|
|
77
78
|
source,
|
|
78
79
|
type: feature.type,
|
|
79
|
-
score:
|
|
80
|
+
score: null,
|
|
81
|
+
// score: feature.score ?? null,
|
|
80
82
|
strand: feature.strand ? (feature.strand === 1 ? '+' : '-') : null,
|
|
81
|
-
phase:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
phase: null,
|
|
84
|
+
// phase:
|
|
85
|
+
// location.phase === 0
|
|
86
|
+
// ? '0'
|
|
87
|
+
// : location.phase === 1
|
|
88
|
+
// ? '1'
|
|
89
|
+
// : location.phase === 2
|
|
90
|
+
// ? '2'
|
|
91
|
+
// : null,
|
|
89
92
|
attributes: Object.keys(attributes).length > 0 ? attributes : null,
|
|
90
93
|
derived_features: [],
|
|
91
94
|
child_features: feature.children
|
package/tsconfig.json
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"include": ["./src"],
|
|
14
14
|
"references": [
|
|
15
|
-
{ "path": "../apollo-mst"},
|
|
16
|
-
{ "path": "../apollo-schemas"},
|
|
17
|
-
{ "path": "../apollo-common"},
|
|
18
|
-
]
|
|
15
|
+
{ "path": "../apollo-mst" },
|
|
16
|
+
{ "path": "../apollo-schemas" },
|
|
17
|
+
{ "path": "../apollo-common" },
|
|
18
|
+
],
|
|
19
19
|
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
2
|
-
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
3
|
-
/* eslint-disable @typescript-eslint/require-await */
|
|
4
|
-
import {
|
|
5
|
-
ChangeOptions,
|
|
6
|
-
ClientDataStore,
|
|
7
|
-
FeatureChange,
|
|
8
|
-
LocalGFF3DataStore,
|
|
9
|
-
SerializedFeatureChange,
|
|
10
|
-
ServerDataStore,
|
|
11
|
-
} from '@apollo-annotation/common'
|
|
12
|
-
|
|
13
|
-
interface SerializedDiscontinuousLocationEndChangeBase
|
|
14
|
-
extends SerializedFeatureChange {
|
|
15
|
-
typeName: 'DiscontinuousLocationEndChange'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface DiscontinuousLocationEndChangeDetails {
|
|
19
|
-
featureId: string
|
|
20
|
-
oldEnd: number
|
|
21
|
-
newEnd: number
|
|
22
|
-
index: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface SerializedDiscontinuousLocationEndChangeSingle
|
|
26
|
-
extends SerializedDiscontinuousLocationEndChangeBase,
|
|
27
|
-
DiscontinuousLocationEndChangeDetails {}
|
|
28
|
-
|
|
29
|
-
interface SerializedDiscontinuousLocationEndChangeMultiple
|
|
30
|
-
extends SerializedDiscontinuousLocationEndChangeBase {
|
|
31
|
-
changes: DiscontinuousLocationEndChangeDetails[]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type SerializedDiscontinuousLocationEndChange =
|
|
35
|
-
| SerializedDiscontinuousLocationEndChangeSingle
|
|
36
|
-
| SerializedDiscontinuousLocationEndChangeMultiple
|
|
37
|
-
|
|
38
|
-
export class DiscontinuousLocationEndChange extends FeatureChange {
|
|
39
|
-
typeName = 'DiscontinuousLocationEndChange' as const
|
|
40
|
-
changes: DiscontinuousLocationEndChangeDetails[]
|
|
41
|
-
|
|
42
|
-
constructor(
|
|
43
|
-
json: SerializedDiscontinuousLocationEndChange,
|
|
44
|
-
options?: ChangeOptions,
|
|
45
|
-
) {
|
|
46
|
-
super(json, options)
|
|
47
|
-
this.changes = 'changes' in json ? json.changes : [json]
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
toJSON(): SerializedDiscontinuousLocationEndChange {
|
|
51
|
-
const { assembly, changedIds, changes, typeName } = this
|
|
52
|
-
if (changes.length === 1) {
|
|
53
|
-
const [{ featureId, index, newEnd, oldEnd }] = changes
|
|
54
|
-
return {
|
|
55
|
-
typeName,
|
|
56
|
-
changedIds,
|
|
57
|
-
assembly,
|
|
58
|
-
featureId,
|
|
59
|
-
oldEnd,
|
|
60
|
-
newEnd,
|
|
61
|
-
index,
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return { typeName, changedIds, assembly, changes }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async executeOnServer(backend: ServerDataStore) {
|
|
68
|
-
const { featureModel, session } = backend
|
|
69
|
-
const { changes, logger } = this
|
|
70
|
-
for (const change of changes) {
|
|
71
|
-
const { featureId, index, newEnd, oldEnd: expectedOldEnd } = change
|
|
72
|
-
const topLevelFeature = await featureModel
|
|
73
|
-
.findOne({ allIds: featureId })
|
|
74
|
-
.session(session)
|
|
75
|
-
.exec()
|
|
76
|
-
|
|
77
|
-
if (!topLevelFeature) {
|
|
78
|
-
const errMsg = `ERROR: The following featureId was not found in database ='${featureId}'`
|
|
79
|
-
logger.error(errMsg)
|
|
80
|
-
throw new Error(errMsg)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const feature = this.getFeatureFromId(topLevelFeature, featureId)
|
|
84
|
-
if (!feature) {
|
|
85
|
-
const errMsg = 'ERROR when searching feature by featureId'
|
|
86
|
-
logger.error(errMsg)
|
|
87
|
-
throw new Error(errMsg)
|
|
88
|
-
}
|
|
89
|
-
logger.debug?.(`*** Found feature: ${JSON.stringify(feature)}`)
|
|
90
|
-
if (
|
|
91
|
-
!feature.discontinuousLocations ||
|
|
92
|
-
feature.discontinuousLocations.length === 0
|
|
93
|
-
) {
|
|
94
|
-
const errMsg =
|
|
95
|
-
'Must use "LocationEndChange" to change a feature end that does not have discontinuous locations'
|
|
96
|
-
logger.error(errMsg)
|
|
97
|
-
throw new Error(errMsg)
|
|
98
|
-
}
|
|
99
|
-
const oldEnd = feature.discontinuousLocations[index].end
|
|
100
|
-
if (oldEnd !== expectedOldEnd) {
|
|
101
|
-
const errMsg = `Location's current end value ${oldEnd} doesn't match with expected value ${expectedOldEnd}`
|
|
102
|
-
logger.error(errMsg)
|
|
103
|
-
throw new Error(errMsg)
|
|
104
|
-
}
|
|
105
|
-
const { start } = feature.discontinuousLocations[index]
|
|
106
|
-
if (newEnd <= start) {
|
|
107
|
-
const errMsg = `location end (${newEnd}) can't be smaller than location start (${start})`
|
|
108
|
-
logger.error(errMsg)
|
|
109
|
-
throw new Error(errMsg)
|
|
110
|
-
}
|
|
111
|
-
const nextLocation = feature.discontinuousLocations[index + 1]
|
|
112
|
-
if (nextLocation && newEnd >= nextLocation.start) {
|
|
113
|
-
const errMsg = `Location end (${newEnd}) can't be larger than the next location's start (${nextLocation.start})`
|
|
114
|
-
logger.error(errMsg)
|
|
115
|
-
throw new Error(errMsg)
|
|
116
|
-
}
|
|
117
|
-
feature.discontinuousLocations[index].end = newEnd
|
|
118
|
-
if (index === feature.discontinuousLocations.length - 1) {
|
|
119
|
-
feature.end = newEnd
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
if (topLevelFeature._id.equals(feature._id)) {
|
|
124
|
-
topLevelFeature.markModified('discontinuousLocations')
|
|
125
|
-
} else {
|
|
126
|
-
topLevelFeature.markModified('children')
|
|
127
|
-
}
|
|
128
|
-
await topLevelFeature.save()
|
|
129
|
-
} catch (error) {
|
|
130
|
-
logger.debug?.(`*** FAILED: ${error}`)
|
|
131
|
-
throw error
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
|
|
137
|
-
throw new Error('executeOnLocalGFF3 not implemented')
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async executeOnClient(dataStore: ClientDataStore) {
|
|
141
|
-
if (!dataStore) {
|
|
142
|
-
throw new Error('No data store')
|
|
143
|
-
}
|
|
144
|
-
for (const [idx, changedId] of this.changedIds.entries()) {
|
|
145
|
-
const feature = dataStore.getFeature(changedId)
|
|
146
|
-
if (!feature) {
|
|
147
|
-
throw new Error(`Could not find feature with identifier "${changedId}"`)
|
|
148
|
-
}
|
|
149
|
-
const { index, newEnd } = this.changes[idx]
|
|
150
|
-
feature.setCDSDiscontinuousLocationEnd(newEnd, index)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
getInverse() {
|
|
155
|
-
const { assembly, changedIds, changes, logger, typeName } = this
|
|
156
|
-
const inverseChangedIds = [...changedIds].reverse()
|
|
157
|
-
const inverseChanges = [...changes].reverse().map((change) => ({
|
|
158
|
-
featureId: change.featureId,
|
|
159
|
-
oldEnd: change.newEnd,
|
|
160
|
-
newEnd: change.oldEnd,
|
|
161
|
-
index: change.index,
|
|
162
|
-
}))
|
|
163
|
-
return new DiscontinuousLocationEndChange(
|
|
164
|
-
{
|
|
165
|
-
changedIds: inverseChangedIds,
|
|
166
|
-
typeName,
|
|
167
|
-
changes: inverseChanges,
|
|
168
|
-
assembly,
|
|
169
|
-
},
|
|
170
|
-
{ logger },
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function isDiscontinuousLocationEndChange(
|
|
176
|
-
change: unknown,
|
|
177
|
-
): change is DiscontinuousLocationEndChange {
|
|
178
|
-
return (
|
|
179
|
-
(change as DiscontinuousLocationEndChange).typeName ===
|
|
180
|
-
'DiscontinuousLocationEndChange'
|
|
181
|
-
)
|
|
182
|
-
}
|