@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,106 +1,48 @@
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
4
  import { FMError } from '../FMError.js';
7
5
  import { Layout } from '../layouts/layout.js';
8
- import fetch from 'node-fetch';
9
- // @ts-expect-error - fetchWithCookies does not have available typescript types
10
- import fetchWithCookies, { CookieJar } from 'node-fetch-cookies';
6
+ import { ApiLayout, ApiResults } from '../models/apiResults.js';
7
+ import { z } from 'zod';
8
+ import { addHeaders } from '../utils/addHeaders.js';
9
+ import process from 'node:process';
11
10
  /**
12
11
  * Represents a database connection.
13
12
  * @template T - The structure of the database.
14
13
  */
15
- export class Database extends EventEmitter {
16
- _token = '';
14
+ export class Database {
17
15
  host;
18
- connection_details;
19
- cookies = new CookieJar();
20
16
  name;
21
17
  debug;
22
18
  #layoutCache = new Map();
19
+ canOpenNewConnections = true;
20
+ /**
21
+ * Used during events where the application must logout
22
+ * @private
23
+ */
24
+ abortController = new AbortController();
23
25
  constructor(host, conn) {
24
- super();
25
26
  this.host = host;
26
27
  this.name = conn.database;
27
- this.connection_details = conn;
28
28
  this.debug = conn.debug ?? false;
29
+ process.on('SIGINT', () => { void this.close(); });
30
+ process.on('SIGTERM', () => { void this.close(); });
31
+ process.on('beforeExit', () => { void this.close(); });
29
32
  }
30
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
31
- generateExternalSourceLogin(data) {
32
- if (data.credentials.method === 'filemaker') {
33
- const _data = data.credentials;
34
- return {
35
- database: data.database,
36
- username: _data.username,
37
- password: _data.password
38
- };
39
- }
40
- else {
41
- throw new Error('Not yet supported login method');
42
- }
33
+ async login() {
34
+ await Promise.resolve();
43
35
  }
44
- /**
45
- * Logs out the user by deleting the current session token.
46
- * Throws an error if the user is not logged in.
47
- *
48
- * @returns {Promise<void>} A promise that resolves with no value once the logout is successful.
49
- * @throws {Error} Throws an error if the user is not logged in.
50
- */
51
36
  async logout() {
52
- if (this.token === '')
53
- throw new Error('Not logged in');
54
- const _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
55
- method: 'DELETE',
56
- headers: {
57
- 'content-type': 'application/json'
58
- }
59
- });
60
- await _fetch.json();
61
- this._token = '';
37
+ await this.close();
62
38
  }
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
- */
71
- async login(forceLogin = false) {
72
- if (this.token !== '' && !forceLogin)
73
- return;
74
- // Reset cookies
75
- this.cookies = new CookieJar();
76
- await this.host.getMetadata();
77
- if (this.connection_details.credentials.method === 'token') {
78
- this._token = (this.connection_details.credentials).token;
79
- return this.token;
80
- }
81
- const url = new URL(`${this.endpoint}/sessions`);
82
- url.hostname = this.host.hostname;
83
- const res = await fetch(url, {
84
- method: 'POST',
85
- headers: generateAuthorizationHeaders(this.connection_details.credentials),
86
- body: JSON.stringify({
87
- fmDataSource: this.connection_details.externalSources.map(i => {
88
- const _i = i;
89
- return this.generateExternalSourceLogin(_i);
90
- })
91
- })
92
- });
93
- const _res = (await res.json());
94
- if (res.status === 200) {
95
- this._token = res.headers.get('x-fm-data-access-token') ?? '';
96
- return this._token;
97
- }
98
- else {
99
- throw new FMError(_res.messages[0].code, _res.status, res);
100
- }
39
+ async close() {
40
+ this.canOpenNewConnections = false;
41
+ if (!this.abortController.signal.aborted)
42
+ this.abortController.abort('Closing connection');
101
43
  }
102
- get token() {
103
- return this._token;
44
+ async [Symbol.asyncDispose]() {
45
+ await this.close();
104
46
  }
105
47
  /**
106
48
  * Returns the endpoint URL for the database connection.
@@ -110,57 +52,49 @@ export class Database extends EventEmitter {
110
52
  get endpoint() {
111
53
  return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
112
54
  }
113
- async _apiRequestRaw(url, options = {}, autoRelogin = true) {
114
- if (this.debug) {
115
- console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof URL
116
- ? url.toString()
117
- : typeof url === 'string' ? url : url.url}`);
118
- }
119
- const urlParsed = (url instanceof URL
120
- ? url
121
- : typeof url === 'string'
122
- ? new URL(url)
123
- : new URL(url.url));
124
- const reqIsToDBHost = urlParsed.hostname === this.host.hostname && urlParsed.pathname.startsWith("/fmi/data");
125
- if (reqIsToDBHost && this.token === '')
126
- await this.login(true);
127
- if (!options.headers)
128
- options.headers = {};
129
- if (reqIsToDBHost)
130
- options.headers.authorization = 'Bearer ' + this._token;
131
- const _fetch = options.useCookieJar
132
- ? await fetchWithCookies(this.cookies, url, options)
133
- : await fetch(url, options);
134
- if (!_fetch.ok && (!options.retries || options.retries > 0)) {
135
- if (this.debug) {
136
- console.log(`EASYFM DEBUG: RE-ATTEMPTING REQUEST (${_fetch.status}) ${url instanceof URL
137
- ? url.toString()
138
- : typeof url === 'string' ? url : url.url}`);
55
+ /**
56
+ * Uses an available session to run a fetch
57
+ * @param url
58
+ * @param options
59
+ */
60
+ async fetch(url, options) {
61
+ return await this.withSession(async (session) => await session.fetch(url, options));
62
+ }
63
+ /**
64
+ * Uses an available session to run a fetch on a FileMaker Data API JSON endpoint. Also applies JSON/Zod type enforcement on result.
65
+ */
66
+ async fetchJSON(url, options) {
67
+ const _options = options ?? {};
68
+ addHeaders(_options, {
69
+ 'Content-Type': 'application/json'
70
+ });
71
+ const res = await this.fetch(url, _options);
72
+ const rawData = await res.json();
73
+ if (this.debug)
74
+ console.log(rawData.response);
75
+ if (options.type !== null) {
76
+ const data = ApiResults.extend({ response: options.type.optional() })
77
+ .parse(rawData);
78
+ if (data.messages[0].code !== 0) {
79
+ throw new FMError(data.messages[0].code, res.status, data);
139
80
  }
140
- return await this._apiRequestRaw(url, { ...options, retries: (options?.retries ?? 1) - 1 });
141
- }
142
- else if (_fetch.status === 401 && reqIsToDBHost && autoRelogin) {
143
- await this.login(true);
144
- return await this._apiRequestRaw(url, options, false);
81
+ return data.response
82
+ // @ts-expect-error is correct
83
+ ? {
84
+ ...data.response,
85
+ httpStatus: res.status
86
+ }
87
+ // @ts-expect-error is correct
88
+ : { httpStatus: res.status };
145
89
  }
146
- else
147
- return _fetch;
148
- }
149
- async _apiRequestJSON(url, options = {}) {
150
- if (!options.headers)
151
- options.headers = {};
152
- options.headers['content-type'] = options.headers['content-type'] ? options.headers['content-type'] : 'application/json';
153
- const _fetch = await this._apiRequestRaw(url, options);
154
- const data = await _fetch.json();
155
- // Remove response if it is empty. This makes checking for an empty response easier
156
- if (data.response && Object.keys(data.response).length === 0)
157
- delete data.response;
158
- // console.log(data.messages[0])
159
- if (data.messages[0].code !== '0') {
160
- throw new FMError(data.messages[0].code, _fetch.status, data);
90
+ const data = ApiResults.parse(rawData);
91
+ if (data.messages[0].code !== 0) {
92
+ throw new FMError(data.messages[0].code, res.status, data);
161
93
  }
162
- data.httpStatus = _fetch.status;
163
- return data;
94
+ // @ts-expect-error is correct
95
+ return {
96
+ httpStatus: res.status
97
+ };
164
98
  }
165
99
  /**
166
100
  * Retrieves a list of layouts in the current FileMaker database.
@@ -169,9 +103,9 @@ export class Database extends EventEmitter {
169
103
  * @throws {FMError} If there was an error retrieving the layouts.
170
104
  */
171
105
  async listLayouts(page = 0) {
172
- const req = await this._apiRequestJSON(`${this.endpoint}/layouts?page=${encodeURIComponent(page)}`);
173
- if (!req.response)
174
- throw new FMError(req.messages[0].code, req.httpStatus, req.messages[0].message);
106
+ const res = await this.fetchJSON(`${this.endpoint}/layouts?page=${encodeURIComponent(page)}`, {
107
+ type: z.object({ layouts: z.array(ApiLayout) })
108
+ });
175
109
  const cycleLayoutNames = (layouts) => {
176
110
  let names = [];
177
111
  for (const layout of layouts) {
@@ -182,8 +116,11 @@ export class Database extends EventEmitter {
182
116
  }
183
117
  return names;
184
118
  };
185
- return cycleLayoutNames(req.response.layouts).map(layout => new Layout(this, layout));
119
+ return cycleLayoutNames(res.layouts).map(layout => new Layout(this, layout));
186
120
  }
121
+ /**
122
+ * Returns a FileMaker Layout object by name.
123
+ */
187
124
  layout(name) {
188
125
  let layout = this.#layoutCache.get(name);
189
126
  if (layout)
@@ -192,9 +129,11 @@ export class Database extends EventEmitter {
192
129
  this.#layoutCache.set(name, layout);
193
130
  return layout;
194
131
  }
132
+ /** Clears any Layout objects previously returned by `database.layout(...)`. */
195
133
  clearLayoutCache() {
196
134
  this.#layoutCache.clear();
197
135
  }
136
+ /** Creates a script reference you can pass to read and write helpers. */
198
137
  script(name, parameter = '') {
199
138
  return { name, parameter };
200
139
  }
@@ -1,16 +1,28 @@
1
1
  import { type HostBase } from './HostBase.js';
2
- import { type ApiResults } from '../models/apiResults.js';
3
- import { type RequestInfo, type RequestInit, type Response } from 'node-fetch';
2
+ import { type z, type ZodType } from 'zod';
4
3
  export interface DatabaseBase {
5
4
  host: HostBase;
6
5
  readonly name: string;
7
6
  endpoint: string;
8
- token: string;
9
- _apiRequestJSON: <T = unknown>(url: URL | RequestInfo, options?: RequestInit & {
10
- headers?: Record<string, string>;
11
- } | undefined, autoRelogin?: boolean) => Promise<ApiResults<T>>;
12
- _apiRequestRaw: (url: URL | RequestInfo, options?: RequestInit & {
13
- headers?: Record<string, string>;
14
- useCookieJar?: boolean;
15
- } | undefined) => Promise<Response>;
7
+ /**
8
+ * @deprecated login is deprecated and is now handled by newer session management. This function is simply a placeholder.
9
+ */
10
+ login: () => Promise<void>;
11
+ /**
12
+ * Immediately closes all open sessions and prevents new ones from being created.
13
+ */
14
+ close: () => Promise<void>;
15
+ /**
16
+ * Immediately closes all open sessions and prevents new ones from being created.
17
+ * Alias of {@link close}
18
+ */
19
+ logout: () => Promise<void>;
20
+ fetch: (url: string | URL, options?: RequestInit) => Promise<Response>;
21
+ fetchJSON: <T extends ZodType | null = null>(url: string | URL, options: RequestInit & {
22
+ type: T;
23
+ }) => Promise<T extends ZodType ? z.infer<T> & {
24
+ httpStatus: number;
25
+ } : {
26
+ httpStatus: number;
27
+ }>;
16
28
  }
@@ -0,0 +1,15 @@
1
+ import { Database } from './database.js';
2
+ import type { DatabaseStructure } from '../databaseStructure.js';
3
+ import { type Session } from './Session.js';
4
+ import { type databaseOptionsWithExternalSources, type loginOptionsClaris, type loginOptionsOAuth, type loginOptionsToken } from '../types.js';
5
+ import type FMHost from './FMHost.js';
6
+ /**
7
+ * DatabaseConstantSession handles database connections that must live on a single session which is kept alive.
8
+ * For example, connections that rely on OAuth.
9
+ */
10
+ export declare class DatabaseConstantSession<T extends DatabaseStructure> extends Database<T> {
11
+ #private;
12
+ constructor(host: FMHost, connectionDetails: databaseOptionsWithExternalSources<loginOptionsOAuth | loginOptionsClaris | loginOptionsToken>, structure: T);
13
+ close(): Promise<void>;
14
+ withSession<T>(callback: (session: Session) => Promise<T>): Promise<T>;
15
+ }
@@ -0,0 +1,70 @@
1
+ import { Database } from './database.js';
2
+ import { session } from './Session.js';
3
+ import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
4
+ import { FMError } from '../FMError.js';
5
+ import { ApiResults } from '../models/apiResults.js';
6
+ import { CookieJar } from './CookieJar.js';
7
+ /**
8
+ * DatabaseConstantSession handles database connections that must live on a single session which is kept alive.
9
+ * For example, connections that rely on OAuth.
10
+ */
11
+ export class DatabaseConstantSession extends Database {
12
+ #session;
13
+ constructor(host, connectionDetails, structure) {
14
+ super(host, connectionDetails);
15
+ if (connectionDetails.externalSources.length !== 0)
16
+ throw new Error('External sources are currently only supported for connections with the \'FileMaker\' login method.');
17
+ this.#session = (async () => {
18
+ // Ensure we have host metadata
19
+ await this.host.getMetadata();
20
+ if (connectionDetails.credentials.method === 'token') {
21
+ return session({
22
+ token: (connectionDetails.credentials).token,
23
+ keepAlive: 60_000,
24
+ endpoint: this.endpoint,
25
+ abortSignal: this.abortController.signal
26
+ });
27
+ }
28
+ const url = new URL(`${this.endpoint}/sessions`);
29
+ url.hostname = this.host.hostname;
30
+ const cookiejar = new CookieJar();
31
+ const res = await fetch(url, {
32
+ method: 'POST',
33
+ headers: generateAuthorizationHeaders(connectionDetails.credentials),
34
+ body: JSON.stringify({
35
+ fmDataSource: connectionDetails.externalSources.map(data => ({
36
+ database: data.database,
37
+ username: data.credentials.username,
38
+ password: data.credentials.password
39
+ }))
40
+ })
41
+ });
42
+ for (const cookie of res.headers.getSetCookie())
43
+ cookiejar.addCookie(url, cookie);
44
+ const json = ApiResults.parse(await res.json());
45
+ if (res.status === 200) {
46
+ return session({
47
+ token: res.headers.get('x-fm-data-access-token') ?? '',
48
+ keepAlive: 60_000,
49
+ endpoint: this.endpoint,
50
+ baseCookieJar: cookiejar,
51
+ abortSignal: this.abortController.signal
52
+ });
53
+ }
54
+ else {
55
+ throw new FMError(json.messages[0].code, res.status, res);
56
+ }
57
+ })();
58
+ }
59
+ async close() {
60
+ if (this.abortController.signal.aborted)
61
+ return;
62
+ await super.close();
63
+ await (await this.#session).logout();
64
+ }
65
+ async withSession(callback) {
66
+ if (!this.canOpenNewConnections)
67
+ throw new Error('Cannot open new connections');
68
+ return await callback(await this.#session);
69
+ }
70
+ }
@@ -0,0 +1,14 @@
1
+ import { Database } from './database.js';
2
+ import type { DatabaseStructure } from '../databaseStructure.js';
3
+ import { type Session } from './Session.js';
4
+ import { type databaseOptionsWithExternalSources, type loginOptionsFileMaker } from '../types.js';
5
+ import type FMHost from './FMHost.js';
6
+ /**
7
+ * DatabaseSessionPool handles database connections that can be pooled.
8
+ */
9
+ export declare class DatabaseSessionPool<T extends DatabaseStructure> extends Database<T> {
10
+ #private;
11
+ constructor(host: FMHost, connectionDetails: databaseOptionsWithExternalSources<loginOptionsFileMaker>);
12
+ close(): Promise<void>;
13
+ withSession<T>(callback: (session: Session) => Promise<T>, retry?: boolean): Promise<T>;
14
+ }
@@ -0,0 +1,148 @@
1
+ import { Database } from './database.js';
2
+ import { HttpError, session } from './Session.js';
3
+ import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
4
+ import { FMError } from '../FMError.js';
5
+ import { ApiResults } from '../models/apiResults.js';
6
+ import { CookieJar } from './CookieJar.js';
7
+ /**
8
+ * DatabaseSessionPool handles database connections that can be pooled.
9
+ */
10
+ export class DatabaseSessionPool extends Database {
11
+ #connectionDetails;
12
+ #activeSessions = new Set();
13
+ #withSessionQueue = [];
14
+ constructor(host, connectionDetails) {
15
+ super(host, connectionDetails);
16
+ this.#connectionDetails = connectionDetails;
17
+ }
18
+ async close() {
19
+ if (this.abortController.signal.aborted)
20
+ return;
21
+ await super.close();
22
+ // System might be shutting down, so we want to make sure we're quick
23
+ const sessions = [...this.#activeSessions];
24
+ this.#activeSessions.clear();
25
+ await Promise.allSettled(sessions.map(session => session.session.logout()));
26
+ }
27
+ get #maxSessions() {
28
+ return this.#connectionDetails.credentials.sessionPoolSize ?? 8;
29
+ }
30
+ // Opens a new database session
31
+ async #openSession() {
32
+ if (!this.canOpenNewConnections)
33
+ throw new Error('Cannot open new connections');
34
+ await this.host.getMetadata();
35
+ const url = new URL(`${this.endpoint}/sessions`);
36
+ url.hostname = this.host.hostname;
37
+ const cookiejar = new CookieJar();
38
+ const res = await fetch(url, {
39
+ method: 'POST',
40
+ headers: generateAuthorizationHeaders(this.#connectionDetails.credentials),
41
+ body: JSON.stringify({
42
+ fmDataSource: this.#connectionDetails.externalSources.map(data => ({
43
+ database: data.database,
44
+ username: data.credentials.username,
45
+ password: data.credentials.password
46
+ }))
47
+ })
48
+ });
49
+ for (const cookie of res.headers.getSetCookie())
50
+ cookiejar.addCookie(url, cookie);
51
+ const _res = ApiResults.parse(await res.json());
52
+ if (res.status === 200) {
53
+ return session({
54
+ token: res.headers.get('x-fm-data-access-token') ?? '',
55
+ endpoint: this.endpoint,
56
+ baseCookieJar: cookiejar,
57
+ abortSignal: this.abortController.signal
58
+ });
59
+ }
60
+ else {
61
+ throw new FMError(_res.messages[0].code, res.status, res);
62
+ }
63
+ }
64
+ /**
65
+ *
66
+ * @param session
67
+ * @param callback
68
+ * @param retry when true, if an HTTP 401 error occurs, we'll re-attempt it with another session.
69
+ * @private
70
+ */
71
+ async #withSessionInternal(session, callback, retry) {
72
+ try {
73
+ session.working = true;
74
+ const result = await callback(session.session);
75
+ return result;
76
+ }
77
+ catch (e) {
78
+ if (e instanceof HttpError && e.status === 401) {
79
+ // Invalidate this session
80
+ this.#activeSessions.delete(session);
81
+ if (retry)
82
+ return await this.withSession(callback, false);
83
+ }
84
+ throw e;
85
+ }
86
+ finally {
87
+ if (this.#activeSessions.has(session)) {
88
+ session.working = false;
89
+ }
90
+ await this.#scheduleQueuedWork();
91
+ }
92
+ }
93
+ async #processWithSessionQueue(session) {
94
+ const job = this.#withSessionQueue.shift();
95
+ if (!job)
96
+ return;
97
+ await this.#withSessionInternal(session, job.callback, job.retry).then(job.resolve).catch(job.reject);
98
+ }
99
+ async #scheduleQueuedWork() {
100
+ if (this.#withSessionQueue.length === 0)
101
+ return;
102
+ for (const session of this.#activeSessions) {
103
+ if (!session.working) {
104
+ session.working = true;
105
+ void this.#processWithSessionQueue(session);
106
+ return;
107
+ }
108
+ }
109
+ if (this.#activeSessions.size < this.#maxSessions) {
110
+ void this.#openSession().then(async (session) => {
111
+ const newSession = {
112
+ working: true,
113
+ session,
114
+ disconnectTimeout: null
115
+ };
116
+ this.#activeSessions.add(newSession);
117
+ await this.#processWithSessionQueue(newSession);
118
+ });
119
+ }
120
+ }
121
+ async withSession(callback, retry = true) {
122
+ return await new Promise((resolve, reject) => {
123
+ // First, see if there's an available session
124
+ for (const session of this.#activeSessions) {
125
+ if (!session.working) {
126
+ session.working = true;
127
+ this.#withSessionInternal(session, callback, retry).then(resolve).catch(reject);
128
+ return;
129
+ }
130
+ }
131
+ // If there's no available sessions, next check if we can open a new one
132
+ if (this.#activeSessions.size < this.#maxSessions) {
133
+ this.#openSession().then(async (session) => {
134
+ const newSession = {
135
+ working: true,
136
+ session,
137
+ disconnectTimeout: null
138
+ };
139
+ this.#activeSessions.add(newSession);
140
+ return await this.#withSessionInternal(newSession, callback, retry);
141
+ }).then(resolve).catch(reject);
142
+ return;
143
+ }
144
+ // Finally if all else fails, queue the job
145
+ this.#withSessionQueue.push({ callback, resolve, reject, retry });
146
+ });
147
+ }
148
+ }
package/dist/index.d.ts CHANGED
@@ -1,18 +1,38 @@
1
1
  import FMHost from './connection/FMHost.js';
