adds zod API validation

This commit is contained in:
Christian Beutel
2024-12-27 01:56:35 +01:00
parent 1f81779a64
commit 75c94a3ce4
80 changed files with 847 additions and 559 deletions

View File

@@ -203,7 +203,9 @@
<a class="btn-primary btn-large" href="/trail/edit/new"
><i class="fa fa-plus mr-2"></i>{$_("new-trail")}</a
>
<NotificationDropdown></NotificationDropdown>
{#if $page.data.notifications}
<NotificationDropdown></NotificationDropdown>
{/if}
<Dropdown
items={dropdownItems}
on:change={(e) => handleDropdownClick(e.detail)}

View File

@@ -32,13 +32,13 @@
onMount(async () => {});
async function openMap(log: SummitLog) {
if (!log.expand.gpx_data) {
if (!log.expand?.gpx_data) {
return;
}
trail = (await gpx2trail(log.expand.gpx_data)).trail;
trail.id = log.id;
trail.expand.gpx_data = log.expand.gpx_data;
trail.expand!.gpx_data = log.expand.gpx_data;
openMapModal();
await tick();
@@ -75,7 +75,7 @@
{#if summitLogs.some((l) => l.text?.length)}
<th>{$_("description")}</th>
{/if}
{#if showAuthor && summitLogs.some(l => l.expand.author)}
{#if showAuthor && summitLogs.some(l => l.expand?.author)}
<th>
{$_("author", { values: { n: 1 } })}
</th>

View File

@@ -14,7 +14,7 @@
export let fullWidth: boolean = false;
$: thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail])
? getFileURL(trail, trail.photos[trail.thumbnail ?? 0])
: "/imgs/default_thumbnail.webp";
$: trailIsShared = (trail.expand?.trail_share_via_trail?.length ?? 0) > 0;
@@ -66,7 +66,7 @@
})}
</p>
{/if}
{#if trail.expand.author}
{#if trail.expand?.author}
<p class="text-xs text-gray-500 mb-3">
{$_("by")}
<img

View File

@@ -34,7 +34,7 @@
const allowEdit =
trail.author == $currentUser?.id ||
trail.expand.trail_share_via_trail?.some((s) => s.permission == "edit");
trail.expand?.trail_share_via_trail?.some((s) => s.permission == "edit");
const dropdownItems: DropdownItem[] = [
mode == "overview"
@@ -142,7 +142,7 @@
}
if (exportSettings.summitLog) {
let summitLogString = "";
for (const summitLog of trail.expand.summit_logs) {
for (const summitLog of trail.expand?.summit_logs ?? []) {
summitLogString += `${summitLog.date},${summitLog.text}\n`;
}
zip.file(

View File

@@ -51,7 +51,7 @@
(trail.expand?.trail_share_via_trail?.length ?? 0) > 0;
let thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail])
? getFileURL(trail, trail.photos[trail.thumbnail ?? 0])
: "/imgs/default_thumbnail.webp";
let openGallery: (idx?: number) => void;
@@ -99,7 +99,7 @@
comments_create(newComment).then((c) => {
newComment.text = "";
c.expand = {
author: $currentUser!,
author: { ...$currentUser!, private: false },
};
const newCommentList = [c, ...$comments];
@@ -182,7 +182,7 @@
)}
</h5>
{/if}
{#if trail.expand.author}
{#if trail.expand?.author}
<p class="my-3">
{$_("by")}
<img
@@ -218,7 +218,7 @@
</h3>
</div>
</div>
{#if ($currentUser && $currentUser.id == trail.author) || trail.expand.trail_share_via_trail?.length || trail.public}
{#if ($currentUser && $currentUser.id == trail.author) || trail.expand?.trail_share_via_trail?.length || trail.public}
<TrailDropdown {trail} {mode}></TrailDropdown>
{/if}
</div>
@@ -251,7 +251,7 @@
>{formatElevation(trail.elevation_loss)}</span
>
</div>
{#if trail.expand.category}
{#if trail.expand?.category}
<div class="flex flex-col items-center">
<span>{$_("category")}</span>
<span class="font-semibold text-lg"
@@ -286,7 +286,7 @@
{/if}
{#if activeTab == 1}
<ul>
{#each trail.expand.waypoints ?? [] as waypoint, i}
{#each trail.expand?.waypoints ?? [] as waypoint, i}
<li
on:mouseenter={() => openMarkerPopup(i)}
on:mouseleave={() => closeMarkerPopup(i)}
@@ -322,7 +322,7 @@
{#if activeTab == 3}
<div class="overflow-x-auto">
<SummitLogTable
summitLogs={trail.expand.summit_logs}
summitLogs={trail.expand?.summit_logs}
showAuthor
showRoute
showPhotos

View File

@@ -15,7 +15,7 @@
export let showDescription: boolean = true;
$: thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail])
? getFileURL(trail, trail.photos[trail.thumbnail ?? 0])
: "/imgs/default_thumbnail.webp";
</script>
@@ -49,7 +49,7 @@
})}
</p>
{/if}
{#if trail.expand.author}
{#if trail.expand?.author}
<p class="text-xs text-gray-500 mb-3">
{$_("by")}
<img

View File

@@ -0,0 +1,23 @@
import { z } from "zod";
const RecordOptionsSchema = z.object({
expand: z.string().optional(),
requestKey: z.string().optional()
})
const RecordListOptionsSchema = RecordOptionsSchema.extend({
page: z.number({ coerce: true }).int().positive().optional(),
perPage: z.number({ coerce: true }).int().optional(),
sort: z.string().optional(),
filter: z.string().optional(),
expand: z.string().optional(),
requestKey: z.string().optional()
})
const RecordIdSchema = z.object({
id: z.string().length(15)
})
export { RecordOptionsSchema, RecordListOptionsSchema, RecordIdSchema }

View File

@@ -0,0 +1,17 @@
import { z, ZodType } from "zod";
import type { Comment } from "../comment";
const CommentCreateSchema = z.object({
text: z.string(),
rating: z.number(),
author: z.string().length(15),
trail: z.string().length(15),
}) satisfies ZodType<Comment>
const CommentUpdateSchema = z.object({
text: z.string().optional(),
rating: z.number().optional(),
}) satisfies ZodType<Partial<Comment>>
export { CommentCreateSchema, CommentUpdateSchema }

View File

@@ -0,0 +1,11 @@
import { z, ZodType } from "zod";
import type { Follow } from "../follow";
const FollowCreateSchema = z.object({
follower: z.string().length(15),
followee: z.string().length(15),
}) satisfies ZodType<Follow>
export { FollowCreateSchema };

View File

@@ -0,0 +1,23 @@
import { z, ZodType } from "zod";
import type { List } from "../list";
const ListCreateSchema = z.object({
name: z.string(),
public: z.boolean(),
description: z.string().optional(),
trails: z.array(z.string().length(15)),
avatar: z.string().optional(),
author: z.string()
}) satisfies ZodType<List>
const ListUpdateSchema = z.object({
name: z.string().optional(),
public: z.boolean().optional(),
description: z.string().optional(),
trails: z.array(z.string().length(15)).optional(),
"trails-": z.string().optional(),
"trails+": z.string().optional(),
avatar: z.string().optional(),
}) satisfies ZodType<Partial<List>>
export { ListCreateSchema, ListUpdateSchema };

View File

@@ -0,0 +1,16 @@
import { z, ZodType } from "zod";
import type { ListShare } from "../list_share";
const ListShareCreateSchema = z.object({
user: z.string().length(15),
list: z.string().length(15),
permission: z.enum(["view", "edit"])
}) satisfies ZodType<ListShare>
const ListShareUpdateSchema = z.object({
permission: z.enum(["view", "edit"])
}) satisfies ZodType<Partial<ListShare>>
export { ListShareCreateSchema, ListShareUpdateSchema };

View File

@@ -0,0 +1,8 @@
import { z, type ZodType } from "zod"
import { type Notification } from "../notification";
const NotificationUpdateSchema = z.object({
seen: z.literal(true)
}) satisfies ZodType<Partial<Notification>>
export { NotificationUpdateSchema }

View File

@@ -0,0 +1,28 @@
import { z, ZodType } from "zod";
import { NotificationType } from "../notification";
import type { Settings } from "../settings";
const SettingsCreateSchema = z.object({
unit: z.enum(["metric", "imperial"]).optional(),
language: z.enum(["en", "de", "fr", "hu", "it", "nl", "pl", "pt", "zh"]).optional(),
mapFocus: z.enum(["trails", "location"]).optional(),
location: z.object({
name: z.string(),
lat: z.number(),
lon: z.number()
}).optional(),
category: z.string().optional(),
tilesets: z.array(z.object({ name: z.string(), url: z.string().url() })).optional(),
terrain: z.object({ terrain: z.string().url(), hillshading: z.string().url() }).optional(),
user: z.string().optional(),
privacy: z.object({
account: z.enum(["public", "private"]),
trails: z.enum(["public", "private"]),
lists: z.enum(["public", "private"])
}).optional(),
notifications: z.record(z.enum(Object.values(NotificationType) as [string, ...string[]]), z.object({ web: z.boolean(), email: z.boolean() })).optional()
}) satisfies ZodType<Settings>
ZodType<Partial<Comment>>
export { SettingsCreateSchema };

View File

@@ -0,0 +1,31 @@
import { z, ZodType } from "zod";
import type { SummitLog } from "../summit_log";
const SummitLogCreateSchema = z.object({
id: z.string().length(15).optional(),
date: z.string().date(),
text: z.string().optional(),
gpx: z.string().optional(),
distance: z.number().nonnegative().optional(),
elevation_gain: z.number().nonnegative().optional(),
elevation_loss: z.number().nonnegative().optional(),
duration: z.number().nonnegative().optional(),
author: z.string().length(15),
photos: z.array(z.string())
}) satisfies ZodType<SummitLog>
const SummitLogUpdateSchema = z.object({
date: z.string().date().optional(),
text: z.string().optional(),
gpx: z.string().optional(),
distance: z.number().nonnegative().optional(),
elevation_gain: z.number().nonnegative().optional(),
elevation_loss: z.number().nonnegative().optional(),
duration: z.number().nonnegative().optional(),
photos: z.array(z.string()).optional(),
"photos-": z.string().optional(),
"photos+": z.string().optional(),
}) satisfies ZodType<Partial<SummitLog>>
export { SummitLogCreateSchema, SummitLogUpdateSchema };

View File

@@ -0,0 +1,49 @@
import { z, ZodType } from "zod";
import type { Trail } from "../trail";
const TrailCreateSchema = z.object({
name: z.string(),
description: z.string().optional(),
location: z.string().optional(),
date: z.string().date(),
public: z.boolean(),
difficulty: z.enum(["easy", "moderate", "difficult"]).optional(),
lat: z.number().optional(),
lon: z.number().optional(),
distance: z.number({coerce: true}).nonnegative().optional(),
elevation_gain: z.number({coerce: true}).nonnegative().optional(),
elevation_loss: z.number({coerce: true}).nonnegative().optional(),
duration: z.number({coerce: true}).nonnegative().optional(),
photos: z.array(z.string()),
thumbnail: z.number().int().nonnegative().optional(),
waypoints: z.array(z.string()),
summit_logs: z.array(z.string()),
category: z.string().optional(),
gpx: z.string().optional(),
author: z.string().length(15),
}) satisfies ZodType<Trail>
const TrailUpdateSchema = z.object({
name: z.string(),
description: z.string().optional(),
location: z.string().optional(),
date: z.string().date(),
public: z.boolean().optional(),
difficulty: z.enum(["easy", "moderate", "difficult"]).optional(),
lat: z.number().optional(),
lon: z.number().optional(),
distance: z.number({coerce: true}).nonnegative().optional(),
elevation_gain: z.number({coerce: true}).nonnegative().optional(),
elevation_loss: z.number({coerce: true}).nonnegative().optional(),
duration: z.number({coerce: true}).nonnegative().optional(),
photos: z.array(z.string()).optional(),
thumbnail: z.number().int().nonnegative().optional(),
waypoints: z.array(z.string()).optional(),
summit_logs: z.array(z.string()).optional(),
category: z.string().optional(),
gpx: z.string().optional(),
}) satisfies ZodType<Partial<Trail>>
export { TrailCreateSchema, TrailUpdateSchema };

View File

@@ -0,0 +1,16 @@
import { z, ZodType } from "zod";
import type { TrailShare } from "../trail_share";
const TrailShareCreateSchema = z.object({
user: z.string().length(15),
trail: z.string().length(15),
permission: z.enum(["view", "edit"])
}) satisfies ZodType<TrailShare>
const TrailShareUpdateSchema = z.object({
permission: z.enum(["view", "edit"])
}) satisfies ZodType<Partial<TrailShare>>
export { TrailShareCreateSchema, TrailShareUpdateSchema };

View File

@@ -0,0 +1,29 @@
import { z, ZodType } from "zod";
import type { User } from "../user";
const UserCreateSchema = (z.object({
username: z.string({ message: "required" }).min(3, "must-be-at-least-n-characters-long").regex(/^[\w][\w\.]*$/, "invalid-username"),
email: z.string({ message: "required" }).email("not-a-valid-email-address"),
password: z.string().min(8, "must-be-at-least-n-characters-long"),
passwordConfirm: z.string().optional()
}) satisfies ZodType<User>).refine((data) => data.password === data.passwordConfirm, {
message: "passwords-must-match",
path: ["passwordConfirm"],
})
const UserUpdateSchema = (z.object({
username: z.string({ message: "required" }).min(3, "must-be-at-least-n-characters-long").regex(/^[\w][\w\.]*$/, "invalid-username").optional(),
email: z.string({ message: "required" }).email("not-a-valid-email-address").optional(),
password: z.string().min(8, "must-be-at-least-n-characters-long").optional(),
oldPassword: z.string().min(8, "must-be-at-least-n-characters-long").optional(),
passwordConfirm: z.string().min(8, "must-be-at-least-n-characters-long").optional(),
bio: z.string().optional()
}) satisfies ZodType<Partial<User>>).refine((data) => data.password === data.passwordConfirm, {
message: "passwords-must-match",
path: ["passwordConfirm"],
});
export { UserCreateSchema, UserUpdateSchema };

View File

@@ -0,0 +1,28 @@
import { z, ZodType } from "zod";
import type { Waypoint } from "../waypoint";
const WaypointCreateSchema = z.object({
id: z.string().length(15).optional(),
name: z.string().optional(),
description: z.string().optional(),
text: z.string().optional(),
lat: z.number(),
lon: z.number(),
icon: z.string().optional(),
author: z.string().length(15),
photos: z.array(z.string()),
}) satisfies ZodType<Waypoint>
const WaypointUpdateSchema = z.object({
name: z.string().optional(),
description: z.string().optional(),
text: z.string().optional(),
lat: z.number().optional(),
lon: z.number().optional(),
icon: z.string().optional(),
photos: z.array(z.string()),
"photos-": z.string().optional(),
"photos+": z.string().optional(),
}) satisfies ZodType<Partial<Waypoint>>
export { WaypointCreateSchema, WaypointUpdateSchema };

View File

@@ -1,4 +1,4 @@
import type { User } from "$lib/stores/user_store";
import type { UserAnonymous } from "./user";
export class Comment {
id?: string;
@@ -8,7 +8,7 @@ export class Comment {
trail: string;
created?: string;
expand?: {
author: User
author: UserAnonymous
}
constructor(text: string, rating: number, author: string, trail: string) {

View File

@@ -6,16 +6,16 @@ class SummitLog {
date: string;
text?: string;
gpx?: string;
_gpx: File | null;
_gpx?: File | null;
photos: string[];
_photos: File[];
_photos?: File[];
distance?: number
elevation_gain?: number
elevation_loss?: number
duration?: number
author?: string;
expand: {
expand?: {
gpx_data?: string;
trails_via_summit_logs?: Trail[];
author?: UserAnonymous

View File

@@ -18,14 +18,14 @@ class Trail {
difficulty?: "easy" | "moderate" | "difficult"
lat?: number;
lon?: number;
thumbnail: number;
thumbnail?: number;
photos: string[];
gpx?: string;
created?: string;
category?: string;
waypoints: string[];
summit_logs: string[];
expand: {
expand?: {
category?: Category;
waypoints: Waypoint[]
summit_logs: SummitLog[]

View File

@@ -7,7 +7,6 @@ export type User = {
password: string,
avatar?: string;
bio?: string;
language?: string;
created?: string;
}

View File

@@ -11,7 +11,7 @@ class Waypoint {
icon?: string;
marker?: M.Marker;
photos: string[];
_photos: File[];
_photos?: File[];
author?: string;
constructor(lat: number, lon: number, params?: {

View File

@@ -5,7 +5,7 @@ let activities: Activity[] = [];
export async function activities_index(author: string, page: number = 1, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f('/api/v1/activity?' + new URLSearchParams({
"per-page": perPage.toString(),
"perPage": perPage.toString(),
page: page.toString(),
filter: `author="${author}"`
}), {

View File

@@ -1,7 +1,7 @@
import { pb } from "$lib/pocketbase";
import type { Category } from "$lib/models/category";
import { writable, type Writable } from "svelte/store";
import { ClientResponseError } from "pocketbase";
import { ClientResponseError, type ListResult } from "pocketbase";
export const categories: Writable<Category[]> = writable([])
@@ -14,9 +14,9 @@ export async function categories_index(f: (url: RequestInfo | URL, config?: Requ
throw new ClientResponseError(await r.json())
}
const response = await r.json();
const response: ListResult<Category> = await r.json();
categories.set(response);
categories.set(response.items);
return response as Category[];
return response.items as Category[];
}

View File

@@ -1,7 +1,7 @@
import { Comment } from "$lib/models/comment";
import type { Trail } from "$lib/models/trail";
import { pb } from "$lib/pocketbase";
import { ClientResponseError } from "pocketbase";
import { ClientResponseError, type ListResult } from "pocketbase";
import { writable, type Writable } from "svelte/store";
export const comments: Writable<Comment[]> = writable([])
@@ -9,6 +9,7 @@ export const comments: Writable<Comment[]> = writable([])
export async function comments_index(trail: Trail) {
let r = await fetch('/api/v1/comment?' + new URLSearchParams({
filter: `trail="${trail.id}"`,
sort: "-created"
}), {
method: 'GET',
})
@@ -17,11 +18,11 @@ export async function comments_index(trail: Trail) {
throw new ClientResponseError(await r.json())
}
const fetchedComments: Comment[] = await r.json();
const fetchedComments: ListResult<Comment> = await r.json();
comments.set(fetchedComments);
comments.set(fetchedComments.items);
return fetchedComments;
return fetchedComments.items;
}
export async function comments_create(comment: Comment) {

View File

@@ -7,7 +7,8 @@ export async function follows_index(data: { follower?: string, followee?: string
const r = await f('/api/v1/follow?' + new URLSearchParams({
filter: data.follower ? `follower='${data.follower}'` : data.followee ? `followee='${data.followee}'` : '',
page: page.toString(),
"per-page": perPage.toString()
perPage: perPage.toString(),
requestKey: "page"
}), {
method: 'GET',
})

View File

@@ -1,5 +1,5 @@
import type { ListShare } from "$lib/models/list_share";
import { ClientResponseError } from "pocketbase";
import { ClientResponseError, type ListResult } from "pocketbase";
import { writable, type Writable } from "svelte/store";
export const shares: Writable<ListShare[]> = writable([])
@@ -16,9 +16,9 @@ export async function list_share_index(list: string, f: (url: RequestInfo | URL,
throw new ClientResponseError(await r.json())
}
const response: ListShare[] = await r.json();
const response: ListResult<ListShare> = await r.json();
shares.set(response);
shares.set(response.items);
return response;

View File

@@ -2,7 +2,7 @@ import { List, type ListFilter } from "$lib/models/list";
import type { Trail } from "$lib/models/trail";
import { pb } from "$lib/pocketbase";
import { ClientResponseError, type ListResult } from "pocketbase";
import { get, writable, type Writable } from "svelte/store";
import { writable, type Writable } from "svelte/store";
import { fetchGPX } from "./trail_store";
let lists: List[] = []
@@ -14,9 +14,10 @@ export async function lists_index(filter?: ListFilter, page: number = 1, perPage
const r = await f('/api/v1/list?' + new URLSearchParams({
sort: `${filter?.sortOrder ?? "-"}${filter?.sort ?? "name"}`,
"per-page": perPage.toString(),
perPage: perPage.toString(),
page: page.toString(),
filter: filterText
filter: filterText,
expand: "trails,trails.waypoints,trails.category,list_share_via_list"
}), {
method: 'GET',
})
@@ -36,7 +37,9 @@ export async function lists_index(filter?: ListFilter, page: number = 1, perPage
}
export async function lists_show(id: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
const r = await f(`/api/v1/list/${id}`, {
const r = await f(`/api/v1/list/${id}?` + new URLSearchParams({
expand: "trails,trails.waypoints,trails.category,list_share_via_list"
}), {
method: 'GET',
})
const response = await r.json()
@@ -132,6 +135,10 @@ export async function lists_add_trail(list: List, trail: Trail) {
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const model: List = await r.json();
return model;
}
export async function lists_remove_trail(list: List, trail: Trail) {
@@ -145,6 +152,10 @@ export async function lists_remove_trail(list: List, trail: Trail) {
if (!r.ok) {
throw new ClientResponseError(await r.json())
}
const model: List = await r.json();
return model;
}
export async function lists_delete(list: List) {

View File

@@ -1,6 +1,6 @@
import { SummitLog, type SummitLogFilter } from "$lib/models/summit_log";
import { pb } from "$lib/pocketbase";
import { ClientResponseError } from "pocketbase";
import { ClientResponseError, type ListResult } from "pocketbase";
import { writable, type Writable } from "svelte/store";
import { fetchGPX } from "./trail_store";
@@ -14,6 +14,8 @@ export async function summit_logs_index(author: string, filter?: SummitLogFilter
const r = await f('/api/v1/summit-log?' + new URLSearchParams({
filter: filterText,
expand: "trails_via_summit_logs.category",
sort: "+date",
}), {
method: 'GET',
})
@@ -22,9 +24,9 @@ export async function summit_logs_index(author: string, filter?: SummitLogFilter
throw new ClientResponseError(await r.json())
}
const fetchedSummitLogs: SummitLog[] = await r.json();
const fetchedSummitLogs: ListResult<SummitLog> = await r.json();
for (const log of fetchedSummitLogs) {
for (const log of fetchedSummitLogs.items) {
if (!log.gpx) {
continue
}
@@ -36,7 +38,7 @@ export async function summit_logs_index(author: string, filter?: SummitLogFilter
log.expand.gpx_data = gpxData;
}
summitLogs.set(fetchedSummitLogs);
summitLogs.set(fetchedSummitLogs.items);
return fetchedSummitLogs;
}
@@ -82,7 +84,7 @@ export async function summit_logs_create(summitLog: SummitLog, f: (url: RequestI
}
if (r.ok) {
return await r.json();
return model;
} else {
throw new ClientResponseError(await r.json())
}

View File

@@ -1,5 +1,5 @@
import type { TrailShare } from "$lib/models/trail_share";
import { ClientResponseError } from "pocketbase";
import { ClientResponseError, type ListResult } from "pocketbase";
import { writable, type Writable } from "svelte/store";
export const shares: Writable<TrailShare[]> = writable([])
@@ -16,11 +16,11 @@ export async function trail_share_index(data: {trail?: string, user?: string}, f
throw new ClientResponseError(await r.json())
}
const response: TrailShare[] = await r.json();
const response: ListResult<TrailShare> = await r.json();
shares.set(response);
shares.set(response.items);
return response;
return response.items;
}

View File

@@ -179,14 +179,14 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
throw new ClientResponseError({ status: 401, response: { message: "Forbidden" } });
}
for (const waypoint of trail.expand.waypoints) {
for (const waypoint of trail.expand?.waypoints ?? []) {
const model = await waypoints_create({
...waypoint,
marker: undefined,
}, f);
trail.waypoints.push(model.id!);
}
for (const summitLog of trail.expand.summit_logs) {
for (const summitLog of trail.expand?.summit_logs ?? []) {
const model = await summit_logs_create(summitLog, f);
trail.summit_logs.push(model.id!);
}
@@ -227,7 +227,7 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: File[], gpx?: File | Blob | null) {
const waypointUpdates = compareObjectArrays<Waypoint>(oldTrail.expand.waypoints ?? [], newTrail.expand.waypoints ?? []);
const waypointUpdates = compareObjectArrays<Waypoint>(oldTrail.expand?.waypoints ?? [], newTrail.expand?.waypoints ?? []);
for (const addedWaypoint of waypointUpdates.added) {
const model = await waypoints_create({
@@ -238,7 +238,7 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
}
for (const updatedWaypoint of waypointUpdates.updated) {
const oldWaypoint = oldTrail.expand.waypoints.find(w => w.id == updatedWaypoint.id);
const oldWaypoint = oldTrail.expand?.waypoints.find(w => w.id == updatedWaypoint.id);
const model = await waypoints_update(oldWaypoint!, {
...updatedWaypoint,
marker: undefined,
@@ -249,7 +249,7 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
const success = await waypoints_delete(deletedWaypoint);
}
const summitLogUpdates = compareObjectArrays<SummitLog>(oldTrail.expand.summit_logs ?? [], newTrail.expand.summit_logs ?? []);
const summitLogUpdates = compareObjectArrays<SummitLog>(oldTrail.expand?.summit_logs ?? [], newTrail.expand?.summit_logs ?? []);
for (const summitLog of summitLogUpdates.added) {
const model = await summit_logs_create(summitLog);
@@ -257,7 +257,7 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
}
for (const updatedSummitLog of summitLogUpdates.updated) {
const oldSummitLog = oldTrail.expand.summit_logs.find(w => w.id == updatedSummitLog.id);
const oldSummitLog = oldTrail.expand?.summit_logs.find(w => w.id == updatedSummitLog.id);
const model = await summit_logs_update(oldSummitLog!, updatedSummitLog);
}
@@ -266,7 +266,9 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
const success = await summit_logs_delete(deletedSummitLog);
}
let r = await fetch('/api/v1/trail/' + newTrail.id, {
let r = await fetch(`/api/v1/trail/${newTrail.id}?` + new URLSearchParams({
expand: "category,waypoints,summit_logs,trail_share_via_trail",
}), {
method: 'POST',
body: JSON.stringify({ ...newTrail, expand: undefined }),
})
@@ -312,12 +314,12 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
export async function trails_delete(trail: Trail) {
if (trail.expand.waypoints) {
if (trail.expand?.waypoints) {
for (const waypoint of trail.expand.waypoints) {
await waypoints_delete(waypoint);
}
}
if (trail.expand.summit_logs) {
if (trail.expand?.summit_logs) {
for (const summit_log of trail.expand.summit_logs) {
await summit_logs_delete(summit_log);
}

View File

@@ -22,7 +22,7 @@ export async function users_create(user: User) {
export async function users_search(q: string, includeSelf: boolean = true) {
let r = await fetch('/api/v1/user/anonymous?' + new URLSearchParams({
"filter": `username~"${q}"${includeSelf ? '' : '&&id!="${pb.authStore.model?.id}"'}`,
"filter": `username~"${q}"${includeSelf ? '' : `&&id!="${pb.authStore.model?.id}"`}`,
}), {
method: 'GET',
})

View File

@@ -0,0 +1,115 @@
import { error, type NumericRange, type RequestEvent } from "@sveltejs/kit";
import { ClientResponseError, type ListResult } from "pocketbase";
import { pb } from "$lib/pocketbase";
import { ZodError, type ZodSchema } from "zod";
import { RecordListOptionsSchema, RecordIdSchema, RecordOptionsSchema } from "$lib/models/api/base_schema";
export enum Collection {
users = "users",
categories = "categories",
comments = "comments",
follows = "follows",
list_share = "list_share",
lists = "lists",
notifications = "notifications",
settings = "settings",
summit_logs = "summit_logs",
trail_share = "trail_share",
trails = "trails",
waypoints = "waypoints",
activities = "activities",
follow_counts = "follow_counts",
trails_bounding_box = "trails_bounding_box",
trails_filter = "trails_filter",
users_anonymous = "users_anonymous"
}
export async function list<T>(event: RequestEvent, collection: Collection) {
const searchParams = Object.fromEntries(event.url.searchParams);
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
let r: ListResult<T>;
if ((safeSearchParams.perPage ?? 0) < 0) {
const activities: T[] = await pb.collection(Collection[collection])
.getFullList<T>(safeSearchParams)
r = {
items: activities,
perPage: -1,
page: 1,
totalItems: activities.length,
totalPages: 1
}
} else {
r = await pb.collection(Collection[collection])
.getList<T>(safeSearchParams.page, safeSearchParams.perPage, { ...safeSearchParams })
}
return r
}
export async function show<T>(event: RequestEvent, collection: Collection) {
const params = event.params
const safeParams = RecordIdSchema.parse(params);
const searchParams = Object.fromEntries(event.url.searchParams);
const safeSearchParams = RecordOptionsSchema.parse(searchParams);
const r = await pb.collection(collection.toString())
.getOne<T>(safeParams.id, safeSearchParams)
return r
}
export async function create<T>(event: RequestEvent, schema: ZodSchema, collection: Collection) {
const data = await event.request.json();
const safeData = schema.parse(data);
const r = await pb.collection(Collection[collection]).create<T>(safeData)
return r
}
export async function update<T>(event: RequestEvent, schema: ZodSchema, collection: Collection) {
const params = event.params
const safeParams = RecordIdSchema.parse(params);
const searchParams = Object.fromEntries(event.url.searchParams);
const safeSearchParams = RecordOptionsSchema.parse(searchParams);
const data = await event.request.json();
const safeData = schema.parse(data);
const r = await pb.collection(Collection[collection]).update<T>(safeParams.id, safeData, safeSearchParams)
return r
}
export async function upload<T>(event: RequestEvent, collection: Collection) {
const params = event.params
const safeParams = RecordIdSchema.parse(params);
const data = await event.request.formData();
const r = await pb.collection(Collection[collection]).update<T>(safeParams.id, data)
return r
}
export async function remove(event: RequestEvent, collection: Collection) {
const params = event.params
const safeParams = RecordIdSchema.parse(params);
const r = await pb.collection(Collection[collection]).delete(safeParams.id)
return { 'acknowledged': r }
}
export function handleError(e: any) {
if (e instanceof ZodError) {
return error(400, e.message)
} else if (e instanceof ClientResponseError && e.status > 0) {
return error(e.status as NumericRange<400, 599>, e.message)
} else {
return e
}
}

View File

@@ -35,7 +35,7 @@ export async function gpx2trail(gpxString: string, fallbackName?: string) {
wp.id = cryptoRandomString({ length: 15 });
wp.name = wpt.name ?? ""
wp.description = wpt.desc;
trail.expand.waypoints.push(wp);
trail.expand!.waypoints.push(wp);
}
const totals = gpx.getTotals()