@jd-data-limited/easy-fm 0.0.12 → 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,592 +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
- // @ts-ignore
29
- const errs = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'fmErrors.json')).toString())
30
-
31
- interface Script {
32
- name: string
33
- parameter: string
34
- }
35
-
36
- interface recordObject {
37
- recordId: number
38
- modId: number,
39
- fieldData: any
40
- }
41
-
42
- export default class FileMakerConnection {
43
- public hostname: string;
44
- public database: string;
45
- public token: any;
46
- private username: any;
47
- private password: any;
48
- private rejectUnauthroized: boolean;
49
-
50
- constructor() {}
51
-
52
- get endpoint(): string {
53
- return `https://${this.hostname}/fmi/data/v2/databases/${encodeURI(this.database)}`
54
- }
55
-
56
- async login(hostname, database, username, password, rejectUnauthorized = true) {
57
- if (this.token) throw new Error("Already logged in. Run logout() first")
58
-
59
- this.username = username
60
- this.password = password
61
- this.hostname = hostname
62
- this.database = database
63
- this.rejectUnauthroized = rejectUnauthorized
64
-
65
- return await this.createSession()
66
- }
67
-
68
- logout(): Promise<void> {
69
- return new Promise(async (resolve, reject) => {
70
- if (!this.token) reject(new Error("Not logged in"))
71
-
72
- let _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
73
- method: "DELETE",
74
- headers: {
75
- "content-type": "application/json"
76
- }
77
- })
78
- let data = await _fetch.json()
79
- // console.log(data)
80
- this.token = null
81
- this.database = null
82
- this.hostname = null
83
- resolve()
84
- })
85
- }
86
-
87
- importSession(hostname, database, token, rejectUnauthorized = true) {
88
- this.token = token
89
- this.database = database
90
- this.hostname = hostname
91
- this.rejectUnauthroized = rejectUnauthorized
92
- }
93
-
94
- private createSession(): Promise<string> {
95
- return new Promise(async (resolve, reject) => {
96
- fetch(`${this.endpoint}/sessions`, {
97
- hostname: this.hostname,
98
- port: 443,
99
- method: "POST",
100
- headers: {
101
- "Content-Type": "application/json",
102
- "Authorization": "Basic " + Buffer.from(this.username + ":" + this.password).toString("base64")
103
- }
104
- }).then(res => {
105
- console.log(res)
106
- return res.json()
107
- })
108
- .then(res => {
109
- let _res = res as any
110
- if (_res.messages[0].code === "0") {
111
- this.token = _res.response.token
112
- console.log(_res)
113
- resolve(this.token)
114
- } else {
115
- reject(new FMError(_res.messages[0].code, _res.status, res))
116
- }
117
- })
118
- .catch(e => {
119
- reject(e)
120
- })
121
- })
122
- }
123
-
124
- getLayout(name): layout {
125
- return new layout(this, name)
126
- }
127
-
128
- async apiRequest(url: string | Request, options: any = {}): Promise<any> {
129
- if (!options.headers) options.headers = {}
130
- options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json"
131
- options.headers["authorization"] = "Bearer " + this.token
132
- options.rejectUnauthorized = this.rejectUnauthroized
133
-
134
- let _fetch = await fetch(url, options)
135
- let data = await _fetch.json()
136
- return (data as any)
137
- }
138
-
139
- setGlobals(globalFields): Promise<void> {
140
- // console.log({globalFields})
141
- return new Promise((resolve, reject) => {
142
- this.apiRequest(`${this.endpoint}/globals`, {
143
- method: "PATCH",
144
- body: JSON.stringify({globalFields})
145
- }).then(res => {
146
- // console.log(res)
147
- if (res.messages[0].code === "0") {
148
- resolve()
149
- } else {
150
- reject(new FMError(res.messages[0].code, res.status, res))
151
- }
152
- })
153
- .catch(e => {
154
- reject(e)
155
- })
156
- })
157
- }
158
-
159
- script(name, parameter): Script {
160
- return ({name, parameter} as Script)
161
- }
162
- }
163
-
164
- class layout {
165
- readonly conn: FileMakerConnection;
166
- protected name: string;
167
- metadata: any;
168
- constructor(conn: FileMakerConnection, name: string) {
169
- this.conn = conn
170
- this.name = name
171
- }
172
-
173
- get endpoint() {
174
- return `${this.conn.endpoint}/layouts/${this.name}`
175
- }
176
-
177
- getScript(script) {
178
- return new script(this, script)
179
- }
180
-
181
- createRecord(): Promise<record | Error | void> {
182
- return new Promise((resolve, reject) => {
183
- // Get the layout's metadata
184
- this.getLayoutMeta().then(layout => {
185
- let fields = {}
186
- for (let _field of this.metadata.fieldMetaData) {
187
- fields[_field.name] = ""
188
- }
189
- console.log(fields)
190
- resolve(new record(this, -1, 0, fields))
191
- })
192
- })
193
- }
194
-
195
- getRecord(recordId): record {
196
- return new record(this, recordId)
197
- }
198
-
199
- newFind(): find {
200
- return new find(this)
201
- }
202
-
203
- runScript(script): Promise<string | FMError | Error> {
204
- return new Promise(async (resolve, reject) => {
205
- let url = `${this.endpoint}/script/${encodeURI(script.name)}`
206
- if (script.parameter) url += "?" + encodeURI(script.parameter)
207
- this.conn.apiRequest(url, {
208
- port: 443,
209
- method: "GET"
210
- })
211
- .then(res => {
212
- if (res.messages[0].code === "0") {
213
- resolve(res.response)
214
- } else {
215
- // console.log(res)
216
- reject(new FMError(res.messages[0].code, res.status, res))
217
- }
218
- })
219
- .catch(e => {
220
- reject(e)
221
- })
222
- })
223
- }
224
-
225
- public getLayoutMeta(): Promise<layout | FMError> {
226
- return new Promise((resolve, reject) => {
227
- this.conn.apiRequest(this.endpoint).then(res => {
228
- this.metadata = res.response
229
- resolve(this)
230
- })
231
- .catch(e => reject(e))
232
- })
233
- }
234
- }
235
-
236
- class record extends EventEmitter {
237
- readonly layout: layout;
238
- public recordId: number;
239
- modId: number;
240
- fields: field[];
241
- readonly portals: portal[];
242
- private portalData: any[];
243
-
244
- constructor(layout, recordId, modId = recordId, fieldData = {}, portalData = null) {
245
- super();
246
- this.layout = layout
247
- this.recordId = recordId
248
- this.modId = modId
249
- this.fields = this.processFieldData(fieldData)
250
- this.portals = []
251
- if (portalData) {
252
- this.processPortalData(portalData)
253
- }
254
- this.on("saved", () => {
255
- for (let field of this.fields) field.edited = false
256
- })
257
- }
258
-
259
- get endpoint(): string {
260
- return `${this.layout.endpoint}/records/${this.recordId}`
261
- }
262
-
263
- get edited(): boolean {
264
- return !!this.fields.find(i => i.edited)
265
- }
266
-
267
- private processPortalData(portalData): void {
268
- for (let item of Object.keys(portalData)) {
269
- this.portals.push(new portal(this, item, portalData[item].map(item => {
270
- let fieldData = item
271
- delete fieldData.recordId
272
- delete fieldData.modId
273
- return new portalItem(this, item.recordId, item.modId, fieldData)
274
- })))
275
- }
276
- }
277
-
278
- processFieldData(fieldData): field[] {
279
- return Object.keys(fieldData).map(item => {
280
- return new field(this, item, fieldData[item])
281
- })
282
- }
283
-
284
- get() {
285
- if (this.recordId === -1) {throw "Cannot get this record until a commit() is done."}
286
- return new Promise(async (resolve, reject) => {
287
- if (!this.layout.metadata) await this.layout.getLayoutMeta()
288
-
289
- this.layout.conn.apiRequest(this.endpoint, {
290
- port: 443,
291
- method: "GET"
292
- })
293
- .then(res => {
294
- if (res.messages[0].code === "0") {
295
- // console.log(res, res.response.data)
296
- this.modId = res.response.data[0].modId
297
- this.fields = this.processFieldData(res.response.data[0].fieldData)
298
- this.portalData = []
299
- if (res.response.data[0].portalData) this.processPortalData(res.response.data[0].portalData)
300
- resolve(this)
301
- } else {
302
- reject(new FMError(res.messages[0].code, res.status, res))
303
- }
304
- })
305
- .catch(e => {
306
- reject(e)
307
- })
308
- })
309
- }
310
-
311
- commit(extraBody = {
312
- fieldData: undefined
313
- }): Promise<record | FMError> {
314
- return new Promise(async (resolve, reject) => {
315
- let data = this.toObject()
316
- delete data.recordId
317
- delete data.modId
318
-
319
- if (this.recordId === -1) {
320
- // This is a new record
321
- extraBody.fieldData = data.fieldData
322
-
323
- this.layout.conn.apiRequest(`${this.layout.endpoint}/records`, {
324
- port: 443,
325
- method: "POST",
326
- body: JSON.stringify(extraBody)
327
- })
328
- .then(res => {
329
- if (res.messages[0].code === "0") {
330
- this.recordId = parseInt(res.response.recordId)
331
- console.log(this.recordId)
332
- this.modId = parseInt(res.response.modId)
333
- resolve(this)
334
- } else {
335
- reject(new FMError(res.messages[0].code, res.status, res))
336
- }
337
- })
338
- .catch(e => {
339
- reject(e)
340
- })
341
-
342
- return
343
- }
344
-
345
- for (let item of Object.keys(extraBody)) data[item] = extraBody[item]
346
- this.layout.conn.apiRequest(this.endpoint, {
347
- port: 443,
348
- method: "PATCH",
349
- body: JSON.stringify(data)
350
- })
351
- .then(res => {
352
- if (res.messages[0].code === "0") {
353
- this.modId = res.response.modId
354
- this.emit("saved")
355
- resolve(this)
356
- } else {
357
- reject(new FMError(res.messages[0].code, res.status, res))
358
- }
359
- })
360
- .catch(e => {
361
- reject(e)
362
- })
363
- })
364
- }
365
-
366
- getField(field) {
367
- return this.fields.find(_field => _field.id === field)
368
- }
369
-
370
- getPortal(portal) {
371
- return this.portals.find(p => p.name === portal)
372
- }
373
-
374
- toObject(filter = (a) => a.edited,
375
- portalFilter = (a) => a.records.find(record => record.edited),
376
- portalRowFilter = (a) => a.edited,
377
- portalFieldFilter = (a) => a.edited
378
- ): recordObject {
379
- let fields_processed = {}
380
- for (let field of this.fields.filter(field => filter(field))) {
381
- fields_processed[field.id] = field.value
382
- }
383
- let obj = {
384
- "recordId": this.recordId,
385
- "modId": this.modId,
386
- "fieldData": fields_processed
387
- }
388
-
389
- // Check if there's been any edited portal information
390
- let portals = this.portals.filter(a => portalFilter(a))
391
- if (portals) {
392
- obj["portalData"] = {}
393
- for (let portal of portals) {
394
- obj["portalData"][portal.name] = portal.records.filter(a => portalRowFilter(a)).map(record => {
395
- return record.toObject(portalFieldFilter)
396
- })
397
- }
398
- }
399
- return obj
400
- }
401
- }
402
-
403
- class field {
404
- record: record;
405
- id: number;
406
- value: string | number;
407
- edited: boolean;
408
- constructor(record, id, contents) {
409
- this.record = record
410
- this.id = id
411
- this.value = contents
412
- this.edited = false
413
- }
414
-
415
- set(content) {
416
- if (this.dataType === "container") throw "Cannot set container value using set(). Use upload() instead."
417
- this.value = content
418
- this.edited = true
419
- }
420
-
421
- get dataType() {
422
- if (!this.record.layout.metadata) return "unknown"
423
- if (this.record instanceof portalItem) {
424
- return this.record.layout.metadata.portalMetaData.find(i => i.name === this.id).result || "unknown"
425
- } else {
426
- return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id).result || "unknown"
427
- }
428
- }
429
-
430
- upload(buffer: Buffer, filename: string, mime: string): Promise<void | FMError> {
431
- if (this.dataType !== "container") throw "Cannot upload a file to the field; " + this.id + " (not a container field)"
432
- return new Promise(async (resolve, reject) => {
433
- let form = new FormData()
434
- form.append("upload", new File([buffer], filename, {type: mime}))
435
-
436
- let _fetch = await fetch(`${this.record.endpoint}/containers/${this.id}/1`, {
437
- method: "POST",
438
- headers: {"Authorization": "Bearer " + this.record.layout.conn.token},
439
- body: form
440
- }).then(res => res.json())
441
- .then(data => {
442
- let _res = data as any
443
- if (_res.messages[0].code === "0") {
444
- resolve()
445
- } else {
446
- reject(new FMError(_res.messages[0].code, _res.status, data))
447
- }
448
- })
449
- .catch(e => {reject(e)})
450
- })
451
- }
452
- download() {
453
- return new Promise((resolve, reject) => {
454
- https.get(this.value.toString(), (res) => {
455
- resolve(res)
456
- })
457
- })
458
- }
459
- }
460
-
461
- class portal {
462
- readonly record: record;
463
- readonly name: string;
464
- public records: portalItem[];
465
-
466
- constructor(record: record, name: string, records: portalItem[] = []) {
467
- this.record = record
468
- this.name = name
469
- this.records = records
470
- for (let item of this.records) item.attachPortal(this)
471
- }
472
- }
473
-
474
- class portalItem extends record {
475
- protected portal: portal;
476
-
477
- constructor(record, recordId, modId = recordId, fieldData = {}) {
478
- super(record.layout, recordId, modId, fieldData);
479
- record.on("saved", () => {
480
- this.emit("saved")
481
- })
482
- }
483
-
484
- attachPortal(portal) {
485
- this.portal = portal
486
- }
487
-
488
- commit(extraBody = {fieldData: undefined}) {
489
- return this.portal.record.commit(extraBody)
490
- }
491
-
492
- toObject(fieldFilter): any {
493
- super.toObject()
494
- let res = {
495
- recordId: this.recordId, modId: this.modId
496
- } as recordObject
497
- for (let field of this.fields.filter(a => fieldFilter(a))) res[field.id] = field.value
498
- // console.log(res)
499
- return res
500
- }
501
- }
502
-
503
- class find {
504
- protected layout: layout
505
- protected queries: object[]
506
- protected scripts: object
507
- protected sort: object[]
508
-
509
- constructor(layout) {
510
- this.layout = layout
511
- this.queries = []
512
- this.scripts = {
513
- "script": null, // Runs after everything
514
- "script.prerequeset": null, // Runs before the request
515
- "script.presort": null // Runs before the sort
516
- }
517
- this.sort = []
518
- }
519
-
520
- private toObject() {
521
- let out = {query: this.queries, sort: undefined}
522
- if (this.sort.length !== 0) out.sort = this.sort
523
-
524
- for (let item of Object.keys(this.scripts)) {
525
- if (this.scripts[item]) {
526
- out[item] = this.scripts[item].name
527
- if (this.scripts[item].parameter) out[item + ".param"] = this.scripts[item].parameter
528
- }
529
- }
530
-
531
- return out
532
- }
533
-
534
- addRequests(...requests) {
535
- for (let item of requests) this.queries.push(item)
536
- return this
537
- }
538
-
539
- addSort(fieldName, sortOrder) {
540
- this.sort.push({fieldName, sortOrder})
541
- return this
542
- }
543
-
544
- find() {
545
- return new Promise((resolve, reject) => {
546
- // console.log(this.#toObject())
547
- this.layout.conn.apiRequest(`${this.layout.endpoint}/_find`, {
548
- port: 443,
549
- method: "POST",
550
- body: JSON.stringify(this.toObject())
551
- }).then(async res => {
552
- // // console.log(res)
553
- if (res.messages[0].code === "0") {
554
- // console.log("RESOLVING")
555
- if (!this.layout.metadata) await this.layout.getLayoutMeta()
556
- let data = res.response.data.map(item => {
557
- return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
558
- })
559
- resolve(data)
560
- } else {
561
- reject(new FMError(res.messages[0].code, res.status, res))
562
- }
563
- })
564
- .catch(e => {
565
- reject(e)
566
- })
567
- })
568
- }
569
- }
570
-
571
- export class FMError extends Error {
572
- readonly httpStatus: number
573
- readonly res: any
574
- readonly code: number
575
- readonly message: string
576
- readonly messages: string
577
-
578
- constructor(code, httpStatus, res) {
579
- if (typeof code === "string") code = parseInt(code)
580
- super(errs.find(err => err.e === code).d || "Unknown error");
581
- this.httpStatus = httpStatus
582
- this.res = res
583
- this.messages = res.messages
584
- this.code = typeof code === "string" ? parseInt(code) : code
585
- Error.captureStackTrace(this, FMError)
586
- }
587
- }
588
-
589
- //
590
- // module.exports = {
591
- // 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}
592
716
  // }