Skip to content

Commit 57e1f53

Browse files
replace gene change
1 parent e7540fb commit 57e1f53

File tree

2 files changed

+238
-0
lines changed

2 files changed

+238
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/* eslint-disable @typescript-eslint/require-await */
2+
import {
3+
type ChangeOptions,
4+
type ClientDataStore,
5+
FeatureChange,
6+
type LocalGFF3DataStore,
7+
type SerializedFeatureChange,
8+
type ServerDataStore,
9+
} from '@apollo-annotation/common'
10+
import type { AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
11+
import type { Feature, FeatureDocument } from '@apollo-annotation/schemas'
12+
13+
import soSequenceTypes from '../Validations/soSequenceTypes.js'
14+
15+
interface SerializedReplaceFeatureChangeBase extends SerializedFeatureChange {
16+
typeName: 'ReplaceFeatureChange'
17+
}
18+
19+
export interface ReplaceFeatureChangeDetails {
20+
oldFeature?: AnnotationFeatureSnapshot
21+
updatedFeature: AnnotationFeatureSnapshot
22+
}
23+
24+
interface SerializedReplaceFeatureChangeSingle
25+
extends SerializedReplaceFeatureChangeBase,
26+
ReplaceFeatureChangeDetails {}
27+
28+
interface SerializedReplaceFeatureChangeMultiple
29+
extends SerializedReplaceFeatureChangeBase {
30+
changes: ReplaceFeatureChangeDetails[]
31+
}
32+
33+
export type SerializedReplaceFeatureChange =
34+
| SerializedReplaceFeatureChangeSingle
35+
| SerializedReplaceFeatureChangeMultiple
36+
37+
export class ReplaceFeatureChange extends FeatureChange {
38+
typeName = 'ReplaceFeatureChange' as const
39+
changes: ReplaceFeatureChangeDetails[]
40+
41+
constructor(json: SerializedReplaceFeatureChange, options?: ChangeOptions) {
42+
super(json, options)
43+
this.changes = 'changes' in json ? json.changes : [json]
44+
this.changedIds = this.changes.map((change) => change.updatedFeature._id)
45+
}
46+
47+
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
48+
get notification() {
49+
return 'Feature replaced successfully'
50+
}
51+
52+
toJSON(): SerializedReplaceFeatureChange {
53+
const { assembly, changedIds, changes, typeName } = this
54+
if (changes.length === 1) {
55+
const [{ oldFeature, updatedFeature }] = changes
56+
return {
57+
typeName,
58+
changedIds,
59+
assembly,
60+
oldFeature,
61+
updatedFeature,
62+
}
63+
}
64+
return { typeName, changedIds, assembly, changes }
65+
}
66+
67+
async executeOnServer(backend: ServerDataStore) {
68+
const { featureModel, session } = backend
69+
const { changes, logger } = this
70+
71+
const { INDEXED_IDS } = process.env
72+
const idsToIndex = INDEXED_IDS ? INDEXED_IDS.split(',') : undefined
73+
74+
for (const [, change] of changes.entries()) {
75+
const targetFeatureId = change.updatedFeature._id
76+
const existingFeature = await featureModel
77+
.findOne({ allIds: targetFeatureId })
78+
.session(session)
79+
.exec()
80+
81+
if (!existingFeature) {
82+
throw new Error(
83+
`Could not find feature "${targetFeatureId}" to replace`,
84+
)
85+
}
86+
if (!existingFeature._id.equals(targetFeatureId)) {
87+
throw new Error(
88+
`Updated feature's _id "${change.updatedFeature._id}" does not match the db feature id "${targetFeatureId}"`,
89+
)
90+
}
91+
if (!soSequenceTypes.includes(change.updatedFeature.type)) {
92+
throw new Error(
93+
`"${change.updatedFeature.type}" is not a valid SO sequence_feature term`,
94+
)
95+
}
96+
if (existingFeature.type !== change.updatedFeature.type) {
97+
throw new Error(
98+
`Feature type mismatch. Existing feature is "${existingFeature.type}" but replacement is "${change.updatedFeature.type}"`,
99+
)
100+
}
101+
102+
change.oldFeature = featureToSnapshot(existingFeature)
103+
const { updatedFeature } = change
104+
const refSeq = existingFeature.refSeq.toString()
105+
106+
existingFeature.type = updatedFeature.type
107+
existingFeature.min = updatedFeature.min
108+
existingFeature.max = updatedFeature.max
109+
existingFeature.strand = updatedFeature.strand
110+
111+
existingFeature.attributes = updatedFeature.attributes
112+
? Object.fromEntries(
113+
Object.entries(updatedFeature.attributes).map(([key, value]) => [
114+
key,
115+
value ? [...value] : [],
116+
]),
117+
)
118+
: undefined
119+
existingFeature.children = snapshotChildrenToMap(
120+
updatedFeature.children,
121+
refSeq,
122+
)
123+
124+
existingFeature.allIds = [
125+
updatedFeature._id,
126+
...this.getChildFeatureIds(updatedFeature),
127+
]
128+
const indexedIds = this.getIndexedIds(updatedFeature, idsToIndex)
129+
existingFeature.indexedIds =
130+
indexedIds.length > 0 ? indexedIds : undefined
131+
132+
existingFeature.markModified('attributes')
133+
existingFeature.markModified('children')
134+
existingFeature.markModified('allIds')
135+
existingFeature.markModified('indexedIds')
136+
await existingFeature.save()
137+
138+
logger.debug?.(`Replaced top-level feature "${existingFeature.id}"`)
139+
}
140+
}
141+
142+
async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
143+
throw new Error('executeOnLocalGFF3 not implemented')
144+
}
145+
146+
async executeOnClient(_dataStore: ClientDataStore) {
147+
throw new Error('executeOnClient not implemented')
148+
}
149+
150+
getInverse(): never {
151+
throw new Error('getInverse not implemented')
152+
}
153+
}
154+
155+
function featureToSnapshot(
156+
featureDoc: FeatureDocument | Feature,
157+
): AnnotationFeatureSnapshot {
158+
const rawFeature = (
159+
'toObject' in featureDoc
160+
? featureDoc.toObject({ flattenMaps: true })
161+
: featureDoc
162+
) as {
163+
refSeq: Feature['refSeq']
164+
type: string
165+
min: number
166+
max: number
167+
strand?: 1 | -1
168+
attributes?: Record<string, string[]>
169+
children?: Feature['children']
170+
}
171+
172+
return {
173+
_id: featureDoc._id.toString(),
174+
refSeq: rawFeature.refSeq.toString(),
175+
type: rawFeature.type,
176+
min: rawFeature.min,
177+
max: rawFeature.max,
178+
strand: rawFeature.strand,
179+
attributes: rawFeature.attributes,
180+
children: rawFeature.children
181+
? Object.fromEntries(
182+
getChildFeatures(rawFeature.children).map((child) => [
183+
child._id.toString(),
184+
featureToSnapshot(child),
185+
]),
186+
)
187+
: undefined,
188+
}
189+
}
190+
191+
function getChildFeatures(
192+
children: NonNullable<Feature['children']> | Record<string, Feature>,
193+
) {
194+
return children instanceof Map
195+
? [...children.values()]
196+
: Object.values(children)
197+
}
198+
199+
function snapshotChildrenToMap(
200+
children?: Record<string, AnnotationFeatureSnapshot>,
201+
refSeq?: string,
202+
): FeatureDocument['children'] {
203+
if (!children) {
204+
return undefined
205+
}
206+
207+
return new Map(
208+
Object.values(children)
209+
.map(
210+
(child) => [child._id, snapshotToNestedFeature(child, refSeq)] as const,
211+
)
212+
.sort((a, b) => a[1].min - b[1].min),
213+
)
214+
}
215+
216+
function snapshotToNestedFeature(
217+
feature: AnnotationFeatureSnapshot,
218+
refSeq?: string,
219+
): Feature {
220+
return {
221+
allIds: [],
222+
...feature,
223+
_id: feature._id as unknown as Feature['_id'],
224+
refSeq: refSeq as unknown as Feature['refSeq'],
225+
attributes: feature.attributes
226+
? Object.fromEntries(
227+
Object.entries(feature.attributes).map(([key, value]) => [
228+
key,
229+
value ? [...value] : [],
230+
]),
231+
)
232+
: undefined,
233+
children: snapshotChildrenToMap(feature.children, refSeq),
234+
} as unknown as Feature
235+
}

