@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/index.js CHANGED
@@ -20,21 +20,73 @@ import path from "path";
20
20
  // @ts-ignore
21
21
  import { fileURLToPath } from "url";
22
22
  import * as https from "https";
23
+ import * as moment from "moment";
23
24
  // @ts-ignore
24
25
  const errs = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'fmErrors.json')).toString());
25
- export default class FileMakerConnection extends EventEmitter {
26
- constructor(conn, rejectUnauthorized = true) {
26
+ export default class FMHost {
27
+ constructor(_hostname, timezoneOffset = 0 - (new Date()).getTimezoneOffset(), verify = true) {
28
+ if (!(/^https?:\/\//).test(_hostname))
29
+ throw "hostname MUST begin with either http:// or https://";
30
+ this.hostname = _hostname;
31
+ this.timezoneOffset = timezoneOffset;
32
+ this.verify = verify;
33
+ }
34
+ listDatabases(credentials) {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ let headers = {};
37
+ if (credentials) {
38
+ headers = generateAuthorizationHeaders(credentials);
39
+ }
40
+ let _fetch = yield fetch(`${this.hostname}/fmi/data/v2/databases`, {
41
+ method: "GET",
42
+ headers
43
+ });
44
+ let data = yield _fetch.json();
45
+ // console.log(data.messages[0])
46
+ if (data.messages[0].code === "0") {
47
+ return data.response.databases;
48
+ }
49
+ else {
50
+ // @ts-ignore
51
+ throw new FMError(data.messages[0].code, data.status, data);
52
+ }
53
+ });
54
+ }
55
+ database(data) {
56
+ return new Database(this, data);
57
+ }
58
+ getMetadata() {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ if (this.metadata)
61
+ return this.metadata;
62
+ let _fetch = yield fetch(`${this.hostname}/fmi/data/v2/productInfo`, {
63
+ method: "GET",
64
+ });
65
+ let data = yield _fetch.json();
66
+ // console.log(data.messages[0])
67
+ if (data.messages[0].code === "0") {
68
+ this.metadata = data.response;
69
+ return data.response;
70
+ }
71
+ else {
72
+ // @ts-ignore
73
+ throw new FMError(data.messages[0].code, data.status, data);
74
+ }
75
+ });
76
+ }
77
+ }
78
+ export class Database extends EventEmitter {
79
+ constructor(host, conn) {
27
80
  super();
28
- this.hostname = conn.hostname;
29
- this.name = conn.database.database;
30
- this.databaseConDetails = conn;
31
- this.rejectUnauthroized = rejectUnauthorized;
81
+ this.host = host;
82
+ this.name = conn.database;
83
+ this.connection_details = conn;
32
84
  }
33
85
  generateExternalSourceLogin(data) {
34
- if (data.method === "filemaker") {
35
- let _data = data;
86
+ if (data.credentials.method === "filemaker") {
87
+ let _data = data.credentials;
36
88
  return {
37
- database: _data.database,
89
+ database: data.database,
38
90
  username: _data.username,
39
91
  password: _data.password
40
92
  };
@@ -56,36 +108,43 @@ export default class FileMakerConnection extends EventEmitter {
56
108
  let data = yield _fetch.json();
57
109
  // console.log(data)
58
110
  this._token = null;
59
- this.hostname = null;
60
111
  resolve();
61
112
  }));
62
113
  }
63
114
  login() {
64
- return new Promise((resolve, reject) => {
115
+ return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
116
+ try {
117
+ yield this.host.getMetadata();
118
+ }
119
+ catch (e) {
120
+ reject(e);
121
+ return;
122
+ }
65
123
  if (this.token)
66
124
  throw new Error("Already logged in. Run logout() first");
67
- if (this.databaseConDetails.database.method === "filemaker") {
125
+ if (this.connection_details.credentials.method === "token") {
126
+ this._token = this.connection_details.credentials.token;
127
+ resolve(this.token);
128
+ }
129
+ else {
68
130
  fetch(`${this.endpoint}/sessions`, {
69
- hostname: this.hostname,
131
+ hostname: this.host.hostname,
70
132
  port: 443,
71
133
  method: "POST",
72
- headers: {
73
- "Content-Type": "application/json",
74
- "Authorization": "Basic " + Buffer.from(this.databaseConDetails.database.username + ":" + this.databaseConDetails.database.password).toString("base64")
75
- },
134
+ headers: generateAuthorizationHeaders(this.connection_details.credentials),
76
135
  body: JSON.stringify({
77
- fmDataSource: this.databaseConDetails.externalSources.map(i => {
136
+ fmDataSource: this.connection_details.externalSources.map(i => {
78
137
  let _i = i;
79
138
  return this.generateExternalSourceLogin(_i);
80
139
  })
81
140
  })
82
141
  }).then((res) => __awaiter(this, void 0, void 0, function* () {
142
+ let _res = (yield res.json());
83
143
  if (res.status === 200) {
84
144
  this._token = res.headers.get('x-fm-data-access-token');
85
145
  resolve(this._token);
86
146
  }
87
147
  else {
88
- let _res = (yield res.json());
89
148
  reject(new FMError(_res.messages[0].code, _res.status, res));
90
149
  }
91
150
  }))
@@ -93,55 +152,13 @@ export default class FileMakerConnection extends EventEmitter {
93
152
  reject(e);
94
153
  });
95
154
  }
96
- else if (this.databaseConDetails.database.method === "token") {
97
- this._token = this.databaseConDetails.database.token;
98
- resolve(this.token);
99
- }
100
- else if (this.databaseConDetails.database.method === "oauth") {
101
- fetch(`${this.endpoint}/sessions`, {
102
- method: "POST",
103
- headers: {
104
- "Content-Type": "application/json",
105
- "X-FM-Data-OAuth-RequestId": this.databaseConDetails.database.oauth.requestId,
106
- "X-FM-Data-OAuth-Identifier": this.databaseConDetails.database.oauth.requestIdentifier
107
- },
108
- body: JSON.stringify({
109
- fmDataSource: this.databaseConDetails.externalSources.map(i => {
110
- let _i = i;
111
- return this.generateExternalSourceLogin(_i);
112
- })
113
- })
114
- })
115
- .then(res => res.json())
116
- .then(res => {
117
- let _res = res;
118
- this._token = _res.headers["x-fm-data-access-token"];
119
- resolve(this.token);
120
- });
121
- }
122
- else if (this.databaseConDetails.database.method === "claris") {
123
- fetch(`${this.endpoint}/sessions`, {
124
- method: "POST",
125
- headers: {
126
- "Content-Type": "application/json",
127
- "Authorization": this.databaseConDetails.database.claris.fmid,
128
- },
129
- body: "{}"
130
- })
131
- .then(res => res.json())
132
- .then(res => {
133
- let _res = res;
134
- this._token = _res.headers["x-fm-data-access-token"];
135
- resolve(this.token);
136
- });
137
- }
138
- });
155
+ }));
139
156
  }
