@apollo-annotation/cli 0.1.10
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 +899 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +9 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +8 -0
- package/dist/Config.d.ts +43 -0
- package/dist/Config.js +233 -0
- package/dist/baseCommand.d.ts +21 -0
- package/dist/baseCommand.js +62 -0
- package/dist/commands/assembly/add-fasta.d.ts +15 -0
- package/dist/commands/assembly/add-fasta.js +98 -0
- package/dist/commands/assembly/add-gff.d.ts +16 -0
- package/dist/commands/assembly/add-gff.js +76 -0
- package/dist/commands/assembly/check.d.ts +15 -0
- package/dist/commands/assembly/check.js +148 -0
- package/dist/commands/assembly/delete.d.ts +14 -0
- package/dist/commands/assembly/delete.js +43 -0
- package/dist/commands/assembly/get.d.ts +9 -0
- package/dist/commands/assembly/get.js +33 -0
- package/dist/commands/assembly/get.test.d.ts +1 -0
- package/dist/commands/assembly/get.test.js +50 -0
- package/dist/commands/assembly/sequence.d.ts +16 -0
- package/dist/commands/assembly/sequence.js +117 -0
- package/dist/commands/change/get.d.ts +9 -0
- package/dist/commands/change/get.js +35 -0
- package/dist/commands/change/get.test.d.ts +1 -0
- package/dist/commands/change/get.test.js +22 -0
- package/dist/commands/config.d.ts +25 -0
- package/dist/commands/config.js +217 -0
- package/dist/commands/config.test.d.ts +1 -0
- package/dist/commands/config.test.js +188 -0
- package/dist/commands/feature/add-child.d.ts +17 -0
- package/dist/commands/feature/add-child.js +120 -0
- package/dist/commands/feature/check.d.ts +14 -0
- package/dist/commands/feature/check.js +97 -0
- package/dist/commands/feature/copy.d.ts +17 -0
- package/dist/commands/feature/copy.js +110 -0
- package/dist/commands/feature/delete.d.ts +11 -0
- package/dist/commands/feature/delete.js +99 -0
- package/dist/commands/feature/edit-attribute.d.ts +16 -0
- package/dist/commands/feature/edit-attribute.js +100 -0
- package/dist/commands/feature/edit-coords.d.ts +15 -0
- package/dist/commands/feature/edit-coords.js +109 -0
- package/dist/commands/feature/edit-type.d.ts +10 -0
- package/dist/commands/feature/edit-type.js +70 -0
- package/dist/commands/feature/edit.d.ts +13 -0
- package/dist/commands/feature/edit.js +81 -0
- package/dist/commands/feature/get-id.d.ts +14 -0
- package/dist/commands/feature/get-id.js +56 -0
- package/dist/commands/feature/get.d.ts +16 -0
- package/dist/commands/feature/get.js +87 -0
- package/dist/commands/feature/import.d.ts +15 -0
- package/dist/commands/feature/import.js +83 -0
- package/dist/commands/feature/search.d.ts +14 -0
- package/dist/commands/feature/search.js +91 -0
- package/dist/commands/login.d.ts +21 -0
- package/dist/commands/login.js +206 -0
- package/dist/commands/login.test.d.ts +1 -0
- package/dist/commands/login.test.js +52 -0
- package/dist/commands/logout.d.ts +10 -0
- package/dist/commands/logout.js +41 -0
- package/dist/commands/logout.test.d.ts +1 -0
- package/dist/commands/logout.test.js +67 -0
- package/dist/commands/refseq/get.d.ts +13 -0
- package/dist/commands/refseq/get.js +44 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +38 -0
- package/dist/commands/status.test.d.ts +1 -0
- package/dist/commands/status.test.js +54 -0
- package/dist/commands/user/get.d.ts +14 -0
- package/dist/commands/user/get.js +46 -0
- package/dist/commands/user/get.test.d.ts +1 -0
- package/dist/commands/user/get.test.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/test/fixtures.d.ts +7 -0
- package/dist/test/fixtures.js +40 -0
- package/dist/utils.d.ts +54 -0
- package/dist/utils.js +373 -0
- package/oclif.manifest.json +1632 -0
- package/package.json +100 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { fetch } from 'undici';
|
|
3
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
4
|
+
import { createFetchErrorMessage, idReader, localhostToAddress, wrapLines, } from '../../utils.js';
|
|
5
|
+
export default class Get extends BaseCommand {
|
|
6
|
+
static summary = 'Get features given their identifiers';
|
|
7
|
+
static description = wrapLines('Invalid identifiers or identifiers not found in the database will be silently ignored');
|
|
8
|
+
static examples = [
|
|
9
|
+
{
|
|
10
|
+
description: 'Get features for these identifiers:',
|
|
11
|
+
command: '<%= config.bin %> <%= command.id %> -i abc...zyz def...foo',
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
'feature-id': Flags.string({
|
|
16
|
+
char: 'i',
|
|
17
|
+
description: wrapLines('Retrieves feature with these IDs. Use "-" to read IDs from stdin (one per line)', 40),
|
|
18
|
+
multiple: true,
|
|
19
|
+
default: ['-'],
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { flags } = await this.parse(Get);
|
|
24
|
+
const access = await this.getAccess(flags['config-file'], flags.profile);
|
|
25
|
+
let ids = idReader(flags['feature-id']);
|
|
26
|
+
ids = [...new Set(ids)];
|
|
27
|
+
const results = [];
|
|
28
|
+
for (const id of ids) {
|
|
29
|
+
const res = await this.getFeatureId(access.address, access.accessToken, id);
|
|
30
|
+
if (Object.keys(res).length === 0) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
results.push(res);
|
|
34
|
+
}
|
|
35
|
+
this.log(JSON.stringify(results, null, 2));
|
|
36
|
+
this.exit(0);
|
|
37
|
+
}
|
|
38
|
+
async getFeatureId(address, token, featureId) {
|
|
39
|
+
const url = new URL(localhostToAddress(`${address}/features/${featureId}`));
|
|
40
|
+
const auth = {
|
|
41
|
+
headers: {
|
|
42
|
+
authorization: `Bearer ${token}`,
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const response = await fetch(url, auth);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
if (response.status === 404) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
const errorMessage = await createFetchErrorMessage(response, 'Failed to access Apollo with the current address and/or access token\nThe server returned:\n');
|
|
52
|
+
throw new Error(errorMessage);
|
|
53
|
+
}
|
|
54
|
+
return (await response.json());
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
2
|
+
export default class Get extends BaseCommand<typeof Get> {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
description: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}[];
|
|
8
|
+
static flags: {
|
|
9
|
+
assembly: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
10
|
+
refseq: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
start: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
end: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
private getFeatures;
|
|
16
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { fetch } from 'undici';
|
|
3
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
4
|
+
import { createFetchErrorMessage, getRefseqId, localhostToAddress, wrapLines, } from '../../utils.js';
|
|
5
|
+
export default class Get extends BaseCommand {
|
|
6
|
+
static description = 'Get features in assembly, reference sequence or genomic window';
|
|
7
|
+
static examples = [
|
|
8
|
+
{
|
|
9
|
+
description: 'Get all features in myAssembly:',
|
|
10
|
+
command: '<%= config.bin %> <%= command.id %> -a myAssembly',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
description: wrapLines('Get features intersecting chr1:1..1000. You can omit the assembly name if there are no other reference sequences named chr1:'),
|
|
14
|
+
command: '<%= config.bin %> <%= command.id %> -a myAssembly -r chr1 -s 1 -e 1000',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
assembly: Flags.string({
|
|
19
|
+
char: 'a',
|
|
20
|
+
description: 'Find input reference sequence in this assembly',
|
|
21
|
+
}),
|
|
22
|
+
refseq: Flags.string({
|
|
23
|
+
char: 'r',
|
|
24
|
+
description: 'Reference sequence. If unset, query all sequences',
|
|
25
|
+
}),
|
|
26
|
+
start: Flags.integer({
|
|
27
|
+
char: 's',
|
|
28
|
+
description: 'Start coordinate (1-based)',
|
|
29
|
+
default: 1,
|
|
30
|
+
}),
|
|
31
|
+
end: Flags.integer({
|
|
32
|
+
char: 'e',
|
|
33
|
+
description: 'End coordinate',
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
async run() {
|
|
37
|
+
const { flags } = await this.parse(Get);
|
|
38
|
+
const endCoord = flags.end ?? Number.MAX_SAFE_INTEGER;
|
|
39
|
+
if (flags.start <= 0 || endCoord <= 0) {
|
|
40
|
+
this.logToStderr('Start and end coordinates must be greater than 0.');
|
|
41
|
+
this.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const access = await this.getAccess(flags['config-file'], flags.profile);
|
|
44
|
+
let refseqIds = [];
|
|
45
|
+
try {
|
|
46
|
+
refseqIds = await getRefseqId(access.address, access.accessToken, flags.refseq, flags.assembly);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.logToStderr(error.message);
|
|
50
|
+
this.exit(1);
|
|
51
|
+
}
|
|
52
|
+
if (refseqIds.length === 0) {
|
|
53
|
+
this.logToStderr('No reference sequence found');
|
|
54
|
+
}
|
|
55
|
+
const results = [];
|
|
56
|
+
for (const refseq of refseqIds) {
|
|
57
|
+
const features = await this.getFeatures(access.address, access.accessToken, refseq, flags.start, endCoord);
|
|
58
|
+
const json = (await features.json());
|
|
59
|
+
for (const x of json[0]) {
|
|
60
|
+
results.push(x);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
this.log(JSON.stringify(results, null, 2));
|
|
64
|
+
this.exit(0);
|
|
65
|
+
}
|
|
66
|
+
async getFeatures(address, token, refSeq, start, end) {
|
|
67
|
+
const url = new URL(localhostToAddress(`${address}/features/getFeatures`));
|
|
68
|
+
const searchParams = new URLSearchParams({
|
|
69
|
+
refSeq,
|
|
70
|
+
start: start.toString(),
|
|
71
|
+
end: end.toString(),
|
|
72
|
+
});
|
|
73
|
+
url.search = searchParams.toString();
|
|
74
|
+
const auth = {
|
|
75
|
+
headers: {
|
|
76
|
+
authorization: `Bearer ${token}`,
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const response = await fetch(url, auth);
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const errorMessage = await createFetchErrorMessage(response, 'Failed to access Apollo with the current address and/or access token\nThe server returned:\n');
|
|
83
|
+
throw new Error(errorMessage);
|
|
84
|
+
}
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
2
|
+
export default class Import extends BaseCommand<typeof Import> {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: {
|
|
6
|
+
description: string;
|
|
7
|
+
command: string;
|
|
8
|
+
}[];
|
|
9
|
+
static flags: {
|
|
10
|
+
'input-file': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
assembly: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
'delete-existing': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { Flags } from '@oclif/core';
|
|
5
|
+
import { fetch } from 'undici';
|
|
6
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
7
|
+
import { convertAssemblyNameToId, createFetchErrorMessage, localhostToAddress, uploadFile, } from '../../utils.js';
|
|
8
|
+
export default class Import extends BaseCommand {
|
|
9
|
+
static summary = 'Import features from local gff file';
|
|
10
|
+
static description = 'By default, features are added to the existing ones.';
|
|
11
|
+
static examples = [
|
|
12
|
+
{
|
|
13
|
+
description: 'Delete features in myAssembly and then import features.gff3:',
|
|
14
|
+
command: '<%= config.bin %> <%= command.id %> -d -i features.gff3 -a myAssembly',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
'input-file': Flags.string({
|
|
19
|
+
char: 'i',
|
|
20
|
+
description: 'Input gff or gtf file',
|
|
21
|
+
required: true,
|
|
22
|
+
}),
|
|
23
|
+
assembly: Flags.string({
|
|
24
|
+
char: 'a',
|
|
25
|
+
description: 'Import into this assembly name or assembly ID',
|
|
26
|
+
required: true,
|
|
27
|
+
}),
|
|
28
|
+
'delete-existing': Flags.boolean({
|
|
29
|
+
char: 'd',
|
|
30
|
+
description: 'Delete existing features before importing',
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
async run() {
|
|
34
|
+
const { flags } = await this.parse(Import);
|
|
35
|
+
if (!fs.existsSync(flags['input-file'])) {
|
|
36
|
+
this.logToStderr(`File "${flags['input-file']}" does not exist`);
|
|
37
|
+
this.exit(1);
|
|
38
|
+
}
|
|
39
|
+
const access = await this.getAccess(flags['config-file'], flags.profile);
|
|
40
|
+
const assembly = await convertAssemblyNameToId(access.address, access.accessToken, [flags.assembly]);
|
|
41
|
+
if (assembly.length === 0) {
|
|
42
|
+
this.logToStderr(`Assembly "${flags.assembly}" does not exist. Perhaps you want to create this assembly first`);
|
|
43
|
+
this.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const uploadId = await uploadFile(access.address, access.accessToken, flags['input-file'], 'text/x-gff3');
|
|
46
|
+
const response = await importFeatures(access.address, access.accessToken, assembly[0], uploadId, flags['delete-existing']);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const json = JSON.parse(await response.text());
|
|
49
|
+
const message = json['message'];
|
|
50
|
+
this.logToStderr(message);
|
|
51
|
+
this.exit(1);
|
|
52
|
+
}
|
|
53
|
+
this.exit(0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function importFeatures(address, accessToken, assembly, fileId, deleteExistingFeatures) {
|
|
57
|
+
const body = {
|
|
58
|
+
typeName: 'AddFeaturesFromFileChange',
|
|
59
|
+
assembly,
|
|
60
|
+
fileId,
|
|
61
|
+
deleteExistingFeatures,
|
|
62
|
+
};
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
controller.abort();
|
|
66
|
+
}, 24 * 60 * 60 * 1000);
|
|
67
|
+
const auth = {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${accessToken}`,
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
},
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
};
|
|
76
|
+
const url = new URL(localhostToAddress(`${address}/changes`));
|
|
77
|
+
const response = await fetch(url, auth);
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorMessage = await createFetchErrorMessage(response, 'importFeatures failed');
|
|
80
|
+
throw new Error(errorMessage);
|
|
81
|
+
}
|
|
82
|
+
return response;
|
|
83
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
2
|
+
export default class Search extends BaseCommand<typeof Search> {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: {
|
|
6
|
+
description: string;
|
|
7
|
+
command: string;
|
|
8
|
+
}[];
|
|
9
|
+
static flags: {
|
|
10
|
+
text: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
assembly: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { fetch } from 'undici';
|
|
4
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
5
|
+
import { convertAssemblyNameToId, createFetchErrorMessage, idReader, localhostToAddress, queryApollo, wrapLines, } from '../../utils.js';
|
|
6
|
+
async function searchFeatures(address, accessToken, assemblies, term) {
|
|
7
|
+
const url = new URL(localhostToAddress(`${address}/features/searchFeatures`));
|
|
8
|
+
const searchParams = new URLSearchParams({
|
|
9
|
+
assemblies: assemblies.join(','),
|
|
10
|
+
term,
|
|
11
|
+
});
|
|
12
|
+
url.search = searchParams.toString();
|
|
13
|
+
const uri = url.toString();
|
|
14
|
+
const auth = {
|
|
15
|
+
headers: {
|
|
16
|
+
authorization: `Bearer ${accessToken}`,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
const response = await fetch(uri, auth);
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const errorMessage = await createFetchErrorMessage(response, 'searchFeatures failed');
|
|
22
|
+
throw new Error(errorMessage);
|
|
23
|
+
}
|
|
24
|
+
return response;
|
|
25
|
+
}
|
|
26
|
+
export default class Search extends BaseCommand {
|
|
27
|
+
static summary = 'Free text search for feature in one or more assemblies';
|
|
28
|
+
static description = wrapLines(`Return features matching a query string. This command searches only in:
|
|
29
|
+
|
|
30
|
+
- Attribute *values* (not attribute names)
|
|
31
|
+
- Source field (which in fact is stored as an attribute)
|
|
32
|
+
- Feature type
|
|
33
|
+
|
|
34
|
+
The search mode is:
|
|
35
|
+
|
|
36
|
+
- Case insensitive
|
|
37
|
+
- Match only full words, but not necessarily the full value
|
|
38
|
+
- Common words are ignored. E.g. "the", "with"
|
|
39
|
+
|
|
40
|
+
For example, given this feature:
|
|
41
|
+
|
|
42
|
+
chr1 example SNP 10 30 0.987 . . "someKey=Fingerprint BAC with reads"
|
|
43
|
+
|
|
44
|
+
Queries "bac" or "mRNA" return the feature. Instead these queries will NOT match:
|
|
45
|
+
|
|
46
|
+
- "someKey"
|
|
47
|
+
- "with"
|
|
48
|
+
- "Finger"
|
|
49
|
+
- "chr1"
|
|
50
|
+
- "0.987"`);
|
|
51
|
+
static examples = [
|
|
52
|
+
{
|
|
53
|
+
description: 'Search "bac" in these assemblies:',
|
|
54
|
+
command: '<%= config.bin %> <%= command.id %> -a mm9 mm10 -t bac',
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
static flags = {
|
|
58
|
+
text: Flags.string({
|
|
59
|
+
char: 't',
|
|
60
|
+
required: true,
|
|
61
|
+
description: 'Search for this text query',
|
|
62
|
+
}),
|
|
63
|
+
assembly: Flags.string({
|
|
64
|
+
char: 'a',
|
|
65
|
+
multiple: true,
|
|
66
|
+
description: wrapLines('Assembly names or IDs to search; use "-" to read it from stdin. If omitted search all assemblies'),
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
async run() {
|
|
70
|
+
const { flags } = await this.parse(Search);
|
|
71
|
+
const access = await this.getAccess(flags['config-file'], flags.profile);
|
|
72
|
+
let assemblyIds = [];
|
|
73
|
+
if (flags.assembly === undefined) {
|
|
74
|
+
const asm = await queryApollo(access.address, access.accessToken, 'assemblies');
|
|
75
|
+
for (const x of (await asm.json())) {
|
|
76
|
+
assemblyIds.push(x['_id']);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const assembly = idReader(flags.assembly);
|
|
81
|
+
assemblyIds = await convertAssemblyNameToId(access.address, access.accessToken, assembly);
|
|
82
|
+
}
|
|
83
|
+
if (assemblyIds.length === 0) {
|
|
84
|
+
this.log(JSON.stringify([], null, 2));
|
|
85
|
+
this.exit(0);
|
|
86
|
+
}
|
|
87
|
+
const response = await searchFeatures(access.address, access.accessToken, assemblyIds, flags.text);
|
|
88
|
+
const results = JSON.parse(await response.text());
|
|
89
|
+
this.log(JSON.stringify(results, null, 2));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseCommand } from '../baseCommand.js';
|
|
2
|
+
export default class Login extends BaseCommand<typeof Login> {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: {
|
|
6
|
+
description: string;
|
|
7
|
+
command: string;
|
|
8
|
+
}[];
|
|
9
|
+
static flags: {
|
|
10
|
+
address: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
username: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
password: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
force: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
14
|
+
port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
private checkUserAlreadyLoggedIn;
|
|
18
|
+
private startRootLogin;
|
|
19
|
+
private startGuestLogin;
|
|
20
|
+
private startAuthorizationCodeFlow;
|
|
21
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-misused-promises */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
4
|
+
import EventEmitter from 'node:events';
|
|
5
|
+
import * as http from 'node:http';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import * as querystring from 'node:querystring';
|
|
8
|
+
import { Errors, Flags, ux } from '@oclif/core';
|
|
9
|
+
import open from 'open';
|
|
10
|
+
import { fetch } from 'undici';
|
|
11
|
+
import { BaseCommand } from '../baseCommand.js';
|
|
12
|
+
import { Config, ConfigError } from '../Config.js';
|
|
13
|
+
import { basicCheckConfig, createFetchErrorMessage, getUserCredentials, localhostToAddress, waitFor, wrapLines, } from '../utils.js';
|
|
14
|
+
export default class Login extends BaseCommand {
|
|
15
|
+
static summary = 'Login to Apollo';
|
|
16
|
+
static description = wrapLines('Use the provided credentials to obtain and save the token to access Apollo. Once the token for \
|
|
17
|
+
the given profile has been saved in the configuration file, users do not normally need to execute \
|
|
18
|
+
this command again unless the token has expired. To setup a new profile use "apollo config"');
|
|
19
|
+
static examples = [
|
|
20
|
+
{
|
|
21
|
+
description: wrapLines('The most basic and probably most typical usage is to login using the default profile in configuration file:'),
|
|
22
|
+
command: '<%= config.bin %> <%= command.id %>',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
description: wrapLines('Login with a different profile:'),
|
|
26
|
+
command: '<%= config.bin %> <%= command.id %> --profile my-profile',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
static flags = {
|
|
30
|
+
address: Flags.string({
|
|
31
|
+
char: 'a',
|
|
32
|
+
description: 'Address of Apollo server',
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
35
|
+
username: Flags.string({
|
|
36
|
+
char: 'u',
|
|
37
|
+
description: 'Username for root login',
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
40
|
+
password: Flags.string({
|
|
41
|
+
char: 'p',
|
|
42
|
+
description: 'Password for <username>',
|
|
43
|
+
required: false,
|
|
44
|
+
}),
|
|
45
|
+
force: Flags.boolean({
|
|
46
|
+
char: 'f',
|
|
47
|
+
description: 'Force re-authentication even if user is already logged in',
|
|
48
|
+
}),
|
|
49
|
+
port: Flags.integer({
|
|
50
|
+
description: 'Get token by listening to this port number (usually this is >= 1024 and < 65536)',
|
|
51
|
+
default: 3000,
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
async run() {
|
|
55
|
+
const { flags } = await this.parse(Login);
|
|
56
|
+
let configFile = flags['config-file'];
|
|
57
|
+
if (configFile === undefined) {
|
|
58
|
+
configFile = path.join(this.config.configDir, 'config.yaml');
|
|
59
|
+
}
|
|
60
|
+
let profileName = flags.profile;
|
|
61
|
+
if (profileName === undefined) {
|
|
62
|
+
profileName = process.env.APOLLO_PROFILE ?? 'default';
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
basicCheckConfig(configFile, profileName);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof ConfigError) {
|
|
69
|
+
this.logToStderr(error.message);
|
|
70
|
+
this.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const config = new Config(configFile);
|
|
74
|
+
const accessType = config.get('accessType', profileName);
|
|
75
|
+
const address = flags.address ?? config.get('address', profileName);
|
|
76
|
+
if (address === undefined) {
|
|
77
|
+
this.logToStderr('Address to apollo must be set');
|
|
78
|
+
this.exit(1);
|
|
79
|
+
}
|
|
80
|
+
let userCredentials = { accessToken: '' };
|
|
81
|
+
try {
|
|
82
|
+
if (!flags.force) {
|
|
83
|
+
await this.checkUserAlreadyLoggedIn();
|
|
84
|
+
}
|
|
85
|
+
if (accessType === 'root' || flags.username !== undefined) {
|
|
86
|
+
const username = flags.username ?? config.get('rootCredentials.username', profileName);
|
|
87
|
+
const password = flags.password ?? config.get('rootCredentials.password', profileName);
|
|
88
|
+
if (username === undefined || password === undefined) {
|
|
89
|
+
this.logToStderr('Username and password must be set');
|
|
90
|
+
this.exit(1);
|
|
91
|
+
}
|
|
92
|
+
userCredentials = await this.startRootLogin(address, username, password);
|
|
93
|
+
}
|
|
94
|
+
else if (accessType === 'guest') {
|
|
95
|
+
userCredentials = await this.startGuestLogin(address);
|
|
96
|
+
}
|
|
97
|
+
else if (accessType === undefined) {
|
|
98
|
+
this.logToStderr('Undefined access type');
|
|
99
|
+
this.exit(1);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
userCredentials = await this.startAuthorizationCodeFlow(address, accessType, flags.port);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if ((error instanceof Errors.CLIError && error.message === 'ctrl-c') ||
|
|
107
|
+
error instanceof Errors.ExitError) {
|
|
108
|
+
this.exit(0);
|
|
109
|
+
}
|
|
110
|
+
else if (error instanceof Error) {
|
|
111
|
+
this.logToStderr(error.stack);
|
|
112
|
+
ux.action.stop(error.message);
|
|
113
|
+
this.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
config.set('accessToken', userCredentials.accessToken, profileName);
|
|
117
|
+
config.writeConfigFile();
|
|
118
|
+
}
|
|
119
|
+
async checkUserAlreadyLoggedIn() {
|
|
120
|
+
const userCredentials = getUserCredentials();
|
|
121
|
+
if (!userCredentials) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const alreadyLoggedIn = Object.keys(userCredentials).every((key) => Boolean(userCredentials[key]));
|
|
125
|
+
if (!alreadyLoggedIn) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const reAuthenticate = await ux.confirm("You're already logged. Do you want to re-authenticate? (y/n)");
|
|
129
|
+
if (!reAuthenticate) {
|
|
130
|
+
this.exit(0);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async startRootLogin(address, username, password) {
|
|
134
|
+
const url = localhostToAddress(`${address}/auth/root`);
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
method: 'POST',
|
|
138
|
+
body: JSON.stringify({ username, password }),
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const errorMessage = await createFetchErrorMessage(response, 'startRootLogin failed');
|
|
142
|
+
throw new Error(errorMessage);
|
|
143
|
+
}
|
|
144
|
+
const dat = await response.json();
|
|
145
|
+
if (typeof dat === 'object' && dat !== null && 'token' in dat) {
|
|
146
|
+
return { accessToken: dat.token };
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`Unexpected response: ${JSON.stringify(dat)}`);
|
|
149
|
+
}
|
|
150
|
+
async startGuestLogin(address) {
|
|
151
|
+
const url = localhostToAddress(`${address}/auth/login?type=guest`);
|
|
152
|
+
const response = await fetch(url, {
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const errorMessage = await createFetchErrorMessage(response, 'startGuestLogin failed');
|
|
157
|
+
throw new Error(errorMessage);
|
|
158
|
+
}
|
|
159
|
+
const dat = await response.json();
|
|
160
|
+
if (typeof dat === 'object' && dat !== null && 'token' in dat) {
|
|
161
|
+
return { accessToken: dat.token };
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`Unexpected response: ${JSON.stringify(dat)}`);
|
|
164
|
+
}
|
|
165
|
+
async startAuthorizationCodeFlow(address, accessType, port) {
|
|
166
|
+
const callbackPath = '/';
|
|
167
|
+
// eslint-disable-next-line unicorn/prefer-event-target
|
|
168
|
+
const emitter = new EventEmitter();
|
|
169
|
+
const eventName = 'authorication_code_callback_params';
|
|
170
|
+
const server = http
|
|
171
|
+
.createServer((req, res) => {
|
|
172
|
+
if (req?.url?.startsWith(callbackPath)) {
|
|
173
|
+
const params = querystring.decode(req?.url.replace(`${callbackPath}?`, ''));
|
|
174
|
+
emitter.emit(eventName, params);
|
|
175
|
+
res.end('This browser window was opened by `apollo login`, you can close it now.');
|
|
176
|
+
res.socket?.end();
|
|
177
|
+
res.socket?.destroy();
|
|
178
|
+
server.close();
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// TODO: handle an invalid URL address
|
|
182
|
+
res.end('Unsupported');
|
|
183
|
+
emitter.emit(eventName, new Error('Invalid URL address'));
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.listen(port);
|
|
187
|
+
server.on('error', async (e) => {
|
|
188
|
+
if (e.message.includes('EADDRINUSE')) {
|
|
189
|
+
this.logToStderr(`It appears that port ${port} is in use. Perhaps you have JBrowse running?\nTry using a different port using the --port option or temporarily stop JBrowse`);
|
|
190
|
+
// eslint-disable-next-line unicorn/no-process-exit
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
this.logToStderr(e.message);
|
|
195
|
+
// eslint-disable-next-line unicorn/no-process-exit
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
// await ux.anykey('Press any key to open your browser') // Do we need this?
|
|
200
|
+
const authorizationCodeURL = `${address}/auth/login?type=${accessType}&redirect_uri=http://localhost:${port}${callbackPath}`;
|
|
201
|
+
await open(authorizationCodeURL);
|
|
202
|
+
ux.action.start('Waiting for authentication');
|
|
203
|
+
const { access_token } = await waitFor(eventName, emitter);
|
|
204
|
+
return { accessToken: access_token };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { expect, test } from '@oclif/test';
|
|
7
|
+
import YAML from 'yaml';
|
|
8
|
+
import { CONFIG_FILE, TEST_DATA_DIR, VERBOSE, copyFile, } from '../test/fixtures.js';
|
|
9
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
10
|
+
describe('apollo login: Config file does not exist', () => {
|
|
11
|
+
const cmd = ['login', '--config-file', '_tmp.yaml'];
|
|
12
|
+
test
|
|
13
|
+
.stderr()
|
|
14
|
+
.command(cmd, { root: dirname(dirname(__dirname)) })
|
|
15
|
+
.exit(1)
|
|
16
|
+
.do((output) => expect(output.stderr).to.contain('apollo config'))
|
|
17
|
+
.it(cmd.join(' '));
|
|
18
|
+
});
|
|
19
|
+
describe('apollo login: Profile does not exist', () => {
|
|
20
|
+
const cmd = [
|
|
21
|
+
'login',
|
|
22
|
+
'--config-file',
|
|
23
|
+
'test_data/complete_config.yaml',
|
|
24
|
+
'--profile',
|
|
25
|
+
'notavailable',
|
|
26
|
+
];
|
|
27
|
+
test
|
|
28
|
+
.stderr()
|
|
29
|
+
.command(cmd, { root: dirname(dirname(__dirname)) })
|
|
30
|
+
.exit(1)
|
|
31
|
+
.do((output) => expect(output.stderr).to.contain('apollo config'))
|
|
32
|
+
.do((output) => expect(output.stderr).to.contain('Profile'))
|
|
33
|
+
.do((output) => expect(output.stderr).to.contain('notavailable'))
|
|
34
|
+
.it(cmd.join(' '));
|
|
35
|
+
});
|
|
36
|
+
// TODO: Mock server
|
|
37
|
+
describe.skip('apollo login: Add token for guest', () => {
|
|
38
|
+
before(() => {
|
|
39
|
+
copyFile(`${TEST_DATA_DIR}/guest.yaml`, CONFIG_FILE, VERBOSE);
|
|
40
|
+
});
|
|
41
|
+
after(() => {
|
|
42
|
+
fs.rmSync(CONFIG_FILE);
|
|
43
|
+
});
|
|
44
|
+
const cmd = ['login'];
|
|
45
|
+
test
|
|
46
|
+
.stdout()
|
|
47
|
+
.command(cmd, { root: dirname(dirname(__dirname)) })
|
|
48
|
+
.it(cmd.join(' '), () => {
|
|
49
|
+
const cfg = YAML.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
50
|
+
expect(cfg.default.accessToken).not.empty;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../baseCommand.js';
|
|
2
|
+
export default class Logout extends BaseCommand<typeof Logout> {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: {
|
|
6
|
+
description: string;
|
|
7
|
+
command: string;
|
|
8
|
+
}[];
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|