UNPKG

@reldens/storage

Version:
788 lines (738 loc) 26.4 kB
/** * * Reldens - PrismaDriver * */ const { BaseDriver } = require('../base-driver'); const { Logger, sc } = require('@reldens/utils'); const { Prisma } = require('@prisma/client'); class PrismaDriver extends BaseDriver { constructor(props) { super(props); if(!props.prisma){ Logger.critical('Missing Prisma client on Prisma driver.'); return false; } if(!props.model){ Logger.critical('Missing Prisma model on Prisma driver.'); return false; } if(!props.server){ Logger.critical('Missing Server Driver on Prisma driver.'); return false; } this.prisma = props.prisma; this.model = props.model; this.server = props.server; this.requiredFields = []; this.fieldTypes = {}; this.idFieldType = 'String'; this.referenceFields = {}; this.relationMetadata = {}; this.initFieldMetadata(); this.initRelationMetadata(); } initFieldMetadata() { try { let dmmf = this.prisma._runtimeDataModel || Prisma.dmmf?.datamodel; if(!dmmf){ return; } let modelName = this.tableName().toLowerCase(); let modelInfo = dmmf.models?.[modelName] || dmmf.models?.find(m => m.name.toLowerCase() === modelName); if(!modelInfo){ return; } for(let field of modelInfo.fields){ if(field.isRequired && !field.hasDefaultValue && !field.isId){ this.requiredFields.push(field.name); } this.fieldTypes[field.name] = field.type; if(field.isId){ this.idFieldType = field.type; } if(field.kind === 'scalar' && field.isList === false){ let isNumericType = ('Int' === field.type || 'BigInt' === field.type || 'Float' === field.type || 'Decimal' === field.type); let isReferenceField = field.relationFromFields && 0 < field.relationFromFields.length; if(isNumericType || isReferenceField){ this.referenceFields[field.name] = field.type; } } } } catch(error) { Logger.warning('Could not initialize field metadata: '+error.message); } } initRelationMetadata() { try { let dmmf = this.prisma._runtimeDataModel || Prisma.dmmf?.datamodel; if(!dmmf){ return; } let modelName = this.tableName().toLowerCase(); let modelInfo = dmmf.models?.[modelName] || dmmf.models?.find(m => m.name.toLowerCase() === modelName); if(!modelInfo){ return; } for(let field of modelInfo.fields){ if('object' === field.kind){ let relationType = 'many'; if(!field.isList){ relationType = 'one'; } this.relationMetadata[field.name] = { type: relationType, model: field.type, isList: field.isList, isOptional: field.isOptional }; } } if(this.rawModel && this.rawModel.relationTypes){ for(let relationName of Object.keys(this.rawModel.relationTypes)){ if(sc.hasOwn(this.relationMetadata, relationName)){ this.relationMetadata[relationName].type = this.rawModel.relationTypes[relationName]; } } } } catch(error) { Logger.warning('Could not initialize relation metadata: '+error.message); } } castIdValue(id) { if(null === id || undefined === id){ return id; } if('Int' === this.idFieldType || 'BigInt' === this.idFieldType){ let numericId = Number(id); if(!isNaN(numericId)){ return numericId; } } return String(id); } castNumericValue(value, fieldType) { if(null === value || undefined === value || '' === value){ return null; } if('Int' === fieldType || 'BigInt' === fieldType){ let numericValue = Number(value); if(!isNaN(numericValue)){ return Math.floor(numericValue); } } if('Float' === fieldType || 'Decimal' === fieldType){ let numericValue = Number(value); if(!isNaN(numericValue)){ return numericValue; } } return value; } databaseName() { return this.config.database || ''; } id() { return this.name || ''; } name() { return this.rawName || ''; } tableName() { return this.model.name || ''; } property(propertyName) { return this.rawModel[propertyName] || null; } prepareData(params) { let data = {...params}; for(let key of Object.keys(data)){ if('status' === key && 'number' === typeof data[key]){ data[key] = String(data[key]); } if(this.isDateTimeField(key)){ data[key] = this.convertToDate(data[key], key); continue; } if(sc.hasOwn(this.referenceFields, key)){ data[key] = this.castNumericValue(data[key], this.referenceFields[key]); } } return data; } isDateTimeField(fieldName) { let fieldType = this.fieldTypes[fieldName]; return fieldType === 'DateTime' || fieldType === 'Date'; } convertToDate(dateValue, fieldName) { if(dateValue instanceof Date){ return dateValue; } if(!dateValue || '' === dateValue){ return null; } let dateString = String(dateValue); if(dateString.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)){ return new Date(dateString.replace(' ', 'T') + 'Z'); } if(dateString.match(/^\d{4}-\d{2}-\d{2}$/)){ return new Date(dateString + 'T00:00:00Z'); } try { return new Date(dateString); } catch(error) { Logger.warning('Failed to convert date value: '+dateString); return dateValue; } } ensureRequiredFields(data) { let missingFields = []; for(let field of this.requiredFields){ if(!sc.hasOwn(data, field)){ missingFields.push(field); } } if(0 < missingFields.length){ Logger.warning('Missing required fields for '+this.tableName()+': '+missingFields.join(', ')); } return data; } processFilters(filters) { if(!sc.isObject(filters)){ return filters; } let processedFilters = {}; for(let key of Object.keys(filters)){ let value = filters[key]; if(sc.hasOwn(value, 'operator') && sc.hasOwn(value, 'value')){ let operatorValue = value.value; if('id' === key && sc.isArray(operatorValue)){ operatorValue = operatorValue.map(id => this.castIdValue(id)); } if('id' === key && !sc.isArray(operatorValue)){ operatorValue = this.castIdValue(operatorValue); } if(sc.hasOwn(this.referenceFields, key) && sc.isArray(operatorValue)){ operatorValue = operatorValue.map(val => this.castNumericValue(val, this.referenceFields[key])); } if(sc.hasOwn(this.referenceFields, key) && !sc.isArray(operatorValue)){ operatorValue = this.castNumericValue(operatorValue, this.referenceFields[key]); } processedFilters[key] = this.applyOperator(operatorValue, value.operator, key); continue; } if(sc.isObject(value) && !sc.isArray(value)){ processedFilters[key] = this.processFilters(value); continue; } if('id' === key && sc.isArray(value)){ processedFilters[key] = value.map(id => this.castIdValue(id)); continue; } if('id' === key){ processedFilters[key] = this.castIdValue(value); continue; } if(sc.hasOwn(this.referenceFields, key) && sc.isArray(value)){ processedFilters[key] = value.map(val => this.castNumericValue(val, this.referenceFields[key])); continue; } if(sc.hasOwn(this.referenceFields, key)){ processedFilters[key] = this.castNumericValue(value, this.referenceFields[key]); continue; } processedFilters[key] = value; } return processedFilters; } transformRelationResults(result, includeConfig) { if(!result || !includeConfig){ return result; } if(sc.isArray(result)){ return result.map(item => this.transformSingleResult(item, includeConfig)); } return this.transformSingleResult(result, includeConfig); } transformSingleResult(item, includeConfig) { if(!item || !sc.isObject(item)){ return item; } let transformed = {...item}; for(let relationName of Object.keys(includeConfig)){ if(!sc.hasOwn(transformed, relationName)){ continue; } let relationMeta = this.relationMetadata[relationName]; if(!relationMeta){ continue; } let relationValue = transformed[relationName]; if('one' === relationMeta.type && sc.isArray(relationValue)){ transformed[relationName] = 0 < relationValue.length ? relationValue[0] : null; continue; } if('many' === relationMeta.type && !sc.isArray(relationValue)){ transformed[relationName] = relationValue ? [relationValue] : []; } } return transformed; } async create(params) { try { let preparedData = this.prepareData(params); this.ensureRequiredFields(preparedData); return await this.model.create({ data: preparedData }); } catch(error) { Logger.error('Create error: '+error.message); return false; } } async createWithRelations(params, relations) { try { let preparedData = this.prepareData(params); this.ensureRequiredFields(preparedData); let createData = {data: preparedData}; if(sc.isArray(relations) && 0 < relations.length){ for(let relation of relations){ if(!sc.hasOwn(params, relation)){ continue; } let relationData = params[relation]; if(sc.isArray(relationData)){ createData.data[relation] = { connect: relationData.map(item => ({id: this.castIdValue(item.id)})) }; continue; } createData.data[relation] = { connect: {id: this.castIdValue(relationData.id)} }; } } return await this.model.create(createData); } catch(error) { Logger.error('Create with relations error: '+error.message); return false; } } async update(filters, updatePatch) { try { let preparedData = this.prepareData(updatePatch); let processedFilters = this.processFilters(filters); return await this.model.updateMany({ where: processedFilters, data: preparedData }); } catch(error) { Logger.error('Update error: '+error.message); return false; } } async updateBy(field, fieldValue, updatePatch, operator = null) { let filter = this.createSingleFilter(field, fieldValue, operator); try { let preparedData = this.prepareData(updatePatch); return await this.model.updateMany({ where: filter, data: preparedData }); } catch(error) { Logger.error('Update by error: '+error.message); return false; } } async updateById(id, params) { try { let castedId = this.castIdValue(id); let found = await this.loadById(castedId); if(!found){ return false; } let preparedData = this.prepareData(params); return await this.model.update({ where: {id: castedId}, data: preparedData }); } catch(error) { Logger.error('Update by ID error: '+error.message); return false; } } async upsert(params, filters) { try { let preparedData = this.prepareData(params); let existing = false; if(params.id){ let castedId = this.castIdValue(params.id); existing = await this.loadById(castedId); if(existing){ let patch = Object.assign({}, preparedData); delete patch.id; return await this.model.update({ where: {id: castedId}, data: patch }); } } if(filters){ let existing = await this.loadOne(filters); if(existing){ return await this.model.update({ where: {id: this.castIdValue(existing.id)}, data: preparedData }); } } return await this.create(preparedData); } catch(error) { Logger.error('Upsert error: '+error.message); return false; } } async delete(filters = {}) { try { let processedFilters = this.processFilters(filters); return await this.model.deleteMany({ where: processedFilters }); } catch(error) { Logger.error('Delete error: '+error.message); return false; } } async deleteById(id) { try { let castedId = this.castIdValue(id); let found = await this.loadById(castedId); if(!found){ return false; } return await this.model.delete({ where: {id: castedId} }); } catch(error) { Logger.error('Delete by ID error: '+error.message); return false; } } async count(filters) { try { let processedFilters = this.processFilters(filters); return await this.model.count({ where: processedFilters }); } catch(error) { Logger.error('Count error: '+error.message); return 0; } } async countWithRelations(filters, relations) { try { let processedFilters = this.processFilters(filters); let query = { where: processedFilters }; if(sc.isArray(relations) && 0 < relations.length){ query.include = this.buildIncludeObject(relations); } return await this.model.count(query); } catch(error) { Logger.error('Count with relations error: '+error.message); return 0; } } async loadAll() { try { return await this.model.findMany(this.buildQueryOptions()); } catch(error) { Logger.error('Load all error: '+error.message); return []; } } async loadAllWithRelations(relations) { try { let query = this.buildQueryOptions(); let includeConfig = {}; if(sc.isArray(relations) && 0 < relations.length){ includeConfig = this.buildIncludeObject(relations); query.include = includeConfig; } let results = await this.model.findMany(query); return this.transformRelationResults(results, includeConfig); } catch(error) { Logger.error('Load all with relations error: '+error.message); return []; } } async load(filters) { try { let processedFilters = this.processFilters(filters); let query = this.buildQueryOptions(); query.where = processedFilters; return await this.model.findMany(query); } catch(error) { Logger.error('Load error: '+error.message); return []; } } async loadWithRelations(filters, relations) { try { let processedFilters = this.processFilters(filters); let query = this.buildQueryOptions(); query.where = processedFilters; let includeConfig = {}; if(sc.isArray(relations) && 0 < relations.length){ includeConfig = this.buildIncludeObject(relations); query.include = includeConfig; } let results = await this.model.findMany(query); return this.transformRelationResults(results, includeConfig); } catch(error) { Logger.error('Load with relations error: '+error.message); return []; } } async loadBy(field, fieldValue, operator = null) { try { let filter = this.createSingleFilter(field, fieldValue, operator); let query = this.buildQueryOptions(); query.where = filter; return await this.model.findMany(query); } catch(error) { Logger.error('Load by error: '+error.message); return []; } } async loadByWithRelations(field, fieldValue, relations, operator = null) { try { let filter = this.createSingleFilter(field, fieldValue, operator); let query = this.buildQueryOptions(); query.where = filter; let includeConfig = {}; if(sc.isArray(relations) && 0 < relations.length){ includeConfig = this.buildIncludeObject(relations); query.include = includeConfig; } let results = await this.model.findMany(query); return this.transformRelationResults(results, includeConfig); } catch(error) { Logger.error('Load by with relations error: '+error.message); return []; } } async loadById(id) { try { let castedId = this.castIdValue(id); return await this.model.findUnique({ where: {id: castedId} }); } catch(error) { Logger.error('Load by ID error: '+error.message); return null; } } async loadByIdWithRelations(id, relations) { try { let castedId = this.castIdValue(id); let query = { where: {id: castedId} }; let includeConfig = {}; if(sc.isArray(relations) && 0 < relations.length){ includeConfig = this.buildIncludeObject(relations); query.include = includeConfig; } let result = await this.model.findUnique(query); return this.transformRelationResults(result, includeConfig); } catch(error) { Logger.error('Load by ID with relations error: '+error.message); return null; } } async loadByIds(ids) { try { let castedIds = ids.map(id => this.castIdValue(id)); return await this.model.findMany({ where: { id: {in: castedIds} } }); } catch(error) { Logger.error('Load by IDs error: '+error.message); return []; } } async loadOne(filters) { try { let processedFilters = this.processFilters(filters); let query = this.buildQueryOptions(false); query.where = processedFilters; return await this.model.findFirst(query); } catch(error) { Logger.error('Load one error: '+error.message); return null; } } async loadOneWithRelations(filters, relations) { try { let processedFilters = this.processFilters(filters); let query = this.buildQueryOptions(false); query.where = processedFilters; let includeConfig = {}; if(sc.isArray(relations) && 0 < relations.length){ includeConfig = this.buildIncludeObject(relations); query.include = includeConfig; } let result = await this.model.findFirst(query); return this.transformRelationResults(result, includeConfig); } catch(error) { Logger.error('Load one with relations error: '+error.message); return null; } } async loadOneBy(field, fieldValue, operator = null) { try { let filter = this.createSingleFilter(field, fieldValue, operator); let query = this.buildQueryOptions(false); query.where = filter; return await this.model.findFirst(query); } catch(error) { Logger.error('Load one by error: '+error.message); return null; } } async loadOneByWithRelations(field, fieldValue, relations, operator = null) { try { let filter = this.createSingleFilter(field, fieldValue, operator); let query = this.buildQueryOptions(false); query.where = filter; let includeConfig = {}; if(sc.isArray(relations) && 0 < relations.length){ includeConfig = this.buildIncludeObject(relations); query.include = includeConfig; } let result = await this.model.findFirst(query); return this.transformRelationResults(result, includeConfig); } catch(error) { Logger.error('Load one by with relations error: '+error.message); return null; } } buildQueryOptions(useLimit = true) { let queryOptions = {}; if(0 < this.select.length){ queryOptions.select = {}; for(let field of this.select){ queryOptions.select[field] = true; } } if(useLimit && 0 !== this.limit){ queryOptions.take = this.limit; } if(0 !== this.offset){ queryOptions.skip = this.offset; } if(false !== this.sortBy && false !== this.sortDirection){ queryOptions.orderBy = { [this.sortBy]: this.sortDirection.toLowerCase() }; } return queryOptions; } createSingleFilter(field, fieldValue, operator = null) { let filter = {}; let processedValue = fieldValue; if(sc.hasOwn(this.referenceFields, field)){ processedValue = this.castNumericValue(fieldValue, this.referenceFields[field]); } if('id' === field){ processedValue = this.castIdValue(fieldValue); } if(null === operator){ filter[field] = processedValue; return filter; } filter[field] = this.applyOperator(processedValue, operator, field); return filter; } applyOperator(value, operator, fieldName = null) { let operatorsMap = { '=': value, '!=': {not: value}, '>': {gt: value}, '>=': {gte: value}, '<': {lt: value}, '<=': {lte: value}, 'LIKE': this.handleLikeOperator(value, fieldName), 'IN': {in: value}, 'NOT IN': {notIn: value} }; return operatorsMap[operator] || value; } handleLikeOperator(value, fieldName) { if('id' === fieldName || (this.referenceFields && sc.hasOwn(this.referenceFields, fieldName))){ let numericValue = Number(value.replace(/%/g, '')); if(!isNaN(numericValue)){ return numericValue; } } return {contains: value}; } buildIncludeObject(relations) { let include = {}; for(let relation of relations){ include[relation] = true; } return include; } } module.exports.PrismaDriver = PrismaDriver;