Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Backend/src/gists/dto/create-gist.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsLatitude, IsLongitude, IsString, MaxLength, IsOptional, IsInt, Min, Max } from 'class-validator';
import { IsInt, IsLatitude, IsLongitude, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator';

export class CreateGistDto {
@ApiProperty({
Expand Down Expand Up @@ -28,6 +28,15 @@ export class CreateGistDto {
@MaxLength(80)
author?: string;

@ApiPropertyOptional({
description: 'Optional Stellar address alias for the author',
example: 'GABC...XYZ',
})
@IsOptional()
@IsString()
@MaxLength(80)
authorAddress?: string;

@ApiPropertyOptional({
description: 'Time-to-live in hours (default: 24, max: 168)',
example: 24,
Expand Down
48 changes: 33 additions & 15 deletions Backend/src/gists/gists.controller.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import {
Controller,
Get,
Post,
Body,
Param,
Query,
ParseUUIDPipe,
} from '@nestjs/common';
import { Controller, Get, Post, Body, Param, Query, ParseUUIDPipe } from '@nestjs/common';
import { Throttle, SkipThrottle } from '@nestjs/throttler';
import { ApiOperation, ApiTags, ApiParam } from '@nestjs/swagger';
import { GistsService } from './gists.service';
import { CreateGistDto } from './dto/create-gist.dto';
import { QueryGistsDto } from './dto/query-gists.dto';
import { Gist } from './entities/gist.entity';
import { PaginatedResponse } from '../common/utils/pagination.helper';

@ApiTags('gists')
@Controller({ path: 'gists', version: '1' })
Expand All @@ -21,15 +15,16 @@ export class GistsController {
@Post()
@Throttle({ default: { limit: 10, ttl: 60000 } })
@ApiOperation({ summary: 'Post a new anonymous gist at a location' })
create(@Body() dto: CreateGistDto) {
return this.gistsService.create(dto);
async create(@Body() dto: CreateGistDto) {
return this.decorateGist(await this.gistsService.create(dto));
}

@Get()
@SkipThrottle()
@ApiOperation({ summary: 'Find gists near a location' })
findNearby(@Query() query: QueryGistsDto) {
return this.gistsService.findNearby(query);
async findNearby(@Query() query: QueryGistsDto) {
const response = await this.gistsService.findNearby(query);
return this.decoratePaginatedResponse(response);
}

// IMPORTANT: must be registered before @Get(':id') so NestJS does not
Expand All @@ -41,11 +36,34 @@ export class GistsController {
return this.gistsService.countNearby(query);
}

@Get(':id/content')
@SkipThrottle()
@ApiOperation({ summary: 'Get the raw IPFS content for a gist' })
@ApiParam({ name: 'id', description: 'Gist UUID' })
findContent(@Param('id', ParseUUIDPipe) id: string) {
return this.gistsService.getContent(id);
}

@Get(':id')
@SkipThrottle()
@ApiOperation({ summary: 'Get a single gist by ID' })
@ApiParam({ name: 'id', description: 'Gist UUID' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.gistsService.findOne(id);
async findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.decorateGist(await this.gistsService.findOne(id));
}

private decorateGist(gist: Gist) {
return {
...gist,
gist_id: gist.stellar_gist_id,
content_cid: gist.content_hash,
};
}

private decoratePaginatedResponse(response: PaginatedResponse<Gist>) {
return {
...response,
data: response.data.map((gist) => this.decorateGist(gist)),
};
}
}
18 changes: 16 additions & 2 deletions Backend/src/gists/gists.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export class GistsService {
created_at: new Date().toISOString(),
});

const { gistId, txHash } = await this.sorobanService.postGist(locationCell, cid, dto.author);
const author = dto.authorAddress ?? dto.author;
const { gistId, txHash } = await this.sorobanService.postGist(locationCell, cid, author);

this.logger.log(`Gist posted → cell=${locationCell} cid=${cid} gistId=${gistId}`);

Expand All @@ -63,7 +64,7 @@ export class GistsService {
content_hash: cid,
stellar_gist_id: gistId,
tx_hash: txHash,
author_address: dto.author,
author_address: author,
expires_at: expiresAt,
},
manager,
Expand Down Expand Up @@ -101,6 +102,19 @@ export class GistsService {
return gist;
}

async getContent(id: string): Promise<Record<string, unknown>> {
const gist = await this.gistRepository.findByGistId(id);
if (!gist) {
throw new NotFoundException(`Gist with ID ${id} not found`);
}

if (!gist.content_hash) {
throw new NotFoundException(`Content for gist ${id} not found`);
}

return this.ipfsService.getJson(gist.content_hash);
}

async countNearby(query: QueryGistsDto): Promise<CountNearbyResult> {
const { lat, lon, radius = 500, breakdown } = query;

Expand Down
14 changes: 12 additions & 2 deletions Backend/test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { INestApplication, ValidationPipe, VersioningType } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { AllExceptionsFilter } from '../src/common/filters/all-exceptions.filter';

describe('AppModule (e2e)', () => {
let app: INestApplication;
Expand All @@ -12,6 +13,15 @@ describe('AppModule (e2e)', () => {
}).compile();

app = moduleFixture.createNestApplication();
app.enableVersioning({ type: VersioningType.URI });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
app.useGlobalFilters(new AllExceptionsFilter());
await app.init();
});

Expand All @@ -20,6 +30,6 @@ describe('AppModule (e2e)', () => {
});

it('/health (GET)', () => {
return request(app.getHttpServer()).get('/health').expect(200);
return request(app.getHttpServer()).get('/v1/health').expect(200);
});
});
Loading
Loading