UNPKG

techveda-lib-dynamodb

Version:

A clean DynamoDB abstraction library with schema and model for Node.js and TypeScript

440 lines (420 loc) 15.8 kB
// src/model/buildJoiSchema.ts import Joi from "joi"; var buildJoiSchema = (instance) => { const joiSchema = {}; for (const config of instance.schema) { const key = config.key; let field = Joi.any(); if (config.type === "string") field = Joi.string(); if (config.type === "number") field = Joi.number(); if (config.type === "boolean") field = Joi.boolean(); if (config.type === "array") field = Joi.array(); if (config.type === "object") field = Joi.object(); if (config.type === "email") field = Joi.string().email(); if (config.required) field = field.required(); if (config.default !== void 0) field = field.default(config.default); if (key === "entity") { console.log(field, "keykey"); } if (config.enum !== void 0) field = field.valid(...config.enum); joiSchema[key] = field; } return Joi.object(joiSchema); }; // src/model/validate.ts import Joi2 from "joi"; var typeMap = { string: (value) => Joi2.string().required().validate(value), email: (value) => Joi2.string().email().required().validate(value), number: (value) => Joi2.number().required().validate(value), boolean: (value) => Joi2.boolean().required().validate(value), array: (value) => Joi2.array().required().validate(value), object: (value) => Joi2.object().required().validate(value) }; var isValuePresent = (value) => value !== null && value !== void 0 && value !== "" && value !== false && !(Array.isArray(value) && value.length === 0) && !(typeof value === "object" && Object.keys(value).length === 0); var validate = (instance, data) => { const errors = []; for (const field of instance.schema) { const { key, type, required, enum: enumValues, default: defaultValue, isConditionalRequired, conditionKey, compareValue, conditionType } = field; let fieldValue = data[key]; if (typeof fieldValue === "string") { fieldValue = fieldValue.trim(); data[key] = fieldValue; } if ((fieldValue === void 0 || fieldValue === null) && defaultValue !== void 0) { data[key] = defaultValue; fieldValue = defaultValue; } if (required && (fieldValue === void 0 || fieldValue === null || fieldValue === "")) { errors.push(`Field '${key}' is required.`); continue; } if (fieldValue !== void 0 && type && typeof type === "string" && type in typeMap) { const { error } = typeMap[type](fieldValue); if (error) { errors.push(`Field '${key}' validation failed: ${error.message}`); continue; } } if (enumValues && fieldValue !== void 0 && !enumValues.includes(fieldValue)) { errors.push(`Field '${key}' should be one of [${enumValues.join(", ")}].`); continue; } if (isConditionalRequired) { const conditionValue = data[conditionKey]; let conditionMet = false; if (conditionType === "contains" && Array.isArray(compareValue)) { conditionMet = compareValue.includes(conditionValue); } else if (conditionType === "equalTo") { conditionMet = conditionValue === compareValue; } else if (conditionType === "isValuePresent") { conditionMet = isValuePresent(conditionValue); } if (conditionMet && (fieldValue === void 0 || fieldValue === null || fieldValue === "")) { errors.push(`Field '${key}' is required when '${conditionKey}' meets condition '${conditionType}'.`); } } } if (errors.length > 0) { throw new Error(errors.join(" | ")); } return data; }; // src/utils/dateNow.ts import dayjs from "dayjs"; var dateNow = () => dayjs().toISOString(); // src/utils/constants.ts var DEFAULT_SK_TYPE = "details"; // src/utils/helpers.ts var generateKeys = (modelName, id, skType = DEFAULT_SK_TYPE, skId = "") => { const pkPrefix = modelName.toUpperCase(); const skPrefix = skType.toUpperCase(); return { PK: `${pkPrefix}#${id}`, SK: `${skPrefix}#${skId}` }; }; var removeExtraFields = (data = {}, validKeys = []) => { const finalValidKeys = [ ...validKeys, "id", "PK", "SK", "created_at", "updated_at", "deleted_at" ]; return Object.fromEntries( Object.entries(data).filter(([key]) => finalValidKeys.includes(key)) ); }; // src/model/create.ts import shortUUID from "short-uuid"; // src/dynamo/put.ts import { PutCommand } from "@aws-sdk/lib-dynamodb"; // src/utils/dynamoClient.ts import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import "dotenv/config"; var docClient; var initDynamoDB = () => { const isLocal = process.env.NODE_ENV === "development" || process.env.USE_LOCAL_DYNAMO === "true"; const client = new DynamoDBClient({ region: process.env.AWS_REGION || "ap-south-1", // default region for consistency endpoint: isLocal ? "http://localhost:8000" : void 0, credentials: isLocal ? { accessKeyId: "fakeMyKeyId", secretAccessKey: "fakeSecretAccessKey" } : { accessKeyId: process.env.AWS_ACCESS_KEY || "", secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "" } }); docClient = DynamoDBDocumentClient.from(client); return docClient; }; // src/dynamo/put.ts var putDataCommand = async (item) => { try { const params = { TableName: process.env.DATABASE_TABLE, Item: item }; await docClient.send(new PutCommand(params)); return item; } catch (error) { console.error("Error in putDataCommand:", error); throw new Error(error.message || "Error in putDataCommand"); } }; // src/model/create.ts var create = async (instance, data) => { try { const validatedData = validate(instance, data); const cleanedData = removeExtraFields(validatedData, instance.schema.map((s) => s.key)); if (!cleanedData.id) cleanedData.id = shortUUID().new(); const { PK, SK } = generateKeys(instance.PKPrefix, cleanedData.id); cleanedData.PK = PK; cleanedData.SK = SK; cleanedData.created_at = dateNow(); cleanedData.updated_at = dateNow(); Object.assign(instance, cleanedData); const modelInstance = new instance.constructor(instance.modelName, { getSchema: () => instance.schema }, instance.PKPrefix, instance.SKPrefix); const result = await putDataCommand(cleanedData); return Object.assign(modelInstance, result); ; } catch (error) { throw new Error(error); } }; // src/dynamo/update.ts import { UpdateCommand } from "@aws-sdk/lib-dynamodb"; var callUpdateByParams = async (params) => { try { if (!params.TableName) { params.TableName = process.env.DATABASE_TABLE; } const result = await docClient.send(new UpdateCommand(params)); return result.Attributes; } catch (error) { console.error("Error in callUpdateByParams:", error); throw new Error(error.message || "Error in callUpdateByParams"); } }; // src/model/save.ts var save = async (instance) => { if (!instance.PK || !instance.SK) { throw new Error("Cannot save: PK/SK missing. Call create() or find() first."); } instance.updated_at = dateNow(); const dataToSave = {}; for (const key of Object.keys(instance)) { if (typeof instance[key] !== "function") { dataToSave[key] = instance[key]; } } const cleanedData = removeExtraFields(dataToSave, instance.schema.map((s) => s.key)); instance.validate(cleanedData); const updateExpression = "SET " + Object.keys(cleanedData).filter((k) => !["PK", "SK"].includes(k)).map((k) => `#${k} = :${k}`).join(", "); const ExpressionAttributeNames = Object.fromEntries( Object.keys(cleanedData).filter((k) => !["PK", "SK"].includes(k)).map((k) => [`#${k}`, k]) ); const ExpressionAttributeValues = Object.fromEntries( Object.entries(cleanedData).filter(([k]) => !["PK", "SK"].includes(k)).map(([k, v]) => [`:${k}`, v]) ); const params = { TableName: process.env.DATABASE_TABLE, Key: { PK: instance.PK, SK: instance.SK }, UpdateExpression: updateExpression, ExpressionAttributeNames, ExpressionAttributeValues, ReturnValues: "ALL_NEW" }; console.log(params, "params"); await callUpdateByParams(params); return instance; }; // src/dynamo/get.ts import { QueryCommand } from "@aws-sdk/lib-dynamodb"; var allQueryByParams = async (params) => { try { if (!params.TableName) { params.TableName = process.env.DATABASE_TABLE; } return await docClient.send(new QueryCommand(params)); } catch (error) { console.error("Error in allQueryByParams:", error); throw new Error(error.message || "Error in allQueryByParams"); } }; // src/dynamo/scan.ts import { ScanCommand } from "@aws-sdk/lib-dynamodb"; var getDataInScanCommand = async (expressions) => { try { const params = { TableName: process.env.DATABASE_TABLE, ...expressions }; return docClient.send(new ScanCommand(params)); } catch (error) { console.error("Error in getDataInScanCommand:", error); throw new Error(error.message || "Error in getDataInScanCommand"); } }; // src/model/scan.ts var scan = async (instance, filters = {}, limit) => { const params = { TableName: process.env.DATABASE_TABLE, ExpressionAttributeNames: {}, ExpressionAttributeValues: {} }; const filterParts = []; filterParts.push(`begins_with(#PK, :pkPrefix)`); params.ExpressionAttributeNames["#PK"] = "PK"; params.ExpressionAttributeValues[":pkPrefix"] = `${instance.PKPrefix}#`; if (instance.SKPrefix) { filterParts.push(`begins_with(#SK, :skPrefix)`); params.ExpressionAttributeNames["#SK"] = "SK"; params.ExpressionAttributeValues[":skPrefix"] = `${instance.SKPrefix}#`; } for (const [key, value] of Object.entries(filters)) { const attrName = `#${key}`; const attrValue = `:${key}`; params.ExpressionAttributeNames[attrName] = key; params.ExpressionAttributeValues[attrValue] = value; if (typeof value === "string") { filterParts.push(`contains(${attrName}, ${attrValue})`); } else { filterParts.push(`${attrName} = ${attrValue}`); } } params.FilterExpression = filterParts.join(" AND "); if (filters.start_key) params.ExclusiveStartKey = JSON.parse(decodeURIComponent(filters.start_key)); params.Limit = limit; return await getDataInScanCommand(params); }; // src/model/find.ts var find = async (instance, filters = {}, limit = 1) => { const usingQuery = !!filters.id || !!filters.PK; const result = usingQuery ? await runQuery(instance, filters, limit) : await scan(instance, filters, limit); const results = []; let LastEvaluatedKey; if (result && result.Items && result.Items.length > 0) { LastEvaluatedKey = result.LastEvaluatedKey; for (const item of result.Items) { const modelInstance = new instance.constructor(instance.modelName, { getSchema: () => instance.schema }, instance.PKPrefix, instance.SKPrefix); Object.assign(modelInstance, item); results.push(modelInstance); } } return { data: results, LastEvaluatedKey }; }; var runQuery = async (instance, filters, limit) => { try { console.log("RUN QUERY"); const params = { TableName: process.env.DATABASE_TABLE, ExpressionAttributeNames: {}, ExpressionAttributeValues: {} }; const filterParts = []; const { PK, SK } = filters.PK ? filters : generateKeys(instance.PKPrefix, filters.id, filters.sktype, filters.skId); params.KeyConditionExpression = "#PK = :pk"; params.ExpressionAttributeNames["#PK"] = "PK"; params.ExpressionAttributeValues[":pk"] = PK; if (filters.skId || SK) { params.KeyConditionExpression += " AND #SK = :sk"; params.ExpressionAttributeNames["#SK"] = "SK"; params.ExpressionAttributeValues[":sk"] = SK; } for (const [key, value] of Object.entries(filters)) { if (["id", "skId", "PK"].includes(key)) continue; const attrName = `#${key}`; const attrValue = `:${key}`; params.ExpressionAttributeNames[attrName] = key; params.ExpressionAttributeValues[attrValue] = value; filterParts.push(`${attrName} = ${attrValue}`); } if (filterParts.length > 0) { params.FilterExpression = filterParts.join(" AND "); } if (filters.start_key) params.ExclusiveStartKey = JSON.parse(decodeURIComponent(filters.start_key)); params.Limit = limit; return await allQueryByParams(params); } catch (error) { } }; // src/model/list.ts var list = async (instance, filters = {}, limit = 100) => { return await find(instance, filters, limit); }; // src/model/findAndUpdate.ts var findAndUpdate = async (instance, updates) => { if (!instance.PK || !instance.SK) { throw new Error("Call find() or create() before updating."); } Object.assign(instance, updates, { updated_at: dateNow() }); return await save(instance); }; // src/model/model.ts var Model = class { constructor(modelName, schemaInstance, PKPrefix, SKPrefix) { this.buildJoiSchema = () => buildJoiSchema(this); this.validate = (data) => validate(this, data); this.create = (data) => create(this, data); this.save = () => save(this); this.find = (filters = {}, limit = 1) => find(this, filters, limit); this.list = (filters = {}, limit = 100) => list(this, filters, limit); this.findAndUpdate = (updates) => findAndUpdate(this, updates); this.scan = (filters = {}, limit = 100) => scan(this, filters, limit); const schema = schemaInstance.getSchema(); if (!schema || !Array.isArray(schema)) { throw new Error("Schema instance must be provided and return an array."); } this.modelName = modelName; this.schema = schema; this.PKPrefix = PKPrefix; this.SKPrefix = SKPrefix; } }; // src/schema/schema.ts var Schema = class { constructor(definition = {}) { if (typeof definition !== "object" || Array.isArray(definition)) { throw new Error("Schema definition must be an object."); } this.fields = []; for (const key in definition) { const config = definition[key]; if (typeof config !== "object" || config === null) { throw new Error(`Invalid config for field '${key}'. Must be an object.`); } const field = { key }; const mappedType = this.parseType(config.type); if (mappedType !== void 0) field.type = mappedType; if (config.required !== void 0) field.required = config.required; if (config.enum !== void 0) field.enum = config.enum; if (config.default !== void 0) field.default = config.default; if (config.minLength !== void 0) field.minLength = config.minLength; if (config.maxLength !== void 0) field.maxLength = config.maxLength; if (config.isConditionalRequired !== void 0) field.isConditionalRequired = config.isConditionalRequired; if (config.conditionKey !== void 0) field.conditionKey = config.conditionKey; if (config.compareValue !== void 0) field.compareValue = config.compareValue; if (config.conditionType !== void 0) field.conditionType = config.conditionType; if (config.ref !== void 0) field.ref = config.ref; this.fields.push(field); } } parseType(type) { if (type === String) return "string"; if (type === Number) return "number"; if (type === Boolean) return "boolean"; if (type === Array) return "array"; if (type === Object) return "object"; if (type === "email") return "email"; if (type === void 0) return void 0; throw new Error(`Unsupported type: ${type}`); } getSchema() { return this.fields; } }; export { Model, Schema, allQueryByParams, callUpdateByParams, docClient, generateKeys, getDataInScanCommand, initDynamoDB, putDataCommand, removeExtraFields };