@jd-data-limited/easy-fm 4.1.0 → 4.1.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/package.json +5 -2
- package/src/connection/database.d.ts +7 -7
- package/src/connection/database.js +36 -47
- package/src/connection/database.ts +61 -69
- package/src/connection/databaseBase.d.ts +4 -5
- package/src/connection/databaseBase.ts +3 -4
- package/src/records/field.d.ts +8 -16
- package/src/records/field.js +231 -124
- package/src/records/field.ts +103 -46
- package/src/records/getOperations/recordGetOperation.js +1 -0
- package/src/records/getOperations/recordGetOperation.ts +1 -0
- package/src/records/recordBase.d.ts +1 -1
- package/src/types.d.ts +2 -6
- package/src/types.js +0 -5
- package/src/types.ts +0 -5
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jd-data-limited/easy-fm",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@types/follow-redirects": "^1.14.4",
|
|
6
6
|
"follow-redirects": "^1.15.5",
|
|
7
|
-
"moment": "^2.29.4"
|
|
7
|
+
"moment": "^2.29.4",
|
|
8
|
+
"node-fetch": "^3.3.2",
|
|
9
|
+
"node-fetch-cookies": "^2.1.1"
|
|
8
10
|
},
|
|
9
11
|
"type": "module",
|
|
10
12
|
"main": "src/index.js",
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
"eslint-plugin-promise": "^6.1.1",
|
|
50
52
|
"inquirer": "^9.2.13",
|
|
51
53
|
"ts-morph": "^21.0.1",
|
|
54
|
+
"typedoc": "^0.25.12",
|
|
52
55
|
"typescript": "^5.4.3",
|
|
53
56
|
"webpack": "^5.81.0",
|
|
54
57
|
"webpack-cli": "^5.0.2"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/// <reference types="node"
|
|
2
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
1
|
+
/// <reference types="node" />
|
|
3
2
|
import { EventEmitter } from 'events';
|
|
4
3
|
import { type LayoutInterface } from '../layouts/layoutInterface.js';
|
|
5
4
|
import { Layout } from '../layouts/layout.js';
|
|
@@ -8,7 +7,7 @@ import { type HostBase } from './HostBase.js';
|
|
|
8
7
|
import { type DatabaseBase } from './databaseBase.js';
|
|
9
8
|
import { type ApiResults } from '../models/apiResults.js';
|
|
10
9
|
import { type DatabaseStructure } from '../databaseStructure.js';
|
|
11
|
-
import {
|
|
10
|
+
import { RequestInfo, RequestInit, Response } from 'node-fetch';
|
|
12
11
|
/**
|
|
13
12
|
* Represents a database connection.
|
|
14
13
|
* @template T - The structure of the database.
|
|
@@ -38,7 +37,7 @@ export declare class Database<T extends DatabaseStructure> extends EventEmitter
|
|
|
38
37
|
* @throws {FMError} - Throws an FMError if login fails.
|
|
39
38
|
* @return {Promise<string>} - Returns a promise that resolves to the access token upon successful login.
|
|
40
39
|
*/
|
|
41
|
-
login(forceLogin?: boolean): Promise<string>;
|
|
40
|
+
login(forceLogin?: boolean): Promise<string | undefined>;
|
|
42
41
|
get token(): string;
|
|
43
42
|
/**
|
|
44
43
|
* Returns the endpoint URL for the database connection.
|
|
@@ -46,10 +45,12 @@ export declare class Database<T extends DatabaseStructure> extends EventEmitter
|
|
|
46
45
|
* @returns {string} The endpoint URL.
|
|
47
46
|
*/
|
|
48
47
|
get endpoint(): string;
|
|
49
|
-
_apiRequestRaw(url:
|
|
48
|
+
_apiRequestRaw(url: URL | RequestInfo, options?: RequestInit & {
|
|
50
49
|
headers?: Record<string, string>;
|
|
50
|
+
useCookieJar?: boolean;
|
|
51
51
|
}, autoRelogin?: boolean): Promise<Response>;
|
|
52
|
-
|
|
52
|
+
private _generateCookieHeader;
|
|
53
|
+
_apiRequestJSON<T = any>(url: URL | RequestInfo, options?: RequestInit & {
|
|
53
54
|
headers?: Record<string, string>;
|
|
54
55
|
}): Promise<ApiResults<T>>;
|
|
55
56
|
/**
|
|
@@ -62,5 +63,4 @@ export declare class Database<T extends DatabaseStructure> extends EventEmitter
|
|
|
62
63
|
layout<R extends keyof T['layouts']>(name: R): Layout<T['layouts'][R]>;
|
|
63
64
|
layout<R extends LayoutInterface>(name: string): Layout<R>;
|
|
64
65
|
script(name: string, parameter?: string): Script;
|
|
65
|
-
streamURL(url: string): Promise<IncomingMessage>;
|
|
66
66
|
}
|
|
@@ -5,7 +5,9 @@ import { EventEmitter } from 'events';
|
|
|
5
5
|
import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
|
|
6
6
|
import { FMError } from '../FMError.js';
|
|
7
7
|
import { Layout } from '../layouts/layout.js';
|
|
8
|
-
import
|
|
8
|
+
import fetch from 'node-fetch';
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
import fetchWithCookies, { CookieJar } from "node-fetch-cookies";
|
|
9
11
|
/**
|
|
10
12
|
* Represents a database connection.
|
|
11
13
|
* @template T - The structure of the database.
|
|
@@ -14,7 +16,7 @@ export class Database extends EventEmitter {
|
|
|
14
16
|
_token = '';
|
|
15
17
|
host;
|
|
16
18
|
connection_details;
|
|
17
|
-
cookies =
|
|
19
|
+
cookies = new CookieJar();
|
|
18
20
|
name;
|
|
19
21
|
debug;
|
|
20
22
|
constructor(host, conn) {
|
|
@@ -67,12 +69,13 @@ export class Database extends EventEmitter {
|
|
|
67
69
|
* @return {Promise<string>} - Returns a promise that resolves to the access token upon successful login.
|
|
68
70
|
*/
|
|
69
71
|
async login(forceLogin = false) {
|
|
70
|
-
|
|
72
|
+
if (this.token !== '' && !forceLogin)
|
|
73
|
+
return;
|
|
74
|
+
console.log("RELOGGING IN");
|
|
75
|
+
console.trace();
|
|
71
76
|
// Reset cookies
|
|
72
|
-
this.cookies =
|
|
77
|
+
this.cookies = new CookieJar();
|
|
73
78
|
await this.host.getMetadata();
|
|
74
|
-
if (this.token !== '' && !forceLogin)
|
|
75
|
-
throw new Error('Already logged in. Run logout() first');
|
|
76
79
|
if (this.connection_details.credentials.method === 'token') {
|
|
77
80
|
this._token = (this.connection_details.credentials).token;
|
|
78
81
|
return this.token;
|
|
@@ -121,19 +124,38 @@ export class Database extends EventEmitter {
|
|
|
121
124
|
if (!options.headers)
|
|
122
125
|
options.headers = {};
|
|
123
126
|
options.headers.authorization = 'Bearer ' + this._token;
|
|
127
|
+
// options.redirect = "manual"
|
|
128
|
+
// options.headers.cookies = this._generateCookieHeader()
|
|
129
|
+
// console.log(options.headers.cookies)
|
|
124
130
|
// options.rejectUnauthorized = this.host.verify
|
|
125
|
-
const _fetch =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
const _fetch = options.useCookieJar ?
|
|
132
|
+
await fetchWithCookies(this.cookies, url, options) :
|
|
133
|
+
await fetch(url, options);
|
|
134
|
+
// if (_fetch.headers.get('set-cookie')) {
|
|
135
|
+
// console.log(_fetch.headers.get('set-cookie'))
|
|
136
|
+
// for (const cookie of (_fetch.headers.get("Set-Cookie") ?? "").split("; ")) {
|
|
137
|
+
// const cookieSplit = cookie.split('=')
|
|
138
|
+
// console.log("COOKIE SPLIT:", cookieSplit)
|
|
139
|
+
// if (cookieSplit[1]) this.cookies[cookieSplit[0]] = cookieSplit[1]
|
|
140
|
+
// }
|
|
141
|
+
// }
|
|
142
|
+
// console.log(this.cookies, _fetch.status)
|
|
143
|
+
// // console.log(this, this.cookies, url)
|
|
144
|
+
// console.trace()
|
|
132
145
|
if (_fetch.status === 401 && autoRelogin) {
|
|
146
|
+
console.trace();
|
|
133
147
|
await this.login(true);
|
|
134
148
|
return await this._apiRequestRaw(url, options, false);
|
|
135
149
|
}
|
|
136
|
-
|
|
150
|
+
else
|
|
151
|
+
return _fetch;
|
|
152
|
+
}
|
|
153
|
+
_generateCookieHeader() {
|
|
154
|
+
let out = [];
|
|
155
|
+
for (let key of Object.keys(this.cookies)) {
|
|
156
|
+
out.push(`${key}=${this.cookies[key]}`);
|
|
157
|
+
}
|
|
158
|
+
return out.join("; ");
|
|
137
159
|
}
|
|
138
160
|
async _apiRequestJSON(url, options = {}) {
|
|
139
161
|
if (!options.headers)
|
|
@@ -179,37 +201,4 @@ export class Database extends EventEmitter {
|
|
|
179
201
|
script(name, parameter = '') {
|
|
180
202
|
return ({ name, parameter });
|
|
181
203
|
}
|
|
182
|
-
async streamURL(url) {
|
|
183
|
-
return await new Promise((resolve, reject) => {
|
|
184
|
-
if (!url || typeof url !== 'string') {
|
|
185
|
-
throw new Error('URL is empty, or has invalid value');
|
|
186
|
-
}
|
|
187
|
-
const headers = {};
|
|
188
|
-
if (Object.keys(this.cookies).length !== 0) {
|
|
189
|
-
headers.Cookie = Object.keys(this.cookies)
|
|
190
|
-
.map(key => {
|
|
191
|
-
return key + '=' + this.cookies[key];
|
|
192
|
-
})
|
|
193
|
-
.join('; ');
|
|
194
|
-
}
|
|
195
|
-
const checkForCookies = (res) => {
|
|
196
|
-
// Check for the 'set-cookie' header. If it exists, remember it and strip it for better security.
|
|
197
|
-
if (res.headers['set-cookie']) {
|
|
198
|
-
for (const cookie of res.headers['set-cookie']) {
|
|
199
|
-
const cookieSplit = cookie.split('=');
|
|
200
|
-
this.cookies[cookieSplit[0]] = cookieSplit[1];
|
|
201
|
-
}
|
|
202
|
-
res.headers['set-cookie'] = undefined;
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
// Automatically switch between the http and https modules, based on which is needed
|
|
206
|
-
(url.startsWith('https') ? HTTP_REDIRECT.https : HTTP_REDIRECT.http).get(url, {
|
|
207
|
-
headers,
|
|
208
|
-
beforeRedirect: (options, res) => { checkForCookies(res); }
|
|
209
|
-
}, (res) => {
|
|
210
|
-
checkForCookies(res);
|
|
211
|
-
resolve(res);
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
204
|
}
|
|
@@ -7,13 +7,14 @@ import {generateAuthorizationHeaders} from './generateAuthorizationHeaders.js'
|
|
|
7
7
|
import {FMError} from '../FMError.js'
|
|
8
8
|
import {type LayoutInterface} from '../layouts/layoutInterface.js'
|
|
9
9
|
import {Layout} from '../layouts/layout.js'
|
|
10
|
-
import HTTP_REDIRECT from 'follow-redirects'
|
|
11
10
|
import {type databaseOptionsBase, type databaseOptionsWithExternalSources, type Script} from '../types.js'
|
|
12
11
|
import {type HostBase} from './HostBase.js'
|
|
13
12
|
import {type DatabaseBase} from './databaseBase.js'
|
|
14
13
|
import {type ApiLayout, type ApiResults} from '../models/apiResults.js'
|
|
15
14
|
import {type DatabaseStructure} from '../databaseStructure.js'
|
|
16
|
-
import {
|
|
15
|
+
import fetch, {HeadersInit, RequestInfo, RequestInit, Response} from 'node-fetch';
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
import fetchWithCookies, {CookieJar} from "node-fetch-cookies"
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Represents a database connection.
|
|
@@ -23,11 +24,11 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
23
24
|
private _token: string = ''
|
|
24
25
|
readonly host: HostBase
|
|
25
26
|
private readonly connection_details: databaseOptionsWithExternalSources
|
|
26
|
-
private cookies
|
|
27
|
+
private cookies = new CookieJar()
|
|
27
28
|
readonly name: string
|
|
28
29
|
readonly debug: boolean
|
|
29
30
|
|
|
30
|
-
constructor
|
|
31
|
+
constructor(host: HostBase, conn: databaseOptionsWithExternalSources) {
|
|
31
32
|
super()
|
|
32
33
|
this.host = host
|
|
33
34
|
this.name = conn.database
|
|
@@ -36,7 +37,7 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
39
|
-
private generateExternalSourceLogin
|
|
40
|
+
private generateExternalSourceLogin(data: databaseOptionsBase) {
|
|
40
41
|
if (data.credentials.method === 'filemaker') {
|
|
41
42
|
const _data = data.credentials
|
|
42
43
|
return {
|
|
@@ -44,7 +45,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
44
45
|
username: _data.username,
|
|
45
46
|
password: _data.password
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
48
50
|
throw new Error('Not yet supported login method')
|
|
49
51
|
}
|
|
50
52
|
}
|
|
@@ -56,7 +58,7 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
56
58
|
* @returns {Promise<void>} A promise that resolves with no value once the logout is successful.
|
|
57
59
|
* @throws {Error} Throws an error if the user is not logged in.
|
|
58
60
|
*/
|
|
59
|
-
async logout
|
|
61
|
+
async logout(): Promise<void> {
|
|
60
62
|
if (this.token === '') throw new Error('Not logged in')
|
|
61
63
|
|
|
62
64
|
const _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
|
|
@@ -78,18 +80,21 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
78
80
|
* @throws {FMError} - Throws an FMError if login fails.
|
|
79
81
|
* @return {Promise<string>} - Returns a promise that resolves to the access token upon successful login.
|
|
80
82
|
*/
|
|
81
|
-
async login
|
|
82
|
-
|
|
83
|
+
async login(forceLogin = false) {
|
|
84
|
+
if (this.token !== '' && !forceLogin) return
|
|
85
|
+
|
|
86
|
+
console.log("RELOGGING IN")
|
|
87
|
+
console.trace()
|
|
83
88
|
// Reset cookies
|
|
84
|
-
this.cookies =
|
|
89
|
+
this.cookies = new CookieJar()
|
|
85
90
|
|
|
86
91
|
await this.host.getMetadata()
|
|
87
|
-
if (this.token !== '' && !forceLogin) throw new Error('Already logged in. Run logout() first')
|
|
88
92
|
|
|
89
93
|
if (this.connection_details.credentials.method === 'token') {
|
|
90
94
|
this._token = (this.connection_details.credentials).token
|
|
91
95
|
return this.token
|
|
92
|
-
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
93
98
|
console.time()
|
|
94
99
|
const url = new URL(`${this.endpoint}/sessions`)
|
|
95
100
|
url.hostname = this.host.hostname
|
|
@@ -104,11 +109,12 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
104
109
|
})
|
|
105
110
|
})
|
|
106
111
|
console.time()
|
|
107
|
-
const _res = (await res.json())
|
|
112
|
+
const _res = (await res.json()) as any
|
|
108
113
|
if (res.status === 200) {
|
|
109
114
|
this._token = res.headers.get('x-fm-data-access-token') ?? ''
|
|
110
115
|
return this._token
|
|
111
|
-
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
112
118
|
throw new FMError(
|
|
113
119
|
_res.messages[0].code as string | number,
|
|
114
120
|
_res.status as number,
|
|
@@ -118,7 +124,7 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
get token
|
|
127
|
+
get token() {
|
|
122
128
|
return this._token
|
|
123
129
|
}
|
|
124
130
|
|
|
@@ -127,34 +133,58 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
127
133
|
*
|
|
128
134
|
* @returns {string} The endpoint URL.
|
|
129
135
|
*/
|
|
130
|
-
get endpoint
|
|
136
|
+
get endpoint(): string {
|
|
131
137
|
return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`
|
|
132
138
|
}
|
|
133
139
|
|
|
134
|
-
async _apiRequestRaw
|
|
140
|
+
async _apiRequestRaw(url: URL | RequestInfo, options: RequestInit & {
|
|
141
|
+
headers?: Record<string, string>,
|
|
142
|
+
useCookieJar?: boolean,
|
|
143
|
+
} = {}, autoRelogin = true): Promise<Response> {
|
|
135
144
|
if (this.debug) console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof Request ? url.url : url}`)
|
|
136
145
|
if (this.token === '') await this.login(true)
|
|
137
146
|
|
|
138
147
|
if (!options.headers) options.headers = {}
|
|
139
148
|
options.headers.authorization = 'Bearer ' + this._token
|
|
149
|
+
// options.redirect = "manual"
|
|
150
|
+
// options.headers.cookies = this._generateCookieHeader()
|
|
151
|
+
// console.log(options.headers.cookies)
|
|
140
152
|
// options.rejectUnauthorized = this.host.verify
|
|
141
153
|
|
|
142
|
-
const _fetch =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
const _fetch: Response =
|
|
155
|
+
options.useCookieJar ?
|
|
156
|
+
await fetchWithCookies(this.cookies, url, options):
|
|
157
|
+
await fetch(url, options)
|
|
158
|
+
// if (_fetch.headers.get('set-cookie')) {
|
|
159
|
+
// console.log(_fetch.headers.get('set-cookie'))
|
|
160
|
+
// for (const cookie of (_fetch.headers.get("Set-Cookie") ?? "").split("; ")) {
|
|
161
|
+
// const cookieSplit = cookie.split('=')
|
|
162
|
+
// console.log("COOKIE SPLIT:", cookieSplit)
|
|
163
|
+
// if (cookieSplit[1]) this.cookies[cookieSplit[0]] = cookieSplit[1]
|
|
164
|
+
// }
|
|
165
|
+
// }
|
|
166
|
+
// console.log(this.cookies, _fetch.status)
|
|
167
|
+
// // console.log(this, this.cookies, url)
|
|
168
|
+
// console.trace()
|
|
150
169
|
if (_fetch.status === 401 && autoRelogin) {
|
|
170
|
+
console.trace()
|
|
151
171
|
await this.login(true)
|
|
152
172
|
return await this._apiRequestRaw(url, options, false)
|
|
153
173
|
}
|
|
154
|
-
return _fetch
|
|
174
|
+
else return _fetch
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private _generateCookieHeader() {
|
|
178
|
+
let out: string[] = []
|
|
179
|
+
for (let key of Object.keys(this.cookies)) {
|
|
180
|
+
out.push(`${key}=${this.cookies[key]}`)
|
|
181
|
+
}
|
|
182
|
+
return out.join("; ")
|
|
155
183
|
}
|
|
156
184
|
|
|
157
|
-
async _apiRequestJSON<T = any>(url:
|
|
185
|
+
async _apiRequestJSON<T = any>(url: URL | RequestInfo, options: RequestInit & {
|
|
186
|
+
headers?: Record<string, string>
|
|
187
|
+
} = {}): Promise<ApiResults<T>> {
|
|
158
188
|
if (!options.headers) options.headers = {}
|
|
159
189
|
options.headers['content-type'] = options.headers['content-type'] ? options.headers['content-type'] : 'application/json'
|
|
160
190
|
const _fetch = await this._apiRequestRaw(url, options)
|
|
@@ -178,7 +208,7 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
178
208
|
* @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
|
|
179
209
|
* @throws {FMError} If there was an error retrieving the layouts.
|
|
180
210
|
*/
|
|
181
|
-
async listLayouts
|
|
211
|
+
async listLayouts() {
|
|
182
212
|
const req = await this._apiRequestJSON<{ layouts: ApiLayout[] }>(`${this.endpoint}/layouts?page=2`)
|
|
183
213
|
if (!req.response) throw new FMError(req.messages[0].code, req.httpStatus, req.messages[0].message)
|
|
184
214
|
|
|
@@ -195,49 +225,11 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
195
225
|
|
|
196
226
|
layout<R extends keyof T['layouts']>(name: R): Layout<T['layouts'][R]>
|
|
197
227
|
layout<R extends LayoutInterface>(name: string): Layout<R>
|
|
198
|
-
layout
|
|
228
|
+
layout(name: string): Layout<any> {
|
|
199
229
|
return new Layout<LayoutInterface>(this, name)
|
|
200
230
|
}
|
|
201
231
|
|
|
202
|
-
script
|
|
203
|
-
return ({
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async streamURL (url: string): Promise<IncomingMessage> {
|
|
207
|
-
return await new Promise((resolve, reject) => {
|
|
208
|
-
if (!url || typeof url !== 'string') {
|
|
209
|
-
throw new Error('URL is empty, or has invalid value')
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const headers: Record<string, string> = {}
|
|
213
|
-
if (Object.keys(this.cookies).length !== 0) {
|
|
214
|
-
headers.Cookie = Object.keys(this.cookies)
|
|
215
|
-
.map(key => {
|
|
216
|
-
return key + '=' + this.cookies[key]
|
|
217
|
-
})
|
|
218
|
-
.join('; ')
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const checkForCookies = (res: HTTP_REDIRECT.ResponseDetails) => {
|
|
222
|
-
// Check for the 'set-cookie' header. If it exists, remember it and strip it for better security.
|
|
223
|
-
if (res.headers['set-cookie']) {
|
|
224
|
-
for (const cookie of res.headers['set-cookie']) {
|
|
225
|
-
const cookieSplit = cookie.split('=')
|
|
226
|
-
this.cookies[cookieSplit[0]] = cookieSplit[1]
|
|
227
|
-
}
|
|
228
|
-
res.headers['set-cookie'] = undefined
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Automatically switch between the http and https modules, based on which is needed
|
|
233
|
-
(url.startsWith('https') ? HTTP_REDIRECT.https : HTTP_REDIRECT.http).get(url, {
|
|
234
|
-
headers,
|
|
235
|
-
beforeRedirect: (options: any, res: HTTP_REDIRECT.ResponseDetails) => { checkForCookies(res) }
|
|
236
|
-
}, (res) => {
|
|
237
|
-
checkForCookies(res)
|
|
238
|
-
|
|
239
|
-
resolve(res)
|
|
240
|
-
})
|
|
241
|
-
})
|
|
232
|
+
script(name: string, parameter = ''): Script {
|
|
233
|
+
return ({name, parameter} satisfies Script)
|
|
242
234
|
}
|
|
243
235
|
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
1
|
import { type HostBase } from './HostBase.js';
|
|
3
2
|
import { type ApiResults } from '../models/apiResults.js';
|
|
4
|
-
import {
|
|
3
|
+
import { RequestInfo, RequestInit, Response } from "node-fetch";
|
|
5
4
|
export interface DatabaseBase {
|
|
6
5
|
host: HostBase;
|
|
7
6
|
readonly name: string;
|
|
8
7
|
endpoint: string;
|
|
9
8
|
token: string;
|
|
10
|
-
_apiRequestJSON: <T = unknown>(url:
|
|
9
|
+
_apiRequestJSON: <T = unknown>(url: URL | RequestInfo, options?: RequestInit & {
|
|
11
10
|
headers?: Record<string, string>;
|
|
12
11
|
} | undefined, autoRelogin?: boolean) => Promise<ApiResults<T>>;
|
|
13
|
-
_apiRequestRaw: (url:
|
|
12
|
+
_apiRequestRaw: (url: URL | RequestInfo, options?: RequestInit & {
|
|
14
13
|
headers?: Record<string, string>;
|
|
14
|
+
useCookieJar?: boolean;
|
|
15
15
|
} | undefined) => Promise<Response>;
|
|
16
|
-
streamURL: (url: string) => Promise<IncomingMessage>;
|
|
17
16
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {type HostBase} from './HostBase.js'
|
|
6
6
|
import {type ApiResults} from '../models/apiResults.js'
|
|
7
|
-
import {
|
|
7
|
+
import {RequestInfo, RequestInit, Response} from "node-fetch";
|
|
8
8
|
|
|
9
9
|
export interface DatabaseBase {
|
|
10
10
|
host: HostBase
|
|
@@ -14,7 +14,6 @@ export interface DatabaseBase {
|
|
|
14
14
|
|
|
15
15
|
// layouts: DatabaseStructure["layouts"]
|
|
16
16
|
|
|
17
|
-
_apiRequestJSON: <T = unknown>(url:
|
|
18
|
-
_apiRequestRaw: (url:
|
|
19
|
-
streamURL: (url: string) => Promise<IncomingMessage>
|
|
17
|
+
_apiRequestJSON: <T = unknown>(url: URL | RequestInfo, options?: RequestInit & { headers?: Record<string, string> } | undefined, autoRelogin?: boolean) => Promise<ApiResults<T>>
|
|
18
|
+
_apiRequestRaw: (url: URL | RequestInfo, options?: RequestInit & { headers?: Record<string, string>, useCookieJar?: boolean } | undefined) => Promise<Response>
|
|
20
19
|
}
|
package/src/records/field.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
/// <reference types="node"
|
|
2
|
-
|
|
3
|
-
import type * as http from 'http';
|
|
4
|
-
import { type ContainerBufferResult, DownloadModes, RecordTypes } from '../types.js';
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { RecordTypes } from '../types.js';
|
|
5
3
|
import { type ApiFieldMetadata } from '../models/apiResults.js';
|
|
6
4
|
import { type LayoutBase } from '../layouts/layoutBase.js';
|
|
7
5
|
import { type Moment } from 'moment';
|
|
@@ -21,10 +19,12 @@ export interface Parentable {
|
|
|
21
19
|
* @template T - The type of the field value.
|
|
22
20
|
*/
|
|
23
21
|
export declare class Field<T extends FieldValue> {
|
|
22
|
+
#private;
|
|
24
23
|
parent: Parentable;
|
|
25
24
|
id: string;
|
|
26
25
|
protected _value: T | null;
|
|
27
26
|
edited: boolean;
|
|
27
|
+
static firstContainerDownload: Promise<any> | null;
|
|
28
28
|
constructor(record: Parentable, id: string, contents: T);
|
|
29
29
|
/**
|
|
30
30
|
* Sets the value of the field.
|
|
@@ -40,22 +40,14 @@ export declare class Field<T extends FieldValue> {
|
|
|
40
40
|
/**
|
|
41
41
|
* Uploads a file to the container field.
|
|
42
42
|
*
|
|
43
|
-
* @param {Buffer}
|
|
44
|
-
* @param {string} filename - The filename of the file.
|
|
45
|
-
* @param {string} mime - The MIME type of the file.
|
|
43
|
+
* @param {Buffer} file - The file content as a buffer.
|
|
46
44
|
*
|
|
47
45
|
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
48
46
|
* @throws {Error} - Upload failed with HTTP error.
|
|
49
47
|
*
|
|
50
48
|
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
51
49
|
*/
|
|
52
|
-
upload(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
* @param {DownloadModes} mode - The mode to use for downloading the resource.
|
|
57
|
-
* @return {Promise<http.IncomingMessage>} - A promise that resolves with the incoming message representing the downloaded resource.
|
|
58
|
-
*/
|
|
59
|
-
download(mode: DownloadModes.Stream): Promise<http.IncomingMessage>;
|
|
60
|
-
download(mode: DownloadModes.Buffer): Promise<ContainerBufferResult>;
|
|
50
|
+
upload(file: File): Promise<void>;
|
|
51
|
+
stream(): Promise<NodeJS.ReadableStream>;
|
|
52
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
61
53
|
}
|
package/src/records/field.js
CHANGED
|
@@ -1,60 +1,100 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
5
|
+
var useValue = arguments.length > 2;
|
|
6
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
7
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
8
|
+
}
|
|
9
|
+
return useValue ? value : void 0;
|
|
10
|
+
};
|
|
11
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
12
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
13
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
14
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
15
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
16
|
+
var _, done = false;
|
|
17
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
18
|
+
var context = {};
|
|
19
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
20
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
21
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
22
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
23
|
+
if (kind === "accessor") {
|
|
24
|
+
if (result === void 0) continue;
|
|
25
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
26
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
27
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
28
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
29
|
+
}
|
|
30
|
+
else if (_ = accept(result)) {
|
|
31
|
+
if (kind === "field") initializers.unshift(_);
|
|
32
|
+
else descriptor[key] = _;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
36
|
+
done = true;
|
|
37
|
+
};
|
|
38
|
+
var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
|
|
39
|
+
if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
|
|
40
|
+
return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
|
|
41
|
+
};
|
|
42
|
+
import { RecordTypes } from '../types.js';
|
|
5
43
|
import { ApiFieldDisplayTypes, ApiFieldResultTypes, ApiFieldTypes } from '../models/apiResults.js';
|
|
44
|
+
import { FMError } from "../FMError.js";
|
|
6
45
|
/**
|
|
7
46
|
* A class representing a field in a record.
|
|
8
47
|
*
|
|
9
48
|
* @template T - The type of the field value.
|
|
10
49
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
displayType: ApiFieldDisplayTypes.EDIT_TEXT,
|
|
42
|
-
result: ApiFieldResultTypes.TEXT,
|
|
43
|
-
global: false,
|
|
44
|
-
autoEnter: true,
|
|
45
|
-
fourDigitYear: false,
|
|
46
|
-
maxRepeat: 1,
|
|
47
|
-
maxCharacters: 0,
|
|
48
|
-
notEmpty: false,
|
|
49
|
-
numeric: false,
|
|
50
|
-
timeOfDay: false,
|
|
51
|
-
repetitions: 1,
|
|
52
|
-
valueList: ''
|
|
53
|
-
};
|
|
50
|
+
let Field = (() => {
|
|
51
|
+
let _instanceExtraInitializers = [];
|
|
52
|
+
let _private_streamAsync_decorators;
|
|
53
|
+
let _private_streamAsync_descriptor;
|
|
54
|
+
let _arrayBuffer_decorators;
|
|
55
|
+
return class Field {
|
|
56
|
+
static {
|
|
57
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
58
|
+
_private_streamAsync_decorators = [containerDownloadFunction];
|
|
59
|
+
_arrayBuffer_decorators = [containerDownloadFunction];
|
|
60
|
+
__esDecorate(this, _private_streamAsync_descriptor = { value: __setFunctionName(async function () {
|
|
61
|
+
let req = await this.parent.layout.database._apiRequestRaw(this.string, { useCookieJar: true });
|
|
62
|
+
if (!req.ok || !req.body) {
|
|
63
|
+
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`);
|
|
64
|
+
}
|
|
65
|
+
return req.body;
|
|
66
|
+
}, "#streamAsync") }, _private_streamAsync_decorators, { kind: "method", name: "#streamAsync", static: false, private: true, access: { has: obj => #streamAsync in obj, get: obj => obj.#streamAsync }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
67
|
+
__esDecorate(this, null, _arrayBuffer_decorators, { kind: "method", name: "arrayBuffer", static: false, private: false, access: { has: obj => "arrayBuffer" in obj, get: obj => obj.arrayBuffer }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
68
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
69
|
+
}
|
|
70
|
+
parent = __runInitializers(this, _instanceExtraInitializers);
|
|
71
|
+
id;
|
|
72
|
+
_value;
|
|
73
|
+
edited;
|
|
74
|
+
static firstContainerDownload = null;
|
|
75
|
+
constructor(record, id, contents) {
|
|
76
|
+
this.parent = record;
|
|
77
|
+
this.id = id;
|
|
78
|
+
this._value = contents;
|
|
79
|
+
this.edited = false;
|
|
54
80
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Sets the value of the field.
|
|
83
|
+
*
|
|
84
|
+
* @param {T | null} content - The new content value to be set. Can be either the type T or null.
|
|
85
|
+
* @throws {Error} Cannot set container value using set(). Use upload() instead, if the result is a 'container'.
|
|
86
|
+
*/
|
|
87
|
+
set(content) {
|
|
88
|
+
if (this.metadata.result === 'container')
|
|
89
|
+
throw new Error('Cannot set container value using set(). Use upload() instead.');
|
|
90
|
+
else
|
|
91
|
+
this._value = content;
|
|
92
|
+
this.edited = true;
|
|
93
|
+
}
|
|
94
|
+
get metadata() {
|
|
95
|
+
if (!this.parent.layout.metadata) {
|
|
96
|
+
// Default to a regular text field
|
|
97
|
+
return {
|
|
58
98
|
name: this.id.toString(),
|
|
59
99
|
type: ApiFieldTypes.NORMAL,
|
|
60
100
|
displayType: ApiFieldDisplayTypes.EDIT_TEXT,
|
|
@@ -70,90 +110,157 @@ export class Field {
|
|
|
70
110
|
repetitions: 1,
|
|
71
111
|
valueList: ''
|
|
72
112
|
};
|
|
113
|
+
}
|
|
114
|
+
if (this.parent.type === RecordTypes.PORTAL && this.parent.portal) {
|
|
115
|
+
return this.parent.layout.metadata.portalMetaData[this.parent.portal.name || 'portal not attached'].find(i => i.name === this.id) ??
|
|
116
|
+
{
|
|
117
|
+
name: this.id.toString(),
|
|
118
|
+
type: ApiFieldTypes.NORMAL,
|
|
119
|
+
displayType: ApiFieldDisplayTypes.EDIT_TEXT,
|
|
120
|
+
result: ApiFieldResultTypes.TEXT,
|
|
121
|
+
global: false,
|
|
122
|
+
autoEnter: true,
|
|
123
|
+
fourDigitYear: false,
|
|
124
|
+
maxRepeat: 1,
|
|
125
|
+
maxCharacters: 0,
|
|
126
|
+
notEmpty: false,
|
|
127
|
+
numeric: false,
|
|
128
|
+
timeOfDay: false,
|
|
129
|
+
repetitions: 1,
|
|
130
|
+
valueList: ''
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
return this.parent.layout.metadata.fieldMetaData.find(i => i.name === this.id) ?? {
|
|
135
|
+
name: this.id.toString(),
|
|
136
|
+
type: ApiFieldTypes.NORMAL,
|
|
137
|
+
displayType: ApiFieldDisplayTypes.EDIT_TEXT,
|
|
138
|
+
result: ApiFieldResultTypes.TEXT,
|
|
139
|
+
global: false,
|
|
140
|
+
autoEnter: true,
|
|
141
|
+
fourDigitYear: false,
|
|
142
|
+
maxRepeat: 1,
|
|
143
|
+
maxCharacters: 0,
|
|
144
|
+
notEmpty: false,
|
|
145
|
+
numeric: false,
|
|
146
|
+
timeOfDay: false,
|
|
147
|
+
repetitions: 1,
|
|
148
|
+
valueList: ''
|
|
149
|
+
};
|
|
150
|
+
}
|
|
73
151
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
name: this.id.toString(),
|
|
77
|
-
type: ApiFieldTypes.NORMAL,
|
|
78
|
-
displayType: ApiFieldDisplayTypes.EDIT_TEXT,
|
|
79
|
-
result: ApiFieldResultTypes.TEXT,
|
|
80
|
-
global: false,
|
|
81
|
-
autoEnter: true,
|
|
82
|
-
fourDigitYear: false,
|
|
83
|
-
maxRepeat: 1,
|
|
84
|
-
maxCharacters: 0,
|
|
85
|
-
notEmpty: false,
|
|
86
|
-
numeric: false,
|
|
87
|
-
timeOfDay: false,
|
|
88
|
-
repetitions: 1,
|
|
89
|
-
valueList: ''
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
get value() {
|
|
94
|
-
// if (this.metadata.result === "container") throw "Use await field.stream() to get the contents of a container field, instead of field.value"
|
|
95
|
-
return this._value;
|
|
96
|
-
}
|
|
97
|
-
set value(value) {
|
|
98
|
-
this._value = value;
|
|
99
|
-
}
|
|
100
|
-
get string() {
|
|
101
|
-
if (typeof this._value === 'string') {
|
|
152
|
+
get value() {
|
|
153
|
+
// if (this.metadata.result === "container") throw "Use await field.stream() to get the contents of a container field, instead of field.value"
|
|
102
154
|
return this._value;
|
|
103
155
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Uploads a file to the container field.
|
|
108
|
-
*
|
|
109
|
-
* @param {Buffer} buffer - The file content as a buffer.
|
|
110
|
-
* @param {string} filename - The filename of the file.
|
|
111
|
-
* @param {string} mime - The MIME type of the file.
|
|
112
|
-
*
|
|
113
|
-
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
114
|
-
* @throws {Error} - Upload failed with HTTP error.
|
|
115
|
-
*
|
|
116
|
-
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
117
|
-
*/
|
|
118
|
-
async upload(buffer, filename, mime) {
|
|
119
|
-
if (this.metadata.result !== 'container') {
|
|
120
|
-
throw new Error('Cannot upload a file to the field; ' + this.id + ' (not a container field)');
|
|
156
|
+
set value(value) {
|
|
157
|
+
this._value = value;
|
|
121
158
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Authorization: 'Bearer ' + this.parent.layout.database.token,
|
|
128
|
-
'Content-Type': 'multipart/form-data'
|
|
129
|
-
},
|
|
130
|
-
body: form
|
|
131
|
-
});
|
|
132
|
-
if (!res.ok)
|
|
133
|
-
throw new Error(`Upload failed with HTTP error: ${res.status} (${res.statusText})`);
|
|
134
|
-
}
|
|
135
|
-
async download(mode = DownloadModes.Stream) {
|
|
136
|
-
if (this.metadata.result !== ApiFieldResultTypes.CONTAINER)
|
|
137
|
-
throw new Error('Cannot perform download() on a non-container field');
|
|
138
|
-
const stream = await this.parent.layout.database.streamURL(this.string);
|
|
139
|
-
if (mode === DownloadModes.Stream) {
|
|
140
|
-
return stream;
|
|
159
|
+
get string() {
|
|
160
|
+
if (typeof this._value === 'string') {
|
|
161
|
+
return this._value;
|
|
162
|
+
}
|
|
163
|
+
throw new Error('Field value is not a string');
|
|
141
164
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Uploads a file to the container field.
|
|
167
|
+
*
|
|
168
|
+
* @param {Buffer} file - The file content as a buffer.
|
|
169
|
+
*
|
|
170
|
+
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
171
|
+
* @throws {Error} - Upload failed with HTTP error.
|
|
172
|
+
*
|
|
173
|
+
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
174
|
+
*/
|
|
175
|
+
async upload(file) {
|
|
176
|
+
if (this.metadata.result !== 'container') {
|
|
177
|
+
throw new Error('Cannot upload a file to the field; ' + this.id + ' (not a container field)');
|
|
178
|
+
}
|
|
179
|
+
const form = new FormData();
|
|
180
|
+
form.append('upload', file);
|
|
181
|
+
console.log(`ENDPOINT: ${this.parent.endpoint}/containers/${this.id}/1`);
|
|
182
|
+
const res = await this.parent.layout.database._apiRequestRaw(`${this.parent.endpoint}/containers/${this.id}/1`, {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers: {
|
|
185
|
+
// Authorization: 'Bearer ' + this.parent.layout.database.token,
|
|
186
|
+
// 'Content-Type': 'multipart/form-data'
|
|
187
|
+
},
|
|
188
|
+
body: form
|
|
146
189
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
console.log("RESULT:", await res.text());
|
|
192
|
+
throw new Error(`Upload failed with HTTP error: ${res.status} (${res.statusText})`);
|
|
193
|
+
}
|
|
194
|
+
const data = await res.json();
|
|
195
|
+
if (data.messages[0].code === "0")
|
|
196
|
+
return;
|
|
197
|
+
else {
|
|
198
|
+
console.log(data);
|
|
199
|
+
throw new FMError(data.messages[0].code, res.status, res);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
get #streamAsync() { return _private_streamAsync_descriptor.value; }
|
|
203
|
+
stream() {
|
|
204
|
+
return this.#streamAsync();
|
|
205
|
+
}
|
|
206
|
+
async arrayBuffer() {
|
|
207
|
+
console.log(this.string);
|
|
208
|
+
const req = await this.parent.layout.database._apiRequestRaw(this.string, { useCookieJar: true });
|
|
209
|
+
if (!req.ok)
|
|
210
|
+
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`);
|
|
211
|
+
console.log(req);
|
|
212
|
+
return await req.arrayBuffer();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
})();
|
|
216
|
+
export { Field };
|
|
217
|
+
function containerDownloadFunction(originalMethod, context) {
|
|
218
|
+
async function replacementMethod(...args) {
|
|
219
|
+
return ContainerDownloadExecutor.processTask(() => originalMethod.call(this, ...args));
|
|
220
|
+
}
|
|
221
|
+
return replacementMethod;
|
|
222
|
+
}
|
|
223
|
+
class ContainerDownloadExecutorClass {
|
|
224
|
+
hasDownloadedFirstContainer = 0;
|
|
225
|
+
waitingFuncs = [];
|
|
226
|
+
constructor() { }
|
|
227
|
+
async processTask(task) {
|
|
228
|
+
let isFirstTask = this.hasDownloadedFirstContainer === 0;
|
|
229
|
+
if (isFirstTask) {
|
|
230
|
+
this.hasDownloadedFirstContainer = 1;
|
|
231
|
+
let res;
|
|
232
|
+
try {
|
|
233
|
+
res = await task();
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
this.hasDownloadedFirstContainer = 0;
|
|
237
|
+
let nextTask = this.waitingFuncs.splice(0, 1)[0];
|
|
238
|
+
if (nextTask) {
|
|
239
|
+
this.processTask(nextTask.func)
|
|
240
|
+
.then(res => nextTask.done(res))
|
|
241
|
+
.catch(e => nextTask.err(e));
|
|
242
|
+
}
|
|
243
|
+
throw e;
|
|
244
|
+
}
|
|
245
|
+
this.hasDownloadedFirstContainer = 2;
|
|
246
|
+
for (let task of this.waitingFuncs) {
|
|
247
|
+
task.func()
|
|
248
|
+
.then((res) => task.done(res))
|
|
249
|
+
.catch((e) => task.err(e));
|
|
250
|
+
}
|
|
251
|
+
return res;
|
|
252
|
+
}
|
|
253
|
+
else if (this.hasDownloadedFirstContainer === 1) {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
this.waitingFuncs.push({
|
|
256
|
+
func: task,
|
|
257
|
+
done: resolve,
|
|
258
|
+
err: reject
|
|
155
259
|
});
|
|
156
260
|
});
|
|
157
|
-
}
|
|
261
|
+
}
|
|
262
|
+
else
|
|
263
|
+
return await task();
|
|
158
264
|
}
|
|
159
265
|
}
|
|
266
|
+
const ContainerDownloadExecutor = new ContainerDownloadExecutorClass();
|
package/src/records/field.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import {type ContainerBufferResult, DownloadModes, RecordTypes} from '../types.js'
|
|
5
|
+
import {RecordTypes} from '../types.js'
|
|
7
6
|
import {ApiFieldDisplayTypes, type ApiFieldMetadata, ApiFieldResultTypes, ApiFieldTypes} from '../models/apiResults.js'
|
|
8
7
|
import {type LayoutBase} from '../layouts/layoutBase.js'
|
|
9
8
|
import {type Moment} from 'moment'
|
|
9
|
+
import {FMError} from "../FMError.js";
|
|
10
10
|
|
|
11
11
|
export type FieldValue = string | number | Moment | Container
|
|
12
12
|
export type Container = null
|
|
@@ -29,7 +29,9 @@ export class Field<T extends FieldValue> {
|
|
|
29
29
|
protected _value: T | null
|
|
30
30
|
edited: boolean
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
static firstContainerDownload: Promise<any> | null = null
|
|
33
|
+
|
|
34
|
+
constructor(record: Parentable, id: string, contents: T) {
|
|
33
35
|
this.parent = record
|
|
34
36
|
this.id = id
|
|
35
37
|
this._value = contents
|
|
@@ -42,13 +44,13 @@ export class Field<T extends FieldValue> {
|
|
|
42
44
|
* @param {T | null} content - The new content value to be set. Can be either the type T or null.
|
|
43
45
|
* @throws {Error} Cannot set container value using set(). Use upload() instead, if the result is a 'container'.
|
|
44
46
|
*/
|
|
45
|
-
set
|
|
47
|
+
set(content: T | null) {
|
|
46
48
|
if (this.metadata.result === 'container') throw new Error('Cannot set container value using set(). Use upload() instead.')
|
|
47
49
|
else this._value = content
|
|
48
50
|
this.edited = true
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
get metadata
|
|
53
|
+
get metadata(): ApiFieldMetadata {
|
|
52
54
|
if (!this.parent.layout.metadata) {
|
|
53
55
|
// Default to a regular text field
|
|
54
56
|
return {
|
|
@@ -86,7 +88,8 @@ export class Field<T extends FieldValue> {
|
|
|
86
88
|
repetitions: 1,
|
|
87
89
|
valueList: ''
|
|
88
90
|
}
|
|
89
|
-
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
90
93
|
return this.parent.layout.metadata.fieldMetaData.find(i => i.name === this.id) ?? {
|
|
91
94
|
name: this.id.toString(),
|
|
92
95
|
type: ApiFieldTypes.NORMAL,
|
|
@@ -106,16 +109,16 @@ export class Field<T extends FieldValue> {
|
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
get value
|
|
112
|
+
get value(): T | null {
|
|
110
113
|
// if (this.metadata.result === "container") throw "Use await field.stream() to get the contents of a container field, instead of field.value"
|
|
111
114
|
return this._value
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
set value
|
|
117
|
+
set value(value: T) {
|
|
115
118
|
this._value = value
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
get string
|
|
121
|
+
get string(): string {
|
|
119
122
|
if (typeof this._value === 'string') {
|
|
120
123
|
return this._value
|
|
121
124
|
}
|
|
@@ -125,64 +128,118 @@ export class Field<T extends FieldValue> {
|
|
|
125
128
|
/**
|
|
126
129
|
* Uploads a file to the container field.
|
|
127
130
|
*
|
|
128
|
-
* @param {Buffer}
|
|
129
|
-
* @param {string} filename - The filename of the file.
|
|
130
|
-
* @param {string} mime - The MIME type of the file.
|
|
131
|
+
* @param {Buffer} file - The file content as a buffer.
|
|
131
132
|
*
|
|
132
133
|
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
133
134
|
* @throws {Error} - Upload failed with HTTP error.
|
|
134
135
|
*
|
|
135
136
|
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
136
137
|
*/
|
|
137
|
-
async upload
|
|
138
|
+
async upload(file: File): Promise<void> {
|
|
138
139
|
if (this.metadata.result !== 'container') {
|
|
139
140
|
throw new Error('Cannot upload a file to the field; ' + this.id + ' (not a container field)')
|
|
140
141
|
}
|
|
141
142
|
const form = new FormData()
|
|
142
|
-
form.append('upload',
|
|
143
|
+
form.append('upload', file)
|
|
143
144
|
|
|
145
|
+
console.log(`ENDPOINT: ${this.parent.endpoint}/containers/${this.id}/1`)
|
|
144
146
|
const res = await this.parent.layout.database._apiRequestRaw(`${this.parent.endpoint}/containers/${this.id}/1`, {
|
|
145
147
|
method: 'POST',
|
|
146
148
|
headers: {
|
|
147
|
-
Authorization: 'Bearer ' + this.parent.layout.database.token,
|
|
148
|
-
'Content-Type': 'multipart/form-data'
|
|
149
|
+
// Authorization: 'Bearer ' + this.parent.layout.database.token,
|
|
150
|
+
// 'Content-Type': 'multipart/form-data'
|
|
149
151
|
},
|
|
150
152
|
body: form
|
|
151
153
|
})
|
|
152
|
-
if (!res.ok)
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
console.log("RESULT:", await res.text())
|
|
156
|
+
throw new Error(`Upload failed with HTTP error: ${res.status} (${res.statusText})`)
|
|
157
|
+
}
|
|
158
|
+
const data: { messages: Array<{ code: string }> } = await res.json() as any
|
|
159
|
+
if (data.messages[0].code === "0") return
|
|
160
|
+
else {
|
|
161
|
+
console.log(data)
|
|
162
|
+
throw new FMError(data.messages[0].code, res.status, res)
|
|
163
|
+
}
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*/
|
|
161
|
-
download (mode: DownloadModes.Stream): Promise<http.IncomingMessage>
|
|
162
|
-
download (mode: DownloadModes.Buffer): Promise<ContainerBufferResult>
|
|
163
|
-
async download (mode: DownloadModes = DownloadModes.Stream): Promise<http.IncomingMessage | ContainerBufferResult> {
|
|
164
|
-
if (this.metadata.result !== ApiFieldResultTypes.CONTAINER) throw new Error('Cannot perform download() on a non-container field')
|
|
165
|
-
|
|
166
|
-
const stream = await this.parent.layout.database.streamURL(this.string)
|
|
167
|
-
if (mode === DownloadModes.Stream) {
|
|
168
|
-
return stream
|
|
166
|
+
@containerDownloadFunction
|
|
167
|
+
async #streamAsync() {
|
|
168
|
+
let req = await this.parent.layout.database._apiRequestRaw(this.string, {useCookieJar: true})
|
|
169
|
+
if (!req.ok || !req.body) {
|
|
170
|
+
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`)
|
|
169
171
|
}
|
|
172
|
+
return req.body
|
|
173
|
+
}
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
return
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
stream() {
|
|
176
|
+
return this.#streamAsync()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@containerDownloadFunction
|
|
180
|
+
async arrayBuffer() {
|
|
181
|
+
console.log(this.string)
|
|
182
|
+
const req = await this.parent.layout.database._apiRequestRaw(this.string, {useCookieJar: true})
|
|
183
|
+
if (!req.ok) throw new Error(`HTTP Error: ${req.status} (${req.statusText})`)
|
|
184
|
+
console.log(req)
|
|
185
|
+
return await req.arrayBuffer()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function containerDownloadFunction<T extends FieldValue, R = unknown>(originalMethod: (...p: any[]) => Promise<R>, context: ClassMethodDecoratorContext<Field<T>>) {
|
|
190
|
+
async function replacementMethod(this: Field<T>, ...args: any[]) {
|
|
191
|
+
return ContainerDownloadExecutor.processTask(() => originalMethod.call(this, ...args))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return replacementMethod
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
class ContainerDownloadExecutorClass {
|
|
198
|
+
hasDownloadedFirstContainer: 0 | 1 | 2 = 0
|
|
199
|
+
waitingFuncs: {
|
|
200
|
+
func: (() => any),
|
|
201
|
+
done: ((...args: any[]) => any),
|
|
202
|
+
err: ((e: Error) => any)
|
|
203
|
+
}[] = []
|
|
204
|
+
|
|
205
|
+
constructor() {}
|
|
206
|
+
|
|
207
|
+
async processTask<T extends () => Promise<any>>(task: T) {
|
|
208
|
+
let isFirstTask = this.hasDownloadedFirstContainer === 0
|
|
209
|
+
if (isFirstTask) {
|
|
210
|
+
this.hasDownloadedFirstContainer = 1
|
|
211
|
+
let res
|
|
212
|
+
try {
|
|
213
|
+
res = await task()
|
|
214
|
+
} catch(e) {
|
|
215
|
+
this.hasDownloadedFirstContainer = 0
|
|
216
|
+
let nextTask = this.waitingFuncs.splice(0, 1)[0]
|
|
217
|
+
if (nextTask) {
|
|
218
|
+
this.processTask(nextTask.func)
|
|
219
|
+
.then(res => nextTask.done(res))
|
|
220
|
+
.catch(e => nextTask.err(e))
|
|
221
|
+
}
|
|
222
|
+
throw e
|
|
223
|
+
}
|
|
224
|
+
this.hasDownloadedFirstContainer = 2
|
|
225
|
+
for (let task of this.waitingFuncs) {
|
|
226
|
+
task.func()
|
|
227
|
+
.then((res: any) => task.done(res))
|
|
228
|
+
.catch((e: any) => task.err(e))
|
|
229
|
+
}
|
|
230
|
+
return res
|
|
231
|
+
}
|
|
232
|
+
else if (this.hasDownloadedFirstContainer === 1) {
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
this.waitingFuncs.push({
|
|
235
|
+
func: task,
|
|
236
|
+
done: resolve,
|
|
237
|
+
err: reject
|
|
238
|
+
})
|
|
185
239
|
})
|
|
186
|
-
}
|
|
240
|
+
}
|
|
241
|
+
else return await task()
|
|
187
242
|
}
|
|
188
243
|
}
|
|
244
|
+
|
|
245
|
+
const ContainerDownloadExecutor = new ContainerDownloadExecutorClass()
|
package/src/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/// <reference types="node"
|
|
2
|
-
/// <reference types="node"
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
3
|
import type * as http from 'http';
|
|
4
4
|
import { type FMError } from './FMError.js';
|
|
5
5
|
import { type LayoutInterface } from './layouts/layoutInterface.js';
|
|
@@ -52,10 +52,6 @@ export interface ScriptRequestData {
|
|
|
52
52
|
export interface extraBodyOptions {
|
|
53
53
|
scripts?: ScriptRequestData;
|
|
54
54
|
}
|
|
55
|
-
export declare enum DownloadModes {
|
|
56
|
-
Stream = 0,
|
|
57
|
-
Buffer = 1
|
|
58
|
-
}
|
|
59
55
|
export interface Script {
|
|
60
56
|
name: string;
|
|
61
57
|
parameter: string;
|
package/src/types.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright (c) 2022-2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
|
-
export var DownloadModes;
|
|
5
|
-
(function (DownloadModes) {
|
|
6
|
-
DownloadModes[DownloadModes["Stream"] = 0] = "Stream";
|
|
7
|
-
DownloadModes[DownloadModes["Buffer"] = 1] = "Buffer";
|
|
8
|
-
})(DownloadModes || (DownloadModes = {}));
|
|
9
4
|
export var RecordTypes;
|
|
10
5
|
(function (RecordTypes) {
|
|
11
6
|
RecordTypes[RecordTypes["UNKNOWN"] = 0] = "UNKNOWN";
|