@apollo-annotation/common 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 +43 -0
- package/src/ApolloPlugin.ts +13 -0
- package/src/AssemblySpecificChange.ts +468 -0
- package/src/Change.ts +80 -0
- package/src/ChangeTypeRegistry.ts +26 -0
- package/src/Check.ts +14 -0
- package/src/CheckRegistry.ts +27 -0
- package/src/FeatureChange.ts +118 -0
- package/src/Operation.ts +98 -0
- package/src/OperationTypeRegistry.ts +26 -0
- package/src/Validation.ts +52 -0
- package/src/index.ts +10 -0
- package/tsconfig.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# apollo-mst
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apollo-annotation/common",
|
|
3
|
+
"version": "0.1.11",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/GMOD/Apollo3.git",
|
|
7
|
+
"directory": "packages/apollo-common"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@apollo-annotation/schemas": "^0.1.11",
|
|
15
|
+
"@gmod/gff": "1.2.0",
|
|
16
|
+
"@jbrowse/core": "^2.7.0",
|
|
17
|
+
"bson-objectid": "^2.0.4",
|
|
18
|
+
"tslib": "^2.3.1"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@mui/material": "^5.11.14",
|
|
22
|
+
"@mui/x-data-grid": "^7.0.0",
|
|
23
|
+
"mobx": "^6.6.1",
|
|
24
|
+
"mobx-react": "^7.2.1",
|
|
25
|
+
"mobx-state-tree": "^5.1.7",
|
|
26
|
+
"prop-types": "^15.8.1",
|
|
27
|
+
"react": "^18.2.0",
|
|
28
|
+
"react-dom": "^18.2.0",
|
|
29
|
+
"rxjs": "^7.4.0",
|
|
30
|
+
"tss-react": "^4.6.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@apollo-annotation/mst": "^0.1.11",
|
|
34
|
+
"@nestjs/common": "^10.1.0",
|
|
35
|
+
"@nestjs/core": "^10.1.0",
|
|
36
|
+
"@types/node": "^18.14.2",
|
|
37
|
+
"mongoose": "^6.12.0",
|
|
38
|
+
"typescript": "^5.1.6"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Plugin from '@jbrowse/core/Plugin'
|
|
2
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
3
|
+
|
|
4
|
+
export type ApolloPluginConstructor = new (...args: unknown[]) => ApolloPlugin
|
|
5
|
+
|
|
6
|
+
export interface ApolloPluginManager {
|
|
7
|
+
addToExtensionPoint: PluginManager['addToExtensionPoint']
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export abstract class ApolloPlugin extends Plugin {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
12
|
+
apolloInstall(_pluginManager: ApolloPluginManager): void {}
|
|
13
|
+
}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
2
|
+
/* 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
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
6
|
+
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
7
|
+
import type { AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
|
|
8
|
+
import { FileDocument, RefSeqDocument } from '@apollo-annotation/schemas'
|
|
9
|
+
import { GFF3Feature } from '@gmod/gff'
|
|
10
|
+
import ObjectID from 'bson-objectid'
|
|
11
|
+
|
|
12
|
+
import { Change, ChangeOptions, SerializedChange, isChange } from './Change'
|
|
13
|
+
import { ServerDataStore } from './Operation'
|
|
14
|
+
|
|
15
|
+
export interface SerializedAssemblySpecificChange extends SerializedChange {
|
|
16
|
+
assembly: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isAssemblySpecificChange(
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
thing: any,
|
|
22
|
+
): thing is AssemblySpecificChange {
|
|
23
|
+
return (
|
|
24
|
+
isChange(thing) && (thing as AssemblySpecificChange).assembly !== undefined
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export abstract class AssemblySpecificChange extends Change {
|
|
29
|
+
assembly: string
|
|
30
|
+
|
|
31
|
+
constructor(json: SerializedAssemblySpecificChange, options?: ChangeOptions) {
|
|
32
|
+
super(json, options)
|
|
33
|
+
this.assembly = json.assembly
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async addRefSeqIntoDb(
|
|
37
|
+
fileDoc: FileDocument,
|
|
38
|
+
assembly: string,
|
|
39
|
+
backend: ServerDataStore,
|
|
40
|
+
) {
|
|
41
|
+
const { logger } = this
|
|
42
|
+
const { filesService, refSeqChunkModel, refSeqModel, user } = backend
|
|
43
|
+
const { CHUNK_SIZE } = process.env
|
|
44
|
+
const customChunkSize = CHUNK_SIZE && Number(CHUNK_SIZE)
|
|
45
|
+
let chunkIndex = 0
|
|
46
|
+
let refSeqLen = 0
|
|
47
|
+
let refSeqDoc: RefSeqDocument | undefined
|
|
48
|
+
let fastaInfoStarted = fileDoc.type !== 'text/x-gff3'
|
|
49
|
+
|
|
50
|
+
// Read data from compressed file and parse the content
|
|
51
|
+
const sequenceStream = filesService.getFileStream(fileDoc)
|
|
52
|
+
let sequenceBuffer = ''
|
|
53
|
+
let incompleteLine = ''
|
|
54
|
+
let lastLineIsIncomplete = true
|
|
55
|
+
let parsingStarted = false
|
|
56
|
+
logger.debug?.('starting sequence stream')
|
|
57
|
+
for await (const data of sequenceStream) {
|
|
58
|
+
const chunk = data.toString()
|
|
59
|
+
lastLineIsIncomplete = !chunk.endsWith('\n')
|
|
60
|
+
// chunk is small enough that you can split the whole thing into lines without having to make it into smaller chunks first.
|
|
61
|
+
const lines = chunk.split(/\r?\n/)
|
|
62
|
+
if (incompleteLine) {
|
|
63
|
+
lines[0] = `${incompleteLine}${lines[0]}`
|
|
64
|
+
incompleteLine = ''
|
|
65
|
+
}
|
|
66
|
+
if (lastLineIsIncomplete) {
|
|
67
|
+
incompleteLine = lines.pop() || ''
|
|
68
|
+
}
|
|
69
|
+
for await (const line of lines) {
|
|
70
|
+
// In case of GFF3 file we start to read sequence after '##FASTA' is found
|
|
71
|
+
if (!fastaInfoStarted) {
|
|
72
|
+
if (line.trim() === '##FASTA') {
|
|
73
|
+
fastaInfoStarted = true
|
|
74
|
+
}
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
const refSeqInfoLine = /^>\s*(\S+)\s*(.*)/.exec(line)
|
|
78
|
+
// Add new ref sequence info if we are reference seq info line
|
|
79
|
+
if (refSeqInfoLine) {
|
|
80
|
+
parsingStarted = true
|
|
81
|
+
logger.debug?.(
|
|
82
|
+
`Reference sequence information line "${refSeqInfoLine}"`,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// If there is sequence from previous reference sequence then we need to add it to previous ref seq
|
|
86
|
+
if (sequenceBuffer !== '') {
|
|
87
|
+
if (!refSeqDoc) {
|
|
88
|
+
throw new Error('No refSeq document found')
|
|
89
|
+
}
|
|
90
|
+
refSeqLen += sequenceBuffer.length
|
|
91
|
+
logger.debug?.(
|
|
92
|
+
`Creating refSeq chunk number ${chunkIndex} of "${refSeqDoc._id}"`,
|
|
93
|
+
)
|
|
94
|
+
// We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
|
|
95
|
+
await refSeqChunkModel.create([
|
|
96
|
+
{
|
|
97
|
+
refSeq: refSeqDoc._id,
|
|
98
|
+
n: chunkIndex,
|
|
99
|
+
sequence: sequenceBuffer,
|
|
100
|
+
user,
|
|
101
|
+
status: -1,
|
|
102
|
+
},
|
|
103
|
+
])
|
|
104
|
+
sequenceBuffer = ''
|
|
105
|
+
}
|
|
106
|
+
await refSeqDoc?.updateOne({ length: refSeqLen })
|
|
107
|
+
// await refSeqDoc?.updateOne({ length: refSeqLen }, { session })
|
|
108
|
+
refSeqLen = 0
|
|
109
|
+
chunkIndex = 0
|
|
110
|
+
|
|
111
|
+
const name = refSeqInfoLine[1].trim()
|
|
112
|
+
const description = refSeqInfoLine[2] ? refSeqInfoLine[2].trim() : ''
|
|
113
|
+
|
|
114
|
+
// We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
|
|
115
|
+
const [newRefSeqDoc] = await refSeqModel.create([
|
|
116
|
+
{
|
|
117
|
+
name,
|
|
118
|
+
description,
|
|
119
|
+
assembly,
|
|
120
|
+
length: 0,
|
|
121
|
+
...(customChunkSize ? { chunkSize: customChunkSize } : null),
|
|
122
|
+
user,
|
|
123
|
+
status: -1,
|
|
124
|
+
},
|
|
125
|
+
])
|
|
126
|
+
logger.debug?.(
|
|
127
|
+
`Added new refSeq "${name}", desc "${description}", docId "${newRefSeqDoc._id}"`,
|
|
128
|
+
)
|
|
129
|
+
refSeqDoc = newRefSeqDoc
|
|
130
|
+
} else if (/\S/.test(line)) {
|
|
131
|
+
if (!refSeqDoc) {
|
|
132
|
+
throw new Error('No refSeq document found')
|
|
133
|
+
}
|
|
134
|
+
const { _id, chunkSize } = refSeqDoc
|
|
135
|
+
sequenceBuffer += line.replaceAll(/\s/g, '')
|
|
136
|
+
// If sequence block > chunk size then save chunk into Mongo
|
|
137
|
+
while (sequenceBuffer.length >= chunkSize) {
|
|
138
|
+
const sequence = sequenceBuffer.slice(0, chunkSize)
|
|
139
|
+
refSeqLen += sequence.length
|
|
140
|
+
logger.debug?.(
|
|
141
|
+
`Creating refSeq chunk number ${chunkIndex} of "${_id}"`,
|
|
142
|
+
)
|
|
143
|
+
// We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
|
|
144
|
+
await refSeqChunkModel.create([
|
|
145
|
+
{ refSeq: _id, n: chunkIndex, sequence, user, status: -1 },
|
|
146
|
+
])
|
|
147
|
+
chunkIndex++
|
|
148
|
+
// Set remaining sequence
|
|
149
|
+
sequenceBuffer = sequenceBuffer.slice(chunkSize)
|
|
150
|
+
logger.debug?.(`Remaining sequence: "${sequenceBuffer}"`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!parsingStarted) {
|
|
156
|
+
throw new Error('No reference sequences found in file')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (sequenceBuffer || lastLineIsIncomplete) {
|
|
160
|
+
if (!refSeqDoc) {
|
|
161
|
+
throw new Error('No refSeq document found')
|
|
162
|
+
}
|
|
163
|
+
// If the file did not end with line break so the last line is incomplete
|
|
164
|
+
if (lastLineIsIncomplete) {
|
|
165
|
+
sequenceBuffer += incompleteLine
|
|
166
|
+
}
|
|
167
|
+
refSeqLen += sequenceBuffer.length
|
|
168
|
+
logger.verbose?.(
|
|
169
|
+
`*** Add the very last chunk to ref seq ("${refSeqDoc._id}", index ${chunkIndex} and total length for ref seq is ${refSeqLen}): "${sequenceBuffer}"`,
|
|
170
|
+
)
|
|
171
|
+
logger.debug?.(
|
|
172
|
+
`Creating refSeq chunk number ${chunkIndex} of "${refSeqDoc._id}"`,
|
|
173
|
+
)
|
|
174
|
+
// We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
|
|
175
|
+
await refSeqChunkModel.create([
|
|
176
|
+
{
|
|
177
|
+
refSeq: refSeqDoc._id,
|
|
178
|
+
n: chunkIndex,
|
|
179
|
+
sequence: sequenceBuffer,
|
|
180
|
+
user,
|
|
181
|
+
status: -1,
|
|
182
|
+
},
|
|
183
|
+
])
|
|
184
|
+
await refSeqDoc.updateOne({ length: refSeqLen })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private refSeqCache = new Map<string, RefSeqDocument>()
|
|
189
|
+
|
|
190
|
+
async removeExistingFeatures(backend: ServerDataStore) {
|
|
191
|
+
const { featureModel, refSeqModel } = backend
|
|
192
|
+
const { assembly, logger } = this
|
|
193
|
+
logger.debug?.(`Removing existing features for assembly = ${assembly}`)
|
|
194
|
+
|
|
195
|
+
const refSeqs: RefSeqDocument[] = await refSeqModel
|
|
196
|
+
.find({ assembly })
|
|
197
|
+
.exec()
|
|
198
|
+
|
|
199
|
+
for (const refSeq of refSeqs) {
|
|
200
|
+
await featureModel.deleteMany({ refSeq: refSeq._id })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async addFeatureIntoDb(gff3Feature: GFF3Feature, backend: ServerDataStore) {
|
|
205
|
+
const { featureModel, refSeqModel, user } = backend
|
|
206
|
+
const { assembly, logger, refSeqCache } = this
|
|
207
|
+
|
|
208
|
+
const [{ seq_id: refName }] = gff3Feature
|
|
209
|
+
if (!refName) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Valid seq_id not found in feature ${JSON.stringify(gff3Feature)}`,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
let refSeqDoc = refSeqCache.get(refName)
|
|
215
|
+
if (!refSeqDoc) {
|
|
216
|
+
refSeqDoc =
|
|
217
|
+
(await refSeqModel.findOne({ assembly, name: refName }).exec()) ??
|
|
218
|
+
undefined
|
|
219
|
+
if (refSeqDoc) {
|
|
220
|
+
refSeqCache.set(refName, refSeqDoc)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!refSeqDoc) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`RefSeq was not found by assembly "${assembly}" and seq_id "${refName}" not found`,
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
// Let's add featureId to parent feature
|
|
229
|
+
const featureIds: string[] = []
|
|
230
|
+
|
|
231
|
+
const newFeature = createFeature(gff3Feature, refSeqDoc._id, featureIds)
|
|
232
|
+
logger.debug?.(`So far feature ids are: ${featureIds.toString()}`)
|
|
233
|
+
// Add value to gffId
|
|
234
|
+
newFeature.attributes?._id
|
|
235
|
+
? (newFeature.gffId = newFeature.attributes?._id.toString())
|
|
236
|
+
: (newFeature.gffId = newFeature._id)
|
|
237
|
+
logger.debug?.(
|
|
238
|
+
`********************* Assembly specific change create ${JSON.stringify(
|
|
239
|
+
newFeature,
|
|
240
|
+
)}`,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
// Add into Mongo
|
|
244
|
+
// We cannot use Mongo 'session' / transaction here because Mongo has 16 MB limit for transaction
|
|
245
|
+
const [newFeatureDoc] = await featureModel.create([
|
|
246
|
+
{ allIds: featureIds, ...newFeature, user, status: -1 },
|
|
247
|
+
])
|
|
248
|
+
logger.verbose?.(`Added docId "${newFeatureDoc._id}"`)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function createFeature(
|
|
253
|
+
gff3Feature: GFF3Feature,
|
|
254
|
+
refSeq: string,
|
|
255
|
+
featureIds?: string[],
|
|
256
|
+
): AnnotationFeatureSnapshot {
|
|
257
|
+
const [firstFeature] = gff3Feature
|
|
258
|
+
const {
|
|
259
|
+
attributes,
|
|
260
|
+
child_features: childFeatures,
|
|
261
|
+
end,
|
|
262
|
+
phase,
|
|
263
|
+
score,
|
|
264
|
+
seq_id: refName,
|
|
265
|
+
source,
|
|
266
|
+
start,
|
|
267
|
+
strand,
|
|
268
|
+
type,
|
|
269
|
+
} = firstFeature
|
|
270
|
+
if (!refName) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`feature does not have seq_id: ${JSON.stringify(firstFeature)}`,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
if (!type) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`feature does not have type: ${JSON.stringify(firstFeature)}`,
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
if (start === null) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`feature does not have start: ${JSON.stringify(firstFeature)}`,
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
if (end === null) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`feature does not have end: ${JSON.stringify(firstFeature)}`,
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
const feature: AnnotationFeatureSnapshot = {
|
|
291
|
+
_id: new ObjectID().toHexString(),
|
|
292
|
+
gffId: '',
|
|
293
|
+
refSeq,
|
|
294
|
+
type,
|
|
295
|
+
start: start - 1,
|
|
296
|
+
end,
|
|
297
|
+
}
|
|
298
|
+
if (gff3Feature.length > 1) {
|
|
299
|
+
const lastEnd = Math.max(
|
|
300
|
+
...gff3Feature.map((f) => {
|
|
301
|
+
if (f.end === null) {
|
|
302
|
+
throw new Error(`feature does not have end: ${JSON.stringify(f)}`)
|
|
303
|
+
}
|
|
304
|
+
return f.end
|
|
305
|
+
}),
|
|
306
|
+
)
|
|
307
|
+
feature.end = lastEnd
|
|
308
|
+
feature.discontinuousLocations = gff3Feature.map((f) => {
|
|
309
|
+
const { end: subEnd, phase: locationPhase, start: subStart } = f
|
|
310
|
+
if (subStart === null || subEnd === null) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`feature does not have start and/or end: ${JSON.stringify(f)}`,
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
let parsedPhase: 0 | 1 | 2 | undefined
|
|
316
|
+
if (locationPhase) {
|
|
317
|
+
switch (locationPhase) {
|
|
318
|
+
case '0': {
|
|
319
|
+
parsedPhase = 0
|
|
320
|
+
|
|
321
|
+
break
|
|
322
|
+
}
|
|
323
|
+
case '1': {
|
|
324
|
+
parsedPhase = 1
|
|
325
|
+
|
|
326
|
+
break
|
|
327
|
+
}
|
|
328
|
+
case '2': {
|
|
329
|
+
parsedPhase = 2
|
|
330
|
+
|
|
331
|
+
break
|
|
332
|
+
}
|
|
333
|
+
default: {
|
|
334
|
+
throw new Error(`Unknown phase: "${locationPhase}"`)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { start: subStart - 1, end: subEnd, phase: parsedPhase }
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
if (strand) {
|
|
342
|
+
if (strand === '+') {
|
|
343
|
+
feature.strand = 1
|
|
344
|
+
} else if (strand === '-') {
|
|
345
|
+
feature.strand = -1
|
|
346
|
+
} else {
|
|
347
|
+
throw new Error(`Unknown strand: "${strand}"`)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (score !== null) {
|
|
351
|
+
feature.score = score
|
|
352
|
+
}
|
|
353
|
+
if (phase) {
|
|
354
|
+
switch (phase) {
|
|
355
|
+
case '0': {
|
|
356
|
+
feature.phase = 0
|
|
357
|
+
|
|
358
|
+
break
|
|
359
|
+
}
|
|
360
|
+
case '1': {
|
|
361
|
+
feature.phase = 1
|
|
362
|
+
|
|
363
|
+
break
|
|
364
|
+
}
|
|
365
|
+
case '2': {
|
|
366
|
+
feature.phase = 2
|
|
367
|
+
|
|
368
|
+
break
|
|
369
|
+
}
|
|
370
|
+
default: {
|
|
371
|
+
throw new Error(`Unknown phase: "${phase}"`)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (featureIds) {
|
|
376
|
+
featureIds.push(feature._id)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (childFeatures?.length) {
|
|
380
|
+
const children: Record<string, AnnotationFeatureSnapshot> = {}
|
|
381
|
+
for (const childFeature of childFeatures) {
|
|
382
|
+
const child = createFeature(childFeature, refSeq, featureIds)
|
|
383
|
+
children[child._id] = child
|
|
384
|
+
// Add value to gffId
|
|
385
|
+
child.attributes?._id
|
|
386
|
+
? (child.gffId = child.attributes?._id.toString())
|
|
387
|
+
: (child.gffId = child._id)
|
|
388
|
+
}
|
|
389
|
+
feature.children = children
|
|
390
|
+
}
|
|
391
|
+
if (source ?? attributes) {
|
|
392
|
+
const attrs: Record<string, string[]> = {}
|
|
393
|
+
if (source) {
|
|
394
|
+
attrs.source = [source]
|
|
395
|
+
}
|
|
396
|
+
if (attributes) {
|
|
397
|
+
for (const [key, val] of Object.entries(attributes)) {
|
|
398
|
+
if (val) {
|
|
399
|
+
const newKey = key.toLowerCase()
|
|
400
|
+
if (newKey !== 'parent') {
|
|
401
|
+
// attrs[key.toLowerCase()] = val
|
|
402
|
+
switch (key) {
|
|
403
|
+
case 'ID': {
|
|
404
|
+
attrs._id = val
|
|
405
|
+
break
|
|
406
|
+
}
|
|
407
|
+
case 'Name': {
|
|
408
|
+
attrs.gff_name = val
|
|
409
|
+
break
|
|
410
|
+
}
|
|
411
|
+
case 'Alias': {
|
|
412
|
+
attrs.gff_alias = val
|
|
413
|
+
break
|
|
414
|
+
}
|
|
415
|
+
case 'Target': {
|
|
416
|
+
attrs.gff_target = val
|
|
417
|
+
break
|
|
418
|
+
}
|
|
419
|
+
case 'Gap': {
|
|
420
|
+
attrs.gff_gap = val
|
|
421
|
+
break
|
|
422
|
+
}
|
|
423
|
+
case 'Derives_from': {
|
|
424
|
+
attrs.gff_derives_from = val
|
|
425
|
+
break
|
|
426
|
+
}
|
|
427
|
+
case 'Note': {
|
|
428
|
+
attrs.gff_note = val
|
|
429
|
+
break
|
|
430
|
+
}
|
|
431
|
+
case 'Dbxref': {
|
|
432
|
+
attrs.gff_dbxref = val
|
|
433
|
+
break
|
|
434
|
+
}
|
|
435
|
+
case 'Ontology_term': {
|
|
436
|
+
const goTerms: string[] = []
|
|
437
|
+
const otherTerms: string[] = []
|
|
438
|
+
for (const v of val) {
|
|
439
|
+
if (v.startsWith('GO:')) {
|
|
440
|
+
goTerms.push(v)
|
|
441
|
+
} else {
|
|
442
|
+
otherTerms.push(v)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (goTerms.length > 0) {
|
|
446
|
+
attrs['Gene Ontology'] = goTerms
|
|
447
|
+
}
|
|
448
|
+
if (otherTerms.length > 0) {
|
|
449
|
+
attrs.gff_ontology_term = otherTerms
|
|
450
|
+
}
|
|
451
|
+
break
|
|
452
|
+
}
|
|
453
|
+
case 'Is_circular': {
|
|
454
|
+
attrs.gff_is_circular = val
|
|
455
|
+
break
|
|
456
|
+
}
|
|
457
|
+
default: {
|
|
458
|
+
attrs[key.toLowerCase()] = val
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
feature.attributes = attrs
|
|
466
|
+
}
|
|
467
|
+
return feature
|
|
468
|
+
}
|
package/src/Change.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
2
|
+
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
3
|
+
import {
|
|
4
|
+
AnnotationFeatureI,
|
|
5
|
+
AnnotationFeatureSnapshot,
|
|
6
|
+
ApolloAssemblyI,
|
|
7
|
+
CheckResultI,
|
|
8
|
+
CheckResultSnapshot,
|
|
9
|
+
} from '@apollo-annotation/mst'
|
|
10
|
+
import { AppRootModel, Region } from '@jbrowse/core/util'
|
|
11
|
+
|
|
12
|
+
import { changeRegistry } from './ChangeTypeRegistry'
|
|
13
|
+
import {
|
|
14
|
+
BackendDataStore,
|
|
15
|
+
Operation,
|
|
16
|
+
OperationOptions,
|
|
17
|
+
SerializedOperation,
|
|
18
|
+
} from './Operation'
|
|
19
|
+
|
|
20
|
+
export interface ClientDataStore {
|
|
21
|
+
typeName: 'Client'
|
|
22
|
+
assemblies: Map<string, ApolloAssemblyI>
|
|
23
|
+
checkResults: Map<string, CheckResultI>
|
|
24
|
+
internetAccounts: AppRootModel['internetAccounts']
|
|
25
|
+
getInternetAccount(
|
|
26
|
+
assemblyName?: string,
|
|
27
|
+
internetAccountId?: string,
|
|
28
|
+
): AppRootModel['internetAccounts'][0]
|
|
29
|
+
loadFeatures(regions: Region[]): void
|
|
30
|
+
loadRefSeq(regions: Region[]): void
|
|
31
|
+
getFeature(featureId: string): AnnotationFeatureI | undefined
|
|
32
|
+
addFeature(assemblyId: string, feature: AnnotationFeatureSnapshot): void
|
|
33
|
+
deleteFeature(featureId: string): void
|
|
34
|
+
deleteAssembly(assemblyId: string): void
|
|
35
|
+
addCheckResults(checkResults: CheckResultSnapshot[]): void
|
|
36
|
+
addAssembly(assemblyId: string): ApolloAssemblyI
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SerializedChange = SerializedOperation
|
|
40
|
+
export type ChangeOptions = OperationOptions
|
|
41
|
+
|
|
42
|
+
export type DataStore = BackendDataStore | ClientDataStore
|
|
43
|
+
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
export function isChange(thing: any): thing is Change {
|
|
46
|
+
return (thing as Change).executeOnClient !== undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export abstract class Change extends Operation {
|
|
50
|
+
/**
|
|
51
|
+
* If a non-empty string, a snackbar will display in JBrowse with this message
|
|
52
|
+
* when a successful response is received from the server.
|
|
53
|
+
*/
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
|
|
55
|
+
get notification(): string {
|
|
56
|
+
return ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static fromJSON(json: SerializedOperation, options?: ChangeOptions): Change {
|
|
60
|
+
const ChangeType = changeRegistry.getChangeType(json.typeName)
|
|
61
|
+
return new ChangeType(json, options?.logger && { logger: options.logger })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async execute(backend: DataStore): Promise<unknown> {
|
|
65
|
+
const backendType = backend.typeName
|
|
66
|
+
if (backendType === 'LocalGFF3' || backendType === 'Server') {
|
|
67
|
+
return super.execute(backend)
|
|
68
|
+
}
|
|
69
|
+
if (backendType === 'Client') {
|
|
70
|
+
return this.executeOnClient(backend)
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
`no change implementation for backend type '${backendType}'`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
abstract executeOnClient(backend: ClientDataStore): Promise<void>
|
|
78
|
+
|
|
79
|
+
abstract getInverse(): Change
|
|
80
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Change } from './Change'
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
type ChangeType = new (...args: any[]) => Change
|
|
5
|
+
|
|
6
|
+
class ChangeTypeRegistry {
|
|
7
|
+
changes = new Map<string, ChangeType>()
|
|
8
|
+
|
|
9
|
+
registerChange(name: string, changeType: ChangeType): void {
|
|
10
|
+
if (this.changes.has(name)) {
|
|
11
|
+
throw new Error(`change type "${name}" has already been registered`)
|
|
12
|
+
}
|
|
13
|
+
this.changes.set(name, changeType)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getChangeType(name: string): ChangeType {
|
|
17
|
+
const RegisteredChangeType = this.changes.get(name)
|
|
18
|
+
if (!RegisteredChangeType) {
|
|
19
|
+
throw new Error(`No change constructor registered for "${name}"`)
|
|
20
|
+
}
|
|
21
|
+
return RegisteredChangeType
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** global singleton of all known types of changes */
|
|
26
|
+
export const changeRegistry = new ChangeTypeRegistry()
|
package/src/Check.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnnotationFeatureSnapshot,
|
|
3
|
+
CheckResultSnapshot,
|
|
4
|
+
} from '@apollo-annotation/mst'
|
|
5
|
+
|
|
6
|
+
export abstract class Check {
|
|
7
|
+
abstract name: string
|
|
8
|
+
abstract version: number
|
|
9
|
+
|
|
10
|
+
abstract checkFeature(
|
|
11
|
+
feature: AnnotationFeatureSnapshot,
|
|
12
|
+
getSequence: (start: number, end: number) => Promise<string>,
|
|
13
|
+
): Promise<CheckResultSnapshot[]>
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Check } from './Check'
|
|
2
|
+
|
|
3
|
+
class CheckRegistry {
|
|
4
|
+
checks = new Map<string, Check>()
|
|
5
|
+
|
|
6
|
+
registerCheck(name: string, check: Check): void {
|
|
7
|
+
if (this.checks.has(name)) {
|
|
8
|
+
throw new Error(`check "${name}" has already been registered`)
|
|
9
|
+
}
|
|
10
|
+
this.checks.set(name, check)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getCheck(name: string): Check {
|
|
14
|
+
const registeredCheck = this.checks.get(name)
|
|
15
|
+
if (!registeredCheck) {
|
|
16
|
+
throw new Error(`No check constructor registered for "${name}"`)
|
|
17
|
+
}
|
|
18
|
+
return registeredCheck
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getChecks() {
|
|
22
|
+
return this.checks
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** global singleton of all known checks */
|
|
27
|
+
export const checkRegistry = new CheckRegistry()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
3
|
+
import type { AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
|
|
4
|
+
import { Feature } from '@apollo-annotation/schemas'
|
|
5
|
+
import ObjectID from 'bson-objectid'
|
|
6
|
+
import type { Types } from 'mongoose'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AssemblySpecificChange,
|
|
10
|
+
SerializedAssemblySpecificChange,
|
|
11
|
+
isAssemblySpecificChange,
|
|
12
|
+
} from './AssemblySpecificChange'
|
|
13
|
+
import { ChangeOptions } from './Change'
|
|
14
|
+
|
|
15
|
+
export interface SerializedFeatureChange
|
|
16
|
+
extends SerializedAssemblySpecificChange {
|
|
17
|
+
/** The IDs of features that were changed in this operation */
|
|
18
|
+
changedIds: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
export function isFeatureChange(thing: any): thing is FeatureChange {
|
|
23
|
+
return (
|
|
24
|
+
isAssemblySpecificChange(thing) &&
|
|
25
|
+
(thing as FeatureChange).changedIds !== undefined
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export abstract class FeatureChange extends AssemblySpecificChange {
|
|
30
|
+
changedIds: string[]
|
|
31
|
+
|
|
32
|
+
constructor(json: SerializedFeatureChange, options?: ChangeOptions) {
|
|
33
|
+
super(json, options)
|
|
34
|
+
this.changedIds = json.changedIds
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get single feature by featureId
|
|
39
|
+
* @param feature -
|
|
40
|
+
* @param featureId -
|
|
41
|
+
* @returns
|
|
42
|
+
*/
|
|
43
|
+
getFeatureFromId(feature: Feature, featureId: string): Feature | null {
|
|
44
|
+
const { logger } = this
|
|
45
|
+
logger.verbose?.(`Entry=${JSON.stringify(feature)}`)
|
|
46
|
+
|
|
47
|
+
if (feature._id.equals(featureId)) {
|
|
48
|
+
logger.debug?.(
|
|
49
|
+
`Top level featureId matches in the object ${JSON.stringify(feature)}`,
|
|
50
|
+
)
|
|
51
|
+
return feature
|
|
52
|
+
}
|
|
53
|
+
// Check if there is also childFeatures in parent feature and it's not empty
|
|
54
|
+
// Let's get featureId from recursive method
|
|
55
|
+
logger.debug?.(
|
|
56
|
+
'FeatureId was not found on top level so lets make recursive call...',
|
|
57
|
+
)
|
|
58
|
+
for (const [, childFeature] of feature.children ?? new Map()) {
|
|
59
|
+
const subFeature = this.getFeatureFromId(childFeature, featureId)
|
|
60
|
+
if (subFeature) {
|
|
61
|
+
return subFeature
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get children's feature ids
|
|
69
|
+
* @param feature - parent feature
|
|
70
|
+
* @returns
|
|
71
|
+
*/
|
|
72
|
+
getChildFeatureIds(feature: Feature | AnnotationFeatureSnapshot): string[] {
|
|
73
|
+
if (!feature.children) {
|
|
74
|
+
return []
|
|
75
|
+
}
|
|
76
|
+
const featureIds = []
|
|
77
|
+
const children =
|
|
78
|
+
feature.children instanceof Map
|
|
79
|
+
? feature.children
|
|
80
|
+
: new Map(Object.entries(feature.children))
|
|
81
|
+
for (const [childFeatureId, childFeature] of children || new Map()) {
|
|
82
|
+
featureIds.push(childFeatureId, ...this.getChildFeatureIds(childFeature))
|
|
83
|
+
}
|
|
84
|
+
return featureIds
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Recursively assign new IDs to a feature
|
|
89
|
+
* @param feature - Parent feature
|
|
90
|
+
* @param featureIds -
|
|
91
|
+
*/
|
|
92
|
+
generateNewIds(
|
|
93
|
+
feature: Feature | AnnotationFeatureSnapshot,
|
|
94
|
+
featureIds: string[],
|
|
95
|
+
): AnnotationFeatureSnapshot {
|
|
96
|
+
const newId = new ObjectID().toHexString()
|
|
97
|
+
featureIds.push(newId)
|
|
98
|
+
|
|
99
|
+
const children: Record<string, AnnotationFeatureSnapshot> = {}
|
|
100
|
+
if (feature.children) {
|
|
101
|
+
for (const child of Object.values(feature.children)) {
|
|
102
|
+
const newChild = this.generateNewIds(child, featureIds)
|
|
103
|
+
children[newChild._id] = newChild
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const refSeq =
|
|
107
|
+
typeof feature.refSeq === 'string'
|
|
108
|
+
? feature.refSeq
|
|
109
|
+
: (feature.refSeq as unknown as Types.ObjectId).toHexString()
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...feature,
|
|
113
|
+
refSeq,
|
|
114
|
+
children: feature.children && children,
|
|
115
|
+
_id: newId,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/Operation.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-confusing-void-expression */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
3
|
+
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
4
|
+
import type { ReadStream } from 'node:fs'
|
|
5
|
+
import type { FileHandle } from 'node:fs/promises'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
AssemblyDocument,
|
|
9
|
+
FeatureDocument,
|
|
10
|
+
FileDocument,
|
|
11
|
+
RefSeqChunkDocument,
|
|
12
|
+
RefSeqDocument,
|
|
13
|
+
UserDocument,
|
|
14
|
+
} from '@apollo-annotation/schemas'
|
|
15
|
+
import type { LoggerService } from '@nestjs/common'
|
|
16
|
+
import type { ClientSession, Model } from 'mongoose'
|
|
17
|
+
|
|
18
|
+
export interface LocalGFF3DataStore {
|
|
19
|
+
typeName: 'LocalGFF3'
|
|
20
|
+
gff3Handle: FileHandle
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CreateFileDto {
|
|
24
|
+
readonly _id: string
|
|
25
|
+
readonly basename: string
|
|
26
|
+
readonly checksum: string
|
|
27
|
+
readonly type: 'text/x-gff3' | 'text/x-fasta'
|
|
28
|
+
readonly user: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ServerDataStore {
|
|
32
|
+
typeName: 'Server'
|
|
33
|
+
featureModel: Model<FeatureDocument>
|
|
34
|
+
assemblyModel: Model<AssemblyDocument>
|
|
35
|
+
refSeqModel: Model<RefSeqDocument>
|
|
36
|
+
refSeqChunkModel: Model<RefSeqChunkDocument>
|
|
37
|
+
fileModel: Model<FileDocument>
|
|
38
|
+
userModel: Model<UserDocument>
|
|
39
|
+
session: ClientSession
|
|
40
|
+
filesService: {
|
|
41
|
+
getFileStream(file: FileDocument): ReadStream
|
|
42
|
+
parseGFF3(stream: ReadStream): ReadStream
|
|
43
|
+
create(createFileDto: CreateFileDto): void
|
|
44
|
+
remove(id: string): void
|
|
45
|
+
}
|
|
46
|
+
pluginsService: {
|
|
47
|
+
evaluateExtensionPoint(
|
|
48
|
+
extensionPointName: string,
|
|
49
|
+
extendee: unknown,
|
|
50
|
+
props?: Record<string, unknown>,
|
|
51
|
+
): void
|
|
52
|
+
}
|
|
53
|
+
counterService: {
|
|
54
|
+
getNextSequenceValue(sequenceName: string): Promise<number>
|
|
55
|
+
}
|
|
56
|
+
user: string
|
|
57
|
+
}
|
|
58
|
+
export interface SerializedOperation {
|
|
59
|
+
typeName: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type BackendDataStore = ServerDataStore | LocalGFF3DataStore
|
|
63
|
+
|
|
64
|
+
export interface OperationOptions {
|
|
65
|
+
logger: LoggerService
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export abstract class Operation implements SerializedOperation {
|
|
69
|
+
protected logger: LoggerService
|
|
70
|
+
abstract typeName: string
|
|
71
|
+
|
|
72
|
+
constructor(json: SerializedOperation, options?: OperationOptions) {
|
|
73
|
+
this.logger = options?.logger ?? console
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
abstract toJSON(): SerializedOperation
|
|
77
|
+
|
|
78
|
+
async execute(backend: BackendDataStore): Promise<unknown> {
|
|
79
|
+
const backendType = backend.typeName
|
|
80
|
+
if (backendType === 'Server') {
|
|
81
|
+
const initialResult = this.executeOnServer(backend)
|
|
82
|
+
return backend.pluginsService.evaluateExtensionPoint(
|
|
83
|
+
`${this.typeName}-transformResults`,
|
|
84
|
+
initialResult,
|
|
85
|
+
{ operation: this, backend },
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
if (backendType === 'LocalGFF3') {
|
|
89
|
+
return this.executeOnLocalGFF3(backend)
|
|
90
|
+
}
|
|
91
|
+
throw new Error(
|
|
92
|
+
`no operation implementation for backend type '${backendType}'`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
abstract executeOnServer(backend: ServerDataStore): Promise<unknown>
|
|
97
|
+
abstract executeOnLocalGFF3(backend: LocalGFF3DataStore): Promise<unknown>
|
|
98
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Operation } from './Operation'
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
type OperationType = new (...args: any[]) => Operation
|
|
5
|
+
|
|
6
|
+
class OperationTypeRegistry {
|
|
7
|
+
operations = new Map<string, OperationType>()
|
|
8
|
+
|
|
9
|
+
registerOperation(name: string, operationType: OperationType): void {
|
|
10
|
+
if (this.operations.has(name)) {
|
|
11
|
+
throw new Error(`operation type "${name}" has already been registered`)
|
|
12
|
+
}
|
|
13
|
+
this.operations.set(name, operationType)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getOperationType(name: string): OperationType {
|
|
17
|
+
const RegisteredOperationType = this.operations.get(name)
|
|
18
|
+
if (!RegisteredOperationType) {
|
|
19
|
+
throw new Error(`No operation constructor registered for "${name}"`)
|
|
20
|
+
}
|
|
21
|
+
return RegisteredOperationType
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** global singleton of all known types of operations */
|
|
26
|
+
export const operationRegistry = new OperationTypeRegistry()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
import { FeatureDocument } from '@apollo-annotation/schemas'
|
|
3
|
+
import type { ExecutionContext } from '@nestjs/common'
|
|
4
|
+
import type { Reflector } from '@nestjs/core'
|
|
5
|
+
import { ClientSession, Model } from 'mongoose'
|
|
6
|
+
|
|
7
|
+
import { Change, ClientDataStore } from './Change'
|
|
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 'context' in thing && 'reflector' in thing
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ValidationResult {
|
|
19
|
+
validationName: string
|
|
20
|
+
error?: { message: string }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export abstract class Validation {
|
|
24
|
+
abstract name: string
|
|
25
|
+
async frontendPreValidate(_change: Change): Promise<ValidationResult> {
|
|
26
|
+
return { validationName: this.name }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async frontendPostValidate(
|
|
30
|
+
_change: Change,
|
|
31
|
+
_dataStore: ClientDataStore,
|
|
32
|
+
): Promise<ValidationResult> {
|
|
33
|
+
return { validationName: this.name }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async backendPreValidate(
|
|
37
|
+
_changeOrContext: Change | Context,
|
|
38
|
+
): Promise<ValidationResult> {
|
|
39
|
+
return { validationName: this.name }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async backendPostValidate(
|
|
43
|
+
_change: Change,
|
|
44
|
+
_context: { session: ClientSession; featureModel: Model<FeatureDocument> },
|
|
45
|
+
): Promise<ValidationResult> {
|
|
46
|
+
return { validationName: this.name }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async possibleValues(_key: string): Promise<unknown[] | undefined> {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './ApolloPlugin'
|
|
2
|
+
export * from './AssemblySpecificChange'
|
|
3
|
+
export * from './Change'
|
|
4
|
+
export * from './ChangeTypeRegistry'
|
|
5
|
+
export * from './Check'
|
|
6
|
+
export * from './CheckRegistry'
|
|
7
|
+
export * from './FeatureChange'
|
|
8
|
+
export * from './Operation'
|
|
9
|
+
export * from './OperationTypeRegistry'
|
|
10
|
+
export * from './Validation'
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"incremental": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"target": "ES2019",
|
|
9
|
+
"lib": ["ES2019", "DOM"],
|
|
10
|
+
"module": "CommonJS",
|
|
11
|
+
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
|
|
12
|
+
"experimentalDecorators": true,
|
|
13
|
+
"emitDecoratorMetadata": true,
|
|
14
|
+
"strictPropertyInitialization": false,
|
|
15
|
+
},
|
|
16
|
+
"include": ["./src"],
|
|
17
|
+
"references": [
|
|
18
|
+
{ "path": "../apollo-schemas"},
|
|
19
|
+
]
|
|
20
|
+
}
|