diff --git a/packages/validations/src/validations/tripRoutesValidator/tripRoutesValidator.ts b/packages/validations/src/validations/tripRoutesValidator/tripRoutesValidator.ts index 27afb355a..0a3d5c1de 100644 --- a/packages/validations/src/validations/tripRoutesValidator/tripRoutesValidator.ts +++ b/packages/validations/src/validations/tripRoutesValidator/tripRoutesValidator.ts @@ -5,14 +5,13 @@ const tripActivityValues = Object.values(TripActivity) as [string, ...string[]]; export const addTripForm = z.object({ name: z.string(), - description: z.string(), - activity: z.enum(tripActivityValues), - is_public: z.union([z.literal('0'), z.literal('1')]), + description: z.string().optional().nullable(), + activity: z.enum(tripActivityValues).optional(), + is_public: z.union([z.literal('0'), z.literal('1')]).optional(), }); -// @ts-ignore const coordinateSchema = z.lazy(() => - z.union([z.number(), z.array(coordinateSchema)]), + z.union([z.number(), z.array(z.number())]), ); const baseGeometrySchema = z.object({ @@ -48,33 +47,35 @@ export const getTripById = z.object({ }); export const addTripDetails = z.object({ - start_date: z.string(), + activity: z.enum(tripActivityValues).optional(), + bounds: z.tuple([z.array(z.number()), z.array(z.number())]).optional(), end_date: z.string(), - destination: z.string(), - activity: z.enum(tripActivityValues), - parks: z.string().optional(), - trails: z.string().optional(), geoJSON: z.string(), - owner_id: z.string(), pack_id: z.string(), - bounds: z.tuple([z.array(z.number()), z.array(z.number())]), + parks: z.string().optional(), + start_date: z.string(), + trails: z.string().optional(), }); export const addTrip = addTripDetails.merge(addTripForm); +export type AddTripType = z.infer; -export const editTrip = z.object({ - id: z.string(), - name: z.string().optional(), - description: z.string().optional(), - activity: z.enum(tripActivityValues).optional(), - start_date: z.string().optional(), - end_date: z.string().optional(), - destination: z.string().optional(), +export const editTrip = addTrip.merge( + z.object({ + id: z.string().min(1), + }), +); + +export type EditTripType = z.infer; + +export const setTripVisibility = z.object({ + tripId: z.string().min(1), is_public: z.union([z.literal('0'), z.literal('1')]), }); +export type SetTripVisibilityType = z.infer; export const deleteTrip = z.object({ - tripId: z.string().nonempty(), + tripId: z.string().min(1), }); export const queryTrip = z.object({ diff --git a/server/src/controllers/trip/addTrip.ts b/server/src/controllers/trip/addTrip.ts index feba2d7db..d0d3d2135 100644 --- a/server/src/controllers/trip/addTrip.ts +++ b/server/src/controllers/trip/addTrip.ts @@ -1,11 +1,13 @@ -import { publicProcedure, protectedProcedure } from '../../trpc'; -import { addTripService } from '../../services/trip/addTripService'; +import { protectedProcedure } from '../../trpc'; +import { Context } from 'hono'; +import { addTripService } from '../../services/trip/trip.service'; import * as validator from '@packrat/validations'; -export const addTrip = async (c) => { +export const addTrip = async (c: Context) => { try { - const tripData = await c.req.json(); - const trip = await addTripService(tripData); + const requestData = (await c.req.json()) satisfies validator.AddTripType; + const tripData = { ...requestData, ownerId: c.user.id }; + const trip = await addTripService(tripData, c.executionCtx); return c.json(trip, 200); } catch (error) { return c.json({ error: `${error.message}` }, 500); @@ -14,7 +16,7 @@ export const addTrip = async (c) => { export function addTripRoute() { return protectedProcedure.input(validator.addTrip).mutation(async (opts) => { - const tripData = opts.input; + const tripData = { ownerId: opts.ctx.user.id, ...opts.input }; return await addTripService(tripData, opts.ctx.executionCtx); }); } diff --git a/server/src/controllers/trip/editTrip.ts b/server/src/controllers/trip/editTrip.ts index a509c8391..e84403a2c 100644 --- a/server/src/controllers/trip/editTrip.ts +++ b/server/src/controllers/trip/editTrip.ts @@ -1,12 +1,12 @@ import { protectedProcedure } from '../../trpc'; import * as validator from '@packrat/validations'; -import { Trip } from '../../drizzle/methods/trip'; +import { editTripService } from '../../services/trip/trip.service'; +import { Context } from 'hono'; -export const editTrip = async (c) => { +export const editTrip = async (c: Context) => { try { const tripData = await c.req.json(); - const tripClass = new Trip(); - const trip = await tripClass.update(tripData); + const trip = await editTripService(tripData, c.executionCtx); return c.json({ trip }, 200); } catch (error) { return c.json({ error: `${error.message}` }, 500); @@ -16,8 +16,7 @@ export const editTrip = async (c) => { export function editTripRoute() { return protectedProcedure.input(validator.editTrip).mutation(async (opts) => { const tripData = { ...opts.input }; - const tripClass = new Trip(); - const trip = await tripClass.update(tripData); + const trip = await editTripService(tripData, opts.ctx.executionCtx); return trip; }); } diff --git a/server/src/controllers/trip/index.ts b/server/src/controllers/trip/index.ts index 85037928d..ff3db090a 100644 --- a/server/src/controllers/trip/index.ts +++ b/server/src/controllers/trip/index.ts @@ -4,3 +4,4 @@ export * from './editTrip'; export * from './getPublicTrips'; export * from './getTrip'; export * from './getTripById'; +export * from './setTripVisibility'; diff --git a/server/src/controllers/trip/setTripVisibility.ts b/server/src/controllers/trip/setTripVisibility.ts new file mode 100644 index 000000000..c3f8ed013 --- /dev/null +++ b/server/src/controllers/trip/setTripVisibility.ts @@ -0,0 +1,12 @@ +import { protectedProcedure } from '../../trpc'; +import * as validator from '@packrat/validations'; +import { setTripVisibilityService } from '../../services/trip/trip.service'; + +export function setTripVisibilityRoute() { + return protectedProcedure + .input(validator.setTripVisibility) + .mutation(async (opts) => { + const trip = await setTripVisibilityService(opts.input); + return trip; + }); +} diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index db8e3a331..9c88b793f 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -404,7 +404,7 @@ export const trip = sqliteTable('trip', { .primaryKey() .$defaultFn(() => createId()), name: text('name').notNull(), - description: text('description').notNull(), + description: text('description'), parks: text('parks', { mode: 'json' }).$type< Array<{ id: string; name: string }> >(), @@ -413,7 +413,6 @@ export const trip = sqliteTable('trip', { >(), start_date: text('start_date').notNull(), end_date: text('end_date').notNull(), - destination: text('destination').notNull(), owner_id: text('owner_id').references(() => user.id, { onDelete: 'cascade', }), @@ -463,7 +462,6 @@ export const tripRelations = relations(trip, ({ one, many }) => ({ fields: [trip.pack_id], references: [pack.id], }), - // geojsons: many(tripGeojsons), tripGeojsons: many(tripGeojsons), })); diff --git a/server/src/drizzle/methods/Geojson.ts b/server/src/drizzle/methods/Geojson.ts index def55775c..ea7e2b4d6 100644 --- a/server/src/drizzle/methods/Geojson.ts +++ b/server/src/drizzle/methods/Geojson.ts @@ -1,3 +1,4 @@ +import { eq } from 'drizzle-orm'; import { DbClient } from '../../db/client'; import { type InsertGeoJson, geojson } from '../../db/schema'; @@ -5,14 +6,29 @@ export class GeoJson { async create(geoJSON: InsertGeoJson) { try { const db = DbClient.instance; - const record = await db - .insert(geojson) - .values({ geoJSON }) - .returning() - .get(); + const record = await db.insert(geojson).values(geoJSON).returning().get(); return record; } catch (error) { throw new Error(`Failed to create geojson record: ${error.message}`); } } + + async update( + id: string, + data: Partial, + filter = eq(geojson.id, id), + ) { + try { + const geojsonValue = await DbClient.instance + .update(geojson) + .set(data) + .where(filter) + .returning() + .get(); + + return geojsonValue; + } catch (error) { + throw new Error(`Failed to geojson record: ${error.message}`); + } + } } diff --git a/server/src/drizzle/methods/trip.ts b/server/src/drizzle/methods/trip.ts index e032bf1c6..8e242e100 100644 --- a/server/src/drizzle/methods/trip.ts +++ b/server/src/drizzle/methods/trip.ts @@ -1,9 +1,13 @@ import { eq } from 'drizzle-orm'; import { DbClient } from '../../db/client'; -import { type InsertTrip, trip as TripTable } from '../../db/schema'; +import { + type InsertTrip, + type Trip as SelectTrip, + trip as TripTable, +} from '../../db/schema'; export class Trip { - async update(trip: Partial) { + async update(trip: InsertTrip) { try { if (!trip.id) { throw new Error('Trip id is required for update operation'); @@ -48,7 +52,7 @@ export class Trip { async findById(id: string) { try { - const trip = await DbClient.instance.query.trip.findFirst({ + return DbClient.instance.query.trip.findFirst({ where: eq(TripTable.id, id), with: { owner: { @@ -59,27 +63,27 @@ export class Trip { }, packs: { columns: { id: true, name: true }, - with: { - itemPacks: { - columns: { packId: true }, - with: { - item: { - columns: { - id: true, - name: true, - weight: true, - quantity: true, - unit: true, - }, - with: { - category: { - columns: { id: true, name: true }, - }, - }, - }, - }, - }, - }, + // with: { + // itemPacks: { + // columns: { packId: true }, + // with: { + // item: { + // columns: { + // id: true, + // name: true, + // weight: true, + // quantity: true, + // unit: true, + // }, + // with: { + // category: { + // columns: { id: true, name: true }, + // }, + // }, + // }, + // }, + // }, + // }, }, tripGeojsons: { columns: {}, @@ -89,7 +93,6 @@ export class Trip { }, }, }); - return trip; } catch (error) { throw new Error(`Failed to find trip by id: ${error.message}`); } diff --git a/server/src/routes/trpcRouter.ts b/server/src/routes/trpcRouter.ts index b7236cc71..e6b16f804 100644 --- a/server/src/routes/trpcRouter.ts +++ b/server/src/routes/trpcRouter.ts @@ -29,6 +29,7 @@ import { getPublicTripsRoute, getTripByIdRoute, getTripsRoute, + setTripVisibilityRoute, } from '../controllers/trip'; import { addTemplateRoute, @@ -113,6 +114,7 @@ import { import { router as trpcRouter } from '../trpc'; import { getOfflineMapsRoute, saveOfflineMapRoute } from '../modules/map'; +import { setTripVisibility } from '@packrat/validations'; export const appRouter = trpcRouter({ getUserById: getUserByIdRoute(), @@ -146,6 +148,7 @@ export const appRouter = trpcRouter({ getTripById: getTripByIdRoute(), addTrip: addTripRoute(), editTrip: editTripRoute(), + setTripVisibility: setTripVisibilityRoute(), deleteTrip: deleteTripRoute(), // templates routes getPackTemplates: getPackTemplatesRoute(), diff --git a/server/src/services/trip/addTripService.ts b/server/src/services/trip/addTripService.ts index cece53d6d..286368252 100644 --- a/server/src/services/trip/addTripService.ts +++ b/server/src/services/trip/addTripService.ts @@ -1,39 +1,54 @@ -import { calculateTripScore } from 'src/utils/scoreTrip'; +import * as validator from '@packrat/validations'; import { GeoJson } from '../../drizzle/methods/Geojson'; import { TripGeoJson } from '../../drizzle/methods/TripGeoJson'; import { Trip } from '../../drizzle/methods/trip'; -import { validateGeojsonId, validateGeojsonType } from '../../utils/geojson'; import { GeojsonStorageService } from '../geojsonStorage'; import { scoreTripService } from './scoreTripService'; export const addTripService = async ( - tripData: any, + tripData: validator.AddTripType & { ownerId: string }, executionCtx: ExecutionContext, ) => { try { - const { geoJSON, ...otherTripData } = tripData; + const geoJSON = tripData.geoJSON; const tripClass = new Trip(); + // Create Trip const newTrip = await tripClass.create({ - ...otherTripData, - trails: otherTripData.trails ? JSON.parse(otherTripData.trails) : null, - parks: otherTripData.parks ? JSON.parse(otherTripData.parks) : null, + name: tripData.name, + description: tripData.description, + start_date: tripData.start_date, + end_date: tripData.end_date, + activity: tripData.activity || 'trip', + owner_id: tripData.ownerId, + pack_id: tripData.pack_id, + is_public: tripData.is_public === '0', + trails: tripData.trails ? JSON.parse(tripData.trails) : null, + parks: tripData.parks ? JSON.parse(tripData.parks) : null, + ...(tripData.bounds && { + bounds: [tripData.bounds[0], tripData.bounds[1]], + }), }); + await scoreTripService(newTrip.id); + const geojsonClass = new GeoJson(); const tripGeoJsonClass = new TripGeoJson(); if (!geoJSON) { throw new Error("Geojson data doesn't exist"); } - // const insertedGeoJson = await geojsonClass.create(geoJSON); - // await tripGeoJsonClass.create({ - // tripId: newTrip.id, - // geojsonId: insertedGeoJson.id, - // }); + const insertedGeoJson = await geojsonClass.create({ + geoJSON, + }); + + await tripGeoJsonClass.create({ + tripId: newTrip.id, + geojsonId: insertedGeoJson.id, + }); executionCtx.waitUntil( - GeojsonStorageService.save('trip', JSON.stringify(geoJSON), newTrip.id), + GeojsonStorageService.save('trip', geoJSON, newTrip.id), ); return newTrip; diff --git a/server/src/services/trip/editTripService.ts b/server/src/services/trip/editTripService.ts new file mode 100644 index 000000000..f7083bb43 --- /dev/null +++ b/server/src/services/trip/editTripService.ts @@ -0,0 +1,83 @@ +import * as validator from '@packrat/validations'; +import { GeoJson } from '../../drizzle/methods/Geojson'; +import { TripGeoJson } from '../../drizzle/methods/TripGeoJson'; +import { Trip } from '../../drizzle/methods/trip'; +import { GeojsonStorageService } from '../geojsonStorage'; +import { scoreTripService } from './scoreTripService'; + +export const editTripService = async ( + tripData: validator.EditTripType, + executionCtx: ExecutionContext, +) => { + try { + const tripClass = new Trip(); + const selectedTrip = await tripClass.findById(tripData.id); + const updatedTrip = await tripClass.update({ + name: tripData.name || selectedTrip.name, + description: tripData.description || selectedTrip.description, + start_date: tripData.start_date || selectedTrip.start_date, + end_date: tripData.end_date || selectedTrip.end_date, + activity: tripData.activity || selectedTrip.activity, + pack_id: tripData.pack_id || selectedTrip.pack_id, + trails: tripData.trails + ? JSON.parse(tripData.trails) + : selectedTrip.trails, + parks: tripData.parks ? JSON.parse(tripData.parks) : selectedTrip.parks, + ...(tripData.bounds && { + bounds: [tripData.bounds[0], tripData.bounds[1]], + }), + }); + + await scoreTripService(selectedTrip.id); + + const serializedGeoJSON = tripData.geoJSON; + if (!serializedGeoJSON) { + return updatedTrip; + } + + const geojsonClass = new GeoJson(); + + const tripGeoJSONs = selectedTrip.tripGeojsons; + if (tripGeoJSONs.length > 0) { + await geojsonClass.update(tripGeoJSONs[0].geojson.id, { + geoJSON: serializedGeoJSON, + }); + } else { + const insertedGeoJson = await geojsonClass.create({ + geoJSON: serializedGeoJSON, + }); + + const tripGeoJsonClass = new TripGeoJson(); + await tripGeoJsonClass.create({ + tripId: selectedTrip.id, + geojsonId: insertedGeoJson.id, + }); + } + + executionCtx.waitUntil( + GeojsonStorageService.save('trip', serializedGeoJSON, selectedTrip.id), + ); + + return updatedTrip; + } catch (error) { + console.error(error); + throw new Error('Unable to add trip and GeoJSON data'); + } +}; + +export const setTripVisibilityService = async ( + tripData: validator.SetTripVisibilityType, +) => { + try { + const tripClass = new Trip(); + const selectedTrip = await tripClass.findById(tripData.id); + const updatedTrip = await tripClass.update({ + id: selectedTrip.id, + is_public: tripData.is_public === '1', + }); + return updatedTrip; + } catch (error) { + console.error(error); + throw new Error('Unable to add trip and GeoJSON data'); + } +}; diff --git a/server/src/services/trip/trip.service.ts b/server/src/services/trip/trip.service.ts index 270d8768f..d40c87337 100644 --- a/server/src/services/trip/trip.service.ts +++ b/server/src/services/trip/trip.service.ts @@ -1,4 +1,5 @@ export * from './addTripService'; +export * from './editTripService'; export * from './getPublicTripService'; export * from './getTripByIdService'; export * from './getTripsService';