underpost 3.2.9 → 3.2.10
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/.github/workflows/npmpkg.ci.yml +1 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -0
- package/.vscode/settings.json +10 -5
- package/CHANGELOG.md +122 -1
- package/CLI-HELP.md +22 -7
- package/README.md +37 -8
- package/bin/build.js +26 -9
- package/bin/deploy.js +20 -21
- package/bin/file.js +31 -13
- package/bin/index.js +2 -1
- package/bin/vs.js +1 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -4
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
- package/manifests/kind-config-dev.yaml +8 -0
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/package.json +27 -12
- package/scripts/k3s-node-setup.sh +28 -9
- package/src/api/core/core.router.js +19 -14
- package/src/api/core/core.service.js +5 -5
- package/src/api/default/default.router.js +22 -18
- package/src/api/default/default.service.js +5 -5
- package/src/api/document/document.router.js +28 -23
- package/src/api/document/document.service.js +100 -23
- package/src/api/file/file.router.js +19 -13
- package/src/api/file/file.service.js +9 -7
- package/src/api/test/test.router.js +17 -12
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +5 -4
- package/src/api/user/user.router.js +297 -288
- package/src/api/user/user.service.js +100 -35
- package/src/cli/baremetal.js +20 -11
- package/src/cli/cluster.js +196 -55
- package/src/cli/db.js +59 -60
- package/src/cli/deploy.js +273 -159
- package/src/cli/fs.js +3 -1
- package/src/cli/index.js +16 -9
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +217 -135
- package/src/cli/release.js +289 -131
- package/src/cli/repository.js +58 -7
- package/src/cli/run.js +152 -25
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +4 -0
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Worker.js +162 -363
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +120 -20
- package/src/db/mongo/MongoBootstrap.js +587 -0
- package/src/db/mongo/MongooseDB.js +126 -22
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/wp/Wp.js +8 -5
- package/src/server/auth.js +2 -2
- package/src/server/client-build-docs.js +1 -1
- package/src/server/client-build.js +94 -129
- package/src/server/conf.js +20 -65
- package/src/server/process.js +180 -19
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +12 -4
- package/src/ws/IoInterface.js +16 -16
- package/src/ws/core/channels/core.ws.chat.js +11 -11
- package/src/ws/core/channels/core.ws.mailer.js +29 -29
- package/src/ws/core/channels/core.ws.stream.js +19 -19
- package/src/ws/core/core.ws.connection.js +8 -8
- package/src/ws/core/core.ws.server.js +6 -5
- package/src/ws/default/channels/default.ws.main.js +10 -10
- package/src/ws/default/default.ws.connection.js +4 -4
- package/src/ws/default/default.ws.server.js +4 -3
- package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
- package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
- /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
- /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
- /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/client/sw/core.sw.js
CHANGED
|
@@ -8,48 +8,108 @@ import { ExpirationPlugin } from 'workbox-expiration';
|
|
|
8
8
|
import { BackgroundSyncPlugin } from 'workbox-background-sync';
|
|
9
9
|
|
|
10
10
|
// ─── Runtime config injected by client-build.js ───────────────────────────────
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
// OFFLINE_URL and MAINTENANCE_URL are absolute (proxy-prefixed) index.html paths
|
|
12
|
+
// resolved at build time from the SSR config's `fallbacks` map.
|
|
13
|
+
const payload = self.renderPayload || {};
|
|
14
|
+
const CACHE_PREFIX = payload.CACHE_PREFIX || 'engine-core';
|
|
15
|
+
const PRE_CACHED_RESOURCES = Array.isArray(payload.PRE_CACHED_RESOURCES) ? payload.PRE_CACHED_RESOURCES : [];
|
|
16
|
+
const OFFLINE_URL = payload.OFFLINE_URL || '/offline/index.html';
|
|
17
|
+
const MAINTENANCE_URL = payload.MAINTENANCE_URL || '/maintenance/index.html';
|
|
18
|
+
|
|
19
|
+
// Dedicated cache for fallback pages so they're available even if precacheAndRoute
|
|
20
|
+
// fails (e.g. a precache manifest entry returns non-200 during install).
|
|
21
|
+
const FALLBACK_CACHE = `${CACHE_PREFIX}-fallbacks`;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ultimate inline HTML fallback used ONLY when the network is unavailable and
|
|
25
|
+
* the custom SSR pages (NoNetworkConnection / Maintenance views from config)
|
|
26
|
+
* have not yet been cached. Once cached on first successful visit, the custom
|
|
27
|
+
* pages take over.
|
|
28
|
+
* @param {'offline'|'maintenance'} kind
|
|
29
|
+
* @returns {Response}
|
|
30
|
+
*/
|
|
31
|
+
const inlineFallback = (kind) => {
|
|
32
|
+
const title = kind === 'offline' ? 'Offline' : 'Maintenance';
|
|
33
|
+
const icon = kind === 'offline' ? '🌐' : '🔧';
|
|
34
|
+
const msg =
|
|
35
|
+
kind === 'offline'
|
|
36
|
+
? 'You are offline. Please check your internet connection and try again.'
|
|
37
|
+
: 'The server is under maintenance. We\'ll be back shortly.';
|
|
38
|
+
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><title>${title}</title><style>body{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f5f5f5;color:#333;padding:1rem;text-align:center}h1{font-size:2rem;margin-bottom:.5rem}p{font-size:1rem;color:#666;max-width:24rem}</style></head><body><h1>${icon} ${title}</h1><p>${msg}</p></body></html>`;
|
|
39
|
+
return new Response(html, {
|
|
40
|
+
status: 200,
|
|
41
|
+
statusText: 'OK',
|
|
42
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
43
|
+
});
|
|
24
44
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns a Response for the cached fallback page.
|
|
48
|
+
* Tries (in order):
|
|
49
|
+
* 1. Dedicated FALLBACK_CACHE (primary & secondary URL)
|
|
50
|
+
* 2. Workbox precache (primary & secondary URL)
|
|
51
|
+
* 3. If online, fetches the custom SSR page from the server (caches it for next time)
|
|
52
|
+
* 4. Ultimate inline HTML string (never a browser error page)
|
|
53
|
+
*
|
|
54
|
+
* @param {'offline'|'maintenance'} kind
|
|
55
|
+
* @returns {Promise<Response>}
|
|
56
|
+
*/
|
|
57
|
+
const getFallback = async (kind) => {
|
|
58
|
+
const primary = kind === 'maintenance' ? MAINTENANCE_URL : OFFLINE_URL;
|
|
59
|
+
const secondary = kind === 'maintenance' ? OFFLINE_URL : MAINTENANCE_URL;
|
|
60
|
+
const cache = await caches.open(FALLBACK_CACHE);
|
|
61
|
+
|
|
62
|
+
// ── 1. Try cache (own + precache) ──────────────────────────────────────
|
|
63
|
+
const cached =
|
|
64
|
+
(await cache.match(primary)) ||
|
|
65
|
+
(await matchPrecache(primary)) ||
|
|
66
|
+
(await cache.match(secondary)) ||
|
|
67
|
+
(await matchPrecache(secondary));
|
|
68
|
+
if (cached) return cached;
|
|
69
|
+
|
|
70
|
+
// ── 2. If we appear online, try to fetch the custom SSR page from server ─
|
|
71
|
+
// This handles the case where the install-time caching didn't happen yet
|
|
72
|
+
// (e.g. first install while server was up, but fallback pages weren't cached).
|
|
73
|
+
if (typeof navigator === 'undefined' || navigator.onLine !== false) {
|
|
74
|
+
try {
|
|
75
|
+
const fetchResp = await fetch(primary, { credentials: 'same-origin' });
|
|
76
|
+
if (fetchResp.ok) {
|
|
77
|
+
cache.put(primary, fetchResp.clone()).catch(() => { });
|
|
78
|
+
return fetchResp;
|
|
79
|
+
}
|
|
80
|
+
} catch (_) { }
|
|
81
|
+
try {
|
|
82
|
+
const fetchResp = await fetch(secondary, { credentials: 'same-origin' });
|
|
83
|
+
if (fetchResp.ok) {
|
|
84
|
+
cache.put(secondary, fetchResp.clone()).catch(() => { });
|
|
85
|
+
return fetchResp;
|
|
86
|
+
}
|
|
87
|
+
} catch (_) { }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── 3. Ultimate inline fallback ────────────────────────────────────────
|
|
91
|
+
return inlineFallback(kind);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Background-caches the offline and maintenance SSR pages for future use.
|
|
96
|
+
* Called after each successful navigation so the custom views are available
|
|
97
|
+
* the next time the network is unreachable.
|
|
98
|
+
*/
|
|
99
|
+
const cacheFallbackPages = async () => {
|
|
100
|
+
const cache = await caches.open(FALLBACK_CACHE);
|
|
101
|
+
for (const url of [OFFLINE_URL, MAINTENANCE_URL]) {
|
|
102
|
+
try {
|
|
103
|
+
if (!(await cache.match(url))) {
|
|
104
|
+
const response = await fetch(url, { credentials: 'same-origin' });
|
|
105
|
+
if (response.ok) {
|
|
106
|
+
await cache.put(url, response);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (_) {
|
|
110
|
+
// Network unavailable now — will try again on next navigation.
|
|
111
|
+
}
|
|
45
112
|
}
|
|
46
|
-
return (
|
|
47
|
-
(await cache.match(offlineUrl)) ||
|
|
48
|
-
(await matchPrecache(offlineUrl)) ||
|
|
49
|
-
(await cache.match(maintenanceUrl)) ||
|
|
50
|
-
(await matchPrecache(maintenanceUrl)) ||
|
|
51
|
-
Response.error()
|
|
52
|
-
);
|
|
53
113
|
};
|
|
54
114
|
|
|
55
115
|
// ─── Core setup ───────────────────────────────────────────────────────────────
|
|
@@ -58,17 +118,10 @@ clientsClaim();
|
|
|
58
118
|
|
|
59
119
|
self.addEventListener('install', (event) => {
|
|
60
120
|
self.skipWaiting();
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Try together first; fall back to individual adds so a single failure
|
|
66
|
-
// does not prevent the other page from being cached.
|
|
67
|
-
cache
|
|
68
|
-
.addAll([offlineUrl, maintenanceUrl])
|
|
69
|
-
.catch(() => Promise.all([cache.add(offlineUrl).catch(() => {}), cache.add(maintenanceUrl).catch(() => {})])),
|
|
70
|
-
),
|
|
71
|
-
);
|
|
121
|
+
// Note: install-time caching of fallback pages is unreliable in some browsers.
|
|
122
|
+
// We rely on the post-navigation eager caching in the navigation handler.
|
|
123
|
+
// The inline HTML fallback ensures the user never sees a browser error page
|
|
124
|
+
// before the custom SSR pages are cached.
|
|
72
125
|
});
|
|
73
126
|
|
|
74
127
|
self.addEventListener('activate', (event) => {
|
|
@@ -82,21 +135,17 @@ self.addEventListener('activate', (event) => {
|
|
|
82
135
|
});
|
|
83
136
|
|
|
84
137
|
self.addEventListener('message', (event) => {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
if (payload.status === 'skipWaiting') {
|
|
138
|
+
const { status } = event.data || {};
|
|
139
|
+
if (status === 'skipWaiting') {
|
|
88
140
|
self.skipWaiting();
|
|
89
141
|
return;
|
|
90
142
|
}
|
|
91
|
-
|
|
92
|
-
if (payload.status === 'workbox-reset') {
|
|
143
|
+
if (status === 'workbox-reset') {
|
|
93
144
|
event.waitUntil(
|
|
94
145
|
(async () => {
|
|
95
|
-
const
|
|
96
|
-
await Promise.all(
|
|
97
|
-
|
|
98
|
-
event.ports[0].postMessage({ status: 'workbox-reset-done', deleted: cacheNames.length });
|
|
99
|
-
}
|
|
146
|
+
const names = await caches.keys();
|
|
147
|
+
await Promise.all(names.map((name) => caches.delete(name)));
|
|
148
|
+
event.ports?.[0]?.postMessage({ status: 'workbox-reset-done', deleted: names.length });
|
|
100
149
|
})(),
|
|
101
150
|
);
|
|
102
151
|
}
|
|
@@ -115,13 +164,8 @@ registerRoute(
|
|
|
115
164
|
new StaleWhileRevalidate({
|
|
116
165
|
cacheName: `${CACHE_PREFIX}-assets`,
|
|
117
166
|
plugins: [
|
|
118
|
-
new CacheableResponsePlugin({
|
|
119
|
-
|
|
120
|
-
}),
|
|
121
|
-
new ExpirationPlugin({
|
|
122
|
-
maxEntries: 350,
|
|
123
|
-
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
|
|
124
|
-
}),
|
|
167
|
+
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
|
168
|
+
new ExpirationPlugin({ maxEntries: 350, maxAgeSeconds: 30 * 24 * 60 * 60 }),
|
|
125
169
|
],
|
|
126
170
|
}),
|
|
127
171
|
);
|
|
@@ -133,13 +177,8 @@ registerRoute(
|
|
|
133
177
|
cacheName: `${CACHE_PREFIX}-api-get`,
|
|
134
178
|
networkTimeoutSeconds: 5,
|
|
135
179
|
plugins: [
|
|
136
|
-
new CacheableResponsePlugin({
|
|
137
|
-
|
|
138
|
-
}),
|
|
139
|
-
new ExpirationPlugin({
|
|
140
|
-
maxEntries: 120,
|
|
141
|
-
maxAgeSeconds: 5 * 60, // 5 minutes
|
|
142
|
-
}),
|
|
180
|
+
new CacheableResponsePlugin({ statuses: [0, 200] }),
|
|
181
|
+
new ExpirationPlugin({ maxEntries: 120, maxAgeSeconds: 5 * 60 }),
|
|
143
182
|
],
|
|
144
183
|
}),
|
|
145
184
|
);
|
|
@@ -148,65 +187,88 @@ registerRoute(
|
|
|
148
187
|
registerRoute(
|
|
149
188
|
({ request, url }) => request.method !== 'GET' && url.pathname.includes('/api/'),
|
|
150
189
|
new NetworkOnly({
|
|
151
|
-
plugins: [
|
|
152
|
-
new BackgroundSyncPlugin('api-mutation-queue', {
|
|
153
|
-
maxRetentionTime: 24 * 60, // 24 hours
|
|
154
|
-
}),
|
|
155
|
-
],
|
|
190
|
+
plugins: [new BackgroundSyncPlugin('api-mutation-queue', { maxRetentionTime: 24 * 60 })],
|
|
156
191
|
}),
|
|
157
192
|
);
|
|
158
193
|
|
|
159
|
-
// ─── Navigation
|
|
194
|
+
// ─── Navigation with offline/maintenance dispatcher ─────────────────────────
|
|
195
|
+
//
|
|
196
|
+
// CRITICAL: We use direct fetch() instead of NetworkFirst for navigation so
|
|
197
|
+
// that when the server is unreachable:
|
|
198
|
+
// - NetworkFirst would silently return a STALE cached real page (status 200)
|
|
199
|
+
// making it look like the app still works when it doesn't
|
|
200
|
+
// - fetch() throws on network failure → catch → fallback page
|
|
201
|
+
// - fetch() returns 5xx → maintenance fallback
|
|
202
|
+
//
|
|
203
|
+
// After each successful fetch, we eagerly cache the custom offline/maintenance
|
|
204
|
+
// SSR pages so they're available when the network goes down.
|
|
205
|
+
//
|
|
206
|
+
// The dispatcher logic:
|
|
207
|
+
// 1. If navigator.onLine === false → offline fallback immediately
|
|
208
|
+
// 2. Try navigation preload — if it succeeds (< 500) use it, else fall through
|
|
209
|
+
// 3. Try direct fetch() — if it succeeds (< 500) cache and return it
|
|
210
|
+
// 4. If fetch returned 5xx → maintenance fallback
|
|
211
|
+
// 5. If fetch threw (network down) → fallback based on connectivity
|
|
212
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
160
214
|
registerRoute(
|
|
161
215
|
({ request }) => request.mode === 'navigate',
|
|
162
216
|
async ({ event }) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
new CacheableResponsePlugin({
|
|
168
|
-
statuses: [0, 200],
|
|
169
|
-
}),
|
|
170
|
-
new ExpirationPlugin({
|
|
171
|
-
maxEntries: 60,
|
|
172
|
-
maxAgeSeconds: 12 * 60 * 60, // 12 hours
|
|
173
|
-
}),
|
|
174
|
-
],
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Distinguish server-down (online but unreachable) from no-network (offline).
|
|
178
|
-
// navigator.onLine is false only when the device has no network at all.
|
|
179
|
-
const isOnline = () => typeof navigator !== 'undefined' && navigator.onLine !== false;
|
|
217
|
+
// ── 1. Check browser offline state ────────────────────────────────────
|
|
218
|
+
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
219
|
+
return getFallback('offline');
|
|
220
|
+
}
|
|
180
221
|
|
|
222
|
+
// ── 2. Try navigation preload ────────────────────────────────────────
|
|
181
223
|
try {
|
|
182
224
|
const preload = await event.preloadResponse;
|
|
183
|
-
if (preload) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
225
|
+
if (preload && preload.status < 500) {
|
|
226
|
+
// Fire-and-forget: eagerly cache fallback pages in the background
|
|
227
|
+
// after a successful navigation so they're available when offline.
|
|
228
|
+
cacheFallbackPages().catch(() => { });
|
|
187
229
|
return preload;
|
|
188
230
|
}
|
|
231
|
+
} catch (_) {
|
|
232
|
+
// Preload threw — fall through to direct fetch
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── 3. Try direct fetch to the server ─────────────────────────────────
|
|
236
|
+
try {
|
|
237
|
+
const response = await fetch(event.request);
|
|
238
|
+
|
|
239
|
+
if (response.status >= 500) {
|
|
240
|
+
return getFallback('maintenance');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (response.ok) {
|
|
244
|
+
// Cache the successful response for future navigations
|
|
245
|
+
const cache = await caches.open(`${CACHE_PREFIX}-pages`);
|
|
246
|
+
cache.put(event.request, response.clone()).catch(() => { });
|
|
247
|
+
|
|
248
|
+
// Eagerly cache fallback pages in the background
|
|
249
|
+
cacheFallbackPages().catch(() => { });
|
|
189
250
|
|
|
190
|
-
|
|
191
|
-
if (networkResponse && networkResponse.status >= 500) {
|
|
192
|
-
return getFallbackResponse(true);
|
|
251
|
+
return response;
|
|
193
252
|
}
|
|
194
|
-
|
|
253
|
+
|
|
254
|
+
return response;
|
|
195
255
|
} catch (_) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return
|
|
256
|
+
const reallyOffline =
|
|
257
|
+
typeof navigator !== 'undefined' && navigator.onLine === false;
|
|
258
|
+
return getFallback(reallyOffline ? 'offline' : 'maintenance');
|
|
199
259
|
}
|
|
200
260
|
},
|
|
201
261
|
);
|
|
202
262
|
|
|
203
|
-
// ─── Global catch handler
|
|
263
|
+
// ─── Global catch handler (non-navigation failures) ──────────────────────────
|
|
204
264
|
setCatchHandler(async ({ request }) => {
|
|
205
265
|
if (request.mode === 'navigate') {
|
|
206
|
-
|
|
266
|
+
const reallyOffline =
|
|
267
|
+
typeof navigator !== 'undefined' && navigator.onLine === false;
|
|
268
|
+
return getFallback(reallyOffline ? 'offline' : 'maintenance');
|
|
207
269
|
}
|
|
208
270
|
return new Response(JSON.stringify({ status: 'error', message: 'request failed' }), {
|
|
209
271
|
status: 503,
|
|
210
272
|
headers: { 'Content-Type': 'application/json' },
|
|
211
273
|
});
|
|
212
|
-
});
|
|
274
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { MongooseDB } from './mongo/MongooseDB.js';
|
|
2
2
|
import { loggerFactory } from '../server/logger.js';
|
|
3
|
+
import { getCapVariableName } from '../client/components/core/CommonJs.js';
|
|
4
|
+
import { resolveHostKeyContext } from '../server/conf.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Module for managing and loading various database connections (e.g., Mongoose, MariaDB).
|
|
@@ -22,16 +24,108 @@ class DataBaseProviderService {
|
|
|
22
24
|
* @type {object.<string, object>}
|
|
23
25
|
* @method
|
|
24
26
|
*/
|
|
25
|
-
#instance = {};
|
|
27
|
+
static #instance = {};
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Retrieves the internal instance storage for direct access (used for backward compatibility).
|
|
29
31
|
* @returns {object.<string, object>} The internal connection instance map.
|
|
30
32
|
*/
|
|
31
|
-
get instance() {
|
|
33
|
+
static get instance() {
|
|
32
34
|
return this.#instance;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Retrieves a loaded provider bucket for a context.
|
|
40
|
+
* @param {{host?: string, path?: string}|string} [context={ host: '', path: '' }] - Context object or key.
|
|
41
|
+
* @param {string} [provider='mongoose'] - Provider name.
|
|
42
|
+
* @returns {{models: object, connection: object, close: Function, dbSignature?: string}} Provider bucket.
|
|
43
|
+
* @throws {Error} When the provider is not loaded for the context.
|
|
44
|
+
*/
|
|
45
|
+
static getProvider(context = { host: '', path: '' }, provider = 'mongoose') {
|
|
46
|
+
const key = resolveHostKeyContext(context);
|
|
47
|
+
const entry = this.#instance[key]?.[provider];
|
|
48
|
+
|
|
49
|
+
if (!entry) throw new Error(`Database provider not loaded for context "${key}" (${provider})`);
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the raw DB connection object for a context/provider.
|
|
55
|
+
* @param {{host?: string, path?: string}|string} [context={ host: '', path: '' }] - Context object or key.
|
|
56
|
+
* @param {string} [provider='mongoose'] - Provider name.
|
|
57
|
+
* @returns {object} Provider connection object.
|
|
58
|
+
*/
|
|
59
|
+
static getConnection(context = { host: '', path: '' }, provider = 'mongoose') {
|
|
60
|
+
return this.getProvider(context, provider).connection;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves a loaded model by name for a given context/provider.
|
|
65
|
+
* @param {string} modelName - API/model identifier.
|
|
66
|
+
* @param {{host?: string, path?: string}|string} [context={ host: '', path: '' }] - Context object or key.
|
|
67
|
+
* @param {string} [provider='mongoose'] - Provider name.
|
|
68
|
+
* @returns {object} Loaded model instance.
|
|
69
|
+
* @throws {Error} When the model is not loaded for the context.
|
|
70
|
+
*/
|
|
71
|
+
static getModel(modelName, context = { host: '', path: '' }, provider = 'mongoose') {
|
|
72
|
+
const models = this.getProvider(context, provider).models || {};
|
|
73
|
+
const normalizedModelName = getCapVariableName(modelName);
|
|
74
|
+
|
|
75
|
+
// First try direct key (supports callers passing exact model names).
|
|
76
|
+
let model = models?.[modelName];
|
|
77
|
+
if (!model) {
|
|
78
|
+
model = models?.[normalizedModelName];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!model) {
|
|
82
|
+
// Final fallback: case-insensitive comparison without separators.
|
|
83
|
+
const target = String(modelName || '')
|
|
84
|
+
.replaceAll('-', '')
|
|
85
|
+
.replaceAll('_', '')
|
|
86
|
+
.replaceAll(' ', '')
|
|
87
|
+
.toLowerCase();
|
|
88
|
+
const resolvedModelName = Object.keys(models).find(
|
|
89
|
+
(key) => key.replaceAll('_', '').replaceAll(' ', '').toLowerCase() === target,
|
|
90
|
+
);
|
|
91
|
+
if (resolvedModelName) model = models[resolvedModelName];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!model) {
|
|
95
|
+
throw new Error(`Model not loaded for context "${resolveHostKeyContext(context)}": ${normalizedModelName}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return model;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds a minimal dispatcher bound to a specific context/provider.
|
|
103
|
+
* @param {{host?: string, path?: string}|string} [context={ host: '', path: '' }] - Context object or key.
|
|
104
|
+
* @param {string} [provider='mongoose'] - Provider name.
|
|
105
|
+
* @returns {{getConnection: () => object, getModel: (modelName: string) => object}} Bound accessor helpers.
|
|
106
|
+
*/
|
|
107
|
+
static getDispatcher(context = { host: '', path: '' }, provider = 'mongoose') {
|
|
108
|
+
return {
|
|
109
|
+
getConnection: () => this.getConnection(context, provider),
|
|
110
|
+
getModel: (modelName) => this.getModel(modelName, context, provider),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Builds a stable signature used to detect provider configuration changes.
|
|
116
|
+
* @param {object} [db={}] - Database configuration object.
|
|
117
|
+
* @returns {string} Stringified signature for change detection.
|
|
118
|
+
*/
|
|
119
|
+
static buildDbSignature(db = {}) {
|
|
120
|
+
return JSON.stringify({
|
|
121
|
+
authSource: db.authSource || '',
|
|
122
|
+
host: db.host || '',
|
|
123
|
+
name: db.name || '',
|
|
124
|
+
provider: db.provider || '',
|
|
125
|
+
replicaSet: db.replicaSet || '',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
35
129
|
/**
|
|
36
130
|
* Loads and initializes a database provider based on the configuration.
|
|
37
131
|
* If the connection is already loaded for the given host/path, it returns the existing instance.
|
|
@@ -48,21 +142,30 @@ class DataBaseProviderService {
|
|
|
48
142
|
* @returns {Promise<object|undefined>} A promise that resolves to the initialized provider object
|
|
49
143
|
* or `undefined` on error or if the provider is already loaded.
|
|
50
144
|
*/
|
|
51
|
-
async load(options = { apis: [], host: '', path: '', db: {} }) {
|
|
145
|
+
static async load(options = { apis: [], host: '', path: '', db: {} }) {
|
|
52
146
|
try {
|
|
53
147
|
const { apis, host, path, db } = options;
|
|
54
|
-
const key =
|
|
148
|
+
const key = resolveHostKeyContext({ host, path });
|
|
149
|
+
const dbSignature = DataBaseProviderService.buildDbSignature(db);
|
|
55
150
|
|
|
56
151
|
if (!this.#instance[key]) this.#instance[key] = {};
|
|
152
|
+
if (!db) return undefined;
|
|
57
153
|
|
|
58
|
-
|
|
154
|
+
const currentProvider = this.#instance[key][db.provider];
|
|
155
|
+
if (currentProvider && currentProvider.dbSignature === dbSignature) return currentProvider;
|
|
156
|
+
|
|
157
|
+
if (currentProvider && currentProvider.close) {
|
|
158
|
+
await currentProvider.close();
|
|
159
|
+
delete this.#instance[key][db.provider];
|
|
160
|
+
}
|
|
59
161
|
|
|
60
162
|
// logger.info(`Load ${db.provider} provider`, key);
|
|
61
163
|
switch (db.provider) {
|
|
62
164
|
case 'mongoose':
|
|
63
165
|
{
|
|
64
|
-
const conn = await MongooseDB.connect(db
|
|
166
|
+
const conn = await MongooseDB.connect(db);
|
|
65
167
|
this.#instance[key][db.provider] = {
|
|
168
|
+
dbSignature,
|
|
66
169
|
models: await MongooseDB.loadModels({ conn, apis }),
|
|
67
170
|
connection: conn,
|
|
68
171
|
close: async () => {
|
|
@@ -88,12 +191,12 @@ class DataBaseProviderService {
|
|
|
88
191
|
path: options.path,
|
|
89
192
|
db: options.db
|
|
90
193
|
? {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
194
|
+
provider: options.db.provider,
|
|
195
|
+
name: options.db.name ? '***' : undefined,
|
|
196
|
+
host: options.db.host ? '***' : undefined,
|
|
197
|
+
user: options.db.user ? '***' : undefined,
|
|
198
|
+
password: options.db.password ? '***' : undefined,
|
|
199
|
+
}
|
|
97
200
|
: {},
|
|
98
201
|
};
|
|
99
202
|
logger.error(error.message, { safeOptions });
|
|
@@ -102,12 +205,9 @@ class DataBaseProviderService {
|
|
|
102
205
|
}
|
|
103
206
|
}
|
|
104
207
|
|
|
105
|
-
/**
|
|
106
|
-
* Singleton instance of the DataBaseProviderService class for backward compatibility.
|
|
107
|
-
* @alias DataBaseProvider
|
|
108
|
-
* @memberof DataBaseProviderService
|
|
109
|
-
* @type {DataBaseProviderService}
|
|
110
|
-
*/
|
|
111
|
-
const DataBaseProvider = new DataBaseProviderService();
|
|
112
208
|
|
|
113
|
-
export {
|
|
209
|
+
export {
|
|
210
|
+
DataBaseProviderService as DataBaseProviderClass,
|
|
211
|
+
DataBaseProviderService as default,
|
|
212
|
+
DataBaseProviderService
|
|
213
|
+
};
|