diff --git a/.vscode/settings.json b/.vscode/settings.json index 0b7dd4e..316fb18 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ ".vscode": true, ".next": true, "node_modules": true - } + }, + "workbench.colorTheme": "poimandres-noitalics" } diff --git a/bun.lockb b/bun.lockb index 6287e4b..a5fd113 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 2763d4e..7c76479 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "start": "next start", "db:generate": "drizzle-kit generate:pg", "db:migrate": "tsx src/server/db/migrate.ts", + "db:seed": "tsx src/server/db/seed/seed.ts && tsx src/server/db/seed/auth.ts", "db:drop": "drizzle-kit drop", "db:pull": "drizzle-kit introspect:pg", "db:studio": "drizzle-kit studio", @@ -19,6 +20,7 @@ "dependencies": { "@auth/core": "^0.22.0", "@auth/drizzle-adapter": "^0.3.16", + "@faker-js/faker": "^8.4.0", "@hookform/resolvers": "^3.3.4", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", @@ -110,4 +112,4 @@ "ct3aMetadata": { "initVersion": "7.25.2" } -} \ No newline at end of file +} diff --git a/scripts/reset.sh b/scripts/reset.sh new file mode 100755 index 0000000..1050b78 --- /dev/null +++ b/scripts/reset.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +export PGUSER=postgres +export PGPASSWORD=hackme +export PGHOST=localhost + +echo Removing migrations +rm -rf drizzle +echo "Dropping db" + +dropdb -f --if-exists kidarr +echo "Creating db" +createdb kidarr + +bun db:generate +bun db:migrate + +# bun run src/db/migrate.ts +bun db:seed diff --git a/src/components/children/child-select-list.tsx b/src/components/children/child-select-list.tsx index f123c8c..00baa53 100644 --- a/src/components/children/child-select-list.tsx +++ b/src/components/children/child-select-list.tsx @@ -1,5 +1,5 @@ -'use client'; -import React from 'react'; +"use client"; +import React from "react"; import { Select, SelectContent, @@ -7,15 +7,15 @@ import { SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select'; -import type ChildModel from '@/lib/models/child'; +} from "@/components/ui/select"; +import { type Child } from "@/server/db/schema/children"; type ChildSelectListProps = { - kids: ChildModel[]; -} -const ChildSelectList: React.FC = ({ kids }) => { + children: Child[]; +}; +const ChildSelectList: React.FC = ({ children: kids }) => { return ( - @@ -23,10 +23,7 @@ const ChildSelectList: React.FC = ({ kids }) => { (All Children) {kids?.map((r) => ( - + {r.name} ))} diff --git a/src/components/children/children-filter.tsx b/src/components/children/children-filter.tsx index 9e0bfc5..7489fd5 100644 --- a/src/components/children/children-filter.tsx +++ b/src/components/children/children-filter.tsx @@ -1,15 +1,19 @@ -import React from "react"; +"use client"; +import React, { useEffect } from "react"; import ChildSelectList from "./child-select-list"; -import { type CompleteChild } from "@/server/db/schema/children"; +import { type Child } from "@/server/db/schema/children"; type ChildrenFilterProps = { - kids: CompleteChild[]; + children: Child[]; }; -const ChildrenFilter: React.FC = async ({ kids }) => { +const ChildrenFilter: React.FC = ({ children }) => { + useEffect(() => { + console.log("ChildrenFilter: useEffect", children); + }, [children]); return (
- + {/* */}
); diff --git a/src/components/maps/main-map.tsx b/src/components/maps/main-map.tsx index 679b677..32837f7 100644 --- a/src/components/maps/main-map.tsx +++ b/src/components/maps/main-map.tsx @@ -27,7 +27,25 @@ const MainMap: React.FC = ({ kids }) => { zoom={10} scrollWheelZoom={true} > - + + <> + {kids?.map((kid) => + kid.devices?.map((device) => + device.pings.map((ping) => ( + + )), + ), + )} + + ); diff --git a/src/components/pages/dashboard-page.tsx b/src/components/pages/dashboard-page.tsx index 334d473..d93a900 100644 --- a/src/components/pages/dashboard-page.tsx +++ b/src/components/pages/dashboard-page.tsx @@ -11,7 +11,7 @@ const DashboardPage = async () => { return (
- +
diff --git a/src/lib/api/children/queries.ts b/src/lib/api/children/queries.ts index 270adc3..dc0c7ca 100644 --- a/src/lib/api/children/queries.ts +++ b/src/lib/api/children/queries.ts @@ -1,24 +1,36 @@ -import { db } from "@/server/db/index"; import { eq, and } from "drizzle-orm"; import { getUserAuth } from "@/lib/auth/utils"; +import { db } from "@/server/db"; import { type ChildId, childIdSchema, children, } from "@/server/db/schema/children"; -import { devices } from "@/server/db/schema/devices"; -import { pings } from "@/server/db/schema/pings"; export const getChildren = async () => { const { session } = await getUserAuth(); - const c = await db - .select() - .from(children) - .where(eq(children.userId, session?.user.id!)) - .innerJoin(devices, eq(devices.childId, children.id)) - .innerJoin(pings, eq(pings.deviceId, devices.id)) - .orderBy(children.name); - return { children: c.map((c) => ({ ...c, devices: [] })) }; + console.log("queries", "getChildren", session?.user.id!); + const c = await db.query.children.findMany({ + where: (children, { eq }) => eq(children.userId, session?.user.id!), + orderBy: (children) => children.name, + with: { + devices: { + with: { + pings: true, + }, + }, + }, + }); + console.log("queries", "gotChildren", c); + return { children: c }; + // const c = await db + // .select() + // .from(children) + // .where(eq(children.userId, session?.user.id!)) + // .innerJoin(devices, eq(devices.childId, children.id)) + // .innerJoin(pings, eq(pings.deviceId, devices.id)) + // .orderBy(children.name); + // return c; }; export const getChildById = async (id: ChildId) => { diff --git a/src/lib/auth/utils.ts b/src/lib/auth/utils.ts index 5eb205b..f8c57fe 100644 --- a/src/lib/auth/utils.ts +++ b/src/lib/auth/utils.ts @@ -2,7 +2,7 @@ import { db } from "@/server/db/index"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; import { DefaultSession, getServerSession, NextAuthOptions } from "next-auth"; import { redirect } from "next/navigation"; -import { env } from "@/env.mjs" +import { env } from "@/env.mjs"; import GoogleProvider from "next-auth/providers/google"; declare module "next-auth" { @@ -32,14 +32,13 @@ export const authOptions: NextAuthOptions = { }, }, providers: [ - GoogleProvider({ + GoogleProvider({ clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, - }) + }), ], }; - export const getUserAuth = async () => { const session = await getServerSession(authOptions); return { session } as AuthSession; @@ -47,6 +46,5 @@ export const getUserAuth = async () => { export const checkAuth = async () => { const { session } = await getUserAuth(); - if (!session) redirect("/api/auth/signin"); + if (!session) redirect("/signin"); }; - diff --git a/src/server/api/routers/children.ts b/src/server/api/routers/children.ts index 26657c5..00dbd75 100644 --- a/src/server/api/routers/children.ts +++ b/src/server/api/routers/children.ts @@ -2,7 +2,6 @@ import { getChildById, getChildren } from "@/lib/api/children/queries"; import { publicProcedure, createTRPCRouter, - protectedProcedure, } from "@/server/api/trpc"; import { childIdSchema, diff --git a/src/server/db/index.ts b/src/server/db/index.ts index 3353ef6..d59843d 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -8,6 +8,9 @@ import { verificationTokens, children, devices, + childrenRelations, + deviceRelations, + pingRelations, } from "@/server/db/schema/_root"; export const client = postgres(env.DATABASE_URL); @@ -19,5 +22,8 @@ export const db = drizzle(client, { accounts, sessions, verificationTokens, + childrenRelations, + deviceRelations, + pingRelations, }, }); diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts index e3142de..b9afa86 100644 --- a/src/server/db/migrate.ts +++ b/src/server/db/migrate.ts @@ -1,26 +1,23 @@ import { env } from "@/env.mjs"; - + import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; - const runMigrate = async () => { if (!env.DATABASE_URL) { throw new Error("DATABASE_URL is not defined"); } - -const connection = postgres(env.DATABASE_URL, { max: 1 }); - -const db = drizzle(connection); + const connection = postgres(env.DATABASE_URL, { max: 1 }); + const db = drizzle(connection); console.log("⏳ Running migrations..."); const start = Date.now(); - await migrate(db, { migrationsFolder: 'src/server/db/migrations' }); + await migrate(db, { migrationsFolder: "src/server/db/migrations" }); const end = Date.now(); @@ -33,4 +30,4 @@ runMigrate().catch((err) => { console.error("❌ Migration failed"); console.error(err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/src/server/db/schema/_root.ts b/src/server/db/schema/_root.ts index 4a18c9e..ac7d07b 100644 --- a/src/server/db/schema/_root.ts +++ b/src/server/db/schema/_root.ts @@ -1,6 +1,16 @@ -import { users, accounts, sessions, verificationTokens } from "./auth" -import { children } from "./children"; -import { devices } from "./devices"; -import { pings } from "./pings"; - -export { pings, devices, children, users, accounts, sessions, verificationTokens } \ No newline at end of file +import { users, accounts, sessions, verificationTokens } from "./auth"; +import { children, childrenRelations } from "./children"; +import { deviceRelations, devices } from "./devices"; +import { pingRelations, pings } from "./pings"; +export { + pings, + devices, + children, + users, + accounts, + sessions, + verificationTokens, + childrenRelations, + deviceRelations, + pingRelations, +}; diff --git a/src/server/db/schema/children.ts b/src/server/db/schema/children.ts index f29fad9..f63d608 100644 --- a/src/server/db/schema/children.ts +++ b/src/server/db/schema/children.ts @@ -6,6 +6,8 @@ import { users } from "@/server/db/schema/auth"; import { type getChildren } from "@/lib/api/children/queries"; import { randomUUID } from "crypto"; +import { relations } from "drizzle-orm"; +import { devices } from "./devices"; export const children = pgTable("children", { id: varchar("id", { length: 191 }) @@ -18,6 +20,9 @@ export const children = pgTable("children", { .references(() => users.id, { onDelete: "cascade" }) .notNull(), }); +export const childrenRelations = relations(children, ({ many }) => ({ + devices: many(devices), +})); // Schema for children - used to validate API requests export const insertChildSchema = createInsertSchema(children); diff --git a/src/server/db/schema/devices.ts b/src/server/db/schema/devices.ts index 9a61ead..3b07d6f 100644 --- a/src/server/db/schema/devices.ts +++ b/src/server/db/schema/devices.ts @@ -1,38 +1,56 @@ -import { varchar, pgTable } from "drizzle-orm/pg-core"; +import { + varchar, + pgTable, + PgColumn, + PgTableWithColumns, +} from "drizzle-orm/pg-core"; import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { z } from "zod"; -import { children } from "./children" +import { children } from "./children"; import { users } from "@/server/db/schema/auth"; import { type getDevices } from "@/lib/api/devices/queries"; import { randomUUID } from "crypto"; +import { relations } from "drizzle-orm"; +import { pings } from "./pings"; - -export const devices = pgTable('devices', { - id: varchar("id", { length: 191 }).primaryKey().$defaultFn(() => randomUUID()), +export const devices = pgTable("devices", { + id: varchar("id", { length: 191 }) + .primaryKey() + .$defaultFn(() => randomUUID()), name: varchar("name", { length: 256 }).notNull(), deviceId: varchar("device_id", { length: 256 }).notNull(), - childId: varchar("child_id", { length: 256 }).references(() => children.id, { onDelete: "cascade" }).notNull(), - userId: varchar("user_id", { length: 256 }).references(() => users.id, { onDelete: "cascade" }).notNull(), + childId: varchar("child_id", { length: 256 }) + .references(() => children.id, { onDelete: "cascade" }) + .notNull(), + userId: varchar("user_id", { length: 256 }) + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), }); - +export const deviceRelations = relations(devices, ({ one, many }) => ({ + child: one(children, { + fields: [devices.childId], + references: [children.id], + }), + pings: many(pings), +})); // Schema for devices - used to validate API requests export const insertDeviceSchema = createInsertSchema(devices); export const insertDeviceParams = createSelectSchema(devices, { - childId: z.coerce.string().min(1) -}).omit({ + childId: z.coerce.string().min(1), +}).omit({ id: true, - userId: true + userId: true, }); export const updateDeviceSchema = createSelectSchema(devices); -export const updateDeviceParams = createSelectSchema(devices,{ - childId: z.coerce.string().min(1) -}).omit({ - userId: true +export const updateDeviceParams = createSelectSchema(devices, { + childId: z.coerce.string().min(1), +}).omit({ + userId: true, }); export const deviceIdSchema = updateDeviceSchema.pick({ id: true }); @@ -43,7 +61,8 @@ export type NewDevice = z.infer; export type NewDeviceParams = z.infer; export type UpdateDeviceParams = z.infer; export type DeviceId = z.infer["id"]; - -// this type infers the return from getDevices() - meaning it will include any joins -export type CompleteDevice = Awaited>["devices"][number]; +// this type infers the return from getDevices() - meaning it will include any joins +export type CompleteDevice = Awaited< + ReturnType +>["devices"][number]; diff --git a/src/server/db/schema/pings.ts b/src/server/db/schema/pings.ts index cbdfdad..acde260 100644 --- a/src/server/db/schema/pings.ts +++ b/src/server/db/schema/pings.ts @@ -1,9 +1,4 @@ -import { - real, - timestamp, - varchar, - pgTable, -} from "drizzle-orm/pg-core"; +import { real, timestamp, varchar, pgTable } from "drizzle-orm/pg-core"; import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { z } from "zod"; import { devices } from "./devices"; @@ -11,6 +6,7 @@ import { users } from "@/server/db/schema/auth"; import { type getPings } from "@/lib/api/pings/queries"; import { randomUUID } from "crypto"; +import { relations } from "drizzle-orm"; export const pings = pgTable("pings", { id: varchar("id", { length: 191 }) @@ -26,7 +22,12 @@ export const pings = pgTable("pings", { .references(() => users.id, { onDelete: "cascade" }) .notNull(), }); - +export const pingRelations = relations(pings, ({ one }) => ({ + device: one(devices, { + fields: [pings.deviceId], + references: [devices.id], + }), +})); // Schema for pings - used to validate API requests export const insertPingSchema = createInsertSchema(pings); diff --git a/src/server/db/seed/auth.ts b/src/server/db/seed/auth.ts new file mode 100644 index 0000000..7e2f816 --- /dev/null +++ b/src/server/db/seed/auth.ts @@ -0,0 +1,31 @@ +import { db } from ".."; +import { accounts } from "../schema/auth"; + +const runAuthSeed = async () => { + console.log("🔐", "Seeding auth"); + await db + .insert(accounts) + .values([ + { + userId: "2250f34e-997a-44de-ab8d-beddeda13525", + provider: "google", + type: "oauth", + providerAccountId: "112561477626832751929", + access_token: "FARTS", + expires_at: 9, + token_type: "Bearer", + scope: + "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile", + id_token: "FARTS", + }, + ]) + .execute(); + + console.log("🌱", "Seeded auth"); + process.exit(0); +}; +runAuthSeed().catch((err) => { + console.error("❌ Auth seeding failed"); + console.error(err); + process.exit(1); +}); diff --git a/src/server/db/seed/seed.ts b/src/server/db/seed/seed.ts new file mode 100644 index 0000000..77fb09b --- /dev/null +++ b/src/server/db/seed/seed.ts @@ -0,0 +1,80 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +import { db } from ".."; +import { faker } from "@faker-js/faker"; +import { users } from "../schema/auth"; +import { children } from "../schema/children"; +import { devices } from "../schema/devices"; +import { pings } from "../schema/pings"; + +console.log("seed", "Seeding"); +const runSeed = async () => { + console.log("👯", "Seeding users"); + const seedUsers = [ + { + id: "2250f34e-997a-44de-ab8d-beddeda13525", + name: "Fergal Moran", + email: "fergal.moran@gmail.com", + emailVerified: new Date(), + }, + ]; + await db.insert(users).values(seedUsers).execute(); + + const seedChildren = [ + { + id: "2250f34e-997a-44de-ab8d-beddeda13525", + name: "Lil Debuggles", + phone: "123 456 789", + email: "lildebuggles@kidarr.com", + avatar: faker.image.avatar(), + userId: seedUsers[0]?.id!, + }, + ]; + await db.insert(children).values(seedChildren).execute(); + + const seedDevices = [ + { + id: "5af79a30-df27-4646-9d9f-77e19b4191c1", + name: "Not an iPhone", + deviceId: "373791e3-afe3-49de-b0a2-842a44071585", + childId: seedChildren[0]?.id!, + userId: seedUsers[0]?.id!, + deviceName: "Not an iPhone", + apiKey: "nQhXtqemsWjzBpbDxlIV2qtDx9xxO4oZVBJADdhJLfA=", + pin: 1234, + expires: new Date(2065), + }, + ]; + await db.insert(devices).values(seedDevices).execute(); + + const seedPings = [ + { + deviceId: seedDevices[0]?.id!, + userId: seedUsers[0]?.id!, + latitude: 51.903614, + longitude: -8.468399, + timestamp: new Date(), + }, + { + deviceId: seedDevices[0]?.id!, + userId: seedUsers[0]?.id!, + latitude: 51.8985, + longitude: -8.4756, + timestamp: new Date(), + }, + { + deviceId: seedDevices[0]?.id!, + userId: seedUsers[0]?.id!, + latitude: 51.93588161110811, + longitude: -8.495129534566756, + timestamp: new Date(), + }, + ]; + await db.insert(pings).values(seedPings).execute(); + console.log("🌱", "Seeded users"); + process.exit(0); +}; +runSeed().catch((err) => { + console.error("❌ Seeding failed"); + console.error(err); + process.exit(1); +});