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
JavaScript
// 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
};