@apollo-annotation/shared 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -0
  2. package/package.json +49 -0
  3. package/src/Changes/AddAssemblyAndFeaturesFromFileChange.ts +135 -0
  4. package/src/Changes/AddAssemblyFromExternalChange.ts +145 -0
  5. package/src/Changes/AddAssemblyFromFileChange.ts +125 -0
  6. package/src/Changes/AddFeatureChange.ts +233 -0
  7. package/src/Changes/AddFeaturesFromFileChange.ts +120 -0
  8. package/src/Changes/DeleteAssemblyChange.ts +100 -0
  9. package/src/Changes/DeleteFeatureChange.ts +200 -0
  10. package/src/Changes/DeleteUserChange.ts +83 -0
  11. package/src/Changes/DiscontinuousLocationEndChange.ts +182 -0
  12. package/src/Changes/DiscontinuousLocationStartChange.ts +182 -0
  13. package/src/Changes/FeatureAttributeChange.ts +164 -0
  14. package/src/Changes/LocationEndChange.ts +175 -0
  15. package/src/Changes/LocationStartChange.ts +175 -0
  16. package/src/Changes/StrandChange.ts +159 -0
  17. package/src/Changes/TypeChange.ts +159 -0
  18. package/src/Changes/UserChange.ts +82 -0
  19. package/src/Changes/index.ts +52 -0
  20. package/src/Checks/CDSCheck.ts +275 -0
  21. package/src/Checks/index.ts +1 -0
  22. package/src/Common/index.ts +1 -0
  23. package/src/Common/jwtPayload.ts +25 -0
  24. package/src/Messages.ts +32 -0
  25. package/src/Operations/GetAssembliesOperation.ts +28 -0
  26. package/src/Operations/GetFeaturesOperation.ts +58 -0
  27. package/src/Operations/index.ts +6 -0
  28. package/src/Validations/CoreValidation.ts +37 -0
  29. package/src/Validations/ParentChildValidation.ts +88 -0
  30. package/src/Validations/Validation.ts +55 -0
  31. package/src/Validations/ValidationSet.ts +106 -0
  32. package/src/Validations/index.ts +4 -0
  33. package/src/Validations/soSequenceTypes.ts +1865 -0
  34. package/src/index.ts +7 -0
  35. package/src/util.ts +109 -0
  36. package/tsconfig.json +19 -0
@@ -0,0 +1,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
+ }
@@ -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
+ }