@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.
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './gffReservedKeys'
2
+ export * from './gff3ToAnnotationFeature'
@@ -45,8 +45,8 @@ export class GetFeaturesOperation extends Operation {
45
45
  return backend.featureModel
46
46
  .find({
47
47
  refSeq: this.refSeq,
48
- start: { $lte: this.end },
49
- end: { $gte: this.start },
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.start !== null &&
76
- feature.end !== null &&
77
- childFeature.start !== null &&
78
- childFeature.end !== null &&
79
- (childFeature.end > feature.end || childFeature.start < feature.start)
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
@@ -5,3 +5,4 @@ export * from './Common'
5
5
  export * from './Checks'
6
6
  export * from './util'
7
7
  export * from './Messages'
8
+ export * from './GFF3'
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.discontinuousLocations?.length
11
- ? feature.discontinuousLocations
12
- : [{ start: feature.start, end: feature.end, phase: feature.phase }]
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: feature.score ?? null,
80
+ score: null,
81
+ // score: feature.score ?? null,
80
82
  strand: feature.strand ? (feature.strand === 1 ? '+' : '-') : null,
81
- phase:
82
- location.phase === 0
83
- ? '0'
84
- : location.phase === 1
85
- ? '1'
86
- : location.phase === 2
87
- ? '2'
88
- : null,
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
- }