diff --git a/packages/apollo-cli/README.md b/packages/apollo-cli/README.md index 9ff839be5..648f4a375 100644 --- a/packages/apollo-cli/README.md +++ b/packages/apollo-cli/README.md @@ -38,6 +38,7 @@ USAGE - [`apollo change get`](#apollo-change-get) - [`apollo config [KEY] [VALUE]`](#apollo-config-key-value) - [`apollo export gff3 ASSEMBLY`](#apollo-export-gff3-assembly) +- [`apollo feature add [FEATURE-JSON]`](#apollo-feature-add-feature-json) - [`apollo feature add-child`](#apollo-feature-add-child) - [`apollo feature check`](#apollo-feature-check) - [`apollo feature copy`](#apollo-feature-copy) @@ -385,6 +386,62 @@ EXAMPLES _See code: [src/commands/export/gff3.ts](https://github.com/GMOD/Apollo3/blob/v0.3.10/packages/apollo-cli/src/commands/export/gff3.ts)_ +## `apollo feature add [FEATURE-JSON]` + +Add one or more features to Apollo + +``` +USAGE + $ apollo feature add [FEATURE-JSON] [--profile ] [--config-file ] [-a ] [-r -s + -e -t ] [-F ] + +ARGUMENTS + FEATURE-JSON Inline JSON describing the feature(s) to add. Can also be provided via stdin. + +FLAGS + -F, --feature-json-file= File with JSON describing the feature(s) to add + -a, --assembly= Name or ID of target assembly. Not required if refseq is unique in the database + -e, --max= End position in target reference sequence + -r, --refSeq= Name or ID of target reference sequence + -s, --min= Start position in target reference sequence + -t, --type= Type of child feature + --config-file= Use this config file (mostly for testing) + --profile= Use credentials from this profile + +DESCRIPTION + Add one or more features to Apollo + + A single simple feature can be added using the --min, --max, etc. flags. + + 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. + + +EXAMPLES + Add a single feature by specifying its location and type + + $ apollo feature add --assembly hg19 --refSeq chr3 --min 1000 --max 5000 --type remark + + Add a single feature from inline JSON + + $ apollo feature add \ + '{"assembly":"","refseq":"","min":1,"max":100,"type":""}' + + Add mutilple features from stdin JSON + + echo '[{"assembly":"","refseq":"","min":1,"max":100,"type":""},{" \ + assembly":"","refseq":"","min":101,"max":200,"type":""}]' | \ + apollo feature add + + Add a feature with children from inline JSON + + $ apollo feature add '{"assembly":"","refseq":"","min":1,"max":100,"type":"","children":[{"min":1,"max":50,"type":""}]}' +``` + +_See code: +[src/commands/feature/add.ts](https://github.com/GMOD/Apollo3/blob/v0.3.10/packages/apollo-cli/src/commands/feature/add.ts)_ + ## `apollo feature add-child` Add a child feature (e.g. add an exon to an mRNA) diff --git a/packages/apollo-cli/src/commands/feature/add.ts b/packages/apollo-cli/src/commands/feature/add.ts new file mode 100644 index 000000000..9d9c06679 --- /dev/null +++ b/packages/apollo-cli/src/commands/feature/add.ts @@ -0,0 +1,484 @@ +import { readFile } from 'node:fs/promises' + +import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst' +import { + type AddFeatureChangeDetails, + type SerializedAddFeatureChange, +} from '@apollo-annotation/shared' +import { Args, Flags } from '@oclif/core' +import { ObjectId } from 'bson' +import { type Response, fetch } from 'undici' + +import { BaseCommand } from '../../baseCommand.js' +import { createFetchErrorMessage, localhostToAddress } from '../../utils.js' + +interface BaseFeatureJSON { + min: number + max: number + type: string + children?: BaseFeatureJSON[] + [key: string]: unknown +} + +interface FeatureJSON extends BaseFeatureJSON { + assembly?: string + refSeq: string +} + +export default class Add extends BaseCommand { + static summary = 'Add one or more features to Apollo' + static description = `A single simple feature can be added using the --min, --max, etc. flags. + +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. +` + + static examples = [ + { + description: 'Add a single feature by specifying its location and type', + command: + '<%= config.bin %> <%= command.id %> --assembly hg19 --refSeq chr3 --min 1000 --max 5000 --type remark', + }, + { + description: 'Add a single feature from inline JSON', + command: + '<%= config.bin %> <%= command.id %> \'{"assembly":"","refseq":"","min":1,"max":100,"type":""}\'', + }, + { + description: 'Add mutilple features from stdin JSON', + command: + 'echo \'[{"assembly":"","refseq":"","min":1,"max":100,"type":""},{"assembly":"","refseq":"","min":101,"max":200,"type":""}]\' | <%= config.bin %> <%= command.id %>', + }, + { + description: 'Add a feature with children from inline JSON', + command: + '<%= config.bin %> <%= command.id %> \'{"assembly":"","refseq":"","min":1,"max":100,"type":"","children":[{"min":1,"max":50,"type":""}]}\'', + }, + ] + + static flags = { + assembly: Flags.string({ + char: 'a', + description: + 'Name or ID of target assembly. Not required if refseq is unique in the database', + }), + refSeq: Flags.string({ + char: 'r', + description: 'Name or ID of target reference sequence', + dependsOn: ['min', 'max', 'type'], + exclusive: ['feature-json-file'], + }), + min: Flags.integer({ + char: 's', + description: 'Start position in target reference sequence', + dependsOn: ['refSeq', 'max', 'type'], + exclusive: ['feature-json-file'], + }), + max: Flags.integer({ + char: 'e', + description: 'End position in target reference sequence', + dependsOn: ['refSeq', 'min', 'type'], + exclusive: ['feature-json-file'], + }), + type: Flags.string({ + char: 't', + description: 'Type of child feature', + dependsOn: ['refSeq', 'min', 'max'], + exclusive: ['feature-json-file'], + }), + 'feature-json-file': Flags.file({ + char: 'F', + description: 'File with JSON describing the feature(s) to add', + exists: true, + }), + } + + static args = { + 'feature-json': Args.string({ + description: + 'Inline JSON describing the feature(s) to add. Can also be provided via stdin.', + }), + } + + public async run(): Promise { + const { args, flags } = this + const { 'feature-json': featureJSONString } = args + const { + assembly, + refSeq, + min, + max, + type, + 'feature-json-file': featureJSONFile, + } = flags + if (featureJSONString) { + if ( + assembly !== undefined || + refSeq !== undefined || + min !== undefined || + max !== undefined || + type !== undefined || + featureJSONFile !== undefined + ) { + this.error( + 'Cannot use the following flags when providing a feature JSON: --assembly, --refSeq, --min, --max, --type, --feature-json-file', + ) + } + await this.addFeatureFromJSON(featureJSONString) + return + } + if (featureJSONFile) { + const fileText = await readFile(featureJSONFile, 'utf8') + await this.addFeatureFromJSON(fileText) + return + } + if ( + refSeq === undefined || + min === undefined || + max === undefined || + type === undefined + ) { + this.error('Must provide all of: --refSeq, --min, --max, and --type') + } + await this.addFeatureFromFlags(refSeq, min, max, type, assembly) + } + + async addFeatureFromJSON(featureJSONString: string) { + let featureJSON: FeatureJSON | FeatureJSON[] + try { + featureJSON = parseFeatureJSON(featureJSONString) + } catch (error) { + this.logToStderr('Error: feature JSON is not valid') + if (error instanceof Error || typeof error === 'string') { + this.error(error) + } + throw error + } + if (Array.isArray(featureJSON)) { + const firstFeature = featureJSON.at(0) + if (!firstFeature) { + throw new Error('Feature array is empty') + } + const { assembly, refSeq } = firstFeature + if (!featureJSON.every((feature) => feature.assembly === assembly)) { + throw new Error( + 'Cannot add features to multiple assemblies at the same time', + ) + } + const [assemblyId] = await this.getAssemblyAndRefSeqIds(refSeq, assembly) + const changedIds: string[] = [] + const changes = await Promise.all( + featureJSON.map( + async (singleFeatureJSON): Promise => { + const { children, assembly, refSeq, ...rest } = singleFeatureJSON + const refSeqDocument = await this.getRefSeq(refSeq) + return { + addedFeature: this.makeFeatureSnapshot( + { + assembly: assemblyId, + refSeq: refSeqDocument._id, + ...rest, + }, + changedIds, + ), + } + }, + ), + ) + const change: SerializedAddFeatureChange = { + changedIds, + typeName: 'AddFeatureChange', + assembly: assemblyId, + changes, + } + return this.submitChange(change) + } + const { assembly, refSeq, ...rest } = featureJSON + const [assemblyId, refSeqId] = await this.getAssemblyAndRefSeqIds( + refSeq, + assembly, + ) + const changedIds: string[] = [] + const details = { + addedFeature: this.makeFeatureSnapshot( + { + assembly: assemblyId, + refSeq: refSeqId, + ...rest, + }, + changedIds, + ), + } + const change: SerializedAddFeatureChange = { + changedIds, + typeName: 'AddFeatureChange', + assembly: assemblyId, + ...details, + } + return this.submitChange(change) + } + + makeFeatureSnapshot( + details: FeatureJSON, + ids: string[], + ): AnnotationFeatureSnapshot { + const { children, ...rest } = details + let childrenDetails: undefined | Record = + undefined + if (children) { + childrenDetails = {} + const { refSeq } = rest + for (const child of children) { + const childDetails = this.makeFeatureSnapshot({ refSeq, ...child }, ids) + childrenDetails[childDetails._id] = childDetails + } + } + const _id = new ObjectId().toHexString() + ids.push(_id) + return { + _id, + ...rest, + ...(childrenDetails ? { children: childrenDetails } : {}), + } + } + + async addFeatureFromFlags( + refSeqNameOrId: string, + min: number, + max: number, + type: string, + assemblyNameOrId?: string, + ) { + const [assemblyId, refSeqId] = await this.getAssemblyAndRefSeqIds( + refSeqNameOrId, + assemblyNameOrId, + ) + const changedIds: string[] = [] + const details = { + addedFeature: this.makeFeatureSnapshot( + { refSeq: refSeqId, min: min - 1, max, type }, + changedIds, + ), + } + const change: SerializedAddFeatureChange = { + changedIds, + typeName: 'AddFeatureChange', + assembly: assemblyId, + ...details, + } + return this.submitChange(change) + } + + async getAssemblyAndRefSeqIds( + refSeqNameOrId: string, + assemblyNameOrId?: string, + ): Promise<[string, string]> { + const refSeqIsObjectId = ObjectId.isValid(refSeqNameOrId) + const assemblyIsObjectId = assemblyNameOrId + ? ObjectId.isValid(assemblyNameOrId) + : false + if (assemblyNameOrId && assemblyIsObjectId && refSeqIsObjectId) { + return [assemblyNameOrId, refSeqNameOrId] + } + if (refSeqIsObjectId) { + if (assemblyNameOrId) { + this.warn('Ignoring provided --assembly because it is not an ID') + } + const refSeqDocument = await this.getRefSeq(refSeqNameOrId) + return [refSeqDocument.assembly, refSeqNameOrId] + } + if (!assemblyNameOrId) { + this.error( + `If provided refSeq (${refSeqNameOrId}) is not an ID, assembly must also be provided`, + ) + } + const assemblyDocument = await this.getAssembly(assemblyNameOrId) + const refSeqDocument = await this.getRefSeq( + refSeqNameOrId, + assemblyDocument._id, + ) + return [assemblyDocument._id, refSeqDocument._id] + } + + async getAssembly( + assemblyNameOrId: string, + ): Promise<{ _id: string; name: string; aliases?: string[] }> { + if (ObjectId.isValid(assemblyNameOrId)) { + const response = await this.fetch(`assemblies/${assemblyNameOrId}`) + if (!response.ok) { + const errorMessage = await createFetchErrorMessage( + response, + `Could not find assembly: "${assemblyNameOrId}"`, + ) + this.error(errorMessage) + } + return response.json() as Promise<{ + _id: string + name: string + aliases?: string[] + }> + } + const response = await this.fetch('assemblies') + if (!response.ok) { + const errorMessage = await createFetchErrorMessage( + response, + `Could not find assembly: "${assemblyNameOrId}"`, + ) + this.error(errorMessage) + } + const assemblies = (await response.json()) as { + _id: string + name: string + aliases?: string[] + }[] + for (const assembly of assemblies) { + if ( + assembly.name === assemblyNameOrId || + assembly.aliases?.includes(assemblyNameOrId) + ) { + return assembly + } + } + throw new Error(`Could not find assembly: "${assemblyNameOrId}"`) + } + + async getRefSeq( + refSeqNameOrId: string, + assemblyId?: string, + ): Promise<{ + _id: string + name: string + assembly: string + aliases?: string[] + }> { + if (ObjectId.isValid(refSeqNameOrId)) { + const response = await this.fetch(`refSeqs/${refSeqNameOrId}`) + if (!response.ok) { + const errorMessage = await createFetchErrorMessage( + response, + `Could not find refSeq: "${refSeqNameOrId}"`, + ) + this.error(errorMessage) + } + return response.json() as Promise<{ + _id: string + name: string + assembly: string + aliases?: string[] + }> + } + let endpoint = 'refSeqs' + if (assemblyId) { + const searchParams = new URLSearchParams({ assembly: assemblyId }) + endpoint = `${endpoint}?${searchParams.toString()}` + } + const response = await this.fetch(endpoint) + if (!response.ok) { + const errorMessage = await createFetchErrorMessage( + response, + `Could not find refSeq: "${refSeqNameOrId}"`, + ) + this.error(errorMessage) + } + const refSeqs = (await response.json()) as { + _id: string + name: string + assembly: string + aliases?: string[] + }[] + for (const refSeq of refSeqs) { + if ( + refSeq.name === refSeqNameOrId || + refSeq.aliases?.includes(refSeqNameOrId) + ) { + return refSeq + } + } + throw new Error(`Could not find refSeq: "${refSeqNameOrId}"`) + } + + async submitChange(change: SerializedAddFeatureChange) { + const options = { method: 'POST', body: JSON.stringify(change) } + const response = await this.fetch('changes', options) + if (!response.ok) { + const errorMessage = await createFetchErrorMessage( + response, + 'Could not add feature', + ) + this.error(errorMessage) + } + this.log(await response.text()) + } + + async fetch( + endpoint: string, + options?: { + method?: string + body?: string + headers?: Record + }, + ): Promise { + const { address, accessToken } = await this.getAccess() + const url = new URL(localhostToAddress(`${address}/${endpoint}`)) + const optionsWithAuth = { + ...options, + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + ...options?.headers, + }, + } + return fetch(url, optionsWithAuth) + } +} + +function parseFeatureJSON( + featureJSONString: string, +): FeatureJSON | FeatureJSON[] { + const featureJSON: unknown = JSON.parse(featureJSONString) + if (Array.isArray(featureJSON)) { + return featureJSON.map((feature) => { + assertFeatureIsValid(feature, true) + return feature + }) + } + assertFeatureIsValid(featureJSON, true) + return featureJSON +} + +function assertFeatureIsValid( + feature: unknown, + topLevel = false, +): asserts feature is FeatureJSON { + if ( + typeof feature !== 'object' || + feature === null || + Array.isArray(feature) + ) { + throw new TypeError( + `Feature is not a key-value record: '${JSON.stringify(feature)}'`, + ) + } + for (const attribute of ['min', 'max', 'type']) { + if (!(attribute in feature)) { + throw new Error( + `Feature does not contain "${attribute}": '${JSON.stringify(feature)}'`, + ) + } + } + if (topLevel && !('refSeq' in feature)) { + throw new Error( + `Top-level feature does not contain "refSeq": '${JSON.stringify(feature)}'`, + ) + } + if ('children' in feature) { + const { children } = feature + if (!Array.isArray(children)) { + throw new TypeError( + `"children" is not an array of features '${JSON.stringify(feature)}'`, + ) + } + for (const child of children) { + assertFeatureIsValid(child) + } + } +} diff --git a/packages/apollo-cli/src/test/test.ts b/packages/apollo-cli/src/test/test.ts index 2302f1b95..a3dc49896 100644 --- a/packages/apollo-cli/src/test/test.ts +++ b/packages/apollo-cli/src/test/test.ts @@ -846,6 +846,90 @@ void describe('Test CLI', () => { out = JSON.parse(p.stdout) assert.strictEqual(out.length, 1) assert.ok(out.at(0)?.type === 'gene') + + // Gets feature and child feature that were added manually (not imported) + p = new Shell( + `${apollo} feature add ${P} < { @@ -872,6 +956,105 @@ void describe('Test CLI', () => { assert.strictEqual(p.returncode, 0) }) + void globalThis.itName('Add features', () => { + let p = new Shell( + `${apollo} assembly add-from-fasta ${P} test_data/tiny.fasta.gz -a tiny -f`, + ) + let out = JSON.parse(p.stdout) + const assemblyId = out._id + p = new Shell(`${apollo} feature get ${P} -a tiny`) + assert.deepStrictEqual(p.stdout.trim(), '[]') + // Can add a feature using flags + p = new Shell( + `${apollo} feature add ${P} -a tiny -r ctgA -s 1 -e 10 -t remark`, + ) + out = JSON.parse(p.stdout) + p = new Shell(`${apollo} feature get ${P} -a tiny`) + out = JSON.parse(p.stdout) + assert.strictEqual(out.length, 1) + const refSeqId = out[0].refSeq + // Can add a feature using assembly and refSeq ids + p = new Shell( + `${apollo} feature add ${P} -a ${assemblyId} -r ${refSeqId} -s 11 -e 20 -t remark`, + ) + p = new Shell(`${apollo} feature get ${P} -a ${assemblyId}`) + out = JSON.parse(p.stdout) + assert.strictEqual(out.length, 2) + // Can add a feature using JSON arg + p = new Shell( + `${apollo} feature add ${P} '{"assembly":"${assemblyId}","refSeq":"${refSeqId}","min":21,"max":30,"type":"remark"}'`, + ) + p = new Shell(`${apollo} feature get ${P} -a ${assemblyId}`) + out = JSON.parse(p.stdout) + assert.strictEqual(out.length, 3) + // Can add a feature using JSON from stdin + p = new Shell( + `${apollo} feature add ${P} < { new Shell( `${apollo} assembly add-from-gff ${P} test_data/tiny.fasta.gff3 -a vv1 -f`, diff --git a/packages/apollo-collaboration-server/src/refSeqs/refSeqs.controller.ts b/packages/apollo-collaboration-server/src/refSeqs/refSeqs.controller.ts index b73b48bc7..a549d1f15 100644 --- a/packages/apollo-collaboration-server/src/refSeqs/refSeqs.controller.ts +++ b/packages/apollo-collaboration-server/src/refSeqs/refSeqs.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Logger, Query } from '@nestjs/common' +import { Controller, Get, Logger, Param, Query } from '@nestjs/common' import { Role } from '../utils/role/role.enum' import { Validations } from '../utils/validation/validatation.decorator' @@ -17,4 +17,9 @@ export class RefSeqsController { findAll(@Query() request: FindRefSeqDto) { return this.refSeqsService.findAll(request) } + + @Get(':refseqid') + getFeature(@Param('refseqid') refseqid: string) { + return this.refSeqsService.findOne(refseqid) + } } diff --git a/packages/apollo-common/src/AssemblySpecificChange.ts b/packages/apollo-common/src/AssemblySpecificChange.ts index 644a4e884..a167d95f6 100644 --- a/packages/apollo-common/src/AssemblySpecificChange.ts +++ b/packages/apollo-common/src/AssemblySpecificChange.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst' +import { type Feature } from '@apollo-annotation/schemas' import { Change, @@ -30,18 +31,27 @@ export abstract class AssemblySpecificChange extends Change { } getIndexedIds( - feature: AnnotationFeatureSnapshot, + feature: AnnotationFeatureSnapshot | Feature, idsToIndex: string[] | undefined, ): string[] { const indexedIds: string[] = [] for (const additionalId of idsToIndex ?? []) { - const idValue = feature.attributes?.[additionalId] + const { attributes } = feature + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const idValue: string[] = + attributes instanceof Map + ? attributes.get(additionalId) + : attributes?.[additionalId] if (idValue) { indexedIds.push(idValue[0]) } } if (feature.children) { - for (const child of Object.values(feature.children)) { + const childrenIterable = + feature.children instanceof Map + ? feature.children.values() + : Object.values(feature.children) + for (const child of childrenIterable) { const childIndexedIds = this.getIndexedIds(child, idsToIndex) indexedIds.push(...childIndexedIds) } diff --git a/packages/apollo-shared/src/Changes/AddFeatureChange.ts b/packages/apollo-shared/src/Changes/AddFeatureChange.ts index 2c743c550..814073749 100644 --- a/packages/apollo-shared/src/Changes/AddFeatureChange.ts +++ b/packages/apollo-shared/src/Changes/AddFeatureChange.ts @@ -123,6 +123,7 @@ export class AddFeatureChange extends FeatureChange { ) featureCnt++ } else { + const indexedIds = this.getIndexedIds(addedFeature, idsToIndex) // Adding new child feature if (parentFeatureId) { const topLevelFeature = await featureModel @@ -146,11 +147,14 @@ export class AddFeatureChange extends FeatureChange { this.addChild(parentFeature, addedFeature) const childIds = this.getChildFeatureIds(addedFeature) topLevelFeature.allIds.push(_id, ...childIds) + if (indexedIds.length > 0 && !topLevelFeature.indexedIds) { + topLevelFeature.indexedIds = [] + } + topLevelFeature.indexedIds?.push(...indexedIds) await topLevelFeature.save() } else { const childIds = this.getChildFeatureIds(addedFeature) const allIdsV2 = [_id, ...childIds] - const indexedIds = this.getIndexedIds(addedFeature, idsToIndex) const [newFeatureDoc] = await featureModel.create( [{ allIds: allIdsV2, indexedIds, status: 0, ...addedFeature }], { session }, diff --git a/packages/apollo-shared/src/Changes/DeleteFeatureChange.ts b/packages/apollo-shared/src/Changes/DeleteFeatureChange.ts index 6782f9366..3beda2091 100644 --- a/packages/apollo-shared/src/Changes/DeleteFeatureChange.ts +++ b/packages/apollo-shared/src/Changes/DeleteFeatureChange.ts @@ -67,6 +67,12 @@ export class DeleteFeatureChange extends FeatureChange { const { featureModel, session } = backend const { changes, logger } = this + const { INDEXED_IDS } = process.env + let idsToIndex: string[] | undefined + if (INDEXED_IDS) { + idsToIndex = INDEXED_IDS.split(',') + } + // Loop the changes for (const change of changes) { const { deletedFeature, parentFeatureId } = change @@ -105,6 +111,18 @@ export class DeleteFeatureChange extends FeatureChange { featureDoc.allIds = featureDoc.allIds.filter( (id) => !deletedIds.includes(id), ) + const indexedIds = this.getIndexedIds(featureDoc, idsToIndex) + if (featureDoc.indexedIds) { + if (indexedIds.length > 0) { + featureDoc.indexedIds = indexedIds + } else { + delete featureDoc.indexedIds + } + } else { + if (indexedIds.length > 0) { + featureDoc.indexedIds = indexedIds + } + } // Save updated document in Mongo featureDoc.markModified('children') // Mark as modified. Without this save() -method is not updating data in database try { diff --git a/packages/apollo-shared/src/Changes/FeatureAttributeChange.ts b/packages/apollo-shared/src/Changes/FeatureAttributeChange.ts index 48f7fb1a0..f2a2922a2 100644 --- a/packages/apollo-shared/src/Changes/FeatureAttributeChange.ts +++ b/packages/apollo-shared/src/Changes/FeatureAttributeChange.ts @@ -100,11 +100,24 @@ export class FeatureAttributeChange extends FeatureChange { featuresForChanges.push({ feature: foundFeature, topLevelFeature }) } + const { INDEXED_IDS } = process.env + let idsToIndex: string[] | undefined + if (INDEXED_IDS) { + idsToIndex = INDEXED_IDS.split(',') + } // Let's update objects for (const [idx, change] of changes.entries()) { const { newAttributes } = change const { feature, topLevelFeature } = featuresForChanges[idx] + const indexedIdsChanged = idsToIndex?.some( + (id) => id in newAttributes || id in (feature?.attributes ?? {}), + ) feature.attributes = newAttributes + if (indexedIdsChanged) { + const indexedIds = this.getIndexedIds(topLevelFeature, idsToIndex) + topLevelFeature.indexedIds = indexedIds + topLevelFeature.markModified('indexedIds') + } if (topLevelFeature._id.equals(feature._id)) { topLevelFeature.markModified('attributes') // Mark as modified. Without this save() -method is not updating data in database } else { diff --git a/packages/website/docs/admin/cli/feature.md b/packages/website/docs/admin/cli/feature.md index e1e0063b8..7f689ca06 100644 --- a/packages/website/docs/admin/cli/feature.md +++ b/packages/website/docs/admin/cli/feature.md @@ -2,6 +2,7 @@ Commands to manage features +- [`apollo feature add [FEATURE-JSON]`](#apollo-feature-add-feature-json) - [`apollo feature add-child`](#apollo-feature-add-child) - [`apollo feature check`](#apollo-feature-check) - [`apollo feature copy`](#apollo-feature-copy) @@ -16,6 +17,62 @@ Commands to manage features - [`apollo feature import INPUT-FILE`](#apollo-feature-import-input-file) - [`apollo feature search`](#apollo-feature-search) +## `apollo feature add [FEATURE-JSON]` + +Add one or more features to Apollo + +``` +USAGE + $ apollo feature add [FEATURE-JSON] [--profile ] [--config-file ] [-a ] [-r -s + -e -t ] [-F ] + +ARGUMENTS + FEATURE-JSON Inline JSON describing the feature(s) to add. Can also be provided via stdin. + +FLAGS + -F, --feature-json-file= File with JSON describing the feature(s) to add + -a, --assembly= Name or ID of target assembly. Not required if refseq is unique in the database + -e, --max= End position in target reference sequence + -r, --refSeq= Name or ID of target reference sequence + -s, --min= Start position in target reference sequence + -t, --type= Type of child feature + --config-file= Use this config file (mostly for testing) + --profile= Use credentials from this profile + +DESCRIPTION + Add one or more features to Apollo + + A single simple feature can be added using the --min, --max, etc. flags. + + 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. + + +EXAMPLES + Add a single feature by specifying its location and type + + $ apollo feature add --assembly hg19 --refSeq chr3 --min 1000 --max 5000 --type remark + + Add a single feature from inline JSON + + $ apollo feature add \ + '{"assembly":"","refseq":"","min":1,"max":100,"type":""}' + + Add mutilple features from stdin JSON + + echo '[{"assembly":"","refseq":"","min":1,"max":100,"type":""},{" \ + assembly":"","refseq":"","min":101,"max":200,"type":""}]' | \ + apollo feature add + + Add a feature with children from inline JSON + + $ apollo feature add '{"assembly":"","refseq":"","min":1,"max":100,"type":"","children":[{"min":1,"max":50,"type":""}]}' +``` + +_See code: +[src/commands/feature/add.ts](https://github.com/GMOD/Apollo3/blob/v0.3.10/packages/apollo-cli/src/commands/feature/add.ts)_ + ## `apollo feature add-child` Add a child feature (e.g. add an exon to an mRNA)