@jd-data-limited/easy-fm 4.1.15 → 5.0.0-beta.2

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 (58) hide show
  1. package/README.md +60 -196
  2. package/dist/bin/generateTypes.js +3 -3
  3. package/dist/bin/stressSearch.d.ts +2 -0
  4. package/dist/bin/stressSearch.js +243 -0
  5. package/dist/connection/CookieJar.d.ts +19 -0
  6. package/dist/connection/CookieJar.js +99 -0
  7. package/dist/connection/FMHost.d.ts +25 -11
  8. package/dist/connection/FMHost.js +29 -11
  9. package/dist/connection/HostBase.d.ts +4 -4
  10. package/dist/connection/Session.d.ts +34 -0
  11. package/dist/connection/Session.js +109 -0
  12. package/dist/connection/database.d.ts +34 -31
  13. package/dist/connection/database.js +72 -133
  14. package/dist/connection/databaseBase.d.ts +22 -10
  15. package/dist/connection/databaseConstantSession.d.ts +15 -0
  16. package/dist/connection/databaseConstantSession.js +70 -0
  17. package/dist/connection/databaseSessionPool.d.ts +14 -0
  18. package/dist/connection/databaseSessionPool.js +148 -0
  19. package/dist/index.d.ts +24 -4
  20. package/dist/index.js +15 -1
  21. package/dist/layouts/layout.d.ts +5 -3
  22. package/dist/layouts/layout.js +11 -15
  23. package/dist/layouts/layoutBase.d.ts +3 -2
  24. package/dist/layouts/layoutInterface.d.ts +2 -1
  25. package/dist/layouts/layoutRecordManager.d.ts +6 -15
  26. package/dist/layouts/layoutRecordManager.js +6 -15
  27. package/dist/models/apiResults.d.ts +209 -93
  28. package/dist/models/apiResults.js +95 -25
  29. package/dist/records/field.d.ts +17 -2
  30. package/dist/records/field.js +161 -237
  31. package/dist/records/fields/baseField.d.ts +57 -0
  32. package/dist/records/fields/baseField.js +59 -0
  33. package/dist/records/fields/containerField.d.ts +35 -0
  34. package/dist/records/fields/containerField.js +78 -0
  35. package/dist/records/fields/field.d.ts +3 -0
  36. package/dist/records/fields/field.js +1 -0
  37. package/dist/records/fields/valueField.d.ts +41 -0
  38. package/dist/records/fields/valueField.js +108 -0
  39. package/dist/records/getOperations/recordGetOperation.d.ts +7 -4
  40. package/dist/records/getOperations/recordGetOperation.js +22 -26
  41. package/dist/records/layoutRecord.d.ts +27 -9
  42. package/dist/records/layoutRecord.js +84 -97
  43. package/dist/records/portal.d.ts +1 -5
  44. package/dist/records/portal.js +0 -4
  45. package/dist/records/portalBase.d.ts +1 -2
  46. package/dist/records/portalRecord.d.ts +7 -4
  47. package/dist/records/portalRecord.js +12 -0
  48. package/dist/records/recordBase.d.ts +8 -6
  49. package/dist/records/recordBase.js +32 -33
  50. package/dist/types.d.ts +42 -18
  51. package/dist/types.js +12 -0
  52. package/dist/utils/addHeaders.d.ts +5 -0
  53. package/dist/utils/addHeaders.js +24 -0
  54. package/dist/utils/query.d.ts +24 -8
  55. package/dist/utils/query.js +60 -20
  56. package/dist/utils/temporal.d.ts +9 -0
  57. package/dist/utils/temporal.js +110 -0
  58. package/package.json +35 -31
@@ -1,22 +1,24 @@
1
- import { Database } from './database.js';
1
+ import { type Database } from './database.js';
2
2
  import { type HostBase } from './HostBase.js';
3
- import { type databaseOptionsWithExternalSources, type FMHostMetadata, type loginOptionsClaris, type loginOptionsFileMaker, type loginOptionsOAuth } from '../types.js';
3
+ import { type databaseOptionsWithExternalSources, FMHostMetadata, type loginOptionsClaris, type loginOptionsFileMaker, type loginOptionsOAuth } from '../types.js';
4
4
  import { type DatabaseStructure } from '../databaseStructure.js';
5
- import { type Moment } from 'moment';
5
+ import z from 'zod';
6
+ import { type DatabaseProtocol } from './Session.js';
6
7
  /**
7
8
  * Represents a FileMaker host.
8
- * @implements {HostBase}
9
9
  */
