@reldens/storage
Version:
1,014 lines (949 loc) • 32.3 kB
JavaScript
/**
*
* 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;
}
this.prisma = props.prisma;
this.model = props.model;
this.requiredFields = [];
this.optionalFields = [];
this.fieldTypes = {};
this.fieldDefaults = {};
this.idFieldType = 'String';
this.referenceFields = {};
this.relationMetadata = {};
this.foreignKeyMappings = {};
this.jsonFields = new Set();
this.initModelMetadata();
}
initModelMetadata()
{
let dmmf = this.getDmmf();
if(!dmmf){
Logger.warning('Could not access Prisma DMMF metadata');
return;
}
let modelName = this.tableName().toLowerCase();
let modelInfo = dmmf.models?.[modelName] || dmmf.models?.find(m => m.name.toLowerCase() === modelName);
if(!modelInfo){
Logger.warning('Could not find model info for: '+modelName);
return;
}
for(let field of modelInfo.fields){
this.processFieldMetadata(field);
}
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];
}
}
}
}
getDmmf()
{
try {
if(this.prisma._dmmf){
return this.prisma._dmmf.datamodel;
}
if(this.prisma._getDmmf && sc.isFunction(this.prisma._getDmmf)){
return this.prisma._getDmmf().datamodel;
}
if(this.prisma._runtimeDataModel){
return this.prisma._runtimeDataModel;
}
if(Prisma.dmmf && Prisma.dmmf.datamodel){
return Prisma.dmmf.datamodel;
}
return null;
} catch(error) {
Logger.warning('Failed to access DMMF: '+error.message);
return null;
}
}
processFieldMetadata(field)
{
this.fieldTypes[field.name] = field.type;
if('Json' === field.type){
this.jsonFields.add(field.name);
}
if(field.isId){
this.idFieldType = field.type;
}
if(field.hasDefaultValue){
this.fieldDefaults[field.name] = field.default;
}
if(field.isRequired && !field.hasDefaultValue && !field.isId && 'object' !== field.kind){
this.requiredFields.push(field.name);
}
if(field.isOptional || field.hasDefaultValue){
this.optionalFields.push(field.name);
}
if(field.kind === 'scalar' && field.isList === false){
let isNumericType = this.isNumericFieldType(field.type);
let isReferenceField = field.relationFromFields && 0 < field.relationFromFields.length;
if(isNumericType || isReferenceField){
this.referenceFields[field.name] = field.type;
}
}
if('object' === field.kind){
let relationType = field.isList ? 'many' : 'one';
this.relationMetadata[field.name] = {
type: relationType,
model: field.type,
isList: field.isList,
isOptional: field.isOptional
};
}
if('object' === field.kind && sc.hasOwn(field, 'relationFromFields') && sc.isArray(field.relationFromFields)){
for(let foreignKeyField of field.relationFromFields){
this.foreignKeyMappings[foreignKeyField] = field.name;
}
}
}
getAllRelations()
{
return Object.keys(this.relationMetadata || {});
}
isNumericFieldType(fieldType)
{
return ('Int' === fieldType || 'BigInt' === fieldType || 'Float' === fieldType || 'Decimal' === fieldType);
}
isBooleanFieldType(fieldType)
{
return 'Boolean' === fieldType;
}
isDateTimeFieldType(fieldType)
{
return 'DateTime' === fieldType || 'Date' === fieldType;
}
isJsonFieldType(fieldType)
{
return 'Json' === fieldType;
}
isJsonFieldByName(fieldName)
{
if(this.jsonFields.has(fieldName)){
return true;
}
if(!this.rawModel || !this.rawModel.properties){
return false;
}
return 'json' === sc.get(this.rawModel.properties[fieldName], 'dbType', '');
}
isStringFieldType(fieldType)
{
return 'String' === fieldType;
}
castValue(value, fieldType, fieldName = null)
{
if('id' === fieldName){
return this.castToIdType(value);
}
if(this.isNullOrEmpty(value)){
if(sc.hasOwn(this.fieldDefaults, fieldName)){
return undefined;
}
if(-1 !== this.optionalFields.indexOf(fieldName)){
return null;
}
if(-1 !== this.requiredFields.indexOf(fieldName)){
Logger.warning('Required field '+fieldName+' cannot be null or empty');
return undefined;
}
return null;
}
if(this.isJsonFieldByName(fieldName)){
return this.castToJsonType(value);
}
if(this.isNumericFieldType(fieldType)){
return this.castToNumericType(value, fieldType);
}
if(this.isBooleanFieldType(fieldType)){
return this.castToBooleanType(value);
}
if(this.isDateTimeFieldType(fieldType)){
return this.castToDateType(value);
}
if(this.isStringFieldType(fieldType)){
return this.castToStringType(value);
}
return value;
}
isNullOrEmpty(value)
{
return null === value || undefined === value || '' === value;
}
castToIdType(value)
{
if(this.isNullOrEmpty(value)){
return value;
}
if(this.isNumericFieldType(this.idFieldType)){
let numeric = Number(value);
if(!isNaN(numeric)){
return ('Int' === this.idFieldType || 'BigInt' === this.idFieldType) ? Math.floor(numeric) : numeric;
}
}
return String(value);
}
castToNumericType(value, fieldType)
{
if(this.isNullOrEmpty(value)){
return null;
}
let numericValue = Number(value);
if(isNaN(numericValue)){
Logger.warning('Cannot convert value to numeric: '+value);
return null;
}
if('Int' === fieldType || 'BigInt' === fieldType){
return Math.floor(numericValue);
}
return numericValue;
}
castToBooleanType(value)
{
if(this.isNullOrEmpty(value)){
return null;
}
if('boolean' === typeof value){
return value;
}
if('number' === typeof value){
return 0 !== value;
}
if('string' === typeof value){
let lowerValue = value.toLowerCase();
if('true' === lowerValue || '1' === lowerValue || 'yes' === lowerValue){
return true;
}
if('false' === lowerValue || '0' === lowerValue || 'no' === lowerValue){
return false;
}
}
return Boolean(value);
}
castToDateType(dateValue)
{
if(dateValue instanceof Date){
return dateValue;
}
if(this.isNullOrEmpty(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 {
let parsedDate = new Date(dateString);
if(isNaN(parsedDate.getTime())){
Logger.warning('Invalid date value: '+dateString);
return null;
}
return parsedDate;
} catch(error) {
Logger.warning('Failed to convert date value: '+dateString);
return null;
}
}
castToJsonType(value)
{
if(this.isNullOrEmpty(value)){
return Prisma.DbNull;
}
if('object' === typeof value){
return value;
}
if('string' === typeof value){
if(value.startsWith('%') && value.endsWith('%')){
return value;
}
try {
return JSON.parse(value);
} catch(error) {
return value;
}
}
return value;
}
castToStringType(value)
{
if(null === value || undefined === value){
return null;
}
if('string' === typeof value){
if('null' === value){
return null;
}
return value;
}
return String(value);
}
databaseName()
{
return this.config.database || '';
}
id()
{
return this.name || '';
}
name()
{
return this.rawName || '';
}
tableName()
{
return this.model.$name || this.model.name || '';
}
property(propertyName)
{
return this.rawModel[propertyName] || null;
}
prepareData(params)
{
let data = {};
for(let key of Object.keys(params)){
let fieldType = this.fieldTypes[key];
if(!fieldType){
data[key] = params[key];
continue;
}
let castedValue = this.castValue(params[key], fieldType, key);
if(undefined !== castedValue){
data[key] = castedValue;
}
}
return data;
}
prepareDataWithRelations(params, isCreate = false)
{
let data = {};
let relations = {};
for(let key of Object.keys(params)){
if(sc.hasOwn(this.foreignKeyMappings, key)){
let relationName = this.foreignKeyMappings[key];
let value = params[key];
if(!this.isNullOrEmpty(value)){
relations[relationName] = {
connect: {id: this.castToIdType(value)}
};
continue;
}
if(!isCreate){
relations[relationName] = {
disconnect: true
};
}
continue;
}
let fieldType = this.fieldTypes[key];
if(!fieldType){
if(!this.isNullOrEmpty(params[key])){
data[key] = params[key];
}
continue;
}
let castedValue = this.castValue(params[key], fieldType, key);
if(undefined !== castedValue){
if(this.isJsonFieldType(fieldType) && null === castedValue){
continue;
}
data[key] = castedValue;
}
}
return {...data, ...relations};
}
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('OR' === key && sc.isArray(value)){
processedFilters.OR = value.map(condition => this.processFilters(condition));
continue;
}
if(sc.hasOwn(value, 'operator') && sc.hasOwn(value, 'value')){
let operatorValue = this.processFilterValue(value.value, key);
processedFilters[key] = this.applyOperator(operatorValue, value.operator, key);
continue;
}
if(sc.isObject(value) && !sc.isArray(value)){
processedFilters[key] = this.processFilters(value);
continue;
}
processedFilters[key] = this.processFilterValue(value, key);
}
return processedFilters;
}
processFilterValue(value, key)
{
if(sc.isArray(value)){
if('id' === key){
return value.map(id => this.castToIdType(id));
}
let fieldType = this.fieldTypes[key];
if(fieldType){
return value.map(val => this.castValue(val, fieldType, key)).filter(val => undefined !== val);
}
return value;
}
if('id' === key){
return this.castToIdType(value);
}
let fieldType = this.fieldTypes[key];
if(fieldType){
let castedValue = this.castValue(value, fieldType, key);
return undefined !== castedValue ? castedValue : value;
}
return value;
}
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)
{
let preparedData = this.prepareDataWithRelations(params, true);
this.ensureRequiredFields(preparedData);
try {
return await this.model.create({
data: preparedData
});
} catch(error) {
Logger.error('Create error: '+error.message);
return false;
}
}
async createWithRelations(params, relations)
{
if(!sc.isArray(relations) || 0 === relations.length){
relations = this.getAllRelations();
}
let preparedData = this.prepareData(params);
this.ensureRequiredFields(preparedData);
let createData = {data: preparedData};
if(0 < relations.length){
let includeConfig = this.buildIncludeObject(relations);
createData.include = includeConfig;
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.castToIdType(item.id)}))
};
continue;
}
createData.data[relation] = {
connect: {id: this.castToIdType(relationData.id)}
};
}
}
try {
return this.transformRelationResults(await this.model.create(createData), createData.include);
} catch(error) {
Logger.error('Create with relations error: '+error.message);
return false;
}
}
async update(filters, updatePatch)
{
let preparedData = this.prepareDataWithRelations(updatePatch);
let processedFilters = this.processFilters(filters);
try {
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);
let preparedData = this.prepareDataWithRelations(updatePatch);
try {
return await this.model.updateMany({
where: filter,
data: preparedData
});
} catch(error) {
Logger.error('Update by error: '+error.message);
return false;
}
}
async updateById(id, params)
{
let castedId = this.castToIdType(id);
let found = await this.loadById(castedId);
if(!found){
return false;
}
let preparedData = this.prepareDataWithRelations(params);
try {
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)
{
let preparedData = this.prepareDataWithRelations(params);
let existing = false;
if(params.id){
let castedId = this.castToIdType(params.id);
existing = await this.loadById(castedId);
if(existing){
let patch = Object.assign({}, preparedData);
delete patch.id;
try {
return await this.model.update({
where: {id: castedId},
data: patch
});
} catch(error) {
Logger.error('Upsert update error: '+error.message);
return false;
}
}
}
if(filters){
let existing = await this.loadOne(filters);
if(existing){
try {
return await this.model.update({
where: {id: this.castToIdType(existing.id)},
data: preparedData
});
} catch(error) {
Logger.error('Upsert update by filter error: '+error.message);
return false;
}
}
}
return await this.create(preparedData);
}
async delete(filters = {})
{
let processedFilters = this.processFilters(filters);
try {
return await this.model.deleteMany({
where: processedFilters
});
} catch(error) {
Logger.error('Delete error: '+error.message);
return false;
}
}
async deleteById(id)
{
let castedId = this.castToIdType(id);
let found = await this.loadById(castedId);
if(!found){
return false;
}
try {
return await this.model.delete({
where: {id: castedId}
});
} catch(error) {
Logger.error('Delete by ID error: '+error.message);
return false;
}
}
async count(filters)
{
let processedFilters = this.processFilters(filters);
try {
return await this.model.count({
where: processedFilters
});
} catch(error) {
Logger.error('Count error: '+error.message);
return 0;
}
}
async countWithRelations(filters, relations)
{
if(!sc.isArray(relations) || 0 === relations.length){
relations = this.getAllRelations();
}
let processedFilters = this.processFilters(filters);
let query = {
where: processedFilters
};
if(0 < relations.length){
query.include = this.buildIncludeObject(relations);
}
try {
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)
{
if(!sc.isArray(relations) || 0 === relations.length){
relations = this.getAllRelations();
}
let query = this.buildQueryOptions();
let includeConfig = {};
if(0 < relations.length){
includeConfig = this.buildIncludeObject(relations);
query.include = includeConfig;
}
try {
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)
{
let processedFilters = this.processFilters(filters);
let query = this.buildQueryOptions();
query.where = processedFilters;
try {
return await this.model.findMany(query);
} catch(error) {
Logger.error('Load error: '+error.message);
return [];
}
}
async loadWithRelations(filters, relations)
{
if(!sc.isArray(relations) || 0 === relations.length){
relations = this.getAllRelations();
}
let processedFilters = this.processFilters(filters);
let query = this.buildQueryOptions();
query.where = processedFilters;
let includeConfig = {};
if(0 < relations.length){
includeConfig = this.buildIncludeObject(relations);
query.include = includeConfig;
}
try {
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)
{
let filter = this.createSingleFilter(field, fieldValue, operator);
let query = this.buildQueryOptions();
query.where = filter;
try {
return await this.model.findMany(query);
} catch(error) {
Logger.error('Load by error: '+error.message);
return [];
}
}
async loadByWithRelations(field, fieldValue, relations, operator = null)
{
if(!sc.isArray(relations) || 0 === relations.length){
relations = this.getAllRelations();
}
let filter = this.createSingleFilter(field, fieldValue, operator);
let query = this.buildQueryOptions();
query.where = filter;
let includeConfig = {};
if(0 < relations.length){
includeConfig = this.buildIncludeObject(relations);
query.include = includeConfig;
}
try {
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)
{
let castedId = this.castToIdType(id);
try {
return await this.model.findUnique({
where: {id: castedId}
});
} catch(error) {
Logger.error('Load by ID error: '+error.message);
return null;
}
}
async loadByIdWithRelations(id, relations)
{
if(!sc.isArray(relations) || 0 === relations.length){
relations = this.getAllRelations();
}
let castedId = this.castToIdType(id);
let query = {
where: {id: castedId}
};
let includeConfig = {};
if(0 < relations.length){
includeConfig = this.buildIncludeObject(relations);
query.include = includeConfig;
}
try {
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)
{
let castedIds = ids.map(id => this.castToIdType(id));
try {
return await this.model.findMany({
where: {
id: {in: castedIds}
}
});
} catch(error) {
Logger.error('Load by IDs error: '+error.message);
return [];
}
}
async loadOne(filters)
{
let processedFilters = this.processFilters(filters);
let query = this.buildQueryOptions(false);
query.where = processedFilters;
try {
return await this.model.findFirst(query);
} catch(error) {
Logger.error('Load one error: '+error.message);
return null;
}
}
async loadOneWithRelations(filters, relations)
{
if(!sc.isArray(relations) || 0 === relations.length){
relations = this.getAllRelations();
}
let processedFilters = this.processFilters(filters);
let query = this.buildQueryOptions(false);
query.where = processedFilters;
let includeConfig = {};
if(0 < relations.length){
includeConfig = this.buildIncludeObject(relations);
query.include = includeConfig;
}
try {
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)
{
let filter = this.createSingleFilter(field, fieldValue, operator);
let query = this.buildQueryOptions(false);
query.where = filter;
try {
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)
{
if(!sc.isArray(relations) || 0 === relations.length){
relations = this.getAllRelations();
}
let filter = this.createSingleFilter(field, fieldValue, operator);
let query = this.buildQueryOptions(false);
query.where = filter;
let includeConfig = {};
if(0 < relations.length){
includeConfig = this.buildIncludeObject(relations);
query.include = includeConfig;
}
try {
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 = this.processFilterValue(fieldValue, field);
if(null === operator){
filter[field] = processedValue;
return filter;
}
filter[field] = {operator, value: processedValue};
return this.processFilters(filter);
}
applyOperator(value, operator, fieldName = null)
{
let upperOperator = operator ? operator.toUpperCase() : '';
if('LIKE' === upperOperator){
return this.handleLikeOperator(value, fieldName);
}
let operatorsMap = {
'=': value,
'!=': {not: value},
'>': {gt: value},
'>=': {gte: value},
'<': {lt: value},
'<=': {lte: value},
'IN': {in: value},
'NOT IN': {notIn: value}
};
return operatorsMap[upperOperator] || value;
}
handleLikeOperator(value, fieldName)
{
let cleanValue = String(value).replace(/%/g, '');
if(this.isJsonFieldByName(fieldName) || this.isJsonField(fieldName)){
return this.handleJsonTextSearch(cleanValue);
}
if('id' === fieldName || sc.hasOwn(this.referenceFields, fieldName)){
let numericValue = Number(cleanValue);
if(!isNaN(numericValue)){
return numericValue;
}
}
return {contains: cleanValue};
}
handleJsonTextSearch(searchValue)
{
return {
string_contains: searchValue
};
}
buildIncludeObject(relations)
{
let include = {};
for(let relation of relations){
include[relation] = true;
}
return include;
}
}
module.exports.PrismaDriver = PrismaDriver;