@apollo-annotation/shared 0.3.6 → 0.3.8

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 (94) hide show
  1. package/dist/Changes/AddAssemblyAliasesChange.d.ts +16 -0
  2. package/dist/Changes/AddAssemblyAliasesChange.js +50 -0
  3. package/dist/Changes/AddAssemblyAliasesChange.js.map +1 -0
  4. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.js +1 -1
  5. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.js.map +1 -1
  6. package/dist/Changes/AddAssemblyFromFileChange.js +1 -1
  7. package/dist/Changes/AddAssemblyFromFileChange.js.map +1 -1
  8. package/dist/Changes/AddFeatureChange.d.ts +1 -0
  9. package/dist/Changes/AddFeatureChange.js +5 -25
  10. package/dist/Changes/AddFeatureChange.js.map +1 -1
  11. package/dist/Changes/AddRefSeqAliasesChange.js +2 -4
  12. package/dist/Changes/AddRefSeqAliasesChange.js.map +1 -1
  13. package/dist/Changes/DeleteFeatureChange.d.ts +9 -8
  14. package/dist/Changes/DeleteFeatureChange.js +37 -32
  15. package/dist/Changes/DeleteFeatureChange.js.map +1 -1
  16. package/dist/Changes/FeatureAttributeChange.d.ts +2 -1
  17. package/dist/Changes/FeatureAttributeChange.js +14 -6
  18. package/dist/Changes/FeatureAttributeChange.js.map +1 -1
  19. package/dist/Changes/ImportJBrowseConfigChange.d.ts +3 -0
  20. package/dist/Changes/ImportJBrowseConfigChange.js +17 -13
  21. package/dist/Changes/ImportJBrowseConfigChange.js.map +1 -1
  22. package/dist/Changes/LocationEndChange.js +40 -37
  23. package/dist/Changes/LocationEndChange.js.map +1 -1
  24. package/dist/Changes/LocationStartChange.js +40 -37
  25. package/dist/Changes/LocationStartChange.js.map +1 -1
  26. package/dist/Changes/MergeExonsChange.d.ts +29 -0
  27. package/dist/Changes/MergeExonsChange.js +114 -0
  28. package/dist/Changes/MergeExonsChange.js.map +1 -0
  29. package/dist/Changes/MergeTranscriptsChange.d.ts +34 -0
  30. package/dist/Changes/MergeTranscriptsChange.js +245 -0
  31. package/dist/Changes/MergeTranscriptsChange.js.map +1 -0
  32. package/dist/Changes/SplitExonChange.d.ts +33 -0
  33. package/dist/Changes/SplitExonChange.js +142 -0
  34. package/dist/Changes/SplitExonChange.js.map +1 -0
  35. package/dist/Changes/UndoMergeExonsChange.d.ts +27 -0
  36. package/dist/Changes/UndoMergeExonsChange.js +104 -0
  37. package/dist/Changes/UndoMergeExonsChange.js.map +1 -0
  38. package/dist/Changes/UndoMergeTranscriptsChange.d.ts +27 -0
  39. package/dist/Changes/UndoMergeTranscriptsChange.js +104 -0
  40. package/dist/Changes/UndoMergeTranscriptsChange.js.map +1 -0
  41. package/dist/Changes/UndoSplitExonChange.d.ts +32 -0
  42. package/dist/Changes/UndoSplitExonChange.js +111 -0
  43. package/dist/Changes/UndoSplitExonChange.js.map +1 -0
  44. package/dist/Changes/index.d.ts +21 -0
  45. package/dist/Changes/index.js +21 -0
  46. package/dist/Changes/index.js.map +1 -1
  47. package/dist/Checks/CDSCheck.d.ts +1 -1
  48. package/dist/Checks/CDSCheck.js +8 -9
  49. package/dist/Checks/CDSCheck.js.map +1 -1
  50. package/dist/Checks/TranscriptCheck.d.ts +9 -0
  51. package/dist/Checks/TranscriptCheck.js +109 -0
  52. package/dist/Checks/TranscriptCheck.js.map +1 -0
  53. package/dist/Checks/index.d.ts +1 -0
  54. package/dist/Checks/index.js +1 -0
  55. package/dist/Checks/index.js.map +1 -1
  56. package/dist/GFF3/annotationFeatureToGFF3.js +7 -1
  57. package/dist/GFF3/annotationFeatureToGFF3.js.map +1 -1
  58. package/dist/GFF3/gff3ToAnnotationFeature.test.js +1 -1
  59. package/dist/GFF3/gff3ToAnnotationFeature.test.js.map +1 -1
  60. package/dist/Validations/ParentChildValidation.js +3 -1
  61. package/dist/Validations/ParentChildValidation.js.map +1 -1
  62. package/dist/tsconfig.tsbuildinfo +1 -1
  63. package/dist/util.d.ts +5 -0
  64. package/dist/util.js +46 -0
  65. package/dist/util.js.map +1 -1
  66. package/package.json +7 -7
  67. package/src/Changes/AddAssemblyAliasesChange.ts +69 -0
  68. package/src/Changes/AddAssemblyAndFeaturesFromFileChange.ts +2 -1
  69. package/src/Changes/AddAssemblyFromFileChange.ts +1 -1
  70. package/src/Changes/AddFeatureChange.ts +5 -29
  71. package/src/Changes/AddRefSeqAliasesChange.ts +2 -4
  72. package/src/Changes/DeleteFeatureChange.ts +42 -36
  73. package/src/Changes/FeatureAttributeChange.ts +16 -7
  74. package/src/Changes/ImportJBrowseConfigChange.ts +33 -18
  75. package/src/Changes/LocationEndChange.ts +41 -50
  76. package/src/Changes/LocationStartChange.ts +41 -50
  77. package/src/Changes/MergeExonsChange.ts +201 -0
  78. package/src/Changes/MergeTranscriptsChange.ts +394 -0
  79. package/src/Changes/SplitExonChange.ts +249 -0
  80. package/src/Changes/UndoMergeExonsChange.ts +149 -0
  81. package/src/Changes/UndoMergeTranscriptsChange.ts +153 -0
  82. package/src/Changes/UndoSplitExonChange.ts +174 -0
  83. package/src/Changes/index.ts +21 -0
  84. package/src/Checks/CDSCheck.ts +10 -10
  85. package/src/Checks/TranscriptCheck.ts +139 -0
  86. package/src/Checks/index.ts +1 -0
  87. package/src/GFF3/annotationFeatureToGFF3.ts +7 -1
  88. package/src/GFF3/gff3ToAnnotationFeature.test.ts +1 -1
  89. package/src/Validations/ParentChildValidation.ts +3 -2
  90. package/src/util.ts +53 -0
  91. package/test_data/example01.json +20 -20
  92. package/test_data/example02.json +38 -38
  93. package/test_data/example04.json +57 -57
  94. package/test_data/gene_representations.gff3 +1054 -276
