@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.
Files changed (64) hide show
  1. package/package.json +16 -7
  2. package/src/FMError.d.ts +2 -2
  3. package/src/FMError.js +507 -508
  4. package/src/FMError.ts +510 -511
  5. package/src/connection/FMHost.d.ts +23 -5
  6. package/src/connection/FMHost.js +42 -26
  7. package/src/connection/FMHost.ts +63 -49
  8. package/src/connection/HostBase.d.ts +3 -3
  9. package/src/connection/HostBase.ts +5 -5
  10. package/src/connection/database.d.ts +53 -20
  11. package/src/connection/database.js +107 -90
  12. package/src/connection/database.ts +133 -121
  13. package/src/connection/databaseBase.d.ts +11 -7
  14. package/src/connection/databaseBase.ts +8 -8
  15. package/src/connection/generateAuthorizationHeaders.d.ts +1 -1
  16. package/src/connection/generateAuthorizationHeaders.js +10 -10
  17. package/src/connection/generateAuthorizationHeaders.ts +21 -21
  18. package/src/databaseStructure.d.ts +2 -4
  19. package/src/databaseStructure.ts +3 -5
  20. package/src/index.d.ts +17 -17
  21. package/src/index.js +13 -13
  22. package/src/index.ts +24 -24
  23. package/src/layouts/layout.d.ts +17 -6
  24. package/src/layouts/layout.js +19 -8
  25. package/src/layouts/layout.ts +33 -23
  26. package/src/layouts/layoutBase.d.ts +5 -5
  27. package/src/layouts/layoutBase.ts +6 -6
  28. package/src/layouts/layoutInterface.d.ts +3 -5
  29. package/src/layouts/layoutInterface.ts +4 -6
  30. package/src/layouts/layoutRecordManager.d.ts +29 -6
  31. package/src/layouts/layoutRecordManager.js +32 -9
  32. package/src/layouts/layoutRecordManager.ts +42 -19
  33. package/src/layouts/recordFieldsMap.d.ts +2 -4
  34. package/src/layouts/recordFieldsMap.ts +2 -4
  35. package/src/models/apiResults.d.ts +7 -13
  36. package/src/models/apiResults.ts +58 -58
  37. package/src/records/field.d.ts +39 -17
  38. package/src/records/field.js +248 -131
  39. package/src/records/field.ts +160 -82
  40. package/src/records/getOperations/recordGetOperation.d.ts +44 -24
  41. package/src/records/getOperations/recordGetOperation.js +88 -61
  42. package/src/records/getOperations/recordGetOperation.ts +128 -108
  43. package/src/records/layoutRecord.d.ts +28 -14
  44. package/src/records/layoutRecord.js +69 -60
  45. package/src/records/layoutRecord.ts +121 -119
  46. package/src/records/layoutRecordBase.d.ts +2 -2
  47. package/src/records/layoutRecordBase.ts +3 -3
  48. package/src/records/portal.d.ts +19 -5
  49. package/src/records/portal.js +19 -5
  50. package/src/records/portal.ts +28 -16
  51. package/src/records/portalBase.d.ts +5 -5
  52. package/src/records/portalBase.ts +6 -6
  53. package/src/records/portalRecord.d.ts +16 -8
  54. package/src/records/portalRecord.js +16 -6
  55. package/src/records/portalRecord.ts +29 -19
  56. package/src/records/recordBase.d.ts +18 -8
  57. package/src/records/recordBase.js +23 -16
  58. package/src/records/recordBase.ts +41 -37
  59. package/src/types.d.ts +23 -30
  60. package/src/types.js +0 -5
  61. package/src/types.ts +55 -64
  62. package/src/utils/query.d.ts +7 -7
  63. package/src/utils/query.js +10 -10
  64. 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 "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";
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 = !!conn.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 === "filemaker") {
25
- let _data = data.credentials;
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 "Not yet supported login method";
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 (!this.token)
38
- throw new Error("Not logged in");
39
- let _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
40
- method: "DELETE",
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
- "content-type": "application/json"
56
+ 'content-type': 'application/json'
43
57
  }
44
58
  });
45
- let data = await _fetch.json();
59
+ await _fetch.json();
46
60
  // console.log(data)
47
- this._token = null;
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.token && !forceLogin)
54
- throw new Error("Already logged in. Run logout() first");
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
- let url = new URL(`${this.endpoint}/sessions`);
85
+ const url = new URL(`${this.endpoint}/sessions`);
62
86
  url.hostname = this.host.hostname;
63
- let res = await fetch(url, {
64
- method: "POST",
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
- let _i = i;
92
+ const _i = i;
69
93
  return this.generateExternalSourceLogin(_i);
70
94
  })
71
95
  })
72
96
  });
73
97
  console.time();
