@jd-data-limited/easy-fm 5.0.0-beta.2 → 5.0.0-beta.5
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 +5 -0
- package/dist/bin/init.js +43 -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 +6 -0
- package/dist/typegen/generate.js +57 -0
- package/dist/typegen/index.d.ts +6 -0
- package/dist/typegen/index.js +6 -0
- package/dist/typegen/init.d.ts +20 -0
- package/dist/typegen/init.js +141 -0
- package/dist/typegen/types.d.ts +49 -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
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { FMError } from '../FMError.js';
|
|
2
|
+
import { FMHostMetadata } from '../types.js';
|
|
3
|
+
import { ApiResults } from '../models/apiResults.js';
|
|
4
|
+
import { CookieJar } from './CookieJar.js';
|
|
5
|
+
import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js';
|
|
6
|
+
import { addHeaders } from '../utils/addHeaders.js';
|
|
7
|
+
import { ProviderSessionExpiredError } from './databaseProvider.js';
|
|
8
|
+
export class DataApiProvider {
|
|
9
|
+
name;
|
|
10
|
+
formatting = { dateFormat: 'MM/DD/YYYY', timeFormat: 'HH:mm:ss', timeStampFormat: 'MM/DD/YYYY HH:mm:ss' };
|
|
11
|
+
#options;
|
|
12
|
+
#origin;
|
|
13
|
+
#endpoint;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
const host = new URL(options.hostname);
|
|
16
|
+
if (host.protocol !== 'http:' && host.protocol !== 'https:')
|
|
17
|
+
throw new Error('hostname MUST begin with either http:// or https://');
|
|
18
|
+
this.#options = options;
|
|
19
|
+
this.name = options.database;
|
|
20
|
+
this.#origin = host.origin;
|
|
21
|
+
this.#endpoint = `${host.origin}/fmi/data/v2/databases/${encodeURIComponent(options.database)}`;
|
|
22
|
+
}
|
|
23
|
+
async connect(context) {
|
|
24
|
+
const response = await fetch(`${this.#origin}/fmi/data/v2/productInfo`, { signal: context.signal });
|
|
25
|
+
const raw = await response.json();
|
|
26
|
+
const parsed = ApiResults.extend({ response: FMHostMetadata.optional() }).parse(raw);
|
|
27
|
+
if (parsed.messages[0].code !== 0 || !parsed.response)
|
|
28
|
+
throw new FMError(parsed.messages[0].code, response.status, parsed);
|
|
29
|
+
const info = parsed.response.productInfo;
|
|
30
|
+
Object.assign(this.formatting, {
|
|
31
|
+
dateFormat: info.dateFormat.replace('dd', 'DD').replace('yyyy', 'YYYY'),
|
|
32
|
+
timeFormat: info.timeFormat,
|
|
33
|
+
timeStampFormat: info.timeStampFormat.replace('dd', 'DD').replace('yyyy', 'YYYY')
|
|
34
|
+
});
|
|
35
|
+
const credentials = this.#options.credentials;
|
|
36
|
+
const maxSessions = credentials.method === 'filemaker' ? credentials.sessionPoolSize ?? 8 : 1;
|
|
37
|
+
return {
|
|
38
|
+
formatting: this.formatting,
|
|
39
|
+
maxSessions,
|
|
40
|
+
openSession: async (signal) => await this.#openSession(signal, context.debug || this.#options.debug === true)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async #openSession(signal, debug) {
|
|
44
|
+
const credentials = this.#options.credentials;
|
|
45
|
+
if (this.#options.externalSources.length && credentials.method !== 'filemaker') {
|
|
46
|
+
throw new Error("External sources are only supported with the 'filemaker' login method");
|
|
47
|
+
}
|
|
48
|
+
if (credentials.method === 'token')
|
|
49
|
+
return new DataApiSession(this.#endpoint, credentials.token, new CookieJar(), signal, debug, false);
|
|
50
|
+
const url = new URL(`${this.#endpoint}/sessions`);
|
|
51
|
+
const jar = new CookieJar();
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: generateAuthorizationHeaders(credentials),
|
|
55
|
+
body: JSON.stringify({ fmDataSource: this.#options.externalSources.map(source => ({ database: source.database, username: source.credentials.username, password: source.credentials.password })) }),
|
|
56
|
+
signal
|
|
57
|
+
});
|
|
58
|
+
for (const cookie of response.headers.getSetCookie())
|
|
59
|
+
jar.addCookie(url, cookie);
|
|
60
|
+
const raw = await response.json();
|
|
61
|
+
const parsed = ApiResults.parse(raw);
|
|
62
|
+
if (!response.ok || parsed.messages[0].code !== 0)
|
|
63
|
+
throw new FMError(parsed.messages[0].code, response.status, parsed);
|
|
64
|
+
return new DataApiSession(this.#endpoint, response.headers.get('x-fm-data-access-token') ?? '', jar, signal, debug, true);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
class DataApiSession {
|
|
68
|
+
endpoint;
|
|
69
|
+
token;
|
|
70
|
+
cookies;
|
|
71
|
+
databaseSignal;
|
|
72
|
+
debug;
|
|
73
|
+
ownsToken;
|
|
74
|
+
#closed = false;
|
|
75
|
+
constructor(endpoint, token, cookies, databaseSignal, debug, ownsToken) {
|
|
76
|
+
this.endpoint = endpoint;
|
|
77
|
+
this.token = token;
|
|
78
|
+
this.cookies = cookies;
|
|
79
|
+
this.databaseSignal = databaseSignal;
|
|
80
|
+
this.debug = debug;
|
|
81
|
+
this.ownsToken = ownsToken;
|
|
82
|
+
}
|
|
83
|
+
async execute(operation) {
|
|
84
|
+
let path;
|
|
85
|
+
let options = {};
|
|
86
|
+
switch (operation.type) {
|
|
87
|
+
case 'layout.list':
|
|
88
|
+
path = `/layouts?page=${encodeURIComponent(operation.page)}`;
|
|
89
|
+
break;
|
|
90
|
+
case 'layout.metadata':
|
|
91
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}`;
|
|
92
|
+
break;
|
|
93
|
+
case 'script.run':
|
|
94
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}/script/${encodeURIComponent(operation.script)}`;
|
|
95
|
+
if (operation.parameter)
|
|
96
|
+
path += `?script.param=${encodeURIComponent(operation.parameter)}`;
|
|
97
|
+
break;
|
|
98
|
+
case 'record.list': {
|
|
99
|
+
const request = operation.options;
|
|
100
|
+
if (!operation.query.length) {
|
|
101
|
+
const params = new URLSearchParams({ _limit: String(request.limit), _offset: String(request.offset), dateformats: '2', portal: JSON.stringify(Object.keys(request.portals)) });
|
|
102
|
+
if (request.sort.length)
|
|
103
|
+
params.set('_sort', JSON.stringify(request.sort));
|
|
104
|
+
addScriptParams(params, request.scripts);
|
|
105
|
+
addPortalParams(params, request.portals);
|
|
106
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}/records?${params}`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const body = { limit: String(request.limit), offset: String(request.offset), dateformats: 2, query: operation.query.map(clause => ({ ...clause.fields, ...(clause.omit ? { omit: 'true' } : {}) })), portal: Object.keys(request.portals) };
|
|
110
|
+
if (request.sort.length)
|
|
111
|
+
body.sort = request.sort;
|
|
112
|
+
addScriptBody(body, request.scripts);
|
|
113
|
+
addPortalBody(body, request.portals);
|
|
114
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}/_find`;
|
|
115
|
+
options = { method: 'POST', body: JSON.stringify(body) };
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'record.get':
|
|
120
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}?${recordParams(operation.options)}`;
|
|
121
|
+
break;
|
|
122
|
+
case 'record.create':
|
|
123
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}/records`;
|
|
124
|
+
options = { method: 'POST', body: JSON.stringify(recordBody(operation.body, operation.options)) };
|
|
125
|
+
break;
|
|
126
|
+
case 'record.update':
|
|
127
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}`;
|
|
128
|
+
options = { method: 'PATCH', body: JSON.stringify(recordBody(operation.body, operation.options)) };
|
|
129
|
+
break;
|
|
130
|
+
case 'record.duplicate':
|
|
131
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}?${recordParams(operation.options)}`;
|
|
132
|
+
options = { method: 'POST' };
|
|
133
|
+
break;
|
|
134
|
+
case 'record.delete':
|
|
135
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}?${recordParams(operation.options)}`;
|
|
136
|
+
options = { method: 'DELETE' };
|
|
137
|
+
break;
|
|
138
|
+
case 'container.upload': {
|
|
139
|
+
path = `/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}/containers/${encodeURIComponent(operation.field)}/1`;
|
|
140
|
+
const form = new FormData();
|
|
141
|
+
form.append('upload', operation.file);
|
|
142
|
+
options = { method: 'POST', body: form };
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (operation.type !== 'container.upload')
|
|
147
|
+
addHeaders(options, { 'Content-Type': 'application/json' });
|
|
148
|
+
const { response, body } = await this.#requestJSON(`${this.endpoint}${path}`, options);
|
|
149
|
+
const value = normalize(operation, body.response, response.status);
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
async #requestJSON(url, options) {
|
|
153
|
+
const response = await this.#request(url, options);
|
|
154
|
+
const body = await response.json();
|
|
155
|
+
if (this.debug)
|
|
156
|
+
console.log(body.response);
|
|
157
|
+
const parsed = ApiResults.passthrough().parse(body);
|
|
158
|
+
if (parsed.messages[0].code !== 0)
|
|
159
|
+
throw new FMError(parsed.messages[0].code, response.status, parsed);
|
|
160
|
+
return { response, body: parsed };
|
|
161
|
+
}
|
|
162
|
+
async #request(input, init = {}) {
|
|
163
|
+
let url = new URL(input);
|
|
164
|
+
let options = { ...init };
|
|
165
|
+
options.signal = options.signal ? AbortSignal.any([options.signal, this.databaseSignal]) : this.databaseSignal;
|
|
166
|
+
for (let count = 0; count < 10; count++) {
|
|
167
|
+
const headers = new Headers(options.headers);
|
|
168
|
+
headers.set('Authorization', `Bearer ${this.token}`);
|
|
169
|
+
const cookie = this.cookies.getCookieHeader(url);
|
|
170
|
+
if (cookie)
|
|
171
|
+
headers.set('Cookie', cookie);
|
|
172
|
+
const response = await fetch(url, { ...options, headers, redirect: 'manual' });
|
|
173
|
+
for (const value of response.headers.getSetCookie())
|
|
174
|
+
this.cookies.addCookie(url, value);
|
|
175
|
+
if (response.status === 401)
|
|
176
|
+
throw new ProviderSessionExpiredError();
|
|
177
|
+
if (response.status >= 300 && response.status < 400) {
|
|
178
|
+
const location = response.headers.get('location');
|
|
179
|
+
if (!location)
|
|
180
|
+
throw new Error(`Redirect response missing Location header for ${url}`);
|
|
181
|
+
url = new URL(location, url);
|
|
182
|
+
const method = (options.method ?? 'GET').toUpperCase();
|
|
183
|
+
if (response.status === 303 || ((response.status === 301 || response.status === 302) && method === 'POST'))
|
|
184
|
+
options = { ...options, method: 'GET', body: undefined };
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
return response;
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`Too many redirects while fetching ${url}`);
|
|
190
|
+
}
|
|
191
|
+
async fetchContainer(reference, signal) { return await this.#request(reference, { signal }); }
|
|
192
|
+
async close() {
|
|
193
|
+
if (this.#closed)
|
|
194
|
+
return;
|
|
195
|
+
this.#closed = true;
|
|
196
|
+
if (!this.ownsToken)
|
|
197
|
+
return;
|
|
198
|
+
await fetch(`${this.endpoint}/sessions/${this.token}`, { method: 'DELETE' }).catch(() => { });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function normalize(operation, response, status) {
|
|
202
|
+
switch (operation.type) {
|
|
203
|
+
case 'layout.list': return flattenLayouts(response?.layouts ?? []);
|
|
204
|
+
case 'layout.metadata': return response;
|
|
205
|
+
case 'script.run': return { scriptError: Number(response?.scriptError ?? 0), scriptResult: response?.scriptResult, status };
|
|
206
|
+
case 'record.list': return response?.data ?? [];
|
|
207
|
+
case 'record.get': return response?.data?.[0];
|
|
208
|
+
case 'record.create':
|
|
209
|
+
case 'record.update':
|
|
210
|
+
case 'record.duplicate':
|
|
211
|
+
case 'record.delete': return { ...(response ?? {}), scriptError: response?.scriptError === undefined ? undefined : Number(response.scriptError), status };
|
|
212
|
+
case 'container.upload': return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function flattenLayouts(layouts) { return layouts.flatMap(item => item.folderLayoutNames ? flattenLayouts(item.folderLayoutNames) : [item.name]); }
|
|
216
|
+
function recordParams(options) { const params = new URLSearchParams(); addScriptParams(params, options.scripts); addPortalParams(params, options.portals); return params; }
|
|
217
|
+
function recordBody(body, options) {
|
|
218
|
+
const result = { ...body };
|
|
219
|
+
if (Array.isArray(result.deleteRelatedRecords)) {
|
|
220
|
+
result.fieldData = { ...(result.fieldData ?? {}), deleteRelated: result.deleteRelatedRecords.map((item) => `${item.table}.${item.recordId}`) };
|
|
221
|
+
delete result.deleteRelatedRecords;
|
|
222
|
+
}
|
|
223
|
+
addScriptBody(result, options.scripts);
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
function addScriptParams(params, scripts) { for (const [key, script] of Object.entries(scripts)) {
|
|
227
|
+
const name = key === 'after' ? 'script' : `script.${key}`;
|
|
228
|
+
params.set(name, script.name);
|
|
229
|
+
if (script.parameter)
|
|
230
|
+
params.set(`${name}.param`, script.parameter);
|
|
231
|
+
} }
|
|
232
|
+
function addScriptBody(body, scripts) { for (const [key, script] of Object.entries(scripts)) {
|
|
233
|
+
const name = key === 'after' ? 'script' : `script.${key}`;
|
|
234
|
+
body[name] = script.name;
|
|
235
|
+
if (script.parameter)
|
|
236
|
+
body[`${name}.param`] = script.parameter;
|
|
237
|
+
} }
|
|
238
|
+
function addPortalParams(params, portals) { const names = Object.keys(portals); if (names.length)
|
|
239
|
+
params.set('portal', JSON.stringify(names)); for (const [name, page] of Object.entries(portals)) {
|
|
240
|
+
params.set(`_offset.${name}`, String(page.offset));
|
|
241
|
+
params.set(`_limit.${name}`, String(page.limit));
|
|
242
|
+
} }
|
|
243
|
+
function addPortalBody(body, portals) { for (const [name, page] of Object.entries(portals)) {
|
|
244
|
+
body[`offset.${name}`] = page.offset;
|
|
245
|
+
body[`limit.${name}`] = page.limit;
|
|
246
|
+
} }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DatabaseOperation, DatabaseOperationResult, DatabaseExecutor } from './databaseProvider.js';
|
|
2
|
+
import type { Session } from './Session.js';
|
|
3
|
+
/** Maps transport-neutral database operations onto FileMaker Data API HTTP. */
|
|
4
|
+
export declare class DataApiDatabaseSession implements DatabaseExecutor {
|
|
5
|
+
private readonly endpoint;
|
|
6
|
+
private readonly session;
|
|
7
|
+
private readonly debug;
|
|
8
|
+
constructor(endpoint: string, session: Session, debug?: boolean);
|
|
9
|
+
execute(operation: DatabaseOperation): Promise<DatabaseOperationResult>;
|
|
10
|
+
downloadContainer(url: string | URL, signal?: AbortSignal): Promise<Response>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { addHeaders } from '../utils/addHeaders.js';
|
|
2
|
+
/** Maps transport-neutral database operations onto FileMaker Data API HTTP. */
|
|
3
|
+
export class DataApiDatabaseSession {
|
|
4
|
+
endpoint;
|
|
5
|
+
session;
|
|
6
|
+
debug;
|
|
7
|
+
constructor(endpoint, session, debug = false) {
|
|
8
|
+
this.endpoint = endpoint;
|
|
9
|
+
this.session = session;
|
|
10
|
+
this.debug = debug;
|
|
11
|
+
}
|
|
12
|
+
async execute(operation) {
|
|
13
|
+
let url;
|
|
14
|
+
let options = {};
|
|
15
|
+
switch (operation.type) {
|
|
16
|
+
case 'layout.list':
|
|
17
|
+
url = `${this.endpoint}/layouts?page=${encodeURIComponent(operation.page)}`;
|
|
18
|
+
break;
|
|
19
|
+
case 'layout.metadata':
|
|
20
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}`;
|
|
21
|
+
break;
|
|
22
|
+
case 'script.run':
|
|
23
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}/script/${encodeURIComponent(operation.script)}`;
|
|
24
|
+
if (operation.parameter)
|
|
25
|
+
url += `?script.param=${encodeURIComponent(operation.parameter)}`;
|
|
26
|
+
break;
|
|
27
|
+
case 'record.list': {
|
|
28
|
+
const request = operation.options;
|
|
29
|
+
if (operation.query.length === 0) {
|
|
30
|
+
const params = new URLSearchParams({
|
|
31
|
+
_limit: request.limit.toString(),
|
|
32
|
+
_offset: request.offset.toString(),
|
|
33
|
+
dateformats: '2',
|
|
34
|
+
portal: JSON.stringify(Object.keys(request.portals))
|
|
35
|
+
});
|
|
36
|
+
if (request.sort.length)
|
|
37
|
+
params.set('_sort', JSON.stringify(request.sort));
|
|
38
|
+
addScriptParams(params, request.scripts);
|
|
39
|
+
for (const [portal, paging] of Object.entries(request.portals)) {
|
|
40
|
+
params.set(`_offset.${portal}`, paging.offset.toString());
|
|
41
|
+
params.set(`_limit.${portal}`, paging.limit.toString());
|
|
42
|
+
}
|
|
43
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}/records?${params}`;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const body = {
|
|
47
|
+
limit: request.limit.toString(),
|
|
48
|
+
offset: request.offset.toString(),
|
|
49
|
+
dateformats: 2,
|
|
50
|
+
query: operation.query.map(clause => ({
|
|
51
|
+
...clause.fields,
|
|
52
|
+
...(clause.omit ? { omit: 'true' } : {})
|
|
53
|
+
})),
|
|
54
|
+
portal: Object.keys(request.portals)
|
|
55
|
+
};
|
|
56
|
+
if (request.sort.length)
|
|
57
|
+
body.sort = request.sort;
|
|
58
|
+
addScriptBody(body, request.scripts);
|
|
59
|
+
for (const [portal, paging] of Object.entries(request.portals)) {
|
|
60
|
+
body[`offset.${portal}`] = paging.offset;
|
|
61
|
+
body[`limit.${portal}`] = paging.limit;
|
|
62
|
+
}
|
|
63
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}/_find`;
|
|
64
|
+
options = { method: 'POST', body: JSON.stringify(body) };
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'record.get':
|
|
69
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}?${recordParams(operation.options)}`;
|
|
70
|
+
break;
|
|
71
|
+
case 'record.create':
|
|
72
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}/records`;
|
|
73
|
+
options = { method: 'POST', body: JSON.stringify(recordBody(operation.body, operation.options)) };
|
|
74
|
+
break;
|
|
75
|
+
case 'record.update':
|
|
76
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}`;
|
|
77
|
+
options = { method: 'PATCH', body: JSON.stringify(recordBody(operation.body, operation.options)) };
|
|
78
|
+
break;
|
|
79
|
+
case 'record.duplicate':
|
|
80
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}?${recordParams(operation.options)}`;
|
|
81
|
+
options = { method: 'POST' };
|
|
82
|
+
break;
|
|
83
|
+
case 'record.delete':
|
|
84
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}?${recordParams(operation.options)}`;
|
|
85
|
+
options = { method: 'DELETE' };
|
|
86
|
+
break;
|
|
87
|
+
case 'container.upload': {
|
|
88
|
+
url = `${this.endpoint}/layouts/${encodeURIComponent(operation.layout)}/records/${operation.recordId}/containers/${encodeURIComponent(operation.field)}/1`;
|
|
89
|
+
const form = new FormData();
|
|
90
|
+
form.append('upload', operation.file);
|
|
91
|
+
options = { method: 'POST', body: form };
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (operation.type !== 'container.upload')
|
|
96
|
+
addHeaders(options, { 'Content-Type': 'application/json' });
|
|
97
|
+
const response = await this.session.fetch(url, options);
|
|
98
|
+
const body = await response.json();
|
|
99
|
+
if (this.debug)
|
|
100
|
+
console.log(body.response);
|
|
101
|
+
return { body, status: response.status };
|
|
102
|
+
}
|
|
103
|
+
async downloadContainer(url, signal) {
|
|
104
|
+
return await this.session.fetch(url, { signal });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function recordParams(options) {
|
|
108
|
+
const params = new URLSearchParams();
|
|
109
|
+
addScriptParams(params, options.scripts);
|
|
110
|
+
const portals = Object.keys(options.portals);
|
|
111
|
+
if (portals.length)
|
|
112
|
+
params.set('portal', JSON.stringify(portals));
|
|
113
|
+
for (const [portal, paging] of Object.entries(options.portals)) {
|
|
114
|
+
params.set(`_offset.${portal}`, paging.offset.toString());
|
|
115
|
+
params.set(`_limit.${portal}`, paging.limit.toString());
|
|
116
|
+
}
|
|
117
|
+
return params;
|
|
118
|
+
}
|
|
119
|
+
function recordBody(body, options) {
|
|
120
|
+
const result = body && typeof body === 'object' ? { ...body } : {};
|
|
121
|
+
addScriptBody(result, options.scripts);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function addScriptParams(params, scripts) {
|
|
125
|
+
for (const [key, script] of Object.entries(scripts)) {
|
|
126
|
+
const apiKey = key === 'after' ? 'script' : `script.${key}`;
|
|
127
|
+
params.set(apiKey, script.name);
|
|
128
|
+
if (script.parameter)
|
|
129
|
+
params.set(`${apiKey}.param`, script.parameter);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function addScriptBody(body, scripts) {
|
|
133
|
+
for (const [key, script] of Object.entries(scripts)) {
|
|
134
|
+
const apiKey = key === 'after' ? 'script' : `script.${key}`;
|
|
135
|
+
body[apiKey] = script.name;
|
|
136
|
+
if (script.parameter)
|
|
137
|
+
body[`${apiKey}.param`] = script.parameter;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -1,70 +1,36 @@
|
|
|
1
|
-
import { type LayoutInterface } from '../layouts/layoutInterface.js';
|
|
2
1
|
import { Layout } from '../layouts/layout.js';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export declare
|
|
2
|
+
import type { LayoutInterface } from '../layouts/layoutInterface.js';
|
|
3
|
+
import type { DatabaseStructure } from '../databaseStructure.js';
|
|
4
|
+
import type { Script } from '../types.js';
|
|
5
|
+
import type { DatabaseBase } from './databaseBase.js';
|
|
6
|
+
import { type DatabaseOperation, type DatabaseOperationResult, type DatabaseOperationType, type DatabaseProvider, type SessionBinding } from './databaseProvider.js';
|
|
7
|
+
export interface DatabaseOptions {
|
|
8
|
+
provider: DatabaseProvider;
|
|
9
|
+
name?: string;
|
|
10
|
+
debug?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class Database<T extends DatabaseStructure> implements DatabaseBase {
|
|
14
13
|
#private;
|
|
15
|
-
readonly host: HostBase;
|
|
16
14
|
readonly name: string;
|
|
17
15
|
readonly debug: boolean;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
private constructor();
|
|
17
|
+
static create<T extends DatabaseStructure = DatabaseStructure>(options: DatabaseOptions): Database<T>;
|
|
18
|
+
connect(): Promise<void>;
|
|
19
|
+
get dateFormat(): string;
|
|
20
|
+
get timeFormat(): string;
|
|
21
|
+
get timeStampFormat(): string;
|
|
22
|
+
execute<K extends DatabaseOperationType>(operation: DatabaseOperation<K>): Promise<{
|
|
23
|
+
value: DatabaseOperationResult<K>;
|
|
24
|
+
binding: SessionBinding;
|
|
25
|
+
}>;
|
|
26
|
+
fetchContainer(reference: string, binding: SessionBinding, signal?: AbortSignal): Promise<Response>;
|
|
25
27
|
login(): Promise<void>;
|
|
26
28
|
logout(): Promise<void>;
|
|
27
29
|
close(): Promise<void>;
|
|
28
30
|
[Symbol.asyncDispose](): Promise<void>;
|
|
29
|
-
/**
|
|
30
|
-
* The inheriting database class must implement this method to provide a session object.
|
|
31
|
-
* @param callback
|
|
32
|
-
* @protected
|
|
33
|
-
*/
|
|
34
|
-
protected abstract withSession<T>(callback: (session: Session) => Promise<T>): Promise<T>;
|
|
35
|
-
/**
|
|
36
|
-
* Returns the endpoint URL for the database connection.
|
|
37
|
-
*
|
|
38
|
-
* @returns {string} The endpoint URL.
|
|
39
|
-
*/
|
|
40
|
-
get endpoint(): DatabaseEndpoint;
|
|
41
|
-
/**
|
|
42
|
-
* Uses an available session to run a fetch
|
|
43
|
-
* @param url
|
|
44
|
-
* @param options
|
|
45
|
-
*/
|
|
46
|
-
fetch(url: string | URL, options?: RequestInit): Promise<Response>;
|
|
47
|
-
/**
|
|
48
|
-
* Uses an available session to run a fetch on a FileMaker Data API JSON endpoint. Also applies JSON/Zod type enforcement on result.
|
|
49
|
-
*/
|
|
50
|
-
fetchJSON<T extends ZodType | null = null>(url: string | URL, options: RequestInit & {
|
|
51
|
-
type: T;
|
|
52
|
-
}): Promise<T extends ZodType ? z.infer<T> & {
|
|
53
|
-
httpStatus: number;
|
|
54
|
-
} : {
|
|
55
|
-
httpStatus: number;
|
|
56
|
-
}>;
|
|
57
|
-
/**
|
|
58
|
-
* Retrieves a list of layouts in the current FileMaker database.
|
|
59
|
-
*
|
|
60
|
-
* @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
|
|
61
|
-
* @throws {FMError} If there was an error retrieving the layouts.
|
|
62
|
-
*/
|
|
63
31
|
listLayouts(page?: number): Promise<Layout<LayoutInterface>[]>;
|
|
64
32
|
layout<R extends keyof T['layouts']>(name: R): Layout<T['layouts'][R]>;
|
|
65
33
|
layout<R extends LayoutInterface>(name: string): Layout<R>;
|
|
66
|
-
/** Clears any Layout objects previously returned by `database.layout(...)`. */
|
|
67
34
|
clearLayoutCache(): void;
|
|
68
|
-
/** Creates a script reference you can pass to read and write helpers. */
|
|
69
35
|
script(name: string, parameter?: string): Script;
|
|
70
36
|
}
|