@jd-data-limited/easy-fm 1.1.6-alpha1 → 1.2.2-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 +297 -119
- package/index.ts +409 -171
- 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;
|
|
@@ -376,18 +475,46 @@ class record extends EventEmitter {
|
|
|
376
475
|
|
|
377
476
|
private processPortalData(portalData): void {
|
|
378
477
|
for (let item of Object.keys(portalData)) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
delete fieldData.
|
|
383
|
-
|
|
384
|
-
|
|
478
|
+
let _portal = new portal(this, item)
|
|
479
|
+
_portal.records = portalData[item].map(item => {
|
|
480
|
+
let fieldData = item;
|
|
481
|
+
delete fieldData.recordId;
|
|
482
|
+
delete fieldData.modId;
|
|
483
|
+
return new portalItem(this, _portal, item.recordId, item.modId, fieldData);
|
|
484
|
+
})
|
|
485
|
+
this.portals.push();
|
|
385
486
|
}
|
|
386
487
|
}
|
|
387
488
|
|
|
388
489
|
processFieldData(fieldData): field[] {
|
|
389
490
|
return Object.keys(fieldData).map(item => {
|
|
390
|
-
|
|
491
|
+
let _field = new field(this, item, fieldData[item])
|
|
492
|
+
if (!!fieldData[item]) {
|
|
493
|
+
if (_field.metadata.result === "timeStamp") {
|
|
494
|
+
// @ts-ignore
|
|
495
|
+
let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.timeStampFormat)
|
|
496
|
+
.utcOffset(this.layout.database.host.timezoneOffset, true)
|
|
497
|
+
.local()
|
|
498
|
+
_field.set(date.toDate())
|
|
499
|
+
_field.edited = false
|
|
500
|
+
|
|
501
|
+
} else if (_field.metadata.result === "time") {
|
|
502
|
+
// @ts-ignore
|
|
503
|
+
let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.timeFormat)
|
|
504
|
+
.utcOffset(this.layout.database.host.timezoneOffset, true)
|
|
505
|
+
.local()
|
|
506
|
+
_field.set(date.toDate())
|
|
507
|
+
_field.edited = false
|
|
508
|
+
} else if (_field.metadata.result === "date") {
|
|
509
|
+
// @ts-ignore
|
|
510
|
+
let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.dateFormat)
|
|
511
|
+
.utcOffset(this.layout.database.host.timezoneOffset, true)
|
|
512
|
+
.local()
|
|
513
|
+
_field.set(date.toDate())
|
|
514
|
+
_field.edited = false
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return _field
|
|
391
518
|
})
|
|
392
519
|
}
|
|
393
520
|
|
|
@@ -441,7 +568,6 @@ class record extends EventEmitter {
|
|
|
441
568
|
if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
|
|
442
569
|
reject(new FMError(res.response.scriptError, res.status, res))
|
|
443
570
|
} else if (res.messages[0].code === "0") {
|
|
444
|
-
console.log(res)
|
|
445
571
|
this.recordId = parseInt(res.response.recordId)
|
|
446
572
|
this.modId = parseInt(res.response.modId)
|
|
447
573
|
resolve(this)
|
|
@@ -456,17 +582,16 @@ class record extends EventEmitter {
|
|
|
456
582
|
return
|
|
457
583
|
}
|
|
458
584
|
|
|
459
|
-
for (let item of Object.keys(
|
|
585
|
+
for (let item of Object.keys(data)) extraBody[item] = data[item]
|
|
460
586
|
this.layout.database.apiRequest(this.endpoint, {
|
|
461
587
|
port: 443,
|
|
462
588
|
method: "PATCH",
|
|
463
|
-
body: JSON.stringify(
|
|
589
|
+
body: JSON.stringify(extraBody)
|
|
464
590
|
})
|
|
465
591
|
.then(res => {
|
|
466
592
|
if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
|
|
467
593
|
reject(new FMError(res.response.scriptError, res.status, res))
|
|
468
594
|
} else if (res.messages[0].code === "0") {
|
|
469
|
-
console.log(res)
|
|
470
595
|
this.modId = res.response.modId
|
|
471
596
|
this.emit("saved")
|
|
472
597
|
resolve(this)
|
|
@@ -487,7 +612,7 @@ class record extends EventEmitter {
|
|
|
487
612
|
getPortal(portal) {
|
|
488
613
|
return this.portals.find(p => p.name === portal)
|
|
489
614
|
}
|
|
490
|
-
|
|
615
|
+
|
|
491
616
|
delete(): Promise<void> {
|
|
492
617
|
return new Promise(async (resolve, reject) => {
|
|
493
618
|
this.layout.database.apiRequest(this.endpoint, {
|
|
@@ -543,7 +668,26 @@ class record extends EventEmitter {
|
|
|
543
668
|
): recordObject {
|
|
544
669
|
let fields_processed = {}
|
|
545
670
|
for (let field of this.fields.filter(field => filter(field))) {
|
|
546
|
-
|
|
671
|
+
let value = field.value
|
|
672
|
+
if (value instanceof Date) {
|
|
673
|
+
// @ts-ignore
|
|
674
|
+
let _value = moment.default(value)
|
|
675
|
+
.utcOffset(this.layout.database.host.timezoneOffset)
|
|
676
|
+
|
|
677
|
+
// @ts-ignore
|
|
678
|
+
|
|
679
|
+
switch (field.metadata.result) {
|
|
680
|
+
case "time":
|
|
681
|
+
value = _value.format(this.layout.database.host.metadata.productInfo.timeFormat.replace("dd", "DD"))
|
|
682
|
+
break
|
|
683
|
+
case "date":
|
|
684
|
+
value = _value.format(this.layout.database.host.metadata.productInfo.dateFormat.replace("dd", "DD"))
|
|
685
|
+
break
|
|
686
|
+
default:
|
|
687
|
+
value = _value.format(this.layout.database.host.metadata.productInfo.timeStampFormat.replace("dd", "DD"))
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
fields_processed[field.id] = value
|
|
547
691
|
}
|
|
548
692
|
let obj = {
|
|
549
693
|
"recordId": this.recordId,
|
|
@@ -568,33 +712,93 @@ class record extends EventEmitter {
|
|
|
568
712
|
class field {
|
|
569
713
|
record: record;
|
|
570
714
|
id: number;
|
|
571
|
-
|
|
715
|
+
private _value: string | number | Date;
|
|
572
716
|
edited: boolean;
|
|
573
717
|
|
|
574
718
|
constructor(record, id, contents) {
|
|
575
719
|
this.record = record
|
|
576
720
|
this.id = id
|
|
577
|
-
this.
|
|
721
|
+
this._value = contents
|
|
578
722
|
this.edited = false
|
|
579
723
|
}
|
|
580
724
|
|
|
581
|
-
set(content) {
|
|
582
|
-
if (this.
|
|
583
|
-
|
|
725
|
+
set(content: string | number | Date) {
|
|
726
|
+
if (this.metadata.result === "container") throw "Cannot set container value using set(). Use upload() instead."
|
|
727
|
+
if (
|
|
728
|
+
(this.metadata.result === "timeStamp" ||
|
|
729
|
+
this.metadata.result === "date" ||
|
|
730
|
+
this.metadata.result === "time")
|
|
731
|
+
&& !(content instanceof Date) && !!content
|
|
732
|
+
) {
|
|
733
|
+
throw "Value was not an instance of Date: " + content
|
|
734
|
+
}
|
|
735
|
+
this._value = content
|
|
584
736
|
this.edited = true
|
|
585
737
|
}
|
|
586
738
|
|
|
587
|
-
get
|
|
588
|
-
if (!this.record.layout.metadata)
|
|
739
|
+
get metadata(): fieldMetaData {
|
|
740
|
+
if (!this.record.layout.metadata) {
|
|
741
|
+
// Default to a regular text field
|
|
742
|
+
return {
|
|
743
|
+
name: this.id.toString(),
|
|
744
|
+
type: 'normal',
|
|
745
|
+
displayType: 'editText',
|
|
746
|
+
result: 'text',
|
|
747
|
+
global: false,
|
|
748
|
+
autoEnter: true,
|
|
749
|
+
fourDigitYear: false,
|
|
750
|
+
maxRepeat: 1,
|
|
751
|
+
maxCharacters: 0,
|
|
752
|
+
notEmpty: false,
|
|
753
|
+
numeric: false,
|
|
754
|
+
timeOfDay: false,
|
|
755
|
+
repetitionStart: 1,
|
|
756
|
+
repetitionEnd: 1
|
|
757
|
+
} as fieldMetaData
|
|
758
|
+
}
|
|
589
759
|
if (this.record instanceof portalItem) {
|
|
590
|
-
return this.record.layout.metadata.portalMetaData.find(i => i.name === this.id)
|
|
760
|
+
return this.record.layout.metadata.portalMetaData[this.record.portal.name || "portal not attached"].find(i => i.name === this.id) as fieldMetaData || {
|
|
761
|
+
name: this.id.toString(),
|
|
762
|
+
type: 'normal',
|
|
763
|
+
displayType: 'editText',
|
|
764
|
+
result: 'text',
|
|
765
|
+
global: false,
|
|
766
|
+
autoEnter: true,
|
|
767
|
+
fourDigitYear: false,
|
|
768
|
+
maxRepeat: 1,
|
|
769
|
+
maxCharacters: 0,
|
|
770
|
+
notEmpty: false,
|
|
771
|
+
numeric: false,
|
|
772
|
+
timeOfDay: false,
|
|
773
|
+
repetitionStart: 1,
|
|
774
|
+
repetitionEnd: 1
|
|
775
|
+
} as fieldMetaData
|
|
591
776
|
} else {
|
|
592
|
-
return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id)
|
|
777
|
+
return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id) as fieldMetaData || {
|
|
778
|
+
name: this.id.toString(),
|
|
779
|
+
type: 'normal',
|
|
780
|
+
displayType: 'editText',
|
|
781
|
+
result: 'text',
|
|
782
|
+
global: false,
|
|
783
|
+
autoEnter: true,
|
|
784
|
+
fourDigitYear: false,
|
|
785
|
+
maxRepeat: 1,
|
|
786
|
+
maxCharacters: 0,
|
|
787
|
+
notEmpty: false,
|
|
788
|
+
numeric: false,
|
|
789
|
+
timeOfDay: false,
|
|
790
|
+
repetitionStart: 1,
|
|
791
|
+
repetitionEnd: 1
|
|
792
|
+
} as fieldMetaData
|
|
593
793
|
}
|
|
594
794
|
}
|
|
595
795
|
|
|
796
|
+
get value() {
|
|
797
|
+
return this._value
|
|
798
|
+
}
|
|
799
|
+
|
|
596
800
|
upload(buffer: Buffer, filename: string, mime: string): Promise<void | FMError> {
|
|
597
|
-
if (this.
|
|
801
|
+
if (this.metadata.result !== "container") throw "Cannot upload a file to the field; " + this.id + " (not a container field)"
|
|
598
802
|
return new Promise(async (resolve, reject) => {
|
|
599
803
|
let form = new FormData()
|
|
600
804
|
form.append("upload", new File([buffer], filename, {type: mime}))
|
|
@@ -632,28 +836,23 @@ class portal {
|
|
|
632
836
|
readonly name: string;
|
|
633
837
|
public records: portalItem[];
|
|
634
838
|
|
|
635
|
-
constructor(record: record, name: string
|
|
839
|
+
constructor(record: record, name: string) {
|
|
636
840
|
this.record = record
|
|
637
841
|
this.name = name
|
|
638
|
-
this.records = records
|
|
639
|
-
for (let item of this.records) item.attachPortal(this)
|
|
640
842
|
}
|
|
641
843
|
}
|
|
642
844
|
|
|
643
845
|
class portalItem extends record {
|
|
644
|
-
|
|
846
|
+
readonly portal: portal;
|
|
645
847
|
|
|
646
|
-
constructor(record, recordId, modId = recordId, fieldData = {}) {
|
|
848
|
+
constructor(record, portal, recordId, modId = recordId, fieldData = {}) {
|
|
647
849
|
super(record.layout, recordId, modId, fieldData);
|
|
850
|
+
this.portal = portal
|
|
648
851
|
record.on("saved", () => {
|
|
649
852
|
this.emit("saved")
|
|
650
853
|
})
|
|
651
854
|
}
|
|
652
855
|
|
|
653
|
-
attachPortal(portal) {
|
|
654
|
-
this.portal = portal
|
|
655
|
-
}
|
|
656
|
-
|
|
657
856
|
commit(extraBody = {fieldData: undefined}) {
|
|
658
857
|
return this.portal.record.commit(extraBody)
|
|
659
858
|
}
|
|
@@ -734,24 +933,27 @@ class recordGetRange extends recordGetOperation {
|
|
|
734
933
|
return "?" + params.join("&")
|
|
735
934
|
}
|
|
736
935
|
|
|
737
|
-
run() {
|
|
936
|
+
run(): Promise<record[]> {
|
|
738
937
|
return new Promise((resolve, reject) => {
|
|
739
938
|
// 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
|
-
}
|
|
939
|
+
this.layout.getLayoutMeta().then(() => {
|
|
940
|
+
return this.layout.database.apiRequest(`${this.layout.endpoint}/records${this.generateQueryParams()}`, {
|
|
941
|
+
method: "GET"
|
|
942
|
+
})
|
|
754
943
|
})
|
|
944
|
+
.then(async res => {
|
|
945
|
+
// // console.log(res)
|
|
946
|
+
if (res.messages[0].code === "0") {
|
|
947
|
+
// console.log("RESOLVING")
|
|
948
|
+
if (!this.layout.metadata) await this.layout.getLayoutMeta()
|
|
949
|
+
let data = res.response.data.map(item => {
|
|
950
|
+
return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
|
|
951
|
+
})
|
|
952
|
+
resolve(data)
|
|
953
|
+
} else {
|
|
954
|
+
reject(new FMError(res.messages[0].code, res.status, res))
|
|
955
|
+
}
|
|
956
|
+
})
|
|
755
957
|
.catch(e => {
|
|
756
958
|
reject(e)
|
|
757
959
|
})
|
|
@@ -800,26 +1002,30 @@ class find extends recordGetOperation {
|
|
|
800
1002
|
return this.run()
|
|
801
1003
|
}
|
|
802
1004
|
|
|
803
|
-
run() {
|
|
1005
|
+
run(): Promise<record[]> {
|
|
804
1006
|
return new Promise((resolve, reject) => {
|
|
805
1007
|
// 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)
|
|
1008
|
+
this.layout.getLayoutMeta()
|
|
1009
|
+
.then(() => {
|
|
1010
|
+
return this.layout.database.apiRequest(`${this.layout.endpoint}/_find`, {
|
|
1011
|
+
port: 443,
|
|
1012
|
+
method: "POST",
|
|
1013
|
+
body: JSON.stringify(this.toObject())
|
|
817
1014
|
})
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1015
|
+
})
|
|
1016
|
+
.then(async res => {
|
|
1017
|
+
// // console.log(res)
|
|
1018
|
+
if (res.messages[0].code === "0") {
|
|
1019
|
+
// console.log("RESOLVING")
|
|
1020
|
+
if (!this.layout.metadata) await this.layout.getLayoutMeta()
|
|
1021
|
+
let data = res.response.data.map(item => {
|
|
1022
|
+
return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
|
|
1023
|
+
})
|
|
1024
|
+
resolve(data)
|
|
1025
|
+
} else {
|
|
1026
|
+
reject(new FMError(res.messages[0].code, res.status, res))
|
|
1027
|
+
}
|
|
1028
|
+
})
|
|
823
1029
|
.catch(e => {
|
|
824
1030
|
reject(e)
|
|
825
1031
|
})
|
|
@@ -848,4 +1054,36 @@ export class FMError extends Error {
|
|
|
848
1054
|
//
|
|
849
1055
|
// module.exports = {
|
|
850
1056
|
// default: {FileMakerConnection}
|
|
851
|
-
// }
|
|
1057
|
+
// }
|
|
1058
|
+
|
|
1059
|
+
interface AuthorizationHeaders {
|
|
1060
|
+
"Content-Type": "application/json"
|
|
1061
|
+
Authorization: string
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
interface AuthorizationHeadersOAuth {
|
|
1065
|
+
"Content-Type": "application/json"
|
|
1066
|
+
"X-FM-Data-OAuth-RequestId": string,
|
|
1067
|
+
"X-FM-Data-OAuth-Identifier": string
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function generateAuthorizationHeaders(credentials: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris): AuthorizationHeaders | AuthorizationHeadersOAuth {
|
|
1071
|
+
switch (credentials.method) {
|
|
1072
|
+
case "filemaker":
|
|
1073
|
+
return {
|
|
1074
|
+
"Content-Type": "application/json",
|
|
1075
|
+
"Authorization": "Basic " + Buffer.from((<loginOptionsFileMaker>credentials).username + ":" + (<loginOptionsFileMaker>credentials).password).toString("base64")
|
|
1076
|
+
} as AuthorizationHeaders
|
|
1077
|
+
case "claris":
|
|
1078
|
+
return {
|
|
1079
|
+
"Content-Type": "application/json",
|
|
1080
|
+
"Authorization": (<loginOptionsClaris>credentials).claris.fmid,
|
|
1081
|
+
} as AuthorizationHeaders
|
|
1082
|
+
case "oauth":
|
|
1083
|
+
return {
|
|
1084
|
+
"Content-Type": "application/json",
|
|
1085
|
+
"X-FM-Data-OAuth-RequestId": (<loginOptionsOAuth>credentials).oauth.requestId,
|
|
1086
|
+
"X-FM-Data-OAuth-Identifier": (<loginOptionsOAuth>credentials).oauth.requestIdentifier
|
|
1087
|
+
} as AuthorizationHeadersOAuth
|
|
1088
|
+
}
|
|
1089
|
+
}
|