From 8973ff7f46803ca74cc9f4f3a6c6c539ea24a133 Mon Sep 17 00:00:00 2001 From: selimyanat Date: Fri, 28 Feb 2025 16:51:15 +0100 Subject: [PATCH 1/3] init project --- .github/workflows/ci.yml | 79 +++++++++++++++++++ api/src/app.controller.spec.ts | 22 ------ api/src/app.controller.ts | 13 --- api/src/app.module.ts | 10 +-- .../shorten-url.controller.spec.ts | 35 ++++++++ api/src/shorten-url/shorten-url.controller.ts | 12 +++ .../shorten-url.id-generator.service.ts} | 50 ++++++------ api/src/shorten-url/shorten-url.module.ts | 12 +++ .../shorten-url.usecase.ts} | 16 ++-- webapp/app/api/url-shortener/route.ts | 29 +++++-- .../url-shortener/ui/URLShortener.test.jsx | 2 +- 11 files changed, 196 insertions(+), 84 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 api/src/app.controller.spec.ts delete mode 100644 api/src/app.controller.ts create mode 100644 api/src/shorten-url/shorten-url.controller.spec.ts create mode 100644 api/src/shorten-url/shorten-url.controller.ts rename api/src/{app.global-id-generator.service.ts => shorten-url/shorten-url.id-generator.service.ts} (79%) create mode 100644 api/src/shorten-url/shorten-url.module.ts rename api/src/{app.service.ts => shorten-url/shorten-url.usecase.ts} (53%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..04365dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI Pipeline + +on: + push: + branches: + - main + - "feature/**" # Trigger workflow for feature branches + - "feat/**" # Trigger workflow for feature branches + - "doc/**" # Trigger workflow for feature branches + - "chore/**" # Trigger workflow for feature branches + - "fix/**" # Trigger workflow for feature branches + pull_request: + branches: + - main + +jobs: + + api: + name: API Build & Test + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: yarn + cache-dependency-path: api/yarn-lock.json # Specify the path to your yarn.lock + + - name: Install Dependencies (API) + working-directory: api + run: yarn install --frozen-lockfile + + #- name: Lint API + # run: npm run lint + + - name: Run Tests (API) + run: yarn test + + # Webapp (Next.js) Build & Test + webapp: + name: Webapp Build & Test + runs-on: ubuntu-latest + defaults: + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: yarn + cache-dependency-path: webapp/yarn-lock.json + + - name: Install Dependencies (Webapp) + run: yarn install --frozen-lockfile + + - name: Lint Webapp + run: yarn lint + + #- name: Run Tests (Webapp) + # run: npm test + + - name: Build Webapp + run: yarn build + + # Merge Blocker: Ensure Both Builds Pass + merge_guard: + name: Ensure All Jobs Passed + needs: [api, webapp] + runs-on: ubuntu-latest + steps: + - name: Check for Failures + run: echo "All jobs passed!" diff --git a/api/src/app.controller.spec.ts b/api/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/api/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/api/src/app.controller.ts b/api/src/app.controller.ts deleted file mode 100644 index b23fd4d..0000000 --- a/api/src/app.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - // return a json object with a field called shorted-url - return this.appService.createTinyURL('https://zapper.xyz'); - } -} diff --git a/api/src/app.module.ts b/api/src/app.module.ts index 60ec5c2..3d05aff 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -1,12 +1,10 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { AppGlobalIdGeneratorService } from './app.global-id-generator.service'; +import { ShortenUrlModule } from './shorten-url/shorten-url.module'; @Module({ - imports: [ConfigModule.forRoot()], // Load environment variables - controllers: [AppController], - providers: [AppGlobalIdGeneratorService, AppService], + imports: [ConfigModule.forRoot(), ShortenUrlModule], // Load environment variables + controllers: [], + providers: [], }) export class AppModule {} diff --git a/api/src/shorten-url/shorten-url.controller.spec.ts b/api/src/shorten-url/shorten-url.controller.spec.ts new file mode 100644 index 0000000..f8f851d --- /dev/null +++ b/api/src/shorten-url/shorten-url.controller.spec.ts @@ -0,0 +1,35 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ShortenUrlController } from './shorten-url.controller'; +import { ShortenUrlUsecase } from './shorten-url.usecase'; +import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service'; +import { ConfigModule } from '@nestjs/config'; + +describe('ShortenUrl controller', () => { + let underTest: ShortenUrlController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + imports: [await ConfigModule.forRoot()], + controllers: [ShortenUrlController], + providers: [ShortenUrlIdGeneratorService, ShortenUrlUsecase], + }).compile(); + + underTest = app.get(ShortenUrlController); + }); + + describe('shortenUrl', () => { + it('should return a valid Base62 short URL with 10 chars length', async () => { + const longUrl = + 'https://zapper.xyz/very-long-url/very-long-url/very-long-url'; + const shortenedUrl = await underTest.shortenUrl(longUrl); // Assuming it's async + + // Extract the unique ID part of the URL + const urlPattern = /^https:\/\/zapper\.xyz\/([A-Za-z0-9]{10})$/; + const match = shortenedUrl.match(urlPattern); + + expect(shortenedUrl).not.toBeNull(); + expect(match).not.toBeNull(); // Ensure it matches the pattern + expect(match![1].length).toBe(10); // Validate the ID part has 10 characters + }); + }); +}); diff --git a/api/src/shorten-url/shorten-url.controller.ts b/api/src/shorten-url/shorten-url.controller.ts new file mode 100644 index 0000000..d0dbe8a --- /dev/null +++ b/api/src/shorten-url/shorten-url.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Post } from '@nestjs/common'; +import { ShortenUrlUsecase } from './shorten-url.usecase'; + +@Controller('/shorten-url') +export class ShortenUrlController { + constructor(private readonly shortenUrlUsecase: ShortenUrlUsecase) {} + + @Post() + shortenUrl(url: string): string { + return this.shortenUrlUsecase.createTinyURL('https://zapper.xyz'); + } +} diff --git a/api/src/app.global-id-generator.service.ts b/api/src/shorten-url/shorten-url.id-generator.service.ts similarity index 79% rename from api/src/app.global-id-generator.service.ts rename to api/src/shorten-url/shorten-url.id-generator.service.ts index dc2ca73..fa5d6b9 100644 --- a/api/src/app.global-id-generator.service.ts +++ b/api/src/shorten-url/shorten-url.id-generator.service.ts @@ -12,43 +12,35 @@ import { ConfigService } from '@nestjs/config'; * The machineId ID is a custom value that should be unique for each instance of the generator. */ @Injectable() -export class AppGlobalIdGeneratorService { +export class ShortenUrlIdGeneratorService { private static readonly MACHINE_ID_BITS = 10; private static readonly SEQUENCE_BITS = 12; - + // indexing starts at 0: 2^(MACHINE_ID_BITS) - 1 + private static readonly MAX_MACHINE_ID = + (1 << ShortenUrlIdGeneratorService.MACHINE_ID_BITS) - 1; + // indexing starts at 0: 2^(SEQUENCE_BITS) - 1 + private static readonly MAX_SEQUENCE = + (1 << ShortenUrlIdGeneratorService.SEQUENCE_BITS) - 1; private readonly epoch: number = 1609459200000; // TODO must be configurable private readonly machineId: number = 1; private sequence: number; private lastTimestamp: number; - // indexing starts at 0: 2^(MACHINE_ID_BITS) - 1 - private static readonly MAX_MACHINE_ID = - (1 << AppGlobalIdGeneratorService.MACHINE_ID_BITS) - 1; - // indexing starts at 0: 2^(SEQUENCE_BITS) - 1 - private static readonly MAX_SEQUENCE = - (1 << AppGlobalIdGeneratorService.SEQUENCE_BITS) - 1; - constructor(private readonly configService: ConfigService) { this.machineId = Number(this.configService.get('MACHINE_ID')); if ( this.machineId < 0 || - this.machineId > AppGlobalIdGeneratorService.MAX_MACHINE_ID + this.machineId > ShortenUrlIdGeneratorService.MAX_MACHINE_ID ) { throw new Error( - `Invalid MACHINE_ID. Must be between 0 and ${AppGlobalIdGeneratorService.MAX_MACHINE_ID}.`, + `Invalid MACHINE_ID. Must be between 0 and ${ShortenUrlIdGeneratorService.MAX_MACHINE_ID}.`, ); } this.sequence = 0; this.lastTimestamp = -1; } - private waitUntilNextMillisecond(): void { - while (this.lastTimestamp <= Date.now()) { - this.lastTimestamp = Date.now(); - } - } - /** * Generate a new unique ID within the current timestamp using the Snowflake algorithm. If the number * of ids generated in the same millisecond exceeds the maximum sequence number, the function will wait @@ -67,7 +59,7 @@ export class AppGlobalIdGeneratorService { if (currentTimestamp === this.lastTimestamp) { this.sequence = - (this.sequence + 1) & AppGlobalIdGeneratorService.MAX_SEQUENCE; + (this.sequence + 1) & ShortenUrlIdGeneratorService.MAX_SEQUENCE; if (this.sequence == 0) { this.waitUntilNextMillisecond(); } @@ -84,15 +76,21 @@ export class AppGlobalIdGeneratorService { // prettier-ignore const id = - (BigInt(currentTimestamp - this.epoch) << - BigInt( - AppGlobalIdGeneratorService.MACHINE_ID_BITS + - AppGlobalIdGeneratorService.SEQUENCE_BITS, - )) | - (BigInt(this.machineId) << - BigInt(AppGlobalIdGeneratorService.SEQUENCE_BITS)) | - BigInt(this.sequence); + (BigInt(currentTimestamp - this.epoch) << + BigInt( + ShortenUrlIdGeneratorService.MACHINE_ID_BITS + + ShortenUrlIdGeneratorService.SEQUENCE_BITS, + )) | + (BigInt(this.machineId) << + BigInt(ShortenUrlIdGeneratorService.SEQUENCE_BITS)) | + BigInt(this.sequence); return id; } + + private waitUntilNextMillisecond(): void { + while (this.lastTimestamp <= Date.now()) { + this.lastTimestamp = Date.now(); + } + } } diff --git a/api/src/shorten-url/shorten-url.module.ts b/api/src/shorten-url/shorten-url.module.ts new file mode 100644 index 0000000..c72c267 --- /dev/null +++ b/api/src/shorten-url/shorten-url.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ShortenUrlController } from './shorten-url.controller'; +import { ShortenUrlUsecase } from './shorten-url.usecase'; +import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service'; + +@Module({ + imports: [ConfigModule.forRoot()], // Load environment variables + controllers: [ShortenUrlController], + providers: [ShortenUrlIdGeneratorService, ShortenUrlUsecase], +}) +export class ShortenUrlModule {} diff --git a/api/src/app.service.ts b/api/src/shorten-url/shorten-url.usecase.ts similarity index 53% rename from api/src/app.service.ts rename to api/src/shorten-url/shorten-url.usecase.ts index 710fc00..8609c8e 100644 --- a/api/src/app.service.ts +++ b/api/src/shorten-url/shorten-url.usecase.ts @@ -1,14 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { AppGlobalIdGeneratorService } from './app.global-id-generator.service'; +import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service'; @Injectable() -export class AppService { +export class ShortenUrlUsecase { private static BASE62_CHARACTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - private static BASE62 = AppService.BASE62_CHARACTERS.length; + private static BASE62 = ShortenUrlUsecase.BASE62_CHARACTERS.length; - constructor(private readonly idGenerator: AppGlobalIdGeneratorService) {} + constructor(private readonly idGenerator: ShortenUrlIdGeneratorService) {} createTinyURL(originalURL: string): string { const id = this.idGenerator.generateId(); @@ -20,13 +20,13 @@ export class AppService { } private encodeBase62(id: number): string { - if (id === 0) return AppService.BASE62_CHARACTERS[0]; + if (id === 0) return ShortenUrlUsecase.BASE62_CHARACTERS[0]; let encoded = ''; while (id > 0) { - const remainder = id % AppService.BASE62; - encoded = AppService.BASE62_CHARACTERS[remainder] + encoded; - id = Math.floor(id / AppService.BASE62); + const remainder = id % ShortenUrlUsecase.BASE62; + encoded = ShortenUrlUsecase.BASE62_CHARACTERS[remainder] + encoded; + id = Math.floor(id / ShortenUrlUsecase.BASE62); } return encoded; } diff --git a/webapp/app/api/url-shortener/route.ts b/webapp/app/api/url-shortener/route.ts index b968ba9..952cdf4 100644 --- a/webapp/app/api/url-shortener/route.ts +++ b/webapp/app/api/url-shortener/route.ts @@ -15,13 +15,26 @@ export async function POST(request: NextRequest) { // Example function to simulate URL shortening logic async function shortenUrl(url: string): Promise { - // Replace this with your actual URL shortening implementation - // call an http service, generate a random string, etc - const response = await fetch('http://localhost:3001'); - if (!response.ok) { - throw new Error('Failed to shorten URL'); + try { + const response = await fetch('http://localhost:3001/shorten-url', { + method: 'POST', // Use POST method + headers: { + 'Content-Type': 'application/json', // Inform the server we are sending JSON + }, + body: JSON.stringify({ url }), // Send URL in request body + }); + + console.log(response); + + if (!response.ok) { + throw new Error(`Failed to shorten URL: ${response.statusText}`); + } + + const data = await response.text(); // Assume server returns JSON + console.log('URL shortened successfully:', data); + return data; // Adjust this based on the API response structure + } catch (error) { + console.error('Error shortening URL:', error); + throw error; } - const data = await response.text(); - console.log('URL shortened successfully:' + data); - return data as string; } diff --git a/webapp/components/url-shortener/ui/URLShortener.test.jsx b/webapp/components/url-shortener/ui/URLShortener.test.jsx index 4568865..b28c6e1 100644 --- a/webapp/components/url-shortener/ui/URLShortener.test.jsx +++ b/webapp/components/url-shortener/ui/URLShortener.test.jsx @@ -93,7 +93,7 @@ describe('URLShortenerV2', () => { }); }); - it("clicking the 'Shorten' for an empty url triggers an error messahe", async () => { + it("clicking the 'Shorten' for an empty url triggers an error message", async () => { render(); const shortenButton = screen.getByRole('button', { name: /shorten url/i }); From 320d0bf635fe92a7a75898887cb8468345f0304e Mon Sep 17 00:00:00 2001 From: selimyanat Date: Fri, 28 Feb 2025 16:52:25 +0100 Subject: [PATCH 2/3] init project --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04365dd..48d3855 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,11 +4,11 @@ on: push: branches: - main - - "feature/**" # Trigger workflow for feature branches - - "feat/**" # Trigger workflow for feature branches - - "doc/**" # Trigger workflow for feature branches - - "chore/**" # Trigger workflow for feature branches - - "fix/**" # Trigger workflow for feature branches + - feature/** # Trigger workflow for feature branches + - feat/** # Trigger workflow for feature branches + - doc/** # Trigger workflow for feature branches + - chore/** # Trigger workflow for feature branches + - fix/** # Trigger workflow for feature branches pull_request: branches: - main From 6c3c8c1f47be191f196140f7a1584b0e98dd34ce Mon Sep 17 00:00:00 2001 From: selimyanat Date: Fri, 28 Feb 2025 17:00:11 +0100 Subject: [PATCH 3/3] init project --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48d3855..55f060f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,14 @@ -name: CI Pipeline +name: Tiny URL CI Pipeline on: push: branches: - main - - feature/** # Trigger workflow for feature branches - - feat/** # Trigger workflow for feature branches - - doc/** # Trigger workflow for feature branches - - chore/** # Trigger workflow for feature branches - - fix/** # Trigger workflow for feature branches + - 'feature/**' # Trigger workflow for feature branches + - 'feat/**' # Trigger workflow for feature branches + - 'doc/**' # Trigger workflow for feature branches + - 'chore/**' # Trigger workflow for feature branches + - 'fix/**' # Trigger workflow for feature branches pull_request: branches: - main