Compare commits

...

12 Commits

Author SHA1 Message Date
96610e0d2f Migration: Depend on Service Base
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m22s
2026-06-04 22:22:40 +02:00
ce93c5f423 Add CORS Support to allow frontend requests
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m28s
2026-06-03 13:22:14 +02:00
d348d0bcbe Script go generate stubs for protobuf implementation
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m8s
2026-06-03 10:03:25 +02:00
6d693db605 Move all tables into a schema
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m19s
2026-06-03 00:02:43 +02:00
ec4159e84c New: Add goose to run migrations as an init container
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m56s
2026-06-02 22:22:21 +02:00
8f1e292995 Get the port from the json config
All checks were successful
Build and Push Docker Image / build (push) Successful in 5m53s
2026-06-02 22:08:47 +02:00
a2d8c5c156 Remove settings.json from docker and git repository
All checks were successful
Build and Push Docker Image / build (push) Successful in 5m54s
2026-06-02 22:02:24 +02:00
ad9223d2d9 Update: config json path
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
2026-06-02 21:59:47 +02:00
a9e1d1e5d9 Implement rudimentary link shortening service and wire everything together
All checks were successful
Build and Push Docker Image / build (push) Successful in 5m55s
2026-06-02 19:34:58 +02:00
1b4d23f9aa Generated Protobuf stubs 2026-06-02 19:34:58 +02:00
e265e2d303 Telemetry system 2026-06-02 19:34:58 +02:00
6799e63993 CI/CD - build docker container 2026-06-02 19:34:48 +02:00
24 changed files with 1261 additions and 71 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
settings.json

46
.github/workflows/docker.yaml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
container:
image: quay.io/buildah/stable
options: --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/fuse:rw --privileged
env:
BUILDAH_ISOLATION: chroot
STORAGE_DRIVER: vfs
steps:
- name: Install Node.js
run: dnf install -y nodejs git
- uses: actions/checkout@v4
with:
ssh-user: ${{ secrets.REGISTRY_USER }}
token: ${{ secrets.REGISTRY_PASSWORD }}
submodules: true
- name: Login to Registry
run: buildah login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASSWORD }} git.kocoder.xyz
- name: Generate Tag
id: tag
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
echo "value=${{ github.ref_name }}-$(date +%s)-${SHORT_SHA}" >> $GITHUB_OUTPUT
- name: Buildah Build
run: |
buildah build -t ${{ steps.tag.outputs.value }} .
buildah tag ${{ steps.tag.outputs.value }} latest
- name: Push Docker Images
run: |
buildah push ${{ steps.tag.outputs.value }} docker://git.kocoder.xyz/vt/link-shortening-service:${{ steps.tag.outputs.value }}
buildah push latest docker://git.kocoder.xyz/vt/link-shortening-service:latest

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
settings.json

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
FROM golang:1.26-bookworm AS base
# Move to working directory /app
WORKDIR /app
# Copy the go.mod and go.sum files to the /app directory
COPY go.mod go.sum ./
# Install dependencies
RUN go mod download
# Copy the entire source code into the container
COPY . .
# Build the application
RUN go build -o shortener
# Install goose migration tool
RUN GOBIN=/app go install github.com/pressly/goose/v3/cmd/goose@latest
FROM golang:1.26-bookworm AS runner
WORKDIR /app
COPY --from=base /app/shortener /app/shortener
COPY --from=base /app/goose /app/goose
COPY --from=base /app/sql/schema /app/sql/schema
# Document the port that may need to be published
EXPOSE 8080
# Start the application
CMD ["/app/shortener"]

18
buf.gen.yaml Normal file
View File

@@ -0,0 +1,18 @@
version: v2
plugins:
- local: [go, tool, protoc-gen-go]
out: internal
opt: paths=source_relative
- local: [go, tool, protoc-gen-connect-go]
out: internal
opt:
- paths=source_relative
- simple
managed:
enabled: true
override:
- file_option: go_package_prefix
value: git.kocoder.xyz/vt/shortener/internal
disable:
- file_option: go_package
module: buf.build/bufbuild/protovalidate

6
buf.lock Normal file
View File

@@ -0,0 +1,6 @@
# Generated by buf. DO NOT EDIT.
version: v2
deps:
- name: buf.build/bufbuild/protovalidate
commit: 50325440f8f24053b047484a6bf60b76
digest: b5:74cb6f5c0853c3c10aafc701614194bbd63326bdb8ef4068214454b8894b03ba4113e04b3a33a8321cdf05336e37db4dc14a5e2495db8462566914f36086ba31

9
buf.yaml Normal file
View File

@@ -0,0 +1,9 @@
version: v2
deps:
- buf.build/bufbuild/protovalidate
lint:
use:
- STANDARD
breaking:
use:
- FILE

2
gen-proto.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
buf generate --path proto/shorten

39
go.mod
View File

