Skip to content

Commit 8973ff7

Browse files
committed
init project
1 parent 5a6f8c3 commit 8973ff7

File tree

11 files changed

+196
-84
lines changed

11 files changed

+196
-84
lines changed

.github/workflows/ci.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: CI Pipeline
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- "feature/**" # Trigger workflow for feature branches
8+
- "feat/**" # Trigger workflow for feature branches
9+
- "doc/**" # Trigger workflow for feature branches
10+
- "chore/**" # Trigger workflow for feature branches
11+
- "fix/**" # Trigger workflow for feature branches
12+
pull_request:
13+
branches:
14+
- main
15+
16+
jobs:
17+
18+
api:
19+
name: API Build & Test
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout Repository
24+
uses: actions/checkout@v4
25+
26+
- name: Setup Node.js
27+
uses: actions/setup-node@v4
28+
with:
29+
node-version: 18
30+
cache: yarn
31+
cache-dependency-path: api/yarn-lock.json # Specify the path to your yarn.lock
32+
33+
- name: Install Dependencies (API)
34+
working-directory: api
35+
run: yarn install --frozen-lockfile
36+
37+
#- name: Lint API
38+
# run: npm run lint
39+
40+
- name: Run Tests (API)
41+
run: yarn test
42+
43+
# Webapp (Next.js) Build & Test
44+
webapp:
45+
name: Webapp Build & Test
46+
runs-on: ubuntu-latest
47+
defaults:
48+
49+
steps:
50+
- name: Checkout Repository
51+
uses: actions/checkout@v4
52+
53+
- name: Setup Node.js
54+
uses: actions/setup-node@v4
55+
with:
56+
node-version: 18
57+
cache: yarn
58+
cache-dependency-path: webapp/yarn-lock.json
59+
60+
- name: Install Dependencies (Webapp)
61+
run: yarn install --frozen-lockfile
62+
63+
- name: Lint Webapp
64+
run: yarn lint
65+
66+
#- name: Run Tests (Webapp)
67+
# run: npm test
68+
69+
- name: Build Webapp
70+
run: yarn build
71+
72+
# Merge Blocker: Ensure Both Builds Pass
73+
merge_guard:
74+
name: Ensure All Jobs Passed
75+
needs: [api, webapp]
76+
runs-on: ubuntu-latest
77+
steps:
78+
- name: Check for Failures
79+
run: echo "All jobs passed!"

api/src/app.controller.spec.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

api/src/app.controller.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

api/src/app.module.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { Module } from '@nestjs/common';
22
import { ConfigModule } from '@nestjs/config';
3-
import { AppController } from './app.controller';
4-
import { AppService } from './app.service';
5-
import { AppGlobalIdGeneratorService } from './app.global-id-generator.service';
3+
import { ShortenUrlModule } from './shorten-url/shorten-url.module';
64

