Migrate to fly.io, add Genre type
This commit is contained in:
@@ -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
18
.github/workflows/fly-deploy.yml
vendored
Normal 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 }}
|
@@ -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
29
fly.toml
Normal 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"
|
@@ -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 {}
|
||||||
|
@@ -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 ?? '',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
@@ -11,116 +11,142 @@ import { Gauge } from 'prom-client';
|
|||||||
import { filter, pipe } from 'ramda';
|
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;
|
||||||
id: string;
|
Genre?: 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 {
|
||||||
private readonly airtableBase: AirtableBase;
|
private readonly airtableBase: AirtableBase;
|
||||||
private readonly logger = new Logger(FocoLiveService.name);
|
private readonly logger = new Logger(FocoLiveService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
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'),
|
||||||
}).base('app1SjPrn5qrhr59J');
|
}).base('app1SjPrn5qrhr59J');
|
||||||
}
|
}
|
||||||
|
|
||||||
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({
|
||||||
id: record.id,
|
view: 'Grid view',
|
||||||
...record.fields
|
})
|
||||||
})) as any as Event[])
|
.all()
|
||||||
.sort(compareDates)
|
).map((record) => ({
|
||||||
.reverse();
|
id: record.id,
|
||||||
}
|
...record.fields,
|
||||||
|
})) as any as Event[]
|
||||||
|
)
|
||||||
|
.sort(compareDates)
|
||||||
|
.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(
|
||||||
if (!events) {
|
cacheKeys.allEvents,
|
||||||
events = await this.getAllEvents();
|
);
|
||||||
this.cacheMisses.inc();
|
if (!events) {
|
||||||
this.cacheManager.set(cacheKeys.allEvents, events, 10 * 60 * 1000);
|
events = await this.getAllEvents();
|
||||||
}
|
this.cacheMisses.inc();
|
||||||
this.eventCount.set(events.length);
|
this.cacheManager.set(cacheKeys.allEvents, events, 10 * 60 * 1000);
|
||||||
return events;
|
|
||||||
}
|
}
|
||||||
|
this.eventCount.set(events.length);
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
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({
|
||||||
id: record.id,
|
view: 'Grid view',
|
||||||
...record.fields
|
})
|
||||||
})) as any as Venue[])
|
.all()
|
||||||
.sort(compareDates)
|
).map((record) => ({
|
||||||
.reverse();
|
id: record.id,
|
||||||
}
|
...record.fields,
|
||||||
|
})) as any as Venue[]
|
||||||
|
)
|
||||||
|
.sort(compareDates)
|
||||||
|
.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(
|
||||||
const events = await this.getAllEventsCached();
|
options: { venue?: string; before?: Date; after?: Date } = {},
|
||||||
const results = pipe(
|
) {
|
||||||
filter((a: Event) => a["Bar or Venue Name"] === (options.venue ?? a["Bar or Venue Name"])),
|
const events = await this.getAllEventsCached();
|
||||||
filter(beforeFilter(options.before)),
|
const results = pipe(
|
||||||
filter(afterFilter(options.after)),
|
filter(
|
||||||
)(events);
|
(a: Event) =>
|
||||||
this.logger.verbose(`Returning ${results.length} events, ${events.length} total events in database.`);
|
a['Bar or Venue Name'] === (options.venue ?? a['Bar or Venue Name']),
|
||||||
return results
|
),
|
||||||
}
|
filter(beforeFilter(options.before)),
|
||||||
|
filter(afterFilter(options.after)),
|
||||||
|
)(events);
|
||||||
|
this.logger.verbose(
|
||||||
|
`Returning ${results.length} events, ${events.length} total events in database.`,
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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', '');
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user