Rearrange everything and migrate to talking directly to the database
This commit is contained in:
parent
363c2fc678
commit
28b01769e5
3
.gitignore
vendored
3
.gitignore
vendored
@ -37,3 +37,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Private env files
|
||||
.env
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/react": "^18",
|
||||
"@types/react-calendar-heatmap": "^1.6.6",
|
||||
"@types/react-dom": "^18",
|
||||
|
||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@ -64,6 +64,9 @@ devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.0.0
|
||||
'@types/pg':
|
||||
specifier: ^8.11.0
|
||||
version: 8.11.0
|
||||
'@types/react':
|
||||
specifier: ^18
|
||||
version: 18.0.0
|
||||
@ -1387,6 +1390,14 @@ packages:
|
||||
/@types/node@20.0.0:
|
||||
resolution: {integrity: sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw==}
|
||||
|
||||
/@types/pg@8.11.0:
|
||||
resolution: {integrity: sha512-sDAlRiBNthGjNFfvt0k6mtotoVYVQ63pA8R4EMWka7crawSR60waVYR0HAgmPRs/e2YaeJTD/43OoZ3PFw80pw==}
|
||||
dependencies:
|
||||
'@types/node': 20.0.0
|
||||
pg-protocol: 1.6.0
|
||||
pg-types: 4.0.2
|
||||
dev: true
|
||||
|
||||
/@types/prop-types@15.7.11:
|
||||
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
|
||||
|
||||
@ -4862,6 +4873,10 @@ packages:
|
||||
es-abstract: 1.22.3
|
||||
dev: true
|
||||
|
||||
/obuf@1.1.2:
|
||||
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
|
||||
dev: true
|
||||
|
||||
/once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
dependencies:
|
||||
@ -4978,6 +4993,11 @@ packages:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
/pg-numeric@1.0.2:
|
||||
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/pg-pool@3.6.1(pg@8.11.3):
|
||||
resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==}
|
||||
peerDependencies:
|
||||
@ -4998,6 +5018,19 @@ packages:
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
|
||||
/pg-types@4.0.2:
|
||||
resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
pg-numeric: 1.0.2
|
||||
postgres-array: 3.0.2
|
||||
postgres-bytea: 3.0.0
|
||||
postgres-date: 2.1.0
|
||||
postgres-interval: 3.0.0
|
||||
postgres-range: 1.1.4
|
||||
dev: true
|
||||
|
||||
/pg@8.11.3:
|
||||
resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
@ -5132,20 +5165,46 @@ packages:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
/postgres-array@3.0.2:
|
||||
resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/postgres-bytea@1.0.0:
|
||||
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/postgres-bytea@3.0.0:
|
||||
resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
obuf: 1.1.2
|
||||
dev: true
|
||||
|
||||
/postgres-date@1.0.7:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/postgres-date@2.1.0:
|
||||
resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/postgres-interval@1.2.0:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
/postgres-interval@3.0.0:
|
||||
resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/postgres-range@1.1.4:
|
||||
resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==}
|
||||
dev: true
|
||||
|
||||
/prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import OverviewPage, {OverviewConfig} from "@/app/OverviewPage";
|
||||
import OverviewPage, {OverviewConfig} from "@/components/OverviewPage";
|
||||
|
||||
const semester1Revision: OverviewConfig = {
|
||||
title: 'Semester 1 Revision',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import OverviewPage, {OverviewConfig} from "@/app/OverviewPage";
|
||||
import OverviewPage, {OverviewConfig} from "@/components/OverviewPage";
|
||||
|
||||
const semester2: OverviewConfig = {
|
||||
title: 'Semester 2',
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
export const fetcher = (
|
||||
input: string | URL | globalThis.Request,
|
||||
init?: RequestInit,
|
||||
) => fetch(input, init).then(res => res.json())
|
||||
@ -1,7 +1,8 @@
|
||||
import {SubjectComparisonCard} from "@/app/cards/subjectComparisonCard";
|
||||
import {CalendarOverviewCard} from "@/app/cards/calendarOverviewCard";
|
||||
import {SubjectOverviewCard} from "@/app/cards/subjectOverviewCard";
|
||||
import {getData} from "@/app/fetchData";
|
||||
import {SubjectComparisonCard} from "@/components/cards/subjectComparisonCard";
|
||||
import {CalendarOverviewCard} from "@/components/cards/calendarOverviewCard";
|
||||
import {SubjectOverviewCard} from "@/components/cards/subjectOverviewCard";
|
||||
import {getData} from "@/data/fetchData";
|
||||
import {getDataSQL} from "@/data/fetchWithSQL";
|
||||
|
||||
export interface OverviewConfig {
|
||||
title: string,
|
||||
@ -22,7 +23,7 @@ export interface OverviewConfig {
|
||||
export default async function OverviewPage({config}: {
|
||||
config: OverviewConfig
|
||||
}) {
|
||||
const data = await getData(config);
|
||||
const data = await getDataSQL(config);
|
||||
|
||||
return (
|
||||
<main className="m-6">
|
||||
@ -4,9 +4,9 @@ import * as R from 'ramda';
|
||||
import * as dFns from 'date-fns';
|
||||
import CalendarHeatmap from 'react-calendar-heatmap';
|
||||
import 'react-calendar-heatmap/dist/styles.css';
|
||||
import '../calendar-styles.css'
|
||||
import '../../app/calendar-styles.css'
|
||||
import {Tooltip} from 'react-tooltip';
|
||||
import {Data} from "@/app/fetchData";
|
||||
import {Data} from "@/data/fetchData";
|
||||
|
||||
const granularity = 4;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import {Card, DonutChart, Title} from "@tremor/react";
|
||||
import * as R from "ramda";
|
||||
import {Data} from "@/app/fetchData";
|
||||
import {Data} from "@/data/fetchData";
|
||||
|
||||
function useBreakdownData(data: Data): {
|
||||
name: string,
|
||||
@ -1,5 +1,5 @@
|
||||
import {Card, Metric, Text, Title} from "@tremor/react";
|
||||
import {Data} from "@/app/fetchData";
|
||||
import {Data} from "@/data/fetchData";
|
||||
|
||||
export function SubjectOverviewCard({
|
||||
title,
|
||||
35
src/data/database.ts
Normal file
35
src/data/database.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {DB as Database} from './db' // this is the Database interface we defined earlier
|
||||
import {Pool} from 'pg'
|
||||
import {Kysely, PostgresDialect} from 'kysely'
|
||||
import * as fs from "node:fs";
|
||||
|
||||
function fileOrEnv(fileKey: string, valueKey: string): string | undefined {
|
||||
const file = process.env[fileKey];
|
||||
|
||||
if (file && fs.existsSync(file)) {
|
||||
return fs.readFileSync(file, 'utf8');
|
||||
}
|
||||
|
||||
return process.env[valueKey];
|
||||
}
|
||||
|
||||
function getCredentials() {
|
||||
return {
|
||||
user: fileOrEnv('POSTGRES_USER_FILE', 'POSTGRES_USER'),
|
||||
password: fileOrEnv('POSTGRES_PASSWORD_FILE', 'POSTGRES_PASSWORD'),
|
||||
}
|
||||
}
|
||||
|
||||
const dialect = new PostgresDialect({
|
||||
pool: new Pool({
|
||||
database: process.env.POSTGRES_DB!,
|
||||
host: process.env.POSTGRES_HOST!,
|
||||
port: parseInt(process.env.POSTGRES_PORT!),
|
||||
...getCredentials(),
|
||||
max: 10,
|
||||
})
|
||||
})
|
||||
|
||||
export const db = new Kysely<Database>({
|
||||
dialect,
|
||||
})
|
||||
70
src/data/db.d.ts
vendored
Normal file
70
src/data/db.d.ts
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
import type {ColumnType, JSONColumnType} from "kysely";
|
||||
|
||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
|
||||
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
|
||||
|
||||
export type Json = ColumnType<JsonValue, string, string>;
|
||||
|
||||
export type JsonArray = JsonValue[];
|
||||
|
||||
export type JsonObject = {
|
||||
[K in string]?: JsonValue;
|
||||
};
|
||||
|
||||
export type JsonPrimitive = boolean | number | string | null;
|
||||
|
||||
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
||||
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
export interface Client {
|
||||
archived: boolean;
|
||||
at: Timestamp;
|
||||
id: number;
|
||||
name: string;
|
||||
server_deleted_at: Timestamp | null;
|
||||
workspace_id: number;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
active: boolean;
|
||||
client_id: number | null;
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
raw_json: JSONColumnType<{
|
||||
color: string;
|
||||
id: number;
|
||||
name: string;
|
||||
}>;
|
||||
toggl_id: Int8;
|
||||
workspace_id: Int8;
|
||||
}
|
||||
|
||||
export interface TimeEntry {
|
||||
description: string;
|
||||
id: Generated<number>;
|
||||
project_id: Int8 | null;
|
||||
raw_json: JSONColumnType<{
|
||||
start: string;
|
||||
end: string;
|
||||
seconds: number;
|
||||
}>;
|
||||
start: Timestamp;
|
||||
stop: Timestamp;
|
||||
toggl_id: Int8;
|
||||
}
|
||||
|
||||
export interface TogglPortalSeaqlMigrations {
|
||||
applied_at: Int8;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
client: Client;
|
||||
project: Project;
|
||||
time_entry: TimeEntry;
|
||||
toggl_portal_seaql_migrations: TogglPortalSeaqlMigrations;
|
||||
}
|
||||
@ -1,7 +1,5 @@
|
||||
"use server";
|
||||
|
||||
|
||||
import {OverviewConfig} from "@/app/OverviewPage";
|
||||
import {OverviewConfig} from "@/components/OverviewPage";
|
||||
|
||||
export interface Data {
|
||||
projects: {
|
||||
@ -19,9 +17,9 @@ export interface Data {
|
||||
}[],
|
||||
}
|
||||
|
||||
export async function getData(config: OverviewConfig) {
|
||||
export async function getData(config: OverviewConfig): Promise<Data> {
|
||||
const projectIds = config.subjects.map((subject) => subject.projectId);
|
||||
const projectResponse = await fetch(`https://revision.joshuacoles.me/api/project?select=raw_json&toggl_id=in.(${projectIds.join(',')})`);
|
||||
const projectResponse = await fetch(`https://cosmos.tail307fc.ts.net/api/project?select=raw_json&toggl_id=in.(${projectIds.join(',')})`);
|
||||
const projects = await projectResponse.json();
|
||||
const projectLensed = projects.map((project: any) => ({
|
||||
projectId: project.raw_json.id,
|
||||
@ -29,7 +27,7 @@ export async function getData(config: OverviewConfig) {
|
||||
color: project.raw_json.color,
|
||||
}));
|
||||
|
||||
const timeEntriesResponse = await fetch(`https://revision.joshuacoles.me/api/time_entry?select=project_id,raw_json&project_id=in.(${projectIds.join(',')})&start=gt.${config.timePeriod.start}&start=lt.${config.timePeriod.end}`);
|
||||
const timeEntriesResponse = await fetch(`https://cosmos.tail307fc.ts.net/api/time_entry?select=project_id,raw_json&project_id=in.(${projectIds.join(',')})&start=gt.${config.timePeriod.start}&start=lt.${config.timePeriod.end}`);
|
||||
const timeEntries = await timeEntriesResponse.json();
|
||||
const timeEntriesLensed = timeEntries.map((timeEntry: any) => ({
|
||||
projectId: timeEntry.project_id,
|
||||
35
src/data/fetchWithSQL.ts
Normal file
35
src/data/fetchWithSQL.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {OverviewConfig} from "@/components/OverviewPage";
|
||||
import {Data} from "@/data/fetchData";
|
||||
import {db} from "@/data/database";
|
||||
import * as dFns from "date-fns";
|
||||
|
||||
export async function getDataSQL(config: OverviewConfig): Promise<Data> {
|
||||
let projectIds = config.subjects.map((subject) => subject.projectId.toString());
|
||||
|
||||
const projects = await db.selectFrom('project')
|
||||
.select('raw_json')
|
||||
.where('project.toggl_id', 'in', projectIds)
|
||||
.execute();
|
||||
|
||||
const timeEntries = await db.selectFrom('time_entry')
|
||||
.select(['project_id', 'raw_json'])
|
||||
.where('project_id', 'in', projectIds)
|
||||
.where('start', '>', dFns.parseISO(config.timePeriod.start))
|
||||
.where('start', '<', dFns.parseISO(config.timePeriod.end))
|
||||
.execute();
|
||||
|
||||
return {
|
||||
projects: projects.map((project) => ({
|
||||
projectId: project.raw_json.id,
|
||||
name: project.raw_json.name,
|
||||
color: project.raw_json.color,
|
||||
})),
|
||||
|
||||
timeEntries: timeEntries.map((timeEntry) => ({
|
||||
projectId: parseInt(timeEntry.project_id!),
|
||||
start: timeEntry.raw_json.start,
|
||||
end: timeEntry.raw_json.end,
|
||||
duration: timeEntry.raw_json.seconds,
|
||||
})),
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user