diff --git a/docs/database-consolidation.md b/docs/database-consolidation.md index 6b5014fb..7e5e2db7 100644 --- a/docs/database-consolidation.md +++ b/docs/database-consolidation.md @@ -46,9 +46,33 @@ Consolidate into **18 unified tables** with a `board_type` discriminator column. 3. Set up environment variable `USE_UNIFIED_TABLES=false` #### Acceptance Criteria -- [ ] Unified schema file exists with board_type type -- [ ] Feature flag infrastructure in place -- [ ] No changes to runtime behavior +- [x] Unified schema file exists with board_type type +- [x] Feature flag infrastructure in place +- [x] No changes to runtime behavior + +--- + +## Implementation Status Summary + +| Component | Status | Notes | +|-----------|--------|-------| +| Phase 0: Preparation | ✅ Complete | unified.ts schema created | +| Phase 1-5: Migrations | ✅ Complete | All 22 tables created, data migrated | +| Task 4: table-select.ts | ✅ Complete | UNIFIED_TABLES, getUnifiedTable(), boardTypeCondition() | +| Task 5: Sync Functions | ✅ Complete | shared-sync.ts and user-sync.ts updated | +| Task 6: Query Files | ✅ Complete | All query files updated to use unified tables | +| Phase 6: Ascents/Bids Cleanup | ✅ Complete | Switched to boardsesh_ticks with NextAuth userId, legacy tables dropped | +| Task 7: MoonBoard Storage | 📋 Planned | Server-side storage for MoonBoard climbs | +| Phase 6b: Legacy Table Cleanup | 📋 Planned | Drop remaining legacy board-specific tables | + +### Phase 6 Key Decisions + +**Ascents/Bids Migration to boardsesh_ticks:** +- All ascent and bid data now uses `boardsesh_ticks` table exclusively +- Uses NextAuth `userId` (string) instead of Aurora `user_id` (integer) +- Data not associated with NextAuth userId was accepted as droppable +- Legacy tables (`kilter_ascents`, `kilter_bids`, `tension_ascents`, `tension_bids`) dropped via migration `0030_drop_legacy_ascents_bids.sql` +- Removed `migrate-user-history.ts` and `/api/internal/migrate-users-cron` route (no longer needed) --- @@ -152,10 +176,12 @@ INSERT INTO board_difficulty_grades (board_type, difficulty, boulder_name, is_li ``` #### Acceptance Criteria -- [ ] Both tables created with composite primary keys -- [ ] All kilter and tension data migrated -- [ ] MoonBoard reference data seeded -- [ ] Indexes created +- [x] Both tables created with composite primary keys +- [x] All kilter and tension data migrated +- [x] MoonBoard reference data seeded +- [x] Indexes created + +**Status**: ✅ Completed in migration `0025_shocking_clint_barton.sql` --- @@ -389,11 +415,13 @@ VALUES ('moonboard', 1, 1, 'Standard 40', true); ``` #### Acceptance Criteria -- [ ] All 9 tables created with proper foreign keys -- [ ] All kilter and tension data migrated -- [ ] MoonBoard product/layout/set data seeded -- [ ] MoonBoard holes generated (198 positions) -- [ ] Foreign key constraints validated +- [x] All 9 tables created with proper foreign keys +- [x] All kilter and tension data migrated +- [ ] MoonBoard product/layout/set data seeded (TODO: add in future migration) +- [ ] MoonBoard holes generated (198 positions) (TODO: add in future migration) +- [x] Foreign key constraints validated + +**Status**: ✅ Tables created and kilter/tension data migrated in `0025_shocking_clint_barton.sql` --- @@ -544,10 +572,12 @@ export const boardBetaLinks = pgTable('board_beta_links', { - MoonBoard climbs will be migrated from IndexedDB (separate task) #### Acceptance Criteria -- [ ] All 5 tables created with proper keys/indexes -- [ ] All kilter and tension climb data migrated -- [ ] Foreign keys to layouts validated -- [ ] Climb statistics preserved accurately +- [x] All 5 tables created with proper keys/indexes +- [x] All kilter and tension climb data migrated +- [x] Foreign keys to layouts validated +- [x] Climb statistics preserved accurately + +**Status**: ✅ Completed in migration `0025_shocking_clint_barton.sql` --- @@ -665,10 +695,12 @@ export const boardTags = pgTable('board_tags', { The `kilter_ascents`, `kilter_bids`, `tension_ascents`, `tension_bids` tables are already consolidated into `boardsesh_ticks`. These legacy tables should be dropped in Phase 6 after validating data integrity. #### Acceptance Criteria -- [ ] All 5 tables created -- [ ] All kilter and tension user data migrated -- [ ] Circuit-climb relationships preserved -- [ ] Wall configurations intact +- [x] All 5 tables created +- [x] All kilter and tension user data migrated +- [x] Circuit-climb relationships preserved +- [x] Wall configurations intact + +**Status**: ✅ Completed in migration `0025_shocking_clint_barton.sql` --- @@ -708,9 +740,11 @@ export const boardSharedSyncs = pgTable('board_shared_syncs', { ``` #### Acceptance Criteria -- [ ] Sync tables created -- [ ] Sync state migrated -- [ ] Aurora sync continues working +- [x] Sync tables created +- [x] Sync state migrated +- [x] Aurora sync continues working (Task 5 complete) + +**Status**: ✅ Tables created and data migrated in `0025_shocking_clint_barton.sql` --- diff --git a/package-lock.json b/package-lock.json index ab544795..a8af944d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4563,7 +4563,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.57.0" @@ -7327,7 +7327,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -8077,7 +8077,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -10233,7 +10233,7 @@ "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "node-gyp-build": "bin.js", @@ -10713,7 +10713,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.57.0" @@ -10732,7 +10732,7 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -10964,6 +10964,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10983,6 +10984,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -11282,6 +11284,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, "license": "MIT" }, "node_modules/scroll-into-view-if-needed": { diff --git a/packages/db/drizzle/0025_shocking_clint_barton.sql b/packages/db/drizzle/0025_shocking_clint_barton.sql new file mode 100644 index 00000000..a329bc76 --- /dev/null +++ b/packages/db/drizzle/0025_shocking_clint_barton.sql @@ -0,0 +1,505 @@ +CREATE TABLE "board_attempts" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "position" integer, + "name" text, + CONSTRAINT "board_attempts_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_beta_links" ( + "board_type" text NOT NULL, + "climb_uuid" text NOT NULL, + "link" text NOT NULL, + "foreign_username" text, + "angle" integer, + "thumbnail" text, + "is_listed" boolean, + "created_at" text, + CONSTRAINT "board_beta_links_board_type_climb_uuid_link_pk" PRIMARY KEY("board_type","climb_uuid","link") +); +--> statement-breakpoint +CREATE TABLE "board_circuits" ( + "board_type" text NOT NULL, + "uuid" text NOT NULL, + "name" text, + "description" text, + "color" text, + "user_id" integer, + "is_public" boolean, + "created_at" text, + "updated_at" text, + CONSTRAINT "board_circuits_board_type_uuid_pk" PRIMARY KEY("board_type","uuid") +); +--> statement-breakpoint +CREATE TABLE "board_circuits_climbs" ( + "board_type" text NOT NULL, + "circuit_uuid" text NOT NULL, + "climb_uuid" text NOT NULL, + "position" integer, + CONSTRAINT "board_circuits_climbs_board_type_circuit_uuid_climb_uuid_pk" PRIMARY KEY("board_type","circuit_uuid","climb_uuid") +); +--> statement-breakpoint +CREATE TABLE "board_climb_holds" ( + "board_type" text NOT NULL, + "climb_uuid" text NOT NULL, + "hold_id" integer NOT NULL, + "frame_number" integer NOT NULL, + "hold_state" text NOT NULL, + "created_at" timestamp DEFAULT now(), + CONSTRAINT "board_climb_holds_board_type_climb_uuid_hold_id_pk" PRIMARY KEY("board_type","climb_uuid","hold_id") +); +--> statement-breakpoint +CREATE TABLE "board_climb_stats" ( + "board_type" text NOT NULL, + "climb_uuid" text NOT NULL, + "angle" integer NOT NULL, + "display_difficulty" double precision, + "benchmark_difficulty" double precision, + "ascensionist_count" bigint, + "difficulty_average" double precision, + "quality_average" double precision, + "fa_username" text, + "fa_at" timestamp, + CONSTRAINT "board_climb_stats_board_type_climb_uuid_angle_pk" PRIMARY KEY("board_type","climb_uuid","angle") +); +--> statement-breakpoint +CREATE TABLE "board_climb_stats_history" ( + "id" bigserial PRIMARY KEY NOT NULL, + "board_type" text NOT NULL, + "climb_uuid" text NOT NULL, + "angle" integer NOT NULL, + "display_difficulty" double precision, + "benchmark_difficulty" double precision, + "ascensionist_count" bigint, + "difficulty_average" double precision, + "quality_average" double precision, + "fa_username" text, + "fa_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "board_climbs" ( + "uuid" text PRIMARY KEY NOT NULL, + "board_type" text NOT NULL, + "layout_id" integer NOT NULL, + "setter_id" integer, + "setter_username" text, + "name" text, + "description" text DEFAULT '', + "hsm" integer, + "edge_left" integer, + "edge_right" integer, + "edge_bottom" integer, + "edge_top" integer, + "angle" integer, + "frames_count" integer DEFAULT 1, + "frames_pace" integer DEFAULT 0, + "frames" text, + "is_draft" boolean DEFAULT false, + "is_listed" boolean, + "created_at" text, + "synced" boolean DEFAULT true NOT NULL, + "sync_error" text +); +--> statement-breakpoint +CREATE TABLE "board_difficulty_grades" ( + "board_type" text NOT NULL, + "difficulty" integer NOT NULL, + "boulder_name" text, + "route_name" text, + "is_listed" boolean, + CONSTRAINT "board_difficulty_grades_board_type_difficulty_pk" PRIMARY KEY("board_type","difficulty") +); +--> statement-breakpoint +CREATE TABLE "board_holes" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "product_id" integer, + "name" text, + "x" integer, + "y" integer, + "mirrored_hole_id" integer, + "mirror_group" integer DEFAULT 0, + CONSTRAINT "board_holes_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_layouts" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "product_id" integer, + "name" text, + "instagram_caption" text, + "is_mirrored" boolean, + "is_listed" boolean, + "password" text, + "created_at" text, + CONSTRAINT "board_layouts_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_leds" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "product_size_id" integer, + "hole_id" integer, + "position" integer, + CONSTRAINT "board_leds_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_placement_roles" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "product_id" integer, + "position" integer, + "name" text, + "full_name" text, + "led_color" text, + "screen_color" text, + CONSTRAINT "board_placement_roles_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_placements" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "layout_id" integer, + "hole_id" integer, + "set_id" integer, + "default_placement_role_id" integer, + CONSTRAINT "board_placements_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_product_sizes" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "product_id" integer NOT NULL, + "edge_left" integer, + "edge_right" integer, + "edge_bottom" integer, + "edge_top" integer, + "name" text, + "description" text, + "image_filename" text, + "position" integer, + "is_listed" boolean, + CONSTRAINT "board_product_sizes_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_product_sizes_layouts_sets" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "product_size_id" integer, + "layout_id" integer, + "set_id" integer, + "image_filename" text, + "is_listed" boolean, + CONSTRAINT "board_product_sizes_layouts_sets_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_products" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "name" text, + "is_listed" boolean, + "password" text, + "min_count_in_frame" integer, + "max_count_in_frame" integer, + CONSTRAINT "board_products_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_sets" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "name" text, + "hsm" integer, + CONSTRAINT "board_sets_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_shared_syncs" ( + "board_type" text NOT NULL, + "table_name" text NOT NULL, + "last_synchronized_at" text, + CONSTRAINT "board_shared_syncs_board_type_table_name_pk" PRIMARY KEY("board_type","table_name") +); +--> statement-breakpoint +CREATE TABLE "board_tags" ( + "board_type" text NOT NULL, + "entity_uuid" text NOT NULL, + "user_id" integer NOT NULL, + "name" text NOT NULL, + "is_listed" boolean, + CONSTRAINT "board_tags_board_type_entity_uuid_user_id_name_pk" PRIMARY KEY("board_type","entity_uuid","user_id","name") +); +--> statement-breakpoint +CREATE TABLE "board_user_syncs" ( + "board_type" text NOT NULL, + "user_id" integer NOT NULL, + "table_name" text NOT NULL, + "last_synchronized_at" text, + CONSTRAINT "board_user_syncs_board_type_user_id_table_name_pk" PRIMARY KEY("board_type","user_id","table_name") +); +--> statement-breakpoint +CREATE TABLE "board_users" ( + "board_type" text NOT NULL, + "id" integer NOT NULL, + "username" text, + "created_at" text, + CONSTRAINT "board_users_board_type_id_pk" PRIMARY KEY("board_type","id") +); +--> statement-breakpoint +CREATE TABLE "board_walls" ( + "board_type" text NOT NULL, + "uuid" text NOT NULL, + "user_id" integer, + "name" text, + "product_id" integer, + "is_adjustable" boolean, + "angle" integer, + "layout_id" integer, + "product_size_id" integer, + "hsm" integer, + "serial_number" text, + "created_at" text, + CONSTRAINT "board_walls_board_type_uuid_pk" PRIMARY KEY("board_type","uuid") +); +--> statement-breakpoint +-- ============================================================================= +-- DATA MIGRATION: Migrate data from board-specific tables to unified tables +-- ============================================================================= + +-- Level 0: Tables with no foreign key dependencies +-- ----------------------------------------------------------------------------- + +-- Migrate attempts data +INSERT INTO board_attempts (board_type, id, position, name) +SELECT 'kilter', id, position, name FROM kilter_attempts;--> statement-breakpoint + +INSERT INTO board_attempts (board_type, id, position, name) +SELECT 'tension', id, position, name FROM tension_attempts;--> statement-breakpoint + +-- Seed MoonBoard attempts +INSERT INTO board_attempts (board_type, id, position, name) VALUES + ('moonboard', 1, 1, 'Flash'), + ('moonboard', 2, 2, 'Send');--> statement-breakpoint + +-- Migrate difficulty grades data +INSERT INTO board_difficulty_grades (board_type, difficulty, boulder_name, route_name, is_listed) +SELECT 'kilter', difficulty, boulder_name, route_name, is_listed FROM kilter_difficulty_grades;--> statement-breakpoint + +INSERT INTO board_difficulty_grades (board_type, difficulty, boulder_name, route_name, is_listed) +SELECT 'tension', difficulty, boulder_name, route_name, is_listed FROM tension_difficulty_grades;--> statement-breakpoint + +-- Seed MoonBoard difficulty grades (Font scale) +INSERT INTO board_difficulty_grades (board_type, difficulty, boulder_name, is_listed) VALUES + ('moonboard', 10, '6A', true), + ('moonboard', 11, '6A+', true), + ('moonboard', 12, '6B', true), + ('moonboard', 13, '6B+', true), + ('moonboard', 14, '6C', true), + ('moonboard', 15, '6C+', true), + ('moonboard', 16, '7A', true), + ('moonboard', 17, '7A+', true), + ('moonboard', 18, '7B', true), + ('moonboard', 19, '7B+', true), + ('moonboard', 20, '7C', true), + ('moonboard', 21, '7C+', true), + ('moonboard', 22, '8A', true), + ('moonboard', 23, '8A+', true), + ('moonboard', 24, '8B', true), + ('moonboard', 25, '8B+', true);--> statement-breakpoint + +-- Migrate products data +INSERT INTO board_products (board_type, id, name, is_listed, password, min_count_in_frame, max_count_in_frame) +SELECT 'kilter', id, name, is_listed, password, min_count_in_frame, max_count_in_frame FROM kilter_products;--> statement-breakpoint + +INSERT INTO board_products (board_type, id, name, is_listed, password, min_count_in_frame, max_count_in_frame) +SELECT 'tension', id, name, is_listed, password, min_count_in_frame, max_count_in_frame FROM tension_products;--> statement-breakpoint + +-- Migrate sets data +INSERT INTO board_sets (board_type, id, name, hsm) +SELECT 'kilter', id, name, hsm FROM kilter_sets;--> statement-breakpoint + +INSERT INTO board_sets (board_type, id, name, hsm) +SELECT 'tension', id, name, hsm FROM tension_sets;--> statement-breakpoint + +-- Migrate users data +INSERT INTO board_users (board_type, id, username, created_at) +SELECT 'kilter', id, username, created_at FROM kilter_users;--> statement-breakpoint + +INSERT INTO board_users (board_type, id, username, created_at) +SELECT 'tension', id, username, created_at FROM tension_users;--> statement-breakpoint + +-- Level 1: Tables that depend on Level 0 +-- ----------------------------------------------------------------------------- + +-- Migrate layouts data +INSERT INTO board_layouts (board_type, id, product_id, name, instagram_caption, is_mirrored, is_listed, password, created_at) +SELECT 'kilter', id, product_id, name, instagram_caption, is_mirrored, is_listed, password, created_at FROM kilter_layouts;--> statement-breakpoint + +INSERT INTO board_layouts (board_type, id, product_id, name, instagram_caption, is_mirrored, is_listed, password, created_at) +SELECT 'tension', id, product_id, name, instagram_caption, is_mirrored, is_listed, password, created_at FROM tension_layouts;--> statement-breakpoint + +-- Migrate product sizes data +INSERT INTO board_product_sizes (board_type, id, product_id, edge_left, edge_right, edge_bottom, edge_top, name, description, image_filename, position, is_listed) +SELECT 'kilter', id, product_id, edge_left, edge_right, edge_bottom, edge_top, name, description, image_filename, position, is_listed FROM kilter_product_sizes;--> statement-breakpoint + +INSERT INTO board_product_sizes (board_type, id, product_id, edge_left, edge_right, edge_bottom, edge_top, name, description, image_filename, position, is_listed) +SELECT 'tension', id, product_id, edge_left, edge_right, edge_bottom, edge_top, name, description, image_filename, position, is_listed FROM tension_product_sizes;--> statement-breakpoint + +-- Migrate holes data +INSERT INTO board_holes (board_type, id, product_id, name, x, y, mirrored_hole_id, mirror_group) +SELECT 'kilter', id, product_id, name, x, y, mirrored_hole_id, mirror_group FROM kilter_holes;--> statement-breakpoint + +INSERT INTO board_holes (board_type, id, product_id, name, x, y, mirrored_hole_id, mirror_group) +SELECT 'tension', id, product_id, name, x, y, mirrored_hole_id, mirror_group FROM tension_holes;--> statement-breakpoint + +-- Migrate placement roles data +INSERT INTO board_placement_roles (board_type, id, product_id, position, name, full_name, led_color, screen_color) +SELECT 'kilter', id, product_id, position, name, full_name, led_color, screen_color FROM kilter_placement_roles;--> statement-breakpoint + +INSERT INTO board_placement_roles (board_type, id, product_id, position, name, full_name, led_color, screen_color) +SELECT 'tension', id, product_id, position, name, full_name, led_color, screen_color FROM tension_placement_roles;--> statement-breakpoint + +-- Migrate shared syncs data +INSERT INTO board_shared_syncs (board_type, table_name, last_synchronized_at) +SELECT 'kilter', table_name, last_synchronized_at FROM kilter_shared_syncs;--> statement-breakpoint + +INSERT INTO board_shared_syncs (board_type, table_name, last_synchronized_at) +SELECT 'tension', table_name, last_synchronized_at FROM tension_shared_syncs;--> statement-breakpoint + +-- Level 2: Tables that depend on Level 1 +-- ----------------------------------------------------------------------------- + +-- Migrate LEDs data +INSERT INTO board_leds (board_type, id, product_size_id, hole_id, position) +SELECT 'kilter', id, product_size_id, hole_id, position FROM kilter_leds;--> statement-breakpoint + +INSERT INTO board_leds (board_type, id, product_size_id, hole_id, position) +SELECT 'tension', id, product_size_id, hole_id, position FROM tension_leds;--> statement-breakpoint + +-- Migrate placements data +INSERT INTO board_placements (board_type, id, layout_id, hole_id, set_id, default_placement_role_id) +SELECT 'kilter', id, layout_id, hole_id, set_id, default_placement_role_id FROM kilter_placements;--> statement-breakpoint + +INSERT INTO board_placements (board_type, id, layout_id, hole_id, set_id, default_placement_role_id) +SELECT 'tension', id, layout_id, hole_id, set_id, default_placement_role_id FROM tension_placements;--> statement-breakpoint + +-- Migrate product_sizes_layouts_sets data +INSERT INTO board_product_sizes_layouts_sets (board_type, id, product_size_id, layout_id, set_id, image_filename, is_listed) +SELECT 'kilter', id, product_size_id, layout_id, set_id, image_filename, is_listed FROM kilter_product_sizes_layouts_sets;--> statement-breakpoint + +INSERT INTO board_product_sizes_layouts_sets (board_type, id, product_size_id, layout_id, set_id, image_filename, is_listed) +SELECT 'tension', id, product_size_id, layout_id, set_id, image_filename, is_listed FROM tension_product_sizes_layouts_sets;--> statement-breakpoint + +-- Migrate climbs data +INSERT INTO board_climbs (uuid, board_type, layout_id, setter_id, setter_username, name, description, hsm, edge_left, edge_right, edge_bottom, edge_top, angle, frames_count, frames_pace, frames, is_draft, is_listed, created_at, synced, sync_error) +SELECT uuid, 'kilter', layout_id, setter_id, setter_username, name, description, hsm, edge_left, edge_right, edge_bottom, edge_top, angle, frames_count, frames_pace, frames, is_draft, is_listed, created_at, synced, sync_error FROM kilter_climbs;--> statement-breakpoint + +INSERT INTO board_climbs (uuid, board_type, layout_id, setter_id, setter_username, name, description, hsm, edge_left, edge_right, edge_bottom, edge_top, angle, frames_count, frames_pace, frames, is_draft, is_listed, created_at, synced, sync_error) +SELECT uuid, 'tension', layout_id, setter_id, setter_username, name, description, hsm, edge_left, edge_right, edge_bottom, edge_top, angle, frames_count, frames_pace, frames, is_draft, is_listed, created_at, synced, sync_error FROM tension_climbs;--> statement-breakpoint + +-- Migrate circuits data +INSERT INTO board_circuits (board_type, uuid, name, description, color, user_id, is_public, created_at, updated_at) +SELECT 'kilter', uuid, name, description, color, user_id, is_public, created_at, updated_at FROM kilter_circuits;--> statement-breakpoint + +INSERT INTO board_circuits (board_type, uuid, name, description, color, user_id, is_public, created_at, updated_at) +SELECT 'tension', uuid, name, description, color, user_id, is_public, created_at, updated_at FROM tension_circuits;--> statement-breakpoint + +-- Migrate walls data +INSERT INTO board_walls (board_type, uuid, user_id, name, product_id, is_adjustable, angle, layout_id, product_size_id, hsm, serial_number, created_at) +SELECT 'kilter', uuid, user_id, name, product_id, is_adjustable, angle, layout_id, product_size_id, hsm, serial_number, created_at FROM kilter_walls;--> statement-breakpoint + +INSERT INTO board_walls (board_type, uuid, user_id, name, product_id, is_adjustable, angle, layout_id, product_size_id, hsm, serial_number, created_at) +SELECT 'tension', uuid, user_id, name, product_id, is_adjustable, angle, layout_id, product_size_id, hsm, serial_number, created_at FROM tension_walls;--> statement-breakpoint + +-- Migrate user syncs data +INSERT INTO board_user_syncs (board_type, user_id, table_name, last_synchronized_at) +SELECT 'kilter', user_id, table_name, last_synchronized_at FROM kilter_user_syncs;--> statement-breakpoint + +INSERT INTO board_user_syncs (board_type, user_id, table_name, last_synchronized_at) +SELECT 'tension', user_id, table_name, last_synchronized_at FROM tension_user_syncs;--> statement-breakpoint + +-- Level 3: Tables that depend on Level 2 +-- ----------------------------------------------------------------------------- + +-- Migrate climb stats data +INSERT INTO board_climb_stats (board_type, climb_uuid, angle, display_difficulty, benchmark_difficulty, ascensionist_count, difficulty_average, quality_average, fa_username, fa_at) +SELECT 'kilter', climb_uuid, angle, display_difficulty, benchmark_difficulty, ascensionist_count, difficulty_average, quality_average, fa_username, fa_at FROM kilter_climb_stats;--> statement-breakpoint + +INSERT INTO board_climb_stats (board_type, climb_uuid, angle, display_difficulty, benchmark_difficulty, ascensionist_count, difficulty_average, quality_average, fa_username, fa_at) +SELECT 'tension', climb_uuid, angle, display_difficulty, benchmark_difficulty, ascensionist_count, difficulty_average, quality_average, fa_username, fa_at FROM tension_climb_stats;--> statement-breakpoint + +-- Migrate climb holds data (using DISTINCT ON to handle duplicates in source data) +INSERT INTO board_climb_holds (board_type, climb_uuid, hold_id, frame_number, hold_state) +SELECT DISTINCT ON (climb_uuid, hold_id) 'kilter', climb_uuid, hold_id, frame_number, hold_state +FROM kilter_climb_holds +ORDER BY climb_uuid, hold_id, created_at DESC NULLS LAST;--> statement-breakpoint + +INSERT INTO board_climb_holds (board_type, climb_uuid, hold_id, frame_number, hold_state) +SELECT DISTINCT ON (climb_uuid, hold_id) 'tension', climb_uuid, hold_id, frame_number, hold_state +FROM tension_climb_holds +ORDER BY climb_uuid, hold_id, created_at DESC NULLS LAST;--> statement-breakpoint + +-- Migrate climb stats history data +INSERT INTO board_climb_stats_history (board_type, climb_uuid, angle, display_difficulty, benchmark_difficulty, ascensionist_count, difficulty_average, quality_average, fa_username, fa_at, created_at) +SELECT 'kilter', climb_uuid, angle, display_difficulty, benchmark_difficulty, ascensionist_count, difficulty_average, quality_average, fa_username, fa_at, created_at FROM kilter_climb_stats_history;--> statement-breakpoint + +INSERT INTO board_climb_stats_history (board_type, climb_uuid, angle, display_difficulty, benchmark_difficulty, ascensionist_count, difficulty_average, quality_average, fa_username, fa_at, created_at) +SELECT 'tension', climb_uuid, angle, display_difficulty, benchmark_difficulty, ascensionist_count, difficulty_average, quality_average, fa_username, fa_at, created_at FROM tension_climb_stats_history;--> statement-breakpoint + +-- Migrate beta links data +INSERT INTO board_beta_links (board_type, climb_uuid, link, foreign_username, angle, thumbnail, is_listed, created_at) +SELECT 'kilter', climb_uuid, link, foreign_username, angle, thumbnail, is_listed, created_at FROM kilter_beta_links;--> statement-breakpoint + +INSERT INTO board_beta_links (board_type, climb_uuid, link, foreign_username, angle, thumbnail, is_listed, created_at) +SELECT 'tension', climb_uuid, link, foreign_username, angle, thumbnail, is_listed, created_at FROM tension_beta_links;--> statement-breakpoint + +-- Migrate circuits_climbs data +INSERT INTO board_circuits_climbs (board_type, circuit_uuid, climb_uuid, position) +SELECT 'kilter', circuit_uuid, climb_uuid, position FROM kilter_circuits_climbs;--> statement-breakpoint + +INSERT INTO board_circuits_climbs (board_type, circuit_uuid, climb_uuid, position) +SELECT 'tension', circuit_uuid, climb_uuid, position FROM tension_circuits_climbs;--> statement-breakpoint + +-- Migrate tags data +INSERT INTO board_tags (board_type, entity_uuid, user_id, name, is_listed) +SELECT 'kilter', entity_uuid, user_id, name, is_listed FROM kilter_tags;--> statement-breakpoint + +INSERT INTO board_tags (board_type, entity_uuid, user_id, name, is_listed) +SELECT 'tension', entity_uuid, user_id, name, is_listed FROM tension_tags;--> statement-breakpoint + +-- ============================================================================= +-- END DATA MIGRATION +-- ============================================================================= + +-- Note: playlist column changes are handled by migration 0024_old_zombie.sql + +ALTER TABLE "board_beta_links" ADD CONSTRAINT "board_beta_links_climb_fk" FOREIGN KEY ("climb_uuid") REFERENCES "public"."board_climbs"("uuid") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_circuits" ADD CONSTRAINT "board_circuits_user_fk" FOREIGN KEY ("board_type","user_id") REFERENCES "public"."board_users"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_circuits_climbs" ADD CONSTRAINT "board_circuits_climbs_circuit_fk" FOREIGN KEY ("board_type","circuit_uuid") REFERENCES "public"."board_circuits"("board_type","uuid") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_circuits_climbs" ADD CONSTRAINT "board_circuits_climbs_climb_fk" FOREIGN KEY ("climb_uuid") REFERENCES "public"."board_climbs"("uuid") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_climb_holds" ADD CONSTRAINT "board_climb_holds_climb_fk" FOREIGN KEY ("climb_uuid") REFERENCES "public"."board_climbs"("uuid") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_climb_stats" ADD CONSTRAINT "board_climb_stats_climb_fk" FOREIGN KEY ("climb_uuid") REFERENCES "public"."board_climbs"("uuid") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_climbs" ADD CONSTRAINT "board_climbs_layout_fk" FOREIGN KEY ("board_type","layout_id") REFERENCES "public"."board_layouts"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_holes" ADD CONSTRAINT "board_holes_product_fk" FOREIGN KEY ("board_type","product_id") REFERENCES "public"."board_products"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_layouts" ADD CONSTRAINT "board_layouts_product_fk" FOREIGN KEY ("board_type","product_id") REFERENCES "public"."board_products"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_leds" ADD CONSTRAINT "board_leds_product_size_fk" FOREIGN KEY ("board_type","product_size_id") REFERENCES "public"."board_product_sizes"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_leds" ADD CONSTRAINT "board_leds_hole_fk" FOREIGN KEY ("board_type","hole_id") REFERENCES "public"."board_holes"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_placement_roles" ADD CONSTRAINT "board_placement_roles_product_fk" FOREIGN KEY ("board_type","product_id") REFERENCES "public"."board_products"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_placements" ADD CONSTRAINT "board_placements_layout_fk" FOREIGN KEY ("board_type","layout_id") REFERENCES "public"."board_layouts"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_placements" ADD CONSTRAINT "board_placements_hole_fk" FOREIGN KEY ("board_type","hole_id") REFERENCES "public"."board_holes"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_placements" ADD CONSTRAINT "board_placements_set_fk" FOREIGN KEY ("board_type","set_id") REFERENCES "public"."board_sets"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_placements" ADD CONSTRAINT "board_placements_role_fk" FOREIGN KEY ("board_type","default_placement_role_id") REFERENCES "public"."board_placement_roles"("board_type","id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_product_sizes" ADD CONSTRAINT "board_product_sizes_product_fk" FOREIGN KEY ("board_type","product_id") REFERENCES "public"."board_products"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_product_sizes_layouts_sets" ADD CONSTRAINT "board_psls_product_size_fk" FOREIGN KEY ("board_type","product_size_id") REFERENCES "public"."board_product_sizes"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_product_sizes_layouts_sets" ADD CONSTRAINT "board_psls_layout_fk" FOREIGN KEY ("board_type","layout_id") REFERENCES "public"."board_layouts"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_product_sizes_layouts_sets" ADD CONSTRAINT "board_psls_set_fk" FOREIGN KEY ("board_type","set_id") REFERENCES "public"."board_sets"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_user_syncs" ADD CONSTRAINT "board_user_syncs_user_fk" FOREIGN KEY ("board_type","user_id") REFERENCES "public"."board_users"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_walls" ADD CONSTRAINT "board_walls_user_fk" FOREIGN KEY ("board_type","user_id") REFERENCES "public"."board_users"("board_type","id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_walls" ADD CONSTRAINT "board_walls_product_fk" FOREIGN KEY ("board_type","product_id") REFERENCES "public"."board_products"("board_type","id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_walls" ADD CONSTRAINT "board_walls_layout_fk" FOREIGN KEY ("board_type","layout_id") REFERENCES "public"."board_layouts"("board_type","id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "board_walls" ADD CONSTRAINT "board_walls_product_size_fk" FOREIGN KEY ("board_type","product_size_id") REFERENCES "public"."board_product_sizes"("board_type","id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +CREATE INDEX "board_climb_holds_search_idx" ON "board_climb_holds" USING btree ("board_type","hold_id","hold_state");--> statement-breakpoint +CREATE INDEX "board_climb_stats_history_lookup_idx" ON "board_climb_stats_history" USING btree ("board_type","climb_uuid","angle");--> statement-breakpoint +CREATE INDEX "board_climbs_board_type_idx" ON "board_climbs" USING btree ("board_type");--> statement-breakpoint +CREATE INDEX "board_climbs_layout_filter_idx" ON "board_climbs" USING btree ("board_type","layout_id","is_listed","is_draft","frames_count");--> statement-breakpoint +CREATE INDEX "board_climbs_edges_idx" ON "board_climbs" USING btree ("board_type","edge_left","edge_right","edge_bottom","edge_top"); +-- Note: boardsesh_ticks_aurora_id_unique and playlists_aurora_id_idx are created by migration 0024_old_zombie.sql \ No newline at end of file diff --git a/packages/db/drizzle/0030_drop_legacy_ascents_bids.sql b/packages/db/drizzle/0030_drop_legacy_ascents_bids.sql new file mode 100644 index 00000000..3a5b1e91 --- /dev/null +++ b/packages/db/drizzle/0030_drop_legacy_ascents_bids.sql @@ -0,0 +1,11 @@ +-- Migration: Drop legacy ascents and bids tables +-- These tables are no longer used after switching to boardsesh_ticks +-- All ascent/bid data is now stored in boardsesh_ticks with NextAuth userId + +-- Drop legacy ascents tables +DROP TABLE IF EXISTS kilter_ascents; +DROP TABLE IF EXISTS tension_ascents; + +-- Drop legacy bids tables +DROP TABLE IF EXISTS kilter_bids; +DROP TABLE IF EXISTS tension_bids; diff --git a/packages/db/drizzle/meta/0025_snapshot.json b/packages/db/drizzle/meta/0025_snapshot.json new file mode 100644 index 00000000..f3ecd05f --- /dev/null +++ b/packages/db/drizzle/meta/0025_snapshot.json @@ -0,0 +1,7593 @@ +{ + "id": "06b2edee-e8ba-40cc-b343-7d62eca9faf7", + "prevId": "ea45b3f2-4992-43de-853f-34165222d8d2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.kilter_ascents": { + "name": "kilter_ascents", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_mirror": { + "name": "is_mirror", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_id": { + "name": "attempt_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bid_count": { + "name": "bid_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "quality": { + "name": "quality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_benchmark": { + "name": "is_benchmark", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "climbed_at": { + "name": "climbed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "ascents_attempt_id_fkey1": { + "name": "ascents_attempt_id_fkey1", + "tableFrom": "kilter_ascents", + "tableTo": "kilter_attempts", + "columnsFrom": [ + "attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "ascents_climb_uuid_fkey1": { + "name": "ascents_climb_uuid_fkey1", + "tableFrom": "kilter_ascents", + "tableTo": "kilter_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "ascents_difficulty_fkey1": { + "name": "ascents_difficulty_fkey1", + "tableFrom": "kilter_ascents", + "tableTo": "kilter_difficulty_grades", + "columnsFrom": [ + "difficulty" + ], + "columnsTo": [ + "difficulty" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "ascents_user_id_fkey1": { + "name": "ascents_user_id_fkey1", + "tableFrom": "kilter_ascents", + "tableTo": "kilter_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_attempts": { + "name": "kilter_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_beta_links": { + "name": "kilter_beta_links", + "schema": "", + "columns": { + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "foreign_username": { + "name": "foreign_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "beta_links_climb_uuid_fkey1": { + "name": "beta_links_climb_uuid_fkey1", + "tableFrom": "kilter_beta_links", + "tableTo": "kilter_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_bids": { + "name": "kilter_bids", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_mirror": { + "name": "is_mirror", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "bid_count": { + "name": "bid_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "climbed_at": { + "name": "climbed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "bids_climb_uuid_fkey1": { + "name": "bids_climb_uuid_fkey1", + "tableFrom": "kilter_bids", + "tableTo": "kilter_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "bids_user_id_fkey1": { + "name": "bids_user_id_fkey1", + "tableFrom": "kilter_bids", + "tableTo": "kilter_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_circuits": { + "name": "kilter_circuits", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_circuits_climbs": { + "name": "kilter_circuits_climbs", + "schema": "", + "columns": { + "circuit_uuid": { + "name": "circuit_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_climb_holds": { + "name": "kilter_climb_holds", + "schema": "", + "columns": { + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frame_number": { + "name": "frame_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hold_state": { + "name": "hold_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "kilter_climb_holds_search_idx": { + "name": "kilter_climb_holds_search_idx", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hold_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "kilter_climb_holds_climb_uuid_hold_id_pk": { + "name": "kilter_climb_holds_climb_uuid_hold_id_pk", + "columns": [ + "climb_uuid", + "hold_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_climb_stats": { + "name": "kilter_climb_stats", + "schema": "", + "columns": { + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "display_difficulty": { + "name": "display_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "benchmark_difficulty": { + "name": "benchmark_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "ascensionist_count": { + "name": "ascensionist_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "difficulty_average": { + "name": "difficulty_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "quality_average": { + "name": "quality_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "fa_username": { + "name": "fa_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fa_at": { + "name": "fa_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "kilter_climb_stats_pk": { + "name": "kilter_climb_stats_pk", + "columns": [ + "climb_uuid", + "angle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_climb_stats_history": { + "name": "kilter_climb_stats_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "display_difficulty": { + "name": "display_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "benchmark_difficulty": { + "name": "benchmark_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "ascensionist_count": { + "name": "ascensionist_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "difficulty_average": { + "name": "difficulty_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "quality_average": { + "name": "quality_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "fa_username": { + "name": "fa_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fa_at": { + "name": "fa_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_climbs": { + "name": "kilter_climbs", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "setter_id": { + "name": "setter_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "setter_username": { + "name": "setter_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "hsm": { + "name": "hsm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_left": { + "name": "edge_left", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_right": { + "name": "edge_right", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_bottom": { + "name": "edge_bottom", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_top": { + "name": "edge_top", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "frames_count": { + "name": "frames_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "frames_pace": { + "name": "frames_pace", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "frames": { + "name": "frames", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_draft": { + "name": "is_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kilter_climbs_layout_filter_idx": { + "name": "kilter_climbs_layout_filter_idx", + "columns": [ + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_listed", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_draft", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "frames_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kilter_climbs_edges_idx": { + "name": "kilter_climbs_edges_idx", + "columns": [ + { + "expression": "edge_left", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_right", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_bottom", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_top", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "climbs_layout_id_fkey1": { + "name": "climbs_layout_id_fkey1", + "tableFrom": "kilter_climbs", + "tableTo": "kilter_layouts", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_difficulty_grades": { + "name": "kilter_difficulty_grades", + "schema": "", + "columns": { + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "boulder_name": { + "name": "boulder_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route_name": { + "name": "route_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_holes": { + "name": "kilter_holes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "x": { + "name": "x", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "y": { + "name": "y", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mirrored_hole_id": { + "name": "mirrored_hole_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mirror_group": { + "name": "mirror_group", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "holes_product_id_fkey1": { + "name": "holes_product_id_fkey1", + "tableFrom": "kilter_holes", + "tableTo": "kilter_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_layouts": { + "name": "kilter_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instagram_caption": { + "name": "instagram_caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_mirrored": { + "name": "is_mirrored", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "layouts_product_id_fkey1": { + "name": "layouts_product_id_fkey1", + "tableFrom": "kilter_layouts", + "tableTo": "kilter_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_leds": { + "name": "kilter_leds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_size_id": { + "name": "product_size_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hole_id": { + "name": "hole_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "leds_hole_id_fkey1": { + "name": "leds_hole_id_fkey1", + "tableFrom": "kilter_leds", + "tableTo": "kilter_holes", + "columnsFrom": [ + "hole_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "leds_product_size_id_fkey1": { + "name": "leds_product_size_id_fkey1", + "tableFrom": "kilter_leds", + "tableTo": "kilter_product_sizes", + "columnsFrom": [ + "product_size_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_placement_roles": { + "name": "kilter_placement_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "led_color": { + "name": "led_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "screen_color": { + "name": "screen_color", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "placement_roles_product_id_fkey1": { + "name": "placement_roles_product_id_fkey1", + "tableFrom": "kilter_placement_roles", + "tableTo": "kilter_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_placements": { + "name": "kilter_placements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hole_id": { + "name": "hole_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_id": { + "name": "set_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "default_placement_role_id": { + "name": "default_placement_role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "placements_default_placement_role_id_fkey1": { + "name": "placements_default_placement_role_id_fkey1", + "tableFrom": "kilter_placements", + "tableTo": "kilter_placement_roles", + "columnsFrom": [ + "default_placement_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "placements_hole_id_fkey1": { + "name": "placements_hole_id_fkey1", + "tableFrom": "kilter_placements", + "tableTo": "kilter_holes", + "columnsFrom": [ + "hole_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "placements_layout_id_fkey1": { + "name": "placements_layout_id_fkey1", + "tableFrom": "kilter_placements", + "tableTo": "kilter_layouts", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "placements_set_id_fkey1": { + "name": "placements_set_id_fkey1", + "tableFrom": "kilter_placements", + "tableTo": "kilter_sets", + "columnsFrom": [ + "set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_product_sizes": { + "name": "kilter_product_sizes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_left": { + "name": "edge_left", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_right": { + "name": "edge_right", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_bottom": { + "name": "edge_bottom", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_top": { + "name": "edge_top", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "product_sizes_product_id_fkey1": { + "name": "product_sizes_product_id_fkey1", + "tableFrom": "kilter_product_sizes", + "tableTo": "kilter_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_product_sizes_layouts_sets": { + "name": "kilter_product_sizes_layouts_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_size_id": { + "name": "product_size_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_id": { + "name": "set_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "product_sizes_layouts_sets_layout_id_fkey1": { + "name": "product_sizes_layouts_sets_layout_id_fkey1", + "tableFrom": "kilter_product_sizes_layouts_sets", + "tableTo": "kilter_layouts", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "product_sizes_layouts_sets_product_size_id_fkey1": { + "name": "product_sizes_layouts_sets_product_size_id_fkey1", + "tableFrom": "kilter_product_sizes_layouts_sets", + "tableTo": "kilter_product_sizes", + "columnsFrom": [ + "product_size_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "product_sizes_layouts_sets_set_id_fkey1": { + "name": "product_sizes_layouts_sets_set_id_fkey1", + "tableFrom": "kilter_product_sizes_layouts_sets", + "tableTo": "kilter_sets", + "columnsFrom": [ + "set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_products": { + "name": "kilter_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "min_count_in_frame": { + "name": "min_count_in_frame", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_count_in_frame": { + "name": "max_count_in_frame", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_sets": { + "name": "kilter_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hsm": { + "name": "hsm", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_shared_syncs": { + "name": "kilter_shared_syncs", + "schema": "", + "columns": { + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_synchronized_at": { + "name": "last_synchronized_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_tags": { + "name": "kilter_tags", + "schema": "", + "columns": { + "entity_uuid": { + "name": "entity_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_user_syncs": { + "name": "kilter_user_syncs", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synchronized_at": { + "name": "last_synchronized_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_syncs_user_id_fkey1": { + "name": "user_syncs_user_id_fkey1", + "tableFrom": "kilter_user_syncs", + "tableTo": "kilter_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "kilter_user_sync_pk": { + "name": "kilter_user_sync_pk", + "columns": [ + "user_id", + "table_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_users": { + "name": "kilter_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilter_walls": { + "name": "kilter_walls", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_adjustable": { + "name": "is_adjustable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "product_size_id": { + "name": "product_size_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hsm": { + "name": "hsm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "serial_number": { + "name": "serial_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "walls_layout_id_fkey1": { + "name": "walls_layout_id_fkey1", + "tableFrom": "kilter_walls", + "tableTo": "kilter_layouts", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "walls_product_id_fkey1": { + "name": "walls_product_id_fkey1", + "tableFrom": "kilter_walls", + "tableTo": "kilter_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "walls_product_size_id_fkey1": { + "name": "walls_product_size_id_fkey1", + "tableFrom": "kilter_walls", + "tableTo": "kilter_product_sizes", + "columnsFrom": [ + "product_size_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "walls_user_id_fkey1": { + "name": "walls_user_id_fkey1", + "tableFrom": "kilter_walls", + "tableTo": "kilter_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_ascents": { + "name": "tension_ascents", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_mirror": { + "name": "is_mirror", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_id": { + "name": "attempt_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bid_count": { + "name": "bid_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "quality": { + "name": "quality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_benchmark": { + "name": "is_benchmark", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "climbed_at": { + "name": "climbed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "ascents_attempt_id_fkey": { + "name": "ascents_attempt_id_fkey", + "tableFrom": "tension_ascents", + "tableTo": "tension_attempts", + "columnsFrom": [ + "attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "ascents_climb_uuid_fkey": { + "name": "ascents_climb_uuid_fkey", + "tableFrom": "tension_ascents", + "tableTo": "tension_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "ascents_difficulty_fkey": { + "name": "ascents_difficulty_fkey", + "tableFrom": "tension_ascents", + "tableTo": "tension_difficulty_grades", + "columnsFrom": [ + "difficulty" + ], + "columnsTo": [ + "difficulty" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "ascents_user_id_fkey": { + "name": "ascents_user_id_fkey", + "tableFrom": "tension_ascents", + "tableTo": "tension_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_attempts": { + "name": "tension_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_beta_links": { + "name": "tension_beta_links", + "schema": "", + "columns": { + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "foreign_username": { + "name": "foreign_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "beta_links_climb_uuid_fkey": { + "name": "beta_links_climb_uuid_fkey", + "tableFrom": "tension_beta_links", + "tableTo": "tension_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_bids": { + "name": "tension_bids", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_mirror": { + "name": "is_mirror", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "bid_count": { + "name": "bid_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "climbed_at": { + "name": "climbed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "bids_climb_uuid_fkey": { + "name": "bids_climb_uuid_fkey", + "tableFrom": "tension_bids", + "tableTo": "tension_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "bids_user_id_fkey": { + "name": "bids_user_id_fkey", + "tableFrom": "tension_bids", + "tableTo": "tension_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_circuits": { + "name": "tension_circuits", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_circuits_climbs": { + "name": "tension_circuits_climbs", + "schema": "", + "columns": { + "circuit_uuid": { + "name": "circuit_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_climb_holds": { + "name": "tension_climb_holds", + "schema": "", + "columns": { + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frame_number": { + "name": "frame_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hold_state": { + "name": "hold_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "tension_climb_holds_search_idx": { + "name": "tension_climb_holds_search_idx", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hold_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tension_climb_holds_climb_uuid_hold_id_pk": { + "name": "tension_climb_holds_climb_uuid_hold_id_pk", + "columns": [ + "climb_uuid", + "hold_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_climb_stats": { + "name": "tension_climb_stats", + "schema": "", + "columns": { + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "display_difficulty": { + "name": "display_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "benchmark_difficulty": { + "name": "benchmark_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "ascensionist_count": { + "name": "ascensionist_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "difficulty_average": { + "name": "difficulty_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "quality_average": { + "name": "quality_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "fa_username": { + "name": "fa_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fa_at": { + "name": "fa_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tension_climb_stats_pk": { + "name": "tension_climb_stats_pk", + "columns": [ + "climb_uuid", + "angle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_climb_stats_history": { + "name": "tension_climb_stats_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "display_difficulty": { + "name": "display_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "benchmark_difficulty": { + "name": "benchmark_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "ascensionist_count": { + "name": "ascensionist_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "difficulty_average": { + "name": "difficulty_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "quality_average": { + "name": "quality_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "fa_username": { + "name": "fa_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fa_at": { + "name": "fa_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_climbs": { + "name": "tension_climbs", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "setter_id": { + "name": "setter_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "setter_username": { + "name": "setter_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "hsm": { + "name": "hsm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_left": { + "name": "edge_left", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_right": { + "name": "edge_right", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_bottom": { + "name": "edge_bottom", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_top": { + "name": "edge_top", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "frames_count": { + "name": "frames_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "frames_pace": { + "name": "frames_pace", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "frames": { + "name": "frames", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_draft": { + "name": "is_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tension_climbs_layout_filter_idx": { + "name": "tension_climbs_layout_filter_idx", + "columns": [ + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_listed", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_draft", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "frames_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tension_climbs_edges_idx": { + "name": "tension_climbs_edges_idx", + "columns": [ + { + "expression": "edge_left", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_right", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_bottom", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_top", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "climbs_layout_id_fkey": { + "name": "climbs_layout_id_fkey", + "tableFrom": "tension_climbs", + "tableTo": "tension_layouts", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_difficulty_grades": { + "name": "tension_difficulty_grades", + "schema": "", + "columns": { + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "boulder_name": { + "name": "boulder_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route_name": { + "name": "route_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_holes": { + "name": "tension_holes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "x": { + "name": "x", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "y": { + "name": "y", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mirrored_hole_id": { + "name": "mirrored_hole_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mirror_group": { + "name": "mirror_group", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "holes_mirrored_hole_id_fkey": { + "name": "holes_mirrored_hole_id_fkey", + "tableFrom": "tension_holes", + "tableTo": "tension_holes", + "columnsFrom": [ + "mirrored_hole_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "holes_product_id_fkey": { + "name": "holes_product_id_fkey", + "tableFrom": "tension_holes", + "tableTo": "tension_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_layouts": { + "name": "tension_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instagram_caption": { + "name": "instagram_caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_mirrored": { + "name": "is_mirrored", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "layouts_product_id_fkey": { + "name": "layouts_product_id_fkey", + "tableFrom": "tension_layouts", + "tableTo": "tension_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_leds": { + "name": "tension_leds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_size_id": { + "name": "product_size_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hole_id": { + "name": "hole_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "leds_hole_id_fkey": { + "name": "leds_hole_id_fkey", + "tableFrom": "tension_leds", + "tableTo": "tension_holes", + "columnsFrom": [ + "hole_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "leds_product_size_id_fkey": { + "name": "leds_product_size_id_fkey", + "tableFrom": "tension_leds", + "tableTo": "tension_product_sizes", + "columnsFrom": [ + "product_size_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_placement_roles": { + "name": "tension_placement_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "led_color": { + "name": "led_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "screen_color": { + "name": "screen_color", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "placement_roles_product_id_fkey": { + "name": "placement_roles_product_id_fkey", + "tableFrom": "tension_placement_roles", + "tableTo": "tension_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_placements": { + "name": "tension_placements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hole_id": { + "name": "hole_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_id": { + "name": "set_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "default_placement_role_id": { + "name": "default_placement_role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "placements_default_placement_role_id_fkey": { + "name": "placements_default_placement_role_id_fkey", + "tableFrom": "tension_placements", + "tableTo": "tension_placement_roles", + "columnsFrom": [ + "default_placement_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "placements_hole_id_fkey": { + "name": "placements_hole_id_fkey", + "tableFrom": "tension_placements", + "tableTo": "tension_holes", + "columnsFrom": [ + "hole_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "placements_layout_id_fkey": { + "name": "placements_layout_id_fkey", + "tableFrom": "tension_placements", + "tableTo": "tension_layouts", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "placements_set_id_fkey": { + "name": "placements_set_id_fkey", + "tableFrom": "tension_placements", + "tableTo": "tension_sets", + "columnsFrom": [ + "set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_product_sizes": { + "name": "tension_product_sizes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_left": { + "name": "edge_left", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_right": { + "name": "edge_right", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_bottom": { + "name": "edge_bottom", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_top": { + "name": "edge_top", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "product_sizes_product_id_fkey": { + "name": "product_sizes_product_id_fkey", + "tableFrom": "tension_product_sizes", + "tableTo": "tension_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_product_sizes_layouts_sets": { + "name": "tension_product_sizes_layouts_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "product_size_id": { + "name": "product_size_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_id": { + "name": "set_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "product_sizes_layouts_sets_layout_id_fkey": { + "name": "product_sizes_layouts_sets_layout_id_fkey", + "tableFrom": "tension_product_sizes_layouts_sets", + "tableTo": "tension_layouts", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "product_sizes_layouts_sets_product_size_id_fkey": { + "name": "product_sizes_layouts_sets_product_size_id_fkey", + "tableFrom": "tension_product_sizes_layouts_sets", + "tableTo": "tension_product_sizes", + "columnsFrom": [ + "product_size_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "product_sizes_layouts_sets_set_id_fkey": { + "name": "product_sizes_layouts_sets_set_id_fkey", + "tableFrom": "tension_product_sizes_layouts_sets", + "tableTo": "tension_sets", + "columnsFrom": [ + "set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_products": { + "name": "tension_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "min_count_in_frame": { + "name": "min_count_in_frame", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_count_in_frame": { + "name": "max_count_in_frame", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_sets": { + "name": "tension_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hsm": { + "name": "hsm", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_shared_syncs": { + "name": "tension_shared_syncs", + "schema": "", + "columns": { + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_synchronized_at": { + "name": "last_synchronized_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_tags": { + "name": "tension_tags", + "schema": "", + "columns": { + "entity_uuid": { + "name": "entity_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_user_syncs": { + "name": "tension_user_syncs", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synchronized_at": { + "name": "last_synchronized_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_syncs_user_id_fkey": { + "name": "user_syncs_user_id_fkey", + "tableFrom": "tension_user_syncs", + "tableTo": "tension_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "tension_user_sync_pk": { + "name": "tension_user_sync_pk", + "columns": [ + "user_id", + "table_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_users": { + "name": "tension_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tension_walls": { + "name": "tension_walls", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_adjustable": { + "name": "is_adjustable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "product_size_id": { + "name": "product_size_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hsm": { + "name": "hsm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "serial_number": { + "name": "serial_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "walls_layout_id_fkey": { + "name": "walls_layout_id_fkey", + "tableFrom": "tension_walls", + "tableTo": "tension_layouts", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "walls_product_id_fkey": { + "name": "walls_product_id_fkey", + "tableFrom": "tension_walls", + "tableTo": "tension_products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "walls_product_size_id_fkey": { + "name": "walls_product_size_id_fkey", + "tableFrom": "tension_walls", + "tableTo": "tension_product_sizes", + "columnsFrom": [ + "product_size_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "walls_user_id_fkey": { + "name": "walls_user_id_fkey", + "tableFrom": "tension_walls", + "tableTo": "tension_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_attempts": { + "name": "board_attempts", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_attempts_board_type_id_pk": { + "name": "board_attempts_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_beta_links": { + "name": "board_beta_links", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "foreign_username": { + "name": "foreign_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_beta_links_climb_fk": { + "name": "board_beta_links_climb_fk", + "tableFrom": "board_beta_links", + "tableTo": "board_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_beta_links_board_type_climb_uuid_link_pk": { + "name": "board_beta_links_board_type_climb_uuid_link_pk", + "columns": [ + "board_type", + "climb_uuid", + "link" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_circuits": { + "name": "board_circuits", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_circuits_user_fk": { + "name": "board_circuits_user_fk", + "tableFrom": "board_circuits", + "tableTo": "board_users", + "columnsFrom": [ + "board_type", + "user_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_circuits_board_type_uuid_pk": { + "name": "board_circuits_board_type_uuid_pk", + "columns": [ + "board_type", + "uuid" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_circuits_climbs": { + "name": "board_circuits_climbs", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "circuit_uuid": { + "name": "circuit_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_circuits_climbs_circuit_fk": { + "name": "board_circuits_climbs_circuit_fk", + "tableFrom": "board_circuits_climbs", + "tableTo": "board_circuits", + "columnsFrom": [ + "board_type", + "circuit_uuid" + ], + "columnsTo": [ + "board_type", + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "board_circuits_climbs_climb_fk": { + "name": "board_circuits_climbs_climb_fk", + "tableFrom": "board_circuits_climbs", + "tableTo": "board_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_circuits_climbs_board_type_circuit_uuid_climb_uuid_pk": { + "name": "board_circuits_climbs_board_type_circuit_uuid_climb_uuid_pk", + "columns": [ + "board_type", + "circuit_uuid", + "climb_uuid" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_climb_holds": { + "name": "board_climb_holds", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frame_number": { + "name": "frame_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hold_state": { + "name": "hold_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "board_climb_holds_search_idx": { + "name": "board_climb_holds_search_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hold_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_climb_holds_climb_fk": { + "name": "board_climb_holds_climb_fk", + "tableFrom": "board_climb_holds", + "tableTo": "board_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_climb_holds_board_type_climb_uuid_hold_id_pk": { + "name": "board_climb_holds_board_type_climb_uuid_hold_id_pk", + "columns": [ + "board_type", + "climb_uuid", + "hold_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_climb_stats": { + "name": "board_climb_stats", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "display_difficulty": { + "name": "display_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "benchmark_difficulty": { + "name": "benchmark_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "ascensionist_count": { + "name": "ascensionist_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "difficulty_average": { + "name": "difficulty_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "quality_average": { + "name": "quality_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "fa_username": { + "name": "fa_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fa_at": { + "name": "fa_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_climb_stats_climb_fk": { + "name": "board_climb_stats_climb_fk", + "tableFrom": "board_climb_stats", + "tableTo": "board_climbs", + "columnsFrom": [ + "climb_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_climb_stats_board_type_climb_uuid_angle_pk": { + "name": "board_climb_stats_board_type_climb_uuid_angle_pk", + "columns": [ + "board_type", + "climb_uuid", + "angle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_climb_stats_history": { + "name": "board_climb_stats_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "display_difficulty": { + "name": "display_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "benchmark_difficulty": { + "name": "benchmark_difficulty", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "ascensionist_count": { + "name": "ascensionist_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "difficulty_average": { + "name": "difficulty_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "quality_average": { + "name": "quality_average", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "fa_username": { + "name": "fa_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fa_at": { + "name": "fa_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_climb_stats_history_lookup_idx": { + "name": "board_climb_stats_history_lookup_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "angle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_climbs": { + "name": "board_climbs", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "setter_id": { + "name": "setter_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "setter_username": { + "name": "setter_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "hsm": { + "name": "hsm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_left": { + "name": "edge_left", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_right": { + "name": "edge_right", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_bottom": { + "name": "edge_bottom", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_top": { + "name": "edge_top", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "frames_count": { + "name": "frames_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "frames_pace": { + "name": "frames_pace", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "frames": { + "name": "frames", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_draft": { + "name": "is_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced": { + "name": "synced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "board_climbs_board_type_idx": { + "name": "board_climbs_board_type_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_climbs_layout_filter_idx": { + "name": "board_climbs_layout_filter_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_listed", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_draft", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "frames_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_climbs_edges_idx": { + "name": "board_climbs_edges_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_left", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_right", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_bottom", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "edge_top", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_climbs_layout_fk": { + "name": "board_climbs_layout_fk", + "tableFrom": "board_climbs", + "tableTo": "board_layouts", + "columnsFrom": [ + "board_type", + "layout_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_difficulty_grades": { + "name": "board_difficulty_grades", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "boulder_name": { + "name": "boulder_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route_name": { + "name": "route_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_difficulty_grades_board_type_difficulty_pk": { + "name": "board_difficulty_grades_board_type_difficulty_pk", + "columns": [ + "board_type", + "difficulty" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_holes": { + "name": "board_holes", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "x": { + "name": "x", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "y": { + "name": "y", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mirrored_hole_id": { + "name": "mirrored_hole_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mirror_group": { + "name": "mirror_group", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "board_holes_product_fk": { + "name": "board_holes_product_fk", + "tableFrom": "board_holes", + "tableTo": "board_products", + "columnsFrom": [ + "board_type", + "product_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_holes_board_type_id_pk": { + "name": "board_holes_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_layouts": { + "name": "board_layouts", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instagram_caption": { + "name": "instagram_caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_mirrored": { + "name": "is_mirrored", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_layouts_product_fk": { + "name": "board_layouts_product_fk", + "tableFrom": "board_layouts", + "tableTo": "board_products", + "columnsFrom": [ + "board_type", + "product_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_layouts_board_type_id_pk": { + "name": "board_layouts_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_leds": { + "name": "board_leds", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_size_id": { + "name": "product_size_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hole_id": { + "name": "hole_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_leds_product_size_fk": { + "name": "board_leds_product_size_fk", + "tableFrom": "board_leds", + "tableTo": "board_product_sizes", + "columnsFrom": [ + "board_type", + "product_size_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "board_leds_hole_fk": { + "name": "board_leds_hole_fk", + "tableFrom": "board_leds", + "tableTo": "board_holes", + "columnsFrom": [ + "board_type", + "hole_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_leds_board_type_id_pk": { + "name": "board_leds_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_placement_roles": { + "name": "board_placement_roles", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "led_color": { + "name": "led_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "screen_color": { + "name": "screen_color", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_placement_roles_product_fk": { + "name": "board_placement_roles_product_fk", + "tableFrom": "board_placement_roles", + "tableTo": "board_products", + "columnsFrom": [ + "board_type", + "product_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_placement_roles_board_type_id_pk": { + "name": "board_placement_roles_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_placements": { + "name": "board_placements", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hole_id": { + "name": "hole_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_id": { + "name": "set_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "default_placement_role_id": { + "name": "default_placement_role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_placements_layout_fk": { + "name": "board_placements_layout_fk", + "tableFrom": "board_placements", + "tableTo": "board_layouts", + "columnsFrom": [ + "board_type", + "layout_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "board_placements_hole_fk": { + "name": "board_placements_hole_fk", + "tableFrom": "board_placements", + "tableTo": "board_holes", + "columnsFrom": [ + "board_type", + "hole_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "board_placements_set_fk": { + "name": "board_placements_set_fk", + "tableFrom": "board_placements", + "tableTo": "board_sets", + "columnsFrom": [ + "board_type", + "set_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "board_placements_role_fk": { + "name": "board_placements_role_fk", + "tableFrom": "board_placements", + "tableTo": "board_placement_roles", + "columnsFrom": [ + "board_type", + "default_placement_role_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_placements_board_type_id_pk": { + "name": "board_placements_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_product_sizes": { + "name": "board_product_sizes", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "edge_left": { + "name": "edge_left", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_right": { + "name": "edge_right", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_bottom": { + "name": "edge_bottom", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "edge_top": { + "name": "edge_top", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_product_sizes_product_fk": { + "name": "board_product_sizes_product_fk", + "tableFrom": "board_product_sizes", + "tableTo": "board_products", + "columnsFrom": [ + "board_type", + "product_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_product_sizes_board_type_id_pk": { + "name": "board_product_sizes_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_product_sizes_layouts_sets": { + "name": "board_product_sizes_layouts_sets", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_size_id": { + "name": "product_size_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_id": { + "name": "set_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_psls_product_size_fk": { + "name": "board_psls_product_size_fk", + "tableFrom": "board_product_sizes_layouts_sets", + "tableTo": "board_product_sizes", + "columnsFrom": [ + "board_type", + "product_size_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "board_psls_layout_fk": { + "name": "board_psls_layout_fk", + "tableFrom": "board_product_sizes_layouts_sets", + "tableTo": "board_layouts", + "columnsFrom": [ + "board_type", + "layout_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "board_psls_set_fk": { + "name": "board_psls_set_fk", + "tableFrom": "board_product_sizes_layouts_sets", + "tableTo": "board_sets", + "columnsFrom": [ + "board_type", + "set_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_product_sizes_layouts_sets_board_type_id_pk": { + "name": "board_product_sizes_layouts_sets_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_products": { + "name": "board_products", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "min_count_in_frame": { + "name": "min_count_in_frame", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_count_in_frame": { + "name": "max_count_in_frame", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_products_board_type_id_pk": { + "name": "board_products_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_sets": { + "name": "board_sets", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hsm": { + "name": "hsm", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_sets_board_type_id_pk": { + "name": "board_sets_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_shared_syncs": { + "name": "board_shared_syncs", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synchronized_at": { + "name": "last_synchronized_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_shared_syncs_board_type_table_name_pk": { + "name": "board_shared_syncs_board_type_table_name_pk", + "columns": [ + "board_type", + "table_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_tags": { + "name": "board_tags", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_uuid": { + "name": "entity_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_listed": { + "name": "is_listed", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_tags_board_type_entity_uuid_user_id_name_pk": { + "name": "board_tags_board_type_entity_uuid_user_id_name_pk", + "columns": [ + "board_type", + "entity_uuid", + "user_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_user_syncs": { + "name": "board_user_syncs", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synchronized_at": { + "name": "last_synchronized_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_user_syncs_user_fk": { + "name": "board_user_syncs_user_fk", + "tableFrom": "board_user_syncs", + "tableTo": "board_users", + "columnsFrom": [ + "board_type", + "user_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_user_syncs_board_type_user_id_table_name_pk": { + "name": "board_user_syncs_board_type_user_id_table_name_pk", + "columns": [ + "board_type", + "user_id", + "table_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_users": { + "name": "board_users", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_users_board_type_id_pk": { + "name": "board_users_board_type_id_pk", + "columns": [ + "board_type", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_walls": { + "name": "board_walls", + "schema": "", + "columns": { + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_adjustable": { + "name": "is_adjustable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "product_size_id": { + "name": "product_size_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hsm": { + "name": "hsm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "serial_number": { + "name": "serial_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_walls_user_fk": { + "name": "board_walls_user_fk", + "tableFrom": "board_walls", + "tableTo": "board_users", + "columnsFrom": [ + "board_type", + "user_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "board_walls_product_fk": { + "name": "board_walls_product_fk", + "tableFrom": "board_walls", + "tableTo": "board_products", + "columnsFrom": [ + "board_type", + "product_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "board_walls_layout_fk": { + "name": "board_walls_layout_fk", + "tableFrom": "board_walls", + "tableTo": "board_layouts", + "columnsFrom": [ + "board_type", + "layout_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "board_walls_product_size_fk": { + "name": "board_walls_product_size_fk", + "tableFrom": "board_walls", + "tableTo": "board_product_sizes", + "columnsFrom": [ + "board_type", + "product_size_id" + ], + "columnsTo": [ + "board_type", + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "board_walls_board_type_uuid_pk": { + "name": "board_walls_board_type_uuid_pk", + "columns": [ + "board_type", + "uuid" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_userId_users_id_fk": { + "name": "accounts_userId_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "accounts_provider_providerAccountId_pk": { + "name": "accounts_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_userId_users_id_fk": { + "name": "sessions_userId_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationTokens": { + "name": "verificationTokens", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationTokens_identifier_token_pk": { + "name": "verificationTokens_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_credentials": { + "name": "user_credentials", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_credentials_user_id_users_id_fk": { + "name": "user_credentials_user_id_users_id_fk", + "tableFrom": "user_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instagram_url": { + "name": "instagram_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_profiles_user_id_users_id_fk": { + "name": "user_profiles_user_id_users_id_fk", + "tableFrom": "user_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.aurora_credentials": { + "name": "aurora_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aurora_user_id": { + "name": "aurora_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "aurora_token": { + "name": "aurora_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_user_board_credential": { + "name": "unique_user_board_credential", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "aurora_credentials_user_idx": { + "name": "aurora_credentials_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "aurora_credentials_user_id_users_id_fk": { + "name": "aurora_credentials_user_id_users_id_fk", + "tableFrom": "aurora_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_board_mappings": { + "name": "user_board_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_user_id": { + "name": "board_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "board_username": { + "name": "board_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_user_board_mapping": { + "name": "unique_user_board_mapping", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_user_mapping_idx": { + "name": "board_user_mapping_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_board_mappings_user_id_users_id_fk": { + "name": "user_board_mappings_user_id_users_id_fk", + "tableFrom": "user_board_mappings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_session_clients": { + "name": "board_session_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_leader": { + "name": "is_leader", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_session_clients_session_id_board_sessions_id_fk": { + "name": "board_session_clients_session_id_board_sessions_id_fk", + "tableFrom": "board_session_clients", + "tableTo": "board_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_session_queues": { + "name": "board_session_queues", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "queue": { + "name": "queue", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "current_climb_queue_item": { + "name": "current_climb_queue_item", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'null'::jsonb" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "board_session_queues_session_id_board_sessions_id_fk": { + "name": "board_session_queues_session_id_board_sessions_id_fk", + "tableFrom": "board_session_queues", + "tableTo": "board_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_sessions": { + "name": "board_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "board_path": { + "name": "board_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_activity": { + "name": "last_activity", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "discoverable": { + "name": "discoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "board_sessions_location_idx": { + "name": "board_sessions_location_idx", + "columns": [ + { + "expression": "latitude", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "longitude", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_sessions_discoverable_idx": { + "name": "board_sessions_discoverable_idx", + "columns": [ + { + "expression": "discoverable", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_sessions_user_idx": { + "name": "board_sessions_user_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_sessions_status_idx": { + "name": "board_sessions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_sessions_last_activity_idx": { + "name": "board_sessions_last_activity_idx", + "columns": [ + { + "expression": "last_activity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_sessions_discovery_idx": { + "name": "board_sessions_discovery_idx", + "columns": [ + { + "expression": "discoverable", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_sessions_created_by_user_id_users_id_fk": { + "name": "board_sessions_created_by_user_id_users_id_fk", + "tableFrom": "board_sessions", + "tableTo": "users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_favorites": { + "name": "user_favorites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_name": { + "name": "board_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_user_favorite": { + "name": "unique_user_favorite", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "angle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_favorites_user_idx": { + "name": "user_favorites_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_favorites_climb_idx": { + "name": "user_favorites_climb_idx", + "columns": [ + { + "expression": "board_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "angle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_favorites_user_id_users_id_fk": { + "name": "user_favorites_user_id_users_id_fk", + "tableFrom": "user_favorites", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.boardsesh_ticks": { + "name": "boardsesh_ticks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_mirror": { + "name": "is_mirror", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "status": { + "name": "status", + "type": "tick_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "quality": { + "name": "quality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_benchmark": { + "name": "is_benchmark", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "climbed_at": { + "name": "climbed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aurora_type": { + "name": "aurora_type", + "type": "aurora_table_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "aurora_id": { + "name": "aurora_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aurora_synced_at": { + "name": "aurora_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aurora_sync_error": { + "name": "aurora_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "boardsesh_ticks_user_board_idx": { + "name": "boardsesh_ticks_user_board_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "boardsesh_ticks_climb_idx": { + "name": "boardsesh_ticks_climb_idx", + "columns": [ + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "boardsesh_ticks_aurora_id_unique": { + "name": "boardsesh_ticks_aurora_id_unique", + "columns": [ + { + "expression": "aurora_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "boardsesh_ticks_sync_pending_idx": { + "name": "boardsesh_ticks_sync_pending_idx", + "columns": [ + { + "expression": "aurora_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "boardsesh_ticks_session_idx": { + "name": "boardsesh_ticks_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "boardsesh_ticks_climbed_at_idx": { + "name": "boardsesh_ticks_climbed_at_idx", + "columns": [ + { + "expression": "climbed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "boardsesh_ticks_user_id_users_id_fk": { + "name": "boardsesh_ticks_user_id_users_id_fk", + "tableFrom": "boardsesh_ticks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardsesh_ticks_session_id_board_sessions_id_fk": { + "name": "boardsesh_ticks_session_id_board_sessions_id_fk", + "tableFrom": "boardsesh_ticks", + "tableTo": "board_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "boardsesh_ticks_uuid_unique": { + "name": "boardsesh_ticks_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlist_climbs": { + "name": "playlist_climbs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "playlist_id": { + "name": "playlist_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "added_at": { + "name": "added_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_playlist_climb": { + "name": "unique_playlist_climb", + "columns": [ + { + "expression": "playlist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlist_climbs_climb_idx": { + "name": "playlist_climbs_climb_idx", + "columns": [ + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlist_climbs_position_idx": { + "name": "playlist_climbs_position_idx", + "columns": [ + { + "expression": "playlist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "playlist_climbs_playlist_id_playlists_id_fk": { + "name": "playlist_climbs_playlist_id_playlists_id_fk", + "tableFrom": "playlist_climbs", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlist_ownership": { + "name": "playlist_ownership", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "playlist_id": { + "name": "playlist_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'owner'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_playlist_ownership": { + "name": "unique_playlist_ownership", + "columns": [ + { + "expression": "playlist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlist_ownership_user_idx": { + "name": "playlist_ownership_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "playlist_ownership_playlist_id_playlists_id_fk": { + "name": "playlist_ownership_playlist_id_playlists_id_fk", + "tableFrom": "playlist_ownership", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playlist_ownership_user_id_users_id_fk": { + "name": "playlist_ownership_user_id_users_id_fk", + "tableFrom": "playlist_ownership", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlists": { + "name": "playlists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aurora_type": { + "name": "aurora_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aurora_id": { + "name": "aurora_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aurora_synced_at": { + "name": "aurora_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "playlists_board_layout_idx": { + "name": "playlists_board_layout_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlists_uuid_idx": { + "name": "playlists_uuid_idx", + "columns": [ + { + "expression": "uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlists_updated_at_idx": { + "name": "playlists_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlists_aurora_id_idx": { + "name": "playlists_aurora_id_idx", + "columns": [ + { + "expression": "aurora_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "playlists_uuid_unique": { + "name": "playlists_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.aurora_table_type": { + "name": "aurora_table_type", + "schema": "public", + "values": [ + "ascents", + "bids" + ] + }, + "public.tick_status": { + "name": "tick_status", + "schema": "public", + "values": [ + "flash", + "send", + "attempt" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 31396fb1..285d619b 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -176,6 +176,20 @@ "when": 1767319335153, "tag": "0024_old_zombie", "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1767498576546, + "tag": "0025_shocking_clint_barton", + "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1767580800000, + "tag": "0030_drop_legacy_ascents_bids", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/view/[climb_uuid]/page.tsx b/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/view/[climb_uuid]/page.tsx index d9bf4948..b703097f 100644 --- a/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/view/[climb_uuid]/page.tsx +++ b/packages/web/app/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/view/[climb_uuid]/page.tsx @@ -19,9 +19,9 @@ import { convertLitUpHoldsStringToMap } from '@/app/components/board-renderer/ut import ClimbViewActions from '@/app/components/climb-view/climb-view-actions'; import { Metadata } from 'next'; import { dbz } from '@/app/lib/db/db'; -import { kilterBetaLinks, tensionBetaLinks } from '@/app/lib/db/schema'; -import { eq } from 'drizzle-orm'; +import { eq, and } from 'drizzle-orm'; import { BetaLink } from '@/app/lib/api-wrappers/sync-api-types'; +import { UNIFIED_TABLES } from '@/app/lib/db/queries/util/table-select'; import styles from './climb-view.module.css'; export async function generateMetadata(props: { params: Promise }): Promise { @@ -134,24 +134,17 @@ export default async function DynamicResultsPage(props: { params: Promise => { try { - let betaLinks; - - if (parsedParams.board_name === 'kilter') { - betaLinks = await dbz - .select() - .from(kilterBetaLinks) - .where(eq(kilterBetaLinks.climbUuid, parsedParams.climb_uuid)); - } else if (parsedParams.board_name === 'tension') { - betaLinks = await dbz - .select() - .from(tensionBetaLinks) - .where(eq(tensionBetaLinks.climbUuid, parsedParams.climb_uuid)); - } else { - return []; - } + const { betaLinks } = UNIFIED_TABLES; + + const results = await dbz + .select() + .from(betaLinks) + .where( + and(eq(betaLinks.boardType, parsedParams.board_name), eq(betaLinks.climbUuid, parsedParams.climb_uuid)), + ); // Transform the database results to match the BetaLink interface - return betaLinks.map((link) => ({ + return results.map((link) => ({ climb_uuid: link.climbUuid, link: link.link, foreign_username: link.foreignUsername, diff --git a/packages/web/app/api/internal/aurora-credentials/route.ts b/packages/web/app/api/internal/aurora-credentials/route.ts index bb11b05f..49505320 100644 --- a/packages/web/app/api/internal/aurora-credentials/route.ts +++ b/packages/web/app/api/internal/aurora-credentials/route.ts @@ -9,7 +9,6 @@ import { encrypt, decrypt } from "@boardsesh/crypto"; import AuroraClimbingClient from "@/app/lib/api-wrappers/aurora-rest-client/aurora-rest-client"; import { BoardName as AuroraBoardName } from "@/app/lib/api-wrappers/aurora-rest-client/types"; import { syncUserData } from "@/app/lib/data-sync/aurora/user-sync"; -import { migrateUserAuroraHistory } from "@/app/lib/data-sync/aurora/migrate-user-history"; const saveCredentialsSchema = z.object({ boardType: z.enum(["kilter", "tension"]), @@ -211,19 +210,12 @@ export async function POST(request: NextRequest) { let finalSyncError: string | null = null; try { - // First sync ongoing data + // Sync user data from Aurora to boardsesh_ticks await syncUserData(boardType as AuroraBoardName, loginResponse.token, loginResponse.user_id); - - // Then migrate historical data - await migrateUserAuroraHistory( - session.user.id, // NextAuth user ID - boardType as AuroraBoardName, - loginResponse.user_id // Aurora user ID - ); } catch (syncError) { - console.error("Sync/migration error (non-blocking):", syncError); + console.error("Sync error (non-blocking):", syncError); finalSyncStatus = "error"; - finalSyncError = syncError instanceof Error ? syncError.message : "Sync/migration failed"; + finalSyncError = syncError instanceof Error ? syncError.message : "Sync failed"; // Update sync status to reflect error await db diff --git a/packages/web/app/api/internal/migrate-users-cron/route.ts b/packages/web/app/api/internal/migrate-users-cron/route.ts deleted file mode 100644 index 93f8ef33..00000000 --- a/packages/web/app/api/internal/migrate-users-cron/route.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { NextResponse } from 'next/server'; -import { migrateUserAuroraHistory } from '@/app/lib/data-sync/aurora/migrate-user-history'; -import { getPool } from '@/app/lib/db/db'; -import { drizzle } from 'drizzle-orm/neon-serverless'; -import { eq, sql } from 'drizzle-orm'; -import { BoardName as AuroraBoardName } from '@/app/lib/api-wrappers/aurora-rest-client/types'; -import * as schema from '@/app/lib/db/schema'; - -export const dynamic = 'force-dynamic'; -export const maxDuration = 300; // 5 minutes max - -const CRON_SECRET = process.env.CRON_SECRET; - -interface MigrationResult { - userId: string; - boardType: string; - migrated?: number; - error?: string; -} - -export async function GET(request: Request) { - try { - // Auth check: skip in development only, require secret in all other environments - const authHeader = request.headers.get('authorization'); - const isDevelopment = process.env.VERCEL_ENV === 'development' || process.env.NODE_ENV === 'development'; - - if (!isDevelopment && authHeader !== `Bearer ${CRON_SECRET}`) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const pool = getPool(); - - // Find users with aurora_credentials but no migrated ticks - let unmigratedUsers; - { - const client = await pool.connect(); - try { - const result = await client.query(` - SELECT DISTINCT - ac.user_id, - ac.board_type, - ac.aurora_user_id - FROM aurora_credentials ac - WHERE ac.sync_status = 'active' - AND NOT EXISTS ( - SELECT 1 FROM boardsesh_ticks bt - WHERE bt.user_id = ac.user_id - AND bt.board_type = ac.board_type - AND bt.aurora_id IS NOT NULL - ) - LIMIT 10 - `); - unmigratedUsers = result.rows; - } finally { - client.release(); - } - } - - console.log(`[Migrate Users Cron] Found ${unmigratedUsers.length} users with unmigrated Aurora data`); - - const results = { - total: unmigratedUsers.length, - successful: 0, - failed: 0, - totalMigrated: 0, - errors: [] as MigrationResult[], - }; - - // Migrate each user sequentially - for (const user of unmigratedUsers) { - try { - console.log(`[Migrate Users Cron] Migrating user ${user.user_id} (${user.board_type})`); - - const result = await migrateUserAuroraHistory( - user.user_id, - user.board_type as AuroraBoardName, - user.aurora_user_id - ); - - results.successful++; - results.totalMigrated += result.migrated; - - console.log(`[Migrate Users Cron] Successfully migrated ${result.migrated} ticks for user ${user.user_id}`); - } catch (error) { - results.failed++; - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - console.error(`[Migrate Users Cron] Failed to migrate user ${user.user_id}:`, errorMessage); - - results.errors.push({ - userId: user.user_id, - boardType: user.board_type, - error: errorMessage, - }); - - // Update sync status in aurora_credentials to reflect error - const client = await pool.connect(); - try { - const db = drizzle(client); - await db - .update(schema.auroraCredentials) - .set({ - syncStatus: 'error', - syncError: `Migration failed: ${errorMessage}`, - updatedAt: new Date(), - }) - .where( - sql`${schema.auroraCredentials.userId} = ${user.user_id} AND ${schema.auroraCredentials.boardType} = ${user.board_type}` - ); - } catch (updateError) { - console.error(`[Migrate Users Cron] Failed to update error status:`, updateError); - } finally { - client.release(); - } - } - } - - console.log( - `[Migrate Users Cron] Completed: ${results.successful}/${results.total} users, ${results.totalMigrated} ticks migrated` - ); - - return NextResponse.json(results); - } catch (error) { - console.error('[Migrate Users Cron] Fatal error:', error); - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error' }, - { status: 500 } - ); - } -} diff --git a/packages/web/app/api/v1/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/heatmap/route.ts b/packages/web/app/api/v1/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/heatmap/route.ts index 1238c07c..d6d72e78 100644 --- a/packages/web/app/api/v1/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/heatmap/route.ts +++ b/packages/web/app/api/v1/[board_name]/[layout_id]/[size_id]/[set_ids]/[angle]/heatmap/route.ts @@ -1,12 +1,12 @@ import { getHoldHeatmapData, HoldHeatmapData } from '@/app/lib/db/queries/climbs/holds-heatmap'; -import { getSession } from '@/app/lib/session'; import { BoardRouteParameters, ErrorResponse, ParsedBoardRouteParameters, SearchRequestPagination } from '@/app/lib/types'; import { urlParamsToSearchParams } from '@/app/lib/url-utils'; import { parseBoardRouteParamsWithSlugs } from '@/app/lib/url-utils.server'; import { sortObjectKeys } from '@/app/lib/cache-utils'; -import { cookies } from 'next/headers'; import { unstable_cache } from 'next/cache'; import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/lib/auth/auth-options'; /** * Cache duration for heatmap queries (in seconds) @@ -95,32 +95,9 @@ export async function GET( const searchParams: SearchRequestPagination = urlParamsToSearchParams(query); - // Extract user authentication - try headers first, then fall back to session - let userId: number | undefined; - - // Check for header-based authentication first (for consistency with search API) - const personalProgressFiltersEnabled = - searchParams.hideAttempted || - searchParams.hideCompleted || - searchParams.showOnlyAttempted || - searchParams.showOnlyCompleted; - - if (personalProgressFiltersEnabled) { - const userIdHeader = req.headers.get('x-user-id'); - const tokenHeader = req.headers.get('x-auth-token'); - - // Only use userId if both user ID and token are provided (basic auth check) - if (userIdHeader && tokenHeader && userIdHeader !== 'null') { - userId = parseInt(userIdHeader, 10); - } - } - - // Fall back to session-based authentication if no header auth - if (!userId) { - const cookieStore = await cookies(); - const session = await getSession(cookieStore, parsedParams.board_name); - userId = session.userId; - } + // Get NextAuth session for user-specific data + const session = await getServerSession(authOptions); + const userId = session?.user?.id; // Get the heatmap data - use cached version for anonymous requests only // User-specific data is not cached to ensure fresh personal progress data diff --git a/packages/web/app/api/v1/[board_name]/beta/[climb_uuid]/route.ts b/packages/web/app/api/v1/[board_name]/beta/[climb_uuid]/route.ts index f9e10675..bdb99c10 100644 --- a/packages/web/app/api/v1/[board_name]/beta/[climb_uuid]/route.ts +++ b/packages/web/app/api/v1/[board_name]/beta/[climb_uuid]/route.ts @@ -1,9 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; import { dbz } from '@/app/lib/db/db'; -import { kilterBetaLinks, tensionBetaLinks } from '@/app/lib/db/schema'; -import { eq } from 'drizzle-orm'; +import { eq, and } from 'drizzle-orm'; import { BoardName } from '@/app/lib/types'; import { extractUuidFromSlug } from '@/app/lib/url-utils'; +import { UNIFIED_TABLES, isValidUnifiedBoardName } from '@/app/lib/db/queries/util/table-select'; export async function GET( request: NextRequest, @@ -13,19 +13,20 @@ export async function GET( const board_name = boardNameParam as BoardName; const climb_uuid = extractUuidFromSlug(rawClimbUuid); + if (!isValidUnifiedBoardName(board_name)) { + return NextResponse.json({ error: 'Invalid board name' }, { status: 400 }); + } + try { - let betaLinks; + const { betaLinks } = UNIFIED_TABLES; - if (board_name === 'kilter') { - betaLinks = await dbz.select().from(kilterBetaLinks).where(eq(kilterBetaLinks.climbUuid, climb_uuid)); - } else if (board_name === 'tension') { - betaLinks = await dbz.select().from(tensionBetaLinks).where(eq(tensionBetaLinks.climbUuid, climb_uuid)); - } else { - return NextResponse.json({ error: 'Invalid board name' }, { status: 400 }); - } + const results = await dbz + .select() + .from(betaLinks) + .where(and(eq(betaLinks.boardType, board_name), eq(betaLinks.climbUuid, climb_uuid))); // Transform the database results to match the BetaLink interface - const transformedLinks = betaLinks.map((link) => ({ + const transformedLinks = results.map((link) => ({ climb_uuid: link.climbUuid, link: link.link, foreign_username: link.foreignUsername, diff --git a/packages/web/app/api/v1/[board_name]/proxy/saveAscent/route.ts b/packages/web/app/api/v1/[board_name]/proxy/saveAscent/route.ts index 93ed0f5a..69526986 100644 --- a/packages/web/app/api/v1/[board_name]/proxy/saveAscent/route.ts +++ b/packages/web/app/api/v1/[board_name]/proxy/saveAscent/route.ts @@ -4,13 +4,15 @@ import { AuroraBoardName } from '@/app/lib/api-wrappers/aurora/types'; import { BoardOnlyRouteParameters } from '@/app/lib/types'; import { NextResponse } from 'next/server'; import { z } from 'zod'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/lib/auth/auth-options'; const saveAscentSchema = z.object({ token: z.string().min(1), options: z .object({ uuid: z.string(), - user_id: z.number(), + user_id: z.number(), // Legacy Aurora user_id (not used for storage anymore) climb_uuid: z.string(), angle: z.number(), is_mirror: z.boolean(), @@ -35,13 +37,18 @@ export async function POST(request: Request, props: { params: Promise { - // Convert the ISO date to the required format "YYYY-MM-DD HH:mm:ss" + // Convert the ISO date to the required format const formattedDate = dayjs(options.climbed_at).format('YYYY-MM-DD HH:mm:ss'); - const createdAt = dayjs().format('YYYY-MM-DD HH:mm:ss'); + const now = new Date().toISOString(); - // Match Kilter Board v3.6.4 payload structure exactly - const requestData = { - user_id: options.user_id, - uuid: options.uuid, - climb_uuid: options.climb_uuid, - angle: options.angle, - difficulty: options.difficulty, - is_mirror: options.is_mirror ? 1 : 0, - attempt_id: options.attempt_id || options.bid_count, - bid_count: options.bid_count, - quality: options.quality, - is_benchmark: options.is_benchmark ? 1 : 0, - comment: options.comment, - climbed_at: formattedDate, - }; + // Determine status based on attempt_id (1 = flash, otherwise send) + const status = options.attempt_id === 1 ? 'flash' : 'send'; - // Save to local database only - const fullTableName = getTableName(board, 'ascents'); - const finalCreatedAt = createdAt; + // Generate a new UUID for the tick (different from the ascent uuid which is Aurora's) + const tickUuid = randomUUID(); - await sql` - INSERT INTO ${sql.unsafe(fullTableName)} ( - uuid, climb_uuid, angle, is_mirror, user_id, attempt_id, - bid_count, quality, difficulty, is_benchmark, comment, - climbed_at, created_at, synced, sync_error - ) - VALUES ( - ${requestData.uuid}, ${requestData.climb_uuid}, ${requestData.angle}, - ${requestData.is_mirror}, ${requestData.user_id}, ${requestData.attempt_id}, - ${requestData.bid_count}, ${requestData.quality}, ${requestData.difficulty}, - ${requestData.is_benchmark}, ${requestData.comment || ''}, - ${requestData.climbed_at}, ${finalCreatedAt}, ${false}, ${null} - ) - ON CONFLICT (uuid) DO UPDATE SET - climb_uuid = EXCLUDED.climb_uuid, - angle = EXCLUDED.angle, - is_mirror = EXCLUDED.is_mirror, - attempt_id = EXCLUDED.attempt_id, - bid_count = EXCLUDED.bid_count, - quality = EXCLUDED.quality, - difficulty = EXCLUDED.difficulty, - is_benchmark = EXCLUDED.is_benchmark, - comment = EXCLUDED.comment, - climbed_at = EXCLUDED.climbed_at, - synced = EXCLUDED.synced, - sync_error = EXCLUDED.sync_error - `; + await dbz + .insert(boardseshTicks) + .values({ + uuid: tickUuid, + userId: nextAuthUserId, + boardType: board, + climbUuid: options.climb_uuid, + angle: options.angle, + isMirror: options.is_mirror, + status: status, + attemptCount: options.bid_count, + quality: options.quality, + difficulty: options.difficulty, + isBenchmark: options.is_benchmark, + comment: options.comment || '', + climbedAt: formattedDate, + createdAt: now, + updatedAt: now, + auroraType: 'ascents', + auroraId: options.uuid, // Store Aurora's UUID for sync reference + }) + .onConflictDoUpdate({ + target: boardseshTicks.auroraId, + set: { + climbUuid: options.climb_uuid, + angle: options.angle, + isMirror: options.is_mirror, + status: status, + attemptCount: options.bid_count, + quality: options.quality, + difficulty: options.difficulty, + isBenchmark: options.is_benchmark, + comment: options.comment || '', + climbedAt: formattedDate, + updatedAt: now, + }, + }); - // Create a local ascent object for the response + // Create a local ascent object for the response (for API compatibility) const localAscent: Ascent = { uuid: options.uuid, - user_id: options.user_id, + user_id: options.user_id, // Keep for API response compatibility climb_uuid: options.climb_uuid, angle: options.angle, is_mirror: options.is_mirror, - attempt_id: requestData.attempt_id, + attempt_id: options.attempt_id || options.bid_count, bid_count: options.bid_count, quality: options.quality, difficulty: options.difficulty, @@ -86,11 +86,11 @@ export async function saveAscent( wall_uuid: null, comment: options.comment, climbed_at: formattedDate, - created_at: finalCreatedAt, - updated_at: finalCreatedAt, + created_at: now, + updated_at: now, }; - // Return response in the expected format - always success from client perspective + // Return response in the expected format return { events: [ { diff --git a/packages/web/app/lib/api-wrappers/aurora/saveClimb.ts b/packages/web/app/lib/api-wrappers/aurora/saveClimb.ts index b04187de..4ea022c3 100644 --- a/packages/web/app/lib/api-wrappers/aurora/saveClimb.ts +++ b/packages/web/app/lib/api-wrappers/aurora/saveClimb.ts @@ -1,8 +1,8 @@ import { BoardName } from '../../types'; import { SaveClimbOptions } from './types'; import { generateUuid } from './util'; -import { sql } from '@/app/lib/db/db'; -import { getTableName } from '../../data-sync/aurora/getTableName'; +import { dbz } from '@/app/lib/db/db'; +import { UNIFIED_TABLES } from '@/app/lib/db/queries/util/table-select'; import dayjs from 'dayjs'; /** @@ -27,35 +27,43 @@ export async function saveClimb( const uuid = generateUuid(); const createdAt = dayjs().format('YYYY-MM-DD HH:mm:ss'); - // Save to local database only - const fullTableName = getTableName(board, 'climbs'); - const finalCreatedAt = createdAt; + const { climbs } = UNIFIED_TABLES; - await sql` - INSERT INTO ${sql.unsafe(fullTableName)} ( - uuid, layout_id, setter_id, name, description, angle, - frames_count, frames_pace, frames, is_draft, is_listed, - created_at, synced, sync_error - ) - VALUES ( - ${uuid}, ${options.layout_id}, ${options.setter_id}, ${options.name}, - ${options.description || ''}, ${options.angle}, ${options.frames_count || 1}, - ${options.frames_pace || 0}, ${options.frames}, ${options.is_draft}, - ${false}, ${finalCreatedAt}, ${false}, ${null} - ) - ON CONFLICT (uuid) DO UPDATE SET - layout_id = EXCLUDED.layout_id, - setter_id = EXCLUDED.setter_id, - name = EXCLUDED.name, - description = EXCLUDED.description, - angle = EXCLUDED.angle, - frames_count = EXCLUDED.frames_count, - frames_pace = EXCLUDED.frames_pace, - frames = EXCLUDED.frames, - is_draft = EXCLUDED.is_draft, - synced = EXCLUDED.synced, - sync_error = EXCLUDED.sync_error - `; + await dbz + .insert(climbs) + .values({ + boardType: board, + uuid, + layoutId: options.layout_id, + setterId: options.setter_id, + name: options.name, + description: options.description || '', + angle: options.angle, + framesCount: options.frames_count || 1, + framesPace: options.frames_pace || 0, + frames: options.frames, + isDraft: options.is_draft, + isListed: false, + createdAt, + synced: false, + syncError: null, + }) + .onConflictDoUpdate({ + target: climbs.uuid, + set: { + layoutId: options.layout_id, + setterId: options.setter_id, + name: options.name, + description: options.description || '', + angle: options.angle, + framesCount: options.frames_count || 1, + framesPace: options.frames_pace || 0, + frames: options.frames, + isDraft: options.is_draft, + synced: false, + syncError: null, + }, + }); // Return response - always success from client perspective return { diff --git a/packages/web/app/lib/data-sync/aurora/migrate-user-history.ts b/packages/web/app/lib/data-sync/aurora/migrate-user-history.ts deleted file mode 100644 index 8e820bb6..00000000 --- a/packages/web/app/lib/data-sync/aurora/migrate-user-history.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { getPool } from '@/app/lib/db/db'; -import { BoardName as AuroraBoardName } from '../../api-wrappers/aurora-rest-client/types'; -import { drizzle } from 'drizzle-orm/neon-serverless'; -import { getTable } from '../../db/queries/util/table-select'; -import { boardseshTicks } from '../../db/schema'; -import { randomUUID } from 'crypto'; -import { eq, and, isNotNull } from 'drizzle-orm'; -import { convertQuality } from './convert-quality'; - -/** - * Migrate a single user's historical Aurora data to boardsesh_ticks - * This function is called when: - * 1. User adds Aurora credentials (user-triggered) - * 2. Background cron job finds unmigrated users (cron-triggered) - * - * @param nextAuthUserId - NextAuth user ID - * @param boardType - Board type ('kilter' or 'tension') - * @param auroraUserId - Aurora user ID - * @returns Object with migrated count - */ -export async function migrateUserAuroraHistory( - nextAuthUserId: string, - boardType: AuroraBoardName, - auroraUserId: number, -): Promise<{ migrated: number }> { - const pool = getPool(); - const client = await pool.connect(); - - try { - await client.query('BEGIN'); - const db = drizzle(client); - - // Use advisory lock to prevent concurrent migrations for same user+board - // Hash the user ID and board type to create a unique lock ID - const lockId = `${nextAuthUserId}-${boardType}`; - const lockHash = lockId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); - const acquired = await client.query('SELECT pg_try_advisory_xact_lock($1)', [lockHash]); - - if (!acquired.rows[0].pg_try_advisory_xact_lock) { - console.log(`Migration already in progress for user ${nextAuthUserId} (${boardType}), skipping`); - await client.query('ROLLBACK'); - return { migrated: 0 }; - } - - let totalMigrated = 0; - - // Check if user already has migrated data - const existingTicks = await db - .select({ count: boardseshTicks.id }) - .from(boardseshTicks) - .where( - and( - eq(boardseshTicks.userId, nextAuthUserId), - eq(boardseshTicks.boardType, boardType), - isNotNull(boardseshTicks.auroraId) - ) - ) - .limit(1); - - if (existingTicks.length > 0) { - console.log(`User ${nextAuthUserId} already has migrated data for ${boardType}, skipping`); - await client.query('COMMIT'); - return { migrated: 0 }; - } - - // Migrate ascents (successful climbs) - const ascentsSchema = getTable('ascents', boardType); - const ascents = await db - .select() - .from(ascentsSchema) - .where(eq(ascentsSchema.userId, auroraUserId)); - - // Prepare batch insert values for ascents - const ascentValues = []; - for (const ascent of ascents) { - // Skip if missing required fields - if (!ascent.climbUuid || !ascent.climbedAt) { - console.warn(`Skipping ascent ${ascent.uuid} - missing required fields`); - continue; - } - - const status = Number(ascent.attemptId) === 1 ? ('flash' as const) : ('send' as const); - const convertedQuality = convertQuality(ascent.quality); - - ascentValues.push({ - uuid: randomUUID(), - userId: nextAuthUserId, - boardType: boardType, - climbUuid: ascent.climbUuid, - angle: Number(ascent.angle), - isMirror: Boolean(ascent.isMirror), - status: status, - attemptCount: Number(ascent.bidCount || 1), - quality: convertedQuality, - difficulty: ascent.difficulty ? Number(ascent.difficulty) : null, - isBenchmark: Boolean(ascent.isBenchmark || 0), - comment: ascent.comment || '', - climbedAt: new Date(ascent.climbedAt).toISOString(), - createdAt: ascent.createdAt ? new Date(ascent.createdAt).toISOString() : new Date().toISOString(), - updatedAt: new Date().toISOString(), - auroraType: 'ascents' as const, - auroraId: ascent.uuid, - auroraSyncedAt: new Date().toISOString(), - }); - } - - // Batch insert ascents (if any) - if (ascentValues.length > 0) { - await db.insert(boardseshTicks).values(ascentValues); - totalMigrated += ascentValues.length; - } - - // Migrate bids (failed attempts) - const bidsSchema = getTable('bids', boardType); - const bids = await db - .select() - .from(bidsSchema) - .where(eq(bidsSchema.userId, auroraUserId)); - - // Prepare batch insert values for bids - const bidValues = []; - for (const bid of bids) { - // Skip if missing required fields - if (!bid.climbUuid || !bid.climbedAt) { - console.warn(`Skipping bid ${bid.uuid} - missing required fields`); - continue; - } - - bidValues.push({ - uuid: randomUUID(), - userId: nextAuthUserId, - boardType: boardType, - climbUuid: bid.climbUuid, - angle: Number(bid.angle), - isMirror: Boolean(bid.isMirror), - status: 'attempt' as const, - attemptCount: Number(bid.bidCount || 1), - quality: null, - difficulty: null, - isBenchmark: false, - comment: bid.comment || '', - climbedAt: new Date(bid.climbedAt).toISOString(), - createdAt: bid.createdAt ? new Date(bid.createdAt).toISOString() : new Date().toISOString(), - updatedAt: new Date().toISOString(), - auroraType: 'bids' as const, - auroraId: bid.uuid, - auroraSyncedAt: new Date().toISOString(), - }); - } - - // Batch insert bids (if any) - if (bidValues.length > 0) { - await db.insert(boardseshTicks).values(bidValues); - totalMigrated += bidValues.length; - } - - await client.query('COMMIT'); - console.log(`Migrated ${totalMigrated} historical ticks for user ${nextAuthUserId} on ${boardType}`); - - return { migrated: totalMigrated }; - } catch (error) { - await client.query('ROLLBACK'); - console.error('Failed to migrate user Aurora history:', error); - throw error; - } finally { - client.release(); - } -} diff --git a/packages/web/app/lib/data-sync/aurora/shared-sync.ts b/packages/web/app/lib/data-sync/aurora/shared-sync.ts index dc9ee280..1601c249 100644 --- a/packages/web/app/lib/data-sync/aurora/shared-sync.ts +++ b/packages/web/app/lib/data-sync/aurora/shared-sync.ts @@ -1,11 +1,11 @@ import { getPool } from '@/app/lib/db/db'; import { SyncOptions, AuroraBoardName } from '../../api-wrappers/aurora/types'; import { sharedSync } from '../../api-wrappers/aurora/sharedSync'; -import { sql } from 'drizzle-orm'; +import { sql, eq } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/neon-serverless'; import { NeonDatabase } from 'drizzle-orm/neon-serverless'; import { Attempt, BetaLink, Climb, ClimbStats, SharedSync, SyncPutFields } from '../../api-wrappers/sync-api-types'; -import { getTable } from '../../db/queries/util/table-select'; +import { UNIFIED_TABLES } from '../../db/queries/util/table-select'; import { convertLitUpHoldsStringToMap } from '@/app/components/board-renderer/util'; // Define shared sync tables in correct dependency order @@ -34,16 +34,17 @@ const TABLES_TO_PROCESS = new Set(['climbs', 'climb_stats', 'beta_links', 'attem const upsertAttempts = (db: NeonDatabase>, board: AuroraBoardName, data: Attempt[]) => Promise.all( data.map(async (item) => { - const attemptsSchema = getTable('attempts', board); + const attemptsSchema = UNIFIED_TABLES.attempts; return db .insert(attemptsSchema) .values({ + boardType: board, id: Number(item.id), position: Number(item.position), name: item.name, }) .onConflictDoUpdate({ - target: attemptsSchema.id, + target: [attemptsSchema.boardType, attemptsSchema.id], set: { // Only allow position updates if they're reasonable (0-100) position: sql`CASE WHEN ${Number(item.position)} >= 0 AND ${Number(item.position)} <= 100 THEN ${Number(item.position)} ELSE ${attemptsSchema.position} END`, @@ -55,18 +56,17 @@ const upsertAttempts = (db: NeonDatabase>, board: AuroraBo ); async function upsertClimbStats(db: NeonDatabase>, board: AuroraBoardName, data: ClimbStats[]) { - // Filter data to only include stats for valid climbs + const climbStatsSchema = UNIFIED_TABLES.climbStats; + const climbStatHistorySchema = UNIFIED_TABLES.climbStatsHistory; await Promise.all( data.map((item) => { - // Changed from data.map to validStats.map - const climbStatsSchema = getTable('climbStats', board); - const climbStatHistorySchema = getTable('climbStatsHistory', board); return Promise.all([ // Update current stats db .insert(climbStatsSchema) .values({ + boardType: board, climbUuid: item.climb_uuid, angle: Number(item.angle), displayDifficulty: Number(item.display_difficulty || item.difficulty_average), @@ -78,7 +78,7 @@ async function upsertClimbStats(db: NeonDatabase>, board: faAt: item.fa_at, }) .onConflictDoUpdate({ - target: [climbStatsSchema.climbUuid, climbStatsSchema.angle], + target: [climbStatsSchema.boardType, climbStatsSchema.climbUuid, climbStatsSchema.angle], set: { displayDifficulty: Number(item.display_difficulty || item.difficulty_average), benchmarkDifficulty: Number(item.benchmark_difficulty), @@ -92,6 +92,7 @@ async function upsertClimbStats(db: NeonDatabase>, board: // Also insert into history table db.insert(climbStatHistorySchema).values({ + boardType: board, climbUuid: item.climb_uuid, angle: Number(item.angle), displayDifficulty: Number(item.display_difficulty || item.difficulty_average), @@ -108,12 +109,14 @@ async function upsertClimbStats(db: NeonDatabase>, board: } async function upsertBetaLinks(db: NeonDatabase>, board: AuroraBoardName, data: BetaLink[]) { + const betaLinksSchema = UNIFIED_TABLES.betaLinks; + await Promise.all( data.map((item) => { - const betaLinksSchema = getTable('betaLinks', board); return db .insert(betaLinksSchema) .values({ + boardType: board, climbUuid: item.climb_uuid, link: item.link, foreignUsername: item.foreign_username, @@ -123,7 +126,7 @@ async function upsertBetaLinks(db: NeonDatabase>, board: A createdAt: item.created_at, }) .onConflictDoUpdate({ - target: [betaLinksSchema.climbUuid, betaLinksSchema.link], + target: [betaLinksSchema.boardType, betaLinksSchema.climbUuid, betaLinksSchema.link], set: { foreignUsername: item.foreign_username, angle: item.angle, @@ -137,16 +140,17 @@ async function upsertBetaLinks(db: NeonDatabase>, board: A } async function upsertClimbs(db: NeonDatabase>, board: AuroraBoardName, data: Climb[]) { + const climbsSchema = UNIFIED_TABLES.climbs; + const climbHoldsSchema = UNIFIED_TABLES.climbHolds; + await Promise.all( data.map(async (item: Climb) => { - const climbsSchema = getTable('climbs', board); - const climbHoldsSchema = getTable('climbHolds', board); - // Insert or update the climb await db .insert(climbsSchema) .values({ uuid: item.uuid, + boardType: board, name: item.name, description: item.description, hsm: item.hsm, @@ -194,12 +198,12 @@ async function upsertClimbs(db: NeonDatabase>, board: Auro const holdsByFrame = convertLitUpHoldsStringToMap(item.frames, board); const holdsToInsert = Object.entries(holdsByFrame).flatMap(([frameNumber, holds]) => - Object.entries(holds).map(([holdId, { state, color }]) => ({ + Object.entries(holds).map(([holdId, { state }]) => ({ + boardType: board, climbUuid: item.uuid, frameNumber: Number(frameNumber), holdId: Number(holdId), holdState: state, - color, })), ); @@ -241,17 +245,18 @@ async function updateSharedSyncs( boardName: AuroraBoardName, sharedSyncs: SharedSync[], ) { - const sharedSyncsSchema = getTable('sharedSyncs', boardName); + const sharedSyncsSchema = UNIFIED_TABLES.sharedSyncs; for (const sync of sharedSyncs) { await tx .insert(sharedSyncsSchema) .values({ + boardType: boardName, tableName: sync.table_name, lastSynchronizedAt: sync.last_synchronized_at, }) .onConflictDoUpdate({ - target: sharedSyncsSchema.tableName, + target: [sharedSyncsSchema.boardType, sharedSyncsSchema.tableName], set: { lastSynchronizedAt: sync.last_synchronized_at, }, @@ -260,7 +265,7 @@ async function updateSharedSyncs( } export async function getLastSharedSyncTimes(boardName: AuroraBoardName) { - const sharedSyncsSchema = getTable('sharedSyncs', boardName); + const sharedSyncsSchema = UNIFIED_TABLES.sharedSyncs; const pool = getPool(); const client = await pool.connect(); @@ -271,7 +276,8 @@ export async function getLastSharedSyncTimes(boardName: AuroraBoardName) { table_name: sharedSyncsSchema.tableName, last_synchronized_at: sharedSyncsSchema.lastSynchronizedAt, }) - .from(sharedSyncsSchema); + .from(sharedSyncsSchema) + .where(eq(sharedSyncsSchema.boardType, boardName)); return result; } finally { diff --git a/packages/web/app/lib/data-sync/aurora/user-sync.ts b/packages/web/app/lib/data-sync/aurora/user-sync.ts index 9a4f80bf..44eefef5 100644 --- a/packages/web/app/lib/data-sync/aurora/user-sync.ts +++ b/packages/web/app/lib/data-sync/aurora/user-sync.ts @@ -4,7 +4,7 @@ import { SyncOptions, USER_TABLES, UserSyncData, AuroraBoardName } from '../../a import { eq, and, inArray } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/neon-serverless'; import { NeonDatabase } from 'drizzle-orm/neon-serverless'; -import { getTable } from '../../db/queries/util/table-select'; +import { UNIFIED_TABLES } from '../../db/queries/util/table-select'; import { boardseshTicks, auroraCredentials, playlists, playlistClimbs, playlistOwnership } from '../../db/schema'; import { randomUUID } from 'crypto'; import { convertQuality } from './convert-quality'; @@ -39,17 +39,18 @@ async function upsertTableData( switch (tableName) { case 'users': { - const usersSchema = getTable('users', boardName); + const usersSchema = UNIFIED_TABLES.users; for (const item of data) { await db .insert(usersSchema) .values({ + boardType: boardName, id: Number(item.id), username: item.username, createdAt: item.created_at, }) .onConflictDoUpdate({ - target: usersSchema.id, + target: [usersSchema.boardType, usersSchema.id], set: { username: item.username, }, @@ -59,11 +60,12 @@ async function upsertTableData( } case 'walls': { - const wallsSchema = getTable('walls', boardName); + const wallsSchema = UNIFIED_TABLES.walls; for (const item of data) { await db .insert(wallsSchema) .values({ + boardType: boardName, uuid: item.uuid, userId: Number(auroraUserId), name: item.name, @@ -77,7 +79,7 @@ async function upsertTableData( createdAt: item.created_at, }) .onConflictDoUpdate({ - target: wallsSchema.uuid, + target: [wallsSchema.boardType, wallsSchema.uuid], set: { name: item.name, isAdjustable: Boolean(item.is_adjustable), @@ -93,12 +95,13 @@ async function upsertTableData( } case 'draft_climbs': { - const climbsSchema = getTable('climbs', boardName); + const climbsSchema = UNIFIED_TABLES.climbs; for (const item of data) { await db .insert(climbsSchema) .values({ uuid: item.uuid, + boardType: boardName, layoutId: Number(item.layout_id), setterId: Number(auroraUserId), setterUsername: item.setter_username || '', @@ -143,53 +146,36 @@ async function upsertTableData( } case 'ascents': { - const ascentsSchema = getTable('ascents', boardName); + // Write directly to boardsesh_ticks (requires NextAuth user ID) for (const item of data) { - // Write to Aurora table + const status = Number(item.attempt_id) === 1 ? 'flash' : 'send'; + const convertedQuality = convertQuality(item.quality); + await db - .insert(ascentsSchema) + .insert(boardseshTicks) .values({ - uuid: item.uuid, + uuid: randomUUID(), + userId: nextAuthUserId, + boardType: boardName, climbUuid: item.climb_uuid, angle: Number(item.angle), isMirror: Boolean(item.is_mirror), - userId: Number(auroraUserId), - attemptId: Number(item.attempt_id), - bidCount: Number(item.bid_count || 1), - quality: Number(item.quality), - difficulty: Number(item.difficulty), - isBenchmark: Number(item.is_benchmark || 0), + status: status, + attemptCount: Number(item.bid_count || 1), + quality: convertedQuality, + difficulty: item.difficulty ? Number(item.difficulty) : null, + isBenchmark: Boolean(item.is_benchmark || 0), comment: item.comment || '', - climbedAt: item.climbed_at, - createdAt: item.created_at, + climbedAt: new Date(item.climbed_at).toISOString(), + createdAt: item.created_at ? new Date(item.created_at).toISOString() : new Date().toISOString(), + updatedAt: new Date().toISOString(), + auroraType: 'ascents', + auroraId: item.uuid, + auroraSyncedAt: new Date().toISOString(), }) .onConflictDoUpdate({ - target: ascentsSchema.uuid, + target: boardseshTicks.auroraId, set: { - climbUuid: item.climb_uuid, - angle: Number(item.angle), - isMirror: Boolean(item.is_mirror), - attemptId: Number(item.attempt_id), - bidCount: Number(item.bid_count || 1), - quality: Number(item.quality), - difficulty: Number(item.difficulty), - isBenchmark: Number(item.is_benchmark || 0), - comment: item.comment || '', - climbedAt: item.climbed_at, - }, - }); - - // Dual write to boardsesh_ticks (only if we have NextAuth user ID) - if (nextAuthUserId) { - const status = Number(item.attempt_id) === 1 ? 'flash' : 'send'; - const convertedQuality = convertQuality(item.quality); - - await db - .insert(boardseshTicks) - .values({ - uuid: randomUUID(), - userId: nextAuthUserId, - boardType: boardName, climbUuid: item.climb_uuid, angle: Number(item.angle), isMirror: Boolean(item.is_mirror), @@ -200,107 +186,58 @@ async function upsertTableData( isBenchmark: Boolean(item.is_benchmark || 0), comment: item.comment || '', climbedAt: new Date(item.climbed_at).toISOString(), - createdAt: item.created_at ? new Date(item.created_at).toISOString() : new Date().toISOString(), updatedAt: new Date().toISOString(), - auroraType: 'ascents', - auroraId: item.uuid, auroraSyncedAt: new Date().toISOString(), - }) - .onConflictDoUpdate({ - target: boardseshTicks.auroraId, - set: { - climbUuid: item.climb_uuid, - angle: Number(item.angle), - isMirror: Boolean(item.is_mirror), - status: status, - attemptCount: Number(item.bid_count || 1), - quality: convertedQuality, - difficulty: item.difficulty ? Number(item.difficulty) : null, - isBenchmark: Boolean(item.is_benchmark || 0), - comment: item.comment || '', - climbedAt: new Date(item.climbed_at).toISOString(), - updatedAt: new Date().toISOString(), - auroraSyncedAt: new Date().toISOString(), - }, - }); - } + }, + }); } break; } case 'bids': { - const bidsSchema = getTable('bids', boardName); + // Write directly to boardsesh_ticks (requires NextAuth user ID) for (const item of data) { - // Write to Aurora table await db - .insert(bidsSchema) + .insert(boardseshTicks) .values({ - uuid: item.uuid, - userId: Number(auroraUserId), + uuid: randomUUID(), + userId: nextAuthUserId, + boardType: boardName, climbUuid: item.climb_uuid, angle: Number(item.angle), isMirror: Boolean(item.is_mirror), - bidCount: Number(item.bid_count || 1), + status: 'attempt', + attemptCount: Number(item.bid_count || 1), + quality: null, + difficulty: null, + isBenchmark: false, comment: item.comment || '', - climbedAt: item.climbed_at, - createdAt: item.created_at, + climbedAt: new Date(item.climbed_at).toISOString(), + createdAt: new Date(item.created_at).toISOString(), + updatedAt: new Date().toISOString(), + auroraType: 'bids', + auroraId: item.uuid, + auroraSyncedAt: new Date().toISOString(), }) .onConflictDoUpdate({ - target: bidsSchema.uuid, + target: boardseshTicks.auroraId, set: { climbUuid: item.climb_uuid, angle: Number(item.angle), isMirror: Boolean(item.is_mirror), - bidCount: Number(item.bid_count || 1), - comment: item.comment || '', - climbedAt: item.climbed_at, - }, - }); - - // Dual write to boardsesh_ticks (only if we have NextAuth user ID) - if (nextAuthUserId) { - await db - .insert(boardseshTicks) - .values({ - uuid: randomUUID(), - userId: nextAuthUserId, - boardType: boardName, - climbUuid: item.climb_uuid, - angle: Number(item.angle), - isMirror: Boolean(item.is_mirror), - status: 'attempt', attemptCount: Number(item.bid_count || 1), - quality: null, - difficulty: null, - isBenchmark: false, comment: item.comment || '', climbedAt: new Date(item.climbed_at).toISOString(), - createdAt: new Date(item.created_at).toISOString(), updatedAt: new Date().toISOString(), - auroraType: 'bids', - auroraId: item.uuid, auroraSyncedAt: new Date().toISOString(), - }) - .onConflictDoUpdate({ - target: boardseshTicks.auroraId, - set: { - climbUuid: item.climb_uuid, - angle: Number(item.angle), - isMirror: Boolean(item.is_mirror), - attemptCount: Number(item.bid_count || 1), - comment: item.comment || '', - climbedAt: new Date(item.climbed_at).toISOString(), - updatedAt: new Date().toISOString(), - auroraSyncedAt: new Date().toISOString(), - }, - }); - } + }, + }); } break; } case 'tags': { - const tagsSchema = getTable('tags', boardName); + const tagsSchema = UNIFIED_TABLES.tags; for (const item of data) { // First try to update existing record const result = await db @@ -310,6 +247,7 @@ async function upsertTableData( }) .where( and( + eq(tagsSchema.boardType, boardName), eq(tagsSchema.entityUuid, item.entity_uuid), eq(tagsSchema.userId, Number(auroraUserId)), eq(tagsSchema.name, item.name), @@ -320,6 +258,7 @@ async function upsertTableData( // If no record was updated, insert a new one if (result.length === 0) { await db.insert(tagsSchema).values({ + boardType: boardName, entityUuid: item.entity_uuid, userId: Number(auroraUserId), name: item.name, @@ -331,12 +270,13 @@ async function upsertTableData( } case 'circuits': { - const circuitsSchema = getTable('circuits', boardName); + const circuitsSchema = UNIFIED_TABLES.circuits; for (const item of data) { - // 1. Write to Aurora circuits table (existing logic) + // 1. Write to unified circuits table await db .insert(circuitsSchema) .values({ + boardType: boardName, uuid: item.uuid, name: item.name, description: item.description, @@ -347,7 +287,7 @@ async function upsertTableData( updatedAt: item.updated_at, }) .onConflictDoUpdate({ - target: circuitsSchema.uuid, + target: [circuitsSchema.boardType, circuitsSchema.uuid], set: { name: item.name, description: item.description, @@ -441,18 +381,19 @@ async function updateUserSyncs( boardName: AuroraBoardName, userSyncs: UserSyncData[], ) { - const userSyncsSchema = getTable('userSyncs', boardName); + const userSyncsSchema = UNIFIED_TABLES.userSyncs; for (const sync of userSyncs) { await tx .insert(userSyncsSchema) .values({ + boardType: boardName, userId: Number(sync.user_id), tableName: sync.table_name, lastSynchronizedAt: sync.last_synchronized_at, }) .onConflictDoUpdate({ - target: [userSyncsSchema.userId, userSyncsSchema.tableName], + target: [userSyncsSchema.boardType, userSyncsSchema.userId, userSyncsSchema.tableName], set: { lastSynchronizedAt: sync.last_synchronized_at, }, @@ -461,7 +402,7 @@ async function updateUserSyncs( } export async function getLastSyncTimes(boardName: AuroraBoardName, userId: number, tableNames: string[]) { - const userSyncsSchema = getTable('userSyncs', boardName); + const userSyncsSchema = UNIFIED_TABLES.userSyncs; const pool = getPool(); const client = await pool.connect(); @@ -470,7 +411,13 @@ export async function getLastSyncTimes(boardName: AuroraBoardName, userId: numbe const result = await db .select() .from(userSyncsSchema) - .where(and(eq(userSyncsSchema.userId, Number(userId)), inArray(userSyncsSchema.tableName, tableNames))); + .where( + and( + eq(userSyncsSchema.boardType, boardName), + eq(userSyncsSchema.userId, Number(userId)), + inArray(userSyncsSchema.tableName, tableNames), + ), + ); return result; } finally { @@ -479,13 +426,16 @@ export async function getLastSyncTimes(boardName: AuroraBoardName, userId: numbe } export async function getLastSharedSyncTimes(boardName: AuroraBoardName, tableNames: string[]) { - const sharedSyncsSchema = getTable('sharedSyncs', boardName); + const sharedSyncsSchema = UNIFIED_TABLES.sharedSyncs; const pool = getPool(); const client = await pool.connect(); try { const db = drizzle(client); - const result = await db.select().from(sharedSyncsSchema).where(inArray(sharedSyncsSchema.tableName, tableNames)); + const result = await db + .select() + .from(sharedSyncsSchema) + .where(and(eq(sharedSyncsSchema.boardType, boardName), inArray(sharedSyncsSchema.tableName, tableNames))); return result; } finally { diff --git a/packages/web/app/lib/data/get-logbook.ts b/packages/web/app/lib/data/get-logbook.ts index 4233454e..c895af8a 100644 --- a/packages/web/app/lib/data/get-logbook.ts +++ b/packages/web/app/lib/data/get-logbook.ts @@ -1,106 +1,53 @@ -import { sql } from '@/app/lib/db/db'; +import { dbz } from '@/app/lib/db/db'; import { ClimbUuid } from '../types'; import { LogbookEntry, AuroraBoardName } from '../api-wrappers/aurora/types'; -import { getTableName } from '../data-sync/aurora/getTableName'; - -export async function getLogbook(board: AuroraBoardName, userId: string, climbUuids?: ClimbUuid[]): Promise { - const ascentsTable = getTableName(board, 'ascents'); - const bidsTable = getTableName(board, 'bids'); +import { boardseshTicks } from '@/app/lib/db/schema'; +import { eq, and, inArray, isNotNull, desc } from 'drizzle-orm'; + +/** + * Get logbook entries for a user from boardsesh_ticks. + * @param board - The board type (kilter, tension) + * @param userId - NextAuth user ID (not Aurora user_id) + * @param climbUuids - Optional array of climb UUIDs to filter by + */ +export async function getLogbook( + board: AuroraBoardName, + userId: string, + climbUuids?: ClimbUuid[], +): Promise { + const baseConditions = [eq(boardseshTicks.boardType, board), eq(boardseshTicks.userId, userId)]; if (climbUuids && climbUuids.length > 0) { - // If climbUuids are provided - const combinedLogbook = await sql` - SELECT - uuid, - climb_uuid, - angle, - is_mirror, - user_id, - attempt_id, - bid_count AS tries, - quality, - difficulty, - is_benchmark::boolean, - comment, - climbed_at, - created_at, - TRUE::boolean AS is_ascent - FROM ${sql.unsafe(ascentsTable)} - WHERE user_id = ${userId} - AND climb_uuid = ANY(${climbUuids}) - - UNION ALL - - SELECT - uuid, - climb_uuid, - angle, - is_mirror, - user_id, - NULL AS attempt_id, - bid_count AS tries, - NULL AS quality, - NULL AS difficulty, - FALSE::boolean AS is_benchmark, - comment, - climbed_at, - created_at, - FALSE::boolean AS is_ascent - FROM ${sql.unsafe(bidsTable)} - WHERE user_id = ${userId} - AND climb_uuid = ANY(${climbUuids}) - - ORDER BY climbed_at DESC - `; - - return combinedLogbook as unknown as LogbookEntry[]; + baseConditions.push(inArray(boardseshTicks.climbUuid, climbUuids)); } else { - // If climbUuids are not provided - const combinedLogbook = await sql` - SELECT * FROM ( - SELECT - uuid, - climb_uuid, - angle, - is_mirror, - user_id, - attempt_id, - bid_count AS tries, - quality, - difficulty, - is_benchmark::boolean, - comment, - climbed_at, - created_at, - TRUE AS is_ascent - FROM ${sql.unsafe(ascentsTable)} - WHERE user_id = ${userId} - - UNION ALL - - SELECT - uuid, - climb_uuid, - angle, - is_mirror, - user_id, - NULL AS attempt_id, - bid_count AS tries, - NULL AS quality, - NULL AS difficulty, - FALSE AS is_benchmark, - comment, - climbed_at, - created_at, - FALSE AS is_ascent - FROM ${sql.unsafe(bidsTable)} - WHERE user_id = ${userId} - - ORDER BY climbed_at DESC - ) subquery - WHERE difficulty IS NOT NULL; - `; - - return combinedLogbook as unknown as LogbookEntry[]; + // When no specific climbs requested, only return entries with difficulty + baseConditions.push(isNotNull(boardseshTicks.difficulty)); } + + const results = await dbz + .select() + .from(boardseshTicks) + .where(and(...baseConditions)) + .orderBy(desc(boardseshTicks.climbedAt)); + + // Transform boardsesh_ticks to LogbookEntry format + return results.map((tick) => ({ + uuid: tick.uuid, + wall_uuid: null, + climb_uuid: tick.climbUuid, + angle: tick.angle, + is_mirror: tick.isMirror ?? false, + user_id: 0, // Placeholder - we use NextAuth userId now, not Aurora user_id + attempt_id: tick.status === 'flash' ? 1 : tick.status === 'send' ? 2 : 0, + tries: tick.attemptCount, + quality: tick.quality ?? 0, + difficulty: tick.difficulty ?? 0, + is_benchmark: tick.isBenchmark ?? false, + is_listed: true, + comment: tick.comment ?? '', + climbed_at: tick.climbedAt, + created_at: tick.createdAt, + updated_at: tick.updatedAt, + is_ascent: tick.status === 'flash' || tick.status === 'send', + })); } diff --git a/packages/web/app/lib/data/queries.ts b/packages/web/app/lib/data/queries.ts index d2aefd16..312869aa 100644 --- a/packages/web/app/lib/data/queries.ts +++ b/packages/web/app/lib/data/queries.ts @@ -12,19 +12,16 @@ import { getSizesForLayoutId, getAllLayouts, getSetsForLayoutAndSize, + getSizeEdges, } from '@/app/lib/__generated__/product-sizes-data'; -const getTableName = (board_name: string, table_name: string) => { - switch (board_name) { - case 'tension': - case 'kilter': - return `${board_name}_${table_name}`; - default: - return `${table_name}`; +export const getClimb = async (params: ParsedBoardRouteParametersWithUuid): Promise => { + // Get hardcoded size edges (eliminates database query) + const sizeEdges = getSizeEdges(params.board_name, params.size_id); + if (!sizeEdges) { + throw new Error(`Invalid size_id ${params.size_id} for board ${params.board_name}`); } -}; -export const getClimb = async (params: ParsedBoardRouteParametersWithUuid): Promise => { const result = await sql` SELECT climbs.uuid, climbs.setter_username, climbs.name, climbs.description, climbs.frames, COALESCE(climb_stats.angle, ${params.angle}) as angle, COALESCE(climb_stats.ascensionist_count, 0) as ascensionist_count, @@ -32,14 +29,16 @@ export const getClimb = async (params: ParsedBoardRouteParametersWithUuid): Prom ROUND(climb_stats.quality_average::numeric, 2) as quality_average, ROUND(climb_stats.difficulty_average::numeric - climb_stats.display_difficulty::numeric, 2) AS difficulty_error, climb_stats.benchmark_difficulty - FROM ${sql.unsafe(getTableName(params.board_name, 'climbs'))} climbs - LEFT JOIN ${sql.unsafe(getTableName(params.board_name, 'climb_stats'))} climb_stats ON climb_stats.climb_uuid = climbs.uuid AND climb_stats.angle = ${params.angle} - LEFT JOIN ${sql.unsafe( - getTableName(params.board_name, 'difficulty_grades'), - )} dg on dg.difficulty = ROUND(climb_stats.display_difficulty::numeric) - INNER JOIN ${sql.unsafe(getTableName(params.board_name, 'product_sizes'))} product_sizes ON product_sizes.id = ${params.size_id} - WHERE climbs.layout_id = ${params.layout_id} - AND product_sizes.id = ${params.size_id} + FROM board_climbs climbs + LEFT JOIN board_climb_stats climb_stats + ON climb_stats.climb_uuid = climbs.uuid + AND climb_stats.angle = ${params.angle} + AND climb_stats.board_type = ${params.board_name} + LEFT JOIN board_difficulty_grades dg + ON dg.difficulty = ROUND(climb_stats.display_difficulty::numeric) + AND dg.board_type = ${params.board_name} + WHERE climbs.board_type = ${params.board_name} + AND climbs.layout_id = ${params.layout_id} AND climbs.uuid = ${params.climb_uuid} AND climbs.frames_count = 1 limit 1 @@ -62,7 +61,7 @@ export const getClimbStatsForAllAngles = async ( params: ParsedBoardRouteParametersWithUuid ): Promise => { const result = await sql` - SELECT + SELECT climb_stats.angle, COALESCE(climb_stats.ascensionist_count, 0) as ascensionist_count, ROUND(climb_stats.quality_average::numeric, 2) as quality_average, @@ -71,11 +70,12 @@ export const getClimbStatsForAllAngles = async ( climb_stats.fa_username, climb_stats.fa_at, dg.boulder_name as difficulty - FROM ${sql.unsafe(getTableName(params.board_name, 'climb_stats'))} climb_stats - LEFT JOIN ${sql.unsafe( - getTableName(params.board_name, 'difficulty_grades'), - )} dg on dg.difficulty = ROUND(climb_stats.display_difficulty::numeric) - WHERE climb_stats.climb_uuid = ${params.climb_uuid} + FROM board_climb_stats climb_stats + LEFT JOIN board_difficulty_grades dg + ON dg.difficulty = ROUND(climb_stats.display_difficulty::numeric) + AND dg.board_type = ${params.board_name} + WHERE climb_stats.board_type = ${params.board_name} + AND climb_stats.climb_uuid = ${params.climb_uuid} ORDER BY climb_stats.angle ASC `; return result as ClimbStatsForAngle[]; diff --git a/packages/web/app/lib/db/queries/climbs/create-climb-filters.ts b/packages/web/app/lib/db/queries/climbs/create-climb-filters.ts index 09e769e7..5a8b07f2 100644 --- a/packages/web/app/lib/db/queries/climbs/create-climb-filters.ts +++ b/packages/web/app/lib/db/queries/climbs/create-climb-filters.ts @@ -1,25 +1,28 @@ import { eq, gte, sql, like, notLike, inArray, SQL } from 'drizzle-orm'; import { ParsedBoardRouteParameters, SearchRequestPagination } from '@/app/lib/types'; -import { TableSet } from '@/lib/db/queries/util/table-select'; -import { getTableName } from '@/app/lib/data-sync/aurora/getTableName'; +import { UNIFIED_TABLES } from '@/lib/db/queries/util/table-select'; import { SizeEdges } from '@/app/lib/__generated__/product-sizes-data'; import { SUPPORTED_BOARDS } from '@/app/lib/board-data'; +import { boardseshTicks } from '@/app/lib/db/schema'; + +// Type for unified tables used by filters +type UnifiedTables = typeof UNIFIED_TABLES; /** * Creates a shared filtering object that can be used by both search climbs and heatmap queries - * @param tables The board-specific tables from getBoardTables - * @param params The route parameters + * Uses unified tables (board_climbs, board_climb_stats, etc.) with board_type filtering + * @param params The route parameters (includes board_name for filtering) * @param searchParams The search parameters * @param sizeEdges Pre-fetched edge values from product_sizes table - * @param userId Optional user ID to include user-specific ascent and attempt data + * @param userId Optional NextAuth user ID to include user-specific ascent and attempt data */ export const createClimbFilters = ( - tables: TableSet, params: ParsedBoardRouteParameters, searchParams: SearchRequestPagination, sizeEdges: SizeEdges, - userId?: number, + userId?: string, ) => { + const tables = UNIFIED_TABLES; // Defense in depth: validate board_name before using in SQL queries if (!SUPPORTED_BOARDS.includes(params.board_name)) { throw new Error(`Invalid board name: ${params.board_name}`); @@ -47,8 +50,9 @@ export const createClimbFilters = ( .filter(([, value]) => ['STARTING', 'HAND', 'FOOT', 'FINISH'].includes(value as string)) .map(([key, state]) => ({ holdId: Number(key), state: state as string })); - // Base conditions for filtering climbs that don't reference the product sizes table + // Base conditions for filtering climbs - includes board_type filter for unified tables const baseConditions: SQL[] = [ + eq(tables.climbs.boardType, params.board_name), eq(tables.climbs.layoutId, params.layout_id), eq(tables.climbs.isListed, true), eq(tables.climbs.isDraft, false), @@ -109,12 +113,12 @@ export const createClimbFilters = ( ...notHolds.map((holdId) => notLike(tables.climbs.frames, `%${holdId}r%`)), ]; - // State-specific hold conditions - use climb_holds table to filter by hold_id AND hold_state - const climbHoldsTable = getTableName(params.board_name, 'climb_holds'); + // State-specific hold conditions - use unified board_climb_holds table to filter by hold_id AND hold_state const holdStateConditions: SQL[] = holdStateFilters.map(({ holdId, state }) => sql`EXISTS ( - SELECT 1 FROM ${sql.identifier(climbHoldsTable)} ch - WHERE ch.climb_uuid = ${tables.climbs.uuid} + SELECT 1 FROM board_climb_holds ch + WHERE ch.board_type = ${params.board_name} + AND ch.climb_uuid = ${tables.climbs.uuid} AND ch.hold_id = ${holdId} AND ch.hold_state = ${state} )` @@ -132,31 +136,30 @@ export const createClimbFilters = ( // Climbs with edge_bottom below this threshold use "tall only" holds // For Kilter Homewall (productId=7), 7x10/10x10 sizes have edgeBottom=24, 8x12/10x12 have edgeBottom=-12 // So "tall climbs" are those with edgeBottom < 24 (using holds only available on 12-tall sizes) - const productSizesTable = getTableName(params.board_name, 'product_sizes'); - tallClimbsConditions.push( sql`${tables.climbs.edgeBottom} < ( SELECT MAX(ps.edge_bottom) - FROM ${sql.identifier(productSizesTable)} ps - WHERE ps.product_id = ${KILTER_HOMEWALL_PRODUCT_ID} + FROM board_product_sizes ps + WHERE ps.board_type = ${params.board_name} + AND ps.product_id = ${KILTER_HOMEWALL_PRODUCT_ID} AND ps.id != ${params.size_id} )` ); } // Personal progress filter conditions (only apply if userId is provided) + // Uses boardsesh_ticks with NextAuth userId const personalProgressConditions: SQL[] = []; if (userId) { - const ascentsTable = getTableName(params.board_name, 'ascents'); - const bidsTable = getTableName(params.board_name, 'bids'); - if (searchParams.hideAttempted) { personalProgressConditions.push( sql`NOT EXISTS ( - SELECT 1 FROM ${sql.identifier(bidsTable)} - WHERE climb_uuid = ${tables.climbs.uuid} - AND user_id = ${userId} - AND angle = ${params.angle} + SELECT 1 FROM ${boardseshTicks} + WHERE ${boardseshTicks.climbUuid} = ${tables.climbs.uuid} + AND ${boardseshTicks.userId} = ${userId} + AND ${boardseshTicks.boardType} = ${params.board_name} + AND ${boardseshTicks.angle} = ${params.angle} + AND ${boardseshTicks.status} = 'attempt' )` ); } @@ -164,10 +167,12 @@ export const createClimbFilters = ( if (searchParams.hideCompleted) { personalProgressConditions.push( sql`NOT EXISTS ( - SELECT 1 FROM ${sql.identifier(ascentsTable)} - WHERE climb_uuid = ${tables.climbs.uuid} - AND user_id = ${userId} - AND angle = ${params.angle} + SELECT 1 FROM ${boardseshTicks} + WHERE ${boardseshTicks.climbUuid} = ${tables.climbs.uuid} + AND ${boardseshTicks.userId} = ${userId} + AND ${boardseshTicks.boardType} = ${params.board_name} + AND ${boardseshTicks.angle} = ${params.angle} + AND ${boardseshTicks.status} IN ('flash', 'send') )` ); } @@ -175,10 +180,12 @@ export const createClimbFilters = ( if (searchParams.showOnlyAttempted) { personalProgressConditions.push( sql`EXISTS ( - SELECT 1 FROM ${sql.identifier(bidsTable)} - WHERE climb_uuid = ${tables.climbs.uuid} - AND user_id = ${userId} - AND angle = ${params.angle} + SELECT 1 FROM ${boardseshTicks} + WHERE ${boardseshTicks.climbUuid} = ${tables.climbs.uuid} + AND ${boardseshTicks.userId} = ${userId} + AND ${boardseshTicks.boardType} = ${params.board_name} + AND ${boardseshTicks.angle} = ${params.angle} + AND ${boardseshTicks.status} = 'attempt' )` ); } @@ -186,57 +193,61 @@ export const createClimbFilters = ( if (searchParams.showOnlyCompleted) { personalProgressConditions.push( sql`EXISTS ( - SELECT 1 FROM ${sql.identifier(ascentsTable)} - WHERE climb_uuid = ${tables.climbs.uuid} - AND user_id = ${userId} - AND angle = ${params.angle} + SELECT 1 FROM ${boardseshTicks} + WHERE ${boardseshTicks.climbUuid} = ${tables.climbs.uuid} + AND ${boardseshTicks.userId} = ${userId} + AND ${boardseshTicks.boardType} = ${params.board_name} + AND ${boardseshTicks.angle} = ${params.angle} + AND ${boardseshTicks.status} IN ('flash', 'send') )` ); } } - // User-specific logbook data selectors + // User-specific logbook data selectors using boardsesh_ticks const getUserLogbookSelects = () => { - const ascentsTable = getTableName(params.board_name, 'ascents'); - const bidsTable = getTableName(params.board_name, 'bids'); - return { userAscents: sql`( - SELECT COUNT(*) - FROM ${sql.identifier(ascentsTable)} - WHERE climb_uuid = ${tables.climbs.uuid} - AND user_id = ${userId || ''} - AND angle = ${params.angle} + SELECT COUNT(*) + FROM ${boardseshTicks} + WHERE ${boardseshTicks.climbUuid} = ${tables.climbs.uuid} + AND ${boardseshTicks.userId} = ${userId || ''} + AND ${boardseshTicks.boardType} = ${params.board_name} + AND ${boardseshTicks.angle} = ${params.angle} + AND ${boardseshTicks.status} IN ('flash', 'send') )`, userAttempts: sql`( - SELECT COUNT(*) - FROM ${sql.identifier(bidsTable)} - WHERE climb_uuid = ${tables.climbs.uuid} - AND user_id = ${userId || ''} - AND angle = ${params.angle} + SELECT COUNT(*) + FROM ${boardseshTicks} + WHERE ${boardseshTicks.climbUuid} = ${tables.climbs.uuid} + AND ${boardseshTicks.userId} = ${userId || ''} + AND ${boardseshTicks.boardType} = ${params.board_name} + AND ${boardseshTicks.angle} = ${params.angle} + AND ${boardseshTicks.status} = 'attempt' )`, }; }; - // Hold-specific user data selectors for heatmap + // Hold-specific user data selectors for heatmap using boardsesh_ticks const getHoldUserLogbookSelects = (climbHoldsTable: typeof tables.climbHolds) => { - const ascentsTable = getTableName(params.board_name, 'ascents'); - const bidsTable = getTableName(params.board_name, 'bids'); - return { userAscents: sql`( - SELECT COUNT(*) - FROM ${sql.identifier(ascentsTable)} a - WHERE a.climb_uuid = ${climbHoldsTable.climbUuid} - AND a.user_id = ${userId || ''} - AND a.angle = ${params.angle} + SELECT COUNT(*) + FROM ${boardseshTicks} + WHERE ${boardseshTicks.climbUuid} = ${climbHoldsTable.climbUuid} + AND ${boardseshTicks.userId} = ${userId || ''} + AND ${boardseshTicks.boardType} = ${params.board_name} + AND ${boardseshTicks.angle} = ${params.angle} + AND ${boardseshTicks.status} IN ('flash', 'send') )`, userAttempts: sql`( - SELECT COUNT(*) - FROM ${sql.identifier(bidsTable)} b - WHERE b.climb_uuid = ${climbHoldsTable.climbUuid} - AND b.user_id = ${userId || ''} - AND b.angle = ${params.angle} + SELECT COUNT(*) + FROM ${boardseshTicks} + WHERE ${boardseshTicks.climbUuid} = ${climbHoldsTable.climbUuid} + AND ${boardseshTicks.userId} = ${userId || ''} + AND ${boardseshTicks.boardType} = ${params.board_name} + AND ${boardseshTicks.angle} = ${params.angle} + AND ${boardseshTicks.status} = 'attempt' )`, }; }; @@ -251,18 +262,26 @@ export const createClimbFilters = ( // Helper function to get all climb stats conditions getClimbStatsConditions: () => climbStatsConditions, - // For use in the subquery with left join + // For use in the subquery with left join - includes board_type for unified tables getClimbStatsJoinConditions: () => [ eq(tables.climbStats.climbUuid, tables.climbs.uuid), + eq(tables.climbStats.boardType, params.board_name), eq(tables.climbStats.angle, params.angle), ], - // For use in getHoldHeatmapData - getHoldHeatmapClimbStatsConditions: (climbHoldsTable: typeof tables.climbHolds) => [ - eq(tables.climbStats.climbUuid, climbHoldsTable.climbUuid), + // For use in getHoldHeatmapData - includes board_type for unified tables + getHoldHeatmapClimbStatsConditions: () => [ + eq(tables.climbStats.climbUuid, tables.climbHolds.climbUuid), + eq(tables.climbStats.boardType, params.board_name), eq(tables.climbStats.angle, params.angle), ], + // For use when joining climbHolds - includes board_type for unified tables + getClimbHoldsJoinConditions: () => [ + eq(tables.climbHolds.climbUuid, tables.climbs.uuid), + eq(tables.climbHolds.boardType, params.board_name), + ], + // User-specific logbook data selectors getUserLogbookSelects, diff --git a/packages/web/app/lib/db/queries/climbs/holds-heatmap.ts b/packages/web/app/lib/db/queries/climbs/holds-heatmap.ts index 56c13055..3170804d 100644 --- a/packages/web/app/lib/db/queries/climbs/holds-heatmap.ts +++ b/packages/web/app/lib/db/queries/climbs/holds-heatmap.ts @@ -1,10 +1,10 @@ import { and, eq, sql } from 'drizzle-orm'; import { dbz as db } from '@/app/lib/db/db'; import { ParsedBoardRouteParameters, SearchRequestPagination } from '@/app/lib/types'; -import { getBoardTables, BoardName as AuroraBoardName } from '@/lib/db/queries/util/table-select'; +import { UNIFIED_TABLES } from '@/lib/db/queries/util/table-select'; import { createClimbFilters } from './create-climb-filters'; -import { getTableName } from '@/app/lib/data-sync/aurora/getTableName'; import { getSizeEdges } from '@/app/lib/__generated__/product-sizes-data'; +import { boardseshTicks } from '@/app/lib/db/schema'; export interface HoldHeatmapData { holdId: number; @@ -22,10 +22,9 @@ export interface HoldHeatmapData { export const getHoldHeatmapData = async ( params: ParsedBoardRouteParameters, searchParams: SearchRequestPagination, - userId?: number, + userId?: string, ): Promise => { - const tables = getBoardTables(params.board_name as AuroraBoardName); - const climbHolds = tables.climbHolds; + const { climbs, climbStats, climbHolds } = UNIFIED_TABLES; // Get hardcoded size edges (eliminates database query) const sizeEdges = getSizeEdges(params.board_name, params.size_id); @@ -34,7 +33,7 @@ export const getHoldHeatmapData = async ( } // Use the shared filter creator with static edge values - const filters = createClimbFilters(tables, params, searchParams, sizeEdges, userId); + const filters = createClimbFilters(params, searchParams, sizeEdges, userId); try { // Check if personal progress filters are active - if so, use user-specific counts @@ -50,7 +49,6 @@ export const getHoldHeatmapData = async ( // When personal progress filters are active, we need to compute user-specific hold statistics // Since the filters already limit climbs to user's attempted/completed ones, // we can use the same base query but the results will be user-filtered - // Note: product_sizes JOIN eliminated - using pre-fetched sizeEdges constants instead const baseQuery = db .select({ holdId: climbHolds.holdId, @@ -60,14 +58,11 @@ export const getHoldHeatmapData = async ( handUses: sql`SUM(CASE WHEN ${climbHolds.holdState} = 'HAND' THEN 1 ELSE 0 END)`, footUses: sql`SUM(CASE WHEN ${climbHolds.holdState} = 'FOOT' THEN 1 ELSE 0 END)`, finishUses: sql`SUM(CASE WHEN ${climbHolds.holdState} = 'FINISH' THEN 1 ELSE 0 END)`, - averageDifficulty: sql`AVG(${tables.climbStats.displayDifficulty})`, + averageDifficulty: sql`AVG(${climbStats.displayDifficulty})`, }) .from(climbHolds) - .innerJoin(tables.climbs, eq(tables.climbs.uuid, climbHolds.climbUuid)) - .leftJoin( - tables.climbStats, - and(eq(tables.climbStats.climbUuid, climbHolds.climbUuid), eq(tables.climbStats.angle, params.angle)), - ) + .innerJoin(climbs, and(...filters.getClimbHoldsJoinConditions())) + .leftJoin(climbStats, and(...filters.getHoldHeatmapClimbStatsConditions())) .where( and(...filters.getClimbWhereConditions(), ...filters.getSizeConditions(), ...filters.getClimbStatsConditions()), ) @@ -76,24 +71,20 @@ export const getHoldHeatmapData = async ( holdStats = await baseQuery; } else { // Use global community stats when no personal progress filters are active - // Note: product_sizes JOIN eliminated - using pre-fetched sizeEdges constants instead const baseQuery = db .select({ holdId: climbHolds.holdId, totalUses: sql`COUNT(DISTINCT ${climbHolds.climbUuid})`, - totalAscents: sql`SUM(${tables.climbStats.ascensionistCount})`, + totalAscents: sql`SUM(${climbStats.ascensionistCount})`, startingUses: sql`SUM(CASE WHEN ${climbHolds.holdState} = 'STARTING' THEN 1 ELSE 0 END)`, handUses: sql`SUM(CASE WHEN ${climbHolds.holdState} = 'HAND' THEN 1 ELSE 0 END)`, footUses: sql`SUM(CASE WHEN ${climbHolds.holdState} = 'FOOT' THEN 1 ELSE 0 END)`, finishUses: sql`SUM(CASE WHEN ${climbHolds.holdState} = 'FINISH' THEN 1 ELSE 0 END)`, - averageDifficulty: sql`AVG(${tables.climbStats.displayDifficulty})`, + averageDifficulty: sql`AVG(${climbStats.displayDifficulty})`, }) .from(climbHolds) - .innerJoin(tables.climbs, eq(tables.climbs.uuid, climbHolds.climbUuid)) - .leftJoin( - tables.climbStats, - and(eq(tables.climbStats.climbUuid, climbHolds.climbUuid), eq(tables.climbStats.angle, params.angle)), - ) + .innerJoin(climbs, and(...filters.getClimbHoldsJoinConditions())) + .leftJoin(climbStats, and(...filters.getHoldHeatmapClimbStatsConditions())) .where( and(...filters.getClimbWhereConditions(), ...filters.getSizeConditions(), ...filters.getClimbStatsConditions()), ) @@ -105,34 +96,27 @@ export const getHoldHeatmapData = async ( // Add user-specific data only if not already computed in the main query if (userId && !personalProgressFiltersEnabled) { // Only fetch separate user data if we're not already using user-specific main stats - const ascentsTableName = getTableName(params.board_name, 'ascents'); - const bidsTableName = getTableName(params.board_name, 'bids'); - const climbHoldsTableName = getTableName(params.board_name, 'climb_holds'); + // Uses boardsesh_ticks (NextAuth userId) // Query for user ascents and attempts per hold in parallel const [userAscentsQuery, userAttemptsQuery] = await Promise.all([ db.execute(sql` SELECT ch.hold_id, COUNT(*) as user_ascents - FROM ${sql.identifier(ascentsTableName)} a - JOIN ${sql.identifier(climbHoldsTableName)} ch ON a.climb_uuid = ch.climb_uuid - WHERE a.user_id = ${userId} - AND a.angle = ${params.angle} + FROM ${boardseshTicks} t + JOIN board_climb_holds ch ON t.climb_uuid = ch.climb_uuid AND ch.board_type = ${params.board_name} + WHERE t.user_id = ${userId} + AND t.board_type = ${params.board_name} + AND t.angle = ${params.angle} + AND t.status IN ('flash', 'send') GROUP BY ch.hold_id `), db.execute(sql` - SELECT ch.hold_id, SUM(attempt_count) as user_attempts - FROM ( - SELECT b.climb_uuid, b.bid_count as attempt_count - FROM ${sql.identifier(bidsTableName)} b - WHERE b.user_id = ${userId} - AND b.angle = ${params.angle} - UNION ALL - SELECT a.climb_uuid, a.bid_count as attempt_count - FROM ${sql.identifier(ascentsTableName)} a - WHERE a.user_id = ${userId} - AND a.angle = ${params.angle} - ) attempts - JOIN ${sql.identifier(climbHoldsTableName)} ch ON attempts.climb_uuid = ch.climb_uuid + SELECT ch.hold_id, SUM(t.attempt_count) as user_attempts + FROM ${boardseshTicks} t + JOIN board_climb_holds ch ON t.climb_uuid = ch.climb_uuid AND ch.board_type = ${params.board_name} + WHERE t.user_id = ${userId} + AND t.board_type = ${params.board_name} + AND t.angle = ${params.angle} GROUP BY ch.hold_id `), ]); @@ -173,7 +157,7 @@ export const getHoldHeatmapData = async ( } }; -function normalizeStats(stats: Record, userId?: number): HoldHeatmapData { +function normalizeStats(stats: Record, userId?: string): HoldHeatmapData { // For numeric fields, ensure we're returning a number and handle null/undefined properly const result: HoldHeatmapData = { holdId: Number(stats.holdId), diff --git a/packages/web/app/lib/db/queries/climbs/setter-stats.ts b/packages/web/app/lib/db/queries/climbs/setter-stats.ts index e5275c3b..eb9b8867 100644 --- a/packages/web/app/lib/db/queries/climbs/setter-stats.ts +++ b/packages/web/app/lib/db/queries/climbs/setter-stats.ts @@ -1,7 +1,8 @@ import { eq, sql, and, ilike } from 'drizzle-orm'; import { dbz as db } from '@/app/lib/db/db'; import { ParsedBoardRouteParameters } from '@/app/lib/types'; -import { getBoardTables, BoardName as AuroraBoardName } from '@/lib/db/queries/util/table-select'; +import { UNIFIED_TABLES } from '@/lib/db/queries/util/table-select'; +import { getSizeEdges } from '@/app/lib/__generated__/product-sizes-data'; export interface SetterStat { setter_username: string; @@ -12,36 +13,45 @@ export const getSetterStats = async ( params: ParsedBoardRouteParameters, searchQuery?: string, ): Promise => { - const tables = getBoardTables(params.board_name as AuroraBoardName); + const { climbs, climbStats } = UNIFIED_TABLES; + + // Get hardcoded size edges (eliminates database query) + const sizeEdges = getSizeEdges(params.board_name, params.size_id); + if (!sizeEdges) { + return []; + } try { // Build WHERE conditions const whereConditions = [ - eq(tables.climbs.layoutId, params.layout_id), - eq(tables.climbStats.angle, params.angle), - sql`${tables.climbs.edgeLeft} > ${tables.productSizes.edgeLeft}`, - sql`${tables.climbs.edgeRight} < ${tables.productSizes.edgeRight}`, - sql`${tables.climbs.edgeBottom} > ${tables.productSizes.edgeBottom}`, - sql`${tables.climbs.edgeTop} < ${tables.productSizes.edgeTop}`, - sql`${tables.climbs.setterUsername} IS NOT NULL`, - sql`${tables.climbs.setterUsername} != ''`, + eq(climbs.boardType, params.board_name), + eq(climbs.layoutId, params.layout_id), + eq(climbStats.angle, params.angle), + sql`${climbs.edgeLeft} > ${sizeEdges.edgeLeft}`, + sql`${climbs.edgeRight} < ${sizeEdges.edgeRight}`, + sql`${climbs.edgeBottom} > ${sizeEdges.edgeBottom}`, + sql`${climbs.edgeTop} < ${sizeEdges.edgeTop}`, + sql`${climbs.setterUsername} IS NOT NULL`, + sql`${climbs.setterUsername} != ''`, ]; // Add search filter if provided if (searchQuery && searchQuery.trim().length > 0) { - whereConditions.push(ilike(tables.climbs.setterUsername, `%${searchQuery}%`)); + whereConditions.push(ilike(climbs.setterUsername, `%${searchQuery}%`)); } const result = await db .select({ - setter_username: tables.climbs.setterUsername, + setter_username: climbs.setterUsername, climb_count: sql`count(*)::int`, }) - .from(tables.climbs) - .innerJoin(tables.climbStats, sql`${tables.climbStats.climbUuid} = ${tables.climbs.uuid}`) - .innerJoin(tables.productSizes, eq(tables.productSizes.id, params.size_id)) + .from(climbs) + .innerJoin(climbStats, and( + eq(climbStats.climbUuid, climbs.uuid), + eq(climbStats.boardType, params.board_name), + )) .where(and(...whereConditions)) - .groupBy(tables.climbs.setterUsername) + .groupBy(climbs.setterUsername) .orderBy(sql`count(*) DESC`) .limit(50); // Limit results for performance diff --git a/packages/web/app/lib/db/queries/util/table-select.ts b/packages/web/app/lib/db/queries/util/table-select.ts index 063056b6..2348f487 100644 --- a/packages/web/app/lib/db/queries/util/table-select.ts +++ b/packages/web/app/lib/db/queries/util/table-select.ts @@ -1,5 +1,7 @@ import { PgTable } from 'drizzle-orm/pg-core'; +import { eq } from 'drizzle-orm'; import { + // Legacy board-specific tables (for backward compatibility during migration) kilterClimbs, kilterClimbStats, kilterDifficultyGrades, @@ -36,6 +38,30 @@ import { tensionWalls, kilterTags, tensionTags, + // Unified tables + boardAttempts, + boardDifficultyGrades, + boardProducts, + boardSets, + boardProductSizes, + boardLayouts, + boardHoles, + boardPlacementRoles, + boardLeds, + boardPlacements, + boardProductSizesLayoutsSets, + boardClimbs, + boardClimbStats, + boardClimbHolds, + boardClimbStatsHistory, + boardBetaLinks, + boardUsers, + boardCircuits, + boardCircuitsClimbs, + boardWalls, + boardTags, + boardUserSyncs, + boardSharedSyncs, } from '@/lib/db/schema'; import { AuroraBoardName } from '@/app/lib/api-wrappers/aurora/types'; @@ -174,3 +200,76 @@ const tableSelectUtils = { }; export default tableSelectUtils; + +// ============================================================================= +// Unified Tables (New API) +// ============================================================================= + +/** + * Unified table set - all queries should filter by board_type + */ +export const UNIFIED_TABLES = { + attempts: boardAttempts, + difficultyGrades: boardDifficultyGrades, + products: boardProducts, + sets: boardSets, + productSizes: boardProductSizes, + layouts: boardLayouts, + holes: boardHoles, + placementRoles: boardPlacementRoles, + leds: boardLeds, + placements: boardPlacements, + productSizesLayoutsSets: boardProductSizesLayoutsSets, + climbs: boardClimbs, + climbStats: boardClimbStats, + climbHolds: boardClimbHolds, + climbStatsHistory: boardClimbStatsHistory, + betaLinks: boardBetaLinks, + users: boardUsers, + circuits: boardCircuits, + circuitsClimbs: boardCircuitsClimbs, + walls: boardWalls, + tags: boardTags, + userSyncs: boardUserSyncs, + sharedSyncs: boardSharedSyncs, +} as const; + +export type UnifiedTableSet = typeof UNIFIED_TABLES; + +/** + * Get a unified table (all queries should filter by board_type) + * @param tableName The name of the unified table to retrieve + * @returns The unified table + */ +export function getUnifiedTable( + tableName: K +): UnifiedTableSet[K] { + return UNIFIED_TABLES[tableName]; +} + +/** + * Helper to create board_type equality condition for WHERE clauses + * @param table A unified table with boardType column + * @param boardName The board name to filter by + * @returns A drizzle eq() condition + */ +export function boardTypeCondition( + table: { boardType: typeof boardClimbs.boardType }, + boardName: BoardName +) { + return eq(table.boardType, boardName); +} + +/** + * Extended board name type that includes moonboard for unified tables + */ +export type UnifiedBoardName = BoardName | 'moonboard'; + +/** + * Check if a board name is valid for unified tables (includes moonboard) + * @param boardName The name to check + * @returns True if the board name is valid for unified tables + */ +export function isValidUnifiedBoardName(boardName: string): boardName is UnifiedBoardName { + return boardName === 'kilter' || boardName === 'tension' || boardName === 'moonboard'; +} diff --git a/packages/web/app/lib/slug-utils.ts b/packages/web/app/lib/slug-utils.ts index d2b501d7..aff45f3b 100644 --- a/packages/web/app/lib/slug-utils.ts +++ b/packages/web/app/lib/slug-utils.ts @@ -1,6 +1,8 @@ -import { sql } from '@/app/lib/db/db'; +import { dbz } from '@/app/lib/db/db'; import { BoardName, LayoutId, Size } from '@/app/lib/types'; import { matchSetNameToSlugParts } from './slug-matching'; +import { UNIFIED_TABLES } from '@/app/lib/db/queries/util/table-select'; +import { eq, and, isNull } from 'drizzle-orm'; // Re-export for backwards compatibility export { matchSetNameToSlugParts } from './slug-matching'; @@ -21,26 +23,17 @@ export type SetRow = { name: string; }; -const getTableName = (board_name: string, table_name: string) => { - switch (board_name) { - case 'tension': - case 'kilter': - return `${board_name}_${table_name}`; - default: - return `${table_name}`; - } -}; - // Reverse lookup functions for slug to ID conversion export const getLayoutBySlug = async (board_name: BoardName, slug: string): Promise => { - const rows = (await sql` - SELECT id, name - FROM ${sql.unsafe(getTableName(board_name, 'layouts'))} layouts - WHERE is_listed = true - AND password IS NULL - `) as LayoutRow[]; + const { layouts } = UNIFIED_TABLES; + + const rows = await dbz + .select({ id: layouts.id, name: layouts.name }) + .from(layouts) + .where(and(eq(layouts.boardType, board_name), eq(layouts.isListed, true), isNull(layouts.password))); const layout = rows.find((l) => { + if (!l.name) return false; const baseSlug = l.name .toLowerCase() .trim() @@ -65,7 +58,8 @@ export const getLayoutBySlug = async (board_name: BoardName, slug: string): Prom return layoutSlug === slug; }); - return layout || null; + if (!layout || !layout.name) return null; + return { id: layout.id, name: layout.name }; }; export const getSizeBySlug = async ( @@ -73,12 +67,23 @@ export const getSizeBySlug = async ( layout_id: LayoutId, slug: string, ): Promise => { - const rows = (await sql` - SELECT product_sizes.id, product_sizes.name, product_sizes.description - FROM ${sql.unsafe(getTableName(board_name, 'product_sizes'))} product_sizes - INNER JOIN ${sql.unsafe(getTableName(board_name, 'layouts'))} layouts ON product_sizes.product_id = layouts.product_id - WHERE layouts.id = ${layout_id} - `) as SizeRow[]; + const { productSizes, layouts } = UNIFIED_TABLES; + + const rows = await dbz + .select({ + id: productSizes.id, + name: productSizes.name, + description: productSizes.description, + }) + .from(productSizes) + .innerJoin( + layouts, + and( + eq(productSizes.boardType, layouts.boardType), + eq(productSizes.productId, layouts.productId), + ), + ) + .where(and(eq(layouts.boardType, board_name), eq(layouts.id, layout_id))); // Parse slug - may be "10x12" or "10x12-full-ride" const dimensionMatch = slug.match(/^(\d+x\d+)(?:-(.+))?$/i); @@ -88,6 +93,7 @@ export const getSizeBySlug = async ( const descSuffix = dimensionMatch[2]; // e.g., "full-ride" or undefined const size = rows.find((s) => { + if (!s.name) return false; const sizeMatch = s.name.match(/(\d+)\s*x\s*(\d+)/i); if (!sizeMatch) return false; @@ -117,22 +123,28 @@ export const getSizeBySlug = async ( return false; }); - if (size) return size; + if (size && size.name) { + return { id: size.id, name: size.name, description: size.description || '' }; + } // If no "Full Ride" found with backward compat, try to match first size with dimensions if (!descSuffix) { const fallbackSize = rows.find((s) => { + if (!s.name) return false; const sizeMatch = s.name.match(/(\d+)\s*x\s*(\d+)/i); if (!sizeMatch) return false; const sizeDimensions = `${sizeMatch[1]}x${sizeMatch[2]}`.toLowerCase(); return sizeDimensions === dimensions; }); - if (fallbackSize) return fallbackSize; + if (fallbackSize && fallbackSize.name) { + return { id: fallbackSize.id, name: fallbackSize.name, description: fallbackSize.description || '' }; + } } } // Fallback to general slug matching const size = rows.find((s) => { + if (!s.name) return false; const sizeSlug = s.name .toLowerCase() .trim() @@ -143,7 +155,8 @@ export const getSizeBySlug = async ( return sizeSlug === slug; }); - return size || null; + if (!size || !size.name) return null; + return { id: size.id, name: size.name, description: size.description || '' }; }; /** @@ -161,18 +174,31 @@ export const getSetsBySlug = async ( size_id: Size, slug: string, ): Promise => { - const rows = (await sql` - SELECT sets.id, sets.name - FROM ${sql.unsafe(getTableName(board_name, 'sets'))} sets - INNER JOIN ${sql.unsafe(getTableName(board_name, 'product_sizes_layouts_sets'))} psls - ON sets.id = psls.set_id - WHERE psls.product_size_id = ${size_id} - AND psls.layout_id = ${layout_id} - `) as SetRow[]; + const { sets, productSizesLayoutsSets } = UNIFIED_TABLES; + + const rows = await dbz + .select({ id: sets.id, name: sets.name }) + .from(sets) + .innerJoin( + productSizesLayoutsSets, + and( + eq(sets.boardType, productSizesLayoutsSets.boardType), + eq(sets.id, productSizesLayoutsSets.setId), + ), + ) + .where( + and( + eq(productSizesLayoutsSets.boardType, board_name), + eq(productSizesLayoutsSets.productSizeId, size_id), + eq(productSizesLayoutsSets.layoutId, layout_id), + ), + ); // Parse the slug to get individual set names const slugParts = slug.split('_'); - const matchingSets = rows.filter((s) => matchSetNameToSlugParts(s.name, slugParts)); + const matchingSets = rows + .filter((s): s is typeof s & { name: string } => s.name !== null && matchSetNameToSlugParts(s.name, slugParts)) + .map((s) => ({ id: s.id, name: s.name })); return matchingSets; };