@apollo-annotation/shared 0.1.11

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 (36) hide show
  1. package/README.md +1 -0
  2. package/package.json +49 -0
  3. package/src/Changes/AddAssemblyAndFeaturesFromFileChange.ts +135 -0
  4. package/src/Changes/AddAssemblyFromExternalChange.ts +145 -0
  5. package/src/Changes/AddAssemblyFromFileChange.ts +125 -0
  6. package/src/Changes/AddFeatureChange.ts +233 -0
  7. package/src/Changes/AddFeaturesFromFileChange.ts +120 -0
  8. package/src/Changes/DeleteAssemblyChange.ts +100 -0
  9. package/src/Changes/DeleteFeatureChange.ts +200 -0
  10. package/src/Changes/DeleteUserChange.ts +83 -0
  11. package/src/Changes/DiscontinuousLocationEndChange.ts +182 -0
  12. package/src/Changes/DiscontinuousLocationStartChange.ts +182 -0
  13. package/src/Changes/FeatureAttributeChange.ts +164 -0
  14. package/src/Changes/LocationEndChange.ts +175 -0
  15. package/src/Changes/LocationStartChange.ts +175 -0
  16. package/src/Changes/StrandChange.ts +159 -0
  17. package/src/Changes/TypeChange.ts +159 -0
  18. package/src/Changes/UserChange.ts +82 -0
  19. package/src/Changes/index.ts +52 -0
  20. package/src/Checks/CDSCheck.ts +275 -0
  21. package/src/Checks/index.ts +1 -0
  22. package/src/Common/index.ts +1 -0
  23. package/src/Common/jwtPayload.ts +25 -0
  24. package/src/Messages.ts +32 -0
  25. package/src/Operations/GetAssembliesOperation.ts +28 -0
  26. package/src/Operations/GetFeaturesOperation.ts +58 -0
  27. package/src/Operations/index.ts +6 -0
  28. package/src/Validations/CoreValidation.ts +37 -0
  29. package/src/Validations/ParentChildValidation.ts +88 -0
  30. package/src/Validations/Validation.ts +55 -0
  31. package/src/Validations/ValidationSet.ts +106 -0
  32. package/src/Validations/index.ts +4 -0
  33. package/src/Validations/soSequenceTypes.ts +1865 -0
  34. package/src/index.ts +7 -0
  35. package/src/util.ts +109 -0
  36. package/tsconfig.json +19 -0
