@yomotech/view-toolkit 0.0.1 → 0.0.3

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 (40) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +36 -22
  3. package/src/lib/app-version.ts +2 -0
  4. package/src/lib/containers/card/card.component.css +0 -0
  5. package/src/lib/containers/card/card.component.html +449 -0
  6. package/src/lib/containers/card/card.component.ts +471 -0
  7. package/src/lib/containers/form/form.component.css +0 -0
  8. package/src/lib/containers/form/form.component.html +1 -0
  9. package/src/lib/containers/form/form.component.ts +12 -0
  10. package/src/lib/containers/grid/grid.component.css +0 -0
  11. package/src/lib/containers/grid/grid.component.html +1 -0
  12. package/src/lib/containers/grid/grid.component.ts +12 -0
  13. package/src/lib/containers/pagination/pagination.component.css +0 -0
  14. package/src/lib/containers/pagination/pagination.component.html +1 -0
  15. package/src/lib/containers/pagination/pagination.component.ts +12 -0
  16. package/src/lib/services/app-version-reset.service.ts +59 -0
  17. package/src/lib/services/config.service.ts +128 -0
  18. package/src/lib/services/couchdb.ts +224 -0
  19. package/src/lib/services/mobile-keycloak.service.ts +116 -0
  20. package/src/lib/services/network-log.service.ts +27 -0
  21. package/src/lib/services/notification.service.ts +26 -0
  22. package/src/lib/services/pouchdb.service.ts +183 -0
  23. package/src/lib/services/request.service.ts +125 -0
  24. package/src/lib/services/utility.service.ts +593 -0
  25. package/src/lib/view-toolkit.module.ts +25 -0
  26. package/src/lib/view-toolkit.spec.ts +23 -0
  27. package/src/lib/view-toolkit.ts +15 -0
  28. package/src/public-api.ts +22 -0
  29. package/tsconfig.lib.json +17 -0
  30. package/tsconfig.lib.prod.json +11 -0
  31. package/tsconfig.spec.json +15 -0
  32. package/fesm2022/abdulahad-view-toolkit.mjs +0 -94
  33. package/fesm2022/abdulahad-view-toolkit.mjs.map +0 -1
  34. package/fesm2022/abdulahad061-view-toolkit.mjs +0 -94
  35. package/fesm2022/abdulahad061-view-toolkit.mjs.map +0 -1
  36. package/fesm2022/yomotech-view-toolkit.mjs +0 -95
  37. package/fesm2022/yomotech-view-toolkit.mjs.map +0 -1
  38. package/types/abdulahad-view-toolkit.d.ts +0 -33
  39. package/types/abdulahad061-view-toolkit.d.ts +0 -33
  40. package/types/yomotech-view-toolkit.d.ts +0 -30
