@apollo-annotation/shared 0.1.19 → 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 (136) 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 +8 -5
  117. package/src/Changes/AddAssemblyAndFeaturesFromFileChange.ts +6 -5
  118. package/src/Changes/AddAssemblyFromFileChange.ts +126 -41
  119. package/src/Checks/CDSCheck.ts +202 -252
  120. package/src/GFF3/gff3ToAnnotationFeature.test.ts +165 -5
  121. package/src/GFF3/gff3ToAnnotationFeature.ts +16 -16
  122. package/src/util.ts +1 -0
  123. package/test_data/braker.gff +13 -0
  124. package/test_data/example01.gff3 +26 -0
  125. package/test_data/example01.json +236 -0
  126. package/test_data/example02.json +225 -0
  127. package/test_data/example04.json +225 -0
  128. package/test_data/example05.gff3 +30 -0
  129. package/test_data/example06.gff3 +30 -0
  130. package/test_data/example07.gff3 +30 -0
  131. package/test_data/gene_mrna.gff3 +4 -0
  132. package/test_data/gene_representations.gff3 +1781 -0
  133. package/test_data/one_cds.gff3 +9 -0
  134. package/test_data/one_cds.json +67 -0
  135. package/test_data/two_cds.gff3 +9 -0
  136. package/test_data/two_cds.json +67 -0
@@ -3,225 +3,203 @@ import {
3
3
  AnnotationFeatureSnapshot,
4
4
  CheckResultSnapshot,
5
5
  } from '@apollo-annotation/mst'
6
- // import ObjectID from 'bson-objectid'
6
+ import { intersection2 } from '@jbrowse/core/util'
7
+ import ObjectID from 'bson-objectid'
7
8
 
8
- // enum STOP_CODONS {
9
- // 'TAG',
10
- // 'TAA',
11
- // 'TGA',
12
- // }
9
+ enum STOP_CODONS {
10
+ 'TAG',
11
+ 'TAA',
12
+ 'TGA',
13
+ }
13
14
 
14
- // const iupacComplements: Record<string, string | undefined> = {
15
- // G: 'C',
16
- // A: 'T',
17
- // T: 'A',
18
- // C: 'G',
19
- // R /* G or A */: 'Y',
20
- // Y /* T or C */: 'R',
21
- // M /* A or C */: 'K',
22
- // K /* G or T */: 'M',
23
- // S /* G or C */: 'S',
24
- // W /* A or T */: 'W',
25
- // H /* A or C or T */: 'D',
26
- // B /* G or T or C */: 'V',
27
- // V /* G or C or A */: 'B',
28
- // D /* G or A or T */: 'H',
29
- // N /* G or A or T or C */: 'N',
30
- // }
15
+ const iupacComplements: Record<string, string | undefined> = {
16
+ G: 'C',
17
+ A: 'T',
18
+ T: 'A',
19
+ C: 'G',
20
+ R /* G or A */: 'Y',
21
+ Y /* T or C */: 'R',
22
+ M /* A or C */: 'K',
23
+ K /* G or T */: 'M',
24
+ S /* G or C */: 'S',
25
+ W /* A or T */: 'W',
26
+ H /* A or C or T */: 'D',
27
+ B /* G or T or C */: 'V',
28
+ V /* G or C or A */: 'B',
29
+ D /* G or A or T */: 'H',
30
+ N /* G or A or T or C */: 'N',
31
+ }
31
32
 
32
- // function reverseComplement(dna: string): string {
33
- // const complement: string[] = []
34
- // for (const nt of dna) {
35
- // const rc = iupacComplements[nt.toUpperCase()]
36
- // if (rc === undefined) {
37
- // throw new TypeError(`Cannot complement nucleotide: "${nt}"`)
38
- // }
39
- // if (nt === nt.toLowerCase()) {
40
- // complement.push(rc.toLowerCase())
41
- // } else {
42
- // complement.push(rc)
43
- // }
44
- // }
45
- // return complement.reverse().join('')
46
- // }
33
+ type CDSLocation = { min: number; max: number; phase: 0 | 1 | 2 }[]
34
+ type CDSLocations = CDSLocation[]
47
35
 
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
- // }
36
+ function reverseComplement(dna: string): string {
37
+ const complement: string[] = []
38
+ for (const nt of dna) {
39
+ const rc = iupacComplements[nt.toUpperCase()]
40
+ if (rc === undefined) {
41
+ throw new TypeError(`Cannot complement nucleotide: "${nt}"`)
42
+ }
43
+ if (nt === nt.toLowerCase()) {
44
+ complement.push(rc.toLowerCase())
45
+ } else {
46
+ complement.push(rc)
47
+ }
48
+ }
49
+ return complement.reverse().join('')
50
+ }
68
51
 
