@tak-ps/node-tak 8.3.0 → 8.4.1
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/.github/workflows/doc.yml +6 -6
- package/.github/workflows/release.yml +3 -3
- package/.github/workflows/test.yml +14 -9
- package/CHANGELOG.md +8 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/api/contacts.js +23 -0
- package/dist/lib/api/contacts.js.map +1 -0
- package/dist/lib/api/credentials.js +62 -0
- package/dist/lib/api/credentials.js.map +1 -0
- package/dist/lib/api/export.js +36 -0
- package/dist/lib/api/export.js.map +1 -0
- package/dist/lib/api/files.js +112 -0
- package/dist/lib/api/files.js.map +1 -0
- package/dist/lib/api/groups.js +47 -0
- package/dist/lib/api/groups.js.map +1 -0
- package/dist/lib/api/mission-layer.js +245 -0
- package/dist/lib/api/mission-layer.js.map +1 -0
- package/dist/lib/api/mission-log.js +108 -0
- package/dist/lib/api/mission-log.js.map +1 -0
- package/dist/lib/api/mission.js +583 -0
- package/dist/lib/api/mission.js.map +1 -0
- package/dist/lib/api/oauth.js +54 -0
- package/dist/lib/api/oauth.js.map +1 -0
- package/dist/lib/api/package.js +42 -0
- package/dist/lib/api/package.js.map +1 -0
- package/dist/lib/api/query.js +60 -0
- package/dist/lib/api/query.js.map +1 -0
- package/dist/lib/api/subscriptions.js +73 -0
- package/dist/lib/api/subscriptions.js.map +1 -0
- package/dist/lib/api/types.js +42 -0
- package/dist/lib/api/types.js.map +1 -0
- package/dist/lib/api/video.js +123 -0
- package/dist/lib/api/video.js.map +1 -0
- package/dist/lib/api.js +123 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/auth.js +92 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/fetch.js +26 -0
- package/dist/lib/fetch.js.map +1 -0
- package/dist/lib/stream.js +9 -0
- package/dist/lib/stream.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +7 -1
- package/lib/api/contacts.ts +27 -0
- package/lib/api/credentials.ts +74 -0
- package/lib/api/export.ts +44 -0
- package/lib/api/files.ts +151 -0
- package/lib/api/groups.ts +63 -0
- package/lib/api/mission-layer.ts +312 -0
- package/lib/api/mission-log.ts +140 -0
- package/lib/api/mission.ts +741 -0
- package/lib/api/oauth.ts +68 -0
- package/lib/api/package.ts +56 -0
- package/lib/api/query.ts +79 -0
- package/lib/api/subscriptions.ts +84 -0
- package/lib/api/types.ts +43 -0
- package/lib/api/video.ts +155 -0
- package/lib/api.ts +136 -0
- package/lib/auth.ts +117 -0
- package/lib/fetch.ts +38 -0
- package/lib/stream.ts +10 -0
- 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
|
+
import TAKAPI 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,7 @@ export default class TAK extends EventEmitter {
|
|
|
234
237
|
}
|
|
235
238
|
}
|
|
236
239
|
|
|
237
|
-
export {
|
|
240
|
+
export {
|
|
241
|
+
TAKAPI,
|
|
242
|
+
CoT,
|
|
243
|
+
}
|
|
@@ -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
|
+
}
|
package/lib/api/files.ts
ADDED
|
@@ -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
|
+
}
|