@tak-ps/node-tak 8.3.0 → 8.4.0

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.
Files changed (63) hide show
  1. package/.github/workflows/doc.yml +6 -6
  2. package/.github/workflows/release.yml +3 -3
  3. package/.github/workflows/test.yml +14 -9
  4. package/CHANGELOG.md +4 -0
  5. package/dist/index.js +3 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/api/contacts.js +23 -0
  8. package/dist/lib/api/contacts.js.map +1 -0
  9. package/dist/lib/api/credentials.js +62 -0
  10. package/dist/lib/api/credentials.js.map +1 -0
  11. package/dist/lib/api/export.js +36 -0
  12. package/dist/lib/api/export.js.map +1 -0
  13. package/dist/lib/api/files.js +112 -0
  14. package/dist/lib/api/files.js.map +1 -0
  15. package/dist/lib/api/groups.js +47 -0
  16. package/dist/lib/api/groups.js.map +1 -0
  17. package/dist/lib/api/mission-layer.js +245 -0
  18. package/dist/lib/api/mission-layer.js.map +1 -0
  19. package/dist/lib/api/mission-log.js +108 -0
  20. package/dist/lib/api/mission-log.js.map +1 -0
  21. package/dist/lib/api/mission.js +583 -0
  22. package/dist/lib/api/mission.js.map +1 -0
  23. package/dist/lib/api/oauth.js +54 -0
  24. package/dist/lib/api/oauth.js.map +1 -0
  25. package/dist/lib/api/package.js +42 -0
  26. package/dist/lib/api/package.js.map +1 -0
  27. package/dist/lib/api/query.js +60 -0
  28. package/dist/lib/api/query.js.map +1 -0
  29. package/dist/lib/api/subscriptions.js +73 -0
  30. package/dist/lib/api/subscriptions.js.map +1 -0
  31. package/dist/lib/api/types.js +42 -0
  32. package/dist/lib/api/types.js.map +1 -0
  33. package/dist/lib/api/video.js +123 -0
  34. package/dist/lib/api/video.js.map +1 -0
  35. package/dist/lib/api.js +123 -0
  36. package/dist/lib/api.js.map +1 -0
  37. package/dist/lib/auth.js +92 -0
  38. package/dist/lib/auth.js.map +1 -0
  39. package/dist/lib/fetch.js +26 -0
  40. package/dist/lib/fetch.js.map +1 -0
  41. package/dist/lib/stream.js +9 -0
  42. package/dist/lib/stream.js.map +1 -0
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/index.ts +6 -1
  45. package/lib/api/contacts.ts +27 -0
  46. package/lib/api/credentials.ts +74 -0
  47. package/lib/api/export.ts +44 -0
  48. package/lib/api/files.ts +151 -0
  49. package/lib/api/groups.ts +63 -0
  50. package/lib/api/mission-layer.ts +312 -0
  51. package/lib/api/mission-log.ts +140 -0
  52. package/lib/api/mission.ts +741 -0
  53. package/lib/api/oauth.ts +68 -0
  54. package/lib/api/package.ts +56 -0
  55. package/lib/api/query.ts +79 -0
  56. package/lib/api/subscriptions.ts +84 -0
  57. package/lib/api/types.ts +43 -0
  58. package/lib/api/video.ts +155 -0
  59. package/lib/api.ts +136 -0
  60. package/lib/auth.ts +117 -0
  61. package/lib/fetch.ts +38 -0
  62. package/lib/stream.ts +10 -0
  63. package/package.json +17 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../lib/stream.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc;IACtD,OAAO,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,IAAI,GAAG,KAAK,EAAU,CAAC;QAC7B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC7C,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE,CAAC,MAAM,CAAC,6BAA6B,GAAG,EAAE,CAAC,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -1 +1 @@