69
- // async function getSequenceFromMultipleFeatures(
70
- // features: AnnotationFeatureSnapshot[],
71
- // 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))
84
- // }
85
- // if (strands[0] === -1) {
86
- // return reverseComplement(seq)
87
- // }
88
- // return seq
89
- // }
52
+ async function getCDSSequence(
53
+ cdsLocation: CDSLocation,
54
+ strand: 1 | -1 | undefined,
55
+ getSequence: (start: number, end: number) => Promise<string>,
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('')
62
+ }
63
+ return sequences.join('')
64
+ }
90
65
 
91
- // function splitSequenceInCodons(cds: string): string[] {
92
- // const codons: string[] = []
93
- // for (let i = 0; i <= cds.length - 3; i += 3) {
94
- // codons.push(cds.slice(i, i + 3))
95
- // }
96
- // return codons
97
- // }
66
+ function splitSequenceInCodons(cds: string): string[] {
67
+ const codons: string[] = []
68
+ for (let i = 0; i <= cds.length - 3; i += 3) {
69
+ codons.push(cds.slice(i, i + 3))
70
+ }
71
+ return codons
72
+ }
98
73
 
99
- // function getOriginalCodonLocation(
100
- // feature: AnnotationFeatureSnapshot | AnnotationFeatureSnapshot[],
101
- // index: number,
102
- // ): [number, number] {
103
- // let lengthToStart = index * 3
104
- // 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
- // }
123
- // }
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')
149
- // } else {
150
- // return [feature.start + lengthToStart, feature.start + lengthToEnd]
151
- // }
152
- // }
153
- // }
74
+ function getOriginalCodonLocation(
75
+ cdsLocation: CDSLocation,
76
+ strand: 1 | -1 | undefined,
77
+ index: number,
78
+ ): [number, number] | undefined {
79
+ let lengthToStart = index * 3
80
+ let lengthToEnd = lengthToStart + 3
154
81
 
155
- // async function checkCDS(
156
- // feature: AnnotationFeatureSnapshot | AnnotationFeatureSnapshot[],
157
- // getSequence: (start: number, end: number) => Promise<string>,
158
- // ): Promise<CheckResultSnapshot[]> {
159
- // 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]
180
- // }
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)) {
188
- // checkResults.push({
189
- // _id: new ObjectID().toHexString(),
190
- // name: 'MissingStopCodonCheck',
191
- // ids,
192
- // refSeq: refSeq.toString(),
193
- // start: end,
194
- // end,
195
- // message: `Feature "${_id}" is missing a stop codon`,
196
- // })
197
- // }
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
- // })
221
- // }
222
- // }
223
- // return checkResults
224
- // }
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
90
+ }
91
+ if (endLocation === undefined && locLength > lengthToEnd) {
92
+ endLocation = loc.min + lengthToEnd
93
+ } else {
94
+ lengthToEnd -= locLength
95
+ }
96
+ if (startLocation !== undefined && endLocation !== undefined) {
97
+ return [startLocation, endLocation]
98
+ }
99
+ }
100
+ return
101
+ }
102
+
103
+ async function checkMRNA(
104
+ feature: AnnotationFeatureSnapshot,
105
+ getSequence: (start: number, end: number) => Promise<string>,
106
+ ): Promise<CheckResultSnapshot[]> {
107
+ const checkResults: CheckResultSnapshot[] = []
108
+ const { _id, max, min, refSeq, strand } = feature
109
+ const cdsLocations = getCDSLocations(feature)
110
+ if (!cdsLocations) {
111
+ return checkResults
112
+ }
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 {
131
+ checkResults.push({
132
+ _id: new ObjectID().toHexString(),
133
+ name: 'MultipleOfThreeCheck',
134
+ ids,
135
+ refSeq: refSeq.toString(),
136
+ start: min,
137
+ end: max,
138
+ message: `The coding sequence for feature "${_id}" is not a multiple of three`,
139
+ })
140
+ }
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
+ }
155
+ }
156
+ }
157
+ return checkResults
158
+ }
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
+ }
225
203
 
