UNPKG

@energica-city/shared-amplify-utils

Version:

Shared utilities for AWS Amplify projects

349 lines 13.9 kB
/* eslint-disable max-lines */ // QueryFactory.ts - Type-safe CRUD operations for AWS Amplify Data models import { throwError } from '../error'; import { ClientManager } from './ClientManager'; import { logSuccess, validateResponse, extractIdentifier, createObjectHash, handlePagination, buildListParams, buildIndexParams, checkQueryCache, setQueryCache, } from './helpers'; import { getGlobalCache } from './cache'; //#region MAIN FACTORY FUNCTION /** * Creates type-safe CRUD operations for AWS Amplify Data models. * * Generates a complete set of database operations (create, read, update, delete, list) * with comprehensive error handling, logging, and optional caching. All operations * preserve TypeScript types across package boundaries. * * **Features:** * - Type-safe operations with full TypeScript support * - Automatic error handling and logging * - Optional LRU caching with smart invalidation * - Pagination support with automatic following * - Schema-aware identifier extraction * - Client isolation via unique keys * * @template Types - Record of all available Amplify model types * @template TName - Specific model name as string literal * @param config - Configuration object for factory creation * @returns Promise resolving to complete CRUD operations interface * * @example * ```typescript * const userFactory = await QueryFactory({ * name: "User", * clientKey: "main", * cache: { enabled: true, maxSize: 10 * 1024 * 1024 } * }); * * const user = await userFactory.get({ input: { userId: "123" } }); * const users = await userFactory.list({ limit: 50, followNextToken: true }); * ``` */ export function QueryFactory(config) { return createQueryFactory(config); } /** * Internal factory implementation that creates the actual query operations. * * @internal * @template Types - Record of all available Amplify model types * @template TName - Specific model name as string literal * @param config - Configuration object for factory creation * @returns Promise resolving to query operations interface */ async function createQueryFactory(config) { const { name, clientKey = 'default', cache: cacheConfig } = config; const nameStr = String(name); const cache = cacheConfig ? getGlobalCache(cacheConfig) : undefined; const manager = ClientManager.getInstance(clientKey); const client = await manager.getClient(); const model = getModelFromClient(client, nameStr); const queryResult = { create: createCreateOperation(model, nameStr, cache), update: createUpdateOperation(model, nameStr, cache), delete: createDeleteOperation(model, nameStr, cache), get: createGetOperation(model, nameStr, cache), list: createListOperation(model, nameStr, cache), queryIndex: createIndexQueryOperation(nameStr, cache), }; return queryResult; } //#endregion //#region OPERATION FACTORY FUNCTIONS /** * Creates a type-safe create operation with caching invalidation. * * @internal * @template Types - Record of all available Amplify model types * @template TName - Specific model name as string literal * @param model - Amplify model instance * @param nameStr - String representation of model name * @param cache - Optional cache instance for invalidation * @returns Function that performs create operations */ function createCreateOperation(model, nameStr, cache) { return async (props) => { try { const { input, selectionSet } = props; // Build request params - ensure input is treated as object const requestParams = Object.assign({}, input); // eslint-disable-next-line @typescript-eslint/no-explicit-any const response = await model.create(requestParams, selectionSet ? { selectionSet } : undefined); const data = validateResponse({ response, operation: 'create', name: nameStr, input, }); cache?.invalidatePattern(`${nameStr}:list`); logSuccess('create', { nameStr }); return data; } catch (error) { throw throwError(`${nameStr} could not be created`, error); } }; } /** * Creates a type-safe update operation with cache invalidation. * * @internal * @template Types - Record of all available Amplify model types * @template TName - Specific model name as string literal * @param model - Amplify model instance * @param nameStr - String representation of model name * @param cache - Optional cache instance for invalidation * @returns Function that performs update operations */ function createUpdateOperation(model, nameStr, cache) { return async (props) => { try { const { input, selectionSet } = props; // Build request params - ensure input is treated as object const requestParams = Object.assign({}, input); // eslint-disable-next-line @typescript-eslint/no-explicit-any const response = await model.update(requestParams, selectionSet ? { selectionSet } : undefined); const data = validateResponse({ response, operation: 'update', name: nameStr, input, }); const identifier = extractIdentifier(input, nameStr); cache?.delete(`${nameStr}:get:${createObjectHash(identifier, nameStr)}`); cache?.invalidatePattern(`${nameStr}:list`); logSuccess('update', { nameStr }); return data; } catch (error) { throw throwError(`${nameStr} could not be updated`, error); } }; } /** * Creates a type-safe delete operation with cache invalidation. * * @internal * @template Types - Record of all available Amplify model types * @template TName - Specific model name as string literal * @param model - Amplify model instance * @param nameStr - String representation of model name * @param cache - Optional cache instance for invalidation * @returns Function that performs delete operations */ function createDeleteOperation(model, nameStr, cache) { return async (props) => { try { const { input, selectionSet } = props; // Build request params - ensure input is treated as object const requestParams = Object.assign({}, input); // eslint-disable-next-line @typescript-eslint/no-explicit-any const response = await model.delete(requestParams, selectionSet ? { selectionSet } : undefined); const data = validateResponse({ response, operation: 'delete', name: nameStr, input, }); const identifier = extractIdentifier(input, nameStr); cache?.delete(`${nameStr}:get:${createObjectHash(identifier, nameStr)}`); cache?.invalidatePattern(`${nameStr}:list`); logSuccess('delete', { nameStr }); return data; } catch (error) { throw throwError(`${nameStr} could not be deleted`, error); } }; } /** * Creates a type-safe get operation with cache-first strategy. * * @internal * @template Types - Record of all available Amplify model types * @template TName - Specific model name as string literal * @param model - Amplify model instance * @param nameStr - String representation of model name * @param cache - Optional cache instance for retrieval and storage * @returns Function that performs get operations */ function createGetOperation(model, nameStr, cache) { return async (props) => { try { const { input, selectionSet } = props; const cacheKey = `${nameStr}:get:${createObjectHash(input, nameStr)}${selectionSet ? `:${JSON.stringify(selectionSet)}` : ''}`; const cached = cache?.get(cacheKey); if (cached) { logSuccess('get', { nameStr, source: 'cache' }); return cached; } // Build request params const inputObj = input; const requestParams = { ...inputObj }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const response = await model.get(requestParams, selectionSet ? { selectionSet } : undefined); const data = validateResponse({ response, operation: 'get', name: nameStr, input, }); cache?.set(cacheKey, data); logSuccess('get', { nameStr, data, selectionSet }); return data; } catch (error) { throw throwError(`${nameStr} could not be retrieved`, error); } }; } /** * Creates a type-safe list operation with pagination and caching. * * @internal * @template Types - Record of all available Amplify model types * @template TName - Specific model name as string literal * @param model - Amplify model instance * @param nameStr - String representation of model name * @param cache - Optional cache instance for retrieval and storage * @returns Function that performs list operations with pagination */ function createListOperation(model, nameStr, cache) { return async (props = {}) => { try { const { filter, sortDirection, limit, nextToken, authMode, followNextToken = false, maxPages = 10, selectionSet, } = props; // Check cache const isCacheable = !nextToken && !filter && !followNextToken && !!cache; const cached = checkQueryCache({ nameStr, cacheType: 'list', isCacheable, hashData: { limit, sortDirection, selectionSet }, cache, }); if (cached) { logSuccess('list', { count: cached.items.length, source: 'cache' }); return cached; } // Build params and execute pagination const listParams = buildListParams({ filter, sortDirection, limit, authMode, selectionSet, }); const result = await handlePagination(async (params = {}) => model.list(params), { ...listParams, nextToken }, { followNextToken, maxPages }); // Cache result if eligible setQueryCache({ nameStr, cacheType: 'list', isCacheable, hashData: { limit, sortDirection, selectionSet }, result, cache, }); logSuccess('list', { count: result.items.length }); return result; } catch (error) { throw throwError(`${nameStr} list could not be retrieved`, error); } }; } //#endregion //#region INDEX QUERY OPERATION /** * Creates an operation to execute named secondary index queries. * * Relies on Amplify client's generated `queries[queryField]` shape. */ function createIndexQueryOperation(nameStr, cache) { return async (props) => { const { queryField, input = {}, filter, limit, nextToken, authMode, followNextToken = false, maxPages = 10, selectionSet, } = props; try { // Check cache const isCacheable = !nextToken && !followNextToken && !!cache; const cached = checkQueryCache({ nameStr, cacheType: 'index', isCacheable, hashData: { queryField, input, filter, limit, selectionSet }, cache, }); if (cached) { logSuccess('list', { source: 'cache' }); return cached; } const manager = ClientManager.getInstance(); const client = await manager.getClient(); const model = client.models[nameStr]; if (!model) { throw throwError(`Model '${nameStr}' not found in client models`); } const fn = model[queryField]; if (typeof fn !== 'function') { throw throwError(`Index query '${queryField}' not found on model '${nameStr}'. Available methods: ${Object.keys(model).join(', ')}`); } const result = await handlePagination(async (p = {}) => fn(buildIndexParams(input, { filter, limit, authMode, selectionSet, nextToken: p.nextToken, })), { nextToken }, { followNextToken, maxPages }); setQueryCache({ nameStr, cacheType: 'index', isCacheable, hashData: { queryField, input, filter, limit, selectionSet }, result, cache, }); logSuccess('list', { count: result.items.length }); return result; } catch (error) { throw throwError(`${nameStr} index query failed`, error); } }; } //#endregion //#region UTILITY FUNCTIONS /** * Extracts a specific model from the Amplify client models collection. * * @internal * @param client - Amplify client with models collection * @param nameStr - Model name to extract * @returns The requested model instance * @throws Error if model is not found, listing available models */ function getModelFromClient(client, nameStr) { const modelRef = client.models[nameStr]; if (!modelRef) { throw throwError(`Model "${nameStr}" not found in client models. Available models: ${Object.keys(client.models).join(', ')}`); } return modelRef; } //#endregion //#region QUERY FACTORY TYPES //# sourceMappingURL=QueryFactory.js.map