@markwharton/pwa-core 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.d.ts CHANGED
@@ -186,20 +186,62 @@ export declare function notFoundResponse(resource: string): HttpResponseInit;
186
186
  * if (existingUser) return conflictResponse('Email already registered');
187
187
  */
188
188
  export declare function conflictResponse(message: string): HttpResponseInit;
189
+ /**
190
+ * Validates that a required parameter is present.
191
+ * Returns an error response if missing, null if valid.
192
+ * @param value - The value to check
193
+ * @param paramName - The parameter name for the error message
194
+ * @returns HttpResponseInit if invalid, null if valid
195
+ * @example
196
+ * const error = validateRequired(userId, 'userId');
197
+ * if (error) return error;
198
+ * // userId is guaranteed to be defined here
199
+ */
200
+ export declare function validateRequired(value: string | undefined, paramName: string): HttpResponseInit | null;
201
+ /**
202
+ * Callback type for error handling hooks (e.g., alerting, monitoring).
203
+ */
204
+ export type ErrorCallback = (operation: string, message: string) => void;
205
+ /**
206
+ * Sets a callback to be invoked when handleFunctionError is called.
207
+ * Use this to integrate with alerting or monitoring systems.
208
+ * @param callback - The callback to invoke (fire-and-forget)
209
+ * @example
210
+ * setErrorCallback((operation, message) => {
211
+ * sendAlert(operation, message); // Fire-and-forget
212
+ * });
213
+ */
214
+ export declare function setErrorCallback(callback: ErrorCallback | null): void;
189
215
  /**
190
216
  * Handles unexpected errors safely by logging details and returning a generic message.
191
217
  * Use in catch blocks to avoid exposing internal error details to clients.
192
218
  * @param error - The caught error
193
219
  * @param context - Azure Functions InvocationContext for logging
220
+ * @param operation - Optional operation name for error callback
194
221
  * @returns Azure Functions HttpResponseInit with 500 status
195
222
  * @example
196
223
  * try {
197
224
  * await riskyOperation();
198
225
  * } catch (error) {
199
- * return handleFunctionError(error, context);
226
+ * return handleFunctionError(error, context, 'riskyOperation');
227
+ * }
228
+ */
229
+ export declare function handleFunctionError(error: unknown, context: InvocationContext, operation?: string): HttpResponseInit;
230
+ /**
231
+ * Checks if an error is an Azure Table Storage error with a specific status code.
232
+ * @param error - The caught error
233
+ * @param statusCode - The HTTP status code to check for
234
+ * @returns True if error has the specified statusCode
235
+ * @example
236
+ * try {
237
+ * await tableClient.upsertEntity(entity);
238
+ * } catch (error) {
239
+ * if (isTableStorageError(error, 412)) {
240
+ * // Precondition failed - ETag mismatch
241
+ * }
200
242
  * }
201
243
  */
202
- export declare function handleFunctionError(error: unknown, context: InvocationContext): HttpResponseInit;
244
+ export declare function isTableStorageError(error: unknown, statusCode: number): boolean;
203
245
  /**
204
246
  * Checks if an error is an Azure Table Storage "not found" error.
205
247
  * @param error - The caught error
@@ -273,6 +315,20 @@ export declare function getTableClient(tableName: string): Promise<TableClient>;
273
315
  * });
274
316
  */
275
317
  export declare function clearTableClientCache(): void;
