mirror of
https://github.com/michivonah/themepark-assistant.git
synced 2025-12-22 22:16:29 +01:00
implement background job for fetching attractions
This commit is contained in:
parent
ff859475a0
commit
ebb3191757
4 changed files with 195 additions and 6 deletions
|
|
@ -3,7 +3,7 @@ import { integer, text, sqliteTable } from "drizzle-orm/sqlite-core";
|
||||||
export const attraction = sqliteTable('attraction', {
|
export const attraction = sqliteTable('attraction', {
|
||||||
id: integer().primaryKey({ autoIncrement: true }),
|
id: integer().primaryKey({ autoIncrement: true }),
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
apiCode: integer(),
|
apiCode: text().notNull().unique(),
|
||||||
themeparkId: integer().notNull().references(() => themepark.id)
|
themeparkId: integer().notNull().references(() => themepark.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const themepark = sqliteTable('themepark', {
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
countrycode: text().notNull(),
|
countrycode: text().notNull(),
|
||||||
website: text(),
|
website: text(),
|
||||||
apiName: text()
|
apiName: text().notNull().unique()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const user = sqliteTable('user', {
|
export const user = sqliteTable('user', {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// Cron Router
|
// Cron Router
|
||||||
import { updateThemeparkData } from "./update-themepark-data";
|
import { updateThemeparkData } from "./update-themepark-data";
|
||||||
|
import { batchAttractionImport } from "./update-attraction-list";
|
||||||
|
|
||||||
export default async function cronRouter(
|
export default async function cronRouter(
|
||||||
event: ScheduledEvent,
|
event: ScheduledEvent,
|
||||||
|
|
@ -7,10 +8,13 @@ export default async function cronRouter(
|
||||||
ctx: ExecutionContext,
|
ctx: ExecutionContext,
|
||||||
){
|
){
|
||||||
switch (event.cron){
|
switch (event.cron){
|
||||||
case '* * * * *':
|
case '*/5 * * * *':
|
||||||
console.log('every minute');
|
console.log('every 5 minutes');
|
||||||
break;
|
break;
|
||||||
case '0 4 7,14,21,28 * *':
|
case '0 1-6 7,14,21,28 * *':
|
||||||
|
await batchAttractionImport(env, event.scheduledTime, event.cron);
|
||||||
|
break;
|
||||||
|
case '0 4 1 * *':
|
||||||
await updateThemeparkData(env);
|
await updateThemeparkData(env);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
184
api/src/jobs/update-attraction-list.ts
Normal file
184
api/src/jobs/update-attraction-list.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { getDbEnv } from '../db/client'
|
||||||
|
import { attraction, themepark } from '../db/schema'
|
||||||
|
import { inArray } from 'drizzle-orm'
|
||||||
|
import fetchData from '../lib/fetch-data'
|
||||||
|
import asyncBatchJob from '../lib/async-batch-job'
|
||||||
|
|
||||||
|
interface AttractionImport {
|
||||||
|
code: string,
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttractionType {
|
||||||
|
name: string,
|
||||||
|
apiCode: string,
|
||||||
|
themeparkId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Themepark {
|
||||||
|
apiName: string,
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetching the attractions from a specified park
|
||||||
|
* @param park API Code for request themepark
|
||||||
|
* @param endpoint Endpoint where to fetch data from (default: https://api.wartezeiten.app/v1/parks)
|
||||||
|
* @param lang Language used for API request
|
||||||
|
* @returns Interface with attraction code & name
|
||||||
|
*/
|
||||||
|
async function fetchAttractions(
|
||||||
|
park: string,
|
||||||
|
endpoint: string = "https://api.wartezeiten.app/v1/parks",
|
||||||
|
lang: string = 'de'
|
||||||
|
): Promise<AttractionImport[]>{
|
||||||
|
try{
|
||||||
|
const headers = {
|
||||||
|
'language':lang,
|
||||||
|
'park':park
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await fetchData<AttractionImport[]>(endpoint, headers);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
throw new Error(`Failed to fetch attractions: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object of all themeparks saved in the database
|
||||||
|
* @param env DB Connection
|
||||||
|
* @returns Object of themeparks with api name & id from internal DB
|
||||||
|
*/
|
||||||
|
async function getThemeparks(env: Env): Promise<Themepark[]>{
|
||||||
|
try{
|
||||||
|
const db = getDbEnv(env);
|
||||||
|
const themeparks: Themepark[] = await db.select({
|
||||||
|
apiName: themepark.apiName,
|
||||||
|
id: themepark.id
|
||||||
|
}).from(themepark);
|
||||||
|
|
||||||
|
return themeparks;
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
throw new Error(`Failed to get themeparks from database: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object of all attractions from a defined park saved in the database
|
||||||
|
* @param env DB Connection
|
||||||
|
* @param parks Object of themeparks to get attractions from
|
||||||
|
* @returns Object of attractions
|
||||||
|
*/
|
||||||
|
async function getAttractionsByParks(env: Env, parks: Themepark[]): Promise<AttractionType[]>{
|
||||||
|
try{
|
||||||
|
const db = getDbEnv(env);
|
||||||
|
|
||||||
|
const parkIds: number[] = parks.map(p => p.id);
|
||||||
|
|
||||||
|
const attractions: AttractionType[] = await db.select({
|
||||||
|
name: attraction.name,
|
||||||
|
apiCode: attraction.apiCode,
|
||||||
|
themeparkId: attraction.themeparkId
|
||||||
|
}).from(attraction)
|
||||||
|
.where(inArray(attraction.themeparkId, parkIds));
|
||||||
|
|
||||||
|
return attractions;
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
throw new Error(`Failed to get attractions from database: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports attraction of the specified themeparks into the DB.
|
||||||
|
* Interacts with the dependant functions.
|
||||||
|
* @param env DB Connection
|
||||||
|
* @param parks Object of the themeparks from which the attractions have to be imported
|
||||||
|
*/
|
||||||
|
async function importAttractionsByParks(env: Env, parks: Themepark[]): Promise<void>{
|
||||||
|
try{
|
||||||
|
const db = getDbEnv(env);
|
||||||
|
|
||||||
|
const currentAttractions = await getAttractionsByParks(env, parks);
|
||||||
|
for (let park of parks){
|
||||||
|
const attractions = await fetchAttractions(park.apiName);
|
||||||
|
|
||||||
|
// get only the new attractions
|
||||||
|
const importableAttractions = attractions.filter(
|
||||||
|
attractionIndex => currentAttractions.length == 0 || !currentAttractions.some(id => id.apiCode == attractionIndex.code)
|
||||||
|
).map(attractionIndex => ({
|
||||||
|
name: attractionIndex.name,
|
||||||
|
apiCode: attractionIndex.code,
|
||||||
|
themeparkId: park.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
// only run the import when new attractions available
|
||||||
|
if (importableAttractions.length > 0){
|
||||||
|
await asyncBatchJob(importableAttractions, 16, async (batch) => {
|
||||||
|
await db.insert(attraction).values(batch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
throw new Error(`Failed to import attractions into database: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does create batches of themeparks from a timestamp & cron statement,
|
||||||
|
* so the imports can be done splitted into multiple batches.
|
||||||
|
* The function is exported, so that it can be run via background task.
|
||||||
|
* @param env DB Connection
|
||||||
|
* @param timestamp Current timestamp when function is run
|
||||||
|
* @param cron The cron statement specified to run the background jobs; used for batch size calculation
|
||||||
|
*/
|
||||||
|
export async function batchAttractionImport(env: Env, timestamp: number, cron: string): Promise<void>{
|
||||||
|
try{
|
||||||
|
const themeparks = await getThemeparks(env); // all themeparks
|
||||||
|
const executionHour = new Date(timestamp).getUTCHours(); // current hour, in which job is executed
|
||||||
|
const executionTimes = getExecutionCountFromCron(cron, 1); // how often the job is executed
|
||||||
|
|
||||||
|
const batchSize = Math.ceil(themeparks.length / executionTimes); // calculate batch size, so that each park gets updated
|
||||||
|
const hourIndex = executionHour - 1;
|
||||||
|
|
||||||
|
// time examples
|
||||||
|
// 01:00 -> 1757811600000
|
||||||
|
// 02:00 -> 1757815200000
|
||||||
|
// 03:00 -> 1757818800000
|
||||||
|
|
||||||
|
const batch = themeparks.slice(hourIndex * batchSize, (hourIndex + 1) * batchSize); // slice array into right batch
|
||||||
|
|
||||||
|
// import attractions from current time batch
|
||||||
|
await importAttractionsByParks(env, batch);
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
throw new Error(`Failed to split attraction import by time: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates how often cron job gets executed
|
||||||
|
* @param cron Cron scheme
|
||||||
|
* @param partNr Which part of cron scheme should be checked
|
||||||
|
* @returns Number of execution times
|
||||||
|
*/
|
||||||
|
function getExecutionCountFromCron(cron: string, partNr: number): number{
|
||||||
|
const parts = cron.split(" ");
|
||||||
|
const specifiedPart = parts[partNr];
|
||||||
|
|
||||||
|
switch (true){
|
||||||
|
case specifiedPart.includes("-"):
|
||||||
|
const [start, end] = specifiedPart.split("-").map(Number);
|
||||||
|
return end - start + 1;
|
||||||
|
case specifiedPart.includes(","):
|
||||||
|
return specifiedPart.split(",").length;
|
||||||
|
case specifiedPart === "*":
|
||||||
|
return 24;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,8 @@
|
||||||
],
|
],
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"crons": [
|
"crons": [
|
||||||
"0 4 7,14,21,28 * *", // At 04:00 on day-of-month 7, 14, 21, and 28.
|
"0 1-6 7,14,21,28 * *", // Each hour from 01:00 to 06:00 on day-of-month 7, 14, 21, and 28. -> update attraction list
|
||||||
|
"0 4 1 * *", // At 04:00 on the 1th of each month -> update themeparks
|
||||||
"* * * * *" // every minute
|
"* * * * *" // every minute
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue