@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,140 +1,204 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
|
-
*/
|
|
4
|
-
import { FMError } from '../FMError.js';
|
|
5
1
|
import { Layout } from '../layouts/layout.js';
|
|
6
|
-
import {
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
import { addHeaders } from '../utils/addHeaders.js';
|
|
9
|
-
import process from 'node:process';
|
|
10
|
-
/**
|
|
11
|
-
* Represents a database connection.
|
|
12
|
-
* @template T - The structure of the database.
|
|
13
|
-
*/
|
|
2
|
+
import { ContainerSessionAffinityError, ProviderSessionExpiredError } from './databaseProvider.js';
|
|
14
3
|
export class Database {
|
|
15
|
-
host;
|
|
16
4
|
name;
|
|
17
5
|
debug;
|
|
6
|
+
#provider;
|
|
18
7
|
#layoutCache = new Map();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this
|
|
28
|
-
this.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
8
|
+
#abortController = new AbortController();
|
|
9
|
+
#sessions = [];
|
|
10
|
+
#closingSessions = new Set();
|
|
11
|
+
#queue = [];
|
|
12
|
+
#connectionPromise;
|
|
13
|
+
#closed = false;
|
|
14
|
+
#draining = false;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.#provider = options.provider;
|
|
17
|
+
this.name = options.name ?? options.provider.name ?? '';
|
|
18
|
+
this.debug = options.debug ?? false;
|
|
19
|
+
}
|
|
20
|
+
static create(options) {
|
|
21
|
+
return new Database(options);
|
|
22
|
+
}
|
|
23
|
+
async #connection() {
|
|
24
|
+
if (this.#closed)
|
|
25
|
+
throw new Error('Database is closed');
|
|
26
|
+
this.#connectionPromise ??= this.#provider.connect({ signal: this.#abortController.signal, debug: this.debug });
|
|
27
|
+
return await this.#connectionPromise;
|
|
28
|
+
}
|
|
29
|
+
async connect() {
|
|
30
|
+
await this.#connection();
|
|
31
|
+
}
|
|
32
|
+
get dateFormat() {
|
|
33
|
+
return this.#formatting('dateFormat', 'MM/DD/YYYY');
|
|
34
|
+
}
|
|
35
|
+
get timeFormat() {
|
|
36
|
+
return this.#formatting('timeFormat', 'HH:mm:ss');
|
|
37
|
+
}
|
|
38
|
+
get timeStampFormat() {
|
|
39
|
+
return this.#formatting('timeStampFormat', 'MM/DD/YYYY HH:mm:ss');
|
|
40
|
+
}
|
|
41
|
+
#formatting(key, fallback) {
|
|
42
|
+
return this.#provider.formatting[key] ?? fallback;
|
|
43
|
+
}
|
|
44
|
+
async #openSession() {
|
|
45
|
+
const connection = await this.#connection();
|
|
46
|
+
const session = await connection.openSession(this.#abortController.signal);
|
|
47
|
+
if (this.#closed) {
|
|
48
|
+
await session.close();
|
|
49
|
+
throw new Error('Database is closed');
|
|
50
|
+
}
|
|
51
|
+
const active = { binding: { id: Symbol('provider-session') }, session, busy: false, valid: true };
|
|
52
|
+
this.#sessions.push(active);
|
|
53
|
+
return active;
|
|
54
|
+
}
|
|
55
|
+
async #schedule(run, preferred) {
|
|
56
|
+
if (this.#closed)
|
|
57
|
+
throw new Error('Database is closed');
|
|
58
|
+
return await new Promise((resolve, reject) => {
|
|
59
|
+
this.#queue.push({ preferred, run, resolve, reject });
|
|
60
|
+
void this.#drain();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async #drain() {
|
|
64
|
+
if (this.#draining || this.#closed || this.#queue.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
this.#draining = true;
|
|
67
|
+
try {
|
|
68
|
+
const connection = await this.#connection().catch(error => {
|
|
69
|
+
for (const job of this.#queue.splice(0))
|
|
70
|
+
job.reject(error);
|
|
71
|
+
return undefined;
|
|
72
|
+
});
|
|
73
|
+
if (!connection)
|
|
74
|
+
return;
|
|
75
|
+
for (let index = 0; index < this.#queue.length;) {
|
|
76
|
+
const job = this.#queue[index];
|
|
77
|
+
let active = job.preferred
|
|
78
|
+
? this.#sessions.find(item => item.binding.id === job.preferred?.id && item.valid && !item.busy)
|
|
79
|
+
: this.#sessions.find(item => item.valid && !item.busy);
|
|
80
|
+
if (!active && job.preferred) {
|
|
81
|
+
if (!this.#sessions.some(item => item.binding.id === job.preferred?.id && item.valid)) {
|
|
82
|
+
this.#queue.splice(index, 1);
|
|
83
|
+
job.reject(new ContainerSessionAffinityError());
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
index++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!active && !job.preferred && this.#sessions.filter(item => item.valid).length < Math.max(1, connection.maxSessions)) {
|
|
90
|
+
try {
|
|
91
|
+
active = await this.#openSession();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.#queue.splice(index, 1);
|
|
95
|
+
job.reject(error);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!active) {
|
|
100
|
+
index++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
this.#queue.splice(index, 1);
|
|
104
|
+
active.busy = true;
|
|
105
|
+
void job.run(active).then(job.resolve, job.reject).finally(() => {
|
|
106
|
+
active.busy = false;
|
|
107
|
+
void this.#drain();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
this.#draining = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async execute(operation) {
|
|
116
|
+
const attempt = async () => await this.#schedule(async (active) => {
|
|
117
|
+
try {
|
|
118
|
+
return { value: await active.session.execute(operation), binding: active.binding };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error instanceof ProviderSessionExpiredError)
|
|
122
|
+
this.#invalidate(active);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
return await attempt();
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error instanceof ProviderSessionExpiredError && isReadOnly(operation.type))
|
|
131
|
+
return await attempt();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async fetchContainer(reference, binding, signal) {
|
|
136
|
+
return await this.#schedule(async (active) => {
|
|
137
|
+
try {
|
|
138
|
+
return await active.session.fetchContainer(reference, signal);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error instanceof ProviderSessionExpiredError) {
|
|
142
|
+
this.#invalidate(active);
|
|
143
|
+
throw new ContainerSessionAffinityError(undefined, { cause: error });
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}, binding);
|
|
148
|
+
}
|
|
149
|
+
#invalidate(active) {
|
|
150
|
+
if (!active.valid)
|
|
151
|
+
return;
|
|
152
|
+
active.valid = false;
|
|
153
|
+
const index = this.#sessions.indexOf(active);
|
|
154
|
+
if (index >= 0)
|
|
155
|
+
this.#sessions.splice(index, 1);
|
|
156
|
+
const closing = active.session.close().catch(() => {
|
|
157
|
+
});
|
|
158
|
+
this.#closingSessions.add(closing);
|
|
159
|
+
void closing.finally(() => this.#closingSessions.delete(closing));
|
|
32
160
|
}
|
|
33
161
|
async login() {
|
|
34
|
-
await
|
|
162
|
+
await this.connect();
|
|
35
163
|
}
|
|
36
164
|
async logout() {
|
|
37
165
|
await this.close();
|
|
38
166
|
}
|
|
39
167
|
async close() {
|
|
40
|
-
this
|
|
41
|
-
|
|
42
|
-
|
|
168
|
+
if (this.#closed)
|
|
169
|
+
return;
|
|
170
|
+
this.#closed = true;
|
|
171
|
+
this.#abortController.abort('Closing database');
|
|
172
|
+
for (const job of this.#queue.splice(0))
|
|
173
|
+
job.reject(new Error('Database is closed'));
|
|
174
|
+
const connection = await this.#connectionPromise?.catch(() => undefined);
|
|
175
|
+
await Promise.allSettled([...this.#closingSessions, ...this.#sessions.map(active => active.session.close())]);
|
|
176
|
+
this.#sessions.splice(0);
|
|
177
|
+
if (connection?.close)
|
|
178
|
+
await connection.close();
|
|
43
179
|
}
|
|
44
180
|
async [Symbol.asyncDispose]() {
|
|
45
181
|
await this.close();
|
|
46
182
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Returns the endpoint URL for the database connection.
|
|
49
|
-
*
|
|
50
|
-
* @returns {string} The endpoint URL.
|
|
51
|
-
*/
|
|
52
|
-
get endpoint() {
|
|
53
|
-
return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
|
|
54
|
-
}
|
|
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);
|
|
80
|
-
}
|
|
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 };
|
|
89
|
-
}
|
|
90
|
-
const data = ApiResults.parse(rawData);
|
|
91
|
-
if (data.messages[0].code !== 0) {
|
|
92
|
-
throw new FMError(data.messages[0].code, res.status, data);
|
|
93
|
-
}
|
|
94
|
-
// @ts-expect-error is correct
|
|
95
|
-
return {
|
|
96
|
-
httpStatus: res.status
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Retrieves a list of layouts in the current FileMaker database.
|
|
101
|
-
*
|
|
102
|
-
* @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
|
|
103
|
-
* @throws {FMError} If there was an error retrieving the layouts.
|
|
104
|
-
*/
|
|
105
183
|
async listLayouts(page = 0) {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
});
|
|
109
|
-
const cycleLayoutNames = (layouts) => {
|
|
110
|
-
let names = [];
|
|
111
|
-
for (const layout of layouts) {
|
|
112
|
-
if (layout.folderLayoutNames)
|
|
113
|
-
names = names.concat(cycleLayoutNames(layout.folderLayoutNames));
|
|
114
|
-
else
|
|
115
|
-
names.push(layout.name);
|
|
116
|
-
}
|
|
117
|
-
return names;
|
|
118
|
-
};
|
|
119
|
-
return cycleLayoutNames(res.layouts).map(layout => new Layout(this, layout));
|
|
184
|
+
const { value } = await this.execute({ type: 'layout.list', page });
|
|
185
|
+
return value.map(name => new Layout(this, name));
|
|
120
186
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Returns a FileMaker Layout object by name.
|
|
123
|
-
*/
|
|
124
187
|
layout(name) {
|
|
125
188
|
let layout = this.#layoutCache.get(name);
|
|
126
|
-
if (layout)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
189
|
+
if (!layout) {
|
|
190
|
+
layout = new Layout(this, name);
|
|
191
|
+
this.#layoutCache.set(name, layout);
|
|
192
|
+
}
|
|
130
193
|
return layout;
|
|
131
194
|
}
|
|
132
|
-
/** Clears any Layout objects previously returned by `database.layout(...)`. */
|
|
133
195
|
clearLayoutCache() {
|
|
134
196
|
this.#layoutCache.clear();
|
|
135
197
|
}
|
|
136
|
-
/** Creates a script reference you can pass to read and write helpers. */
|
|
137
198
|
script(name, parameter = '') {
|
|
138
199
|
return { name, parameter };
|
|
139
200
|
}
|
|
140
201
|
}
|
|
202
|
+
function isReadOnly(type) {
|
|
203
|
+
return type === 'layout.list' || type === 'layout.metadata' || type === 'record.list' || type === 'record.get';
|
|
204
|
+
}
|
|
@@ -1,28 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type z, type ZodType } from 'zod';
|
|
1
|
+
import type { DatabaseOperation, DatabaseOperationResult, DatabaseOperationType, SessionBinding } from './databaseProvider.js';
|
|
3
2
|
export interface DatabaseBase {
|
|
4
|
-
host: HostBase;
|
|
5
3
|
readonly name: string;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
login: () => Promise<void>;
|
|
11
|
-
/**
|
|
12
|
-
* Immediately closes all open sessions and prevents new ones from being created.
|
|
13
|
-
*/
|
|
4
|
+
readonly timeFormat: string;
|
|
5
|
+
readonly dateFormat: string;
|
|
6
|
+
readonly timeStampFormat: string;
|
|
7
|
+
connect: () => Promise<void>;
|
|
14
8
|
close: () => Promise<void>;
|
|
15
|
-
/**
|
|
16
|
-
* Immediately closes all open sessions and prevents new ones from being created.
|
|
17
|
-
* Alias of {@link close}
|
|
18
|
-
*/
|
|
19
9
|
logout: () => Promise<void>;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}) => Promise<T extends ZodType ? z.infer<T> & {
|
|
24
|
-
httpStatus: number;
|
|
25
|
-
} : {
|
|
26
|
-
httpStatus: number;
|
|
10
|
+
execute: <K extends DatabaseOperationType>(operation: DatabaseOperation<K>) => Promise<{
|
|
11
|
+
value: DatabaseOperationResult<K>;
|
|
12
|
+
binding: SessionBinding;
|
|
27
13
|
}>;
|
|
14
|
+
fetchContainer: (reference: string, binding: SessionBinding, signal?: AbortSignal) => Promise<Response>;
|
|
28
15
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { ApiFieldData, ApiLayoutMetadata, ApiPortalData } from '../models/apiResults.js';
|
|
3
|
+
export interface DatabaseFormatting {
|
|
4
|
+
readonly dateFormat: string;
|
|
5
|
+
readonly timeFormat: string;
|
|
6
|
+
readonly timeStampFormat: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ProviderContext {
|
|
9
|
+
readonly signal: AbortSignal;
|
|
10
|
+
readonly debug: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface DatabaseProvider {
|
|
13
|
+
readonly name?: string;
|
|
14
|
+
/** Synchronous formatting defaults, refined in place when connect resolves. */
|
|
15
|
+
readonly formatting: DatabaseFormatting;
|
|
16
|
+
connect: (context: ProviderContext) => Promise<ProviderConnection>;
|
|
17
|
+
}
|
|
18
|
+
export interface ProviderConnection {
|
|
19
|
+
readonly formatting: DatabaseFormatting;
|
|
20
|
+
readonly maxSessions: number;
|
|
21
|
+
openSession: (signal: AbortSignal) => Promise<ProviderSession>;
|
|
22
|
+
close?: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface ProviderSession {
|
|
25
|
+
execute: <K extends DatabaseOperationType>(operation: DatabaseOperation<K>) => Promise<DatabaseOperationResult<K>>;
|
|
26
|
+
/** Use this exact session. Non-HTTP transports may synthesize a Web Response. */
|
|
27
|
+
fetchContainer: (reference: string, signal?: AbortSignal) => Promise<Response>;
|
|
28
|
+
close: () => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface ProviderRecord {
|
|
31
|
+
recordId: string;
|
|
32
|
+
modId: string;
|
|
33
|
+
fieldData: z.infer<typeof ApiFieldData>;
|
|
34
|
+
portalData?: z.infer<typeof ApiPortalData>;
|
|
35
|
+
}
|
|
36
|
+
export interface RecordQueryClause {
|
|
37
|
+
omit: boolean;
|
|
38
|
+
fields: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
export interface RecordOperationOptions {
|
|
41
|
+
scripts: {
|
|
42
|
+
prerequest?: {
|
|
43
|
+
name: string;
|
|
44
|
+
parameter: string;
|
|
45
|
+
};
|
|
46
|
+
presort?: {
|
|
47
|
+
name: string;
|
|
48
|
+
parameter: string;
|
|
49
|
+
};
|
|
50
|
+
after?: {
|
|
51
|
+
name: string;
|
|
52
|
+
parameter: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
portals: Record<string, {
|
|
56
|
+
limit: number;
|
|
57
|
+
offset: number;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
export interface RecordQueryOptions extends RecordOperationOptions {
|
|
61
|
+
limit: number;
|
|
62
|
+
offset: number;
|
|
63
|
+
sort: Array<{
|
|
64
|
+
fieldName: string;
|
|
65
|
+
sortOrder: 'ascend' | 'descend';
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
export interface RecordWriteBody {
|
|
69
|
+
fieldData: Record<string, unknown>;
|
|
70
|
+
portalData?: Record<string, unknown>;
|
|
71
|
+
options?: Record<string, unknown>;
|
|
72
|
+
deleteRelatedRecords?: Array<{
|
|
73
|
+
table: string;
|
|
74
|
+
recordId: number;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
export interface DatabaseOperationMap {
|
|
78
|
+
'layout.list': {
|
|
79
|
+
operation: {
|
|
80
|
+
page: number;
|
|
81
|
+
};
|
|
82
|
+
result: string[];
|
|
83
|
+
};
|
|
84
|
+
'layout.metadata': {
|
|
85
|
+
operation: {
|
|
86
|
+
layout: string;
|
|
87
|
+
};
|
|
88
|
+
result: z.infer<typeof ApiLayoutMetadata>;
|
|
89
|
+
};
|
|
90
|
+
'script.run': {
|
|
91
|
+
operation: {
|
|
92
|
+
layout: string;
|
|
93
|
+
script: string;
|
|
94
|
+
parameter?: string;
|
|
95
|
+
};
|
|
96
|
+
result: {
|
|
97
|
+
scriptError?: number;
|
|
98
|
+
scriptResult?: string;
|
|
99
|
+
status?: number;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
'record.list': {
|
|
103
|
+
operation: {
|
|
104
|
+
layout: string;
|
|
105
|
+
query: RecordQueryClause[];
|
|
106
|
+
options: RecordQueryOptions;
|
|
107
|
+
};
|
|
108
|
+
result: ProviderRecord[];
|
|
109
|
+
};
|
|
110
|
+
'record.get': {
|
|
111
|
+
operation: {
|
|
112
|
+
layout: string;
|
|
113
|
+
recordId: number;
|
|
114
|
+
options: RecordOperationOptions;
|
|
115
|
+
};
|
|
116
|
+
result: ProviderRecord;
|
|
117
|
+
};
|
|
118
|
+
'record.create': {
|
|
119
|
+
operation: {
|
|
120
|
+
layout: string;
|
|
121
|
+
body: RecordWriteBody;
|
|
122
|
+
options: RecordOperationOptions;
|
|
123
|
+
};
|
|
124
|
+
result: {
|
|
125
|
+
recordId: string;
|
|
126
|
+
modId: string;
|
|
127
|
+
scriptError?: number;
|
|
128
|
+
status?: number;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
'record.update': {
|
|
132
|
+
operation: {
|
|
133
|
+
layout: string;
|
|
134
|
+
recordId: number;
|
|
135
|
+
body: RecordWriteBody;
|
|
136
|
+
options: RecordOperationOptions;
|
|
137
|
+
};
|
|
138
|
+
result: {
|
|
139
|
+
modId: string;
|
|
140
|
+
scriptError?: number;
|
|
141
|
+
status?: number;
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
'record.duplicate': {
|
|
145
|
+
operation: {
|
|
146
|
+
layout: string;
|
|
147
|
+
recordId: number;
|
|
148
|
+
options: RecordOperationOptions;
|
|
149
|
+
};
|
|
150
|
+
result: {
|
|
151
|
+
recordId: string;
|
|
152
|
+
modId: string;
|
|
153
|
+
scriptError?: number;
|
|
154
|
+
status?: number;
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
'record.delete': {
|
|
158
|
+
operation: {
|
|
159
|
+
layout: string;
|
|
160
|
+
recordId: number;
|
|
161
|
+
options: RecordOperationOptions;
|
|
162
|
+
};
|
|
163
|
+
result: {
|
|
164
|
+
scriptError?: number;
|
|
165
|
+
status?: number;
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
'container.upload': {
|
|
169
|
+
operation: {
|
|
170
|
+
layout: string;
|
|
171
|
+
recordId: number;
|
|
172
|
+
field: string;
|
|
173
|
+
file: File;
|
|
174
|
+
};
|
|
175
|
+
result: undefined;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export type DatabaseOperationType = keyof DatabaseOperationMap;
|
|
179
|
+
export type DatabaseOperation<K extends DatabaseOperationType = DatabaseOperationType> = K extends DatabaseOperationType ? {
|
|
180
|
+
type: K;
|
|
181
|
+
} & DatabaseOperationMap[K]['operation'] : never;
|
|
182
|
+
export type DatabaseOperationResult<K extends DatabaseOperationType> = DatabaseOperationMap[K]['result'];
|
|
183
|
+
export declare class ProviderSessionExpiredError extends Error {
|
|
184
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
185
|
+
}
|
|
186
|
+
export declare class ContainerSessionAffinityError extends Error {
|
|
187
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
188
|
+
}
|
|
189
|
+
export interface ContainerDownloadOptions {
|
|
190
|
+
signal?: AbortSignal;
|
|
191
|
+
refreshOnSessionLoss?: boolean;
|
|
192
|
+
}
|
|
193
|
+
/** @internal */
|
|
194
|
+
export interface SessionBinding {
|
|
195
|
+
readonly id: symbol;
|
|
196
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export class ProviderSessionExpiredError extends Error {
|
|
2
|
+
constructor(message = 'Provider session expired', options) { super(message, options); this.name = 'ProviderSessionExpiredError'; }
|
|
3
|
+
}
|
|
4
|
+
export class ContainerSessionAffinityError extends Error {
|
|
5
|
+
constructor(message = 'The session associated with this container is no longer available', options) { super(message, options); this.name = 'ContainerSessionAffinityError'; }
|
|
6
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { Database } from './database.js';
|
|
2
1
|
import type { DatabaseStructure } from '../databaseStructure.js';
|
|
3
|
-
import { type Session } from './Session.js';
|
|
4
2
|
import { type databaseOptionsWithExternalSources, type loginOptionsFileMaker } from '../types.js';
|
|
5
3
|
import type FMHost from './FMHost.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
export declare class DatabaseSessionPool<T extends DatabaseStructure
|
|
4
|
+
import { type DatabaseEndpoint } from './Session.js';
|
|
5
|
+
import { type DatabaseProvider, type DatabaseExecutor } from './databaseProvider.js';
|
|
6
|
+
/** Reserves independent FileMaker Data API sessions from a bounded pool. */
|
|
7
|
+
export declare class DatabaseSessionPool<T extends DatabaseStructure = DatabaseStructure> implements DatabaseProvider {
|
|
10
8
|
#private;
|
|
9
|
+
readonly host: FMHost;
|
|
11
10
|
constructor(host: FMHost, connectionDetails: databaseOptionsWithExternalSources<loginOptionsFileMaker>);
|
|
11
|
+
get formatting(): FMHost;
|
|
12
|
+
get endpoint(): DatabaseEndpoint;
|
|
13
|
+
withSession<R>(callback: (executor: DatabaseExecutor) => Promise<R>): Promise<R>;
|
|
12
14
|
close(): Promise<void>;
|
|
13
|
-
withSession<T>(callback: (session: Session) => Promise<T>, retry?: boolean): Promise<T>;
|
|
14
15
|
}
|