@jd-data-limited/easy-fm 0.0.1

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 ADDED
@@ -0,0 +1,533 @@
1
+ // const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
2
+ import fetch, {
3
+ Blob,
4
+ blobFrom,
5
+ blobFromSync,
6
+ File,
7
+ fileFrom,
8
+ fileFromSync,
9
+ FormData,
10
+ Headers,
11
+ Request,
12
+ Response
13
+ } from 'node-fetch';
14
+ import { EventEmitter } from "events";
15
+ // @ts-ignore
16
+ import fs from "fs"
17
+ // @ts-ignore
18
+ import path from "path"
19
+ // @ts-ignore
20
+ import { fileURLToPath } from "url"
21
+ import btoa = require("btoa");
22
+
23
+ // @ts-ignore
24
+ const errs = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'fmErrors.json')).toString())
25
+
26
+ interface Script {
27
+ name: string
28
+ parameter: string
29
+ }
30
+
31
+ interface recordObject {
32
+ recordId: number
33
+ modId: number,
34
+ fieldData: any
35
+ }
36
+
37
+ export default class FileMakerConnection {
38
+ public hostname: string;
39
+ public database: string;
40
+ public token: any;
41
+ private username: any;
42
+ private password: any;
43
+ private rejectUnauthroized: boolean;
44
+
45
+ constructor() {
46
+ }
47
+
48
+ get endpoint(): string {
49
+ return `https://${this.hostname}/fmi/data/v2/databases/${encodeURI(this.database)}`
50
+ }
51
+
52
+ async login(hostname, database, username, password, rejectUnauthorized = true) {
53
+ if (this.token) throw new Error("Already logged in. Run logout() first")
54
+
55
+ this.username = username
56
+ this.password = password
57
+ this.hostname = hostname
58
+ this.database = database
59
+ this.rejectUnauthroized = rejectUnauthorized
60
+
61
+ return await this.createSession()
62
+ }
63
+
64
+ logout(): Promise<void> {
65
+ return new Promise(async (resolve, reject) => {
66
+ if (!this.token) reject(new Error("Not logged in"))
67
+
68
+ let _fetch = await fetch(`${this.endpoint}/sessions/${this.token}`, {
69
+ method: "DELETE",
70
+ headers: {
71
+ "content-type": "application/json"
72
+ }
73
+ })
74
+ let data = await _fetch.json()
75
+ // console.log(data)
76
+ this.token = null
77
+ this.database = null
78
+ this.hostname = null
79
+ resolve()
80
+ })
81
+ }
82
+
83
+ importSession(hostname, database, token, rejectUnauthorized = true) {
84
+ this.token = token
85
+ this.database = database
86
+ this.hostname = hostname
87
+ this.rejectUnauthroized = rejectUnauthorized
88
+ }
89
+
90
+ private createSession(): Promise<string> {
91
+ return new Promise(async (resolve, reject) => {
92
+ fetch(`${this.endpoint}/sessions`, {
93
+ hostname: this.hostname,
94
+ port: 443,
95
+ method: "POST",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ "Authorization": "Basic " + btoa(this.username + ":" + this.password)
99
+ }
100
+ }).then(res => {
101
+ console.log(res)
102
+ return res.json()
103
+ })
104
+ .then(res => {
105
+ let _res = res as any
106
+ if (_res.messages[0].code === "0") {
107
+ this.token = _res.response.token
108
+ console.log(_res)
109
+ resolve(this.token)
110
+ } else {
111
+ reject(new FMError(_res.messages[0].code, _res.status, res))
112
+ }
113
+ })
114
+ .catch(e => {
115
+ reject(e)
116
+ })
117
+ })
118
+ }
119
+
120
+ getLayout(name) {
121
+ return new layout(this, name)
122
+ }
123
+
124
+ async apiRequest(url, options): Promise<any> {
125
+ if (!options.headers) options.headers = {}
126
+ options.headers["content-type"] = options.headers["content-type"] ? options.headers["content-type"] : "application/json"
127
+ options.headers["authorization"] = "Bearer " + this.token
128
+ options.rejectUnauthorized = this.rejectUnauthroized
129
+
130
+ let _fetch = await fetch(url, options)
131
+ let data = await _fetch.json()
132
+ return (data as any)
133
+ }
134
+
135
+ setGlobals(globalFields): Promise<void> {
136
+ // console.log({globalFields})
137
+ return new Promise((resolve, reject) => {
138
+ this.apiRequest(`${this.endpoint}/globals`, {
139
+ method: "PATCH",
140
+ body: JSON.stringify({globalFields})
141
+ }).then(res => {
142
+ // console.log(res)
143
+ if (res.messages[0].code === "0") {
144
+ resolve()
145
+ } else {
146
+ reject(new FMError(res.messages[0].code, res.status, res))
147
+ }
148
+ })
149
+ .catch(e => {
150
+ reject(e)
151
+ })
152
+ })
153
+ }
154
+
155
+ script(name, parameter): Script {
156
+ return ({name, parameter} as Script)
157
+ }
158
+ }
159
+
160
+ class layout {
161
+ readonly conn: FileMakerConnection;
162
+ protected name: string;
163
+ constructor(conn: FileMakerConnection, name: string) {
164
+ this.conn = conn
165
+ this.name = name
166
+ }
167
+
168
+ get endpoint() {
169
+ return `${this.conn.endpoint}/layouts/${this.name}`
170
+ }
171
+
172
+ getScript(script) {
173
+ return new script(this, script)
174
+ }
175
+
176
+ createRecord(body = {}): Promise<record | Error> {
177
+ // console.log(fieldData)
178
+ return new Promise(async (resolve, reject) => {
179
+ this.conn.apiRequest(`${this.endpoint}/records`, {
180
+ port: 443,
181
+ method: "POST",
182
+ body: JSON.stringify(body)
183
+ })
184
+ .then(res => {
185
+ if (res.messages[0].code === "0") {
186
+ resolve(
187
+ new record(this, res.response.recordId, res.response.modId)
188
+ )
189
+ } else {
190
+ reject(new FMError(res.messages[0].code, res.status, res))
191
+ }
192
+ })
193
+ .catch(e => {
194
+ reject(e)
195
+ })
196
+ })
197
+ }
198
+
199
+ getRecord(recordId): record {
200
+ return new record(this, recordId)
201
+ }
202
+
203
+ newFind(): find {
204
+ return new find(this)
205
+ }
206
+
207
+ runScript(script): Promise<string | FMError | Error> {
208
+ return new Promise(async (resolve, reject) => {
209
+ let url = `${this.endpoint}/script/${encodeURI(script.name)}`
210
+ if (script.parameter) url += "?" + encodeURI(script.parameter)
211
+ this.conn.apiRequest(url, {
212
+ port: 443,
213
+ method: "GET"
214
+ })
215
+ .then(res => {
216
+ if (res.messages[0].code === "0") {
217
+ resolve(res.response)
218
+ } else {
219
+ // console.log(res)
220
+ reject(new FMError(res.messages[0].code, res.status, res))
221
+ }
222
+ })
223
+ .catch(e => {
224
+ reject(e)
225
+ })
226
+ })
227
+ }
228
+ }
229
+
230
+ class record extends EventEmitter {
231
+ readonly layout: layout;
232
+ readonly recordId: number;
233
+ modId: number;
234
+ fields: field[];
235
+ readonly portals: portal[];
236
+ private portalData: any[];
237
+
238
+ constructor(layout, recordId, modId = recordId, fieldData = {}, portalData = null) {
239
+ super();
240
+ this.layout = layout
241
+ this.recordId = recordId
242
+ this.modId = modId
243
+ this.fields = this.processFieldData(fieldData)
244
+ this.portals = []
245
+ if (portalData) {
246
+ this.processPortalData(portalData)
247
+ }
248
+ this.on("saved", () => {
249
+ for (let field of this.fields) field.edited = false
250
+ })
251
+ }
252
+
253
+ get endpoint(): string {
254
+ return `${this.layout.endpoint}/records/${this.recordId}`
255
+ }
256
+
257
+ get edited(): boolean {
258
+ return !!this.fields.find(i => i.edited)
259
+ }
260
+
261
+ private processPortalData(portalData): void {
262
+ for (let item of Object.keys(portalData)) {
263
+ this.portals.push(new portal(this, item, portalData[item].map(item => {
264
+ let fieldData = item
265
+ delete fieldData.recordId
266
+ delete fieldData.modId
267
+ return new portalItem(this, item.recordId, item.modId, fieldData)
268
+ })))
269
+ }
270
+ }
271
+
272
+ processFieldData(fieldData): field[] {
273
+ return Object.keys(fieldData).map(item => {
274
+ return new field(this, item, fieldData[item])
275
+ })
276
+ }
277
+
278
+ get() {
279
+ return new Promise(async (resolve, reject) => {
280
+ this.layout.conn.apiRequest(this.endpoint, {
281
+ port: 443,
282
+ method: "GET"
283
+ })
284
+ .then(res => {
285
+ if (res.messages[0].code === "0") {
286
+ // console.log(res, res.response.data)
287
+ this.modId = res.response.data[0].modId
288
+ this.fields = this.processFieldData(res.response.data[0].fieldData)
289
+ this.portalData = []
290
+ if (res.response.data[0].portalData) this.processPortalData(res.response.data[0].portalData)
291
+ resolve(this)
292
+ } else {
293
+ reject(new FMError(res.messages[0].code, res.status, res))
294
+ }
295
+ })
296
+ .catch(e => {
297
+ reject(e)
298
+ })
299
+ })
300
+ }
301
+
302
+ save(extraBody = {}) {
303
+ return new Promise(async (resolve, reject) => {
304
+ let data = this.toObject()
305
+ delete data.recordId
306
+ delete data.modId
307
+
308
+ for (let item of Object.keys(extraBody)) data[item] = extraBody[item]
309
+ this.layout.conn.apiRequest(this.endpoint, {
310
+ port: 443,
311
+ method: "PATCH",
312
+ body: JSON.stringify(data)
313
+ })
314
+ .then(res => {
315
+ if (res.messages[0].code === "0") {
316
+ this.modId = res.response.modId
317
+ this.emit("saved")
318
+ resolve(this)
319
+ } else {
320
+ reject(new FMError(res.messages[0].code, res.status, res))
321
+ }
322
+ })
323
+ .catch(e => {
324
+ reject(e)
325
+ })
326
+ })
327
+ }
328
+
329
+ getField(field) {
330
+ return this.fields.find(_field => _field.id === field)
331
+ }
332
+
333
+ getPortal(portal) {
334
+ return this.portals.find(p => p.name === portal)
335
+ }
336
+
337
+ toObject(filter = (a) => a.edited,
338
+ portalFilter = (a) => a.records.find(record => record.edited),
339
+ portalRowFilter = (a) => a.edited,
340
+ portalFieldFilter = (a) => a.edited
341
+ ): recordObject {
342
+ let fields_processed = {}
343
+ for (let field of this.fields.filter(field => filter(field))) {
344
+ fields_processed[field.id] = field.value
345
+ }
346
+ let obj = {
347
+ "recordId": this.recordId,
348
+ "modId": this.modId,
349
+ "fieldData": fields_processed
350
+ }
351
+
352
+ // Check if there's been any edited portal information
353
+ let portals = this.portals.filter(a => portalFilter(a))
354
+ if (portals) {
355
+ obj["portalData"] = {}
356
+ for (let portal of portals) {
357
+ obj["portalData"][portal.name] = portal.records.filter(a => portalRowFilter(a)).map(record => {
358
+ return record.toObject(portalFieldFilter)
359
+ })
360
+ }
361
+ }
362
+ return obj
363
+ }
364
+ }
365
+
366
+ class field {
367
+ record: record;
368
+ id: number;
369
+ value: string | number;
370
+ edited: boolean;
371
+ constructor(record, id, contents) {
372
+ this.record = record
373
+ this.id = id
374
+ this.value = contents
375
+ this.edited = false
376
+ }
377
+
378
+ set(content) {
379
+ this.value = content
380
+ this.edited = true
381
+ }
382
+
383
+ containerUpload(buffer, filename, mime): Promise<void | FMError> {
384
+ return new Promise(async (resolve, reject) => {
385
+ let form = new FormData()
386
+ form.append("upload", new File([buffer], filename, {type: mime}))
387
+
388
+ let _fetch = await fetch(`${this.record.endpoint}/containers/${this.id}/1`, {
389
+ method: "POST",
390
+ headers: {"Authorization": "Bearer " + this.record.layout.conn.token},
391
+ body: form
392
+ }).then(res => res.json())
393
+ .then(data => {
394
+ let _res = data as any
395
+ if (_res.messages[0].code === "0") {
396
+ resolve()
397
+ } else {
398
+ reject(new FMError(_res.messages[0].code, _res.status, data))
399
+ }
400
+ })
401
+ .catch(e => {reject(e)})
402
+ })
403
+ }
404
+ }
405
+
406
+ class portal {
407
+ readonly record: record;
408
+ readonly name: string;
409
+ public records: portalItem[];
410
+
411
+ constructor(record: record, name: string, records: portalItem[] = []) {
412
+ this.record = record
413
+ this.name = name
414
+ this.records = records
415
+ for (let item of this.records) item.attachPortal(this)
416
+ }
417
+ }
418
+
419
+ class portalItem extends record {
420
+ protected portal: portal;
421
+
422
+ constructor(record, recordId, modId = recordId, fieldData = {}) {
423
+ super(record.layout, recordId, modId, fieldData);
424
+ record.on("saved", () => {
425
+ this.emit("saved")
426
+ })
427
+ }
428
+
429
+ attachPortal(portal) {
430
+ this.portal = portal
431
+ }
432
+
433
+ save(extraBody = {}) {
434
+ return this.portal.record.save(extraBody)
435
+ }
436
+
437
+ toObject(fieldFilter): any {
438
+ super.toObject()
439
+ let res = {
440
+ recordId: this.recordId, modId: this.modId
441
+ } as recordObject
442
+ for (let field of this.fields.filter(a => fieldFilter(a))) res[field.id] = field.value
443
+ // console.log(res)
444
+ return res
445
+ }
446
+ }
447
+
448
+ class find {
449
+ protected layout: layout
450
+ protected queries: object[]
451
+ protected scripts: object
452
+ protected sort: object[]
453
+
454
+ constructor(layout) {
455
+ this.layout = layout
456
+ this.queries = []
457
+ this.scripts = {
458
+ "script": null, // Runs after everything
459
+ "script.prerequeset": null, // Runs before the request
460
+ "script.presort": null // Runs before the sort
461
+ }
462
+ this.sort = []
463
+ }
464
+
465
+ private toObject() {
466
+ let out = {query: this.queries, sort: undefined}
467
+ if (this.sort.length !== 0) out.sort = this.sort
468
+
469
+ for (let item of Object.keys(this.scripts)) {
470
+ if (this.scripts[item]) {
471
+ out[item] = this.scripts[item].name
472
+ if (this.scripts[item].parameter) out[item + ".param"] = this.scripts[item].parameter
473
+ }
474
+ }
475
+
476
+ return out
477
+ }
478
+
479
+ addRequests(...requests) {
480
+ for (let item of requests) this.queries.push(item)
481
+ return this
482
+ }
483
+
484
+ addSort(fieldName, sortOrder) {
485
+ this.sort.push({fieldName, sortOrder})
486
+ return this
487
+ }
488
+
489
+ find() {
490
+ return new Promise((resolve, reject) => {
491
+ // console.log(this.#toObject())
492
+ this.layout.conn.apiRequest(`${this.layout.endpoint}/_find`, {
493
+ port: 443,
494
+ method: "POST",
495
+ body: JSON.stringify(this.toObject())
496
+ }).then(res => {
497
+ // // console.log(res)
498
+ if (res.messages[0].code === "0") {
499
+ // console.log("RESOLVING")
500
+ let data = res.response.data.map(item => {
501
+ return new record(this.layout, item.recordId, item.modId, item.fieldData, item.portalData)
502
+ })
503
+ resolve(data)
504
+ } else {
505
+ reject(new FMError(res.messages[0].code, res.status, res))
506
+ }
507
+ })
508
+ .catch(e => {
509
+ reject(e)
510
+ })
511
+ })
512
+ }
513
+ }
514
+
515
+ export class FMError extends Error {
516
+ readonly httpStatus: number
517
+ readonly res: any
518
+ readonly code: number
519
+ readonly message: string
520
+
521
+ constructor(code, httpStatus, res) {
522
+ super(code);
523
+ this.httpStatus = httpStatus
524
+ this.res = res
525
+ this.code = typeof code === "string" ? parseInt(code) : code
526
+ this.message = errs.find(err => err.e === this.code) || "Unknown error"
527
+ }
528
+ }
529
+
530
+ //
531
+ // module.exports = {
532
+ // default: {FileMakerConnection}
533
+ // }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@jd-data-limited/easy-fm",
3
+ "version": "0.0.1",
4
+ "dependencies": {
5
+ "@types/btoa": "^1.2.3",
6
+ "@types/events": "^3.0.0",
7
+ "node-fetch": "^3.3.0",
8
+ "typescript": "^4.9.4"
9
+ },
10
+ "type": "module",
11
+ "main": "index.js",
12
+ "module": "es2022",
13
+ "homepage": "https://github.com/JD-Data-Limited/easy-fm",
14
+ "url": "https://github.com/JD-Data-Limited/easy-fm",
15
+ "license": "SEE LICENSE IN LICENSE",
16
+ "description": "easy-fm is a Node.js module that allows you to interact with\r a [FileMaker database stored](https://www.claris.com/filemaker/) on\r a [FileMaker server](https://www.claris.com/filemaker/server/). This module interacts with your server using the\r [FileMaker Data API](https://help.claris.com/en/data-api-guide/content/index.html).",
17
+ "devDependencies": {},
18
+ "scripts": {
19
+ "test": "echo \"Error: no test specified\" && exit 1"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/JD-Data-Limited/easy-fm.git"
24
+ },
25
+ "keywords": [
26
+ "FileMaker",
27
+ "Data API",
28
+ "REST"
29
+ ],
30
+ "author": "JD-Data Limited",
31
+ "bugs": {
32
+ "url": "https://github.com/JD-Data-Limited/easy-fm/issues"
33
+ }
34
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2015",
4
+ "module": "commonjs",
5
+ "lib": ["es2021.intl", "es2015"],
6
+ },
7
+ "files": ["index.ts"]
8
+ }