Skip to content

Commit

Permalink
backup to json and restore from json
Browse files Browse the repository at this point in the history
  • Loading branch information
ze-kel committed Aug 18, 2024
1 parent 6bca03e commit 5508010
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 15 deletions.
12 changes: 5 additions & 7 deletions apps/next/src/app/login/form.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
"use client";

import { useState } from "react";
import { Input } from "@tyl/ui/input";
import { useRouter } from "next/navigation";

import { cn } from "@tyl/ui";
import { Alert, AlertDescription, AlertTitle } from "@tyl/ui/alert";
import { Button } from "@tyl/ui/button";

import { useRouter } from "next/navigation";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@tyl/ui/card";
import { RadioTabs, RadioTabItem } from "@tyl/ui/radio-tabs";
import { Alert, AlertDescription, AlertTitle } from "@tyl/ui/alert";

import { cn } from "@tyl/ui"
import { Input } from "@tyl/ui/input";
import { RadioTabItem, RadioTabs } from "@tyl/ui/radio-tabs";

type ActionState = "login" | "register";

Expand Down
196 changes: 196 additions & 0 deletions apps/next/src/app/settings/backup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
"use client";

import { useState } from "react";
import { z } from "zod";

import { Button } from "@tyl/ui/button";
import {
ITrackable,
ITrackableUnsaved,
ITrackableUpdate,
ZTrackable,
} from "@tyl/validators/trackable";

import { api } from "~/trpc/react";

const getBackup = async () => {
const res = await api.trackablesRouter.getAllTrackables.query({
limits: {
type: "range",
from: {
year: 1990,
month: 0,
},
to: {
year: new Date().getFullYear() + 1,
month: 0,
},
},
});
const a = document.createElement("a"); // Create "a" element
const blob = new Blob([JSON.stringify(res, null, 4)], {
type: "application/json",
}); // Create a blob (file-like object)
const url = URL.createObjectURL(blob); // Create an object URL from blob
a.setAttribute("href", url); // Set "a" element link
a.setAttribute("download", `TYL_BACKUP_${new Date().getTime()}`); // Set download filename
a.click(); // Start downloading
};

export const BackupAndRestore = () => {
const [fileData, setFileData] = useState(["", ""]);
return (
<div>
<h2 className="mb-2 mt-4 text-xl">Backup and Restore</h2>

<div className="flex items-center gap-4">
<Button variant={"outline"} onClick={() => getBackup()}>
Backup all Trackables
</Button>

<Button asChild variant={"outline"} className="cursor-pointer">
<label htmlFor="backupJsonLoad">Load backup file</label>
</Button>

<div className="text-sm opacity-50">{fileData[0]}</div>

<input
className="hidden"
type="file"
id="backupJsonLoad"
onChange={(e) => {
const firstFile = e.target.files?.[0];
console.log("ff", firstFile);
if (!firstFile) return;

const name = firstFile.name;
let reader = new FileReader();

reader.readAsText(firstFile);

reader.onload = () => {
if (typeof reader.result === "string") {
setFileData([name, reader.result as string]);
}
};
}}
/>
</div>

{fileData[1] && (
<div className="mt-4">
<FileParser content={fileData[1]} />
</div>
)}
</div>
);
};

const backupZ = z.array(ZTrackable);

const parseContentJson = (content: string) => {
try {
return { result: JSON.parse(content) };
} catch (e) {
return { error: "Error when parsing JSON: " + e };
}
};

const FileParser = ({ content }: { content?: string }) => {
if (!content || !content.length) return;

const objectFromJson = parseContentJson(content);

if (objectFromJson.error) {
return <div className="font-mono text-sm">{objectFromJson.error}</div>;
}

const parsed = backupZ.safeParse(objectFromJson.result);

if (!parsed.success) {
console.log(parsed.error);
return (
<div className="font-mono text-sm">
Error{parsed.error.errors.length > 1 ? "s" : ""} when checking against
backup schema:{" "}
{parsed.error.errors.map((v, i) => (
<div key={i}>{v.message}</div>
))}
</div>
);
}

return (
<div className="grid auto-cols-auto grid-cols-7 items-center justify-center gap-2 py-3 text-xs">
<div className="col-span-3">Id</div>
<div className="col-span-2">Name</div>
<div className="">Type</div>
<div className=""></div>

{parsed.data.map((v) => {
return <ParsedItem trackable={v} key={v.id} />;
})}
</div>
);
};

const ParsedItem = ({ trackable }: { trackable: ITrackable }) => {
const [isLoading, setIsLoading] = useState(false);

const [savedId, setSavedId] = useState("");

const save = async () => {
setIsLoading(true);
const newOne = await api.trackablesRouter.createTrackable.mutate({
name: `restored_${trackable.name}`,
settings: trackable.settings,
type: trackable.type,
});

console.log(newOne);

const allEntries: ITrackableUpdate[] = [];

Object.entries(trackable.data).forEach(([year, yearData]) => {
Object.entries(yearData).forEach(([month, monthData]) => {
Object.entries(monthData).forEach(([day, value]) => {
allEntries.push({
id: newOne.id,
value,
year: Number(year),
month: Number(month),
day: Number(day),
});
});
});
});

console.log(allEntries);

await api.trackablesRouter.updateTrackableEntries.mutate(allEntries);

console.log("all good");
setIsLoading(false);
setSavedId(newOne.id);
};

return (
<>
<div className="col-span-3">{trackable.id}</div>
<div className="col-span-2"> {trackable.name}</div>
<div>{trackable.type}</div>

{!savedId ? (
<Button variant={"ghost"} isLoading={isLoading} onClick={() => save()}>
Save
</Button>
) : (
<Button asChild>
<a href={"/trackables/" + savedId} target="_blank">
Open
</a>
</Button>
)}
</>
);
};
4 changes: 4 additions & 0 deletions apps/next/src/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { UserSettings } from "src/app/settings/userSettings";

