@jd-data-limited/easy-fm 4.0.14 → 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/bin/cli.js +7 -5
- package/src/bin/generateTypes.js +215 -121
- package/src/connection/FMHost.d.ts +28 -6
- package/src/connection/FMHost.js +52 -25
- package/src/connection/FMHost.ts +75 -43
- package/src/connection/HostBase.d.ts +7 -3
- package/src/connection/HostBase.ts +8 -4
- 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 +14 -14
- package/src/index.ts +28 -25
- 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 +89 -58
- package/src/records/getOperations/recordGetOperation.ts +137 -108
- package/src/records/layoutRecord.d.ts +28 -14
- package/src/records/layoutRecord.js +75 -65
- package/src/records/layoutRecord.ts +126 -123
- 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 +32 -22
- package/src/records/recordBase.ts +50 -43
- package/src/types.d.ts +21 -24
- package/src/types.ts +55 -59
- package/src/utils/query.d.ts +12 -5
- package/src/utils/query.js +28 -9
- package/src/utils/query.ts +40 -15
package/src/connection/FMHost.ts
CHANGED
|
@@ -2,88 +2,120 @@
|
|
|
2
2
|
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {generateAuthorizationHeaders} from
|
|
6
|
-
import {FMError} from
|
|
7
|
-
import {Database} from
|
|
8
|
-
import {HostBase} from
|
|
5
|
+
import {generateAuthorizationHeaders} from './generateAuthorizationHeaders.js'
|
|
6
|
+
import {FMError} from '../FMError.js'
|
|
7
|
+
import {Database} from './database.js'
|
|
8
|
+
import {type HostBase} from './HostBase.js'
|
|
9
9
|
import {
|
|
10
|
-
databaseOptionsWithExternalSources,
|
|
11
|
-
FMHostMetadata,
|
|
12
|
-
loginOptionsClaris,
|
|
13
|
-
loginOptionsFileMaker,
|
|
14
|
-
loginOptionsOAuth
|
|
15
|
-
} from
|
|
16
|
-
import {ApiResults} from
|
|
17
|
-
import {DatabaseStructure} from
|
|
10
|
+
type databaseOptionsWithExternalSources,
|
|
11
|
+
type FMHostMetadata,
|
|
12
|
+
type loginOptionsClaris,
|
|
13
|
+
type loginOptionsFileMaker,
|
|
14
|
+
type loginOptionsOAuth
|
|
15
|
+
} from '../types.js'
|
|
16
|
+
import {type ApiResults} from '../models/apiResults.js'
|
|
17
|
+
import {type DatabaseStructure} from '../databaseStructure.js'
|
|
18
|
+
import {type Moment} from 'moment'
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Represents a FileMaker host.
|
|
22
|
+
* @implements {HostBase}
|
|
23
|
+
*/
|
|
19
24
|
export default class FMHost implements HostBase {
|
|
20
25
|
readonly hostname: string
|
|
21
|
-
readonly
|
|
26
|
+
readonly timezoneOffsetFunc: (moment: Moment) => number
|
|
22
27
|
readonly verify: boolean
|
|
23
28
|
_metadata: FMHostMetadata | null = null
|
|
24
29
|
|
|
25
|
-
constructor
|
|
26
|
-
|
|
30
|
+
constructor (
|
|
31
|
+
_hostname: string,
|
|
32
|
+
timezoneOffset = (moment: Moment) => 0 - (new Date()).getTimezoneOffset(),
|
|
33
|
+
verify = true
|
|
34
|
+
) {
|
|
35
|
+
if (!(/^https?:\/\//).test(_hostname)) throw new Error('hostname MUST begin with either http:// or https://')
|
|
27
36
|
this.hostname = _hostname
|
|
28
|
-
this.
|
|
37
|
+
this.timezoneOffsetFunc = timezoneOffset
|
|
29
38
|
this.verify = verify
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
get metadata(): FMHostMetadata {
|
|
33
|
-
return this._metadata
|
|
41
|
+
get metadata (): FMHostMetadata {
|
|
42
|
+
return this._metadata ?? {
|
|
34
43
|
productInfo: {
|
|
35
44
|
buildDate: new Date(),
|
|
36
|
-
name:
|
|
37
|
-
version:
|
|
38
|
-
dateFormat:
|
|
39
|
-
timeFormat:
|
|
40
|
-
timeStampFormat:
|
|
45
|
+
name: '',
|
|
46
|
+
version: '',
|
|
47
|
+
dateFormat: 'MM/dd/yyyy',
|
|
48
|
+
timeFormat: 'HH:mm:ss',
|
|
49
|
+
timeStampFormat: 'MM/dd/yyyy HH:mm:ss'
|
|
41
50
|
}
|
|
42
|
-
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get dateFormat () {
|
|
55
|
+
return this.metadata.productInfo.dateFormat
|
|
56
|
+
.replace('dd', 'DD')
|
|
57
|
+
.replace('yyyy', 'YYYY')
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
get timeFormat () { return this.metadata.productInfo.timeFormat }
|
|
61
|
+
get timeStampFormat () {
|
|
62
|
+
return this.metadata.productInfo.timeStampFormat
|
|
63
|
+
.replace('dd', 'DD')
|
|
64
|
+
.replace('yyyy', 'YYYY')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Retrieves a list of databases from the FileMaker Server.
|
|
69
|
+
*
|
|
70
|
+
* @param {loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris} [credentials] - Optional credentials required for authentication.
|
|
71
|
+
* @throws {FMError} If the request to the FileMaker Server fails or if the response contains an error.
|
|
72
|
+
* @returns {Promise<any[]>} A promise that resolves to an array of database objects if successful.
|
|
73
|
+
*/
|
|
74
|
+
async listDatabases (credentials?: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris) {
|
|
46
75
|
let headers = {}
|
|
47
76
|
if (credentials) {
|
|
48
77
|
headers = generateAuthorizationHeaders(credentials)
|
|
49
78
|
}
|
|
50
79
|
|
|
51
|
-
|
|
52
|
-
method:
|
|
80
|
+
const _fetch = await fetch(`${this.hostname}/fmi/data/v2/databases`, {
|
|
81
|
+
method: 'GET',
|
|
53
82
|
headers
|
|
54
83
|
})
|
|
55
|
-
|
|
84
|
+
const data = await _fetch.json() as ApiResults<{ databases: any }>
|
|
56
85
|
// console.log(data.messages[0])
|
|
57
86
|
|
|
58
|
-
if (data.messages[0].code ===
|
|
87
|
+
if (data.messages[0].code === '0' && data.response) {
|
|
59
88
|
return data.response.databases
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// @ts-ignore
|
|
63
|
-
throw new FMError(data.messages[0].code, data.status, data)
|
|
89
|
+
} else {
|
|
90
|
+
throw new FMError(data.messages[0].code, _fetch.status, data)
|
|
64
91
|
}
|
|
65
92
|
}
|
|
66
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Creates a new database connection with the specified options.
|
|
96
|
+
*
|
|
97
|
+
* @template T - The type of the database structure.
|
|
98
|
+
* @param {databaseOptionsWithExternalSources} data - The options for the database, including external sources.
|
|
99
|
+
* @return {Database<T>} A new Database instance.
|
|
100
|
+
*/
|
|
67
101
|
database<T extends DatabaseStructure>(data: databaseOptionsWithExternalSources) {
|
|
68
102
|
return new Database<T>(this, data)
|
|
69
103
|
}
|
|
70
104
|
|
|
71
|
-
async getMetadata() {
|
|
105
|
+
async getMetadata () {
|
|
72
106
|
if (this._metadata) return this._metadata
|
|
73
107
|
|
|
74
|
-
|
|
75
|
-
method:
|
|
108
|
+
const _fetch = await fetch(`${this.hostname}/fmi/data/v2/productInfo`, {
|
|
109
|
+
method: 'GET'
|
|
76
110
|
})
|
|
77
|
-
|
|
111
|
+
const data = await _fetch.json() as ApiResults<FMHostMetadata>
|
|
78
112
|
// console.log(data.messages[0])
|
|
79
113
|
|
|
80
|
-
if (data.messages[0].code ===
|
|
114
|
+
if (data.messages[0].code === '0' && data.response) {
|
|
81
115
|
this._metadata = data.response
|
|
82
116
|
return data.response
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// @ts-ignore
|
|
86
|
-
throw new FMError(data.messages[0].code, data.status, data)
|
|
117
|
+
} else {
|
|
118
|
+
throw new FMError(data.messages[0].code, _fetch.status, data)
|
|
87
119
|
}
|
|
88
120
|
}
|
|
89
|
-
}
|
|
121
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { FMHostMetadata } from
|
|
1
|
+
import { type FMHostMetadata } from '../types.js';
|
|
2
|
+
import { type Moment } from 'moment';
|
|
2
3
|
export interface HostBase {
|
|
3
4
|
readonly hostname: string;
|
|
4
|
-
readonly
|
|
5
|
+
readonly timezoneOffsetFunc: (moment: Moment) => number;
|
|
5
6
|
readonly verify: boolean;
|
|
6
7
|
metadata: FMHostMetadata;
|
|
7
|
-
getMetadata()
|
|
8
|
+
getMetadata: () => PromiseLike<any>;
|
|
9
|
+
timeFormat: string;
|
|
10
|
+
dateFormat: string;
|
|
11
|
+
timeStampFormat: string;
|
|
8
12
|
}
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {FMHostMetadata} from
|
|
5
|
+
import {type FMHostMetadata} from '../types.js'
|
|
6
|
+
import {type Moment} from 'moment'
|
|
6
7
|
|
|
7
8
|
export interface HostBase {
|
|
8
9
|
readonly hostname: string
|
|
9
|
-
readonly
|
|
10
|
+
readonly timezoneOffsetFunc: (moment: Moment) => number
|
|
10
11
|
readonly verify: boolean
|
|
11
12
|
metadata: FMHostMetadata
|
|
12
|
-
getMetadata()
|
|
13
|
-
|
|
13
|
+
getMetadata: () => PromiseLike<any>
|
|
14
|
+
timeFormat: string
|
|
15
|
+
dateFormat: string
|
|
16
|
+
timeStampFormat: string
|
|
17
|
+
}
|
|
@@ -1,33 +1,66 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
2
|
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
-
import { EventEmitter } from
|
|
4
|
-
import { LayoutInterface } from
|
|
5
|
-
import { Layout } from
|
|
6
|
-
import { databaseOptionsWithExternalSources, Script } from
|
|
7
|
-
import { HostBase } from
|
|
8
|
-
import { DatabaseBase } from
|
|
9
|
-
import { ApiResults } from
|
|
10
|
-
import { DatabaseStructure } from
|
|
11
|
-
import { IncomingMessage } from
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
import { type LayoutInterface } from '../layouts/layoutInterface.js';
|
|
5
|
+
import { Layout } from '../layouts/layout.js';
|
|
6
|
+
import { type databaseOptionsWithExternalSources, type Script } from '../types.js';
|
|
7
|
+
import { type HostBase } from './HostBase.js';
|
|
8
|
+
import { type DatabaseBase } from './databaseBase.js';
|
|
9
|
+
import { type ApiResults } from '../models/apiResults.js';
|
|
10
|
+
import { type DatabaseStructure } from '../databaseStructure.js';
|
|
11
|
+
import { type IncomingMessage } from 'http';
|
|
12
|
+
/**
|
|
13
|
+
* Represents a database connection.
|
|
14
|
+
* @template T - The structure of the database.
|
|
15
|
+
*/
|
|
12
16
|
export declare class Database<T extends DatabaseStructure> extends EventEmitter implements DatabaseBase {
|
|
13
17
|
private _token;
|
|
14
18
|
readonly host: HostBase;
|
|
15
|
-
private connection_details;
|
|
19
|
+
private readonly connection_details;
|
|
16
20
|
private cookies;
|
|
17
21
|
readonly name: string;
|
|
18
22
|
readonly debug: boolean;
|
|
19
23
|
constructor(host: HostBase, conn: databaseOptionsWithExternalSources);
|
|
20
24
|
private generateExternalSourceLogin;
|
|
25
|
+
/**
|
|
26
|
+
* Logs out the user by deleting the current session token.
|
|
27
|
+
* Throws an error if the user is not logged in.
|
|
28
|
+
*
|
|
29
|
+
* @returns {Promise<void>} A promise that resolves with no value once the logout is successful.
|
|
30
|
+
* @throws {Error} Throws an error if the user is not logged in.
|
|
31
|
+
*/
|
|
21
32
|
logout(): Promise<void>;
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Logs in to the database. Not required, as this is often done automatically
|
|
35
|
+
*
|
|
36
|
+
* @param {boolean} [forceLogin=false] - Whether to force login even if already logged in.
|
|
37
|
+
* @throws {Error} - Throws an error if already logged in and forceLogin is false.
|
|
38
|
+
* @throws {FMError} - Throws an FMError if login fails.
|
|
39
|
+
* @return {Promise<string>} - Returns a promise that resolves to the access token upon successful login.
|
|
40
|
+
*/
|
|
41
|
+
login(forceLogin?: boolean): Promise<string>;
|
|
42
|
+
get token(): string;
|
|
43
|
+
/**
|
|
44
|
+
* Returns the endpoint URL for the database connection.
|
|
45
|
+
*
|
|
46
|
+
* @returns {string} The endpoint URL.
|
|
47
|
+
*/
|
|
24
48
|
get endpoint(): string;
|
|
25
|
-
|
|
26
|
-
|
|
49
|
+
_apiRequestRaw(url: string | Request, options?: RequestInit & {
|
|
50
|
+
headers?: Record<string, string>;
|
|
51
|
+
}, autoRelogin?: boolean): Promise<Response>;
|
|
52
|
+
_apiRequestJSON<T = any>(url: string | Request, options?: RequestInit & {
|
|
53
|
+
headers?: Record<string, string>;
|
|
54
|
+
}): Promise<ApiResults<T>>;
|
|
55
|
+
/**
|
|
56
|
+
* Retrieves a list of layouts in the current FileMaker database.
|
|
57
|
+
*
|
|
58
|
+
* @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
|
|
59
|
+
* @throws {FMError} If there was an error retrieving the layouts.
|
|
60
|
+
*/
|
|
27
61
|
listLayouts(): Promise<Layout<LayoutInterface>[]>;
|
|
28
|
-
|
|
29
|
-
|
|
62
|
+
layout<R extends keyof T['layouts']>(name: R): Layout<T['layouts'][R]>;
|
|
63
|
+
layout<R extends LayoutInterface>(name: string): Layout<R>;
|
|
30
64
|
script(name: string, parameter?: string): Script;
|
|
31
|
-
_tokenExpired(): void;
|
|
32
65
|
streamURL(url: string): Promise<IncomingMessage>;
|
|
33
66
|
}
|
|
@@ -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
|
});
|