2
2
  import type * as TYPES from './types.js';
3
+ /** Error type returned for FileMaker Data API failures. */
3
4
  export { FMError } from './FMError.js';
5
+ /** Utility type for narrowing portal data on typed layouts. */
4
6
  export { type PickPortals } from './types.js';
7
+ /** Base database connection abstraction used by all auth modes. */
5
8
  export { Database } from './connection/database.js';
9
+ /** Layout-scoped API wrapper for metadata, scripts, and record operations. */
6
10
  export { Layout } from './layouts/layout.js';
11
+ /** Base record implementation shared by layout and portal records. */
7
12
  export { RecordBase } from './records/recordBase.js';
13
+ /** Record returned from a layout. Supports fetch, commit, duplicate, and delete. */
8
14
  export { LayoutRecord } from './records/layoutRecord.js';
9
- export { type LayoutInterface, type PortalInterface } from './layouts/layoutInterface.js';
15
+ /** Type helpers for describing typed layouts and portals. */
16
+ export { type LayoutInterface, type PortalInterface, type RecordFieldsMap } from './layouts/layoutInterface.js';
17
+ /** Record returned from a portal row. */
10
18
  export { PortalRecord } from './records/portalRecord.js';
19
+ /** Wrapper around portal rows included in a layout record. */
11
20
  export { Portal } from './records/portal.js';