318
+ /**
319
+ * Gets an entity from Azure Table Storage if it exists, or returns null.
320
+ * Eliminates the common try-catch-isNotFoundError pattern.
321
+ * @typeParam T - The entity type (extends object)
322
+ * @param client - The TableClient instance
323
+ * @param partitionKey - The partition key
324
+ * @param rowKey - The row key
325
+ * @returns The entity if found, null if not found
326
+ * @throws Re-throws non-404 errors
327
+ * @example
328
+ * const user = await getEntityIfExists<UserEntity>(client, 'users', id);
329
+ * if (!user) return notFoundResponse('User');
330
+ */
331
+ export declare function getEntityIfExists<T extends object>(client: TableClient, partitionKey: string, rowKey: string): Promise<T | null>;
276
332
  /**
277
333
  * Generate a unique row key from an identifier string.
278
334
  * Uses SHA-256 hash for consistent, URL-safe keys.
package/dist/server.js CHANGED
@@ -26,7 +26,10 @@ exports.unauthorizedResponse = unauthorizedResponse;
26
26
  exports.forbiddenResponse = forbiddenResponse;
27
27
  exports.notFoundResponse = notFoundResponse;
28
28
  exports.conflictResponse = conflictResponse;
29
+ exports.validateRequired = validateRequired;
30
+ exports.setErrorCallback = setErrorCallback;
29
31
  exports.handleFunctionError = handleFunctionError;
32
+ exports.isTableStorageError = isTableStorageError;
30
33
  exports.isNotFoundError = isNotFoundError;
31
34
  exports.isConflictError = isConflictError;
32
35
  exports.initStorage = initStorage;
@@ -34,6 +37,7 @@ exports.initStorageFromEnv = initStorageFromEnv;
34
37
  exports.useManagedIdentity = useManagedIdentity;
35
38
  exports.getTableClient = getTableClient;
36
39
  exports.clearTableClientCache = clearTableClientCache;
40
+ exports.getEntityIfExists = getEntityIfExists;
37
41
  exports.generateRowKey = generateRowKey;
38
42
  const crypto_1 = require("crypto");
39
43
  const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
@@ -300,27 +304,86 @@ function conflictResponse(message) {
300
304
  jsonBody: { error: message }
301
305
  };
302
306
  }
307
+ /**
308
+ * Validates that a required parameter is present.
309
+ * Returns an error response if missing, null if valid.
310
+ * @param value - The value to check
311
+ * @param paramName - The parameter name for the error message
312
+ * @returns HttpResponseInit if invalid, null if valid
313
+ * @example
314
+ * const error = validateRequired(userId, 'userId');
315
+ * if (error) return error;
316
+ * // userId is guaranteed to be defined here
317
+ */
318
+ function validateRequired(value, paramName) {
319
+ if (!value) {
320
+ return badRequestResponse(`${paramName} is required`);
321
+ }
322
+ return null;
323
+ }
324
+ let errorCallback = null;
325
+ /**
326
+ * Sets a callback to be invoked when handleFunctionError is called.
327
+ * Use this to integrate with alerting or monitoring systems.
328
+ * @param callback - The callback to invoke (fire-and-forget)
329
+ * @example
330
+ * setErrorCallback((operation, message) => {
331
+ * sendAlert(operation, message); // Fire-and-forget
332
+ * });
333
+ */
334
+ function setErrorCallback(callback) {
335
+ errorCallback = callback;
336
+ }
303
337
  /**
304
338
  * Handles unexpected errors safely by logging details and returning a generic message.
305
339
  * Use in catch blocks to avoid exposing internal error details to clients.
306
340
  * @param error - The caught error
307
341
  * @param context - Azure Functions InvocationContext for logging
342
+ * @param operation - Optional operation name for error callback
308
343
  * @returns Azure Functions HttpResponseInit with 500 status
309
344
  * @example
310
345
  * try {
311
346
  * await riskyOperation();
312
347
  * } catch (error) {
313
- * return handleFunctionError(error, context);
348
+ * return handleFunctionError(error, context, 'riskyOperation');
314
349
  * }
315
350
  */
