first commit

Made-with: Cursor
This commit is contained in:
Michilis
2026-02-28 02:17:55 +00:00
commit 41f6ae916f
92 changed files with 12332 additions and 0 deletions

23
sqlc/queries/api_keys.sql Normal file
View File

@@ -0,0 +1,23 @@
-- name: CreateAPIKey :one
INSERT INTO api_keys (id, user_id, name, key_hash, scopes)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, user_id, name, key_hash, scopes, created_at, revoked_at;
-- name: ListAPIKeysByUser :many
SELECT id, name, scopes, created_at, revoked_at
FROM api_keys
WHERE user_id = $1
ORDER BY created_at DESC;
-- name: GetAPIKeyByHash :one
SELECT id, user_id, name, key_hash, scopes, created_at, revoked_at
FROM api_keys
WHERE key_hash = $1 AND revoked_at IS NULL;
-- name: RevokeAPIKey :exec
UPDATE api_keys SET revoked_at = now()
WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL;
-- name: RevokeAllUserAPIKeys :exec
UPDATE api_keys SET revoked_at = now()
WHERE user_id = $1 AND revoked_at IS NULL;

View File

@@ -0,0 +1,13 @@
-- name: ListAttachmentsByEvent :many
SELECT id, event_id, file_url
FROM event_attachments
WHERE event_id = $1
ORDER BY id ASC;
-- name: CreateAttachment :one
INSERT INTO event_attachments (id, event_id, file_url)
VALUES ($1, $2, $3)
RETURNING id, event_id, file_url;
-- name: DeleteAttachment :exec
DELETE FROM event_attachments WHERE id = $1 AND event_id = $2;

View File

@@ -0,0 +1,25 @@
-- name: CreateAttendee :one
INSERT INTO event_attendees (id, event_id, user_id, email, status)
VALUES ($1, $2, $3, $4, 'pending')
RETURNING id, event_id, user_id, email, status;
-- name: ListAttendeesByEvent :many
SELECT id, event_id, user_id, email, status
FROM event_attendees
WHERE event_id = $1
ORDER BY id ASC;
-- name: UpdateAttendeeStatus :one
UPDATE event_attendees
SET status = $2
WHERE id = $1
RETURNING id, event_id, user_id, email, status;
-- name: DeleteAttendee :exec
DELETE FROM event_attendees
WHERE id = $1 AND event_id = $2;
-- name: GetAttendeeByID :one
SELECT id, event_id, user_id, email, status
FROM event_attendees
WHERE id = $1;

View File

@@ -0,0 +1,3 @@
-- name: CreateAuditLog :exec
INSERT INTO audit_logs (entity_type, entity_id, action, user_id)
VALUES ($1, $2, $3, $4);

View File