21
+ /** Types and builder used for list/find record operations. */
12
22
  export { type FindRequest, type FindRequestRaw, RecordGetOperation } from './records/getOperations/recordGetOperation.js';
23
+ /** Entry point for `layout.records.*` operations. */
13
24
  export { LayoutRecordManager } from './layouts/layoutRecordManager.js';
14
- export { type RecordFieldsMap } from './layouts/recordFieldsMap.js';
25
+ /** Helpers for safe FileMaker find query construction and date/time formatting. */
15
26
  export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.js';
16
- export { type Container, Field } from './records/field.js';
27
+ /** Conversion helpers for FileMaker-formatted Temporal values. */
28
+ export * from './records/fields/valueField.js';
29
+ export * from './records/fields/containerField.js';
30
+ export { type Field } from './records/fields/field.js';
31
+ export { stringToTemporal, temporalToString, type TemporalValue, type TemporalValueType } from './utils/temporal.js';
32
+ /** Field wrapper used for reading, editing, and container access. */
33
+ export { type BaseField } from './records/fields/baseField.js';
34
+ /** Default export. Represents FileMaker host/server. */
17
35
  export default FMHost;
18
- export { type TYPES };
36
+ export {
37
+ /** Namespace re-export of library public types. */
38
+ type TYPES };
package/dist/index.js CHANGED
@@ -2,15 +2,29 @@
2
2
  * Copyright (c) 2023-2024. See LICENSE file for more information