1
- {"root":["../index.ts","../test/default.test.ts","../test/findCoT.test.ts"],"version":"5.8.3"}
1
+ {"root":["../index.ts","../test/default.test.ts","../test/findCoT.test.ts","../lib/api.ts","../lib/auth.ts","../lib/fetch.ts","../lib/stream.ts","../lib/api/contacts.ts","../lib/api/credentials.ts","../lib/api/export.ts","../lib/api/files.ts","../lib/api/groups.ts","../lib/api/mission-layer.ts","../lib/api/mission-log.ts","../lib/api/mission.ts","../lib/api/oauth.ts","../lib/api/package.ts","../lib/api/query.ts","../lib/api/subscriptions.ts","../lib/api/types.ts","../lib/api/video.ts"],"version":"5.8.3"}
package/index.ts CHANGED
@@ -3,6 +3,9 @@ import tls from 'node:tls';
3
3
  import CoT from '@tak-ps/node-cot';
4
4
  import type { TLSSocket } from 'node:tls'
5
5
 
6
+ export * from './lib/api.js';
7
+ export * from './lib/auth.js';
8
+
6
9
  /* eslint-disable no-control-regex */
7
10
  export const REGEX_CONTROL = /[\u000B-\u001F\u007F-\u009F]/g;
8
11
 
@@ -234,4 +237,6 @@ export default class TAK extends EventEmitter {
234
237
  }
235
238
  }
236
239
 
