Skip to content

Instantly share code, notes, and snippets.

@AlexandroMtzG
Created August 15, 2023 18:27
Show Gist options
  • Save AlexandroMtzG/6faf7f3ffa4ff719fa8a8feaab5a27e6 to your computer and use it in GitHub Desktop.
Save AlexandroMtzG/6faf7f3ffa4ff719fa8a8feaab5a27e6 to your computer and use it in GitHub Desktop.
import { ActionArgs, json } from "@remix-run/node";
import { useTypedActionData, useTypedLoaderData } from "remix-typedjson";
import { i18nHelper } from "~/locale/i18n.utils";
import EditPageLayout from "~/components/ui/layouts/EditPageLayout";
import { EntityWithDetails, getAllEntities } from "~/utils/db/entities/entities.db.server";
import { TenantWithDetails, adminGetAllTenants, getTenant } from "~/utils/db/tenants.db.server";
import TableSimple from "~/components/ui/tables/TableSimple";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import NumberUtils from "~/utils/shared/NumberUtils";
import { useSearchParams, useSubmit } from "@remix-run/react";
import { PropertyType } from "~/application/enums/entities/PropertyType";
import EntitiesSingleton from "~/modules/rows/repositories/EntitiesSingleton";
import { toast } from "react-hot-toast";
import { db } from "~/utils/db.server";
import MenuWithPopper from "~/components/ui/dropdowns/MenuWithPopper";
import { Prisma, Row, RowValue } from "@prisma/client";
import { Colors } from "~/application/enums/shared/Colors";
import InputCombobox from "~/components/ui/input/InputCombobox";
import { DefaultEntityTypes } from "~/application/dtos/shared/DefaultEntityTypes";
type TenantDataDto = {
entity: EntityWithDetails;
tenant: TenantWithDetails;
activeRows: number;
shadowRows: number;
};
type LoaderData = {
allEntities: EntityWithDetails[];
allTenants: TenantWithDetails[];
items: TenantDataDto[];
isDevelopment: boolean;
};
export let loader = async () => {
const allEntities = await getAllEntities({ tenantId: null, types: [DefaultEntityTypes.All, DefaultEntityTypes.AppOnly] });
const allTenants = await adminGetAllTenants();
const items: TenantDataDto[] = [];
const activeRows = await db.row.groupBy({
by: ["entityId", "tenantId"],
_count: { id: true },
where: { deletedAt: null },
});
const shadowRows = await db.row.groupBy({
by: ["entityId", "tenantId"],
_count: { id: true },
where: { deletedAt: { not: null } },
});
for (const entity of allEntities) {
for (const tenant of allTenants) {
items.push({
entity,
tenant,
activeRows: activeRows.find((f) => f.entityId === entity.id && f.tenantId === tenant.id)?._count?.id ?? 0,
shadowRows: shadowRows.find((f) => f.entityId === entity.id && f.tenantId === tenant.id)?._count?.id ?? 0,
});
}
}
const data: LoaderData = {
allEntities,
allTenants,
items,
isDevelopment: process.env.NODE_ENV !== "production",
};
return json(data);
};
const BATCH_SIZE = 10_000;
type ActionData = {
error?: string;
success?: string;
};
export const action = async ({ request }: ActionArgs) => {
const { t } = await i18nHelper(request);
const form = await request.formData();
const action = form.get("action")?.toString();
const allEntities = await getAllEntities({ tenantId: null });
const entityId = form.get("entityId")?.toString() ?? "";
const tenantId = form.get("tenantId")?.toString() ?? "";
const entity = allEntities.find((e) => e.id === entityId);
if (!entity) {
return json({ error: "Entity not found" }, { status: 400 });
}
const tenant = await getTenant(tenantId);
if (!tenant) {
return json({ error: "Tenant not found" }, { status: 400 });
}
if (action === "create-rows") {
const numberOfRows = Number(form.get("numberOfRows")?.toString());
try {
const status = {
totalRows: 0,
};
const start = performance.now();
for (let i = 0; i < numberOfRows; i += BATCH_SIZE) {
await Promise.all(
Array.from({ length: Math.min(BATCH_SIZE, numberOfRows - i) }).map(async (_, idx) => {
await createFakeRow({ entity, tenantId, idx: i + idx, status });
})
);
}
const end = performance.now();
const formattedTime = `${NumberUtils.intFormat(end - start)}ms`;
return json({ success: `${status.totalRows} ${t(entity.titlePlural)} created (${tenant.name}) in ${formattedTime}` });
} catch (e: any) {
return json({ error: e.message }, { status: 400 });
}
} else if (action === "update-rows") {
const numberOfRows = Number(form.get("numberOfRows")?.toString());
const start = performance.now();
let rows = await db.row.findMany({
where: { entityId, tenantId, deletedAt: null },
include: { values: true },
});
rows = rows.slice(0, numberOfRows);
if (rows.length === 0) {
return json({ error: "No rows found" }, { status: 400 });
}
const status = {
totalRows: 0,
};
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
await Promise.all(
rows.slice(i, i + BATCH_SIZE).map(async (row, idx) => {
await updateFakeRow(row, { entity, idx: i + idx, status });
})
);
}
const end = performance.now();
const formattedTime = `${NumberUtils.intFormat(end - start)}ms`;
return json({ success: `${status.totalRows} ${t(entity.titlePlural)} updated (${tenant.name}) in ${formattedTime}` });
} else if (action === "delete-rows") {
if (process.env.NODE_ENV !== "development") {
return json({ error: "Not allowed in production" }, { status: 400 });
}
const numberOfRows = Number(form.get("numberOfRows")?.toString());
let rowsToDelete = await db.row.findMany({
where: { entityId, tenantId },
});
rowsToDelete = rowsToDelete.slice(0, numberOfRows);
if (rowsToDelete.length === 0) {
return json({ error: "No rows found" }, { status: 400 });
}
const start = performance.now();
for (let i = 0; i < rowsToDelete.length; i += BATCH_SIZE) {
await db.row.deleteMany({
where: { id: { in: rowsToDelete.slice(i, i + BATCH_SIZE).map((f) => f.id) } },
});
}
const end = performance.now();
const formattedTime = `${NumberUtils.intFormat(end - start)}ms`;
return json({ success: `${rowsToDelete.length} ${t(entity.titlePlural)} deleted (${tenant.name}) in ${formattedTime}` });
} else if (action === "shadow-delete-rows") {
if (process.env.NODE_ENV !== "development") {
return json({ error: "Not allowed in production" }, { status: 400 });
}
const numberOfRows = Number(form.get("numberOfRows")?.toString());
let rowsToDelete = await db.row.findMany({
where: { entityId, tenantId, deletedAt: null },
});
rowsToDelete = rowsToDelete.slice(0, numberOfRows);
if (rowsToDelete.length === 0) {
return json({ error: "No rows found" }, { status: 400 });
}
const start = performance.now();
for (let i = 0; i < rowsToDelete.length; i += BATCH_SIZE) {
await db.row.updateMany({
where: { id: { in: rowsToDelete.slice(i, i + BATCH_SIZE).map((f) => f.id) } },
data: { deletedAt: new Date() },
});
}
const end = performance.now();
const formattedTime = `${NumberUtils.intFormat(end - start)}ms`;
return json({ success: `${rowsToDelete.length} ${t(entity.titlePlural)} deleted (${tenant.name}) in ${formattedTime}` });
} else {
return json({ error: "Unknown action" }, { status: 400 });
}
};
async function createFakeRow({ entity, tenantId, idx, status }: { entity: EntityWithDetails; tenantId: string; idx: number; status: { totalRows: number } }) {
const values: Prisma.RowValueUncheckedCreateWithoutRowInput[] = [];
let tag = entity.tags.find((f) => f.value === "fake-row");
if (!tag) {
tag = await db.entityTag.create({
data: {
entityId: entity.id,
value: "fake-row",
color: Colors.RED,
},
});
entity.tags.push(tag);
}
tag = entity.tags.find((f) => f.value === "fake-row");
if (!tag) {
throw Error("Could not create tag: fake-row");
}
for (const property of entity.properties) {
const propertyId = property.id;
if ([PropertyType.TEXT].includes(property.type)) {
values.push({ propertyId, textValue: `Fake ${idx}` });
} else if ([PropertyType.NUMBER].includes(property.type)) {
values.push({ propertyId, numberValue: idx.toString() });
} else if ([PropertyType.BOOLEAN].includes(property.type)) {
values.push({ propertyId, booleanValue: idx % 2 === 0 });
} else if ([PropertyType.DATE].includes(property.type)) {
values.push({ propertyId, dateValue: new Date().toISOString() });
} else if ([PropertyType.SELECT].includes(property.type)) {
const firstOption = property.options.length > 0 ? property.options[0] : null;
values.push({ propertyId, textValue: firstOption?.value ?? idx.toString() });
} else if ([PropertyType.MEDIA].includes(property.type)) {
values.push({ propertyId, media: { create: { title: "Fake", name: "Fake", file: "Fake", type: "Fake" } } });
} else if ([PropertyType.RANGE_DATE].includes(property.type)) {
values.push({ propertyId, range: { create: { dateMin: new Date(), dateMax: new Date(), numberMin: null, numberMax: null } } });
} else if ([PropertyType.RANGE_NUMBER].includes(property.type)) {
values.push({ propertyId, range: { create: { dateMin: null, dateMax: null, numberMin: 1, numberMax: 2 } } });
} else {
throw new Error(`[${entity.name}] Unknown property type ${PropertyType[property.type]}`);
}
}
const row = await db.row.create({
data: {
entityId: entity.id,
tenantId,
folio: idx,
order: idx,
values: {
create: values,
},
permissions: { create: { tenantId, access: "delete" } },
tags: { create: { tagId: tag.id } },
},
});
status.totalRows++;
await Promise.all(
entity.childEntities.map(async (childRel) => {
const childEntity = EntitiesSingleton.getInstance()
.getEntities()
.find((e) => e.id === childRel.childId);
if (!childEntity) {
return;
}
const childRow = await createFakeRow({ entity: childEntity, tenantId, idx, status });
status.totalRows++;
return await db.rowRelationship.create({
data: {
relationshipId: childRel.id,
parentId: row.id,
childId: childRow.id,
},
});
})
);
return row;
}
async function updateFakeRow(
row: Row & { values: RowValue[] },
{ entity, idx, status }: { entity: EntityWithDetails; idx: number; status: { totalRows: number } }
) {
const values: Prisma.RowValueUpdateWithWhereUniqueWithoutRowInput[] = [];
for (const property of entity.properties) {
const value = row.values.find((f) => f.propertyId === property.id);
if (!value) {
continue;
}
if ([PropertyType.TEXT].includes(property.type)) {
values.push({ where: { id: value.id }, data: { textValue: "Updated " + value.textValue } });
} else if ([PropertyType.NUMBER].includes(property.type)) {
values.push({ where: { id: value.id }, data: { numberValue: idx + 100 } });
} else if ([PropertyType.BOOLEAN].includes(property.type)) {
values.push({ where: { id: value.id }, data: { booleanValue: idx % 2 !== 0 } });
} else if ([PropertyType.DATE].includes(property.type)) {
values.push({ where: { id: value.id }, data: { dateValue: new Date().toISOString() } });
} else if ([PropertyType.SELECT].includes(property.type)) {
const firstOption = property.options.length > 0 ? property.options[0] : null;
values.push({ where: { id: value.id }, data: { textValue: firstOption?.value ?? idx.toString() } });
} else if ([PropertyType.MEDIA].includes(property.type)) {
values.push({ where: { id: value.id }, data: { media: { create: { title: "Fake", name: "Fake", file: "Fake", type: "Fake" } } } });
} else if ([PropertyType.RANGE_DATE].includes(property.type)) {
values.push({ where: { id: value.id }, data: { range: { create: { dateMin: new Date(), dateMax: new Date(), numberMin: null, numberMax: null } } } });
} else if ([PropertyType.RANGE_NUMBER].includes(property.type)) {
values.push({ where: { id: value.id }, data: { range: { create: { dateMin: null, dateMax: null, numberMin: 1, numberMax: 2 } } } });
} else {
throw new Error(`[${entity.name}] Unknown property type ${PropertyType[property.type]}`);
}
}
await db.row.update({
where: { id: row.id },
data: {
values: {
update: values,
},
},
});
status.totalRows++;
return row;
}
export default function () {
const { t } = useTranslation();
const data = useTypedLoaderData<LoaderData>();
const actionData = useTypedActionData<ActionData>();
const submit = useSubmit();
const [searchParams, setSearchParams] = useSearchParams();
useEffect(() => {
if (actionData?.error) {
toast.error(actionData.error);
} else if (actionData?.success) {
toast.success(actionData.success);
}
}, [actionData]);
function getTenantIds() {
const ids = searchParams.get("tenantIds")?.split(",") ?? [];
return ids.filter((f) => f);
}
function getEntityIds() {
const ids = searchParams.get("entityIds")?.split(",") ?? [];
return ids.filter((f) => f);
}
function filteredItems() {
const tenantIds = getTenantIds();
const entityIds = getEntityIds();
return data.items.filter((f) => {
if (!tenantIds.includes(f.tenant.id)) {
return false;
}
if (!entityIds.includes(f.entity.id)) {
return false;
}
return true;
});
}
function onCreateRows(item: TenantDataDto, numberOfRows: number) {
const form = new FormData();
form.set("action", "create-rows");
form.set("entityId", item.entity.id);
form.set("tenantId", item.tenant.id);
form.set("numberOfRows", String(numberOfRows));
submit(form, {
method: "post",
});
}
function onUpdateRows(item: TenantDataDto, numberOfRows: number) {
const form = new FormData();
form.set("action", "update-rows");
form.set("entityId", item.entity.id);
form.set("tenantId", item.tenant.id);
form.set("numberOfRows", String(numberOfRows));
submit(form, {
method: "post",
});
}
function onDeleteRows(item: TenantDataDto, { shadow, numberOfRows }: { shadow: boolean; numberOfRows: number }) {
const form = new FormData();
if (shadow) {
form.set("action", "shadow-delete-rows");
} else {
form.set("action", "delete-rows");
}
form.set("entityId", item.entity.id);
form.set("tenantId", item.tenant.id);
form.set("numberOfRows", String(numberOfRows));
submit(form, {
method: "post",
});
}
return (
<EditPageLayout title="Testing">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<InputCombobox
title="Tenants"
value={getTenantIds()}
selectPlaceholder="Select tenants"
options={data.allTenants.map((f) => {
return {
value: f.id,
name: f.name,
};
})}
onChange={(value) => {
if (value) {
searchParams.set("tenantIds", value.join(","));
} else {
searchParams.delete("tenantIds");
}
setSearchParams(searchParams);
}}
/>
<InputCombobox
title="Entities"
value={getEntityIds()}
selectPlaceholder="Select entities"
options={data.allEntities.map((f) => {
return {
value: f.id,
name: f.name,
};
})}
onChange={(value) => {
if (value) {
searchParams.set("entityIds", value.join(","));
} else {
searchParams.delete("entityIds");
}
setSearchParams(searchParams);
}}
/>
</div>
{filteredItems().length === 0 ? (
<div>Select at least one tenant and one entity</div>
) : (
<TableSimple
items={filteredItems()}
headers={[
{
name: "entity",
title: "Entity",
value: (i) => t(i.entity.title),
},
{
name: "tenant",
title: "Tenant",
className: "w-full",
value: (i) => i.tenant.name,
},
{
name: "activeRows",
title: "Active Rows",
value: (i) => NumberUtils.intFormat(i.activeRows),
},
{
name: "shadowRows",
title: "Shadow Rows",
value: (i) => NumberUtils.intFormat(i.shadowRows),
},
{
name: "totalRows",
title: "Total Rows",
value: (i) => NumberUtils.intFormat(i.activeRows + i.shadowRows),
},
{
name: "actions",
title: "",
value: (i) => (
<div className="flex items-center space-x-2">
<MenuWithPopper
onClick={(e) => {
e.stopPropagation();
}}
className="rounded-md border border-gray-200 bg-white px-2 py-1.5 font-medium hover:bg-gray-50"
options={[
...[1, 10, 100, 1_000, 10_000, 100_000].map((numberOfRows) => {
return {
title: `${numberOfRows === 1 ? "1 row" : `${NumberUtils.intFormat(numberOfRows)} rows`}`,
onClick: () => onCreateRows(i, numberOfRows),
};
}),
]}
button={<>Create</>}
/>
<MenuWithPopper
onClick={(e) => {
e.stopPropagation();
}}
className="rounded-md border border-gray-200 bg-white px-2 py-1.5 font-medium hover:bg-gray-50"
options={[
...[1, 10, 100, 1_000, 10_000, 100_000].map((numberOfRows) => {
return {
title: `${numberOfRows === 1 ? "1 row" : `${NumberUtils.intFormat(numberOfRows)} rows`}`,
onClick: () => onUpdateRows(i, numberOfRows),
};
}),
]}
button={<>Update</>}
/>
<MenuWithPopper
onClick={(e) => {
e.stopPropagation();
}}
className="rounded-md border border-gray-200 bg-white px-2 py-1.5 font-medium hover:bg-gray-50"
options={[
...[1, 10, 100, 1_000, 10_000, 100_000].map((numberOfRows) => {
return {
title: `${numberOfRows === 1 ? "1 row" : `${NumberUtils.intFormat(numberOfRows)} rows`}`,
onClick: () => onDeleteRows(i, { shadow: true, numberOfRows }),
className: "text-orange-600",
};
}),
]}
button={<>Shadow Delete</>}
/>
<MenuWithPopper
onClick={(e) => {
e.stopPropagation();
}}
className="rounded-md border border-gray-200 bg-white px-2 py-1.5 font-medium hover:bg-gray-50"
options={[
...[1, 10, 100, 1_000, 10_000, 100_000].map((numberOfRows) => {
return {
title: `${numberOfRows === 1 ? "1 row" : `${NumberUtils.intFormat(numberOfRows)} rows`}`,
onClick: () => onDeleteRows(i, { shadow: false, numberOfRows }),
className: "text-red-600",
};
}),
]}
button={<>Delete</>}
/>
</div>
),
},
]}
/>
)}
</div>
</EditPageLayout>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment