@jd-data-limited/easy-fm 1.1.5-alpha1 → 1.2.0-alpha1
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 +61 -235
- package/TEMP/FILEMAKER.txt +218 -0
- package/TEMP/WEBAPP.txt +92 -0
- package/TEMP/config.yaml +40 -0
- package/TEMP/data.json +19837 -0
- package/index.js +337 -108
- package/index.ts +449 -155
- package/package.json +2 -2
- package/test.js +32 -0
package/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import fetch, {
|
|
|
11
11
|
fileFrom,
|
|
12
12
|
fileFromSync,
|
|
13
13
|
FormData,
|
|
14
|
-
Headers,
|
|
14
|
+
Headers, HeadersInit,
|
|
15
15
|
Request,
|
|
16
16
|
Response
|
|
17
17
|
} from 'node-fetch';
|
|
@@ -23,31 +23,26 @@ import path from "path"
|
|
|
23
23
|
// @ts-ignore
|
|
24
24
|
import {fileURLToPath} from "url"
|
|
25
25
|
import * as https from "https";
|
|
26
|
+
import * as moment from "moment"
|
|
26
27
|
|
|
27
28
|
// import * as btoa from "btoa";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
database: databaseOptions
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface hostConnectionOptions {
|
|
35
|
-
hostname: string
|
|
36
|
-
database: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken,
|
|
37
|
-
externalSources: (loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken)[]
|
|
29
|
+
interface databaseOptionsBase {
|
|
30
|
+
database: string
|
|
31
|
+
credentials: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken,
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
interface
|
|
41
|
-
method: any
|
|
34
|
+
interface databaseOptionsWithExternalSources extends databaseOptionsBase {
|
|
42
35
|
database: string
|
|
36
|
+
credentials: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken,
|
|
37
|
+
externalSources: databaseOptionsBase[]
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
interface loginOptionsToken
|
|
40
|
+
interface loginOptionsToken {
|
|
46
41
|
method: "token"
|
|
47
42
|
token: string
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
interface loginOptionsOAuth
|
|
45
|
+
interface loginOptionsOAuth {
|
|
51
46
|
method: "oauth"
|
|
52
47
|
oauth: {
|
|
53
48
|
requestId: string,
|
|
@@ -55,13 +50,13 @@ interface loginOptionsOAuth extends databaseOptions {
|
|
|
55
50
|
}
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
interface loginOptionsFileMaker
|
|
53
|
+
interface loginOptionsFileMaker {
|
|
59
54
|
method: "filemaker"
|
|
60
55
|
username: string,
|
|
61
56
|
password: string,
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
interface loginOptionsClaris
|
|
59
|
+
interface loginOptionsClaris {
|
|
65
60
|
method: "claris"
|
|
66
61
|
claris: {
|
|
67
62
|
fmid: string
|
|
@@ -85,29 +80,122 @@ interface Script {
|
|
|
85
80
|
interface recordObject {
|
|
86
81
|
recordId: number
|
|
87
82
|
modId: number,
|
|
88
|
-
fieldData: any
|
|
83
|
+
fieldData: any,
|
|
84
|
+
portalData?: portalDataObject
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface fileMakerResponse {
|
|
88
|
+
response: any,
|
|
89
|
+
messages: any[]
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
interface portalDataObject {
|
|
93
|
+
[key: string]: recordObject
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface fieldMetaData {
|
|
97
|
+
name: string,
|
|
98
|
+
type: string,
|
|
99
|
+
displayType: string,
|
|
100
|
+
result: string,
|
|
101
|
+
global: boolean,
|
|
102
|
+
autoEnter: boolean,
|
|
103
|
+
fourDigitYear: boolean,
|
|
104
|
+
maxRepeat: number,
|
|
105
|
+
maxCharacters: number,
|
|
106
|
+
notEmpty: boolean,
|
|
107
|
+
numeric: boolean,
|
|
108
|
+
timeOfDay: false,
|
|
109
|
+
repetitionStart: number,
|
|
110
|
+
repetitionEnd: number
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface FMHostMetadata {
|
|
114
|
+
productInfo: {
|
|
115
|
+
buildDate: Date,
|
|
116
|
+
name: string,
|
|
117
|
+
version: string,
|
|
118
|
+
dateFormat: string,
|
|
119
|
+
timeFormat: string,
|
|
120
|
+
timeStampFormat: string
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default class FMHost {
|
|
125
|
+
readonly hostname: string
|
|
126
|
+
readonly timezoneOffset: number
|
|
127
|
+
readonly verify: boolean
|
|
128
|
+
metadata: FMHostMetadata
|
|
129
|
+
|
|
130
|
+
constructor(_hostname: string, timezoneOffset = 0 - (new Date()).getTimezoneOffset(), verify = true) {
|
|
131
|
+
if (!(/^https?:\/\//).test(_hostname)) throw "hostname MUST begin with either http:// or https://"
|
|
132
|
+
this.hostname = _hostname
|
|
133
|
+
this.timezoneOffset = timezoneOffset
|
|
134
|
+
this.verify = verify
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async listDatabases(credentials?: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris) {
|
|
138
|
+
let headers = {}
|
|
139
|
+
if (credentials) {
|
|
140
|
+
headers = generateAuthorizationHeaders(credentials)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let _fetch = await fetch(`${this.hostname}/fmi/data/v2/databases`, {
|
|
144
|
+
method: "GET",
|
|
145
|
+
headers
|
|
146
|
+
})
|
|
147
|
+
let data = await _fetch.json() as fileMakerResponse
|
|
148
|
+
// console.log(data.messages[0])
|
|
149
|
+
|
|
150
|
+
if (data.messages[0].code === "0") {
|
|
151
|
+
return data.response.databases
|
|
152
|
+
} else {
|
|
153
|
+
// @ts-ignore
|
|
154
|
+
throw new FMError(data.messages[0].code, data.status, data)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
database(data: databaseOptionsWithExternalSources) {
|
|
159
|
+
return new Database(this, data)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getMetadata() {
|
|
163
|
+
if (this.metadata) return this.metadata
|
|
164
|
+
|
|
165
|
+
let _fetch = await fetch(`${this.hostname}/fmi/data/v2/productInfo`, {
|
|
166
|
+
method: "GET",
|
|
167
|
+
})
|
|
168
|
+
let data = await _fetch.json() as fileMakerResponse
|
|
169
|
+
// console.log(data.messages[0])
|
|
170
|
+
|
|
171
|
+
if (data.messages[0].code === "0") {
|
|
172
|
+
this.metadata = data.response
|
|
173
|
+
return data.response
|
|
174
|
+
} else {
|
|
175
|
+
// @ts-ignore
|
|
176
|
+
throw new FMError(data.messages[0].code, data.status, data)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class Database extends EventEmitter {
|
|
92
182
|
private _token: any;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
public name: string;
|
|
183
|
+
readonly host: FMHost;
|
|
184
|
+
private connection_details: databaseOptionsWithExternalSources
|
|
185
|
+
readonly name: string;
|
|
97
186
|
|
|
98
|
-
constructor(
|
|
187
|
+
constructor(host: FMHost, conn: databaseOptionsWithExternalSources) {
|
|
99
188
|
super()
|
|
100
|
-
this.
|
|
101
|
-
this.name = conn.database
|
|
102
|
-
this.
|
|
103
|
-
this.rejectUnauthroized = rejectUnauthorized
|
|
189
|
+
this.host = host
|
|
190
|
+
this.name = conn.database
|
|
191
|
+
this.connection_details = conn
|
|
104
192
|
}
|
|
105
193
|
|
|
106
|
-
private generateExternalSourceLogin(data:
|
|
107
|
-
if (data.method === "filemaker") {
|
|
108
|
-
let _data = <loginOptionsFileMaker>data
|
|
194
|
+
private generateExternalSourceLogin(data: databaseOptionsBase) {
|
|
195
|
+
if (data.credentials.method === "filemaker") {
|
|
196
|
+
let _data = <loginOptionsFileMaker>data.credentials
|
|
109
197
|
return {
|
|
110
|
-
database:
|
|
198
|
+
database: data.database,
|
|
111
199
|
username: _data.username,
|
|
112
200
|
password: _data.password
|
|
113
201
|
}
|
|
@@ -129,81 +217,47 @@ export default class FileMakerConnection extends EventEmitter {
|
|
|
129
217
|
let data = await _fetch.json()
|
|
130
218
|
// console.log(data)
|
|
131
219
|
this._token = null
|
|
132
|
-
this.hostname = null
|
|
133
220
|
resolve()
|
|
134
221
|
})
|
|
135
222
|
}
|
|
136
223
|
|
|
137
224
|
login() {
|
|
138
|
-
return new Promise<string>((resolve, reject) => {
|
|
225
|
+
return new Promise<string>(async (resolve, reject) => {
|
|
226
|
+
try {
|
|
227
|
+
await this.host.getMetadata()
|
|
228
|
+
} catch (e) {
|
|
229
|
+
reject(e)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
139
232
|
if (this.token) throw new Error("Already logged in. Run logout() first")
|
|
140
233
|
|
|
141
|
-
if (this.
|
|
234
|
+
if (this.connection_details.credentials.method === "token") {
|
|
235
|
+
this._token = (<loginOptionsToken>this.connection_details.credentials).token
|
|
236
|
+
resolve(this.token)
|
|
237
|
+
} else {
|
|
142
238
|
fetch(`${this.endpoint}/sessions`, {
|
|
143
|
-
hostname: this.hostname,
|
|
239
|
+
hostname: this.host.hostname,
|
|
144
240
|
port: 443,
|
|
145
241
|
method: "POST",
|
|
146
|
-
headers:
|
|
147
|
-
"Content-Type": "application/json",
|
|
148
|
-
"Authorization": "Basic " + Buffer.from((<loginOptionsFileMaker>this.databaseConDetails.database).username + ":" + (<loginOptionsFileMaker>this.databaseConDetails.database).password).toString("base64")
|
|
149
|
-
},
|
|
242
|
+
headers: generateAuthorizationHeaders(this.connection_details.credentials) as unknown as HeadersInit,
|
|
150
243
|
body: JSON.stringify({
|
|
151
|
-
fmDataSource: this.
|
|
152
|
-
let _i = <
|
|
244
|
+
fmDataSource: this.connection_details.externalSources.map(i => {
|
|
245
|
+
let _i = <databaseOptionsBase>i
|
|
153
246
|
return this.generateExternalSourceLogin(_i)
|
|
154
247
|
})
|
|
155
248
|
})
|
|
156
249
|
}).then(async res => {
|
|
250
|
+
let _res = <any>(await res.json())
|
|
157
251
|
if (res.status === 200) {
|
|
158
252
|
this._token = res.headers.get('x-fm-data-access-token')
|
|
159
253
|
resolve(this._token)
|
|
160
254
|
} else {
|
|
161
|
-
let _res = <any>(await res.json())
|
|
162
255
|
reject(new FMError(_res.messages[0].code, _res.status, res))
|
|
163
256
|
}
|
|
164
257
|
})
|
|
165
258
|
.catch(e => {
|
|
166
259
|
reject(e)
|
|
167
260
|
})
|
|
168
|
-
} else if (this.databaseConDetails.database.method === "token") {
|
|
169
|
-
this._token = (<loginOptionsToken>this.databaseConDetails.database).token
|
|
170
|
-
resolve(this.token)
|
|
171
|
-
} else if (this.databaseConDetails.database.method === "oauth") {
|
|
172
|
-
fetch(`${this.endpoint}/sessions`, {
|
|
173
|
-
method: "POST",
|
|
174
|
-
headers: {
|
|
175
|
-
"Content-Type": "application/json",
|
|
176
|
-
"X-FM-Data-OAuth-RequestId": (<loginOptionsOAuth>this.databaseConDetails.database).oauth.requestId,
|
|
177
|
-
"X-FM-Data-OAuth-Identifier": (<loginOptionsOAuth>this.databaseConDetails.database).oauth.requestIdentifier
|
|
178
|
-
},
|
|
179
|
-
body: JSON.stringify({
|
|
180
|
-
fmDataSource: this.databaseConDetails.externalSources.map(i => {
|
|
181
|
-
let _i = <databaseOptions>i
|
|
182
|
-
return this.generateExternalSourceLogin(_i)
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
})
|
|
186
|
-
.then(res => res.json())
|
|
187
|
-
.then(res => {
|
|
188
|
-
let _res = <any>res
|
|
189
|
-
this._token = _res.headers["x-fm-data-access-token"]
|
|
190
|
-
resolve(this.token)
|
|
191
|
-
})
|
|
192
|
-
} else if (this.databaseConDetails.database.method === "claris") {
|
|
193
|
-
fetch(`${this.endpoint}/sessions`, {
|
|
194
|
-
method: "POST",
|
|
195
|
-
headers: {
|
|
196
|
-
"Content-Type": "application/json",
|
|
197
|
-
"Authorization": (<loginOptionsClaris>this.databaseConDetails.database).claris.fmid,
|
|
198
|
-
},
|
|
199
|
-
body: "{}"
|
|
200
|
-
})
|
|
201
|
-
.then(res => res.json())
|
|
202
|
-
.then(res => {
|
|
203
|
-
let _res = <any>res
|
|
204
|
-
this._token = _res.headers["x-fm-data-access-token"]
|
|
205
|
-
resolve(this.token)
|
|
206
|
-
})
|
|
207
261
|
}
|
|
208
262
|
})
|
|
209
263
|
}
|
|
@@ -213,22 +267,22 @@ export default class FileMakerConnection extends EventEmitter {
|
|
|
213
267
|
}
|
|
214
268
|
|
|
215
269
|
get endpoint(): string {
|
|
216
|
-
return
|
|
270
|
+
return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`
|
|
217
271
|
}
|
|
218
272
|
|
|
219
273
|
async apiRequest(url: string | Request, options: any = {}, autoRelogin = true): Promise<any> {
|
|
220
274
|
if (!options.headers) options.headers = {}
|
|
221
275
|
options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json"
|
|
222
276
|
options.headers["authorization"] = "Bearer " + this._token
|
|
223
|
-
options.rejectUnauthorized = this.
|
|
277
|
+
options.rejectUnauthorized = this.host.verify
|
|
224
278
|
|
|
225
279
|
let _fetch = await fetch(url, options)
|
|
226
|
-
let data = await _fetch.json()
|
|
227
|
-
console.log(data.messages[0])
|
|
280
|
+
let data = await _fetch.json() as fileMakerResponse
|
|
281
|
+
// console.log(data.messages[0])
|
|
228
282
|
if (data.messages[0].code == 952 && autoRelogin) {
|
|
229
283
|
this._token = null
|
|
230
284
|
await this.login()
|
|
231
|
-
return await this.apiRequest(url, options,false)
|
|
285
|
+
return await this.apiRequest(url, options, false)
|
|
232
286
|
}
|
|
233
287
|
return (data as any)
|
|
234
288
|
}
|
|
@@ -267,11 +321,12 @@ export default class FileMakerConnection extends EventEmitter {
|
|
|
267
321
|
}
|
|
268
322
|
|
|
269
323
|
class layout {
|
|
270
|
-
readonly database:
|
|
324
|
+
readonly database: Database;
|
|
325
|
+
readonly records = new layoutRecordManager(this)
|
|
271
326
|
protected name: string;
|
|
272
327
|
metadata: any;
|
|
273
328
|
|
|
274
|
-
constructor(database:
|
|
329
|
+
constructor(database: Database, name: string) {
|
|
275
330
|
this.database = database
|
|
276
331
|
this.name = name
|
|
277
332
|
}
|
|
@@ -280,29 +335,25 @@ class layout {
|
|
|
280
335
|
return `${this.database.endpoint}/layouts/${this.name}`
|
|
281
336
|
}
|
|
282
337
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
338
|
+
/**
|
|
339
|
+
* @deprecated use layout.records.create() instead
|
|
340
|
+
*/
|
|
287
341
|
createRecord(): Promise<record | Error | void> {
|
|
288
|
-
return
|
|
289
|
-
// Get the layout's metadata
|
|
290
|
-
this.getLayoutMeta().then(layout => {
|
|
291
|
-
let fields = {}
|
|
292
|
-
for (let _field of this.metadata.fieldMetaData) {
|
|
293
|
-
fields[_field.name] = ""
|
|
294
|
-
}
|
|
295
|
-
resolve(new record(this, -1, 0, fields))
|
|
296
|
-
}).catch(e => {reject(e)})
|
|
297
|
-
})
|
|
342
|
+
return this.records.create()
|
|
298
343
|
}
|
|
299
344
|
|
|
300
|
-
|
|
301
|
-
|
|
345
|
+
/**
|
|
346
|
+
* @deprecated use layout.records.get() instead
|
|
347
|
+
*/
|
|
348
|
+
getRecord(recordId): Promise<record> {
|
|
349
|
+
return this.records.get(recordId)
|
|
302
350
|
}
|
|
303
351
|
|
|
352
|
+
/**
|
|
353
|
+
* @deprecated use layout.records.find() instead
|
|
354
|
+
*/
|
|
304
355
|
newFind(): find {
|
|
305
|
-
return
|
|
356
|
+
return this.records.find()
|
|
306
357
|
}
|
|
307
358
|
|
|
308
359
|
runScript(script): Promise<string | FMError | Error> {
|
|
@@ -329,6 +380,11 @@ class layout {
|
|
|
329
380
|
|
|
330
381
|
public getLayoutMeta(): Promise<layout | FMError> {
|
|
331
382
|
return new Promise((resolve, reject) => {
|
|
383
|
+
if (this.metadata) {
|
|
384
|
+
resolve(this.metadata)
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
332
388
|
this.database.apiRequest(this.endpoint).then(res => {
|
|
333
389
|
this.metadata = res.response
|
|
334
390
|
resolve(this)
|
|
@@ -338,6 +394,54 @@ class layout {
|
|
|
338
394
|
}
|
|
339
395
|
}
|
|
340
396
|
|
|
397
|
+
class layoutRecordManager {
|
|
398
|
+
readonly layout: layout
|
|
399
|
+
|
|
400
|
+
constructor(layout: layout) {
|
|
401
|
+
this.layout = layout
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
create(): Promise<record | Error | void> {
|
|
405
|
+
return new Promise((resolve, reject) => {
|
|
406
|
+
// Get the layout's metadata
|
|
407
|
+
this.layout.getLayoutMeta().then(layout => {
|
|
408
|
+
let fields = {}
|
|
409
|
+
for (let _field of this.layout.metadata.fieldMetaData) {
|
|
410
|
+
fields[_field.name] = ""
|
|
411
|
+
}
|
|
412
|
+
resolve(new record(this, -1, 0, fields))
|
|
413
|
+
}).catch(e => {
|
|
414
|
+
reject(e)
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
get(recordId): Promise<record> {
|
|
420
|
+
return new Promise((resolve, reject) => {
|
|
421
|
+
let record
|
|
422
|
+
this.layout.getLayoutMeta()
|
|
423
|
+
.then(layout => {
|
|
424
|
+
record = new record(this.layout, recordId)
|
|
425
|
+
return record.get()
|
|
426
|
+
})
|
|
427
|
+
.then(() => {
|
|
428
|
+
resolve(record)
|
|
429
|
+
})
|
|
430
|
+
.catch(e => {
|
|
431
|
+
reject(e)
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
range() {
|
|
437
|
+
return new recordGetRange(this.layout)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
find(): find {
|
|
441
|
+
return new find(this.layout)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
341
445
|
class record extends EventEmitter {
|
|
342
446
|
readonly layout: layout;
|
|
343
447
|
public recordId: number;
|
|
@@ -382,7 +486,33 @@ class record extends EventEmitter {
|
|
|
382
486
|
|
|
383
487
|
processFieldData(fieldData): field[] {
|
|
384
488
|
return Object.keys(fieldData).map(item => {
|
|
385
|
-
|
|
489
|
+
let _field = new field(this, item, fieldData[item])
|
|
490
|
+
if (!!fieldData[item]) {
|
|
491
|
+
if (_field.metadata.result === "timeStamp") {
|
|
492
|
+
// @ts-ignore
|
|
493
|
+
let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.timeStampFormat)
|
|
494
|
+
.utcOffset(this.layout.database.host.timezoneOffset, true)
|
|
495
|
+
.local()
|
|
496
|
+
_field.set(date.toDate())
|
|
497
|
+
_field.edited = false
|
|
498
|
+
|
|
499
|
+
} else if (_field.metadata.result === "time") {
|
|
500
|
+
// @ts-ignore
|
|
501
|
+
let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.timeFormat)
|
|
502
|
+
.utcOffset(this.layout.database.host.timezoneOffset, true)
|
|
503
|
+
.local()
|
|
504
|
+
_field.set(date.toDate())
|
|
505
|
+
_field.edited = false
|
|
506
|
+
} else if (_field.metadata.result === "date") {
|
|
507
|
+
// @ts-ignore
|
|
508
|
+
let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.dateFormat)
|
|
509
|
+
.utcOffset(this.layout.database.host.timezoneOffset, true)
|
|
510
|
+
.local()
|
|
511
|
+
_field.set(date.toDate())
|
|
512
|
+
_field.edited = false
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return _field
|
|
386
516
|
})
|
|
387
517
|
}
|
|
388
518
|
|
|
@@ -436,7 +566,6 @@ class record extends EventEmitter {
|
|
|
436
566
|
if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
|
|
437
567
|
reject(new FMError(res.response.scriptError, res.status, res))
|
|
438
568
|
} else if (res.messages[0].code === "0") {
|
|
439
|
-
console.log(res)
|
|
440
569
|
this.recordId = parseInt(res.response.recordId)
|
|
441
570
|
this.modId = parseInt(res.response.modId)
|
|
442
571
|
resolve(this)
|
|
@@ -451,17 +580,16 @@ class record extends EventEmitter {
|
|
|
451
580
|
return
|
|
452
581
|
}
|
|
453
582
|
|
|
454
|
-
for (let item of Object.keys(
|
|
583
|
+
for (let item of Object.keys(data)) extraBody[item] = data[item]
|
|
455
584
|
this.layout.database.apiRequest(this.endpoint, {
|
|
456
585
|
port: 443,
|
|
457
586
|
method: "PATCH",
|
|
458
|
-
body: JSON.stringify(
|
|
587
|
+
body: JSON.stringify(extraBody)
|
|
459
588
|
})
|
|
460
589
|
.then(res => {
|
|
461
590
|
if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
|
|
462
591
|
reject(new FMError(res.response.scriptError, res.status, res))
|
|
463
592
|
} else if (res.messages[0].code === "0") {
|
|
464
|
-
console.log(res)
|
|
465
593
|
this.modId = res.response.modId
|
|
466
594
|
this.emit("saved")
|
|
467
595
|
resolve(this)
|
|
@@ -483,6 +611,54 @@ class record extends EventEmitter {
|
|
|
483
611
|
return this.portals.find(p => p.name === portal)
|
|
484
612
|
}
|
|
485
613
|
|
|
614
|
+
delete(): Promise<void> {
|
|
615
|
+
return new Promise(async (resolve, reject) => {
|
|
616
|
+
this.layout.database.apiRequest(this.endpoint, {
|
|
617
|
+
port: 443,
|
|
618
|
+
method: "DELETE"
|
|
619
|
+
})
|
|
620
|
+
.then(res => {
|
|
621
|
+
if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
|
|
622
|
+
reject(new FMError(res.response.scriptError, res.status, res))
|
|
623
|
+
} else if (res.messages[0].code === "0") {
|
|
624
|
+
this.emit("deleted")
|
|
625
|
+
resolve()
|
|
626
|
+
} else {
|
|
627
|
+
reject(new FMError(res.messages[0].code, res.status, res))
|
|
628
|
+
}
|
|
629
|
+
})
|
|
630
|
+
.catch(e => {
|
|
631
|
+
reject(e)
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
duplicate(): Promise<record> {
|
|
637
|
+
return new Promise(async (resolve, reject) => {
|
|
638
|
+
this.layout.database.apiRequest(this.endpoint, {
|
|
639
|
+
port: 443,
|
|
640
|
+
method: "POST"
|
|
641
|
+
})
|
|
642
|
+
.then(res => {
|
|
643
|
+
if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
|
|
644
|
+
reject(new FMError(res.response.scriptError, res.status, res))
|
|
645
|
+
} else if (res.messages[0].code === "0") {
|
|
646
|
+
let data = this.toObject((a) => true, (a) => true, (a) => false, (a) => false)
|
|
647
|
+
let _res = new record(this.layout, res.response.recordId, res.response.modId, data.fieldData, data.portalData)
|
|
648
|
+
|
|
649
|
+
this.emit("duplicated")
|
|
650
|
+
resolve(_res)
|
|
651
|
+
} else {
|
|
652
|
+
reject(new FMError(res.messages[0].code, res.status, res))
|
|
653
|
+
}
|
|
654
|
+
})
|
|
655
|
+
.catch(e => {
|
|
656
|
+
reject(e)
|
|
657
|
+
})
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
|
|
486
662
|
toObject(filter = (a) => a.edited,
|
|
487
663
|
portalFilter = (a) => a.records.find(record => record.edited),
|
|
488
664
|
portalRowFilter = (a) => a.edited,
|
|
@@ -490,7 +666,26 @@ class record extends EventEmitter {
|
|
|
490
666
|
): recordObject {
|
|
491
667
|
let fields_processed = {}
|
|
492
668
|
for (let field of this.fields.filter(field => filter(field))) {
|
|
493
|
-
|
|
669
|
+
let value = field.value
|
|
670
|
+
if (value instanceof Date) {
|
|
671
|
+
// @ts-ignore
|
|
672
|
+
let _value = moment.default(value)
|
|
673
|
+
.utcOffset(this.layout.database.host.timezoneOffset)
|
|
674
|
+
|
|
675
|
+
// @ts-ignore
|
|
676
|
+
|
|
677
|
+
switch (field.metadata.result) {
|
|
678
|
+
case "time":
|
|
679
|
+
value = _value.format(this.layout.database.host.metadata.productInfo.timeFormat.replace("dd", "DD"))
|
|
680
|
+
break
|
|
681
|
+
case "date":
|
|
682
|
+
value = _value.format(this.layout.database.host.metadata.productInfo.dateFormat.replace("dd", "DD"))
|
|
683
|
+
break
|
|
684
|
+
default:
|
|
685
|
+
value = _value.format(this.layout.database.host.metadata.productInfo.timeStampFormat.replace("dd", "DD"))
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
fields_processed[field.id] = value
|
|
494
689
|
}
|
|
495
690
|
let obj = {
|
|
496
691
|
"recordId": this.recordId,
|
|
@@ -515,33 +710,93 @@ class record extends EventEmitter {
|
|
|
515
710
|
class field {
|
|
516
711
|
record: record;
|
|
517
712
|
id: number;
|
|
518
|
-
|
|
713
|
+
private _value: string | number | Date;
|
|
519
714
|
edited: boolean;
|
|
520
715
|
|
|
521
716
|
constructor(record, id, contents) {
|
|
522
717
|
this.record = record
|
|
523
718
|
this.id = id
|
|
524
|
-
this.
|
|
719
|
+
this._value = contents
|
|
525
720
|
this.edited = false
|
|
526
721
|
}
|
|
527
722
|
|
|
528
|
-
set(content) {
|
|
529
|
-
if (this.
|
|
530
|
-
|
|
723
|
+
set(content: string | number | Date) {
|
|
724
|
+
if (this.metadata.result === "container") throw "Cannot set container value using set(). Use upload() instead."
|
|
725
|
+
if (
|
|
726
|
+
(this.metadata.result === "timeStamp" ||
|
|
727
|
+
this.metadata.result === "date" ||
|
|
728
|
+
this.metadata.result === "time")
|
|
729
|
+
&& !(content instanceof Date) && !!content
|
|
730
|
+
) {
|
|
731
|
+
throw "Value was not an instance of Date: " + content
|
|
732
|
+
}
|
|
733
|
+
this._value = content
|
|
531
734
|
this.edited = true
|
|
532
735
|
}
|
|
533
736
|
|
|
534
|
-
get
|
|
535
|
-
if (!this.record.layout.metadata)
|
|
737
|
+
get metadata(): fieldMetaData {
|
|
738
|
+
if (!this.record.layout.metadata) {
|
|
739
|
+
// Default to a regular text field
|
|
740
|
+
return {
|
|
741
|
+
name: this.id.toString(),
|
|
742
|
+
type: 'normal',
|
|
743
|
+
displayType: 'editText',
|
|
744
|
+
result: 'text',
|
|
745
|
+
global: false,
|
|
746
|
+
autoEnter: true,
|
|
747
|
+
fourDigitYear: false,
|
|
748
|
+
maxRepeat: 1,
|
|
749
|
+
maxCharacters: 0,
|
|
750
|
+
notEmpty: false,
|
|
751
|
+
numeric: false,
|
|
752
|
+
timeOfDay: false,
|
|
753
|
+
repetitionStart: 1,
|
|
754
|
+
repetitionEnd: 1
|
|
755
|
+
} as fieldMetaData
|
|
756
|
+
}
|
|
536
757
|
if (this.record instanceof portalItem) {
|
|
537
|
-
return this.record.layout.metadata.portalMetaData.find(i => i.name === this.id)
|
|
758
|
+
return this.record.layout.metadata.portalMetaData.find(i => i.name === this.id) as fieldMetaData || {
|
|
759
|
+
name: this.id.toString(),
|
|
760
|
+
type: 'normal',
|
|
761
|
+
displayType: 'editText',
|
|
762
|
+
result: 'text',
|
|
763
|
+
global: false,
|
|
764
|
+
autoEnter: true,
|
|
765
|
+
fourDigitYear: false,
|
|
766
|
+
maxRepeat: 1,
|
|
767
|
+
maxCharacters: 0,
|
|
768
|
+
notEmpty: false,
|
|
769
|
+
numeric: false,
|
|
770
|
+
timeOfDay: false,
|
|
771
|
+
repetitionStart: 1,
|
|
772
|
+
repetitionEnd: 1
|
|
773
|
+
} as fieldMetaData
|
|
538
774
|
} else {
|
|
539
|
-
return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id)
|
|
775
|
+
return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id) as fieldMetaData || {
|
|
776
|
+
name: this.id.toString(),
|
|
777
|
+
type: 'normal',
|
|
778
|
+
displayType: 'editText',
|
|
779
|
+
result: 'text',
|
|
780
|
+
global: false,
|
|
781
|
+
autoEnter: true,
|
|
782
|
+
fourDigitYear: false,
|
|
783
|
+
maxRepeat: 1,
|
|
784
|
+
maxCharacters: 0,
|
|
785
|
+
notEmpty: false,
|
|
786
|
+
numeric: false,
|
|
787
|
+
timeOfDay: false,
|
|
788
|
+
repetitionStart: 1,
|
|
789
|
+
repetitionEnd: 1
|
|
790
|
+
} as fieldMetaData
|
|
540
791
|
}
|
|
541
792
|
}
|
|
542
793
|
|
|
794
|
+
get value() {
|
|
795
|
+
return this._value
|
|
796
|
+
}
|
|
797
|
+
|
|
543
798
|
upload(buffer: Buffer, filename: string, mime: string): Promise<void | FMError> {
|
|
544
|
-
if (this.
|
|
799
|
+
if (this.metadata.result !== "container") throw "Cannot upload a file to the field; " + this.id + " (not a container field)"
|
|
545
800
|
return new Promise(async (resolve, reject) => {
|
|
546
801
|
let form = new FormData()
|
|
547
802
|
form.append("upload", new File([buffer], filename, {type: mime}))
|
|
@@ -681,24 +936,27 @@ class recordGetRange extends recordGetOperation {
|
|
|
681
936
|
return "?" + params.join("&")
|
|
682
937
|
}
|
|
683
938
|
|
|
684
|
-
run() {
|
|
939
|
+
run(): Promise<record[]> {
|
|
685
940
|
return new Promise((resolve, reject) => {
|
|
686
941
|
// console.log(this.#toObject())
|
|
687
|
-
this.layout.
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (res.messages[0].code === "0") {
|
|
692
|
-
// console.log("RESOLVING")
|
|
693
|
-
if (!this.layout.metadata) await this.layout.getLayoutMeta()
|
|
694
|
-
let data = res.response.data.map(item => {
|
|
695
|
-
return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
|
|
696
|
-
})
|
|
697
|
-
resolve(data)
|
|
698
|
-
} else {
|
|
699
|
-
reject(new FMError(res.messages[0].code, res.status, res))
|
|
700
|
-
}
|
|
942
|
+
this.layout.getLayoutMeta().then(() => {
|
|
943
|
+
return this.layout.database.apiRequest(`${this.layout.endpoint}/records${this.generateQueryParams()}`, {
|
|
944
|
+
method: "GET"
|
|
945
|
+
})
|
|
701
946
|
})
|
|
947
|
+
.then(async res => {
|
|
948
|
+
// // console.log(res)
|
|
949
|
+
if (res.messages[0].code === "0") {
|
|
950
|
+
// console.log("RESOLVING")
|
|
951
|
+
if (!this.layout.metadata) await this.layout.getLayoutMeta()
|
|
952
|
+
let data = res.response.data.map(item => {
|
|
953
|
+
return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
|
|
954
|
+
})
|
|
955
|
+
resolve(data)
|
|
956
|
+
} else {
|
|
957
|
+
reject(new FMError(res.messages[0].code, res.status, res))
|
|
958
|
+
}
|
|
959
|
+
})
|
|
702
960
|
.catch(e => {
|
|
703
961
|
reject(e)
|
|
704
962
|
})
|
|
@@ -747,26 +1005,30 @@ class find extends recordGetOperation {
|
|
|
747
1005
|
return this.run()
|
|
748
1006
|
}
|
|
749
1007
|
|
|
750
|
-
run() {
|
|
1008
|
+
run(): Promise<record[]> {
|
|
751
1009
|
return new Promise((resolve, reject) => {
|
|
752
1010
|
// console.log(this.#toObject())
|
|
753
|
-
this.layout.
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (res.messages[0].code === "0") {
|
|
760
|
-
// console.log("RESOLVING")
|
|
761
|
-
if (!this.layout.metadata) await this.layout.getLayoutMeta()
|
|
762
|
-
let data = res.response.data.map(item => {
|
|
763
|
-
return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
|
|
1011
|
+
this.layout.getLayoutMeta()
|
|
1012
|
+
.then(() => {
|
|
1013
|
+
return this.layout.database.apiRequest(`${this.layout.endpoint}/_find`, {
|
|
1014
|
+
port: 443,
|
|
1015
|
+
method: "POST",
|
|
1016
|
+
body: JSON.stringify(this.toObject())
|
|
764
1017
|
})
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1018
|
+
})
|
|
1019
|
+
.then(async res => {
|
|
1020
|
+
// // console.log(res)
|
|
1021
|
+
if (res.messages[0].code === "0") {
|
|
1022
|
+
// console.log("RESOLVING")
|
|
1023
|
+
if (!this.layout.metadata) await this.layout.getLayoutMeta()
|
|
1024
|
+
let data = res.response.data.map(item => {
|
|
1025
|
+
return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
|
|
1026
|
+
})
|
|
1027
|
+
resolve(data)
|
|
1028
|
+
} else {
|
|
1029
|
+
reject(new FMError(res.messages[0].code, res.status, res))
|
|
1030
|
+
}
|
|
1031
|
+
})
|
|
770
1032
|
.catch(e => {
|
|
771
1033
|
reject(e)
|
|
772
1034
|
})
|
|
@@ -795,4 +1057,36 @@ export class FMError extends Error {
|
|
|
795
1057
|
//
|
|
796
1058
|
// module.exports = {
|
|
797
1059
|
// default: {FileMakerConnection}
|
|
798
|
-
// }
|
|
1060
|
+
// }
|
|
1061
|
+
|
|
1062
|
+
interface AuthorizationHeaders {
|
|
1063
|
+
"Content-Type": "application/json"
|
|
1064
|
+
Authorization: string
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
interface AuthorizationHeadersOAuth {
|
|
1068
|
+
"Content-Type": "application/json"
|
|
1069
|
+
"X-FM-Data-OAuth-RequestId": string,
|
|
1070
|
+
"X-FM-Data-OAuth-Identifier": string
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function generateAuthorizationHeaders(credentials: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris): AuthorizationHeaders | AuthorizationHeadersOAuth {
|
|
1074
|
+
switch (credentials.method) {
|
|
1075
|
+
case "filemaker":
|
|
1076
|
+
return {
|
|
1077
|
+
"Content-Type": "application/json",
|
|
1078
|
+
"Authorization": "Basic " + Buffer.from((<loginOptionsFileMaker>credentials).username + ":" + (<loginOptionsFileMaker>credentials).password).toString("base64")
|
|
1079
|
+
} as AuthorizationHeaders
|
|
1080
|
+
case "claris":
|
|
1081
|
+
return {
|
|
1082
|
+
"Content-Type": "application/json",
|
|
1083
|
+
"Authorization": (<loginOptionsClaris>credentials).claris.fmid,
|
|
1084
|
+
} as AuthorizationHeaders
|
|
1085
|
+
case "oauth":
|
|
1086
|
+
return {
|
|
1087
|
+
"Content-Type": "application/json",
|
|
1088
|
+
"X-FM-Data-OAuth-RequestId": (<loginOptionsOAuth>credentials).oauth.requestId,
|
|
1089
|
+
"X-FM-Data-OAuth-Identifier": (<loginOptionsOAuth>credentials).oauth.requestIdentifier
|
|
1090
|
+
} as AuthorizationHeadersOAuth
|
|
1091
|
+
}
|
|
1092
|
+
}
|