@jd-data-limited/easy-fm 4.1.3 → 4.1.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/README.md +24 -16
- package/package.json +1 -1
- package/src/bin/cli.js +5 -7
- package/src/bin/dist/bin/generateTypes.d.ts +1 -0
- package/src/bin/generateTypes.js +121 -215
- 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 +4 -2
- package/src/connection/database.js +37 -18
- package/src/connection/database.ts +52 -21
- package/src/connection/requestHoldable.d.ts +10 -0
- package/src/connection/requestHoldable.js +44 -0
- 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/README.md
CHANGED
|
@@ -15,6 +15,7 @@ FileMaker Data API.
|
|
|
15
15
|
* [Installation](#installation)
|
|
16
16
|
* [Usage](#usage)
|
|
17
17
|
* [Connecting to a database](#connecting-to-a-database)
|
|
18
|
+
* [An important note about timezones](#an-important-note-about-timezones)
|
|
18
19
|
* [Getting records](#getting-records)
|
|
19
20
|
* [Fetch a range of records](#fetch-a-range-of-records)
|
|
20
21
|
* [Searching for records](#searching-for-records)
|
|
@@ -43,21 +44,11 @@ easy-fm also requires the following to be configured within your FileMaker envir
|
|
|
43
44
|
|
|
44
45
|
# Usage
|
|
45
46
|
|
|
46
|
-
- You need to know what your server's UTC time offset (in minutes) is.
|
|
47
|
-
- Running `0 - (new Date()).getTimezoneOffset()` in javascript will give you the UTC time offset for your current
|
|
48
|
-
timezone.
|
|
49
|
-
|
|
50
47
|
## Connecting to a database
|
|
51
48
|
|
|
52
|
-
easy-fm currently does not support connecting to external data sources, or authenticating by any method other than plain
|
|
53
|
-
FileMaker authentication.
|
|
54
|
-
|
|
55
49
|
```javascript
|
|
56
|
-
import FMHost
|
|
57
|
-
const host = new FMHost(
|
|
58
|
-
"https://<your-servers-address>",
|
|
59
|
-
700 // Timezone offset
|
|
60
|
-
)
|
|
50
|
+
import FMHost from "easy-fm"; // Import the module
|
|
51
|
+
const host = new FMHost("https://<your-servers-address>")
|
|
61
52
|
const database = host.database({
|
|
62
53
|
database: "your_database.fmp12",
|
|
63
54
|
credentials: {
|
|
@@ -68,17 +59,33 @@ const database = host.database({
|
|
|
68
59
|
externalSources: []
|
|
69
60
|
})
|
|
70
61
|
|
|
62
|
+
// OPTIONAL - EasyFM will automatically attempt a login anyway when you perform your first operation
|
|
71
63
|
database.login().then(() => {
|
|
72
|
-
|
|
64
|
+
|
|
73
65
|
})
|
|
74
66
|
```
|
|
75
67
|
|
|
76
68
|
> **NOTE:** A connection will only give you access to the layouts in the database you are connected to, and not the
|
|
77
69
|
> layouts
|
|
78
70
|
> in
|
|
79
|
-
>
|
|
71
|
+
> any external sources that you have specified.
|
|
80
72
|
>
|
|
81
|
-
> If you need to interact with layouts on multiple databases, you need to open
|
|
73
|
+
> If you need to interact with layouts on multiple databases, you need to open a separate connection for each.
|
|
74
|
+
|
|
75
|
+
## An important note about timezones
|
|
76
|
+
|
|
77
|
+
Although it is recommended, timestamps in FileMaker databases are not always stored in UTC time. To account for this,
|
|
78
|
+
EasyFM allows you to specify a function/method that determines the server's current timezone.
|
|
79
|
+
EasyFM will use this timezone offset to convert timestamps to and from JavaScript Date objects.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import FMHost from "easy-fm";
|
|
83
|
+
import {type Moment} from 'moment'
|
|
84
|
+
|
|
85
|
+
const host = new FMHost("https://<your-servers-address>", (moment: Moment) => {
|
|
86
|
+
|
|
87
|
+
})
|
|
88
|
+
```
|
|
82
89
|
|
|
83
90
|
## Getting records
|
|
84
91
|
|
|
@@ -101,6 +108,7 @@ console.log(records)
|
|
|
101
108
|
```
|
|
102
109
|
|
|
103
110
|
### Searching for records
|
|
111
|
+
|
|
104
112
|
Searching for records uses the same syntax as above, but with additional steps to add your search parameters.
|
|
105
113
|
|
|
106
114
|
```javascript
|
|
@@ -218,4 +226,4 @@ const layout = database.getLayout("users") // The UsersLayout interface will be
|
|
|
218
226
|
const record = await layout.records.create()
|
|
219
227
|
record.fields["first_name"].value = "Joe"
|
|
220
228
|
record.fields["age"].value = 38
|
|
221
|
-
```
|
|
229
|
+
```
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
2
|
/*
|
|
4
3
|
* Copyright (c) 2024. See LICENSE file for more information
|
|
5
4
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var program = new commander_1.Command();
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { generateTypesCLI } from "./generateTypes.js";
|
|
7
|
+
const program = new Command();
|
|
10
8
|
program
|
|
11
9
|
.name("easyfm")
|
|
12
10
|
.description("A NodeJS wrapper for the FileMaker Data CLI");
|
|
13
11
|
program
|
|
14
12
|
.command("generate-types")
|
|
15
13
|
.description("Automatically generate layout interfaces from your database")
|
|
16
|
-
.action(
|
|
17
|
-
|
|
14
|
+
.action(() => {
|
|
15
|
+
generateTypesCLI();
|
|
18
16
|
});
|
|
19
17
|
program.parse(process.argv);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateTypesCLI(): Promise<void>;
|
package/src/bin/generateTypes.js
CHANGED
|
@@ -1,53 +1,10 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/*
|
|
3
2
|
* Copyright (c) 2024. See LICENSE file for more information
|
|
4
3
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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");
|
|
4
|
+
import { ApiFieldResultTypes } from "../models/apiResults.js";
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import FMHost from "../connection/FMHost.js";
|
|
7
|
+
import * as fs from "fs";
|
|
51
8
|
function sub(str) {
|
|
52
9
|
return str
|
|
53
10
|
.replace(/[^a-zA-Z0-9_]/g, "") // Remove non-alphanumeric characters
|
|
@@ -55,42 +12,38 @@ function sub(str) {
|
|
|
55
12
|
}
|
|
56
13
|
function generateFieldType(field) {
|
|
57
14
|
switch (field.result) {
|
|
58
|
-
case
|
|
15
|
+
case ApiFieldResultTypes.TEXT:
|
|
59
16
|
return "Field<string>";
|
|
60
|
-
case
|
|
17
|
+
case ApiFieldResultTypes.CONTAINER:
|
|
61
18
|
return "Field<Container>";
|
|
62
|
-
case
|
|
19
|
+
case ApiFieldResultTypes.DATE:
|
|
63
20
|
return "Field<Date>";
|
|
64
|
-
case
|
|
21
|
+
case ApiFieldResultTypes.NUMBER:
|
|
65
22
|
return "Field<number>";
|
|
66
|
-
case
|
|
23
|
+
case ApiFieldResultTypes.TIME:
|
|
67
24
|
return "Field<Date>";
|
|
68
|
-
case
|
|
25
|
+
case ApiFieldResultTypes.TIMESTAMP:
|
|
69
26
|
return "Field<Date>";
|
|
70
27
|
}
|
|
71
28
|
}
|
|
72
29
|
function processFieldsChunk(fields) {
|
|
73
|
-
|
|
74
|
-
fields = fields.filter(
|
|
30
|
+
let existing_fields = [];
|
|
31
|
+
fields = fields.filter(field => {
|
|
75
32
|
if (existing_fields.includes(field.name))
|
|
76
33
|
return false;
|
|
77
34
|
existing_fields.push(field.name);
|
|
78
35
|
return true;
|
|
79
36
|
});
|
|
80
|
-
return fields.map(
|
|
37
|
+
return fields.map(field => {
|
|
81
38
|
return JSON.stringify(field.name) + ": " + generateFieldType(field);
|
|
82
39
|
});
|
|
83
40
|
}
|
|
84
41
|
function camelToSnakeCase(str) {
|
|
85
42
|
return str.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
86
43
|
}
|
|
87
|
-
function safeStringInjection(str) {
|
|
88
|
-
|
|
89
|
-
for (
|
|
90
|
-
args[_i - 1] = arguments[_i];
|
|
91
|
-
}
|
|
92
|
-
var result = "";
|
|
93
|
-
for (var i = 0; i < str.length; i++) {
|
|
44
|
+
function safeStringInjection(str, ...args) {
|
|
45
|
+
let result = "";
|
|
46
|
+
for (let i = 0; i < str.length; i++) {
|
|
94
47
|
result += str[i];
|
|
95
48
|
if (i < args.length) {
|
|
96
49
|
result += JSON.stringify(args[i].toString());
|
|
@@ -98,158 +51,111 @@ function safeStringInjection(str) {
|
|
|
98
51
|
}
|
|
99
52
|
return result;
|
|
100
53
|
}
|
|
101
|
-
function generateTypesCLI() {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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*/];
|
|
54
|
+
export async function generateTypesCLI() {
|
|
55
|
+
process.stdout.write('\x1Bc');
|
|
56
|
+
console.log("For this tool to work, you are required to have access to an account on the target database.\n" +
|
|
57
|
+
"This account must have the fmrest extended privilege enabled.\n" +
|
|
58
|
+
"This tool can only export type interfaces for layouts that the given account has access to.");
|
|
59
|
+
console.log("\n");
|
|
60
|
+
const data = await inquirer.prompt([
|
|
61
|
+
{
|
|
62
|
+
type: "input",
|
|
63
|
+
name: "hostname",
|
|
64
|
+
message: "The IP address/domain of your FileMaker server (must start with either http:// or https://)",
|
|
65
|
+
validate(input) {
|
|
66
|
+
return input.startsWith("http://") || input.startsWith("https://") || "Please enter a valid IP address/domain starting with http:// or https://";
|
|
250
67
|
}
|
|
251
|
-
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: "number",
|
|
71
|
+
name: "timezoneOffset",
|
|
72
|
+
message: "Server timezone offset (minutes)",
|
|
73
|
+
default: 0 - (new Date()).getTimezoneOffset()
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: "confirm",
|
|
77
|
+
name: "verify",
|
|
78
|
+
default: true,
|
|
79
|
+
message: "Verify your server's identity",
|
|
80
|
+
when(previousAnswers) {
|
|
81
|
+
return previousAnswers.hostname.startsWith("https://");
|
|
82
|
+
}
|
|
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: []
|
|
252
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
|
+
});
|
|
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");
|
|
253
161
|
}
|
|
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;
|
|
@@ -13,6 +13,7 @@ import { type RequestInfo, type RequestInit, type Response } from 'node-fetch';
|
|
|
13
13
|
* @template T - The structure of the database.
|
|
14
14
|
*/
|
|
15
15
|
export declare class Database<T extends DatabaseStructure> extends EventEmitter implements DatabaseBase {
|
|
16
|
+
#private;
|
|
16
17
|
private _token;
|
|
17
18
|
readonly host: HostBase;
|
|
18
19
|
private readonly connection_details;
|
|
@@ -48,8 +49,8 @@ export declare class Database<T extends DatabaseStructure> extends EventEmitter
|
|
|
48
49
|
_apiRequestRaw(url: URL | RequestInfo, options?: RequestInit & {
|
|
49
50
|
headers?: Record<string, string>;
|
|
50
51
|
useCookieJar?: boolean;
|
|
52
|
+
retries?: number;
|
|
51
53
|
}, autoRelogin?: boolean): Promise<Response>;
|
|
52
|
-
private _generateCookieHeader;
|
|
53
54
|
_apiRequestJSON<T = any>(url: URL | RequestInfo, options?: RequestInit & {
|
|
54
55
|
headers?: Record<string, string>;
|
|
55
56
|
}): Promise<ApiResults<T>>;
|
|
@@ -59,8 +60,9 @@ export declare class Database<T extends DatabaseStructure> extends EventEmitter
|
|
|
59
60
|
* @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
|
|
60
61
|
* @throws {FMError} If there was an error retrieving the layouts.
|
|
61
62
|
*/
|
|
62
|
-
listLayouts(): Promise<Layout<LayoutInterface>[]>;
|
|
63
|
+
listLayouts(page?: number): Promise<Layout<LayoutInterface>[]>;
|
|
63
64
|
layout<R extends keyof T['layouts']>(name: R): Layout<T['layouts'][R]>;
|
|
64
65
|
layout<R extends LayoutInterface>(name: string): Layout<R>;
|
|
66
|
+
clearLayoutCache(): void;
|
|
65
67
|
script(name: string, parameter?: string): Script;
|
|
66
68
|
}
|
|
@@ -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.
|
|
@@ -19,6 +19,7 @@ export class Database extends EventEmitter {
|
|
|
19
19
|
cookies = new CookieJar();
|
|
20
20
|
name;
|
|
21
21
|
debug;
|
|
22
|
+
#layoutCache = new Map();
|
|
22
23
|
constructor(host, conn) {
|
|
23
24
|
super();
|
|
24
25
|
this.host = host;
|
|
@@ -109,16 +110,25 @@ export class Database extends EventEmitter {
|
|
|
109
110
|
* @returns {string} The endpoint URL.
|
|
110
111
|
*/
|
|
111
112
|
get endpoint() {
|
|
112
|
-
return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
|
|
113
|
+
return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
|
|
113
114
|
}
|
|
114
115
|
async _apiRequestRaw(url, options = {}, autoRelogin = true) {
|
|
115
|
-
if (this.debug)
|
|
116
|
-
console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof
|
|
117
|
-
|
|
116
|
+
if (this.debug) {
|
|
117
|
+
console.log(`EASYFM DEBUG: ${JSON.stringify(options)} ${url instanceof URL
|
|
118
|
+
? url.toString()
|
|
119
|
+
: typeof url === 'string' ? url : url.url}`);
|
|
120
|
+
}
|
|
121
|
+
const reqIsToDBHost = (url instanceof URL
|
|
122
|
+
? url
|
|
123
|
+
: typeof url === 'string'
|
|
124
|
+
? new URL(url)
|
|
125
|
+
: new URL(url.url)).hostname === this.host.hostname;
|
|
126
|
+
if (reqIsToDBHost && this.token === '')
|
|
118
127
|
await this.login(true);
|
|
119
128
|
if (!options.headers)
|
|
120
129
|
options.headers = {};
|
|
121
|
-
|
|
130
|
+
if (reqIsToDBHost)
|
|
131
|
+
options.headers.authorization = 'Bearer ' + this._token;
|
|
122
132
|
// options.redirect = "manual"
|
|
123
133
|
// options.headers.cookies = this._generateCookieHeader()
|
|
124
134
|
// console.log(options.headers.cookies)
|
|
@@ -137,21 +147,21 @@ export class Database extends EventEmitter {
|
|
|
137
147
|
// console.log(this.cookies, _fetch.status)
|
|
138
148
|
// // console.log(this, this.cookies, url)
|
|
139
149
|
// console.trace()
|
|
140
|
-
if (_fetch.
|
|
141
|
-
|
|
150
|
+
if (!_fetch.ok && (!options.retries || options.retries > 0)) {
|
|
151
|
+
if (this.debug) {
|
|
152
|
+
console.log(`EASYFM DEBUG: RE-ATTEMPTING REQUEST (${_fetch.status}) ${url instanceof URL
|
|
153
|
+
? url.toString()
|
|
154
|
+
: typeof url === 'string' ? url : url.url}`);
|
|
155
|
+
}
|
|
156
|
+
return await this._apiRequestRaw(url, { ...options, retries: (options?.retries ?? 1) - 1 });
|
|
157
|
+
}
|
|
158
|
+
else if (_fetch.status === 401 && reqIsToDBHost && autoRelogin) {
|
|
142
159
|
await this.login(true);
|
|
143
160
|
return await this._apiRequestRaw(url, options, false);
|
|
144
161
|
}
|
|
145
162
|
else
|
|
146
163
|
return _fetch;
|
|
147
164
|
}
|
|
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
165
|
async _apiRequestJSON(url, options = {}) {
|
|
156
166
|
if (!options.headers)
|
|
157
167
|
options.headers = {};
|
|
@@ -174,8 +184,9 @@ export class Database extends EventEmitter {
|
|
|
174
184
|
* @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
|
|
175
185
|
* @throws {FMError} If there was an error retrieving the layouts.
|
|
176
186
|
*/
|
|
177
|
-
async listLayouts() {
|
|
178
|
-
const req = await this._apiRequestJSON(`${this.endpoint}/layouts?page
|
|
187
|
+
async listLayouts(page = 0) {
|
|
188
|
+
const req = await this._apiRequestJSON(`${this.endpoint}/layouts?page=${encodeURIComponent(page)}`);
|
|
189
|
+
console.log(req);
|
|
179
190
|
if (!req.response)
|
|
180
191
|
throw new FMError(req.messages[0].code, req.httpStatus, req.messages[0].message);
|
|
181
192
|
const cycleLayoutNames = (layouts) => {
|
|
@@ -191,7 +202,15 @@ export class Database extends EventEmitter {
|
|
|
191
202
|
return cycleLayoutNames(req.response.layouts).map(layout => new Layout(this, layout));
|
|
192
203
|
}
|
|
193
204
|
layout(name) {
|
|
194
|
-
|
|
205
|
+
let layout = this.#layoutCache.get(name);
|
|
206
|
+
if (layout)
|
|
207
|
+
return layout;
|
|
208
|
+
layout = new Layout(this, name);
|
|
209
|
+
this.#layoutCache.set(name, layout);
|
|
210
|
+
return layout;
|
|
211
|
+
}
|
|
212
|
+
clearLayoutCache() {
|
|
213
|
+
this.#layoutCache.clear();
|
|
195
214
|
}
|
|
196
215
|
script(name, parameter = '') {
|
|
197
216
|
return ({ name, parameter });
|
|
@@ -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
|
/**
|
|
@@ -27,6 +27,7 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
27
27
|
private cookies = new CookieJar()
|
|
28
28
|
readonly name: string
|
|
29
29
|
readonly debug: boolean
|
|
30
|
+
readonly #layoutCache = new Map<string, Layout<any>>()
|
|
30
31
|
|
|
31
32
|
constructor (host: HostBase, conn: databaseOptionsWithExternalSources) {
|
|
32
33
|
super()
|
|
@@ -45,7 +46,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
45
46
|
username: _data.username,
|
|
46
47
|
password: _data.password
|
|
47
48
|
}
|
|
48
|
-
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
49
51
|
throw new Error('Not yet supported login method')
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -89,7 +91,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
89
91
|
if (this.connection_details.credentials.method === 'token') {
|
|
90
92
|
this._token = (this.connection_details.credentials).token
|
|
91
93
|
return this.token
|
|
92
|
-
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
93
96
|
const url = new URL(`${this.endpoint}/sessions`)
|
|
94
97
|
url.hostname = this.host.hostname
|
|
95
98
|
const res = await fetch(url, {
|
|
@@ -106,7 +109,8 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
106
109
|
if (res.status === 200) {
|
|
107
110
|
this._token = res.headers.get('x-fm-data-access-token') ?? ''
|
|
108
111
|
return this._token
|
|
109
|
-
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
110
114
|
throw new FMError(
|
|
111
115
|
_res.messages[0].code as string | number,
|
|
112
116
|
_res.status as number,
|
|
@@ -126,18 +130,34 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
126
130
|
* @returns {string} The endpoint URL.
|
|
127
131
|
*/
|
|
128
132
|
get endpoint (): string {
|
|
129
|
-
return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`
|
|
133
|
+
return `${this.host.protocol}//${this.host.hostname}/fmi/data/v2/databases/${this.name}`
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
async _apiRequestRaw (url: URL | RequestInfo, options: RequestInit & {
|
|
133
137
|
headers?: Record<string, string>
|
|
134
138
|
useCookieJar?: boolean
|
|
139
|
+
retries?: number
|
|
135
140
|
} = {}, autoRelogin = true): Promise<Response> {
|
|
136
|
-
if (this.debug)
|
|
137
|
-
|
|
141
|
+
if (this.debug) {
|
|
142
|
+
console.log(
|
|
143
|
+
`EASYFM DEBUG: ${JSON.stringify(options)} ${
|
|
144
|
+
url instanceof URL
|
|
145
|
+
? url.toString()
|
|
146
|
+
: typeof url === 'string' ? url : url.url
|
|
147
|
+
}`
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
const reqIsToDBHost = (
|
|
151
|
+
url instanceof URL
|
|
152
|
+
? url
|
|
153
|
+
: typeof url === 'string'
|
|
154
|
+
? new URL(url)
|
|
155
|
+
: new URL(url.url)
|
|
156
|
+
).hostname === this.host.hostname
|
|
157
|
+
if (reqIsToDBHost && this.token === '') await this.login(true)
|
|
138
158
|
|
|
139
159
|
if (!options.headers) options.headers = {}
|
|
140
|
-
options.headers.authorization = 'Bearer ' + this._token
|
|
160
|
+
if (reqIsToDBHost) options.headers.authorization = 'Bearer ' + this._token
|
|
141
161
|
// options.redirect = "manual"
|
|
142
162
|
// options.headers.cookies = this._generateCookieHeader()
|
|
143
163
|
// console.log(options.headers.cookies)
|
|
@@ -158,19 +178,21 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
158
178
|
// console.log(this.cookies, _fetch.status)
|
|
159
179
|
// // console.log(this, this.cookies, url)
|
|
160
180
|
// console.trace()
|
|
161
|
-
if (_fetch.
|
|
162
|
-
|
|
181
|
+
if (!_fetch.ok && (!options.retries || options.retries > 0)) {
|
|
182
|
+
if (this.debug) {
|
|
183
|
+
console.log(`EASYFM DEBUG: RE-ATTEMPTING REQUEST (${_fetch.status}) ${
|
|
184
|
+
url instanceof URL
|
|
185
|
+
? url.toString()
|
|
186
|
+
: typeof url === 'string' ? url : url.url
|
|
187
|
+
}`)
|
|
188
|
+
}
|
|
189
|
+
return await this._apiRequestRaw(url, {...options, retries: (options?.retries ?? 1) - 1})
|
|
190
|
+
}
|
|
191
|
+
else if (_fetch.status === 401 && reqIsToDBHost && autoRelogin) {
|
|
163
192
|
await this.login(true)
|
|
164
193
|
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
194
|
}
|
|
173
|
-
return
|
|
195
|
+
else return _fetch
|
|
174
196
|
}
|
|
175
197
|
|
|
176
198
|
async _apiRequestJSON<T = any>(url: URL | RequestInfo, options: RequestInit & {
|
|
@@ -199,8 +221,9 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
199
221
|
* @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
|
|
200
222
|
* @throws {FMError} If there was an error retrieving the layouts.
|
|
201
223
|
*/
|
|
202
|
-
async listLayouts () {
|
|
203
|
-
const req = await this._apiRequestJSON<{ layouts: ApiLayout[] }>(`${this.endpoint}/layouts?page
|
|
224
|
+
async listLayouts (page: number = 0) {
|
|
225
|
+
const req = await this._apiRequestJSON<{ layouts: ApiLayout[] }>(`${this.endpoint}/layouts?page=${encodeURIComponent(page)}`)
|
|
226
|
+
console.log(req)
|
|
204
227
|
if (!req.response) throw new FMError(req.messages[0].code, req.httpStatus, req.messages[0].message)
|
|
205
228
|
|
|
206
229
|
const cycleLayoutNames = (layouts: ApiLayout[]) => {
|
|
@@ -217,7 +240,15 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
|
|
|
217
240
|
layout<R extends keyof T['layouts']>(name: R): Layout<T['layouts'][R]>
|
|
218
241
|
layout<R extends LayoutInterface>(name: string): Layout<R>
|
|
219
242
|
layout (name: string): Layout<any> {
|
|
220
|
-
|
|
243
|
+
let layout = this.#layoutCache.get(name)
|
|
244
|
+
if (layout) return layout
|
|
245
|
+
layout = new Layout<LayoutInterface>(this, name)
|
|
246
|
+
this.#layoutCache.set(name, layout)
|
|
247
|
+
return layout
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
clearLayoutCache () {
|
|
251
|
+
this.#layoutCache.clear()
|
|
221
252
|
}
|
|
222
253
|
|
|
223
254
|
script (name: string, parameter = ''): Script {
|
|
@@ -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.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 { data: req.body, mime: req.headers.get(
|
|
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 { data: await req.arrayBuffer(), mime: req.headers.get(
|
|
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 (): Promise<{data: NodeJS.ReadableStream, mime: string}> {
|
|
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 {data: req.body, mime: req.headers.get(
|
|
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 (): Promise<{data: ArrayBuffer, mime: string}> {
|
|
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 {data: await req.arrayBuffer(), mime: req.headers.get(
|
|
179
|
+
return {data: await req.arrayBuffer(), mime: req.headers.get('Content-Type') ?? ''}
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|