@apollo-annotation/shared 0.3.8 → 0.3.10

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 (56) hide show
  1. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.d.ts +3 -0
  2. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.js +5 -4
  3. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.js.map +1 -1
  4. package/dist/Changes/AddFeatureChange.js +23 -4
  5. package/dist/Changes/AddFeatureChange.js.map +1 -1
  6. package/dist/Changes/AddFeaturesFromFileChange.d.ts +3 -0
  7. package/dist/Changes/AddFeaturesFromFileChange.js +5 -2
  8. package/dist/Changes/AddFeaturesFromFileChange.js.map +1 -1
  9. package/dist/Changes/AddRefSeqAliasesChange.js +1 -4
  10. package/dist/Changes/AddRefSeqAliasesChange.js.map +1 -1
  11. package/dist/Changes/FromFileBaseChange.d.ts +2 -0
  12. package/dist/Changes/FromFileBaseChange.js +26 -11
  13. package/dist/Changes/FromFileBaseChange.js.map +1 -1
  14. package/dist/Checks/CDSCheck.js +5 -4
  15. package/dist/Checks/CDSCheck.js.map +1 -1
  16. package/dist/Checks/TranscriptCheck.js +12 -12
  17. package/dist/Checks/TranscriptCheck.js.map +1 -1
  18. package/dist/Checks/util.d.ts +2 -0
  19. package/dist/Checks/util.js +15 -0
  20. package/dist/Checks/util.js.map +1 -0
  21. package/dist/GFF3/annotationFeatureToGFF3.js +3 -1
  22. package/dist/GFF3/annotationFeatureToGFF3.js.map +1 -1
  23. package/dist/GFF3/annotationFeatureToGFF3.test.js +19 -5
  24. package/dist/GFF3/annotationFeatureToGFF3.test.js.map +1 -1
  25. package/dist/GFF3/gff3ToAnnotationFeature.d.ts +1 -1
  26. package/dist/GFF3/gff3ToAnnotationFeature.js +9 -12
  27. package/dist/GFF3/gff3ToAnnotationFeature.js.map +1 -1
  28. package/dist/GFF3/gff3ToAnnotationFeature.test.d.ts +1 -2
  29. package/dist/GFF3/gff3ToAnnotationFeature.test.js +25 -98
  30. package/dist/GFF3/gff3ToAnnotationFeature.test.js.map +1 -1
  31. package/dist/GFF3/testUtil.d.ts +6 -0
  32. package/dist/GFF3/testUtil.js +24 -0
  33. package/dist/GFF3/testUtil.js.map +1 -0
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +6 -6
  36. package/src/Changes/AddAssemblyAndFeaturesFromFileChange.ts +6 -5
  37. package/src/Changes/AddFeatureChange.ts +26 -4
  38. package/src/Changes/AddFeaturesFromFileChange.ts +6 -1
  39. package/src/Changes/AddRefSeqAliasesChange.ts +1 -5
  40. package/src/Changes/FromFileBaseChange.ts +28 -16
  41. package/src/Checks/CDSCheck.ts +6 -4
  42. package/src/Checks/TranscriptCheck.ts +11 -12
  43. package/src/Checks/util.ts +13 -0
  44. package/src/GFF3/annotationFeatureToGFF3.test.ts +24 -2
  45. package/src/GFF3/annotationFeatureToGFF3.ts +4 -1
  46. package/src/GFF3/gff3ToAnnotationFeature.test.ts +26 -97
  47. package/src/GFF3/gff3ToAnnotationFeature.ts +7 -16
  48. package/src/GFF3/testUtil.ts +25 -0
  49. package/test_data/gene_with_two_cds.gff3 +6 -0
  50. package/test_data/gene_with_two_cds.json +69 -0
  51. package/test_data/single_feature_no_children.gff3 +1 -0
  52. package/test_data/single_feature_no_children.json +14 -0
  53. package/test_data/single_feature_two_children.gff3 +3 -0
  54. package/test_data/single_feature_two_children.json +44 -0
  55. package/test_data/two_cds.gff3 +0 -9
  56. package/test_data/two_cds.json +0 -67
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollo-annotation/shared",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "main": "./dist/index.js",
5
5
  "scripts": {
6
6
  "build": "yarn clean && tsc --build",
@@ -10,10 +10,10 @@
10
10
  "test:ci": "NODE_V8_COVERAGE=./coverage glob -c \"tsx --test --test-reporter spec --experimental-test-coverage \" \"**/*.test.ts\""
11
11
  },