140
157
  get token() {
141
158
  return this._token;
142
159
  }
143
160
  get endpoint() {
144
- return `https://${this.hostname}/fmi/data/v2/databases/${this.name}`;
161
+ return `${this.host.hostname}/fmi/data/v2/databases/${this.name}`;
145
162
  }
146
163
  apiRequest(url, options = {}, autoRelogin = true) {
147
164
  return __awaiter(this, void 0, void 0, function* () {
@@ -149,10 +166,10 @@ export default class FileMakerConnection extends EventEmitter {
149
166
  options.headers = {};
150
167
  options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json";
151
168
  options.headers["authorization"] = "Bearer " + this._token;
152
- options.rejectUnauthorized = this.rejectUnauthroized;
169
+ options.rejectUnauthorized = this.host.verify;
153
170
  let _fetch = yield fetch(url, options);
154
171
  let data = yield _fetch.json();
155
- console.log(data.messages[0]);
172
+ // console.log(data.messages[0])
156
173
  if (data.messages[0].code == 952 && autoRelogin) {
157
174
  this._token = null;
158
175
  yield this.login();
@@ -193,32 +210,30 @@ export default class FileMakerConnection extends EventEmitter {
193
210
  }
194
211
  class layout {
195
212
  constructor(database, name) {
213
+ this.records = new layoutRecordManager(this);
196
214
  this.database = database;
197
215
  this.name = name;
198
216
  }
199
217
  get endpoint() {
200
218
  return `${this.database.endpoint}/layouts/${this.name}`;
201
219
  }
202
- getScript(script) {
203
- return new script(this, script);
204
- }
220
+ /**
221
+ * @deprecated use layout.records.create() instead
222
+ */
205
223
  createRecord() {
206
- return new Promise((resolve, reject) => {
207
- // Get the layout's metadata
208
- this.getLayoutMeta().then(layout => {
209
- let fields = {};
210
- for (let _field of this.metadata.fieldMetaData) {
211
- fields[_field.name] = "";
212
- }
213
- resolve(new record(this, -1, 0, fields));
214
- }).catch(e => { reject(e); });
215
- });
224
+ return this.records.create();
216
225
  }
226
+ /**
227
+ * @deprecated use layout.records.get() instead
228
+ */
217
229
  getRecord(recordId) {
218
- return new record(this, recordId);
230
+ return this.records.get(recordId);
219
231
  }
232
+ /**
233
+ * @deprecated use layout.records.find() instead
234
+ */
220
235
  newFind() {
221
- return new find(this);
236
+ return this.records.find();
222
237
  }
223
238
  runScript(script) {
224
239
  return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
@@ -245,6 +260,10 @@ class layout {
245
260
  }
246
261
  getLayoutMeta() {
247
262
  return new Promise((resolve, reject) => {
263
+ if (this.metadata) {
264
+ resolve(this.metadata);
265
+ return;
266
+ }
248
267
  this.database.apiRequest(this.endpoint).then(res => {
249
268
  this.metadata = res.response;
250
269
  resolve(this);
@@ -253,6 +272,47 @@ class layout {
253
272
  });
254
273
  }
255
274
  }
275
+ class layoutRecordManager {
276
+ constructor(layout) {
277
+ this.layout = layout;
278
+ }
279
+ create() {
280
+ return new Promise((resolve, reject) => {
281
+ // Get the layout's metadata
282
+ this.layout.getLayoutMeta().then(layout => {
283
+ let fields = {};
284
+ for (let _field of this.layout.metadata.fieldMetaData) {
285
+ fields[_field.name] = "";
286
+ }
287
+ resolve(new record(this, -1, 0, fields));
288
+ }).catch(e => {
289
+ reject(e);
290
+ });
291
+ });
292
+ }
293
+ get(recordId) {
294
+ return new Promise((resolve, reject) => {
295
+ let record;
296
+ this.layout.getLayoutMeta()
297
+ .then(layout => {
298
+ record = new record(this.layout, recordId);
299
+ return record.get();
300
+ })
301
+ .then(() => {
302
+ resolve(record);
303
+ })
304
+ .catch(e => {
305
+ reject(e);
306
+ });
307
+ });
308
+ }
309
+ range() {
310
+ return new recordGetRange(this.layout);
311
+ }
312
+ find() {
313
+ return new find(this.layout);
314
+ }
315
+ }
256
316
  class record extends EventEmitter {
257
317
  constructor(layout, recordId, modId = recordId, fieldData = {}, portalData = null) {
258
318
  super();
@@ -277,17 +337,46 @@ class record extends EventEmitter {
277
337
  }
278
338
  processPortalData(portalData) {
279
339
  for (let item of Object.keys(portalData)) {
280
- this.portals.push(new portal(this, item, portalData[item].map(item => {
340
+ let _portal = new portal(this, item);
341
+ _portal.records = portalData[item].map(item => {
281
342
  let fieldData = item;
282
343
  delete fieldData.recordId;
283
344
  delete fieldData.modId;
284
- return new portalItem(this, item.recordId, item.modId, fieldData);
285
- })));
345
+ return new portalItem(this, _portal, item.recordId, item.modId, fieldData);
346
+ });
347
+ this.portals.push();
286
348
  }
287
349
  }
288
350
  processFieldData(fieldData) {
289
351
  return Object.keys(fieldData).map(item => {
290
- return new field(this, item, fieldData[item]);
352
+ let _field = new field(this, item, fieldData[item]);
353
+ if (!!fieldData[item]) {
354
+ if (_field.metadata.result === "timeStamp") {
355
+ // @ts-ignore
356
+ let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.timeStampFormat)
357
+ .utcOffset(this.layout.database.host.timezoneOffset, true)
358
+ .local();
359
+ _field.set(date.toDate());
360
+ _field.edited = false;
361
+ }
362
+ else if (_field.metadata.result === "time") {
363
+ // @ts-ignore
364
+ let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.timeFormat)
365
+ .utcOffset(this.layout.database.host.timezoneOffset, true)
366
+ .local();
367
+ _field.set(date.toDate());
368
+ _field.edited = false;
369
+ }
370
+ else if (_field.metadata.result === "date") {
371
+ // @ts-ignore
372
+ let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.dateFormat)
373
+ .utcOffset(this.layout.database.host.timezoneOffset, true)
374
+ .local();
375
+ _field.set(date.toDate());
376
+ _field.edited = false;
377
+ }
378
+ }
379
+ return _field;
291
380
  });
292
381
  }
293
382
  get() {
@@ -340,7 +429,6 @@ class record extends EventEmitter {
340
429
  reject(new FMError(res.response.scriptError, res.status, res));
341
430
  }
342
431
  else if (res.messages[0].code === "0") {
343
- console.log(res);
344
432
  this.recordId = parseInt(res.response.recordId);
345
433
  this.modId = parseInt(res.response.modId);
346
434
  resolve(this);
@@ -354,19 +442,18 @@ class record extends EventEmitter {
354
442
  });
355
443
  return;
356
444
  }
357
- for (let item of Object.keys(extraBody))
358
- data[item] = extraBody[item];
445
+ for (let item of Object.keys(data))
446
+ extraBody[item] = data[item];
359
447
  this.layout.database.apiRequest(this.endpoint, {
360
448
  port: 443,
361
449
  method: "PATCH",
362
- body: JSON.stringify(data)
450
+ body: JSON.stringify(extraBody)
363
451
  })
364
452
  .then(res => {
365
453
  if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
366
454
  reject(new FMError(res.response.scriptError, res.status, res));
367
455
  }
368
456
  else if (res.messages[0].code === "0") {
369
- console.log(res);
370
457
  this.modId = res.response.modId;
371
458
  this.emit("saved");
372
459
  resolve(this);
@@ -437,7 +524,24 @@ class record extends EventEmitter {
437
524
  toObject(filter = (a) => a.edited, portalFilter = (a) => a.records.find(record => record.edited), portalRowFilter = (a) => a.edited, portalFieldFilter = (a) => a.edited) {
438
525
  let fields_processed = {};
439
526
  for (let field of this.fields.filter(field => filter(field))) {
440
- fields_processed[field.id] = field.value;
527
+ let value = field.value;
528
+ if (value instanceof Date) {
529
+ // @ts-ignore
530
+ let _value = moment.default(value)
531
+ .utcOffset(this.layout.database.host.timezoneOffset);
532
+ // @ts-ignore
533
+ switch (field.metadata.result) {
534
+ case "time":
535
+ value = _value.format(this.layout.database.host.metadata.productInfo.timeFormat.replace("dd", "DD"));
536
+ break;
537
+ case "date":
538
+ value = _value.format(this.layout.database.host.metadata.productInfo.dateFormat.replace("dd", "DD"));
539
+ break;
540
+ default:
541
+ value = _value.format(this.layout.database.host.metadata.productInfo.timeStampFormat.replace("dd", "DD"));
542
+ }
543
+ }
544
+ fields_processed[field.id] = value;
441
545
  }
442
546
  let obj = {
443
547
  "recordId": this.recordId,
@@ -461,27 +565,83 @@ class field {
461
565
  constructor(record, id, contents) {
462
566
  this.record = record;
463
567
  this.id = id;
464
- this.value = contents;
568
+ this._value = contents;
465
569
  this.edited = false;
466
570
  }
467
571
  set(content) {
468
- if (this.dataType === "container")
572
+ if (this.metadata.result === "container")
469
573
  throw "Cannot set container value using set(). Use upload() instead.";
470
- this.value = content;
574
+ if ((this.metadata.result === "timeStamp" ||
575
+ this.metadata.result === "date" ||
576
+ this.metadata.result === "time")
577
+ && !(content instanceof Date) && !!content) {
578
+ throw "Value was not an instance of Date: " + content;
579
+ }
580
+ this._value = content;
471
581
  this.edited = true;
472
582
  }
473
- get dataType() {
474
- if (!this.record.layout.metadata)
475
- return "unknown";
583
+ get metadata() {
584
+ if (!this.record.layout.metadata) {
585
+ // Default to a regular text field
586
+ return {
587
+ name: this.id.toString(),
588
+ type: 'normal',
589
+ displayType: 'editText',
590
+ result: 'text',
591
+ global: false,
592
+ autoEnter: true,
593
+ fourDigitYear: false,
594
+ maxRepeat: 1,
595
+ maxCharacters: 0,
596
+ notEmpty: false,
597
+ numeric: false,
598
+ timeOfDay: false,
599
+ repetitionStart: 1,
600
+ repetitionEnd: 1
601
+ };
602
+ }
476
603
  if (this.record instanceof portalItem) {
477
- return this.record.layout.metadata.portalMetaData.find(i => i.name === this.id).result || "unknown";
604
+ return this.record.layout.metadata.portalMetaData[this.record.portal.name || "portal not attached"].find(i => i.name === this.id) || {
605
+ name: this.id.toString(),
606
+ type: 'normal',
607
+ displayType: 'editText',
608
+ result: 'text',
609
+ global: false,
610
+ autoEnter: true,
611
+ fourDigitYear: false,
612
+ maxRepeat: 1,
613
+ maxCharacters: 0,
614
+ notEmpty: false,
615
+ numeric: false,
616
+ timeOfDay: false,
617
+ repetitionStart: 1,
618
+ repetitionEnd: 1
619
+ };
478
620
  }
479
621
  else {
480
- return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id).result || "unknown";
622
+ return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id) || {
623
+ name: this.id.toString(),
624
+ type: 'normal',
625
+ displayType: 'editText',
626
+ result: 'text',
627
+ global: false,
628
+ autoEnter: true,
629
+ fourDigitYear: false,
630
+ maxRepeat: 1,
631
+ maxCharacters: 0,
632
+ notEmpty: false,
633
+ numeric: false,
634
+ timeOfDay: false,
635
+ repetitionStart: 1,
636
+ repetitionEnd: 1
637
+ };
481
638
  }
482
639
  }
640
+ get value() {
641
+ return this._value;
642
+ }
483
643
  upload(buffer, filename, mime) {
484
- if (this.dataType !== "container")
644
+ if (this.metadata.result !== "container")
485
645
  throw "Cannot upload a file to the field; " + this.id + " (not a container field)";
486
646
  return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
487
647
  let form = new FormData();
@@ -514,24 +674,19 @@ class field {
514
674
  }
515
675
  }
516
676
  class portal {
517
- constructor(record, name, records = []) {
677
+ constructor(record, name) {
518
678
  this.record = record;
519
679
  this.name = name;
520
- this.records = records;
521
- for (let item of this.records)
522
- item.attachPortal(this);
523
680
  }
524
681
  }
525
682
  class portalItem extends record {
526
- constructor(record, recordId, modId = recordId, fieldData = {}) {
683
+ constructor(record, portal, recordId, modId = recordId, fieldData = {}) {
527
684
  super(record.layout, recordId, modId, fieldData);
685
+ this.portal = portal;
528
686
  record.on("saved", () => {
529
687
  this.emit("saved");
530
688
  });
531
689
  }
532
- attachPortal(portal) {
533
- this.portal = portal;
534
- }
535
690
  commit(extraBody = { fieldData: undefined }) {
536
691
  return this.portal.record.commit(extraBody);
537
692
  }
@@ -611,9 +766,12 @@ class recordGetRange extends recordGetOperation {
611
766
  run() {
612
767
  return new Promise((resolve, reject) => {
613
768
  // console.log(this.#toObject())
614
- this.layout.database.apiRequest(`${this.layout.endpoint}/records${this.generateQueryParams()}`, {
615
- method: "GET"
616
- }).then((res) => __awaiter(this, void 0, void 0, function* () {
769
+ this.layout.getLayoutMeta().then(() => {
770
+ return this.layout.database.apiRequest(`${this.layout.endpoint}/records${this.generateQueryParams()}`, {
771
+ method: "GET"
772
+ });
773
+ })
774
+ .then((res) => __awaiter(this, void 0, void 0, function* () {
617
775
  // // console.log(res)
618
776
  if (res.messages[0].code === "0") {
619
777
  // console.log("RESOLVING")
@@ -674,11 +832,15 @@ class find extends recordGetOperation {
674
832
  run() {
675
833
  return new Promise((resolve, reject) => {
676
834
  // console.log(this.#toObject())
677
- this.layout.database.apiRequest(`${this.layout.endpoint}/_find`, {
678
- port: 443,
679
- method: "POST",
680
- body: JSON.stringify(this.toObject())
681
- }).then((res) => __awaiter(this, void 0, void 0, function* () {
835
+ this.layout.getLayoutMeta()
836
+ .then(() => {
837
+ return this.layout.database.apiRequest(`${this.layout.endpoint}/_find`, {
838
+ port: 443,
839
+ method: "POST",
840
+ body: JSON.stringify(this.toObject())
841
+ });
842
+ })
843
+ .then((res) => __awaiter(this, void 0, void 0, function* () {
682
844
  // // console.log(res)
683
845
  if (res.messages[0].code === "0") {
684
846
  // console.log("RESOLVING")
@@ -711,7 +873,23 @@ export class FMError extends Error {
711
873
  Error.captureStackTrace(this, FMError);
712
874
  }
713
875
  }
714
- //
715
- // module.exports = {
716
- // default: {FileMakerConnection}
717
- // }
876
+ function generateAuthorizationHeaders(credentials) {
877
+ switch (credentials.method) {
878
+ case "filemaker":
879
+ return {
880
+ "Content-Type": "application/json",
881
+ "Authorization": "Basic " + Buffer.from(credentials.username + ":" + credentials.password).toString("base64")
882
+ };
883
+ case "claris":
884
+ return {
885
+ "Content-Type": "application/json",
886
+ "Authorization": credentials.claris.fmid,
887
+ };
888
+ case "oauth":
889
+ return {
890
+ "Content-Type": "application/json",
891
+ "X-FM-Data-OAuth-RequestId": credentials.oauth.requestId,
892
+ "X-FM-Data-OAuth-Identifier": credentials.oauth.requestIdentifier
893
+ };
894
+ }
895
+ }