UNPKG

@funztic/db

Version:

TypeScript기반 파일 데이터베이스, zod를 지원합니다.

238 lines (237 loc) 8.19 kB
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); }