@@ -0,0 +1,120 @@
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ import {
3
+ AssemblySpecificChange,
4
+ ChangeOptions,
5
+ ClientDataStore,
6
+ LocalGFF3DataStore,
7
+ SerializedAssemblySpecificChange,
8
+ ServerDataStore,
9
+ } from '@apollo-annotation/common'
10
+ import { GFF3Feature } from '@gmod/gff'
11
+
12
+ export interface SerializedAddFeaturesFromFileChangeBase
13
+ extends SerializedAssemblySpecificChange {
14
+ typeName: 'AddFeaturesFromFileChange'
15
+ deleteExistingFeatures?: boolean
16
+ }
17
+
18
+ export interface AddFeaturesFromFileChangeDetails {
19
+ fileId: string
20
+ }
21
+
22
+ export interface SerializedAddFeaturesFromFileChangeSingle
23
+ extends SerializedAddFeaturesFromFileChangeBase,
24
+ AddFeaturesFromFileChangeDetails {}
25
+
26
+ export interface SerializedAddFeaturesFromFileChangeMultiple
27
+ extends SerializedAddFeaturesFromFileChangeBase {
28
+ changes: AddFeaturesFromFileChangeDetails[]
29
+ }
30
+
31
+ export type SerializedAddFeaturesFromFileChange =
32
+ | SerializedAddFeaturesFromFileChangeSingle
33
+ | SerializedAddFeaturesFromFileChangeMultiple
34
+
35
+ export class AddFeaturesFromFileChange extends AssemblySpecificChange {
36
+ typeName = 'AddFeaturesFromFileChange' as const
37
+ changes: AddFeaturesFromFileChangeDetails[]
38
+ deleteExistingFeatures = false
39
+
40
+ constructor(
41
+ json: SerializedAddFeaturesFromFileChange,
42
+ options?: ChangeOptions,
43
+ ) {
44
+ super(json, options)
45
+ this.deleteExistingFeatures = json.deleteExistingFeatures ?? false
46
+ this.changes = 'changes' in json ? json.changes : [json]
47
+ }
48
+
49
+ // eslint-disable-next-line @typescript-eslint/class-literal-property-style
50
+ get notification(): string {
51
+ return 'Features have been added. To see them, please refresh the page.'
52
+ }
53
+
54
+ toJSON(): SerializedAddFeaturesFromFileChange {
55
+ const { assembly, changes, deleteExistingFeatures, typeName } = this
56
+ if (changes.length === 1) {
57
+ const [{ fileId }] = changes
58
+ return { typeName, assembly, fileId, deleteExistingFeatures }
59
+ }
60
+ return { typeName, assembly, changes, deleteExistingFeatures }
61
+ }
62
+
63
+ /**
64
+ * Applies the required change to database
65
+ * @param backend - parameters from backend
66
+ * @returns
67
+ */
68
+ async executeOnServer(backend: ServerDataStore) {
69
+ const { fileModel, filesService } = backend
70
+ const { changes, deleteExistingFeatures, logger } = this
71
+
72
+ if (deleteExistingFeatures) {
73
+ await this.removeExistingFeatures(backend)
74
+ }
75
+
76
+ for (const change of changes) {
77
+ const { fileId } = change
78
+
79
+ const { FILE_UPLOAD_FOLDER } = process.env
80
+ if (!FILE_UPLOAD_FOLDER) {
81
+ throw new Error('No FILE_UPLOAD_FOLDER found in .env file')
82
+ }
83
+ // Get file checksum
84
+ const fileDoc = await fileModel.findById(fileId).exec()
85
+ if (!fileDoc) {
86
+ throw new Error(`File "${fileId}" not found in Mongo`)
87
+ }
88
+ logger.debug?.(`FileId "${fileId}", checksum "${fileDoc.checksum}"`)
89
+
90
+ // Read data from compressed file and parse the content
91
+ const featureStream = filesService.parseGFF3(
92
+ filesService.getFileStream(fileDoc),
93
+ )
94
+ for await (const f of featureStream) {
95
+ const gff3Feature = f as GFF3Feature
96
+ logger.verbose?.(`ENTRY=${JSON.stringify(gff3Feature)}`)
97
+
98
+ // Add new feature into database
99
+ // We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
100
+ await this.addFeatureIntoDb(gff3Feature, backend)
101
+ }
102
+ }
103
+ logger.debug?.('New features added into database!')
104
+ }
105
+
106
+ async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
107
+ throw new Error('executeOnLocalGFF3 not implemented')
108
+ }
109
+
110
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
111
+ async executeOnClient(_dataStore: ClientDataStore) {}
112
+
113
+ getInverse() {
114
+ const { assembly, changes, logger, typeName } = this
115
+ return new AddFeaturesFromFileChange(
116
+ { typeName, changes, assembly },
117
+ { logger },
118
+ )
119
+ }
120
+ }
@@ -0,0 +1,100 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
2
+ /* eslint-disable @typescript-eslint/require-await */
3
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
5
+ import {
6
+ AssemblySpecificChange,
7
+ ClientDataStore,
8
+ LocalGFF3DataStore,
9
+ SerializedAssemblySpecificChange,
10
+ ServerDataStore,
11
+ } from '@apollo-annotation/common'
12
+ import { getSession } from '@jbrowse/core/util'
13
+
14
+ interface SerializedDeleteAssemblyChange
15
+ extends SerializedAssemblySpecificChange {
16
+ typeName: 'DeleteAssemblyChange'
17
+ }
18
+ export class DeleteAssemblyChange extends AssemblySpecificChange {
19
+ typeName = 'DeleteAssemblyChange' as const
20
+
21
+ get notification(): string {
22
+ return `Assembly "${this.assembly}" deleted successfully.`
23
+ }
24
+
25
+ toJSON(): SerializedDeleteAssemblyChange {
26
+ const { assembly, typeName } = this
27
+ return { typeName, assembly }
28
+ }
29
+
30
+ /**
31
+ * Applies the required change to database
32
+ * @param backend - parameters from backend
33
+ * @returns
34
+ */
35
+ async executeOnServer(backend: ServerDataStore) {
36
+ const {
37
+ assemblyModel,
38
+ featureModel,
39
+ refSeqChunkModel,
40
+ refSeqModel,
41
+ session,
42
+ } = backend
43
+ const { assembly, logger } = this
44
+
45
+ const assemblyDoc = await assemblyModel
46
+ .findById(assembly)
47
+ .session(session)
48
+ .exec()
49
+ if (!assemblyDoc) {
50
+ const errMsg = `*** ERROR: Assembly with id "${assembly}" not found`
51
+ logger.error(errMsg)
52
+ throw new Error(errMsg)
53
+ }
54
+
55
+ // We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
56
+
57
+ // Get RefSeqs
58
+ const refSeqs = await refSeqModel.find({ assembly }).exec()
59
+ const refSeqIds = refSeqs.map((refSeq) => refSeq._id)
60
+
61
+ // Get and delete RefSeqChunks
62
+ await refSeqChunkModel.deleteMany({ refSeq: refSeqIds }).exec()
63
+
64
+ // Get and delete Features
65
+ await featureModel.deleteMany({ refSeq: refSeqIds }).exec()
66
+
67
+ // Delete RefSeqs and Assembly
68
+ await refSeqModel.deleteMany({ assembly }).exec()
69
+ await assemblyModel.findByIdAndDelete(assembly).exec()
70
+
71
+ logger.debug?.(`Assembly "${assembly}" deleted from database.`)
72
+ }
73
+
74
+ async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
75
+ throw new Error('executeOnLocalGFF3 not implemented')
76
+ }
77
+
78
+ async executeOnClient(dataStore: ClientDataStore) {
79
+ const { assembly } = this
80
+ if (!dataStore) {
81
+ throw new Error('No data store')
82
+ }
83
+ const session = getSession(dataStore)
84
+ // If assemblyId is not present in client data store
85
+ if (dataStore.assemblies.has(assembly)) {
86
+ dataStore.deleteAssembly(assembly)
87
+ }
88
+ await session.removeAssembly?.(assembly)
89
+ // @ts-expect-error this isn't on the AbstractSessionModel
90
+ await session.removeSessionAssembly?.(assembly)
91
+ }
92
+
93
+ getInverse() {
94
+ const { assembly, logger } = this
95
+ return new DeleteAssemblyChange(
96
+ { typeName: 'DeleteAssemblyChange', assembly },
97
+ { logger },
98
+ )
99
+ }
100
+ }
@@ -0,0 +1,200 @@
1
+ /* eslint-disable @typescript-eslint/restrict-template-expressions */
2
+ /* eslint-disable @typescript-eslint/require-await */
3
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
4
+ import {
5
+ ChangeOptions,
6
+ ClientDataStore,
7
+ FeatureChange,
8
+ LocalGFF3DataStore,
9
+ SerializedFeatureChange,
10
+ ServerDataStore,
11
+ } from '@apollo-annotation/common'
12
+ import { AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
13
+ import { Feature } from '@apollo-annotation/schemas'
14
+
15
+ import { AddFeatureChange } from './AddFeatureChange'
16
+
17
+ interface SerializedDeleteFeatureChangeBase extends SerializedFeatureChange {
18
+ typeName: 'DeleteFeatureChange'
19
+ }
20
+
21
+ export interface DeleteFeatureChangeDetails {
22
+ deletedFeature: AnnotationFeatureSnapshot
23
+ parentFeatureId?: string // Parent feature from where feature was deleted.
24
+ }
25
+
26
+ interface SerializedDeleteFeatureChangeSingle
27
+ extends SerializedDeleteFeatureChangeBase,
28
+ DeleteFeatureChangeDetails {}
29
+
30
+ interface SerializedDeleteFeatureChangeMultiple
31
+ extends SerializedDeleteFeatureChangeBase {
32
+ changes: DeleteFeatureChangeDetails[]
33
+ }
34
+
35
+ type SerializedDeleteFeatureChange =
36
+ | SerializedDeleteFeatureChangeSingle
37
+ | SerializedDeleteFeatureChangeMultiple
38
+
39
+ export class DeleteFeatureChange extends FeatureChange {
40
+ typeName = 'DeleteFeatureChange' as const
41
+ changes: DeleteFeatureChangeDetails[]
42
+
43
+ constructor(json: SerializedDeleteFeatureChange, options?: ChangeOptions) {
44
+ super(json, options)
45
+ this.changes = 'changes' in json ? json.changes : [json]
46
+ }
47
+
48
+ toJSON(): SerializedDeleteFeatureChange {
49
+ const { assembly, changedIds, changes, typeName } = this
50
+ if (changes.length === 1) {
51
+ const [{ deletedFeature, parentFeatureId }] = changes
52
+ return { typeName, changedIds, assembly, deletedFeature, parentFeatureId }
53
+ }
54
+ return { typeName, changedIds, assembly, changes }
55
+ }
56
+
57
+ /**
58
+ * Applies the required change to database
59
+ * @param backend - parameters from backend
60
+ * @returns
61
+ */
62
+ async executeOnServer(backend: ServerDataStore) {
63
+ const { featureModel, session } = backend
64
+ const { changes, logger } = this
65
+
66
+ // Loop the changes
67
+ for (const change of changes) {
68
+ const { deletedFeature, parentFeatureId } = change
69
+
70
+ // Search feature
71
+ const featureDoc = await featureModel
72
+ .findOne({ allIds: deletedFeature._id })
73
+ .session(session)
74
+ .exec()
75
+ if (!featureDoc) {
76
+ const errMsg = `*** ERROR: The following featureId was not found in database ='${deletedFeature._id}'`
77
+ logger.error(errMsg)
78
+ throw new Error(errMsg)
79
+ }
80
+
81
+ // Check if feature is on top level, then simply delete the whole document (i.e. not just sub-feature inside document)
82
+ if (featureDoc._id.equals(deletedFeature._id)) {
83
+ if (parentFeatureId) {
84
+ throw new Error(
85
+ `Feature "${deletedFeature._id}" is top-level, but received a parent feature ID`,
86
+ )
87
+ }
88
+ await featureModel.findByIdAndDelete(featureDoc._id)
89
+ logger.debug?.(
90
+ `Feature "${deletedFeature._id}" deleted from document "${featureDoc._id}". Whole document deleted.`,
91
+ )
92
+ continue
93
+ }
94
+
95
+ const deletedIds = this.findAndDeleteChildFeature(
96
+ featureDoc,
97
+ deletedFeature._id,
98
+ )
99
+ deletedIds.push(deletedFeature._id)
100
+ featureDoc.allIds = featureDoc.allIds.filter(
101
+ (id) => !deletedIds.includes(id),
102
+ )
103
+ // Save updated document in Mongo
104
+ featureDoc.markModified('children') // Mark as modified. Without this save() -method is not updating data in database
105
+ try {
106
+ await featureDoc.save()
107
+ } catch (error) {
108
+ logger.debug?.(`*** FAILED: ${error}`)
109
+ throw error
110
+ }
111
+
112
+ logger.debug?.(
113
+ `Feature "${deletedFeature._id}" deleted from document "${featureDoc._id}"`,
114
+ )
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Delete feature's subfeatures that match an ID and return the IDs of any
120
+ * sub-subfeatures that were deleted
121
+ * @param feature -
122
+ * @param featureIdToDelete -
123
+ * @returns - list of deleted feature IDs
124
+ */
125
+ findAndDeleteChildFeature(
126
+ feature: Feature,
127
+ featureIdToDelete: string,
128
+ ): string[] {
129
+ if (!feature.children) {
130
+ throw new Error(`Feature ${feature._id} has no children`)
131
+ }
132
+ const { _id, children } = feature
133
+ const child = children.get(featureIdToDelete)
134
+ if (child) {
135
+ const deletedIds = this.getChildFeatureIds(child)
136
+ children.delete(featureIdToDelete)
137
+ return deletedIds
138
+ }
139
+ for (const [, childFeature] of children) {
140
+ try {
141
+ return this.findAndDeleteChildFeature(childFeature, featureIdToDelete)
142
+ } catch {
143
+ // pass
144
+ }
145
+ }
146
+
147
+ throw new Error(`Feature "${featureIdToDelete}" not found in ${_id}`)
148
+ }
149
+
150
+ async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
151
+ throw new Error('executeOnLocalGFF3 not implemented')
152
+ }
153
+
154
+ async executeOnClient(dataStore: ClientDataStore) {
155
+ if (!dataStore) {
156
+ throw new Error('No data store')
157
+ }
158
+ for (const change of this.changes) {
159
+ const { deletedFeature, parentFeatureId } = change
160
+ if (parentFeatureId) {
161
+ const parentFeature = dataStore.getFeature(parentFeatureId)
162
+ if (!parentFeature) {
163
+ throw new Error(`Could not find parent feature "${parentFeatureId}"`)
164
+ }
165
+ parentFeature.deleteChild(deletedFeature._id)
166
+ } else {
167
+ if (dataStore.getFeature(deletedFeature._id)) {
168
+ dataStore.deleteFeature(deletedFeature._id)
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ getInverse() {
175
+ const { assembly, changedIds, changes, logger } = this
176
+ const inverseChangedIds = [...changedIds].reverse()
177
+ const inverseChanges = [...changes]
178
+ .reverse()
179
+ .map((deleteFeatuerChange) => ({
180
+ addedFeature: deleteFeatuerChange.deletedFeature,
181
+ parentFeatureId: deleteFeatuerChange.parentFeatureId,
182
+ }))
183
+ logger.debug?.(`INVERSE CHANGE '${JSON.stringify(inverseChanges)}'`)
184
+ return new AddFeatureChange(
185
+ {
186
+ changedIds: inverseChangedIds,
187
+ typeName: 'AddFeatureChange',
188
+ changes: inverseChanges,
189
+ assembly,
190
+ },
191
+ { logger },
192
+ )
193
+ }
194
+ }
195
+
196
+ export function isDeleteFeatureChange(
197
+ change: unknown,
198
+ ): change is DeleteFeatureChange {
199
+ return (change as DeleteFeatureChange).typeName === 'DeleteFeatureChange'
200
+ }
@@ -0,0 +1,83 @@
1
+ /* eslint-disable @typescript-eslint/require-await */
2
+ import {
3
+ Change,
4
+ ChangeOptions,
5
+ ClientDataStore,
6
+ LocalGFF3DataStore,
7
+ SerializedChange,
8
+ ServerDataStore,
9
+ } from '@apollo-annotation/common'
10
+
11
+ export interface SerializedDeleteUserChangeBase extends SerializedChange {
12
+ typeName: 'DeleteUserChange'
13
+ userId: string
14
+ }
15
+
16
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
17
+ export interface DeleteUserChangeDetails {}
18
+
19
+ interface SerializedDeleteUserChangeSingle
20
+ extends SerializedDeleteUserChangeBase,
21
+ DeleteUserChangeDetails {}
22
+
23
+ interface SerializedDeleteUserChangeMultiple
24
+ extends SerializedDeleteUserChangeBase {
25
+ changes: DeleteUserChangeDetails[]
26
+ }
27
+
28
+ export type SerializedDeleteUserChange =
29
+ | SerializedDeleteUserChangeSingle
30
+ | SerializedDeleteUserChangeMultiple
31
+
32
+ export class DeleteUserChange extends Change {
33
+ typeName = 'DeleteUserChange' as const
34
+ changes: DeleteUserChangeDetails[]
35
+ userId: string
36
+
37
+ constructor(json: SerializedDeleteUserChange, options?: ChangeOptions) {
38
+ super(json, options)
39
+ this.changes = 'changes' in json ? json.changes : [json]
40
+ this.userId = json.userId
41
+ }
42
+
43
+ toJSON(): SerializedDeleteUserChange {
44
+ const { typeName, userId } = this
45
+ return { typeName, userId }
46
+ }
47
+
48
+ async executeOnServer(backend: ServerDataStore) {
49
+ const { session, userModel } = backend
50
+ const { logger, userId } = this
51
+ const user = await userModel
52
+ .findOneAndDelete({ _id: userId })
53
+ .session(session)
54
+ .exec()
55
+ if (!user) {
56
+ const errMsg = `*** ERROR: User with id "${userId}" not found`
57
+ logger.error(errMsg)
58
+ throw new Error(errMsg)
59
+ }
60
+ }
61
+
62
+ async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
63
+ throw new Error('executeOnLocalGFF3 not implemented')
64
+ }
65
+
66
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
67
+ async executeOnClient(_dataStore: ClientDataStore) {}
68
+
69
+ getInverse() {
70
+ const { logger, typeName, userId } = this
71
+ return new DeleteUserChange({ typeName, userId }, { logger })
72
+ // const inverseChangedIds = this.changedIds.slice().reverse()
73
+ // const inverseChanges = this.changes
74
+ // .slice()
75
+ // .reverse()
76
+ // .map((deleteUserChange) => ({
77
+ // addedUser: deleteUserChange.userId,
78
+ // }))
79
+ // this.logger.debug?.(`INVERSE CHANGE '${JSON.stringify(inverseChanges)}'`)
80
+ // // return new AddUserChange()
81
+ // }
82
+ }
83
+ }
@@ -0,0 +1,182 @@
1
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2
+ /* eslint-disable @typescript-eslint/restrict-template-expressions */
3
+ /* eslint-disable @typescript-eslint/require-await */
4
+ import {
5
+ ChangeOptions,
6
+ ClientDataStore,
7
+ FeatureChange,
8
+ LocalGFF3DataStore,
9
+ SerializedFeatureChange,
10
+ ServerDataStore,
11
+ } from '@apollo-annotation/common'
12
+
13
+ interface SerializedDiscontinuousLocationEndChangeBase
14
+ extends SerializedFeatureChange {
15
+ typeName: 'DiscontinuousLocationEndChange'
16
+ }
17
+
18
+ interface DiscontinuousLocationEndChangeDetails {
19
+ featureId: string
20
+ oldEnd: number
21
+ newEnd: number
22
+ index: number
23
+ }
24
+
25
+ interface SerializedDiscontinuousLocationEndChangeSingle
26
+ extends SerializedDiscontinuousLocationEndChangeBase,
27
+ DiscontinuousLocationEndChangeDetails {}
28
+
29
+ interface SerializedDiscontinuousLocationEndChangeMultiple
30
+ extends SerializedDiscontinuousLocationEndChangeBase {
31
+ changes: DiscontinuousLocationEndChangeDetails[]
32
+ }
33
+
34
+ type SerializedDiscontinuousLocationEndChange =
35
+ | SerializedDiscontinuousLocationEndChangeSingle
36
+ | SerializedDiscontinuousLocationEndChangeMultiple
37
+
38
+ export class DiscontinuousLocationEndChange extends FeatureChange {
39
+ typeName = 'DiscontinuousLocationEndChange' as const
40
+ changes: DiscontinuousLocationEndChangeDetails[]
41
+
42
+ constructor(
43
+ json: SerializedDiscontinuousLocationEndChange,
44
+ options?: ChangeOptions,
45
+ ) {
46
+ super(json, options)
47
+ this.changes = 'changes' in json ? json.changes : [json]
48
+ }
49
+
50
+ toJSON(): SerializedDiscontinuousLocationEndChange {
51
+ const { assembly, changedIds, changes, typeName } = this
52
+ if (changes.length === 1) {
53
+ const [{ featureId, index, newEnd, oldEnd }] = changes
54
+ return {
55
+ typeName,
56
+ changedIds,
57
+ assembly,
58
+ featureId,
59
+ oldEnd,
60
+ newEnd,
61
+ index,
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
+ for (const change of changes) {
71
+ const { featureId, index, newEnd, oldEnd: expectedOldEnd } = change
72
+ const topLevelFeature = await featureModel
73
+ .findOne({ allIds: featureId })
74
+ .session(session)
75
+ .exec()
76
+
77
+ if (!topLevelFeature) {
78
+ const errMsg = `ERROR: The following featureId was not found in database ='${featureId}'`
79
+ logger.error(errMsg)
80
+ throw new Error(errMsg)
81
+ }
82
+
83
+ const feature = this.getFeatureFromId(topLevelFeature, featureId)
84
+ if (!feature) {
85
+ const errMsg = 'ERROR when searching feature by featureId'
86
+ logger.error(errMsg)
87
+ throw new Error(errMsg)
88
+ }
89
+ logger.debug?.(`*** Found feature: ${JSON.stringify(feature)}`)
90
+ if (
91
+ !feature.discontinuousLocations ||
92
+ feature.discontinuousLocations.length === 0
93
+ ) {
94
+ const errMsg =
95
+ 'Must use "LocationEndChange" to change a feature end that does not have discontinuous locations'
96
+ logger.error(errMsg)
97
+ throw new Error(errMsg)
98
+ }
99
+ const oldEnd = feature.discontinuousLocations[index].end
100
+ if (oldEnd !== expectedOldEnd) {
101
+ const errMsg = `Location's current end value ${oldEnd} doesn't match with expected value ${expectedOldEnd}`
102
+ logger.error(errMsg)
103
+ throw new Error(errMsg)
104
+ }
105
+ const { start } = feature.discontinuousLocations[index]
106
+ if (newEnd <= start) {
107
+ const errMsg = `location end (${newEnd}) can't be smaller than location start (${start})`
108
+ logger.error(errMsg)
109
+ throw new Error(errMsg)
110
+ }
111
+ const nextLocation = feature.discontinuousLocations[index + 1]
112
+ if (nextLocation && newEnd >= nextLocation.start) {
113
+ const errMsg = `Location end (${newEnd}) can't be larger than the next location's start (${nextLocation.start})`
114
+ logger.error(errMsg)
115
+ throw new Error(errMsg)
116
+ }
117
+ feature.discontinuousLocations[index].end = newEnd
118
+ if (index === feature.discontinuousLocations.length - 1) {
119
+ feature.end = newEnd
120
+ }
121
+
122
+ try {
123
+ if (topLevelFeature._id.equals(feature._id)) {
124
+ topLevelFeature.markModified('discontinuousLocations')
125
+ } else {
126
+ topLevelFeature.markModified('children')
127
+ }
128
+ await topLevelFeature.save()
129
+ } catch (error) {
130
+ logger.debug?.(`*** FAILED: ${error}`)
131
+ throw error
132
+ }
133
+ }
134
+ }
135
+
136
+ async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
137
+ throw new Error('executeOnLocalGFF3 not implemented')
138
+ }
139
+
140
+ async executeOnClient(dataStore: ClientDataStore) {
141
+ if (!dataStore) {
142
+ throw new Error('No data store')
143
+ }
144
+ for (const [idx, changedId] of this.changedIds.entries()) {
145
+ const feature = dataStore.getFeature(changedId)
146
+ if (!feature) {
147
+ throw new Error(`Could not find feature with identifier "${changedId}"`)
148
+ }
149
+ const { index, newEnd } = this.changes[idx]
150
+ feature.setCDSDiscontinuousLocationEnd(newEnd, index)
151
+ }
152
+ }
153
+
154
+ getInverse() {
155
+ const { assembly, changedIds, changes, logger, typeName } = this
156
+ const inverseChangedIds = [...changedIds].reverse()
157
+ const inverseChanges = [...changes].reverse().map((change) => ({
158
+ featureId: change.featureId,
159
+ oldEnd: change.newEnd,
160
+ newEnd: change.oldEnd,
161
+ index: change.index,
162
+ }))
163
+ return new DiscontinuousLocationEndChange(
164
+ {
165
+ changedIds: inverseChangedIds,
166
+ typeName,
167
+ changes: inverseChanges,
168
+ assembly,
169
+ },
170
+ { logger },
171
+ )
172
+ }
173
+ }
174
+
175
+ export function isDiscontinuousLocationEndChange(
176
+ change: unknown,
177
+ ): change is DiscontinuousLocationEndChange {
178
+ return (
179
+ (change as DiscontinuousLocationEndChange).typeName ===
180
+ 'DiscontinuousLocationEndChange'
181
+ )
182
+ }