@apollo-annotation/shared 0.1.18 → 0.1.20

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.
Files changed (157) hide show
  1. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.d.ts +33 -0
  2. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.js +83 -0
  3. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.js.map +1 -0
  4. package/dist/Changes/AddAssemblyFromExternalChange.d.ts +34 -0
  5. package/dist/Changes/AddAssemblyFromExternalChange.js +97 -0
  6. package/dist/Changes/AddAssemblyFromExternalChange.js.map +1 -0
  7. package/dist/Changes/AddAssemblyFromFileChange.d.ts +43 -0
  8. package/dist/Changes/AddAssemblyFromFileChange.js +140 -0
  9. package/dist/Changes/AddAssemblyFromFileChange.js.map +1 -0
  10. package/dist/Changes/AddFeatureChange.d.ts +35 -0
  11. package/dist/Changes/AddFeatureChange.js +169 -0
  12. package/dist/Changes/AddFeatureChange.js.map +1 -0
  13. package/dist/Changes/AddFeaturesFromFileChange.d.ts +32 -0
  14. package/dist/Changes/AddFeaturesFromFileChange.js +76 -0
  15. package/dist/Changes/AddFeaturesFromFileChange.js.map +1 -0
  16. package/dist/Changes/AddRefSeqAliasesChange.d.ts +20 -0
  17. package/dist/Changes/AddRefSeqAliasesChange.js +61 -0
  18. package/dist/Changes/AddRefSeqAliasesChange.js.map +1 -0
  19. package/dist/Changes/DeleteAssemblyChange.d.ts +18 -0
  20. package/dist/Changes/DeleteAssemblyChange.js +72 -0
  21. package/dist/Changes/DeleteAssemblyChange.js.map +1 -0
  22. package/dist/Changes/DeleteFeatureChange.d.ts +42 -0
  23. package/dist/Changes/DeleteFeatureChange.js +143 -0
  24. package/dist/Changes/DeleteFeatureChange.js.map +1 -0
  25. package/dist/Changes/DeleteUserChange.d.ts +25 -0
  26. package/dist/Changes/DeleteUserChange.js +53 -0
  27. package/dist/Changes/DeleteUserChange.js.map +1 -0
  28. package/dist/Changes/FeatureAttributeChange.d.ts +31 -0
  29. package/dist/Changes/FeatureAttributeChange.js +112 -0
  30. package/dist/Changes/FeatureAttributeChange.js.map +1 -0
  31. package/dist/Changes/FromFileBaseChange.d.ts +9 -0
  32. package/dist/Changes/FromFileBaseChange.js +187 -0
  33. package/dist/Changes/FromFileBaseChange.js.map +1 -0
  34. package/dist/Changes/ImportJBrowseConfigChange.d.ts +43 -0
  35. package/dist/Changes/ImportJBrowseConfigChange.js +67 -0
  36. package/dist/Changes/ImportJBrowseConfigChange.js.map +1 -0
  37. package/dist/Changes/LocationEndChange.d.ts +32 -0
  38. package/dist/Changes/LocationEndChange.js +129 -0
  39. package/dist/Changes/LocationEndChange.js.map +1 -0
  40. package/dist/Changes/LocationStartChange.d.ts +32 -0
  41. package/dist/Changes/LocationStartChange.js +129 -0
  42. package/dist/Changes/LocationStartChange.js.map +1 -0
  43. package/dist/Changes/StrandChange.d.ts +31 -0
  44. package/dist/Changes/StrandChange.js +114 -0
  45. package/dist/Changes/StrandChange.js.map +1 -0
  46. package/dist/Changes/TypeChange.d.ts +31 -0
  47. package/dist/Changes/TypeChange.js +114 -0
  48. package/dist/Changes/TypeChange.js.map +1 -0
  49. package/dist/Changes/UserChange.d.ts +25 -0
  50. package/dist/Changes/UserChange.js +51 -0
  51. package/dist/Changes/UserChange.js.map +1 -0
  52. package/dist/Changes/index.d.ts +50 -0
  53. package/dist/Changes/index.js +55 -0
  54. package/dist/Changes/index.js.map +1 -0
  55. package/dist/Checks/CDSCheck.d.ts +8 -0
  56. package/dist/Checks/CDSCheck.js +197 -0
  57. package/dist/Checks/CDSCheck.js.map +1 -0
  58. package/dist/Checks/index.d.ts +1 -0
  59. package/dist/Checks/index.js +5 -0
  60. package/dist/Checks/index.js.map +1 -0
  61. package/dist/Common/index.d.ts +1 -0
  62. package/dist/Common/index.js +5 -0
  63. package/dist/Common/index.js.map +1 -0
  64. package/dist/Common/jwtPayload.d.ts +12 -0
  65. package/dist/Common/jwtPayload.js +16 -0
  66. package/dist/Common/jwtPayload.js.map +1 -0
  67. package/dist/GFF3/gff3ToAnnotationFeature.d.ts +3 -0
  68. package/dist/GFF3/gff3ToAnnotationFeature.js +211 -0
  69. package/dist/GFF3/gff3ToAnnotationFeature.js.map +1 -0
  70. package/dist/GFF3/gff3ToAnnotationFeature.test.d.ts +1 -0
  71. package/dist/GFF3/gff3ToAnnotationFeature.test.js +174 -0
  72. package/dist/GFF3/gff3ToAnnotationFeature.test.js.map +1 -0
  73. package/dist/GFF3/gffReservedKeys.d.ts +5 -0
  74. package/dist/GFF3/gffReservedKeys.js +34 -0
  75. package/dist/GFF3/gffReservedKeys.js.map +1 -0
  76. package/dist/GFF3/index.d.ts +2 -0
  77. package/dist/GFF3/index.js +6 -0
  78. package/dist/GFF3/index.js.map +1 -0
  79. package/dist/Messages.d.ts +28 -0
  80. package/dist/Messages.js +3 -0
  81. package/dist/Messages.js.map +1 -0
  82. package/dist/Operations/GetAssembliesOperation.d.ts +13 -0
  83. package/dist/Operations/GetAssembliesOperation.js +20 -0
  84. package/dist/Operations/GetAssembliesOperation.js.map +1 -0
  85. package/dist/Operations/GetFeaturesOperation.d.ts +26 -0
  86. package/dist/Operations/GetFeaturesOperation.js +42 -0
  87. package/dist/Operations/GetFeaturesOperation.js.map +1 -0
  88. package/dist/Operations/index.d.ts +8 -0
  89. package/dist/Operations/index.js +10 -0
  90. package/dist/Operations/index.js.map +1 -0
  91. package/dist/Validations/CoreValidation.d.ts +17 -0
  92. package/dist/Validations/CoreValidation.js +36 -0
  93. package/dist/Validations/CoreValidation.js.map +1 -0
  94. package/dist/Validations/ParentChildValidation.d.ts +17 -0
  95. package/dist/Validations/ParentChildValidation.js +61 -0
  96. package/dist/Validations/ParentChildValidation.js.map +1 -0
  97. package/dist/Validations/Validation.d.ts +27 -0
  98. package/dist/Validations/Validation.js +27 -0
  99. package/dist/Validations/Validation.js.map +1 -0
  100. package/dist/Validations/ValidationSet.d.ts +24 -0
  101. package/dist/Validations/ValidationSet.js +86 -0
  102. package/dist/Validations/ValidationSet.js.map +1 -0
  103. package/dist/Validations/index.d.ts +4 -0
  104. package/dist/Validations/index.js +8 -0
  105. package/dist/Validations/index.js.map +1 -0
  106. package/dist/Validations/soSequenceTypes.d.ts +2 -0
  107. package/dist/Validations/soSequenceTypes.js +1868 -0
  108. package/dist/Validations/soSequenceTypes.js.map +1 -0
  109. package/dist/index.d.ts +8 -0
  110. package/dist/index.js +12 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/tsconfig.tsbuildinfo +1 -0
  113. package/dist/util.d.ts +4 -0
  114. package/dist/util.js +102 -0
  115. package/dist/util.js.map +1 -0
  116. package/package.json +17 -8
  117. package/src/Changes/AddAssemblyAndFeaturesFromFileChange.ts +13 -9
  118. package/src/Changes/AddAssemblyFromFileChange.ts +129 -43
  119. package/src/Changes/AddFeatureChange.ts +2 -3
  120. package/src/Changes/AddFeaturesFromFileChange.ts +8 -3
  121. package/src/Changes/AddRefSeqAliasesChange.ts +88 -0
  122. package/src/Changes/DeleteAssemblyChange.ts +1 -1
  123. package/src/Changes/DeleteFeatureChange.ts +1 -1
  124. package/src/Changes/FeatureAttributeChange.ts +1 -1
  125. package/src/Changes/FromFileBaseChange.ts +221 -0
  126. package/src/Changes/ImportJBrowseConfigChange.ts +116 -0
  127. package/src/Changes/LocationEndChange.ts +25 -18
  128. package/src/Changes/LocationStartChange.ts +25 -18
  129. package/src/Changes/TypeChange.ts +1 -1
  130. package/src/Changes/index.ts +6 -6
  131. package/src/Checks/CDSCheck.ts +125 -173
  132. package/src/Common/jwtPayload.ts +1 -1
  133. package/src/GFF3/gff3ToAnnotationFeature.test.ts +209 -0
  134. package/src/GFF3/gff3ToAnnotationFeature.ts +257 -0
  135. package/src/GFF3/gffReservedKeys.ts +61 -0
  136. package/src/GFF3/index.ts +2 -0
  137. package/src/Operations/GetFeaturesOperation.ts +2 -2
  138. package/src/Validations/ParentChildValidation.ts +5 -5
  139. package/src/index.ts +1 -0
  140. package/src/util.ts +16 -12
  141. package/test_data/braker.gff +13 -0
  142. package/test_data/example01.gff3 +26 -0
  143. package/test_data/example01.json +236 -0
  144. package/test_data/example02.json +225 -0
  145. package/test_data/example04.json +225 -0
  146. package/test_data/example05.gff3 +30 -0
  147. package/test_data/example06.gff3 +30 -0
  148. package/test_data/example07.gff3 +30 -0
  149. package/test_data/gene_mrna.gff3 +4 -0
  150. package/test_data/gene_representations.gff3 +1781 -0
  151. package/test_data/one_cds.gff3 +9 -0
  152. package/test_data/one_cds.json +67 -0
  153. package/test_data/two_cds.gff3 +9 -0
  154. package/test_data/two_cds.json +67 -0
  155. package/tsconfig.json +4 -4
  156. package/src/Changes/DiscontinuousLocationEndChange.ts +0 -182
  157. package/src/Changes/DiscontinuousLocationStartChange.ts +0 -182