@@ -0,0 +1,23 @@
-- name: CreateBookingLink :one
INSERT INTO booking_links (id, calendar_id, token, duration_minutes, buffer_minutes, timezone, working_hours, active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: GetBookingLinkByToken :one
SELECT * FROM booking_links
WHERE token = $1;
-- name: GetBookingLinkByCalendar :one
SELECT * FROM booking_links
WHERE calendar_id = $1;
-- name: UpdateBookingLink :one
UPDATE booking_links
SET duration_minutes = COALESCE($2, duration_minutes),
buffer_minutes = COALESCE($3, buffer_minutes),
timezone = COALESCE($4, timezone),
working_hours = COALESCE($5, working_hours),
active = COALESCE($6, active),
updated_at = now()
WHERE id = $1
RETURNING *;

View File

@@ -0,0 +1,23 @@
-- name: UpsertCalendarMember :exec
INSERT INTO calendar_members (calendar_id, user_id, role)
VALUES ($1, $2, $3)
ON CONFLICT (calendar_id, user_id) DO UPDATE SET role = $3;
-- name: GetCalendarMemberRole :one
SELECT role FROM calendar_members
WHERE calendar_id = $1 AND user_id = $2;
-- name: ListCalendarMembers :many
SELECT cm.user_id, u.email, cm.role
FROM calendar_members cm
JOIN users u ON u.id = cm.user_id
WHERE cm.calendar_id = $1 AND u.deleted_at IS NULL
ORDER BY cm.role ASC;
-- name: DeleteCalendarMember :exec
DELETE FROM calendar_members
WHERE calendar_id = $1 AND user_id = $2;
-- name: DeleteAllCalendarMembers :exec
DELETE FROM calendar_members
WHERE calendar_id = $1;

View File

@@ -0,0 +1,33 @@
-- name: CreateCalendar :one
INSERT INTO calendars (id, owner_id, name, color, is_public)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, owner_id, name, color, is_public, public_token, created_at, updated_at;
-- name: GetCalendarByID :one
SELECT id, owner_id, name, color, is_public, public_token, created_at, updated_at
FROM calendars
WHERE id = $1 AND deleted_at IS NULL;
-- name: ListCalendarsByUser :many
SELECT c.id, c.owner_id, c.name, c.color, c.is_public, c.created_at, c.updated_at, cm.role
FROM calendars c
JOIN calendar_members cm ON cm.calendar_id = c.id
WHERE cm.user_id = $1 AND c.deleted_at IS NULL
ORDER BY c.created_at ASC;
-- name: UpdateCalendar :one
UPDATE calendars
SET name = COALESCE(sqlc.narg('name')::TEXT, name),
color = COALESCE(sqlc.narg('color')::TEXT, color),
is_public = COALESCE(sqlc.narg('is_public')::BOOLEAN, is_public),
updated_at = now()
WHERE id = @id AND deleted_at IS NULL
RETURNING id, owner_id, name, color, is_public, public_token, created_at, updated_at;
-- name: SoftDeleteCalendar :exec
UPDATE calendars SET deleted_at = now(), updated_at = now()
WHERE id = $1 AND deleted_at IS NULL;
-- name: SoftDeleteCalendarsByOwner :exec
UPDATE calendars SET deleted_at = now(), updated_at = now()
WHERE owner_id = $1 AND deleted_at IS NULL;

46
sqlc/queries/contacts.sql Normal file
View File

@@ -0,0 +1,46 @@
-- name: CreateContact :one
INSERT INTO contacts (id, owner_id, first_name, last_name, email, phone, company, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: GetContactByID :one
SELECT * FROM contacts
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL;
-- name: ListContacts :many
SELECT * FROM contacts
WHERE owner_id = @owner_id
AND deleted_at IS NULL
AND (
sqlc.narg('search')::TEXT IS NULL
OR first_name ILIKE '%' || sqlc.narg('search')::TEXT || '%'
OR last_name ILIKE '%' || sqlc.narg('search')::TEXT || '%'
OR email ILIKE '%' || sqlc.narg('search')::TEXT || '%'
OR company ILIKE '%' || sqlc.narg('search')::TEXT || '%'
)
AND (
sqlc.narg('cursor_time')::TIMESTAMPTZ IS NULL
OR (created_at, id) > (sqlc.narg('cursor_time')::TIMESTAMPTZ, sqlc.narg('cursor_id')::UUID)
)
ORDER BY created_at ASC, id ASC
LIMIT @lim;
-- name: UpdateContact :one
UPDATE contacts
SET first_name = COALESCE(sqlc.narg('first_name'), first_name),
last_name = COALESCE(sqlc.narg('last_name'), last_name),
email = COALESCE(sqlc.narg('email'), email),
phone = COALESCE(sqlc.narg('phone'), phone),
company = COALESCE(sqlc.narg('company'), company),
notes = COALESCE(sqlc.narg('notes'), notes),
updated_at = now()
WHERE id = @id AND owner_id = @owner_id AND deleted_at IS NULL
RETURNING *;
-- name: SoftDeleteContact :exec
UPDATE contacts SET deleted_at = now(), updated_at = now()
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL;
-- name: SoftDeleteContactsByOwner :exec
UPDATE contacts SET deleted_at = now(), updated_at = now()
WHERE owner_id = $1 AND deleted_at IS NULL;

View File

@@ -0,0 +1,10 @@
-- name: ListExceptionsByEvent :many
SELECT id, event_id, exception_date, action
FROM event_exceptions
WHERE event_id = $1
ORDER BY exception_date ASC;
-- name: CreateEventException :one
INSERT INTO event_exceptions (id, event_id, exception_date, action)
VALUES ($1, $2, $3, $4)
RETURNING id, event_id, exception_date, action;

98
sqlc/queries/events.sql Normal file
View File

@@ -0,0 +1,98 @@
-- name: CreateEvent :one
INSERT INTO events (id, calendar_id, title, description, location, start_time, end_time, timezone, all_day, recurrence_rule, tags, created_by, updated_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING *;
-- name: GetEventByID :one
SELECT * FROM events
WHERE id = $1 AND deleted_at IS NULL;
-- name: ListEventsInRange :many
SELECT e.* FROM events e
JOIN calendar_members cm ON cm.calendar_id = e.calendar_id
WHERE cm.user_id = @user_id
AND e.deleted_at IS NULL
AND e.start_time < @range_end
AND e.end_time > @range_start
AND (sqlc.narg('calendar_id')::UUID IS NULL OR e.calendar_id = sqlc.narg('calendar_id')::UUID)
AND (sqlc.narg('search')::TEXT IS NULL OR (e.title ILIKE '%' || sqlc.narg('search')::TEXT || '%' OR e.description ILIKE '%' || sqlc.narg('search')::TEXT || '%'))
AND (sqlc.narg('tag')::TEXT IS NULL OR sqlc.narg('tag')::TEXT = ANY(e.tags))
AND (
sqlc.narg('cursor_time')::TIMESTAMPTZ IS NULL
OR (e.start_time, e.id) > (sqlc.narg('cursor_time')::TIMESTAMPTZ, sqlc.narg('cursor_id')::UUID)
)
ORDER BY e.start_time ASC, e.id ASC
LIMIT @lim;
-- name: ListRecurringEventsInRange :many
SELECT e.* FROM events e
JOIN calendar_members cm ON cm.calendar_id = e.calendar_id
WHERE cm.user_id = @user_id
AND e.deleted_at IS NULL
AND e.recurrence_rule IS NOT NULL
AND e.start_time <= @range_end
AND (sqlc.narg('calendar_id')::UUID IS NULL OR e.calendar_id = sqlc.narg('calendar_id')::UUID)
ORDER BY e.start_time ASC;
-- name: UpdateEvent :one
UPDATE events
SET title = COALESCE(sqlc.narg('title'), title),
description = COALESCE(sqlc.narg('description'), description),
location = COALESCE(sqlc.narg('location'), location),
start_time = COALESCE(sqlc.narg('start_time'), start_time),
end_time = COALESCE(sqlc.narg('end_time'), end_time),
timezone = COALESCE(sqlc.narg('timezone'), timezone),
all_day = COALESCE(sqlc.narg('all_day'), all_day),
recurrence_rule = sqlc.narg('recurrence_rule'),
tags = COALESCE(sqlc.narg('tags'), tags),
updated_by = @updated_by,
updated_at = now()
WHERE id = @id AND deleted_at IS NULL
RETURNING *;
-- name: SoftDeleteEvent :exec
UPDATE events SET deleted_at = now(), updated_at = now()
WHERE id = $1 AND deleted_at IS NULL;
-- name: SoftDeleteEventsByCalendar :exec
UPDATE events SET deleted_at = now(), updated_at = now()
WHERE calendar_id = $1 AND deleted_at IS NULL;
-- name: SoftDeleteEventsByCreator :exec
UPDATE events SET deleted_at = now(), updated_at = now()
WHERE created_by = $1 AND deleted_at IS NULL;
-- name: CheckEventOverlap :one
SELECT EXISTS(
SELECT 1 FROM events
WHERE calendar_id = $1
AND deleted_at IS NULL
AND start_time < $3
AND end_time > $2
) AS overlap;
-- name: CheckEventOverlapForUpdate :one
SELECT EXISTS(
SELECT 1 FROM events
WHERE calendar_id = $1
AND deleted_at IS NULL
AND start_time < $3
AND end_time > $2
FOR UPDATE
) AS overlap;
-- name: ListEventsByCalendarInRange :many
SELECT * FROM events
WHERE calendar_id = $1
AND deleted_at IS NULL
AND start_time < $3
AND end_time > $2
ORDER BY start_time ASC;
-- name: ListRecurringEventsByCalendar :many
SELECT * FROM events
WHERE calendar_id = $1
AND deleted_at IS NULL
AND recurrence_rule IS NOT NULL
AND start_time <= $2
ORDER BY start_time ASC;

View File

@@ -0,0 +1,16 @@
-- name: CreateRefreshToken :one
INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at)
VALUES ($1, $2, $3, $4)
RETURNING id, user_id, token_hash, expires_at, revoked_at, created_at;
-- name: GetRefreshTokenByHash :one
SELECT id, user_id, token_hash, expires_at, revoked_at, created_at
FROM refresh_tokens
WHERE token_hash = $1 AND revoked_at IS NULL;
-- name: RevokeRefreshToken :exec
UPDATE refresh_tokens SET revoked_at = now() WHERE token_hash = $1;
-- name: RevokeAllUserRefreshTokens :exec
UPDATE refresh_tokens SET revoked_at = now()
WHERE user_id = $1 AND revoked_at IS NULL;

View File

@@ -0,0 +1,18 @@
-- name: CreateReminder :one
INSERT INTO event_reminders (id, event_id, minutes_before)
VALUES ($1, $2, $3)
RETURNING id, event_id, minutes_before;
-- name: ListRemindersByEvent :many
SELECT id, event_id, minutes_before
FROM event_reminders
WHERE event_id = $1
ORDER BY minutes_before ASC;
-- name: DeleteReminder :exec
DELETE FROM event_reminders
WHERE id = $1 AND event_id = $2;
-- name: DeleteRemindersByEvent :exec
DELETE FROM event_reminders
WHERE event_id = $1;

25
sqlc/queries/users.sql Normal file
View File

@@ -0,0 +1,25 @@
-- name: CreateUser :one
INSERT INTO users (id, email, password_hash, timezone)
VALUES ($1, $2, $3, $4)
RETURNING id, email, password_hash, timezone, is_active, created_at, updated_at;
-- name: GetUserByID :one
SELECT id, email, password_hash, timezone, is_active, created_at, updated_at
FROM users
WHERE id = $1 AND deleted_at IS NULL;
-- name: GetUserByEmail :one
SELECT id, email, password_hash, timezone, is_active, created_at, updated_at
FROM users
WHERE email = $1 AND deleted_at IS NULL;
-- name: UpdateUser :one
UPDATE users
SET timezone = COALESCE(sqlc.narg('timezone')::TEXT, timezone),
updated_at = now()
WHERE id = @id AND deleted_at IS NULL
RETURNING id, email, password_hash, timezone, is_active, created_at, updated_at;
-- name: SoftDeleteUser :exec
UPDATE users SET deleted_at = now(), is_active = false, updated_at = now()
WHERE id = $1 AND deleted_at IS NULL;

174
sqlc/schema.sql Normal file
View File

@@ -0,0 +1,174 @@
-- Calendar & Contacts API Schema
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
timezone TEXT NOT NULL DEFAULT 'UTC',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX idx_users_email ON users (email) WHERE deleted_at IS NULL;
-- Refresh Tokens
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
token_hash TEXT NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens (user_id);
-- API Keys
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
scopes JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked_at TIMESTAMPTZ
);
CREATE INDEX idx_api_keys_user_id ON api_keys (user_id);
-- Calendars
CREATE TABLE calendars (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id UUID NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#3B82F6',
is_public BOOLEAN NOT NULL DEFAULT false,
public_token TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_calendars_owner_id ON calendars (owner_id);
-- Calendar Members
CREATE TABLE calendar_members (
calendar_id UUID NOT NULL REFERENCES calendars(id),
user_id UUID NOT NULL REFERENCES users(id),
role TEXT NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
PRIMARY KEY (calendar_id, user_id)
);
-- Events
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
calendar_id UUID NOT NULL REFERENCES calendars(id),
title TEXT NOT NULL,
description TEXT,
location TEXT,
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ NOT NULL,
timezone TEXT NOT NULL DEFAULT 'UTC',
all_day BOOLEAN NOT NULL DEFAULT false,
recurrence_rule TEXT,
tags TEXT[] NOT NULL DEFAULT '{}',
created_by UUID NOT NULL REFERENCES users(id),
updated_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_events_calendar_start ON events (calendar_id, start_time);
CREATE INDEX idx_events_start_time ON events (start_time);
CREATE INDEX idx_events_tags ON events USING GIN (tags);
-- Event Reminders
CREATE TABLE event_reminders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES events(id),
minutes_before INTEGER NOT NULL CHECK (minutes_before >= 0 AND minutes_before <= 10080)
);
CREATE INDEX idx_event_reminders_event_id ON event_reminders (event_id);
-- Event Attendees
CREATE TABLE event_attendees (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES events(id),
user_id UUID REFERENCES users(id),
email TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined', 'tentative'))
);
CREATE INDEX idx_event_attendees_event_id ON event_attendees (event_id);
-- Event Exceptions (for recurrence)
CREATE TABLE event_exceptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES events(id),
exception_date DATE NOT NULL,
action TEXT NOT NULL DEFAULT 'skip' CHECK (action IN ('skip'))
);
CREATE INDEX idx_event_exceptions_event_id ON event_exceptions (event_id);
-- Event Attachments
CREATE TABLE event_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES events(id),
file_url TEXT NOT NULL
);
CREATE INDEX idx_event_attachments_event_id ON event_attachments (event_id);
-- Contacts
CREATE TABLE contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id UUID NOT NULL REFERENCES users(id),
first_name TEXT,
last_name TEXT,
email TEXT,
phone TEXT,
company TEXT,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_contacts_owner_id ON contacts (owner_id);
CREATE INDEX idx_contacts_search ON contacts (owner_id, first_name, last_name, email, company);
-- Booking Links
CREATE TABLE booking_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
calendar_id UUID NOT NULL REFERENCES calendars(id),
token TEXT NOT NULL UNIQUE,
duration_minutes INTEGER NOT NULL,
buffer_minutes INTEGER NOT NULL DEFAULT 0,
timezone TEXT NOT NULL DEFAULT 'UTC',
working_hours JSONB NOT NULL DEFAULT '{}',
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_booking_links_token ON booking_links (token);
-- Audit Logs
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type TEXT NOT NULL,
entity_id UUID NOT NULL,
action TEXT NOT NULL,
user_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_logs_entity ON audit_logs (entity_type, entity_id);

12
sqlc/sqlc.yaml Normal file
View File

@@ -0,0 +1,12 @@
version: "2"
sql:
- engine: "postgresql"
queries: "queries/"
schema: "schema.sql"
gen:
go:
package: "repository"
out: "../internal/repository"
sql_package: "pgx/v5"
emit_json_tags: true
emit_empty_slices: true