@apollo-annotation/cli 0.3.9 → 0.3.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 +125 -35
- package/dist/commands/feature/add.d.ts +57 -0
- package/dist/commands/feature/add.js +323 -0
- package/dist/commands/feature/get-indexed-id.d.ts +17 -0
- package/dist/commands/feature/get-indexed-id.js +66 -0
- package/dist/test/test.js +194 -2
- package/oclif.manifest.json +303 -100
- package/package.json +4 -3
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { ObjectId } from 'bson';
|
|
4
|
+
import { fetch } from 'undici';
|
|
5
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
6
|
+
import { createFetchErrorMessage, localhostToAddress } from '../../utils.js';
|
|
7
|
+
export default class Add extends BaseCommand {
|
|
8
|
+
static summary = 'Add one or more features to Apollo';
|
|
9
|
+
static description = `A single simple feature can be added using the --min, --max, etc. flags.
|
|
10
|
+
|
|
11
|
+
To add multiple features, features with more details, or features with children, you can pass in JSON via argument or stdin or use the --feature-json-file options.
|
|
12
|
+
`;
|
|
13
|
+
static examples = [
|
|
14
|
+
{
|
|
15
|
+
description: 'Add a single feature by specifying its location and type',
|
|
16
|
+
command: '<%= config.bin %> <%= command.id %> --assembly hg19 --refSeq chr3 --min 1000 --max 5000 --type remark',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
description: 'Add a single feature from inline JSON',
|
|
20
|
+
command: '<%= config.bin %> <%= command.id %> \'{"assembly":"<assemblyNameOrId>","refseq":"<refSeqNameOrId>","min":1,"max":100,"type":"<featureType>"}\'',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
description: 'Add mutilple features from stdin JSON',
|
|
24
|
+
command: 'echo \'[{"assembly":"<assemblyNameOrId>","refseq":"<refSeqNameOrId>","min":1,"max":100,"type":"<featureType>"},{"assembly":"<assemblyNameOrId>","refseq":"<refSeqNameOrId>","min":101,"max":200,"type":"<featureType>"}]\' | <%= config.bin %> <%= command.id %>',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
description: 'Add a feature with children from inline JSON',
|
|
28
|
+
command: '<%= config.bin %> <%= command.id %> \'{"assembly":"<assemblyNameOrId>","refseq":"<refSeqNameOrId>","min":1,"max":100,"type":"<featureType>","children":[{"min":1,"max":50,"type":"<featureType>"}]}\'',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
static flags = {
|
|
32
|
+
assembly: Flags.string({
|
|
33
|
+
char: 'a',
|
|
34
|
+
description: 'Name or ID of target assembly. Not required if refseq is unique in the database',
|
|
35
|
+
}),
|
|
36
|
+
refSeq: Flags.string({
|
|
37
|
+
char: 'r',
|
|
38
|
+
description: 'Name or ID of target reference sequence',
|
|
39
|
+
dependsOn: ['min', 'max', 'type'],
|
|
40
|
+
exclusive: ['feature-json-file'],
|
|
41
|
+
}),
|
|
42
|
+
min: Flags.integer({
|
|
43
|
+
char: 's',
|
|
44
|
+
description: 'Start position in target reference sequence',
|
|
45
|
+
dependsOn: ['refSeq', 'max', 'type'],
|
|
46
|
+
exclusive: ['feature-json-file'],
|
|
47
|
+
}),
|
|
48
|
+
max: Flags.integer({
|
|
49
|
+
char: 'e',
|
|
50
|
+
description: 'End position in target reference sequence',
|
|
51
|
+
dependsOn: ['refSeq', 'min', 'type'],
|
|
52
|
+
exclusive: ['feature-json-file'],
|
|
53
|
+
}),
|
|
54
|
+
type: Flags.string({
|
|
55
|
+
char: 't',
|
|
56
|
+
description: 'Type of child feature',
|
|
57
|
+
dependsOn: ['refSeq', 'min', 'max'],
|
|
58
|
+
exclusive: ['feature-json-file'],
|
|
59
|
+
}),
|
|
60
|
+
'feature-json-file': Flags.file({
|
|
61
|
+
char: 'F',
|
|
62
|
+
description: 'File with JSON describing the feature(s) to add',
|
|
63
|
+
exists: true,
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
static args = {
|
|
67
|
+
'feature-json': Args.string({
|
|
68
|
+
description: 'Inline JSON describing the feature(s) to add. Can also be provided via stdin.',
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
async run() {
|
|
72
|
+
const { args, flags } = this;
|
|
73
|
+
const { 'feature-json': featureJSONString } = args;
|
|
74
|
+
const { assembly, refSeq, min, max, type, 'feature-json-file': featureJSONFile, } = flags;
|
|
75
|
+
if (featureJSONString) {
|
|
76
|
+
if (assembly !== undefined ||
|
|
77
|
+
refSeq !== undefined ||
|
|
78
|
+
min !== undefined ||
|
|
79
|
+
max !== undefined ||
|
|
80
|
+
type !== undefined ||
|
|
81
|
+
featureJSONFile !== undefined) {
|
|
82
|
+
this.error('Cannot use the following flags when providing a feature JSON: --assembly, --refSeq, --min, --max, --type, --feature-json-file');
|
|
83
|
+
}
|
|
84
|
+
await this.addFeatureFromJSON(featureJSONString);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (featureJSONFile) {
|
|
88
|
+
const fileText = await readFile(featureJSONFile, 'utf8');
|
|
89
|
+
await this.addFeatureFromJSON(fileText);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (refSeq === undefined ||
|
|
93
|
+
min === undefined ||
|
|
94
|
+
max === undefined ||
|
|
95
|
+
type === undefined) {
|
|
96
|
+
this.error('Must provide all of: --refSeq, --min, --max, and --type');
|
|
97
|
+
}
|
|
98
|
+
await this.addFeatureFromFlags(refSeq, min, max, type, assembly);
|
|
99
|
+
}
|
|
100
|
+
async addFeatureFromJSON(featureJSONString) {
|
|
101
|
+
let featureJSON;
|
|
102
|
+
try {
|
|
103
|
+
featureJSON = parseFeatureJSON(featureJSONString);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
this.logToStderr('Error: feature JSON is not valid');
|
|
107
|
+
if (error instanceof Error || typeof error === 'string') {
|
|
108
|
+
this.error(error);
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(featureJSON)) {
|
|
113
|
+
const firstFeature = featureJSON.at(0);
|
|
114
|
+
if (!firstFeature) {
|
|
115
|
+
throw new Error('Feature array is empty');
|
|
116
|
+
}
|
|
117
|
+
const { assembly, refSeq } = firstFeature;
|
|
118
|
+
if (!featureJSON.every((feature) => feature.assembly === assembly)) {
|
|
119
|
+
throw new Error('Cannot add features to multiple assemblies at the same time');
|
|
120
|
+
}
|
|
121
|
+
const [assemblyId] = await this.getAssemblyAndRefSeqIds(refSeq, assembly);
|
|
122
|
+
const changedIds = [];
|
|
123
|
+
const changes = await Promise.all(featureJSON.map(async (singleFeatureJSON) => {
|
|
124
|
+
const { children, assembly, refSeq, ...rest } = singleFeatureJSON;
|
|
125
|
+
const refSeqDocument = await this.getRefSeq(refSeq);
|
|
126
|
+
return {
|
|
127
|
+
addedFeature: this.makeFeatureSnapshot({
|
|
128
|
+
assembly: assemblyId,
|
|
129
|
+
refSeq: refSeqDocument._id,
|
|
130
|
+
...rest,
|
|
131
|
+
}, changedIds),
|
|
132
|
+
};
|
|
133
|
+
}));
|
|
134
|
+
const change = {
|
|
135
|
+
changedIds,
|
|
136
|
+
typeName: 'AddFeatureChange',
|
|
137
|
+
assembly: assemblyId,
|
|
138
|
+
changes,
|
|
139
|
+
};
|
|
140
|
+
return this.submitChange(change);
|
|
141
|
+
}
|
|
142
|
+
const { assembly, refSeq, ...rest } = featureJSON;
|
|
143
|
+
const [assemblyId, refSeqId] = await this.getAssemblyAndRefSeqIds(refSeq, assembly);
|
|
144
|
+
const changedIds = [];
|
|
145
|
+
const details = {
|
|
146
|
+
addedFeature: this.makeFeatureSnapshot({
|
|
147
|
+
assembly: assemblyId,
|
|
148
|
+
refSeq: refSeqId,
|
|
149
|
+
...rest,
|
|
150
|
+
}, changedIds),
|
|
151
|
+
};
|
|
152
|
+
const change = {
|
|
153
|
+
changedIds,
|
|
154
|
+
typeName: 'AddFeatureChange',
|
|
155
|
+
assembly: assemblyId,
|
|
156
|
+
...details,
|
|
157
|
+
};
|
|
158
|
+
return this.submitChange(change);
|
|
159
|
+
}
|
|
160
|
+
makeFeatureSnapshot(details, ids) {
|
|
161
|
+
const { children, ...rest } = details;
|
|
162
|
+
let childrenDetails = undefined;
|
|
163
|
+
if (children) {
|
|
164
|
+
childrenDetails = {};
|
|
165
|
+
const { refSeq } = rest;
|
|
166
|
+
for (const child of children) {
|
|
167
|
+
const childDetails = this.makeFeatureSnapshot({ refSeq, ...child }, ids);
|
|
168
|
+
childrenDetails[childDetails._id] = childDetails;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const _id = new ObjectId().toHexString();
|
|
172
|
+
ids.push(_id);
|
|
173
|
+
return {
|
|
174
|
+
_id,
|
|
175
|
+
...rest,
|
|
176
|
+
...(childrenDetails ? { children: childrenDetails } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async addFeatureFromFlags(refSeqNameOrId, min, max, type, assemblyNameOrId) {
|
|
180
|
+
const [assemblyId, refSeqId] = await this.getAssemblyAndRefSeqIds(refSeqNameOrId, assemblyNameOrId);
|
|
181
|
+
const changedIds = [];
|
|
182
|
+
const details = {
|
|
183
|
+
addedFeature: this.makeFeatureSnapshot({ refSeq: refSeqId, min: min - 1, max, type }, changedIds),
|
|
184
|
+
};
|
|
185
|
+
const change = {
|
|
186
|
+
changedIds,
|
|
187
|
+
typeName: 'AddFeatureChange',
|
|
188
|
+
assembly: assemblyId,
|
|
189
|
+
...details,
|
|
190
|
+
};
|
|
191
|
+
return this.submitChange(change);
|
|
192
|
+
}
|
|
193
|
+
async getAssemblyAndRefSeqIds(refSeqNameOrId, assemblyNameOrId) {
|
|
194
|
+
const refSeqIsObjectId = ObjectId.isValid(refSeqNameOrId);
|
|
195
|
+
const assemblyIsObjectId = assemblyNameOrId
|
|
196
|
+
? ObjectId.isValid(assemblyNameOrId)
|
|
197
|
+
: false;
|
|
198
|
+
if (assemblyNameOrId && assemblyIsObjectId && refSeqIsObjectId) {
|
|
199
|
+
return [assemblyNameOrId, refSeqNameOrId];
|
|
200
|
+
}
|
|
201
|
+
if (refSeqIsObjectId) {
|
|
202
|
+
if (assemblyNameOrId) {
|
|
203
|
+
this.warn('Ignoring provided --assembly because it is not an ID');
|
|
204
|
+
}
|
|
205
|
+
const refSeqDocument = await this.getRefSeq(refSeqNameOrId);
|
|
206
|
+
return [refSeqDocument.assembly, refSeqNameOrId];
|
|
207
|
+
}
|
|
208
|
+
if (!assemblyNameOrId) {
|
|
209
|
+
this.error(`If provided refSeq (${refSeqNameOrId}) is not an ID, assembly must also be provided`);
|
|
210
|
+
}
|
|
211
|
+
const assemblyDocument = await this.getAssembly(assemblyNameOrId);
|
|
212
|
+
const refSeqDocument = await this.getRefSeq(refSeqNameOrId, assemblyDocument._id);
|
|
213
|
+
return [assemblyDocument._id, refSeqDocument._id];
|
|
214
|
+
}
|
|
215
|
+
async getAssembly(assemblyNameOrId) {
|
|
216
|
+
if (ObjectId.isValid(assemblyNameOrId)) {
|
|
217
|
+
const response = await this.fetch(`assemblies/${assemblyNameOrId}`);
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
const errorMessage = await createFetchErrorMessage(response, `Could not find assembly: "${assemblyNameOrId}"`);
|
|
220
|
+
this.error(errorMessage);
|
|
221
|
+
}
|
|
222
|
+
return response.json();
|
|
223
|
+
}
|
|
224
|
+
const response = await this.fetch('assemblies');
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
const errorMessage = await createFetchErrorMessage(response, `Could not find assembly: "${assemblyNameOrId}"`);
|
|
227
|
+
this.error(errorMessage);
|
|
228
|
+
}
|
|
229
|
+
const assemblies = (await response.json());
|
|
230
|
+
for (const assembly of assemblies) {
|
|
231
|
+
if (assembly.name === assemblyNameOrId ||
|
|
232
|
+
assembly.aliases?.includes(assemblyNameOrId)) {
|
|
233
|
+
return assembly;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`Could not find assembly: "${assemblyNameOrId}"`);
|
|
237
|
+
}
|
|
238
|
+
async getRefSeq(refSeqNameOrId, assemblyId) {
|
|
239
|
+
if (ObjectId.isValid(refSeqNameOrId)) {
|
|
240
|
+
const response = await this.fetch(`refSeqs/${refSeqNameOrId}`);
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
const errorMessage = await createFetchErrorMessage(response, `Could not find refSeq: "${refSeqNameOrId}"`);
|
|
243
|
+
this.error(errorMessage);
|
|
244
|
+
}
|
|
245
|
+
return response.json();
|
|
246
|
+
}
|
|
247
|
+
let endpoint = 'refSeqs';
|
|
248
|
+
if (assemblyId) {
|
|
249
|
+
const searchParams = new URLSearchParams({ assembly: assemblyId });
|
|
250
|
+
endpoint = `${endpoint}?${searchParams.toString()}`;
|
|
251
|
+
}
|
|
252
|
+
const response = await this.fetch(endpoint);
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
const errorMessage = await createFetchErrorMessage(response, `Could not find refSeq: "${refSeqNameOrId}"`);
|
|
255
|
+
this.error(errorMessage);
|
|
256
|
+
}
|
|
257
|
+
const refSeqs = (await response.json());
|
|
258
|
+
for (const refSeq of refSeqs) {
|
|
259
|
+
if (refSeq.name === refSeqNameOrId ||
|
|
260
|
+
refSeq.aliases?.includes(refSeqNameOrId)) {
|
|
261
|
+
return refSeq;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
throw new Error(`Could not find refSeq: "${refSeqNameOrId}"`);
|
|
265
|
+
}
|
|
266
|
+
async submitChange(change) {
|
|
267
|
+
const options = { method: 'POST', body: JSON.stringify(change) };
|
|
268
|
+
const response = await this.fetch('changes', options);
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
const errorMessage = await createFetchErrorMessage(response, 'Could not add feature');
|
|
271
|
+
this.error(errorMessage);
|
|
272
|
+
}
|
|
273
|
+
this.log(await response.text());
|
|
274
|
+
}
|
|
275
|
+
async fetch(endpoint, options) {
|
|
276
|
+
const { address, accessToken } = await this.getAccess();
|
|
277
|
+
const url = new URL(localhostToAddress(`${address}/${endpoint}`));
|
|
278
|
+
const optionsWithAuth = {
|
|
279
|
+
...options,
|
|
280
|
+
headers: {
|
|
281
|
+
authorization: `Bearer ${accessToken}`,
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
...options?.headers,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
return fetch(url, optionsWithAuth);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function parseFeatureJSON(featureJSONString) {
|
|
290
|
+
const featureJSON = JSON.parse(featureJSONString);
|
|
291
|
+
if (Array.isArray(featureJSON)) {
|
|
292
|
+
return featureJSON.map((feature) => {
|
|
293
|
+
assertFeatureIsValid(feature, true);
|
|
294
|
+
return feature;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
assertFeatureIsValid(featureJSON, true);
|
|
298
|
+
return featureJSON;
|
|
299
|
+
}
|
|
300
|
+
function assertFeatureIsValid(feature, topLevel = false) {
|
|
301
|
+
if (typeof feature !== 'object' ||
|
|
302
|
+
feature === null ||
|
|
303
|
+
Array.isArray(feature)) {
|
|
304
|
+
throw new TypeError(`Feature is not a key-value record: '${JSON.stringify(feature)}'`);
|
|
305
|
+
}
|
|
306
|
+
for (const attribute of ['min', 'max', 'type']) {
|
|
307
|
+
if (!(attribute in feature)) {
|
|
308
|
+
throw new Error(`Feature does not contain "${attribute}": '${JSON.stringify(feature)}'`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (topLevel && !('refSeq' in feature)) {
|
|
312
|
+
throw new Error(`Top-level feature does not contain "refSeq": '${JSON.stringify(feature)}'`);
|
|
313
|
+
}
|
|
314
|
+
if ('children' in feature) {
|
|
315
|
+
const { children } = feature;
|
|
316
|
+
if (!Array.isArray(children)) {
|
|
317
|
+
throw new TypeError(`"children" is not an array of features '${JSON.stringify(feature)}'`);
|
|
318
|
+
}
|
|
319
|
+
for (const child of children) {
|
|
320
|
+
assertFeatureIsValid(child);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
2
|
+
export default class Get extends BaseCommand<typeof Get> {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: {
|
|
6
|
+
description: string;
|
|
7
|
+
command: string;
|
|
8
|
+
}[];
|
|
9
|
+
static args: {
|
|
10
|
+
id: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
11
|
+
};
|
|
12
|
+
static flags: {
|
|
13
|
+
assembly: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
14
|
+
topLevel: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { fetch } from 'undici';
|
|
3
|
+
import { BaseCommand } from '../../baseCommand.js';
|
|
4
|
+
import { convertAssemblyNameToId, createFetchErrorMessage, idReader, localhostToAddress, } from '../../utils.js';
|
|
5
|
+
export default class Get extends BaseCommand {
|
|
6
|
+
static summary = 'Get features given an indexed identifier';
|
|
7
|
+
static description = 'Get features that match a given indexed identifier, such as the ID of a feature from an imported GFF3 file';
|
|
8
|
+
static examples = [
|
|
9
|
+
{
|
|
10
|
+
description: 'Get features for this indexed identifier:',
|
|
11
|
+
command: '<%= config.bin %> <%= command.id %> -i abc...zyz def...foo',
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
static args = {
|
|
15
|
+
id: Args.string({
|
|
16
|
+
description: 'Indexed identifier to search for',
|
|
17
|
+
required: true,
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
static flags = {
|
|
21
|
+
assembly: Flags.string({
|
|
22
|
+
char: 'a',
|
|
23
|
+
multiple: true,
|
|
24
|
+
description: 'Assembly names or IDs to search; use "-" to read it from stdin. If omitted search all assemblies',
|
|
25
|
+
}),
|
|
26
|
+
topLevel: Flags.boolean({
|
|
27
|
+
description: 'Return the top-level parent of the feature instead of the feature itself',
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
async run() {
|
|
31
|
+
const { args, flags } = await this.parse(Get);
|
|
32
|
+
const access = await this.getAccess();
|
|
33
|
+
const { topLevel } = flags;
|
|
34
|
+
const { id } = args;
|
|
35
|
+
const assembly = flags.assembly && (await idReader(flags.assembly));
|
|
36
|
+
const assemblyIds = assembly &&
|
|
37
|
+
(await convertAssemblyNameToId(access.address, access.accessToken, assembly));
|
|
38
|
+
if (assemblyIds?.length === 0) {
|
|
39
|
+
this.log(JSON.stringify([], null, 2));
|
|
40
|
+
this.exit(0);
|
|
41
|
+
}
|
|
42
|
+
const url = new URL(localhostToAddress(`${access.address}/features/getByIndexedId`));
|
|
43
|
+
const searchParams = new URLSearchParams({ id });
|
|
44
|
+
if (assemblyIds) {
|
|
45
|
+
searchParams.append('assemblies', assemblyIds.join(','));
|
|
46
|
+
}
|
|
47
|
+
if (topLevel) {
|
|
48
|
+
searchParams.append('topLevel', 'true');
|
|
49
|
+
}
|
|
50
|
+
url.search = searchParams.toString();
|
|
51
|
+
const uri = url.toString();
|
|
52
|
+
const auth = {
|
|
53
|
+
headers: {
|
|
54
|
+
authorization: `Bearer ${access.accessToken}`,
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const response = await fetch(uri, auth);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const errorMessage = await createFetchErrorMessage(response, 'Failed to access Apollo with the current address and/or access token\nThe server returned:\n');
|
|
61
|
+
throw new Error(errorMessage);
|
|
62
|
+
}
|
|
63
|
+
const results = (await response.json());
|
|
64
|
+
this.log(JSON.stringify(results, null, 2));
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/test/test.js
CHANGED
|
@@ -18,14 +18,19 @@
|
|
|
18
18
|
import assert from 'node:assert';
|
|
19
19
|
import * as crypto from 'node:crypto';
|
|
20
20
|
import fs from 'node:fs';
|
|
21
|
-
import { afterEach, before, beforeEach, describe } from 'node:test';
|
|
21
|
+
import { after, afterEach, before, beforeEach, describe } from 'node:test';
|
|
22
|
+
import { MongoClient } from 'mongodb';
|
|
22
23
|
import { Shell, deleteAllChecks } from './utils.js';
|
|
23
24
|
const apollo = 'yarn dev';
|
|
24
25
|
const P = '--profile testAdmin';
|
|
26
|
+
// let client = MongoClient
|
|
27
|
+
let client;
|
|
25
28
|
let configFile = '';
|
|
26
29
|
let configFileBak = '';
|
|
27
30
|
void describe('Test CLI', () => {
|
|
28
31
|
before(() => {
|
|
32
|
+
const uri = 'mongodb://localhost:27017/apolloTestCliDb?directConnection=true';
|
|
33
|
+
client = new MongoClient(uri);
|
|
29
34
|
configFile = new Shell(`${apollo} config --get-config-file`).stdout.trim();
|
|
30
35
|
configFileBak = `${configFile}.bak`;
|
|
31
36
|
if (fs.existsSync(configFileBak)) {
|
|
@@ -36,11 +41,24 @@ void describe('Test CLI', () => {
|
|
|
36
41
|
new Shell(`${apollo} config ${P} rootPassword pass`);
|
|
37
42
|
new Shell(`${apollo} login ${P} -f`);
|
|
38
43
|
});
|
|
44
|
+
after(async () => {
|
|
45
|
+
await client.close();
|
|
46
|
+
});
|
|
39
47
|
beforeEach(() => {
|
|
40
48
|
// Backup starting config file
|
|
41
49
|
fs.copyFileSync(configFile, configFileBak);
|
|
42
50
|
});
|
|
43
|
-
afterEach(() => {
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
const database = client.db('apolloTestCliDb');
|
|
53
|
+
await Promise.all([
|
|
54
|
+
'assemblies',
|
|
55
|
+
'changes',
|
|
56
|
+
'counters',
|
|
57
|
+
'features',
|
|
58
|
+
'files',
|
|
59
|
+
'refseqchunks',
|
|
60
|
+
'refseqs',
|
|
61
|
+
].map((collectionName) => database.collection(collectionName).deleteMany({})));
|
|
44
62
|
// Put back starting config file
|
|
45
63
|
fs.renameSync(configFileBak, configFile);
|
|
46
64
|
});
|
|
@@ -501,6 +519,111 @@ void describe('Test CLI', () => {
|
|
|
501
519
|
p = new Shell(`${apollo} feature search ${P} -a vv1 -t Q`);
|
|
502
520
|
assert.ok(p.stdout.includes('"Q"'));
|
|
503
521
|
});
|
|
522
|
+
void globalThis.itName('Get feature by indexed ID', () => {
|
|
523
|
+
new Shell(`${apollo} assembly add-from-gff ${P} test_data/tiny.fasta.gff3 -a vv1 -f`);
|
|
524
|
+
new Shell(`${apollo} assembly add-from-gff ${P} test_data/tiny.fasta.gff3 -a vv2 -f`);
|
|
525
|
+
// Search multiple assemblies
|
|
526
|
+
let p = new Shell(`${apollo} feature get-indexed-id ${P} MyGene -a vv1 vv2`);
|
|
527
|
+
let out = JSON.parse(p.stdout);
|
|
528
|
+
assert.strictEqual(out.length, 2);
|
|
529
|
+
assert.ok(p.stdout.includes('MyGene'));
|
|
530
|
+
// Specifying no assembly defaults to searching all assemblies
|
|
531
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} MyGene`);
|
|
532
|
+
out = JSON.parse(p.stdout);
|
|
533
|
+
assert.strictEqual(out.length, 2);
|
|
534
|
+
assert.ok(p.stdout.includes('MyGene'));
|
|
535
|
+
// Search single assembly
|
|
536
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} MyGene -a vv1`);
|
|
537
|
+
out = JSON.parse(p.stdout);
|
|
538
|
+
assert.strictEqual(out.length, 1);
|
|
539
|
+
assert.ok(p.stdout.includes('MyGene'));
|
|
540
|
+
// Warn on unknown assembly
|
|
541
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} EDEN -a foobar`);
|
|
542
|
+
assert.strictEqual('[]', p.stdout.trim());
|
|
543
|
+
assert.ok(p.stderr.includes('Warning'));
|
|
544
|
+
// Return empty array with no matches
|
|
545
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} foobarspam -a vv1`);
|
|
546
|
+
assert.deepStrictEqual(p.stdout.trim(), '[]');
|
|
547
|
+
// Gets subfeature
|
|
548
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} myCDS.1 -a vv1`);
|
|
549
|
+
out = JSON.parse(p.stdout);
|
|
550
|
+
assert.strictEqual(out.length, 1);
|
|
551
|
+
assert.ok(out.at(0)?.type === 'CDS');
|
|
552
|
+
// Gets top-level feature from subfeature id
|
|
553
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} myCDS.1 -a vv1 --topLevel`);
|
|
554
|
+
out = JSON.parse(p.stdout);
|
|
555
|
+
assert.strictEqual(out.length, 1);
|
|
556
|
+
assert.ok(out.at(0)?.type === 'gene');
|
|
557
|
+
// Gets feature and child feature that were added manually (not imported)
|
|
558
|
+
p = new Shell(`${apollo} feature add ${P} <<EOF
|
|
559
|
+
{
|
|
560
|
+
"assembly": "vv1",
|
|
561
|
+
"refSeq": "ctgA",
|
|
562
|
+
"min": 301,
|
|
563
|
+
"max": 310,
|
|
564
|
+
"type": "match",
|
|
565
|
+
"attributes": {"gff_id": ["match1"]},
|
|
566
|
+
"children": [
|
|
567
|
+
{
|
|
568
|
+
"min": 301,
|
|
569
|
+
"max": 305,
|
|
570
|
+
"type": "match_part",
|
|
571
|
+
"attributes": {"gff_id": ["matchPart1"]}
|
|
572
|
+
}
|
|
573
|
+
]
|
|
574
|
+
}
|
|
575
|
+
EOF`);
|
|
576
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} match1 -a vv1`);
|
|
577
|
+
out = JSON.parse(p.stdout);
|
|
578
|
+
assert.strictEqual(out.length, 1);
|
|
579
|
+
assert.ok(p.stdout.includes('match1'));
|
|
580
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} matchPart1 -a vv1`);
|
|
581
|
+
out = JSON.parse(p.stdout);
|
|
582
|
+
assert.strictEqual(out.length, 1);
|
|
583
|
+
assert.ok(p.stdout.includes('matchPart1'));
|
|
584
|
+
// Doesn't get child feature after it was deleted
|
|
585
|
+
const idToDelete = out[0]._id;
|
|
586
|
+
new Shell(`${apollo} feature delete ${P} -i ${idToDelete}`);
|
|
587
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} matchPart1 -a vv1`);
|
|
588
|
+
assert.deepStrictEqual(p.stdout.trim(), '[]');
|
|
589
|
+
// Gets feature after ID was manually added
|
|
590
|
+
p = new Shell(`${apollo} feature add ${P} <<EOF
|
|
591
|
+
{
|
|
592
|
+
"assembly": "vv1",
|
|
593
|
+
"refSeq": "ctgA",
|
|
594
|
+
"min": 311,
|
|
595
|
+
"max": 320,
|
|
596
|
+
"type": "match"
|
|
597
|
+
}
|
|
598
|
+
EOF`);
|
|
599
|
+
out = JSON.parse(p.stdout);
|
|
600
|
+
const { assembly, changes } = out;
|
|
601
|
+
const { _id, refSeq } = changes[0].addedFeature;
|
|
602
|
+
new Shell(`${apollo} feature edit-attribute ${P} -i ${_id} -a gff_id -v match2`);
|
|
603
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} match2 -a vv1`);
|
|
604
|
+
out = JSON.parse(p.stdout);
|
|
605
|
+
assert.strictEqual(out.length, 1);
|
|
606
|
+
assert.ok(p.stdout.includes('match2'));
|
|
607
|
+
// Gets child featuer after it was added with an ID
|
|
608
|
+
// add-child CLI command doesn't support adding attributes yet, so we'll
|
|
609
|
+
// manually do it with curl for testing for neow
|
|
610
|
+
p = new Shell(`${apollo} config ${P} accessToken`);
|
|
611
|
+
const token = p.stdout.trim();
|
|
612
|
+
const newChildFeatureID = '69408088d502fc21aea1bb0a';
|
|
613
|
+
new Shell(`curl -X POST http://127.0.0.1:3999/changes -d '{"typeName":"AddFeatureChange","changedIds":["${newChildFeatureID}"],"assembly":"${assembly}","addedFeature":{"_id":"${newChildFeatureID}","refSeq":"${refSeq}","min":311,"max":315,"type":"match_part","attributes":{"gff_id":["matchPart2"]}},"parentFeatureId":"${_id}"}' -H "Content-Type: application/json" -H "Authorization: Bearer ${token}"`);
|
|
614
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} matchPart2 -a vv1`);
|
|
615
|
+
out = JSON.parse(p.stdout);
|
|
616
|
+
assert.strictEqual(out.length, 1);
|
|
617
|
+
assert.ok(p.stdout.includes('matchPart2'));
|
|
618
|
+
// Doesn't get feature or child after IDs were manually removed
|
|
619
|
+
new Shell(`${apollo} feature edit-attribute ${P} -i ${_id} -a gff_id -d`);
|
|
620
|
+
const childId = out[0]._id;
|
|
621
|
+
new Shell(`${apollo} feature edit-attribute ${P} -i ${childId} -a gff_id -d`);
|
|
622
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} match2 -a vv1`);
|
|
623
|
+
assert.deepStrictEqual(p.stdout.trim(), '[]');
|
|
624
|
+
p = new Shell(`${apollo} feature get-indexed-id ${P} matchPart2 -a vv1`);
|
|
625
|
+
assert.deepStrictEqual(p.stdout.trim(), '[]');
|
|
626
|
+
});
|
|
504
627
|
void globalThis.itName('Delete features', () => {
|
|
505
628
|
new Shell(`${apollo} assembly add-from-gff ${P} test_data/tiny.fasta.gff3 -a vv1 -f`);
|
|
506
629
|
let p = new Shell(`${apollo} feature search ${P} -a vv1 -t EDEN`);
|
|
@@ -516,6 +639,75 @@ void describe('Test CLI', () => {
|
|
|
516
639
|
p = new Shell(`${apollo} feature delete ${P} --force -i ${fid}`);
|
|
517
640
|
assert.strictEqual(p.returncode, 0);
|
|
518
641
|
});
|
|
642
|
+
void globalThis.itName('Add features', () => {
|
|
643
|
+
let p = new Shell(`${apollo} assembly add-from-fasta ${P} test_data/tiny.fasta.gz -a tiny -f`);
|
|
644
|
+
let out = JSON.parse(p.stdout);
|
|
645
|
+
const assemblyId = out._id;
|
|
646
|
+
p = new Shell(`${apollo} feature get ${P} -a tiny`);
|
|
647
|
+
assert.deepStrictEqual(p.stdout.trim(), '[]');
|
|
648
|
+
// Can add a feature using flags
|
|
649
|
+
p = new Shell(`${apollo} feature add ${P} -a tiny -r ctgA -s 1 -e 10 -t remark`);
|
|
650
|
+
out = JSON.parse(p.stdout);
|
|
651
|
+
p = new Shell(`${apollo} feature get ${P} -a tiny`);
|
|
652
|
+
out = JSON.parse(p.stdout);
|
|
653
|
+
assert.strictEqual(out.length, 1);
|
|
654
|
+
const refSeqId = out[0].refSeq;
|
|
655
|
+
// Can add a feature using assembly and refSeq ids
|
|
656
|
+
p = new Shell(`${apollo} feature add ${P} -a ${assemblyId} -r ${refSeqId} -s 11 -e 20 -t remark`);
|
|
657
|
+
p = new Shell(`${apollo} feature get ${P} -a ${assemblyId}`);
|
|
658
|
+
out = JSON.parse(p.stdout);
|
|
659
|
+
assert.strictEqual(out.length, 2);
|
|
660
|
+
// Can add a feature using JSON arg
|
|
661
|
+
p = new Shell(`${apollo} feature add ${P} '{"assembly":"${assemblyId}","refSeq":"${refSeqId}","min":21,"max":30,"type":"remark"}'`);
|
|
662
|
+
p = new Shell(`${apollo} feature get ${P} -a ${assemblyId}`);
|
|
663
|
+
out = JSON.parse(p.stdout);
|
|
664
|
+
assert.strictEqual(out.length, 3);
|
|
665
|
+
// Can add a feature using JSON from stdin
|
|
666
|
+
p = new Shell(`${apollo} feature add ${P} <<EOF
|
|
667
|
+
{
|
|
668
|
+
"assembly": "${assemblyId}",
|
|
669
|
+
"refSeq": "${refSeqId}",
|
|
670
|
+
"min": 31,
|
|
671
|
+
"max": 40,
|
|
672
|
+
"type": "remark"
|
|
673
|
+
}
|
|
674
|
+
EOF`);
|
|
675
|
+
p = new Shell(`${apollo} feature get ${P} -a ${assemblyId}`);
|
|
676
|
+
out = JSON.parse(p.stdout);
|
|
677
|
+
assert.strictEqual(out.length, 4);
|
|
678
|
+
// Can add a feature using JSON from a file
|
|
679
|
+
fs.writeFileSync('test_data/tmp.json', `{"assembly":"${assemblyId}","refSeq":"${refSeqId}","min":41,"max":50,"type":"remark"}\n`);
|
|
680
|
+
p = new Shell(`${apollo} feature add ${P} --feature-json-file test_data/tmp.json`);
|
|
681
|
+
fs.unlinkSync('test_data/tmp.json');
|
|
682
|
+
p = new Shell(`${apollo} feature get ${P} -a ${assemblyId}`);
|
|
683
|
+
out = JSON.parse(p.stdout);
|
|
684
|
+
assert.strictEqual(out.length, 5);
|
|
685
|
+
// Can add multiple features using JSON
|
|
686
|
+
p = new Shell(`${apollo} feature add ${P} '[{"assembly":"${assemblyId}","refSeq":"${refSeqId}","min":51,"max":60,"type":"remark"},{"assembly":"${assemblyId}","refSeq":"${refSeqId}","min":61,"max":70,"type":"remark"}]'`);
|
|
687
|
+
p = new Shell(`${apollo} feature get ${P} -a ${assemblyId}`);
|
|
688
|
+
out = JSON.parse(p.stdout);
|
|
689
|
+
assert.strictEqual(out.length, 7);
|
|
690
|
+
// Can add a feature with children from JSON
|
|
691
|
+
p = new Shell(`${apollo} feature add ${P} '{"assembly":"${assemblyId}","refSeq":"${refSeqId}","min":71,"max":80,"type":"match","children":[{"min":71,"max":75,"type":"match_part"}]}'`);
|
|
692
|
+
p = new Shell(`${apollo} feature get ${P} -a ${assemblyId} -r ${refSeqId} -s 71 -e 80`);
|
|
693
|
+
out = JSON.parse(p.stdout);
|
|
694
|
+
let feature = out.at(0);
|
|
695
|
+
assert.strictEqual(Object.keys(feature?.children).length, 1);
|
|
696
|
+
// Can add a feature with attributes from JSON
|
|
697
|
+
p = new Shell(`${apollo} feature add ${P} '{"assembly":"${assemblyId}","refSeq":"${refSeqId}","min":81,"max":90,"type":"remark","attributes":{"key1":["val1"]}}'`);
|
|
698
|
+
p = new Shell(`${apollo} feature get ${P} -a ${assemblyId} -r ${refSeqId} -s 81 -e 90`);
|
|
699
|
+
out = JSON.parse(p.stdout);
|
|
700
|
+
feature = out.at(0);
|
|
701
|
+
assert.strictEqual(feature?.attributes?.key1?.[0], 'val1');
|
|
702
|
+
// Can add a feature with children from JSON
|
|
703
|
+
p = new Shell(`${apollo} feature add ${P} '{"assembly":"${assemblyId}","refSeq":"${refSeqId}","min":91,"max":100,"type":"match","children":[{"min":91,"max":95,"type":"match_part","attributes":{"key2":["val2"]}}]}'`);
|
|
704
|
+
p = new Shell(`${apollo} feature get ${P} -a ${assemblyId} -r ${refSeqId} -s 91 -e 100`);
|
|
705
|
+
out = JSON.parse(p.stdout);
|
|
706
|
+
feature = out.at(0);
|
|
707
|
+
const keys = Object.keys(feature?.children);
|
|
708
|
+
assert.strictEqual(keys.length, 1);
|
|
709
|
+
assert.strictEqual(feature.children[keys[0]].attributes.key2[0], 'val2');
|
|
710
|
+
});
|
|
519
711
|
void globalThis.itName('Add child features', () => {
|
|
520
712
|
new Shell(`${apollo} assembly add-from-gff ${P} test_data/tiny.fasta.gff3 -a vv1 -f`);
|
|
521
713
|
let p = new Shell(`${apollo} feature search ${P} -a vv1 -t contig`);
|