@@ -3,6 +3,7 @@ import {
3
3
  AnnotationFeatureSnapshot,
4
4
  CheckResultSnapshot,
5
5
  } from '@apollo-annotation/mst'
6
+ import { intersection2 } from '@jbrowse/core/util'
6
7
  import ObjectID from 'bson-objectid'
7
8
 
8
9
  enum STOP_CODONS {
@@ -29,6 +30,9 @@ const iupacComplements: Record<string, string | undefined> = {
29
30
  N /* G or A or T or C */: 'N',
30
31
  }
31
32
 
33
+ type CDSLocation = { min: number; max: number; phase: 0 | 1 | 2 }[]
34
+ type CDSLocations = CDSLocation[]
35
+
32
36
  function reverseComplement(dna: string): string {
33
37
  const complement: string[] = []
34
38
  for (const nt of dna) {
@@ -45,47 +49,18 @@ function reverseComplement(dna: string): string {
45
49
  return complement.reverse().join('')
46
50
  }
47
51
 
48
- async function getSequenceFromSingleFeature(
49
- feature: AnnotationFeatureSnapshot,
50
- getSequence: (start: number, end: number) => Promise<string>,
51
- ) {
52
- let seq = ''
53
- if (
54
- feature.discontinuousLocations !== undefined &&
55
- feature.discontinuousLocations.length > 0
56
- ) {
57
- for (const loc of feature.discontinuousLocations) {
58
- seq = seq + (await getSequence(loc.start, loc.end))
59
- }
60
- } else {
61
- seq = await getSequence(feature.start, feature.end)
62
- }
63
- if (feature.strand === -1) {
64
- return reverseComplement(seq)
65
- }
66
- return seq
67
- }
68
-
69
- async function getSequenceFromMultipleFeatures(
70
- features: AnnotationFeatureSnapshot[],
52
+ async function getCDSSequence(
53
+ cdsLocation: CDSLocation,
54
+ strand: 1 | -1 | undefined,
71
55
  getSequence: (start: number, end: number) => Promise<string>,
72
- ) {
73
- const strands = features.map((feature) => feature.strand)
74
- if (!strands.every((strand) => strand === strands[0])) {
75
- throw new Error(
76
- `Strands do not match in features: "${features
77
- .map((f) => f._id)
78
- .join(', ')}"`,
79
- )
80
- }
81
- let seq = ''
82
- for (const feature of features) {
83
- seq = seq + (await getSequence(feature.start, feature.end))
56
+ ): Promise<string> {
57
+ const sequences = await Promise.all(
58
+ cdsLocation.map(({ max, min }) => getSequence(min, max)),
59
+ )
60
+ if (strand === -1) {
61
+ return sequences.map((seq) => reverseComplement(seq)).join('')
84
62
  }
85
- if (strands[0] === -1) {
86
- return reverseComplement(seq)
87
- }
88
- return seq
63
+ return sequences.join('')
89
64
  }
90
65
 
91
66
  function splitSequenceInCodons(cds: string): string[] {
@@ -97,132 +72,135 @@ function splitSequenceInCodons(cds: string): string[] {
97
72
  }
98
73
 
99
74
  function getOriginalCodonLocation(
100
- feature: AnnotationFeatureSnapshot | AnnotationFeatureSnapshot[],
75
+ cdsLocation: CDSLocation,
76
+ strand: 1 | -1 | undefined,
101
77
  index: number,
102
- ): [number, number] {
78
+ ): [number, number] | undefined {
103
79
  let lengthToStart = index * 3
104
80
  let lengthToEnd = lengthToStart + 3
105
- if (Array.isArray(feature)) {
106
- let startLocation: number | undefined = undefined,
107
- endLocation: number | undefined = undefined
108
- for (const f of feature) {
109
- const featureLength = f.end - f.start
110
- if (startLocation === undefined && featureLength > lengthToStart) {
111
- startLocation = f.start + lengthToStart
112
- } else {
113
- lengthToStart -= featureLength
114
- }
115
- if (endLocation === undefined && featureLength > lengthToEnd) {
116
- endLocation = f.start + lengthToEnd
117
- } else {
118
- lengthToEnd -= featureLength
119
- }
120
- if (startLocation !== undefined && endLocation !== undefined) {
121
- return [startLocation, endLocation]
122
- }
81
+
82
+ let startLocation: number | undefined = undefined,
83
+ endLocation: number | undefined = undefined
84
+ for (const loc of cdsLocation) {
85
+ const locLength = loc.max - loc.min
86
+ if (startLocation === undefined && locLength > lengthToStart) {
87
+ startLocation = loc.min + lengthToStart
88
+ } else {
89
+ lengthToStart -= locLength
123
90
  }
124
- throw new Error('Could not determine original CDS location')
125
- } else {
126
- if (
127
- feature.discontinuousLocations !== undefined &&
128
- feature.discontinuousLocations.length > 0
129
- ) {
130
- let startLocation: number | undefined = undefined,
131
- endLocation: number | undefined = undefined
132
- for (const loc of feature.discontinuousLocations) {
133
- const locLength = loc.end - loc.start
134
- if (startLocation === undefined && locLength > lengthToStart) {
135
- startLocation = loc.start + lengthToStart
136
- } else {
137
- lengthToStart -= locLength
138
- }
139
- if (endLocation === undefined && locLength > lengthToEnd) {
140
- endLocation = loc.start + lengthToEnd
141
- } else {
142
- lengthToEnd -= locLength
143
- }
144
- if (startLocation !== undefined && endLocation !== undefined) {
145
- return [startLocation, endLocation]
146
- }
147
- }
148
- throw new Error('Could not determine original CDS location')
91
+ if (endLocation === undefined && locLength > lengthToEnd) {
92
+ endLocation = loc.min + lengthToEnd
149
93
  } else {
150
- return [feature.start + lengthToStart, feature.start + lengthToEnd]
94
+ lengthToEnd -= locLength
95
+ }
96
+ if (startLocation !== undefined && endLocation !== undefined) {
97
+ return [startLocation, endLocation]
151
98
  }
152
99
  }
100
+ return
153
101
  }
154
102
 
155
- async function checkCDS(
156
- feature: AnnotationFeatureSnapshot | AnnotationFeatureSnapshot[],
103
+ async function checkMRNA(
104
+ feature: AnnotationFeatureSnapshot,
157
105
  getSequence: (start: number, end: number) => Promise<string>,
158
106
  ): Promise<CheckResultSnapshot[]> {
159
107
  const checkResults: CheckResultSnapshot[] = []
160
- let _id: string,
161
- ids: string[],
162
- start: number,
163
- end: number,
164
- refSeq: string,
165
- sequence: string
166
- if (Array.isArray(feature)) {
167
- sequence = await getSequenceFromMultipleFeatures(feature, getSequence)
168
- ids = feature.map((f) => f._id)
169
- _id = ids.join(',')
170
- ;[{ refSeq, start }] = feature
171
- const lastFeature = feature.at(-1)
172
- if (!lastFeature) {
173
- throw new Error('Zero-length feature array encountered')
174
- }
175
- ;({ end } = lastFeature)
176
- } else {
177
- sequence = await getSequenceFromSingleFeature(feature, getSequence)
178
- ;({ _id, end, refSeq, start } = feature)
179
- ids = [_id]
108
+ const { _id, max, min, refSeq, strand } = feature
109
+ const cdsLocations = getCDSLocations(feature)
110
+ if (!cdsLocations) {
111
+ return checkResults
180
112
  }
181
- const codons = splitSequenceInCodons(sequence)
182
- if (sequence.length % 3 === 0) {
183
- const lastCodon = codons.pop() // Last codon is supposed to be a stop
184
- if (!lastCodon) {
185
- throw new Error(`No sequence found for feature "${_id}"`)
186
- }
187
- if (!(lastCodon.toUpperCase() in STOP_CODONS)) {
113
+ const ids = [_id]
114
+ for (const cdsLocation of cdsLocations) {
115
+ const sequence = await getCDSSequence(cdsLocation, strand, getSequence)
116
+ const codons = splitSequenceInCodons(sequence)
117
+ if (sequence.length % 3 === 0) {
118
+ const lastCodon = codons.pop() // Last codon is supposed to be a stop
119
+ if (lastCodon && !(lastCodon.toUpperCase() in STOP_CODONS)) {
120
+ checkResults.push({
121
+ _id: new ObjectID().toHexString(),
122
+ name: 'MissingStopCodonCheck',
123
+ ids,
124
+ refSeq: refSeq.toString(),
125
+ start: max,
126
+ end: max,
127
+ message: `Feature "${_id}" is missing a stop codon`,
128
+ })
129
+ }
130
+ } else {
188
131
  checkResults.push({
189
132
  _id: new ObjectID().toHexString(),
190
- name: 'MissingStopCodonCheck',
133
+ name: 'MultipleOfThreeCheck',
191
134
  ids,
192
135
  refSeq: refSeq.toString(),
193
- start: end,
194
- end,
195
- message: `Feature "${_id}" is missing a stop codon`,
136
+ start: min,
137
+ end: max,
138
+ message: `The coding sequence for feature "${_id}" is not a multiple of three`,
196
139
  })
197
140
  }
198
- } else {
199
- checkResults.push({
200
- _id: new ObjectID().toHexString(),
201
- name: 'MultipleOfThreeCheck',
202
- ids,
203
- refSeq: refSeq.toString(),
204
- start,
205
- end,
206
- message: `The coding sequence for feature "${_id}" is not a multiple of three`,
207
- })
208
- }
209
- for (const [idx, codon] of codons.entries()) {
210
- const [codonStart, codonEnd] = getOriginalCodonLocation(feature, idx)
211
- if (codon.toUpperCase() in STOP_CODONS) {
212
- checkResults.push({
213
- _id: new ObjectID().toHexString(),
214
- name: 'InternalStopCodonCheck',
215
- ids,
216
- refSeq: refSeq.toString(),
217
- start: codonStart,
218
- end: codonEnd,
219
- message: `The coding sequence for feature "${_id}" has an internal stop codon`,
220
- })
141
+ for (const [idx, codon] of codons.entries()) {
142
+ const location = getOriginalCodonLocation(cdsLocation, strand, idx)
143
+ if (location && codon.toUpperCase() in STOP_CODONS) {
144
+ const [codonStart, codonEnd] = location
145
+ checkResults.push({
146
+ _id: new ObjectID().toHexString(),
147
+ name: 'InternalStopCodonCheck',
148
+ ids,
149
+ refSeq: refSeq.toString(),
150
+ start: codonStart,
151
+ end: codonEnd,
152
+ message: `The coding sequence for feature "${_id}" has an internal stop codon`,
153
+ })
154
+ }
221
155
  }
222
156
  }
223
157
  return checkResults
224
158
  }
225
159
 
160
+ function getCDSLocations(
161
+ feature: AnnotationFeatureSnapshot,
162
+ ): CDSLocations | undefined {
163
+ if (feature.type !== 'mRNA') {
164
+ return
165
+ }
166
+ const { children, strand } = feature
167
+ if (!children) {
168
+ return
169
+ }
170
+ const cdsChildren = Object.values(children).filter(
171
+ (child) => child.type === 'CDS',
172
+ )
173
+ if (cdsChildren.length === 0) {
174
+ return
175
+ }
176
+ const cdsLocations: CDSLocations = []
177
+ for (const cds of cdsChildren) {
178
+ const { max: cdsMax, min: cdsMin } = cds
179
+ const locs: { min: number; max: number }[] = []
180
+ for (const child of Object.values(children)) {
181
+ if (child.type !== 'exon') {
182
+ continue
183
+ }
184
+ const [start, end] = intersection2(cdsMin, cdsMax, child.min, child.max)
185
+ if (start !== undefined && end !== undefined) {
186
+ locs.push({ min: start, max: end })
187
+ }
188
+ }
189
+ locs.sort(({ min: a }, { min: b }) => a - b)
190
+ if (strand === -1) {
191
+ locs.reverse()
192
+ }
193
+ let nextPhase: 0 | 1 | 2 = 0
194
+ const phasedLocs = locs.map((loc) => {
195
+ const phase = nextPhase
196
+ nextPhase = ((3 - ((loc.max - loc.min - phase + 3) % 3)) % 3) as 0 | 1 | 2
197
+ return { ...loc, phase }
198
+ })
199
+ cdsLocations.push(phasedLocs)
200
+ }
201
+ return cdsLocations
202
+ }
203
+
226
204
  export class CDSCheck extends Check {
227
205
  name = 'CDSCheck'
228
206
  version = 1
@@ -232,44 +210,18 @@ export class CDSCheck extends Check {
232
210
  feature: AnnotationFeatureSnapshot,
233
211
  getSequence: (start: number, end: number) => Promise<string>,
234
212
  ): Promise<CheckResultSnapshot[]> {
235
- if (feature.type === 'CDS') {
236
- return checkCDS(feature, getSequence)
213
+ if (feature.type === 'mRNA') {
214
+ return checkMRNA(feature, getSequence)
237
215
  }
238
216
 
239
217
  if (!feature.children) {
240
218
  return []
241
219
  }
242
220
 
243
- if (feature.type !== 'mRNA') {
244
- const checkResults: CheckResultSnapshot[] = []
245
- for (const child of Object.values(feature.children)) {
246
- checkResults.push(...(await this.checkFeature(child, getSequence)))
247
- }
248
- return checkResults
249
- }
250
-
251
- const cdsChildren = Object.values(feature.children).filter(
252
- (child) => child.type === 'CDS',
253
- )
254
- if (cdsChildren.length === 0) {
255
- throw new Error(`mRNA "${feature._id}" has no CDS children`)
256
- }
257
- const cdsChildrenWithDiscontinuousLocations = cdsChildren.filter(
258
- (child) =>
259
- child.discontinuousLocations && child.discontinuousLocations.length > 0,
260
- )
261
- if (cdsChildrenWithDiscontinuousLocations.length === 0) {
262
- return checkCDS(cdsChildren, getSequence)
263
- }
264
- if (cdsChildrenWithDiscontinuousLocations.length === cdsChildren.length) {
265
- const checkResults: CheckResultSnapshot[] = []
266
- for (const child of cdsChildren) {
267
- checkResults.push(...(await this.checkFeature(child, getSequence)))
268
- }
269
- return checkResults
221
+ const checkResults: CheckResultSnapshot[] = []
222
+ for (const child of Object.values(feature.children)) {
223
+ checkResults.push(...(await this.checkFeature(child, getSequence)))
270
224
  }
271
- throw new Error(
272
- `Mix of CDS with and without discontinuous locations found in mRNA "${feature._id}"`,
273
- )
225
+ return checkResults
274
226
  }
275
227
  }
@@ -3,7 +3,7 @@ import jwtDecode from 'jwt-decode'
3
3
  export interface JWTPayload {
4
4
  username: string
5
5
  email: string
6
- role?: 'admin' | 'user' | 'readOnly'
6
+ role?: 'admin' | 'user' | 'readOnly' | 'none'
7
7
  id: string
8
8
  }
9
9
 
@@ -0,0 +1,209 @@
1
+ /* eslint-disable @typescript-eslint/no-floating-promises */
2
+ import { describe, it } from 'node:test'
3
+ import { readFileSync } from 'node:fs'
4
+
5
+ import gff, { GFF3Feature } from '@gmod/gff'
6
+ import { assert, use } from 'chai'
7
+ import chaiExclude from 'chai-exclude'
8
+
9
+ import { gff3ToAnnotationFeature } from './gff3ToAnnotationFeature'
10
+ import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
11
+
12
+ use(chaiExclude)
13
+
14
+ const testCases: [string, string, AnnotationFeatureSnapshot][] = [
15
+ [
16
+ 'a feature with no children',
17
+ 'ctgA example remark 1000 2000 . . . Name=Remark:hga;Alias=hga\n',
18
+ {
19
+ _id: '66c51f3e002c683eaf98a223',
20
+ refSeq: 'ctgA',
21
+ type: 'remark',
22
+ min: 999,
23
+ max: 2000,
24
+ attributes: {
25
+ gff_source: ['example'],
26
+ gff_name: ['Remark:hga'],
27
+ gff_alias: ['hga'],
28
+ },
29
+ },
30
+ ],
31
+ [
32
+ 'a feature with two children',
33
+ `ctgA est EST_match 1050 3202 . + . ID=Match1;Name=agt830.5;Target=agt830.5 1 654
34
+ ctgA est match_part 1050 1500 . + . Parent=Match1;Name=agt830.5;Target=agt830.5 1 451
35
+ ctgA est match_part 3000 3202 . + . Parent=Match1;Name=agt830.5;Target=agt830.5 452 654
36
+ `,
37
+ {
38
+ _id: '66cf9fbb4e947fa2c27d3d6a',
39
+ refSeq: 'ctgA',
40
+ type: 'EST_match',
41
+ min: 1049,
42
+ max: 3202,
43
+ strand: 1,
44
+ children: {
45
+ '66cf9fbb4e947fa2c27d3d68': {
46
+ _id: '66cf9fbb4e947fa2c27d3d68',
47
+ refSeq: 'ctgA',
48
+ type: 'match_part',
49
+ min: 1049,
50
+ max: 1500,
51
+ strand: 1,
52
+ attributes: {
53
+ gff_source: ['est'],
54
+ gff_name: ['agt830.5'],
55
+ gff_target: ['agt830.5 1 451'],
56
+ },
57
+ },
58
+ '66cf9fbb4e947fa2c27d3d69': {
59
+ _id: '66cf9fbb4e947fa2c27d3d69',
60
+ refSeq: 'ctgA',
61
+ type: 'match_part',
62
+ min: 2999,
63
+ max: 3202,
64
+ strand: 1,
65
+ attributes: {
66
+ gff_source: ['est'],
67
+ gff_name: ['agt830.5'],
68
+ gff_target: ['agt830.5 452 654'],
69
+ },
70
+ },
71
+ },
72
+ attributes: {
73
+ gff_source: ['est'],
74
+ gff_id: ['Match1'],
75
+ gff_name: ['agt830.5'],
76
+ gff_target: ['agt830.5 1 654'],
77
+ },
78
+ },
79
+ ],
80
+ ]
81
+
82
+ interface AnnotationFeatureSnapshotWithChildrenArray
83
+ extends Omit<AnnotationFeatureSnapshot, 'children'> {
84
+ children?: AnnotationFeatureSnapshotWithChildrenArray[]
85
+ }
86
+
87
+ function childrenToArray(
88
+ feature: AnnotationFeatureSnapshot,
89
+ ): AnnotationFeatureSnapshotWithChildrenArray {
90
+ const { children } = feature
91
+ if (!children) {
92
+ return feature as AnnotationFeatureSnapshotWithChildrenArray
93
+ }
94
+ const childrenArray = Object.values(children).map((child) =>
95
+ childrenToArray(child),
96
+ )
97
+ return { ...feature, children: childrenArray }
98
+ }
99
+
100
+ function compareFeatures(
101
+ feature1: AnnotationFeatureSnapshot,
102
+ feature2: AnnotationFeatureSnapshot,
103
+ ) {
104
+ assert.deepEqualExcludingEvery(
105
+ childrenToArray(feature1),
106
+ childrenToArray(feature2),
107
+ '_id',
108
+ )
109
+ }
110
+
111
+ function readFeatureFile(fn: string): GFF3Feature[] {
112
+ const lines = readFileSync(fn).toString().split('\n')
113
+ const feature: string[] = []
114
+ for (const line of lines) {
115
+ if (!line.startsWith('#')) {
116
+ feature.push(line)
117
+ }
118
+ }
119
+ const inGff = gff.parseStringSync(feature.join('\n')) as GFF3Feature[]
120
+ return inGff
121
+ }
122
+
123
+ function readAnnotationFeatureSnapshot(fn: string): AnnotationFeatureSnapshot {
124
+ const lines = readFileSync(fn).toString()
125
+ return JSON.parse(lines) as AnnotationFeatureSnapshot
126
+ }
127
+
128
+ const [ex1, ex2, ex3, ex4] = readFeatureFile(
129
+ 'test_data/gene_representations.gff3',
130
+ )
131
+
132
+ describe('gff3ToAnnotationFeature examples', () => {
133
+ it('Convert one CDS', () => {
134
+ const actual = gff3ToAnnotationFeature(
135
+ readFeatureFile('test_data/one_cds.gff3')[0],
136
+ )
137
+ const expected = readAnnotationFeatureSnapshot('test_data/one_cds.json')
138
+ compareFeatures(actual, expected)
139
+ })
140
+ it('Convert two CDSs', () => {
141
+ const actual = gff3ToAnnotationFeature(
142
+ readFeatureFile('test_data/two_cds.gff3')[0],
143
+ )
144
+ const expected = readAnnotationFeatureSnapshot('test_data/two_cds.json')
145
+ compareFeatures(actual, expected)
146
+ })
147
+ it('Convert example 1', () => {
148
+ const actual = gff3ToAnnotationFeature(ex1)
149
+ const txt = JSON.stringify(actual, null, 2)
150
+
151
+ assert.equal(txt.match(/"type": "CDS"/g)?.length, 4)
152
+ assert.equal(txt.match(/"type": "TF_binding_site"/g)?.length, 1)
153
+
154
+ const expected = readAnnotationFeatureSnapshot('test_data/example01.json')
155
+ compareFeatures(actual, expected)
156
+ })
157
+ it('Convert example 2', () => {
158
+ const actual = gff3ToAnnotationFeature(ex2)
159
+ const txt = JSON.stringify(actual, null, 2)
160
+ assert.equal(txt.match(/"type": "CDS"/g)?.length, 4)
161
+ const expected = readAnnotationFeatureSnapshot('test_data/example02.json')
162
+ compareFeatures(actual, expected)
163
+ })
164
+ it('Convert example 3', () => {
165
+ // NB: In example 3 (and in the other examples) mRNA10003 produces two proteins.
166
+ // In the other examples the two proteins are identified by sharing the same cds id.
167
+ // In example 3 instead each cds has a unique id so the two proteins are identified by the order they
168
+ // appear in the gff.
169
+ const actual = gff3ToAnnotationFeature(ex3)
170
+ const txt = JSON.stringify(actual, null, 2)
171
+ assert.equal(txt.match(/"type": "CDS"/g)?.length, 4)
172
+
173
+ // const expected = readAnnotationFeatureSnapshot('test_data/example03.json')
174
+ // compareFeatures(actual, expected)
175
+ })
176
+ it('Convert example 4', () => {
177
+ const ft = JSON.stringify(ex4, null, 2)
178
+ assert.equal(ft.match(/"type": "five_prime_UTR"/g)?.length, 6)
179
+ assert.equal(ft.match(/"type": "three_prime_UTR"/g)?.length, 3)
180
+
181
+ const actual = gff3ToAnnotationFeature(ex4)
182
+ const txt = JSON.stringify(actual, null, 2)
183
+ assert.equal(txt.match(/"type": "CDS"/g)?.length, 4)
184
+ assert.equal(txt.match(/prime_UTR/g), null)
185
+
186
+ const expected = readAnnotationFeatureSnapshot('test_data/example04.json')
187
+ compareFeatures(actual, expected)
188
+ })
189
+ it('Convert braker gff', () => {
190
+ const [gffFeature] = readFeatureFile('test_data/braker.gff')
191
+ const actual = gff3ToAnnotationFeature(gffFeature)
192
+ const txt = JSON.stringify(actual, null, 2)
193
+ assert.equal(txt.match(/intron/g), null)
194
+ assert.equal(txt.match(/_codon/g), null)
195
+ })
196
+ })
197
+
198
+ describe('gff3ToAnnotationFeature', () => {
199
+ for (const testCase of testCases) {
200
+ const [description, featureLine, convertedFeature] = testCase
201
+ it(`converts ${description}`, () => {
202
+ const gff3Feature = gff.parseStringSync(featureLine, {
203
+ parseSequences: false,
204
+ })
205
+ const feature = gff3ToAnnotationFeature(gff3Feature[0])
206
+ compareFeatures(convertedFeature, feature)
207
+ })
208
+ }
209
+ })