import { BackupAndRestore } from "~/app/settings/backup";

const Page = () => {
return (
<div className="content-container">
<h1 className="mb-2 text-2xl font-semibold lg:text-4xl">User settings</h1>

<UserSettings />

<BackupAndRestore />
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion apps/next/src/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useTheme } from "next-themes";
import { useMediaQuery } from "usehooks-ts";

import type { ButtonProps } from "@tyl/ui/button";
import { User } from "@tyl/auth";
import { Button } from "@tyl/ui/button";
import {
Drawer,
Expand All @@ -29,7 +30,6 @@ import {
DropdownContent,
DropdownTrigger,
} from "~/components/Dropdown";
import { User } from "@tyl/auth";

const SigOutButton = () => {
const router = useRouter();
Expand Down
22 changes: 21 additions & 1 deletion packages/api/src/router/trackables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { format } from "date-fns";
import { z } from "zod";

import type { DbTrackableRecordInsert } from "@tyl/db/schema";
import { and, between, eq } from "@tyl/db";
import { and, between, eq, sql } from "@tyl/db";
import { trackable, trackableRecord } from "@tyl/db/schema";
import { ZGETLimits } from "@tyl/validators/api";
import {
Expand Down Expand Up @@ -168,6 +168,26 @@ export const trackablesRouter = {
})
.returning();

return input;
}),
updateTrackableEntries: protectedProcedure
.input(z.array(ZTrackableUpdate))
.mutation(async ({ ctx, input }) => {
const toInsert: DbTrackableRecordInsert[] = input.map((i) => ({
trackableId: i.id,
value: i.value,
date: format(new Date(i.year, i.month, i.day), "yyyy-MM-dd"),
userId: ctx.user.id,
}));

await ctx.db
.insert(trackableRecord)
.values(toInsert)
.onConflictDoUpdate({
target: [trackableRecord.trackableId, trackableRecord.date],
set: { value: sql.raw(`excluded.${trackableRecord.value.name}`) },
});

return input;
}),
updateTrackableName: protectedProcedure
Expand Down
2 changes: 2 additions & 0 deletions packages/db/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ import { migrate } from "drizzle-orm/node-postgres/migrator";

import { db } from ".";

// Folder path is correct for docker deployment of production build.
// When developing locally do not set migration to true, use commands in package.json instead
void migrate(db, { migrationsFolder: "./drizzle" });
4 changes: 2 additions & 2 deletions packages/ui/src/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ export interface ButtonProps
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
({ className, variant, size, asChild = false, isLoading, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
{props.isLoading ? <Spinner /> : props.children}
{isLoading ? <Spinner /> : props.children}
</Comp>
);
},
Expand Down
43 changes: 39 additions & 4 deletions packages/validators/src/trackable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,17 @@ export type ITrackableSettings =
//
// Trackable
//
export type ITrackableDataMonth = Record<number, string>;
export type ITrackableDataYear = Record<number, ITrackableDataMonth>;
export type ITrackableData = Record<number, ITrackableDataYear>;

export const zTrackableDataMonth = z.record(z.coerce.number(), z.string());
export const zTrackableDataYear = z.record(
z.coerce.number(),
zTrackableDataMonth,
);
export const zTrackableData = z.record(z.coerce.number(), zTrackableDataYear);

export type ITrackableDataMonth = z.infer<typeof zTrackableDataMonth>;
export type ITrackableDataYear = z.infer<typeof zTrackableDataYear>;
export type ITrackableData = z.infer<typeof zTrackableData>;

export type ITrackableUnsaved =
| {
Expand All @@ -133,7 +141,34 @@ export type ITrackableUnsaved =
data: ITrackableData;
};

export type ITrackable = ITrackableUnsaved & { id: string };
export const ZTrackable = z
.object({
id: z.string(),
name: z.string(),
type: z.literal("boolean"),
settings: ZTrackableSettingsBoolean,
data: zTrackableData,
})
.or(
z.object({
id: z.string(),
name: z.string(),
type: z.literal("number"),
settings: ZTrackableSettingsNumber,
data: zTrackableData,
}),
)
.or(
z.object({
id: z.string(),
name: z.string(),
type: z.literal("range"),
settings: ZTrackableSettingsRange,
data: zTrackableData,
}),
);

export type ITrackable = z.infer<typeof ZTrackable>;
export type ITrackableBasic = Omit<Omit<ITrackable, "data">, "settings">;

//
Expand Down

0 comments on commit 5508010

Please sign in to comment.