@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.
- package/README.md +60 -196
- package/dist/bin/generateTypes.js +3 -3
- package/dist/bin/stressSearch.d.ts +2 -0
- package/dist/bin/stressSearch.js +243 -0
- package/dist/connection/CookieJar.d.ts +19 -0
- package/dist/connection/CookieJar.js +99 -0
- package/dist/connection/FMHost.d.ts +25 -11
- package/dist/connection/FMHost.js +29 -11
- package/dist/connection/HostBase.d.ts +4 -4
- package/dist/connection/Session.d.ts +34 -0
- package/dist/connection/Session.js +109 -0
- package/dist/connection/database.d.ts +34 -31
- package/dist/connection/database.js +72 -133
- package/dist/connection/databaseBase.d.ts +22 -10
- package/dist/connection/databaseConstantSession.d.ts +15 -0
- package/dist/connection/databaseConstantSession.js +70 -0
- package/dist/connection/databaseSessionPool.d.ts +14 -0
- package/dist/connection/databaseSessionPool.js +148 -0
- package/dist/index.d.ts +24 -4
- package/dist/index.js +15 -1
- package/dist/layouts/layout.d.ts +5 -3
- package/dist/layouts/layout.js +11 -15
- package/dist/layouts/layoutBase.d.ts +3 -2
- package/dist/layouts/layoutInterface.d.ts +2 -1
- package/dist/layouts/layoutRecordManager.d.ts +6 -15
- package/dist/layouts/layoutRecordManager.js +6 -15
- package/dist/models/apiResults.d.ts +209 -93
- package/dist/models/apiResults.js +95 -25
- package/dist/records/field.d.ts +17 -2
- package/dist/records/field.js +161 -237
- package/dist/records/fields/baseField.d.ts +57 -0
- package/dist/records/fields/baseField.js +59 -0
- package/dist/records/fields/containerField.d.ts +35 -0
- package/dist/records/fields/containerField.js +78 -0
- package/dist/records/fields/field.d.ts +3 -0
- package/dist/records/fields/field.js +1 -0
- package/dist/records/fields/valueField.d.ts +41 -0
- package/dist/records/fields/valueField.js +108 -0
- package/dist/records/getOperations/recordGetOperation.d.ts +7 -4
- package/dist/records/getOperations/recordGetOperation.js +22 -26
- package/dist/records/layoutRecord.d.ts +27 -9
- package/dist/records/layoutRecord.js +84 -97
- package/dist/records/portal.d.ts +1 -5
- package/dist/records/portal.js +0 -4
- package/dist/records/portalBase.d.ts +1 -2
- package/dist/records/portalRecord.d.ts +7 -4
- package/dist/records/portalRecord.js +12 -0
- package/dist/records/recordBase.d.ts +8 -6
- package/dist/records/recordBase.js +32 -33
- package/dist/types.d.ts +42 -18
- package/dist/types.js +12 -0
- package/dist/utils/addHeaders.d.ts +5 -0
- package/dist/utils/addHeaders.js +24 -0
- package/dist/utils/query.d.ts +24 -8
- package/dist/utils/query.js +60 -20
- package/dist/utils/temporal.d.ts +9 -0
- package/dist/utils/temporal.js +110 -0
- 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
|
|
9
|
-
|
|
10
|
-
import
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
163
|
-
return
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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;
|
package/dist/layouts/layout.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
}
|