@opencitylabs/formio-sdk 1.0.0

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.
Files changed (3) hide show
  1. package/README.md +56 -0
  2. package/index.js +671 -0
  3. package/package.json +28 -0
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # formio-sdk
2
+
3
+ Node/browser helper utilities for Form.io APIs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install formio-sdk
9
+ ```
10
+
11
+ ## Quick start (Node)
12
+
13
+ ```js
14
+ const { FormioHelper } = require("formio-sdk");
15
+
16
+ const helper = new FormioHelper({
17
+ baseUrl: "https://servizi.example.it/lang",
18
+ token: process.env.AUTH_TOKEN,
19
+ locale: "it",
20
+ siteUrl: "https://servizi.example.it",
21
+ applicationId: "12345"
22
+ });
23
+
24
+ async function run() {
25
+ const tenant = await helper.getTenantInfo();
26
+ console.log("tenant:", tenant);
27
+
28
+ const profile = await helper.authenticatedCall("users/me");
29
+ console.log("profile:", profile);
30
+ }
31
+
32
+ run().catch(console.error);
33
+ ```
34
+
35
+ ## API notes
36
+
37
+ - `getFieldApplication(getParams, applicationId)`
38
+ : in Node pass `applicationId` as argument or constructor option.
39
+ - `getFieldMeta(getParams)`
40
+ : returns parsed tenant meta (or nested key via lodash path).
41
+ - `authenticatedCall`, `authenticatedPOSTCall`, `authenticatedRequest`
42
+ : return `undefined` when token is missing (same behavior as original helper).
43
+
44
+ ## Constructor options
45
+
46
+ - `baseUrl`: Formio backend base URL.
47
+ - `token`: auth token string (or function returning token).
48
+ - `locale`: locale string (defaults to `it`).
49
+ - `siteUrl`: optional site URL used by `getBookingConfig`.
50
+ - `applicationId`: fallback id used by `getFieldApplication`.
51
+ - `origin`: base origin for relative URLs in `addLimitParam`.
52
+ - `storage`: custom storage adapter (`getItem`) to read `auth-token`.
53
+ - `httpClient`: custom axios-like client.
54
+ - `logger`: custom logger with `error`/`warn` methods.
55
+ - `getBaseUrl`, `getCurrentToken`, `getCurrentLocale`, `getSiteUrl`
56
+ : override functions for full control.
package/index.js ADDED
@@ -0,0 +1,671 @@
1
+ "use strict";
2
+
3
+ const axios = require("axios");
4
+ const get = require("lodash.get");
5
+
6
+ class FormioHelper {
7
+ constructor(options = {}) {
8
+ this.options = options;
9
+ this.httpClient = options.httpClient || axios;
10
+ this.logger = options.logger || console;
11
+ this.token = null;
12
+ this.basePath = null;
13
+ this.init();
14
+ }
15
+
16
+ init() {
17
+ this.basePath = this.getBaseUrl();
18
+ this.token = this.getCurrentToken();
19
+ }
20
+
21
+ logError(...args) {
22
+ if (this.logger && typeof this.logger.error === "function") {
23
+ this.logger.error(...args);
24
+ }
25
+ }
26
+
27
+ logWarn(...args) {
28
+ if (this.logger && typeof this.logger.warn === "function") {
29
+ this.logger.warn(...args);
30
+ }
31
+ }
32
+
33
+ getWindow() {
34
+ return typeof window !== "undefined" ? window : null;
35
+ }
36
+
37
+ getDocument() {
38
+ return typeof document !== "undefined" ? document : null;
39
+ }
40
+
41
+ getStorage() {
42
+ if (this.options.storage) {
43
+ return this.options.storage;
44
+ }
45
+
46
+ const win = this.getWindow();
47
+ if (win && win.sessionStorage) {
48
+ return win.sessionStorage;
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ resolveOption(optionName) {
55
+ const optionValue = this.options[optionName];
56
+ if (typeof optionValue === "function") {
57
+ return optionValue();
58
+ }
59
+
60
+ return optionValue ?? null;
61
+ }
62
+
63
+ getCurrentLocale() {
64
+ if (typeof this.options.getCurrentLocale === "function") {
65
+ return this.options.getCurrentLocale();
66
+ }
67
+
68
+ const optionLocale = this.resolveOption("locale");
69
+ if (optionLocale) {
70
+ return optionLocale.toString();
71
+ }
72
+
73
+ const doc = this.getDocument();
74
+ if (doc && doc.documentElement && doc.documentElement.lang) {
75
+ return doc.documentElement.lang.toString();
76
+ }
77
+
78
+ return "it";
79
+ }
80
+
81
+ getCurrentToken() {
82
+ if (this.token) {
83
+ return this.token;
84
+ }
85
+
86
+ if (typeof this.options.getCurrentToken === "function") {
87
+ return this.options.getCurrentToken();
88
+ }
89
+
90
+ const optionToken = this.resolveOption("token");
91
+ if (typeof optionToken === "string") {
92
+ return optionToken;
93
+ }
94
+
95
+ if (optionToken && typeof optionToken === "object") {
96
+ return optionToken.value || null;
97
+ }
98
+
99
+ const storage = this.getStorage();
100
+ if (!storage || typeof storage.getItem !== "function") {
101
+ return null;
102
+ }
103
+
104
+ const rawToken = storage.getItem("auth-token");
105
+ if (!rawToken) {
106
+ return null;
107
+ }
108
+
109
+ try {
110
+ const parsedToken = JSON.parse(rawToken);
111
+ if (typeof parsedToken === "string") {
112
+ return parsedToken;
113
+ }
114
+
115
+ return parsedToken && parsedToken.value ? parsedToken.value : null;
116
+ } catch (error) {
117
+ return rawToken;
118
+ }
119
+ }
120
+
121
+ getBaseUrl() {
122
+ if (typeof this.options.getBaseUrl === "function") {
123
+ return this.options.getBaseUrl();
124
+ }
125
+
126
+ const optionBaseUrl = this.resolveOption("baseUrl");
127
+ if (optionBaseUrl) {
128
+ return optionBaseUrl;
129
+ }
130
+
131
+ const win = this.getWindow();
132
+ if (win && win.BASE_URL) {
133
+ return win.BASE_URL;
134
+ }
135
+
136
+ if (win && win.location && win.location.origin && win.location.pathname) {
137
+ const explodedPath = win.location.pathname.split("/");
138
+ return `${win.location.origin}/${explodedPath[1]}`;
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ getSiteUrl() {
145
+ if (typeof this.options.getSiteUrl === "function") {
146
+ return this.options.getSiteUrl();
147
+ }
148
+
149
+ const optionSiteUrl = this.resolveOption("siteUrl");
150
+ if (optionSiteUrl) {
151
+ return optionSiteUrl;
152
+ }
153
+
154
+ const win = this.getWindow();
155
+ if (win && win.SITE_URL) {
156
+ return win.SITE_URL;
157
+ }
158
+
159
+ return null;
160
+ }
161
+
162
+ buildApiUrl(endPoint = "") {
163
+ const baseUrl = this.getBaseUrl();
164
+ if (!baseUrl) {
165
+ throw new Error("Missing baseUrl. Pass { baseUrl } to FormioHelper.");
166
+ }
167
+
168
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
169
+ const normalizedEndpoint = endPoint.toString().replace(/^\/+/, "");
170
+ return `${normalizedBaseUrl}/api/${normalizedEndpoint}`;
171
+ }
172
+
173
+ async getTenantInfo() {
174
+ const response = await this.httpClient.get(this.buildApiUrl("tenants/info"), {
175
+ headers: {
176
+ "x-locale": this.getCurrentLocale(),
177
+ },
178
+ });
179
+
180
+ return response.data;
181
+ }
182
+
183
+ async authenticatedCall(endPoint) {
184
+ const token = this.getCurrentToken();
185
+ if (!token) {
186
+ return;
187
+ }
188
+
189
+ const response = await this.httpClient.get(this.buildApiUrl(endPoint), {
190
+ headers: {
191
+ "Content-Type": "application/json",
192
+ Authorization: `Bearer ${token}`,
193
+ },
194
+ });
195
+
196
+ return response.data;
197
+ }
198
+
199
+ async authenticatedPOSTCall(endPoint, params) {
200
+ const token = this.getCurrentToken();
201
+ if (!token) {
202
+ return;
203
+ }
204
+
205
+ const response = await this.httpClient.post(
206
+ this.buildApiUrl(endPoint),
207
+ JSON.stringify(params),
208
+ {
209
+ headers: {
210
+ "Content-Type": "application/json",
211
+ Authorization: `Bearer ${token}`,
212
+ },
213
+ },
214
+ );
215
+
216
+ return response.data;
217
+ }
218
+
219
+ async authenticatedRequest(method, endPoint, params = {}) {
220
+ const token = this.getCurrentToken();
221
+ if (!token) {
222
+ return;
223
+ }
224
+
225
+ try {
226
+ const response = await this.httpClient({
227
+ method,
228
+ url: this.buildApiUrl(endPoint),
229
+ data: params,
230
+ headers: {
231
+ "Content-Type": "application/json",
232
+ Authorization: `Bearer ${token}`,
233
+ },
234
+ });
235
+
236
+ return response.data;
237
+ } catch (error) {
238
+ this.logError(`Error on ${method.toUpperCase()} ${endPoint}:`, error);
239
+ throw error;
240
+ }
241
+ }
242
+
243
+ async postGraphql(url, method = "post", payload) {
244
+ const graphqlQuery = {
245
+ operationName: "fetchList",
246
+ query: `${payload}`,
247
+ variables: {},
248
+ };
249
+
250
+ const response = await this.httpClient({
251
+ url,
252
+ method,
253
+ headers: {
254
+ "Content-Type": "application/json",
255
+ },
256
+ data: graphqlQuery,
257
+ });
258
+
259
+ return response.data;
260
+ }
261
+
262
+ async getRemoteJson(url, method = "get", headers = null) {
263
+ const requestConfig = {
264
+ method,
265
+ url,
266
+ };
267
+
268
+ if (headers) {
269
+ requestConfig.headers = headers;
270
+ }
271
+
272
+ const response = await this.httpClient(requestConfig);
273
+ return response.data;
274
+ }
275
+
276
+ async anonymousCall(endPoint) {
277
+ const response = await this.httpClient.get(this.buildApiUrl(endPoint), {
278
+ headers: {
279
+ "Content-Type": "application/json",
280
+ },
281
+ });
282
+
283
+ return response.data;
284
+ }
285
+
286
+ // See Doc {@link https://gitlab.com/opencity-labs/area-personale/core/-/wikis/Guida-alla-creazione-dei-moduli/Form.io-Sdk#esempio-recupero-dato-dai-meta}
287
+ async getFieldMeta(getParams) {
288
+ try {
289
+ const result = await this.getTenantInfo();
290
+ const rawMeta = Array.isArray(result && result.meta) ? result.meta[0] : null;
291
+ if (!rawMeta) {
292
+ return null;
293
+ }
294
+
295
+ const meta = JSON.parse(rawMeta);
296
+ if (!meta) {
297
+ return null;
298
+ }
299
+
300
+ if (getParams) {
301
+ return get(meta, getParams, false);
302
+ }
303
+
304
+ return meta;
305
+ } catch (error) {
306
+ this.logError("error getFieldMeta", error);
307
+ return null;
308
+ }
309
+ }
310
+
311
+ readApplicationIdFromDom() {
312
+ const doc = this.getDocument();
313
+ if (!doc) {
314
+ return null;
315
+ }
316
+
317
+ const formioElement = doc.querySelector("#formio");
318
+ return formioElement && formioElement.dataset
319
+ ? formioElement.dataset.applicationId || null
320
+ : null;
321
+ }
322
+
323
+ async getFieldApplication(getParams, applicationId = null) {
324
+ const resolvedApplicationId =
325
+ applicationId ||
326
+ this.resolveOption("applicationId") ||
327
+ this.readApplicationIdFromDom();
328
+
329
+ if (!resolvedApplicationId) {
330
+ this.logWarn("No applicationId provided to getFieldApplication");
331
+ return null;
332
+ }
333
+
334
+ try {
335
+ const result = await this.authenticatedCall(
336
+ `applications/${resolvedApplicationId}`,
337
+ );
338
+
339
+ if (result && getParams) {
340
+ return get(result, getParams);
341
+ }
342
+
343
+ return result;
344
+ } catch (error) {
345
+ this.logError("error getFieldApplication", error);
346
+ return null;
347
+ }
348
+ }
349
+
350
+ addLimitParam(url, limit) {
351
+ const origin = this.resolveOption("origin") || this.getWindow()?.location?.origin;
352
+ const parsedUrl = origin ? new URL(url, origin) : new URL(url);
353
+ parsedUrl.searchParams.set("limit", String(limit));
354
+ return parsedUrl.toString();
355
+ }
356
+
357
+ async fetchBookingUrl(url) {
358
+ let nextUrl = this.addLimitParam(url, 99);
359
+ const allData = [];
360
+
361
+ try {
362
+ while (nextUrl) {
363
+ const response = await this.httpClient.get(nextUrl);
364
+ const json = response.data;
365
+
366
+ if (Array.isArray(json && json.items)) {
367
+ allData.push(...json.items);
368
+ }
369
+
370
+ nextUrl = json && json.next ? json.next : null;
371
+ }
372
+
373
+ return allData;
374
+ } catch (error) {
375
+ this.logError(
376
+ "Errore durante il recupero delle disponibilita complete:",
377
+ error,
378
+ );
379
+ return [];
380
+ }
381
+ }
382
+
383
+ async fetchAPIUrlLimit(url, limit = 99) {
384
+ let nextUrl = this.addLimitParam(url, limit);
385
+ const allData = [];
386
+
387
+ try {
388
+ while (nextUrl) {
389
+ const response = await this.httpClient.get(nextUrl);
390
+ const json = response.data;
391
+
392
+ if (Array.isArray(json && json.data)) {
393
+ allData.push(...json.data);
394
+ }
395
+
396
+ nextUrl = json && json.next ? json.next : null;
397
+ }
398
+
399
+ return allData;
400
+ } catch (error) {
401
+ this.logError(
402
+ "Errore durante il recupero delle disponibilita complete:",
403
+ error,
404
+ );
405
+ return [];
406
+ }
407
+ }
408
+
409
+ async getBookingConfig(id = null) {
410
+ try {
411
+ let siteUrl = this.getSiteUrl();
412
+ if (!siteUrl) {
413
+ const tenantInfo = await this.getTenantInfo();
414
+ siteUrl = tenantInfo && tenantInfo.site_url ? tenantInfo.site_url : null;
415
+ }
416
+
417
+ if (!siteUrl) {
418
+ this.logWarn("Nessun site_url trovato, impossibile proseguire.");
419
+ return null;
420
+ }
421
+
422
+ const cleanedBaseUrl = siteUrl.replace(/\/$/, "");
423
+ const query = id ? `?id=${encodeURIComponent(id)}` : "";
424
+ const apiUrl = `${cleanedBaseUrl}/api/openapi/booking-config${query}`;
425
+
426
+ const response = await this.fetchBookingUrl(apiUrl);
427
+ return response;
428
+ } catch (error) {
429
+ this.logError("Errore in getBookingConfig:", error);
430
+ return null;
431
+ }
432
+ }
433
+
434
+ async getRatesLists(url, calendarId) {
435
+ const tenant = await this.getTenantInfo();
436
+
437
+ if (tenant && tenant.id) {
438
+ return this.postGraphql(
439
+ url,
440
+ "POST",
441
+ `query fetchList {
442
+ rooms(filter: {id:{ _eq: "${calendarId}"},tenant:{id:{_eq:"${tenant.id}"}}}) {
443
+ id
444
+ title
445
+ rates_list(filter:{status:{_eq:"published"}}) {
446
+ id
447
+ title
448
+ rates {
449
+ id
450
+ rate
451
+ title
452
+ amount_lower_limit
453
+ amount_upper_limit
454
+ hours_lower_limit
455
+ hours_upper_limit
456
+ }
457
+ }
458
+ }}`,
459
+ )
460
+ .then((result) => {
461
+ return result;
462
+ })
463
+ .catch((error) => {
464
+ this.logError("error getRatesLists", error);
465
+ return null;
466
+ });
467
+ }
468
+
469
+ return null;
470
+ }
471
+
472
+ async editFormProperties(instance, edits) {
473
+ if (!instance || typeof instance.getComponent !== "function") {
474
+ this.logError("[editFormProperties] Invalid instance:", instance);
475
+ return;
476
+ }
477
+
478
+ for (const [fullPath, changes] of Object.entries(edits)) {
479
+ const pathParts = fullPath.split(".");
480
+ const fieldKey = pathParts.pop();
481
+ let currentInstance = instance;
482
+
483
+ for (const part of pathParts) {
484
+ let comp;
485
+ try {
486
+ comp = currentInstance.getComponent(part);
487
+ } catch (error) {
488
+ this.logError("[editFormProperties:getComponent - nested]", part, error);
489
+ comp = null;
490
+ }
491
+
492
+ if (!comp || comp.type !== "form") {
493
+ currentInstance = null;
494
+ break;
495
+ }
496
+
497
+ if (!comp.subForm) {
498
+ currentInstance = null;
499
+ break;
500
+ }
501
+
502
+ if (typeof comp.subForm.ready === "function") {
503
+ try {
504
+ await comp.subForm.ready;
505
+ } catch (error) {
506
+ this.logError("[editFormProperties] subForm.ready failed:", part, error);
507
+ currentInstance = null;
508
+ break;
509
+ }
510
+ }
511
+
512
+ currentInstance = comp.subForm;
513
+ }
514
+
515
+ if (!currentInstance) {
516
+ continue;
517
+ }
518
+
519
+ let comp;
520
+ try {
521
+ comp = currentInstance.getComponent(fieldKey);
522
+ } catch (error) {
523
+ this.logError("[editFormProperties:getComponent - field]", fieldKey, error);
524
+ continue;
525
+ }
526
+
527
+ if (!comp) {
528
+ continue;
529
+ }
530
+
531
+ for (const [propPath, newVal] of Object.entries(changes)) {
532
+ try {
533
+ const parts = propPath.split(".");
534
+ let target = comp.component;
535
+
536
+ for (let i = 0; i < parts.length - 1; i += 1) {
537
+ if (!target[parts[i]]) {
538
+ target[parts[i]] = {};
539
+ }
540
+ target = target[parts[i]];
541
+ }
542
+
543
+ const last = parts[parts.length - 1];
544
+ if (last === "hidden") {
545
+ comp._hasCondition = newVal ? false : true;
546
+ }
547
+
548
+ if (target[last] !== newVal) {
549
+ target[last] = newVal;
550
+
551
+ if (typeof comp.redraw === "function") {
552
+ try {
553
+ comp.redraw();
554
+ } catch (error) {
555
+ this.logError("[editFormProperties:redraw]", fullPath, propPath, error);
556
+ }
557
+ }
558
+ }
559
+ } catch (error) {
560
+ this.logError(
561
+ "[editFormProperties:property assignment]",
562
+ fullPath,
563
+ propPath,
564
+ error,
565
+ );
566
+ }
567
+ }
568
+ }
569
+ }
570
+
571
+ checkResidences(data, residenceFields, requireAllResidences = false) {
572
+ if (!data) {
573
+ throw new Error("[checkResidences] Data object is missing.");
574
+ }
575
+
576
+ if (!data.residence_tenant) {
577
+ return false;
578
+ }
579
+
580
+ const tenantName = data.residence_tenant.toLowerCase();
581
+ const residenceMatchFn = requireAllResidences ? "every" : "some";
582
+
583
+ const isLocationInTenant = residenceFields[residenceMatchFn]((path) => {
584
+ const locality = (get(data, path, "") || "").toLowerCase().trim();
585
+ return locality !== "" && tenantName.includes(locality);
586
+ });
587
+
588
+ const COUNTRY_FIELDS = ["address_country", "country"];
589
+
590
+ const hasEmptyCountry = residenceFields.every((config) => {
591
+ const basePath = config.split(".").slice(0, -1).join(".");
592
+ return COUNTRY_FIELDS.every((field) => {
593
+ const value = get(data, `${basePath}.${field}`, "");
594
+ return typeof value === "string" && value.trim() === "";
595
+ });
596
+ });
597
+
598
+ return isLocationInTenant ? false : !hasEmptyCountry;
599
+ }
600
+
601
+ syncApplicantToBeneficiary(data, instance, applicantRequester) {
602
+ if (!data) {
603
+ throw new Error("[syncApplicantToBeneficiary] Data object is missing.");
604
+ }
605
+
606
+ if (!instance || typeof instance.getComponent !== "function") {
607
+ this.logError("[syncApplicantToBeneficiary] Invalid instance:", instance);
608
+ return;
609
+ }
610
+
611
+ const jsonString = JSON.stringify(data.applicant);
612
+ let hash = 0;
613
+ for (let i = 0; i < jsonString.length; i += 1) {
614
+ const char = jsonString.charCodeAt(i);
615
+ hash = (hash << 5) - hash + char;
616
+ hash &= hash;
617
+ }
618
+ hash = hash.toString(16);
619
+
620
+ if (applicantRequester && hash !== data.applicant_hash) {
621
+ const sub = Object.assign({}, instance.calculatedValue);
622
+
623
+ sub.data.given_name = data.applicant.data.completename.data.name;
624
+ sub.data.family_name = data.applicant.data.completename.data.surname;
625
+
626
+ if (data.applicant.data.Born && data.applicant.data.Born.data) {
627
+ sub.data.birth_place = data.applicant.data.Born.data.place_of_birth;
628
+ sub.data.birth_date = data.applicant.data.Born.data.natoAIl;
629
+ }
630
+
631
+ if (data.applicant.data.gender && data.applicant.data.gender.data) {
632
+ sub.data.gender = data.applicant.data.gender.data.gender;
633
+ }
634
+
635
+ if (data.applicant.data.fiscal_code && data.applicant.data.fiscal_code.data) {
636
+ sub.data.tax_id = data.applicant.data.fiscal_code.data.fiscal_code;
637
+ }
638
+
639
+ if (data.applicant.data.address && data.applicant.data.address.data) {
640
+ if (!sub.data.address) {
641
+ sub.data.address = { data: {} };
642
+ }
643
+
644
+ sub.data.address.data.street_address = data.applicant.data.address.data.address;
645
+ sub.data.address.data.address_number =
646
+ data.applicant.data.address.data.house_number;
647
+ sub.data.address.data.address_locality =
648
+ data.applicant.data.address.data.municipality;
649
+ sub.data.address.data.postal_code = data.applicant.data.address.data.postal_code;
650
+ sub.data.address.data.address_country = data.applicant.data.address.data.country;
651
+ }
652
+
653
+ sub.data.nationality = data.beneficiary.data.nationality;
654
+ sub.data.nation = data.beneficiary.data.nation;
655
+
656
+ instance.setValue(sub);
657
+ data.applicant_hash = hash;
658
+ } else if (!applicantRequester && data.applicant_hash !== "") {
659
+ instance.setValue(instance.defaultValue);
660
+ data.applicant_hash = "";
661
+ }
662
+ }
663
+ }
664
+
665
+ function createFormioHelper(options = {}) {
666
+ return new FormioHelper(options);
667
+ }
668
+
669
+ module.exports = FormioHelper;
670
+ module.exports.FormioHelper = FormioHelper;
671
+ module.exports.createFormioHelper = createFormioHelper;
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@opencitylabs/formio-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Node/browser SDK helper for Form.io APIs",
5
+ "main": "index.js",
6
+ "exports": {
7
+ ".": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "test": "node -e \"require('./index.js')\""
15
+ },
16
+ "keywords": [
17
+ "formio",
18
+ "sdk",
19
+ "node",
20
+ "api"
21
+ ],
22
+ "author": "OpencityLabs",
23
+ "license": "ISC",
24
+ "dependencies": {
25
+ "axios": "^1.13.5",
26
+ "lodash.get": "^4.4.2"
27
+ }
28
+ }