@jd-data-limited/easy-fm 4.1.2 → 4.1.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/package.json +1 -1
- package/src/bin/generateTypes.js +215 -121
- package/src/connection/FMHost.d.ts +1 -0
- package/src/connection/FMHost.js +4 -2
- package/src/connection/FMHost.ts +4 -2
- package/src/connection/HostBase.d.ts +1 -0
- package/src/connection/HostBase.ts +1 -0
- package/src/connection/database.d.ts +1 -1
- package/src/connection/database.js +24 -15
- package/src/connection/database.ts +39 -18
- package/src/connection/requestHoldable.d.ts +10 -0
- package/src/connection/requestHoldable.js +44 -0
- package/src/records/field.d.ts +8 -2
- package/src/records/field.js +2 -2
- package/src/records/field.ts +4 -4
- package/src/records/layoutRecord.js +1 -0
- package/src/records/layoutRecord.ts +1 -0
package/package.json
CHANGED
package/src/bin/generateTypes.js
CHANGED
|
@@ -1,10 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/*
|
|
2
3
|
* Copyright (c) 2024. See LICENSE file for more information
|
|
3
4
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
|
6
|
+
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
|
7
|
+
return cooked;
|
|
8
|
+
};
|
|
9
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
10
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
11
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
12
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
13
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
14
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
15
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
19
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
20
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
21
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
22
|
+
function step(op) {
|
|
23
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
24
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
25
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
26
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
27
|
+
switch (op[0]) {
|
|
28
|
+
case 0: case 1: t = op; break;
|
|
29
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
30
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
31
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
32
|
+
default:
|
|
33
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
34
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
35
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
36
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
37
|
+
if (t[2]) _.ops.pop();
|
|
38
|
+
_.trys.pop(); continue;
|
|
39
|
+
}
|
|
40
|
+
op = body.call(thisArg, _);
|
|
41
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
42
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.generateTypesCLI = void 0;
|
|
47
|
+
var apiResults_js_1 = require("../models/apiResults.js");
|
|
48
|
+
var inquirer_1 = require("inquirer");
|
|
49
|
+
var FMHost_js_1 = require("../connection/FMHost.js");
|
|
50
|
+
var fs = require("fs");
|
|
8
51
|
function sub(str) {
|
|
9
52
|
return str
|
|
10
53
|
.replace(/[^a-zA-Z0-9_]/g, "") // Remove non-alphanumeric characters
|
|
@@ -12,38 +55,42 @@ function sub(str) {
|
|
|
12
55
|
}
|
|
13
56
|
function generateFieldType(field) {
|
|
14
57
|
switch (field.result) {
|
|
15
|
-
case ApiFieldResultTypes.TEXT:
|
|
58
|
+
case apiResults_js_1.ApiFieldResultTypes.TEXT:
|
|
16
59
|
return "Field<string>";
|
|
17
|
-
case ApiFieldResultTypes.CONTAINER:
|
|
60
|
+
case apiResults_js_1.ApiFieldResultTypes.CONTAINER:
|
|
18
61
|
return "Field<Container>";
|
|
19
|
-
case ApiFieldResultTypes.DATE:
|
|
62
|
+
case apiResults_js_1.ApiFieldResultTypes.DATE:
|
|
20
63
|
return "Field<Date>";
|
|
21
|
-
case ApiFieldResultTypes.NUMBER:
|
|
64
|
+
case apiResults_js_1.ApiFieldResultTypes.NUMBER:
|
|
22
65
|
return "Field<number>";
|
|
23
|
-
case ApiFieldResultTypes.TIME:
|
|
66
|
+
case apiResults_js_1.ApiFieldResultTypes.TIME:
|
|
24
67
|
return "Field<Date>";
|
|
25
|
-
case ApiFieldResultTypes.TIMESTAMP:
|
|
68
|
+
case apiResults_js_1.ApiFieldResultTypes.TIMESTAMP:
|
|
26
69
|
return "Field<Date>";
|
|
27
70
|
}
|
|
28
71
|
}
|
|
29
72
|
function processFieldsChunk(fields) {
|
|
30
|
-
|
|
31
|
-
fields = fields.filter(field
|
|
73
|
+
var existing_fields = [];
|
|
74
|
+
fields = fields.filter(function (field) {
|
|
32
75
|
if (existing_fields.includes(field.name))
|
|
33
76
|
return false;
|
|
34
77
|
existing_fields.push(field.name);
|
|
35
78
|
return true;
|
|
36
79
|
});
|
|
37
|
-
return fields.map(field
|
|
80
|
+
return fields.map(function (field) {
|
|
38
81
|
return JSON.stringify(field.name) + ": " + generateFieldType(field);
|
|
39
82
|
});
|
|
40
83
|
}
|
|
41
84
|
function camelToSnakeCase(str) {
|
|
42
85
|
return str.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
43
86
|
}
|
|
44
|
-
function safeStringInjection(str
|
|
45
|
-
|
|
46
|
-
for (
|
|
87
|
+
function safeStringInjection(str) {
|
|
88
|
+
var args = [];
|
|
89
|
+
for (var _i = 1; _i < arguments.length; _i++) {
|
|
90
|
+
args[_i - 1] = arguments[_i];
|
|
91
|
+
}
|
|
92
|
+
var result = "";
|
|
93
|
+
for (var i = 0; i < str.length; i++) {
|
|
47
94
|
result += str[i];
|
|
48
95
|
if (i < args.length) {
|
|
49
96
|
result += JSON.stringify(args[i].toString());
|
|
@@ -51,111 +98,158 @@ function safeStringInjection(str, ...args) {
|
|
|
51
98
|
}
|
|
52
99
|
return result;
|
|
53
100
|
}
|
|
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
|
-
|
|
101
|
+
function generateTypesCLI() {
|
|
102
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
103
|
+
var data, HOST, DATABASE, stream, layouts, layoutInterfaces, _loop_1, _i, layouts_1, layout, layoutInterfaceLinks, _a, layoutInterfaces_1, layout, _b, layoutInterfaces_2, layout;
|
|
104
|
+
return __generator(this, function (_c) {
|
|
105
|
+
switch (_c.label) {
|
|
106
|
+
case 0:
|
|
107
|
+
process.stdout.write('\x1Bc');
|
|
108
|
+
console.log("For this tool to work, you are required to have access to an account on the target database.\n" +
|
|
109
|
+
"This account must have the fmrest extended privilege enabled.\n" +
|
|
110
|
+
"This tool can only export type interfaces for layouts that the given account has access to.");
|
|
111
|
+
console.log("\n");
|
|
112
|
+
return [4 /*yield*/, inquirer_1.default.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: "input",
|
|
115
|
+
name: "hostname",
|
|
116
|
+
message: "The IP address/domain of your FileMaker server (must start with either http:// or https://)",
|
|
117
|
+
validate: function (input) {
|
|
118
|
+
return input.startsWith("http://") || input.startsWith("https://") || "Please enter a valid IP address/domain starting with http:// or https://";
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: "number",
|
|
123
|
+
name: "timezoneOffset",
|
|
124
|
+
message: "Server timezone offset (minutes)",
|
|
125
|
+
default: 0 - (new Date()).getTimezoneOffset()
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: "confirm",
|
|
129
|
+
name: "verify",
|
|
130
|
+
default: true,
|
|
131
|
+
message: "Verify your server's identity",
|
|
132
|
+
when: function (previousAnswers) {
|
|
133
|
+
return previousAnswers.hostname.startsWith("https://");
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: "input",
|
|
138
|
+
name: "database",
|
|
139
|
+
message: "Database filename"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: "input",
|
|
143
|
+
name: "username",
|
|
144
|
+
message: "Database username"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: "password",
|
|
148
|
+
name: "password",
|
|
149
|
+
message: "Database password",
|
|
150
|
+
mask: "*"
|
|
151
|
+
}
|
|
152
|
+
])];
|
|
153
|
+
case 1:
|
|
154
|
+
data = _c.sent();
|
|
155
|
+
HOST = new FMHost_js_1.default(data.hostname, data.timezoneOffset, data.verify);
|
|
156
|
+
DATABASE = HOST.database({
|
|
157
|
+
database: data.database,
|
|
158
|
+
credentials: {
|
|
159
|
+
method: "filemaker",
|
|
160
|
+
username: data.username,
|
|
161
|
+
password: data.password
|
|
162
|
+
},
|
|
163
|
+
externalSources: []
|
|
164
|
+
});
|
|
165
|
+
return [4 /*yield*/, DATABASE.login()
|
|
166
|
+
// Create file write stream
|
|
167
|
+
];
|
|
168
|
+
case 2:
|
|
169
|
+
_c.sent();
|
|
170
|
+
stream = fs.createWriteStream("./types.ts");
|
|
171
|
+
return [4 /*yield*/, DATABASE.listLayouts()];
|
|
172
|
+
case 3:
|
|
173
|
+
layouts = _c.sent();
|
|
174
|
+
stream.write("import FMHost, {Container, FieldBase, LayoutInterface, Portal} from \"@jd-data-limited/easy-fm\";\n");
|
|
175
|
+
stream.write("// " + layouts.length + " layouts found\n\n");
|
|
176
|
+
layoutInterfaces = new Map();
|
|
177
|
+
_loop_1 = function (layout) {
|
|
178
|
+
var substituteName, metadata, fields, portals, portalsProcessed, interfaceName;
|
|
179
|
+
return __generator(this, function (_d) {
|
|
180
|
+
switch (_d.label) {
|
|
181
|
+
case 0:
|
|
182
|
+
substituteName = sub(layout.name);
|
|
183
|
+
if (!substituteName)
|
|
184
|
+
return [2 /*return*/, "continue"];
|
|
185
|
+
console.log(substituteName);
|
|
186
|
+
return [4 /*yield*/, layout.getLayoutMeta()];
|
|
187
|
+
case 1:
|
|
188
|
+
metadata = _d.sent();
|
|
189
|
+
fields = processFieldsChunk(metadata.fieldMetaData);
|
|
190
|
+
portals = metadata.portalMetaData;
|
|
191
|
+
portalsProcessed = Object.keys(portals).map(function (portalName) {
|
|
192
|
+
return "".concat(JSON.stringify(portalName), ": Portal<{").concat(processFieldsChunk(portals[portalName]), "}>");
|
|
193
|
+
});
|
|
194
|
+
interfaceName = substituteName + "LayoutInterface";
|
|
195
|
+
layoutInterfaces.set(layout.name, {
|
|
196
|
+
interfaceName: interfaceName,
|
|
197
|
+
interface: "export interface ".concat(interfaceName, " extends LayoutInterface {\n fields: {").concat(fields.join(",\n "), "},\n portals: {").concat(portalsProcessed.join(",\n "), "}\n }")
|
|
198
|
+
});
|
|
199
|
+
return [2 /*return*/];
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
_i = 0, layouts_1 = layouts;
|
|
204
|
+
_c.label = 4;
|
|
205
|
+
case 4:
|
|
206
|
+
if (!(_i < layouts_1.length)) return [3 /*break*/, 7];
|
|
207
|
+
layout = layouts_1[_i];
|
|
208
|
+
return [5 /*yield**/, _loop_1(layout)];
|
|
209
|
+
case 5:
|
|
210
|
+
_c.sent();
|
|
211
|
+
_c.label = 6;
|
|
212
|
+
case 6:
|
|
213
|
+
_i++;
|
|
214
|
+
return [3 /*break*/, 4];
|
|
215
|
+
case 7:
|
|
216
|
+
layoutInterfaceLinks = [];
|
|
217
|
+
for (_a = 0, layoutInterfaces_1 = layoutInterfaces; _a < layoutInterfaces_1.length; _a++) {
|
|
218
|
+
layout = layoutInterfaces_1[_a];
|
|
219
|
+
layoutInterfaceLinks.push([
|
|
220
|
+
"".concat(JSON.stringify(layout[0]), ": ").concat(layout[1].interfaceName),
|
|
221
|
+
"".concat(sub(camelToSnakeCase(layout[0]).toUpperCase().replace(" ", "_")), ": HOST.getLayout<").concat(layout[1].interfaceName, ">(").concat(JSON.stringify(layout[0]), ")")
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
stream.write(safeStringInjection(templateObject_1 || (templateObject_1 = __makeTemplateObject(["const HOST = new FMHost(", ", ", ", ", ");"], ["const HOST = new FMHost(", ", ", ", ", ");"])), data.hostname, data.timezoneOffset, data.verify));
|
|
225
|
+
stream.write("HOST.database<{layouts:{".concat(layoutInterfaceLinks.map(function (i) { return i[0]; }).join(","), "}}>();"));
|
|
226
|
+
stream.write("\n\n");
|
|
227
|
+
stream.write("const LAYOUTS = {".concat(layoutInterfaceLinks.map(function (i) { return i[1]; }).join(","), "};"));
|
|
228
|
+
for (_b = 0, layoutInterfaces_2 = layoutInterfaces; _b < layoutInterfaces_2.length; _b++) {
|
|
229
|
+
layout = layoutInterfaces_2[_b];
|
|
230
|
+
stream.write("\n\n");
|
|
231
|
+
stream.write(layout[1].interface + ";");
|
|
232
|
+
}
|
|
233
|
+
// StructureKind
|
|
234
|
+
//
|
|
235
|
+
// const project = new Project({
|
|
236
|
+
// tsConfigFilePath: "../tsconfig.json"
|
|
237
|
+
// })
|
|
238
|
+
// project.resolveSourceFileDependencies()
|
|
239
|
+
return [4 /*yield*/, DATABASE.logout()];
|
|
240
|
+
case 8:
|
|
241
|
+
// StructureKind
|
|
242
|
+
//
|
|
243
|
+
// const project = new Project({
|
|
244
|
+
// tsConfigFilePath: "../tsconfig.json"
|
|
245
|
+
// })
|
|
246
|
+
// project.resolveSourceFileDependencies()
|
|
247
|
+
_c.sent();
|
|
248
|
+
console.log("DONE! Wrote database types to ./types.ts");
|
|
249
|
+
return [2 /*return*/];
|
|
82
250
|
}
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
type: "input",
|
|
86
|
-
name: "database",
|
|
87
|
-
message: "Database filename"
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
type: "input",
|
|
91
|
-
name: "username",
|
|
92
|
-
message: "Database username"
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
type: "password",
|
|
96
|
-
name: "password",
|
|
97
|
-
message: "Database password",
|
|
98
|
-
mask: "*"
|
|
99
|
-
}
|
|
100
|
-
]);
|
|
101
|
-
const HOST = new FMHost(data.hostname, data.timezoneOffset, data.verify);
|
|
102
|
-
const DATABASE = HOST.database({
|
|
103
|
-
database: data.database,
|
|
104
|
-
credentials: {
|
|
105
|
-
method: "filemaker",
|
|
106
|
-
username: data.username,
|
|
107
|
-
password: data.password
|
|
108
|
-
},
|
|
109
|
-
externalSources: []
|
|
110
|
-
});
|
|
111
|
-
await DATABASE.login();
|
|
112
|
-
// Create file write stream
|
|
113
|
-
const stream = fs.createWriteStream("./types.ts");
|
|
114
|
-
let layouts = await DATABASE.listLayouts();
|
|
115
|
-
stream.write(`import FMHost, {Container, FieldBase, LayoutInterface, Portal} from "@jd-data-limited/easy-fm";\n`);
|
|
116
|
-
stream.write("// " + layouts.length + " layouts found\n\n");
|
|
117
|
-
let layoutInterfaces = new Map();
|
|
118
|
-
for (let layout of layouts) {
|
|
119
|
-
let substituteName = sub(layout.name);
|
|
120
|
-
if (!substituteName)
|
|
121
|
-
continue;
|
|
122
|
-
console.log(substituteName);
|
|
123
|
-
let metadata = await layout.getLayoutMeta();
|
|
124
|
-
let fields = processFieldsChunk(metadata.fieldMetaData);
|
|
125
|
-
let portals = metadata.portalMetaData;
|
|
126
|
-
let portalsProcessed = Object.keys(portals).map(portalName => {
|
|
127
|
-
return `${JSON.stringify(portalName)}: Portal<{${processFieldsChunk(portals[portalName])}}>`;
|
|
128
|
-
});
|
|
129
|
-
let interfaceName = substituteName + "LayoutInterface";
|
|
130
|
-
layoutInterfaces.set(layout.name, {
|
|
131
|
-
interfaceName,
|
|
132
|
-
interface: `export interface ${interfaceName} extends LayoutInterface {
|
|
133
|
-
fields: {${fields.join(",\n ")}},
|
|
134
|
-
portals: {${portalsProcessed.join(",\n ")}}
|
|
135
|
-
}`
|
|
136
251
|
});
|
|
137
|
-
}
|
|
138
|
-
let layoutInterfaceLinks = [];
|
|
139
|
-
for (let layout of layoutInterfaces) {
|
|
140
|
-
layoutInterfaceLinks.push([
|
|
141
|
-
`${JSON.stringify(layout[0])}: ${layout[1].interfaceName}`,
|
|
142
|
-
`${sub(camelToSnakeCase(layout[0]).toUpperCase().replace(" ", "_"))}: HOST.getLayout<${layout[1].interfaceName}>(${JSON.stringify(layout[0])})`
|
|
143
|
-
]);
|
|
144
|
-
}
|
|
145
|
-
stream.write(safeStringInjection `const HOST = new FMHost(${data.hostname}, ${data.timezoneOffset}, ${data.verify});`);
|
|
146
|
-
stream.write(`HOST.database<{layouts:{${layoutInterfaceLinks.map(i => i[0]).join(",")}}}>();`);
|
|
147
|
-
stream.write("\n\n");
|
|
148
|
-
stream.write(`const LAYOUTS = {${layoutInterfaceLinks.map(i => i[1]).join(",")}};`);
|
|
149
|
-
for (let layout of layoutInterfaces) {
|
|
150
|
-
stream.write("\n\n");
|
|
151
|
-
stream.write(layout[1].interface + ";");
|
|
152
|
-
}
|
|
153
|
-
// StructureKind
|
|
154
|
-
//
|
|
155
|
-
// const project = new Project({
|
|
156
|
-
// tsConfigFilePath: "../tsconfig.json"
|
|
157
|
-
// })
|
|
158
|
-
// project.resolveSourceFileDependencies()
|
|
159
|
-
await DATABASE.logout();
|
|
160
|
-
console.log("DONE! Wrote database types to ./types.ts");
|
|
252
|
+
});
|
|
161
253
|
}
|
|
254
|
+
exports.generateTypesCLI = generateTypesCLI;
|
|
255
|
+
var templateObject_1;
|
|
@@ -11,6 +11,7 @@ export default class FMHost implements HostBase {
|
|
|
11
11
|
readonly hostname: string;
|
|
12
12
|
readonly timezoneOffsetFunc: (moment: Moment) => number;
|
|
13
13
|
readonly verify: boolean;
|
|
14
|
+
readonly protocol: 'http:' | 'https:';
|
|
14
15
|
_metadata: FMHostMetadata | null;
|
|
15
16
|
constructor(_hostname: string, timezoneOffset?: (moment: Moment) => number, verify?: boolean);
|
|
16
17
|
get metadata(): FMHostMetadata;
|
package/src/connection/FMHost.js
CHANGED
|
@@ -12,11 +12,13 @@ export default class FMHost {
|
|
|
12
12
|
hostname;
|
|
13
13
|
timezoneOffsetFunc;
|
|
14
14
|
verify;
|
|
15
|
+
protocol;
|
|
15
16
|
_metadata = null;
|
|
16
17
|
constructor(_hostname, timezoneOffset = (moment) => 0 - (new Date()).getTimezoneOffset(), verify = true) {
|
|
17
18
|
if (!(/^https?:\/\//).test(_hostname))
|
|
18
19
|
throw new Error('hostname MUST begin with either http:// or https://');
|
|
19
|
-
this.
|
|
20
|
+
this.protocol = _hostname.startsWith('https:') ? 'https:' : 'http:';
|
|
21
|
+
this.hostname = _hostname.split('//')[1];
|
|
20
22
|
this.timezoneOffsetFunc = timezoneOffset;
|
|
21
23
|
this.verify = verify;
|
|
22
24
|
}
|
|
@@ -81,7 +83,7 @@ export default class FMHost {
|
|
|
81
83
|
async getMetadata() {
|
|
82
84
|
if (this._metadata)
|
|
83
85
|
return this._metadata;
|
|
84
|
-
const _fetch = await fetch(`${this.hostname}/fmi/data/v2/productInfo`, {
|
|
86
|
+
const _fetch = await fetch(`${this.protocol}//${this.hostname}/fmi/data/v2/productInfo`, {
|
|
85
87
|
method: 'GET'
|
|
86
88
|
});
|
|
87
89
|
const data = await _fetch.json();
|
package/src/connection/FMHost.ts
CHANGED
|
@@ -25,6 +25,7 @@ export default class FMHost implements HostBase {
|
|
|
25
25
|
readonly hostname: string
|
|
26
26
|
readonly timezoneOffsetFunc: (moment: Moment) => number
|
|
27
27
|
readonly verify: boolean
|
|
28
|
+
readonly protocol: 'http:' | 'https:'
|
|
28
29
|
_metadata: FMHostMetadata | null = null
|
|
29
30
|
|
|
30
31
|
constructor (
|
|
@@ -33,7 +34,8 @@ export default class FMHost implements HostBase {
|
|
|
33
34
|
verify = true
|
|
34
35
|
) {
|
|
35
36
|
if (!(/^https?:\/\//).test(_hostname)) throw new Error('hostname MUST begin with either http:// or https://')
|
|
36
|
-
this.
|
|
37
|
+
this.protocol = _hostname.startsWith('https:') ? 'https:' : 'http:'
|
|
38
|
+
this.hostname = _hostname.split('//')[1]
|
|
37
39
|
this.timezoneOffsetFunc = timezoneOffset
|
|
38
40
|
this.verify = verify
|
|
39
41
|
}
|
|
@@ -105,7 +107,7 @@ export default class FMHost implements HostBase {
|
|
|
105
107
|
async getMetadata () {
|
|
106
108
|
if (this._metadata) return this._metadata
|
|
107
109
|
|
|
108
|
-
const _fetch = await fetch(`${this.hostname}/fmi/data/v2/productInfo`, {
|
|
110
|
+
const _fetch = await fetch(`${this.protocol}//${this.hostname}/fmi/data/v2/productInfo`, {
|
|
109
111
|
method: 'GET'
|
|
110
112
|
})
|
|
111
113
|
const data = await _fetch.json() as ApiResults<FMHostMetadata>
|
|
@@ -2,6 +2,7 @@ import { type FMHostMetadata } from '../types.js';
|
|
|
2
2
|
import { type Moment } from 'moment';
|
|
3
3
|
export interface HostBase {
|
|
4
4
|
readonly hostname: string;
|
|
5
|
+
readonly protocol: string;
|
|
5
6
|
readonly timezoneOffsetFunc: (moment: Moment) => number;
|
|
6
7
|
readonly verify: boolean;
|
|
7
8
|
metadata: FMHostMetadata;
|
|
@@ -48,8 +48,8 @@ export declare class Database<T extends DatabaseStructure> extends EventEmitter
|
|
|
48
48
|
_apiRequestRaw(url: URL | RequestInfo, options?: RequestInit & {
|
|
49
49
|
headers?: Record<string, string>;
|
|
50
50
|
useCookieJar?: boolean;
|
|
51
|
+
retries?: number;
|
|
51
52
|
}, autoRelogin?: boolean): Promise<Response>;
|
|
52
|
-
private _generateCookieHeader;
|
|
53
53
|
_apiRequestJSON<T = any>(url: URL | RequestInfo, options?: RequestInit & {
|
|
54
54
|
headers?: Record<string, string>;
|
|
55
55
|
}): Promise<ApiResults<T>>;
|
|
@@ -6,7 +6,7 @@ import { generateAuthorizationHeaders } from './generateAuthorizationHeaders.js'
|
|
|
6
6
|
import { FMError } from '../FMError.js';
|
|
7
7
|
import { Layout } from '../layouts/layout.js';
|
|
8
8
|
import fetch from 'node-fetch';
|
|
9
|
-
// @ts-expect-error
|
|
9
|
+
// @ts-expect-error - fetchWithCookies does not have available typescript types
|
|
10
10
|
import fetchWithCookies, { CookieJar } from 'node-fetch-cookies';
|
|
11
11
|
/**
|
|
12
12
|
* Represents a database connection.
|
|
@@ -109,16 +109,25 @@ export class Database extends EventEmitter {
|
|
|
109
109
|
* @returns {string} The endpoint URL.
|
|
110
110
|
*/
|
|
111
111
|
get endpoint() {
|
|
112
|
-
return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
|
|
112
|
+
return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
|
|
113
113
|
}
|
|
114
114
|
async _apiRequestRaw(url, options = {}, autoRelogin = true) {
|
|
115
|
-
if (this.debug)
|
|
116
|
-
console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof
|
|
117
|
-
|
|
115
|
+
if (this.debug) {
|
|
116
|
+
console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof URL
|
|
117
|
+
? url.toString()
|
|
118
|
+
: typeof url === 'string' ? url : url.url}`);
|
|
119
|
+
}
|
|
120
|
+
const reqIsToDBHost = (url instanceof URL
|
|
121
|
+
? url
|
|
122
|
+
: typeof url === 'string'
|
|
123
|
+
? new URL(url)
|
|
124
|
+
: new URL(url.url)).hostname === this.host.hostname;
|
|
125
|
+
if (reqIsToDBHost && this.token === '')
|
|
118
126
|
await this.login(true);
|
|
119
127
|
if (!options.headers)
|
|
120
128
|
options.headers = {};
|
|
121
|
-
|
|
129
|
+
if (reqIsToDBHost)
|
|
130
|
+
options.headers.authorization = 'Bearer ' + this._token;
|
|
122
131
|
// options.redirect = "manual"
|
|
123
132
|
// options.headers.cookies = this._generateCookieHeader()
|
|
124
133
|
// console.log(options.headers.cookies)
|
|
@@ -137,21 +146,21 @@ export class Database extends EventEmitter {
|
|
|
137
146
|
// console.log(this.cookies, _fetch.status)
|
|
138
147
|
// // console.log(this, this.cookies, url)
|
|
139
148
|
// console.trace()
|
|
140
|
-
if (_fetch.
|
|
141
|
-
|
|
149
|
+
if (!_fetch.ok && (!options.retries || options.retries > 0)) {
|
|
150
|
+
if (this.debug) {
|
|
151
|
+
console.log(`EASYFM DEBUG: RE-ATTEMPTING REQUEST (${_fetch.status}) ${url instanceof URL
|
|
152
|
+
? url.toString()
|
|
153
|
+
: typeof url === 'string' ? url : url.url}`);
|
|
154
|
+
}
|
|
155
|
+
return await this._apiRequestRaw(url, { ...options, retries: (options?.retries ?? 1) - 1 });
|
|
156
|
+
}
|
|
157
|
+
else if (_fetch.status === 401 && reqIsToDBHost && autoRelogin) {
|
|
142
158
|
await this.login(true);
|
|
143
159
|
return await this._apiRequestRaw(url, options, false);
|
|
144
160
|
}
|
|
145
161
|
else
|
|
146
162
|
return _fetch;
|
|
147
163
|
}
|
|
148
|
-
_generateCookieHeader() {
|
|
149
|
-
const out = [];
|
|
150
|
-
for (const key of Object.keys(this.cookies)) {
|
|
151
|
-
out.push(`${key}=${this.cookies[key]}`);
|
|
152
|
-
}
|
|
153
|
-
return out.join('; ');
|
|
154
|
-
}
|
|
155
164
|
async _apiRequestJSON(url, options = {}) {
|
|
156
165
|
if (!options.headers)
|
|
157
166
|
options.headers = {};
|
|
@@ -13,7 +13,7 @@ import {type DatabaseBase} from './databaseBase.js'
|
|
|
13
13
|
import {type ApiLayout, type ApiResults} from '../models/apiResults.js'
|
|
14
14
|
import {type DatabaseStructure} from '../databaseStructure.js'
|
|
15
15
|
import fetch, {type HeadersInit, type RequestInfo, type RequestInit, type Response} from 'node-fetch'
|
|
16
|
-
// @ts-expect-error
|
|
16
|
+
// @ts-expect-error - fetchWithCookies does not have available typescript types
|
|
17
17
|
import fetchWithCookies, {CookieJar} from 'node-fetch-cookies'
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -45,7 +45,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
45
45
|
username: _data.username,
|
|
46
46
|
password: _data.password
|
|
47
47
|
}
|
|
48
|
-
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
49
50
|
throw new Error('Not yet supported login method')
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -89,7 +90,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
89
90
|
if (this.connection_details.credentials.method === 'token') {
|
|
90
91
|
this._token = (this.connection_details.credentials).token
|
|
91
92
|
return this.token
|
|
92
|
-
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
93
95
|
const url = new URL(`${this.endpoint}/sessions`)
|
|
94
96
|
url.hostname = this.host.hostname
|
|
95
97
|
const res = await fetch(url, {
|
|
@@ -106,7 +108,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
106
108
|
if (res.status === 200) {
|
|
107
109
|
this._token = res.headers.get('x-fm-data-access-token') ?? ''
|
|
108
110
|
return this._token
|
|
109
|
-
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
110
113
|
throw new FMError(
|
|
111
114
|
_res.messages[0].code as string | number,
|
|
112
115
|
_res.status as number,
|
|
@@ -126,18 +129,34 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
126
129
|
* @returns {string} The endpoint URL.
|
|
127
130
|
*/
|
|
128
131
|
get endpoint (): string {
|
|
129
|
-
return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`
|
|
132
|
+
return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.name}`
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
async _apiRequestRaw (url: URL | RequestInfo, options: RequestInit & {
|
|
133
136
|
headers?: Record<string, string>
|
|
134
137
|
useCookieJar?: boolean
|
|
138
|
+
retries?: number
|
|
135
139
|
} = {}, autoRelogin = true): Promise<Response> {
|
|
136
|
-
if (this.debug)
|
|
137
|
-
|
|
140
|
+
if (this.debug) {
|
|
141
|
+
console.log(
|
|
142
|
+
`EASYFM DEBUG: ${JSON.stringify(options)} ${
|
|
143
|
+
url instanceof URL
|
|
144
|
+
? url.toString()
|
|
145
|
+
: typeof url === 'string' ? url : url.url
|
|
146
|
+
}`
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
const reqIsToDBHost = (
|
|
150
|
+
url instanceof URL
|
|
151
|
+
? url
|
|
152
|
+
: typeof url === 'string'
|
|
153
|
+
? new URL(url)
|
|
154
|
+
: new URL(url.url)
|
|
155
|
+
).hostname === this.host.hostname
|
|
156
|
+
if (reqIsToDBHost && this.token === '') await this.login(true)
|
|
138
157
|
|
|
139
158
|
if (!options.headers) options.headers = {}
|
|
140
|
-
options.headers.authorization = 'Bearer ' + this._token
|
|
159
|
+
if (reqIsToDBHost) options.headers.authorization = 'Bearer ' + this._token
|
|
141
160
|
// options.redirect = "manual"
|
|
142
161
|
// options.headers.cookies = this._generateCookieHeader()
|
|
143
162
|
// console.log(options.headers.cookies)
|
|
@@ -158,19 +177,21 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
158
177
|
// console.log(this.cookies, _fetch.status)
|
|
159
178
|
// // console.log(this, this.cookies, url)
|
|
160
179
|
// console.trace()
|
|
161
|
-
if (_fetch.
|
|
162
|
-
|
|
180
|
+
if (!_fetch.ok && (!options.retries || options.retries > 0)) {
|
|
181
|
+
if (this.debug) {
|
|
182
|
+
console.log(`EASYFM DEBUG: RE-ATTEMPTING REQUEST (${_fetch.status}) ${
|
|
183
|
+
url instanceof URL
|
|
184
|
+
? url.toString()
|
|
185
|
+
: typeof url === 'string' ? url : url.url
|
|
186
|
+
}`)
|
|
187
|
+
}
|
|
188
|
+
return await this._apiRequestRaw(url, {...options, retries: (options?.retries ?? 1) - 1})
|
|
189
|
+
}
|
|
190
|
+
else if (_fetch.status === 401 && reqIsToDBHost && autoRelogin) {
|
|
163
191
|
await this.login(true)
|
|
164
192
|
return await this._apiRequestRaw(url, options, false)
|
|
165
|
-
} else return _fetch
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private _generateCookieHeader () {
|
|
169
|
-
const out: string[] = []
|
|
170
|
-
for (const key of Object.keys(this.cookies)) {
|
|
171
|
-
out.push(`${key}=${this.cookies[key]}`)
|
|
172
193
|
}
|
|
173
|
-
return
|
|
194
|
+
else return _fetch
|
|
174
195
|
}
|
|
175
196
|
|
|
176
197
|
async _apiRequestJSON<T = any>(url: URL | RequestInfo, options: RequestInit & {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RequestInfo, type RequestInit, Response } from 'node-fetch';
|
|
2
|
+
export declare class RequestHoldableClass {
|
|
3
|
+
#private;
|
|
4
|
+
protected cookies: any;
|
|
5
|
+
_addNewFuncToQueue(func: () => Promise<void>): void;
|
|
6
|
+
protected _holdableFetch(url: URL | RequestInfo, options: RequestInit & {
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
}): Promise<Response>;
|
|
9
|
+
}
|
|
10
|
+
export declare function RequestHoldable<PROPS extends any[], R = any>(originalMethod: (...p: PROPS) => Promise<R>, context: ClassMethodDecoratorContext<RequestHoldableClass>): (this: RequestHoldableClass, ...args: PROPS) => Promise<R>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024. See LICENSE file for more information
|
|
3
|
+
*/
|
|
4
|
+
// @ts-expect-error - NO types module for this
|
|
5
|
+
import fetchWithCookies, { CookieJar } from 'node-fetch-cookies';
|
|
6
|
+
export class RequestHoldableClass {
|
|
7
|
+
#functionQueue = [];
|
|
8
|
+
#requestsAreBeingHeld = false;
|
|
9
|
+
cookies = new CookieJar();
|
|
10
|
+
_addNewFuncToQueue(func) {
|
|
11
|
+
this.#functionQueue.push(func);
|
|
12
|
+
if (this.#functionQueue.length === 1)
|
|
13
|
+
void this.#executeQueue();
|
|
14
|
+
}
|
|
15
|
+
async _holdableFetch(url, options) {
|
|
16
|
+
url.
|
|
17
|
+
if(!this.#requestsAreBeingHeld);
|
|
18
|
+
{
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/return-await
|
|
20
|
+
return await fetchWithCookies(this.cookies, url, options);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async #executeQueue() {
|
|
24
|
+
while (this.#functionQueue.length !== 0) {
|
|
25
|
+
try {
|
|
26
|
+
await this.#functionQueue[0]();
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.error(e);
|
|
30
|
+
}
|
|
31
|
+
this.#functionQueue.shift();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function RequestHoldable(originalMethod, context) {
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
37
|
+
function replacementMethod(...args) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
40
|
+
this._addNewFuncToQueue(() => originalMethod.call(this, ...args).then(resolve).catch(reject));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return replacementMethod;
|
|
44
|
+
}
|
package/src/records/field.d.ts
CHANGED
|
@@ -48,6 +48,12 @@ export declare class Field<T extends FieldValue> {
|
|
|
48
48
|
* @returns {Promise<void>} - A promise that resolves when the file is successfully uploaded.
|
|
49
49
|
*/
|
|
50
50
|
upload(file: File): Promise<void>;
|
|
51
|
-
stream(): Promise<
|
|
52
|
-
|
|
51
|
+
stream(): Promise<{
|
|
52
|
+
data: NodeJS.ReadableStream;
|
|
53
|
+
mime: string;
|
|
54
|
+
}>;
|
|
55
|
+
arrayBuffer(): Promise<{
|
|
56
|
+
data: ArrayBuffer;
|
|
57
|
+
mime: string;
|
|
58
|
+
}>;
|
|
53
59
|
}
|
package/src/records/field.js
CHANGED
|
@@ -62,7 +62,7 @@ let Field = (() => {
|
|
|
62
62
|
if (!req.ok || !req.body) {
|
|
63
63
|
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`);
|
|
64
64
|
}
|
|
65
|
-
return req.body;
|
|
65
|
+
return { data: req.body, mime: req.headers.get("Content-Type") ?? '' };
|
|
66
66
|
}, "#streamAsync") }, _private_streamAsync_decorators, { kind: "method", name: "#streamAsync", static: false, private: true, access: { has: obj => #streamAsync in obj, get: obj => obj.#streamAsync }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
67
67
|
__esDecorate(this, null, _arrayBuffer_decorators, { kind: "method", name: "arrayBuffer", static: false, private: false, access: { has: obj => "arrayBuffer" in obj, get: obj => obj.arrayBuffer }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
68
68
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
@@ -204,7 +204,7 @@ let Field = (() => {
|
|
|
204
204
|
const req = await this.parent.layout.database._apiRequestRaw(this.string, { useCookieJar: true });
|
|
205
205
|
if (!req.ok)
|
|
206
206
|
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`);
|
|
207
|
-
return await req.arrayBuffer();
|
|
207
|
+
return { data: await req.arrayBuffer(), mime: req.headers.get("Content-Type") ?? '' };
|
|
208
208
|
}
|
|
209
209
|
};
|
|
210
210
|
})();
|
package/src/records/field.ts
CHANGED
|
@@ -160,12 +160,12 @@ export class Field<T extends FieldValue> {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
@containerDownloadFunction
|
|
163
|
-
async #streamAsync () {
|
|
163
|
+
async #streamAsync (): Promise<{data: NodeJS.ReadableStream, mime: string}> {
|
|
164
164
|
const req = await this.parent.layout.database._apiRequestRaw(this.string, {useCookieJar: true})
|
|
165
165
|
if (!req.ok || !req.body) {
|
|
166
166
|
throw new Error(`HTTP Error: ${req.status} (${req.statusText})`)
|
|
167
167
|
}
|
|
168
|
-
return req.body
|
|
168
|
+
return {data: req.body, mime: req.headers.get("Content-Type") ?? ''}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
async stream () {
|
|
@@ -173,10 +173,10 @@ export class Field<T extends FieldValue> {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
@containerDownloadFunction
|
|
176
|
-
async arrayBuffer () {
|
|
176
|
+
async arrayBuffer (): Promise<{data: ArrayBuffer, mime: string}> {
|
|
177
177
|
const req = await this.parent.layout.database._apiRequestRaw(this.string, {useCookieJar: true})
|
|
178
178
|
if (!req.ok) throw new Error(`HTTP Error: ${req.status} (${req.statusText})`)
|
|
179
|
-
return await req.arrayBuffer()
|
|
179
|
+
return {data: await req.arrayBuffer(), mime: req.headers.get("Content-Type") ?? ''}
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|