Compare commits

...

3 commits

14 changed files with 105 additions and 48 deletions

View file

@ -1,14 +1,50 @@
# themepark-assistant # themepark-assistant
A tool for improving your trips to themeparks - once developed A tool for improving your trips to themeparks - once developed
## Testing ## Repo structure
Send request - /api: API implementation
- ./: config files
- /src: API Code
- /db: Database client, schema & migrations
- /errors: Error types
- index.ts: Exporter for all error classes
- /jobs: Background tasks
- /lib: Reusable functions
- /routes: API Endpoints
- /types: Data type definitions
- index.ts: Entrypoint for API Requests & background tasks on Cloudflare Workers
## Development
### Run enviromnment
Run worker locally (without remote d1 access, scheduled tasks not available)
```bash
npx wrangler dev
```
Run worker locally (without remote d1 access, scheduled tasks available)
```bash
npx wrangler dev --test-scheduled
```
Run worker locally (with remote connection to d1, scheduled tasks available)
```bash
npx wrangler dev --remote --test-scheduled
```
### Requests
Send request with bearer authentication
```bash ```bash
curl -H "Authorization: Bearer insecure-token" http://127.0.0.1:8787/notification/list curl -H "Authorization: Bearer insecure-token" http://127.0.0.1:8787/notification/list
``` ```
## Update cloudflare d1 db Run request with cron expression (for executing background tasks)
```bash
curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"
```
### Drizzle DB migrations
Update cloudflare d1 db
DB scheme is defined in typescript DB scheme is defined in typescript
apply changes apply changes
@ -21,35 +57,25 @@ export sql statements instead of running migration
npx drizzle-kit export --config=drizzle-dev.config.ts npx drizzle-kit export --config=drizzle-dev.config.ts
``` ```
## SQLite / D1 ### Useful sql statements for SQLite / D1
Delete view Delete view
```sql ```sql
DROP VIEW IF EXISTS attraction_subscriptions; DROP VIEW IF EXISTS attraction_subscriptions;
``` ```
## Cloudflare workers tricks ### Cloudflare workers tricks
If types are missing, run: If types are missing, run:
```bash ```bash
npx wrangler types npx wrangler types
``` ```
## Testing cronjobs ## Authentication endpoints (auth.js)
Run worker locally (without remote d1 access)
```bash
npx wrangler dev --test-scheduled
```
Run worker locally (with remote connection to d1)
```bash
npx wrangler dev --remote --test-scheduled
```
Run curl request with cron expression
```bash
curl "http://localhost:8787/__scheduled?cron=*+*+*+*+*"
```
## Authentication endpoints
- /auth/signin -> Login - /auth/signin -> Login
- /auth/signout -> Logout - /auth/signout -> Logout
- /auth/callback/github -> Callback for GitHub OAuth config - /auth/callback/github -> Callback for GitHub OAuth config
## Contributing
TBD
## License
TBD

View file

