diff --git a/src/lib/api/children/queries.ts b/src/lib/api/children/queries.ts index 66848fb..270adc3 100644 --- a/src/lib/api/children/queries.ts +++ b/src/lib/api/children/queries.ts @@ -7,6 +7,7 @@ import { 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(); @@ -14,6 +15,8 @@ export const getChildren = async () => { .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: [] })) }; }; diff --git a/src/lib/api/pings/mutations.ts b/src/lib/api/pings/mutations.ts new file mode 100644 index 0000000..a1bb16c --- /dev/null +++ b/src/lib/api/pings/mutations.ts @@ -0,0 +1,58 @@ +import { db } from "@/server/db/index"; +import { and, eq } from "drizzle-orm"; +import { + PingId, + NewPingParams, + UpdatePingParams, + updatePingSchema, + insertPingSchema, + pings, + pingIdSchema +} from "@/server/db/schema/pings"; +import { getUserAuth } from "@/lib/auth/utils"; + +export const createPing = async (ping: NewPingParams) => { + const { session } = await getUserAuth(); + const newPing = insertPingSchema.parse({ ...ping, userId: session?.user.id! }); + try { + const [p] = await db.insert(pings).values(newPing).returning(); + return { ping: p }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + +export const updatePing = async (id: PingId, ping: UpdatePingParams) => { + const { session } = await getUserAuth(); + const { id: pingId } = pingIdSchema.parse({ id }); + const newPing = updatePingSchema.parse({ ...ping, userId: session?.user.id! }); + try { + const [p] = await db + .update(pings) + .set(newPing) + .where(and(eq(pings.id, pingId!), eq(pings.userId, session?.user.id!))) + .returning(); + return { ping: p }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + +export const deletePing = async (id: PingId) => { + const { session } = await getUserAuth(); + const { id: pingId } = pingIdSchema.parse({ id }); + try { + const [p] = await db.delete(pings).where(and(eq(pings.id, pingId!), eq(pings.userId, session?.user.id!))) + .returning(); + return { ping: p }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + diff --git a/src/lib/api/pings/queries.ts b/src/lib/api/pings/queries.ts new file mode 100644 index 0000000..6263f33 --- /dev/null +++ b/src/lib/api/pings/queries.ts @@ -0,0 +1,19 @@ +import { db } from "@/server/db/index"; +import { eq, and } from "drizzle-orm"; +import { getUserAuth } from "@/lib/auth/utils"; +import { type PingId, pingIdSchema, pings } from "@/server/db/schema/pings"; +import { devices } from "@/server/db/schema/devices"; + +export const getPings = async () => { + const { session } = await getUserAuth(); + const p = await db.select({ ping: pings, device: devices }).from(pings).leftJoin(devices, eq(pings.deviceId, devices.id)).where(eq(pings.userId, session?.user.id!)); + return { pings: p }; +}; + +export const getPingById = async (id: PingId) => { + const { session } = await getUserAuth(); + const { id: pingId } = pingIdSchema.parse({ id }); + const [p] = await db.select().from(pings).where(and(eq(pings.id, pingId), eq(pings.userId, session?.user.id!))).leftJoin(devices, eq(pings.deviceId, devices.id)); + return { ping: p }; +}; + diff --git a/src/server/db/migrations/0002_abandoned_luckman.sql b/src/server/db/migrations/0002_abandoned_luckman.sql new file mode 100644 index 0000000..cdf9553 --- /dev/null +++ b/src/server/db/migrations/0002_abandoned_luckman.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS "pings" ( + "id" varchar(191) PRIMARY KEY NOT NULL, + "latitude" real NOT NULL, + "longitude" real NOT NULL, + "timestamp" timestamp NOT NULL, + "device_id" varchar(256) NOT NULL, + "user_id" varchar(256) NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "pings" ADD CONSTRAINT "pings_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "devices"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "pings" ADD CONSTRAINT "pings_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/server/db/migrations/meta/0002_snapshot.json b/src/server/db/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..d972530 --- /dev/null +++ b/src/server/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,425 @@ +{ + "id": "34d0a88b-b9f0-4d72-a427-5f9a72104706", + "prevId": "3520a784-4841-418f-8cf4-7e58249c843f", + "version": "5", + "dialect": "pg", + "tables": { + "account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {} + }, + "children": { + "name": "children", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "children_user_id_user_id_fk": { + "name": "children_user_id_user_id_fk", + "tableFrom": "children", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "devices": { + "name": "devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "child_id": { + "name": "child_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "devices_child_id_children_id_fk": { + "name": "devices_child_id_children_id_fk", + "tableFrom": "devices", + "tableTo": "children", + "columnsFrom": [ + "child_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "devices_user_id_user_id_fk": { + "name": "devices_user_id_user_id_fk", + "tableFrom": "devices", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "pings": { + "name": "pings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "pings_device_id_devices_id_fk": { + "name": "pings_device_id_devices_id_fk", + "tableFrom": "pings", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pings_user_id_user_id_fk": { + "name": "pings_user_id_user_id_fk", + "tableFrom": "pings", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json index f46c363..3207b0e 100644 --- a/src/server/db/migrations/meta/_journal.json +++ b/src/server/db/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1705877897654, "tag": "0001_sticky_frightful_four", "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1706464847221, + "tag": "0002_abandoned_luckman", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/server/db/schema/_root.ts b/src/server/db/schema/_root.ts index 39f5ff0..4a18c9e 100644 --- a/src/server/db/schema/_root.ts +++ b/src/server/db/schema/_root.ts @@ -1,5 +1,6 @@ import { users, accounts, sessions, verificationTokens } from "./auth" import { children } from "./children"; import { devices } from "./devices"; +import { pings } from "./pings"; -export { devices, children, users, accounts, sessions, verificationTokens } \ No newline at end of file +export { pings, devices, children, users, accounts, sessions, verificationTokens } \ No newline at end of file diff --git a/src/server/db/schema/pings.ts b/src/server/db/schema/pings.ts new file mode 100644 index 0000000..cbdfdad --- /dev/null +++ b/src/server/db/schema/pings.ts @@ -0,0 +1,66 @@ +import { + real, + timestamp, + varchar, + pgTable, +} from "drizzle-orm/pg-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; +import { devices } from "./devices"; +import { users } from "@/server/db/schema/auth"; +import { type getPings } from "@/lib/api/pings/queries"; + +import { randomUUID } from "crypto"; + +export const pings = pgTable("pings", { + id: varchar("id", { length: 191 }) + .primaryKey() + .$defaultFn(() => randomUUID()), + latitude: real("latitude").notNull(), + longitude: real("longitude").notNull(), + timestamp: timestamp("timestamp").notNull(), + deviceId: varchar("device_id", { length: 256 }) + .references(() => devices.id, { onDelete: "cascade" }) + .notNull(), + userId: varchar("user_id", { length: 256 }) + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), +}); + +// Schema for pings - used to validate API requests +export const insertPingSchema = createInsertSchema(pings); + +export const insertPingParams = createSelectSchema(pings, { + latitude: z.coerce.number(), + longitude: z.coerce.number(), + timestamp: z.coerce.string().min(1), + deviceId: z.coerce.string().min(1), +}).omit({ + id: true, + userId: true, +}); + +export const updatePingSchema = createSelectSchema(pings); + +export const updatePingParams = createSelectSchema(pings, { + latitude: z.coerce.number(), + longitude: z.coerce.number(), + timestamp: z.coerce.string().min(1), + deviceId: z.coerce.string().min(1), +}).omit({ + userId: true, +}); + +export const pingIdSchema = updatePingSchema.pick({ id: true }); + +// Types for pings - used to type API request params and within Components +export type Ping = z.infer; +export type NewPing = z.infer; +export type NewPingParams = z.infer; +export type UpdatePingParams = z.infer; +export type PingId = z.infer["id"]; + +// this type infers the return from getPings() - meaning it will include any joins +export type CompletePing = Awaited< + ReturnType +>["pings"][number];