@jd-data-limited/easy-fm 1.1.6-alpha1 → 1.2.0-alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.layout, -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();
@@ -287,7 +347,34 @@ class record extends EventEmitter {
287
347
  }
288
348
  processFieldData(fieldData) {
289
349
  return Object.keys(fieldData).map(item => {
290
- return new field(this, item, fieldData[item]);
350
+ let _field = new field(this, item, fieldData[item]);
351
+ if (!!fieldData[item]) {
352
+ if (_field.metadata.result === "timeStamp") {
353
+ // @ts-ignore
354
+ let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.timeStampFormat)
355
+ .utcOffset(this.layout.database.host.timezoneOffset, true)
356
+ .local();
357
+ _field.set(date.toDate());
358
+ _field.edited = false;
359
+ }
360
+ else if (_field.metadata.result === "time") {
361
+ // @ts-ignore
362
+ let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.timeFormat)
363
+ .utcOffset(this.layout.database.host.timezoneOffset, true)
364
+ .local();
365
+ _field.set(date.toDate());
366
+ _field.edited = false;
367
+ }
368
+ else if (_field.metadata.result === "date") {
369
+ // @ts-ignore
370
+ let date = moment.default(fieldData[item], this.layout.database.host.metadata.productInfo.dateFormat)
371
+ .utcOffset(this.layout.database.host.timezoneOffset, true)
372
+ .local();
373
+ _field.set(date.toDate());
374
+ _field.edited = false;
375
+ }
376
+ }
377
+ return _field;
291
378
  });
292
379
  }
293
380
  get() {
@@ -340,7 +427,6 @@ class record extends EventEmitter {
340
427
  reject(new FMError(res.response.scriptError, res.status, res));
341
428
  }
342
429
  else if (res.messages[0].code === "0") {
343
- console.log(res);
344
430
  this.recordId = parseInt(res.response.recordId);
345
431
  this.modId = parseInt(res.response.modId);
346
432
  resolve(this);
@@ -354,19 +440,18 @@ class record extends EventEmitter {
354
440
  });
355
441
  return;
356
442
  }
357
- for (let item of Object.keys(extraBody))
358
- data[item] = extraBody[item];
443
+ for (let item of Object.keys(data))
444
+ extraBody[item] = data[item];
359
445
  this.layout.database.apiRequest(this.endpoint, {
360
446
  port: 443,
361
447
  method: "PATCH",
362
- body: JSON.stringify(data)
448
+ body: JSON.stringify(extraBody)
363
449
  })
