@jd-data-limited/easy-fm 4.0.7 → 4.0.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.
- package/README.md +27 -19
- package/package.json +1 -1
- package/src/connection/database.d.ts +3 -3
- package/src/connection/database.js +14 -11
- package/src/connection/database.ts +14 -11
- package/src/records/fieldBase.js +0 -1
- package/src/records/fieldBase.ts +0 -1
- package/src/records/getOperations/recordGetOperation.d.ts +3 -1
- package/src/records/getOperations/recordGetOperation.js +15 -1
- package/src/records/getOperations/recordGetOperation.ts +14 -2
- package/src/utils/query.d.ts +7 -1
- package/src/utils/query.js +2 -1
- package/src/utils/query.ts +3 -1
- package/src/utils/fmQuery.d.ts +0 -4
- package/src/utils/fmQuery.js +0 -20
package/README.md
CHANGED
|
@@ -1,30 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Introduction
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
|
7
|
-
or
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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 = {}
|
|
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
|
|
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 = {}
|
|
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
|
|
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/records/fieldBase.js
CHANGED
|
@@ -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)
|
package/src/records/fieldBase.ts
CHANGED
|
@@ -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]:
|
|
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.
|
|
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]:
|
|
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.
|
|
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
|
package/src/utils/query.d.ts
CHANGED
|
@@ -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[]):
|
|
7
|
+
export declare function query(strings: TemplateStringsArray, ...args: QueryParameter[]): {
|
|
8
|
+
[FindRequestSymbol]: string;
|
|
9
|
+
};
|
|
4
10
|
export {};
|
package/src/utils/query.js
CHANGED
|
@@ -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
|
}
|
package/src/utils/query.ts
CHANGED
|
@@ -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
|
}
|
package/src/utils/fmQuery.d.ts
DELETED
package/src/utils/fmQuery.js
DELETED
|
@@ -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
|
-
}
|