@funztic/db
Version:
TypeScript기반 파일 데이터베이스, zod를 지원합니다.
238 lines (237 loc) • 8.19 kB
JavaScript
export function* autoIncrement(start = 0) {
let i = start;
while (true)
yield i++;
}
export class Repository {
constructor(collection, options) {
this.collection = collection;
this.schema = collection.schema;
if (!this.schema)
throw new Error("Repository requires a Collection instance with a Zod schema.");
this.options = options;
}
getSchema() {
return this.schema;
}
getType() {
return typeof this;
}
async save(entityData) {
let processedEntity = { ...entityData };
const id = processedEntity.id;
let existingEntity = id ? await this.collection.get(id) : null;
const isUpdate = !!existingEntity;
if (isUpdate && !existingEntity) {
throw new Error(`Save failed: Entity with ID '${id}' marked for update but not found.`);
}
if (!isUpdate) {
if (this.options.beforeCreate)
processedEntity = await this.options.beforeCreate(processedEntity);
if (!processedEntity.id)
processedEntity.id = this.options.idGenerator();
if (await this.collection.get(processedEntity.id))
throw new Error(`Save failed: Generated ID '${processedEntity.id}' already exists.`);
}
const entityToValidate = isUpdate && existingEntity
? { ...existingEntity, ...processedEntity }
: { ...processedEntity };
let validatedEntity;
try {
const validationResult = this.schema.safeParse(entityToValidate);
if (!validationResult.success) {
throw validationResult.error;
}
validatedEntity = validationResult.data;
if (validatedEntity.id !== entityToValidate.id) {
throw new Error(`ID mismatch after validation during save: expected ${entityToValidate.id}, got ${validatedEntity.id}`);
}
}
catch (error) {
throw error;
}
if (this.options.beforeSave)
validatedEntity = await this.options.beforeSave(validatedEntity);
let savedEntity;
if (isUpdate && existingEntity) {
const updates = {};
let hasChanges = false;
const keys = Object.keys(validatedEntity);
for (const key of keys) {
if (key !== 'id' && key in validatedEntity && validatedEntity[key] !== existingEntity[key]) {
updates[key] = validatedEntity[key];
hasChanges = true;
}
}
if (hasChanges) {
savedEntity = await this.collection.update(validatedEntity.id, updates);
}
else {
savedEntity = existingEntity;
}
}
else {
savedEntity = await this.collection.create(validatedEntity);
}
if (!savedEntity) {
throw new Error(`Save operation failed: Collection did not return a saved entity for ID ${validatedEntity.id}.`);
}
if (this.options.afterSave) {
await this.options.afterSave(savedEntity);
}
return savedEntity;
}
async findById(id) {
const entity = await this.collection.get(id);
return entity || null;
}
async findAll() {
return await this.collection.getAll();
}
async findBy(criteria) {
const entries = Object.entries(criteria);
if (entries.length === 0)
return await this.findAll();
return await this.collection.find((entity) => {
return entries.every(([key, value]) => {
return key in entity && entity[key] === value;
});
});
}
async findOne(criteria) {
const entries = Object.entries(criteria);
if (entries.length === 0) {
const all = await this.collection.getAll();
return all.length > 0 ? all[0] : null;
}
const result = await this.collection.query(data => {
for (const id in data) {
const entity = data[id];
const match = entries.every(([key, value]) => {
return key in entity && entity[key] === value;
});
if (match) {
return entity;
}
}
return null;
});
return result;
}
async update(id, data) {
const entity = await this.collection.get(id);
if (!entity)
return null;
let updates = { ...data };
if (this.options.beforeUpdate) {
updates = await this.options.beforeUpdate(entity, updates);
}
const potentialItem = {
...entity,
...updates,
id: id
};
let validatedItem;
try {
validatedItem = this.schema.parse(potentialItem);
if (validatedItem.id !== id) {
throw new Error(`ID mismatch after update validation: expected ${id}, got ${validatedItem.id}`);
}
}
catch (error) {
throw error;
}
const actualUpdates = {};
let hasChanges = false;
for (const key of Object.keys(validatedItem)) {
if (key !== 'id' && key in validatedItem && validatedItem[key] !== entity[key]) {
actualUpdates[key] = validatedItem[key];
hasChanges = true;
}
}
if (!hasChanges) {
return entity;
}
const updatedEntity = await this.collection.update(id, actualUpdates);
if (updatedEntity && this.options.afterSave) {
await this.options.afterSave(updatedEntity);
}
return updatedEntity || null;
}
async delete(id) {
return await this.collection.delete(id);
}
async count() {
const all = await this.collection.getAll();
return all.length;
}
async exists(id) {
const entity = await this.collection.get(id);
return !!entity;
}
async clear() {
await this.collection.clear();
}
async query(queryFn) {
const entities = await this.collection.getAll();
return queryFn(entities);
}
async paginate(page = 1, limit = 10) {
const allItems = await this.collection.getAll();
const total = allItems.length;
const start = (page - 1) * limit;
const end = start + limit;
const data = allItems.slice(start, end);
const totalPages = Math.ceil(total / limit);
return {
data,
total,
page,
limit,
totalPages
};
}
async *createPaginator(limit = 10, sortBy, order = 'asc') {
const data = await this.findAllSorted(sortBy, order);
const total = data.length;
const totalPages = Math.ceil(total / limit);
let currentPage = 1;
while (currentPage <= totalPages) {
const start = (currentPage - 1) * limit;
const end = start + limit;
yield {
data: data.slice(start, end),
total,
page: currentPage,
isLast: currentPage === totalPages
};
currentPage++;
}
}
async findAllSorted(sortBy, order = 'asc') {
const entities = await this.collection.getAll();
return entities.sort((a, b) => {
const valueA = a[sortBy];
const valueB = b[sortBy];
if (valueA === valueB)
return 0;
if (order === 'asc') {
return valueA < valueB ? -1 : 1;
}
else {
return valueA > valueB ? -1 : 1;
}
});
}
getRawCollection() {
return this.collection;
}
}
export function createRepository(collection, options = (() => {
const idGenerator = autoIncrement();
return {
idGenerator: () => idGenerator.next().value.toString()
};
})()) {
return new Repository(collection, options);
}