3
3
  */
4
4
  import FMHost from './connection/FMHost.js';
5
+ /** Error type returned for FileMaker Data API failures. */
5
6
  export { FMError } from './FMError.js';
7
+ /** Base database connection abstraction used by all auth modes. */
6
8
  export { Database } from './connection/database.js';
9
+ /** Layout-scoped API wrapper for metadata, scripts, and record operations. */
7
10
  export { Layout } from './layouts/layout.js';
11
+ /** Base record implementation shared by layout and portal records. */
8
12
  export { RecordBase } from './records/recordBase.js';
13
+ /** Record returned from a layout. Supports fetch, commit, duplicate, and delete. */
9
14
  export { LayoutRecord } from './records/layoutRecord.js';
15
+ /** Record returned from a portal row. */
10
16
  export { PortalRecord } from './records/portalRecord.js';
17
+ /** Wrapper around portal rows included in a layout record. */
11
18
  export { Portal } from './records/portal.js';
19
+ /** Types and builder used for list/find record operations. */
12
20
  export { RecordGetOperation } from './records/getOperations/recordGetOperation.js';
21
+ /** Entry point for `layout.records.*` operations. */
13
22
  export { LayoutRecordManager } from './layouts/layoutRecordManager.js';
23
+ /** Helpers for safe FileMaker find query construction and date/time formatting. */
14
24
  export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.js';
