Skip to content

Commit 0a06f5c

Browse files
Add API and CLI command for getting by indexed id
1 parent b7270ed commit 0a06f5c

File tree

8 files changed

+273
-3
lines changed

8 files changed

+273
-3
lines changed

.github/workflows/pull_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
- name: Start apollo
4747
run: |
4848
yarn --cwd packages/apollo-shared start &
49-
ALLOW_ROOT_USER=true ROOT_USER_PASSWORD=pass yarn --cwd packages/apollo-collaboration-server start &
49+
yarn --cwd packages/apollo-collaboration-server test:cli:start &
5050
- name: Run CLI tests
5151
run: yarn test:cli
5252
working-directory: packages/apollo-cli

packages/apollo-cli/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ USAGE
4848
- [`apollo feature edit-type`](#apollo-feature-edit-type)
4949
- [`apollo feature get`](#apollo-feature-get)
5050
- [`apollo feature get-id`](#apollo-feature-get-id)
51+
- [`apollo feature get-indexed-id ID`](#apollo-feature-get-indexed-id-id)
5152
- [`apollo feature import INPUT-FILE`](#apollo-feature-import-input-file)
5253
- [`apollo feature search`](#apollo-feature-search)
5354
- [`apollo file delete`](#apollo-file-delete)
@@ -705,6 +706,38 @@ EXAMPLES
705706
_See code:
706707
[src/commands/feature/get-id.ts](https://github.com/GMOD/Apollo3/blob/v0.3.9/packages/apollo-cli/src/commands/feature/get-id.ts)_
707708

709+
## `apollo feature get-indexed-id ID`
710+
711+
Get features given an indexed identifier
712+
713+
```
714+
USAGE
715+
$ apollo feature get-indexed-id ID [--profile <value>] [--config-file <value>] [-a <value>] [--topLevel]
716+
717+
ARGUMENTS
718+
ID Indexed identifier to search for
719+
720+
FLAGS
721+
-a, --assembly=<value>... Assembly names or IDs to search; use "-" to read it from stdin. If omitted search all
722+
assemblies
723+
--config-file=<value> Use this config file (mostly for testing)
724+
--profile=<value> Use credentials from this profile
725+
--topLevel Return the top-level parent of the feature instead of the feature itself
726+
727+
DESCRIPTION
728+
Get features given an indexed identifier
729+
730+
Get features that match a given indexed identifier, such as the ID of a feature from an imported GFF3 file
731+
732+
EXAMPLES
733+
Get features for this indexed identifier:
734+
735+
$ apollo feature get-indexed-id -i abc...zyz def...foo
736+
```
737+
738+
_See code:
739+
[src/commands/feature/get-indexed-id.ts](https://github.com/GMOD/Apollo3/blob/v0.3.9/packages/apollo-cli/src/commands/feature/get-indexed-id.ts)_
740+
708741
## `apollo feature import INPUT-FILE`
709742

710743
Import features from local gff file
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Args, Flags } from '@oclif/core'
2+
import { fetch } from 'undici'
3+
4+
import { BaseCommand } from '../../baseCommand.js'
5+
import {
6+
convertAssemblyNameToId,
7+
createFetchErrorMessage,
8+
idReader,
9+
localhostToAddress,
10+
} from '../../utils.js'
11+
12+
export default class Get extends BaseCommand<typeof Get> {
13+
static summary = 'Get features given an indexed identifier'
14+
static description =
15+
'Get features that match a given indexed identifier, such as the ID of a feature from an imported GFF3 file'
16+
17+
static examples = [
18+
{
19+
description: 'Get features for this indexed identifier:',
20+
command: '<%= config.bin %> <%= command.id %> -i abc...zyz def...foo',
21+
},
22+
]
23+
24+
static args = {
25+
id: Args.string({
26+
description: 'Indexed identifier to search for',
27+
required: true,
28+
}),
29+
}
30+
31+
static flags = {
32+
assembly: Flags.string({
33+
char: 'a',
34+
multiple: true,
35+
description:
36+
'Assembly names or IDs to search; use "-" to read it from stdin. If omitted search all assemblies',
37+
}),
38+
topLevel: Flags.boolean({
39+
description:
40+
'Return the top-level parent of the feature instead of the feature itself',
41+
}),
42+
}
43+
44+
public async run(): Promise<void> {
45+
const { args, flags } = await this.parse(Get)
46+
47+
const access = await this.getAccess()
48+
49+
const { topLevel } = flags
50+
const { id } = args
51+
52+
const assembly = flags.assembly && (await idReader(flags.assembly))
53+
const assemblyIds =
54+
assembly &&
55+
(await convertAssemblyNameToId(
56+
access.address,
57+
access.accessToken,
58+
assembly,
59+
))
60+
61+
if (assemblyIds?.length === 0) {
62+
this.log(JSON.stringify([], null, 2))
63+
this.exit(0)
64+
}
65+
66+
const url = new URL(
67+
localhostToAddress(`${access.address}/features/getByIndexedId`),
68+
)
69+
const searchParams = new URLSearchParams({ id })
70+
if (assemblyIds) {
71+
searchParams.append('assemblies', assemblyIds.join(','))
72+
}
73+
if (topLevel) {
74+
searchParams.append('topLevel', 'true')
75+
}
76+
url.search = searchParams.toString()
77+
const uri = url.toString()
78+
const auth = {
79+
headers: {
80+
authorization: `Bearer ${access.accessToken}`,
81+
'Content-Type': 'application/json',
82+
},
83+
}
84+
const response = await fetch(uri, auth)
85+
if (!response.ok) {
86+
const errorMessage = await createFetchErrorMessage(
87+
response,
88+
'Failed to access Apollo with the current address and/or access token\nThe server returned:\n',
89+
)
90+
throw new Error(errorMessage)
91+
}
92+
const results = (await response.json()) as object
93+
94+
this.log(JSON.stringify(results, null, 2))
95+
}
96+
}

packages/apollo-cli/src/test/test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,56 @@ void describe('Test CLI', () => {
774774
assert.ok(p.stdout.includes('"Q"'))
775775
})
776776

777+
void globalThis.itName('Get feature by indexed ID', () => {
778+
new Shell(
779+
`${apollo} assembly add-from-gff ${P} test_data/tiny.fasta.gff3 -a vv1 -f`,
780+
)
781+
new Shell(
782+
`${apollo} assembly add-from-gff ${P} test_data/tiny.fasta.gff3 -a vv2 -f`,
783+
)
784+
785+
// Search multiple assemblies
786+
let p = new Shell(`${apollo} feature get-indexed-id ${P} MyGene -a vv1 vv2`)
787+
let out = JSON.parse(p.stdout)
788+
assert.strictEqual(out.length, 2)
789+
assert.ok(p.stdout.includes('MyGene'))
790+
791+
// Specifying no assembly defaults to searching all assemblies
792+
p = new Shell(`${apollo} feature get-indexed-id ${P} MyGene`)
793+
out = JSON.parse(p.stdout)
794+
assert.strictEqual(out.length, 2)
795+
assert.ok(p.stdout.includes('MyGene'))
796+
797+
// Search single assembly
798+
p = new Shell(`${apollo} feature get-indexed-id ${P} MyGene -a vv1`)
799+
out = JSON.parse(p.stdout)
800+
assert.strictEqual(out.length, 1)
801+
assert.ok(p.stdout.includes('MyGene'))
802+
803+
// Warn on unknown assembly
804+
p = new Shell(`${apollo} feature get-indexed-id ${P} EDEN -a foobar`)
805+
assert.strictEqual('[]', p.stdout.trim())
806+
assert.ok(p.stderr.includes('Warning'))
807+
808+
// Return empty array with no matches
809+
p = new Shell(`${apollo} feature get-indexed-id ${P} foobarspam -a vv1`)
810+
assert.deepStrictEqual(p.stdout.trim(), '[]')
811+
812+
// Gets subfeature
813+
p = new Shell(`${apollo} feature get-indexed-id ${P} myCDS.1 -a vv1`)
814+
out = JSON.parse(p.stdout)
815+
assert.strictEqual(out.length, 1)
816+
assert.ok(out.at(0)?.type === 'CDS')
817+
818+
// Gets top-level feature from subfeature id
819+
p = new Shell(
820+
`${apollo} feature get-indexed-id ${P} myCDS.1 -a vv1 --topLevel`,
821+
)
822+
out = JSON.parse(p.stdout)
823+
assert.strictEqual(out.length, 1)
824+
assert.ok(out.at(0)?.type === 'gene')
825+
})
826+
777827
void globalThis.itName('Delete features', () => {
778828
new Shell(
779829
`${apollo} assembly add-from-gff ${P} test_data/tiny.fasta.gff3 -a vv1 -f`,

packages/apollo-collaboration-server/src/features/dto/feature.dto.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ export interface FeatureCountRequest {
44
start?: number
55
end?: number
66
}
7+
8+
export interface GetByIndexedIdRequest {
9+
id: string
10+
assemblies?: string
11+
topLevel?: boolean
12+
}

packages/apollo-collaboration-server/src/features/features.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { FeatureRangeSearchDto } from '../entity/gff3Object.dto'
1111
import { Role } from '../utils/role/role.enum'
1212
import { Validations } from '../utils/validation/validatation.decorator'
1313

14-
import { FeatureCountRequest } from './dto/feature.dto'
14+
import { FeatureCountRequest, GetByIndexedIdRequest } from './dto/feature.dto'
1515
import { FeaturesService } from './features.service'
1616

1717
@Validations(Role.ReadOnly)
@@ -55,6 +55,11 @@ export class FeaturesController {
5555
return { count }
5656
}
5757

58+
@Get('getByIndexedId')
59+
async getById(@Query() getByIndexedIdRequest: GetByIndexedIdRequest) {
60+
return this.featuresService.getByIndexedId(getByIndexedIdRequest)
61+
}
62+
5863
/**
5964
* Get feature by featureId. When retrieving features by id, the features and any of its children are returned, but not any of its parent or sibling features.
6065
* @param featureid - featureId

packages/apollo-collaboration-server/src/features/features.service.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ChecksService } from '../checks/checks.service'
1515
import { FeatureRangeSearchDto } from '../entity/gff3Object.dto'
1616
import { OperationsService } from '../operations/operations.service'
1717

18-
import { FeatureCountRequest } from './dto/feature.dto'
18+
import { FeatureCountRequest, GetByIndexedIdRequest } from './dto/feature.dto'
1919

2020
@Injectable()
2121
export class FeaturesService {
@@ -70,6 +70,53 @@ export class FeaturesService {
7070
return count
7171
}
7272

73+
async getByIndexedId(getByIndexedIdRequest: GetByIndexedIdRequest) {
74+
const { assemblies, id, topLevel } = getByIndexedIdRequest
75+
const refSeqsQuery: { refSeq?: RefSeqDocument[] } = {}
76+
if (assemblies) {
77+
const assemblyIds = assemblies.split(',')
78+
const refSeqs = await this.refSeqModel
79+
.find({ assembly: assemblyIds })
80+
.exec()
81+
refSeqsQuery.refSeq = refSeqs
82+
}
83+
const topLevelFeatures = await this.featureModel
84+
.find({ indexedIds: id, ...refSeqsQuery })
85+
.exec()
86+
if (topLevelFeatures.length === 0) {
87+
return []
88+
}
89+
if (topLevel) {
90+
return topLevelFeatures
91+
}
92+
return topLevelFeatures
93+
.map((topLevelFeature) => this.findIndexedId(id, topLevelFeature))
94+
.filter((feature): feature is Feature => feature !== undefined)
95+
}
96+
97+
findIndexedId(id: string, feature: FeatureDocument): Feature | undefined {
98+
const { attributes } = feature.toObject({
99+
flattenMaps: true,
100+
})
101+
if (attributes) {
102+
for (const attributeValue of Object.values(attributes)) {
103+
if (attributeValue.includes(id)) {
104+
return feature
105+
}
106+
}
107+
}
108+
if (!feature.children) {
109+
return
110+
}
111+
for (const [, childFeature] of feature.children) {
112+
const subFeature = this.findIndexedId(id, childFeature as FeatureDocument)
113+
if (subFeature) {
114+
return subFeature
115+
}
116+
}
117+
return
118+
}
119+
73120
/**
74121
* Get feature by featureId. When retrieving features by id, the features and any of its children are returned, but not any of its parent or sibling features.
75122
* @param featureId - featureId

packages/website/docs/admin/cli/feature.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Commands to manage features
1212
- [`apollo feature edit-type`](#apollo-feature-edit-type)
1313
- [`apollo feature get`](#apollo-feature-get)
1414
- [`apollo feature get-id`](#apollo-feature-get-id)
15+
- [`apollo feature get-indexed-id ID`](#apollo-feature-get-indexed-id-id)
1516
- [`apollo feature import INPUT-FILE`](#apollo-feature-import-input-file)
1617
- [`apollo feature search`](#apollo-feature-search)
1718

@@ -336,6 +337,38 @@ EXAMPLES
336337
_See code:
337338
[src/commands/feature/get-id.ts](https://github.com/GMOD/Apollo3/blob/v0.3.9/packages/apollo-cli/src/commands/feature/get-id.ts)_
338339

340+
## `apollo feature get-indexed-id ID`
341+
342+
Get features given an indexed identifier
343+
344+
```
345+
USAGE
346+
$ apollo feature get-indexed-id ID [--profile <value>] [--config-file <value>] [-a <value>] [--topLevel]
347+
348+
ARGUMENTS
349+
ID Indexed identifier to search for
350+
351+
FLAGS
352+
-a, --assembly=<value>... Assembly names or IDs to search; use "-" to read it from stdin. If omitted search all
353+
assemblies
354+
--config-file=<value> Use this config file (mostly for testing)
355+
--profile=<value> Use credentials from this profile
356+
--topLevel Return the top-level parent of the feature instead of the feature itself
357+
358+
DESCRIPTION
359+
Get features given an indexed identifier
360+
361+
Get features that match a given indexed identifier, such as the ID of a feature from an imported GFF3 file
362+
363+
EXAMPLES
364+
Get features for this indexed identifier:
365+
366+
$ apollo feature get-indexed-id -i abc...zyz def...foo
367+
```
368+
369+
_See code:
370+
[src/commands/feature/get-indexed-id.ts](https://github.com/GMOD/Apollo3/blob/v0.3.9/packages/apollo-cli/src/commands/feature/get-indexed-id.ts)_
371+
339372
## `apollo feature import INPUT-FILE`
340373

341374
Import features from local gff file

0 commit comments

Comments
 (0)