Migrate to fly.io, add Genre type

This commit is contained in:
2024-09-01 11:28:37 -06:00
parent c25a5230d9
commit 758cf386b4
8 changed files with 192 additions and 117 deletions

View File

@@ -33,3 +33,6 @@ lerna-debug.log*
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
# fly.io
fly.toml

18
.github/workflows/fly-deploy.yml vendored Normal file
View File

@@ -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 }}

View File

@@ -21,8 +21,6 @@ services:
- db_net - db_net
ports: ports:
- 6379:6379 - 6379:6379
command: >
--requirepass ${REDIS_PASS}
env_file: env_file:
- .env - .env
networks: networks:

29
fly.toml Normal file
View File

@@ -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"

View File

@@ -37,16 +37,17 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
RedisModule.forRootAsync({ RedisModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: async (configService: ConfigService): Promise<RedisModuleOptions> => { useFactory: async (
configService: ConfigService,
): Promise<RedisModuleOptions> => {
return { return {
config: { config: {
host: configService.get<string>('redis.host'), url: configService.get<string>('redis.url'),
port: configService.get<number>('redis.port'),
password: configService.get<string>('redis.password'),
db: configService.get<number>('redis.db'), db: configService.get<number>('redis.db'),
} family: 6,
},
}; };
} },
}), }),
CacheModule.registerAsync<RedisClientOptions>({ CacheModule.registerAsync<RedisClientOptions>({
isGlobal: true, isGlobal: true,
@@ -54,12 +55,9 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
imports: [ConfigModule], imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
store: redisStore, store: redisStore,
socket: { url: configService.get<string>('redis.url'),
host: configService.get<string>('redis.host'), family: 6,
port: configService.get<number>('redis.port'),
},
database: configService.get<number>('redis.db'), database: configService.get<number>('redis.db'),
password: configService.get<string>('redis.password'),
}), }),
}), }),
BullModule.forRootAsync({ BullModule.forRootAsync({
@@ -68,10 +66,9 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
useFactory: async (configService: ConfigService) => { useFactory: async (configService: ConfigService) => {
const config = { const config = {
redis: { redis: {
password: configService.get<string>('redis.password'), url: configService.get<string>('redis.url'),
host: configService.get<string>('redis.host'),
port: configService.get<number>('redis.port'),
db: configService.get<number>('redis.db'), db: configService.get<number>('redis.db'),
family: 6,
}, },
}; };
return config; return config;
@@ -103,4 +100,4 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
}) })
export class AppModule { } export class AppModule {}

View File

@@ -7,7 +7,7 @@ export default () => ({
userAgent: 'api.us.dev/@chip@talking.dev', userAgent: 'api.us.dev/@chip@talking.dev',
rapidApiKey: process.env.RAPID_API_KEY || '', rapidApiKey: process.env.RAPID_API_KEY || '',
irc: { irc: {
enabled: process.env.IRC_SERVER === "" ? false : true, enabled: process.env.IRC_SERVER === '' ? false : true,
server: process.env.IRC_SERVER, server: process.env.IRC_SERVER,
tls: process.env.IRC_TLS === 'true', tls: process.env.IRC_TLS === 'true',
port: parseInt(process.env.IRC_PORT ?? '6697'), port: parseInt(process.env.IRC_PORT ?? '6697'),
@@ -19,19 +19,22 @@ export default () => ({
: 'us-dev', : 'us-dev',
}, },
redis: { redis: {
host: process.env.REDIS_HOST ?? 'redis-master', url: process.env.REDIS_URL ?? '',
port: parseInt(process.env.REDIS_PORT ?? '6379'),
password: process.env.REDIS_PASS ?? '',
db: parseInt(process.env.REDIS_DB ?? '1'), db: parseInt(process.env.REDIS_DB ?? '1'),
}, },
file: { 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 // 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: { focoLive: {
airtable: { airtable: {
apiKey: process.env.FOCO_LIVE_AIRTABLE_APIKEY ?? '', apiKey: process.env.FOCO_LIVE_AIRTABLE_APIKEY ?? '',
} },
} },
}); });

View File

@@ -13,44 +13,48 @@ import { filter, pipe } from 'ramda';
const tables = { const tables = {
venues: 'tblRi4wDorKqNJJbs', venues: 'tblRi4wDorKqNJJbs',
events: 'tbl4RZ75QF5WefE7L', 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 beforeFilter = (before?: Date) => (a: Event) =>
const afterFilter = (after?: Date) => (a: Event) => after ? new Date(a.Date) >= after : true; before ? new Date(a.Date) <= before : true;
const afterFilter = (after?: Date) => (a: Event) =>
after ? new Date(a.Date) >= after : true;
export interface Event { export interface Event {
"Date": string, Date: string;
"Music Start Time": string, 'Music Start Time': string;
"Bar or Venue Name": string, 'Bar or Venue Name': string;
"Band/DJ/Musician Name": string, 'Band/DJ/Musician Name': string;
"Cost Select": string; 'Cost Select': string;
Cost: string, Cost: string;
"Date Select": string 'Date Select': string;
'"Specials" at Venue': string, '"Specials" at Venue': string;
Genre?: string;
id: string; id: string;
} }
export interface Venue { export interface Venue {
id: string; id: string;
"Bar or Venue Name": string, 'Bar or Venue Name': string;
"Street Address": string, 'Street Address': string;
"City": string, City: string;
"Zip Code": number, 'Zip Code': number;
State: string, State: string;
"Phone Number": string, 'Phone Number': string;
Website: string, Website: string;
"Has Calendar Of Events": string, 'Has Calendar Of Events': string;
"Facebook Page": string, 'Facebook Page': string;
"Instagram": string, Instagram: string;
"Twitter Account": string, 'Twitter Account': string;
} }
const cacheKeys = { const cacheKeys = {
allEvents: 'foco_live_events', allEvents: 'foco_live_events',
allVenues: 'foco_live_venues', allVenues: 'foco_live_venues',
} };
@Injectable() @Injectable()
export class FocoLiveService { export class FocoLiveService {
@@ -61,7 +65,7 @@ export class FocoLiveService {
private readonly config: ConfigService, private readonly config: ConfigService,
@InjectMetric('event_count') public eventCount: Gauge<string>, @InjectMetric('event_count') public eventCount: Gauge<string>,
@InjectMetric('event_cache_misses') public cacheMisses: Gauge<string>, @InjectMetric('event_cache_misses') public cacheMisses: Gauge<string>,
@Inject(CACHE_MANAGER) private cacheManager: Cache @Inject(CACHE_MANAGER) private cacheManager: Cache,
) { ) {
this.airtableBase = new Airtable({ this.airtableBase = new Airtable({
apiKey: config.get('focoLive.airtable.apiKey'), apiKey: config.get('focoLive.airtable.apiKey'),
@@ -69,19 +73,26 @@ export class FocoLiveService {
} }
async getAllEvents(): Promise<Event[]> { async getAllEvents(): Promise<Event[]> {
return ((await this.airtableBase('Events').select({ return (
view: "Grid view", (
}).all()) await this.airtableBase('Events')
.map(record => ({ .select({
view: 'Grid view',
})
.all()
).map((record) => ({
id: record.id, id: record.id,
...record.fields ...record.fields,
})) as any as Event[]) })) as any as Event[]
)
.sort(compareDates) .sort(compareDates)
.reverse(); .reverse();
} }
async getAllEventsCached(): Promise<Event[]> { async getAllEventsCached(): Promise<Event[]> {
let events: Event[] | null | undefined = await this.cacheManager.get(cacheKeys.allEvents); let events: Event[] | null | undefined = await this.cacheManager.get(
cacheKeys.allEvents,
);
if (!events) { if (!events) {
events = await this.getAllEvents(); events = await this.getAllEvents();
this.cacheMisses.inc(); this.cacheMisses.inc();
@@ -92,35 +103,50 @@ export class FocoLiveService {
} }
async getAllVenues(): Promise<Venue[]> { async getAllVenues(): Promise<Venue[]> {
return ((await this.airtableBase('Venues').select({ return (
view: "Grid view", (
}).all()) await this.airtableBase('Venues')
.map(record => ({ .select({
view: 'Grid view',
})
.all()
).map((record) => ({
id: record.id, id: record.id,
...record.fields ...record.fields,
})) as any as Venue[]) })) as any as Venue[]
)
.sort(compareDates) .sort(compareDates)
.reverse(); .reverse();
} }
async getAllVenuesCached(): Promise<Venue[]> { async getAllVenuesCached(): Promise<Venue[]> {
return await this.cacheManager.get(cacheKeys.allVenues) || await this.getAllVenues(); return (
(await this.cacheManager.get(cacheKeys.allVenues)) ||
(await this.getAllVenues())
);
} }
@Cron("0 */5 * * * *") @Cron('0 */5 * * * *')
async refreshEvents() { async refreshEvents() {
this.logger.verbose("Refreshing events cache."); this.logger.verbose('Refreshing events cache.');
await this.getAllEventsCached(); await this.getAllEventsCached();
} }
async getEvents(options: { venue?: string, before?: Date, after?: Date } = {}) { async getEvents(
options: { venue?: string; before?: Date; after?: Date } = {},
) {
const events = await this.getAllEventsCached(); const events = await this.getAllEventsCached();
const results = pipe( const results = pipe(
filter((a: Event) => a["Bar or Venue Name"] === (options.venue ?? a["Bar or Venue Name"])), filter(
(a: Event) =>
a['Bar or Venue Name'] === (options.venue ?? a['Bar or Venue Name']),
),
filter(beforeFilter(options.before)), filter(beforeFilter(options.before)),
filter(afterFilter(options.after)), filter(afterFilter(options.after)),
)(events); )(events);
this.logger.verbose(`Returning ${results.length} events, ${events.length} total events in database.`); this.logger.verbose(
return results `Returning ${results.length} events, ${events.length} total events in database.`,
);
return results;
} }
} }

View File

@@ -19,6 +19,7 @@ export class MinioService {
useSSL: this.configService.get<string>('S3_USE_SSL', 'true') === 'true', useSSL: this.configService.get<string>('S3_USE_SSL', 'true') === 'true',
accessKey: this.configService.get<string>('S3_ACCESS_KEY', ''), accessKey: this.configService.get<string>('S3_ACCESS_KEY', ''),
secretKey: this.configService.get<string>('S3_SECRET_KEY', ''), secretKey: this.configService.get<string>('S3_SECRET_KEY', ''),
region: this.configService.get<string>('S3_REGION', 'auto'),
}); });
this.defaultBucketName = this.configService.get<string>('S3_BUCKET', ''); this.defaultBucketName = this.configService.get<string>('S3_BUCKET', '');
} }