12
12
  "dependencies": {
13
- "@apollo-annotation/common": "^0.3.8",
14
- "@apollo-annotation/mst": "^0.3.8",
15
- "@apollo-annotation/schemas": "^0.3.8",
16
- "@gmod/gff": "1.2.0",
13
+ "@apollo-annotation/common": "^0.3.10",
14
+ "@apollo-annotation/mst": "^0.3.10",
15
+ "@apollo-annotation/schemas": "^0.3.10",
16
+ "@gmod/gff": "^2.0.0",
17
17
  "@gmod/indexedfasta": "^2.0.4",
18
18
  "@jbrowse/core": "^3.6.5",
19
19
  "bson-objectid": "^2.0.4",
@@ -25,7 +25,7 @@
25
25
  "@nestjs/common": "^10.1.0",
26
26
  "@nestjs/core": "^10.1.0",
27
27
  "@types/chai": "^4.3.19",
28
- "@types/node": "^18.14.2",
28
+ "@types/node": "^20.19.15",
29
29
  "chai": "^5.1.1",
30
30
  "chai-exclude": "^3.0.0",
31
31
  "glob": "^11.0.0",
@@ -19,6 +19,7 @@ export interface SerializedAddAssemblyAndFeaturesFromFileChangeBase
19
19
  export interface AddAssemblyAndFeaturesFromFileChangeDetails {
20
20
  assemblyName: string
21
21
  fileIds: { fa: string }
22
+ parseOptions?: { bufferSize: number }
22
23
  }
23
24
 
24
25
  export interface SerializedAddAssemblyAndFeaturesFromFileChangeSingle
@@ -68,7 +69,7 @@ export class AddAssemblyAndFeaturesFromFileChange extends FromFileBaseChange {
68
69
  const { assemblyModel, checkModel, fileModel, filesService, user } = backend
69
70
  const { assembly, changes, logger } = this
70
71
  for (const change of changes) {
71
- const { assemblyName, fileIds } = change
72
+ const { assemblyName, fileIds, parseOptions } = change
72
73
  const fileId = fileIds.fa
73
74
 
74
75
  const { FILE_UPLOAD_FOLDER } = process.env
@@ -110,13 +111,13 @@ export class AddAssemblyAndFeaturesFromFileChange extends FromFileBaseChange {
110
111
  backend,
111
112
  )
112
113
 
113
- // Loop all features
114
- logger.debug?.(
115
- `**************** LOOPATAAN KAIKKI FEATURET SEURAAVAKSI File type: "${fileDoc.type}"`,
116
- )
114
+ const { bufferSize = 10_000 } = parseOptions ?? {}
117
115
  const featureStream = filesService.parseGFF3(
118
116
  filesService.getFileStream(fileDoc),
117
+ { bufferSize },
119
118
  )
119
+ // @ts-expect-error type is wrong here
120
+ // eslint-disable-next-line @typescript-eslint/await-thenable
120
121
  for await (const f of featureStream) {
121
122
  const gff3Feature = f as GFF3Feature
122
123
  logger.verbose?.(`ENTRY=${JSON.stringify(gff3Feature)}`)
@@ -89,6 +89,12 @@ export class AddFeatureChange extends FeatureChange {
89
89
  let featureCnt = 0
90
90
  logger.debug?.(`changes: ${JSON.stringify(changes)}`)
91
91
 
92
+ const { INDEXED_IDS } = process.env
93
+ let idsToIndex: string[] | undefined
94
+ if (INDEXED_IDS) {
95
+ idsToIndex = INDEXED_IDS.split(',')
96
+ }
97
+
92
98
  // Loop the changes
93
99
  for (const change of changes) {
94
100
  logger.debug?.(`change: ${JSON.stringify(change)}`)
@@ -106,9 +112,10 @@ export class AddFeatureChange extends FeatureChange {
106
112
 
107
113
  // CopyFeature is called from CopyFeature.tsx
108
114
  if (copyFeature) {
115
+ const indexedIds = this.getIndexedIds(addedFeature, idsToIndex)
109
116
  // Add into Mongo
110
117
  const [newFeatureDoc] = await featureModel.create(
111
- [{ ...addedFeature, allIds, status: -1, user }],
118
+ [{ ...addedFeature, allIds, indexedIds, status: -1, user }],
112
119
  { session },
113
120
  )
114
121
  logger.debug?.(
@@ -143,8 +150,9 @@ export class AddFeatureChange extends FeatureChange {
143
150
  } else {
144
151
  const childIds = this.getChildFeatureIds(addedFeature)
145
152
  const allIdsV2 = [_id, ...childIds]
153
+ const indexedIds = this.getIndexedIds(addedFeature, idsToIndex)
146
154
  const [newFeatureDoc] = await featureModel.create(
147
- [{ allIds: allIdsV2, status: 0, ...addedFeature }],
155
+ [{ allIds: allIdsV2, indexedIds, status: 0, ...addedFeature }],
148
156
  { session },
149
157
  )
150
158
  logger.verbose?.(`Added docId "${newFeatureDoc._id}"`)
@@ -167,9 +175,23 @@ export class AddFeatureChange extends FeatureChange {
167
175
  for (const change of changes) {
168
176
  const { addedFeature, parentFeatureId } = change
169
177
  if (parentFeatureId) {
170
- const parentFeature = dataStore.getFeature(parentFeatureId)
178
+ let parentFeature = dataStore.getFeature(parentFeatureId)
179
+ // maybe the parent feature hasn't been loaded yet
171
180
  if (!parentFeature) {
172
- throw new Error(`Could not find parent feature "${parentFeatureId}"`)
181
+ await dataStore.loadFeatures([
182
+ {
183
+ assemblyName: assembly,
184
+ refName: addedFeature.refSeq,
185
+ start: addedFeature.min,
186
+ end: addedFeature.max,
187
+ },
188
+ ])
189
+ parentFeature = dataStore.getFeature(parentFeatureId)
190
+ if (!parentFeature) {
191
+ throw new Error(
192
+ `Could not find parent feature "${parentFeatureId}"`,
193
+ )
194
+ }
173
195
  }
174
196
  // create an ID for the parent feature if it does not have one
175
197
  if (!parentFeature.attributes.get('_id')) {
@@ -18,6 +18,7 @@ export interface SerializedAddFeaturesFromFileChangeBase
18
18
 
19
19
  export interface AddFeaturesFromFileChangeDetails {
20
20
  fileId: string
21
+ parseOptions?: { bufferSize: number }
21
22
  }
22
23
 
23
24
  export interface SerializedAddFeaturesFromFileChangeSingle
@@ -75,7 +76,7 @@ export class AddFeaturesFromFileChange extends FromFileBaseChange {
75
76
  }
76
77
 
77
78
  for (const change of changes) {
78
- const { fileId } = change
79
+ const { fileId, parseOptions } = change
79
80
 
80
81
  const { FILE_UPLOAD_FOLDER } = process.env
81
82
  if (!FILE_UPLOAD_FOLDER) {
@@ -89,10 +90,14 @@ export class AddFeaturesFromFileChange extends FromFileBaseChange {
89
90
  logger.debug?.(`FileId "${fileId}", checksum "${fileDoc.checksum}"`)
90
91
 
91
92
  // Read data from compressed file and parse the content
93
+ const { bufferSize = 10_000 } = parseOptions ?? {}
92
94
  const featureStream = filesService.parseGFF3(
93
95
  filesService.getFileStream(fileDoc),
96
+ { bufferSize },
94
97
  )
95
98
  let featureCount = 0
99
+ // @ts-expect-error type is wrong here
100
+ // eslint-disable-next-line @typescript-eslint/await-thenable
96
101
  for await (const f of featureStream) {
97
102
  const gff3Feature = f as GFF3Feature
98
103
 
@@ -35,11 +35,7 @@ export class AddRefSeqAliasesChange extends AssemblySpecificChange {
35
35
  if (!assembly) {
36
36
  throw new Error(`assembly ${this.assembly} not found`)
37
37
  }
38
- const sessionAliases = assembly.refNameAliases
39
-
40
- if (!sessionAliases) {
41
- throw new Error('Session refNameAliases not found in assembly')
42
- }
38
+ const sessionAliases = assembly.refNameAliases ?? {}
43
39
 
44
40
  for (const refSeqAlias of this.refSeqAliases) {
45
41
  const { aliases, refName } = refSeqAlias
@@ -1,12 +1,10 @@
1
- /* eslint-disable @typescript-eslint/restrict-plus-operands */
2
1
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
4
- /* eslint-disable @typescript-eslint/no-unsafe-call */
5
2
  /* eslint-disable @typescript-eslint/no-unsafe-argument */
6
3
  import {
7
4
  AssemblySpecificChange,
8
5
  type ServerDataStore,
9
6
  } from '@apollo-annotation/common'
7
+ import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
10
8
  import {
11
9
  type FileDocument,
12
10
  type RefSeqDocument,
@@ -38,8 +36,11 @@ export abstract class FromFileBaseChange extends AssemblySpecificChange {
38
36
  let parsingStarted = false
39
37
  logger.debug?.('starting sequence stream')
40
38
  let lineCount = 0
39
+ const decoder = new TextDecoder()
40
+ // @ts-expect-error type is wrong here
41
+ // eslint-disable-next-line @typescript-eslint/await-thenable
41
42
  for await (const data of sequenceStream) {
42
- const chunk = data.toString()
43
+ const chunk = decoder.decode(data)
43
44
  lastLineIsIncomplete = !chunk.endsWith('\n')
44
45
  // chunk is small enough that you can split the whole thing into lines without having to make it into smaller chunks first.
45
46
  const lines = chunk.split(/\r?\n/)
@@ -48,9 +49,9 @@ export abstract class FromFileBaseChange extends AssemblySpecificChange {
48
49
  incompleteLine = ''
49
50
  }
50
51
  if (lastLineIsIncomplete) {
51
- incompleteLine = lines.pop() || ''
52
+ incompleteLine = lines.pop() ?? ''
52
53
  }
53
- for await (const line of lines) {
54
+ for (const line of lines) {
54
55
  lineCount++
55
56
  if (lineCount % 1_000_000 === 0) {
56
57
  logger.debug?.(`Processed ${lineCount} lines`)
@@ -183,8 +184,13 @@ export abstract class FromFileBaseChange extends AssemblySpecificChange {
183
184
  }
184
185
 
185
186
  async addFeatureIntoDb(gff3Feature: GFF3Feature, backend: ServerDataStore) {
186
- const { featureModel, refSeqModel, user } = backend
187
+ const { INDEXED_IDS } = process.env
188
+ let idsToIndex: string[] | undefined
189
+ if (INDEXED_IDS) {
190
+ idsToIndex = INDEXED_IDS.split(',')
191
+ }
187
192
  const { assembly, refSeqCache } = this
193
+ const { featureModel, refSeqModel, user } = backend
188
194
 
189
195
  const [{ seq_id: refName }] = gff3Feature
190
196
  if (!refName) {
@@ -206,19 +212,25 @@ export abstract class FromFileBaseChange extends AssemblySpecificChange {
206
212
  `RefSeq was not found by assembly "${assembly}" and seq_id "${refName}" not found`,
207
213
  )
208
214
  }
209
- // Let's add featureId to parent feature
210
- const featureIds: string[] = []
211
-
212
- const newFeature = gff3ToAnnotationFeature(
213
- gff3Feature,
214
- refSeqDoc._id,
215
- featureIds,
216
- )
215
+ const newFeature = gff3ToAnnotationFeature(gff3Feature, refSeqDoc._id)
216
+ const allIds = this.getAllIds(newFeature)
217
+ const indexedIds = this.getIndexedIds(newFeature, idsToIndex)
217
218
 
218
219
  // Add into Mongo
219
220
  // We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
220
221
  await featureModel.create([
221
- { allIds: featureIds, ...newFeature, user, status: -1 },
222
+ { allIds, indexedIds, ...newFeature, user, status: -1 },
222
223
  ])
223
224
  }
225
+
226
+ getAllIds(feature: AnnotationFeatureSnapshot): string[] {
227
+ const allIds = [feature._id]
228
+ if (feature.children) {
229
+ for (const child of Object.values(feature.children)) {
230
+ const childIds = this.getAllIds(child)
231
+ allIds.push(...childIds)
232
+ }
233
+ }
234
+ return allIds
235
+ }
224
236
  }
@@ -6,6 +6,8 @@ import {
6
6
  import { intersection2 } from '@jbrowse/core/util'
7
7
  import ObjectID from 'bson-objectid'
8
8
 
9
+ import { getPrintableId } from './util'
10
+
9
11
  enum STOP_CODONS {
10
12
  'TAG',
11
13
  'TAA',
@@ -175,7 +177,7 @@ async function checkMRNA(
175
177
  refSeq: refSeq.toString(),
176
178
  start: cdsStart,
177
179
  end: cdsStart,
178
- message: `Unexpected start codon in feature "${_id}": ${start_codon}`,
180
+ message: `Unexpected start codon "${start_codon}" in feature "${getPrintableId(feature)}": `,
179
181
  })
180
182
  }
181
183
 
@@ -189,7 +191,7 @@ async function checkMRNA(
189
191
  refSeq: refSeq.toString(),
190
192
  start: cdsEnd,
191
193
  end: cdsEnd,
192
- message: `Missing stop codon for feature "${_id}"`,
194
+ message: `Missing stop codon in feature "${getPrintableId(feature)}"`,
193
195
  })
194
196
  }
195
197
  } else {
@@ -201,7 +203,7 @@ async function checkMRNA(
201
203
  refSeq: refSeq.toString(),
202
204
  start: cdsEnd,
203
205
  end: cdsEnd,
204
- message: `Missing stop codon: The coding sequence for feature "${_id}" is not a multiple of three`,
206
+ message: `Missing stop codon in feature "${getPrintableId(feature)}"`,
205
207
  })
206
208
  }
207
209
  for (const [idx, codon] of codons.entries()) {
@@ -220,7 +222,7 @@ async function checkMRNA(
220
222
  refSeq: refSeq.toString(),
221
223
  start: codonStart,
222
224
  end: codonEnd,
223
- message: `The coding sequence for feature "${_id}" has an internal stop codon`,
225
+ message: `Internal stop codon in feature "${getPrintableId(feature)}"`,
224
226
  })
225
227
  }
226
228
  }
@@ -6,6 +6,8 @@ import {
6
6
  import { revcom } from '@jbrowse/core/util'
7
7
  import ObjectID from 'bson-objectid'
8
8
 
9
+ import { getPrintableId } from './util'
10
+
9
11
  interface SpliceSequence {
10
12
  fivePrimeSeq: string
11
13
  fivePrimeMin: number
@@ -35,15 +37,17 @@ async function getSpliceSequences(
35
37
  if (exons.length < 2) {
36
38
  return []
37
39
  }
40
+ exons.sort((a, b) => (a.min < b.min ? -1 : 1))
38
41
 
39
42
  const spliceSeq: SpliceSequence[] = []
40
43
  for (let i = 0; i < exons.length - 1; i++) {
41
44
  let fivePrimeMin = exons[i].max
42
- let threePrimeMin = exons[i + 1].min - 2
45
+ let threePrimeMin = exons[i + 1].min
43
46
  if (transcript.strand === -1) {
44
- const _fivePrimeMin = fivePrimeMin
45
- fivePrimeMin = threePrimeMin
46
- threePrimeMin = _fivePrimeMin
47
+ ;[threePrimeMin, fivePrimeMin] = [fivePrimeMin, threePrimeMin]
48
+ fivePrimeMin -= 2
49
+ } else {
50
+ threePrimeMin -= 2
47
51
  }
48
52
 
49
53
  let fivePrimeSeq = await getSequence(fivePrimeMin, fivePrimeMin + 2)
@@ -53,12 +57,7 @@ async function getSpliceSequences(
53
57
  fivePrimeSeq = revcom(fivePrimeSeq)
54
58
  }
55
59
 
56
- spliceSeq.push({
57
- fivePrimeSeq,
58
- fivePrimeMin,
59
- threePrimeSeq,
60
- threePrimeMin,
61
- })
60
+ spliceSeq.push({ fivePrimeSeq, fivePrimeMin, threePrimeSeq, threePrimeMin })
62
61
  }
63
62
  return spliceSeq
64
63
  }
@@ -82,7 +81,7 @@ async function checkTranscript(
82
81
  refSeq: feature.refSeq.toString(),
83
82
  start: spliceSequence.fivePrimeMin,
84
83
  end: spliceSequence.fivePrimeMin + 2,
85
- message: `Unexpected 5' splice site in "${feature._id}". Expected: ${[...VALID_FIVE_PRIME_SEQ].join('|')}, got: ${spliceSequence.fivePrimeSeq}`,
84
+ message: `Unexpected 5 splice site in "${getPrintableId(feature)}". Expected: ${[...VALID_FIVE_PRIME_SEQ].join('|')}, got: ${spliceSequence.fivePrimeSeq}`,
86
85
  })
87
86
  }
88
87
  if (
@@ -96,7 +95,7 @@ async function checkTranscript(
96
95
  refSeq: feature.refSeq.toString(),
97
96
  start: spliceSequence.threePrimeMin,
98
97
  end: spliceSequence.threePrimeMin + 2,
99
- message: `Unexpected 3' splice site in "${feature._id}". Expected: ${[...VALID_THREE_PRIME_SEQ].join('|')}, got: ${spliceSequence.threePrimeSeq}`,
98
+ message: `Unexpected 3 splice site in "${getPrintableId(feature)}". Expected: ${[...VALID_THREE_PRIME_SEQ].join('|')}, got: ${spliceSequence.threePrimeSeq}`,
100
99
  })
101
100
  }
102
101
  }
@@ -0,0 +1,13 @@
1
+ import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
2
+
3
+ export function getPrintableId(feature: AnnotationFeatureSnapshot): string {
4
+ const gff_id = feature.attributes?.gff_id?.join(', ')
5
+ if (gff_id) {
6
+ return `${gff_id} (_id: ${feature._id.toString()})`
7
+ }
8
+ const gff_name = feature.attributes?.gff_name?.join(', ')
9
+ if (gff_name) {
10
+ return `${gff_name} (_id: ${feature._id.toString()})`
11
+ }
12
+ return `_id: ${feature._id.toString()}`
13
+ }
@@ -1,12 +1,34 @@
1
1
  /* eslint-disable prefer-destructuring */
2
2
  /* eslint-disable @typescript-eslint/no-floating-promises */
3
+ import { readFileSync } from 'node:fs'
3
4
  import { describe, it } from 'node:test'
4
5
 
5
6
  import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
6
- import { assert } from 'chai'
7
+ import { formatSync } from '@gmod/gff'
8
+ import { assert, expect } from 'chai'
7
9
 
8
10
  import { annotationFeatureToGFF3 } from './annotationFeatureToGFF3'
9
- import { readAnnotationFeatureSnapshot } from './gff3ToAnnotationFeature.test'
11
+ import { readAnnotationFeatureSnapshot, testCases } from './testUtil'
12
+
13
+ describe('Converts AnnotationFeatureSnapshot JSON to GFF3 when', () => {
14
+ for (const testCase of testCases) {
15
+ const { filenameStem, description } = testCase
16
+ it(description, () => {
17
+ const annotationFeatures = JSON.parse(
18
+ readFileSync(`test_data/${filenameStem}.json`, 'utf8'),
19
+ ) as AnnotationFeatureSnapshot[]
20
+ const expectedGFF3 = readFileSync(
21
+ `test_data/${filenameStem}.gff3`,
22
+ 'utf8',
23
+ )
24
+ const gffFeatures = annotationFeatures.map((annotationFeature) =>
25
+ annotationFeatureToGFF3(annotationFeature),
26
+ )
27
+ const gff3 = formatSync(gffFeatures)
28
+ expect(gff3).to.equal(expectedGFF3)
29
+ })
30
+ }
31
+ })
10
32
 
11
33
  describe('annotationFeatureToGFF3', () => {
12
34
  it('Test mandatory columns', () => {
@@ -101,7 +101,10 @@ export function annotationFeatureToGFF3(
101
101
  score,
102
102
  strand: feature.strand ? (feature.strand === 1 ? '+' : '-') : null,
103
103
  phase: null,
104
- attributes: Object.keys(attributes).length > 0 ? attributes : null,
104
+ attributes:
105
+ Object.keys(attributes).length > 0
106
+ ? (attributes as Record<string, string[]>)
107
+ : null,
105
108
  derived_features: [],
106
109
  child_features: prepareChildFeatures(
107
110
  feature,
@@ -3,82 +3,15 @@ import { readFileSync } from 'node:fs'
3
3
  import { describe, it } from 'node:test'
4
4
 
5
5
  import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
6
- import gff, { type GFF3Feature } from '@gmod/gff'
6
+ import { type GFF3Feature, parseStringSync } from '@gmod/gff'
7
7
  import { assert, use } from 'chai'
8
8
  import chaiExclude from 'chai-exclude'
9
9
 
10
10
  import { gff3ToAnnotationFeature } from './gff3ToAnnotationFeature'
11
+ import { readAnnotationFeatureSnapshot, testCases } from './testUtil'
11
12
 
12
13
  use(chaiExclude)
13
14
 
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
15
  interface AnnotationFeatureSnapshotWithChildrenArray
83
16
  extends Omit<AnnotationFeatureSnapshot, 'children'> {
84
17
  children?: AnnotationFeatureSnapshotWithChildrenArray[]
@@ -108,6 +41,29 @@ function compareFeatures(
108
41
  )
109
42
  }
110
43
 
44
+ describe('Converts GFF3 to AnnotationFeatureSnapshot JSON when', () => {
45
+ for (const testCase of testCases) {
46
+ const { filenameStem, description } = testCase
47
+ it(description, () => {
48
+ const fileText = readFileSync(`test_data/${filenameStem}.gff3`, 'utf8')
49
+ const gffFeatures = parseStringSync(fileText, { parseSequences: false })
50
+ const annotationFeatures = gffFeatures.map((gff3Feature) =>
51
+ gff3ToAnnotationFeature(gff3Feature),
52
+ )
53
+ const annotationFeaturesExpected = JSON.parse(
54
+ readFileSync(`test_data/${filenameStem}.json`, 'utf8'),
55
+ ) as AnnotationFeatureSnapshot[]
56
+ for (const [
57
+ i,
58
+ annotationFeatureExpected,
59
+ ] of annotationFeaturesExpected.entries()) {
60
+ const annotationFeature = annotationFeatures[i]
61
+ compareFeatures(annotationFeature, annotationFeatureExpected)
62
+ }
63
+ })
64
+ }
65
+ })
66
+
111
67
  function readFeatureFile(fn: string): GFF3Feature[] {
112
68
  const lines = readFileSync(fn).toString().split('\n')
113
69
  const feature: string[] = []
@@ -116,17 +72,10 @@ function readFeatureFile(fn: string): GFF3Feature[] {
116
72
  feature.push(line)
117
73
  }
118
74
  }
119
- const inGff = gff.parseStringSync(feature.join('\n')) as GFF3Feature[]
75
+ const inGff = parseStringSync(feature.join('\n')) as GFF3Feature[]
120
76
  return inGff
121
77
  }
122
78
 
123
- export function readAnnotationFeatureSnapshot(
124
- fn: string,
125
- ): AnnotationFeatureSnapshot {
126
- const lines = readFileSync(fn).toString()
127
- return JSON.parse(lines) as AnnotationFeatureSnapshot
128
- }
129
-
130
79
  const [ex1, , ex2, , ex3, , ex4] = readFeatureFile(
131
80
  'test_data/gene_representations.gff3',
132
81
  )
@@ -139,13 +88,6 @@ describe('gff3ToAnnotationFeature examples', () => {
139
88
  const expected = readAnnotationFeatureSnapshot('test_data/one_cds.json')
140
89
  compareFeatures(actual, expected)
141
90
  })
142
- it('Convert two CDSs', () => {
143
- const actual = gff3ToAnnotationFeature(
144
- readFeatureFile('test_data/two_cds.gff3')[0],
145
- )
146
- const expected = readAnnotationFeatureSnapshot('test_data/two_cds.json')
147
- compareFeatures(actual, expected)
148
- })
149
91
  it('Convert example 1', () => {
150
92
  const actual = gff3ToAnnotationFeature(ex1)
151
93
  const txt = JSON.stringify(actual, null, 2)
@@ -228,19 +170,6 @@ describe('CDS without exons', () => {
228
170
  })
229
171
  })
230
172
 
231
- describe('gff3ToAnnotationFeature', () => {
232
- for (const testCase of testCases) {
233
- const [description, featureLine, convertedFeature] = testCase
234
- it(`converts ${description}`, () => {
235
- const gff3Feature = gff.parseStringSync(featureLine, {
236
- parseSequences: false,
237
- })
238
- const feature = gff3ToAnnotationFeature(gff3Feature[0])
239
- compareFeatures(convertedFeature, feature)
240
- })
241
- }
242
- })
243
-
244
173
  describe('Source and score', () => {
245
174
  it('Convert score and source', () => {
246
175
  const gffFeature: GFF3Feature = [