@energica-city/shared-amplify-utils
Version:
Shared utilities for AWS Amplify projects
349 lines • 13.9 kB
JavaScript
/* 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