@jd-data-limited/easy-fm 4.1.4 → 4.1.6

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 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, {FMError} from "easy-fm"; // Import the module
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
- // Record operations can only be performed after a successful login
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
- > the external sources that you have specified.
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 one connection per database.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jd-data-limited/easy-fm",
3
- "version": "4.1.4",
3
+ "version": "4.1.6",
4
4
  "dependencies": {
5
5
  "@types/follow-redirects": "^1.14.4",
6
6
  "follow-redirects": "^1.15.5",
@@ -11,7 +11,7 @@
11
11
  "type": "module",
12
12
  "main": "src/index.js",
13
13
  "module": "es2022",
14
- "homepage": "https://github.com/JD-Data-Limited/easy-fm",
14
+ "homepage": "https://jd-data.com/project/easyfm/",
15
15
  "url": "https://github.com/JD-Data-Limited/easy-fm",
16
16
  "license": "SEE LICENSE IN LICENSE",
17
17
  "description": "easy-fm is a Node.js module that allows you to interact with\r a [FileMaker database stored](https://www.claris.com/filemaker/) on\r a [FileMaker server](https://www.claris.com/filemaker/server/). This module interacts with your server using the\r [FileMaker Data API](https://help.claris.com/en/data-api-guide/content/index.html).",
@@ -19,7 +19,7 @@
19
19
  "./src/**/*"
20
20
  ],
21
21
  "scripts": {
22
- "test": "node --test tests/login.js",
22
+ "test": "npx tsc -p . && node --test tests/login.js",
23
23
  "build": "webpack"
24
24
  },
25
25
  "repository": {
@@ -28,8 +28,11 @@
28
28
  },
29
29
  "keywords": [
30
30
  "FileMaker",
31
- "Data API",
32
- "REST"
31
+ "Data",
32
+ "API",
33
+ "REST",
34
+ "simplicity",
35
+ "business"
33
36
  ],
34
37
  "author": "JD-Data Limited",
