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