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
1 change: 1 addition & 0 deletions docs/AI-USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ the part that matters most.
| 2026-10-01 | robertky | Codex (ChatGPT) | Assisted with formulating good reviews of the architecture and UI-specification PRs. The group decided to include decade filtering; Rachel implemented the corresponding schema and documentation update in #38. |
| 2026-10-10 | evenkkl | Claude | Assisted with implementing the GraphQL Yoga server with `searchAlbums` for #6, and formulated an easy to understand PR description. Guided the reset of the prototype tables in the VM database (backup taken first). |
| 2026-10-13 | evenkkl | Claude | Fixed the review feedback on #6: cursor values validated against the sort, `reviewCount` returns 0 until reviews exist. The group decided genre filters combine with AND; Claude made the filter and the genre counts match that. |
| 2026-10-09 | robertky | Codex (ChatGPT) | AI was a good sparring partner when I drafted the initial #5 PostgreSQL migration. It gave me ideas on how to structure and improve the tables, relation tables, constraints, indexes, full-text search triggers and TypeScript migration runner. |

## Notes

Expand Down
4 changes: 4 additions & 0 deletions docs/VM-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pnpm --filter @t19/server build
pnpm --filter @t19/server start # http://localhost:3001/graphql
```

`db:seed` is a development/reset command. It replaces the catalog and, through
foreign-key cascades, deletes its associated reviews and crate items. Do not run
it against a database with user content you intend to keep.

## Smoke suite

Run on the VM after a deploy. Each command prints what to expect. The
Expand Down
128 changes: 128 additions & 0 deletions server/drizzle/0001_user-content-and-tracks.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
CREATE TABLE "app_user" (
"id" uuid PRIMARY KEY NOT NULL,
"display_name" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "crate_item" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" uuid NOT NULL,
"album_id" integer NOT NULL,
"added_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "review" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" uuid NOT NULL,
"album_id" integer NOT NULL,
"rating" smallint NOT NULL,
"comment" text DEFAULT '' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "review_rating_range" CHECK ("review"."rating" BETWEEN 1 AND 5)
);
--> statement-breakpoint
CREATE TABLE "track" (
"id" serial PRIMARY KEY NOT NULL,
"album_id" integer NOT NULL,
"title" text NOT NULL,
"track_number" integer NOT NULL,
"duration_seconds" integer
);
--> statement-breakpoint
ALTER TABLE "crate_item" ADD CONSTRAINT "crate_item_user_id_app_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."app_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "crate_item" ADD CONSTRAINT "crate_item_album_id_album_id_fk" FOREIGN KEY ("album_id") REFERENCES "public"."album"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "review" ADD CONSTRAINT "review_user_id_app_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."app_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "review" ADD CONSTRAINT "review_album_id_album_id_fk" FOREIGN KEY ("album_id") REFERENCES "public"."album"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "track" ADD CONSTRAINT "track_album_id_album_id_fk" FOREIGN KEY ("album_id") REFERENCES "public"."album"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "crate_item_user_album_unique" ON "crate_item" USING btree ("user_id","album_id");--> statement-breakpoint
CREATE INDEX "crate_item_user_added_at_idx" ON "crate_item" USING btree ("user_id","added_at","id");--> statement-breakpoint
CREATE UNIQUE INDEX "review_user_album_unique" ON "review" USING btree ("user_id","album_id");--> statement-breakpoint
CREATE INDEX "review_album_idx" ON "review" USING btree ("album_id");--> statement-breakpoint
CREATE UNIQUE INDEX "track_album_track_number_unique" ON "track" USING btree ("album_id","track_number");--> statement-breakpoint
CREATE INDEX "track_album_track_number_idx" ON "track" USING btree ("album_id","track_number");
--> statement-breakpoint
CREATE FUNCTION set_album_search_vector()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('simple', NEW.title), 'A') ||
setweight(
to_tsvector(
'simple',
COALESCE((
SELECT string_agg(artist.name, ' ')
FROM album_artist
JOIN artist ON artist.id = album_artist.artist_id
WHERE album_artist.album_id = NEW.id
), '')
),
'B'
);
RETURN NEW;
END;
$$;
--> statement-breakpoint
CREATE TRIGGER album_search_vector_before_write
BEFORE INSERT OR UPDATE OF title ON album
FOR EACH ROW EXECUTE FUNCTION set_album_search_vector();
--> statement-breakpoint
CREATE FUNCTION refresh_album_search_vector_from_artist_link()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP <> 'DELETE' THEN
UPDATE album
SET title = title
WHERE id = NEW.album_id;
END IF;

IF TG_OP <> 'INSERT' AND OLD.album_id IS DISTINCT FROM NEW.album_id THEN
UPDATE album
SET title = title
WHERE id = OLD.album_id;
END IF;
RETURN NULL;
END;
$$;
--> statement-breakpoint
CREATE TRIGGER album_artist_refresh_search_vector
AFTER INSERT OR UPDATE OR DELETE ON album_artist
FOR EACH ROW EXECUTE FUNCTION refresh_album_search_vector_from_artist_link();
--> statement-breakpoint
CREATE FUNCTION refresh_album_search_vector_from_artist()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE album
SET title = title
WHERE id IN (
SELECT album_id FROM album_artist WHERE artist_id = NEW.id
);
RETURN NEW;
END;
$$;
--> statement-breakpoint
CREATE TRIGGER artist_refresh_search_vector
AFTER UPDATE OF name ON artist
FOR EACH ROW EXECUTE FUNCTION refresh_album_search_vector_from_artist();
--> statement-breakpoint
CREATE FUNCTION set_review_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$;
--> statement-breakpoint
CREATE TRIGGER review_set_updated_at
BEFORE UPDATE ON review
FOR EACH ROW EXECUTE FUNCTION set_review_updated_at();
--> statement-breakpoint
UPDATE album SET title = title;
Loading