@jd-data-limited/easy-fm 4.1.3 → 4.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jd-data-limited/easy-fm",
3
- "version": "4.1.3",
3
+ "version": "4.1.4",
4
4
  "dependencies": {
5
5
  "@types/follow-redirects": "^1.14.4",
6
6
  "follow-redirects": "^1.15.5",
@@ -11,6 +11,7 @@ export default class FMHost implements HostBase {
11
11
  readonly hostname: string;
12
12
  readonly timezoneOffsetFunc: (moment: Moment) => number;
13
13
  readonly verify: boolean;
14
+ readonly protocol: 'http:' | 'https:';
14
15
  _metadata: FMHostMetadata | null;
15
16
  constructor(_hostname: string, timezoneOffset?: (moment: Moment) => number, verify?: boolean);
16
17
  get metadata(): FMHostMetadata;
@@ -12,11 +12,13 @@ export default class FMHost {
12
12
  hostname;
13
13
  timezoneOffsetFunc;
14
14
  verify;
15
+ protocol;
15
16
  _metadata = null;
16
17
  constructor(_hostname, timezoneOffset = (moment) => 0 - (new Date()).getTimezoneOffset(), verify = true) {
17
18
  if (!(/^https?:\/\//).test(_hostname))
18
19
  throw new Error('hostname MUST begin with either http:// or https://');
19
- this.hostname = _hostname;
20
+ this.protocol = _hostname.startsWith('https:') ? 'https:' : 'http:';
21
+ this.hostname = _hostname.split('//')[1];
20
22
  this.timezoneOffsetFunc = timezoneOffset;
21
23
  this.verify = verify;
22
24
  }
@@ -81,7 +83,7 @@ export default class FMHost {
81
83
  async getMetadata() {
82
84
  if (this._metadata)
83
85
  return this._metadata;
84
- const _fetch = await fetch(`${this.hostname}/fmi/data/v2/productInfo`, {
86
+ const _fetch = await fetch(`${this.protocol}//${this.hostname}/fmi/data/v2/productInfo`, {
85
87
  method: 'GET'
86
88
  });
87
89
  const data = await _fetch.json();
@@ -25,6 +25,7 @@ export default class FMHost implements HostBase {
25
25
  readonly hostname: string
26
26
  readonly timezoneOffsetFunc: (moment: Moment) => number
27
27
  readonly verify: boolean
28
+ readonly protocol: 'http:' | 'https:'
28
29
  _metadata: FMHostMetadata | null = null
29
30
 
30
31
  constructor (
@@ -33,7 +34,8 @@ export default class FMHost implements HostBase {
33
34
  verify = true
34
35
  ) {
35
36
  if (!(/^https?:\/\//).test(_hostname)) throw new Error('hostname MUST begin with either http:// or https://')
36
- this.hostname = _hostname
37
+ this.protocol = _hostname.startsWith('https:') ? 'https:' : 'http:'
38
+ this.hostname = _hostname.split('//')[1]
37
39
  this.timezoneOffsetFunc = timezoneOffset
38
40
  this.verify = verify
39
41
  }
@@ -105,7 +107,7 @@ export default class FMHost implements HostBase {
105
107
  async getMetadata () {
106
108
  if (this._metadata) return this._metadata
107
109
 
108
- const _fetch = await fetch(`${this.hostname}/fmi/data/v2/productInfo`, {
110
+ const _fetch = await fetch(`${this.protocol}//${this.hostname}/fmi/data/v2/productInfo`, {
109
111
  method: 'GET'
110
112
  })
111
113
  const data = await _fetch.json() as ApiResults<FMHostMetadata>
@@ -2,6 +2,7 @@ import { type FMHostMetadata } from '../types.js';
2
2
  import { type Moment } from 'moment';
3
3
  export interface HostBase {
4
4
  readonly hostname: string;
5
+ readonly protocol: string;
5
6
  readonly timezoneOffsetFunc: (moment: Moment) => number;
6
7
  readonly verify: boolean;
7
8
  metadata: FMHostMetadata;
@@ -7,6 +7,7 @@ import {type Moment} from 'moment'
7
7
 
8
8
  export interface HostBase {
9
9
  readonly hostname: string
10
+ readonly protocol: string
10
11
  readonly timezoneOffsetFunc: (moment: Moment) => number
11
12
  readonly verify: boolean
12
13
  metadata: FMHostMetadata
@@ -48,8 +48,8 @@ export declare class Database<T extends DatabaseStructure> extends EventEmitter
48
48
  _apiRequestRaw(url: URL | RequestInfo, options?: RequestInit & {
49
49
  headers?: Record<string, string>;
50
50
  useCookieJar?: boolean;
51
+ retries?: number;
51
52
  }, autoRelogin?: boolean): Promise<Response>;
52
- private _generateCookieHeader;
53
53
  _apiRequestJSON<T = any>(url: URL | RequestInfo, options?: RequestInit & {
54
54
  headers?: Record<string, string>;
55
55
  }): Promise<ApiResults<T>>;
@@ -6,7 +6,7 @@ import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js'
6
6
  import { FMError } from '../FMError.js';
7
7
  import { Layout } from '../layouts/layout.js';
8
8
  import fetch from 'node-fetch';
9
- // @ts-expect-error
9
+ // @ts-expect-error - fetchWithCookies does not have available typescript types
10
10
  import fetchWithCookies, { CookieJar } from 'node-fetch-cookies';
11
11
  /**
12
12
  * Represents a database connection.
@@ -109,16 +109,25 @@ export class Database extends EventEmitter {
109
109
  * @returns {string} The endpoint URL.
110
110
  */
111
111
  get endpoint() {
112
- return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
112
+ return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
113
113
  }
114
114
  async _apiRequestRaw(url, options = {}, autoRelogin = true) {
115
- if (this.debug)
116
- console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof Request ? url.url : url}`);
117
- if (this.token === '')
115
+ if (this.debug) {
116
+ console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof URL
117
+ ? url.toString()
118
+ : typeof url === 'string' ? url : url.url}`);
119
+ }
120
+ const reqIsToDBHost = (url instanceof URL
121
+ ? url
122
+ : typeof url === 'string'
123
+ ? new URL(url)
124
+ : new URL(url.url)).hostname === this.host.hostname;
125
+ if (reqIsToDBHost && this.token === '')
118
126
  await this.login(true);
119
127
  if (!options.headers)
120
128
  options.headers = {};
121
- options.headers.authorization = 'Bearer ' + this._token;
129
+ if (reqIsToDBHost)
130
+ options.headers.authorization = 'Bearer ' + this._token;
122
131
  // options.redirect = "manual"
123
132
  // options.headers.cookies = this._generateCookieHeader()
124
133
  // console.log(options.headers.cookies)
@@ -137,21 +146,21 @@ export class Database extends EventEmitter {
137
146
  // console.log(this.cookies, _fetch.status)
138
147
  // // console.log(this, this.cookies, url)
139
148
  // console.trace()
140
- if (_fetch.status === 401 && autoRelogin) {
141
- console.trace();
149
+ if (!_fetch.ok && (!options.retries || options.retries > 0)) {
150
+ if (this.debug) {
151
+ console.log(`EASYFM DEBUG: RE-ATTEMPTING REQUEST (${_fetch.status}) ${url instanceof URL
152
+ ? url.toString()
153
+ : typeof url === 'string' ? url : url.url}`);
154
+ }
155
+ return await this._apiRequestRaw(url, { ...options, retries: (options?.retries ?? 1) - 1 });
156
+ }
157
+ else if (_fetch.status === 401 && reqIsToDBHost && autoRelogin) {
142
158
  await this.login(true);
143
159
  return await this._apiRequestRaw(url, options, false);
144
160
  }
145
161
  else
146
162
  return _fetch;
147
163
  }
148
- _generateCookieHeader() {
149
- const out = [];
150
- for (const key of Object.keys(this.cookies)) {
151
- out.push(`${key}=${this.cookies[key]}`);
152
- }
153
- return out.join('; ');
154
- }
155
164
  async _apiRequestJSON(url, options = {}) {
156
165
  if (!options.headers)
157
166
  options.headers = {};
@@ -13,7 +13,7 @@ import {type DatabaseBase} from './databaseBase.js'
13
13
  import {type ApiLayout, type ApiResults} from '../models/apiResults.js'
14
14
  import {type DatabaseStructure} from '../databaseStructure.js'
15
15
  import fetch, {type HeadersInit, type RequestInfo, type RequestInit, type Response} from 'node-fetch'
16
- // @ts-expect-error
16
+ // @ts-expect-error - fetchWithCookies does not have available typescript types
17
17
  import fetchWithCookies, {CookieJar} from 'node-fetch-cookies'
18
18
 
19
19
  /**
@@ -45,7 +45,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
45
45
  username: _data.username,
46
46
  password: _data.password
47
47
  }
48
- } else {
48
+ }
49
+ else {
49
50
  throw new Error('Not yet supported login method')
50
51
  }
51
52
  }
@@ -89,7 +90,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
89
90
  if (this.connection_details.credentials.method === 'token') {
90
91
  this._token = (this.connection_details.credentials).token
91
92
  return this.token
92
- } else {
93
+ }
94
+ else {
93
95
  const url = new URL(`${this.endpoint}/sessions`)
94
96
  url.hostname = this.host.hostname
95
97
  const res = await fetch(url, {
@@ -106,7 +108,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
106
108
  if (res.status === 200) {
107
109
  this._token = res.headers.get('x-fm-data-access-token') ?? ''
108
110
  return this._token
109
- } else {
111
+ }
112
+ else {
110
113
  throw new FMError(
111
114
  _res.messages[0].code as string | number,
112
115
  _res.status as number,
@@ -126,18 +129,34 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
126
129
  * @returns {string} The endpoint URL.
127
130
  */
128
131
  get endpoint (): string {
129
- return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`
132
+ return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.name}`
130
133
  }
131
134
 
132
135
  async _apiRequestRaw (url: URL | RequestInfo, options: RequestInit & {
133
136
  headers?: Record<string, string>
134
137
  useCookieJar?: boolean
138
+ retries?: number
135
139
  } = {}, autoRelogin = true): Promise<Response> {
136
- if (this.debug) console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof Request ? url.url : url}`)
137
- if (this.token === '') await this.login(true)
140
+ if (this.debug) {
141
+ console.log(
142
+ `EASYFM DEBUG: ${JSON.stringify(options)} ${
143
+ url instanceof URL
144
+ ? url.toString()
145
+ : typeof url === 'string' ? url : url.url
146
+ }`
147
+ )
148
+ }
149
+ const reqIsToDBHost = (
150
+ url instanceof URL
151
+ ? url
152
+ : typeof url === 'string'
153
+ ? new URL(url)
154
+ : new URL(url.url)
155
+ ).hostname === this.host.hostname
156
+ if (reqIsToDBHost && this.token === '') await this.login(true)
138
157
 
139
158
  if (!options.headers) options.headers = {}
140
- options.headers.authorization = 'Bearer ' + this._token
159
+ if (reqIsToDBHost) options.headers.authorization = 'Bearer ' + this._token
141
160
  // options.redirect = "manual"
142
161
  // options.headers.cookies = this._generateCookieHeader()
143
162
  // console.log(options.headers.cookies)
@@ -158,19 +177,21 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
158
177
  // console.log(this.cookies, _fetch.status)
159
178
  // // console.log(this, this.cookies, url)
160
179
  // console.trace()
161
- if (_fetch.status === 401 && autoRelogin) {
162
- console.trace()
180
+ if (!_fetch.ok && (!options.retries || options.retries > 0)) {
181
+ if (this.debug) {
182
+ console.log(`EASYFM DEBUG: RE-ATTEMPTING REQUEST (${_fetch.status}) ${
183
+ url instanceof URL
184
+ ? url.toString()
185
+ : typeof url === 'string' ? url : url.url
186
+ }`)
187
+ }
188
+ return await this._apiRequestRaw(url, {...options, retries: (options?.retries ?? 1) - 1})
189
+ }
190
+ else if (_fetch.status === 401 && reqIsToDBHost && autoRelogin) {
163
191
  await this.login(true)
164
192
  return await this._apiRequestRaw(url, options, false)
165
- } else return _fetch
166
- }
167
-
168
- private _generateCookieHeader () {
169
- const out: string[] = []
170
- for (const key of Object.keys(this.cookies)) {
171
- out.push(`${key}=${this.cookies[key]}`)
172
193
  }
173
- return out.join('; ')
194
+ else return _fetch
174
195
  }
175
196
 
176
197
  async _apiRequestJSON<T = any>(url: URL | RequestInfo, options: RequestInit & {
@@ -0,0 +1,10 @@
1
+ import { type RequestInfo, type RequestInit, Response } from 'node-fetch';
2
+ export declare class RequestHoldableClass {
3
+ #private;
4
+ protected cookies: any;
5
+ _addNewFuncToQueue(func: () => Promise<void>): void;
6
+ protected _holdableFetch(url: URL | RequestInfo, options: RequestInit & {
7
+ headers?: Record<string, string>;
8
+ }): Promise<Response>;
9
+ }
10
+ export declare function RequestHoldable<PROPS extends any[], R = any>(originalMethod: (...p: PROPS) => Promise<R>, context: ClassMethodDecoratorContext<RequestHoldableClass>): (this: RequestHoldableClass, ...args: PROPS) => Promise<R>;
@@ -0,0 +1,44 @@
1
+ /*
2
+ * Copyright (c) 2024. See LICENSE file for more information
3
+ */
4
+ // @ts-expect-error - NO types module for this
5
+ import fetchWithCookies, { CookieJar } from 'node-fetch-cookies';
6
+ export class RequestHoldableClass {
7
+ #functionQueue = [];
8
+ #requestsAreBeingHeld = false;
9
+ cookies = new CookieJar();
10
+ _addNewFuncToQueue(func) {
11
+ this.#functionQueue.push(func);
12
+ if (this.#functionQueue.length === 1)
13
+ void this.#executeQueue();
14
+ }
15
+ async _holdableFetch(url, options) {
16
+ url.
17
+ if(!this.#requestsAreBeingHeld);
18
+ {
19
+ // eslint-disable-next-line @typescript-eslint/return-await
20
+ return await fetchWithCookies(this.cookies, url, options);
21
+ }
22
+ }
23
+ async #executeQueue() {
24
+ while (this.#functionQueue.length !== 0) {
25
+ try {
26
+ await this.#functionQueue[0]();
27
+ }
28
+ catch (e) {
29
+ console.error(e);
30
+ }
31
+ this.#functionQueue.shift();
32
+ }
33
+ }
34
+ }
35
+ export function RequestHoldable(originalMethod, context) {
36
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
37
+ function replacementMethod(...args) {
38
+ return new Promise((resolve, reject) => {
39
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
40
+ this._addNewFuncToQueue(() => originalMethod.call(this, ...args).then(resolve).catch(reject));
41
+ });
42
+ }
43
+ return replacementMethod;
44
+ }
@@ -50,6 +50,7 @@ export class LayoutRecord extends RecordBase {
50
50
  method: 'POST',
51
51
  body: JSON.stringify(data)
52
52
  });
53
+ console.log(res);
53
54
  if (!res.response) {
54
55
  throw new FMError(res.messages[0].code, res.httpStatus, res);
55
56
  }
@@ -70,6 +70,7 @@ export class LayoutRecord<LAYOUT extends LayoutInterface> extends RecordBase<LAY
70
70
  method: 'POST',
71
71
  body: JSON.stringify(data)
72
72
  })
73
+ console.log(res)
73
74
 
74
75
  if (!res.response) {
75
76
  throw new FMError(res.messages[0].code, res.httpStatus, res)