import { hardDeleteRowById, updateRowWithValues, type LegacyRowWithDetails, type RowMutationData, type RowMutationValueInput, type RowMutationRelationshipChildInput, type RowMutationRelationshipParentInput, } from "@toolctrl/db/rows.server"; import { Prisma, Row, Tenant, RowValue, ApiKey, RowMedia, RowRelationship, SampleCustomEntity, RowPermission, RowValueMultiple, RowValueRange, } from "@toolctrl/db/prisma-browser"; import { RowFiltersDto } from "~/application/dtos/data/RowFiltersDto"; import { MediaDto } from "~/application/dtos/entities/MediaDto"; import { deleteRowMediaFromStorageProvider, storeRowMediaInStorageProvider } from "~/utils/helpers/.server/MediaService"; import RowFiltersHelper from "~/utils/helpers/RowFiltersHelper"; import TenantHelper from "~/utils/helpers/TenantHelper"; import { db } from "../../db.server"; import { UserSimple } from "../users.db.server"; import { EntityWithDetails, getEntityById } from "./entities.db.server"; import { RowTagWithDetails } from "./rowTags.db.server"; import { getRowPermissionsCondition } from "~/utils/helpers/.server/PermissionsService"; import { RowValueMultipleDto } from "~/application/dtos/entities/RowValueMultipleDto"; import { RowValueRangeDto } from "~/application/dtos/entities/RowValueRangeDto"; import RowModelHelper from "~/utils/helpers/models/RowModelHelper"; export type RowValueWithDetails = RowValue & { media: RowMedia[]; multiple: RowValueMultiple[]; range: RowValueRange | null; }; export type RowWithValues = Row & { createdByUser: UserSimple | null; createdByApiKey: ApiKey | null; values: RowValueWithDetails[]; }; export type RowWithCreatedBy = Row & { createdByUser: UserSimple | null; createdByApiKey: ApiKey | null; }; export type RowWithDetails = Row & { createdByUser: UserSimple | null; createdByApiKey: ApiKey | null; tenant: Tenant | null; values: RowValueWithDetails[]; tags: RowTagWithDetails[]; parentRows: | (RowRelationship & { parent: RowWithValues })[] | (RowRelationship & { parent: RowWithValues & { parentRows: (RowRelationship & { parent: { values: RowValueWithDetails[] }; })[]; childRows: (RowRelationship & { child: { values: RowValueWithDetails[] }; })[]; }; })[]; childRows: (RowRelationship & { child: RowWithValues & { parentRows: (RowRelationship & { parent: { values: RowValueWithDetails[] }; })[]; childRows: (RowRelationship & { child: { values: RowValueWithDetails[] }; })[]; }; })[]; permissions: RowPermission[]; sampleCustomEntity: SampleCustomEntity | null; }; export type RowMutationScope = { id: string; entityId: string; tenantId: string | null }; export async function getAllRows(entityId: string): Promise { return await db.row.findMany({ where: { deletedAt: null, entityId, }, include: RowModelHelper.includeRowDetails, }); } export async function getRows({ entityId, entityName, tenantId, userId, take, skip, orderBy, filters, ids, rowWhere, includePublic, }: { entityId?: string; entityName?: string; tenantId?: string | null; userId?: string; take?: number; skip?: number; orderBy?: Prisma.RowOrderByWithRelationInput[]; filters?: RowFiltersDto; ids?: string[]; rowWhere?: Prisma.RowWhereInput; includePublic?: boolean; }): Promise { const whereFilters = RowFiltersHelper.getRowFiltersCondition(filters); let filterEntity: Prisma.RowWhereInput | undefined = undefined; if (entityId) { filterEntity = { entityId }; } else if (entityName) { filterEntity = { entity: { name: entityName } }; } const accessFilters = await getAccessFilters({ tenantId }); const where: Prisma.RowWhereInput = { AND: [ { deletedAt: null }, { ...rowWhere }, whereFilters, { id: { in: ids } }, { ...filterEntity }, { ...accessFilters }, // ...getSearchCondition(filters?.query), ], }; return await db.row.findMany({ take, skip, where, include: RowModelHelper.createScopedIncludeRowDetails(tenantId ?? null), orderBy, }); } export async function getRowsBetween(name: string, tenantId: string | null, startDate: Date, endDate: Date): Promise { return await db.row.findMany({ where: { AND: [ { deletedAt: null }, { entity: { name }, ...TenantHelper.tenantCondition({ tenantId }), }, { createdAt: { lte: endDate, gte: startDate, }, }, ], }, include: RowModelHelper.createScopedIncludeRowDetails(tenantId), orderBy: { createdAt: "asc" }, }); } export async function getRowsInIds(ids: string[], tenantId?: string | null): Promise { return await db.row.findMany({ where: { deletedAt: null, id: { in: ids, }, tenantId, }, include: tenantId === undefined ? RowModelHelper.includeRowDetails : RowModelHelper.createScopedIncludeRowDetails(tenantId), }); } export async function countRows({ entityId, entityName, tenantId, userId, filters, rowWhere, includePublic, }: { entityId?: string; entityName?: string; tenantId?: string | null; userId?: string | undefined; filters?: RowFiltersDto; rowWhere?: Prisma.RowWhereInput; includePublic?: boolean; }): Promise { const whereFilters = RowFiltersHelper.getRowFiltersCondition(filters); let filterEntity: { entityId: string } | { entity: { name: string } } | undefined = undefined; if (entityId) { filterEntity = { entityId }; } else if (entityName) { filterEntity = { entity: { name: entityName } }; } const accessFilters = await getAccessFilters({ tenantId }); const where: Prisma.RowWhereInput = { AND: [ { deletedAt: null }, { ...rowWhere }, whereFilters, // { id: { in: ids } }, { ...filterEntity }, { ...accessFilters }, // ...getSearchCondition(filters?.query), ], }; return await db.row.count({ where, }); } export async function getRow({ entityId, id, tenantId }: { entityId: string; id: string; tenantId?: string | null }): Promise { const accessFilters = await getAccessFilters({ tenantId }); return await db.row.findFirst({ where: { deletedAt: null, id, entityId, ...accessFilters, }, include: { ...RowModelHelper.createScopedIncludeRowDetails(tenantId ?? null), }, }); } export async function getRowById(id: string, scope: { entityId?: string; tenantId: string | null }): Promise { return await db.row.findFirst({ where: { deletedAt: null, id, ...(scope.entityId ? { entityId: scope.entityId } : {}), tenantId: scope.tenantId, }, include: { ...RowModelHelper.createScopedIncludeRowDetails(scope.tenantId), }, }); } export async function getMaxRowFolio({ tenantId, entityId }: { tenantId: string | null; entityId: string }) { let where: any = { tenantId, entityId }; return await db.row.aggregate({ where, _max: { folio: true, }, }); } export async function getNextRowFolio({ tenantId, entityId }: { tenantId: string | null; entityId: string }) { const maxFolio = await getMaxRowFolio({ tenantId, entityId }); let next = 1; if (maxFolio && maxFolio._max.folio !== null) { next = maxFolio._max.folio + 1; } return next; } export async function getMaxRowOrder({ tenantId, entityId }: { tenantId: string | null; entityId: string }) { let where: any = { tenantId, entityId }; return await db.row.aggregate({ where, _max: { order: true, }, }); } export async function getNextRowOrder({ tenantId, entityId }: { tenantId: string | null; entityId: string }) { const maxOrder = await getMaxRowOrder({ tenantId, entityId }); let next = 1; if (maxOrder && maxOrder._max.order !== null) { next = maxOrder._max.order + 1; } return next; } export async function createRow({ entity, data, nextFolio, nextOrder, }: { entity: EntityWithDetails; data: { tenantId: string | null; createdByUserId?: string | null; createdByApiKeyId?: string | null; properties: any; dynamicProperties?: { propertyId: string; id?: string | null; textValue?: string | null; numberValue?: number | string | null; dateValue?: Date | string | null; booleanValue?: boolean | null; media?: MediaDto[]; multiple?: RowValueMultipleDto[]; range?: RowValueRangeDto; }[]; parentRows?: { relationshipId: string; parentId: string }[]; childRows?: { relationshipId: string; childId: string }[]; rowCreateInput?: Partial; }; nextFolio?: number | undefined; nextOrder?: number | undefined; }) { let folio = nextFolio ?? 1; if (!nextFolio) { const maxFolio = await getMaxRowFolio({ tenantId: data.tenantId, entityId: entity.id }); if (maxFolio && maxFolio._max.folio !== null) { folio = maxFolio._max.folio + 1; } } let order = nextOrder ?? 1; if (!nextOrder) { const maxOrder = await getMaxRowOrder({ tenantId: data.tenantId, entityId: entity.id }); if (maxOrder && maxOrder._max.order !== null) { order = maxOrder._max.order + 1; } } const createInput: Prisma.XOR = { folio, order, entityId: entity.id, tenantId: data.tenantId, createdByUserId: data.createdByUserId ?? null, createdByApiKeyId: data.createdByApiKeyId ?? null, ...data.properties, values: { create: data.dynamicProperties // .filter((f) => !f.id) ?.map((value) => { return { propertyId: value.propertyId, textValue: value.textValue, numberValue: value.numberValue, dateValue: value.dateValue, booleanValue: value.booleanValue, media: { create: value.media?.map((m) => { return { name: m.name, title: m.title, type: m.type, file: m.file, publicUrl: m.publicUrl, }; }), }, multiple: { create: value.multiple?.map((m) => { return { order: m.order, value: m.value, }; }), }, range: { create: { numberMin: value.range?.numberMin, numberMax: value.range?.numberMax, dateMin: value.range?.dateMin, dateMax: value.range?.dateMax, }, }, }; }), }, parentRows: { create: data.parentRows?.map(({ relationshipId, parentId }) => { return { relationshipId, parentId, }; }), }, childRows: { create: data.childRows?.map(({ relationshipId, childId }) => { return { relationshipId, childId, }; }), }, ...data.rowCreateInput, }; const row = await db.row.create({ data: createInput, }); return row; } export async function updateRow( scope: RowMutationScope, data: { properties: any; dynamicProperties: { propertyId: string; id?: string | null; textValue?: string | null; numberValue?: number | string | null; dateValue?: Date | string | null; booleanValue?: boolean | null; media?: MediaDto[]; multiple?: RowValueMultipleDto[]; range?: RowValueRangeDto | undefined; }[]; parentRows?: { relationshipId: string; parentId: string }[]; childRows?: { relationshipId: string; childId: string }[]; rowUpdateInput?: Prisma.RowUpdateInput; } ) { const row = await updateRowWithValues(scope, { properties: data.properties, dynamicProperties: data.dynamicProperties as RowMutationValueInput[], parentRows: data.parentRows as RowMutationRelationshipParentInput[] | undefined, childRows: data.childRows as RowMutationRelationshipChildInput[] | undefined, rowUpdateInput: data.rowUpdateInput, } satisfies Omit) as LegacyRowWithDetails as RowWithDetails; const entity = await getEntityById({ tenantId: row.tenantId, id: row.entityId }); if (entity) { await storeRowMediaInStorageProvider(entity, row); } return row; } export async function deleteRow(scope: RowMutationScope) { const row = await getRowById(scope.id, { entityId: scope.entityId, tenantId: scope.tenantId }); await deleteRowMediaFromStorageProvider(row); return await hardDeleteRowById(scope); } export async function deleteRows({ entityId, tenantId }: { entityId: string; tenantId: string | null }) { return await db.row.deleteMany({ where: { entityId, tenantId }, }); } export async function deleteRowsInIds({ ids, entityId, tenantId }: { ids: string[]; entityId: string; tenantId: string | null }) { return await db.row.deleteMany({ where: { id: { in: ids }, entityId, tenantId }, }); } export async function updateRowMedia( scope: { id: string; rowId: string; entityId: string; tenantId: string | null }, data: { file?: string; publicUrl?: string | null; storageBucket?: string | null; storageProvider?: string | null; } ) { const result = await db.rowMedia.updateMany({ where: { id: scope.id, rowValue: { rowId: scope.rowId, row: { entityId: scope.entityId, tenantId: scope.tenantId, deletedAt: null }, }, }, data, }); if (result.count !== 1) { throw new Error(`Row media ${scope.id} not found`); } } async function getAccessFilters({ tenantId, userId }: { tenantId?: string | null; userId?: string }) { let tenantFilters: Prisma.RowWhereInput | undefined = undefined; let userPermissionFilters: Prisma.RowWhereInput | undefined = undefined; if (tenantId !== undefined) { tenantFilters = TenantHelper.tenantCondition({ tenantId }); } if (userId) { userPermissionFilters = await getRowPermissionsCondition({ tenantId, userId }); } const filters: Prisma.RowWhereInput = { AND: [{ ...tenantFilters }, { ...userPermissionFilters }], }; return filters; }