@jd-data-limited/easy-fm 1.0.0-alpha1 → 1.0.1-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/index.ts CHANGED
@@ -1,708 +1,716 @@
1
- /*
2
- * Copyright (c) 2022. See LICENSE file for more information
3
- */
4
-
5
- // const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
6
- import fetch, {
7
- Blob,
8
- blobFrom,
9
- blobFromSync,
10
- File,
11
- fileFrom,
12
- fileFromSync,
13
- FormData,
14
- Headers,
15
- Request,
16
- Response
17
- } from 'node-fetch';
18
- import { EventEmitter } from "events";
19
- // @ts-ignore
20
- import fs from "fs"
21
- // @ts-ignore
22
- import path from "path"
23
- // @ts-ignore
24
- import { fileURLToPath } from "url"
25
- import * as https from "https";
26
- // import * as btoa from "btoa";
27
-
28
- interface connectionOptions {
29
- hostname: string | undefined,
30
- database: databaseOptions
31
- }
32
-
33
- interface hostConnectionOptions {
34
- hostname: string
35
- database: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken
36
- }
37
-
38
- interface databaseOptions {
39
- method: any
40
- database: string
41
- }
42
-
43
- interface loginOptionsToken extends databaseOptions {
44
- method: "token"
45
- token: string
46
- }
47
-
48
- interface loginOptionsOAuth extends databaseOptions {
49
- method: "oauth"
50
- oauth: {
51
- requestId: string,
52
- requestIdentifier: string
53
- }
54
- }
55
-
56
- interface loginOptionsFileMaker extends databaseOptions {
57
- method: "filemaker"
58
- username: string,
59
- password: string,
60
- }
61
-
62
- interface loginOptionsClaris extends databaseOptions {
63
- method: "claris"
64
- claris: {
65
- fmid: string
66
- }
67
- }
68
-
69
- // @ts-ignore
70
- const errs = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'fmErrors.json')).toString())
71
-
72
- interface Script {
73
- name: string
74
- parameter: string
75
- }
76
-
77
- interface recordObject {
78
- recordId: number
79
- modId: number,
80
- fieldData: any
81
- }
82
-
83
- class database {
84
- external: boolean;
85
- public name: string;
86
-
87
- protected host: FileMakerConnection;
88
- protected props: databaseOptions;
89
-
90
- public hostname: string;
91
-
92
- constructor(props: databaseOptions, host: FileMakerConnection = null) {
93
- this.props = props
94
- this.name = this.props.database
95
- if (host) this.host = host
96
- }
97
-
98
- get token() {
99
- return this.host.token
100
- }
101
-
102
- getLayout(name): layout {
103
- return new layout(this, name)
104
- }
105
-
106
- async apiRequest(url: string | Request, options: any = {}): Promise<any> {
107
- if (!options.headers) options.headers = {}
108
- options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json"
109
- options.headers["authorization"] = "Bearer " + this.host.token
110
- options.rejectUnauthorized = this.host.rejectUnauthroized
111
-
112
- let _fetch = await fetch(url, options)
113
- let data = await _fetch.json()
114
- return (data as any)
115
- }
116
-
117
- setGlobals(globalFields): Promise<void> {
118
- // console.log({globalFields})
119
- return new Promise((resolve, reject) => {
120
- this.apiRequest(`${this.endpoint}/globals`, {
121
- method: "PATCH",
122
- body: JSON.stringify({globalFields})
123
- }).then(res => {
124
- // console.log(res)
125
- if (res.messages[0].code === "0") {
126
- resolve()
127
- } else {
128
- reject(new FMError(res.messages[0].code, res.status, res))
129
- }
130
- })
131
- .catch(e => {
132
- reject(e)
133
- })
134
- })
135
- }
136
-
137
- script(name, parameter): Script {
138
- return ({name, parameter} as Script)
139
- }
140
-
141
- get endpoint(): string {
142
- return `https://${this.hostname || this.host.hostname}/fmi/data/v2/databases/${encodeURI(this.name)}`
143
- }
144
-
145
- get externalLoginInfo() {
146
- // Returns an object that can be used when using this database object as an external source
147
- let out = {
148
- database: this.name,
149
- username: undefined,
150
- password: undefined,
151
- oauthRequestId: undefined,
152
- oauthIdentifier: undefined
153
- }
154
- switch (this.props.method) {
155
- case "filemaker":
156
- out.username = (<loginOptionsFileMaker>this.props).username
157
- out.password = (<loginOptionsFileMaker>this.props).password
158
- case "token":
159
- throw "Token logins cannot be used for connecting to external sources. Please either use no login method, or open a second connection."
160
- case "oauth":
161
- out.oauthRequestId = (<loginOptionsOAuth>this.props).oauth.requestId
162
- out.oauthIdentifier = (<loginOptionsOAuth>this.props).oauth.requestIdentifier
163
-
164
- case "claris":
165
- throw "Claris logins cannot be used for connecting to external sources. Please either use no login method, or open a second connection."
166
- }
167
- return out
168
- }
169
- }
170
-
171
- export default class FileMakerConnection extends database{
172
- private _token: any;
173
- private username: any;
174
- private password: any;
175
- public readonly rejectUnauthroized: boolean;
176
- externalSources: database[];
177
-
178
- constructor(conn: hostConnectionOptions, externalDataSources:databaseOptions[] = [], rejectUnauthorized = true) {
179
- super(conn.database);
180
- this.host = this
181
- this.hostname = conn.hostname
182
- this.externalSources = externalDataSources.map(i => {let e = new database(i, this); e.external = true; return e;})
183
- this.rejectUnauthroized = rejectUnauthorized
184
- }
185
-
186
- logout(): Promise<void> {
187
- return new Promise(async (resolve, reject) => {
188
- if (!this.token) reject(new Error("Not logged in"))
189
-
190
- let _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
191
- method: "DELETE",
192
- headers: {
193
- "content-type": "application/json"
194
- }
195
- })
196
- let data = await _fetch.json()
197
- // console.log(data)
198
- this._token = null
199
- this.name = null
200
- this.hostname = null
201
- resolve()
202
- })
203
- }
204
-
205
- login() {
206
- return new Promise<string>((resolve, reject) => {
207
- if (this.token) throw new Error("Already logged in. Run logout() first")
208
-
209
- if (this.props.method === "filemaker") {
210
- this.username = (<loginOptionsFileMaker>this.props).username
211
- this.name = this.props.database
212
-
213
- fetch(`${this.endpoint}/sessions`, {
214
- hostname: this.hostname,
215
- port: 443,
216
- method: "POST",
217
- headers: {
218
- "Content-Type": "application/json",
219
- "Authorization": "Basic " + Buffer.from(this.username + ":" + (<loginOptionsFileMaker>this.props).password).toString("base64")
220
- }
221
- }).then(async res => {
222
- if (res.status === 200) {
223
- this._token = res.headers.get('x-fm-data-access-token')
224
- resolve(this._token)
225
- } else {
226
- let _res = <any>(await res.json())
227
- reject(new FMError(_res.messages[0].code, _res.status, res))
228
- }
229
- })
230
- .catch(e => {
231
- reject(e)
232
- })
233
- }
234
- else if (this.props.method === "token") {
235
- this._token = (<loginOptionsToken>this.props).token
236
- this.name = this.props.database
237
- resolve(this.token)
238
- }
239
- else if (this.props.method === "oauth") {
240
- this.name = this.props.database
241
- fetch(`${this.endpoint}/sessions`, {
242
- method: "POST",
243
- headers: {
244
- "Content-Type": "application/json",
245
- "X-FM-Data-OAuth-RequestId": (<loginOptionsOAuth>this.props).oauth.requestId,
246
- "X-FM-Data-OAuth-Identifier": (<loginOptionsOAuth>this.props).oauth.requestIdentifier
247
- },
248
- body: "{}"
249
- })
250
- .then(res => res.json())
251
- .then(res => {
252
- let _res = <any>res
253
- this._token = _res.headers["x-fm-data-access-token"]
254
- resolve(this.token)
255
- })
256
- }
257
- else if (this.props.method === "claris") {
258
- this.name = this.props.database
259
- fetch(`${this.endpoint}/sessions`, {
260
- method: "POST",
261
- headers: {
262
- "Content-Type": "application/json",
263
- "Authorization": (<loginOptionsClaris>this.props).claris.fmid,
264
- },
265
- body: "{}"
266
- })
267
- .then(res => res.json())
268
- .then(res => {
269
- let _res = <any>res
270
- this._token = _res.headers["x-fm-data-access-token"]
271
- resolve(this.token)
272
- })
273
- }
274
- })
275
- }
276
-
277
- get token() {
278
- return this._token
279
- }
280
- }
281
-
282
- class layout {
283
- readonly database: database;
284
- protected name: string;
285
- metadata: any;
286
- constructor(database: database, name: string) {
287
- this.database = database
288
- this.name = name
289
- }
290
-
291
- get endpoint() {
292
- return `${this.database.endpoint}/layouts/${this.name}`
293
- }
294
-
295
- getScript(script) {
296
- return new script(this, script)
297
- }
298
-
299
- createRecord(): Promise<record | Error | void> {
300
- return new Promise((resolve, reject) => {
301
- // Get the layout's metadata
302
- this.getLayoutMeta().then(layout => {
303
- let fields = {}
304
- for (let _field of this.metadata.fieldMetaData) {
305
- fields[_field.name] = ""
306
- }
307
- resolve(new record(this, -1, 0, fields))
308
- })
309
- })
310
- }
311
-
312
- getRecord(recordId): record {
313
- return new record(this, recordId)
314
- }
315
-
316
- newFind(): find {
317
- return new find(this)
318
- }
319
-
320
- runScript(script): Promise<string | FMError | Error> {
321
- return new Promise(async (resolve, reject) => {
322
- let url = `${this.endpoint}/script/${encodeURI(script.name)}`
323
- if (script.parameter) url += "?" + encodeURI(script.parameter)
324
- this.database.apiRequest(url, {
325
- port: 443,
326
- method: "GET"
327
- })
328
- .then(res => {
329
- if (res.messages[0].code === "0") {
330
- resolve(res.response)
331
- } else {
332
- // console.log(res)
333
- reject(new FMError(res.messages[0].code, res.status, res))
334
- }
335
- })
336
- .catch(e => {
337
- reject(e)
338
- })
339
- })
340
- }
341
-
342
- public getLayoutMeta(): Promise<layout | FMError> {
343
- return new Promise((resolve, reject) => {
344
- this.database.apiRequest(this.endpoint).then(res => {
345
- this.metadata = res.response
346
- resolve(this)
347
- })
348
- .catch(e => reject(e))
349
- })
350
- }
351
- }
352
-
353
- class record extends EventEmitter {
354
- readonly layout: layout;
355
- public recordId: number;
356
- modId: number;
357
- fields: field[];
358
- readonly portals: portal[];
359
- private portalData: any[];
360
-
361
- constructor(layout, recordId, modId = recordId, fieldData = {}, portalData = null) {
362
- super();
363
- this.layout = layout
364
- this.recordId = recordId
365
- this.modId = modId
366
- this.fields = this.processFieldData(fieldData)
367
- this.portals = []
368
- if (portalData) {
369
- this.processPortalData(portalData)
370
- }
371
- this.on("saved", () => {
372
- for (let field of this.fields) field.edited = false
373
- })
374
- }
375
-
376
- get endpoint(): string {
377
- return `${this.layout.endpoint}/records/${this.recordId}`
378
- }
379
-
380
- get edited(): boolean {
381
- return !!this.fields.find(i => i.edited)
382
- }
383
-
384
- private processPortalData(portalData): void {
385
- for (let item of Object.keys(portalData)) {
386
- this.portals.push(new portal(this, item, portalData[item].map(item => {
387
- let fieldData = item
388
- delete fieldData.recordId
389
- delete fieldData.modId
390
- return new portalItem(this, item.recordId, item.modId, fieldData)
391
- })))
392
- }
393
- }
394
-
395
- processFieldData(fieldData): field[] {
396
- return Object.keys(fieldData).map(item => {
397
- return new field(this, item, fieldData[item])
398
- })
399
- }
400
-
401
- get() {
402
- if (this.recordId === -1) {throw "Cannot get this record until a commit() is done."}
403
- return new Promise(async (resolve, reject) => {
404
- if (!this.layout.metadata) await this.layout.getLayoutMeta()
405
-
406
- this.layout.database.apiRequest(this.endpoint, {
407
- port: 443,
408
- method: "GET"
409
- })
410
- .then(res => {
411
- if (res.messages[0].code === "0") {
412
- // console.log(res, res.response.data)
413
- this.modId = res.response.data[0].modId
414
- this.fields = this.processFieldData(res.response.data[0].fieldData)
415
- this.portalData = []
416
- if (res.response.data[0].portalData) this.processPortalData(res.response.data[0].portalData)
417
- resolve(this)
418
- } else {
419
- reject(new FMError(res.messages[0].code, res.status, res))
420
- }
421
- })
422
- .catch(e => {
423
- reject(e)
424
- })
425
- })
426
- }
427
-
428
- commit(extraBody = {
429
- fieldData: undefined
430
- }): Promise<record | FMError> {
431
- return new Promise(async (resolve, reject) => {
432
- let data = this.toObject()
433
- delete data.recordId
434
- delete data.modId
435
-
436
- if (this.recordId === -1) {
437
- // This is a new record
438
- extraBody.fieldData = data.fieldData
439
-
440
- this.layout.database.apiRequest(`${this.layout.endpoint}/records`, {
441
- port: 443,
442
- method: "POST",
443
- body: JSON.stringify(extraBody)
444
- })
445
- .then(res => {
446
- if (res.messages[0].code === "0") {
447
- this.recordId = parseInt(res.response.recordId)
448
- this.modId = parseInt(res.response.modId)
449
- resolve(this)
450
- } else {
451
- reject(new FMError(res.messages[0].code, res.status, res))
452
- }
453
- })
454
- .catch(e => {
455
- reject(e)
456
- })
457
-
458
- return
459
- }
460
-
461
- for (let item of Object.keys(extraBody)) data[item] = extraBody[item]
462
- this.layout.database.apiRequest(this.endpoint, {
463
- port: 443,
464
- method: "PATCH",
465
- body: JSON.stringify(data)
466
- })
467
- .then(res => {
468
- if (res.messages[0].code === "0") {
469
- this.modId = res.response.modId
470
- this.emit("saved")
471
- resolve(this)
472
- } else {
473
- reject(new FMError(res.messages[0].code, res.status, res))
474
- }
475
- })
476
- .catch(e => {
477
- reject(e)
478
- })
479
- })
480
- }
481
-
482
- getField(field) {
483
- return this.fields.find(_field => _field.id === field)
484
- }
485
-
486
- getPortal(portal) {
487
- return this.portals.find(p => p.name === portal)
488
- }
489
-
490
- toObject(filter = (a) => a.edited,
491
- portalFilter = (a) => a.records.find(record => record.edited),
492
- portalRowFilter = (a) => a.edited,
493
- portalFieldFilter = (a) => a.edited
494
- ): recordObject {
495
- let fields_processed = {}
496
- for (let field of this.fields.filter(field => filter(field))) {
497
- fields_processed[field.id] = field.value
498
- }
499
- let obj = {
500
- "recordId": this.recordId,
501
- "modId": this.modId,
502
- "fieldData": fields_processed
503
- }
504
-
505
- // Check if there's been any edited portal information
506
- let portals = this.portals.filter(a => portalFilter(a))
507
- if (portals) {
508
- obj["portalData"] = {}
509
- for (let portal of portals) {
510
- obj["portalData"][portal.name] = portal.records.filter(a => portalRowFilter(a)).map(record => {
511
- return record.toObject(portalFieldFilter)
512
- })
513
- }
514
- }
515
- return obj
516
- }
517
- }
518
-
519
- class field {
520
- record: record;
521
- id: number;
522
- value: string | number;
523
- edited: boolean;
524
- constructor(record, id, contents) {
525
- this.record = record
526
- this.id = id
527
- this.value = contents
528
- this.edited = false
529
- }
530
-
531
- set(content) {
532
- if (this.dataType === "container") throw "Cannot set container value using set(). Use upload() instead."
533
- this.value = content
534
- this.edited = true
535
- }
536
-
537
- get dataType() {
538
- if (!this.record.layout.metadata) return "unknown"
539
- if (this.record instanceof portalItem) {
540
- return this.record.layout.metadata.portalMetaData.find(i => i.name === this.id).result || "unknown"
541
- } else {
542
- return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id).result || "unknown"
543
- }
544
- }
545
-
546
- upload(buffer: Buffer, filename: string, mime: string): Promise<void | FMError> {
547
- if (this.dataType !== "container") throw "Cannot upload a file to the field; " + this.id + " (not a container field)"
548
- return new Promise(async (resolve, reject) => {
549
- let form = new FormData()
550
- form.append("upload", new File([buffer], filename, {type: mime}))
551
-
552
- let _fetch = await fetch(`${this.record.endpoint}/containers/${this.id}/1`, {
553
- method: "POST",
554
- headers: {"Authorization": "Bearer " + this.record.layout.database.token},
555
- body: form
556
- }).then(res => res.json())
557
- .then(data => {
558
- let _res = data as any
559
- if (_res.messages[0].code === "0") {
560
- resolve()
561
- } else {
562
- reject(new FMError(_res.messages[0].code, _res.status, data))
563
- }
564
- })
565
- .catch(e => {reject(e)})
566
- })
567
- }
568
- download() {
569
- return new Promise((resolve, reject) => {
570
- https.get(this.value.toString(), (res) => {
571
- resolve(res)
572
- })
573
- })
574
- }
575
- }
576
-
577
- class portal {
578
- readonly record: record;
579
- readonly name: string;
580
- public records: portalItem[];
581
-
582
- constructor(record: record, name: string, records: portalItem[] = []) {
583
- this.record = record
584
- this.name = name
585
- this.records = records
586
- for (let item of this.records) item.attachPortal(this)
587
- }
588
- }
589
-
590
- class portalItem extends record {
591
- protected portal: portal;
592
-
593
- constructor(record, recordId, modId = recordId, fieldData = {}) {
594
- super(record.layout, recordId, modId, fieldData);
595
- record.on("saved", () => {
596
- this.emit("saved")
597
- })
598
- }
599
-
600
- attachPortal(portal) {
601
- this.portal = portal
602
- }
603
-
604
- commit(extraBody = {fieldData: undefined}) {
605
- return this.portal.record.commit(extraBody)
606
- }
607
-
608
- toObject(fieldFilter): any {
609
- super.toObject()
610
- let res = {
611
- recordId: this.recordId, modId: this.modId
612
- } as recordObject
613
- for (let field of this.fields.filter(a => fieldFilter(a))) res[field.id] = field.value
614
- // console.log(res)
615
- return res
616
- }
617
- }
618
-
619
- class find {
620
- protected layout: layout
621
- protected queries: object[]
622
- protected scripts: object
623
- protected sort: object[]
624
-
625
- constructor(layout) {
626
- this.layout = layout
627
- this.queries = []
628
- this.scripts = {
629
- "script": null, // Runs after everything
630
- "script.prerequeset": null, // Runs before the request
631
- "script.presort": null // Runs before the sort
632
- }
633
- this.sort = []
634
- }
635
-
636
- private toObject() {
637
- let out = {query: this.queries, sort: undefined}
638
- if (this.sort.length !== 0) out.sort = this.sort
639
-
640
- for (let item of Object.keys(this.scripts)) {
641
- if (this.scripts[item]) {
642
- out[item] = this.scripts[item].name
643
- if (this.scripts[item].parameter) out[item + ".param"] = this.scripts[item].parameter
644
- }
645
- }
646
-
647
- return out
648
- }
649
-
650
- addRequests(...requests) {
651
- for (let item of requests) this.queries.push(item)
652
- return this
653
- }
654
-
655
- addSort(fieldName, sortOrder) {
656
- this.sort.push({fieldName, sortOrder})
657
- return this
658
- }
659
-
660
- find() {
661
- return new Promise((resolve, reject) => {
662
- // console.log(this.#toObject())
663
- this.layout.database.apiRequest(`${this.layout.endpoint}/_find`, {
664
- port: 443,
665
- method: "POST",
666
- body: JSON.stringify(this.toObject())
667
- }).then(async res => {
668
- // // console.log(res)
669
- if (res.messages[0].code === "0") {
670
- // console.log("RESOLVING")
671
- if (!this.layout.metadata) await this.layout.getLayoutMeta()
672
- let data = res.response.data.map(item => {
673
- return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
674
- })
675
- resolve(data)
676
- } else {
677
- reject(new FMError(res.messages[0].code, res.status, res))
678
- }
679
- })
680
- .catch(e => {
681
- reject(e)
682
- })
683
- })
684
- }
685
- }
686
-
687
- export class FMError extends Error {
688
- readonly httpStatus: number
689
- readonly res: any
690
- readonly code: number
691
- readonly message: string
692
- readonly messages: string
693
-
694
- constructor(code, httpStatus, res) {
695
- if (typeof code === "string") code = parseInt(code)
696
- super(errs.find(err => err.e === code).d || "Unknown error");
697
- this.httpStatus = httpStatus
698
- this.res = res
699
- this.messages = res.messages
700
- this.code = typeof code === "string" ? parseInt(code) : code
701
- Error.captureStackTrace(this, FMError)
702
- }
703
- }
704
-
705
- //
706
- // module.exports = {
707
- // default: {FileMakerConnection}
1
+ /*
2
+ * Copyright (c) 2022-2023. See LICENSE file for more information
3
+ */
4
+
5
+ // const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
6
+ import fetch, {
7
+ Blob,
8
+ blobFrom,
9
+ blobFromSync,
10
+ File,
11
+ fileFrom,
12
+ fileFromSync,
13
+ FormData,
14
+ Headers,
15
+ Request,
16
+ Response
17
+ } from 'node-fetch';
18
+ import { EventEmitter } from "events";
19
+ // @ts-ignore
20
+ import fs from "fs"
21
+ // @ts-ignore
22
+ import path from "path"
23
+ // @ts-ignore
24
+ import { fileURLToPath } from "url"
25
+ import * as https from "https";
26
+ // import * as btoa from "btoa";
27
+
28
+ interface connectionOptions {
29
+ hostname: string | undefined,
30
+ database: databaseOptions
31
+ }
32
+
33
+ interface hostConnectionOptions {
34
+ hostname: string
35
+ database: loginOptionsOAuth | loginOptionsFileMaker | loginOptionsClaris | loginOptionsToken
36
+ }
37
+
38
+ interface databaseOptions {
39
+ method: any
40
+ database: string
41
+ }
42
+
43
+ interface loginOptionsToken extends databaseOptions {
44
+ method: "token"
45
+ token: string
46
+ }
47
+
48
+ interface loginOptionsOAuth extends databaseOptions {
49
+ method: "oauth"
50
+ oauth: {
51
+ requestId: string,
52
+ requestIdentifier: string
53
+ }
54
+ }
55
+
56
+ interface loginOptionsFileMaker extends databaseOptions {
57
+ method: "filemaker"
58
+ username: string,
59
+ password: string,
60
+ }
61
+
62
+ interface loginOptionsClaris extends databaseOptions {
63
+ method: "claris"
64
+ claris: {
65
+ fmid: string
66
+ }
67
+ }
68
+
69
+ // @ts-ignore
70
+ const errs = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'fmErrors.json')).toString())
71
+
72
+ interface Script {
73
+ name: string
74
+ parameter: string
75
+ }
76
+
77
+ interface recordObject {
78
+ recordId: number
79
+ modId: number,
80
+ fieldData: any
81
+ }
82
+
83
+ class database {
84
+ external: boolean;
85
+ public name: string;
86
+
87
+ protected host: FileMakerConnection;
88
+ protected props: databaseOptions;
89
+
90
+ public hostname: string;
91
+
92
+ constructor(props: databaseOptions, host: FileMakerConnection = null) {
93
+ this.props = props
94
+ this.name = this.props.database
95
+ if (host) this.host = host
96
+ }
97
+
98
+ get token() {
99
+ return this.host.token
100
+ }
101
+
102
+ getLayout(name): layout {
103
+ return new layout(this, name)
104
+ }
105
+
106
+ async apiRequest(url: string | Request, options: any = {}): Promise<any> {
107
+ if (!options.headers) options.headers = {}
108
+ options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json"
109
+ options.headers["authorization"] = "Bearer " + this.host.token
110
+ options.rejectUnauthorized = this.host.rejectUnauthroized
111
+
112
+ let _fetch = await fetch(url, options)
113
+ let data = await _fetch.json()
114
+ return (data as any)
115
+ }
116
+
117
+ setGlobals(globalFields): Promise<void> {
118
+ // console.log({globalFields})
119
+ return new Promise((resolve, reject) => {
120
+ this.apiRequest(`${this.endpoint}/globals`, {
121
+ method: "PATCH",
122
+ body: JSON.stringify({globalFields})
123
+ }).then(res => {
124
+ // console.log(res)
125
+ if (res.messages[0].code === "0") {
126
+ resolve()
127
+ } else {
128
+ reject(new FMError(res.messages[0].code, res.status, res))
129
+ }
130
+ })
131
+ .catch(e => {
132
+ reject(e)
133
+ })
134
+ })
135
+ }
136
+
137
+ script(name, parameter): Script {
138
+ return ({name, parameter} as Script)
139
+ }
140
+
141
+ get endpoint(): string {
142
+ return `https://${this.hostname || this.host.hostname}/fmi/data/v2/databases/${encodeURI(this.name)}`
143
+ }
144
+
145
+ get externalLoginInfo() {
146
+ // Returns an object that can be used when using this database object as an external source
147
+ let out = {
148
+ database: this.name,
149
+ username: undefined,
150
+ password: undefined,
151
+ oauthRequestId: undefined,
152
+ oauthIdentifier: undefined
153
+ }
154
+ switch (this.props.method) {
155
+ case "filemaker":
156
+ out.username = (<loginOptionsFileMaker>this.props).username
157
+ out.password = (<loginOptionsFileMaker>this.props).password
158
+ case "token":
159
+ throw "Token logins cannot be used for connecting to external sources. Please either use no login method, or open a second connection."
160
+ case "oauth":
161
+ out.oauthRequestId = (<loginOptionsOAuth>this.props).oauth.requestId
162
+ out.oauthIdentifier = (<loginOptionsOAuth>this.props).oauth.requestIdentifier
163
+
164
+ case "claris":
165
+ throw "Claris logins cannot be used for connecting to external sources. Please either use no login method, or open a second connection."
166
+ }
167
+ return out
168
+ }
169
+ }
170
+
171
+ export default class FileMakerConnection extends database{
172
+ private _token: any;
173
+ private username: any;
174
+ private password: any;
175
+ public readonly rejectUnauthroized: boolean;
176
+ externalSources: database[];
177
+
178
+ constructor(conn: hostConnectionOptions, externalDataSources:databaseOptions[] = [], rejectUnauthorized = true) {
179
+ super(conn.database);
180
+ this.host = this
181
+ this.hostname = conn.hostname
182
+ this.externalSources = externalDataSources.map(i => {let e = new database(i, this); e.external = true; return e;})
183
+ this.rejectUnauthroized = rejectUnauthorized
184
+ }
185
+
186
+ logout(): Promise<void> {
187
+ return new Promise(async (resolve, reject) => {
188
+ if (!this.token) reject(new Error("Not logged in"))
189
+
190
+ let _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
191
+ method: "DELETE",
192
+ headers: {
193
+ "content-type": "application/json"
194
+ }
195
+ })
196
+ let data = await _fetch.json()
197
+ // console.log(data)
198
+ this._token = null
199
+ this.name = null
200
+ this.hostname = null
201
+ resolve()
202
+ })
203
+ }
204
+
205
+ login() {
206
+ return new Promise<string>((resolve, reject) => {
207
+ if (this.token) throw new Error("Already logged in. Run logout() first")
208
+
209
+ if (this.props.method === "filemaker") {
210
+ this.username = (<loginOptionsFileMaker>this.props).username
211
+ this.name = this.props.database
212
+
213
+ fetch(`${this.endpoint}/sessions`, {
214
+ hostname: this.hostname,
215
+ port: 443,
216
+ method: "POST",
217
+ headers: {
218
+ "Content-Type": "application/json",
219
+ "Authorization": "Basic " + Buffer.from(this.username + ":" + (<loginOptionsFileMaker>this.props).password).toString("base64")
220
+ }
221
+ }).then(async res => {
222
+ if (res.status === 200) {
223
+ this._token = res.headers.get('x-fm-data-access-token')
224
+ resolve(this._token)
225
+ } else {
226
+ let _res = <any>(await res.json())
227
+ reject(new FMError(_res.messages[0].code, _res.status, res))
228
+ }
229
+ })
230
+ .catch(e => {
231
+ reject(e)
232
+ })
233
+ }
234
+ else if (this.props.method === "token") {
235
+ this._token = (<loginOptionsToken>this.props).token
236
+ this.name = this.props.database
237
+ resolve(this.token)
238
+ }
239
+ else if (this.props.method === "oauth") {
240
+ this.name = this.props.database
241
+ fetch(`${this.endpoint}/sessions`, {
242
+ method: "POST",
243
+ headers: {
244
+ "Content-Type": "application/json",
245
+ "X-FM-Data-OAuth-RequestId": (<loginOptionsOAuth>this.props).oauth.requestId,
246
+ "X-FM-Data-OAuth-Identifier": (<loginOptionsOAuth>this.props).oauth.requestIdentifier
247
+ },
248
+ body: "{}"
249
+ })
250
+ .then(res => res.json())
251
+ .then(res => {
252
+ let _res = <any>res
253
+ this._token = _res.headers["x-fm-data-access-token"]
254
+ resolve(this.token)
255
+ })
256
+ }
257
+ else if (this.props.method === "claris") {
258
+ this.name = this.props.database
259
+ fetch(`${this.endpoint}/sessions`, {
260
+ method: "POST",
261
+ headers: {
262
+ "Content-Type": "application/json",
263
+ "Authorization": (<loginOptionsClaris>this.props).claris.fmid,
264
+ },
265
+ body: "{}"
266
+ })
267
+ .then(res => res.json())
268
+ .then(res => {
269
+ let _res = <any>res
270
+ this._token = _res.headers["x-fm-data-access-token"]
271
+ resolve(this.token)
272
+ })
273
+ }
274
+ })
275
+ }
276
+
277
+ get token() {
278
+ return this._token
279
+ }
280
+ }
281
+
282
+ class layout {
283
+ readonly database: database;
284
+ protected name: string;
285
+ metadata: any;
286
+ constructor(database: database, name: string) {
287
+ this.database = database
288
+ this.name = name
289
+ }
290
+
291
+ get endpoint() {
292
+ return `${this.database.endpoint}/layouts/${this.name}`
293
+ }
294
+
295
+ getScript(script) {
296
+ return new script(this, script)
297
+ }
298
+
299
+ createRecord(): Promise<record | Error | void> {
300
+ return new Promise((resolve, reject) => {
301
+ // Get the layout's metadata
302
+ this.getLayoutMeta().then(layout => {
303
+ let fields = {}
304
+ for (let _field of this.metadata.fieldMetaData) {
305
+ fields[_field.name] = ""
306
+ }
307
+ resolve(new record(this, -1, 0, fields))
308
+ })
309
+ })
310
+ }
311
+
312
+ getRecord(recordId): record {
313
+ return new record(this, recordId)
314
+ }
315
+
316
+ newFind(): find {
317
+ return new find(this)
318
+ }
319
+
320
+ runScript(script): Promise<string | FMError | Error> {
321
+ return new Promise(async (resolve, reject) => {
322
+ let url = `${this.endpoint}/script/${encodeURI(script.name)}`
323
+ if (script.parameter) url += "?" + encodeURI(script.parameter)
324
+ this.database.apiRequest(url, {
325
+ port: 443,
326
+ method: "GET"
327
+ })
328
+ .then(res => {
329
+ if (res.messages[0].code === "0") {
330
+ resolve(res.response)
331
+ } else {
332
+ // console.log(res)
333
+ reject(new FMError(res.messages[0].code, res.status, res))
334
+ }
335
+ })
336
+ .catch(e => {
337
+ reject(e)
338
+ })
339
+ })
340
+ }
341
+
342
+ public getLayoutMeta(): Promise<layout | FMError> {
343
+ return new Promise((resolve, reject) => {
344
+ this.database.apiRequest(this.endpoint).then(res => {
345
+ this.metadata = res.response
346
+ resolve(this)
347
+ })
348
+ .catch(e => reject(e))
349
+ })
350
+ }
351
+ }
352
+
353
+ class record extends EventEmitter {
354
+ readonly layout: layout;
355
+ public recordId: number;
356
+ modId: number;
357
+ fields: field[];
358
+ readonly portals: portal[];
359
+ private portalData: any[];
360
+
361
+ constructor(layout, recordId, modId = recordId, fieldData = {}, portalData = null) {
362
+ super();
363
+ this.layout = layout
364
+ this.recordId = recordId
365
+ this.modId = modId
366
+ this.fields = this.processFieldData(fieldData)
367
+ this.portals = []
368
+ if (portalData) {
369
+ this.processPortalData(portalData)
370
+ }
371
+ this.on("saved", () => {
372
+ for (let field of this.fields) field.edited = false
373
+ })
374
+ }
375
+
376
+ get endpoint(): string {
377
+ return `${this.layout.endpoint}/records/${this.recordId}`
378
+ }
379
+
380
+ get edited(): boolean {
381
+ return !!this.fields.find(i => i.edited)
382
+ }
383
+
384
+ private processPortalData(portalData): void {
385
+ for (let item of Object.keys(portalData)) {
386
+ this.portals.push(new portal(this, item, portalData[item].map(item => {
387
+ let fieldData = item
388
+ delete fieldData.recordId
389
+ delete fieldData.modId
390
+ return new portalItem(this, item.recordId, item.modId, fieldData)
391
+ })))
392
+ }
393
+ }
394
+
395
+ processFieldData(fieldData): field[] {
396
+ return Object.keys(fieldData).map(item => {
397
+ return new field(this, item, fieldData[item])
398
+ })
399
+ }
400
+
401
+ get() {
402
+ if (this.recordId === -1) {throw "Cannot get this record until a commit() is done."}
403
+ return new Promise(async (resolve, reject) => {
404
+ if (!this.layout.metadata) await this.layout.getLayoutMeta()
405
+
406
+ this.layout.database.apiRequest(this.endpoint, {
407
+ port: 443,
408
+ method: "GET"
409
+ })
410
+ .then(res => {
411
+ if (res.messages[0].code === "0") {
412
+ // console.log(res, res.response.data)
413
+ this.modId = res.response.data[0].modId
414
+ this.fields = this.processFieldData(res.response.data[0].fieldData)
415
+ this.portalData = []
416
+ if (res.response.data[0].portalData) this.processPortalData(res.response.data[0].portalData)
417
+ resolve(this)
418
+ } else {
419
+ reject(new FMError(res.messages[0].code, res.status, res))
420
+ }
421
+ })
422
+ .catch(e => {
423
+ reject(e)
424
+ })
425
+ })
426
+ }
427
+
428
+ commit(extraBody = {
429
+ fieldData: undefined
430
+ }): Promise<record | FMError> {
431
+ return new Promise(async (resolve, reject) => {
432
+ let data = this.toObject()
433
+ delete data.recordId
434
+ delete data.modId
435
+
436
+ if (this.recordId === -1) {
437
+ // This is a new record
438
+ extraBody.fieldData = data.fieldData
439
+
440
+ this.layout.database.apiRequest(`${this.layout.endpoint}/records`, {
441
+ port: 443,
442
+ method: "POST",
443
+ body: JSON.stringify(extraBody)
444
+ })
445
+ .then(res => {
446
+ if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
447
+ reject(new FMError(res.response.scriptError, res.status, res))
448
+ }
449
+ else if (res.messages[0].code === "0") {
450
+ console.log(res)
451
+ this.recordId = parseInt(res.response.recordId)
452
+ this.modId = parseInt(res.response.modId)
453
+ resolve(this)
454
+ } else {
455
+ reject(new FMError(res.messages[0].code, res.status, res))
456
+ }
457
+ })
458
+ sh .catch(e => {
459
+ reject(e)
460
+ })
461
+
462
+ return
463
+ }
464
+
465
+ for (let item of Object.keys(extraBody)) data[item] = extraBody[item]
466
+ this.layout.database.apiRequest(this.endpoint, {
467
+ port: 443,
468
+ method: "PATCH",
469
+ body: JSON.stringify(data)
470
+ })
471
+ .then(res => {
472
+ if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
473
+ reject(new FMError(res.response.scriptError, res.status, res))
474
+ }
475
+ else if (res.messages[0].code === "0") {
476
+ console.log(res)
477
+ this.modId = res.response.modId
478
+ this.emit("saved")
479
+ resolve(this)
480
+ } else {
481
+ reject(new FMError(res.messages[0].code, res.status, res))
482
+ }
483
+ })
484
+ .catch(e => {
485
+ reject(e)
486
+ })
487
+ })
488
+ }
489
+
490
+ getField(field) {
491
+ return this.fields.find(_field => _field.id === field)
492
+ }
493
+
494
+ getPortal(portal) {
495
+ return this.portals.find(p => p.name === portal)
496
+ }
497
+
498
+ toObject(filter = (a) => a.edited,
499
+ portalFilter = (a) => a.records.find(record => record.edited),
500
+ portalRowFilter = (a) => a.edited,
501
+ portalFieldFilter = (a) => a.edited
502
+ ): recordObject {
503
+ let fields_processed = {}
504
+ for (let field of this.fields.filter(field => filter(field))) {
505
+ fields_processed[field.id] = field.value
506
+ }
507
+ let obj = {
508
+ "recordId": this.recordId,
509
+ "modId": this.modId,
510
+ "fieldData": fields_processed
511
+ }
512
+
513
+ // Check if there's been any edited portal information
514
+ let portals = this.portals.filter(a => portalFilter(a))
515
+ if (portals) {
516
+ obj["portalData"] = {}
517
+ for (let portal of portals) {
518
+ obj["portalData"][portal.name] = portal.records.filter(a => portalRowFilter(a)).map(record => {
519
+ return record.toObject(portalFieldFilter)
520
+ })
521
+ }
522
+ }
523
+ return obj
524
+ }
525
+ }
526
+
527
+ class field {
528
+ record: record;
529
+ id: number;
530
+ value: string | number;
531
+ edited: boolean;
532
+ constructor(record, id, contents) {
533
+ this.record = record
534
+ this.id = id
535
+ this.value = contents
536
+ this.edited = false
537
+ }
538
+
539
+ set(content) {
540
+ if (this.dataType === "container") throw "Cannot set container value using set(). Use upload() instead."
541
+ this.value = content
542
+ this.edited = true
543
+ }
544
+
545
+ get dataType() {
546
+ if (!this.record.layout.metadata) return "unknown"
547
+ if (this.record instanceof portalItem) {
548
+ return this.record.layout.metadata.portalMetaData.find(i => i.name === this.id).result || "unknown"
549
+ } else {
550
+ return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id).result || "unknown"
551
+ }
552
+ }
553
+
554
+ upload(buffer: Buffer, filename: string, mime: string): Promise<void | FMError> {
555
+ if (this.dataType !== "container") throw "Cannot upload a file to the field; " + this.id + " (not a container field)"
556
+ return new Promise(async (resolve, reject) => {
557
+ let form = new FormData()
558
+ form.append("upload", new File([buffer], filename, {type: mime}))
559
+
560
+ let _fetch = await fetch(`${this.record.endpoint}/containers/${this.id}/1`, {
561
+ method: "POST",
562
+ headers: {"Authorization": "Bearer " + this.record.layout.database.token},
563
+ body: form
564
+ }).then(res => res.json())
565
+ .then(data => {
566
+ let _res = data as any
567
+ if (_res.messages[0].code === "0") {
568
+ resolve()
569
+ } else {
570
+ reject(new FMError(_res.messages[0].code, _res.status, data))
571
+ }
572
+ })
573
+ .catch(e => {reject(e)})
574
+ })
575
+ }
576
+ download() {
577
+ return new Promise((resolve, reject) => {
578
+ https.get(this.value.toString(), (res) => {
579
+ resolve(res)
580
+ })
581
+ })
582
+ }
583
+ }
584
+
585
+ class portal {
586
+ readonly record: record;
587
+ readonly name: string;
588
+ public records: portalItem[];
589
+
590
+ constructor(record: record, name: string, records: portalItem[] = []) {
591
+ this.record = record
592
+ this.name = name
593
+ this.records = records
594
+ for (let item of this.records) item.attachPortal(this)
595
+ }
596
+ }
597
+
598
+ class portalItem extends record {
599
+ protected portal: portal;
600
+
601
+ constructor(record, recordId, modId = recordId, fieldData = {}) {
602
+ super(record.layout, recordId, modId, fieldData);
603
+ record.on("saved", () => {
604
+ this.emit("saved")
605
+ })
606
+ }
607
+
608
+ attachPortal(portal) {
609
+ this.portal = portal
610
+ }
611
+
612
+ commit(extraBody = {fieldData: undefined}) {
613
+ return this.portal.record.commit(extraBody)
614
+ }
615
+
616
+ toObject(fieldFilter): any {
617
+ super.toObject()
618
+ let res = {
619
+ recordId: this.recordId, modId: this.modId
620
+ } as recordObject
621
+ for (let field of this.fields.filter(a => fieldFilter(a))) res[field.id] = field.value
622
+ // console.log(res)
623
+ return res
624
+ }
625
+ }
626
+
627
+ class find {
628
+ protected layout: layout
629
+ protected queries: object[]
630
+ protected scripts: object
631
+ protected sort: object[]
632
+
633
+ constructor(layout) {
634
+ this.layout = layout
635
+ this.queries = []
636
+ this.scripts = {
637
+ "script": null, // Runs after everything
638
+ "script.prerequeset": null, // Runs before the request
639
+ "script.presort": null // Runs before the sort
640
+ }
641
+ this.sort = []
642
+ }
643
+
644
+ private toObject() {
645
+ let out = {query: this.queries, sort: undefined}
646
+ if (this.sort.length !== 0) out.sort = this.sort
647
+
648
+ for (let item of Object.keys(this.scripts)) {
649
+ if (this.scripts[item]) {
650
+ out[item] = this.scripts[item].name
651
+ if (this.scripts[item].parameter) out[item + ".param"] = this.scripts[item].parameter
652
+ }
653
+ }
654
+
655
+ return out
656
+ }
657
+
658
+ addRequests(...requests) {
659
+ for (let item of requests) this.queries.push(item)
660
+ return this
661
+ }
662
+
663
+ addSort(fieldName, sortOrder) {
664
+ this.sort.push({fieldName, sortOrder})
665
+ return this
666
+ }
667
+
668
+ find() {
669
+ return new Promise((resolve, reject) => {
670
+ // console.log(this.#toObject())
671
+ this.layout.database.apiRequest(`${this.layout.endpoint}/_find`, {
672
+ port: 443,
673
+ method: "POST",
674
+ body: JSON.stringify(this.toObject())
675
+ }).then(async res => {
676
+ // // console.log(res)
677
+ if (res.messages[0].code === "0") {
678
+ // console.log("RESOLVING")
679
+ if (!this.layout.metadata) await this.layout.getLayoutMeta()
680
+ let data = res.response.data.map(item => {
681
+ return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
682
+ })
683
+ resolve(data)
684
+ } else {
685
+ reject(new FMError(res.messages[0].code, res.status, res))
686
+ }
687
+ })
688
+ .catch(e => {
689
+ reject(e)
690
+ })
691
+ })
692
+ }
693
+ }
694
+
695
+ export class FMError extends Error {
696
+ readonly httpStatus: number
697
+ readonly res: any
698
+ readonly code: number
699
+ readonly message: string
700
+ readonly messages: string
701
+
702
+ constructor(code, httpStatus, res) {
703
+ if (typeof code === "string") code = parseInt(code)
704
+ super(errs.find(err => err.e === code).d || "Unknown error");
705
+ this.httpStatus = httpStatus
706
+ this.res = res
707
+ this.messages = res.messages
708
+ this.code = typeof code === "string" ? parseInt(code) : code
709
+ Error.captureStackTrace(this, FMError)
710
+ }
711
+ }
712
+
713
+ //
714
+ // module.exports = {
715
+ // default: {FileMakerConnection}
708
716
  // }