75
@Module({
8-
imports: [ConfigModule.forRoot()], // Load environment variables
9-
controllers: [AppController],
10-
providers: [AppGlobalIdGeneratorService, AppService],
6+
imports: [ConfigModule.forRoot(), ShortenUrlModule], // Load environment variables
7+
controllers: [],
8+
providers: [],
119
})
1210
export class AppModule {}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { ShortenUrlController } from './shorten-url.controller';
3+
import { ShortenUrlUsecase } from './shorten-url.usecase';
4+
import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service';
5+
import { ConfigModule } from '@nestjs/config';
6+
7+
describe('ShortenUrl controller', () => {
8+
let underTest: ShortenUrlController;
9+
10+
beforeEach(async () => {
11+
const app: TestingModule = await Test.createTestingModule({
12+
imports: [await ConfigModule.forRoot()],
13+
controllers: [ShortenUrlController],
14+
providers: [ShortenUrlIdGeneratorService, ShortenUrlUsecase],
15+
}).compile();
16+
17+
underTest = app.get<ShortenUrlController>(ShortenUrlController);
18+
});
19+
20+
describe('shortenUrl', () => {
21+
it('should return a valid Base62 short URL with 10 chars length', async () => {
22+
const longUrl =
23+
'https://zapper.xyz/very-long-url/very-long-url/very-long-url';
24+
const shortenedUrl = await underTest.shortenUrl(longUrl); // Assuming it's async
25+
26+
// Extract the unique ID part of the URL
27+
const urlPattern = /^https:\/\/zapper\.xyz\/([A-Za-z0-9]{10})$/;
28+
const match = shortenedUrl.match(urlPattern);
29+
30+
expect(shortenedUrl).not.toBeNull();
31+
expect(match).not.toBeNull(); // Ensure it matches the pattern
32+
expect(match![1].length).toBe(10); // Validate the ID part has 10 characters
33+
});
34+
});
35+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Controller, Post } from '@nestjs/common';
2+
import { ShortenUrlUsecase } from './shorten-url.usecase';
3+
4+
@Controller('/shorten-url')
5+
export class ShortenUrlController {
6+
constructor(private readonly shortenUrlUsecase: ShortenUrlUsecase) {}
7+
8+
@Post()
9+
shortenUrl(url: string): string {
10+
return this.shortenUrlUsecase.createTinyURL('https://zapper.xyz');
11+
}
12+
}

api/src/app.global-id-generator.service.ts renamed to api/src/shorten-url/shorten-url.id-generator.service.ts

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,43 +12,35 @@ import { ConfigService } from '@nestjs/config';
1212
* The machineId ID is a custom value that should be unique for each instance of the generator.
1313
*/
1414
@Injectable()
15-
export class AppGlobalIdGeneratorService {
15+
export class ShortenUrlIdGeneratorService {
1616
private static readonly MACHINE_ID_BITS = 10;
1717
private static readonly SEQUENCE_BITS = 12;
18-
18+
// indexing starts at 0: 2^(MACHINE_ID_BITS) - 1
19+
private static readonly MAX_MACHINE_ID =
20+
(1 << ShortenUrlIdGeneratorService.MACHINE_ID_BITS) - 1;
21+
// indexing starts at 0: 2^(SEQUENCE_BITS) - 1
22+
private static readonly MAX_SEQUENCE =
23+
(1 << ShortenUrlIdGeneratorService.SEQUENCE_BITS) - 1;
1924
private readonly epoch: number = 1609459200000;
2025
// TODO must be configurable
2126
private readonly machineId: number = 1;
2227
private sequence: number;
2328
private lastTimestamp: number;
2429

25-
// indexing starts at 0: 2^(MACHINE_ID_BITS) - 1
26-
private static readonly MAX_MACHINE_ID =
27-
(1 << AppGlobalIdGeneratorService.MACHINE_ID_BITS) - 1;
28-
// indexing starts at 0: 2^(SEQUENCE_BITS) - 1
29-
private static readonly MAX_SEQUENCE =
30-
(1 << AppGlobalIdGeneratorService.SEQUENCE_BITS) - 1;
31-
3230
constructor(private readonly configService: ConfigService) {
3331
this.machineId = Number(this.configService.get<number>('MACHINE_ID'));
3432
if (
3533
this.machineId < 0 ||
36-
this.machineId > AppGlobalIdGeneratorService.MAX_MACHINE_ID
34+
this.machineId > ShortenUrlIdGeneratorService.MAX_MACHINE_ID
3735
) {
3836
throw new Error(
39-
`Invalid MACHINE_ID. Must be between 0 and ${AppGlobalIdGeneratorService.MAX_MACHINE_ID}.`,
37+
`Invalid MACHINE_ID. Must be between 0 and ${ShortenUrlIdGeneratorService.MAX_MACHINE_ID}.`,
4038
);
4139
}
4240
this.sequence = 0;
4341
this.lastTimestamp = -1;
4442
}
4543

46-
private waitUntilNextMillisecond(): void {
47-
while (this.lastTimestamp <= Date.now()) {
48-
this.lastTimestamp = Date.now();
49-
}
50-
}
51-
5244
/**
5345
* Generate a new unique ID within the current timestamp using the Snowflake algorithm. If the number
5446
* of ids generated in the same millisecond exceeds the maximum sequence number, the function will wait
@@ -67,7 +59,7 @@ export class AppGlobalIdGeneratorService {
6759

6860
if (currentTimestamp === this.lastTimestamp) {
6961
this.sequence =
70-
(this.sequence + 1) & AppGlobalIdGeneratorService.MAX_SEQUENCE;
62+
(this.sequence + 1) & ShortenUrlIdGeneratorService.MAX_SEQUENCE;
7163
if (this.sequence == 0) {
7264
this.waitUntilNextMillisecond();
7365
}
@@ -84,15 +76,21 @@ export class AppGlobalIdGeneratorService {
8476

8577
// prettier-ignore
8678
const id =
87-
(BigInt(currentTimestamp - this.epoch) <<
88-
BigInt(
89-
AppGlobalIdGeneratorService.MACHINE_ID_BITS +
90-
AppGlobalIdGeneratorService.SEQUENCE_BITS,
91-
)) |
92-
(BigInt(this.machineId) <<
93-
BigInt(AppGlobalIdGeneratorService.SEQUENCE_BITS)) |
94-
BigInt(this.sequence);
79+
(BigInt(currentTimestamp - this.epoch) <<
80+
BigInt(
81+
ShortenUrlIdGeneratorService.MACHINE_ID_BITS +
82+
ShortenUrlIdGeneratorService.SEQUENCE_BITS,
83+
)) |
84+
(BigInt(this.machineId) <<
85+
BigInt(ShortenUrlIdGeneratorService.SEQUENCE_BITS)) |
86+
BigInt(this.sequence);
9587

9688
return id;
9789
}
90+
91+
private waitUntilNextMillisecond(): void {
92+
while (this.lastTimestamp <= Date.now()) {
93+
this.lastTimestamp = Date.now();
94+
}
95+
}
9896
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
3+
import { ShortenUrlController } from './shorten-url.controller';
4+
import { ShortenUrlUsecase } from './shorten-url.usecase';
5+
import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service';
6+
7+
@Module({
8+
imports: [ConfigModule.forRoot()], // Load environment variables
9+
controllers: [ShortenUrlController],
10+
providers: [ShortenUrlIdGeneratorService, ShortenUrlUsecase],
11+
})
12+
export class ShortenUrlModule {}

api/src/app.service.ts renamed to api/src/shorten-url/shorten-url.usecase.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { Injectable } from '@nestjs/common';
2-
import { AppGlobalIdGeneratorService } from './app.global-id-generator.service';
2+
import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service';
33

44
@Injectable()
5-
export class AppService {
5+
export class ShortenUrlUsecase {
66
private static BASE62_CHARACTERS =
77
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
88

9-
private static BASE62 = AppService.BASE62_CHARACTERS.length;
9+
private static BASE62 = ShortenUrlUsecase.BASE62_CHARACTERS.length;
1010

11-
constructor(private readonly idGenerator: AppGlobalIdGeneratorService) {}
11+
constructor(private readonly idGenerator: ShortenUrlIdGeneratorService) {}
1212

1313
createTinyURL(originalURL: string): string {
1414
const id = this.idGenerator.generateId();
@@ -20,13 +20,13 @@ export class AppService {
2020
}
2121

2222
private encodeBase62(id: number): string {
23-
if (id === 0) return AppService.BASE62_CHARACTERS[0];
23+
if (id === 0) return ShortenUrlUsecase.BASE62_CHARACTERS[0];
2424

2525
let encoded = '';
2626
while (id > 0) {
27-
const remainder = id % AppService.BASE62;
28-
encoded = AppService.BASE62_CHARACTERS[remainder] + encoded;
29-
id = Math.floor(id / AppService.BASE62);
27+
const remainder = id % ShortenUrlUsecase.BASE62;
28+
encoded = ShortenUrlUsecase.BASE62_CHARACTERS[remainder] + encoded;
29+
id = Math.floor(id / ShortenUrlUsecase.BASE62);
3030
}
3131
return encoded;
3232
}

webapp/app/api/url-shortener/route.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,26 @@ export async function POST(request: NextRequest) {
1515

1616
// Example function to simulate URL shortening logic
1717
async function shortenUrl(url: string): Promise<string> {
18-
// Replace this with your actual URL shortening implementation
19-
// call an http service, generate a random string, etc
20-
const response = await fetch('http://localhost:3001');
21-
if (!response.ok) {
22-
throw new Error('Failed to shorten URL');
18+
try {
19+
const response = await fetch('http://localhost:3001/shorten-url', {
20+
method: 'POST', // Use POST method
21+
headers: {
22+
'Content-Type': 'application/json', // Inform the server we are sending JSON
23+
},
24+
body: JSON.stringify({ url }), // Send URL in request body
25+
});
26+
27+
console.log(response);
28+
29+
if (!response.ok) {
30+
throw new Error(`Failed to shorten URL: ${response.statusText}`);
31+
}
32+
33+
const data = await response.text(); // Assume server returns JSON
34+
console.log('URL shortened successfully:', data);
35+
return data; // Adjust this based on the API response structure
36+
} catch (error) {
37+
console.error('Error shortening URL:', error);
38+
throw error;
2339
}
24-
const data = await response.text();
25-
console.log('URL shortened successfully:' + data);
26-
return data as string;
2740
}

0 commit comments

Comments
 (0)