@@ -8,27 +8,33 @@ tool (
)
require (
git.kocoder.xyz/vt/service-base v1.0.0
github.com/lib/pq v1.12.3
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
go.opentelemetry.io/otel/log v0.20.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/log v0.20.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect
connectrpc.com/cors v0.1.0 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect
go.opentelemetry.io/otel/log v0.20.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.20.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1
buf.build/go/protovalidate v1.2.0 // indirect
cel.dev/expr v0.25.1 // indirect
connectrpc.com/connect v1.20.0 // indirect
connectrpc.com/grpcreflect v1.3.0
connectrpc.com/validate v0.6.0 // indirect
connectrpc.com/connect v1.20.0
connectrpc.com/grpcreflect v1.3.0 // indirect
connectrpc.com/validate v0.6.0
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -38,7 +44,6 @@ require (
github.com/google/cel-go v0.28.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
@@ -51,5 +56,5 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
google.golang.org/protobuf v1.36.11
)

29
go.sum
View File

@@ -1,26 +1,27 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 h1:DQLS/rRxLHuugVzjJU5AvOwD57pdFl9he/0O7e5P294=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1/go.mod h1:aY3zbkNan5F+cGm9lITDP6oxJIwu0dn9KjJuJjWaHkg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/go/protovalidate v1.0.0 h1:IAG1etULddAy93fiBsFVhpj7es5zL53AfB/79CVGtyY=
buf.build/go/protovalidate v1.0.0/go.mod h1:KQmEUrcQuC99hAw+juzOEAmILScQiKBP1Oc36vvCLW8=
buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0=
buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
connectrpc.com/cors v0.1.0 h1:f3gTXJyDZPrDIZCQ567jxfD9PAIpopHiRDnJRt3QuOQ=
connectrpc.com/cors v0.1.0/go.mod h1:v8SJZCPfHtGH1zsm+Ttajpozd4cYIUryl4dFB6QEpfg=
connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc=
connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs=
connectrpc.com/validate v0.6.0 h1:DcrgDKt2ZScrUs/d/mh9itD2yeEa0UbBBa+i0mwzx+4=
connectrpc.com/validate v0.6.0/go.mod h1:ihrpI+8gVbLH1fvVWJL1I3j0CfWnF8P/90LsmluRiZs=
git.kocoder.xyz/vt/service-base v1.0.0 h1:WeakTvsz9389dZm3BXEbnWt2SVkLRc/9RfXjesFUqEo=
git.kocoder.xyz/vt/service-base v1.0.0/go.mod h1:FBXO015/GkyPP6CBan4Rku76Y/vrZgIL7zQyz39dTgs=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -32,8 +33,6 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -46,14 +45,10 @@ github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c=
@@ -94,6 +89,8 @@ go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpu
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
@@ -112,7 +109,5 @@ google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -7,12 +7,14 @@ import (
type Config struct {
DB_URL string `json:"db_url"`
LISTEN_ON string `json:"listen_on"`
TRUSTED_PROXIES []string `json:"trusted_proxies"`
OTLP_ENDPOINT string `json:"otlp_endpoint"`
ALLOWED_ORIGINS []string `json:"allowed_origins"`
}
func Read() *Config {
file, err := os.ReadFile("/home/kocoder/src/go/shortener/settings.json")
file, err := os.ReadFile("./settings.json")
if err != nil {
panic(err)
}

View File

@@ -11,18 +11,18 @@ import (
)
const getClicks = `-- name: GetClicks :many
SELECT click_id, url_id, clicked_at, referrer, user_agent, ip_address FROM click_logs
SELECT click_id, url_id, clicked_at, referrer, user_agent, ip_address FROM lss.click_logs
`
func (q *Queries) GetClicks(ctx context.Context) ([]ClickLog, error) {
func (q *Queries) GetClicks(ctx context.Context) ([]LssClickLog, error) {
rows, err := q.db.QueryContext(ctx, getClicks)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ClickLog
var items []LssClickLog
for rows.Next() {
var i ClickLog
var i LssClickLog
if err := rows.Scan(
&i.ClickID,
&i.UrlID,
@@ -45,7 +45,7 @@ func (q *Queries) GetClicks(ctx context.Context) ([]ClickLog, error) {
}
const trackClick = `-- name: TrackClick :one
INSERT INTO click_logs (url_id, referrer, user_agent, ip_address)
INSERT INTO lss.click_logs (url_id, referrer, user_agent, ip_address)
VALUES (
$1,
$2,
@@ -62,14 +62,14 @@ type TrackClickParams struct {
IpAddress sql.NullString
}
func (q *Queries) TrackClick(ctx context.Context, arg TrackClickParams) (ClickLog, error) {
func (q *Queries) TrackClick(ctx context.Context, arg TrackClickParams) (LssClickLog, error) {
row := q.db.QueryRowContext(ctx, trackClick,
arg.UrlID,
arg.Referrer,
arg.UserAgent,
arg.IpAddress,
)
var i ClickLog
var i LssClickLog
err := row.Scan(
&i.ClickID,
&i.UrlID,

View File

@@ -9,7 +9,7 @@ import (
"time"
)
type ClickLog struct {
type LssClickLog struct {
ClickID int64
UrlID int32
ClickedAt time.Time
@@ -18,7 +18,7 @@ type ClickLog struct {
IpAddress sql.NullString
}
type Url struct {
type LssUrl struct {
UrlID int32
LongUrl string
ShortCode string

View File

@@ -12,7 +12,7 @@ import (
)
const createURL = `-- name: CreateURL :one
INSERT INTO urls (created_at, expires_at, long_url, short_code, is_active)
INSERT INTO lss.urls (created_at, expires_at, long_url, short_code, is_active)
VALUES (
$1,
$2,
@@ -31,7 +31,7 @@ type CreateURLParams struct {
IsActive bool
}
func (q *Queries) CreateURL(ctx context.Context, arg CreateURLParams) (Url, error) {
func (q *Queries) CreateURL(ctx context.Context, arg CreateURLParams) (LssUrl, error) {
row := q.db.QueryRowContext(ctx, createURL,
arg.CreatedAt,
arg.ExpiresAt,
@@ -39,7 +39,7 @@ func (q *Queries) CreateURL(ctx context.Context, arg CreateURLParams) (Url, erro
arg.ShortCode,
arg.IsActive,
)
var i Url
var i LssUrl
err := row.Scan(
&i.UrlID,
&i.LongUrl,
@@ -52,12 +52,12 @@ func (q *Queries) CreateURL(ctx context.Context, arg CreateURLParams) (Url, erro
}
const deleteURL = `-- name: DeleteURL :one
DELETE FROM urls WHERE url_id = $1 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active
DELETE FROM lss.urls WHERE url_id = $1 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active
`
func (q *Queries) DeleteURL(ctx context.Context, urlID int32) (Url, error) {
func (q *Queries) DeleteURL(ctx context.Context, urlID int32) (LssUrl, error) {
row := q.db.QueryRowContext(ctx, deleteURL, urlID)
var i Url
var i LssUrl
err := row.Scan(
&i.UrlID,
&i.LongUrl,
@@ -70,7 +70,7 @@ func (q *Queries) DeleteURL(ctx context.Context, urlID int32) (Url, error) {
}
const getURLByShortCode = `-- name: GetURLByShortCode :one
SELECT url_id, long_url FROM urls WHERE is_active = true AND short_code = $1
SELECT url_id, long_url FROM lss.urls WHERE is_active = true AND short_code = $1
`
type GetURLByShortCodeRow struct {
@@ -86,18 +86,18 @@ func (q *Queries) GetURLByShortCode(ctx context.Context, shortCode string) (GetU
}
const getURLs = `-- name: GetURLs :many
SELECT url_id, long_url, short_code, created_at, expires_at, is_active FROM urls
SELECT url_id, long_url, short_code, created_at, expires_at, is_active FROM lss.urls
`
func (q *Queries) GetURLs(ctx context.Context) ([]Url, error) {
func (q *Queries) GetURLs(ctx context.Context) ([]LssUrl, error) {
rows, err := q.db.QueryContext(ctx, getURLs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Url
var items []LssUrl
for rows.Next() {
var i Url
var i LssUrl
if err := rows.Scan(
&i.UrlID,
&i.LongUrl,
@@ -120,7 +120,7 @@ func (q *Queries) GetURLs(ctx context.Context) ([]Url, error) {
}
const setActive = `-- name: SetActive :one
UPDATE urls SET is_active = $1 WHERE url_id = $2 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active
UPDATE lss.urls SET is_active = $1 WHERE url_id = $2 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active
`
type SetActiveParams struct {
@@ -128,9 +128,9 @@ type SetActiveParams struct {
UrlID int32
}
func (q *Queries) SetActive(ctx context.Context, arg SetActiveParams) (Url, error) {
func (q *Queries) SetActive(ctx context.Context, arg SetActiveParams) (LssUrl, error) {
row := q.db.QueryRowContext(ctx, setActive, arg.IsActive, arg.UrlID)
var i Url
var i LssUrl
err := row.Scan(
&i.UrlID,
&i.LongUrl,

View File

@@ -0,0 +1,586 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: proto/shorten/v1/shorten.proto
package shortenv1
import (
_ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type CreateURLRedirectionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ShortCode string `protobuf:"bytes,1,opt,name=short_code,json=shortCode,proto3" json:"short_code,omitempty"`
Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateURLRedirectionRequest) Reset() {
*x = CreateURLRedirectionRequest{}
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateURLRedirectionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateURLRedirectionRequest) ProtoMessage() {}
func (x *CreateURLRedirectionRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateURLRedirectionRequest.ProtoReflect.Descriptor instead.
func (*CreateURLRedirectionRequest) Descriptor() ([]byte, []int) {
return file_proto_shorten_v1_shorten_proto_rawDescGZIP(), []int{0}
}
func (x *CreateURLRedirectionRequest) GetShortCode() string {
if x != nil {
return x.ShortCode
}
return ""
}
func (x *CreateURLRedirectionRequest) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
type CreateURLRedirectionResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateURLRedirectionResponse) Reset() {
*x = CreateURLRedirectionResponse{}
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateURLRedirectionResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateURLRedirectionResponse) ProtoMessage() {}
func (x *CreateURLRedirectionResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateURLRedirectionResponse.ProtoReflect.Descriptor instead.
func (*CreateURLRedirectionResponse) Descriptor() ([]byte, []int) {
return file_proto_shorten_v1_shorten_proto_rawDescGZIP(), []int{1}
}
func (x *CreateURLRedirectionResponse) GetOk() bool {
if x != nil {
return x.Ok
}
return false
}
type URLRedirection struct {
state protoimpl.MessageState `protogen:"open.v1"`
UrlId int32 `protobuf:"varint,1,opt,name=url_id,json=urlId,proto3" json:"url_id,omitempty"`
ShortCode string `protobuf:"bytes,3,opt,name=short_code,json=shortCode,proto3" json:"short_code,omitempty"`
Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
IsActive bool `protobuf:"varint,6,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"`
ClickCountByDay []int32 `protobuf:"varint,7,rep,packed,name=click_count_by_day,json=clickCountByDay,proto3" json:"click_count_by_day,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *URLRedirection) Reset() {
*x = URLRedirection{}
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *URLRedirection) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*URLRedirection) ProtoMessage() {}
func (x *URLRedirection) ProtoReflect() protoreflect.Message {
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use URLRedirection.ProtoReflect.Descriptor instead.
func (*URLRedirection) Descriptor() ([]byte, []int) {
return file_proto_shorten_v1_shorten_proto_rawDescGZIP(), []int{2}
}
func (x *URLRedirection) GetUrlId() int32 {
if x != nil {
return x.UrlId
}
return 0
}
func (x *URLRedirection) GetShortCode() string {
if x != nil {
return x.ShortCode
}
return ""
}
func (x *URLRedirection) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *URLRedirection) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *URLRedirection) GetExpiresAt() *timestamppb.Timestamp {
if x != nil {
return x.ExpiresAt
}
return nil
}
func (x *URLRedirection) GetIsActive() bool {
if x != nil {
return x.IsActive
}
return false
}
func (x *URLRedirection) GetClickCountByDay() []int32 {
if x != nil {
return x.ClickCountByDay
}
return nil
}
type ListURLRedirectionsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListURLRedirectionsRequest) Reset() {
*x = ListURLRedirectionsRequest{}
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListURLRedirectionsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListURLRedirectionsRequest) ProtoMessage() {}
func (x *ListURLRedirectionsRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListURLRedirectionsRequest.ProtoReflect.Descriptor instead.
func (*ListURLRedirectionsRequest) Descriptor() ([]byte, []int) {
return file_proto_shorten_v1_shorten_proto_rawDescGZIP(), []int{3}
}
type ListURLRedirectionsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
UrlRedirections []*URLRedirection `protobuf:"bytes,1,rep,name=url_redirections,json=urlRedirections,proto3" json:"url_redirections,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListURLRedirectionsResponse) Reset() {
*x = ListURLRedirectionsResponse{}
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListURLRedirectionsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListURLRedirectionsResponse) ProtoMessage() {}
func (x *ListURLRedirectionsResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListURLRedirectionsResponse.ProtoReflect.Descriptor instead.
func (*ListURLRedirectionsResponse) Descriptor() ([]byte, []int) {
return file_proto_shorten_v1_shorten_proto_rawDescGZIP(), []int{4}
}
func (x *ListURLRedirectionsResponse) GetUrlRedirections() []*URLRedirection {
if x != nil {
return x.UrlRedirections
}
return nil
}
type DeactivateURLRedirectionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UrlId int32 `protobuf:"varint,1,opt,name=url_id,json=urlId,proto3" json:"url_id,omitempty"`
IsActive bool `protobuf:"varint,2,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeactivateURLRedirectionRequest) Reset() {
*x = DeactivateURLRedirectionRequest{}
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeactivateURLRedirectionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeactivateURLRedirectionRequest) ProtoMessage() {}
func (x *DeactivateURLRedirectionRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeactivateURLRedirectionRequest.ProtoReflect.Descriptor instead.
func (*DeactivateURLRedirectionRequest) Descriptor() ([]byte, []int) {
return file_proto_shorten_v1_shorten_proto_rawDescGZIP(), []int{5}
}
func (x *DeactivateURLRedirectionRequest) GetUrlId() int32 {
if x != nil {
return x.UrlId
}
return 0
}
func (x *DeactivateURLRedirectionRequest) GetIsActive() bool {
if x != nil {
return x.IsActive
}
return false
}
type DeactivateURLRedirectionResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeactivateURLRedirectionResponse) Reset() {
*x = DeactivateURLRedirectionResponse{}
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeactivateURLRedirectionResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeactivateURLRedirectionResponse) ProtoMessage() {}
func (x *DeactivateURLRedirectionResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeactivateURLRedirectionResponse.ProtoReflect.Descriptor instead.
func (*DeactivateURLRedirectionResponse) Descriptor() ([]byte, []int) {
return file_proto_shorten_v1_shorten_proto_rawDescGZIP(), []int{6}
}
func (x *DeactivateURLRedirectionResponse) GetOk() bool {
if x != nil {
return x.Ok
}
return false
}
type DeleteURLRedirectionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UrlId int32 `protobuf:"varint,1,opt,name=url_id,json=urlId,proto3" json:"url_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteURLRedirectionRequest) Reset() {
*x = DeleteURLRedirectionRequest{}
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteURLRedirectionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteURLRedirectionRequest) ProtoMessage() {}
func (x *DeleteURLRedirectionRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteURLRedirectionRequest.ProtoReflect.Descriptor instead.
func (*DeleteURLRedirectionRequest) Descriptor() ([]byte, []int) {
return file_proto_shorten_v1_shorten_proto_rawDescGZIP(), []int{7}
}
func (x *DeleteURLRedirectionRequest) GetUrlId() int32 {
if x != nil {
return x.UrlId
}
return 0
}
type DeleteURLRedirectionResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteURLRedirectionResponse) Reset() {
*x = DeleteURLRedirectionResponse{}
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteURLRedirectionResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteURLRedirectionResponse) ProtoMessage() {}
func (x *DeleteURLRedirectionResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_shorten_v1_shorten_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteURLRedirectionResponse.ProtoReflect.Descriptor instead.
func (*DeleteURLRedirectionResponse) Descriptor() ([]byte, []int) {
return file_proto_shorten_v1_shorten_proto_rawDescGZIP(), []int{8}
}
func (x *DeleteURLRedirectionResponse) GetOk() bool {
if x != nil {
return x.Ok
}
return false
}
var File_proto_shorten_v1_shorten_proto protoreflect.FileDescriptor
const file_proto_shorten_v1_shorten_proto_rawDesc = "" +
"\n" +
"\x1eproto/shorten/v1/shorten.proto\x12\x10proto.shorten.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"b\n" +
"\x1bCreateURLRedirectionRequest\x12'\n" +
"\n" +
"short_code\x18\x01 \x01(\tB\b\xbaH\x05r\x03\x98\x01\n" +
"R\tshortCode\x12\x1a\n" +
"\x03url\x18\x02 \x01(\tB\b\xbaH\x05r\x03\x88\x01\x01R\x03url\".\n" +
"\x1cCreateURLRedirectionResponse\x12\x0e\n" +
"\x02ok\x18\x01 \x01(\bR\x02ok\"\x98\x02\n" +
"\x0eURLRedirection\x12\x15\n" +
"\x06url_id\x18\x01 \x01(\x05R\x05urlId\x12\x1d\n" +
"\n" +
"short_code\x18\x03 \x01(\tR\tshortCode\x12\x10\n" +
"\x03url\x18\x02 \x01(\tR\x03url\x129\n" +
"\n" +
"created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
"\n" +
"expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12\x1b\n" +
"\tis_active\x18\x06 \x01(\bR\bisActive\x12+\n" +
"\x12click_count_by_day\x18\a \x03(\x05R\x0fclickCountByDay\"\x1c\n" +
"\x1aListURLRedirectionsRequest\"j\n" +
"\x1bListURLRedirectionsResponse\x12K\n" +
"\x10url_redirections\x18\x01 \x03(\v2 .proto.shorten.v1.URLRedirectionR\x0furlRedirections\"U\n" +
"\x1fDeactivateURLRedirectionRequest\x12\x15\n" +
"\x06url_id\x18\x01 \x01(\x05R\x05urlId\x12\x1b\n" +
"\tis_active\x18\x02 \x01(\bR\bisActive\"2\n" +
" DeactivateURLRedirectionResponse\x12\x0e\n" +
"\x02ok\x18\x01 \x01(\bR\x02ok\"4\n" +
"\x1bDeleteURLRedirectionRequest\x12\x15\n" +
"\x06url_id\x18\x01 \x01(\x05R\x05urlId\".\n" +
"\x1cDeleteURLRedirectionResponse\x12\x0e\n" +
"\x02ok\x18\x01 \x01(\bR\x02ok2\xfe\x03\n" +
"\x0eShortenService\x12w\n" +
"\x14CreateURLRedirection\x12-.proto.shorten.v1.CreateURLRedirectionRequest\x1a..proto.shorten.v1.CreateURLRedirectionResponse\"\x00\x12t\n" +
"\x13ListURLRedirections\x12,.proto.shorten.v1.ListURLRedirectionsRequest\x1a-.proto.shorten.v1.ListURLRedirectionsResponse\"\x00\x12\x83\x01\n" +
"\x18DeactivateURLRedirection\x121.proto.shorten.v1.DeactivateURLRedirectionRequest\x1a2.proto.shorten.v1.DeactivateURLRedirectionResponse\"\x00\x12w\n" +
"\x14DeleteURLRedirection\x12-.proto.shorten.v1.DeleteURLRedirectionRequest\x1a..proto.shorten.v1.DeleteURLRedirectionResponse\"\x00B\xc8\x01\n" +
"\x14com.proto.shorten.v1B\fShortenProtoP\x01Z@git.kocoder.xyz/vt/shortener/internal/proto/shorten/v1;shortenv1\xa2\x02\x03PSX\xaa\x02\x10Proto.Shorten.V1\xca\x02\x10Proto\\Shorten\\V1\xe2\x02\x1cProto\\Shorten\\V1\\GPBMetadata\xea\x02\x12Proto::Shorten::V1b\x06proto3"
var (
file_proto_shorten_v1_shorten_proto_rawDescOnce sync.Once
file_proto_shorten_v1_shorten_proto_rawDescData []byte
)
func file_proto_shorten_v1_shorten_proto_rawDescGZIP() []byte {
file_proto_shorten_v1_shorten_proto_rawDescOnce.Do(func() {
file_proto_shorten_v1_shorten_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_shorten_v1_shorten_proto_rawDesc), len(file_proto_shorten_v1_shorten_proto_rawDesc)))
})
return file_proto_shorten_v1_shorten_proto_rawDescData
}
var file_proto_shorten_v1_shorten_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_proto_shorten_v1_shorten_proto_goTypes = []any{
(*CreateURLRedirectionRequest)(nil), // 0: proto.shorten.v1.CreateURLRedirectionRequest
(*CreateURLRedirectionResponse)(nil), // 1: proto.shorten.v1.CreateURLRedirectionResponse
(*URLRedirection)(nil), // 2: proto.shorten.v1.URLRedirection
(*ListURLRedirectionsRequest)(nil), // 3: proto.shorten.v1.ListURLRedirectionsRequest
(*ListURLRedirectionsResponse)(nil), // 4: proto.shorten.v1.ListURLRedirectionsResponse
(*DeactivateURLRedirectionRequest)(nil), // 5: proto.shorten.v1.DeactivateURLRedirectionRequest
(*DeactivateURLRedirectionResponse)(nil), // 6: proto.shorten.v1.DeactivateURLRedirectionResponse
(*DeleteURLRedirectionRequest)(nil), // 7: proto.shorten.v1.DeleteURLRedirectionRequest
(*DeleteURLRedirectionResponse)(nil), // 8: proto.shorten.v1.DeleteURLRedirectionResponse
(*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
}
var file_proto_shorten_v1_shorten_proto_depIdxs = []int32{
9, // 0: proto.shorten.v1.URLRedirection.created_at:type_name -> google.protobuf.Timestamp
9, // 1: proto.shorten.v1.URLRedirection.expires_at:type_name -> google.protobuf.Timestamp
2, // 2: proto.shorten.v1.ListURLRedirectionsResponse.url_redirections:type_name -> proto.shorten.v1.URLRedirection
0, // 3: proto.shorten.v1.ShortenService.CreateURLRedirection:input_type -> proto.shorten.v1.CreateURLRedirectionRequest
3, // 4: proto.shorten.v1.ShortenService.ListURLRedirections:input_type -> proto.shorten.v1.ListURLRedirectionsRequest
5, // 5: proto.shorten.v1.ShortenService.DeactivateURLRedirection:input_type -> proto.shorten.v1.DeactivateURLRedirectionRequest
7, // 6: proto.shorten.v1.ShortenService.DeleteURLRedirection:input_type -> proto.shorten.v1.DeleteURLRedirectionRequest
1, // 7: proto.shorten.v1.ShortenService.CreateURLRedirection:output_type -> proto.shorten.v1.CreateURLRedirectionResponse
4, // 8: proto.shorten.v1.ShortenService.ListURLRedirections:output_type -> proto.shorten.v1.ListURLRedirectionsResponse
6, // 9: proto.shorten.v1.ShortenService.DeactivateURLRedirection:output_type -> proto.shorten.v1.DeactivateURLRedirectionResponse
8, // 10: proto.shorten.v1.ShortenService.DeleteURLRedirection:output_type -> proto.shorten.v1.DeleteURLRedirectionResponse
7, // [7:11] is the sub-list for method output_type
3, // [3:7] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_proto_shorten_v1_shorten_proto_init() }
func file_proto_shorten_v1_shorten_proto_init() {
if File_proto_shorten_v1_shorten_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_shorten_v1_shorten_proto_rawDesc), len(file_proto_shorten_v1_shorten_proto_rawDesc)),
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_shorten_v1_shorten_proto_goTypes,
DependencyIndexes: file_proto_shorten_v1_shorten_proto_depIdxs,
MessageInfos: file_proto_shorten_v1_shorten_proto_msgTypes,
}.Build()
File_proto_shorten_v1_shorten_proto = out.File
file_proto_shorten_v1_shorten_proto_goTypes = nil
file_proto_shorten_v1_shorten_proto_depIdxs = nil
}

View File

@@ -0,0 +1,216 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: proto/shorten/v1/shorten.proto
package shortenv1connect
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
v1 "git.kocoder.xyz/vt/shortener/internal/proto/shorten/v1"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// ShortenServiceName is the fully-qualified name of the ShortenService service.
ShortenServiceName = "proto.shorten.v1.ShortenService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// ShortenServiceCreateURLRedirectionProcedure is the fully-qualified name of the ShortenService's
// CreateURLRedirection RPC.
ShortenServiceCreateURLRedirectionProcedure = "/proto.shorten.v1.ShortenService/CreateURLRedirection"
// ShortenServiceListURLRedirectionsProcedure is the fully-qualified name of the ShortenService's
// ListURLRedirections RPC.
ShortenServiceListURLRedirectionsProcedure = "/proto.shorten.v1.ShortenService/ListURLRedirections"
// ShortenServiceDeactivateURLRedirectionProcedure is the fully-qualified name of the
// ShortenService's DeactivateURLRedirection RPC.
ShortenServiceDeactivateURLRedirectionProcedure = "/proto.shorten.v1.ShortenService/DeactivateURLRedirection"
// ShortenServiceDeleteURLRedirectionProcedure is the fully-qualified name of the ShortenService's
// DeleteURLRedirection RPC.
ShortenServiceDeleteURLRedirectionProcedure = "/proto.shorten.v1.ShortenService/DeleteURLRedirection"
)
// ShortenServiceClient is a client for the proto.shorten.v1.ShortenService service.
type ShortenServiceClient interface {
CreateURLRedirection(context.Context, *v1.CreateURLRedirectionRequest) (*v1.CreateURLRedirectionResponse, error)
ListURLRedirections(context.Context, *v1.ListURLRedirectionsRequest) (*v1.ListURLRedirectionsResponse, error)
// rpc GetURLRedirection(GetURLRedirectionRequest) returns
// (GetURLRedirectionResponse) {}
DeactivateURLRedirection(context.Context, *v1.DeactivateURLRedirectionRequest) (*v1.DeactivateURLRedirectionResponse, error)
DeleteURLRedirection(context.Context, *v1.DeleteURLRedirectionRequest) (*v1.DeleteURLRedirectionResponse, error)
}
// NewShortenServiceClient constructs a client for the proto.shorten.v1.ShortenService service. By
// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,
// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the
// connect.WithGRPC() or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewShortenServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ShortenServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
shortenServiceMethods := v1.File_proto_shorten_v1_shorten_proto.Services().ByName("ShortenService").Methods()
return &shortenServiceClient{
createURLRedirection: connect.NewClient[v1.CreateURLRedirectionRequest, v1.CreateURLRedirectionResponse](
httpClient,
baseURL+ShortenServiceCreateURLRedirectionProcedure,
connect.WithSchema(shortenServiceMethods.ByName("CreateURLRedirection")),
connect.WithClientOptions(opts...),
),
listURLRedirections: connect.NewClient[v1.ListURLRedirectionsRequest, v1.ListURLRedirectionsResponse](
httpClient,
baseURL+ShortenServiceListURLRedirectionsProcedure,
connect.WithSchema(shortenServiceMethods.ByName("ListURLRedirections")),
connect.WithClientOptions(opts...),
),
deactivateURLRedirection: connect.NewClient[v1.DeactivateURLRedirectionRequest, v1.DeactivateURLRedirectionResponse](
httpClient,
baseURL+ShortenServiceDeactivateURLRedirectionProcedure,
connect.WithSchema(shortenServiceMethods.ByName("DeactivateURLRedirection")),
connect.WithClientOptions(opts...),
),
deleteURLRedirection: connect.NewClient[v1.DeleteURLRedirectionRequest, v1.DeleteURLRedirectionResponse](
httpClient,
baseURL+ShortenServiceDeleteURLRedirectionProcedure,
connect.WithSchema(shortenServiceMethods.ByName("DeleteURLRedirection")),
connect.WithClientOptions(opts...),
),
}
}
// shortenServiceClient implements ShortenServiceClient.
type shortenServiceClient struct {
createURLRedirection *connect.Client[v1.CreateURLRedirectionRequest, v1.CreateURLRedirectionResponse]
listURLRedirections *connect.Client[v1.ListURLRedirectionsRequest, v1.ListURLRedirectionsResponse]
deactivateURLRedirection *connect.Client[v1.DeactivateURLRedirectionRequest, v1.DeactivateURLRedirectionResponse]
deleteURLRedirection *connect.Client[v1.DeleteURLRedirectionRequest, v1.DeleteURLRedirectionResponse]
}
// CreateURLRedirection calls proto.shorten.v1.ShortenService.CreateURLRedirection.
func (c *shortenServiceClient) CreateURLRedirection(ctx context.Context, req *v1.CreateURLRedirectionRequest) (*v1.CreateURLRedirectionResponse, error) {
response, err := c.createURLRedirection.CallUnary(ctx, connect.NewRequest(req))
if response != nil {
return response.Msg, err
}
return nil, err
}
// ListURLRedirections calls proto.shorten.v1.ShortenService.ListURLRedirections.
func (c *shortenServiceClient) ListURLRedirections(ctx context.Context, req *v1.ListURLRedirectionsRequest) (*v1.ListURLRedirectionsResponse, error) {
response, err := c.listURLRedirections.CallUnary(ctx, connect.NewRequest(req))
if response != nil {
return response.Msg, err
}
return nil, err
}
// DeactivateURLRedirection calls proto.shorten.v1.ShortenService.DeactivateURLRedirection.
func (c *shortenServiceClient) DeactivateURLRedirection(ctx context.Context, req *v1.DeactivateURLRedirectionRequest) (*v1.DeactivateURLRedirectionResponse, error) {
response, err := c.deactivateURLRedirection.CallUnary(ctx, connect.NewRequest(req))
if response != nil {
return response.Msg, err
}
return nil, err
}
// DeleteURLRedirection calls proto.shorten.v1.ShortenService.DeleteURLRedirection.
func (c *shortenServiceClient) DeleteURLRedirection(ctx context.Context, req *v1.DeleteURLRedirectionRequest) (*v1.DeleteURLRedirectionResponse, error) {
response, err := c.deleteURLRedirection.CallUnary(ctx, connect.NewRequest(req))
if response != nil {
return response.Msg, err
}
return nil, err
}
// ShortenServiceHandler is an implementation of the proto.shorten.v1.ShortenService service.
type ShortenServiceHandler interface {
CreateURLRedirection(context.Context, *v1.CreateURLRedirectionRequest) (*v1.CreateURLRedirectionResponse, error)
ListURLRedirections(context.Context, *v1.ListURLRedirectionsRequest) (*v1.ListURLRedirectionsResponse, error)
// rpc GetURLRedirection(GetURLRedirectionRequest) returns
// (GetURLRedirectionResponse) {}
DeactivateURLRedirection(context.Context, *v1.DeactivateURLRedirectionRequest) (*v1.DeactivateURLRedirectionResponse, error)
DeleteURLRedirection(context.Context, *v1.DeleteURLRedirectionRequest) (*v1.DeleteURLRedirectionResponse, error)
}
// NewShortenServiceHandler builds an HTTP handler from the service implementation. It returns the
// path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewShortenServiceHandler(svc ShortenServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
shortenServiceMethods := v1.File_proto_shorten_v1_shorten_proto.Services().ByName("ShortenService").Methods()
shortenServiceCreateURLRedirectionHandler := connect.NewUnaryHandlerSimple(
ShortenServiceCreateURLRedirectionProcedure,
svc.CreateURLRedirection,
connect.WithSchema(shortenServiceMethods.ByName("CreateURLRedirection")),
connect.WithHandlerOptions(opts...),
)
shortenServiceListURLRedirectionsHandler := connect.NewUnaryHandlerSimple(
ShortenServiceListURLRedirectionsProcedure,
svc.ListURLRedirections,
connect.WithSchema(shortenServiceMethods.ByName("ListURLRedirections")),
connect.WithHandlerOptions(opts...),
)
shortenServiceDeactivateURLRedirectionHandler := connect.NewUnaryHandlerSimple(
ShortenServiceDeactivateURLRedirectionProcedure,
svc.DeactivateURLRedirection,
connect.WithSchema(shortenServiceMethods.ByName("DeactivateURLRedirection")),
connect.WithHandlerOptions(opts...),
)
shortenServiceDeleteURLRedirectionHandler := connect.NewUnaryHandlerSimple(
ShortenServiceDeleteURLRedirectionProcedure,
svc.DeleteURLRedirection,
connect.WithSchema(shortenServiceMethods.ByName("DeleteURLRedirection")),
connect.WithHandlerOptions(opts...),
)
return "/proto.shorten.v1.ShortenService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case ShortenServiceCreateURLRedirectionProcedure:
shortenServiceCreateURLRedirectionHandler.ServeHTTP(w, r)
case ShortenServiceListURLRedirectionsProcedure:
shortenServiceListURLRedirectionsHandler.ServeHTTP(w, r)
case ShortenServiceDeactivateURLRedirectionProcedure:
shortenServiceDeactivateURLRedirectionHandler.ServeHTTP(w, r)
case ShortenServiceDeleteURLRedirectionProcedure:
shortenServiceDeleteURLRedirectionHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedShortenServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedShortenServiceHandler struct{}
func (UnimplementedShortenServiceHandler) CreateURLRedirection(context.Context, *v1.CreateURLRedirectionRequest) (*v1.CreateURLRedirectionResponse, error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.shorten.v1.ShortenService.CreateURLRedirection is not implemented"))
}
func (UnimplementedShortenServiceHandler) ListURLRedirections(context.Context, *v1.ListURLRedirectionsRequest) (*v1.ListURLRedirectionsResponse, error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.shorten.v1.ShortenService.ListURLRedirections is not implemented"))
}
func (UnimplementedShortenServiceHandler) DeactivateURLRedirection(context.Context, *v1.DeactivateURLRedirectionRequest) (*v1.DeactivateURLRedirectionResponse, error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.shorten.v1.ShortenService.DeactivateURLRedirection is not implemented"))
}
func (UnimplementedShortenServiceHandler) DeleteURLRedirection(context.Context, *v1.DeleteURLRedirectionRequest) (*v1.DeleteURLRedirectionResponse, error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("proto.shorten.v1.ShortenService.DeleteURLRedirection is not implemented"))
}

83
internal/service/http.go Normal file
View File

@@ -0,0 +1,83 @@
package service
import (
"database/sql"
"log/slog"
"net/http"
"slices"
"strings"
"git.kocoder.xyz/vt/shortener/internal/config"
"git.kocoder.xyz/vt/shortener/internal/database"
)
type Server struct {
conf *config.Config
db *database.Queries
}
func NewServer(conf *config.Config, db *database.Queries) *Server {
return &Server{
conf: conf,
db: db,
}
}
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
path := req.URL.Path
path = strings.Trim(path, "/")
slog.InfoContext(ctx, "processing request", slog.String("path", path))
res, err := s.db.GetURLByShortCode(ctx, path)
if err != nil {
slog.ErrorContext(ctx, "failed to get url by short code", slog.String("path", path), slog.String("err", err.Error()))
w.WriteHeader(http.StatusInternalServerError)
return
}
forwardedFor := req.Header.Get("X-Forwarded-For")
remoteAddr := req.RemoteAddr
if slices.Contains(s.conf.TRUSTED_PROXIES, strings.Split(remoteAddr, ":")[0]) {
if strings.Contains(forwardedFor, ", ") {
remoteAddr = strings.Split(forwardedFor, ", ")[0]
} else {
remoteAddr = forwardedFor
}
}
slog.InfoContext(ctx, "tracking click",
slog.String("ip", remoteAddr),
slog.String("user_agent", req.UserAgent()),
slog.String("referrer", req.Referer()),
)
_, err = s.db.TrackClick(ctx, database.TrackClickParams{
UrlID: res.UrlID,
UserAgent: sql.NullString{
String: req.UserAgent(),
Valid: true,
},
Referrer: sql.NullString{
String: req.Referer(),
Valid: true,
},
IpAddress: sql.NullString{
String: remoteAddr,
Valid: true,
},
})
if err != nil {
slog.ErrorContext(ctx, "failed to track click", slog.String("err", err.Error()))
// Still continuing to redirect even if tracking fails, or we can return.
// Usually we still want to redirect the user so they reach their destination.
}
slog.InfoContext(ctx, "redirecting to destination", slog.String("long_url", res.LongUrl))
http.Redirect(w, req, res.LongUrl, http.StatusFound)
}

View File

@@ -0,0 +1,105 @@
package service
import (
"context"
"log/slog"
"time"
"git.kocoder.xyz/vt/shortener/internal/config"
"git.kocoder.xyz/vt/shortener/internal/database"
shortenv1 "git.kocoder.xyz/vt/shortener/internal/proto/shorten/v1"
"git.kocoder.xyz/vt/shortener/internal/proto/shorten/v1/shortenv1connect"
"google.golang.org/protobuf/types/known/timestamppb"
)
type ShortenerService struct {
conf *config.Config
db *database.Queries
}
// CreateURLRedirection implements [shortenv1connect.ShortenServiceHandler].
func (s *ShortenerService) CreateURLRedirection(ctx context.Context, req *shortenv1.CreateURLRedirectionRequest) (*shortenv1.CreateURLRedirectionResponse, error) {
_, err := s.db.CreateURL(ctx, database.CreateURLParams{
ShortCode: req.ShortCode,
LongUrl: req.Url,
IsActive: true,
CreatedAt: time.Now(),
})
if err != nil {
slog.Log(ctx, slog.LevelError, err.Error())
return &shortenv1.CreateURLRedirectionResponse{
Ok: false,
}, err
}
return &shortenv1.CreateURLRedirectionResponse{
Ok: true,
}, nil
}
// DeactivateURLRedirection implements [shortenv1connect.ShortenServiceHandler].
func (s *ShortenerService) DeactivateURLRedirection(ctx context.Context, req *shortenv1.DeactivateURLRedirectionRequest) (*shortenv1.DeactivateURLRedirectionResponse, error) {
_, err := s.db.SetActive(ctx, database.SetActiveParams{
IsActive: req.IsActive,
UrlID: req.UrlId,
})
if err != nil {
slog.Log(ctx, slog.LevelError, err.Error())
return &shortenv1.DeactivateURLRedirectionResponse{
Ok: false,
}, err
}
return &shortenv1.DeactivateURLRedirectionResponse{
Ok: true,
}, nil
}
// DeleteURLRedirection implements [shortenv1connect.ShortenServiceHandler].
func (s *ShortenerService) DeleteURLRedirection(ctx context.Context, req *shortenv1.DeleteURLRedirectionRequest) (*shortenv1.DeleteURLRedirectionResponse, error) {
_, err := s.db.DeleteURL(ctx, req.UrlId)
if err != nil {
slog.Log(ctx, slog.LevelError, err.Error())
return &shortenv1.DeleteURLRedirectionResponse{
Ok: false,
}, err
}
return &shortenv1.DeleteURLRedirectionResponse{
Ok: true,
}, nil
}
// ListURLRedirections implements [shortenv1connect.ShortenServiceHandler].
func (s *ShortenerService) ListURLRedirections(ctx context.Context, req *shortenv1.ListURLRedirectionsRequest) (*shortenv1.ListURLRedirectionsResponse, error) {
urls, err := s.db.GetURLs(ctx)
if err != nil {
slog.Log(ctx, slog.LevelError, err.Error())
return nil, err
}
var redirections []*shortenv1.URLRedirection
for _, url := range urls {
redirections = append(redirections, &shortenv1.URLRedirection{
UrlId: int32(url.UrlID),
ShortCode: url.ShortCode,
Url: url.LongUrl,
CreatedAt: timestamppb.New(url.CreatedAt),
ExpiresAt: timestamppb.New(url.ExpiresAt.Time),
IsActive: url.IsActive,
ClickCountByDay: []int32{},
})
}
return &shortenv1.ListURLRedirectionsResponse{
UrlRedirections: redirections,
}, nil
}
func NewShortenerService(conf *config.Config, db *database.Queries) shortenv1connect.ShortenServiceHandler {
return &ShortenerService{
conf: conf,
db: db,
}
}

12
k6.js Normal file
View File

@@ -0,0 +1,12 @@
import http from "k6/http";
import { sleep } from "k6";
export const options = {
iterations: 100,
};
export default function () {
http.get("http://localhost:3001/abcdefghij");
sleep(0.2);
}

69
main.go Normal file
View File

@@ -0,0 +1,69 @@
package main
import (
"context"
"log/slog"
"os"
"connectrpc.com/connect"
"connectrpc.com/validate"
sdb "git.kocoder.xyz/vt/service-base/database"
"git.kocoder.xyz/vt/service-base/middleware"
"git.kocoder.xyz/vt/service-base/rpc"
"git.kocoder.xyz/vt/service-base/server"
"git.kocoder.xyz/vt/service-base/telemetry"
"git.kocoder.xyz/vt/shortener/internal/config"
"git.kocoder.xyz/vt/shortener/internal/database"
"git.kocoder.xyz/vt/shortener/internal/proto/shorten/v1/shortenv1connect"
"git.kocoder.xyz/vt/shortener/internal/service"
_ "github.com/lib/pq"
)
func main() {
// Fallback logger for early startup errors
telemetry.SetupFallbackLogger()
conf := config.Read()
ctx := context.Background()
shutdown, err := telemetry.Init(ctx, conf.OTLP_ENDPOINT, "shortener-server")
if err != nil {
slog.Error("Failed to initialize telemetry", slog.String("err", err.Error()))
}
defer func() {
if err := shutdown(ctx); err != nil {
slog.Error("Error shutting down telemetry", slog.String("err", err.Error()))
}
}()
// Connect to DB and wrap with otelsql for database tracing
db, err := sdb.OpenPostgres(conf.DB_URL)
if err != nil {
slog.Error("failed to connect to database", slog.String("err", err.Error()))
os.Exit(1)
}
defer db.Close()
dbQueries := database.New(db)
srv := service.NewServer(conf, dbQueries)
mux := rpc.NewServeMux()
path, handler := shortenv1connect.NewShortenServiceHandler(
service.NewShortenerService(conf, dbQueries),
connect.WithInterceptors(validate.NewInterceptor()),
)
mux.RegisterConnect(shortenv1connect.ShortenServiceName, path, handler)
mux.Handle("/", srv)
mux.MountReflection()
c := middleware.NewCORS(conf.ALLOWED_ORIGINS)
corsMux := c.Handler(mux)
// Start server with OpenTelemetry tracing, HTTP/1.1 & HTTP/2 (h2c) support
if err := server.ListenAndServeH2C(conf.LISTEN_ON, corsMux, "shortener-server"); err != nil {
slog.Error("Server failed", slog.String("err", err.Error()))
}
}

View File

@@ -1,5 +1,5 @@
-- name: TrackClick :one
INSERT INTO click_logs (url_id, referrer, user_agent, ip_address)
INSERT INTO lss.click_logs (url_id, referrer, user_agent, ip_address)
VALUES (
$1,
$2,
@@ -9,4 +9,4 @@ VALUES (
RETURNING *;
-- name: GetClicks :many
SELECT * FROM click_logs;
SELECT * FROM lss.click_logs;

View File

@@ -1,5 +1,5 @@
-- name: CreateURL :one
INSERT INTO urls (created_at, expires_at, long_url, short_code, is_active)
INSERT INTO lss.urls (created_at, expires_at, long_url, short_code, is_active)
VALUES (
$1,
$2,
@@ -10,13 +10,13 @@ VALUES (
RETURNING *;
-- name: GetURLs :many
SELECT * FROM urls;
SELECT * FROM lss.urls;
-- name: GetURLByShortCode :one
SELECT url_id, long_url FROM urls WHERE is_active = true AND short_code = $1;
SELECT url_id, long_url FROM lss.urls WHERE is_active = true AND short_code = $1;
-- name: SetActive :one
UPDATE urls SET is_active = $1 WHERE url_id = $2 RETURNING *;
UPDATE lss.urls SET is_active = $1 WHERE url_id = $2 RETURNING *;
-- name: DeleteURL :one
DELETE FROM urls WHERE url_id = $1 RETURNING *;
DELETE FROM lss.urls WHERE url_id = $1 RETURNING *;

View File

@@ -1,5 +1,7 @@
-- +goose Up
CREATE TABLE urls (
CREATE SCHEMA IF NOT EXISTS lss;
CREATE TABLE lss.urls (
url_id SERIAL PRIMARY KEY,
long_url TEXT NOT NULL,
short_code VARCHAR(10) UNIQUE NOT NULL,
@@ -9,8 +11,8 @@ CREATE TABLE urls (
);
-- Crucial index for lightning-fast lookups when redirecting
CREATE INDEX idx_urls_short_code ON urls(short_code);
CREATE INDEX idx_urls_short_code ON lss.urls(short_code);
-- +goose Down
DROP INDEX idx_urls_short_code;
DROP TABLE urls;
DROP TABLE lss.urls;

View File

@@ -1,5 +1,5 @@
-- +goose Up
CREATE TABLE click_logs (
CREATE TABLE lss.click_logs (
click_id BIGSERIAL PRIMARY KEY,
url_id INT NOT NULL REFERENCES urls(url_id) ON DELETE CASCADE,
clicked_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -9,8 +9,8 @@ CREATE TABLE click_logs (
);
-- Index to optimize analytics queries for a specific link
CREATE INDEX idx_click_logs_url_id ON click_logs(url_id);
CREATE INDEX idx_click_logs_url_id ON lss.click_logs(url_id);
-- +goose Down
DROP INDEX idx_click_logs_url_id;
DROP TABLE click_logs;
DROP TABLE lss.click_logs;