10
10
  export default class FMHost implements HostBase {
11
11
  readonly hostname: string;
12
- readonly timezoneOffsetFunc: (moment: Moment) => number;
13
12
  readonly verify: boolean;
14
- readonly protocol: 'http:' | 'https:';
15
- _metadata: FMHostMetadata | null;
16
- constructor(_hostname: string, timezoneOffset?: (moment: Moment) => number, verify?: boolean);
17
- get metadata(): FMHostMetadata;
13
+ readonly protocol: DatabaseProtocol;
14
+ _metadata: z.infer<typeof FMHostMetadata> | null;
15
+ constructor(_hostname: string, verify?: boolean);
16
+ get metadata(): z.infer<typeof FMHostMetadata>;
17
+ /** FileMaker host date format converted to Moment-compatible tokens. */
18
18
  get dateFormat(): string;
19
+ /** FileMaker host time format. */
19
20
  get timeFormat(): string;
21
+ /** FileMaker host timestamp format converted to Moment-compatible tokens. */
20
22
  get timeStampFormat(): string;
21
23
  /**
22
24
  * Retrieves a list of databases from the FileMaker Server.
@@ -25,7 +27,9 @@ export default class FMHost implements HostBase {
25
27
  * @throws {FMError} If the request to the FileMaker Server fails or if the response contains an error.
26
28
  * @returns {Promise<any[]>} A promise that resolves to an array of database objects if successful.
27
29
  */
28
- listDatabases(credentials?: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris): Promise<any>;
30
+ listDatabases(credentials?: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris): Promise<{
31
+ name: string;
32
+ }[]>;
29
33
  /**
30
34
  * Creates a new database connection with the specified options.
31
35
  *
@@ -34,5 +38,15 @@ export default class FMHost implements HostBase {
34
38
  * @return {Database<T>} A new Database instance.
35
39
  */
36
40
  database<T extends DatabaseStructure>(data: databaseOptionsWithExternalSources): Database<T>;
37
- getMetadata(): Promise<FMHostMetadata>;
41
+ /** Fetch and cache FileMaker host product metadata. */
42
+ getMetadata(): Promise<{
43
+ productInfo: {
44
+ name: string;
45
+ dateFormat: string;
46
+ timeFormat: string;
47
+ timeStampFormat: string;
48
+ buildDate?: Date | undefined;
49
+ version?: string | undefined;
50
+ };
51
+ }>;
38
52
  }
@@ -3,24 +3,25 @@
3
3
  */
4
4
  import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
5
5
  import { FMError } from '../FMError.js';
6
- import { Database } from './database.js';
6
+ import { FMHostMetadata } from '../types.js';
7
+ import { ApiResults } from '../models/apiResults.js';
8
+ import z from 'zod';
9
+ import { DatabaseSessionPool } from './databaseSessionPool.js';
10
+ import { DatabaseConstantSession } from './databaseConstantSession.js';
7
11
  /**
8
12
  * Represents a FileMaker host.
9
- * @implements {HostBase}
10
13
  */
