@omen.foundation/node-microservice-runtime 0.1.118 → 0.1.119
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/dist/storage.cjs +50 -10
- package/dist/storage.d.ts +5 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +57 -11
- package/dist/storage.js.map +1 -1
- package/package.json +1 -1
package/dist/storage.cjs
CHANGED
|
@@ -21,6 +21,10 @@ function listRegisteredStorageObjects() {
|
|
|
21
21
|
return Array.from(STORAGE_OBJECT_METADATA.values());
|
|
22
22
|
}
|
|
23
23
|
const CONNECTION_STRING_ENV_PREFIX = 'STORAGE_CONNSTR_';
|
|
24
|
+
const DEFAULT_MONGODB_MAX_POOL_SIZE = 10;
|
|
25
|
+
const MONGODB_POOL_SIZE_MIN = 1;
|
|
26
|
+
const MONGODB_POOL_SIZE_MAX = 100;
|
|
27
|
+
const MONGODB_MAX_POOL_SIZE_ENV = 'MONGODB_MAX_POOL_SIZE';
|
|
24
28
|
class StorageService {
|
|
25
29
|
constructor(dependencies) {
|
|
26
30
|
this.databaseCache = new Map();
|
|
@@ -120,6 +124,22 @@ class StorageService {
|
|
|
120
124
|
return connectionString;
|
|
121
125
|
}
|
|
122
126
|
}
|
|
127
|
+
getMaxPoolSize() {
|
|
128
|
+
const raw = process.env[MONGODB_MAX_POOL_SIZE_ENV];
|
|
129
|
+
if (raw === undefined || raw === '') {
|
|
130
|
+
return DEFAULT_MONGODB_MAX_POOL_SIZE;
|
|
131
|
+
}
|
|
132
|
+
const n = parseInt(raw, 10);
|
|
133
|
+
if (Number.isNaN(n)) {
|
|
134
|
+
this.logger.warn({ value: raw, envKey: MONGODB_MAX_POOL_SIZE_ENV }, 'Invalid MONGODB_MAX_POOL_SIZE, using default');
|
|
135
|
+
return DEFAULT_MONGODB_MAX_POOL_SIZE;
|
|
136
|
+
}
|
|
137
|
+
const clamped = Math.max(MONGODB_POOL_SIZE_MIN, Math.min(MONGODB_POOL_SIZE_MAX, n));
|
|
138
|
+
if (clamped !== n) {
|
|
139
|
+
this.logger.warn({ value: n, clamped, min: MONGODB_POOL_SIZE_MIN, max: MONGODB_POOL_SIZE_MAX }, 'MONGODB_MAX_POOL_SIZE clamped to allowed range');
|
|
140
|
+
}
|
|
141
|
+
return clamped;
|
|
142
|
+
}
|
|
123
143
|
async getMongoClient(connectionString) {
|
|
124
144
|
const normalizedConnectionString = this.normalizeConnectionString(connectionString);
|
|
125
145
|
if (normalizedConnectionString !== connectionString) {
|
|
@@ -128,9 +148,12 @@ class StorageService {
|
|
|
128
148
|
if (this.clientCache.has(normalizedConnectionString)) {
|
|
129
149
|
return this.clientCache.get(normalizedConnectionString);
|
|
130
150
|
}
|
|
151
|
+
const maxPoolSize = this.getMaxPoolSize();
|
|
131
152
|
try {
|
|
132
153
|
const client = new mongodb_1.MongoClient(normalizedConnectionString, {
|
|
133
|
-
maxPoolSize
|
|
154
|
+
maxPoolSize,
|
|
155
|
+
serverSelectionTimeoutMS: 15000,
|
|
156
|
+
connectTimeoutMS: 10000,
|
|
134
157
|
});
|
|
135
158
|
await client.connect();
|
|
136
159
|
this.clientCache.set(normalizedConnectionString, client);
|
|
@@ -149,11 +172,14 @@ class StorageService {
|
|
|
149
172
|
const variableName = `${CONNECTION_STRING_ENV_PREFIX}${storageName}`;
|
|
150
173
|
const envValue = process.env[variableName];
|
|
151
174
|
if (envValue && envValue.trim()) {
|
|
175
|
+
this.logger.debug({ source: 'env', variableName }, 'MongoDB connection string from env');
|
|
152
176
|
return envValue.trim();
|
|
153
177
|
}
|
|
154
178
|
if (this.cachedConnectionString) {
|
|
179
|
+
this.logger.debug('MongoDB connection string from cache');
|
|
155
180
|
return this.cachedConnectionString;
|
|
156
181
|
}
|
|
182
|
+
this.logger.debug('MongoDB connection string fetching from Beamable API');
|
|
157
183
|
const response = await this.fetchConnectionString();
|
|
158
184
|
if (!response.connectionString || !response.connectionString.trim()) {
|
|
159
185
|
throw new Error(`Connection string for storage "${storageName}" is empty.`);
|
|
@@ -174,16 +200,30 @@ class StorageService {
|
|
|
174
200
|
this.logger.debug({ error: error instanceof Error ? error.message : String(error) }, 'beamoGetStorageConnectionBasic failed, falling back to requester');
|
|
175
201
|
}
|
|
176
202
|
}
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
203
|
+
const maxAttempts = 2;
|
|
204
|
+
let lastError;
|
|
205
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
206
|
+
try {
|
|
207
|
+
const response = await this.requester.request({
|
|
208
|
+
method: 'GET',
|
|
209
|
+
url: '/basic/beamo/storage/connection',
|
|
210
|
+
withAuth: true,
|
|
211
|
+
});
|
|
212
|
+
const body = response.body;
|
|
213
|
+
if (!body || typeof body.connectionString !== 'string') {
|
|
214
|
+
throw new Error('Failed to retrieve Beamable storage connection string.');
|
|
215
|
+
}
|
|
216
|
+
return body;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
lastError = err;
|
|
220
|
+
this.logger.warn({ attempt, maxAttempts, error: err instanceof Error ? err.message : String(err) }, 'Beamable storage connection fetch failed');
|
|
221
|
+
if (attempt < maxAttempts) {
|
|
222
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
185
225
|
}
|
|
186
|
-
|
|
226
|
+
throw lastError;
|
|
187
227
|
}
|
|
188
228
|
buildDatabaseName(storageName) {
|
|
189
229
|
const cid = this.sanitize(this.env.cid);
|
package/dist/storage.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export declare class StorageService {
|
|
|
42
42
|
* Safely handles already-encoded credentials by decoding first, then re-encoding.
|
|
43
43
|
*/
|
|
44
44
|
private normalizeConnectionString;
|
|
45
|
+
/**
|
|
46
|
+
* Reads MONGODB_MAX_POOL_SIZE from env, clamped to [1, 100]. Default 10.
|
|
47
|
+
* All game services share the same MongoDB; total connections = sum of each service's pool.
|
|
48
|
+
*/
|
|
49
|
+
private getMaxPoolSize;
|
|
45
50
|
private getMongoClient;
|
|
46
51
|
private getConnectionString;
|
|
47
52
|
private fetchConnectionString;
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAAe,KAAK,EAAE,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;CACrB;AAID,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAOjE;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,QAAQ,GAAG,eAAe,GAAG,SAAS,CAEhF;AAED,wBAAgB,4BAA4B,IAAI,eAAe,EAAE,CAEhE;AAED,UAAU,0BAA0B;IAClC,SAAS,EAAE,aAAa,CAAC;IACzB,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,GAAG,EAAE,iBAAiB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAAe,KAAK,EAAE,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;CACrB;AAID,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAOjE;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,QAAQ,GAAG,eAAe,GAAG,SAAS,CAEhF;AAED,wBAAgB,4BAA4B,IAAI,eAAe,EAAE,CAEhE;AAED,UAAU,0BAA0B;IAClC,SAAS,EAAE,aAAa,CAAC;IACzB,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,GAAG,EAAE,iBAAiB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAeD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IACvD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,sBAAsB,CAAC,CAAS;gBAE5B,YAAY,EAAE,0BAA0B;IAO9C,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,EAAE,CAAC;IAkBrF,cAAc,CAAC,CAAC,EAAE,WAAW,EAAE,UAAU,CAAC,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,EAAE,CAAC;IAUhG,aAAa,CAAC,SAAS,SAAS,QAAQ,EAC5C,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAS3B,gBAAgB,CAAC,QAAQ,EAAE,SAAS,SAAS,QAAQ,EACzD,WAAW,EAAE,UAAU,QAAQ,EAC/B,cAAc,EAAE,UAAU,SAAS,EACnC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAYjC;;;;;;OAMG;IACH,OAAO,CAAC,yBAAyB;IAqEjC;;;OAGG;IACH,OAAO,CAAC,cAAc;YAuBR,cAAc;YAqCd,mBAAmB;YAsBnB,qBAAqB;IA+CnC,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,oBAAoB;IAQtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ/B"}
|
package/dist/storage.js
CHANGED
|
@@ -15,6 +15,12 @@ export function listRegisteredStorageObjects() {
|
|
|
15
15
|
return Array.from(STORAGE_OBJECT_METADATA.values());
|
|
16
16
|
}
|
|
17
17
|
const CONNECTION_STRING_ENV_PREFIX = 'STORAGE_CONNSTR_';
|
|
18
|
+
/** Default MongoDB pool size when MONGODB_MAX_POOL_SIZE is not set. */
|
|
19
|
+
const DEFAULT_MONGODB_MAX_POOL_SIZE = 10;
|
|
20
|
+
/** Min/max allowed pool size (env is clamped to this range). */
|
|
21
|
+
const MONGODB_POOL_SIZE_MIN = 1;
|
|
22
|
+
const MONGODB_POOL_SIZE_MAX = 100;
|
|
23
|
+
const MONGODB_MAX_POOL_SIZE_ENV = 'MONGODB_MAX_POOL_SIZE';
|
|
18
24
|
export class StorageService {
|
|
19
25
|
requester;
|
|
20
26
|
api; // Optional - not required for operation
|
|
@@ -135,6 +141,26 @@ export class StorageService {
|
|
|
135
141
|
return connectionString;
|
|
136
142
|
}
|
|
137
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Reads MONGODB_MAX_POOL_SIZE from env, clamped to [1, 100]. Default 10.
|
|
146
|
+
* All game services share the same MongoDB; total connections = sum of each service's pool.
|
|
147
|
+
*/
|
|
148
|
+
getMaxPoolSize() {
|
|
149
|
+
const raw = process.env[MONGODB_MAX_POOL_SIZE_ENV];
|
|
150
|
+
if (raw === undefined || raw === '') {
|
|
151
|
+
return DEFAULT_MONGODB_MAX_POOL_SIZE;
|
|
152
|
+
}
|
|
153
|
+
const n = parseInt(raw, 10);
|
|
154
|
+
if (Number.isNaN(n)) {
|
|
155
|
+
this.logger.warn({ value: raw, envKey: MONGODB_MAX_POOL_SIZE_ENV }, 'Invalid MONGODB_MAX_POOL_SIZE, using default');
|
|
156
|
+
return DEFAULT_MONGODB_MAX_POOL_SIZE;
|
|
157
|
+
}
|
|
158
|
+
const clamped = Math.max(MONGODB_POOL_SIZE_MIN, Math.min(MONGODB_POOL_SIZE_MAX, n));
|
|
159
|
+
if (clamped !== n) {
|
|
160
|
+
this.logger.warn({ value: n, clamped, min: MONGODB_POOL_SIZE_MIN, max: MONGODB_POOL_SIZE_MAX }, 'MONGODB_MAX_POOL_SIZE clamped to allowed range');
|
|
161
|
+
}
|
|
162
|
+
return clamped;
|
|
163
|
+
}
|
|
138
164
|
async getMongoClient(connectionString) {
|
|
139
165
|
// Normalize the connection string to ensure username/password are properly encoded
|
|
140
166
|
const normalizedConnectionString = this.normalizeConnectionString(connectionString);
|
|
@@ -145,9 +171,12 @@ export class StorageService {
|
|
|
145
171
|
if (this.clientCache.has(normalizedConnectionString)) {
|
|
146
172
|
return this.clientCache.get(normalizedConnectionString);
|
|
147
173
|
}
|
|
174
|
+
const maxPoolSize = this.getMaxPoolSize();
|
|
148
175
|
try {
|
|
149
176
|
const client = new MongoClient(normalizedConnectionString, {
|
|
150
|
-
maxPoolSize
|
|
177
|
+
maxPoolSize,
|
|
178
|
+
serverSelectionTimeoutMS: 15000,
|
|
179
|
+
connectTimeoutMS: 10000,
|
|
151
180
|
});
|
|
152
181
|
await client.connect();
|
|
153
182
|
this.clientCache.set(normalizedConnectionString, client);
|
|
@@ -167,11 +196,14 @@ export class StorageService {
|
|
|
167
196
|
const variableName = `${CONNECTION_STRING_ENV_PREFIX}${storageName}`;
|
|
168
197
|
const envValue = process.env[variableName];
|
|
169
198
|
if (envValue && envValue.trim()) {
|
|
199
|
+
this.logger.debug({ source: 'env', variableName }, 'MongoDB connection string from env');
|
|
170
200
|
return envValue.trim();
|
|
171
201
|
}
|
|
172
202
|
if (this.cachedConnectionString) {
|
|
203
|
+
this.logger.debug('MongoDB connection string from cache');
|
|
173
204
|
return this.cachedConnectionString;
|
|
174
205
|
}
|
|
206
|
+
this.logger.debug('MongoDB connection string fetching from Beamable API');
|
|
175
207
|
const response = await this.fetchConnectionString();
|
|
176
208
|
if (!response.connectionString || !response.connectionString.trim()) {
|
|
177
209
|
throw new Error(`Connection string for storage "${storageName}" is empty.`);
|
|
@@ -195,16 +227,30 @@ export class StorageService {
|
|
|
195
227
|
}
|
|
196
228
|
// Fall back to direct requester call (works without request context)
|
|
197
229
|
// This is the same approach used by C# StorageObjectConnectionProvider
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
230
|
+
const maxAttempts = 2;
|
|
231
|
+
let lastError;
|
|
232
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
233
|
+
try {
|
|
234
|
+
const response = await this.requester.request({
|
|
235
|
+
method: 'GET',
|
|
236
|
+
url: '/basic/beamo/storage/connection',
|
|
237
|
+
withAuth: true,
|
|
238
|
+
});
|
|
239
|
+
const body = response.body;
|
|
240
|
+
if (!body || typeof body.connectionString !== 'string') {
|
|
241
|
+
throw new Error('Failed to retrieve Beamable storage connection string.');
|
|
242
|
+
}
|
|
243
|
+
return body;
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
lastError = err;
|
|
247
|
+
this.logger.warn({ attempt, maxAttempts, error: err instanceof Error ? err.message : String(err) }, 'Beamable storage connection fetch failed');
|
|
248
|
+
if (attempt < maxAttempts) {
|
|
249
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw lastError;
|
|
208
254
|
}
|
|
209
255
|
buildDatabaseName(storageName) {
|
|
210
256
|
const cid = this.sanitize(this.env.cid);
|
package/dist/storage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAA2C,MAAM,SAAS,CAAC;AAiB/E,MAAM,uBAAuB,GAAG,IAAI,GAAG,EAA6B,CAAC;AAErE,MAAM,UAAU,aAAa,CAAC,WAAmB;IAC/C,IAAI,CAAC,WAAW,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,CAAC,MAAM,EAAE,EAAE;QAChB,uBAAuB,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC3E,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAgB;IACjD,OAAO,uBAAuB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC1C,OAAO,KAAK,CAAC,IAAI,CAAC,uBAAuB,CAAC,MAAM,EAAE,CAAC,CAAC;AACtD,CAAC;AAaD,MAAM,4BAA4B,GAAG,kBAAkB,CAAC;AAExD,MAAM,OAAO,cAAc;IACR,SAAS,CAAgB;IACzB,GAAG,CAAgB,CAAC,wCAAwC;IAC5D,GAAG,CAAoB;IACvB,MAAM,CAAS;IACf,aAAa,GAAG,IAAI,GAAG,EAAc,CAAC;IACtC,WAAW,GAAG,IAAI,GAAG,EAAuB,CAAC;IACtD,sBAAsB,CAAU;IAExC,YAAY,YAAwC;QAClD,IAAI,CAAC,SAAS,GAAG,YAAY,CAAC,SAAS,CAAC;QACxC,IAAI,CAAC,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,8BAA8B;QAC3D,IAAI,CAAC,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,WAAmB,EAAE,UAAoC,EAAE;QAC3E,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,WAAW,CAAC,CAAC;QAC1D,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YACtB,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACxC,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAClD,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;QAC3D,MAAM,YAAY,GAAG,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC;QACxD,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC;QACzC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC7C,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,cAAc,CAAI,WAAwB,EAAE,UAAoC,EAAE;QACtF,MAAM,QAAQ,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;QACjD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,wBAAwB,WAAW,CAAC,IAAI,qEAAqE,CAC9G,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,WAAmB,EACnB,UAAoC,EAAE;QAEtC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAC9D,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC;QACtD,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,kFAAkF,CAAC,CAAC;QACtG,CAAC;QACD,OAAO,QAAQ,CAAC,UAAU,CAAY,cAAc,CAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,gBAAgB,CACpB,WAA+B,EAC/B,cAAmC,EACnC,UAAoC,EAAE;QAEtC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;QACjD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,wBAAwB,WAAW,CAAC,IAAI,qEAAqE,CAC9G,CAAC;QACJ,CAAC;QACD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,cAAc,CAAC,IAAI,CAAC;QAC7E,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACvE,OAAO,QAAQ,CAAC,UAAU,CAAY,cAAc,CAAC,CAAC;IACxD,CAAC;IAED;;;;;;OAMG;IACK,yBAAyB,CAAC,gBAAwB;QACxD,IAAI,CAAC;YACH,uEAAuE;YACvE,+EAA+E;YAC/E,MAAM,aAAa,GAAG,yFAAyF,CAAC;YAChH,MAAM,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAEpD,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,sFAAsF;gBACtF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,2EAA2E,CAAC,CAAC;gBAC9F,OAAO,gBAAgB,CAAC;YAC1B,CAAC;YAED,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC;YAExE,wCAAwC;YACxC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,OAAO,gBAAgB,CAAC;YAC1B,CAAC;YAED,uEAAuE;YACvE,wEAAwE;YACxE,IAAI,eAAuB,CAAC;YAC5B,IAAI,eAAmC,CAAC;YAExC,IAAI,CAAC;gBACH,eAAe,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YACjD,CAAC;YAAC,MAAM,CAAC;gBACP,2DAA2D;gBAC3D,eAAe,GAAG,QAAQ,CAAC;YAC7B,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,IAAI,CAAC;oBACH,eAAe,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;gBACjD,CAAC;gBAAC,MAAM,CAAC;oBACP,2DAA2D;oBAC3D,eAAe,GAAG,QAAQ,CAAC;gBAC7B,CAAC;YACH,CAAC;YAED,+EAA+E;YAC/E,MAAM,eAAe,GAAG,kBAAkB,CAAC,eAAe,CAAC,CAAC;YAC5D,MAAM,eAAe,GAAG,eAAe,CAAC,CAAC,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAE1F,oCAAoC;YACpC,IAAI,UAAU,GAAG,GAAG,QAAQ,GAAG,eAAe,EAAE,CAAC;YACjD,IAAI,eAAe,EAAE,CAAC;gBACpB,UAAU,IAAI,IAAI,eAAe,EAAE,CAAC;YACtC,CAAC;YACD,UAAU,IAAI,IAAI,IAAI,EAAE,CAAC;YACzB,IAAI,QAAQ,EAAE,CAAC;gBACb,UAAU,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC/B,CAAC;YACD,IAAI,OAAO,EAAE,CAAC;gBACZ,UAAU,IAAI,IAAI,OAAO,EAAE,CAAC;YAC9B,CAAC;YAED,OAAO,UAAU,CAAC;QACpB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,oDAAoD;YACpD,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EACjE,4DAA4D,CAC7D,CAAC;YACF,OAAO,gBAAgB,CAAC;QAC1B,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,gBAAwB;QACnD,mFAAmF;QACnF,MAAM,0BAA0B,GAAG,IAAI,CAAC,yBAAyB,CAAC,gBAAgB,CAAC,CAAC;QAEpF,4EAA4E;QAC5E,IAAI,0BAA0B,KAAK,gBAAgB,EAAE,CAAC;YACpD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,sEAAsE,CAAC,CAAC;QAC5F,CAAC;QAED,IAAI,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,0BAA0B,CAAC,EAAE,CAAC;YACrD,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,0BAA0B,CAAgB,CAAC;QACzE,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,0BAA0B,EAAE;gBACzD,WAAW,EAAE,CAAC;aACf,CAAC,CAAC;YACH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;YACvB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,0BAA0B,EAAE,MAAM,CAAC,CAAC;YACzD,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,uFAAuF;YACvF,MAAM,yBAAyB,GAAG,0BAA0B,CAAC,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;YAC7F,IAAI,CAAC,MAAM,CAAC,KAAK,CACf;gBACE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC7D,gBAAgB,EAAE,yBAAyB;aAC5C,EACD,8BAA8B,CAC/B,CAAC;YACF,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,mBAAmB,CAAC,WAAmB;QACnD,MAAM,YAAY,GAAG,GAAG,4BAA4B,GAAG,WAAW,EAAE,CAAC;QACrE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC3C,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YAChC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;QACzB,CAAC;QAED,IAAI,IAAI,CAAC,sBAAsB,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,sBAAsB,CAAC;QACrC,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,qBAAqB,EAAE,CAAC;QACpD,IAAI,CAAC,QAAQ,CAAC,gBAAgB,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,EAAE,EAAE,CAAC;YACpE,MAAM,IAAI,KAAK,CAAC,kCAAkC,WAAW,aAAa,CAAC,CAAC;QAC9E,CAAC;QACD,IAAI,CAAC,sBAAsB,GAAG,QAAQ,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;QAC/D,OAAO,IAAI,CAAC,sBAAsB,CAAC;IACrC,CAAC;IAEO,KAAK,CAAC,qBAAqB;QACjC,wDAAwD;QACxD,IAAI,IAAI,CAAC,GAAG,IAAI,OAAO,IAAI,CAAC,GAAG,CAAC,8BAA8B,KAAK,UAAU,EAAE,CAAC;YAC9E,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,8BAA8B,EAAE,CAAC;gBAC/D,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;oBAC7D,OAAQ,MAA6C,CAAC,IAAI,CAAC;gBAC7D,CAAC;gBACD,OAAO,MAAkC,CAAC;YAC5C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EACjE,kEAAkE,CACnE,CAAC;YACJ,CAAC;QACH,CAAC;QAED,qEAAqE;QACrE,uEAAuE;QACvE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;YAC5C,MAAM,EAAE,KAAK;YACb,GAAG,EAAE,iCAAiC;YACtC,QAAQ,EAAE,IAAI;SACf,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,IAA4C,CAAC;QACnE,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,gBAAgB,KAAK,QAAQ,EAAE,CAAC;YACvD,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,iBAAiB,CAAC,WAAmB;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC3C,OAAO,GAAG,GAAG,GAAG,GAAG,IAAI,OAAO,EAAE,CAAC;IACnC,CAAC;IAEO,QAAQ,CAAC,KAAa;QAC5B,OAAO,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;IAC9C,CAAC;IAEO,oBAAoB,CAAC,WAAmB;QAC9C,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;QACtC,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,OAAO;QACX,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC;YAC/C,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC;QAC3B,IAAI,CAAC,sBAAsB,GAAG,SAAS,CAAC;IAC1C,CAAC;CACF","sourcesContent":["import type { Logger } from 'pino';\r\nimport { MongoClient, type Db, type Collection, type Document } from 'mongodb';\r\nimport type { BoundBeamApi } from './services.js';\r\nimport type { EnvironmentConfig } from './types.js';\r\nimport type { HttpRequester } from 'beamable-sdk';\r\n\r\nexport interface StorageConnectionOptions {\r\n useCache?: boolean;\r\n}\r\n\r\nexport interface StorageCollectionOptions extends StorageConnectionOptions {\r\n collectionName?: string;\r\n}\r\n\r\nexport interface StorageMetadata {\r\n storageName: string;\r\n}\r\n\r\nconst STORAGE_OBJECT_METADATA = new Map<Function, StorageMetadata>();\r\n\r\nexport function StorageObject(storageName: string): ClassDecorator {\r\n if (!storageName || !storageName.trim()) {\r\n throw new Error('@StorageObject requires a non-empty storage name.');\r\n }\r\n return (target) => {\r\n STORAGE_OBJECT_METADATA.set(target, { storageName: storageName.trim() });\r\n };\r\n}\r\n\r\nexport function getStorageMetadata(target: Function): StorageMetadata | undefined {\r\n return STORAGE_OBJECT_METADATA.get(target);\r\n}\r\n\r\nexport function listRegisteredStorageObjects(): StorageMetadata[] {\r\n return Array.from(STORAGE_OBJECT_METADATA.values());\r\n}\r\n\r\ninterface StorageServiceDependencies {\r\n requester: HttpRequester;\r\n api?: BoundBeamApi; // Optional - falls back to requester if not provided\r\n env: EnvironmentConfig;\r\n logger: Logger;\r\n}\r\n\r\ninterface ConnectionStringResponse {\r\n connectionString: string;\r\n}\r\n\r\nconst CONNECTION_STRING_ENV_PREFIX = 'STORAGE_CONNSTR_';\r\n\r\nexport class StorageService {\r\n private readonly requester: HttpRequester;\r\n private readonly api?: BoundBeamApi; // Optional - not required for operation\r\n private readonly env: EnvironmentConfig;\r\n private readonly logger: Logger;\r\n private readonly databaseCache = new Map<string, Db>();\r\n private readonly clientCache = new Map<string, MongoClient>();\r\n private cachedConnectionString?: string;\r\n\r\n constructor(dependencies: StorageServiceDependencies) {\r\n this.requester = dependencies.requester;\r\n this.api = dependencies.api; // Optional - can be undefined\r\n this.env = dependencies.env;\r\n this.logger = dependencies.logger.child({ component: 'StorageService' });\r\n }\r\n\r\n async getDatabase(storageName: string, options: StorageConnectionOptions = {}): Promise<Db> {\r\n const normalized = this.normalizeStorageName(storageName);\r\n if (!options.useCache) {\r\n this.databaseCache.delete(normalized);\r\n }\r\n const cached = this.databaseCache.get(normalized);\r\n if (cached) {\r\n return cached;\r\n }\r\n\r\n const connectionString = await this.getConnectionString(normalized);\r\n const client = await this.getMongoClient(connectionString);\r\n const databaseName = this.buildDatabaseName(normalized);\r\n const database = client.db(databaseName);\r\n this.databaseCache.set(normalized, database);\r\n return database;\r\n }\r\n\r\n async getDatabaseFor<T>(storageCtor: new () => T, options: StorageConnectionOptions = {}): Promise<Db> {\r\n const metadata = getStorageMetadata(storageCtor);\r\n if (!metadata) {\r\n throw new Error(\r\n `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,\r\n );\r\n }\r\n return this.getDatabase(metadata.storageName, options);\r\n }\r\n\r\n async getCollection<TDocument extends Document>(\r\n storageName: string,\r\n options: StorageCollectionOptions = {},\r\n ): Promise<Collection<TDocument>> {\r\n const database = await this.getDatabase(storageName, options);\r\n const collectionName = options.collectionName?.trim();\r\n if (!collectionName) {\r\n throw new Error('Collection name must be provided when using getCollection with raw storage name.');\r\n }\r\n return database.collection<TDocument>(collectionName);\r\n }\r\n\r\n async getCollectionFor<TStorage, TDocument extends Document>(\r\n storageCtor: new () => TStorage,\r\n collectionCtor: new () => TDocument,\r\n options: StorageCollectionOptions = {},\r\n ): Promise<Collection<TDocument>> {\r\n const metadata = getStorageMetadata(storageCtor);\r\n if (!metadata) {\r\n throw new Error(\r\n `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,\r\n );\r\n }\r\n const collectionName = options.collectionName?.trim() ?? collectionCtor.name;\r\n const database = await this.getDatabase(metadata.storageName, options);\r\n return database.collection<TDocument>(collectionName);\r\n }\r\n\r\n /**\r\n * Normalizes a MongoDB connection string by ensuring username and password are properly URL-encoded.\r\n * This prevents \"Password contains unescaped characters\" errors when passwords contain special characters.\r\n * \r\n * Handles both mongodb:// and mongodb+srv:// formats.\r\n * Safely handles already-encoded credentials by decoding first, then re-encoding.\r\n */\r\n private normalizeConnectionString(connectionString: string): string {\r\n try {\r\n // Match MongoDB connection string format: mongodb:// or mongodb+srv://\r\n // Format: mongodb[+srv]://[username:password@]host[:port][/database][?options]\r\n const mongoUriRegex = /^(mongodb(?:\\+srv)?:\\/\\/)(?:([^:@]+)(?::([^@]+))?@)?([^\\/?]+)(?:\\/([^?]*))?(?:\\?(.*))?$/;\r\n const match = connectionString.match(mongoUriRegex);\r\n \r\n if (!match) {\r\n // If it doesn't match the expected format, return as-is (might be a different format)\r\n this.logger.warn('Connection string does not match expected MongoDB URI format, using as-is');\r\n return connectionString;\r\n }\r\n\r\n const [, protocol, username, password, host, database, options] = match;\r\n \r\n // If no username/password, return as-is\r\n if (!username) {\r\n return connectionString;\r\n }\r\n\r\n // Decode username and password first (in case they're already encoded)\r\n // Then re-encode them properly to ensure special characters are handled\r\n let decodedUsername: string;\r\n let decodedPassword: string | undefined;\r\n \r\n try {\r\n decodedUsername = decodeURIComponent(username);\r\n } catch {\r\n // If decoding fails, assume it's not encoded and use as-is\r\n decodedUsername = username;\r\n }\r\n \r\n if (password) {\r\n try {\r\n decodedPassword = decodeURIComponent(password);\r\n } catch {\r\n // If decoding fails, assume it's not encoded and use as-is\r\n decodedPassword = password;\r\n }\r\n }\r\n\r\n // Now encode them properly (encodeURIComponent handles all special characters)\r\n const encodedUsername = encodeURIComponent(decodedUsername);\r\n const encodedPassword = decodedPassword ? encodeURIComponent(decodedPassword) : undefined;\r\n\r\n // Reconstruct the connection string\r\n let normalized = `${protocol}${encodedUsername}`;\r\n if (encodedPassword) {\r\n normalized += `:${encodedPassword}`;\r\n }\r\n normalized += `@${host}`;\r\n if (database) {\r\n normalized += `/${database}`;\r\n }\r\n if (options) {\r\n normalized += `?${options}`;\r\n }\r\n\r\n return normalized;\r\n } catch (error) {\r\n // If parsing fails, log warning and return original\r\n this.logger.warn(\r\n { error: error instanceof Error ? error.message : String(error) },\r\n 'Failed to normalize MongoDB connection string, using as-is'\r\n );\r\n return connectionString;\r\n }\r\n }\r\n\r\n private async getMongoClient(connectionString: string): Promise<MongoClient> {\r\n // Normalize the connection string to ensure username/password are properly encoded\r\n const normalizedConnectionString = this.normalizeConnectionString(connectionString);\r\n \r\n // Log the normalization for debugging (only log if different to avoid spam)\r\n if (normalizedConnectionString !== connectionString) {\r\n this.logger.debug('MongoDB connection string was normalized (password encoding applied)');\r\n }\r\n \r\n if (this.clientCache.has(normalizedConnectionString)) {\r\n return this.clientCache.get(normalizedConnectionString) as MongoClient;\r\n }\r\n \r\n try {\r\n const client = new MongoClient(normalizedConnectionString, {\r\n maxPoolSize: 5,\r\n });\r\n await client.connect();\r\n this.clientCache.set(normalizedConnectionString, client);\r\n return client;\r\n } catch (error) {\r\n // If connection fails, log the error with connection string details (without password)\r\n const sanitizedConnectionString = normalizedConnectionString.replace(/:([^:@]+)@/, ':****@');\r\n this.logger.error(\r\n { \r\n error: error instanceof Error ? error.message : String(error),\r\n connectionString: sanitizedConnectionString \r\n },\r\n 'Failed to connect to MongoDB'\r\n );\r\n throw error;\r\n }\r\n }\r\n\r\n private async getConnectionString(storageName: string): Promise<string> {\r\n const variableName = `${CONNECTION_STRING_ENV_PREFIX}${storageName}`;\r\n const envValue = process.env[variableName];\r\n if (envValue && envValue.trim()) {\r\n return envValue.trim();\r\n }\r\n\r\n if (this.cachedConnectionString) {\r\n return this.cachedConnectionString;\r\n }\r\n\r\n const response = await this.fetchConnectionString();\r\n if (!response.connectionString || !response.connectionString.trim()) {\r\n throw new Error(`Connection string for storage \"${storageName}\" is empty.`);\r\n }\r\n this.cachedConnectionString = response.connectionString.trim();\r\n return this.cachedConnectionString;\r\n }\r\n\r\n private async fetchConnectionString(): Promise<ConnectionStringResponse> {\r\n // Try using BoundBeamApi if available (for convenience)\r\n if (this.api && typeof this.api.beamoGetStorageConnectionBasic === 'function') {\r\n try {\r\n const result = await this.api.beamoGetStorageConnectionBasic();\r\n if (result && typeof result === 'object' && 'body' in result) {\r\n return (result as { body: ConnectionStringResponse }).body;\r\n }\r\n return result as ConnectionStringResponse;\r\n } catch (error) {\r\n this.logger.debug(\r\n { error: error instanceof Error ? error.message : String(error) },\r\n 'beamoGetStorageConnectionBasic failed, falling back to requester',\r\n );\r\n }\r\n }\r\n\r\n // Fall back to direct requester call (works without request context)\r\n // This is the same approach used by C# StorageObjectConnectionProvider\r\n const response = await this.requester.request({\r\n method: 'GET',\r\n url: '/basic/beamo/storage/connection',\r\n withAuth: true,\r\n });\r\n const body = response.body as ConnectionStringResponse | undefined;\r\n if (!body || typeof body.connectionString !== 'string') {\r\n throw new Error('Failed to retrieve Beamable storage connection string.');\r\n }\r\n return body;\r\n }\r\n\r\n private buildDatabaseName(storageName: string): string {\r\n const cid = this.sanitize(this.env.cid);\r\n const pid = this.sanitize(this.env.pid);\r\n const storage = this.sanitize(storageName);\r\n return `${cid}${pid}_${storage}`;\r\n }\r\n\r\n private sanitize(value: string): string {\r\n return value.replace(/[^A-Za-z0-9_]/g, '_');\r\n }\r\n\r\n private normalizeStorageName(storageName: string): string {\r\n const normalized = storageName.trim();\r\n if (!normalized) {\r\n throw new Error('Storage name cannot be empty.');\r\n }\r\n return normalized;\r\n }\r\n\r\n async dispose(): Promise<void> {\r\n for (const client of this.clientCache.values()) {\r\n await client.close();\r\n }\r\n this.clientCache.clear();\r\n this.databaseCache.clear();\r\n this.cachedConnectionString = undefined;\r\n }\r\n}\r\n\r\n"]}
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Logger } from 'pino';\nimport { MongoClient, type Db, type Collection, type Document } from 'mongodb';\nimport type { BoundBeamApi } from './services.js';\nimport type { EnvironmentConfig } from './types.js';\nimport type { HttpRequester } from 'beamable-sdk';\n\nexport interface StorageConnectionOptions {\n useCache?: boolean;\n}\n\nexport interface StorageCollectionOptions extends StorageConnectionOptions {\n collectionName?: string;\n}\n\nexport interface StorageMetadata {\n storageName: string;\n}\n\nconst STORAGE_OBJECT_METADATA = new Map<Function, StorageMetadata>();\n\nexport function StorageObject(storageName: string): ClassDecorator {\n if (!storageName || !storageName.trim()) {\n throw new Error('@StorageObject requires a non-empty storage name.');\n }\n return (target) => {\n STORAGE_OBJECT_METADATA.set(target, { storageName: storageName.trim() });\n };\n}\n\nexport function getStorageMetadata(target: Function): StorageMetadata | undefined {\n return STORAGE_OBJECT_METADATA.get(target);\n}\n\nexport function listRegisteredStorageObjects(): StorageMetadata[] {\n return Array.from(STORAGE_OBJECT_METADATA.values());\n}\n\ninterface StorageServiceDependencies {\n requester: HttpRequester;\n api?: BoundBeamApi; // Optional - falls back to requester if not provided\n env: EnvironmentConfig;\n logger: Logger;\n}\n\ninterface ConnectionStringResponse {\n connectionString: string;\n}\n\nconst CONNECTION_STRING_ENV_PREFIX = 'STORAGE_CONNSTR_';\n\n/** Default MongoDB pool size when MONGODB_MAX_POOL_SIZE is not set. */\nconst DEFAULT_MONGODB_MAX_POOL_SIZE = 10;\n/** Min/max allowed pool size (env is clamped to this range). */\nconst MONGODB_POOL_SIZE_MIN = 1;\nconst MONGODB_POOL_SIZE_MAX = 100;\nconst MONGODB_MAX_POOL_SIZE_ENV = 'MONGODB_MAX_POOL_SIZE';\n\nexport class StorageService {\n private readonly requester: HttpRequester;\n private readonly api?: BoundBeamApi; // Optional - not required for operation\n private readonly env: EnvironmentConfig;\n private readonly logger: Logger;\n private readonly databaseCache = new Map<string, Db>();\n private readonly clientCache = new Map<string, MongoClient>();\n private cachedConnectionString?: string;\n\n constructor(dependencies: StorageServiceDependencies) {\n this.requester = dependencies.requester;\n this.api = dependencies.api; // Optional - can be undefined\n this.env = dependencies.env;\n this.logger = dependencies.logger.child({ component: 'StorageService' });\n }\n\n async getDatabase(storageName: string, options: StorageConnectionOptions = {}): Promise<Db> {\n const normalized = this.normalizeStorageName(storageName);\n if (!options.useCache) {\n this.databaseCache.delete(normalized);\n }\n const cached = this.databaseCache.get(normalized);\n if (cached) {\n return cached;\n }\n\n const connectionString = await this.getConnectionString(normalized);\n const client = await this.getMongoClient(connectionString);\n const databaseName = this.buildDatabaseName(normalized);\n const database = client.db(databaseName);\n this.databaseCache.set(normalized, database);\n return database;\n }\n\n async getDatabaseFor<T>(storageCtor: new () => T, options: StorageConnectionOptions = {}): Promise<Db> {\n const metadata = getStorageMetadata(storageCtor);\n if (!metadata) {\n throw new Error(\n `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,\n );\n }\n return this.getDatabase(metadata.storageName, options);\n }\n\n async getCollection<TDocument extends Document>(\n storageName: string,\n options: StorageCollectionOptions = {},\n ): Promise<Collection<TDocument>> {\n const database = await this.getDatabase(storageName, options);\n const collectionName = options.collectionName?.trim();\n if (!collectionName) {\n throw new Error('Collection name must be provided when using getCollection with raw storage name.');\n }\n return database.collection<TDocument>(collectionName);\n }\n\n async getCollectionFor<TStorage, TDocument extends Document>(\n storageCtor: new () => TStorage,\n collectionCtor: new () => TDocument,\n options: StorageCollectionOptions = {},\n ): Promise<Collection<TDocument>> {\n const metadata = getStorageMetadata(storageCtor);\n if (!metadata) {\n throw new Error(\n `Storage metadata for ${storageCtor.name} not found. Did you decorate the class with @StorageObject('Name')?`,\n );\n }\n const collectionName = options.collectionName?.trim() ?? collectionCtor.name;\n const database = await this.getDatabase(metadata.storageName, options);\n return database.collection<TDocument>(collectionName);\n }\n\n /**\n * Normalizes a MongoDB connection string by ensuring username and password are properly URL-encoded.\n * This prevents \"Password contains unescaped characters\" errors when passwords contain special characters.\n * \n * Handles both mongodb:// and mongodb+srv:// formats.\n * Safely handles already-encoded credentials by decoding first, then re-encoding.\n */\n private normalizeConnectionString(connectionString: string): string {\n try {\n // Match MongoDB connection string format: mongodb:// or mongodb+srv://\n // Format: mongodb[+srv]://[username:password@]host[:port][/database][?options]\n const mongoUriRegex = /^(mongodb(?:\\+srv)?:\\/\\/)(?:([^:@]+)(?::([^@]+))?@)?([^\\/?]+)(?:\\/([^?]*))?(?:\\?(.*))?$/;\n const match = connectionString.match(mongoUriRegex);\n \n if (!match) {\n // If it doesn't match the expected format, return as-is (might be a different format)\n this.logger.warn('Connection string does not match expected MongoDB URI format, using as-is');\n return connectionString;\n }\n\n const [, protocol, username, password, host, database, options] = match;\n \n // If no username/password, return as-is\n if (!username) {\n return connectionString;\n }\n\n // Decode username and password first (in case they're already encoded)\n // Then re-encode them properly to ensure special characters are handled\n let decodedUsername: string;\n let decodedPassword: string | undefined;\n \n try {\n decodedUsername = decodeURIComponent(username);\n } catch {\n // If decoding fails, assume it's not encoded and use as-is\n decodedUsername = username;\n }\n \n if (password) {\n try {\n decodedPassword = decodeURIComponent(password);\n } catch {\n // If decoding fails, assume it's not encoded and use as-is\n decodedPassword = password;\n }\n }\n\n // Now encode them properly (encodeURIComponent handles all special characters)\n const encodedUsername = encodeURIComponent(decodedUsername);\n const encodedPassword = decodedPassword ? encodeURIComponent(decodedPassword) : undefined;\n\n // Reconstruct the connection string\n let normalized = `${protocol}${encodedUsername}`;\n if (encodedPassword) {\n normalized += `:${encodedPassword}`;\n }\n normalized += `@${host}`;\n if (database) {\n normalized += `/${database}`;\n }\n if (options) {\n normalized += `?${options}`;\n }\n\n return normalized;\n } catch (error) {\n // If parsing fails, log warning and return original\n this.logger.warn(\n { error: error instanceof Error ? error.message : String(error) },\n 'Failed to normalize MongoDB connection string, using as-is'\n );\n return connectionString;\n }\n }\n\n /**\n * Reads MONGODB_MAX_POOL_SIZE from env, clamped to [1, 100]. Default 10.\n * All game services share the same MongoDB; total connections = sum of each service's pool.\n */\n private getMaxPoolSize(): number {\n const raw = process.env[MONGODB_MAX_POOL_SIZE_ENV];\n if (raw === undefined || raw === '') {\n return DEFAULT_MONGODB_MAX_POOL_SIZE;\n }\n const n = parseInt(raw, 10);\n if (Number.isNaN(n)) {\n this.logger.warn(\n { value: raw, envKey: MONGODB_MAX_POOL_SIZE_ENV },\n 'Invalid MONGODB_MAX_POOL_SIZE, using default'\n );\n return DEFAULT_MONGODB_MAX_POOL_SIZE;\n }\n const clamped = Math.max(MONGODB_POOL_SIZE_MIN, Math.min(MONGODB_POOL_SIZE_MAX, n));\n if (clamped !== n) {\n this.logger.warn(\n { value: n, clamped, min: MONGODB_POOL_SIZE_MIN, max: MONGODB_POOL_SIZE_MAX },\n 'MONGODB_MAX_POOL_SIZE clamped to allowed range'\n );\n }\n return clamped;\n }\n\n private async getMongoClient(connectionString: string): Promise<MongoClient> {\n // Normalize the connection string to ensure username/password are properly encoded\n const normalizedConnectionString = this.normalizeConnectionString(connectionString);\n \n // Log the normalization for debugging (only log if different to avoid spam)\n if (normalizedConnectionString !== connectionString) {\n this.logger.debug('MongoDB connection string was normalized (password encoding applied)');\n }\n \n if (this.clientCache.has(normalizedConnectionString)) {\n return this.clientCache.get(normalizedConnectionString) as MongoClient;\n }\n \n const maxPoolSize = this.getMaxPoolSize();\n try {\n const client = new MongoClient(normalizedConnectionString, {\n maxPoolSize,\n serverSelectionTimeoutMS: 15000,\n connectTimeoutMS: 10000,\n });\n await client.connect();\n this.clientCache.set(normalizedConnectionString, client);\n return client;\n } catch (error) {\n // If connection fails, log the error with connection string details (without password)\n const sanitizedConnectionString = normalizedConnectionString.replace(/:([^:@]+)@/, ':****@');\n this.logger.error(\n { \n error: error instanceof Error ? error.message : String(error),\n connectionString: sanitizedConnectionString \n },\n 'Failed to connect to MongoDB'\n );\n throw error;\n }\n }\n\n private async getConnectionString(storageName: string): Promise<string> {\n const variableName = `${CONNECTION_STRING_ENV_PREFIX}${storageName}`;\n const envValue = process.env[variableName];\n if (envValue && envValue.trim()) {\n this.logger.debug({ source: 'env', variableName }, 'MongoDB connection string from env');\n return envValue.trim();\n }\n\n if (this.cachedConnectionString) {\n this.logger.debug('MongoDB connection string from cache');\n return this.cachedConnectionString;\n }\n\n this.logger.debug('MongoDB connection string fetching from Beamable API');\n const response = await this.fetchConnectionString();\n if (!response.connectionString || !response.connectionString.trim()) {\n throw new Error(`Connection string for storage \"${storageName}\" is empty.`);\n }\n this.cachedConnectionString = response.connectionString.trim();\n return this.cachedConnectionString;\n }\n\n private async fetchConnectionString(): Promise<ConnectionStringResponse> {\n // Try using BoundBeamApi if available (for convenience)\n if (this.api && typeof this.api.beamoGetStorageConnectionBasic === 'function') {\n try {\n const result = await this.api.beamoGetStorageConnectionBasic();\n if (result && typeof result === 'object' && 'body' in result) {\n return (result as { body: ConnectionStringResponse }).body;\n }\n return result as ConnectionStringResponse;\n } catch (error) {\n this.logger.debug(\n { error: error instanceof Error ? error.message : String(error) },\n 'beamoGetStorageConnectionBasic failed, falling back to requester',\n );\n }\n }\n\n // Fall back to direct requester call (works without request context)\n // This is the same approach used by C# StorageObjectConnectionProvider\n const maxAttempts = 2;\n let lastError: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const response = await this.requester.request({\n method: 'GET',\n url: '/basic/beamo/storage/connection',\n withAuth: true,\n });\n const body = response.body as ConnectionStringResponse | undefined;\n if (!body || typeof body.connectionString !== 'string') {\n throw new Error('Failed to retrieve Beamable storage connection string.');\n }\n return body;\n } catch (err) {\n lastError = err;\n this.logger.warn(\n { attempt, maxAttempts, error: err instanceof Error ? err.message : String(err) },\n 'Beamable storage connection fetch failed'\n );\n if (attempt < maxAttempts) {\n await new Promise((r) => setTimeout(r, 1000));\n }\n }\n }\n throw lastError;\n }\n\n private buildDatabaseName(storageName: string): string {\n const cid = this.sanitize(this.env.cid);\n const pid = this.sanitize(this.env.pid);\n const storage = this.sanitize(storageName);\n return `${cid}${pid}_${storage}`;\n }\n\n private sanitize(value: string): string {\n return value.replace(/[^A-Za-z0-9_]/g, '_');\n }\n\n private normalizeStorageName(storageName: string): string {\n const normalized = storageName.trim();\n if (!normalized) {\n throw new Error('Storage name cannot be empty.');\n }\n return normalized;\n }\n\n async dispose(): Promise<void> {\n for (const client of this.clientCache.values()) {\n await client.close();\n }\n this.clientCache.clear();\n this.databaseCache.clear();\n this.cachedConnectionString = undefined;\n }\n}\n\n"]}
|