@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
package/dist/records/field.js
CHANGED
|
@@ -1,145 +1,75 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
3
|
*/
|
|
4
|
-
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
5
|
-
var useValue = arguments.length > 2;
|
|
6
|
-
for (var i = 0; i < initializers.length; i++) {
|
|
7
|
-
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
8
|
-
}
|
|
9
|
-
return useValue ? value : void 0;
|
|
10
|
-
};
|
|
11
|
-
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
12
|
-
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
13
|
-
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
14
|
-
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
15
|
-
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
16
|
-
var _, done = false;
|
|
17
|
-
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
18
|
-
var context = {};
|
|
19
|
-
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
20
|
-
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
21
|
-
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
22
|
-
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
23
|
-
if (kind === "accessor") {
|
|
24
|
-
if (result === void 0) continue;
|
|
25
|
-
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
26
|
-
if (_ = accept(result.get)) descriptor.get = _;
|
|
27
|
-
if (_ = accept(result.set)) descriptor.set = _;
|
|
28
|
-
if (_ = accept(result.init)) initializers.unshift(_);
|
|
29
|
-
}
|
|
30
|
-
else if (_ = accept(result)) {
|
|
31
|
-
if (kind === "field") initializers.unshift(_);
|
|
32
|
-
else descriptor[key] = _;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
36
|
-
done = true;
|
|
37
|
-
};
|
|
38
|
-
var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
|
|
39
|
-
if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
|
|
40
|
-
return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
|
|
41
|
-
};
|
|
42
4
|
import { RecordTypes } from '../types.js';
|
|
43
|
-
import { ApiFieldDisplayTypes, ApiFieldResultTypes, ApiFieldTypes } from '../models/apiResults.js';
|
|
5
|
+
import { ApiFieldDisplayTypes, ApiFieldResultTypes, ApiFieldTypes, ApiResults } from '../models/apiResults.js';
|
|
44
6
|
import { FMError } from '../FMError.js';
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
import { HttpError } from '../connection/Session.js';
|
|
45
9
|
/**
|
|
46
10
|
* A class representing a field in a record.
|
|
47
11
|
*
|
|
48
12
|
* @template T - The type of the field value.
|
|
49
13
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
result: ApiFieldResultTypes.TEXT,
|
|
110
|
-
global: false,
|
|
111
|
-
autoEnter: true,
|
|
112
|
-
fourDigitYear: false,
|
|
113
|
-
maxRepeat: 1,
|
|
114
|
-
maxCharacters: 0,
|
|
115
|
-
notEmpty: false,
|
|
116
|
-
numeric: false,
|
|
117
|
-
timeOfDay: false,
|
|
118
|
-
repetitions: 1,
|
|
119
|
-
valueList: ''
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
if (this.parent.type === RecordTypes.PORTAL && this.parent.portal) {
|
|
123
|
-
return this.parent.layout.metadata.portalMetaData[this.parent.portal.name || 'portal not attached'].find(i => i.name === this.id) ??
|
|
124
|
-
{
|
|
125
|
-
name: this.id.toString(),
|
|
126
|
-
type: ApiFieldTypes.NORMAL,
|
|
127
|
-
displayType: ApiFieldDisplayTypes.EDIT_TEXT,
|
|
128
|
-
result: ApiFieldResultTypes.TEXT,
|
|
129
|
-
global: false,
|
|
130
|
-
autoEnter: true,
|
|
131
|
-
fourDigitYear: false,
|
|
132
|
-
maxRepeat: 1,
|
|
133
|
-
maxCharacters: 0,
|
|
134
|
-
notEmpty: false,
|
|
135
|
-
numeric: false,
|
|
136
|
-
timeOfDay: false,
|
|
137
|
-
repetitions: 1,
|
|
138
|
-
valueList: ''
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
return this.parent.layout.metadata.fieldMetaData.find(i => i.name === this.id) ?? {
|
|
14
|
+
export class Field {
|
|
15
|
+
parent;
|
|
16
|
+
id;
|
|
17
|
+
_value;
|
|
18
|
+
#originalContents;
|
|
19
|
+
static firstContainerDownload = null;
|
|
20
|
+
constructor(record, id, originalContents) {
|
|
21
|
+
this.parent = record;
|
|
22
|
+
this.id = id;
|
|
23
|
+
this._value = originalContents;
|
|
24
|
+
this.#originalContents = originalContents;
|
|
25
|
+
}
|
|
26
|
+
/** `true` if this field has changed since it was last loaded or saved. */
|
|
27
|
+
get edited() {
|
|
28
|
+
return this.#originalContents !== this._value;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resets the 'edited' state of this field, without changing its value
|
|
32
|
+
*/
|
|
33
|
+
updateOriginalContents() {
|
|
34
|
+
this.#originalContents = this._value;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sets the value of the field.
|
|
38
|
+
*
|
|
39
|
+
* @param {T | null} content - The new content value to be set. Can be either the type T or null.
|
|
40
|
+
* @throws {Error} Cannot set container value using set(). Use upload() instead, if the result is a 'container'.
|
|
41
|
+
*/
|
|
42
|
+
set(content) {
|
|
43
|
+
if (this.metadata.result === 'container')
|
|
44
|
+
throw new Error('Cannot set container value using set(). Use upload() instead.');
|
|
45
|
+
else
|
|
46
|
+
this._value = content;
|
|
47
|
+
}
|
|
48
|
+
/** Returns the FileMaker metadata for this field. */
|
|
49
|
+
get metadata() {
|
|
50
|
+
if (!this.parent.layout.metadata) {
|
|
51
|
+
// Default to a regular text field
|
|
52
|
+
return {
|
|
53
|
+
name: this.id.toString(),
|
|
54
|
+
type: ApiFieldTypes.NORMAL,
|
|
55
|
+
displayType: ApiFieldDisplayTypes.EDIT_TEXT,
|
|
56
|
+
result: ApiFieldResultTypes.TEXT,
|
|
57
|
+
global: false,
|
|
58
|
+
autoEnter: true,
|
|
59
|
+
fourDigitYear: false,
|
|
60
|
+
maxRepeat: 1,
|
|
61
|
+
maxCharacters: 0,
|
|
62
|
+
notEmpty: false,
|
|
63
|
+
numeric: false,
|
|
64
|
+
timeOfDay: false,
|
|
65
|
+
repetitionStart: 1,
|
|
66
|
+
repetitionEnd: 1,
|
|
67
|
+
valueList: ''
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (this.parent.type === RecordTypes.PORTAL && this.parent.portal) {
|
|
71
|
+
return this.parent.layout.metadata.portalMetaData[this.parent.portal.name || 'portal not attached'].find(i => i.name === this.id) ??
|
|
72
|
+
{
|
|
143
73
|
name: this.id.toString(),
|
|
144
74
|
type: ApiFieldTypes.NORMAL,
|
|
145
75
|
displayType: ApiFieldDisplayTypes.EDIT_TEXT,
|
|
@@ -152,117 +82,111 @@ let Field = (() => {
|
|
|
152
82
|
notEmpty: false,
|
|
153
83
|
numeric: false,
|
|
154
84
|
timeOfDay: false,
|
|
155
|
-
|
|
85
|
+
repetitionStart: 1,
|
|
86
|
+
repetitionEnd: 1,
|
|
156
87
|
valueList: ''
|
|
157
88
|
};
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
get value() {
|
|
161
|
-
// if (this.metadata.result === "container") throw "Use await field.stream() to get the contents of a container field, instead of field.value"
|
|
162
|
-
return this._value;
|
|
163
|
-
}
|
|
164
|
-
set value(value) {
|
|
165
|
-
this.set(value);
|
|
166
|
-
}
|
|
167
|
-
get string() {
|
|
168
|
-
if (typeof this._value === 'string') {
|
|
169
|
-
return this._value;
|
|
170
|
-
}
|
|
171
|
-
throw new Error('Field value is not a string');
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Uploads a file to the container field.
|
|
175
|
-
*
|
|
176
|
-
* @param {Buffer} file - The file content as a buffer.
|
|
177
|
-
*
|
|
178
|
-
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
179
|
-
* @throws {Error} - Upload failed with HTTP error.
|
|
180
|
-
*
|
|
181
|
-
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
182
|
-
*/
|
|
183
|
-
async upload(file) {
|
|
184
|
-
if (this.metadata.result !== 'container') {
|
|
185
|
-
throw new Error('Cannot upload a file to the field; ' + this.id + ' (not a container field)');
|
|
186
|
-
}
|
|
187
|
-
const form = new FormData();
|
|
188
|
-
form.append('upload', file);
|
|
189
|
-
const res = await this.parent.layout.database._apiRequestRaw(`${this.parent.endpoint}/containers/${this.id}/1`, {
|
|
190
|
-
method: 'POST',
|
|
191
|
-
headers: {
|
|
192
|
-
// Authorization: 'Bearer ' + this.parent.layout.database.token,
|
|
193
|
-
// 'Content-Type': 'multipart/form-data'
|
|
194
|
-
},
|
|
195
|
-
body: form
|
|
196
|
-
});
|
|
197
|
-
if (!res.ok) {
|
|
198
|
-
throw new Error(`Upload failed with HTTP error: ${res.status} (${res.statusText})`);
|
|
199
|
-
}
|
|
200
|
-
const data = await res.json();
|
|
201
|
-
if (data.messages[0].code === '0')
|
|
202
|
-
return;
|
|
203
|
-
else {
|
|
204
|
-
throw new FMError(data.messages[0].code, res.status, res);
|
|
205
|
-
}
|
|
206
89
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
90
|
+
else {
|
|
91
|
+
return this.parent.layout.metadata.fieldMetaData.find(i => i.name === this.id) ?? {
|
|
92
|
+
name: this.id.toString(),
|
|
93
|
+
type: ApiFieldTypes.NORMAL,
|
|
94
|
+
displayType: ApiFieldDisplayTypes.EDIT_TEXT,
|
|
95
|
+
result: ApiFieldResultTypes.TEXT,
|
|
96
|
+
global: false,
|
|
97
|
+
autoEnter: true,
|
|
98
|
+
fourDigitYear: false,
|
|
99
|
+
maxRepeat: 1,
|
|
100
|
+
maxCharacters: 0,
|
|
101
|
+
notEmpty: false,
|
|
102
|
+
numeric: false,
|
|
103
|
+
timeOfDay: false,
|
|
104
|
+
repetitionStart: 1,
|
|
105
|
+
repetitionEnd: 1,
|
|
106
|
+
valueList: ''
|
|
107
|
+
};
|
|
210
108
|
}
|
|
211
|
-
async arrayBuffer() {
|
|
212
|
-
const req = await this.parent.layout.database._apiRequestRaw(this.string, { useCookieJar: true });
|
|
213
|
-
if (!req.ok)
|
|
214
|
-
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`);
|
|
215
|
-
return { data: await req.arrayBuffer(), mime: req.headers.get('Content-Type') ?? '' };
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
})();
|
|
219
|
-
export { Field };
|
|
220
|
-
function containerDownloadFunction(originalMethod, context) {
|
|
221
|
-
async function replacementMethod(...args) {
|
|
222
|
-
return await ContainerDownloadExecutor.processTask(async () => await originalMethod.call(this, ...args));
|
|
223
109
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
catch (e) {
|
|
238
|
-
this.hasDownloadedFirstContainer = 0;
|
|
239
|
-
const nextTask = this.waitingFuncs.splice(0, 1)[0];
|
|
240
|
-
if (nextTask) {
|
|
241
|
-
this.processTask(nextTask.func)
|
|
242
|
-
.then(res => nextTask.done(res))
|
|
243
|
-
.catch(e => nextTask.err(e));
|
|
244
|
-
}
|
|
245
|
-
throw e;
|
|
246
|
-
}
|
|
247
|
-
this.hasDownloadedFirstContainer = 2;
|
|
248
|
-
for (const task of this.waitingFuncs) {
|
|
249
|
-
task.func()
|
|
250
|
-
.then((res) => task.done(res))
|
|
251
|
-
.catch((e) => task.err(e));
|
|
252
|
-
}
|
|
253
|
-
return res;
|
|
110
|
+
/** Gets or sets the current field value. */
|
|
111
|
+
get value() {
|
|
112
|
+
// if (this.metadata.result === "container") throw "Use await field.stream() to get the contents of a container field, instead of field.value"
|
|
113
|
+
return this._value;
|
|
114
|
+
}
|
|
115
|
+
set value(value) {
|
|
116
|
+
this.set(value);
|
|
117
|
+
}
|
|
118
|
+
/** Returns this field value as a string. Throws if the value is not a string. */
|
|
119
|
+
get string() {
|
|
120
|
+
if (typeof this._value === 'string') {
|
|
121
|
+
return this._value;
|
|
254
122
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
123
|
+
throw new Error('Field value is not a string');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Uploads a file to the container field.
|
|
127
|
+
*
|
|
128
|
+
* @param {Buffer} file - The file content as a buffer.
|
|
129
|
+
*
|
|
130
|
+
* @throws {Error} - Cannot upload a file to the field if it's not a container field.
|
|
131
|
+
* @throws {Error} - Upload failed with HTTP error.
|
|
132
|
+
*
|
|
133
|
+
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
134
|
+
*/
|
|
135
|
+
async upload(file) {
|
|
136
|
+
if (this.metadata.result !== 'container') {
|
|
137
|
+
throw new Error('Cannot upload a file to the field; ' + this.id + ' (not a container field)');
|
|
138
|
+
}
|
|
139
|
+
const form = new FormData();
|
|
140
|
+
form.append('upload', file);
|
|
141
|
+
const res = await this.parent.layout.database.fetch(`${this.parent.endpoint}/containers/${this.id}/1`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
body: form
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
throw await HttpError.new(res);
|
|
147
|
+
}
|
|
148
|
+
const data = ApiResults.parse(await res.json());
|
|
149
|
+
if (data.messages[0].code === 0)
|
|
150
|
+
return;
|
|
151
|
+
else {
|
|
152
|
+
throw new FMError(data.messages[0].code, res.status, res);
|
|
263
153
|
}
|
|
264
|
-
|
|
265
|
-
|
|
154
|
+
}
|
|
155
|
+
async #streamAsync(options = {}) {
|
|
156
|
+
const req = await this.parent.layout.database.fetch(this.string, {
|
|
157
|
+
signal: options.abortSignal ?? null,
|
|
158
|
+
});
|
|
159
|
+
// const req = await this.parent.layout.database._apiRequestRaw(this.string, {useCookieJar: true})
|
|
160
|
+
if (!req.ok || !req.body) {
|
|
161
|
+
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`);
|
|
162
|
+
}
|
|
163
|
+
return req;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* @deprecated use webStream instead.
|
|
167
|
+
*/
|
|
168
|
+
async stream() {
|
|
169
|
+
const stream = await this.#streamAsync();
|
|
170
|
+
if (!stream.body) {
|
|
171
|
+
throw await HttpError.new(stream);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
// @ts-expect-error stream types are correct
|
|
175
|
+
data: Readable.fromWeb(stream.body),
|
|
176
|
+
mime: stream.headers.get('Content-Type') ?? ''
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Returns the container download as a Web `Response`.
|
|
181
|
+
*/
|
|
182
|
+
async webStream(options) {
|
|
183
|
+
return await this.#streamAsync(options);
|
|
184
|
+
}
|
|
185
|
+
/** Downloads the full container contents into memory. */
|
|
186
|
+
async arrayBuffer() {
|
|
187
|
+
const stream = await this.#streamAsync();
|
|
188
|
+
if (!stream.ok)
|
|
189
|
+
throw await HttpError.new(stream);
|
|
190
|
+
return { data: await stream.arrayBuffer(), mime: stream.headers.get('Content-Type') ?? '' };
|
|
266
191
|
}
|
|
267
192
|
}
|
|
268
|
-
const ContainerDownloadExecutor = new ContainerDownloadExecutorClass();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { RecordTypes } from '../../types.js';
|
|
2
|
+
import { type ApiFieldMetadata, FieldResultTypes } from '../../models/apiResults.js';
|
|
3
|
+
import { type LayoutBase } from '../../layouts/layoutBase.js';
|
|
4
|
+
import { type z } from 'zod';
|
|
5
|
+
export type ContainerValue = null;
|
|
6
|
+
/**
|
|
7
|
+
* RawValueData refers to the raw shapes of data we receive from or sent to FileMaker.
|
|
8
|
+
*/
|
|
9
|
+
export type RawValueData = string | number | null;
|
|
10
|
+
export interface Parentable {
|
|
11
|
+
layout: LayoutBase;
|
|
12
|
+
type: RecordTypes;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
portal?: {
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
getFieldMetadata(fieldId: string): z.infer<typeof ApiFieldMetadata>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A class representing a field in a record.
|
|
21
|
+
* API_FIELD_RESULT defines the value of field.metadata.result for the abstracted field type
|
|
22
|
+
*
|
|
23
|
+
* @template T - The type of the field value.
|
|
24
|
+
*/
|
|
25
|
+
export declare abstract class BaseField<T extends unknown, API_FIELD_RESULT extends FieldResultTypes> {
|
|
26
|
+
parent: Parentable;
|
|
27
|
+
id: string;
|
|
28
|
+
protected _value: T;
|
|
29
|
+
/**
|
|
30
|
+
* @internal
|
|
31
|
+
* Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
|
|
32
|
+
* @param record
|
|
33
|
+
* @param id
|
|
34
|
+
* @param value
|
|
35
|
+
* @protected
|
|
36
|
+
*/
|
|
37
|
+
protected constructor(record: Parentable, id: string, value: RawValueData);
|
|
38
|
+
/**
|
|
39
|
+
* Takes in data in the format that FileMaker gives us and converts it to a more JavaScript-friendly format.
|
|
40
|
+
* This method is intended to be overriden.
|
|
41
|
+
* @param value
|
|
42
|
+
*/
|
|
43
|
+
parseRawValue(value: RawValueData): T;
|
|
44
|
+
/**
|
|
45
|
+
* Converts data back into a format that can be sent to FileMaker. Intended to be overriden if conversion is non-standard.
|
|
46
|
+
*/
|
|
47
|
+
serializeRawValue(): RawValueData;
|
|
48
|
+
updateFromRawValue(value: RawValueData): void;
|
|
49
|
+
/** Returns the FileMaker metadata for this field. */
|
|
50
|
+
get metadata(): z.infer<typeof ApiFieldMetadata> & {
|
|
51
|
+
result: API_FIELD_RESULT;
|
|
52
|
+
};
|
|
53
|
+
/** Gets or sets the current field value. */
|
|
54
|
+
get value(): T;
|
|
55
|
+
isCalculationField(): boolean;
|
|
56
|
+
isSummaryField(): boolean;
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2024. See LICENSE file for more information
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* A class representing a field in a record.
|
|
6
|
+
* API_FIELD_RESULT defines the value of field.metadata.result for the abstracted field type
|
|
7
|
+
*
|
|
8
|
+
* @template T - The type of the field value.
|
|
9
|
+
*/
|
|
10
|
+
export class BaseField {
|
|
11
|
+
parent;
|
|
12
|
+
id;
|
|
13
|
+
_value;
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
* Use `layout.records.create()` instead to create a new LayoutRecord, as this may be safer.
|
|
17
|
+
* @param record
|
|
18
|
+
* @param id
|
|
19
|
+
* @param value
|
|
20
|
+
* @protected
|
|
21
|
+
*/
|
|
22
|
+
constructor(record, id, value) {
|
|
23
|
+
this.parent = record;
|
|
24
|
+
this.id = id;
|
|
25
|
+
this._value = this.parseRawValue(value);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Takes in data in the format that FileMaker gives us and converts it to a more JavaScript-friendly format.
|
|
29
|
+
* This method is intended to be overriden.
|
|
30
|
+
* @param value
|
|
31
|
+
*/
|
|
32
|
+
parseRawValue(value) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Converts data back into a format that can be sent to FileMaker. Intended to be overriden if conversion is non-standard.
|
|
37
|
+
*/
|
|
38
|
+
serializeRawValue() {
|
|
39
|
+
return this._value;
|
|
40
|
+
}
|
|
41
|
+
updateFromRawValue(value) {
|
|
42
|
+
this._value = this.parseRawValue(value);
|
|
43
|
+
}
|
|
44
|
+
/** Returns the FileMaker metadata for this field. */
|
|
45
|
+
get metadata() {
|
|
46
|
+
return this.parent.getFieldMetadata(this.id);
|
|
47
|
+
}
|
|
48
|
+
/** Gets or sets the current field value. */
|
|
49
|
+
get value() {
|
|
50
|
+
// if (this.metadata.result === "container") throw "Use await field.stream() to get the contents of a container field, instead of field.value"
|
|
51
|
+
return this._value;
|
|
52
|
+
}
|
|
53
|
+
isCalculationField() {
|
|
54
|
+
return this.metadata.type === 'calculation';
|
|
55
|
+
}
|
|
56
|
+
isSummaryField() {
|
|
57
|
+
return this.metadata.type === 'summary';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseField, Parentable, RawValueData } from "./baseField.js";
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
interface StreamOptions {
|
|
4
|
+
abortSignal?: AbortSignal;
|
|
5
|
+
}
|
|
6
|
+
export declare class ContainerField extends BaseField<string, 'container'> {
|
|
7
|
+
#private;
|
|
8
|
+
static firstContainerDownload: Promise<any> | null;
|
|
9
|
+
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
|
+
upload(file: File): Promise<void>;
|
|
21
|
+
stream(): Promise<{
|
|
22
|
+
data: Readable;
|
|
23
|
+
mime: string;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Returns the container download as a Web `Response`.
|
|
27
|
+
*/
|
|
28
|
+
webStream(options: StreamOptions): Promise<Response>;
|
|
29
|
+
/** Downloads the full container contents into memory. */
|
|
30
|
+
arrayBuffer(): Promise<{
|
|
31
|
+
data: ArrayBuffer;
|
|
32
|
+
mime: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { BaseField } from "./baseField.js";
|
|
2
|
+
import { FMError } from '../../FMError.js';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { HttpError } from '../../connection/Session.js';
|
|
5
|
+
import { ApiResults } from "../../models/apiResults.js";
|
|
6
|
+
export class ContainerField extends BaseField {
|
|
7
|
+
static firstContainerDownload = null;
|
|
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
|
+
*/
|
|
21
|
+
async upload(file) {
|
|
22
|
+
if (this.metadata.result !== 'container') {
|
|
23
|
+
throw new Error('Cannot upload a file to the field; ' + this.id + ' (not a container field)');
|
|
24
|
+
}
|
|
25
|
+
const form = new FormData();
|
|
26
|
+
form.append('upload', file);
|
|
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
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async #streamAsync(options = {}) {
|
|
42
|
+
const req = await this.parent.layout.database.fetch(this.value, {
|
|
43
|
+
signal: options.abortSignal ?? null
|
|
44
|
+
});
|
|
45
|
+
// const req = await this.parent.layout.database._apiRequestRaw(this.string, {useCookieJar: true})
|
|
46
|
+
if (!req.ok || !req.body) {
|
|
47
|
+
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`);
|
|
48
|
+
}
|
|
49
|
+
return req;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* @deprecated use webStream instead.
|
|
53
|
+
*/
|
|
54
|
+
async stream() {
|
|
55
|
+
const stream = await this.#streamAsync();
|
|
56
|
+
if (!stream.body) {
|
|
57
|
+
throw await HttpError.new(stream);
|
|
58
|
+
}
|
|
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
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns the container download as a Web `Response`.
|
|
67
|
+
*/
|
|
68
|
+
async webStream(options) {
|
|
69
|
+
return await this.#streamAsync(options);
|
|
70
|
+
}
|
|
71
|
+
/** Downloads the full container contents into memory. */
|
|
72
|
+
async arrayBuffer() {
|
|
73
|
+
const stream = await this.#streamAsync();
|
|
74
|
+
if (!stream.ok)
|
|
75
|
+
throw await HttpError.new(stream);
|
|
76
|
+
return { data: await stream.arrayBuffer(), mime: stream.headers.get('Content-Type') ?? '' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|