shared refactor

This commit is contained in:
compilando 2025-02-25 20:30:00 +01:00
parent 49b7ba2c78
commit 079cde591e
16 changed files with 6592 additions and 90 deletions

View file

@ -66,5 +66,11 @@
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3",
"xss": "^1.0.15"
},
"pnpm": {
"onlyBuiltDependencies": [
"@nestjs/core",
"@scarf/scarf"
]
}
}

6495
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,9 @@
--DROP TABLE IF EXISTS rices;
CREATE TABLE rices (
CREATE TABLE shared (
id UUID NOT NULL, -- Unique identifier
slug VARCHAR(75) NOT NULL, -- Unique user-friendly identifier
type INTEGER DEFAULT 0 NOT NULL, -- Type: 1-WORKSPACE 2-RICE
version VARCHAR(10) NOT NULL, -- Data version
os VARCHAR(30) NOT NULL, -- Operating system
name VARCHAR(75) NOT NULL, -- Name of the rice
@ -19,7 +20,7 @@ CREATE TABLE rices (
CREATE OR REPLACE FUNCTION increment_visits(slug_param TEXT)
RETURNS VOID AS $$
BEGIN
UPDATE rices
UPDATE shared
SET visits = visits + 1
WHERE slug = slug_param;
END;

View file

@ -3,7 +3,7 @@ import { ConfigModule } from '@nestjs/config';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { GitHubModule } from './github/github.module';
import { RicesModule } from './rices/rices.module';
import { SharedModule } from './shared/shared.module';
import { ThrottlerExceptionFilter } from './common/filters/throttler-exception.filter';
@Module({
@ -29,7 +29,7 @@ import { ThrottlerExceptionFilter } from './common/filters/throttler-exception.f
},
]),
GitHubModule,
RicesModule,
SharedModule,
],
controllers: [],
providers: [

View file

@ -1,6 +1,6 @@
import { IsString } from 'class-validator';
export class CreateRiceDto {
export class CreateSharedDto {
@IsString()
name!: string;

View file

@ -1,6 +1,6 @@
import { IsString } from 'class-validator';
export class UpdateRiceDto {
export class UpdateSharedDto {
@IsString()
content!: string;
}

View file

@ -14,24 +14,24 @@ import {
Res,
} from '@nestjs/common';
import { Response } from 'express';
import { RicesService } from './rices.service';
import { SharedService } from './shared.service';
import { ApiTags, ApiOperation, ApiResponse, ApiHeader } from '@nestjs/swagger';
@ApiTags('rices')
@Controller('rices')
export class RicesController {
constructor(private readonly ricesService: RicesService) {}
constructor(private readonly sharedService: SharedService) {}
@ApiOperation({ summary: 'Upload a new Rice' })
@ApiResponse({ status: 201, description: 'Rice successfully created.' })
@ApiHeader({
name: 'X-Zen-Rice-Name',
description: 'Name of the rice',
name: 'x-zen-shared-name',
description: 'Name of the Shared',
required: true,
})
@ApiHeader({
name: 'X-Zen-Rice-Author',
description: 'Author of the rice',
name: 'x-zen-shared-author',
description: 'Author of the Shared',
required: true,
})
@ApiHeader({
@ -43,14 +43,14 @@ export class RicesController {
async createRice(
@Body() content: string,
@Headers() headers: Record<string, string>,
@Headers('x-zen-rices-token') token: string,
@Headers('x-zen-shared-token') token: string,
) {
const contentString =
typeof content === 'string' ? content : JSON.stringify(content);
this.validateFileSize(contentString); // Validate file size
return this.ricesService.create(contentString, token, headers);
return this.sharedService.create(contentString, token, headers);
}
@ApiOperation({ summary: 'Get information about a Rice' })
@ -60,7 +60,7 @@ export class RicesController {
})
@Get(':slug')
async getRice(@Param('slug') slug: string, @Res() res: Response) {
const riceMetadata = await this.ricesService.getRiceMetadata(slug);
const riceMetadata = await this.sharedService.getRiceMetadata(slug);
const htmlContent = `<!DOCTYPE html>
<html lang="en">
@ -94,13 +94,13 @@ export class RicesController {
@ApiOperation({ summary: 'Update an existing Rice' })
@ApiResponse({ status: 200, description: 'Rice successfully updated.' })
@ApiHeader({
name: 'X-Zen-Rice-Name',
description: 'Name of the rice',
name: 'x-zen-shared-name',
description: 'Name of the shared',
required: true,
})
@ApiHeader({
name: 'X-Zen-Rice-Author',
description: 'Author of the rice',
name: 'x-zen-shared-author',
description: 'Author of the shared',
required: true,
})
@ApiHeader({
@ -113,14 +113,14 @@ export class RicesController {
@Param('slug') slug: string,
@Body() content: string,
@Headers() headers: Record<string, string>,
@Headers('x-zen-rices-token') token: string,
@Headers('x-zen-shared-token') token: string,
) {
const contentString =
typeof content === 'string' ? content : JSON.stringify(content);
this.validateFileSize(contentString); // Validate file size
return this.ricesService.update(slug, token, contentString, headers);
return this.sharedService.update(slug, token, contentString, headers);
}
@ApiOperation({ summary: 'Delete an existing Rice' })
@ -129,9 +129,9 @@ export class RicesController {
@Delete(':slug')
async removeRice(
@Param('slug') slug: string,
@Headers('x-zen-rices-token') token: string,
@Headers('x-zen-shared-token') token: string,
) {
await this.ricesService.remove(slug, token);
await this.sharedService.remove(slug, token);
return;
}
@ -150,7 +150,7 @@ export class RicesController {
if (moderationSecret !== process.env.MODERATION_SECRET) {
throw new UnauthorizedException('Invalid moderation secret');
}
await this.ricesService.moderateRemove(slug);
await this.sharedService.moderateRemove(slug);
return;
}

View file

@ -1,15 +1,15 @@
// src/rices/rices.module.ts
import { Module } from '@nestjs/common';
import { RicesService } from './rices.service';
import { SharedService } from './shared.service';
import { GitHubModule } from '../github/github.module';
import { RicesController } from './rices.controller';
import { SupabaseService } from '../supabase/supabase.service';
@Module({
imports: [GitHubModule],
providers: [RicesService, SupabaseService],
providers: [SharedService, SupabaseService],
controllers: [RicesController],
exports: [RicesService],
exports: [SharedService],
})
export class RicesModule {}
export class SharedModule {}

View file

@ -17,7 +17,7 @@ import { SupabaseService } from '../supabase/supabase.service';
const userAgentRegex = /ZenBrowser\/(\d+\.\d\w?\.\d) \((.+)\)/;
@Injectable()
export class RicesService {
export class SharedService {
constructor(
private readonly gitHubService: GitHubService,
private readonly supabaseService: SupabaseService,
@ -31,8 +31,8 @@ export class RicesService {
) {
try {
// Validate headers
const name = headers['x-zen-rice-name'];
const author = headers['x-zen-rice-author'];
const name = headers['x-zen-shared-name'];
const author = headers['x-zen-shared-author'];
const userAgent = headers['user-agent'];
if (!name || !author || !userAgent) {
@ -58,13 +58,13 @@ export class RicesService {
// Validate lengths
if (name.length > 75) {
throw new BadRequestException(
`The value of X-Zen-Rice-Name exceeds the maximum allowed length of 75 characters.`,
`The value of x-zen-shared-name exceeds the maximum allowed length of 75 characters.`,
);
}
if (author.length > 100) {
throw new BadRequestException(
`The value of X-Zen-Rice-Author exceeds the maximum allowed length of 100 characters.`,
`The value of x-zen-shared-author exceeds the maximum allowed length of 100 characters.`,
);
}
@ -113,7 +113,7 @@ export class RicesService {
'MAX_RICES_BY_TOKEN',
5,
);
const tokenCount = await this.supabaseService.countRicesByToken(token);
const tokenCount = await this.supabaseService.countSharedByToken(token);
if (tokenCount >= tokenMaxCount) {
throw new ConflictException(
`The token '${token}' is already associated with 5 or more rices.`,
@ -135,7 +135,7 @@ export class RicesService {
};
// Insert metadata into Supabase
await this.supabaseService.insertRice(metadata);
await this.supabaseService.insertShared(metadata);
const uploadedFilePath = `rices/${slug}/data.zenrice`;
await this.gitHubService.createOrUpdateFile(
@ -153,7 +153,7 @@ export class RicesService {
async findOne(slug: string) {
// Check if the rice exists in the database
const rice = await this.supabaseService.getRiceBySlug(slug);
const rice = await this.supabaseService.getSharedBySlug(slug);
if (!rice) throw new NotFoundException('Rice not found');
// Fetch the file from GitHub
@ -171,7 +171,7 @@ export class RicesService {
}
async getRiceMetadata(slug: string) {
const rice = await this.supabaseService.getRiceBySlug(slug);
const rice = await this.supabaseService.getSharedBySlug(slug);
if (!rice) throw new NotFoundException('Rice not found');
return rice;
@ -216,7 +216,7 @@ export class RicesService {
const [, version, os] = match;
// Check if the rice exists
const rice = await this.supabaseService.getRiceBySlug(slug);
const rice = await this.supabaseService.getSharedBySlug(slug);
if (!rice) {
throw new NotFoundException('Rice not found');
}
@ -233,7 +233,7 @@ export class RicesService {
updated_at: new Date().toISOString(),
};
await this.supabaseService.updateRice(slug, updatedMetadata);
await this.supabaseService.updateShared(slug, updatedMetadata);
const uploadedFilePath = `rices/${slug}/data.zenrice`;
await this.gitHubService.createOrUpdateFile(
@ -250,7 +250,7 @@ export class RicesService {
}
async remove(slug: string, token: string): Promise<void> {
const rice = await this.supabaseService.getRiceBySlug(slug);
const rice = await this.supabaseService.getSharedBySlug(slug);
if (!rice) throw new NotFoundException('Rice not found');
if (rice.token !== token) throw new UnauthorizedException('Invalid token');
@ -259,7 +259,7 @@ export class RicesService {
throw new UnauthorizedException('Invalid token.');
}
await this.supabaseService.deleteRice(slug);
await this.supabaseService.deleteShared(slug);
const folderPath = `rices/${slug}`;
@ -289,13 +289,13 @@ export class RicesService {
public async moderateRemove(slug: string): Promise<void> {
try {
// 1. Check if rice exists in Supabase
const rice = await this.supabaseService.getRiceBySlug(slug);
const rice = await this.supabaseService.getSharedBySlug(slug);
if (!rice) {
throw new NotFoundException('Rice not found');
}
// 2. Delete metadata from Supabase
await this.supabaseService.deleteRice(slug);
await this.supabaseService.deleteShared(slug);
// 3. Delete data.zenrice from GitHub
const riceJsonPath = `rices/${slug}/data.zenrice`;

View file

@ -17,42 +17,42 @@ export class SupabaseService {
this.supabase = createClient(this.supabase_url, this.supabase_key);
}
async insertRice(metadata: any) {
const { error } = await this.supabase.from('rices').insert(metadata);
async insertShared(metadata: any) {
const { error } = await this.supabase.from('shared').insert(metadata);
if (error) {
this.logger.error(
`Failed to insert rice: ${error.message}`,
`Failed to insert shared: ${error.message}`,
error.details,
);
throw new Error(`Failed to insert rice: ${error.message}`);
throw new Error(`Failed to insert shared: ${error.message}`);
}
}
async getRiceById(id: string) {
async getSharedById(id: string) {
const { data, error } = await this.supabase
.from('rices')
.from('shared')
.select('*')
.eq('id', id)
.single();
if (error) {
this.logger.error(
`Failed to fetch rice with ID ${id}: ${error.message}`,
`Failed to fetch shared with ID ${id}: ${error.message}`,
error.details,
);
throw new Error(`Failed to fetch rice: ${error.message}`);
throw new Error(`Failed to fetch shared: ${error.message}`);
}
return data;
}
async getRiceBySlug(slug: string) {
async getSharedBySlug(slug: string) {
const { data, error } = await this.supabase
.from('rices')
.from('shared')
.select('*')
.eq('slug', slug)
.single();
if (error) {
this.logger.error(
`Failed to fetch rice with slug ${slug}: ${error.message}`,
`Failed to fetch shared with slug ${slug}: ${error.message}`,
error.details,
);
return null;
@ -60,48 +60,48 @@ export class SupabaseService {
return data;
}
async getRiceByName(name: string) {
async getSharedByName(name: string) {
const { data, error } = await this.supabase
.from('rices')
.from('shared')
.select('*')
.eq('name', name)
.single();
if (error && error.code !== 'PGRST116') {
// Handle "no rows found" separately
this.logger.error(
`Failed to fetch rice with name ${name}: ${error.message}`,
`Failed to fetch shared with name ${name}: ${error.message}`,
error.details,
);
throw new Error(`Failed to fetch rice: ${error.message}`);
throw new Error(`Failed to fetch shared: ${error.message}`);
}
return data;
}
async updateRice(slug: string, metadata: any) {
async updateShared(slug: string, metadata: any) {
const { error } = await this.supabase
.from('rices')
.from('shared')
.update(metadata)
.eq('slug', slug);
if (error) {
this.logger.error(
`Failed to update rice with slug ${slug}: ${error.message}`,
`Failed to update shared with slug ${slug}: ${error.message}`,
error.details,
);
throw new Error(`Failed to update rice: ${error.message}`);
throw new Error(`Failed to update shared: ${error.message}`);
}
}
async deleteRice(slug: string) {
async deleteShared(slug: string) {
const { error } = await this.supabase
.from('rices')
.from('shared')
.delete()
.eq('slug', slug);
if (error) {
this.logger.error(
`Failed to delete rice with slug ${slug}: ${error.message}`,
`Failed to delete shared with slug ${slug}: ${error.message}`,
error.details,
);
throw new Error(`Failed to delete rice: ${error.message}`);
throw new Error(`Failed to delete shared: ${error.message}`);
}
}
@ -112,7 +112,7 @@ export class SupabaseService {
if (error) {
this.logger.error(
`Failed to increment visits for rice with slug ${slug}: ${error.message}`,
`Failed to increment visits for shared with slug ${slug}: ${error.message}`,
error.details,
);
throw new Error(`Failed to increment visits: ${error.message}`);
@ -121,27 +121,27 @@ export class SupabaseService {
async updateLevel(slug: string, level: number) {
const { error } = await this.supabase
.from('rices')
.from('shared')
.update({ level })
.eq('slug', slug);
if (error) {
this.logger.error(
`Failed to update level for rice with slug ${slug}: ${error.message}`,
`Failed to update level for shared with slug ${slug}: ${error.message}`,
error.details,
);
throw new Error(`Failed to update rice level: ${error.message}`);
throw new Error(`Failed to update shared level: ${error.message}`);
}
}
async countRicesByToken(token: string): Promise<number> {
async countSharedByToken(token: string): Promise<number> {
const { data, error, count } = await this.supabase
.from('rices') // Nombre de tu tabla en Supabase
.from('shared') // Nombre de tu tabla en Supabase
.select('*', { count: 'exact' })
.eq('token', token);
if (error) {
console.error('Error counting rices by token:', error);
throw new Error('Failed to count rices by token');
console.error('Error counting shared by token:', error);
throw new Error('Failed to count shared by token');
}
return count || 0;

View file

@ -48,7 +48,7 @@ describe('Rices API E2E', () => {
expect(slug).toBeDefined();
expect(token).toBeDefined();
const riceInDatabase = await supabaseService.getRiceBySlug(slug);
const riceInDatabase = await supabaseService.getSharedBySlug(slug);
expect(riceInDatabase).not.toBeNull();
expect(riceInDatabase.name).toBe('Test Rice');
@ -67,12 +67,12 @@ describe('Rices API E2E', () => {
const { slug } = createResponse.body;
const initialData = await supabaseService.getRiceBySlug(slug);
const initialData = await supabaseService.getSharedBySlug(slug);
expect(initialData.visits).toBe(0);
await request(app.getHttpServer()).get(`/rices/${slug}`).expect(200);
const updatedData = await supabaseService.getRiceBySlug(slug);
const updatedData = await supabaseService.getSharedBySlug(slug);
expect(updatedData.visits).toBe(1);
});
@ -87,14 +87,14 @@ describe('Rices API E2E', () => {
const updateResponse = await request(app.getHttpServer())
.put(`/rices/${slug}`)
.set('x-zen-rices-token', token)
.set('x-zen-shared-token', token)
.field('name', 'Updated Rice')
.attach('file', path.join(__dirname, 'files', 'example_update.zenrice'))
.expect(200);
expect(updateResponse.body.message).toBe(`ok`);
const updatedData = await supabaseService.getRiceBySlug(slug);
const updatedData = await supabaseService.getSharedBySlug(slug);
expect(updatedData.name).toBe('Updated Rice');
const updatedFile = await gitHubService.getFileContent(
@ -116,10 +116,10 @@ describe('Rices API E2E', () => {
await request(app.getHttpServer())
.delete(`/rices/${slug}`)
.set('x-zen-rices-token', token)
.set('x-zen-shared-token', token)
.expect(204);
const riceInDatabase = await supabaseService.getRiceBySlug(slug);
const riceInDatabase = await supabaseService.getSharedBySlug(slug);
expect(riceInDatabase).toBeNull();
const fileInGitHub = await gitHubService.getFileContent(
@ -142,7 +142,7 @@ describe('Rices API E2E', () => {
.set('x-moderation-secret', moderationSecret)
.expect(204);
const riceInDatabase = await supabaseService.getRiceBySlug(slug);
const riceInDatabase = await supabaseService.getSharedBySlug(slug);
expect(riceInDatabase).toBeNull();
const fileInGitHub = await gitHubService.getFileContent(
@ -165,7 +165,7 @@ describe('Rices API E2E', () => {
.set('x-moderation-secret', 'wrongSecret')
.expect(401);
const riceInDatabase = await supabaseService.getRiceBySlug(slug);
const riceInDatabase = await supabaseService.getSharedBySlug(slug);
expect(riceInDatabase).not.toBeNull();
});
});

View file

@ -2,8 +2,8 @@
POST {{baseUrl}}/rices
Content-Type: application/json
X-Zen-Rice-Name: cool-zenrice-test-base
X-Zen-Rice-Author: jhon@doe.com
x-zen-shared-name: cool-zenrice-test-base
x-zen-shared-author: jhon@doe.com
User-Agent: ZenBrowser/1.2b.0 (EndeavourOS x86_64)
{

View file

@ -4,9 +4,9 @@
POST {{baseUrl}}/rices
Content-Type: application/json
X-Zen-Rice-Name: cool-zenrice-test-base5
X-Zen-Rice-Author: jhon@doe.com
x-zen-rices-token: {{previous_token}}
x-zen-shared-name: cool-zenrice-test-base
x-zen-shared-author: jhon@doe.com
x-zen-shared-token: {{previous_token}}
User-Agent: ZenBrowser/1.2b.0 (EndeavourOS x86_64)
{

View file

@ -4,7 +4,7 @@
PUT {{baseUrl}}/rices/{{previous_slug}}
Content-Type: application/json
x-zen-rices-token: {{previous_token}}
x-zen-shared-token: {{previous_token}}
User-Agent: ZenBrowser/1.2b.0 (EndeavourOS x86_64)
{

View file

@ -3,4 +3,4 @@
@previous_token = 03fbfdb4-d3a5-4d64-8740-feac7d32e7a8
DELETE {{baseUrl}}/rices/{{previous_slug}}
x-zen-rices-token: {{previous_token}}
x-zen-shared-token: {{previous_token}}