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
2 changes: 2 additions & 0 deletions apps/backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql")

developmentOnly("org.springframework.boot:spring-boot-devtools")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ data class UrlCreateDto(
"^https?://(?:www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b[-a-zA-Z0-9()@:%_+.~#?&/=]*$",
)
val url: String,
@field:Future(message = "Expiration time must be in the future") val expiresTime: Instant?,
@field:Future(message = "Expiration time must be in the future") val expireTime: Instant?,
@field:Size(message = "Alias must be longer than 7 characters", min = 7)
@field:Size(message = "Alias cannot exceed 255 characters", max = 255)
@field:Pattern(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.tobynguyen.solitar.repository

import java.time.Instant
import org.springframework.cache.annotation.Cacheable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
Expand All @@ -10,7 +11,12 @@ interface UrlRepository : JpaRepository<UrlEntity, Long> {
@Cacheable(value = ["urlEntities"], key = "#shortCode", unless = "#result == null")
fun findByShortCode(shortCode: String): UrlEntity?

fun findByOriginalUrl(originalUrl: String): List<UrlEntity>
fun findByOriginalUrlAndExpiresAtAndHasAliasFalse(
originalUrl: String,
expiresAt: Instant,
): UrlEntity?

fun findByOriginalUrlAndExpiresAtIsNullAndHasAliasFalse(originalUrl: String): UrlEntity?

@Modifying
@Query("UPDATE UrlEntity u SET u.clickCount = u.clickCount + 1 WHERE u.id = :id")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.tobynguyen.solitar.service

import java.time.Instant
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.sqids.Sqids
Expand Down Expand Up @@ -36,23 +35,48 @@ class UrlService(private val urlRepository: UrlRepository, private val sqids: Sq

@Transactional
fun createUrl(data: UrlCreateDto): UrlEntity {
val (url, expireTime, alias) = data

if (data.alias == null) {
val existing =
urlRepository.findByOriginalUrl(data.url).firstOrNull {
(it.expiresAt == null || it.expiresAt!!.isAfter(Instant.now())) && !it.hasAlias
}
if (alias != null) {
val existing = urlRepository.findByShortCode(alias)

if (existing != null) {
if (existing.isDisabled) {
throw UrlDisabledException("We cannot shorten this URL")
return if (existing != null) {
if (existing.originalUrl == url && existing.expiresAt == expireTime) {
existing
} else {
throw UrlShortCodeConflictedException("This alias already exists.")
}
} else {
createAndSaveUrl(url, alias, expireTime)
}
} else {
if (expireTime == null) {
val existing =
urlRepository.findByOriginalUrlAndExpiresAtIsNullAndHasAliasFalse(url)

return existing
return existing ?: createAndSaveUrl(url, null, null)
} else {
val existing =
urlRepository.findByOriginalUrlAndExpiresAtAndHasAliasFalse(url, expireTime)

return existing ?: createAndSaveUrl(url, null, expireTime)
}
}
}

fun createAndSaveUrl(url: String, alias: String?, expireTime: Instant?): UrlEntity {
if (alias != null) {
val entity =
UrlEntity(shortCode = "", originalUrl = data.url, expiresAt = data.expiresTime)
UrlEntity(
shortCode = alias,
originalUrl = url,
expiresAt = expireTime,
hasAlias = true,
)

return urlRepository.saveAndFlush(entity)
} else {
val entity = UrlEntity(shortCode = "", originalUrl = url, expiresAt = expireTime)

val savedEntity = urlRepository.save(entity)

Expand All @@ -61,20 +85,6 @@ class UrlService(private val urlRepository: UrlRepository, private val sqids: Sq
savedEntity.shortCode = generatedCode

return savedEntity
} else {
val entity =
UrlEntity(
shortCode = data.alias,
originalUrl = data.url,
expiresAt = data.expiresTime,
hasAlias = true,
)

try {
return urlRepository.saveAndFlush(entity)
} catch (e: DataIntegrityViolationException) {
throw UrlShortCodeConflictedException("This alias already exists.")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.tobynguyen.solitar.config
package org.tobynguyen.solitar.util

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.sqids.Sqids

@Configuration
class AppConfig {
class SqidsUtil {

@Bean
fun sqids(): Sqids {
Expand Down
3 changes: 0 additions & 3 deletions apps/backend/src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
spring:
application:
version: 1.0.0-dev
jpa:
hibernate:
ddl-auto: update

app:
cors:
Expand Down
3 changes: 0 additions & 3 deletions apps/backend/src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
spring:
application:
version: 1.0.0-prod
jpa:
hibernate:
ddl-auto: update

app:
cors:
Expand Down
6 changes: 6 additions & 0 deletions apps/backend/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ spring:
data:
redis:
url: ${REDIS_URL}
flyway:
enabled: true
locations: classpath:db/migration
jpa:
hibernate:
ddl-auto: validate

server:
port: ${SERVER_PORT:8080}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE SEQUENCE IF NOT EXISTS url_seq START WITH 1 INCREMENT BY 1;

CREATE TABLE urls
(
id BIGINT NOT NULL,
short_code VARCHAR(255) NOT NULL,
has_alias BOOLEAN NOT NULL,
original_url TEXT NOT NULL,
click_count BIGINT NOT NULL,
is_disabled BOOLEAN NOT NULL,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,
expires_at TIMESTAMP WITHOUT TIME ZONE,
CONSTRAINT pk_urls PRIMARY KEY (id)
);

ALTER TABLE urls
ADD CONSTRAINT uc_urls_shortcode UNIQUE (short_code);
2 changes: 1 addition & 1 deletion apps/frontend/src/app/components/form/UrlShortenerForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async function onSubmit(event: FormSubmitEvent<v.InferOutput<typeof schema>>) {
url: longUrl,
alias,
...(!neverExpire && {
expiresTime: generateExpireTime(event.data.expireTime, event.data.expireUnit),
expireTime: generateExpireTime(event.data.expireTime, event.data.expireUnit),
}),
};

Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/utils/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type UrlShortenerResponse = {
export type UrlShortenerBody = {
url: string;
alias?: string;
expiresTime?: string;
expireTime?: string;
};

export const repository = <T>(fetch: $Fetch<T, NitroFetchRequest>) => ({
Expand Down