421 lines
13 KiB
TypeScript
421 lines
13 KiB
TypeScript
import { MediaDto } from "~/application/dtos/entities/MediaDto";
|
|
import { RowValueDto } from "~/application/dtos/entities/RowValueDto";
|
|
import { PropertyType } from "~/application/enums/entities/PropertyType";
|
|
import { EntityWithDetails, EntityWithProperties, PropertyWithDetails } from "../db/entities/entities.db.server";
|
|
import { RowWithDetails, RowWithValues } from "../db/entities/rows.db.server";
|
|
import RowHelper from "./RowHelper";
|
|
import bcrypt from "bcryptjs";
|
|
import { TFunction } from "i18next";
|
|
import { validatePropertyValue, validatePropertyValue_Media, validatePropertyValue_Multiple } from "./PropertyHelper";
|
|
import PropertyAttributeHelper from "./PropertyAttributeHelper";
|
|
import { PropertyAttributeName } from "~/application/enums/entities/PropertyAttributeName";
|
|
import { EntityRelationshipWithDetails } from "../db/entities/entityRelationships.db.server";
|
|
import RelationshipHelper from "./RelationshipHelper";
|
|
import { RowMedia, RowValueMultiple, RowValueRange } from "@toolctrl/db/prisma-browser";
|
|
import { RowValueMultipleDto } from "~/application/dtos/entities/RowValueMultipleDto";
|
|
import { RowValueRangeDto } from "~/application/dtos/entities/RowValueRangeDto";
|
|
|
|
const generateKey = () => {
|
|
const id: string = crypto.randomUUID();
|
|
return id;
|
|
};
|
|
|
|
const hashKey = async (key: string) => {
|
|
return await bcrypt.hash(key, 10);
|
|
};
|
|
|
|
const validateKey = async (key: string, hashedKey: string) => {
|
|
return await bcrypt.compare(key, hashedKey);
|
|
};
|
|
|
|
const getRowPropertiesFromJson = (t: TFunction | undefined, entity: EntityWithDetails, jsonObject: any, existing?: RowWithDetails) => {
|
|
const dynamicProperties: RowValueDto[] = [];
|
|
const properties: any = {};
|
|
|
|
entity.properties
|
|
.filter((f) => !f.isHidden)
|
|
.forEach((property) => {
|
|
const propertyValue = getPropertyValueFromJson(t, property, jsonObject, existing);
|
|
if (propertyValue?.rowValue) {
|
|
dynamicProperties.push(propertyValue?.rowValue);
|
|
}
|
|
});
|
|
|
|
return {
|
|
dynamicProperties,
|
|
properties,
|
|
};
|
|
};
|
|
|
|
function getPropertyValueFromJson(t: TFunction | undefined, property: PropertyWithDetails, object: any, existing?: RowWithDetails) {
|
|
let jsonValue: any | null = null;
|
|
let media: MediaDto[] = [];
|
|
let multiple: RowValueMultipleDto[] = [];
|
|
let range: RowValueRangeDto | null = null;
|
|
|
|
let name = property.name;
|
|
if (property.type === PropertyType.RANGE_NUMBER) {
|
|
try {
|
|
range = {
|
|
numberMin: object[name]?.min ?? null,
|
|
numberMax: object[name]?.min ?? null,
|
|
dateMin: null,
|
|
dateMax: null,
|
|
};
|
|
} catch (e: any) {
|
|
throw Error(`Invalid number range (${property.name}): ${e.message}`);
|
|
}
|
|
} else if (property.type === PropertyType.RANGE_DATE) {
|
|
try {
|
|
const min = object[name]?.min;
|
|
const max = object[name]?.max;
|
|
range = {
|
|
numberMin: null,
|
|
numberMax: null,
|
|
dateMin: min ? new Date(min?.toString()) : null,
|
|
dateMax: max ? new Date(max?.toString()) : null,
|
|
};
|
|
} catch (e: any) {
|
|
throw Error(`Invalid date range (${property.name}): ${e.message}`);
|
|
}
|
|
} else if ([PropertyType.MULTI_SELECT, PropertyType.MULTI_TEXT].includes(property.type)) {
|
|
multiple =
|
|
object[name]?.map((f: string, idx: number) => {
|
|
const value: RowValueMultipleDto = {
|
|
order: idx + 1,
|
|
value: f,
|
|
};
|
|
return value;
|
|
}) ?? [];
|
|
validatePropertyValue_Multiple({ t, property, multiple });
|
|
} else if (property.type === PropertyType.MEDIA) {
|
|
media =
|
|
object[name]?.map((f: MediaDto) => {
|
|
return f;
|
|
}) ?? [];
|
|
validatePropertyValue_Media({ t, property, media });
|
|
} else {
|
|
jsonValue = object[name];
|
|
if (property.isRequired && !property.isRequired && (jsonValue === null || jsonValue === undefined || jsonValue === "")) {
|
|
const defaultValue = PropertyAttributeHelper.getPropertyAttributeValue_String(property, PropertyAttributeName.DefaultValue);
|
|
if (defaultValue) {
|
|
jsonValue = defaultValue;
|
|
} else {
|
|
if (existing && !object.hasOwnProperty(name)) {
|
|
return null;
|
|
}
|
|
if (t) {
|
|
throw Error(`${t(property.title)}: required`);
|
|
} else {
|
|
throw Error(`${property.name}: required`);
|
|
}
|
|
}
|
|
}
|
|
validatePropertyValue({ t, property, value: jsonValue });
|
|
}
|
|
const value = RowHelper.getValueFromType(property.type, jsonValue);
|
|
value.media = media;
|
|
value.multiple = multiple;
|
|
value.range = range;
|
|
const existingValue = existing?.values.find((f) => f.propertyId === property.id);
|
|
const rowValue: RowValueDto = {
|
|
id: existingValue?.id ?? null,
|
|
propertyId: property.id,
|
|
property: property,
|
|
...value,
|
|
};
|
|
return { rowValue };
|
|
}
|
|
|
|
const getApiFormat = (entity: EntityWithProperties, item: RowWithValues | null, options: ApiFormatOptions = {}) => {
|
|
if (item === null) {
|
|
return null;
|
|
}
|
|
let properties: any = {};
|
|
entity.properties
|
|
.filter((f) => !f.isDefault)
|
|
.forEach((property) => {
|
|
const value = getPropertyApiFormat({ entity, property, item });
|
|
properties[property.name] = value;
|
|
});
|
|
const object: any = {
|
|
id: item.id,
|
|
folio: RowHelper.getRowFolio(entity, item),
|
|
...properties,
|
|
createdAt: item.createdAt,
|
|
createdByUser: item.createdByUser
|
|
? {
|
|
id: item.createdByUserId,
|
|
email: item.createdByUser.email,
|
|
firstName: item.createdByUser.firstName,
|
|
lastName: item.createdByUser.lastName,
|
|
}
|
|
: null,
|
|
createdByApiKey: item.createdByApiKey
|
|
? {
|
|
alias: item.createdByApiKey.alias,
|
|
}
|
|
: null,
|
|
};
|
|
options?.exclude?.forEach((toExclude) => {
|
|
delete object[toExclude];
|
|
});
|
|
return object;
|
|
};
|
|
|
|
export type ApiFormatOptions = {
|
|
exclude?: string[];
|
|
};
|
|
function getApiFormatWithRelationships({
|
|
entities,
|
|
item,
|
|
options,
|
|
originalParentId,
|
|
}: {
|
|
entities: EntityWithDetails[];
|
|
item: RowWithDetails | null;
|
|
options?: ApiFormatOptions;
|
|
originalParentId?: string;
|
|
}) {
|
|
if (!item) {
|
|
return null;
|
|
}
|
|
const entity = entities.find((f) => f.id === item.entityId);
|
|
if (entity === undefined) {
|
|
return null;
|
|
}
|
|
const apiFormat = getApiFormat(entity, item, options);
|
|
|
|
Object.assign(
|
|
apiFormat,
|
|
getRelationship({ entity, entities, relationships: entity.parentEntities, type: "parent", item, options, originalParentId: originalParentId ?? item.id })
|
|
);
|
|
Object.assign(
|
|
apiFormat,
|
|
getRelationship({ entity, entities, relationships: entity.childEntities, type: "child", item, options, originalParentId: originalParentId ?? item.id })
|
|
);
|
|
|
|
return apiFormat;
|
|
}
|
|
|
|
function getRelationship({
|
|
entity,
|
|
entities,
|
|
relationships,
|
|
type,
|
|
item,
|
|
options,
|
|
originalParentId,
|
|
}: {
|
|
entity: EntityWithDetails;
|
|
entities: EntityWithDetails[];
|
|
relationships: EntityRelationshipWithDetails[];
|
|
type: "parent" | "child";
|
|
item: RowWithDetails;
|
|
options?: ApiFormatOptions;
|
|
originalParentId: string;
|
|
}) {
|
|
const fromEntityId = entity.id;
|
|
const apiFormat: any = {};
|
|
relationships.forEach((relationship) => {
|
|
const { id, parent, child } = relationship;
|
|
const multiple = RelationshipHelper.getInputType({ fromEntityId, relationship }) === "multi-select";
|
|
const entity = entities.find((e) => e.id === (type === "parent" ? parent.id : child.id));
|
|
if (!entity) {
|
|
return;
|
|
}
|
|
const rows =
|
|
type === "parent"
|
|
? (item.parentRows
|
|
?.filter((f) => f.parentId !== originalParentId)
|
|
?.map((f) => {
|
|
return { id: f.id, relationshipId: f.relationshipId, row: f.parent };
|
|
}) ?? [])
|
|
: (item.childRows
|
|
?.filter((f) => f.childId !== originalParentId)
|
|
?.map((f) => {
|
|
return { id: f.id, relationshipId: f.relationshipId, row: f.child };
|
|
}) ?? []);
|
|
|
|
if (multiple && entity.id === child.id) {
|
|
apiFormat[entity.name] = [];
|
|
rows
|
|
.filter((f) => f.relationshipId === id)
|
|
.forEach((r) => {
|
|
if (!r.row) {
|
|
return;
|
|
}
|
|
const rowApiFormat = getApiFormat(entity!, r.row, options);
|
|
apiFormat[entity.name].push({
|
|
// relationshipId: r.id,
|
|
...rowApiFormat,
|
|
...getApiFormatWithRelationships({ entities, item: r.row as RowWithDetails, options, originalParentId }),
|
|
});
|
|
});
|
|
} else {
|
|
const row = rows?.find((f) => f.relationshipId === id);
|
|
if (row) {
|
|
const rowApiFormat = getApiFormat(entity!, row.row, options);
|
|
apiFormat[entity.name] = {
|
|
// relationshipId: row.id,
|
|
...rowApiFormat,
|
|
...getApiFormatWithRelationships({ entities, item: row.row as RowWithDetails, options, originalParentId }),
|
|
};
|
|
} else {
|
|
apiFormat[entity.name] = null;
|
|
}
|
|
}
|
|
});
|
|
|
|
return apiFormat;
|
|
}
|
|
|
|
const getPropertyApiFormat = ({ entity, item, property }: { entity: EntityWithProperties; item: RowWithValues; property: PropertyWithDetails }) => {
|
|
const value = RowHelper.getPropertyValue({ entity, item, property });
|
|
if (property.type === PropertyType.TEXT) {
|
|
return value?.toString();
|
|
} else if (property.type === PropertyType.NUMBER) {
|
|
return value as number;
|
|
} else if (property.type === PropertyType.BOOLEAN) {
|
|
return value as boolean;
|
|
} else if (property.type === PropertyType.DATE) {
|
|
let date = value as Date;
|
|
return date?.toISOString();
|
|
} else if (property.type === PropertyType.SELECT) {
|
|
return value?.toString();
|
|
} else if (property.type === PropertyType.MEDIA) {
|
|
const media = value as RowMedia[];
|
|
return (
|
|
media?.map((f) => {
|
|
return {
|
|
id: f.id,
|
|
name: f.name,
|
|
file: f.publicUrl ?? f.file,
|
|
type: f.type,
|
|
};
|
|
}) ?? []
|
|
);
|
|
} else if (property.type === PropertyType.MULTI_SELECT) {
|
|
const multiple = value as RowValueMultiple[];
|
|
return (
|
|
multiple?.map((f) => {
|
|
return f.value;
|
|
}) ?? []
|
|
);
|
|
} else if (property.type === PropertyType.MULTI_TEXT) {
|
|
const multiple = value as RowValueMultiple[];
|
|
return (
|
|
multiple?.map((f) => {
|
|
return f.value;
|
|
}) ?? []
|
|
);
|
|
} else if (property.type === PropertyType.RANGE_NUMBER) {
|
|
const range = value as RowValueRange;
|
|
return {
|
|
min: range?.numberMin ?? null,
|
|
max: range?.numberMax ?? null,
|
|
};
|
|
} else if (property.type === PropertyType.RANGE_DATE) {
|
|
const range = value as RowValueRange;
|
|
return {
|
|
min: range?.dateMin?.toISOString() ?? null,
|
|
max: range?.dateMax?.toISOString() ?? null,
|
|
};
|
|
} else {
|
|
return RowHelper.getPropertyValue({ entity, item, property });
|
|
}
|
|
};
|
|
|
|
function getSchemaType({ property }: { property: PropertyWithDetails }) {
|
|
switch (property.type) {
|
|
case PropertyType.TEXT:
|
|
case PropertyType.SELECT:
|
|
return { type: "string" };
|
|
|
|
case PropertyType.NUMBER:
|
|
return { type: "integer" };
|
|
|
|
case PropertyType.BOOLEAN:
|
|
return { type: "boolean" };
|
|
|
|
case PropertyType.DATE:
|
|
return { type: "string", format: "date" }; // or "date-time" if ISO 8601 format
|
|
|
|
case PropertyType.MULTI_SELECT:
|
|
case PropertyType.MULTI_TEXT:
|
|
return { type: "array", items: { type: "string" } };
|
|
|
|
case PropertyType.MEDIA:
|
|
return {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
title: { type: "string" },
|
|
name: { type: "string" },
|
|
file: { type: "string" },
|
|
type: { type: "string" },
|
|
},
|
|
},
|
|
};
|
|
|
|
case PropertyType.RANGE_NUMBER:
|
|
return {
|
|
type: "object",
|
|
properties: {
|
|
min: { type: "integer" },
|
|
max: { type: "integer" },
|
|
},
|
|
};
|
|
|
|
case PropertyType.RANGE_DATE:
|
|
return {
|
|
type: "object",
|
|
properties: {
|
|
min: { type: "string", format: "date" },
|
|
max: { type: "string", format: "date" },
|
|
},
|
|
};
|
|
|
|
default:
|
|
return { type: "string" };
|
|
}
|
|
}
|
|
|
|
function getSampleValue({ property }: { property: PropertyWithDetails }) {
|
|
switch (property.type) {
|
|
case PropertyType.TEXT:
|
|
return "Sample text";
|
|
case PropertyType.NUMBER:
|
|
return 123;
|
|
case PropertyType.BOOLEAN:
|
|
return true;
|
|
case PropertyType.DATE:
|
|
return new Date();
|
|
case PropertyType.SELECT:
|
|
return property.options.length > 0 ? property.options[0].value : "Sample select";
|
|
case PropertyType.MEDIA:
|
|
return [{ name: "My image.png", type: "image/png", title: "My image", file: "https://placehold.co/1000x500.png?text=My%20image" }];
|
|
case PropertyType.MULTI_SELECT:
|
|
case PropertyType.MULTI_TEXT:
|
|
return property.options.map((f) => f.value);
|
|
case PropertyType.RANGE_DATE:
|
|
return { min: new Date(), max: new Date() };
|
|
case PropertyType.RANGE_NUMBER:
|
|
return { min: 1, max: 100 };
|
|
default:
|
|
return "{NOT SUPPORTED}";
|
|
}
|
|
}
|
|
|
|
export default {
|
|
generateKey,
|
|
hashKey,
|
|
validateKey,
|
|
getRowPropertiesFromJson,
|
|
getApiFormat,
|
|
getApiFormatWithRelationships,
|
|
getSchemaType,
|
|
getSampleValue,
|
|
};
|