35
38
  "bugs": {
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
- Object.defineProperty(exports, "__esModule", { value: true });
7
- var commander_1 = require("commander");
8
- var generateTypes_js_1 = require("./generateTypes.js");
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(function () {
17
- (0, generateTypes_js_1.generateTypesCLI)();
14
+ .action(() => {
15
+ generateTypesCLI();
18
16
  });
19
17
  program.parse(process.argv);
@@ -0,0 +1 @@
1
+ export declare function generateTypesCLI(): Promise<void>;
@@ -1,53 +1,10 @@
1
- "use strict";
2
1
  /*
3
2
  * Copyright (c) 2024. See LICENSE file for more information
4
3
  */
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");
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 apiResults_js_1.ApiFieldResultTypes.TEXT:
15
+ case ApiFieldResultTypes.TEXT:
59
16
  return "Field<string>";
60
- case apiResults_js_1.ApiFieldResultTypes.CONTAINER:
17
+ case ApiFieldResultTypes.CONTAINER:
61
18
  return "Field<Container>";
62
- case apiResults_js_1.ApiFieldResultTypes.DATE:
19
+ case ApiFieldResultTypes.DATE:
63
20
  return "Field<Date>";
64
- case apiResults_js_1.ApiFieldResultTypes.NUMBER:
21
+ case ApiFieldResultTypes.NUMBER:
65
22
  return "Field<number>";
66
- case apiResults_js_1.ApiFieldResultTypes.TIME:
23
+ case ApiFieldResultTypes.TIME:
67
24
  return "Field<Date>";
68
- case apiResults_js_1.ApiFieldResultTypes.TIMESTAMP:
25
+ case ApiFieldResultTypes.TIMESTAMP:
69
26
  return "Field<Date>";
70
27
  }
71
28
  }
72
29
  function processFieldsChunk(fields) {
73
- var existing_fields = [];
74
- fields = fields.filter(function (field) {
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(function (field) {
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
- 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++) {
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
- 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*/];
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;
@@ -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;
@@ -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
  }
@@ -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;
@@ -183,8 +184,9 @@ export class Database extends EventEmitter {
183
184
  * @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
184
185
  * @throws {FMError} If there was an error retrieving the layouts.
185
186
  */
186
- async listLayouts() {
187
- const req = await this._apiRequestJSON(`${this.endpoint}/layouts?page=2`);
187
+ async listLayouts(page = 0) {
188
+ const req = await this._apiRequestJSON(`${this.endpoint}/layouts?page=${encodeURIComponent(page)}`);
189
+ console.log(req);
188
190
  if (!req.response)
189
191
  throw new FMError(req.messages[0].code, req.httpStatus, req.messages[0].message);
190
192
  const cycleLayoutNames = (layouts) => {
@@ -200,7 +202,15 @@ export class Database extends EventEmitter {
200
202
  return cycleLayoutNames(req.response.layouts).map(layout => new Layout(this, layout));
201
203
  }
202
204
  layout(name) {
203
- return new Layout(this, name);
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();
204
214
  }
205
215
  script(name, parameter = '') {
206
216
  return ({ name, parameter });
@@ -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()
@@ -220,8 +221,9 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
220
221
  * @returns {Promise<Layout[]>} A promise that resolves to an array of Layout objects.
221
222
  * @throws {FMError} If there was an error retrieving the layouts.
222
223
  */
223
- async listLayouts () {
224
- const req = await this._apiRequestJSON<{ layouts: ApiLayout[] }>(`${this.endpoint}/layouts?page=2`)
224
+ async listLayouts (page: number = 0) {
225
+ const req = await this._apiRequestJSON<{ layouts: ApiLayout[] }>(`${this.endpoint}/layouts?page=${encodeURIComponent(page)}`)
226
+ console.log(req)
225
227
  if (!req.response) throw new FMError(req.messages[0].code, req.httpStatus, req.messages[0].message)
226
228
 
227
229
  const cycleLayoutNames = (layouts: ApiLayout[]) => {
@@ -238,7 +240,15 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
238
240
  layout<R extends keyof T['layouts']>(name: R): Layout<T['layouts'][R]>
239
241
  layout<R extends LayoutInterface>(name: string): Layout<R>
240
242
  layout (name: string): Layout<any> {
241
- return new Layout<LayoutInterface>(this, name)
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()
242
252
  }
243
253
 
244
254
  script (name: string, parameter = ''): Script {
@@ -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("Content-Type") ?? '' };
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("Content-Type") ?? '' };
207
+ return { data: await req.arrayBuffer(), mime: req.headers.get('Content-Type') ?? '' };
208
208
  }
209
209
  };
210
210
  })();
@@ -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("Content-Type") ?? ''}
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("Content-Type") ?? ''}
179
+ return {data: await req.arrayBuffer(), mime: req.headers.get('Content-Type') ?? ''}
180
180
  }
181
181
  }
182
182
 
@@ -44,13 +44,16 @@ export class LayoutRecord extends RecordBase {
44
44
  if (extraBody.scripts.presort.parameter)
45
45
  data['script.presort.param'] = extraBody.scripts.presort.parameter;
46
46
  }
47
+ if (extraBody.options)
48
+ data.options = extraBody.options;
49
+ if (extraBody.deleteRelatedRecords)
50
+ data.fieldData.deleteRelated = extraBody.deleteRelatedRecords.map(i => `${i.table}.${i.recordId}`);
47
51
  if (this.recordId === -1) {
48
52
  // This is a new LayoutRecord
49
53
  const res = await this.layout.database._apiRequestJSON(`${this.layout.endpoint}/records`, {
50
54
  method: 'POST',
51
55
  body: JSON.stringify(data)
52
56
  });
53
- console.log(res);
54
57
  if (!res.response) {
55
58
  throw new FMError(res.messages[0].code, res.httpStatus, res);
56
59
  }
@@ -63,6 +63,8 @@ export class LayoutRecord<LAYOUT extends LayoutInterface> extends RecordBase<LAY
63
63
  data['script.presort'] = extraBody.scripts.presort.name
64
64
  if (extraBody.scripts.presort.parameter) data['script.presort.param'] = extraBody.scripts.presort.parameter
65
65
  }
66
+ if (extraBody.options) data.options = extraBody.options
67
+ if (extraBody.deleteRelatedRecords) data.fieldData.deleteRelated = extraBody.deleteRelatedRecords.map(i => `${i.table}.${i.recordId}`)
66
68
 
67
69
  if (this.recordId === -1) {
68
70
  // This is a new LayoutRecord
@@ -70,7 +72,6 @@ export class LayoutRecord<LAYOUT extends LayoutInterface> extends RecordBase<LAY
70
72
  method: 'POST',
71
73
  body: JSON.stringify(data)
72
74
  })
73
- console.log(res)
74
75
 
75
76
  if (!res.response) {
76
77
  throw new FMError(res.messages[0].code, res.httpStatus, res)
package/src/types.d.ts CHANGED
@@ -51,6 +51,18 @@ export interface ScriptRequestData {
51
51
  }
52
52
  export interface extraBodyOptions {
53
53
  scripts?: ScriptRequestData;
54
+ options?: {
55
+ /**
56
+ * Defines what entry mode to use (FileMaker Server 2024 and newer)
57
+ * user (default) - Field validation occurs as per normal. Request is blocked if validation failed.
58
+ * script - Field validation is overridden/ignored when allowed in the database schema.
59
+ */
60
+ entrymode: 'user' | 'script';
61
+ };
62
+ deleteRelatedRecords?: Array<{
63
+ table: string;
64
+ recordId: number;
65
+ }>;
54
66
  }
55
67
  export interface Script {
56
68
  name: string;
package/src/types.ts CHANGED
@@ -63,6 +63,18 @@ export interface ScriptRequestData {
63
63
 
64
64
  export interface extraBodyOptions {
65
65
  scripts?: ScriptRequestData
66
+ options?: {
67
+ /**
68
+ * Defines what entry mode to use (FileMaker Server 2024 and newer)
69
+ * user (default) - Field validation occurs as per normal. Request is blocked if validation failed.
70
+ * script - Field validation is overridden/ignored when allowed in the database schema.
71
+ */
72
+ entrymode: 'user' | 'script'
73
+ }
74
+ deleteRelatedRecords?: Array<{
75
+ table: string
76
+ recordId: number
77
+ }>
66
78
  }
67
79
 
68
80
  export interface Script {