364
450
  .then(res => {
365
451
  if (typeof res.response.scriptError !== "undefined" && res.response.scriptError !== '0') {
366
452
  reject(new FMError(res.response.scriptError, res.status, res));
367
453
  }
368
454
  else if (res.messages[0].code === "0") {
369
- console.log(res);
370
455
  this.modId = res.response.modId;
371
456
  this.emit("saved");
372
457
  resolve(this);
@@ -437,7 +522,24 @@ class record extends EventEmitter {
437
522
  toObject(filter = (a) => a.edited, portalFilter = (a) => a.records.find(record => record.edited), portalRowFilter = (a) => a.edited, portalFieldFilter = (a) => a.edited) {
438
523
  let fields_processed = {};
439
524
  for (let field of this.fields.filter(field => filter(field))) {
440
- fields_processed[field.id] = field.value;
525
+ let value = field.value;
526
+ if (value instanceof Date) {
527
+ // @ts-ignore
528
+ let _value = moment.default(value)
529
+ .utcOffset(this.layout.database.host.timezoneOffset);
530
+ // @ts-ignore
531
+ switch (field.metadata.result) {
532
+ case "time":
533
+ value = _value.format(this.layout.database.host.metadata.productInfo.timeFormat.replace("dd", "DD"));
534
+ break;
535
+ case "date":
536
+ value = _value.format(this.layout.database.host.metadata.productInfo.dateFormat.replace("dd", "DD"));
537
+ break;
538
+ default:
539
+ value = _value.format(this.layout.database.host.metadata.productInfo.timeStampFormat.replace("dd", "DD"));
540
+ }
541
+ }
542
+ fields_processed[field.id] = value;
441
543
  }
442
544
  let obj = {
443
545
  "recordId": this.recordId,
@@ -461,27 +563,83 @@ class field {
461
563
  constructor(record, id, contents) {
462
564
  this.record = record;
463
565
  this.id = id;
464
- this.value = contents;
566
+ this._value = contents;
465
567
  this.edited = false;
466
568
  }
467
569
  set(content) {
468
- if (this.dataType === "container")
570
+ if (this.metadata.result === "container")
469
571
  throw "Cannot set container value using set(). Use upload() instead.";
470
- this.value = content;
572
+ if ((this.metadata.result === "timeStamp" ||
573
+ this.metadata.result === "date" ||
574
+ this.metadata.result === "time")
575
+ && !(content instanceof Date) && !!content) {
576
+ throw "Value was not an instance of Date: " + content;
577
+ }
578
+ this._value = content;
471
579
  this.edited = true;
472
580
  }
473
- get dataType() {
474
- if (!this.record.layout.metadata)
475
- return "unknown";
581
+ get metadata() {
582
+ if (!this.record.layout.metadata) {
583
+ // Default to a regular text field
584
+ return {
585
+ name: this.id.toString(),
586
+ type: 'normal',
587
+ displayType: 'editText',
588
+ result: 'text',
589
+ global: false,
590
+ autoEnter: true,
591
+ fourDigitYear: false,
592
+ maxRepeat: 1,
593
+ maxCharacters: 0,
594
+ notEmpty: false,
595
+ numeric: false,
596
+ timeOfDay: false,
597
+ repetitionStart: 1,
598
+ repetitionEnd: 1
599
+ };
600
+ }
476
601
  if (this.record instanceof portalItem) {
477
- return this.record.layout.metadata.portalMetaData.find(i => i.name === this.id).result || "unknown";
602
+ return this.record.layout.metadata.portalMetaData.find(i => i.name === this.id) || {
603
+ name: this.id.toString(),
604
+ type: 'normal',
605
+ displayType: 'editText',
606
+ result: 'text',
607
+ global: false,
608
+ autoEnter: true,
609
+ fourDigitYear: false,
610
+ maxRepeat: 1,
611
+ maxCharacters: 0,
612
+ notEmpty: false,
613
+ numeric: false,
614
+ timeOfDay: false,
615
+ repetitionStart: 1,
616
+ repetitionEnd: 1
617
+ };
478
618
  }
479
619
  else {
480
- return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id).result || "unknown";
620
+ return this.record.layout.metadata.fieldMetaData.find(i => i.name === this.id) || {
621
+ name: this.id.toString(),
622
+ type: 'normal',
623
+ displayType: 'editText',
624
+ result: 'text',
625
+ global: false,
626
+ autoEnter: true,
627
+ fourDigitYear: false,
628
+ maxRepeat: 1,
629
+ maxCharacters: 0,
630
+ notEmpty: false,
631
+ numeric: false,
632
+ timeOfDay: false,
633
+ repetitionStart: 1,
634
+ repetitionEnd: 1
635
+ };
481
636
  }
482
637
  }
638
+ get value() {
639
+ return this._value;
640
+ }
483
641
  upload(buffer, filename, mime) {
484
- if (this.dataType !== "container")
642
+ if (this.metadata.result !== "container")
485
643
  throw "Cannot upload a file to the field; " + this.id + " (not a container field)";
486
644
  return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
487
645
  let form = new FormData();
@@ -611,9 +769,12 @@ class recordGetRange extends recordGetOperation {
611
769
  run() {
612
770
  return new Promise((resolve, reject) => {
613
771
  // 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* () {
772
+ this.layout.getLayoutMeta().then(() => {
773
+ return this.layout.database.apiRequest(`${this.layout.endpoint}/records${this.generateQueryParams()}`, {
774
+ method: "GET"
775
+ });
776
+ })
777
+ .then((res) => __awaiter(this, void 0, void 0, function* () {
617
778
  // // console.log(res)
618
779
  if (res.messages[0].code === "0") {
619
780
  // console.log("RESOLVING")
@@ -674,11 +835,15 @@ class find extends recordGetOperation {
674
835
  run() {
675
836
  return new Promise((resolve, reject) => {
676
837
  // 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* () {
838
+ this.layout.getLayoutMeta()
839
+ .then(() => {
840
+ return this.layout.database.apiRequest(`${this.layout.endpoint}/_find`, {
841
+ port: 443,
842
+ method: "POST",
843
+ body: JSON.stringify(this.toObject())
844
+ });
845
+ })
846
+ .then((res) => __awaiter(this, void 0, void 0, function* () {
682
847
  // // console.log(res)
683
848
  if (res.messages[0].code === "0") {
684
849
  // console.log("RESOLVING")
@@ -711,7 +876,23 @@ export class FMError extends Error {
711
876
  Error.captureStackTrace(this, FMError);
712
877
  }
713
878
  }
714
- //
715
- // module.exports = {
716
- // default: {FileMakerConnection}
717
- // }
879
+ function generateAuthorizationHeaders(credentials) {
880
+ switch (credentials.method) {
881
+ case "filemaker":
882
+ return {
883
+ "Content-Type": "application/json",
884
+ "Authorization": "Basic " + Buffer.from(credentials.username + ":" + credentials.password).toString("base64")
885
+ };
886
+ case "claris":
887
+ return {
888
+ "Content-Type": "application/json",
889
+ "Authorization": credentials.claris.fmid,
890
+ };
891
+ case "oauth":
892
+ return {
893
+ "Content-Type": "application/json",
894
+ "X-FM-Data-OAuth-RequestId": credentials.oauth.requestId,
895
+ "X-FM-Data-OAuth-Identifier": credentials.oauth.requestIdentifier
896
+ };
897
+ }
898
+ }