Add initial baby-names service

This commit is contained in:
2025-06-17 17:17:54 -06:00
parent a4c75cfbd2
commit 5a7bad327f
11 changed files with 281 additions and 12 deletions

View File

@@ -33,6 +33,7 @@ import { HoardingModule } from './hoarding/hoarding.module';
import { NamesModule } from './names/names.module';
import { AssemblyAiModule } from './assembly-ai/assembly-ai.module';
import { ClaudeModule } from './claude/claude.module';
import { BabyNamesModule } from './baby-names/baby-names.module';
@Module({
imports: [
@@ -112,6 +113,7 @@ import { ClaudeModule } from './claude/claude.module';
NamesModule,
AssemblyAiModule,
ClaudeModule,
BabyNamesModule,
],
controllers: [AppController],
providers: [

View File

@@ -0,0 +1,80 @@
import {
Body,
Controller,
Get,
Post,
Redirect,
Render,
Req,
Res,
} from '@nestjs/common';
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { BabyNamesService } from './baby-names.service';
import { Request, Response } from 'express';
@Controller('baby-names')
@ApiTags('baby-names')
export class BabyNamesController {
constructor(private readonly babyNamesService: BabyNamesService) {}
@Get()
@Render('baby-names/index')
async index(@Req() request: Request) {
const key = request.cookies['baby-names-key'] || null;
return { previousKey: key };
}
@Get('form')
@Render('baby-names/form')
async form(@Req() request: Request) {
const { key } = request.query;
if (!key) {
throw new Error("Missing 'key' field");
}
const currentIndex = await this.babyNamesService.getCurrentNumber(
key as string,
);
return {
key: key,
name: this.babyNamesService.nameList[currentIndex],
index: currentIndex,
message: request.query.message || null,
};
}
@Post()
@ApiConsumes('multipart/form-data')
@Redirect('/baby-names', 302)
async submit(
@Body()
body: {
key: string;
name: string;
nameindex: string;
opinion: string;
},
@Res({ passthrough: true }) res: Response,
) {
const { key, name, opinion, nameindex } = body;
if (!key) {
throw new Error("Missing 'key' field");
}
await this.babyNamesService.addUserScore(key, name, parseInt(opinion, 10));
await this.babyNamesService.writeUserNumber(
key,
parseInt(nameindex, 10) + 1,
);
res.cookie('baby-names-key', key, { maxAge: 30 * 24 * 60 * 60 * 1000 }); // 30 days
return {
url: `/baby-names/form?key=${key}&message=Logged ${name} as ${opinion}`,
};
}
@Get('data/names.json')
async getNamesData() {
return {
names: this.babyNamesService.nameList,
nameCount: this.babyNamesService.nameCountMap,
};
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { BabyNamesController } from './baby-names.controller';
import { KvService } from 'src/kv/kv.service';
import { MinioService } from 'src/minio/minio.service';
import { BullModule } from '@nestjs/bull';
import { BabyNamesService } from './baby-names.service';
@Module({
providers: [KvService, MinioService, BabyNamesService],
imports: [],
controllers: [BabyNamesController],
})
export class BabyNamesModule {}

View File

@@ -0,0 +1,100 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { KvService } from 'src/kv/kv.service';
import { MinioService } from 'src/minio/minio.service';
export interface Name {
name: string;
year: number;
gender: string;
count: number;
}
export interface NameMap {
[key: string]: Name[];
}
export type NameList = string[];
export interface NameCountMap {
[key: string]: number;
}
@Injectable()
export class BabyNamesService {
public nameList: NameList = [];
public nameCountMap: NameCountMap = {};
private readonly kvNamespace = 'baby-names';
constructor(private readonly minioService: MinioService) {
this.refreshNames();
}
private async refreshNames() {
this.nameCountMap = (
await axios.get('https://cache.sh/baby-name-data/namecount.json')
).data;
this.nameList = (
await axios.get('https://cache.sh/baby-name-data/namecount-list.json')
).data;
}
public async getCurrentNumber(userKey: string): Promise<number> {
const currentKey = await this.minioService
.getBuffer(
this.minioService.defaultBucketName,
`baby-names/${userKey}-current`,
)
.then((buffer) => buffer.toString())
.catch(() => null);
if (currentKey === null) {
await this.writeUserNumber(userKey, 0);
}
const currentNumber = parseInt(currentKey || '0', 10);
return currentNumber + 1;
}
public async writeUserNumber(userKey: string, number: number): Promise<void> {
await this.minioService.uploadBuffer(
this.minioService.defaultBucketName,
`baby-names/${userKey}-current`,
Buffer.from(number.toString()),
);
}
public async getUserScores(userKey: string): Promise<NameCountMap> {
const scoresKey = await this.minioService
.getBuffer(
this.minioService.defaultBucketName,
`baby-names/${userKey}-scores`,
)
.then((buffer) => buffer.toString())
.catch(() => null);
if (scoresKey === null) {
return {};
}
return JSON.parse(scoresKey);
}
public async saveUserScores(
userKey: string,
scores: NameCountMap,
): Promise<void> {
await this.minioService.uploadBuffer(
this.minioService.defaultBucketName,
`baby-names/${userKey}-scores`,
Buffer.from(JSON.stringify(scores)),
);
}
public async addUserScore(
userKey: string,
name: string,
score: number,
): Promise<void> {
const scores = await this.getUserScores(userKey);
scores[name] = score;
await this.saveUserScores(userKey, scores);
}
}

View File

@@ -6,11 +6,7 @@ import { BullModule } from '@nestjs/bull';
@Module({
providers: [KvService, MinioService],
imports: [
BullModule.registerQueue({
name: 'kv',
}),
],
imports: [],
controllers: [KvController],
})
export class KvModule {}

View File

@@ -1,6 +1,4 @@
import { InjectQueue } from '@nestjs/bull';
import { Injectable, Logger } from '@nestjs/common';
import { Queue } from 'bull';
import { UploadedObjectInfo } from 'minio/dist/main/internal/type';
import { MinioService } from 'src/minio/minio.service';
@@ -12,10 +10,7 @@ export class KvService {
private readonly kvMetadataPath = `${this.kvPrefix}/${this.kvMetadataFileName}`;
private readonly logger: Logger = new Logger(KvService.name);
constructor(
private readonly minioService: MinioService,
@InjectQueue('kv') private kvProcessingQueue: Queue,
) {}
constructor(private readonly minioService: MinioService) {}
public generateFilePath(namespace: string, key: string): string {
return `${this.kvPrefix}/${namespace}/${key}`;
@@ -123,7 +118,6 @@ export class KvService {
};
await this.setMetadataFile(Buffer.from(JSON.stringify(metadata)));
this.logger.verbose(`Claimed namespace ${namespace}`);
this.kvProcessingQueue.add('namespaceModeration', metadata[namespace]);
return metadata[namespace];
}

View File

@@ -3,6 +3,7 @@ import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import * as cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
@@ -20,6 +21,7 @@ async function bootstrap() {
app.enableCors({ origin: '*' });
app.useBodyParser('json', { limit: '50mb' });
app.useBodyParser('text', { limit: '50mb' });
app.use(cookieParser());
app.disable('x-powered-by');
await app.listen(3000);
}