316
- function handleFunctionError(error, context) {
351
+ function handleFunctionError(error, context, operation) {
317
352
  const message = error instanceof Error ? error.message : 'Unknown error';
318
353
  context.error(`Function error: ${message}`);
354
+ // Fire-and-forget callback (for alerting, monitoring)
355
+ if (errorCallback && operation) {
356
+ try {
357
+ errorCallback(operation, message);
358
+ }
359
+ catch {
360
+ // Swallow callback errors to prevent cascading failures
361
+ }
362
+ }
319
363
  return {
320
364
  status: shared_1.HTTP_STATUS.INTERNAL_ERROR,
321
365
  jsonBody: { error: 'Internal server error' }
322
366
  };
323
367
  }
368
+ /**
369
+ * Checks if an error is an Azure Table Storage error with a specific status code.
370
+ * @param error - The caught error
371
+ * @param statusCode - The HTTP status code to check for
372
+ * @returns True if error has the specified statusCode
373
+ * @example
374
+ * try {
375
+ * await tableClient.upsertEntity(entity);
376
+ * } catch (error) {
377
+ * if (isTableStorageError(error, 412)) {
378
+ * // Precondition failed - ETag mismatch
379
+ * }
380
+ * }
381
+ */
382
+ function isTableStorageError(error, statusCode) {
383
+ return (error instanceof Error &&
384
+ 'statusCode' in error &&
385
+ error.statusCode === statusCode);
386
+ }
324
387
  /**
325
388
  * Checks if an error is an Azure Table Storage "not found" error.
326
389
  * @param error - The caught error
@@ -334,9 +397,7 @@ function handleFunctionError(error, context) {
334
397
  * }
335
398
  */
336
399
  function isNotFoundError(error) {
337
- return (error instanceof Error &&
338
- 'statusCode' in error &&
339
- error.statusCode === 404);
400
+ return isTableStorageError(error, shared_1.HTTP_STATUS.NOT_FOUND);
340
401
  }
341
402
  /**
342
403
  * Checks if an error is an Azure Table Storage "conflict" error.
@@ -351,15 +412,21 @@ function isNotFoundError(error) {
351
412
  * }
352
413
  */
353
414
  function isConflictError(error) {
354
- return (error instanceof Error &&
355
- 'statusCode' in error &&
356
- error.statusCode === 409);
415
+ return isTableStorageError(error, shared_1.HTTP_STATUS.CONFLICT);
357
416
  }
358
417
  // =============================================================================
359
418
  // Azure Table Storage
360
419
  // =============================================================================
361
420
  // Cache table clients to avoid repeated connections
362
421
  const tableClients = new Map();
422
+ // Shared credential instance for managed identity (avoids repeated instantiation)
423
+ let credential = null;
424
+ function getCredential() {
425
+ if (!credential) {
426
+ credential = new identity_1.DefaultAzureCredential();
427
+ }
428
+ return credential;
429
+ }
363
430
  // Configuration
364
431
  let storageAccountName;
365
432
  let storageConnectionString;
@@ -416,10 +483,9 @@ async function getTableClient(tableName) {
416
483
  }
417
484
  let client;
418
485
  if (useManagedIdentity()) {
419
- // Azure: Use managed identity
420
- const credential = new identity_1.DefaultAzureCredential();
486
+ // Azure: Use managed identity (credential is cached)
421
487
  const endpoint = `https://${storageAccountName}.table.core.windows.net`;
422
- client = new data_tables_1.TableClient(endpoint, tableName, credential);
488
+ client = new data_tables_1.TableClient(endpoint, tableName, getCredential());
423
489
  }
424
490
  else if (storageConnectionString) {
425
491
  // Local/Azurite: Use connection string
@@ -453,6 +519,30 @@ async function getTableClient(tableName) {
453
519
  function clearTableClientCache() {
454
520
  tableClients.clear();
455
521
  }
522
+ /**
523
+ * Gets an entity from Azure Table Storage if it exists, or returns null.
524
+ * Eliminates the common try-catch-isNotFoundError pattern.
525
+ * @typeParam T - The entity type (extends object)
526
+ * @param client - The TableClient instance
527
+ * @param partitionKey - The partition key
528
+ * @param rowKey - The row key
529
+ * @returns The entity if found, null if not found
530
+ * @throws Re-throws non-404 errors
531
+ * @example
532
+ * const user = await getEntityIfExists<UserEntity>(client, 'users', id);
533
+ * if (!user) return notFoundResponse('User');
534
+ */
535
+ async function getEntityIfExists(client, partitionKey, rowKey) {
536
+ try {
537
+ return await client.getEntity(partitionKey, rowKey);
538
+ }
539
+ catch (error) {
540
+ if (isNotFoundError(error)) {
541
+ return null;
542
+ }
543
+ throw error;
544
+ }
545
+ }
456
546
  /**
457
547
  * Generate a unique row key from an identifier string.
458
548
  * Uses SHA-256 hash for consistent, URL-safe keys.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/pwa-core",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Shared patterns for Azure PWA projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",