@jd-data-limited/easy-fm 4.1.5 → 4.1.8

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.
Files changed (86) hide show
  1. package/dist/bin/cli.js +17 -0
  2. package/dist/bin/generateTypes.js +161 -0
  3. package/{src → dist}/connection/FMHost.js +3 -2
  4. package/{src → dist}/connection/database.d.ts +0 -1
  5. package/{src → dist}/connection/database.js +22 -39
  6. package/dist/index.d.ts +18 -0
  7. package/dist/index.js +16 -0
  8. package/{src → dist}/records/field.d.ts +0 -1
  9. package/{src → dist}/records/getOperations/recordGetOperation.d.ts +6 -2
  10. package/{src → dist}/records/getOperations/recordGetOperation.js +9 -5
  11. package/{src → dist}/records/layoutRecord.d.ts +1 -1
  12. package/{src → dist}/records/layoutRecord.js +9 -7
  13. package/{src → dist}/records/recordBase.d.ts +0 -1
  14. package/{src → dist}/types.d.ts +12 -2
  15. package/package.json +23 -12
  16. package/src/FMError.ts +0 -1033
  17. package/src/bin/cli.js +0 -17
  18. package/src/bin/cli.ts +0 -22
  19. package/src/bin/generateTypes.d.ts +0 -1
  20. package/src/bin/generateTypes.js +0 -161
  21. package/src/bin/generateTypes.ts +0 -189
  22. package/src/connection/FMHost.ts +0 -123
  23. package/src/connection/HostBase.ts +0 -18
  24. package/src/connection/database.ts +0 -257
  25. package/src/connection/databaseBase.ts +0 -19
  26. package/src/connection/generateAuthorizationHeaders.ts +0 -32
  27. package/src/connection/requestHoldable.d.ts +0 -10
  28. package/src/connection/requestHoldable.js +0 -44
  29. package/src/databaseStructure.ts +0 -10
  30. package/src/index.d.ts +0 -18
  31. package/src/index.js +0 -18
  32. package/src/index.ts +0 -47
  33. package/src/layouts/layout.ts +0 -66
  34. package/src/layouts/layoutBase.ts +0 -16
  35. package/src/layouts/layoutInterface.ts +0 -13
  36. package/src/layouts/layoutRecordManager.ts +0 -66
  37. package/src/layouts/recordFieldsMap.ts +0 -7
  38. package/src/models/apiResults.ts +0 -110
  39. package/src/records/field.ts +0 -235
  40. package/src/records/getOperations/recordGetOperation.ts +0 -268
  41. package/src/records/layoutRecord.ts +0 -246
  42. package/src/records/layoutRecordBase.ts +0 -10
  43. package/src/records/portal.ts +0 -43
  44. package/src/records/portalBase.ts +0 -14
  45. package/src/records/portalRecord.ts +0 -52
  46. package/src/records/recordBase.ts +0 -91
  47. package/src/types.ts +0 -150
  48. package/src/utils/query.ts +0 -59
  49. /package/{src → dist}/FMError.d.ts +0 -0
  50. /package/{src → dist}/FMError.js +0 -0
  51. /package/{src → dist}/bin/cli.d.ts +0 -0
  52. /package/{src/bin/dist → dist}/bin/generateTypes.d.ts +0 -0
  53. /package/{src → dist}/connection/FMHost.d.ts +0 -0
  54. /package/{src → dist}/connection/HostBase.d.ts +0 -0
  55. /package/{src → dist}/connection/HostBase.js +0 -0
  56. /package/{src → dist}/connection/databaseBase.d.ts +0 -0
  57. /package/{src → dist}/connection/databaseBase.js +0 -0
  58. /package/{src → dist}/connection/generateAuthorizationHeaders.d.ts +0 -0
  59. /package/{src → dist}/connection/generateAuthorizationHeaders.js +0 -0
  60. /package/{src → dist}/databaseStructure.d.ts +0 -0
  61. /package/{src → dist}/databaseStructure.js +0 -0
  62. /package/{src → dist}/layouts/layout.d.ts +0 -0
  63. /package/{src → dist}/layouts/layout.js +0 -0
  64. /package/{src → dist}/layouts/layoutBase.d.ts +0 -0
  65. /package/{src → dist}/layouts/layoutBase.js +0 -0
  66. /package/{src → dist}/layouts/layoutInterface.d.ts +0 -0
  67. /package/{src → dist}/layouts/layoutInterface.js +0 -0
  68. /package/{src → dist}/layouts/layoutRecordManager.d.ts +0 -0
  69. /package/{src → dist}/layouts/layoutRecordManager.js +0 -0
  70. /package/{src → dist}/layouts/recordFieldsMap.d.ts +0 -0
  71. /package/{src → dist}/layouts/recordFieldsMap.js +0 -0
  72. /package/{src → dist}/models/apiResults.d.ts +0 -0
  73. /package/{src → dist}/models/apiResults.js +0 -0
  74. /package/{src → dist}/records/field.js +0 -0
  75. /package/{src → dist}/records/layoutRecordBase.d.ts +0 -0
  76. /package/{src → dist}/records/layoutRecordBase.js +0 -0
  77. /package/{src → dist}/records/portal.d.ts +0 -0
  78. /package/{src → dist}/records/portal.js +0 -0
  79. /package/{src → dist}/records/portalBase.d.ts +0 -0
  80. /package/{src → dist}/records/portalBase.js +0 -0
  81. /package/{src → dist}/records/portalRecord.d.ts +0 -0
  82. /package/{src → dist}/records/portalRecord.js +0 -0
  83. /package/{src → dist}/records/recordBase.js +0 -0
  84. /package/{src → dist}/types.js +0 -0
  85. /package/{src → dist}/utils/query.d.ts +0 -0
  86. /package/{src → dist}/utils/query.js +0 -0
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2024. See LICENSE file for more information
4
+ */
5
+ import { Command } from 'commander';
6
+ import { generateTypesCLI } from './generateTypes.js';
7
+ const program = new Command();
8
+ program
9
+ .name('easyfm')
10
+ .description('A NodeJS wrapper for the FileMaker Data CLI');
11
+ program
12
+ .command('generate-types')
13
+ .description('Automatically generate layout interfaces from your database')
14
+ .action(async () => {
15
+ await generateTypesCLI();
16
+ });
17
+ program.parse(process.argv);
@@ -0,0 +1,161 @@
1
+ /*
2
+ * Copyright (c) 2024. See LICENSE file for more information
3
+ */
4
+ import { ApiFieldResultTypes } from '../models/apiResults.js';
5
+ import inquirer from 'inquirer';
6
+ import FMHost from '../connection/FMHost.js';
7
+ import * as fs from 'node:fs';
8
+ function sub(str) {
9
+ return str
10
+ .replace(/[^a-zA-Z0-9_]/g, '') // Remove non-alphanumeric characters
11
+ .replace(/^[0-9]+/g, ''); // Removing leading numbers
12
+ }
13
+ function generateFieldType(field) {
14
+ switch (field.result) {
15
+ case ApiFieldResultTypes.TEXT:
16
+ return 'Field<string>';
17
+ case ApiFieldResultTypes.CONTAINER:
18
+ return 'Field<Container>';
19
+ case ApiFieldResultTypes.DATE:
20
+ return 'Field<Date>';
21
+ case ApiFieldResultTypes.NUMBER:
22
+ return 'Field<number>';
23
+ case ApiFieldResultTypes.TIME:
24
+ return 'Field<Date>';
25
+ case ApiFieldResultTypes.TIMESTAMP:
26
+ return 'Field<Date>';
27
+ }
28
+ }
29
+ function processFieldsChunk(fields) {
30
+ const existingFields = [];
31
+ fields = fields.filter(field => {
32
+ if (existingFields.includes(field.name))
33
+ return false;
34
+ existingFields.push(field.name);
35
+ return true;
36
+ });
37
+ return fields.map(field => {
38
+ return JSON.stringify(field.name) + ': ' + generateFieldType(field);
39
+ });
40
+ }
41
+ function camelToSnakeCase(str) {
42
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
43
+ }
44
+ function safeStringInjection(str, ...args) {
45
+ let result = '';
46
+ for (let i = 0; i < str.length; i++) {
47
+ result += str[i];
48
+ if (i < args.length) {
49
+ result += JSON.stringify(args[i].toString());
50
+ }
51
+ }
52
+ return result;
53
+ }
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://';
67
+ }
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: []
110
+ });
111
+ await DATABASE.login();
112
+ // Create file write stream
113
+ const stream = fs.createWriteStream('./types.ts');
114
+ const 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
+ const layoutInterfaces = new Map();
118
+ for (const layout of layouts) {
119
+ const substituteName = sub(layout.name);
120
+ if (!substituteName)
121
+ continue;
122
+ console.log(substituteName);
123
+ const metadata = await layout.getLayoutMeta();
124
+ const fields = processFieldsChunk(metadata.fieldMetaData);
125
+ const portals = metadata.portalMetaData;
126
+ const portalsProcessed = Object.keys(portals).map(portalName => {
127
+ return `${JSON.stringify(portalName)}: Portal<{${processFieldsChunk(portals[portalName]).join('\n')}}>`;
128
+ });
129
+ const 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
+ const layoutInterfaceLinks = [];
139
+ for (const 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 (const 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');
161
+ }
@@ -17,8 +17,9 @@ export default class FMHost {
17
17
  constructor(_hostname, timezoneOffset = (moment) => 0 - (new Date()).getTimezoneOffset(), verify = true) {
18
18
  if (!(/^https?:\/\//).test(_hostname))
19
19
  throw new Error('hostname MUST begin with either http:// or https://');
20
- this.protocol = _hostname.startsWith('https:') ? 'https:' : 'http:';
21
- this.hostname = _hostname.split('//')[1];
20
+ const url = new URL(_hostname);
21
+ this.protocol = url.protocol;
22
+ this.hostname = url.hostname;
22
23
  this.timezoneOffsetFunc = timezoneOffset;
23
24
  this.verify = verify;
24
25
  }
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { EventEmitter } from 'events';
3
2
  import { type LayoutInterface } from '../layouts/layoutInterface.js';
4
3
  import { Layout } from '../layouts/layout.js';
@@ -78,27 +78,25 @@ export class Database extends EventEmitter {
78
78
  this._token = (this.connection_details.credentials).token;
79
79
  return this.token;
80
80
  }
81
- else {
82
- const url = new URL(`${this.endpoint}/sessions`);
83
- url.hostname = this.host.hostname;
84
- const res = await fetch(url, {
85
- method: 'POST',
86
- headers: generateAuthorizationHeaders(this.connection_details.credentials),
87
- body: JSON.stringify({
88
- fmDataSource: this.connection_details.externalSources.map(i => {
89
- const _i = i;
90
- return this.generateExternalSourceLogin(_i);
91
- })
81
+ const url = new URL(`${this.endpoint}/sessions`);
82
+ url.hostname = this.host.hostname;
83
+ const res = await fetch(url, {
84
+ method: 'POST',
85
+ headers: generateAuthorizationHeaders(this.connection_details.credentials),
86
+ body: JSON.stringify({
87
+ fmDataSource: this.connection_details.externalSources.map(i => {
88
+ const _i = i;
89
+ return this.generateExternalSourceLogin(_i);
92
90
  })
93
- });
94
- const _res = (await res.json());
95
- if (res.status === 200) {
96
- this._token = res.headers.get('x-fm-data-access-token') ?? '';
97
- return this._token;
98
- }
99
- else {
100
- throw new FMError(_res.messages[0].code, _res.status, res);
101
- }
91
+ })
92
+ });
93
+ const _res = (await res.json());
94
+ if (res.status === 200) {
95
+ this._token = res.headers.get('x-fm-data-access-token') ?? '';
96
+ return this._token;
97
+ }
98
+ else {
99
+ throw new FMError(_res.messages[0].code, _res.status, res);
102
100
  }
103
101
  }
104
102
  get token() {
@@ -118,35 +116,21 @@ export class Database extends EventEmitter {
118
116
  ? url.toString()
119
117
  : typeof url === 'string' ? url : url.url}`);
120
118
  }
121
- const reqIsToDBHost = (url instanceof URL
119
+ const urlParsed = (url instanceof URL
122
120
  ? url
123
121
  : typeof url === 'string'
124
122
  ? new URL(url)
125
- : new URL(url.url)).hostname === this.host.hostname;
123
+ : new URL(url.url));
124
+ const reqIsToDBHost = urlParsed.hostname === this.host.hostname && urlParsed.pathname.startsWith("/fmi/data");
126
125
  if (reqIsToDBHost && this.token === '')
127
126
  await this.login(true);
128
127
  if (!options.headers)
129
128
  options.headers = {};
130
129
  if (reqIsToDBHost)
131
130
  options.headers.authorization = 'Bearer ' + this._token;
132
- // options.redirect = "manual"
133
- // options.headers.cookies = this._generateCookieHeader()
134
- // console.log(options.headers.cookies)
135
- // options.rejectUnauthorized = this.host.verify
136
131
  const _fetch = options.useCookieJar
137
132
  ? await fetchWithCookies(this.cookies, url, options)
138
133
  : await fetch(url, options);
139
- // if (_fetch.headers.get('set-cookie')) {
140
- // console.log(_fetch.headers.get('set-cookie'))
141
- // for (const cookie of (_fetch.headers.get("Set-Cookie") ?? "").split("; ")) {
142
- // const cookieSplit = cookie.split('=')
143
- // console.log("COOKIE SPLIT:", cookieSplit)
144
- // if (cookieSplit[1]) this.cookies[cookieSplit[0]] = cookieSplit[1]
145
- // }
146
- // }
147
- // console.log(this.cookies, _fetch.status)
148
- // // console.log(this, this.cookies, url)
149
- // console.trace()
150
134
  if (!_fetch.ok && (!options.retries || options.retries > 0)) {
151
135
  if (this.debug) {
152
136
  console.log(`EASYFM DEBUG: RE-ATTEMPTING REQUEST (${_fetch.status}) ${url instanceof URL
@@ -186,7 +170,6 @@ export class Database extends EventEmitter {
186
170
  */
187
171
  async listLayouts(page = 0) {
188
172
  const req = await this._apiRequestJSON(`${this.endpoint}/layouts?page=${encodeURIComponent(page)}`);
189
- console.log(req);
190
173
  if (!req.response)
191
174
  throw new FMError(req.messages[0].code, req.httpStatus, req.messages[0].message);
192
175
  const cycleLayoutNames = (layouts) => {
@@ -213,6 +196,6 @@ export class Database extends EventEmitter {
213
196
  this.#layoutCache.clear();
214
197
  }
215
198
  script(name, parameter = '') {
216
- return ({ name, parameter });
199
+ return { name, parameter };
217
200
  }
218
201
  }
@@ -0,0 +1,18 @@
1
+ import FMHost from './connection/FMHost.js';
2
+ import type * as TYPES from './types.js';
3
+ export { FMError } from './FMError.js';
4
+ export { type PickPortals } from './types.js';
5
+ export { Database } from './connection/database.js';
6
+ export { Layout } from './layouts/layout.js';
7
+ export { RecordBase } from './records/recordBase.js';
8
+ export { LayoutRecord } from './records/layoutRecord.js';
9
+ export { type LayoutInterface, type PortalInterface } from './layouts/layoutInterface.js';
10
+ export { PortalRecord } from './records/portalRecord.js';
11
+ export { Portal } from './records/portal.js';
12
+ export { type FindRequest, type FindRequestRaw, RecordGetOperation } from './records/getOperations/recordGetOperation.js';
13
+ export { LayoutRecordManager } from './layouts/layoutRecordManager.js';
14
+ export { type RecordFieldsMap } from './layouts/recordFieldsMap.js';
15
+ export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.js';
16
+ export { type Container, Field } from './records/field.js';
17
+ export default FMHost;
18
+ export { type TYPES };
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /*
2
+ * Copyright (c) 2023-2024. See LICENSE file for more information
3
+ */
4
+ import FMHost from './connection/FMHost.js';
5
+ export { FMError } from './FMError.js';
6
+ export { Database } from './connection/database.js';
7
+ export { Layout } from './layouts/layout.js';
8
+ export { RecordBase } from './records/recordBase.js';
9
+ export { LayoutRecord } from './records/layoutRecord.js';
10
+ export { PortalRecord } from './records/portalRecord.js';
11
+ export { Portal } from './records/portal.js';
12
+ export { RecordGetOperation } from './records/getOperations/recordGetOperation.js';
13
+ export { LayoutRecordManager } from './layouts/layoutRecordManager.js';
14
+ export { asDate, asTime, asTimestamp, query, queryEscape } from './utils/query.js';
15
+ export { Field } from './records/field.js';
16
+ export default FMHost;
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { RecordTypes } from '../types.js';
3
2
  import { type ApiFieldMetadata } from '../models/apiResults.js';
4
3
  import { type LayoutBase } from '../layouts/layoutBase.js';
@@ -17,6 +17,10 @@ type PortalData<T extends LayoutInterface> = {
17
17
  };
18
18
  export interface GetOperationOptions<T extends LayoutInterface> {
19
19
  portals: Partial<PortalData<T>>;
20
+ requests?: Array<{
21
+ req: FindRequest;
22
+ omit?: boolean;
23
+ }>;
20
24
  limit?: number;
21
25
  offset?: number;
22
26
  }
@@ -30,9 +34,9 @@ export declare class RecordGetOperation<T extends LayoutInterface, OPTIONS exten
30
34
  }>;
31
35
  protected portals: Partial<PortalData<T>>;
32
36
  protected offset: number;
33
- protected queries: Array<{
37
+ protected requests: Array<{
34
38
  req: FindRequestRaw;
35
- omit: boolean;
39
+ omit?: boolean;
36
40
  }>;
37
41
  constructor(layout: LayoutBase, options: OPTIONS);
38
42
  get isFindRequest(): boolean;
@@ -11,19 +11,23 @@ export class RecordGetOperation {
11
11
  sortData = [];
12
12
  portals;
13
13
  offset = 1;
14
- queries = [];
14
+ requests = [];
15
15
  constructor(layout, options) {
16
16
  this.layout = layout;
17
17
  this.sortData = [];
18
18
  this.portals = options.portals;
19
19
  this.offset = options.offset ?? 1; // Offset refers to the starting record. offset 1 is the same as no offset.
20
20
  this.limit = options.limit ?? 100;
21
+ if (options.requests) {
22
+ for (const req of options.requests)
23
+ this.addRequest(req.req, req.omit ?? false);
24
+ }
21
25
  }
22
26
  get isFindRequest() {
23
- return this.queries.length !== 0;
27
+ return this.requests.length !== 0;
24
28
  }
25
29
  formatQueries() {
26
- const test = this.queries.map(query => {
30
+ const test = this.requests.map(query => {
27
31
  const out = {};
28
32
  for (const key of Object.keys(query.req)) {
29
33
  if (query.req[key])
@@ -58,7 +62,7 @@ export class RecordGetOperation {
58
62
  params['script.prerequest'] = this.scriptData.prerequest.name;
59
63
  if (this.scriptData.prerequest?.parameter)
60
64
  params['script.prerequest.param'] = this.scriptData.prerequest.parameter;
61
- if (this.queries.length !== 0)
65
+ if (this.requests.length !== 0)
62
66
  params.query = this.formatQueries();
63
67
  const portals = Object.keys(this.portals);
64
68
  params.portal = portals;
@@ -146,7 +150,7 @@ export class RecordGetOperation {
146
150
  * @return {Object} - The current object instance.
147
151
  */
148
152
  addRequest(query, omit = false) {
149
- this.queries.push({ req: this.parseFindRequest(query), omit });
153
+ this.requests.push({ req: this.parseFindRequest(query), omit });
150
154
  return this;
151
155
  }
152
156
  /**
@@ -32,5 +32,5 @@ export declare class LayoutRecord<LAYOUT extends LayoutInterface> extends Record
32
32
  duplicate(): Promise<LayoutRecord<LAYOUT>>;
33
33
  delete(): Promise<void>;
34
34
  fieldsToObject(filter?: (a: Field<FieldValue>) => boolean): Omit<ApiRowDataDef, 'portalData'>;
35
- toObject(filter?: (a: Field<FieldValue>) => any, portalFilter?: (a: Portal<any>) => any, portalRowFilter?: (a: PortalRecord<never>) => any, portalFieldFilter?: (a: Field<FieldValue>) => any): ApiRowDataDef;
35
+ toObject(filter?: (a: Field<FieldValue>) => any, portalFilter?: (a: Portal<any>) => any, portalRowFilter?: (a: PortalRecord<any>) => any, portalFieldFilter?: (a: Field<FieldValue>) => any): ApiRowDataDef;
36
36
  }
@@ -5,7 +5,7 @@ import { RecordBase } from './recordBase.js';
5
5
  import { PortalRecord } from './portalRecord.js';
6
6
  import { Portal } from './portal.js';
7
7
  import { FMError } from '../FMError.js';
8
- import * as moment from 'moment/moment.js';
8
+ import moment from 'moment';
9
9
  export class LayoutRecord extends RecordBase {
10
10
  portals = {};
11
11
  portalsToInclude;
@@ -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
  }
@@ -165,12 +168,12 @@ export class LayoutRecord extends RecordBase {
165
168
  }
166
169
  }
167
170
  fieldsToObject(filter = (a) => a.edited) {
168
- const fields_processed = {};
171
+ const fieldsProcessed = {};
169
172
  let field;
170
173
  for (field of this.fieldsArray.filter(field => filter(field))) {
171
174
  let value = field.value;
172
175
  if (value instanceof Date) {
173
- let _value = moment.default(value);
176
+ let _value = moment(value);
174
177
  _value = _value
175
178
  .utcOffset(this.layout.database.host.timezoneOffsetFunc(_value));
176
179
  switch (field.metadata.result) {
@@ -184,12 +187,12 @@ export class LayoutRecord extends RecordBase {
184
187
  value = _value.format(this.layout.database.host.timeStampFormat);
185
188
  }
186
189
  }
187
- fields_processed[field.id] = value;
190
+ fieldsProcessed[field.id] = value;
188
191
  }
189
192
  const obj = {
190
193
  recordId: this.recordId.toString(),
191
194
  modId: this.modId.toString(),
192
- fieldData: fields_processed
195
+ fieldData: fieldsProcessed
193
196
  };
194
197
  return obj;
195
198
  }
@@ -203,7 +206,6 @@ export class LayoutRecord extends RecordBase {
203
206
  if (portals) {
204
207
  obj.portalData = {};
205
208
  for (const portal of portals) {
206
- // @ts-expect-error
207
209
  obj.portalData[portal.name] = portal.records.filter(a => portalRowFilter(a)).map(record => {
208
210
  return record.toObject(portalFieldFilter);
209
211
  });
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { EventEmitter } from 'events';
3
2
  import { RecordTypes } from '../types.js';
4
3
  import { type RecordFieldsMap } from '../layouts/recordFieldsMap.js';
@@ -1,5 +1,3 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  import type * as http from 'http';
4
2
  import { type FMError } from './FMError.js';
5
3
  import { type LayoutInterface } from './layouts/layoutInterface.js';
@@ -51,6 +49,18 @@ export interface ScriptRequestData {
51
49
  }
52
50
  export interface extraBodyOptions {
53
51
  scripts?: ScriptRequestData;
52
+ options?: {
53
+ /**
54
+ * Defines what entry mode to use (FileMaker Server 2024 and newer)
55
+ * user (default) - Field validation occurs as per normal. Request is blocked if validation failed.
56
+ * script - Field validation is overridden/ignored when allowed in the database schema.
57
+ */
58
+ entrymode: 'user' | 'script';
59
+ };
60
+ deleteRelatedRecords?: Array<{
61
+ table: string;
62
+ recordId: number;
63
+ }>;
54
64
  }
55
65
  export interface Script {
56
66
  name: string;
package/package.json CHANGED
@@ -1,26 +1,28 @@
1
1
  {
2
2
  "name": "@jd-data-limited/easy-fm",
3
- "version": "4.1.5",
3
+ "version": "4.1.8",
4
4
  "dependencies": {
5
5
  "@types/follow-redirects": "^1.14.4",
6
+ "@types/node": "^22.7.7",
7
+ "commander": "^14.0.0",
6
8
  "follow-redirects": "^1.15.5",
7
9
  "moment": "^2.29.4",
8
10
  "node-fetch": "^3.3.2",
9
11
  "node-fetch-cookies": "^2.1.1"
10
12
  },
11
13
  "type": "module",
12
- "main": "src/index.js",
14
+ "main": "dist/index.js",
13
15
  "module": "es2022",
14
- "homepage": "https://github.com/JD-Data-Limited/easy-fm",
16
+ "homepage": "https://jd-data.com/project/easyfm/",
15
17
  "url": "https://github.com/JD-Data-Limited/easy-fm",
16
18
  "license": "SEE LICENSE IN LICENSE",
17
19
  "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).",
18
20
  "files": [
19
- "./src/**/*"
21
+ "./dist/**/*"
20
22
  ],
21
23
  "scripts": {
22
- "test": "node --test tests/login.js",
23
- "build": "webpack"
24
+ "build": "tsc",
25
+ "test": "npm run build && jest"
24
26
  },
25
27
  "repository": {
26
28
  "type": "git",
@@ -28,8 +30,11 @@
28
30
  },
29
31
  "keywords": [
30
32
  "FileMaker",
31
- "Data API",
32
- "REST"
33
+ "Data",
34
+ "API",
35
+ "REST",
36
+ "simplicity",
37
+ "business"
33
38
  ],
34
39
  "author": "JD-Data Limited",
35
40
  "bugs": {
@@ -39,10 +44,16 @@
39
44
  "node": ">=20.9.0"
40
45
  },
41
46
  "devDependencies": {
47
+ "@babel/cli": "^7.25.7",
48
+ "@babel/core": "^7.25.8",
49
+ "@babel/preset-env": "^7.25.8",
50
+ "@babel/preset-typescript": "^7.25.7",
42
51
  "@types/btoa": "^1.2.3",
43
52
  "@types/events": "^3.0.0",
44
53
  "@types/inquirer": "^9.0.7",
54
+ "@types/jest": "^29.5.13",
45
55
  "@typescript-eslint/eslint-plugin": "^6.21.0",
56
+ "babel-jest": "^29.7.0",
46
57
  "dotenv": "^16.3.1",
47
58
  "eslint": "^8.57.0",
48
59
  "eslint-config-standard-with-typescript": "^43.0.1",
@@ -50,11 +61,11 @@
50
61
  "eslint-plugin-n": "^16.6.2",
51
62
  "eslint-plugin-promise": "^6.1.1",
52
63
  "inquirer": "^9.2.13",
64
+ "jest": "^29.7.0",
65
+ "ts-jest": "^29.2.5",
53
66
  "ts-morph": "^21.0.1",
54
- "typedoc": "^0.25.12",
55
- "typescript": "^5.4.3",
56
- "webpack": "^5.81.0",
57
- "webpack-cli": "^5.0.2"
67
+ "tsx": "^4.19.4",
68
+ "typescript": "^5.6.3"
58
69
  },
59
70
  "bin": {
60
71
  "easyfm": "src/bin/cli.js"