UNPKG

@tak-ps/node-tak

Version:

Lightweight JavaScript library for communicating with TAK Server

889 lines (765 loc) 30.4 kB
import xmljs from 'xml-js'; import type { ParsedArgs } from 'minimist' import CoT, { CoTParser } from '@tak-ps/node-cot'; import { Type, Static } from '@sinclair/typebox'; import Err from '@openaddresses/batch-error'; import { Readable } from 'node:stream' import { TAKItem, TAKList } from './types.js'; import { MissionLog } from './mission-log.js'; import type { Feature } from '@tak-ps/node-cot'; import Commands, { CommandOutputFormat } from '../commands.js'; export enum MissionSubscriberRole { MISSION_OWNER = 'MISSION_OWNER', MISSION_SUBSCRIBER = 'MISSION_SUBSCRIBER', MISSION_READONLY_SUBSCRIBER = 'MISSION_READONLY_SUBSCRIBER' } export const MissionContent = Type.Object({ keywords: Type.Array(Type.String()), name: Type.String(), hash: Type.String(), submissionTime: Type.String(), uid: Type.String(), size: Type.Integer(), creatorUid: Type.Optional(Type.String()), mimeType: Type.Optional(Type.String()), submitter: Type.Optional(Type.String()), expiration: Type.Integer() }); export const MissionChange = Type.Object({ isFederatedChange: Type.Boolean(), type: Type.String(), missionName: Type.String(), timestamp: Type.String(), serverTime: Type.String(), creatorUid: Type.Optional(Type.String()), contentUid: Type.Optional(Type.String()), details: Type.Optional(Type.Object({ type: Type.String(), callsign: Type.String(), color: Type.Optional(Type.String()), location: Type.Object({ lat: Type.Number(), lon: Type.Number() }) })), contentResource: Type.Optional(MissionContent) }); export const Mission = Type.Object({ name: Type.String(), description: Type.String(), chatRoom: Type.Optional(Type.String()), baseLayer: Type.Optional(Type.String()), bbox: Type.Optional(Type.String()), path: Type.Optional(Type.String()), classification: Type.Optional(Type.String()), tool: Type.String(), keywords: Type.Array(Type.String()), creatorUid: Type.Optional(Type.String()), createTime: Type.String(), externalData: Type.Array(Type.Unknown()), feeds: Type.Array(Type.Unknown()), mapLayers: Type.Array(Type.Unknown()), ownerRole: Type.Optional(Type.Object({ permissions: Type.Array(Type.String()), type: Type.Enum(MissionSubscriberRole) })), inviteOnly: Type.Boolean(), expiration: Type.Number(), guid: Type.String(), uids: Type.Array(Type.Unknown()), logs: Type.Optional(Type.Array(MissionLog)), // Only present if ?logs=true contents: Type.Array(Type.Object({ timestamp: Type.String(), creatorUid: Type.Optional(Type.String()), data: MissionContent })), passwordProtected: Type.Boolean(), token: Type.Optional(Type.String()), // Only present when mission created groups: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])), // Only present on Mission.get() missionChanges: Type.Optional(Type.Array(MissionChange)) // Only present on Mission.get() }); export const MissionRole = Type.Object({ permissions: Type.Array(Type.String()), hibernateLazyInitializer: Type.Optional(Type.Any()), type: Type.Enum(MissionSubscriberRole) }) export const MissionSubscriber = Type.Object({ token: Type.Optional(Type.String()), clientUid: Type.String(), username: Type.String(), createTime: Type.String(), role: MissionRole }) export const MissionOptions = Type.Object({ token: Type.Optional(Type.String()) }); export const AttachContentsInput = Type.Object({ hashes: Type.Optional(Type.Array(Type.String())), uids: Type.Optional(Type.Array(Type.String())), }); export const DetachContentsInput = Type.Object({ hash: Type.Optional(Type.String()), uid: Type.Optional(Type.String()) }); export const MissionChangesInput = Type.Object({ secago: Type.Optional(Type.Integer()), start: Type.Optional(Type.String()), end: Type.Optional(Type.String()), squashed: Type.Optional(Type.Boolean()) }) export const SubscribedInput = Type.Object({ uid: Type.String(), }) export const UnsubscribeInput = Type.Object({ uid: Type.String(), disconnectOnly: Type.Optional(Type.Boolean()) }) export const SubscriptionInput = Type.Object({ uid: Type.String(), }); export const SubscribeInput = Type.Object({ uid: Type.String(), password: Type.Optional(Type.String()), secago: Type.Optional(Type.Integer()), start: Type.Optional(Type.String()), end: Type.Optional(Type.String()) }) export const MissionDeleteInput = Type.Object({ creatorUid: Type.Optional(Type.String()), deepDelete: Type.Optional(Type.Boolean()) }) export const GetInput = Type.Object({ password: Type.Optional(Type.String()), changes: Type.Optional(Type.Boolean()), logs: Type.Optional(Type.Boolean()), secago: Type.Optional(Type.Integer()), start: Type.Optional(Type.String()), end: Type.Optional(Type.String()) }); export const SetRoleInput = Type.Object({ clientUid: Type.String(), username: Type.String(), role: MissionRole }); export const MissionListInput = Type.Object({ passwordProtected: Type.Optional(Type.Boolean()), defaultRole: Type.Optional(Type.Boolean()), tool: Type.Optional(Type.String()) }); export const MissionCreateInput = Type.Object({ name: Type.String(), group: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()])), keywords: Type.Optional(Type.Array(Type.String())), creatorUid: Type.String(), description: Type.Optional(Type.String({ default: '' })), chatRoom: Type.Optional(Type.String()), baseLayer: Type.Optional(Type.String()), bbox: Type.Optional(Type.String()), boundingPolygon: Type.Optional(Type.Array(Type.String())), path: Type.Optional(Type.String()), classification: Type.Optional(Type.String()), tool: Type.Optional(Type.String({ default: 'public' })), password: Type.Optional(Type.String()), defaultRole: Type.Optional(Type.String()), expiration: Type.Optional(Type.Integer()), inviteOnly: Type.Optional(Type.Boolean({ default: false })), allowDupe: Type.Optional(Type.Boolean({ default: false })), }); export const MissionUpdateInput = Type.Object({ creatorUid: Type.Optional(Type.String()), description: Type.Optional(Type.String()), keywords: Type.Optional(Type.Array(Type.String())), chatRoom: Type.Optional(Type.String()), baseLayer: Type.Optional(Type.String()), group: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()])), bbox: Type.Optional(Type.String()), path: Type.Optional(Type.String()), classification: Type.Optional(Type.String()), tool: Type.Optional(Type.String()), expiration: Type.Optional(Type.Integer()), inviteOnly: Type.Optional(Type.Boolean()), }); export const GUIDMatch = new RegExp(/^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$/); export const TAKList_Mission = TAKList(Mission); export const TAKList_MissionInvites = TAKList(Type.String()); export const TAKList_MissionChange = TAKList(MissionChange); export const TAKList_MissionSubscriber = TAKList(MissionSubscriber); export const TAKItem_MissionSubscriber = TAKItem(MissionSubscriber); /** * @class */ export default class MissionCommands extends Commands { schema = { list: { description: 'List Missions', params: Type.Object({}), query: Type.Object({}), formats: [ CommandOutputFormat.JSON ] }, } async cli(args: ParsedArgs): Promise<object | string> { if (args._[3] === 'list') { const list = await this.list({}); if (args.format === 'json') { return list; } else { return list.data.map((mission) => { return `${mission.name} - ${mission.description}`; }).join('\n'); } } else { throw new Error('Unsupported Subcommand'); } } #isGUID(id: string): boolean { return GUIDMatch.test(id) } #encodeName(name: string): string { return encodeURIComponent(name.trim()) } /* * The Mission type is currently cast from an unknown to a Mission * from the fetch call from the server. There are a few standardizations * we make for better usability. */ #stdMission(mission: Static<typeof Mission>): Static<typeof Mission> { if (mission.description === undefined) mission.description = ''; return mission; } #headers(opts?: Static<typeof MissionOptions>): object { if (opts && opts.token) { return { MissionAuthorization: `Bearer ${opts.token}` } } else { return {}; } } /** * Return Zip archive of Mission Sync * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getMissionArchive_1 TAK Server Docs}. */ async getArchive( name: string, opts?: Static<typeof MissionOptions> ): Promise<Readable> { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/archive`, this.api.url); const res = await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts), }, true); return res.body; } /** * Return Mission Sync changes in a given time range * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getMissionChanges TAK Server Docs}. */ async changes( name: string, query: Static<typeof MissionChangesInput>, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof TAKList_MissionChange>> { if (this.#isGUID(name)) name = (await this.getGuid(name, {}, opts)).name; const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/changes`, this.api.url); let q: keyof Static<typeof MissionChangesInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } const changes = await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts), }); return changes; } /** * Return all current features in the Data Sync as CoT GeoJSON Features */ async latestFeats( name: string, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof Feature.Feature>[]> { const feats: Static<typeof Feature.Feature>[] = []; const res: any = xmljs.xml2js(await this.latestCots(name, opts), { compact: true }); if (!Object.keys(res.events).length) return feats; if (!res.events.event || (Array.isArray(res.events.event) && !res.events.event.length)) return feats; for (const event of Array.isArray(res.events.event) ? res.events.event : [res.events.event] ) { feats.push(await CoTParser.to_geojson(new CoT({ event }))); } return feats; } /** * Return all current features in the Data Sync as CoT GeoJSON Features * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getLatestMissionCotEvents TAK Server Docs}. */ async latestCots( name: string, opts?: Static<typeof MissionOptions> ): Promise<string> { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/cot`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/cot`, this.api.url); return await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts) }); } /** * Return users associated with this mission * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getMissionContacts TAK Server Docs}. */ async contacts( name: string, opts?: Static<typeof MissionOptions> ) { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/contacts`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/contacts`, this.api.url); return await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts) }); } /** * Remove a file from the mission * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/removeMissionContent TAK Server Docs}. */ async detachContents( name: string, body: Static<typeof DetachContentsInput>, opts?: Static<typeof MissionOptions> ) { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/contents`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/contents`, this.api.url); if (body.hash) url.searchParams.append('hash', body.hash); if (body.uid) url.searchParams.append('uid', body.uid); return await this.api.fetch(url, { method: 'DELETE', headers: this.#headers(opts), }); } /** * Attach a file resource by hash from the TAK Server file manager * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/addMissionContent TAK Server Docs}. */ async attachContents( name: string, body: Static<typeof AttachContentsInput>, opts?: Static<typeof MissionOptions> ) { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/contents`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/contents`, this.api.url); return await this.api.fetch(url, { method: 'PUT', headers: this.#headers(opts), body }); } /** * Upload a Mission Package * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/addMissionPackage TAK Server Docs}. */ async upload( name: string, creatorUid: string, body: Readable, opts?: Static<typeof MissionOptions> ) { if (this.#isGUID(name)) name = (await this.getGuid(name, {}, opts)).name; const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/contents/missionpackage`, this.api.url); url.searchParams.append('creatorUid', creatorUid); return await this.api.fetch(url, { method: 'PUT', headers: this.#headers(opts), body }); } /** * Return UIDs associated with any subscribed users * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getMissionSubscriptions TAK Server Docs}. */ async subscriptions( name: string, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof TAKItem_MissionSubscriber>> { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/subscriptions`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscriptions`, this.api.url); return await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts), }); } /** * Return permissions associated with any subscribed users * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getMissionSubscriptionRoles TAK Server Docs}. */ async subscriptionRoles( name: string, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof TAKList_MissionSubscriber>> { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/subscriptions/roles`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscriptions/roles`, this.api.url); return await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts), }); } /** * Return Role associated with a given mission if subscribed * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/setMissionRole TAK Server Docs}. */ async setRole( name: string, query: Static<typeof SetRoleInput>, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof MissionRole>> { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/role`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/role`, this.api.url); let q: keyof Static<typeof SetRoleInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } const res = await this.api.fetch(url, { method: 'PUT', headers: this.#headers(opts), }); return res.data; } /** * Return Role associated with a given mission if subscribed * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getMissionRoleFromToken TAK Server Docs}. */ async role( name: string, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof MissionRole>> { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/role`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/role`, this.api.url); const res = await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts), }); return res.data; } /** * Return subscription associated with a given mission if subscribed * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getSubscriptionForUser TAK Server Docs}. */ async subscription( name: string, query: Static<typeof SubscriptionInput>, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof MissionSubscriber>> { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/subscription`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscription`, this.api.url); let q: keyof Static<typeof SubscriptionInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } const res = await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts), }); return res.data; } /** * Subscribe to a mission * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/createMissionSubscription TAK Server Docs}. */ async subscribe( name: string, query: Static<typeof SubscribeInput>, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof TAKItem_MissionSubscriber>> { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/subscription`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscription`, this.api.url); let q: keyof Static<typeof SubscribeInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } return await this.api.fetch(url, { method: 'PUT', headers: this.#headers(opts), }); } /** * Unsubscribe from a mission * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/deleteMissionSubscription TAK Server Docs}. */ async unsubscribe( name: string, query: Static<typeof UnsubscribeInput>, opts?: Static<typeof MissionOptions> ) { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}/subscription`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}/subscription`, this.api.url); let q: keyof Static<typeof UnsubscribeInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } return await this.api.fetch(url, { method: 'DELETE', headers: this.#headers(opts), }); } /** * List missions in currently active channels * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getAllMissions_1 TAK Server Docs}. */ async list(query: Static<typeof MissionListInput>): Promise<Static<typeof TAKList_Mission>> { const url = new URL('/Marti/api/missions', this.api.url); let q: keyof Static<typeof MissionListInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } return await this.api.fetch(url, { method: 'GET' }); } /** * Get mission by its GUID * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getMissionByGuid TAK Server Docs}. */ async getGuid( guid: string, query: Static<typeof GetInput>, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof Mission>> { const url = new URL(`/Marti/api/missions/guid/${encodeURIComponent(guid)}`, this.api.url); let q: keyof Static<typeof GetInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } const missions: Static<typeof TAKList_Mission> = await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts), }); if (!missions.data.length) throw new Err(404, null, `No Mission for GUID: ${guid}`); return this.#stdMission(missions.data[0]); } /** * Check if you have access to a given mission */ async access( name: string, opts?: Static<typeof MissionOptions> ): Promise<boolean> { try { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}`, this.api.url); const missions: Static<typeof TAKList_Mission> = await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts), }); if (!missions.data.length) return false; return true; } catch (err) { console.error(err); return false; } } /** * Update Mission * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/createMissionAllowDupe TAK Server Docs}. */ async update( name: string, body: Static<typeof MissionUpdateInput>, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof Mission>> { let mission = await this.get(name, {}, opts); const url = new URL(`/Marti/api/missions/${encodeURIComponent(mission.name)}`, this.api.url) const bodyParams: Static<typeof MissionUpdateInput> = { group: body.group ?? mission.groups, creatorUid: body.creatorUid ?? mission.creatorUid, description: body.description ?? mission.description, chatRoom: body.chatRoom ?? mission.chatRoom, baseLayer: body.baseLayer ?? mission.baseLayer, bbox: body.bbox ?? mission.bbox, path: body.path ?? mission.path, classification: body.classification ?? mission.classification, tool: body.tool ?? mission.tool, expiration: body.expiration ?? mission.expiration, inviteOnly: body.inviteOnly ?? mission.inviteOnly }; let q: keyof Static<typeof MissionUpdateInput>; for (q in bodyParams) { if (body[q] !== undefined) { url.searchParams.append(q, String(body[q])); } } url.searchParams.append('allowDupe', 'false'); const missions = await this.api.fetch(url, { method: 'POST' }); if (!missions.data.length) throw new Error('Create Mission didn\'t return a mission or an error'); mission = missions.data[0]; if (body.keywords && body.keywords.length) { mission = await this.#putKeywords(mission.name, body.keywords, { token: mission.token }); } return this.#stdMission(mission); } /** * Get mission by its Name * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/getMission TAK Server Docs}. */ async get( name: string, query: Static<typeof GetInput>, opts?: Static<typeof MissionOptions> ): Promise<Static<typeof Mission>> { const url = this.#isGUID(name) ? new URL(`/Marti/api/missions/guid/${encodeURIComponent(name)}`, this.api.url) : new URL(`/Marti/api/missions/${this.#encodeName(name)}`, this.api.url); let q: keyof Static<typeof GetInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } const missions: Static<typeof TAKList_Mission> = await this.api.fetch(url, { method: 'GET', headers: this.#headers(opts), }); if (!missions.data.length) throw new Err(404, null, `No Mission for Name: ${name}`); return this.#stdMission(missions.data[0]); } /** * Create a new mission * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/createMission TAK Server Docs}. */ async create( body: Static<typeof MissionCreateInput> ): Promise<Static<typeof Mission>> { const url = new URL(`/Marti/api/missions/${this.#encodeName(body.name)}`, this.api.url); // I want to keep this 1:1 with the TAK Server Source Code // eslint-disable-next-line no-useless-escape if (!body.name.match(/^[\p{L}\p{N}\w\d\s\.\(\)!=@#$&^*_\-\+\[\]\{\}:,\.\/\|\\]*$/u)) { throw new Err(400, null, 'Mission Name contains an invalid Character'); } else if (body.name.length === 0) { throw new Err(400, null, 'Mission Name must have a length > 0'); } else if (body.name.length > 1024) { throw new Err(400, null, 'Mission Name cannot exceed 1024 characters'); } else if (body.name.includes('/')) { throw new Err(400, null, 'Mission Name cannot contain forward slashes'); } if (body.description === undefined) body.description = ''; if (body.group && Array.isArray(body.group)) body.group = body.group.join(','); let q: keyof Static<typeof MissionCreateInput>; for (q in body) { if (body[q] !== undefined && !['name', 'keywords'].includes(q)) { url.searchParams.append(q, String(body[q])); } } const missions = await this.api.fetch(url, { method: 'POST' }); if (!missions.data.length) throw new Error('Create Mission didn\'t return a mission or an error'); let mission = missions.data[0]; if (body.keywords && body.keywords.length) { mission = await this.#putKeywords(mission.name, body.keywords, { token: mission.token }); } return this.#stdMission(mission); } /** * Update Mission Keywords */ async #putKeywords( name: string, keywords: string[], opts?: Static<typeof MissionOptions> ): Promise<Static<typeof Mission>> { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/keywords`, this.api.url); const missions = await this.api.fetch(url, { method: 'PUT', headers: this.#headers(opts), body: keywords }); if (!missions.data.length) throw new Error('Create Mission didn\'t return a mission or an error'); const mission = missions.data[0]; return this.#stdMission(mission); } /** * Delete a mission * * {@link https://docs.tak.gov/api/takserver/redoc#tag/mission-api/operation/deleteMission TAK Server Docs}. */ async delete( name: string, query: Static<typeof MissionDeleteInput>, opts?: Static<typeof MissionOptions> ) { if (this.#isGUID(name)) { const url = new URL('/Marti/api/missions', this.api.url); url.searchParams.append('guid', name); let q: keyof Static<typeof MissionDeleteInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } return await this.api.fetch(url, { method: 'DELETE', headers: this.#headers(opts), }); } else { const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}`, this.api.url); let q: keyof Static<typeof MissionDeleteInput>; for (q in query) { if (query[q] !== undefined) { url.searchParams.append(q, String(query[q])); } } return await this.api.fetch(url, { method: 'DELETE', headers: this.#headers(opts), }); } } }