@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.
- package/README.md +1 -0
- package/package.json +49 -0
- package/src/Changes/AddAssemblyAndFeaturesFromFileChange.ts +135 -0
- package/src/Changes/AddAssemblyFromExternalChange.ts +145 -0
- package/src/Changes/AddAssemblyFromFileChange.ts +125 -0
- package/src/Changes/AddFeatureChange.ts +233 -0
- package/src/Changes/AddFeaturesFromFileChange.ts +120 -0
- package/src/Changes/DeleteAssemblyChange.ts +100 -0
- package/src/Changes/DeleteFeatureChange.ts +200 -0
- package/src/Changes/DeleteUserChange.ts +83 -0
- package/src/Changes/DiscontinuousLocationEndChange.ts +182 -0
- package/src/Changes/DiscontinuousLocationStartChange.ts +182 -0
- package/src/Changes/FeatureAttributeChange.ts +164 -0
- package/src/Changes/LocationEndChange.ts +175 -0
- package/src/Changes/LocationStartChange.ts +175 -0
- package/src/Changes/StrandChange.ts +159 -0
- package/src/Changes/TypeChange.ts +159 -0
- package/src/Changes/UserChange.ts +82 -0
- package/src/Changes/index.ts +52 -0
- package/src/Checks/CDSCheck.ts +275 -0
- package/src/Checks/index.ts +1 -0
- package/src/Common/index.ts +1 -0
- package/src/Common/jwtPayload.ts +25 -0
- package/src/Messages.ts +32 -0
- package/src/Operations/GetAssembliesOperation.ts +28 -0
- package/src/Operations/GetFeaturesOperation.ts +58 -0
- package/src/Operations/index.ts +6 -0
- package/src/Validations/CoreValidation.ts +37 -0
- package/src/Validations/ParentChildValidation.ts +88 -0
- package/src/Validations/Validation.ts +55 -0
- package/src/Validations/ValidationSet.ts +106 -0
- package/src/Validations/index.ts +4 -0
- package/src/Validations/soSequenceTypes.ts +1865 -0
- package/src/index.ts +7 -0
- package/src/util.ts +109 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { Check } from '@apollo-annotation/common'
|
|
2
|
+
import {
|
|
3
|
+
AnnotationFeatureSnapshot,
|
|
4
|
+
CheckResultSnapshot,
|
|
5
|
+
} from '@apollo-annotation/mst'
|
|
6
|
+
import ObjectID from 'bson-objectid'
|
|
7
|
+
|
|
8
|
+
enum STOP_CODONS {
|
|
9
|
+
'TAG',
|
|
10
|
+
'TAA',
|
|
11
|
+
'TGA',
|
|
12
|
+
}
|
|
13
|
+
|
|
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
|
+
}
|
|
31
|
+
|
|
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
|
+
}
|
|
47
|
+
|
|
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
|
+
}
|
|
68
|
+
|
|
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
|
+
}
|
|
90
|
+
|
|
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
|
+
}
|
|
98
|
+
|
|
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
|
+
}
|
|
154
|
+
|
|
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
|
+
}
|
|
225
|
+
|
|
226
|
+
export class CDSCheck extends Check {
|
|
227
|
+
name = 'CDSCheck'
|
|
228
|
+
version = 1
|
|
229
|
+
default = true
|
|
230
|
+
|
|
231
|
+
async checkFeature(
|
|
232
|
+
feature: AnnotationFeatureSnapshot,
|
|
233
|
+
getSequence: (start: number, end: number) => Promise<string>,
|
|
234
|
+
): Promise<CheckResultSnapshot[]> {
|
|
235
|
+
if (feature.type === 'CDS') {
|
|
236
|
+
return checkCDS(feature, getSequence)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!feature.children) {
|
|
240
|
+
return []
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (feature.type !== 'mRNA') {
|
|
244
|
+
const checkResults: CheckResultSnapshot[] = []
|
|
245
|
+
for (const child of Object.values(feature.children)) {
|
|
246
|
+
checkResults.push(...(await this.checkFeature(child, getSequence)))
|
|
247
|
+
}
|
|
248
|
+
return checkResults
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const cdsChildren = Object.values(feature.children).filter(
|
|
252
|
+
(child) => child.type === 'CDS',
|
|
253
|
+
)
|
|
254
|
+
if (cdsChildren.length === 0) {
|
|
255
|
+
throw new Error(`mRNA "${feature._id}" has no CDS children`)
|
|
256
|
+
}
|
|
257
|
+
const cdsChildrenWithDiscontinuousLocations = cdsChildren.filter(
|
|
258
|
+
(child) =>
|
|
259
|
+
child.discontinuousLocations && child.discontinuousLocations.length > 0,
|
|
260
|
+
)
|
|
261
|
+
if (cdsChildrenWithDiscontinuousLocations.length === 0) {
|
|
262
|
+
return checkCDS(cdsChildren, getSequence)
|
|
263
|
+
}
|
|
264
|
+
if (cdsChildrenWithDiscontinuousLocations.length === cdsChildren.length) {
|
|
265
|
+
const checkResults: CheckResultSnapshot[] = []
|
|
266
|
+
for (const child of cdsChildren) {
|
|
267
|
+
checkResults.push(...(await this.checkFeature(child, getSequence)))
|
|
268
|
+
}
|
|
269
|
+
return checkResults
|
|
270
|
+
}
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Mix of CDS with and without discontinuous locations found in mRNA "${feature._id}"`,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CDSCheck'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './jwtPayload'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import jwtDecode from 'jwt-decode'
|
|
2
|
+
|
|
3
|
+
export interface JWTPayload {
|
|
4
|
+
username: string
|
|
5
|
+
email: string
|
|
6
|
+
role?: 'admin' | 'user' | 'readOnly'
|
|
7
|
+
id: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DecodedJWT extends JWTPayload {
|
|
11
|
+
iat: number
|
|
12
|
+
exp: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function makeUserSessionId(userOrToken: DecodedJWT | string): string {
|
|
16
|
+
const user =
|
|
17
|
+
typeof userOrToken === 'string'
|
|
18
|
+
? jwtDecode<DecodedJWT>(userOrToken)
|
|
19
|
+
: userOrToken
|
|
20
|
+
return `${user.id}-${user.iat}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDecodedToken(token: string): DecodedJWT {
|
|
24
|
+
return jwtDecode<DecodedJWT>(token)
|
|
25
|
+
}
|
package/src/Messages.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SerializedChange } from '@apollo-annotation/common'
|
|
2
|
+
import { CheckResultSnapshot } from '@apollo-annotation/mst'
|
|
3
|
+
|
|
4
|
+
interface BaseMessage {
|
|
5
|
+
channel: string
|
|
6
|
+
userName: string
|
|
7
|
+
userSessionId: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ChangeMessage extends BaseMessage {
|
|
11
|
+
changeInfo: SerializedChange
|
|
12
|
+
changeSequence: number
|
|
13
|
+
}
|
|
14
|
+
export interface CheckResultUpdate extends BaseMessage {
|
|
15
|
+
checkResult: CheckResultSnapshot
|
|
16
|
+
deleted?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UserLocation {
|
|
20
|
+
assemblyId: string
|
|
21
|
+
refSeq: string
|
|
22
|
+
start: number
|
|
23
|
+
end: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UserLocationMessage extends BaseMessage {
|
|
27
|
+
locations: UserLocation[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RequestUserInformationMessage extends BaseMessage {
|
|
31
|
+
readonly reqType: 'CURRENT_LOCATION'
|
|
32
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
import {
|
|
3
|
+
LocalGFF3DataStore,
|
|
4
|
+
Operation,
|
|
5
|
+
SerializedOperation,
|
|
6
|
+
ServerDataStore,
|
|
7
|
+
} from '@apollo-annotation/common'
|
|
8
|
+
|
|
9
|
+
interface SerializedGetAssembliesOperation extends SerializedOperation {
|
|
10
|
+
typeName: 'GetAssembliesOperation'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class GetAssembliesOperation extends Operation {
|
|
14
|
+
typeName = 'GetAssembliesOperation' as const
|
|
15
|
+
|
|
16
|
+
toJSON(): SerializedGetAssembliesOperation {
|
|
17
|
+
const { typeName } = this
|
|
18
|
+
return { typeName }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
executeOnServer(backend: ServerDataStore) {
|
|
22
|
+
return backend.assemblyModel.find({ status: 0 }).exec()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
|
|
26
|
+
throw new Error('executeOnLocalGFF3 not implemented')
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
import {
|
|
3
|
+
LocalGFF3DataStore,
|
|
4
|
+
Operation,
|
|
5
|
+
OperationOptions,
|
|
6
|
+
SerializedOperation,
|
|
7
|
+
ServerDataStore,
|
|
8
|
+
} from '@apollo-annotation/common'
|
|
9
|
+
|
|
10
|
+
interface SerializedGetFeaturesOperation extends SerializedOperation {
|
|
11
|
+
typeName: 'GetFeaturesOperation'
|
|
12
|
+
refSeq: string
|
|
13
|
+
start: number
|
|
14
|
+
end: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class GetFeaturesOperation extends Operation {
|
|
18
|
+
typeName = 'GetFeaturesOperation' as const
|
|
19
|
+
refSeq: string
|
|
20
|
+
start: number
|
|
21
|
+
end: number
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
json: SerializedGetFeaturesOperation,
|
|
25
|
+
options?: OperationOptions,
|
|
26
|
+
) {
|
|
27
|
+
super(json, options)
|
|
28
|
+
this.refSeq = json.refSeq
|
|
29
|
+
this.start = json.start
|
|
30
|
+
this.end = json.end
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
toJSON(): SerializedGetFeaturesOperation {
|
|
34
|
+
const { end, refSeq, start, typeName } = this
|
|
35
|
+
return { typeName, refSeq, start, end }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fetch features based on Reference seq, Start and End -values
|
|
40
|
+
* @param request - Contain search criteria i.e. refSeq, start and end -parameters
|
|
41
|
+
* @returns Return Array of features if search was successful
|
|
42
|
+
* or if search data was not found or in case of error throw exception
|
|
43
|
+
*/
|
|
44
|
+
executeOnServer(backend: ServerDataStore) {
|
|
45
|
+
return backend.featureModel
|
|
46
|
+
.find({
|
|
47
|
+
refSeq: this.refSeq,
|
|
48
|
+
start: { $lte: this.end },
|
|
49
|
+
end: { $gte: this.start },
|
|
50
|
+
status: 0,
|
|
51
|
+
})
|
|
52
|
+
.exec()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
|
|
56
|
+
throw new Error('executeOnLocalGFF3 not implemented')
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { GetAssembliesOperation } from './GetAssembliesOperation'
|
|
2
|
+
import { GetFeaturesOperation } from './GetFeaturesOperation'
|
|
3
|
+
|
|
4
|
+
export const operations = { GetAssembliesOperation, GetFeaturesOperation }
|
|
5
|
+
export * from './GetAssembliesOperation'
|
|
6
|
+
export * from './GetFeaturesOperation'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
import { Change } from '@apollo-annotation/common'
|
|
3
|
+
|
|
4
|
+
import { TypeChange } from '../Changes'
|
|
5
|
+
import soSequenceTypes from './soSequenceTypes'
|
|
6
|
+
import { Validation } from './Validation'
|
|
7
|
+
|
|
8
|
+
export function isTypeChange(thing: Change): thing is TypeChange {
|
|
9
|
+
return 'oldType' in thing && 'newType' in thing
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CoreValidation extends Validation {
|
|
13
|
+
name = 'Core' as const
|
|
14
|
+
|
|
15
|
+
async frontendPreValidate(change: Change) {
|
|
16
|
+
if (isTypeChange(change)) {
|
|
17
|
+
for (const subChange of change.changes) {
|
|
18
|
+
if (!soSequenceTypes.includes(subChange.newType)) {
|
|
19
|
+
return {
|
|
20
|
+
validationName: this.name,
|
|
21
|
+
error: {
|
|
22
|
+
message: `"${subChange.newType}" is not a valid SO sequence_feature term`,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { validationName: this.name }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async possibleValues(key: string) {
|
|
32
|
+
if (key === 'type') {
|
|
33
|
+
return soSequenceTypes
|
|
34
|
+
}
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
2
|
+
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
3
|
+
import { Change } from '@apollo-annotation/common'
|
|
4
|
+
import { Feature, FeatureDocument } from '@apollo-annotation/schemas'
|
|
5
|
+
import { ClientSession, Model } from 'mongoose'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
LocationEndChange,
|
|
9
|
+
LocationStartChange,
|
|
10
|
+
isLocationEndChange,
|
|
11
|
+
isLocationStartChange,
|
|
12
|
+
} from '../Changes'
|
|
13
|
+
import { Validation, ValidationResult } from './Validation'
|
|
14
|
+
|
|
15
|
+
export class ParentChildValidation extends Validation {
|
|
16
|
+
name = 'ParentChildValidation' as const
|
|
17
|
+
|
|
18
|
+
async backendPostValidate(
|
|
19
|
+
change: Change,
|
|
20
|
+
{
|
|
21
|
+
featureModel,
|
|
22
|
+
session,
|
|
23
|
+
}: { session: ClientSession; featureModel: Model<FeatureDocument> },
|
|
24
|
+
): Promise<ValidationResult> {
|
|
25
|
+
if (isLocationEndChange(change) || isLocationStartChange(change)) {
|
|
26
|
+
return this.validateParentChildRelationships(change, {
|
|
27
|
+
session,
|
|
28
|
+
featureModel,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
return { validationName: this.name }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async validateParentChildRelationships(
|
|
35
|
+
change: LocationEndChange | LocationStartChange,
|
|
36
|
+
{
|
|
37
|
+
featureModel,
|
|
38
|
+
session,
|
|
39
|
+
}: { session: ClientSession; featureModel: Model<FeatureDocument> },
|
|
40
|
+
): Promise<ValidationResult> {
|
|
41
|
+
const topLevelFeatures: FeatureDocument[] = []
|
|
42
|
+
for (const ch of change.changes) {
|
|
43
|
+
const { featureId } = ch
|
|
44
|
+
|
|
45
|
+
// Search correct feature
|
|
46
|
+
const topLevelFeature = await featureModel
|
|
47
|
+
.findOne({ allIds: featureId })
|
|
48
|
+
.session(session)
|
|
49
|
+
.exec()
|
|
50
|
+
|
|
51
|
+
if (!topLevelFeature) {
|
|
52
|
+
const errMsg = `ERROR: The following featureId was not found in database ='${featureId}'`
|
|
53
|
+
throw new Error(errMsg)
|
|
54
|
+
}
|
|
55
|
+
if (!topLevelFeatures.some((f) => f._id === topLevelFeature._id)) {
|
|
56
|
+
topLevelFeatures.push(topLevelFeature)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const topLevelFeature of topLevelFeatures) {
|
|
60
|
+
try {
|
|
61
|
+
this.checkChildFeatureBoundaries(topLevelFeature)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { validationName: this.name, error: { message: String(error) } }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { validationName: this.name }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
checkChildFeatureBoundaries(feature: Feature) {
|
|
70
|
+
if (!feature.children) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
for (const [, childFeature] of feature.children || new Map()) {
|
|
74
|
+
if (
|
|
75
|
+
feature.start !== null &&
|
|
76
|
+
feature.end !== null &&
|
|
77
|
+
childFeature.start !== null &&
|
|
78
|
+
childFeature.end !== null &&
|
|
79
|
+
(childFeature.end > feature.end || childFeature.start < feature.start)
|
|
80
|
+
) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Feature "${childFeature._id}" exceeds the bounds of its parent, "${feature._id}"`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
this.checkChildFeatureBoundaries(childFeature)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
2
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
3
|
+
import { Change, ClientDataStore } from '@apollo-annotation/common'
|
|
4
|
+
import { FeatureDocument } from '@apollo-annotation/schemas'
|
|
5
|
+
import type { ExecutionContext } from '@nestjs/common'
|
|
6
|
+
import type { Reflector } from '@nestjs/core'
|
|
7
|
+
import { ClientSession, Model } from 'mongoose'
|
|
8
|
+
|
|
9
|
+
export interface Context {
|
|
10
|
+
context: ExecutionContext
|
|
11
|
+
reflector: Reflector
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isContext(thing: Change | Context): thing is Context {
|
|
15
|
+
return (
|
|
16
|
+
(thing as Context).context !== undefined &&
|
|
17
|
+
(thing as Context).reflector !== undefined
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ValidationResult {
|
|
22
|
+
validationName: string
|
|
23
|
+
error?: { message: string }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export abstract class Validation {
|
|
27
|
+
abstract name: string
|
|
28
|
+
async frontendPreValidate(_change: Change): Promise<ValidationResult> {
|
|
29
|
+
return { validationName: this.name }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async frontendPostValidate(
|
|
33
|
+
_change: Change,
|
|
34
|
+
_dataStore: ClientDataStore,
|
|
35
|
+
): Promise<ValidationResult> {
|
|
36
|
+
return { validationName: this.name }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async backendPreValidate(
|
|
40
|
+
_changeOrContext: Change | Context,
|
|
41
|
+
): Promise<ValidationResult> {
|
|
42
|
+
return { validationName: this.name }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async backendPostValidate(
|
|
46
|
+
_change: Change,
|
|
47
|
+
_context: { session: ClientSession; featureModel: Model<FeatureDocument> },
|
|
48
|
+
): Promise<ValidationResult> {
|
|
49
|
+
return { validationName: this.name }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async possibleValues(_key: string): Promise<unknown[] | undefined> {
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
}
|