15
- export { Field } from './records/field.js';
25
+ /** Conversion helpers for FileMaker-formatted Temporal values. */
26
+ export * from './records/fields/valueField.js';
27
+ export * from './records/fields/containerField.js';
28
+ export { stringToTemporal, temporalToString } from './utils/temporal.js';
29
+ /** Default export. Represents FileMaker host/server. */
16
30
  export default FMHost;
@@ -3,13 +3,15 @@ import { type Script, type ScriptResult } from '../types.js';
3
3
  import { type LayoutInterface } from './layoutInterface.js';
4
4
  import { type LayoutBase } from './layoutBase.js';
5
5
  import { type DatabaseBase } from '../connection/databaseBase.js';
6
- import { type ApiLayoutMetadata } from '../models/apiResults.js';
6
+ import { ApiLayoutMetadata } from '../models/apiResults.js';
7
+ import { type z } from 'zod';
7
8
  export declare class Layout<T extends LayoutInterface> implements LayoutBase {
8
9
  readonly database: DatabaseBase;
9
10
  readonly name: string;
10
11
  readonly records: LayoutRecordManager<T>;
11
- metadata: ApiLayoutMetadata | null;
12
+ metadata: z.infer<typeof ApiLayoutMetadata> | null;
12
13
  constructor(database: DatabaseBase, name: string);
14
+ /** Base endpoint for this layout on FileMaker Data API. */
13
15
  get endpoint(): string;
14
16
  /**
15
17
  * Executes a FileMaker script on this layout asynchronously and returns the result.
@@ -23,5 +25,5 @@ export declare class Layout<T extends LayoutInterface> implements LayoutBase {
23
25
  * @returns {Promise<ApiLayoutMetadata>} The layout metadata.
24
26
  * @throws {FMError} If an error occurs during the API request.
25
27
  */
26
- getLayoutMeta(): Promise<ApiLayoutMetadata>;
28
+ getLayoutMeta(): Promise<z.infer<typeof ApiLayoutMetadata>>;
27
29
  }