@jd-data-limited/easy-fm 4.0.7 → 4.0.9

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
@@ -1,30 +1,38 @@
1
- # easy-fm
1
+ # Introduction
2
2
 
3
- A [FileMaker Data API](https://help.claris.com/en/data-api-guide/content/index.html) client for NodeJS
3
+ A FileMaker Data API client for NodeJS
4
4
 
5
5
  easy-fm is a Node.js module that allows you to interact with
6
- a [FileMaker database](https://www.claris.com/filemaker/) stored on a [FileMaker server](https://www.claris.com/filemaker/server/)
7
- or [FileMaker Cloud](https://store.claris.com/filemaker-cloud). This module interacts with your server using the
8
- [FileMaker Data API](https://help.claris.com/en/data-api-guide/content/index.html).
9
- <!-- TOC -->
6
+ a FileMaker database stored on a FileMaker server
7
+ or FileMaker Cloud. This module interacts with your server using the
8
+ FileMaker Data API.
9
+
10
+ # Contents
10
11
 
11
- * [easy-fm](#easy-fm)
12
- * [FileMaker setup instructions](#filemaker-setup-instructions)
13
- * [Before you begin](#before-you-begin)
14
- * [Connecting to a database](#connecting-to-a-database)
15
- * [Getting records](#getting-records)
16
- * [Fetch a range of records](#fetch-a-range-of-records)
17
- * [Searching for records](#searching-for-records)
18
- * [Fetch a record using its record ID (NOT RECOMMENDED)](#fetch-a-record-using-its-record-id-not-recommended)
19
- * [Create a record](#create-a-record)
20
- * [Modify a record](#modify-a-record)
12
+ <!-- TOC -->
13
+ * [Introduction](#introduction)
14
+ * [Contents](#contents)
15
+ * [Installation](#installation)
16
+ * [Usage](#usage)
17
+ * [Connecting to a database](#connecting-to-a-database)
18
+ * [Getting records](#getting-records)
19
+ * [Fetch a range of records](#fetch-a-range-of-records)
20
+ * [Searching for records](#searching-for-records)
21
+ * [Fetch a record using its record ID (NOT RECOMMENDED)](#fetch-a-record-using-its-record-id-not-recommended)
22
+ * [Create a record](#create-a-record)
23
+ * [Modify a record](#modify-a-record)
21
24
  * [Field names](#field-names)
22
25
  * [Portal names](#portal-names)
23
26
  * [Typescript Implementation](#typescript-implementation)
24
-
25
27
  <!-- TOC -->
26
28
 
27
- ## FileMaker setup instructions
29
+ # Installation
30
+
31
+ ```npm
32
+ npm install @jd-data-limited/easy-fm --save
33
+ ```
34
+
35
+ easy-fm also requires the following to be configured within your FileMaker enviroment:
28
36
 
29
37
  1. Enable the FileMaker Data API from the server's admin console. This setting is located
30
38
  in `Connectors > FileMaker Data API`.
@@ -33,7 +41,7 @@ or [FileMaker Cloud](https://store.claris.com/filemaker-cloud). This module inte
33
41
 
34
42
  ---
35
43
 
36
- ## Before you begin
44
+ # Usage
37
45
 
38
46
  - You need to know what your server's UTC time offset (in minutes) is.
39
47
  - Running `0 - (new Date()).getTimezoneOffset()` in javascript will give you the UTC time offset for your current
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jd-data-limited/easy-fm",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
4
4
  "dependencies": {
5
5
  "follow-redirects": "^1.15.5",
6
6
  "@types/follow-redirects": "^1.14.4",
@@ -18,11 +18,11 @@ export declare class Database<T extends DatabaseStructure> extends EventEmitter
18
18
  constructor(host: HostBase, conn: databaseOptionsWithExternalSources);
19
19
  private generateExternalSourceLogin;
20
20
  logout(): Promise<void>;
21
- login(): Promise<any>;
21
+ login(forceLogin?: boolean): Promise<any>;
22
22
  get token(): any;
23
23
  get endpoint(): string;
24
- apiRequestRaw(url: string | Request, options?: any): Promise<Response>;
25
- apiRequestJSON<T = any>(url: string | Request, options?: any, autoRelogin?: boolean): Promise<ApiResults<T>>;
24
+ apiRequestRaw(url: string | Request, options?: any, autoRelogin?: boolean): Promise<Response>;
25
+ apiRequestJSON<T = any>(url: string | Request, options?: any): Promise<ApiResults<T>>;
26
26
  listLayouts(): Promise<Layout<LayoutInterface>[]>;
27
27
  getLayout<R extends keyof T["layouts"]>(name: R): Layout<T["layouts"][R]>;
28
28
  getLayout<R extends LayoutInterface>(name: string): Layout<R>;
@@ -44,11 +44,11 @@ export class Database extends EventEmitter {
44
44
  // console.log(data)
45
45
  this._token = null;
46
46
  }
47
- async login() {
47
+ async login(forceLogin = false) {
48
48
  // Reset cookies
49
49
  this.cookies = {};
50
50
  await this.host.getMetadata();
51
- if (this.token)
51
+ if (this.token && !forceLogin)
52
52
  throw new Error("Already logged in. Run logout() first");
53
53
  if (this.connection_details.credentials.method === "token") {
54
54
  this._token = this.connection_details.credentials.token;
@@ -85,10 +85,11 @@ export class Database extends EventEmitter {
85
85
  get endpoint() {
86
86
  return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
87
87
  }
88
- async apiRequestRaw(url, options = {}) {
88
+ async apiRequestRaw(url, options = {}, autoRelogin = true) {
89
+ if (!this.token)
90
+ await this.login(true);
89
91
  if (!options.headers)
90
92
  options.headers = {};
91
- options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json";
92
93
  options.headers["authorization"] = "Bearer " + this._token;
93
94
  options.rejectUnauthorized = this.host.verify;
94
95
  let _fetch = await fetch(url, options);
@@ -98,21 +99,23 @@ export class Database extends EventEmitter {
98
99
  this.cookies[cookie_split[0]] = cookie_split[1];
99
100
  }
100
101
  }
102
+ if (_fetch.status === 401 && autoRelogin) {
103
+ await this.login(true);
104
+ return await this.apiRequestRaw(url, options, false);
105
+ }
101
106
  return _fetch;
102
107
  }
103
- async apiRequestJSON(url, options = {}, autoRelogin = true) {
108
+ async apiRequestJSON(url, options = {}) {
109
+ if (!options.headers)
110
+ options.headers = {};
111
+ options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json";
104
112
  let _fetch = await this.apiRequestRaw(url, options);
105
113
  let data = await _fetch.json();
106
114
  // Remove response if it is empty. This makes checking for an empty response easier
107
115
  if (data.response && Object.keys(data.response).length === 0)
108
116
  delete data.response;
109
117
  // console.log(data.messages[0])
110
- if (data.messages[0].code === "952" && autoRelogin) {
111
- this._token = null;
112
- await this.login();
113
- return await this.apiRequestJSON(url, options, false);
114
- }
115
- else if (data.messages[0].code !== "0") {
118
+ if (data.messages[0].code !== "0") {
116
119
  throw new FMError(data.messages[0].code, _fetch.status, data);
117
120
  }
118
121
  data.httpStatus = _fetch.status;
@@ -65,12 +65,12 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
65
65
  this._token = null
66
66
  }
67
67
 
68
- async login() {
68
+ async login(forceLogin = false) {
69
69
  // Reset cookies
70
70
  this.cookies = {}
71
71
 
72
72
  await this.host.getMetadata()
73
- if (this.token) throw new Error("Already logged in. Run logout() first")
73
+ if (this.token && !forceLogin) throw new Error("Already logged in. Run logout() first")
74
74
 
75
75
  if (this.connection_details.credentials.method === "token") {
76
76
  this._token = (<loginOptionsToken>this.connection_details.credentials).token
@@ -110,9 +110,10 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
110
110
  return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`
111
111
  }
112
112
 
113
- async apiRequestRaw(url: string | Request, options: any = {}) {
113
+ async apiRequestRaw(url: string | Request, options: any = {}, autoRelogin = true): Promise<Response> {
114
+ if (!this.token) await this.login(true)
115
+
114
116
  if (!options.headers) options.headers = {}
115
- options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json"
116
117
  options.headers["authorization"] = "Bearer " + this._token
117
118
  options.rejectUnauthorized = this.host.verify
118
119
 
@@ -123,9 +124,16 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
123
124
  this.cookies[cookie_split[0]] = cookie_split[1]
124
125
  }
125
126
  }
127
+
128
+ if (_fetch.status === 401 && autoRelogin) {
129
+ await this.login(true)
130
+ return await this.apiRequestRaw(url, options, false)
131
+ }
126
132
  return _fetch
127
133
  }
128
- async apiRequestJSON<T = any>(url: string | Request, options: any = {}, autoRelogin = true): Promise<ApiResults<T>> {
134
+ async apiRequestJSON<T = any>(url: string | Request, options: any = {}): Promise<ApiResults<T>> {
135
+ if (!options.headers) options.headers = {}
136
+ options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json"
129
137
  let _fetch = await this.apiRequestRaw(url, options)
130
138
  let data = await _fetch.json() as ApiResults<T>
131
139
 
@@ -133,12 +141,7 @@ export class Database<T extends DatabaseStructure> extends EventEmitter implemen
133
141
  if (data.response && Object.keys(data.response).length === 0) delete data.response
134
142
 
135
143
  // console.log(data.messages[0])
136
- if (data.messages[0].code === "952" && autoRelogin) {
137
- this._token = null
138
- await this.login()
139
- return await this.apiRequestJSON(url, options, false)
140
- }
141
- else if (data.messages[0].code !== "0") {
144
+ if (data.messages[0].code !== "0") {
142
145
  throw new FMError(data.messages[0].code, _fetch.status, data)
143
146
  }
144
147
 
package/src/index.d.ts CHANGED
@@ -10,9 +10,9 @@ import { LayoutInterface, PortalInterface } from "./layouts/layoutInterface.js";
10
10
  import { PortalRecord } from "./records/portalRecord.js";
11
11
  import { Portal } from "./records/portal.js";
12
12
  import { Container, FieldBase } from "./records/fieldBase.js";
13
- import { RecordGetOperation } from "./records/getOperations/recordGetOperation.js";
13
+ import { FindRequest, RecordGetOperation } from "./records/getOperations/recordGetOperation.js";
14
14
  import { LayoutRecordManager } from "./layouts/layoutRecordManager.js";
15
15
  import { RecordFieldsMap } from "./layouts/recordFieldsMap.js";
16
16
  import { query, queryEscape } from "./utils/query.js";
17
17
  export default FMHost;
18
- export { FMError, TYPES, Database, Layout, RecordBase, LayoutRecord, LayoutInterface, RecordFieldsMap, PortalInterface, PortalRecord, Portal, FieldBase, RecordGetOperation, LayoutRecordManager, Container, PickPortals, query, queryEscape };
18
+ export { FMError, TYPES, Database, Layout, RecordBase, LayoutRecord, LayoutInterface, RecordFieldsMap, PortalInterface, PortalRecord, Portal, FieldBase, RecordGetOperation, LayoutRecordManager, Container, PickPortals, FindRequest, query, queryEscape };
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@ import {LayoutInterface, PortalInterface} from "./layouts/layoutInterface.js";
14
14
  import {PortalRecord} from "./records/portalRecord.js";
15
15
  import {Portal} from "./records/portal.js";
16
16
  import {Container, FieldBase} from "./records/fieldBase.js";
17
- import {RecordGetOperation} from "./records/getOperations/recordGetOperation.js";
17
+ import {FindRequest, RecordGetOperation} from "./records/getOperations/recordGetOperation.js";
18
18
  import {LayoutRecordManager} from "./layouts/layoutRecordManager.js";
19
19
  import {RecordFieldsMap} from "./layouts/recordFieldsMap.js";
20
20
  import {query, queryEscape} from "./utils/query.js";
@@ -37,6 +37,7 @@ export {
37
37
  LayoutRecordManager,
38
38
  Container,
39
39
  PickPortals,
40
+ FindRequest,
40
41
  query,
41
42
  queryEscape
42
43
  }
@@ -123,7 +123,6 @@ export class FieldBase {
123
123
  let res = await this.parent.layout.database.apiRequestRaw(`${this.parent.endpoint}/containers/${this.id}/1`, {
124
124
  method: "POST",
125
125
  // @ts-ignore
126
- headers: { "Authorization": "Bearer " + this.parent.layout.database.token },
127
126
  body: form
128
127
  });
129
128
  if (!res.ok)
@@ -139,7 +139,6 @@ export class FieldBase<T extends FieldValue> {
139
139
  let res = await this.parent.layout.database.apiRequestRaw(`${this.parent.endpoint}/containers/${this.id}/1`, {
140
140
  method: "POST",
141
141
  // @ts-ignore
142
- headers: {"Authorization": "Bearer " + this.parent.layout.database.token},
143
142
  body: form
144
143
  })
145
144
  if (!res.ok) throw new Error(`Upload failed with HTTP error: ${res.status} (${res.statusText})`)
@@ -2,9 +2,10 @@ import { LayoutInterface } from "../../layouts/layoutInterface.js";
2
2
  import { PickPortals, ScriptRequestData } from "../../types.js";
3
3
  import { LayoutBase } from "../../layouts/layoutBase.js";
4
4
  import { LayoutRecord } from "../layoutRecord.js";
5
+ import { Query } from "../../utils/query.js";
5
6
  export type SortOrder = "ascend" | "descend";
6
7
  export type FindRequest = {
7
- [key: string]: string;
8
+ [key: string]: Query;
8
9
  } & {
9
10
  omit?: "true" | "false";
10
11
  };
@@ -35,6 +36,7 @@ export declare class RecordGetOperation<T extends LayoutInterface, OPTIONS exten
35
36
  protected queries: FindRequest[];
36
37
  constructor(layout: LayoutBase, options: OPTIONS);
37
38
  get isFindRequest(): boolean;
39
+ private formatQueries;
38
40
  protected generateParamsBody(offset: number, limit: number): {
39
41
  [key: string]: any;
40
42
  };
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { LayoutRecord } from "../layoutRecord.js";
5
5
  import { FMError } from "../../FMError.js";
6
+ import { FindRequestSymbol } from "../../utils/query.js";
6
7
  export class RecordGetOperation {
7
8
  layout;
8
9
  limit = 100;
@@ -21,6 +22,19 @@ export class RecordGetOperation {
21
22
  get isFindRequest() {
22
23
  return this.queries.length !== 0;
23
24
  }
25
+ formatQueries() {
26
+ return this.queries.map(query => {
27
+ let out = {};
28
+ for (let key of Object.keys(query)) {
29
+ if (query[key][FindRequestSymbol])
30
+ out[key] = query[key][FindRequestSymbol];
31
+ else {
32
+ out[key] = query[key];
33
+ }
34
+ }
35
+ return out;
36
+ });
37
+ }
24
38
  generateParamsBody(offset, limit) {
25
39
  const params = {
26
40
  limit: limit.toString(),
@@ -41,7 +55,7 @@ export class RecordGetOperation {
41
55
  if (this.scriptData.prerequest?.parameter)
42
56
  params["script.prerequest.param"] = this.scriptData.prerequest.parameter;
43
57
  if (this.queries.length !== 0)
44
- params["query"] = this.queries;
58
+ params["query"] = this.formatQueries();
45
59
  let portals = Object.keys(this.portals);
46
60
  params["portal"] = portals;
47
61
  for (let portal of portals) {
@@ -8,10 +8,11 @@ import {LayoutBase} from "../../layouts/layoutBase.js"
8
8
  import {LayoutRecord} from "../layoutRecord.js";
9
9
  import {ApiRecordResponseObj} from "../../models/apiResults.js";
10
10
  import {FMError} from "../../FMError.js";
11
+ import {FindRequestSymbol, Query} from "../../utils/query.js";
11
12
 
12
13
  export type SortOrder = "ascend" | "descend"
13
14
  export type FindRequest = {
14
- [key: string]: string,
15
+ [key: string]: Query
15
16
  } & {omit?: "true" | "false"}
16
17
 
17
18
  export type PortalRequest = {
@@ -51,6 +52,17 @@ export class RecordGetOperation<T extends LayoutInterface, OPTIONS extends GetOp
51
52
  return this.queries.length !== 0
52
53
  }
53
54
 
55
+ private formatQueries() {
56
+ return this.queries.map(query => {
57
+ let out: any = {}
58
+ for (let key of Object.keys(query)) {
59
+ if (query[key][FindRequestSymbol]) out[key] = query[key][FindRequestSymbol]
60
+ else {out[key] = query[key]}
61
+ }
62
+ return out
63
+ })
64
+ }
65
+
54
66
  protected generateParamsBody(offset: number, limit: number) {
55
67
  const params: {[key: string]: any} = {
56
68
  limit: limit.toString(),
@@ -68,7 +80,7 @@ export class RecordGetOperation<T extends LayoutInterface, OPTIONS extends GetOp
68
80
  if (this.scriptData.prerequest) params["script.prerequest"] = this.scriptData.prerequest.name
69
81
  if (this.scriptData.prerequest?.parameter) params["script.prerequest.param"] = this.scriptData.prerequest.parameter
70
82
 
71
- if (this.queries.length !== 0) params["query"] = this.queries
83
+ if (this.queries.length !== 0) params["query"] = this.formatQueries()
72
84
 
73
85
  let portals: (keyof typeof this.portals)[] = Object.keys(this.portals)
74
86
  params["portal"] = portals
@@ -1,4 +1,10 @@
1
+ export declare const FindRequestSymbol: unique symbol;
1
2
  type QueryParameter = string | number | Date;
3
+ export type Query = {
4
+ [FindRequestSymbol]: string;
5
+ };
2
6
  export declare function queryEscape(str: string): string;
3
- export declare function query(strings: TemplateStringsArray, ...args: QueryParameter[]): string;
7
+ export declare function query(strings: TemplateStringsArray, ...args: QueryParameter[]): {
8
+ [FindRequestSymbol]: string;
9
+ };
4
10
  export {};
@@ -1,6 +1,7 @@
1
1
  /*
2
2
  * Copyright (c) 2024. See LICENSE file for more information
3
3
  */
4
+ export const FindRequestSymbol = Symbol("easyfm-findrequest");
4
5
  const SPECIAL_CHARACTERS = ["\\", "=", "<", "≤", "≥", ">", "…", "...", "//", "@", "#", "*", "\"", "~"];
5
6
  export function queryEscape(str) {
6
7
  for (let char of SPECIAL_CHARACTERS) {
@@ -16,5 +17,5 @@ export function query(strings, ...args) {
16
17
  const query = strings.map((str, index) => {
17
18
  return str + (argStrings[index] || '');
18
19
  });
19
- return query.join("");
20
+ return { [FindRequestSymbol]: query.join("") };
20
21
  }
@@ -2,8 +2,10 @@
2
2
  * Copyright (c) 2024. See LICENSE file for more information
3
3
  */
4
4
 
5
+ export const FindRequestSymbol = Symbol("easyfm-findrequest");
5
6
  const SPECIAL_CHARACTERS = ["\\", "=", "<", "≤", "≥", ">", "…", "...", "//", "@", "#", "*", "\"", "~"]
6
7
  type QueryParameter = string | number | Date
8
+ export type Query = {[FindRequestSymbol]: string}
7
9
 
8
10
  export function queryEscape(str: string) {
9
11
  for (let char of SPECIAL_CHARACTERS) {
@@ -21,5 +23,5 @@ export function query(strings: TemplateStringsArray, ...args: QueryParameter[])
21
23
  const query = strings.map((str, index) => {
22
24
  return str + (argStrings[index] || '')
23
25
  })
24
- return query.join("")
26
+ return {[FindRequestSymbol]: query.join("")}
25
27
  }
@@ -1,4 +0,0 @@
1
- type QueryParameter = string | number | Date;
2
- export declare function fmQueryEscape(str: string): string;
3
- export declare function fmQuery(strings: TemplateStringsArray, ...args: QueryParameter[]): string;
4
- export {};
@@ -1,20 +0,0 @@
1
- /*
2
- * Copyright (c) 2024. See LICENSE file for more information
3
- */
4
- const SPECIAL_CHARACTERS = ["\\", "=", "<", "≤", "≥", ">", "…", "...", "//", "@", "#", "*", "\"", "~"];
5
- export function fmQueryEscape(str) {
6
- for (let char of SPECIAL_CHARACTERS) {
7
- str = str.replace(char, `\\${char}`);
8
- }
9
- return str;
10
- }
11
- export function fmQuery(strings, ...args) {
12
- let argStrings = args.map(item => {
13
- return fmQueryEscape(item.toString());
14
- });
15
- // Zip the parameters together
16
- const query = strings.map((str, index) => {
17
- return str + (args[index] || '');
18
- });
19
- return query.join("");
20
- }