@jd-data-limited/easy-fm 1.1.6-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 +289 -108
- package/index.ts +397 -156
- 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
|
|
@@ -86,33 +81,121 @@ interface recordObject {
|
|
|
86
81
|
recordId: number
|
|
87
82
|
modId: number,
|
|
88
83
|
fieldData: any,
|
|
89
|
-
portalData
|
|
84
|
+
portalData?: portalDataObject
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface fileMakerResponse {
|
|
88
|
+
response: any,
|
|
89
|
+
messages: any[]
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
interface portalDataObject {
|
|
93
93
|
[key: string]: recordObject
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
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 {
|
|
97
182
|
private _token: any;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
public name: string;
|
|
183
|
+
readonly host: FMHost;
|
|
184
|
+
private connection_details: databaseOptionsWithExternalSources
|
|
185
|
+
readonly name: string;
|
|
102
186
|
|
|
103
|
-
constructor(
|
|
187
|
+
constructor(host: FMHost, conn: databaseOptionsWithExternalSources) {
|
|
104
188
|
super()
|
|
105
|
-
this.
|
|
106
|
-
this.name = conn.database
|
|
107
|
-
this.
|
|
108
|
-
this.rejectUnauthroized = rejectUnauthorized
|
|
189
|
+
this.host = host
|
|
190
|
+
this.name = conn.database
|
|
191
|
+
this.connection_details = conn
|
|
109
192
|
}
|
|
110
193
|
|
|
111
|
-
private generateExternalSourceLogin(data:
|
|
112
|
-
if (data.method === "filemaker") {
|
|
113
|
-
let _data = <loginOptionsFileMaker>data
|
|
194
|
+
private generateExternalSourceLogin(data: databaseOptionsBase) {
|
|
195
|
+
if (data.credentials.method === "filemaker") {
|
|
196
|
+
let _data = <loginOptionsFileMaker>data.credentials
|
|
114
197
|
return {
|
|
115
|
-
database:
|
|
198
|
+
database: data.database,
|
|
116
199
|
username: _data.username,
|
|
117
200
|
password: _data.password
|
|
118
201
|
}
|
|
@@ -134,81 +217,47 @@ export default class FileMakerConnection extends EventEmitter {
|
|
|
134
217
|
let data = await _fetch.json()
|
|
135
218
|
// console.log(data)
|
|
136
219
|
this._token = null
|
|
137
|
-
this.hostname = null
|
|
138
220
|
resolve()
|
|
139
221
|
})
|
|
140
222
|
}
|
|
141
223
|
|
|
142
224
|
login() {
|
|
143
|
-
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
|
+
}
|
|
144
232
|
if (this.token) throw new Error("Already logged in. Run logout() first")
|
|
145
233
|
|
|
146
|
-
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 {
|
|
147
238
|
fetch(`${this.endpoint}/sessions`, {
|
|
148
|
-
hostname: this.hostname,
|
|
239
|
+
hostname: this.host.hostname,
|
|
149
240
|
port: 443,
|
|
150
241
|
method: "POST",
|
|
151
|
-
headers:
|
|
152
|
-
"Content-Type": "application/json",
|
|
153
|
-
"Authorization": "Basic " + Buffer.from((<loginOptionsFileMaker>this.databaseConDetails.database).username + ":" + (<loginOptionsFileMaker>this.databaseConDetails.database).password).toString("base64")
|
|
154
|
-
},
|
|
242
|
+
headers: generateAuthorizationHeaders(this.connection_details.credentials) as unknown as HeadersInit,
|
|
155
243
|
body: JSON.stringify({
|
|
156
|
-
fmDataSource: this.
|
|
157
|
-
let _i = <
|
|
244
|
+
fmDataSource: this.connection_details.externalSources.map(i => {
|
|
245
|
+
let _i = <databaseOptionsBase>i
|
|
158
246
|
return this.generateExternalSourceLogin(_i)
|
|
159
247
|
})
|
|
160
248
|
})
|
|
161
249
|
}).then(async res => {
|
|
250
|
+
let _res = <any>(await res.json())
|
|
162
251
|
if (res.status === 200) {
|
|
163
252
|
this._token = res.headers.get('x-fm-data-access-token')
|
|
164
253
|
resolve(this._token)
|
|
165
254
|
} else {
|
|
166
|
-
let _res = <any>(await res.json())
|
|
167
255
|
reject(new FMError(_res.messages[0].code, _res.status, res))
|
|
168
256
|
}
|
|
169
257
|
})
|
|
170
258
|
.catch(e => {
|
|
171
259
|
reject(e)
|
|
172
260
|
})
|
|
173
|
-
} else if (this.databaseConDetails.database.method === "token") {
|
|
174
|
-
this._token = (<loginOptionsToken>this.databaseConDetails.database).token
|
|
175
|
-
resolve(this.token)
|
|
176
|
-
} else if (this.databaseConDetails.database.method === "oauth") {
|
|
177
|
-
fetch(`${this.endpoint}/sessions`, {
|
|
178
|
-
method: "POST",
|
|
179
|
-
headers: {
|
|
180
|
-
"Content-Type": "application/json",
|
|
181
|
-
"X-FM-Data-OAuth-RequestId": (<loginOptionsOAuth>this.databaseConDetails.database).oauth.requestId,
|
|
182
|
-
"X-FM-Data-OAuth-Identifier": (<loginOptionsOAuth>this.databaseConDetails.database).oauth.requestIdentifier
|
|
183
|
-
},
|
|
184
|
-
body: JSON.stringify({
|
|
185
|
-
fmDataSource: this.databaseConDetails.externalSources.map(i => {
|
|
186
|
-
let _i = <databaseOptions>i
|
|
187
|
-
return this.generateExternalSourceLogin(_i)
|
|
188
|
-
})
|
|
189
|
-
})
|
|
190
|
-
})
|
|
191
|
-
.then(res => res.json())
|
|
192
|
-
.then(res => {
|
|
193
|
-
let _res = <any>res
|
|
194
|
-
this._token = _res.headers["x-fm-data-access-token"]
|
|
195
|
-
resolve(this.token)
|
|
196
|
-
})
|
|
197
|
-
} else if (this.databaseConDetails.database.method === "claris") {
|
|
198
|
-
fetch(`${this.endpoint}/sessions`, {
|
|
199
|
-
method: "POST",
|
|
200
|
-
headers: {
|
|
201
|
-
"Content-Type": "application/json",
|
|
202
|
-
"Authorization": (<loginOptionsClaris>this.databaseConDetails.database).claris.fmid,
|
|
203
|
-
},
|
|
204
|
-
body: "{}"
|
|
205
|
-
})
|
|
206
|
-
.then(res => res.json())
|
|
207
|
-
.then(res => {
|
|
208
|
-
let _res = <any>res
|
|
209
|
-
this._token = _res.headers["x-fm-data-access-token"]
|
|
210
|
-
resolve(this.token)
|
|
211
|
-
})
|
|
212
261
|
}
|
|
213
262
|
})
|
|
214
263
|
}
|
|
@@ -218,22 +267,22 @@ export default class FileMakerConnection extends EventEmitter {
|
|
|
218
267
|
}
|
|
219
268
|
|
|
220
269
|
get endpoint(): string {
|
|
221
|
-
return
|
|
270
|
+
return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`
|
|
222
271
|
}
|
|
223
272
|
|
|
224
273
|
async apiRequest(url: string | Request, options: any = {}, autoRelogin = true): Promise<any> {
|
|
225
274
|
if (!options.headers) options.headers = {}
|
|
226
275
|
options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json"
|
|
227
276
|
options.headers["authorization"] = "Bearer " + this._token
|
|
228
|
-
options.rejectUnauthorized = this.
|
|
277
|
+
options.rejectUnauthorized = this.host.verify
|
|
229
278
|
|
|
230
279
|
let _fetch = await fetch(url, options)
|
|
231
|
-
let data = await _fetch.json()
|
|
232
|
-
console.log(data.messages[0])
|
|
280
|
+
let data = await _fetch.json() as fileMakerResponse
|
|
281
|
+
// console.log(data.messages[0])
|
|
233
282
|
if (data.messages[0].code == 952 && autoRelogin) {
|
|
234
283
|
this._token = null
|
|
235
284
|
await this.login()
|
|
236
|
-
return await this.apiRequest(url, options,false)
|
|
285
|
+
return await this.apiRequest(url, options, false)
|
|
237
286
|
}
|
|
238
287
|
return (data as any)
|
|
239
288
|
}
|
|
@@ -272,11 +321,12 @@ export default class FileMakerConnection extends EventEmitter {
|
|
|
272
321
|
}
|
|
273
322
|
|
|
274
323
|
class layout {
|
|
275
|
-
readonly database:
|
|
324
|
+
readonly database: Database;
|
|
325
|
+
readonly records = new layoutRecordManager(this)
|
|
276
326
|
protected name: string;
|
|
277
327
|
metadata: any;
|
|
278
328
|
|
|
279
|
-
constructor(database:
|
|
329
|
+
constructor(database: Database, name: string) {
|
|
280
330
|
this.database = database
|
|
281
331
|
this.name = name
|
|
282
332
|
}
|
|
@@ -285,29 +335,25 @@ class layout {
|
|
|
285
335
|
return `${this.database.endpoint}/layouts/${this.name}`
|
|
286
336
|
}
|
|
287
337
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
338
|
+
/**
|
|
339
|
+
* @deprecated use layout.records.create() instead
|
|
340
|
+
*/
|
|
292
341
|
createRecord(): Promise<record | Error | void> {
|
|
293
|
-
return
|
|
294
|
-
// Get the layout's metadata
|
|
295
|
-
this.getLayoutMeta().then(layout => {
|
|
296
|
-
let fields = {}
|
|
297
|
-
for (let _field of this.metadata.fieldMetaData) {
|
|
298
|
-
fields[_field.name] = ""
|
|
299
|
-
}
|
|
300
|
-
resolve(new record(this, -1, 0, fields))
|
|
301
|
-
}).catch(e => {reject(e)})
|
|
302
|
-
})
|
|
342
|
+
return this.records.create()
|
|
303
343
|
}
|
|
304
344
|
|
|
305
|
-
|
|
306
|
-
|
|
345
|
+
/**
|
|
346
|
+
* @deprecated use layout.records.get() instead
|
|
347
|
+
*/
|
|
348
|
+
getRecord(recordId): Promise<record> {
|
|
349
|
+
return this.records.get(recordId)
|
|
307
350
|
}
|
|
308
351
|
|
|
352
|
+
/**
|
|
353
|
+
* @deprecated use layout.records.find() instead
|
|
354
|
+
*/
|
|
309
355
|
newFind(): find {
|
|
310
|
-
return
|
|
356
|
+
return this.records.find()
|
|
311
357
|
}
|
|
312
358
|
|
|
313
359
|
runScript(script): Promise<string | FMError | Error> {
|
|
@@ -334,6 +380,11 @@ class layout {
|
|
|
334
380
|
|
|
335
381
|
public getLayoutMeta(): Promise<layout | FMError> {
|
|
336
382
|
return new Promise((resolve, reject) => {
|
|
383
|
+
if (this.metadata) {
|
|
384
|
+
resolve(this.metadata)
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
337
388
|
this.database.apiRequest(this.endpoint).then(res => {
|
|
338
389
|
this.metadata = res.response
|
|
339
390
|
resolve(this)
|
|
@@ -343,6 +394,54 @@ class layout {
|
|
|
343
394
|
}
|
|
344
395
|
}
|
|
345
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
|
+
|
|
346
445
|
class record extends EventEmitter {
|
|
347
446
|
readonly layout: layout;
|
|
348
447
|
public recordId: number;
|
|
@@ -387,7 +486,33 @@ class record extends EventEmitter {
|
|
|
387
486
|
|
|
388
487
|
processFieldData(fieldData): field[] {
|
|
389
488
|
return Object.keys(fieldData).map(item => {
|
|
390
|
-
|
|
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
|
|
391
516
|
})
|
|
392
517
|
}
|
|
393
518
|
|
|
@@ -441,7 +566,6 @@ class record extends EventEmitter {
|
|
|
441
566
|
if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
|
|
442
567
|
reject(new FMError(res.response.scriptError, res.status, res))
|
|
443
568
|
} else if (res.messages[0].code === "0") {
|
|
444
|
-
console.log(res)
|
|
445
569
|
this.recordId = parseInt(res.response.recordId)
|
|
446
570
|
this.modId = parseInt(res.response.modId)
|
|
447
571
|
resolve(this)
|
|
@@ -456,17 +580,16 @@ class record extends EventEmitter {
|
|
|
456
580
|
return
|
|
457
581
|
}
|
|
458
582
|
|
|
459
|
-
for (let item of Object.keys(
|
|
583
|
+
for (let item of Object.keys(data)) extraBody[item] = data[item]
|
|
460
584
|
this.layout.database.apiRequest(this.endpoint, {
|
|
461
585
|
port: 443,
|
|
462
586
|
method: "PATCH",
|
|
463
|
-
body: JSON.stringify(
|
|
587
|
+
body: JSON.stringify(extraBody)
|
|
464
588
|
})
|
|
465
589
|
.then(res => {
|
|
466
590
|
if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
|
|
467
591
|
reject(new FMError(res.response.scriptError, res.status, res))
|
|
468
592
|
} else if (res.messages[0].code === "0") {
|
|
469
|
-
console.log(res)
|
|
470
593
|
this.modId = res.response.modId
|
|
471
594
|
this.emit("saved")
|
|
472
595
|
resolve(this)
|
|
@@ -487,7 +610,7 @@ class record extends EventEmitter {
|
|
|
487
610
|
getPortal(portal) {
|
|
488
611
|
return this.portals.find(p => p.name === portal)
|
|
489
612
|
}
|
|
490
|
-
|
|
613
|
+
|
|
491
614
|
delete(): Promise<void> {
|
|
492
615
|
return new Promise(async (resolve, reject) => {
|
|
493
616
|
this.layout.database.apiRequest(this.endpoint, {
|
|
@@ -543,7 +666,26 @@ class record extends EventEmitter {
|
|
|
543
666
|
): recordObject {
|
|
544
667
|
let fields_processed = {}
|
|
545
668
|
for (let field of this.fields.filter(field => filter(field))) {
|
|
546
|
-
|
|
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
|
|
547
689
|
}
|
|
548
690
|
let obj = {
|
|
549
691
|
"recordId": this.recordId,
|
|
@@ -568,33 +710,93 @@ class record extends EventEmitter {
|
|
|
568
710
|
class field {
|
|
569
711
|
record: record;
|
|
570
712
|
id: number;
|
|
571
|
-
|
|
713
|
+
private _value: string | number | Date;
|
|
572
714
|
edited: boolean;
|
|
573
715
|
|
|
574
716
|
constructor(record, id, contents) {
|
|
575
717
|
this.record = record
|
|
576
718
|
this.id = id
|
|
577
|
-
this.
|
|
719
|
+
this._value = contents
|
|
578
720
|
this.edited = false
|
|
579
721
|
}
|
|
580
722
|
|
|
581
|
-
set(content) {
|
|
582
|
-
if (this.
|
|
583
|
-
|
|
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
|
|
584
734
|
this.edited = true
|
|
585
735
|
}
|
|
586
736
|
|
|
587
|
-
get
|
|
588
|
-
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
|
+
}
|
|
589
757
|
if (this.record instanceof portalItem) {
|
|
590
|
-
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
|
|
591
774
|
} else {
|
|
592
|
-
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
|
|
593
791
|
}
|
|
594
792
|
}
|
|
595
793
|
|
|
794
|
+
get value() {
|
|
795
|
+
return this._value
|
|
796
|
+
}
|
|
797
|
+
|
|
596
798
|
upload(buffer: Buffer, filename: string, mime: string): Promise<void | FMError> {
|
|
597
|
-
if (this.
|
|
799
|
+
if (this.metadata.result !== "container") throw "Cannot upload a file to the field; " + this.id + " (not a container field)"
|
|
598
800
|
return new Promise(async (resolve, reject) => {
|
|
599
801
|
let form = new FormData()
|
|
600
802
|
form.append("upload", new File([buffer], filename, {type: mime}))
|
|
@@ -734,24 +936,27 @@ class recordGetRange extends recordGetOperation {
|
|
|
734
936
|
return "?" + params.join("&")
|
|
735
937
|
}
|
|
736
938
|
|
|
737
|
-
run() {
|
|
939
|
+
run(): Promise<record[]> {
|
|
738
940
|
return new Promise((resolve, reject) => {
|
|
739
941
|
// console.log(this.#toObject())
|
|
740
|
-
this.layout.
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (res.messages[0].code === "0") {
|
|
745
|
-
// console.log("RESOLVING")
|
|
746
|
-
if (!this.layout.metadata) await this.layout.getLayoutMeta()
|
|
747
|
-
let data = res.response.data.map(item => {
|
|
748
|
-
return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
|
|
749
|
-
})
|
|
750
|
-
resolve(data)
|
|
751
|
-
} else {
|
|
752
|
-
reject(new FMError(res.messages[0].code, res.status, res))
|
|
753
|
-
}
|
|
942
|
+
this.layout.getLayoutMeta().then(() => {
|
|
943
|
+
return this.layout.database.apiRequest(`${this.layout.endpoint}/records${this.generateQueryParams()}`, {
|
|
944
|
+
method: "GET"
|
|
945
|
+
})
|
|
754
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
|
+
})
|
|
755
960
|
.catch(e => {
|
|
756
961
|
reject(e)
|
|
757
962
|
})
|
|
@@ -800,26 +1005,30 @@ class find extends recordGetOperation {
|
|
|
800
1005
|
return this.run()
|
|
801
1006
|
}
|
|
802
1007
|
|
|
803
|
-
run() {
|
|
1008
|
+
run(): Promise<record[]> {
|
|
804
1009
|
return new Promise((resolve, reject) => {
|
|
805
1010
|
// console.log(this.#toObject())
|
|
806
|
-
this.layout.
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (res.messages[0].code === "0") {
|
|
813
|
-
// console.log("RESOLVING")
|
|
814
|
-
if (!this.layout.metadata) await this.layout.getLayoutMeta()
|
|
815
|
-
let data = res.response.data.map(item => {
|
|
816
|
-
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())
|
|
817
1017
|
})
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
+
})
|
|
823
1032
|
.catch(e => {
|
|
824
1033
|
reject(e)
|
|
825
1034
|
})
|
|
@@ -848,4 +1057,36 @@ export class FMError extends Error {
|
|
|
848
1057
|
//
|
|
849
1058
|
// module.exports = {
|
|
850
1059
|
// default: {FileMakerConnection}
|
|
851
|
-
// }
|
|
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
|
+
}
|