@@ -0,0 +1,394 @@
1
+ /* eslint-disable unicorn/prefer-structured-clone */
2
+ /* eslint-disable @typescript-eslint/require-await */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+
5
+ import {
6
+ type ChangeOptions,
7
+ type ClientDataStore,
8
+ FeatureChange,
9
+ type LocalGFF3DataStore,
10
+ type SerializedFeatureChange,
11
+ type ServerDataStore,
12
+ } from '@apollo-annotation/common'
13
+ import {
14
+ type AnnotationFeature,
15
+ type AnnotationFeatureSnapshot,
16
+ } from '@apollo-annotation/mst'
17
+ import { type Feature } from '@apollo-annotation/schemas'
18
+ import { doesIntersect2 } from '@jbrowse/core/util'
19
+ import { getSnapshot } from 'mobx-state-tree'
20
+
21
+ import { attributesToRecords, stringifyAttributes } from '../util'
22
+
23
+ import { findAndDeleteChildFeature } from './DeleteFeatureChange'
24
+ import { UndoMergeTranscriptsChange } from './UndoMergeTranscriptsChange'
25
+
26
+ interface SerializedMergeTranscriptsChangeBase extends SerializedFeatureChange {
27
+ typeName: 'MergeTranscriptsChange'
28
+ }
29
+
30
+ export interface MergeTranscriptsChangeDetails {
31
+ firstTranscript: AnnotationFeatureSnapshot
32
+ secondTranscript: AnnotationFeatureSnapshot
33
+ parentFeatureId: string
34
+ }
35
+
36
+ interface SerializedMergeTranscriptsChangeSingle
37
+ extends SerializedMergeTranscriptsChangeBase,
38
+ MergeTranscriptsChangeDetails {}
39
+
40
+ interface SerializedMergeTranscriptsChangeMultiple
41
+ extends SerializedMergeTranscriptsChangeBase {
42
+ changes: MergeTranscriptsChangeDetails[]
43
+ }
44
+
45
+ export type SerializedMergeTranscriptsChange =
46
+ | SerializedMergeTranscriptsChangeSingle
47
+ | SerializedMergeTranscriptsChangeMultiple
48
+
49
+ export class MergeTranscriptsChange extends FeatureChange {
50
+ typeName = 'MergeTranscriptsChange' as const
51
+ changes: MergeTranscriptsChangeDetails[]
52
+
53
+ constructor(json: SerializedMergeTranscriptsChange, options?: ChangeOptions) {
54
+ super(json, options)
55
+ this.changes = 'changes' in json ? json.changes : [json]
56
+ }
57
+ // eslint-disable-next-line @typescript-eslint/class-literal-property-style
58
+ get notification() {
59
+ return 'Transcripts successfully merged'
60
+ }
61
+
62
+ toJSON(): SerializedMergeTranscriptsChange {
63
+ const { assembly, changedIds, changes, typeName } = this
64
+ if (changes.length === 1) {
65
+ const [{ firstTranscript, secondTranscript, parentFeatureId }] = changes
66
+
67
+ return {
68
+ typeName,
69
+ changedIds,
70
+ assembly,
71
+ firstTranscript,
72
+ secondTranscript,
73
+ parentFeatureId,
74
+ }
75
+ }
76
+ return { typeName, changedIds, assembly, changes }
77
+ }
78
+
79
+ async executeOnServer(backend: ServerDataStore) {
80
+ const { featureModel, session } = backend
81
+ const { changes, logger } = this
82
+ for (const change of changes) {
83
+ const { firstTranscript, secondTranscript } = change
84
+ const topLevelFeature = await featureModel
85
+ .findOne({ allIds: firstTranscript._id })
86
+ .session(session)
87
+ .exec()
88
+ if (!topLevelFeature) {
89
+ const errMsg = `*** ERROR: The following featureId was not found in database ='${firstTranscript._id}'`
90
+ logger.error(errMsg)
91
+ throw new Error(errMsg)
92
+ }
93
+ const mergedTranscript = this.getFeatureFromId(
94
+ topLevelFeature,
95
+ firstTranscript._id,
96
+ )
97
+ if (!mergedTranscript) {
98
+ const errMsg = 'ERROR when searching feature by featureId'
99
+ logger.error(errMsg)
100
+ throw new Error(errMsg)
101
+ }
102
+ this.mergeTranscriptsOnServer(mergedTranscript, secondTranscript)
103
+ const deletedIds = findAndDeleteChildFeature(
104
+ topLevelFeature,
105
+ secondTranscript._id,
106
+ this,
107
+ )
108
+ deletedIds.push(secondTranscript._id)
109
+ topLevelFeature.allIds = topLevelFeature.allIds.filter(
110
+ (id) => !deletedIds.includes(id),
111
+ )
112
+ await topLevelFeature.save()
113
+ }
114
+ }
115
+
116
+ mergeTranscriptsOnServer(
117
+ firstTranscript: Feature,
118
+ secondTranscript: AnnotationFeatureSnapshot,
119
+ ) {
120
+ firstTranscript.min = Math.min(firstTranscript.min, secondTranscript.min)
121
+ firstTranscript.max = Math.max(firstTranscript.max, secondTranscript.max)
122
+
123
+ const mergedAttributes: Record<string, string[]> =
124
+ firstTranscript.attributes
125
+ ? JSON.parse(JSON.stringify(firstTranscript.attributes))
126
+ : {}
127
+ if (secondTranscript.attributes) {
128
+ if (!Object.keys(mergedAttributes).includes('merged_with')) {
129
+ mergedAttributes.merged_with = []
130
+ }
131
+ mergedAttributes.merged_with.push(
132
+ stringifyAttributes(attributesToRecords(secondTranscript.attributes)),
133
+ )
134
+ }
135
+ firstTranscript.attributes = mergedAttributes
136
+
137
+ if (secondTranscript.children) {
138
+ for (const [, secondFeatureChild] of Object.entries(
139
+ secondTranscript.children,
140
+ )) {
141
+ this.mergeFeatureIntoTranscriptOnServer(
142
+ secondFeatureChild,
143
+ firstTranscript,
144
+ )
145
+ }
146
+ }
147
+ }
148
+
149
+ mergeFeatureIntoTranscriptOnServer(
150
+ secondFeatureChild: AnnotationFeatureSnapshot,
151
+ firstTranscript: Feature,
152
+ ) {
153
+ if (!firstTranscript.children) {
154
+ firstTranscript.children = new Map<string, Feature>()
155
+ }
156
+ let merged = false
157
+ let mrgChild: Feature | undefined
158
+ let toDelete
159
+ for (const [, firstFeatureChild] of firstTranscript.children) {
160
+ if (!merged || !mrgChild) {
161
+ toDelete = false
162
+ mrgChild = firstFeatureChild
163
+ } else {
164
+ toDelete = true
165
+ }
166
+ if (
167
+ mrgChild.type === secondFeatureChild.type &&
168
+ mrgChild.type === firstFeatureChild.type &&
169
+ doesIntersect2(
170
+ secondFeatureChild.min,
171
+ secondFeatureChild.max,
172
+ mrgChild.min,
173
+ mrgChild.max,
174
+ ) &&
175
+ doesIntersect2(
176
+ firstFeatureChild.min,
177
+ firstFeatureChild.max,
178
+ mrgChild.min,
179
+ mrgChild.max,
180
+ )
181
+ ) {
182
+ mrgChild.min = Math.min(
183
+ secondFeatureChild.min,
184
+ mrgChild.min,
185
+ firstFeatureChild.min,
186
+ )
187
+ mrgChild.max = Math.max(
188
+ secondFeatureChild.max,
189
+ mrgChild.max,
190
+ firstFeatureChild.max,
191
+ )
192
+
193
+ if (!mrgChild.attributes) {
194
+ mrgChild.attributes = {}
195
+ }
196
+
197
+ const mrgChildAttr: Record<string, string[]> = JSON.parse(
198
+ JSON.stringify(mrgChild.attributes),
199
+ )
200
+
201
+ if (!Object.keys(mrgChildAttr).includes('merged_with')) {
202
+ mrgChildAttr.merged_with = []
203
+ }
204
+ const mergedWithAttributes = mrgChildAttr.merged_with
205
+ mergedWithAttributes.push(
206
+ stringifyAttributes(
207
+ attributesToRecords(secondFeatureChild.attributes),
208
+ ),
209
+ )
210
+
211
+ if (toDelete) {
212
+ const recs: Record<string, string[] | undefined> =
213
+ firstFeatureChild.attributes
214
+ ? JSON.parse(JSON.stringify(firstFeatureChild.attributes))
215
+ : undefined
216
+ mergedWithAttributes.push(stringifyAttributes(recs))
217
+ firstTranscript.children.delete(firstFeatureChild._id.toString())
218
+ }
219
+
220
+ mrgChildAttr.merged_with = [...new Set(mergedWithAttributes)]
221
+ mrgChild.attributes = mrgChildAttr
222
+ merged = true
223
+ }
224
+ }
225
+
226
+ if (merged && mrgChild && secondFeatureChild.children) {
227
+ // Add the children of the source feature
228
+ // (secondFeatureChild.children) to the merged feature (mrgChild)
229
+ Object.entries(secondFeatureChild.children).map(([, child]) => {
230
+ this.addChild(mrgChild, child)
231
+ })
232
+ }
233
+
234
+ if (merged && mrgChild) {
235
+ this.addChild(
236
+ firstTranscript,
237
+ mrgChild as unknown as AnnotationFeatureSnapshot,
238
+ )
239
+ } else {
240
+ // This secondFeatureChild has no overlap with any feature in the
241
+ // receiving transcript so we add it as it is to the receiving transcript
242
+ this.addChild(firstTranscript, secondFeatureChild)
243
+ }
244
+ }
245
+
246
+ /* --------------------------------- */
247
+
248
+ async executeOnClient(dataStore: ClientDataStore) {
249
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
250
+ if (!dataStore) {
251
+ throw new Error('No data store')
252
+ }
253
+ for (const [idx, changedId] of this.changedIds.entries()) {
254
+ const { firstTranscript, secondTranscript } = this.changes[idx]
255
+ const mergedTranscript = dataStore.getFeature(firstTranscript._id)
256
+ if (!mergedTranscript) {
257
+ throw new Error(`Could not find feature with identifier "${changedId}"`)
258
+ }
259
+ this.mergeTranscriptsOnClient(mergedTranscript, secondTranscript)
260
+ mergedTranscript.parent?.deleteChild(secondTranscript._id)
261
+ }
262
+ }
263
+
264
+ mergeTranscriptsOnClient(
265
+ firstTranscript: AnnotationFeature,
266
+ secondTranscript: AnnotationFeatureSnapshot,
267
+ ) {
268
+ firstTranscript.setMin(Math.min(firstTranscript.min, secondTranscript.min))
269
+ firstTranscript.setMax(Math.max(firstTranscript.max, secondTranscript.max))
270
+
271
+ const mrg = firstTranscript.attributes.get('merged_with')?.slice() ?? []
272
+ const mergedWith = stringifyAttributes(
273
+ attributesToRecords(secondTranscript.attributes),
274
+ )
275
+
276
+ if (!mrg.includes(mergedWith)) {
277
+ // executeOnClient runs twice (?!) so avoid adding this key again
278
+ mrg.push(mergedWith)
279
+ }
280
+ firstTranscript.setAttribute('merged_with', mrg)
281
+
282
+ if (secondTranscript.children) {
283
+ for (const [, secondFeatureChild] of Object.entries(
284
+ secondTranscript.children,
285
+ )) {
286
+ this.mergeFeatureIntoTranscriptOnClient(
287
+ secondFeatureChild,
288
+ firstTranscript,
289
+ )
290
+ }
291
+ }
292
+ }
293
+
294
+ mergeFeatureIntoTranscriptOnClient(
295
+ secondFeatureChild: AnnotationFeatureSnapshot,
296
+ firstTranscript: AnnotationFeature,
297
+ ) {
298
+ if (!firstTranscript.children) {
299
+ firstTranscript.children = new Map<string, AnnotationFeature>()
300
+ }
301
+ let merged = false
302
+ let mrgChild: AnnotationFeature | undefined
303
+ let toDelete
304
+ for (const [, firstFeatureChild] of firstTranscript.children) {
305
+ if (!merged || !mrgChild) {
306
+ toDelete = false
307
+ mrgChild = firstFeatureChild
308
+ } else {
309
+ toDelete = true
310
+ }
311
+ if (
312
+ mrgChild.type === secondFeatureChild.type &&
313
+ mrgChild.type === firstFeatureChild.type &&
314
+ doesIntersect2(
315
+ secondFeatureChild.min,
316
+ secondFeatureChild.max,
317
+ mrgChild.min,
318
+ mrgChild.max,
319
+ ) &&
320
+ doesIntersect2(
321
+ firstFeatureChild.min,
322
+ firstFeatureChild.max,
323
+ mrgChild.min,
324
+ mrgChild.max,
325
+ )
326
+ ) {
327
+ mrgChild.setMin(
328
+ Math.min(secondFeatureChild.min, mrgChild.min, firstFeatureChild.min),
329
+ )
330
+ mrgChild.setMax(
331
+ Math.max(secondFeatureChild.max, mrgChild.max, firstFeatureChild.max),
332
+ )
333
+
334
+ const mergedWithAttributes =
335
+ mrgChild.attributes.get('merged_with')?.slice() ?? []
336
+ mergedWithAttributes.push(
337
+ stringifyAttributes(
338
+ attributesToRecords(secondFeatureChild.attributes),
339
+ ),
340
+ )
341
+ if (toDelete) {
342
+ mergedWithAttributes.push(
343
+ stringifyAttributes(getSnapshot(firstFeatureChild).attributes),
344
+ )
345
+ firstTranscript.deleteChild(firstFeatureChild._id)
346
+ }
347
+ mrgChild.setAttribute('merged_with', [...new Set(mergedWithAttributes)])
348
+ merged = true
349
+ }
350
+ }
351
+
352
+ if (merged && mrgChild && secondFeatureChild.children) {
353
+ Object.entries(secondFeatureChild.children).map(([, child]) => {
354
+ mrgChild.addChild(child)
355
+ })
356
+ }
357
+
358
+ if (merged && mrgChild) {
359
+ firstTranscript.addChild(getSnapshot(mrgChild))
360
+ } else {
361
+ // This secondFeatureChild has no overlap with any feature in the
362
+ // receiving transcript so we add it as it is to the receiving transcript
363
+ firstTranscript.addChild(secondFeatureChild)
364
+ }
365
+ }
366
+
367
+ async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
368
+ throw new Error('executeOnLocalGFF3 not implemented')
369
+ }
370
+
371
+ getInverse() {
372
+ const { assembly, changedIds, changes, logger } = this
373
+ const inverseChangedIds = [...changedIds].reverse()
374
+ const inverseChanges = [...changes]
375
+ .reverse()
376
+ .map((mergeTranscriptChange) => ({
377
+ transcriptsToRestore: [
378
+ mergeTranscriptChange.firstTranscript,
379
+ mergeTranscriptChange.secondTranscript,
380
+ ],
381
+ parentFeatureId: mergeTranscriptChange.parentFeatureId,
382
+ }))
383
+ logger.debug?.(`INVERSE CHANGE '${JSON.stringify(inverseChanges)}'`)
384
+ return new UndoMergeTranscriptsChange(
385
+ {
386
+ changedIds: inverseChangedIds,
387
+ typeName: 'UndoMergeTranscriptsChange',
388
+ changes: inverseChanges,
389
+ assembly,
390
+ },
391
+ { logger },
392
+ )
393
+ }
394
+ }
@@ -0,0 +1,249 @@
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
+
4
+ import {
5
+ type ChangeOptions,
6
+ type ClientDataStore,
7
+ FeatureChange,
8
+ type LocalGFF3DataStore,
9
+ type SerializedFeatureChange,
10
+ type ServerDataStore,
11
+ } from '@apollo-annotation/common'
12
+ import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
13
+
14
+ import { findAndDeleteChildFeature } from './DeleteFeatureChange'
15
+ import { UndoSplitExonChange } from './UndoSplitExonChange'
16
+
17
+ interface SerializedSplitExonChangeBase extends SerializedFeatureChange {
18
+ typeName: 'SplitExonChange'
19
+ }
20
+
21
+ export interface SplitExonChangeDetails {
22
+ exonToBeSplit: AnnotationFeatureSnapshot
23
+ parentFeatureId: string
24
+ upstreamCut: number
25
+ downstreamCut: number
26
+ leftExonId: string
27
+ rightExonId: string
28
+ }
29
+
30
+ interface SerializedSplitExonChangeSingle
31
+ extends SerializedSplitExonChangeBase,
32
+ SplitExonChangeDetails {}
33
+
34
+ interface SerializedSplitExonChangeMultiple
35
+ extends SerializedSplitExonChangeBase {
36
+ changes: SplitExonChangeDetails[]
37
+ }
38
+
39
+ export type SerializedSplitExonChange =
40
+ | SerializedSplitExonChangeSingle
41
+ | SerializedSplitExonChangeMultiple
42
+ export class SplitExonChange extends FeatureChange {
43
+ typeName = 'SplitExonChange' as const
44
+ changes: SplitExonChangeDetails[]
45
+
46
+ constructor(json: SerializedSplitExonChange, options?: ChangeOptions) {
47
+ super(json, options)
48
+ this.changes = 'changes' in json ? json.changes : [json]
49
+ }
50
+ // eslint-disable-next-line @typescript-eslint/class-literal-property-style
51
+ get notification() {
52
+ return 'Exon successfully split'
53
+ }
54
+
55
+ toJSON(): SerializedSplitExonChange {
56
+ const { assembly, changedIds, changes, typeName } = this
57
+ if (changes.length === 1) {
58
+ const [
59
+ {
60
+ exonToBeSplit,
61
+ parentFeatureId,
62
+ upstreamCut,
63
+ downstreamCut,
64
+ leftExonId,
65
+ rightExonId,
66
+ },
67
+ ] = changes
68
+
69
+ return {
70
+ typeName,
71
+ changedIds,
72
+ assembly,
73
+ exonToBeSplit,
74
+ parentFeatureId,
75
+ upstreamCut,
76
+ downstreamCut,
77
+ leftExonId,
78
+ rightExonId,
79
+ }
80
+ }
81
+ return { typeName, changedIds, assembly, changes }
82
+ }
83
+
84
+ async executeOnServer(backend: ServerDataStore) {
85
+ const { featureModel, session } = backend
86
+ const { changes, logger } = this
87
+ for (const change of changes) {
88
+ const {
89
+ exonToBeSplit,
90
+ parentFeatureId,
91
+ upstreamCut,
92
+ downstreamCut,
93
+ leftExonId,
94
+ rightExonId,
95
+ } = change
96
+ const topLevelFeature = await featureModel
97
+ .findOne({ allIds: exonToBeSplit._id })
98
+ .session(session)
99
+ .exec()
100
+ if (!topLevelFeature) {
101
+ const errMsg = `*** ERROR: The following featureId was not found in database ='${exonToBeSplit._id}'`
102
+ logger.error(errMsg)
103
+ throw new Error(errMsg)
104
+ }
105
+ const tx = this.getFeatureFromId(topLevelFeature, parentFeatureId)
106
+ if (!tx?.children) {
107
+ throw new Error(
108
+ 'ERROR: There should be at least one child (i.e. the exon to be split)',
109
+ )
110
+ }
111
+
112
+ const [leftExon, rightExon] = this.makeSplitExons(
113
+ exonToBeSplit,
114
+ upstreamCut,
115
+ downstreamCut,
116
+ leftExonId,
117
+ rightExonId,
118
+ )
119
+
120
+ tx.children.set(leftExon._id, {
121
+ allIds: [],
122
+ ...leftExon,
123
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
124
+ // @ts-expect-error
125
+ _id: leftExon._id,
126
+ })
127
+ tx.children.set(rightExon._id, {
128
+ allIds: [],
129
+ ...rightExon,
130
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
131
+ // @ts-expect-error
132
+ _id: rightExon._id,
133
+ })
134
+ // Child features should be sorted for click and drag of gene glyphs to work properly
135
+ tx.children = new Map(
136
+ [...tx.children.entries()].sort((a, b) => a[1].min - b[1].min),
137
+ )
138
+
139
+ const deletedIds = findAndDeleteChildFeature(
140
+ topLevelFeature,
141
+ exonToBeSplit._id,
142
+ this,
143
+ )
144
+ deletedIds.push(exonToBeSplit._id)
145
+ topLevelFeature.allIds = topLevelFeature.allIds.filter(
146
+ (id) => !deletedIds.includes(id),
147
+ )
148
+ topLevelFeature.allIds.push(leftExon._id, rightExon._id)
149
+ await topLevelFeature.save()
150
+ }
151
+ }
152
+
153
+ async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
154
+ throw new Error('executeOnLocalGFF3 not implemented')
155
+ }
156
+
157
+ async executeOnClient(dataStore: ClientDataStore) {
158
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
159
+ if (!dataStore) {
160
+ throw new Error('No data store')
161
+ }
162
+
163
+ for (const [idx] of this.changedIds.entries()) {
164
+ const {
165
+ exonToBeSplit,
166
+ parentFeatureId,
167
+ upstreamCut,
168
+ downstreamCut,
169
+ leftExonId,
170
+ rightExonId,
171
+ } = this.changes[idx]
172
+ if (!parentFeatureId) {
173
+ throw new Error('TODO: Split exon without parent')
174
+ }
175
+
176
+ const [leftExon, rightExon] = this.makeSplitExons(
177
+ exonToBeSplit,
178
+ upstreamCut,
179
+ downstreamCut,
180
+ leftExonId,
181
+ rightExonId,
182
+ )
183
+
184
+ const parentFeature = dataStore.getFeature(parentFeatureId)
185
+ if (!parentFeature) {
186
+ throw new Error(`Could not find parent feature "${parentFeatureId}"`)
187
+ }
188
+
189
+ parentFeature.addChild(leftExon)
190
+ parentFeature.addChild(rightExon)
191
+ if (dataStore.getFeature(exonToBeSplit._id)) {
192
+ dataStore.deleteFeature(exonToBeSplit._id)
193
+ }
194
+ }
195
+ }
196
+
197
+ getInverse() {
198
+ const { assembly, changedIds, changes, logger } = this
199
+ const inverseChangedIds = [...changedIds].reverse()
200
+ const inverseChanges = [...changes].reverse().map((splitExonChange) => ({
201
+ exonToRestore: splitExonChange.exonToBeSplit,
202
+ parentFeatureId: splitExonChange.parentFeatureId,
203
+ idsToDelete: [splitExonChange.leftExonId, splitExonChange.rightExonId],
204
+ upstreamCut: splitExonChange.upstreamCut,
205
+ downstreamCut: splitExonChange.downstreamCut,
206
+ leftExonId: splitExonChange.leftExonId,
207
+ rightExonId: splitExonChange.rightExonId,
208
+ }))
209
+ logger.debug?.(`INVERSE CHANGE '${JSON.stringify(inverseChanges)}'`)
210
+ return new UndoSplitExonChange(
211
+ {
212
+ changedIds: inverseChangedIds,
213
+ typeName: 'UndoSplitExonChange',
214
+ changes: inverseChanges,
215
+ assembly,
216
+ },
217
+ { logger },
218
+ )
219
+ }
220
+
221
+ makeSplitExons(
222
+ exonToBeSplit: AnnotationFeatureSnapshot,
223
+ upstreamCut: number,
224
+ downstreamCut: number,
225
+ leftExonId: string,
226
+ rightExonId: string,
227
+ ): AnnotationFeatureSnapshot[] {
228
+ // eslint-disable-next-line unicorn/prefer-structured-clone
229
+ const exon = JSON.parse(JSON.stringify(exonToBeSplit))
230
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
231
+ delete exon.attributes._id
232
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
233
+ delete exon.attributes.gff_id
234
+
235
+ const leftExon = structuredClone(
236
+ exon,
237
+ ) as unknown as AnnotationFeatureSnapshot
238
+ leftExon._id = leftExonId
239
+ leftExon.max = upstreamCut
240
+
241
+ const rightExon = structuredClone(
242
+ exon,
243
+ ) as unknown as AnnotationFeatureSnapshot
244
+ rightExon.min = downstreamCut
245
+ rightExon._id = rightExonId
246
+
247
+ return [leftExon, rightExon]
248
+ }
249
+ }