diff --git a/.dockerignore b/.dockerignore index 22f55ad..78c81ea 100644 --- a/.dockerignore +++ b/.dockerignore @@ -32,4 +32,7 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +# fly.io +fly.toml \ No newline at end of file diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml new file mode 100644 index 0000000..b0c246e --- /dev/null +++ b/.github/workflows/fly-deploy.yml @@ -0,0 +1,18 @@ +# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ + +name: Fly Deploy +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + concurrency: deploy-group # optional: ensure only one action runs at a time + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/docker-compose.yaml b/docker-compose.yaml index 2d5a336..dfeda33 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,8 +21,6 @@ services: - db_net ports: - 6379:6379 - command: > - --requirepass ${REDIS_PASS} env_file: - .env networks: diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..9738a82 --- /dev/null +++ b/fly.toml @@ -0,0 +1,29 @@ +# fly.toml app configuration file generated for us-api on 2024-09-01T10:29:03-06:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'us-api' +primary_region = 'den' + +[build] + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + size = 'shared-cpu-1x' + +[env] +FILE_DEFAULT_TTL = "600" +S3_ENDPOINT="fly.storage.tigris.dev" +S3_PORT="443" +S3_USE_SSL="true" +S3_BUCKET="api-us-dev-prod" +REDIS_DB="0" +NODE_ENV="production" \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index f206678..08f9476 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -37,16 +37,17 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis'; RedisModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], - useFactory: async (configService: ConfigService): Promise => { + useFactory: async ( + configService: ConfigService, + ): Promise => { return { config: { - host: configService.get('redis.host'), - port: configService.get('redis.port'), - password: configService.get('redis.password'), + url: configService.get('redis.url'), db: configService.get('redis.db'), - } + family: 6, + }, }; - } + }, }), CacheModule.registerAsync({ isGlobal: true, @@ -54,12 +55,9 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis'; imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ store: redisStore, - socket: { - host: configService.get('redis.host'), - port: configService.get('redis.port'), - }, + url: configService.get('redis.url'), + family: 6, database: configService.get('redis.db'), - password: configService.get('redis.password'), }), }), BullModule.forRootAsync({ @@ -68,10 +66,9 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis'; useFactory: async (configService: ConfigService) => { const config = { redis: { - password: configService.get('redis.password'), - host: configService.get('redis.host'), - port: configService.get('redis.port'), + url: configService.get('redis.url'), db: configService.get('redis.db'), + family: 6, }, }; return config; @@ -103,4 +100,4 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis'; controllers: [AppController], providers: [AppService], }) -export class AppModule { } +export class AppModule {} diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 5a5d24c..351ac57 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -7,7 +7,7 @@ export default () => ({ userAgent: 'api.us.dev/@chip@talking.dev', rapidApiKey: process.env.RAPID_API_KEY || '', irc: { - enabled: process.env.IRC_SERVER === "" ? false : true, + enabled: process.env.IRC_SERVER === '' ? false : true, server: process.env.IRC_SERVER, tls: process.env.IRC_TLS === 'true', port: parseInt(process.env.IRC_PORT ?? '6697'), @@ -19,19 +19,22 @@ export default () => ({ : 'us-dev', }, redis: { - host: process.env.REDIS_HOST ?? 'redis-master', - port: parseInt(process.env.REDIS_PORT ?? '6379'), - password: process.env.REDIS_PASS ?? '', + url: process.env.REDIS_URL ?? '', db: parseInt(process.env.REDIS_DB ?? '1'), }, file: { - bucketName: process.env.FILE_BUCKET_NAME ?? process.env.S3_BUCKET ?? 'api.us.dev-files', + bucketName: + process.env.FILE_BUCKET_NAME ?? + process.env.S3_BUCKET ?? + 'api.us.dev-files', // default file ttl in seconds - defaultTtl: parseInt(process.env.FILE_DEFAULT_TTL ?? (30 * 24 * 60 * 60).toString()), + defaultTtl: parseInt( + process.env.FILE_DEFAULT_TTL ?? (30 * 24 * 60 * 60).toString(), + ), }, focoLive: { airtable: { apiKey: process.env.FOCO_LIVE_AIRTABLE_APIKEY ?? '', - } - } + }, + }, }); diff --git a/src/foco-live/foco-live.service.ts b/src/foco-live/foco-live.service.ts index 4c122e2..96724bc 100644 --- a/src/foco-live/foco-live.service.ts +++ b/src/foco-live/foco-live.service.ts @@ -11,116 +11,142 @@ import { Gauge } from 'prom-client'; import { filter, pipe } from 'ramda'; const tables = { - venues: 'tblRi4wDorKqNJJbs', - events: 'tbl4RZ75QF5WefE7L', -} + venues: 'tblRi4wDorKqNJJbs', + events: 'tbl4RZ75QF5WefE7L', +}; -const compareDates = (a: any, b: any) => new Date(a.Date).getTime() - new Date(b.Date).getTime(); +const compareDates = (a: any, b: any) => + new Date(a.Date).getTime() - new Date(b.Date).getTime(); -const beforeFilter = (before?: Date) => (a: Event) => before ? new Date(a.Date) <= before : true; -const afterFilter = (after?: Date) => (a: Event) => after ? new Date(a.Date) >= after : true; +const beforeFilter = (before?: Date) => (a: Event) => + before ? new Date(a.Date) <= before : true; +const afterFilter = (after?: Date) => (a: Event) => + after ? new Date(a.Date) >= after : true; export interface Event { - "Date": string, - "Music Start Time": string, - "Bar or Venue Name": string, - "Band/DJ/Musician Name": string, - "Cost Select": string; - Cost: string, - "Date Select": string - '"Specials" at Venue': string, - id: string; + Date: string; + 'Music Start Time': string; + 'Bar or Venue Name': string; + 'Band/DJ/Musician Name': string; + 'Cost Select': string; + Cost: string; + 'Date Select': string; + '"Specials" at Venue': string; + Genre?: string; + id: string; } export interface Venue { - id: string; - "Bar or Venue Name": string, - "Street Address": string, - "City": string, - "Zip Code": number, - State: string, - "Phone Number": string, - Website: string, - "Has Calendar Of Events": string, - "Facebook Page": string, - "Instagram": string, - "Twitter Account": string, + id: string; + 'Bar or Venue Name': string; + 'Street Address': string; + City: string; + 'Zip Code': number; + State: string; + 'Phone Number': string; + Website: string; + 'Has Calendar Of Events': string; + 'Facebook Page': string; + Instagram: string; + 'Twitter Account': string; } const cacheKeys = { - allEvents: 'foco_live_events', - allVenues: 'foco_live_venues', -} + allEvents: 'foco_live_events', + allVenues: 'foco_live_venues', +}; @Injectable() export class FocoLiveService { - private readonly airtableBase: AirtableBase; - private readonly logger = new Logger(FocoLiveService.name); + private readonly airtableBase: AirtableBase; + private readonly logger = new Logger(FocoLiveService.name); - constructor( - private readonly config: ConfigService, - @InjectMetric('event_count') public eventCount: Gauge, - @InjectMetric('event_cache_misses') public cacheMisses: Gauge, - @Inject(CACHE_MANAGER) private cacheManager: Cache - ) { - this.airtableBase = new Airtable({ - apiKey: config.get('focoLive.airtable.apiKey'), - }).base('app1SjPrn5qrhr59J'); - } + constructor( + private readonly config: ConfigService, + @InjectMetric('event_count') public eventCount: Gauge, + @InjectMetric('event_cache_misses') public cacheMisses: Gauge, + @Inject(CACHE_MANAGER) private cacheManager: Cache, + ) { + this.airtableBase = new Airtable({ + apiKey: config.get('focoLive.airtable.apiKey'), + }).base('app1SjPrn5qrhr59J'); + } - async getAllEvents(): Promise { - return ((await this.airtableBase('Events').select({ - view: "Grid view", - }).all()) - .map(record => ({ - id: record.id, - ...record.fields - })) as any as Event[]) - .sort(compareDates) - .reverse(); - } + async getAllEvents(): Promise { + return ( + ( + await this.airtableBase('Events') + .select({ + view: 'Grid view', + }) + .all() + ).map((record) => ({ + id: record.id, + ...record.fields, + })) as any as Event[] + ) + .sort(compareDates) + .reverse(); + } - async getAllEventsCached(): Promise { - let events: Event[] | null | undefined = await this.cacheManager.get(cacheKeys.allEvents); - if (!events) { - events = await this.getAllEvents(); - this.cacheMisses.inc(); - this.cacheManager.set(cacheKeys.allEvents, events, 10 * 60 * 1000); - } - this.eventCount.set(events.length); - return events; + async getAllEventsCached(): Promise { + let events: Event[] | null | undefined = await this.cacheManager.get( + cacheKeys.allEvents, + ); + if (!events) { + events = await this.getAllEvents(); + this.cacheMisses.inc(); + this.cacheManager.set(cacheKeys.allEvents, events, 10 * 60 * 1000); } + this.eventCount.set(events.length); + return events; + } - async getAllVenues(): Promise { - return ((await this.airtableBase('Venues').select({ - view: "Grid view", - }).all()) - .map(record => ({ - id: record.id, - ...record.fields - })) as any as Venue[]) - .sort(compareDates) - .reverse(); - } + async getAllVenues(): Promise { + return ( + ( + await this.airtableBase('Venues') + .select({ + view: 'Grid view', + }) + .all() + ).map((record) => ({ + id: record.id, + ...record.fields, + })) as any as Venue[] + ) + .sort(compareDates) + .reverse(); + } - async getAllVenuesCached(): Promise { - return await this.cacheManager.get(cacheKeys.allVenues) || await this.getAllVenues(); - } + async getAllVenuesCached(): Promise { + return ( + (await this.cacheManager.get(cacheKeys.allVenues)) || + (await this.getAllVenues()) + ); + } - @Cron("0 */5 * * * *") - async refreshEvents() { - this.logger.verbose("Refreshing events cache."); - await this.getAllEventsCached(); - } + @Cron('0 */5 * * * *') + async refreshEvents() { + this.logger.verbose('Refreshing events cache.'); + await this.getAllEventsCached(); + } - async getEvents(options: { venue?: string, before?: Date, after?: Date } = {}) { - const events = await this.getAllEventsCached(); - const results = pipe( - filter((a: Event) => a["Bar or Venue Name"] === (options.venue ?? a["Bar or Venue Name"])), - filter(beforeFilter(options.before)), - filter(afterFilter(options.after)), - )(events); - this.logger.verbose(`Returning ${results.length} events, ${events.length} total events in database.`); - return results - } + async getEvents( + options: { venue?: string; before?: Date; after?: Date } = {}, + ) { + const events = await this.getAllEventsCached(); + const results = pipe( + filter( + (a: Event) => + a['Bar or Venue Name'] === (options.venue ?? a['Bar or Venue Name']), + ), + filter(beforeFilter(options.before)), + filter(afterFilter(options.after)), + )(events); + this.logger.verbose( + `Returning ${results.length} events, ${events.length} total events in database.`, + ); + return results; + } } diff --git a/src/minio/minio.service.ts b/src/minio/minio.service.ts index 5e0fa37..9cb3d15 100644 --- a/src/minio/minio.service.ts +++ b/src/minio/minio.service.ts @@ -19,6 +19,7 @@ export class MinioService { useSSL: this.configService.get('S3_USE_SSL', 'true') === 'true', accessKey: this.configService.get('S3_ACCESS_KEY', ''), secretKey: this.configService.get('S3_SECRET_KEY', ''), + region: this.configService.get('S3_REGION', 'auto'), }); this.defaultBucketName = this.configService.get('S3_BUCKET', ''); }