226
204
  export class CDSCheck extends Check {
227
205
  name = 'CDSCheck'
@@ -229,49 +207,21 @@ export class CDSCheck extends Check {
229
207
  default = true
230
208
 
231
209
  async checkFeature(
232
- _feature: AnnotationFeatureSnapshot,
233
- _getSequence: (start: number, end: number) => Promise<string>,
210
+ feature: AnnotationFeatureSnapshot,
211
+ getSequence: (start: number, end: number) => Promise<string>,
234
212
  ): Promise<CheckResultSnapshot[]> {
235
- await Promise.resolve()
236
- return []
237
- // if (feature.type === 'CDS') {
238
- // return checkCDS(feature, getSequence)
239
- // }
240
-
241
- // if (!feature.children) {
242
- // return []
243
- // }
213
+ if (feature.type === 'mRNA') {
214
+ return checkMRNA(feature, getSequence)
215
+ }
244
216
 
245
- // if (feature.type !== 'mRNA') {
246
- // const checkResults: CheckResultSnapshot[] = []
247
- // for (const child of Object.values(feature.children)) {
248
- // checkResults.push(...(await this.checkFeature(child, getSequence)))
249
- // }
250
- // return checkResults
251
- // }
217
+ if (!feature.children) {
218
+ return []
219
+ }
252
220
 
253
- // const cdsChildren = Object.values(feature.children).filter(
254
- // (child) => child.type === 'CDS',
255
- // )
256
- // if (cdsChildren.length === 0) {
257
- // throw new Error(`mRNA "${feature._id}" has no CDS children`)
258
- // }
259
- // const cdsChildrenWithDiscontinuousLocations = cdsChildren.filter(
260
- // (child) =>
261
- // child.discontinuousLocations && child.discontinuousLocations.length > 0,
262
- // )
263
- // if (cdsChildrenWithDiscontinuousLocations.length === 0) {
264
- // return checkCDS(cdsChildren, getSequence)
265
- // }
266
- // if (cdsChildrenWithDiscontinuousLocations.length === cdsChildren.length) {
267
- // const checkResults: CheckResultSnapshot[] = []
268
- // for (const child of cdsChildren) {
269
- // checkResults.push(...(await this.checkFeature(child, getSequence)))
270
- // }
271
- // return checkResults
272
- // }
273
- // throw new Error(
274
- // `Mix of CDS with and without discontinuous locations found in mRNA "${feature._id}"`,
275
- // )
221
+ const checkResults: CheckResultSnapshot[] = []
222
+ for (const child of Object.values(feature.children)) {
223
+ checkResults.push(...(await this.checkFeature(child, getSequence)))
224
+ }
225
+ return checkResults
276
226
  }
277
227
  }
@@ -1,11 +1,16 @@
1
1
  /* eslint-disable @typescript-eslint/no-floating-promises */
2
- import { strict as assert } from 'node:assert'
3
2
  import { describe, it } from 'node:test'
4
- import gff from '@gmod/gff'
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'
5
8
 
6
9
  import { gff3ToAnnotationFeature } from './gff3ToAnnotationFeature'
7
10
  import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
8
11
 
12
+ use(chaiExclude)
13
+
9
14
  const testCases: [string, string, AnnotationFeatureSnapshot][] = [
10
15
  [
11
16
  'a feature with no children',
@@ -23,18 +28,173 @@ const testCases: [string, string, AnnotationFeatureSnapshot][] = [
23
28
  },
24
29
  },
25
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
+ ],
26
80
  ]
27
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
+
28
100
  function compareFeatures(
29
101
  feature1: AnnotationFeatureSnapshot,
30
102
  feature2: AnnotationFeatureSnapshot,
31
103
  ) {
32
- assert.deepEqual(
33
- { ...feature1, _id: undefined },
34
- { ...feature2, _id: undefined },
104
+ assert.deepEqualExcludingEvery(
105
+ childrenToArray(feature1),
106
+ childrenToArray(feature2),
107
+ '_id',
35
108
  )
36
109
  }
37
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
+
38
198
  describe('gff3ToAnnotationFeature', () => {
39
199
  for (const testCase of testCases) {
40
200
  const [description, featureLine, convertedFeature] = testCase