UNPKG

@energica-city/shared-amplify-utils

Version:

Shared utilities for AWS Amplify projects

363 lines 13.6 kB
// ClientManager.ts import { generateClient } from 'aws-amplify/data'; import { Amplify } from 'aws-amplify'; import { logger } from '../log'; //#region GLOBAL STATE AND UTILITIES /** * Global state for schema and configuration management. * These variables maintain the system-wide Amplify configuration. */ let globalAmplifyOutputs = null; let globalSchema = null; let initializationPromise = null; /** * Extracts identifier field names from Amplify model schema. * * Attempts to find identifier fields using various schema properties: * - identifier (array or string) * - primaryKey (array) * - keys (array) * - Falls back to convention-based naming * * @param schema - The Amplify schema containing model definitions * @param entityName - Name of the entity to extract identifiers for * @returns Array of identifier field names */ function extractIdentifierFields(schema, entityName) { try { const model = schema.models[entityName]; if (!model || typeof model !== 'object') { logger.warn(`Model ${entityName} not found in schema`); return [`${entityName.toLowerCase()}Id`]; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const modelAny = model; if (modelAny.identifier && Array.isArray(modelAny.identifier)) { return modelAny.identifier; } if (modelAny.identifier && typeof modelAny.identifier === 'string') { return [modelAny.identifier]; } if (modelAny.primaryKey && Array.isArray(modelAny.primaryKey)) { return modelAny.primaryKey; } if (modelAny.keys && Array.isArray(modelAny.keys)) { return modelAny.keys; } const conventionId = `${entityName.toLowerCase()}Id`; return [conventionId]; } catch (error) { logger.warn(`Failed to extract identifier for ${entityName}, using convention`, { error }); return [`${entityName.toLowerCase()}Id`]; } } //#endregion //#region CLIENT MANAGER CLASS /** * Singleton manager for AWS Amplify client instances and query factories. * * Handles complete lifecycle management including: * - Amplify client initialization * - Query factory creation and storage * - Global system configuration * - Multi-client isolation via unique keys */ export class ClientManager { clientKey; static instances = new Map(); client = null; initPromise = null; queryFactories = new Map(); schema = null; constructor(clientKey) { this.clientKey = clientKey; } //#region STATIC FACTORY METHODS /** * Gets or creates a ClientManager instance for the specified key with optional schema. * * @template TSchema - Schema type containing model definitions * @param clientKey - Unique identifier for client isolation (default: "default") * @param schema - Optional schema to use for typing the client * @returns ClientManager instance for the specified key */ static getInstance(clientKey = 'default', schema) { if (!ClientManager.instances.has(clientKey)) { ClientManager.instances.set(clientKey, new ClientManager(clientKey)); } const instance = ClientManager.instances.get(clientKey); // Store the schema if provided if (schema) { instance.setSchema(schema); } return instance; } /** * Main initialization method for the entire system. * * Configures Amplify globally and initializes query factories for specified entities. * This is the primary entry point for system setup. * * @template TSchema - Schema type containing model definitions * @template TTypes - Record of all available Amplify model types * @template TSelected - Selected entity names to initialize * @param config - Configuration object for initialization * @returns Promise resolving to query factories for all specified entities */ static async initializeQueries(config) { const { amplifyOutputs, schema, entities, clientKey = 'default', cache, } = config; // Initialize Amplify system once if (!initializationPromise) { initializationPromise = (async () => { try { Amplify.configure(amplifyOutputs); globalAmplifyOutputs = amplifyOutputs; globalSchema = schema; const manager = ClientManager.getInstance(clientKey); await manager.initializeClient(); } catch (error) { initializationPromise = null; const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Failed to initialize Amplify system', { error: errorMessage, }); throw error; } })(); } await initializationPromise; // Get entity names to initialize const schemaModelNames = Object.keys(schema.models || {}).filter((key) => typeof key === 'string'); const entitiesToInitialize = entities ? [...entities] : schemaModelNames; const manager = ClientManager.getInstance(clientKey); // Use centralized logic to ensure query factories exist const { queries } = await manager.ensureQueryFactories({ entities: entitiesToInitialize, cache, createMissing: true, }); return queries; } /** * Gets global identifier fields for an entity from the schema. * * @param entityName - Name of the entity to get identifier fields for * @returns Array of identifier field names */ static getIdentifierFields(entityName) { if (!globalSchema) { return [`${entityName.toLowerCase()}Id`]; // Fallback } return extractIdentifierFields(globalSchema, entityName); } /** * Gets the current global Amplify configuration. * * @returns Global Amplify outputs or null if not configured */ static getGlobalAmplifyOutputs() { return globalAmplifyOutputs; } /** * Resets all client instances and global state. * Primarily used for testing scenarios. */ static resetAll() { ClientManager.instances.forEach(manager => manager.reset()); ClientManager.instances.clear(); logger.debug('All clients reset'); } //#endregion //#region CLIENT MANAGEMENT /** * Sets the schema for this client manager instance. * * @template TSchema - Schema type containing model definitions * @param schema - Schema to use for typing */ setSchema(schema) { this.schema = schema; } /** * Gets the stored schema for this instance. * * @returns The stored schema or null if not set */ getSchema() { return this.schema; } /** * Gets the initialized Amplify client with proper typing based on the schema. * * @template TTypes - Record of all available Amplify model types (inferred from schema) * @returns Promise resolving to the initialized client */ async getClient() { // If client is already initialized, return it if (this.client !== null) { return this.client; } // Check if Amplify has been configured globally if (!globalAmplifyOutputs) { throw new Error('Amplify not configured. Call ClientManager.initializeQueries() with amplifyOutputs first.'); } // Initialize the client if not already in progress await this.initializeClient(); // After initialization, client should not be null if (this.client === null) { throw new Error(`Client initialization failed for key: ${this.clientKey}`); } return this.client; } /** * Checks if the client has been initialized. * * @returns True if client is initialized, false otherwise */ isInitialized() { return this.client !== null; } /** * Resets this client instance, clearing all state. */ reset() { this.client = null; this.initPromise = null; this.queryFactories.clear(); logger.debug(`Client reset for key: ${this.clientKey}`); } //#endregion //#region QUERY FACTORY STORAGE /** * Stores a query factory for reuse. * * @template T - Entity name as string literal * @template TTypes - Record of all available Amplify model types * @param entityName - Name of the entity * @param factory - Query factory instance to store */ setQueryFactory(entityName, factory) { this.queryFactories.set(entityName, factory); } /** * Retrieves a stored query factory. * * @template T - Entity name as string literal * @template TTypes - Record of all available Amplify model types * @param entityName - Name of the entity * @returns Query factory instance or null if not found */ getQueryFactory(entityName) { const factory = this.queryFactories.get(entityName); return factory ? factory : null; } //#endregion //#region QUERY FACTORY OPERATIONS /** * Gets query factories with automatic initialization of missing entities. * * This is the main method for retrieving query factories. If entities haven't * been initialized yet, they will be created automatically. * * @template TTypes - Record of all available Amplify model types * @template TSelected - Selected entity names to retrieve * @param config - Configuration for query factory retrieval * @returns Promise resolving to query factories for all specified entities */ async getQueryFactories(config) { const { entities, cache } = config; // Ensure Amplify is configured if (!globalAmplifyOutputs) { throw new Error('Amplify not configured. Call ClientManager.initializeQueries() with amplifyOutputs first.'); } const { queries } = await this.ensureQueryFactories({ entities, cache, createMissing: true, }); return queries; } /** * Ensures query factories exist, creating them if needed. * * This is a centralized method that eliminates code duplication between * different initialization functions. * * @template TTypes - Record of all available Amplify model types * @template TSelected - Selected entity names to ensure * @param config - Configuration for factory creation * @returns Promise resolving to factory creation results */ async ensureQueryFactories(config) { const { entities, cache, createMissing = true } = config; const queries = {}; const missing = []; const initialized = []; // Check which entities are already initialized for (const entityName of entities) { const entityKey = String(entityName); const existingFactory = this.getQueryFactory(entityKey); if (existingFactory) { queries[entityName] = existingFactory; } else { missing.push(entityKey); } } // Create missing entities if requested if (createMissing && missing.length > 0) { const { QueryFactory } = await import('./QueryFactory'); for (const entityKey of missing) { try { const queryFactory = await QueryFactory({ name: entityKey, clientKey: this.clientKey, ...(cache && { cache }), }); this.setQueryFactory(entityKey, queryFactory); queries[entityKey] = queryFactory; initialized.push(entityKey); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to initialize entity "${entityKey}": ${errorMessage}`); } } } return { queries, missing, initialized }; } //#endregion //#region PRIVATE METHODS /** * Initializes the Amplify client instance. * * @private * @template T - Type of the client (should extend Record<string, unknown>) * @returns Promise that resolves when client is initialized * @throws Error if client generation fails */ async initializeClient() { if (!this.initPromise) { this.initPromise = (async () => { try { this.client = generateClient(); } catch (error) { this.initPromise = null; // Allow retry const message = error instanceof Error ? error.message : String(error); logger.error(`Client generation failed for key: ${this.clientKey}`, { error: message, }); throw new Error(`Failed to generate client '${this.clientKey}': ${message}`); } })(); } await this.initPromise; } } //#endregion //# sourceMappingURL=ClientManager.js.map