11
14
  export default class FMHost {
12
15
  hostname;
13
- timezoneOffsetFunc;
14
16
  verify;
15
17
  protocol;
16
18
  _metadata = null;
17
- constructor(_hostname, timezoneOffset = (moment) => 0 - (new Date()).getTimezoneOffset(), verify = true) {
19
+ constructor(_hostname, verify = true) {
18
20
  if (!(/^https?:\/\//).test(_hostname))
19
21
  throw new Error('hostname MUST begin with either http:// or https://');
20
22
  const url = new URL(_hostname);
21
23
  this.protocol = url.protocol;
22
24
  this.hostname = url.hostname;
23
- this.timezoneOffsetFunc = timezoneOffset;
24
25
  this.verify = verify;
25
26
  }
26
27
  get metadata() {
@@ -35,12 +36,17 @@ export default class FMHost {
35
36
  }
36
37
  };
37
38
  }
39
+ /** FileMaker host date format converted to Moment-compatible tokens. */
38
40
  get dateFormat() {
39
41
  return this.metadata.productInfo.dateFormat
40
42
  .replace('dd', 'DD')
41
43
  .replace('yyyy', 'YYYY');
42
44
  }
43
- get timeFormat() { return this.metadata.productInfo.timeFormat; }
45
+ /** FileMaker host time format. */
46
+ get timeFormat() {
47
+ return this.metadata.productInfo.timeFormat;
48
+ }
49
+ /** FileMaker host timestamp format converted to Moment-compatible tokens. */
44
50
  get timeStampFormat() {
45
51
  return this.metadata.productInfo.timeStampFormat
46
52
  .replace('dd', 'DD')
@@ -62,9 +68,15 @@ export default class FMHost {
62
68
  method: 'GET',
63
69
  headers
64
70
  });
65
- const data = await _fetch.json();
71
+ const data = ApiResults.extend({
72
+ response: z.object({
73
+ databases: z.array(z.object({
74
+ name: z.string()
75
+ }))
76
+ }).optional()
77
+ }).parse(await _fetch.json());
66
78
  // console.log(data.messages[0])
67
- if (data.messages[0].code === '0' && data.response) {
79
+ if (data.messages[0].code === 0 && data.response) {
68
80
  return data.response.databases;
69
81
  }
70
82
  else {
@@ -79,17 +91,23 @@ export default class FMHost {
79
91
  * @return {Database<T>} A new Database instance.
80
92
  */
81
93
  database(data) {
82
- return new Database(this, data);
94
+ if (data.credentials.method === 'filemaker')
95
+ return new DatabaseSessionPool(this, data);
96
+ // @ts-expect-error types are correct here
97
+ return new DatabaseConstantSession(this, data);
83
98
  }
99
+ /** Fetch and cache FileMaker host product metadata. */
84
100
  async getMetadata() {
85
101
  if (this._metadata)
86
102
  return this._metadata;
87
103
  const _fetch = await fetch(`${this.protocol}//${this.hostname}/fmi/data/v2/productInfo`, {
88
104
  method: 'GET'
89
105
  });
90
- const data = await _fetch.json();
106
+ const raw = await _fetch.json();
107
+ console.log(raw);
108
+ const data = ApiResults.extend({ response: FMHostMetadata.optional() }).parse(raw);
91
109
  // console.log(data.messages[0])
92
- if (data.messages[0].code === '0' && data.response) {
110
+ if (data.messages[0].code === 0 && data.response) {
93
111
  this._metadata = data.response;
94
112
  return data.response;
95
113
  }
@@ -1,11 +1,11 @@
1
1
  import { type FMHostMetadata } from '../types.js';
2
- import { type Moment } from 'moment';
2
+ import { type z } from 'zod';
3
+ import { type DatabaseProtocol } from './Session.js';
3
4
  export interface HostBase {
4
5
  readonly hostname: string;
5
- readonly protocol: string;
6
- readonly timezoneOffsetFunc: (moment: Moment) => number;
6
+ readonly protocol: DatabaseProtocol;
7
7
  readonly verify: boolean;
8
- metadata: FMHostMetadata;
8
+ metadata: z.infer<typeof FMHostMetadata>;
9
9
  getMetadata: () => PromiseLike<any>;
10
10
  timeFormat: string;
11
11
  dateFormat: string;
@@ -0,0 +1,34 @@
1
+ import { CookieJar } from './CookieJar.js';
2
+ export type DatabaseProtocol = 'http:' | 'https:';
3
+ export type DatabaseEndpoint = `${DatabaseProtocol}//${string}/fmi/data/v2/databases/${string}`;
4
+ interface SessionProps {
5
+ token: string;
6
+ /**
7
+ * A cookie jar that will be automatically cloned and used for this session
8
+ */
9
+ baseCookieJar?: CookieJar;
10
+ /**
11
+ * The full database endpoint
12
+ */
13
+ endpoint: DatabaseEndpoint;
14
+ /**
15
+ * Defines if a session should be kept alive, and how often to ping the database (in milliseconds)
16
+ */
17
+ keepAlive?: number;
18
+ abortSignal: AbortSignal;
19
+ }
20
+ export type Session = ReturnType<typeof session>;
21
+ export declare function session(props: SessionProps): {
22
+ fetch: (url: string | URL, init?: RequestInit) => Promise<Response>;
23
+ logout(): Promise<void>;
24
+ validate: () => Promise<void>;
25
+ };
26
+ export declare class HttpError extends Error {
27
+ readonly res: Response;
28
+ readonly message: string;
29
+ static new(res: Response): Promise<HttpError>;
30
+ protected constructor(res: Response, message: string);
31
+ get status(): number;
32
+ get statusText(): string;
33
+ }
34
+ export {};
@@ -0,0 +1,109 @@
1
+ // import fetchWithCookies, {CookieJar} from 'node-fetch-cookies'
2
+ import { CookieJar } from './CookieJar.js';
3
+ import { ApiResults } from '../models/apiResults.js';
4
+ import { z } from 'zod';
5
+ export function session(props) {
6
+ const cookieJar = props.baseCookieJar ? CookieJar.clone(props.baseCookieJar) : new CookieJar();
7
+ let keepAliveInterval;
8
+ const shouldRedirectWithGet = (status, method) => (status === 303 ||
9
+ ((status === 301 || status === 302) && method === 'POST'));
10
+ const sessionFetch = async (url, init = {}) => {
11
+ let urlParsed = url instanceof URL ? url : new URL(url);
12
+ let requestInit = { ...init };
13
+ requestInit.signal = requestInit.signal ? AbortSignal.any([requestInit.signal, props.abortSignal]) : props.abortSignal;
14
+ for (let redirectCount = 0; redirectCount < 10; redirectCount++) {
15
+ /**
16
+ * Bind the authorization token to the request
17
+ */
18
+ const headers = new Headers(requestInit.headers);
19
+ headers.set('Authorization', 'Bearer ' + props.token);
20
+ const cookieHeader = cookieJar.getCookieHeader(urlParsed);
21
+ if (cookieHeader)
22
+ headers.set('Cookie', cookieHeader);
23
+ else
24
+ headers.delete('Cookie');
25
+ const res = await fetch(urlParsed, {
26
+ ...requestInit,
27
+ headers,
28
+ redirect: 'manual'
29
+ });
30
+ for (const header of res.headers.getSetCookie()) {
31
+ cookieJar.addCookie(urlParsed, header);
32
+ }
33
+ if (res.status >= 300 && res.status < 400) {
34
+ const location = res.headers.get('location');
35
+ if (!location) {
36
+ throw new Error(`Redirect response missing Location header for ${urlParsed.toString()}`);
37
+ }
38
+ urlParsed = new URL(location, urlParsed);
39
+ const method = (requestInit.method ?? 'GET').toUpperCase();
40
+ if (shouldRedirectWithGet(res.status, method)) {
41
+ requestInit = {
42
+ ...requestInit,
43
+ method: 'GET',
44
+ body: undefined
45
+ };
46
+ }
47
+ continue;
48
+ }
49
+ if (!res.ok) {
50
+ throw await HttpError.new(res);
51
+ }
52
+ return res;
53
+ }
54
+ throw new Error(`Too many redirects while fetching ${urlParsed.toString()}`);
55
+ };
56
+ /**
57
+ * sessionValidate throws an error if the session is invalid
58
+ */
59
+ const sessionValidate = async () => {
60
+ const res = await sessionFetch(`${props.endpoint}/validateSession`);
61
+ ApiResults.extend({
62
+ isSessionInUse: z.boolean()
63
+ }).parse(await res.json());
64
+ };
65
+ if (props.keepAlive) {
66
+ keepAliveInterval = setInterval(() => {
67
+ void sessionValidate().catch(() => {
68
+ // Keep-alive failures should not create unhandled rejections.
69
+ });
70
+ }, props.keepAlive);
71
+ keepAliveInterval.unref?.();
72
+ }
73
+ return {
74
+ fetch: sessionFetch,
75
+ async logout() {
76
+ if (keepAliveInterval)
77
+ clearInterval(keepAliveInterval);
78
+ await fetch(`${props.endpoint}/sessions/${props.token}`, {
79
+ method: 'DELETE'
80
+ });
81
+ },
82
+ validate: sessionValidate
83
+ };
84
+ }
85
+ export class HttpError extends Error {
86
+ res;
87
+ message;
88
+ static async new(res) {
89
+ let message = `HTTP Error: [${res.status} ${res.statusText}]:`;
90
+ try {
91
+ message += await res.text();
92
+ }
93
+ catch {
94
+ message += ' (failed to read response body)';
95
+ }
96
+ return new this(res, message);
97
+ }
98
+ constructor(res, message) {
99
+ super();
100
+ this.res = res;
101
+ this.message = message;
102
+ }
103
+ get status() {
104
+ return this.res.status;
105
+ }
106
+ get statusText() {
107
+ return this.res.statusText;
108
+ }
109
+ }
@@ -1,58 +1,59 @@
1
- import { EventEmitter } from 'events';
2
1
  import { type LayoutInterface } from '../layouts/layoutInterface.js';
3
2
  import { Layout } from '../layouts/layout.js';
4
3
  import { type databaseOptionsWithExternalSources, type Script } from '../types.js';
5
4
  import { type HostBase } from './HostBase.js';
6
5
  import { type DatabaseBase } from './databaseBase.js';
7
- import { type ApiResults } from '../models/apiResults.js';
8
6
  import { type DatabaseStructure } from '../databaseStructure.js';
9
- import { type RequestInfo, type RequestInit, type Response } from 'node-fetch';
7
+ import { type DatabaseEndpoint, type Session } from './Session.js';
8
+ import { z, type ZodType } from 'zod';
10
9
  /**
11
10
  * Represents a database connection.
12
11
  * @template T - The structure of the database.
13
12
  */
14
- export declare class Database<T extends DatabaseStructure> extends EventEmitter implements DatabaseBase {
13
+ export declare abstract class Database<T extends DatabaseStructure> implements DatabaseBase {
15
14
  #private;
16
- private _token;
17
15
  readonly host: HostBase;
18
- private readonly connection_details;
19
- private cookies;
20
16
  readonly name: string;
21
17
  readonly debug: boolean;
22
- constructor(host: HostBase, conn: databaseOptionsWithExternalSources);
23
- private generateExternalSourceLogin;
18
+ protected canOpenNewConnections: boolean;
24
19
  /**
25
- * Logs out the user by deleting the current session token.
26
- * Throws an error if the user is not logged in.
27
- *
28
- * @returns {Promise<void>} A promise that resolves with no value once the logout is successful.
29
- * @throws {Error} Throws an error if the user is not logged in.
20
+ * Used during events where the application must logout
21
+ * @private
30
22
  */
23
+ protected abortController: AbortController;
24
+ protected constructor(host: HostBase, conn: databaseOptionsWithExternalSources<unknown>);
25
+ login(): Promise<void>;
31
26
  logout(): Promise<void>;
27
+ close(): Promise<void>;
28
+ [Symbol.asyncDispose](): Promise<void>;
32
29
  /**
33
- * Logs in to the database. Not required, as this is often done automatically
34
- *
35
- * @param {boolean} [forceLogin=false] - Whether to force login even if already logged in.
36
- * @throws {Error} - Throws an error if already logged in and forceLogin is false.
37
- * @throws {FMError} - Throws an FMError if login fails.
38
- * @return {Promise<string>} - Returns a promise that resolves to the access token upon successful login.
30
+ * The inheriting database class must implement this method to provide a session object.
31
+ * @param callback
32
+ * @protected
39
33
  */
40
- login(forceLogin?: boolean): Promise<string | undefined>;
41
- get token(): string;
34
+ protected abstract withSession<T>(callback: (session: Session) => Promise<T>): Promise<T>;
42
35
  /**
43
36
  * Returns the endpoint URL for the database connection.
44
37
  *
45
38
  * @returns {string} The endpoint URL.
46
39
  */
47
- get endpoint(): string;
48
- _apiRequestRaw(url: URL | RequestInfo, options?: RequestInit & {
49
- headers?: Record<string, string>;
50
- useCookieJar?: boolean;
51
- retries?: number;
52
- }, autoRelogin?: boolean): Promise<Response>;
53
- _apiRequestJSON<T = any>(url: URL | RequestInfo, options?: RequestInit & {
54
- headers?: Record<string, string>;
55
- }): Promise<ApiResults<T>>;
40
+ get endpoint(): DatabaseEndpoint;
41
+ /**
42
+ * Uses an available session to run a fetch
43
+ * @param url
44
+ * @param options
45
+ */
46
+ fetch(url: string | URL, options?: RequestInit): Promise<Response>;
47
+ /**
48
+ * Uses an available session to run a fetch on a FileMaker Data API JSON endpoint. Also applies JSON/Zod type enforcement on result.
49
+ */
50
+ fetchJSON<T extends ZodType | null = null>(url: string | URL, options: RequestInit & {
51
+ type: T;
52
+ }): Promise<T extends ZodType ? z.infer<T> & {
53
+ httpStatus: number;
54
+ } : {
55
+ httpStatus: number;
56
+ }>;
56
57
  /**
57
58
  * Retrieves a list of layouts in the current FileMaker database.
58
59
  *
@@ -62,6 +63,8 @@ export declare class Database<T extends DatabaseStructure> extends EventEmitter
62
63
  listLayouts(page?: number): Promise<Layout<LayoutInterface>[]>;
63
64
  layout<R extends keyof T['layouts']>(name: R): Layout<T['layouts'][R]>;
64
65
  layout<R extends LayoutInterface>(name: string): Layout<R>;
66
+ /** Clears any Layout objects previously returned by `database.layout(...)`. */
65
67
  clearLayoutCache(): void;
68
+ /** Creates a script reference you can pass to read and write helpers. */
66
69
  script(name: string, parameter?: string): Script;
67
70
  }