@jd-data-limited/easy-fm 4.0.16 → 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 +16 -7
- package/src/FMError.d.ts +2 -2
- package/src/FMError.js +507 -508
- package/src/FMError.ts +510 -511
- package/src/connection/FMHost.d.ts +23 -5
- package/src/connection/FMHost.js +42 -26
- package/src/connection/FMHost.ts +63 -49
- package/src/connection/HostBase.d.ts +3 -3
- package/src/connection/HostBase.ts +5 -5
- package/src/connection/database.d.ts +53 -20
- package/src/connection/database.js +107 -90
- package/src/connection/database.ts +133 -121
- package/src/connection/databaseBase.d.ts +11 -7
- package/src/connection/databaseBase.ts +8 -8
- package/src/connection/generateAuthorizationHeaders.d.ts +1 -1
- package/src/connection/generateAuthorizationHeaders.js +10 -10
- package/src/connection/generateAuthorizationHeaders.ts +21 -21
- package/src/databaseStructure.d.ts +2 -4
- package/src/databaseStructure.ts +3 -5
- package/src/index.d.ts +17 -17
- package/src/index.js +13 -13
- package/src/index.ts +24 -24
- package/src/layouts/layout.d.ts +17 -6
- package/src/layouts/layout.js +19 -8
- package/src/layouts/layout.ts +33 -23
- package/src/layouts/layoutBase.d.ts +5 -5
- package/src/layouts/layoutBase.ts +6 -6
- package/src/layouts/layoutInterface.d.ts +3 -5
- package/src/layouts/layoutInterface.ts +4 -6
- package/src/layouts/layoutRecordManager.d.ts +29 -6
- package/src/layouts/layoutRecordManager.js +32 -9
- package/src/layouts/layoutRecordManager.ts +42 -19
- package/src/layouts/recordFieldsMap.d.ts +2 -4
- package/src/layouts/recordFieldsMap.ts +2 -4
- package/src/models/apiResults.d.ts +7 -13
- package/src/models/apiResults.ts +58 -58
- package/src/records/field.d.ts +39 -17
- package/src/records/field.js +248 -131
- package/src/records/field.ts +160 -82
- package/src/records/getOperations/recordGetOperation.d.ts +44 -24
- package/src/records/getOperations/recordGetOperation.js +88 -61
- package/src/records/getOperations/recordGetOperation.ts +128 -108
- package/src/records/layoutRecord.d.ts +28 -14
- package/src/records/layoutRecord.js +69 -60
- package/src/records/layoutRecord.ts +121 -119
- package/src/records/layoutRecordBase.d.ts +2 -2
- package/src/records/layoutRecordBase.ts +3 -3
- package/src/records/portal.d.ts +19 -5
- package/src/records/portal.js +19 -5
- package/src/records/portal.ts +28 -16
- package/src/records/portalBase.d.ts +5 -5
- package/src/records/portalBase.ts +6 -6
- package/src/records/portalRecord.d.ts +16 -8
- package/src/records/portalRecord.js +16 -6
- package/src/records/portalRecord.ts +29 -19
- package/src/records/recordBase.d.ts +18 -8
- package/src/records/recordBase.js +23 -16
- package/src/records/recordBase.ts +41 -37
- package/src/types.d.ts +23 -30
- package/src/types.js +0 -5
- package/src/types.ts +55 -64
- package/src/utils/query.d.ts +7 -7
- package/src/utils/query.js +10 -10
- package/src/utils/query.ts +19 -20
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
|
-
import { EventEmitter } from
|
|
5
|
-
import { generateAuthorizationHeaders } from
|
|
6
|
-
import { FMError } from
|
|
7
|
-
import { Layout } from
|
|
8
|
-
import
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
|
|
6
|
+
import { FMError } from '../FMError.js';
|
|
7
|
+
import { Layout } from '../layouts/layout.js';
|
|
8
|
+
import fetch from 'node-fetch';
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
import fetchWithCookies, { CookieJar } from "node-fetch-cookies";
|
|
11
|
+
/**
|
|
12
|
+
* Represents a database connection.
|
|
13
|
+
* @template T - The structure of the database.
|
|
14
|
+
*/
|
|
9
15
|
export class Database extends EventEmitter {
|
|
10
|
-
_token;
|
|
16
|
+
_token = '';
|
|
11
17
|
host;
|
|
12
18
|
connection_details;
|
|
13
|
-
cookies =
|
|
19
|
+
cookies = new CookieJar();
|
|
14
20
|
name;
|
|
15
21
|
debug;
|
|
16
22
|
constructor(host, conn) {
|
|
@@ -18,11 +24,12 @@ export class Database extends EventEmitter {
|
|
|
18
24
|
this.host = host;
|
|
19
25
|
this.name = conn.database;
|
|
20
26
|
this.connection_details = conn;
|
|
21
|
-
this.debug =
|
|
27
|
+
this.debug = conn.debug ?? false;
|
|
22
28
|
}
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
23
30
|
generateExternalSourceLogin(data) {
|
|
24
|
-
if (data.credentials.method ===
|
|
25
|
-
|
|
31
|
+
if (data.credentials.method === 'filemaker') {
|
|
32
|
+
const _data = data.credentials;
|
|
26
33
|
return {
|
|
27
34
|
database: data.database,
|
|
28
35
|
username: _data.username,
|
|
@@ -30,50 +37,67 @@ export class Database extends EventEmitter {
|
|
|
30
37
|
};
|
|
31
38
|
}
|
|
32
39
|
else {
|
|
33
|
-
throw
|
|
40
|
+
throw new Error('Not yet supported login method');
|
|
34
41
|
}
|
|
35
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Logs out the user by deleting the current session token.
|
|
45
|
+
* Throws an error if the user is not logged in.
|
|
46
|
+
*
|
|
47
|
+
* @returns {Promise<void>} A promise that resolves with no value once the logout is successful.
|
|
48
|
+
* @throws {Error} Throws an error if the user is not logged in.
|
|
49
|
+
*/
|
|
36
50
|
async logout() {
|
|
37
|
-
if (
|
|
38
|
-
throw new Error(
|
|
39
|
-
|
|
40
|
-
method:
|
|
51
|
+
if (this.token === '')
|
|
52
|
+
throw new Error('Not logged in');
|
|
53
|
+
const _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
|
|
54
|
+
method: 'DELETE',
|
|
41
55
|
headers: {
|
|
42
|
-
|
|
56
|
+
'content-type': 'application/json'
|
|
43
57
|
}
|
|
44
58
|
});
|
|
45
|
-
|
|
59
|
+
await _fetch.json();
|
|
46
60
|
// console.log(data)
|
|
47
|
-
this._token =
|
|
61
|
+
this._token = '';
|
|
48
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Logs in to the database. Not required, as this is often done automatically
|
|
65
|
+
*
|
|
66
|
+
* @param {boolean} [forceLogin=false] - Whether to force login even if already logged in.
|
|
67
|
+
* @throws {Error} - Throws an error if already logged in and forceLogin is false.
|
|
68
|
+
* @throws {FMError} - Throws an FMError if login fails.
|
|
69
|
+
* @return {Promise<string>} - Returns a promise that resolves to the access token upon successful login.
|
|
70
|
+
*/
|
|
49
71
|
async login(forceLogin = false) {
|
|
72
|
+
if (this.token !== '' && !forceLogin)
|
|
73
|
+
return;
|
|
74
|
+
console.log("RELOGGING IN");
|
|
75
|
+
console.trace();
|
|
50
76
|
// Reset cookies
|
|
51
|
-
this.cookies =
|
|
77
|
+
this.cookies = new CookieJar();
|
|
52
78
|
await this.host.getMetadata();
|
|
53
|
-
if (this.
|
|
54
|
-
|
|
55
|
-
if (this.connection_details.credentials.method === "token") {
|
|
56
|
-
this._token = this.connection_details.credentials.token;
|
|
79
|
+
if (this.connection_details.credentials.method === 'token') {
|
|
80
|
+
this._token = (this.connection_details.credentials).token;
|
|
57
81
|
return this.token;
|
|
58
82
|
}
|
|
59
83
|
else {
|
|
60
84
|
console.time();
|
|
61
|
-
|
|
85
|
+
const url = new URL(`${this.endpoint}/sessions`);
|
|
62
86
|
url.hostname = this.host.hostname;
|
|
63
|
-
|
|
64
|
-
method:
|
|
87
|
+
const res = await fetch(url, {
|
|
88
|
+
method: 'POST',
|
|
65
89
|
headers: generateAuthorizationHeaders(this.connection_details.credentials),
|
|
66
90
|
body: JSON.stringify({
|
|
67
91
|
fmDataSource: this.connection_details.externalSources.map(i => {
|
|
68
|
-
|
|
92
|
+
const _i = i;
|
|
69
93
|
return this.generateExternalSourceLogin(_i);
|
|
70
94
|
})
|
|
71
95
|
})
|
|
72
96
|
});
|
|
73
97
|
console.time();
|
|
74
|
-
|
|
98
|
+
const _res = (await res.json());
|
|
75
99
|
if (res.status === 200) {
|
|
76
|
-
this._token = res.headers.get('x-fm-data-access-token');
|
|
100
|
+
this._token = res.headers.get('x-fm-data-access-token') ?? '';
|
|
77
101
|
return this._token;
|
|
78
102
|
}
|
|
79
103
|
else {
|
|
@@ -84,54 +108,84 @@ export class Database extends EventEmitter {
|
|
|
84
108
|
get token() {
|
|
85
109
|
return this._token;
|
|
86
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Returns the endpoint URL for the database connection.
|
|
113
|
+
*
|
|
114
|
+
* @returns {string} The endpoint URL.
|
|
115
|
+
*/
|
|
87
116
|
get endpoint() {
|
|
88
117
|
return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
|
|
89
118
|
}
|
|
90
|
-
async
|
|
119
|
+
async _apiRequestRaw(url, options = {}, autoRelogin = true) {
|
|
91
120
|
if (this.debug)
|
|
92
|
-
console.log(`EASYFM DEBUG: ${
|
|
93
|
-
if (
|
|
121
|
+
console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof Request ? url.url : url}`);
|
|
122
|
+
if (this.token === '')
|
|
94
123
|
await this.login(true);
|
|
95
124
|
if (!options.headers)
|
|
96
125
|
options.headers = {};
|
|
97
|
-
options.headers
|
|
98
|
-
options.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
options.headers.authorization = 'Bearer ' + this._token;
|
|
127
|
+
// options.redirect = "manual"
|
|
128
|
+
// options.headers.cookies = this._generateCookieHeader()
|
|
129
|
+
// console.log(options.headers.cookies)
|
|
130
|
+
// options.rejectUnauthorized = this.host.verify
|
|
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()
|
|
106
145
|
if (_fetch.status === 401 && autoRelogin) {
|
|
146
|
+
console.trace();
|
|
107
147
|
await this.login(true);
|
|
108
|
-
return await this.
|
|
148
|
+
return await this._apiRequestRaw(url, options, false);
|
|
109
149
|
}
|
|
110
|
-
|
|
150
|
+
else
|
|
151
|
+
return _fetch;
|
|
111
152
|
}
|
|
112
|
-
|
|
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("; ");
|
|
159
|
+
}
|
|
160
|
+
async _apiRequestJSON(url, options = {}) {
|
|
113
161
|
if (!options.headers)
|
|
114
162
|
options.headers = {};
|
|
115
|
-
options.headers[
|
|
116
|
-
|
|
117
|
-
|
|
163
|
+
options.headers['content-type'] = options.headers['content-type'] ? options.headers['content-type'] : 'application/json';
|
|
164
|
+
const _fetch = await this._apiRequestRaw(url, options);
|
|
165
|
+
const data = await _fetch.json();
|
|
118
166
|
// Remove response if it is empty. This makes checking for an empty response easier
|
|
119
167
|
if (data.response && Object.keys(data.response).length === 0)
|
|
120
168
|
delete data.response;
|
|
121
169
|
// console.log(data.messages[0])
|
|
122
|
-
if (data.messages[0].code !==
|
|
170
|
+
if (data.messages[0].code !== '0') {
|
|
123
171
|
throw new FMError(data.messages[0].code, _fetch.status, data);
|
|
124
172
|
}
|
|
125
173
|
data.httpStatus = _fetch.status;
|
|
126
174
|
return data;
|
|
127
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Retrieves a list of layouts in the current FileMaker database.
|
|
178
|
+
*
|
|
179
|
+
* @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
|
|
180
|
+
* @throws {FMError} If there was an error retrieving the layouts.
|
|
181
|
+
*/
|
|
128
182
|
async listLayouts() {
|
|
129
|
-
|
|
183
|
+
const req = await this._apiRequestJSON(`${this.endpoint}/layouts?page=2`);
|
|
130
184
|
if (!req.response)
|
|
131
185
|
throw new FMError(req.messages[0].code, req.httpStatus, req.messages[0].message);
|
|
132
186
|
const cycleLayoutNames = (layouts) => {
|
|
133
187
|
let names = [];
|
|
134
|
-
for (
|
|
188
|
+
for (const layout of layouts) {
|
|
135
189
|
if (layout.folderLayoutNames)
|
|
136
190
|
names = names.concat(cycleLayoutNames(layout.folderLayoutNames));
|
|
137
191
|
else
|
|
@@ -139,49 +193,12 @@ export class Database extends EventEmitter {
|
|
|
139
193
|
}
|
|
140
194
|
return names;
|
|
141
195
|
};
|
|
142
|
-
let layoutNames;
|
|
143
196
|
return cycleLayoutNames(req.response.layouts).map(layout => new Layout(this, layout));
|
|
144
197
|
}
|
|
145
|
-
|
|
198
|
+
layout(name) {
|
|
146
199
|
return new Layout(this, name);
|
|
147
200
|
}
|
|
148
|
-
script(name, parameter =
|
|
149
|
-
return { name, parameter };
|
|
150
|
-
}
|
|
151
|
-
_tokenExpired() {
|
|
152
|
-
this.emit("token_expired");
|
|
153
|
-
}
|
|
154
|
-
streamURL(url) {
|
|
155
|
-
return new Promise((resolve, reject) => {
|
|
156
|
-
if (!url || typeof url !== "string") {
|
|
157
|
-
throw new Error("URL is empty, or has invalid value");
|
|
158
|
-
}
|
|
159
|
-
let headers = {};
|
|
160
|
-
if (Object.keys(this.cookies).length !== 0) {
|
|
161
|
-
headers["Cookie"] = Object.keys(this.cookies)
|
|
162
|
-
.map(key => {
|
|
163
|
-
return key + "=" + this.cookies[key];
|
|
164
|
-
})
|
|
165
|
-
.join("; ");
|
|
166
|
-
}
|
|
167
|
-
const check_for_cookies = (res) => {
|
|
168
|
-
// Check for the 'set-cookie' header. If it exists, remember it and strip it for better security.
|
|
169
|
-
if (res.headers['set-cookie']) {
|
|
170
|
-
for (let cookie of res.headers['set-cookie']) {
|
|
171
|
-
let cookie_split = cookie.split("=");
|
|
172
|
-
this.cookies[cookie_split[0]] = cookie_split[1];
|
|
173
|
-
}
|
|
174
|
-
res.headers['set-cookie'] = undefined;
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
// Automatically switch between the http and https modules, based on which is needed
|
|
178
|
-
(url.startsWith("https") ? HTTP_REDIRECT.https : HTTP_REDIRECT.http).get(url, {
|
|
179
|
-
headers,
|
|
180
|
-
beforeRedirect: (options, res) => check_for_cookies(res)
|
|
181
|
-
}, (res) => {
|
|
182
|
-
check_for_cookies(res);
|
|
183
|
-
resolve(res);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
201
|
+
script(name, parameter = '') {
|
|
202
|
+
return ({ name, parameter });
|
|
186
203
|
}
|
|
187
204
|
}
|
|
@@ -2,33 +2,30 @@
|
|
|
2
2
|
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {EventEmitter} from
|
|
6
|
-
import {generateAuthorizationHeaders} from
|
|
7
|
-
import {FMError} from
|
|
8
|
-
import {LayoutInterface} from
|
|
9
|
-
import {Layout} from
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type GetLayoutReturnType<T extends DatabaseStructure, R extends LayoutInterface | string> = R extends string ? T["layouts"][R] : R
|
|
25
|
-
|
|
5
|
+
import {EventEmitter} from 'events'
|
|
6
|
+
import {generateAuthorizationHeaders} from './generateAuthorizationHeaders.js'
|
|
7
|
+
import {FMError} from '../FMError.js'
|
|
8
|
+
import {type LayoutInterface} from '../layouts/layoutInterface.js'
|
|
9
|
+
import {Layout} from '../layouts/layout.js'
|
|
10
|
+
import {type databaseOptionsBase, type databaseOptionsWithExternalSources, type Script} from '../types.js'
|
|
11
|
+
import {type HostBase} from './HostBase.js'
|
|
12
|
+
import {type DatabaseBase} from './databaseBase.js'
|
|
13
|
+
import {type ApiLayout, type ApiResults} from '../models/apiResults.js'
|
|
14
|
+
import {type DatabaseStructure} from '../databaseStructure.js'
|
|
15
|
+
import fetch, {HeadersInit, RequestInfo, RequestInit, Response} from 'node-fetch';
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
import fetchWithCookies, {CookieJar} from "node-fetch-cookies"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Represents a database connection.
|
|
21
|
+
* @template T - The structure of the database.
|
|
22
|
+
*/
|
|
26
23
|
export class Database<T extends DatabaseStructure> extends EventEmitter implements DatabaseBase {
|
|
27
|
-
private _token:
|
|
28
|
-
readonly host: HostBase
|
|
29
|
-
private connection_details: databaseOptionsWithExternalSources
|
|
30
|
-
private cookies
|
|
31
|
-
readonly name: string
|
|
24
|
+
private _token: string = ''
|
|
25
|
+
readonly host: HostBase
|
|
26
|
+
private readonly connection_details: databaseOptionsWithExternalSources
|
|
27
|
+
private cookies = new CookieJar()
|
|
28
|
+
readonly name: string
|
|
32
29
|
readonly debug: boolean
|
|
33
30
|
|
|
34
31
|
constructor(host: HostBase, conn: databaseOptionsWithExternalSources) {
|
|
@@ -36,12 +33,13 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
36
33
|
this.host = host
|
|
37
34
|
this.name = conn.database
|
|
38
35
|
this.connection_details = conn
|
|
39
|
-
this.debug =
|
|
36
|
+
this.debug = conn.debug ?? false
|
|
40
37
|
}
|
|
41
38
|
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
42
40
|
private generateExternalSourceLogin(data: databaseOptionsBase) {
|
|
43
|
-
if (data.credentials.method ===
|
|
44
|
-
|
|
41
|
+
if (data.credentials.method === 'filemaker') {
|
|
42
|
+
const _data = data.credentials
|
|
45
43
|
return {
|
|
46
44
|
database: data.database,
|
|
47
45
|
username: _data.username,
|
|
@@ -49,57 +47,79 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
49
47
|
}
|
|
50
48
|
}
|
|
51
49
|
else {
|
|
52
|
-
throw
|
|
50
|
+
throw new Error('Not yet supported login method')
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Logs out the user by deleting the current session token.
|
|
56
|
+
* Throws an error if the user is not logged in.
|
|
57
|
+
*
|
|
58
|
+
* @returns {Promise<void>} A promise that resolves with no value once the logout is successful.
|
|
59
|
+
* @throws {Error} Throws an error if the user is not logged in.
|
|
60
|
+
*/
|
|
56
61
|
async logout(): Promise<void> {
|
|
57
|
-
if (
|
|
62
|
+
if (this.token === '') throw new Error('Not logged in')
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
method:
|
|
64
|
+
const _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
|
|
65
|
+
method: 'DELETE',
|
|
61
66
|
headers: {
|
|
62
|
-
|
|
67
|
+
'content-type': 'application/json'
|
|
63
68
|
}
|
|
64
69
|
})
|
|
65
|
-
|
|
70
|
+
await _fetch.json()
|
|
66
71
|
// console.log(data)
|
|
67
|
-
this._token =
|
|
72
|
+
this._token = ''
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Logs in to the database. Not required, as this is often done automatically
|
|
77
|
+
*
|
|
78
|
+
* @param {boolean} [forceLogin=false] - Whether to force login even if already logged in.
|
|
79
|
+
* @throws {Error} - Throws an error if already logged in and forceLogin is false.
|
|
80
|
+
* @throws {FMError} - Throws an FMError if login fails.
|
|
81
|
+
* @return {Promise<string>} - Returns a promise that resolves to the access token upon successful login.
|
|
82
|
+
*/
|
|
70
83
|
async login(forceLogin = false) {
|
|
84
|
+
if (this.token !== '' && !forceLogin) return
|
|
85
|
+
|
|
86
|
+
console.log("RELOGGING IN")
|
|
87
|
+
console.trace()
|
|
71
88
|
// Reset cookies
|
|
72
|
-
this.cookies =
|
|
89
|
+
this.cookies = new CookieJar()
|
|
73
90
|
|
|
74
91
|
await this.host.getMetadata()
|
|
75
|
-
if (this.token && !forceLogin) throw new Error("Already logged in. Run logout() first")
|
|
76
92
|
|
|
77
|
-
if (this.connection_details.credentials.method ===
|
|
78
|
-
this._token = (
|
|
93
|
+
if (this.connection_details.credentials.method === 'token') {
|
|
94
|
+
this._token = (this.connection_details.credentials).token
|
|
79
95
|
return this.token
|
|
80
96
|
}
|
|
81
97
|
else {
|
|
82
98
|
console.time()
|
|
83
|
-
|
|
99
|
+
const url = new URL(`${this.endpoint}/sessions`)
|
|
84
100
|
url.hostname = this.host.hostname
|
|
85
|
-
|
|
86
|
-
method:
|
|
101
|
+
const res = await fetch(url, {
|
|
102
|
+
method: 'POST',
|
|
87
103
|
headers: generateAuthorizationHeaders(this.connection_details.credentials) as unknown as HeadersInit,
|
|
88
104
|
body: JSON.stringify({
|
|
89
105
|
fmDataSource: this.connection_details.externalSources.map(i => {
|
|
90
|
-
|
|
106
|
+
const _i = i
|
|
91
107
|
return this.generateExternalSourceLogin(_i)
|
|
92
108
|
})
|
|
93
109
|
})
|
|
94
110
|
})
|
|
95
111
|
console.time()
|
|
96
|
-
|
|
112
|
+
const _res = (await res.json()) as any
|
|
97
113
|
if (res.status === 200) {
|
|
98
|
-
this._token = res.headers.get('x-fm-data-access-token')
|
|
114
|
+
this._token = res.headers.get('x-fm-data-access-token') ?? ''
|
|
99
115
|
return this._token
|
|
100
116
|
}
|
|
101
117
|
else {
|
|
102
|
-
throw new FMError(
|
|
118
|
+
throw new FMError(
|
|
119
|
+
_res.messages[0].code as string | number,
|
|
120
|
+
_res.status as number,
|
|
121
|
+
res
|
|
122
|
+
)
|
|
103
123
|
}
|
|
104
124
|
}
|
|
105
125
|
}
|
|
@@ -108,43 +128,73 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
108
128
|
return this._token
|
|
109
129
|
}
|
|
110
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Returns the endpoint URL for the database connection.
|
|
133
|
+
*
|
|
134
|
+
* @returns {string} The endpoint URL.
|
|
135
|
+
*/
|
|
111
136
|
get endpoint(): string {
|
|
112
137
|
return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`
|
|
113
138
|
}
|
|
114
139
|
|
|
115
|
-
async
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
async _apiRequestRaw(url: URL | RequestInfo, options: RequestInit & {
|
|
141
|
+
headers?: Record<string, string>,
|
|
142
|
+
useCookieJar?: boolean,
|
|
143
|
+
} = {}, autoRelogin = true): Promise<Response> {
|
|
144
|
+
if (this.debug) console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof Request ? url.url : url}`)
|
|
145
|
+
if (this.token === '') await this.login(true)
|
|
118
146
|
|
|
119
147
|
if (!options.headers) options.headers = {}
|
|
120
|
-
options.headers
|
|
121
|
-
options.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
options.headers.authorization = 'Bearer ' + this._token
|
|
149
|
+
// options.redirect = "manual"
|
|
150
|
+
// options.headers.cookies = this._generateCookieHeader()
|
|
151
|
+
// console.log(options.headers.cookies)
|
|
152
|
+
// options.rejectUnauthorized = this.host.verify
|
|
153
|
+
|
|
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()
|
|
131
169
|
if (_fetch.status === 401 && autoRelogin) {
|
|
170
|
+
console.trace()
|
|
132
171
|
await this.login(true)
|
|
133
|
-
return await this.
|
|
172
|
+
return await this._apiRequestRaw(url, options, false)
|
|
134
173
|
}
|
|
135
|
-
return _fetch
|
|
174
|
+
else return _fetch
|
|
136
175
|
}
|
|
137
|
-
|
|
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("; ")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async _apiRequestJSON<T = any>(url: URL | RequestInfo, options: RequestInit & {
|
|
186
|
+
headers?: Record<string, string>
|
|
187
|
+
} = {}): Promise<ApiResults<T>> {
|
|
138
188
|
if (!options.headers) options.headers = {}
|
|
139
|
-
options.headers[
|
|
140
|
-
|
|
141
|
-
|
|
189
|
+
options.headers['content-type'] = options.headers['content-type'] ? options.headers['content-type'] : 'application/json'
|
|
190
|
+
const _fetch = await this._apiRequestRaw(url, options)
|
|
191
|
+
const data = await _fetch.json() as ApiResults<T>
|
|
142
192
|
|
|
143
193
|
// Remove response if it is empty. This makes checking for an empty response easier
|
|
144
194
|
if (data.response && Object.keys(data.response).length === 0) delete data.response
|
|
145
195
|
|
|
146
196
|
// console.log(data.messages[0])
|
|
147
|
-
if (data.messages[0].code !==
|
|
197
|
+
if (data.messages[0].code !== '0') {
|
|
148
198
|
throw new FMError(data.messages[0].code, _fetch.status, data)
|
|
149
199
|
}
|
|
150
200
|
|
|
@@ -152,72 +202,34 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
152
202
|
return data
|
|
153
203
|
}
|
|
154
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Retrieves a list of layouts in the current FileMaker database.
|
|
207
|
+
*
|
|
208
|
+
* @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
|
|
209
|
+
* @throws {FMError} If there was an error retrieving the layouts.
|
|
210
|
+
*/
|
|
155
211
|
async listLayouts() {
|
|
156
|
-
|
|
212
|
+
const req = await this._apiRequestJSON<{ layouts: ApiLayout[] }>(`${this.endpoint}/layouts?page=2`)
|
|
157
213
|
if (!req.response) throw new FMError(req.messages[0].code, req.httpStatus, req.messages[0].message)
|
|
158
214
|
|
|
159
215
|
const cycleLayoutNames = (layouts: ApiLayout[]) => {
|
|
160
|
-
let names: string[] =
|
|
161
|
-
for (
|
|
216
|
+
let names: string[] = []
|
|
217
|
+
for (const layout of layouts) {
|
|
162
218
|
if (layout.folderLayoutNames) names = names.concat(cycleLayoutNames(layout.folderLayoutNames))
|
|
163
219
|
else names.push(layout.name)
|
|
164
220
|
}
|
|
165
221
|
return names
|
|
166
222
|
}
|
|
167
|
-
let layoutNames
|
|
168
|
-
|
|
169
223
|
return cycleLayoutNames(req.response.layouts).map(layout => new Layout(this, layout))
|
|
170
224
|
}
|
|
171
225
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
226
|
+
layout<R extends keyof T['layouts']>(name: R): Layout<T['layouts'][R]>
|
|
227
|
+
layout<R extends LayoutInterface>(name: string): Layout<R>
|
|
228
|
+
layout(name: string): Layout<any> {
|
|
175
229
|
return new Layout<LayoutInterface>(this, name)
|
|
176
230
|
}
|
|
177
231
|
|
|
178
|
-
script(name: string, parameter =
|
|
179
|
-
return ({name, parameter}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
_tokenExpired() {
|
|
183
|
-
this.emit("token_expired")
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
streamURL(url: string): Promise<IncomingMessage> {
|
|
187
|
-
return new Promise((resolve, reject) => {
|
|
188
|
-
if (!url || typeof url !== "string") {
|
|
189
|
-
throw new Error("URL is empty, or has invalid value")
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
let headers: {[key: string]: string} = {}
|
|
193
|
-
if (Object.keys(this.cookies).length !== 0) {
|
|
194
|
-
headers["Cookie"] = Object.keys(this.cookies)
|
|
195
|
-
.map(key => {
|
|
196
|
-
return key + "=" + this.cookies[key]
|
|
197
|
-
})
|
|
198
|
-
.join("; ")
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const check_for_cookies = (res: HTTP_REDIRECT.ResponseDetails) => {
|
|
202
|
-
// Check for the 'set-cookie' header. If it exists, remember it and strip it for better security.
|
|
203
|
-
if (res.headers['set-cookie']) {
|
|
204
|
-
for (let cookie of res.headers['set-cookie']) {
|
|
205
|
-
let cookie_split = cookie.split("=")
|
|
206
|
-
this.cookies[cookie_split[0]] = cookie_split[1]
|
|
207
|
-
}
|
|
208
|
-
res.headers['set-cookie'] = undefined
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Automatically switch between the http and https modules, based on which is needed
|
|
213
|
-
(url.startsWith("https") ? HTTP_REDIRECT.https : HTTP_REDIRECT.http).get(url, {
|
|
214
|
-
headers,
|
|
215
|
-
beforeRedirect: (options: any, res: any) => check_for_cookies(res)
|
|
216
|
-
}, (res) => {
|
|
217
|
-
check_for_cookies(res)
|
|
218
|
-
|
|
219
|
-
resolve(res)
|
|
220
|
-
})
|
|
221
|
-
})
|
|
232
|
+
script(name: string, parameter = ''): Script {
|
|
233
|
+
return ({name, parameter} satisfies Script)
|
|
222
234
|
}
|
|
223
|
-
}
|
|
235
|
+
}
|