@@ -0,0 +1,128 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { UtilityService } from './utility.service';
3
+ import { Platform } from '@angular/cdk/platform';
4
+ import { Couchdb } from './couchdb';
5
+
6
+ @Injectable({
7
+ providedIn: 'root'
8
+ })
9
+ export class ConfigService {
10
+ private config: any;
11
+ private skipOkta = false;
12
+ private keycloakConfig: any;
13
+
14
+ constructor(private couchdbService: Couchdb, private utilityService: UtilityService, private platform: Platform) { }
15
+
16
+ async loadOktaConfig(projectName: string): Promise<void> {
17
+ try {
18
+ let tenantKey: any = "";
19
+ let tenantName: any = "";
20
+ if (this.platform.ANDROID || this.platform.IOS) {
21
+ tenantKey = await this.utilityService.getLocalStorage('tenantKey');
22
+ tenantName = await this.utilityService.getLocalStorage('tenantName');
23
+ } else {
24
+ tenantKey = await this.utilityService.getStorage('tenantKey');
25
+ tenantName = await this.utilityService.getStorage('tenantName');
26
+ }
27
+ this.config = await this.couchdbService.getFileContentFromCouchDb(
28
+ `${tenantName}_${projectName}`,
29
+ 'environments/environment.json'
30
+ );
31
+ if (this.platform.ANDROID || this.platform.IOS) {
32
+ if (this.config.environment.login.mobile) {
33
+ this.setConfiguration(this.config.environment.login.mobile);
34
+ } else {
35
+ this.setConfiguration(null);
36
+ }
37
+ } else {
38
+ if (this.config.environment.login.web) {
39
+ this.setConfiguration(this.config.environment.login.web);
40
+ } else {
41
+ this.setConfiguration(null);
42
+ }
43
+ }
44
+
45
+ } catch (err) {
46
+ console.error('Error loading config:', err);
47
+ }
48
+ }
49
+
50
+ setConfiguration(config: any) {
51
+ if (config) {
52
+ this.config = config;
53
+ } else {
54
+ this.config = {
55
+ issuer: 'http://dummy',
56
+ clientId: 'dummy',
57
+ redirectUri: 'http://dummy',
58
+ pkce: true,
59
+ };
60
+ }
61
+ }
62
+
63
+ skipOktaConfig() {
64
+ this.skipOkta = true;
65
+ }
66
+
67
+ get oktaConfig() {
68
+ if (this.skipOkta) {
69
+ return null;
70
+ }
71
+ return this.config;
72
+ }
73
+
74
+ async loadKeycloakConfig(projectName: string): Promise<void> {
75
+ let tenantKey: any = "";
76
+ let tenantName: any = "";
77
+ if (this.platform.ANDROID || this.platform.IOS) {
78
+ tenantKey = await this.utilityService.getLocalStorage('tenantKey');
79
+ tenantName = await this.utilityService.getLocalStorage('tenantName');
80
+ } else {
81
+ tenantKey = await this.utilityService.getStorage('tenantKey');
82
+ tenantName = await this.utilityService.getStorage('tenantName');
83
+ }
84
+ const env = await this.couchdbService.getFileContentFromCouchDb(
85
+ `${tenantName}_${projectName}`,
86
+ 'environments/environment.json'
87
+ );
88
+ if (env?.environment?.login.type === "keycloak") {
89
+ if (this.platform.ANDROID || this.platform.IOS) {
90
+ this.keycloakConfig = {
91
+ issuer: "https://keycloak-poc.wilken.de/realms/p5",
92
+ clientId: "mobile",
93
+ redirectUri: "de.yomotech:/login/callback",
94
+ logoutRedirectUri: "de.yomotech:/login/callback",
95
+ scope: "openid profile email",
96
+ responseType: "code"
97
+ }
98
+ } else {
99
+ this.keycloakConfig = {
100
+ issuer: env?.environment?.login?.web?.issuer ?? 'http://dummy',
101
+ clientId: env?.environment?.login?.web?.clientId ?? 'dummy',
102
+ redirectUri: window.location.origin,
103
+ responseType: 'code',
104
+ scope: 'openid profile email',
105
+ showDebugInformation: true
106
+ };
107
+ }
108
+ } else {
109
+ this.keycloakConfig = {};
110
+ }
111
+ }
112
+
113
+ getKeycloakConfig() {
114
+ return this.keycloakConfig;
115
+ }
116
+
117
+ getKeycloakMobileConfig() {
118
+ if (!this.keycloakConfig) return null;
119
+ return {
120
+ issuer: this.keycloakConfig?.issuer,
121
+ clientId: this.keycloakConfig?.clientId,
122
+ redirectUri: this.keycloakConfig.redirectUri,
123
+ logoutRedirectUri: this.keycloakConfig.logoutRedirectUri,
124
+ scope: this.keycloakConfig.scope,
125
+ responseType: this.keycloakConfig.responseType
126
+ };
127
+ }
128
+ }
@@ -0,0 +1,224 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { UtilityService } from './utility.service';
3
+
4
+ @Injectable({
5
+ providedIn: 'root',
6
+ })
7
+ export class Couchdb {
8
+ private readonly couchDbBaseUrl = 'https://couchdb.yomotech.de';
9
+
10
+ constructor(private utilityService: UtilityService) { }
11
+
12
+ async getTenantDatabases(tenantKey: any): Promise<any[]> {
13
+ if (!tenantKey || !tenantKey.trim()) {
14
+ throw new Error('Missing tenantKey');
15
+ }
16
+
17
+ const response = await fetch(`https://couchdb.yomotech.de/tenant-dbs`, {
18
+ method: 'GET',
19
+ headers: {
20
+ Accept: 'application/json',
21
+ 'X-Tenant-Key': tenantKey.trim(),
22
+ },
23
+ });
24
+
25
+ const data = await this.parseJsonOrText(response);
26
+
27
+ if (!response.ok) {
28
+ throw new Error(
29
+ `GET /tenant-dbs -> ${response.status} ${this.stringifyErrorDetails(data)}`
30
+ );
31
+ }
32
+
33
+ return data;
34
+ }
35
+
36
+ private async parseJsonOrText(response: Response): Promise<any> {
37
+ const rawText = await response.text();
38
+
39
+ if (!rawText) return null;
40
+
41
+ try {
42
+ return JSON.parse(rawText);
43
+ } catch {
44
+ return rawText;
45
+ }
46
+ }
47
+
48
+ private stringifyErrorDetails(data: any): string {
49
+ if (typeof data === 'string') return data;
50
+
51
+ try {
52
+ return JSON.stringify(data);
53
+ } catch {
54
+ return 'Unknown error';
55
+ }
56
+ }
57
+
58
+ async getFileContentFromCouchDb(
59
+ couchDbName: string,
60
+ filePath: string
61
+ ): Promise<any | null> {
62
+ try {
63
+
64
+ const dbName = couchDbName;
65
+ console.log('dbName: ', dbName);
66
+ console.log('filePath: ', filePath);
67
+ const docId = filePath?.trim();
68
+ if (!docId) {
69
+ console.error('Missing file path');
70
+ return null;
71
+ }
72
+
73
+ const response = await fetch(this.getDocumentUrl(dbName, docId), {
74
+ method: 'GET',
75
+ headers: await this.getReadHeaders(),
76
+ });
77
+
78
+ const doc = await this.parseJsonOrText(response);
79
+
80
+ if (!response.ok) {
81
+ console.error(
82
+ `Failed to fetch file "${docId}" from "${dbName}": ${response.status}`,
83
+ doc
84
+ );
85
+ return null;
86
+ }
87
+
88
+ if (!doc?.data) {
89
+ console.error(`Document "${docId}" does not contain file data`);
90
+ return null;
91
+ }
92
+
93
+ // do not decode images as utf8 text
94
+ if (this.isImageFile(doc.name || doc.path || docId, doc.contentType)) {
95
+ return {
96
+ type: 'base64file',
97
+ mimeType: doc.contentType || this.getImageMimeType(docId),
98
+ base64: doc.data,
99
+ };
100
+ }
101
+
102
+ const decodedContent = this.fromBase64(doc.data);
103
+
104
+ if (this.isJsonFile(doc.name || doc.path || docId, doc.contentType)) {
105
+ try {
106
+ return JSON.parse(decodedContent);
107
+ } catch (jsonError) {
108
+ console.error(`Failed to parse JSON for file "${docId}"`, jsonError);
109
+ return decodedContent;
110
+ }
111
+ }
112
+
113
+ return decodedContent;
114
+ } catch (error) {
115
+ console.error(
116
+ `Unexpected error while fetching file "${filePath}" from "${couchDbName}"`,
117
+ error
118
+ );
119
+ return null;
120
+ }
121
+ }
122
+
123
+ private isImageFile(fileNameOrPath: string, contentType?: string): boolean {
124
+ const lower = (fileNameOrPath || '').toLowerCase();
125
+
126
+ return (
127
+ lower.endsWith('.png') ||
128
+ lower.endsWith('.jpg') ||
129
+ lower.endsWith('.jpeg') ||
130
+ lower.endsWith('.ico') ||
131
+ lower.endsWith('.svg') ||
132
+ (contentType || '').startsWith('image/')
133
+ );
134
+ }
135
+
136
+ private getImageMimeType(fileNameOrPath: string): string {
137
+ const lower = (fileNameOrPath || '').toLowerCase();
138
+
139
+ if (lower.endsWith('.png')) return 'image/png';
140
+ if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
141
+ if (lower.endsWith('.ico')) return 'image/x-icon';
142
+ if (lower.endsWith('.svg')) return 'image/svg+xml';
143
+
144
+ return 'image/png';
145
+ }
146
+
147
+ private fromBase64(base64: string): string {
148
+ try {
149
+ return decodeURIComponent(escape(atob(base64)));
150
+ } catch {
151
+ return atob(base64);
152
+ }
153
+ }
154
+
155
+ private isJsonFile(fileNameOrPath: string, contentType?: string): boolean {
156
+ const lower = (fileNameOrPath || '').toLowerCase();
157
+ return lower.endsWith('.json') || contentType === 'application/json';
158
+ }
159
+
160
+ private getDocumentUrl(dbName: string, docId: string): string {
161
+ return `${this.getDbUrl(dbName)}/${encodeURIComponent(docId)}`;
162
+ }
163
+
164
+ private getDbUrl(dbName: string): string {
165
+ return `${this.couchDbBaseUrl}/${dbName}`;
166
+ }
167
+
168
+ private async getReadHeaders(): Promise<HeadersInit> {
169
+ return {
170
+ Accept: 'application/json',
171
+ ...(await this.buildTenantHeaders()),
172
+ };
173
+ }
174
+
175
+ private async buildTenantHeaders(): Promise<HeadersInit> {
176
+ let tenantKey: any = await this.utilityService.getStorage('tenantKey');
177
+ let tenantName: any = await this.utilityService.getStorage('tenantName');
178
+
179
+ if (typeof tenantKey !== 'string' || !tenantKey.trim()) {
180
+ return {
181
+ 'X-Tenant-Key': tenantKey.trim(),
182
+ };
183
+ } else if (typeof tenantName !== 'string' || !tenantName.trim()) {
184
+ return {
185
+ 'X-Tenant': tenantName.trim(),
186
+ };
187
+ }
188
+ throw new Error('Missing tenantKey');
189
+
190
+ }
191
+
192
+ async getImageSrcFromCouchDb(
193
+ couchDbName: string,
194
+ filePath: string
195
+ ): Promise<string> {
196
+ const dbName = couchDbName;
197
+ const docId = filePath?.trim();
198
+
199
+ if (!docId) {
200
+ throw new Error('Missing file path');
201
+ }
202
+
203
+ const response = await fetch(this.getDocumentUrl(dbName, docId), {
204
+ method: 'GET',
205
+ headers: await this.getReadHeaders(),
206
+ });
207
+
208
+ const doc = await this.parseJsonOrText(response);
209
+
210
+ if (!response.ok) {
211
+ throw new Error(
212
+ `Failed to fetch image "${docId}" from "${dbName}": ${response.status} ${this.stringifyErrorDetails(doc)}`
213
+ );
214
+ }
215
+
216
+ if (!doc?.data) {
217
+ throw new Error(`Document "${docId}" does not contain image data`);
218
+ }
219
+
220
+ const mimeType = doc.contentType || 'image/png';
221
+
222
+ return `data:${mimeType};base64,${doc.data}`;
223
+ }
224
+ }
@@ -0,0 +1,116 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { ConfigService } from './config.service';
3
+ import { Capacitor } from '@capacitor/core';
4
+ import { App } from '@capacitor/app';
5
+ import { Browser } from '@capacitor/browser';
6
+ import { OAuth2Client } from '@byteowls/capacitor-oauth2';
7
+ import { HttpClient, HttpHeaders } from '@angular/common/http';
8
+ import { Platform } from '@angular/cdk/platform';
9
+ import { UtilityService } from './utility.service';
10
+
11
+
12
+ export interface MobileAuthTokens {
13
+ access_token: string;
14
+ refresh_token?: string;
15
+ id_token?: string;
16
+ expires_in?: number;
17
+ token_type?: string;
18
+ }
19
+
20
+
21
+ @Injectable({
22
+ providedIn: 'root'
23
+ })
24
+ export class MobileKeycloakService {
25
+
26
+ private tokens: MobileAuthTokens | any = null;
27
+ constructor(private config: ConfigService, private http: HttpClient, private utilityService: UtilityService, private platform: Platform) {
28
+ if (Capacitor.isNativePlatform()) {
29
+ App.addListener('appUrlOpen', async (url: any) => {
30
+ // Nothing to do here; capacitor-oauth2 resolves internally.
31
+ // Keep listener to satisfy deep link expectations.
32
+ });
33
+ }
34
+ }
35
+
36
+ async login(): Promise<any> {
37
+ if (!Capacitor.isNativePlatform()) return null;
38
+ const cfg: any = this.config.getKeycloakMobileConfig();
39
+ const discoveryUrl = `${cfg.issuer}/.well-known/openid-configuration`;
40
+ const discovery: any = await (await fetch(discoveryUrl)).json();
41
+ const result = await OAuth2Client.authenticate({
42
+ appId: cfg.clientId, // <--- Keycloak clientId goes here
43
+ authorizationBaseUrl: discovery.authorization_endpoint,
44
+ accessTokenEndpoint: discovery.token_endpoint,
45
+ responseType: 'code',
46
+ redirectUrl: cfg.redirectUri,
47
+ scope: cfg.scope,
48
+ pkceEnabled: true,
49
+ logsEnabled: true
50
+ });
51
+ console.log('result-KEyClaok: ', result);
52
+ this.tokens = {
53
+ access_token: result?.access_token_response?.access_token ? result?.access_token_response?.access_token : result.access_token,
54
+ refresh_token: result?.access_token_response?.refresh_token,
55
+ id_token: result?.access_token_response?.id_token,
56
+ expires_in: result?.access_token_response?.expires_at ? result?.access_token_response?.expires_at : result?.access_token_response?.expires_in,
57
+ token_type: result?.access_token_response?.token_type
58
+ };
59
+ let keycloakLoginUser = "";
60
+ if (this.tokens.access_token) {
61
+ keycloakLoginUser = await this.getUserInfo(this.tokens.access_token, cfg.issuer);
62
+ console.log('keycloakLoginUser: ', keycloakLoginUser);
63
+ if (keycloakLoginUser) {
64
+ this.tokens['issuer'] = cfg.issuer;
65
+ this.utilityService.setLocalStorage("KeycloakInfo", this.tokens);
66
+ if (this.platform.ANDROID || this.platform.IOS) {
67
+ window.location.reload();
68
+ }
69
+ }
70
+ }
71
+ return keycloakLoginUser;
72
+ }
73
+
74
+ async getUserInfo(accessToken: any, issuer: any): Promise<any> {
75
+ try {
76
+ const headers = new HttpHeaders({
77
+ Authorization: `Bearer ${accessToken}`,
78
+ });
79
+ const url = `${issuer}/protocol/openid-connect/userinfo`;
80
+ return await this.http.get(url, { headers }).toPromise();
81
+ } catch (err) {
82
+ console.error('Error fetching user info:', err);
83
+ throw err;
84
+ }
85
+ }
86
+
87
+ getAccessToken(): string | null {
88
+ return this.tokens?.access_token ?? null;
89
+ }
90
+
91
+ getRefreshToken(): string | null {
92
+ return this.tokens?.refresh_token ?? null;
93
+ }
94
+
95
+ async logout(): Promise<void> {
96
+ const keycloakInfo: any = await this.utilityService.getLocalStorage('KeycloakInfo');
97
+ const cfg: any = this.config.getKeycloakMobileConfig();
98
+ const idToken = keycloakInfo?.keycloakInfo?.id_token;
99
+ const accessToken = keycloakInfo?.keycloakInfo?.access_token;
100
+ if (!idToken || !accessToken) {
101
+ console.warn('No tokens found for logout');
102
+ return;
103
+ }
104
+ const endSessionUrl =
105
+ `${cfg.issuer}/protocol/openid-connect/logout` +
106
+ `?id_token_hint=${encodeURIComponent(idToken)}` +
107
+ `&client_id=${encodeURIComponent(cfg.clientId)}` +
108
+ `&post_logout_redirect_uri=${encodeURIComponent(cfg.logoutRedirectUri)}`;
109
+ console.log('Logout URL:', endSessionUrl);
110
+ await Browser.open({ url: endSessionUrl });
111
+ this.tokens = null;
112
+ await this.utilityService.removeLocalStorage('KeycloakInfo');
113
+ window.location.reload();
114
+ }
115
+
116
+ }
@@ -0,0 +1,27 @@
1
+ import { Injectable } from '@angular/core';
2
+
3
+ @Injectable({
4
+ providedIn: 'root'
5
+ })
6
+ export class NetworkLogService {
7
+
8
+ private logs: any[] = [];
9
+
10
+ addLog(log: any) {
11
+ this.logs.unshift(log); // latest first
12
+ localStorage.setItem('networkLogs', JSON.stringify(this.logs));
13
+ }
14
+
15
+ getLogsByDate(date: string): any[] {
16
+ return this.logs.filter(l => l.date === date);
17
+ }
18
+
19
+ getAll(): any[] {
20
+ return [...this.logs];
21
+ }
22
+
23
+ clear() {
24
+ this.logs = [];
25
+ localStorage.removeItem('networkLogs');
26
+ }
27
+ }
@@ -0,0 +1,26 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { ToastrService } from 'ngx-toastr';
3
+
4
+ @Injectable({
5
+ providedIn: 'root'
6
+ })
7
+ export class NotificationService {
8
+
9
+ constructor(public toastr: ToastrService) { }
10
+
11
+ success(message: string, time: number = 5000) {
12
+ this.toastr.success(message, "", { messageClass: 'toaster-font', titleClass: 'toaster-font', closeButton: true, timeOut: time });
13
+ }
14
+
15
+ info(message: string, time: number = 5000) {
16
+ this.toastr.info(message, "", { messageClass: 'toaster-font', titleClass: 'toaster-font', closeButton: true, timeOut: time });
17
+ }
18
+
19
+ warning(message: string, time: number = 20000) {
20
+ this.toastr.warning(message, "", { messageClass: 'toaster-font', titleClass: 'toaster-font', closeButton: true, timeOut: time });
21
+ }
22
+
23
+ error(error: string, time: number = 20000, message?: string,) {
24
+ this.toastr.error(error, message, { messageClass: 'toaster-font', titleClass: 'toaster-font', closeButton: true, timeOut: time });
25
+ }
26
+ }
@@ -0,0 +1,183 @@
1
+ import { Injectable } from '@angular/core';
2
+ import PouchDB from 'pouchdb';
3
+ import JSZip from 'jszip';
4
+
5
+ @Injectable({
6
+ providedIn: 'root'
7
+ })
8
+ export class PouchdbService {
9
+ private db = new PouchDB("");
10
+
11
+ async uploadAndStoreZip(zipFile: File, couchDbURl?: any): Promise<void> {
12
+ const zip = await JSZip.loadAsync(zipFile);
13
+ const fileName = zipFile?.name?.replace('.zip', '');
14
+ const envFilePath = `${fileName}/environments/environment.json`;
15
+ const envFile = zip.file(envFilePath);
16
+ if (envFile && !couchDbURl) {
17
+ const envFileContent = await envFile.async('string');
18
+ try {
19
+ const jsonData = JSON.parse(envFileContent);
20
+ if (jsonData?.environment?.couchdbUrl) {
21
+ this.db = new PouchDB(jsonData.environment.couchdbUrl);
22
+ }
23
+ } catch (e) {
24
+ console.error('Invalid JSON in environment.json:', e);
25
+ }
26
+ } else {
27
+ this.db = new PouchDB(couchDbURl);
28
+ }
29
+
30
+ // Proceed to upload all files
31
+ const savePromises: Promise<any>[] = [];
32
+ zip.forEach((relativePath: any, file: any) => {
33
+ if (!file.dir) {
34
+ savePromises.push(
35
+ file.async('base64').then((content: any) => {
36
+ const docId = relativePath.toString();
37
+ console.log('docId: ', docId);
38
+ return this.db.put({
39
+ _id: docId,
40
+ data: content,
41
+ type: 'file',
42
+ contentType: this.getMimeType(file.name)
43
+ }).catch((err: any) => {
44
+ if (err.status === 409) {
45
+ return this.db.get(docId).then((existing: any) => {
46
+ return this.db.put({
47
+ ...existing,
48
+ data: content,
49
+ contentType: this.getMimeType(file.name)
50
+ });
51
+ });
52
+ }
53
+ throw err;
54
+ });
55
+ })
56
+ );
57
+ }
58
+ });
59
+ await Promise.all(savePromises);
60
+ }
61
+
62
+ async getFile(path: string, remoteDB: any): Promise<Blob> {
63
+ try {
64
+ if (remoteDB) {
65
+ this.db = new PouchDB(remoteDB);
66
+ }
67
+ const doc: any = await this.db.get(path);
68
+ return this.decodeBlob(doc);
69
+ } catch (err: any) {
70
+ if (err.status === 404 && remoteDB) {
71
+ try {
72
+ const remoteDoc: any = await this.db.get(path);
73
+ return this.decodeBlob(remoteDoc);
74
+ } catch (remoteErr) {
75
+ throw remoteErr;
76
+ }
77
+ } else {
78
+ throw err;
79
+ }
80
+ }
81
+ }
82
+
83
+ private decodeBlob(doc: any): Blob {
84
+ const byteCharacters = atob(doc.data);
85
+ const byteNumbers = new Array(byteCharacters.length).fill(0).map((_, i) => byteCharacters.charCodeAt(i));
86
+ const byteArray = new Uint8Array(byteNumbers);
87
+ return new Blob([byteArray], { type: doc.contentType });
88
+ }
89
+
90
+ async getTextFile(path: string, remotedbUrl: any): Promise<any> {
91
+ const blob = await this.getFile(path, remotedbUrl);
92
+ if (blob?.type && blob?.type === 'application/x-x509-ca-cert') {
93
+ const base64Der = await this.blobToBase64(blob);
94
+ return base64Der;
95
+ }
96
+ if (blob?.type && blob?.type.startsWith('image/')) {
97
+ return URL.createObjectURL(blob);
98
+ } else if (blob?.type && blob?.type === 'application/json') {
99
+ const text = await blob.text();
100
+ return JSON.parse(text);
101
+ } else {
102
+ return await blob.text();
103
+ }
104
+ }
105
+
106
+ blobToBase64(blob: Blob): Promise<string> {
107
+ return new Promise((resolve, reject) => {
108
+ const reader = new FileReader();
109
+ reader.onloadend = () => {
110
+ const base64 = (reader.result as string).split(',')[1];
111
+ resolve(base64);
112
+ };
113
+ reader.onerror = reject;
114
+ reader.readAsDataURL(blob);
115
+ });
116
+ }
117
+
118
+ getMimeType(fileName: string): string {
119
+ const extension = fileName.split('.').pop()?.toLowerCase();
120
+ const mimeMap: any = {
121
+ 'json': 'application/json',
122
+ 'css': 'text/css',
123
+ 'scss': 'text/x-scss',
124
+ 'jpg': 'image/jpeg',
125
+ 'jpeg': 'image/jpeg',
126
+ 'png': 'image/png',
127
+ 'ico': 'image/x-icon',
128
+ 'svg': 'image/svg+xml',
129
+ 'html': 'text/html',
130
+ 'js': 'application/javascript',
131
+ 'ts': 'application/typescript',
132
+ 'txt': 'text/plain',
133
+ 'der': 'application/x-x509-ca-cert'
134
+ };
135
+ return mimeMap[extension || ''] || 'application/octet-stream';
136
+ }
137
+
138
+ async getAllDatabasesWithContent(couchdbBaseUrl: any): Promise<{ databaseName: string; content: boolean }[]> {
139
+ const connStr = couchdbBaseUrl;
140
+ const urlObj = new URL(connStr);
141
+ const credentials = `${urlObj.username}:${urlObj.password}`;
142
+ const originalPortMatch = connStr.match(/:(\d+)(?=\/|$)/);
143
+ const port = originalPortMatch ? originalPortMatch[1] : "";
144
+ const baseUrl = `${urlObj.protocol}//${urlObj.hostname}${port ? `:${port}` : ""}`;
145
+ const authHeader = {
146
+ 'Authorization': 'Basic ' + btoa(`${credentials}`)
147
+ };
148
+ const response = await fetch(`${baseUrl}/_all_dbs`, {
149
+ headers: authHeader
150
+ });
151
+
152
+ if (!response.ok) {
153
+ throw new Error(`Error fetching databases: ${response.statusText}`);
154
+ }
155
+
156
+ const dbNames: string[] = await response.json();
157
+ const results: { databaseName: string; content: boolean }[] = [];
158
+ for (const dbName of dbNames) {
159
+ try {
160
+ const docsResponse = await fetch(`${baseUrl}/${dbName}/_all_docs?limit=1`, {
161
+ headers: authHeader
162
+ });
163
+
164
+ if (!docsResponse.ok) {
165
+ results.push({ databaseName: dbName, content: false });
166
+ continue;
167
+ }
168
+
169
+ const docsData = await docsResponse.json();
170
+ results.push({
171
+ databaseName: dbName,
172
+ content: docsData.total_rows > 0
173
+ });
174
+
175
+ } catch (err) {
176
+ console.log('err: ', err);
177
+ results.push({ databaseName: dbName, content: false });
178
+ }
179
+ }
180
+ return results;
181
+ }
182
+
183
+ }