237
- export { CoT }
240
+ export {
241
+ CoT,
242
+ }
@@ -0,0 +1,27 @@
1
+ import TAKAPI from '../api.js';
2
+ import { Type, Static } from '@sinclair/typebox';
3
+
4
+ export const Contact = Type.Object({
5
+ filterGroups: Type.Any(), // I'm not familiar with this one
6
+ notes: Type.String(),
7
+ callsign: Type.String(),
8
+ team: Type.String(),
9
+ role: Type.String(),
10
+ takv: Type.String(),
11
+ uid: Type.String()
12
+ });
13
+
14
+ export default class {
15
+ api: TAKAPI;
16
+
17
+ constructor(api: TAKAPI) {
18
+ this.api = api;
19
+ }
20
+
21
+ async list(): Promise<Array<Static<typeof Contact>>> {
22
+ const url = new URL(`/Marti/api/contacts/all`, this.api.url);
23
+ return await this.api.fetch(url, {
24
+ method: 'GET'
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,74 @@
1
+ import TAKAPI from '../api.js';
2
+ import { APIAuthPassword } from '../auth.js';
3
+ import { Static, Type } from '@sinclair/typebox';
4
+ import pem from 'pem';
5
+ import xml2js from 'xml2js';
6
+
7
+ export const CertificateResponse = Type.Object({
8
+ cert: Type.String(),
9
+ key: Type.String()
10
+ });
11
+
12
+ export default class {
13
+ api: TAKAPI;
14
+
15
+ constructor(api: TAKAPI) {
16
+ this.api = api;
17
+ }
18
+
19
+ async config() {
20
+ const url = new URL(`/Marti/api/tls/config`, this.api.url);
21
+ return await this.api.fetch(url, {
22
+ method: 'GET'
23
+ });
24
+ }
25
+
26
+ async generate(): Promise<Static<typeof CertificateResponse>> {
27
+ if (!(this.api.auth instanceof APIAuthPassword)) throw new Error('Must use Password Auth');
28
+
29
+ const config = await xml2js.parseStringPromise(await this.config());
30
+
31
+ let organization = null;
32
+ let organizationUnit = null;
33
+ for (const nameEntry of config['ns2:certificateConfig'].nameEntries) {
34
+ for (const ne of nameEntry.nameEntry) {
35
+ if (ne['$'].name === 'O') organization = ne['$'].value;
36
+ if (ne['$'].name === 'OU') organizationUnit = ne['$'].value;
37
+ }
38
+ }
39
+
40
+ const createCSR = pem.promisified.createCSR;
41
+
42
+ const keys: {
43
+ csr: string,
44
+ clientKey: string
45
+ } = await createCSR({
46
+ organization,
47
+ organizationUnit,
48
+ commonName: this.api.auth.username
49
+ });
50
+
51
+ const url = new URL(`/Marti/api/tls/signClient/v2`, this.api.url);
52
+ url.searchParams.append('clientUid', this.api.auth.username + ' (ETL)');
53
+ url.searchParams.append('version', '3');
54
+
55
+ const res = await this.api.fetch(url, {
56
+ method: 'POST',
57
+ nocookies: true,
58
+ headers: {
59
+ Accept: 'application/json',
60
+ Authorization: 'Basic ' + btoa(this.api.auth.username + ":" + this.api.auth.password)
61
+ },
62
+ body: keys.csr
63
+ });
64
+
65
+ let cert = '-----BEGIN CERTIFICATE-----\n' + res.signedCert;
66
+ if (!res.signedCert.endsWith('\n')) cert = cert + '\n';
67
+ cert = cert + '-----END CERTIFICATE-----';
68
+
69
+ return {
70
+ cert,
71
+ key: keys.clientKey
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,44 @@
1
+ import TAKAPI from '../api.js';
2
+ import { Type, Static } from '@sinclair/typebox';
3
+ import { Readable } from 'node:stream';
4
+
5
+ export const ExportInput = Type.Object({
6
+ startTime: Type.String(),
7
+ endTime: Type.String(),
8
+ groups: Type.Array(Type.String()),
9
+ format: Type.String({ enum: ['kmz', 'kml'] }),
10
+ interval: Type.Optional(Type.Number()),
11
+ multiTrackThreshold: Type.Optional(Type.String()),
12
+ extendedData: Type.Optional(Type.Boolean()),
13
+ optimizeExport: Type.Optional(Type.Boolean()),
14
+ });
15
+
16
+ /**
17
+ * @class
18
+ */
19
+ export default class {
20
+ api: TAKAPI;
21
+
22
+ constructor(api: TAKAPI) {
23
+ this.api = api;
24
+ }
25
+
26
+ async export(query: Static<typeof ExportInput>): Promise<Readable> {
27
+ const url = new URL(`/Marti/ExportMissionKML`, this.api.url);
28
+
29
+ const params = new URLSearchParams();
30
+ let q: keyof Static<typeof ExportInput>;
31
+ for (q in query) {
32
+ if (query[q] !== undefined ) {
33
+ params.append(q, String(query[q]));
34
+ }
35
+ }
36
+
37
+ const res = await this.api.fetch(url, {
38
+ method: 'POST',
39
+ body: params
40
+ }, true);
41
+
42
+ return res.body;
43
+ }
44
+ }
@@ -0,0 +1,151 @@
1
+ import TAKAPI from '../api.js';
2
+ import FormData from 'form-data';
3
+ import { Readable } from 'node:stream';
4
+ import mime from 'mime';
5
+ import { Type, Static } from '@sinclair/typebox';
6
+
7
+ export const Content = Type.Object({
8
+ UID: Type.String(),
9
+ SubmissionDateTime: Type.String(),
10
+ Keywords: Type.Array(Type.String()),
11
+ MIMEType: Type.String(),
12
+ SubmissionUser: Type.String(),
13
+ PrimaryKey: Type.String(),
14
+ Hash: Type.String(),
15
+ CreatorUid: Type.String(),
16
+ Name: Type.String()
17
+ });
18
+
19
+ export const Config = Type.Object({
20
+ uploadSizeLimit: Type.Integer()
21
+ })
22
+
23
+ export default class File {
24
+ api: TAKAPI;
25
+
26
+ constructor(api: TAKAPI) {
27
+ this.api = api;
28
+ }
29
+
30
+ // TODO Investigate this endpoint
31
+ list() {
32
+ new URL(`/Marti/api/sync/search`, this.api.url);
33
+ // param hash=<hash>
34
+ }
35
+
36
+ async meta(hash: string): Promise<string> {
37
+ const url = new URL(`/Marti/sync/${encodeURIComponent(hash)}/metadata`, this.api.url);
38
+
39
+ const res = await this.api.fetch(url, {
40
+ method: 'GET'
41
+ }, true);
42
+
43
+ const body = await res.text();
44
+
45
+ return body;
46
+ }
47
+
48
+ async download(hash: string): Promise<Readable> {
49
+ const url = new URL(`/Marti/sync/content`, this.api.url);
50
+ url.searchParams.append('hash', hash);
51
+
52
+ const res = await this.api.fetch(url, {
53
+ method: 'GET'
54
+ }, true);
55
+
56
+ return res.body;
57
+ }
58
+
59
+ async adminDelete(hash: string) {
60
+ const url = new URL(`/Marti/api/files/${hash}`, this.api.url);
61
+
62
+ return await this.api.fetch(url, {
63
+ method: 'DELETE',
64
+ });
65
+ }
66
+
67
+ async delete(hash: string) {
68
+ const url = new URL(`/Marti/sync/delete`, this.api.url);
69
+ url.searchParams.append('hash', hash)
70
+
71
+ return await this.api.fetch(url, {
72
+ method: 'DELETE',
73
+ });
74
+ }
75
+
76
+ // TODO Return a Content Object
77
+ async uploadPackage(opts: {
78
+ name: string;
79
+ creatorUid: string;
80
+ hash: string;
81
+ }, body: Readable | Buffer): Promise<string> {
82
+ const url = new URL(`/Marti/sync/missionupload`, this.api.url);
83
+ url.searchParams.append('filename', opts.name)
84
+ url.searchParams.append('creatorUid', opts.creatorUid)
85
+ url.searchParams.append('hash', opts.hash)
86
+
87
+ if (body instanceof Buffer) {
88
+ body = Readable.from(body as Buffer);
89
+ }
90
+
91
+ const form = new FormData()
92
+ form.append('assetfile', body as Readable);
93
+
94
+ const res = await this.api.fetch(url, {
95
+ method: 'POST',
96
+ body: form
97
+ }) as string;
98
+
99
+ return res;
100
+ }
101
+
102
+ async upload(opts: {
103
+ name: string;
104
+ contentLength: number;
105
+ contentType?: string;
106
+ keywords: string[];
107
+ creatorUid: string;
108
+ latitude?: string;
109
+ longitude?: string;
110
+ altitude?: string;
111
+ }, body: Readable | Buffer): Promise<Static<typeof Content>> {
112
+ const url = new URL(`/Marti/sync/upload`, this.api.url);
113
+ url.searchParams.append('name', opts.name)
114
+ url.searchParams.append('keywords', opts.keywords.join(','))
115
+ url.searchParams.append('creatorUid', opts.creatorUid)
116
+ if (opts.altitude) url.searchParams.append('altitude', opts.altitude);
117
+ if (opts.longitude) url.searchParams.append('longitude', opts.longitude);
118
+ if (opts.latitude) url.searchParams.append('latitude', opts.latitude);
119
+
120
+ if (body instanceof Buffer) {
121
+ body = Readable.from(body as Buffer);
122
+ }
123
+
124
+ const res = await this.api.fetch(url, {
125
+ method: 'POST',
126
+ headers: {
127
+ 'Content-Type': opts.contentType ? opts.contentType : mime.getType(opts.name),
128
+ 'Content-Length': opts.contentLength
129
+ },
130
+ body: body as Readable
131
+ });
132
+
133
+ return JSON.parse(res);
134
+ }
135
+
136
+ async count() {
137
+ const url = new URL('/Marti/api/files/metadata/count', this.api.url);
138
+
139
+ return await this.api.fetch(url, {
140
+ method: 'GET'
141
+ });
142
+ }
143
+
144
+ async config(): Promise<Static<typeof Config>> {
145
+ const url = new URL('/files/api/config', this.api.url);
146
+
147
+ return await this.api.fetch(url, {
148
+ method: 'GET'
149
+ });
150
+ }
151
+ }
@@ -0,0 +1,63 @@
1
+ import { Static, Type } from '@sinclair/typebox';
2
+ import TAKAPI from '../api.js';
3
+ import { TAKList } from './types.js';
4
+
5
+ export const Group = Type.Object({
6
+ name: Type.String(),
7
+ direction: Type.String(),
8
+ created: Type.String(),
9
+ type: Type.String(),
10
+ bitpos: Type.Number(),
11
+ active: Type.Boolean(),
12
+ description: Type.Optional(Type.String())
13
+ })
14
+
15
+ export const GroupListInput = Type.Object({
16
+ useCache: Type.Optional(Type.Boolean())
17
+ })
18
+
19
+ export const TAKList_Group = TAKList(Group);
20
+
21
+ export default class {
22
+ api: TAKAPI;
23
+
24
+ constructor(api: TAKAPI) {
25
+ this.api = api;
26
+ }
27
+
28
+ async list(
29
+ query: Static<typeof GroupListInput> = {}
30
+ ): Promise<Static<typeof TAKList_Group>> {
31
+ const url = new URL(`/Marti/api/groups/all`, this.api.url);
32
+
33
+ let q: keyof Static<typeof GroupListInput>;
34
+ for (q in query) {
35
+ if (query[q] !== undefined) {
36
+ url.searchParams.append(q, String(query[q]));
37
+ }
38
+ }
39
+
40
+ return await this.api.fetch(url, {
41
+ method: 'GET'
42
+ });
43
+ }
44
+
45
+ async update(
46
+ body: Static<typeof Group>[],
47
+ query: Static<typeof GroupListInput> = {}
48
+ ): Promise<void> {
49
+ const url = new URL(`/Marti/api/groups/active`, this.api.url);
50
+
51
+ let q: keyof Static<typeof GroupListInput>;
52
+ for (q in query) {
53
+ if (query[q] !== undefined) {
54
+ url.searchParams.append(q, String(query[q]));
55
+ }
56
+ }
57
+
58
+ await this.api.fetch(url, {
59
+ method: 'PUT',
60
+ body
61
+ });
62
+ }
63
+ }
@@ -0,0 +1,312 @@
1
+ import TAKAPI from '../api.js';
2
+ import { TAKList, TAKItem } from './types.js';
3
+ import { Type, Static } from '@sinclair/typebox';
4
+ import type { MissionOptions } from './mission.js';
5
+ import { GUIDMatch } from './mission.js';
6
+ import Err from '@openaddresses/batch-error';
7
+ import type { Feature } from '@tak-ps/node-cot';
8
+
9
+ export enum MissionLayerType {
10
+ GROUP = 'GROUP',
11
+ UID = 'UID',
12
+ CONTENTS = 'CONTENTS',
13
+ MAPLAYER = 'MAPLAYER',
14
+ ITEM = 'ITEM'
15
+ }
16
+
17
+ export const MissionLayer = Type.Object({
18
+ name: Type.String({ minLength: 1 }),
19
+ type: Type.Enum(MissionLayerType),
20
+ parentUid: Type.Optional(Type.String()),
21
+ uid: Type.String(),
22
+ mission_layers: Type.Optional(Type.Array(Type.Any())),
23
+ uids: Type.Optional(Type.Array(Type.Object({
24
+ data: Type.String({
25
+ description: 'The UID of the COT'
26
+ }),
27
+ timestamp: Type.String(),
28
+ creatorUid: Type.String(),
29
+ keywords: Type.Optional(Type.Array(Type.String())),
30
+ details: Type.Optional(Type.Object({
31
+ type: Type.String(),
32
+ callsign: Type.String(),
33
+ color: Type.Optional(Type.String()),
34
+ location: Type.Object({
35
+ lat: Type.Number(),
36
+ lon: Type.Number()
37
+ })
38
+ }))
39
+ }))),
40
+ contents: Type.Optional(Type.Array(Type.Any())),
41
+ maplayers: Type.Optional(Type.Array(Type.Any()))
42
+ });
43
+
44
+ export const DeleteInput = Type.Object({
45
+ uid: Type.Array(Type.String()),
46
+ creatorUid: Type.String(),
47
+ });
48
+
49
+ export const RenameInput = Type.Object({
50
+ name: Type.String(),
51
+ creatorUid: Type.String(),
52
+ });
53
+
54
+ export const CreateInput = Type.Object({
55
+ name: Type.String(),
56
+ type: Type.Enum(MissionLayerType),
57
+ uid: Type.Optional(Type.String()),
58
+ parentUid: Type.Optional(Type.String()),
59
+ afterUid: Type.Optional(Type.String()),
60
+ creatorUid: Type.String()
61
+ })
62
+
63
+ export const TAKList_MissionLayer = TAKList(MissionLayer);
64
+ export const TAKItem_MissionLayer = TAKItem(MissionLayer);
65
+
66
+ export default class {
67
+ api: TAKAPI;
68
+
69
+ constructor(api: TAKAPI) {
70
+ this.api = api;
71
+ }
72
+
73
+ #encodeName(name: string): string {
74
+ return encodeURIComponent(name.trim())
75
+ }
76
+
77
+ #isGUID(id: string): boolean {
78
+ return GUIDMatch.test(id)
79
+ }
80
+
81
+ #headers(opts?: Static<typeof MissionOptions>): object {
82
+ if (opts && opts.token) {
83
+ return {
84
+ MissionAuthorization: `Bearer ${opts.token}`
85
+ }
86
+ } else {
87
+ return {};
88
+ }
89
+ }
90
+
91
+ isEmpty(layer: Static<typeof MissionLayer>): boolean {
92
+ if (layer.mission_layers && layer.mission_layers.length) return false;
93
+ if (layer.uids && layer.uids.length) return false;
94
+ if (layer.contents && layer.contents.length) return false;
95
+ if (layer.maplayers && layer.maplayers.length) return false;
96
+ return true;
97
+ }
98
+
99
+ async listAsPathMap(
100
+ name: string,
101
+ opts?: Static<typeof MissionOptions>
102
+ ): Promise<Map<string, Static<typeof MissionLayer>>> {
103
+ const layers = (await this.list(name, opts)).data;
104
+
105
+ const pathMap: Map<string, Static<typeof MissionLayer>> = new Map();
106
+
107
+ for (const layer of layers) {
108
+ this.#listAsPathMapRecurse(pathMap, '', layer);
109
+ }
110
+
111
+ return pathMap;
112
+ }
113
+
114
+ #listAsPathMapRecurse(
115
+ pathMap: Map<string, Static<typeof MissionLayer>>,
116
+ pathCurrent: string,
117
+ layer: Static<typeof MissionLayer>
118
+ ) {
119
+ pathCurrent = `${pathCurrent}/${encodeURIComponent(layer.name)}`;
120
+ pathMap.set(`${pathCurrent}/`, layer);
121
+
122
+ for (const l of (layer.mission_layers || []) as Array<Static<typeof MissionLayer>>) {
123
+ this.#listAsPathMapRecurse(pathMap, pathCurrent, l);
124
+ }
125
+ }
126
+
127
+ async list(
128
+ name: string,
129
+ opts?: Static<typeof MissionOptions>
130
+ ): Promise<Static<typeof TAKList_MissionLayer>> {
131
+ let res;
132
+
133
+ if (this.#isGUID(name)) {
134
+ const url = new URL(`/Marti/api/missions/guid/${this.#encodeName(name)}/layers`, this.api.url);
135
+
136
+ res = await this.api.fetch(url, {
137
+ method: 'GET',
138
+ headers: this.#headers(opts),
139
+ });
140
+ } else {
141
+ const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/layers`, this.api.url);
142
+
143
+ res = await this.api.fetch(url, {
144
+ method: 'GET',
145
+ headers: this.#headers(opts),
146
+ });
147
+ }
148
+
149
+ res.data.map((l: Static<typeof MissionLayer>) => {
150
+ if (l.type === MissionLayerType.UID && !l.uids) {
151
+ l.uids = [];
152
+ }
153
+ return l;
154
+ })
155
+
156
+ return res;
157
+ }
158
+
159
+ /**
160
+ * Stopgap function until the main latestFeats function can accept a path
161
+ * parameter
162
+ */
163
+ async latestFeats(
164
+ name: string,
165
+ layerUid: string, // Layer UID
166
+ opts?: Static<typeof MissionOptions>
167
+ ): Promise<Static<typeof Feature.Feature>[]> {
168
+ const layer = (await this.get(name, layerUid, opts)).data;
169
+ const feats = await this.api.Mission.latestFeats(name, opts);
170
+
171
+ const layerUids = new Set((layer.uids || []).map((u) => {
172
+ return u.data
173
+ }));
174
+
175
+ const filtered = feats.filter((f) => {
176
+ return layerUids.has(f.id)
177
+ });
178
+
179
+ return filtered;
180
+ }
181
+
182
+ async get(
183
+ name: string,
184
+ layerUid: string, // Layer UID
185
+ opts?: Static<typeof MissionOptions>
186
+ ): Promise<Static<typeof TAKItem_MissionLayer>> {
187
+ const layers = await this.list(name, opts);
188
+
189
+ // TODO this will only return top level layers - need to recurse to lower level layers
190
+ for (const layer of layers.data) {
191
+ if (layer.uid === layerUid) {
192
+ return {
193
+ version: layers.version,
194
+ type: layers.type,
195
+ data: layer,
196
+ nodeId: layers.nodeId
197
+ }
198
+ }
199
+ }
200
+
201
+ throw new Err(404, null, `Layer ${layerUid} not found - only top level layers will be returned`);
202
+ }
203
+
204
+ async create(
205
+ name: string,
206
+ query: Static<typeof CreateInput>,
207
+ opts?: Static<typeof MissionOptions>
208
+ ): Promise<Static<typeof TAKItem_MissionLayer>> {
209
+ if (this.#isGUID(name)) {
210
+ const url = new URL(`/Marti/api/missions/guid/${this.#encodeName(name)}/layers`, this.api.url);
211
+
212
+ let q: keyof Static<typeof CreateInput>;
213
+ for (q in query) {
214
+ if (query[q] !== undefined) {
215
+ url.searchParams.append(q, String(query[q]));
216
+ }
217
+ }
218
+
219
+ return await this.api.fetch(url, {
220
+ method: 'PUT',
221
+ headers: this.#headers(opts),
222
+ });
223
+ } else {
224
+ const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/layers`, this.api.url);
225
+
226
+ let q: keyof Static<typeof CreateInput>;
227
+ for (q in query) {
228
+ if (query[q] !== undefined) {
229
+ url.searchParams.append(q, String(query[q]));
230
+ }
231
+ }
232
+
233
+ return await this.api.fetch(url, {
234
+ method: 'PUT',
235
+ headers: this.#headers(opts),
236
+ });
237
+ }
238
+ }
239
+
240
+ async rename(
241
+ name: string,
242
+ layer: string,
243
+ query: Static<typeof RenameInput>,
244
+ opts?: Static<typeof MissionOptions>
245
+ ) {
246
+ if (this.#isGUID(name)) {
247
+ const url = new URL(`/Marti/api/missions/guid/${this.#encodeName(name)}/layers/${layer}/name`, this.api.url);
248
+
249
+ let q: keyof Static<typeof RenameInput>;
250
+ for (q in query) {
251
+ if (query[q] !== undefined) {
252
+ url.searchParams.append(q, String(query[q]));
253
+ }
254
+ }
255
+
256
+ return await this.api.fetch(url, {
257
+ method: 'PUT',
258
+ headers: this.#headers(opts),
259
+ });
260
+ } else {
261
+ const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/layers/${layer}/name`, this.api.url);
262
+
263
+ let q: keyof Static<typeof RenameInput>;
264
+ for (q in query) {
265
+ if (query[q] !== undefined) {
266
+ url.searchParams.append(q, String(query[q]));
267
+ }
268
+ }
269
+
270
+ return await this.api.fetch(url, {
271
+ method: 'PUT',
272
+ headers: this.#headers(opts),
273
+ });
274
+ }
275
+ }
276
+
277
+ async delete(
278
+ name: string,
279
+ query: Static<typeof DeleteInput>,
280
+ opts?: Static<typeof MissionOptions>
281
+ ) {
282
+ if (this.#isGUID(name)) {
283
+ const url = new URL(`/Marti/api/missions/guid/${this.#encodeName(name)}/layers`, this.api.url);
284
+
285
+ let q: keyof Static<typeof DeleteInput>;
286
+ for (q in query) {
287
+ if (query[q] !== undefined) {
288
+ url.searchParams.append(q, String(query[q]));
289
+ }
290
+ }
291
+
292
+ return await this.api.fetch(url, {
293
+ method: 'DELETE',
294
+ headers: this.#headers(opts),
295
+ });
296
+ } else {
297
+ const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/layers`, this.api.url);
298
+
299
+ let q: keyof Static<typeof DeleteInput>;
300
+ for (q in query) {
301
+ if (query[q] !== undefined) {
302
+ url.searchParams.append(q, String(query[q]));
303
+ }
304
+ }
305
+
306
+ return await this.api.fetch(url, {
307
+ method: 'DELETE',
308
+ headers: this.#headers(opts),
309
+ });
310
+ }
311
+ }
312
+ }