@ -1,15 +1,8 @@
// Error classes for background jobs // Error classes for background jobs
import { BaseError } from "./base-error";
// Class for custom background errors // Class for custom background errors
export class BackgroundJobError extends Error{ export class BackgroundJobError extends BaseError{}
cause?: unknown;
constructor(message: string, cause?: unknown){
super(message);
this.name = this.constructor.name;
this.cause = cause;
}
}
// Errors based on class BackgroundJobError // Errors based on class BackgroundJobError
export class BackgroundDatabaseError extends BackgroundJobError{ export class BackgroundDatabaseError extends BackgroundJobError{
@ -18,12 +11,6 @@ export class BackgroundDatabaseError extends BackgroundJobError{
} }
} }
export class BackgroundFetchError extends BackgroundJobError{
constructor(cause?: unknown){
super('Fetching data failed', cause);
}
}
export class AttractionImportError extends BackgroundJobError{ export class AttractionImportError extends BackgroundJobError{
constructor(cause?: unknown){ constructor(cause?: unknown){
super('Failed to import attractions into database.', cause); super('Failed to import attractions into database.', cause);

View file

@ -0,0 +1,10 @@
// Base for new error domains/classes
export class BaseError extends Error{
cause?: unknown;
constructor(message: string, cause?: unknown){
super(message);
this.name = this.constructor.name;
this.cause = cause;
}
}

3
api/src/errors/index.ts Normal file
View file

@ -0,0 +1,3 @@
export * from './background-error'
export * from './http-error'
export * from './lib-error'

View file

@ -0,0 +1,26 @@
// Errors in custom libs
import { BaseError } from "./base-error";
export class LibError extends BaseError{}
export class FetchError extends LibError{
constructor(cause?: unknown, source?: string){
super(
source
? `Fetching data from ${source} failed`
: 'Fetching data failed',
cause);
}
}
export class BatchExecutionError extends LibError{
constructor(cause?: unknown){
super('Batched execution failed.', cause);
}
}
export class HTTPError extends LibError{
constructor(errorCode: number, cause?: unknown){
super(`Received HTTP error code: ${errorCode}`, cause);
}
}

View file

@ -3,7 +3,7 @@ import { getDbEnv } from '../db/client'
import { subscribedThemeparks, attractionSubscriptions } from "../db/schema"; import { subscribedThemeparks, attractionSubscriptions } from "../db/schema";
import { SubscribedThemeparks } from "../types/subscribed-themeparks"; import { SubscribedThemeparks } from "../types/subscribed-themeparks";
import { AttractionSubscription } from "../types/attraction-subscriptions"; import { AttractionSubscription } from "../types/attraction-subscriptions";
import { KVParseError, SendNotificationError } from "../errors/background-error"; import { KVParseError, SendNotificationError } from "../errors";
import httpRequest from "../lib/http-request"; import httpRequest from "../lib/http-request";
import fetchAttractions from "../lib/fetch-attractions"; import fetchAttractions from "../lib/fetch-attractions";

View file

@ -3,7 +3,7 @@ import { attraction, themepark } from '../db/schema'
import { inArray } from 'drizzle-orm' import { inArray } from 'drizzle-orm'
import { Attraction } from '../types/attraction' import { Attraction } from '../types/attraction'
import { ThemeparkSelect } from '../types/themepark' import { ThemeparkSelect } from '../types/themepark'
import { AttractionImportError, BackgroundDatabaseError } from '../errors/background-error' import { AttractionImportError, BackgroundDatabaseError } from '../errors'
import asyncBatchJob from '../lib/async-batch-job' import asyncBatchJob from '../lib/async-batch-job'
import fetchAttractions from '../lib/fetch-attractions' import fetchAttractions from '../lib/fetch-attractions'

View file

@ -1,7 +1,7 @@
import { getDbEnv } from '../db/client' import { getDbEnv } from '../db/client'
import { themepark } from '../db/schema' import { themepark } from '../db/schema'
import { countryCodesDE } from '../lib/countries' import { countryCodesDE } from '../lib/countries'
import { BackgroundFetchError, ThemeparkUpdateError } from '../errors/background-error' import { FetchError, ThemeparkUpdateError } from '../errors'
import httpRequest from '../lib/http-request' import httpRequest from '../lib/http-request'
import asyncBatchJob from '../lib/async-batch-job' import asyncBatchJob from '../lib/async-batch-job'
@ -31,7 +31,7 @@ async function fetchThemeparks(
return result; return result;
} }
catch(e){ catch(e){
throw new BackgroundFetchError(e); throw new FetchError(e);
} }
} }

View file

@ -1,3 +1,5 @@
import { BatchExecutionError } from "../errors";
/** /**
* Run any async operation in (multiple) batches * Run any async operation in (multiple) batches
* @param data Array to split into batches * @param data Array to split into batches
@ -12,6 +14,6 @@ export default async function asyncBatchJob<T>(data: T[], batchSize: number = 20
} }
} }
catch(e){ catch(e){
throw new Error(`Batch execution failed: ${e}`); throw new BatchExecutionError(e);
} }
} }

View file

@ -1,5 +1,6 @@
import { AttractionImport } from '../types/attraction' import { AttractionImport } from '../types/attraction'
import httpRequest from '../lib/http-request' import httpRequest from '../lib/http-request'
import { FetchError } from '../errors';
/** /**
* Fetching the attractions from a specified park * Fetching the attractions from a specified park
@ -25,6 +26,6 @@ export default async function fetchAttractions(
return result; return result;
} }
catch(e){ catch(e){
throw new Error(`Failed to fetch attractions: ${e}`); throw new FetchError(e, endpoint);
} }
} }

View file

@ -1,3 +1,5 @@
import { HTTPError } from "../errors";
/** /**
* Executes a HTTP Request with option for custom header & body parameters * Executes a HTTP Request with option for custom header & body parameters
* @param endpoint Request endpoint * @param endpoint Request endpoint
@ -26,7 +28,7 @@ export default async function httpRequest<TResponse, TBody = undefined>(
if (!response.ok){ if (!response.ok){
throw new Error(`HTTP error! Status: ${response.status}`); throw new HTTPError(response.status);
} }
if(response.status === 204){ if(response.status === 204){

View file

@ -4,7 +4,7 @@ import { UserSelect } from "../types/user";
import { user } from "../db/schema"; import { user } from "../db/schema";
import { like } from "drizzle-orm"; import { like } from "drizzle-orm";
import { DrizzleD1Database } from "drizzle-orm/d1"; import { DrizzleD1Database } from "drizzle-orm/d1";
import { MissingMailError, UserInactiveError, DatabaseError } from "../errors/http-error"; import { MissingMailError, UserInactiveError, DatabaseError } from "../errors";
/** /**
* Returns the details of a user from the given context * Returns the details of a user from the given context

View file

@ -2,7 +2,7 @@ import { Hono, Context } from 'hono'
import { getDbContext } from '../db/client' import { getDbContext } from '../db/client'
import { attractionNotification, notificationMethod } from '../db/schema' import { attractionNotification, notificationMethod } from '../db/schema'
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { DatabaseError, InvalidParameter, MissingParameter } from '../errors/http-error' import { DatabaseError, InvalidParameter, MissingParameter } from '../errors'
import { getUser } from '../lib/user-auth' import { getUser } from '../lib/user-auth'
import { Message } from '../types/response' import { Message } from '../types/response'
import { NotificationMethodSelect } from '../types/notification-method' import { NotificationMethodSelect } from '../types/notification-method'

View file

@ -3,7 +3,7 @@ import { getDbContext } from '../db/client'
import { themepark, attraction } from '../db/schema' import { themepark, attraction } from '../db/schema'
import { responseCache } from '../lib/cache' import { responseCache } from '../lib/cache'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { DatabaseError } from '../errors/http-error' import { DatabaseError } from '../errors'
const app = new Hono() const app = new Hono()