@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 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
+ }
@@ -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
+ }