@jd-data-limited/easy-fm 5.0.0-beta.2 → 5.0.0-beta.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/LICENSE +21 -21
- package/README.md +93 -91
- package/dist/bin/cli.js +34 -5
- package/dist/bin/generateClient.d.ts +1 -0
- package/dist/bin/generateClient.js +48 -0
- package/dist/bin/generateTypes.js +19 -18
- package/dist/bin/init.d.ts +6 -0
- package/dist/bin/init.js +46 -0
- package/dist/bin/stressSearch.js +12 -12
- package/dist/connection/DataApiProvider.d.ts +15 -0
- package/dist/connection/DataApiProvider.js +246 -0
- package/dist/connection/dataApiDatabaseSession.d.ts +11 -0
- package/dist/connection/dataApiDatabaseSession.js +139 -0
- package/dist/connection/database.d.ts +22 -56
- package/dist/connection/database.js +175 -111
- package/dist/connection/databaseBase.d.ts +9 -22
- package/dist/connection/databaseBase.js +0 -3
- package/dist/connection/databaseProvider.d.ts +196 -0
- package/dist/connection/databaseProvider.js +6 -0
- package/dist/connection/databaseSessionPool.d.ts +8 -7
- package/dist/connection/databaseSessionPool.js +110 -106
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -3
- package/dist/layouts/layout.d.ts +0 -2
- package/dist/layouts/layout.js +6 -16
- package/dist/layouts/layoutBase.d.ts +0 -1
- package/dist/layouts/layoutInterface.d.ts +4 -2
- package/dist/records/fields/baseField.d.ts +4 -1
- package/dist/records/fields/containerField.d.ts +6 -23
- package/dist/records/fields/containerField.js +34 -68
- package/dist/records/fields/valueField.js +6 -6
- package/dist/records/getOperations/recordGetOperation.js +19 -18
- package/dist/records/layoutRecord.d.ts +8 -2
- package/dist/records/layoutRecord.js +45 -74
- package/dist/records/portalRecord.d.ts +4 -1
- package/dist/records/portalRecord.js +12 -2
- package/dist/records/recordBase.d.ts +6 -3
- package/dist/records/recordBase.js +6 -5
- package/dist/schema.d.ts +54 -0
- package/dist/schema.js +87 -0
- package/dist/typegen/dataApiSchemaSource.d.ts +3 -0
- package/dist/typegen/dataApiSchemaSource.js +32 -0
- package/dist/typegen/dependencies.d.ts +11 -0
- package/dist/typegen/dependencies.js +67 -0
- package/dist/typegen/generate.d.ts +5 -0
- package/dist/typegen/generate.js +85 -0
- package/dist/typegen/index.d.ts +6 -0
- package/dist/typegen/index.js +6 -0
- package/dist/typegen/init.d.ts +13 -0
- package/dist/typegen/init.js +113 -0
- package/dist/typegen/types.d.ts +40 -0
- package/dist/typegen/types.js +1 -0
- package/dist/typegen/zod.d.ts +3 -0
- package/dist/typegen/zod.js +32 -0
- package/package.json +26 -4
- package/dist/layouts/recordFieldsMap.d.ts +0 -2
- package/dist/layouts/recordFieldsMap.js +0 -4
- package/dist/records/field.d.ts +0 -77
- package/dist/records/field.js +0 -192
|
@@ -1,40 +1,37 @@
|
|
|
1
|
-
import { Database } from './database.js';
|
|
2
|
-
import { HttpError, session } from './Session.js';
|
|
3
|
-
import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
|
|
4
1
|
import { FMError } from '../FMError.js';
|
|
5
2
|
import { ApiResults } from '../models/apiResults.js';
|
|
6
3
|
import { CookieJar } from './CookieJar.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
|
|
5
|
+
import { HttpError, session } from './Session.js';
|
|
6
|
+
import { DataApiDatabaseSession } from './dataApiDatabaseSession.js';
|
|
7
|
+
/** Reserves independent FileMaker Data API sessions from a bounded pool. */
|
|
8
|
+
export class DatabaseSessionPool {
|
|
9
|
+
host;
|
|
11
10
|
#connectionDetails;
|
|
12
11
|
#activeSessions = new Set();
|
|
13
|
-
#
|
|
12
|
+
#waiters = [];
|
|
13
|
+
#abortController = new AbortController();
|
|
14
|
+
#openingSessions = 0;
|
|
15
|
+
#closed = false;
|
|
14
16
|
constructor(host, connectionDetails) {
|
|
15
|
-
|
|
17
|
+
this.host = host;
|
|
16
18
|
this.#connectionDetails = connectionDetails;
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const sessions = [...this.#activeSessions];
|
|
24
|
-
this.#activeSessions.clear();
|
|
25
|
-
await Promise.allSettled(sessions.map(session => session.session.logout()));
|
|
20
|
+
get formatting() {
|
|
21
|
+
return this.host;
|
|
22
|
+
}
|
|
23
|
+
get endpoint() {
|
|
24
|
+
return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.#connectionDetails.database}`;
|
|
26
25
|
}
|
|
27
26
|
get #maxSessions() {
|
|
28
27
|
return this.#connectionDetails.credentials.sessionPoolSize ?? 8;
|
|
29
28
|
}
|
|
30
|
-
// Opens a new database session
|
|
31
29
|
async #openSession() {
|
|
32
|
-
if (
|
|
33
|
-
throw new Error('
|
|
30
|
+
if (this.#closed)
|
|
31
|
+
throw new Error('Database provider is closed');
|
|
34
32
|
await this.host.getMetadata();
|
|
35
33
|
const url = new URL(`${this.endpoint}/sessions`);
|
|
36
|
-
|
|
37
|
-
const cookiejar = new CookieJar();
|
|
34
|
+
const cookieJar = new CookieJar();
|
|
38
35
|
const res = await fetch(url, {
|
|
39
36
|
method: 'POST',
|
|
40
37
|
headers: generateAuthorizationHeaders(this.#connectionDetails.credentials),
|
|
@@ -44,105 +41,112 @@ export class DatabaseSessionPool extends Database {
|
|
|
44
41
|
username: data.credentials.username,
|
|
45
42
|
password: data.credentials.password
|
|
46
43
|
}))
|
|
47
|
-
})
|
|
44
|
+
}),
|
|
45
|
+
signal: this.#abortController.signal
|
|
48
46
|
});
|
|
49
47
|
for (const cookie of res.headers.getSetCookie())
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
if (res.status
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
throw new FMError(_res.messages[0].code, res.status, res);
|
|
62
|
-
}
|
|
48
|
+
cookieJar.addCookie(url, cookie);
|
|
49
|
+
const data = ApiResults.parse(await res.json());
|
|
50
|
+
if (res.status !== 200)
|
|
51
|
+
throw new FMError(data.messages[0].code, res.status, data);
|
|
52
|
+
return session({
|
|
53
|
+
token: res.headers.get('x-fm-data-access-token') ?? '',
|
|
54
|
+
endpoint: this.endpoint,
|
|
55
|
+
baseCookieJar: cookieJar,
|
|
56
|
+
abortSignal: this.#abortController.signal
|
|
57
|
+
});
|
|
63
58
|
}
|
|
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) {
|
|
59
|
+
async #createReservedSession() {
|
|
60
|
+
this.#openingSessions++;
|
|
72
61
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
62
|
+
const active = { working: true, session: await this.#openSession() };
|
|
63
|
+
this.#activeSessions.add(active);
|
|
64
|
+
return active;
|
|
76
65
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
finally {
|
|
67
|
+
this.#openingSessions--;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async #acquire() {
|
|
71
|
+
if (this.#closed)
|
|
72
|
+
throw new Error('Database provider is closed');
|
|
73
|
+
for (const active of this.#activeSessions) {
|
|
74
|
+
if (!active.working) {
|
|
75
|
+
active.working = true;
|
|
76
|
+
return active;
|
|
83
77
|
}
|
|
84
|
-
throw e;
|
|
85
78
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
if (this.#activeSessions.size + this.#openingSessions < this.#maxSessions) {
|
|
80
|
+
try {
|
|
81
|
+
return await this.#createReservedSession();
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.#serveWaitingRequest();
|
|
85
|
+
throw error;
|
|
89
86
|
}
|
|
90
|
-
await this.#scheduleQueuedWork();
|
|
91
87
|
}
|
|
88
|
+
return await new Promise((resolve, reject) => {
|
|
89
|
+
this.#waiters.push({ resolve, reject });
|
|
90
|
+
});
|
|
92
91
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
#release(active) {
|
|
93
|
+
if (!this.#activeSessions.has(active))
|
|
94
|
+
return;
|
|
95
|
+
const waiter = this.#waiters.shift();
|
|
96
|
+
if (waiter) {
|
|
97
|
+
waiter.resolve(active);
|
|
96
98
|
return;
|
|
97
|
-
|
|
99
|
+
}
|
|
100
|
+
active.working = false;
|
|
98
101
|
}
|
|
99
|
-
async #
|
|
100
|
-
if (this.#
|
|
102
|
+
async #discard(active) {
|
|
103
|
+
if (!this.#activeSessions.delete(active))
|
|
104
|
+
return;
|
|
105
|
+
await active.session.logout().catch(() => undefined);
|
|
106
|
+
this.#serveWaitingRequest();
|
|
107
|
+
}
|
|
108
|
+
#serveWaitingRequest() {
|
|
109
|
+
const waiter = this.#waiters.shift();
|
|
110
|
+
if (!waiter)
|
|
111
|
+
return;
|
|
112
|
+
if (this.#closed) {
|
|
113
|
+
waiter.reject(new Error('Database provider is closed'));
|
|
101
114
|
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
115
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
void this.#createReservedSession()
|
|
117
|
+
.then(waiter.resolve)
|
|
118
|
+
.catch(error => {
|
|
119
|
+
waiter.reject(error);
|
|
120
|
+
this.#serveWaitingRequest();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async withSession(callback) {
|
|
124
|
+
const active = await this.#acquire();
|
|
125
|
+
let discard = false;
|
|
126
|
+
try {
|
|
127
|
+
return await callback(new DataApiDatabaseSession(this.endpoint, active.session, this.#connectionDetails.debug ?? false));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
discard = error instanceof HttpError && error.status === 401;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
if (discard)
|
|
135
|
+
await this.#discard(active);
|
|
136
|
+
else
|
|
137
|
+
this.#release(active);
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
|
-
async
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
});
|
|
140
|
+
async close() {
|
|
141
|
+
if (this.#closed)
|
|
142
|
+
return;
|
|
143
|
+
this.#closed = true;
|
|
144
|
+
this.#abortController.abort('Closing database provider');
|
|
145
|
+
const error = new Error('Database provider is closed');
|
|
146
|
+
for (const waiter of this.#waiters.splice(0))
|
|
147
|
+
waiter.reject(error);
|
|
148
|
+
const sessions = [...this.#activeSessions];
|
|
149
|
+
this.#activeSessions.clear();
|
|
150
|
+
await Promise.allSettled(sessions.map(active => active.session.logout()));
|
|
147
151
|
}
|
|
148
152
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import FMHost from './connection/FMHost.js';
|
|
2
1
|
import type * as TYPES from './types.js';
|
|
3
2
|
/** Error type returned for FileMaker Data API failures. */
|
|
4
3
|
export { FMError } from './FMError.js';
|
|
5
4
|
/** Utility type for narrowing portal data on typed layouts. */
|
|
6
5
|
export { type PickPortals } from './types.js';
|
|
6
|
+
export { type DatabaseStructure } from './databaseStructure.js';
|
|
7
7
|
/** Base database connection abstraction used by all auth modes. */
|
|
8
8
|
export { Database } from './connection/database.js';
|
|
9
9
|
/** Layout-scoped API wrapper for metadata, scripts, and record operations. */
|
|
@@ -31,8 +31,9 @@ export { type Field } from './records/fields/field.js';
|
|
|
31
31
|
export { stringToTemporal, temporalToString, type TemporalValue, type TemporalValueType } from './utils/temporal.js';
|
|
32
32
|
/** Field wrapper used for reading, editing, and container access. */
|
|
33
33
|
export { type BaseField } from './records/fields/baseField.js';
|
|
34
|
-
|
|
35
|
-
export
|
|
34
|
+
export { SchemaDriftError, withSchemaValidation, zodValidator, type DatabaseRuntimeSchema, type LayoutRuntimeSchema, type ReadonlyField, type RuntimeValidator, type ValidationIssue, type ValidationResult } from './schema.js';
|
|
35
|
+
export { DataApiProvider, type DataApiProviderOptions } from './connection/DataApiProvider.js';
|
|
36
|
+
export { type DatabaseProvider, type ProviderConnection, type ProviderSession, type ProviderContext, type DatabaseFormatting, type DatabaseOperation, type DatabaseOperationResult, type DatabaseOperationType, type ContainerDownloadOptions, ProviderSessionExpiredError, ContainerSessionAffinityError } from './connection/databaseProvider.js';
|
|
36
37
|
export {
|
|
37
38
|
/** Namespace re-export of library public types. */
|
|
38
39
|
type TYPES };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
|
-
import FMHost from './connection/FMHost.js';
|
|
5
4
|
/** Error type returned for FileMaker Data API failures. */
|
|
6
5
|
export { FMError } from './FMError.js';
|
|
7
6
|
/** Base database connection abstraction used by all auth modes. */
|
|
@@ -26,5 +25,6 @@ export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.j
|
|
|
26
25
|
export * from './records/fields/valueField.js';
|
|
27
26
|
export * from './records/fields/containerField.js';
|
|
28
27
|
export { stringToTemporal, temporalToString } from './utils/temporal.js';
|
|
29
|
-
|
|
30
|
-
export
|
|
28
|
+
export { SchemaDriftError, withSchemaValidation, zodValidator } from './schema.js';
|
|
29
|
+
export { DataApiProvider } from './connection/DataApiProvider.js';
|
|
30
|
+
export { ProviderSessionExpiredError, ContainerSessionAffinityError } from './connection/databaseProvider.js';
|
package/dist/layouts/layout.d.ts
CHANGED
|
@@ -11,8 +11,6 @@ export declare class Layout<T extends LayoutInterface> implements LayoutBase {
|
|
|
11
11
|
readonly records: LayoutRecordManager<T>;
|
|
12
12
|
metadata: z.infer<typeof ApiLayoutMetadata> | null;
|
|
13
13
|
constructor(database: DatabaseBase, name: string);
|
|
14
|
-
/** Base endpoint for this layout on FileMaker Data API. */
|
|
15
|
-
get endpoint(): string;
|
|
16
14
|
/**
|
|
17
15
|
* Executes a FileMaker script on this layout asynchronously and returns the result.
|
|
18
16
|
* @param {Script} script - The script to be executed.
|
package/dist/layouts/layout.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { LayoutRecordManager } from './layoutRecordManager.js';
|
|
5
5
|
import { FMError } from '../FMError.js';
|
|
6
|
-
import { ApiLayoutMetadata
|
|
6
|
+
import { ApiLayoutMetadata } from '../models/apiResults.js';
|
|
7
7
|
export class Layout {
|
|
8
8
|
database;
|
|
9
9
|
name;
|
|
@@ -13,26 +13,16 @@ export class Layout {
|
|
|
13
13
|
this.database = database;
|
|
14
14
|
this.name = name;
|
|
15
15
|
}
|
|
16
|
-
/** Base endpoint for this layout on FileMaker Data API. */
|
|
17
|
-
get endpoint() {
|
|
18
|
-
return `${this.database.endpoint}/layouts/${this.name}`;
|
|
19
|
-
}
|
|
20
16
|
/**
|
|
21
17
|
* Executes a FileMaker script on this layout asynchronously and returns the result.
|
|
22
18
|
* @param {Script} script - The script to be executed.
|
|
23
19
|
* @returns {Promise<ScriptResult>} - A promise that resolves to the script result or rejects with an error.
|
|
24
20
|
*/
|
|
25
21
|
async runScript(script) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
url += '?script.param=' + encodeURIComponent(script.parameter);
|
|
29
|
-
const res = await this.database.fetchJSON(url, {
|
|
30
|
-
type: ApiScriptResult,
|
|
31
|
-
method: 'GET'
|
|
32
|
-
});
|
|
33
|
-
const error = parseInt(res.scriptError);
|
|
22
|
+
const { value: res } = await this.database.execute({ type: 'script.run', layout: this.name, script: script.name, parameter: script.parameter || undefined });
|
|
23
|
+
const error = res.scriptError ?? 0;
|
|
34
24
|
return {
|
|
35
|
-
scriptError: error ? new FMError(error, 200, res) : undefined,
|
|
25
|
+
scriptError: error ? new FMError(error, res.status ?? 200, res) : undefined,
|
|
36
26
|
scriptResult: res.scriptResult
|
|
37
27
|
};
|
|
38
28
|
}
|
|
@@ -46,8 +36,8 @@ export class Layout {
|
|
|
46
36
|
if (this.metadata) {
|
|
47
37
|
return this.metadata;
|
|
48
38
|
}
|
|
49
|
-
const res = await this.database.
|
|
50
|
-
this.metadata = res;
|
|
39
|
+
const { value: res } = await this.database.execute({ type: 'layout.metadata', layout: this.name });
|
|
40
|
+
this.metadata = ApiLayoutMetadata.parse(res);
|
|
51
41
|
return this.metadata;
|
|
52
42
|
}
|
|
53
43
|
}
|
|
@@ -5,7 +5,6 @@ import { type z } from 'zod';
|
|
|
5
5
|
export interface LayoutBase {
|
|
6
6
|
readonly name: string;
|
|
7
7
|
metadata: z.infer<typeof ApiLayoutMetadata> | null;
|
|
8
|
-
endpoint: string;
|
|
9
8
|
runScript: (script: Script) => Promise<ScriptResult>;
|
|
10
9
|
getLayoutMeta: () => Promise<z.infer<typeof ApiLayoutMetadata>>;
|
|
11
10
|
database: DatabaseBase;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type Portal } from '../records/portal.js';
|
|
2
|
-
import { type Field } from '../records/fields/field.js';
|
|
3
2
|
export interface LayoutInterface {
|
|
4
3
|
fields: RecordFieldsMap;
|
|
5
4
|
portals: PortalInterface;
|
|
6
5
|
}
|
|
7
|
-
|
|
6
|
+
/** Public field views, including generated read-only views, satisfy this shape. */
|
|
7
|
+
export type RecordFieldsMap = Record<string, {
|
|
8
|
+
readonly value: unknown;
|
|
9
|
+
}>;
|
|
8
10
|
export type PortalInterface = Record<string | number | symbol, Portal<RecordFieldsMap>>;
|
|
@@ -2,6 +2,7 @@ import { RecordTypes } from '../../types.js';
|
|
|
2
2
|
import { type ApiFieldMetadata, FieldResultTypes } from '../../models/apiResults.js';
|
|
3
3
|
import { type LayoutBase } from '../../layouts/layoutBase.js';
|
|
4
4
|
import { type z } from 'zod';
|
|
5
|
+
import { type SessionBinding } from '../../connection/databaseProvider.js';
|
|
5
6
|
export type ContainerValue = null;
|
|
6
7
|
/**
|
|
7
8
|
* RawValueData refers to the raw shapes of data we receive from or sent to FileMaker.
|
|
@@ -10,11 +11,13 @@ export type RawValueData = string | number | null;
|
|
|
10
11
|
export interface Parentable {
|
|
11
12
|
layout: LayoutBase;
|
|
12
13
|
type: RecordTypes;
|
|
13
|
-
|
|
14
|
+
recordId: number;
|
|
15
|
+
sessionBinding?: SessionBinding;
|
|
14
16
|
portal?: {
|
|
15
17
|
name: string;
|
|
16
18
|
};
|
|
17
19
|
getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
|
|
20
|
+
refreshContainer(fieldId: string): Promise<import('./containerField.js').ContainerField>;
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
23
|
* A class representing a field in a record.
|
|
@@ -1,35 +1,18 @@
|
|
|
1
|
-
import { BaseField, Parentable, RawValueData } from "./baseField.js";
|
|
2
1
|
import { Readable } from 'node:stream';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
2
|
+
import { BaseField, type Parentable, type RawValueData } from './baseField.js';
|
|
3
|
+
import { type ContainerDownloadOptions } from '../../connection/databaseProvider.js';
|
|
6
4
|
export declare class ContainerField extends BaseField<string, 'container'> {
|
|
7
5
|
#private;
|
|
8
|
-
static firstContainerDownload: Promise<any> | null;
|
|
9
6
|
constructor(record: Parentable, id: string, value: RawValueData);
|
|
10
|
-
/**
|
|
11
|
-
* Uploads a file to the container field.
|
|
12
|
-
*
|
|
13
|
-
* @param {Buffer} file - The file content as a buffer.
|
|
14
|
-
*
|
|
15
|
-
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
16
|
-
* @throws {Error} - Upload failed with HTTP error.
|
|
17
|
-
*
|
|
18
|
-
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
19
|
-
*/
|
|
20
7
|
upload(file: File): Promise<void>;
|
|
21
|
-
|
|
8
|
+
/** @deprecated Use webStream instead. */
|
|
9
|
+
stream(options?: ContainerDownloadOptions): Promise<{
|
|
22
10
|
data: Readable;
|
|
23
11
|
mime: string;
|
|
24
12
|
}>;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*/
|
|
28
|
-
webStream(options: StreamOptions): Promise<Response>;
|
|
29
|
-
/** Downloads the full container contents into memory. */
|
|
30
|
-
arrayBuffer(): Promise<{
|
|
13
|
+
webStream(options?: ContainerDownloadOptions): Promise<Response>;
|
|
14
|
+
arrayBuffer(options?: ContainerDownloadOptions): Promise<{
|
|
31
15
|
data: ArrayBuffer;
|
|
32
16
|
mime: string;
|
|
33
17
|
}>;
|
|
34
18
|
}
|
|
35
|
-
export {};
|
|
@@ -1,78 +1,44 @@
|
|
|
1
|
-
import { BaseField } from "./baseField.js";
|
|
2
|
-
import { FMError } from '../../FMError.js';
|
|
3
1
|
import { Readable } from 'node:stream';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { BaseField } from './baseField.js';
|
|
3
|
+
import { ContainerSessionAffinityError } from '../../connection/databaseProvider.js';
|
|
6
4
|
export class ContainerField extends BaseField {
|
|
7
|
-
|
|
8
|
-
constructor(record, id, value) {
|
|
9
|
-
super(record, id, value);
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Uploads a file to the container field.
|
|
13
|
-
*
|
|
14
|
-
* @param {Buffer} file - The file content as a buffer.
|
|
15
|
-
*
|
|
16
|
-
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
17
|
-
* @throws {Error} - Upload failed with HTTP error.
|
|
18
|
-
*
|
|
19
|
-
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
20
|
-
*/
|
|
5
|
+
constructor(record, id, value) { super(record, id, value); }
|
|
21
6
|
async upload(file) {
|
|
22
|
-
if (this.metadata.result !== 'container')
|
|
23
|
-
throw new Error(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const res = await this.parent.layout.database.fetch(`${this.parent.endpoint}/containers/${this.id}/1`, {
|
|
28
|
-
method: 'POST',
|
|
29
|
-
body: form
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
throw await HttpError.new(res);
|
|
33
|
-
}
|
|
34
|
-
const data = ApiResults.parse(await res.json());
|
|
35
|
-
if (data.messages[0].code === 0)
|
|
36
|
-
return;
|
|
37
|
-
else {
|
|
38
|
-
throw new FMError(data.messages[0].code, res.status, res);
|
|
39
|
-
}
|
|
7
|
+
if (this.metadata.result !== 'container')
|
|
8
|
+
throw new Error(`Cannot upload a file to the field; ${this.id} (not a container field)`);
|
|
9
|
+
if (this.parent.recordId === -1)
|
|
10
|
+
throw new Error('Cannot upload to an unsaved record');
|
|
11
|
+
await this.parent.layout.database.execute({ type: 'container.upload', layout: this.parent.layout.name, recordId: this.parent.recordId, field: this.id, file });
|
|
40
12
|
}
|
|
41
|
-
async #
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
13
|
+
async #response(options = {}) {
|
|
14
|
+
if (!this.value)
|
|
15
|
+
throw new Error(`Container field ${this.id} is empty`);
|
|
16
|
+
const binding = this.parent.sessionBinding;
|
|
17
|
+
if (!binding)
|
|
18
|
+
throw new ContainerSessionAffinityError('Container has no originating provider session');
|
|
19
|
+
try {
|
|
20
|
+
const response = await this.parent.layout.database.fetchContainer(this.value, binding, options.signal);
|
|
21
|
+
if (!response.ok || !response.body)
|
|
22
|
+
throw new Error(`Container response failed: ${response.status} (${response.statusText})`);
|
|
23
|
+
return response;
|
|
48
24
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!stream.body) {
|
|
57
|
-
throw await HttpError.new(stream);
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (!(error instanceof ContainerSessionAffinityError) || !options.refreshOnSessionLoss)
|
|
27
|
+
throw error;
|
|
28
|
+
const refreshed = await this.parent.refreshContainer(this.id);
|
|
29
|
+
this.updateFromRawValue(refreshed.value);
|
|
30
|
+
this.parent.sessionBinding = refreshed.parent.sessionBinding;
|
|
31
|
+
return await this.#response({ ...options, refreshOnSessionLoss: false });
|
|
58
32
|
}
|
|
59
|
-
return {
|
|
60
|
-
// @ts-expect-error stream types are correct
|
|
61
|
-
data: Readable.fromWeb(stream.body),
|
|
62
|
-
mime: stream.headers.get('Content-Type') ?? ''
|
|
63
|
-
};
|
|
64
33
|
}
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return await this.#streamAsync(options);
|
|
34
|
+
/** @deprecated Use webStream instead. */
|
|
35
|
+
async stream(options = {}) {
|
|
36
|
+
const response = await this.#response(options);
|
|
37
|
+
return { data: Readable.fromWeb(response.body), mime: response.headers.get('Content-Type') ?? '' };
|
|
70
38
|
}
|
|
71
|
-
|
|
72
|
-
async arrayBuffer() {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
throw await HttpError.new(stream);
|
|
76
|
-
return { data: await stream.arrayBuffer(), mime: stream.headers.get('Content-Type') ?? '' };
|
|
39
|
+
async webStream(options = {}) { return await this.#response(options); }
|
|
40
|
+
async arrayBuffer(options = {}) {
|
|
41
|
+
const response = await this.#response(options);
|
|
42
|
+
return { data: await response.arrayBuffer(), mime: response.headers.get('Content-Type') ?? '' };
|
|
77
43
|
}
|
|
78
44
|
}
|
|
@@ -56,7 +56,7 @@ export class TimeStampField extends ValueFieldBase {
|
|
|
56
56
|
if (typeof value !== 'string')
|
|
57
57
|
throw new Error("Received an invalid value for a temporal field.");
|
|
58
58
|
try {
|
|
59
|
-
return stringToTemporal(value, 'timestamp', this.parent.layout.database.
|
|
59
|
+
return stringToTemporal(value, 'timestamp', this.parent.layout.database.timeStampFormat);
|
|
60
60
|
}
|
|
61
61
|
catch {
|
|
62
62
|
return Temporal.PlainDateTime.from(value);
|
|
@@ -65,7 +65,7 @@ export class TimeStampField extends ValueFieldBase {
|
|
|
65
65
|
serializeRawValue() {
|
|
66
66
|
if (this._value === null)
|
|
67
67
|
return '';
|
|
68
|
-
return temporalToString(this._value, this.parent.layout.database.
|
|
68
|
+
return temporalToString(this._value, this.parent.layout.database.timeStampFormat);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
export class DateField extends ValueFieldBase {
|
|
@@ -75,7 +75,7 @@ export class DateField extends ValueFieldBase {
|
|
|
75
75
|
if (typeof value !== 'string')
|
|
76
76
|
throw new Error("Received an invalid value for a temporal field.");
|
|
77
77
|
try {
|
|
78
|
-
return stringToTemporal(value, 'date', this.parent.layout.database.
|
|
78
|
+
return stringToTemporal(value, 'date', this.parent.layout.database.dateFormat);
|
|
79
79
|
}
|
|
80
80
|
catch {
|
|
81
81
|
return Temporal.PlainDate.from(value);
|
|
@@ -84,7 +84,7 @@ export class DateField extends ValueFieldBase {
|
|
|
84
84
|
serializeRawValue() {
|
|
85
85
|
if (this._value === null)
|
|
86
86
|
return '';
|
|
87
|
-
return temporalToString(this._value, this.parent.layout.database.
|
|
87
|
+
return temporalToString(this._value, this.parent.layout.database.dateFormat);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
export class TimeField extends ValueFieldBase {
|
|
@@ -94,7 +94,7 @@ export class TimeField extends ValueFieldBase {
|
|
|
94
94
|
if (typeof value !== 'string')
|
|
95
95
|
throw new Error("Received an invalid value for a temporal field.");
|
|
96
96
|
try {
|
|
97
|
-
return stringToTemporal(value, 'time', this.parent.layout.database.
|
|
97
|
+
return stringToTemporal(value, 'time', this.parent.layout.database.timeFormat);
|
|
98
98
|
}
|
|
99
99
|
catch {
|
|
100
100
|
return Temporal.PlainTime.from(value);
|
|
@@ -103,6 +103,6 @@ export class TimeField extends ValueFieldBase {
|
|
|
103
103
|
serializeRawValue() {
|
|
104
104
|
if (this._value === null)
|
|
105
105
|
return '';
|
|
106
|
-
return temporalToString(this._value, this.parent.layout.database.
|
|
106
|
+
return temporalToString(this._value, this.parent.layout.database.timeFormat);
|
|
107
107
|
}
|
|
108
108
|
}
|