packages/apollo-shared/src/Changes/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { LocationEndChange } from './LocationEndChange.js'
1414
import { LocationStartChange } from './LocationStartChange.js'
1515
import { MergeExonsChange } from './MergeExonsChange.js'
1616
import { MergeTranscriptsChange } from './MergeTranscriptsChange.js'
17+
import { ReplaceFeatureChange } from './ReplaceFeatureChange.js'
1718
import { SplitExonChange } from './SplitExonChange.js'
1819
import { StrandChange } from './StrandChange.js'
1920
import { TypeChange } from './TypeChange.js'
@@ -43,6 +44,7 @@ export const changes = {
4344
UndoMergeTranscriptsChange,
4445
StrandChange,
4546
TypeChange,
47+
ReplaceFeatureChange,
4648
UserChange,
4749
AddRefSeqAliasesChange,
4850
AddAssemblyAliasesChange,
@@ -68,6 +70,7 @@ export * from './UndoSplitExonChange.js'
6870
export * from './UndoMergeTranscriptsChange.js'
6971
export * from './StrandChange.js'
7072
export * from './TypeChange.js'
73+
export * from './ReplaceFeatureChange.js'
7174
export * from './UserChange.js'
7275
export * from './AddRefSeqAliasesChange.js'
7376
export * from './AddAssemblyAliasesChange.js'

0 commit comments

Comments
 (0)