74
- let _res = (await res.json());
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 apiRequestRaw(url, options = {}, autoRelogin = true) {
119
+ async _apiRequestRaw(url, options = {}, autoRelogin = true) {
91
120
  if (this.debug)
92
- console.log(`EASYFM DEBUG: ${options.method?.toString().toUpperCase() || "GET"} ${url instanceof Request ? url.url : url}`);
93
- if (!this.token)
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["authorization"] = "Bearer " + this._token;
98
- options.rejectUnauthorized = this.host.verify;
99
- let _fetch = await fetch(url, options);
100
- if (_fetch.headers.get('set-cookie')) {
101
- for (let cookie of _fetch.headers.get('set-cookie') || []) {
102
- let cookie_split = cookie.split("=");
103
- this.cookies[cookie_split[0]] = cookie_split[1];
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.apiRequestRaw(url, options, false);
148
+ return await this._apiRequestRaw(url, options, false);
109
149
  }
110
- return _fetch;
150
+ else
151
+ return _fetch;
111
152
  }
112
- async apiRequestJSON(url, options = {}) {
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["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json";
116
- let _fetch = await this.apiRequestRaw(url, options);
117
- let data = await _fetch.json();
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 !== "0") {
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
- let req = await this.apiRequestJSON(`${this.endpoint}/layouts?page=2`);
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 (let layout of layouts) {
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
- getLayout(name) {
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 "events";
6
- import {generateAuthorizationHeaders} from "./generateAuthorizationHeaders.js";
7
- import {FMError} from "../FMError.js";
8
- import {LayoutInterface} from "../layouts/layoutInterface.js";
9
- import {Layout} from "../layouts/layout.js";
10
- import HTTP_REDIRECT from "follow-redirects"
11
- import {
12
- databaseOptionsBase,
13
- databaseOptionsWithExternalSources,
14
- loginOptionsFileMaker,
15
- loginOptionsToken,
16
- Script
17
- } from "../types.js";
18
- import {HostBase} from "./HostBase.js"
19
- import {DatabaseBase} from "./databaseBase.js";
20
- import {ApiLayout, ApiResults} from "../models/apiResults.js";
21
- import {DatabaseStructure} from "../databaseStructure.js";
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 {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: any;
28
- readonly host: HostBase;
29
- private connection_details: databaseOptionsWithExternalSources
30
- private cookies: { [key: string]: string } = {}
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 = !!conn.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 === "filemaker") {
44
- let _data = <loginOptionsFileMaker>data.credentials
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 "Not yet supported login method"
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 (!this.token) throw new Error("Not logged in")
62
+ if (this.token === '') throw new Error('Not logged in')
58
63
 
59
- let _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
60
- method: "DELETE",
64
+ const _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
65
+ method: 'DELETE',
61
66
  headers: {
62
- "content-type": "application/json"
67
+ 'content-type': 'application/json'
63
68
  }
64
69
  })
65
- let data = await _fetch.json()
70
+ await _fetch.json()
66
71
  // console.log(data)
67
- this._token = null
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 === "token") {
78
- this._token = (<loginOptionsToken>this.connection_details.credentials).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
- let url = new URL(`${this.endpoint}/sessions`)
99
+ const url = new URL(`${this.endpoint}/sessions`)
84
100
  url.hostname = this.host.hostname
85
- let res = await fetch(url, {
86
- method: "POST",
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
- let _i = <databaseOptionsBase>i
106
+ const _i = i
91
107
  return this.generateExternalSourceLogin(_i)
92
108
  })
93
109
  })
94
110
  })
95
111
  console.time()
96
- let _res = <any>(await res.json())
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(_res.messages[0].code, _res.status, res)
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 apiRequestRaw(url: string | Request, options: any = {}, autoRelogin = true): Promise<Response> {
116
- if (this.debug) console.log(`EASYFM DEBUG: ${options.method?.toString().toUpperCase() || "GET"} ${url instanceof Request ? url.url : url}`)
117
- if (!this.token) await this.login(true)
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["authorization"] = "Bearer " + this._token
121
- options.rejectUnauthorized = this.host.verify
122
-
123
- let _fetch = await fetch(url, options)
124
- if (_fetch.headers.get('set-cookie')) {
125
- for (let cookie of _fetch.headers.get('set-cookie') || []) {
126
- let cookie_split = cookie.split("=")
127
- this.cookies[cookie_split[0]] = cookie_split[1]
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.apiRequestRaw(url, options, false)
172
+ return await this._apiRequestRaw(url, options, false)
134
173
  }
135
- return _fetch
174
+ else return _fetch
136
175
  }
137
- async apiRequestJSON<T = any>(url: string | Request, options: any = {}): Promise<ApiResults<T>> {
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["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json"
140
- let _fetch = await this.apiRequestRaw(url, options)
141
- let data = await _fetch.json() as ApiResults<T>
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 !== "0") {
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
- let req = await this.apiRequestJSON<{layouts: ApiLayout[]}>(`${this.endpoint}/layouts?page=2`)
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 (let layout of layouts) {
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
- getLayout<R extends keyof T["layouts"]>(name: R): Layout<T["layouts"][R]>
173
- getLayout<R extends LayoutInterface>(name: string): Layout<R>
174
- getLayout(name: string): Layout<any> {
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 = ""): Script {
179
- return ({name, parameter} as Script)
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
+ }