diff --git a/package-lock.json b/package-lock.json index 0eb91a80..cbc47f70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9162,9 +9162,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -15681,7 +15681,7 @@ "@faker-js/faker": "^9.0.0", "@types/node": "^25", "@types/ws": "^8.18.1", - "dotenv": "^17.2.3", + "dotenv": "^17.3.1", "drizzle-kit": "^0.31.8", "tsx": "^4.19.0", "typescript": "^5.9.3" diff --git a/packages/backend/src/__tests__/playlist-follows.test.ts b/packages/backend/src/__tests__/playlist-follows.test.ts new file mode 100644 index 00000000..c0aa1529 --- /dev/null +++ b/packages/backend/src/__tests__/playlist-follows.test.ts @@ -0,0 +1,186 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const { mockDb } = vi.hoisted(() => { + const mockDb = { + execute: vi.fn(), + select: vi.fn(), + insert: vi.fn(), + delete: vi.fn(), + update: vi.fn(), + }; + return { mockDb }; +}); + +vi.mock('../db/client', () => ({ + db: mockDb, +})); + +vi.mock('../events/index', () => ({ + publishSocialEvent: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock('../utils/rate-limiter', () => ({ + checkRateLimit: vi.fn(), +})); + +vi.mock('../utils/redis-rate-limiter', () => ({ + checkRateLimitRedis: vi.fn(), +})); + +import type { ConnectionContext } from '@boardsesh/shared-schema'; +import { playlistMutations } from '../graphql/resolvers/playlists/mutations'; + +function makeCtx(overrides: Partial = {}): ConnectionContext { + return { + connectionId: 'conn-1', + isAuthenticated: true, + userId: 'user-123', + sessionId: null, + boardPath: null, + controllerId: null, + controllerApiKey: null, + ...overrides, + } as ConnectionContext; +} + +function createMockChain(resolveValue: unknown = []): Record { + const chain: Record = {}; + const methods = [ + 'select', 'from', 'where', 'leftJoin', 'innerJoin', + 'groupBy', 'orderBy', 'limit', 'offset', + 'insert', 'values', 'onConflictDoNothing', 'returning', + 'delete', 'update', 'set', + ]; + + chain.then = (resolve: (value: unknown) => unknown) => Promise.resolve(resolveValue).then(resolve); + + for (const method of methods) { + chain[method] = vi.fn((..._args: unknown[]) => chain); + } + + return chain; +} + +describe('followPlaylist mutation', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should throw for unauthenticated users', async () => { + const ctx = makeCtx({ isAuthenticated: false }); + await expect( + playlistMutations.followPlaylist(null, { input: { playlistUuid: 'playlist-1' } }, ctx), + ).rejects.toThrow('Authentication required'); + }); + + it('should throw if playlist not found', async () => { + const ctx = makeCtx(); + + // select().from().where().limit() → empty array (not found) + const selectChain = createMockChain([]); + mockDb.select.mockReturnValueOnce(selectChain); + + await expect( + playlistMutations.followPlaylist(null, { input: { playlistUuid: 'nonexistent' } }, ctx), + ).rejects.toThrow('Playlist not found'); + }); + + it('should throw if playlist is private', async () => { + const ctx = makeCtx(); + + // select() → playlist found but not public + const selectChain = createMockChain([{ uuid: 'playlist-1', isPublic: false }]); + mockDb.select.mockReturnValueOnce(selectChain); + + await expect( + playlistMutations.followPlaylist(null, { input: { playlistUuid: 'playlist-1' } }, ctx), + ).rejects.toThrow('Cannot follow a private playlist'); + }); + + it('should insert follow and return true', async () => { + const ctx = makeCtx(); + + // 1. Playlist exists and is public + const selectChain = createMockChain([{ uuid: 'playlist-1', isPublic: true }]); + mockDb.select.mockReturnValueOnce(selectChain); + + // 2. Insert follow (onConflictDoNothing) + const insertChain = createMockChain([{ id: 1 }]); + mockDb.insert.mockReturnValueOnce(insertChain); + + const result = await playlistMutations.followPlaylist( + null, + { input: { playlistUuid: 'playlist-1' } }, + ctx, + ); + + expect(result).toBe(true); + expect(mockDb.insert).toHaveBeenCalledTimes(1); + }); + + it('should handle idempotent follow (already following)', async () => { + const ctx = makeCtx(); + + // 1. Playlist exists and is public + const selectChain = createMockChain([{ uuid: 'playlist-1', isPublic: true }]); + mockDb.select.mockReturnValueOnce(selectChain); + + // 2. Insert follow → onConflictDoNothing returns empty (already exists) + const insertChain = createMockChain([]); + mockDb.insert.mockReturnValueOnce(insertChain); + + const result = await playlistMutations.followPlaylist( + null, + { input: { playlistUuid: 'playlist-1' } }, + ctx, + ); + + expect(result).toBe(true); + }); +}); + +describe('unfollowPlaylist mutation', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should throw for unauthenticated users', async () => { + const ctx = makeCtx({ isAuthenticated: false }); + await expect( + playlistMutations.unfollowPlaylist(null, { input: { playlistUuid: 'playlist-1' } }, ctx), + ).rejects.toThrow('Authentication required'); + }); + + it('should delete follow and return true', async () => { + const ctx = makeCtx(); + + // delete().where() → affected 1 row + const deleteChain = createMockChain([{ id: 1 }]); + mockDb.delete.mockReturnValueOnce(deleteChain); + + const result = await playlistMutations.unfollowPlaylist( + null, + { input: { playlistUuid: 'playlist-1' } }, + ctx, + ); + + expect(result).toBe(true); + expect(mockDb.delete).toHaveBeenCalledTimes(1); + }); + + it('should handle unfollowing when not following (no-op)', async () => { + const ctx = makeCtx(); + + // delete().where() → affected 0 rows + const deleteChain = createMockChain([]); + mockDb.delete.mockReturnValueOnce(deleteChain); + + const result = await playlistMutations.unfollowPlaylist( + null, + { input: { playlistUuid: 'playlist-1' } }, + ctx, + ); + + expect(result).toBe(true); + }); +}); diff --git a/packages/backend/src/__tests__/playlist-queries.test.ts b/packages/backend/src/__tests__/playlist-queries.test.ts new file mode 100644 index 00000000..f9667a53 --- /dev/null +++ b/packages/backend/src/__tests__/playlist-queries.test.ts @@ -0,0 +1,331 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const { mockDb } = vi.hoisted(() => { + const mockDb = { + execute: vi.fn(), + select: vi.fn(), + insert: vi.fn(), + delete: vi.fn(), + update: vi.fn(), + }; + return { mockDb }; +}); + +vi.mock('../db/client', () => ({ + db: mockDb, +})); + +vi.mock('../events/index', () => ({ + publishSocialEvent: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock('../utils/rate-limiter', () => ({ + checkRateLimit: vi.fn(), +})); + +vi.mock('../utils/redis-rate-limiter', () => ({ + checkRateLimitRedis: vi.fn(), +})); + +vi.mock('../db/queries/util/table-select', () => ({ + getBoardTables: vi.fn().mockReturnValue({ + climbs: { uuid: 'uuid', layoutId: 'layoutId', boardType: 'boardType', setterUsername: 'setterUsername', name: 'name', description: 'description', frames: 'frames', createdAt: 'createdAt', edgeLeft: 'edgeLeft', edgeRight: 'edgeRight', edgeBottom: 'edgeBottom', edgeTop: 'edgeTop' }, + climbStats: { climbUuid: 'climbUuid', boardType: 'boardType', angle: 'angle', ascensionistCount: 'ascensionistCount', qualityAverage: 'qualityAverage', difficultyAverage: 'difficultyAverage', displayDifficulty: 'displayDifficulty', benchmarkDifficulty: 'benchmarkDifficulty' }, + difficultyGrades: { boardType: 'boardType', difficulty: 'difficulty', boulderName: 'boulderName' }, + }), + isValidBoardName: vi.fn().mockReturnValue(true), +})); + +vi.mock('../db/queries/util/hold-state', () => ({ + convertLitUpHoldsStringToMap: vi.fn().mockReturnValue([{}]), +})); + +vi.mock('../db/queries/util/product-sizes-data', () => ({ + getSizeEdges: vi.fn().mockReturnValue({ edgeLeft: 0, edgeRight: 100, edgeBottom: 0, edgeTop: 100 }), +})); + +import type { ConnectionContext } from '@boardsesh/shared-schema'; +import { playlistQueries, getPlaylistFollowStats } from '../graphql/resolvers/playlists/queries'; + +function makeCtx(overrides: Partial = {}): ConnectionContext { + return { + connectionId: 'conn-1', + isAuthenticated: true, + userId: 'user-123', + sessionId: null, + boardPath: null, + controllerId: null, + controllerApiKey: null, + ...overrides, + } as ConnectionContext; +} + +function createMockChain(resolveValue: unknown = []): Record { + const chain: Record = {}; + const methods = [ + 'select', 'from', 'where', 'leftJoin', 'innerJoin', + 'groupBy', 'orderBy', 'limit', 'offset', + 'insert', 'values', 'onConflictDoNothing', 'returning', + 'delete', 'update', 'set', + ]; + + chain.then = (resolve: (value: unknown) => unknown) => Promise.resolve(resolveValue).then(resolve); + + for (const method of methods) { + chain[method] = vi.fn((..._args: unknown[]) => chain); + } + + return chain; +} + +describe('getPlaylistFollowStats', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return empty map for empty input', async () => { + const result = await getPlaylistFollowStats([], null); + expect(result.size).toBe(0); + expect(mockDb.select).not.toHaveBeenCalled(); + }); + + it('should return followerCount and isFollowedByMe for each playlist', async () => { + // 1. Follower counts query + const followerCountChain = createMockChain([ + { playlistUuid: 'pl-1', count: 5 }, + { playlistUuid: 'pl-2', count: 0 }, + ]); + mockDb.select.mockReturnValueOnce(followerCountChain); + + // 2. Is-followed-by-me query + const followedChain = createMockChain([ + { playlistUuid: 'pl-1' }, + ]); + mockDb.select.mockReturnValueOnce(followedChain); + + const result = await getPlaylistFollowStats(['pl-1', 'pl-2'], 'user-123'); + + expect(result.get('pl-1')).toEqual({ followerCount: 5, isFollowedByMe: true }); + expect(result.get('pl-2')).toEqual({ followerCount: 0, isFollowedByMe: false }); + }); + + it('should skip follow check when userId is null', async () => { + // Only follower counts query — no follow check query + const followerCountChain = createMockChain([ + { playlistUuid: 'pl-1', count: 3 }, + ]); + mockDb.select.mockReturnValueOnce(followerCountChain); + + const result = await getPlaylistFollowStats(['pl-1'], null); + + expect(result.get('pl-1')).toEqual({ followerCount: 3, isFollowedByMe: false }); + // Only one select call (no follow check) + expect(mockDb.select).toHaveBeenCalledTimes(1); + }); +}); + +describe('playlistClimbs resolver', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should throw when playlist not found', async () => { + const ctx = makeCtx(); + + // Playlist lookup → empty + const selectChain = createMockChain([]); + mockDb.select.mockReturnValueOnce(selectChain); + + await expect( + playlistQueries.playlistClimbs( + null, + { input: { playlistId: 'nonexistent' } }, + ctx, + ), + ).rejects.toThrow('Playlist not found or access denied'); + }); + + it('should throw for private playlist when not authenticated', async () => { + const ctx = makeCtx({ isAuthenticated: false, userId: null }); + + // Playlist exists but is private + const playlistChain = createMockChain([{ id: BigInt(1), isPublic: false }]); + mockDb.select.mockReturnValueOnce(playlistChain); + + await expect( + playlistQueries.playlistClimbs( + null, + { input: { playlistId: 'private-pl' } }, + ctx, + ), + ).rejects.toThrow('Playlist not found or access denied'); + }); + + it('should return climbs in all-boards mode when boardName is omitted', async () => { + const ctx = makeCtx(); + + // 1. Playlist exists and is public + const playlistChain = createMockChain([{ id: BigInt(1), isPublic: true }]); + mockDb.select.mockReturnValueOnce(playlistChain); + + // 2. Count query + const countChain = createMockChain([{ count: 2 }]); + mockDb.select.mockReturnValueOnce(countChain); + + // 3. Climbs query (all-boards mode — no boardName filter) + const climbsChain = createMockChain([ + { + climbUuid: 'climb-1', + position: 0, + playlistAngle: 40, + uuid: 'climb-1', + layoutId: 1, + boardType: 'kilter', + setter_username: 'setter1', + name: 'Kilter Climb', + description: '', + frames: '', + statsAngle: 40, + ascensionist_count: 10, + difficulty: 'V5', + quality_average: 3.5, + difficulty_error: 0.2, + benchmark_difficulty: null, + }, + { + climbUuid: 'climb-2', + position: 1, + playlistAngle: null, + uuid: 'climb-2', + layoutId: 2, + boardType: 'tension', + setter_username: 'setter2', + name: 'Tension Climb', + description: '', + frames: '', + statsAngle: 30, + ascensionist_count: 5, + difficulty: 'V3', + quality_average: 2.0, + difficulty_error: 0.1, + benchmark_difficulty: null, + }, + ]); + mockDb.select.mockReturnValueOnce(climbsChain); + + const result = await playlistQueries.playlistClimbs( + null, + { input: { playlistId: 'test-pl' } }, + ctx, + ); + + expect(result.totalCount).toBe(2); + expect(result.climbs).toHaveLength(2); + expect(result.hasMore).toBe(false); + + // Check first climb is kilter + expect(result.climbs[0].uuid).toBe('climb-1'); + expect(result.climbs[0].boardType).toBe('kilter'); + expect(result.climbs[0].name).toBe('Kilter Climb'); + + // Check second climb is tension (cross-board!) + expect(result.climbs[1].uuid).toBe('climb-2'); + expect(result.climbs[1].boardType).toBe('tension'); + expect(result.climbs[1].name).toBe('Tension Climb'); + }); + + it('should return climbs in specific-board mode when boardName is provided', async () => { + const ctx = makeCtx(); + + // 1. Playlist exists and is public + const playlistChain = createMockChain([{ id: BigInt(1), isPublic: true }]); + mockDb.select.mockReturnValueOnce(playlistChain); + + // 2. Count query + const countChain = createMockChain([{ count: 1 }]); + mockDb.select.mockReturnValueOnce(countChain); + + // 3. Climbs query (specific-board mode) + const climbsChain = createMockChain([ + { + climbUuid: 'climb-1', + playlistAngle: 40, + position: 0, + uuid: 'climb-1', + layoutId: 1, + setter_username: 'setter1', + name: 'Kilter Climb', + description: '', + frames: '', + ascensionist_count: 10, + difficulty: 'V5', + quality_average: 3.5, + difficulty_error: 0.2, + benchmark_difficulty: null, + }, + ]); + mockDb.select.mockReturnValueOnce(climbsChain); + + const result = await playlistQueries.playlistClimbs( + null, + { + input: { + playlistId: 'test-pl', + boardName: 'kilter', + layoutId: 1, + sizeId: 10, + angle: 40, + }, + }, + ctx, + ); + + expect(result.totalCount).toBe(1); + expect(result.climbs).toHaveLength(1); + expect(result.climbs[0].boardType).toBe('kilter'); + }); + + it('should paginate correctly with hasMore', async () => { + const ctx = makeCtx(); + + // 1. Playlist exists + const playlistChain = createMockChain([{ id: BigInt(1), isPublic: true }]); + mockDb.select.mockReturnValueOnce(playlistChain); + + // 2. Count query (more than page size) + const countChain = createMockChain([{ count: 25 }]); + mockDb.select.mockReturnValueOnce(countChain); + + // 3. Return pageSize + 1 results to indicate hasMore + const climbResults = Array.from({ length: 21 }, (_, i) => ({ + climbUuid: `climb-${i}`, + position: i, + playlistAngle: 40, + uuid: `climb-${i}`, + layoutId: 1, + boardType: 'kilter', + setter_username: 'setter1', + name: `Climb ${i}`, + description: '', + frames: '', + statsAngle: 40, + ascensionist_count: 1, + difficulty: 'V1', + quality_average: 1.0, + difficulty_error: 0, + benchmark_difficulty: null, + })); + const climbsChain = createMockChain(climbResults); + mockDb.select.mockReturnValueOnce(climbsChain); + + const result = await playlistQueries.playlistClimbs( + null, + { input: { playlistId: 'test-pl', page: 0, pageSize: 20 } }, + ctx, + ); + + expect(result.hasMore).toBe(true); + expect(result.climbs).toHaveLength(20); // Trimmed to pageSize + expect(result.totalCount).toBe(25); + }); +}); diff --git a/packages/backend/src/graphql/resolvers/playlists/mutations.ts b/packages/backend/src/graphql/resolvers/playlists/mutations.ts index 1e78a069..93b29ab7 100644 --- a/packages/backend/src/graphql/resolvers/playlists/mutations.ts +++ b/packages/backend/src/graphql/resolvers/playlists/mutations.ts @@ -9,7 +9,9 @@ import { UpdatePlaylistInputSchema, AddClimbToPlaylistInputSchema, RemoveClimbFromPlaylistInputSchema, + FollowPlaylistInputSchema, } from '../../../validation/schemas'; +import { getPlaylistFollowStats } from './queries'; export const playlistMutations = { /** @@ -66,6 +68,8 @@ export const playlistMutations = { updatedAt: playlist.updatedAt.toISOString(), climbCount: 0, userRole: 'owner', + followerCount: 0, + isFollowedByMe: false, }; }, @@ -123,13 +127,16 @@ export const playlistMutations = { .where(eq(dbSchema.playlists.id, playlistId)) .returning(); - // Get climb count + // Get climb count and follow stats const climbCount = await db .select({ count: sql`count(*)::int` }) .from(dbSchema.playlistClimbs) .where(eq(dbSchema.playlistClimbs.playlistId, playlistId)) .limit(1); + const followStats = await getPlaylistFollowStats([updated.uuid], userId); + const stats = followStats.get(updated.uuid) ?? { followerCount: 0, isFollowedByMe: false }; + return { id: updated.id.toString(), uuid: updated.uuid, @@ -144,6 +151,8 @@ export const playlistMutations = { updatedAt: updated.updatedAt.toISOString(), climbCount: climbCount[0]?.count || 0, userRole: 'owner', + followerCount: stats.followerCount, + isFollowedByMe: stats.isFollowedByMe, }; }, @@ -384,4 +393,68 @@ export const playlistMutations = { return true; }, + + /** + * Follow a public playlist. Idempotent (onConflictDoNothing). + */ + followPlaylist: async ( + _: unknown, + { input }: { input: { playlistUuid: string } }, + ctx: ConnectionContext, + ): Promise => { + requireAuthenticated(ctx); + const validatedInput = validateInput(FollowPlaylistInputSchema, input, 'input'); + const userId = ctx.userId!; + + // Verify playlist exists and is public + const [playlist] = await db + .select({ + uuid: dbSchema.playlists.uuid, + isPublic: dbSchema.playlists.isPublic, + }) + .from(dbSchema.playlists) + .where(eq(dbSchema.playlists.uuid, validatedInput.playlistUuid)) + .limit(1); + + if (!playlist) { + throw new Error('Playlist not found'); + } + if (!playlist.isPublic) { + throw new Error('Cannot follow a private playlist'); + } + + await db + .insert(dbSchema.playlistFollows) + .values({ + followerId: userId, + playlistUuid: validatedInput.playlistUuid, + }) + .onConflictDoNothing(); + + return true; + }, + + /** + * Unfollow a playlist. + */ + unfollowPlaylist: async ( + _: unknown, + { input }: { input: { playlistUuid: string } }, + ctx: ConnectionContext, + ): Promise => { + requireAuthenticated(ctx); + const validatedInput = validateInput(FollowPlaylistInputSchema, input, 'input'); + const userId = ctx.userId!; + + await db + .delete(dbSchema.playlistFollows) + .where( + and( + eq(dbSchema.playlistFollows.followerId, userId), + eq(dbSchema.playlistFollows.playlistUuid, validatedInput.playlistUuid) + ) + ); + + return true; + }, }; diff --git a/packages/backend/src/graphql/resolvers/playlists/queries.ts b/packages/backend/src/graphql/resolvers/playlists/queries.ts index 2d29a700..cfc225b3 100644 --- a/packages/backend/src/graphql/resolvers/playlists/queries.ts +++ b/packages/backend/src/graphql/resolvers/playlists/queries.ts @@ -17,6 +17,57 @@ import { getBoardTables, isValidBoardName } from '../../../db/queries/util/table import { getSizeEdges } from '../../../db/queries/util/product-sizes-data'; import { convertLitUpHoldsStringToMap } from '../../../db/queries/util/hold-state'; +/** + * Batch-fetch followerCount and isFollowedByMe for a list of playlist UUIDs. + * Returns a Map keyed by playlist UUID. + */ +export async function getPlaylistFollowStats( + playlistUuids: string[], + currentUserId: string | null, +): Promise> { + const result = new Map(); + + if (playlistUuids.length === 0) return result; + + // Follower counts + const followerCounts = await db + .select({ + playlistUuid: dbSchema.playlistFollows.playlistUuid, + count: sql`count(*)::int`, + }) + .from(dbSchema.playlistFollows) + .where(inArray(dbSchema.playlistFollows.playlistUuid, playlistUuids)) + .groupBy(dbSchema.playlistFollows.playlistUuid); + + const countMap = new Map(followerCounts.map(r => [r.playlistUuid, r.count])); + + // Is-followed-by-me check (only if authenticated) + const followedSet = new Set(); + if (currentUserId) { + const followed = await db + .select({ playlistUuid: dbSchema.playlistFollows.playlistUuid }) + .from(dbSchema.playlistFollows) + .where( + and( + eq(dbSchema.playlistFollows.followerId, currentUserId), + inArray(dbSchema.playlistFollows.playlistUuid, playlistUuids), + ) + ); + for (const r of followed) { + followedSet.add(r.playlistUuid); + } + } + + for (const uuid of playlistUuids) { + result.set(uuid, { + followerCount: countMap.get(uuid) ?? 0, + isFollowedByMe: followedSet.has(uuid), + }); + } + + return result; +} + export const playlistQueries = { /** * Get all playlists owned by the authenticated user for a specific board and layout @@ -85,22 +136,33 @@ export const playlistQueries = { const countMap = new Map(climbCounts.map(c => [c.playlistId.toString(), c.count])); - return userPlaylists.map(p => ({ - id: p.id.toString(), - uuid: p.uuid, - boardType: p.boardType, - layoutId: p.layoutId, - name: p.name, - description: p.description, - isPublic: p.isPublic, - color: p.color, - icon: p.icon, - createdAt: p.createdAt.toISOString(), - updatedAt: p.updatedAt.toISOString(), - lastAccessedAt: p.lastAccessedAt?.toISOString() ?? null, - climbCount: countMap.get(p.id.toString()) || 0, - userRole: p.role, - })); + // Fetch follow stats for all playlists + const followStats = await getPlaylistFollowStats( + userPlaylists.map(p => p.uuid), + userId, + ); + + return userPlaylists.map(p => { + const stats = followStats.get(p.uuid) ?? { followerCount: 0, isFollowedByMe: false }; + return { + id: p.id.toString(), + uuid: p.uuid, + boardType: p.boardType, + layoutId: p.layoutId, + name: p.name, + description: p.description, + isPublic: p.isPublic, + color: p.color, + icon: p.icon, + createdAt: p.createdAt.toISOString(), + updatedAt: p.updatedAt.toISOString(), + lastAccessedAt: p.lastAccessedAt?.toISOString() ?? null, + climbCount: countMap.get(p.id.toString()) || 0, + userRole: p.role, + followerCount: stats.followerCount, + isFollowedByMe: stats.isFollowedByMe, + }; + }); }, /** @@ -165,22 +227,33 @@ export const playlistQueries = { const countMap = new Map(climbCounts.map(c => [c.playlistId.toString(), c.count])); - return userPlaylists.map(p => ({ - id: p.id.toString(), - uuid: p.uuid, - boardType: p.boardType, - layoutId: p.layoutId, - name: p.name, - description: p.description, - isPublic: p.isPublic, - color: p.color, - icon: p.icon, - createdAt: p.createdAt.toISOString(), - updatedAt: p.updatedAt.toISOString(), - lastAccessedAt: p.lastAccessedAt?.toISOString() ?? null, - climbCount: countMap.get(p.id.toString()) || 0, - userRole: p.role, - })); + // Fetch follow stats for all playlists + const followStats = await getPlaylistFollowStats( + userPlaylists.map(p => p.uuid), + userId, + ); + + return userPlaylists.map(p => { + const stats = followStats.get(p.uuid) ?? { followerCount: 0, isFollowedByMe: false }; + return { + id: p.id.toString(), + uuid: p.uuid, + boardType: p.boardType, + layoutId: p.layoutId, + name: p.name, + description: p.description, + isPublic: p.isPublic, + color: p.color, + icon: p.icon, + createdAt: p.createdAt.toISOString(), + updatedAt: p.updatedAt.toISOString(), + lastAccessedAt: p.lastAccessedAt?.toISOString() ?? null, + climbCount: countMap.get(p.id.toString()) || 0, + userRole: p.role, + followerCount: stats.followerCount, + isFollowedByMe: stats.isFollowedByMe, + }; + }); }, /** @@ -249,6 +322,10 @@ export const playlistQueries = { .where(eq(dbSchema.playlistClimbs.playlistId, playlist.id)) .limit(1); + // Get follow stats + const followStats = await getPlaylistFollowStats([playlist.uuid], userId ?? null); + const stats = followStats.get(playlist.uuid) ?? { followerCount: 0, isFollowedByMe: false }; + return { id: playlist.id.toString(), uuid: playlist.uuid, @@ -264,6 +341,8 @@ export const playlistQueries = { lastAccessedAt: playlist.lastAccessedAt?.toISOString() ?? null, climbCount: climbCount[0]?.count || 0, userRole, + followerCount: stats.followerCount, + isFollowedByMe: stats.isFollowedByMe, }; }, @@ -310,17 +389,20 @@ export const playlistQueries = { }, /** - * Get climbs in a playlist with full climb data + * Get climbs in a playlist with full climb data. + * Supports two modes: + * - Specific-board mode (when boardName is provided): filters by board type, layout, and size edges + * - All-boards mode (when boardName is omitted): returns climbs across all board types */ playlistClimbs: async ( _: unknown, { input }: { input: { playlistId: string; - boardName: string; - layoutId: number; - sizeId: number; - setIds: string; - angle: number; + boardName?: string; + layoutId?: number; + sizeId?: number; + setIds?: string; + angle?: number; page?: number; pageSize?: number; } }, @@ -329,19 +411,6 @@ export const playlistQueries = { validateInput(GetPlaylistClimbsInputSchema, input, 'input'); const userId = ctx.userId; - const boardName = input.boardName as BoardName; - - // Validate board name - if (!isValidBoardName(boardName)) { - throw new Error(`Invalid board name: ${boardName}. Must be one of: ${SUPPORTED_BOARDS.join(', ')}`); - } - - // Get size edges for filtering - const sizeEdges = getSizeEdges(boardName, input.sizeId); - if (!sizeEdges) { - throw new Error(`Invalid size ID: ${input.sizeId} for board: ${boardName}`); - } - const page = input.page ?? 0; const pageSize = input.pageSize ?? 20; @@ -382,7 +451,6 @@ export const playlistQueries = { } const playlistId = playlistResult[0].id; - const tables = getBoardTables(boardName); // Get total count of climbs in the playlist const countResult = await db @@ -392,86 +460,188 @@ export const playlistQueries = { const totalCount = countResult[0]?.count || 0; - // Get playlist climbs with full climb data - // Note: We use a subquery approach to avoid duplicates when joining stats - // The issue: when playlistClimbs.angle is NULL, directly using COALESCE causes cartesian product - const results = await db - .select({ - // Playlist climb data - climbUuid: dbSchema.playlistClimbs.climbUuid, - angle: dbSchema.playlistClimbs.angle, - position: dbSchema.playlistClimbs.position, - // Climb data - uuid: tables.climbs.uuid, - layoutId: tables.climbs.layoutId, - setter_username: tables.climbs.setterUsername, - name: tables.climbs.name, - description: tables.climbs.description, - frames: tables.climbs.frames, - // Stats data - use the angle from playlist if available, otherwise use climb's default angle - ascensionist_count: tables.climbStats.ascensionistCount, - difficulty: tables.difficultyGrades.boulderName, - quality_average: sql`ROUND(${tables.climbStats.qualityAverage}::numeric, 2)`, - difficulty_error: sql`ROUND(${tables.climbStats.difficultyAverage}::numeric - ${tables.climbStats.displayDifficulty}::numeric, 2)`, - benchmark_difficulty: tables.climbStats.benchmarkDifficulty, - }) - .from(dbSchema.playlistClimbs) - .innerJoin( - tables.climbs, - and( - eq(tables.climbs.uuid, dbSchema.playlistClimbs.climbUuid), - eq(tables.climbs.boardType, boardName) - ) - ) - .leftJoin( - tables.climbStats, - and( - eq(tables.climbStats.climbUuid, dbSchema.playlistClimbs.climbUuid), - eq(tables.climbStats.boardType, boardName), - // Use the route angle (from input) to fetch stats for the current board angle - eq(tables.climbStats.angle, input.angle) - ) - ) - .leftJoin( - tables.difficultyGrades, - and( - eq(tables.difficultyGrades.difficulty, sql`ROUND(${tables.climbStats.displayDifficulty}::numeric)`), - eq(tables.difficultyGrades.boardType, boardName) - ) - ) - .where(eq(dbSchema.playlistClimbs.playlistId, playlistId)) - .orderBy(asc(dbSchema.playlistClimbs.position), asc(dbSchema.playlistClimbs.addedAt)) - .limit(pageSize + 1) - .offset(page * pageSize); + if (input.boardName) { + // === Specific-board mode === + const boardName = input.boardName as BoardName; + if (!isValidBoardName(boardName)) { + throw new Error(`Invalid board name: ${boardName}. Must be one of: ${SUPPORTED_BOARDS.join(', ')}`); + } - // Check if there are more results available - const hasMore = results.length > pageSize; - const trimmedResults = hasMore ? results.slice(0, pageSize) : results; + const tables = getBoardTables(boardName); - // Transform results to Climb type - // Use the input angle (route angle) for consistent stats display - const climbs: Climb[] = trimmedResults.map((result) => ({ - uuid: result.uuid || result.climbUuid, - layoutId: result.layoutId, - setter_username: result.setter_username || '', - name: result.name || '', - description: result.description || '', - frames: result.frames || '', - angle: input.angle, - ascensionist_count: Number(result.ascensionist_count || 0), - difficulty: result.difficulty || '', - quality_average: result.quality_average?.toString() || '0', - stars: Math.round((Number(result.quality_average) || 0) * 5), - difficulty_error: result.difficulty_error?.toString() || '0', - benchmark_difficulty: result.benchmark_difficulty && result.benchmark_difficulty > 0 ? result.benchmark_difficulty.toString() : null, - litUpHoldsMap: convertLitUpHoldsStringToMap(result.frames || '', boardName)[0], - })); + // Build climb join conditions + const climbJoinConditions = [ + eq(tables.climbs.uuid, dbSchema.playlistClimbs.climbUuid), + eq(tables.climbs.boardType, boardName), + ]; - return { - climbs, - totalCount, - hasMore, - }; + // Filter by layout if provided + if (input.layoutId != null) { + climbJoinConditions.push(eq(tables.climbs.layoutId, input.layoutId)); + } + + // Filter by size edges if sizeId is provided + if (input.sizeId != null) { + const sizeEdges = getSizeEdges(boardName, input.sizeId); + if (sizeEdges && boardName !== 'moonboard') { + climbJoinConditions.push( + sql`${tables.climbs.edgeLeft} > ${sizeEdges.edgeLeft}`, + sql`${tables.climbs.edgeRight} < ${sizeEdges.edgeRight}`, + sql`${tables.climbs.edgeBottom} > ${sizeEdges.edgeBottom}`, + sql`${tables.climbs.edgeTop} < ${sizeEdges.edgeTop}`, + ); + } + } + + const inputAngle = input.angle ?? 40; + + const results = await db + .select({ + climbUuid: dbSchema.playlistClimbs.climbUuid, + playlistAngle: dbSchema.playlistClimbs.angle, + position: dbSchema.playlistClimbs.position, + uuid: tables.climbs.uuid, + layoutId: tables.climbs.layoutId, + setter_username: tables.climbs.setterUsername, + name: tables.climbs.name, + description: tables.climbs.description, + frames: tables.climbs.frames, + ascensionist_count: tables.climbStats.ascensionistCount, + difficulty: tables.difficultyGrades.boulderName, + quality_average: sql`ROUND(${tables.climbStats.qualityAverage}::numeric, 2)`, + difficulty_error: sql`ROUND(${tables.climbStats.difficultyAverage}::numeric - ${tables.climbStats.displayDifficulty}::numeric, 2)`, + benchmark_difficulty: tables.climbStats.benchmarkDifficulty, + }) + .from(dbSchema.playlistClimbs) + .innerJoin( + tables.climbs, + and(...climbJoinConditions) + ) + .leftJoin( + tables.climbStats, + and( + eq(tables.climbStats.climbUuid, dbSchema.playlistClimbs.climbUuid), + eq(tables.climbStats.boardType, boardName), + eq(tables.climbStats.angle, inputAngle) + ) + ) + .leftJoin( + tables.difficultyGrades, + and( + eq(tables.difficultyGrades.difficulty, sql`ROUND(${tables.climbStats.displayDifficulty}::numeric)`), + eq(tables.difficultyGrades.boardType, boardName) + ) + ) + .where(eq(dbSchema.playlistClimbs.playlistId, playlistId)) + .orderBy(asc(dbSchema.playlistClimbs.position), asc(dbSchema.playlistClimbs.addedAt)) + .limit(pageSize + 1) + .offset(page * pageSize); + + const hasMore = results.length > pageSize; + const trimmedResults = hasMore ? results.slice(0, pageSize) : results; + + const climbs: Climb[] = trimmedResults.map((result) => ({ + uuid: result.uuid || result.climbUuid, + layoutId: result.layoutId, + setter_username: result.setter_username || '', + name: result.name || '', + description: result.description || '', + frames: result.frames || '', + angle: inputAngle, + ascensionist_count: Number(result.ascensionist_count || 0), + difficulty: result.difficulty || '', + quality_average: result.quality_average?.toString() || '0', + stars: Math.round((Number(result.quality_average) || 0) * 5), + difficulty_error: result.difficulty_error?.toString() || '0', + benchmark_difficulty: result.benchmark_difficulty && result.benchmark_difficulty > 0 ? result.benchmark_difficulty.toString() : null, + litUpHoldsMap: convertLitUpHoldsStringToMap(result.frames || '', boardName)[0], + boardType: boardName, + })); + + return { climbs, totalCount, hasMore }; + } else { + // === All-boards mode === + // Query across all board types, following the setterClimbsFull all-boards pattern + const DEFAULT_ANGLE = 40; + const tables = getBoardTables('kilter'); // All unified — just need table refs + + const results = await db + .select({ + climbUuid: dbSchema.playlistClimbs.climbUuid, + position: dbSchema.playlistClimbs.position, + playlistAngle: dbSchema.playlistClimbs.angle, + uuid: tables.climbs.uuid, + layoutId: tables.climbs.layoutId, + boardType: tables.climbs.boardType, + setter_username: tables.climbs.setterUsername, + name: tables.climbs.name, + description: tables.climbs.description, + frames: tables.climbs.frames, + statsAngle: tables.climbStats.angle, + ascensionist_count: tables.climbStats.ascensionistCount, + difficulty: tables.difficultyGrades.boulderName, + quality_average: sql`ROUND(${tables.climbStats.qualityAverage}::numeric, 2)`, + difficulty_error: sql`ROUND(${tables.climbStats.difficultyAverage}::numeric - ${tables.climbStats.displayDifficulty}::numeric, 2)`, + benchmark_difficulty: tables.climbStats.benchmarkDifficulty, + }) + .from(dbSchema.playlistClimbs) + .innerJoin( + tables.climbs, + eq(tables.climbs.uuid, dbSchema.playlistClimbs.climbUuid) + // No boardType filter — join across all boards + ) + .leftJoin( + tables.climbStats, + and( + eq(tables.climbStats.boardType, tables.climbs.boardType), + eq(tables.climbStats.climbUuid, tables.climbs.uuid), + eq(tables.climbStats.angle, sql`( + SELECT s.angle FROM board_climb_stats s + WHERE s.board_type = ${tables.climbs.boardType} + AND s.climb_uuid = ${tables.climbs.uuid} + ORDER BY s.ascensionist_count DESC NULLS LAST + LIMIT 1 + )`) + ) + ) + .leftJoin( + tables.difficultyGrades, + and( + eq(tables.difficultyGrades.boardType, tables.climbs.boardType), + eq(tables.difficultyGrades.difficulty, sql`CAST(${tables.climbStats.displayDifficulty} AS INTEGER)`) + ) + ) + .where(eq(dbSchema.playlistClimbs.playlistId, playlistId)) + .orderBy(asc(dbSchema.playlistClimbs.position), asc(dbSchema.playlistClimbs.addedAt)) + .limit(pageSize + 1) + .offset(page * pageSize); + + const hasMore = results.length > pageSize; + const trimmedResults = hasMore ? results.slice(0, pageSize) : results; + + const climbs: Climb[] = trimmedResults.map((result) => { + const bt = (result.boardType || 'kilter') as BoardName; + return { + uuid: result.uuid || result.climbUuid, + layoutId: result.layoutId, + setter_username: result.setter_username || '', + name: result.name || '', + description: result.description || '', + frames: result.frames || '', + angle: result.playlistAngle ?? result.statsAngle ?? DEFAULT_ANGLE, + ascensionist_count: Number(result.ascensionist_count || 0), + difficulty: result.difficulty || '', + quality_average: result.quality_average?.toString() || '0', + stars: Math.round((Number(result.quality_average) || 0) * 5), + difficulty_error: result.difficulty_error?.toString() || '0', + benchmark_difficulty: result.benchmark_difficulty && result.benchmark_difficulty > 0 ? result.benchmark_difficulty.toString() : null, + litUpHoldsMap: convertLitUpHoldsStringToMap(result.frames || '', bt)[0], + boardType: bt, + }; + }); + + return { climbs, totalCount, hasMore }; + } }, /** diff --git a/packages/backend/src/graphql/resolvers/social/setter-follows.ts b/packages/backend/src/graphql/resolvers/social/setter-follows.ts index c4c16f47..cad615dc 100644 --- a/packages/backend/src/graphql/resolvers/social/setter-follows.ts +++ b/packages/backend/src/graphql/resolvers/social/setter-follows.ts @@ -219,18 +219,38 @@ export const setterFollowQueries = { } const angle = validatedInput.angle ?? DEFAULT_ANGLE; + const { layoutId, sizeId } = validatedInput; const tables = getBoardTables(boardName); + // Build WHERE conditions for board filter + const filterConditions: ReturnType[] = [ + eq(tables.climbs.setterUsername, username), + eq(tables.climbs.boardType, boardName), + ]; + + // Filter by layout if provided + if (layoutId != null) { + filterConditions.push(eq(tables.climbs.layoutId, layoutId)); + } + + // Filter by size edges if sizeId is provided + if (sizeId != null) { + const sizeEdges = getSizeEdges(boardName, sizeId); + if (sizeEdges && boardName !== 'moonboard') { + filterConditions.push( + sql`${tables.climbs.edgeLeft} > ${sizeEdges.edgeLeft}`, + sql`${tables.climbs.edgeRight} < ${sizeEdges.edgeRight}`, + sql`${tables.climbs.edgeBottom} > ${sizeEdges.edgeBottom}`, + sql`${tables.climbs.edgeTop} < ${sizeEdges.edgeTop}`, + ); + } + } + // Get total count const [countResult] = await db .select({ count: sql`count(*)::int` }) .from(tables.climbs) - .where( - and( - eq(tables.climbs.setterUsername, username), - eq(tables.climbs.boardType, boardName) - ) - ); + .where(and(...filterConditions)); const totalCount = Number(countResult?.count ?? 0); @@ -265,12 +285,7 @@ export const setterFollowQueries = { eq(tables.difficultyGrades.boardType, boardName) ) ) - .where( - and( - eq(tables.climbs.setterUsername, username), - eq(tables.climbs.boardType, boardName) - ) - ) + .where(and(...filterConditions)) .orderBy( sortBy === 'popular' ? sql`COALESCE(${tables.climbStats.ascensionistCount}, 0) DESC` diff --git a/packages/backend/src/validation/schemas.ts b/packages/backend/src/validation/schemas.ts index 10ed42cc..8de3592c 100644 --- a/packages/backend/src/validation/schemas.ts +++ b/packages/backend/src/validation/schemas.ts @@ -440,11 +440,11 @@ export const GetPlaylistsForClimbInputSchema = z.object({ export const GetPlaylistClimbsInputSchema = z.object({ playlistId: z.string().min(1), - boardName: BoardNameSchema, - layoutId: z.number().int().positive(), - sizeId: z.number().int().positive(), - setIds: z.string().min(1), - angle: z.number().int(), + boardName: BoardNameSchema.optional(), + layoutId: z.number().int().positive().optional(), + sizeId: z.number().int().positive().optional(), + setIds: z.string().min(1).optional(), + angle: z.number().int().optional(), page: z.number().int().min(0).optional(), pageSize: z.number().int().min(1).max(100).optional(), }); @@ -524,6 +524,13 @@ export const FollowSetterInputSchema = z.object({ setterUsername: z.string().min(1, 'Setter username cannot be empty').max(100), }); +/** + * Follow playlist input validation schema + */ +export const FollowPlaylistInputSchema = z.object({ + playlistUuid: z.string().min(1, 'Playlist UUID cannot be empty'), +}); + /** * Setter profile input validation schema */ diff --git a/packages/db/drizzle/0061_silky_cassandra_nova.sql b/packages/db/drizzle/0061_silky_cassandra_nova.sql new file mode 100644 index 00000000..df15ec84 --- /dev/null +++ b/packages/db/drizzle/0061_silky_cassandra_nova.sql @@ -0,0 +1,11 @@ +CREATE TABLE "playlist_follows" ( + "id" bigserial PRIMARY KEY NOT NULL, + "follower_id" text NOT NULL, + "playlist_uuid" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "playlist_follows" ADD CONSTRAINT "playlist_follows_follower_id_users_id_fk" FOREIGN KEY ("follower_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "unique_playlist_follow" ON "playlist_follows" USING btree ("follower_id","playlist_uuid");--> statement-breakpoint +CREATE INDEX "playlist_follows_follower_idx" ON "playlist_follows" USING btree ("follower_id");--> statement-breakpoint +CREATE INDEX "playlist_follows_playlist_idx" ON "playlist_follows" USING btree ("playlist_uuid"); \ No newline at end of file diff --git a/packages/db/drizzle/0062_add-playlist-follows-uuid-fk.sql b/packages/db/drizzle/0062_add-playlist-follows-uuid-fk.sql new file mode 100644 index 00000000..1043b6b1 --- /dev/null +++ b/packages/db/drizzle/0062_add-playlist-follows-uuid-fk.sql @@ -0,0 +1 @@ +ALTER TABLE "playlist_follows" ADD CONSTRAINT "playlist_follows_playlist_uuid_playlists_uuid_fk" FOREIGN KEY ("playlist_uuid") REFERENCES "public"."playlists"("uuid") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0061_snapshot.json b/packages/db/drizzle/meta/0061_snapshot.json new file mode 100644 index 00000000..2e8542c1 --- /dev/null +++ b/packages/db/drizzle/meta/0061_snapshot.json @@ -0,0 +1,7713 @@ +{ + "id": "155f288c-ffd4-4ec9-a927-67d56bea327c", + "prevId": "c51176dd-82ae-4626-9955-701e1c605e7d", + "version": "7", + "dialect": "postgresql", + "tables": { + "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": {}, + "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": {}, + "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 + }, + "user_id": { + "name": "user_id", + "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": {} + }, + "board_climbs_setter_username_idx": { + "name": "board_climbs_setter_username_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "setter_username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_climbs_user_id_users_id_fk": { + "name": "board_climbs_user_id_users_id_fk", + "tableFrom": "board_climbs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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.gym_follows": { + "name": "gym_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "gym_id": { + "name": "gym_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "gym_follows_unique_gym_user": { + "name": "gym_follows_unique_gym_user", + "columns": [ + { + "expression": "gym_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gym_follows_gym_id_gyms_id_fk": { + "name": "gym_follows_gym_id_gyms_id_fk", + "tableFrom": "gym_follows", + "tableTo": "gyms", + "columnsFrom": [ + "gym_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gym_follows_user_id_users_id_fk": { + "name": "gym_follows_user_id_users_id_fk", + "tableFrom": "gym_follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gym_members": { + "name": "gym_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "gym_id": { + "name": "gym_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "gym_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "gym_members_unique_gym_user": { + "name": "gym_members_unique_gym_user", + "columns": [ + { + "expression": "gym_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gym_members_gym_id_gyms_id_fk": { + "name": "gym_members_gym_id_gyms_id_fk", + "tableFrom": "gym_members", + "tableTo": "gyms", + "columnsFrom": [ + "gym_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gym_members_user_id_users_id_fk": { + "name": "gym_members_user_id_users_id_fk", + "tableFrom": "gym_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gyms": { + "name": "gyms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_phone": { + "name": "contact_phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "gyms_unique_slug": { + "name": "gyms_unique_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"gyms\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gyms_uuid_idx": { + "name": "gyms_uuid_idx", + "columns": [ + { + "expression": "uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gyms_owner_idx": { + "name": "gyms_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"gyms\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gyms_public_idx": { + "name": "gyms_public_idx", + "columns": [ + { + "expression": "is_public", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"gyms\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gyms_owner_id_users_id_fk": { + "name": "gyms_owner_id_users_id_fk", + "tableFrom": "gyms", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "gyms_uuid_unique": { + "name": "gyms_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_follows": { + "name": "board_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_uuid": { + "name": "board_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_follows_unique_user_board": { + "name": "board_follows_unique_user_board", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_follows_user_idx": { + "name": "board_follows_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_follows_board_uuid_idx": { + "name": "board_follows_board_uuid_idx", + "columns": [ + { + "expression": "board_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_follows_user_id_users_id_fk": { + "name": "board_follows_user_id_users_id_fk", + "tableFrom": "board_follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "board_follows_board_uuid_user_boards_uuid_fk": { + "name": "board_follows_board_uuid_user_boards_uuid_fk", + "tableFrom": "board_follows", + "tableTo": "user_boards", + "columnsFrom": [ + "board_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_boards": { + "name": "user_boards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "size_id": { + "name": "size_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "set_ids": { + "name": "set_ids", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location_name": { + "name": "location_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_owned": { + "name": "is_owned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "angle": { + "name": "angle", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 40 + }, + "is_angle_adjustable": { + "name": "is_angle_adjustable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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()" + }, + "gym_id": { + "name": "gym_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_boards_gym_idx": { + "name": "user_boards_gym_idx", + "columns": [ + { + "expression": "gym_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_unique_owner_config": { + "name": "user_boards_unique_owner_config", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "size_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "set_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_boards\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_owner_owned_idx": { + "name": "user_boards_owner_owned_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_owned", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_boards\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_public_idx": { + "name": "user_boards_public_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_public", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_boards\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_unique_slug": { + "name": "user_boards_unique_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_boards\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_uuid_idx": { + "name": "user_boards_uuid_idx", + "columns": [ + { + "expression": "uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_boards_owner_id_users_id_fk": { + "name": "user_boards_owner_id_users_id_fk", + "tableFrom": "user_boards", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_boards_gym_id_gyms_id_fk": { + "name": "user_boards_gym_id_gyms_id_fk", + "tableFrom": "user_boards", + "tableTo": "gyms", + "columnsFrom": [ + "gym_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_boards_uuid_unique": { + "name": "user_boards_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "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 + }, + "board_id": { + "name": "board_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "goal": { + "name": "goal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_permanent": { + "name": "is_permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "color": { + "name": "color", + "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" + }, + "board_sessions_board_id_user_boards_id_fk": { + "name": "board_sessions_board_id_user_boards_id_fk", + "tableFrom": "board_sessions", + "tableTo": "user_boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_boards": { + "name": "session_boards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "session_boards_session_board_idx": { + "name": "session_boards_session_board_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_boards_session_idx": { + "name": "session_boards_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_boards_board_idx": { + "name": "session_boards_board_idx", + "columns": [ + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_boards_session_id_board_sessions_id_fk": { + "name": "session_boards_session_id_board_sessions_id_fk", + "tableFrom": "session_boards", + "tableTo": "board_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_boards_board_id_user_boards_id_fk": { + "name": "session_boards_board_id_user_boards_id_fk", + "tableFrom": "session_boards", + "tableTo": "user_boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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.inferred_sessions": { + "name": "inferred_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_tick_at": { + "name": "first_tick_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_sends": { + "name": "total_sends", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_attempts": { + "name": "total_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_flashes": { + "name": "total_flashes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tick_count": { + "name": "tick_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inferred_sessions_user_idx": { + "name": "inferred_sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inferred_sessions_user_last_tick_idx": { + "name": "inferred_sessions_user_last_tick_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_tick_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inferred_sessions_last_tick_idx": { + "name": "inferred_sessions_last_tick_idx", + "columns": [ + { + "expression": "last_tick_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inferred_sessions_user_id_users_id_fk": { + "name": "inferred_sessions_user_id_users_id_fk", + "tableFrom": "inferred_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_member_overrides": { + "name": "session_member_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_member_overrides_session_idx": { + "name": "session_member_overrides_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_member_overrides_user_idx": { + "name": "session_member_overrides_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_member_overrides_session_id_inferred_sessions_id_fk": { + "name": "session_member_overrides_session_id_inferred_sessions_id_fk", + "tableFrom": "session_member_overrides", + "tableTo": "inferred_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_member_overrides_user_id_users_id_fk": { + "name": "session_member_overrides_user_id_users_id_fk", + "tableFrom": "session_member_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_member_overrides_added_by_user_id_users_id_fk": { + "name": "session_member_overrides_added_by_user_id_users_id_fk", + "tableFrom": "session_member_overrides", + "tableTo": "users", + "columnsFrom": [ + "added_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_member_overrides_session_user_unique": { + "name": "session_member_overrides_session_user_unique", + "nullsNotDistinct": false, + "columns": [ + "session_id", + "user_id" + ] + } + }, + "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 + }, + "inferred_session_id": { + "name": "inferred_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_inferred_session_id": { + "name": "previous_inferred_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_id": { + "name": "board_id", + "type": "bigint", + "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_inferred_session_idx": { + "name": "boardsesh_ticks_inferred_session_idx", + "columns": [ + { + "expression": "inferred_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": {} + }, + "boardsesh_ticks_board_climbed_at_idx": { + "name": "boardsesh_ticks_board_climbed_at_idx", + "columns": [ + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "climbed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "boardsesh_ticks_board_user_idx": { + "name": "boardsesh_ticks_board_user_idx", + "columns": [ + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "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" + }, + "boardsesh_ticks_inferred_session_id_inferred_sessions_id_fk": { + "name": "boardsesh_ticks_inferred_session_id_inferred_sessions_id_fk", + "tableFrom": "boardsesh_ticks", + "tableTo": "inferred_sessions", + "columnsFrom": [ + "inferred_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "boardsesh_ticks_previous_inferred_session_id_inferred_sessions_id_fk": { + "name": "boardsesh_ticks_previous_inferred_session_id_inferred_sessions_id_fk", + "tableFrom": "boardsesh_ticks", + "tableTo": "inferred_sessions", + "columnsFrom": [ + "previous_inferred_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "boardsesh_ticks_board_id_user_boards_id_fk": { + "name": "boardsesh_ticks_board_id_user_boards_id_fk", + "tableFrom": "boardsesh_ticks", + "tableTo": "user_boards", + "columnsFrom": [ + "board_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()" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "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_last_accessed_at_idx": { + "name": "playlists_last_accessed_at_idx", + "columns": [ + { + "expression": "last_accessed_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 + }, + "public.user_hold_classifications": { + "name": "user_hold_classifications", + "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 + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "size_id": { + "name": "size_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hold_type": { + "name": "hold_type", + "type": "hold_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "hand_rating": { + "name": "hand_rating", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "foot_rating": { + "name": "foot_rating", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pull_direction": { + "name": "pull_direction", + "type": "integer", + "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": { + "user_hold_classifications_user_board_idx": { + "name": "user_hold_classifications_user_board_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "size_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_hold_classifications_unique_idx": { + "name": "user_hold_classifications_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "size_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_hold_classifications_hold_idx": { + "name": "user_hold_classifications_hold_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_hold_classifications_user_id_users_id_fk": { + "name": "user_hold_classifications_user_id_users_id_fk", + "tableFrom": "user_hold_classifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.esp32_controllers": { + "name": "esp32_controllers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "board_name": { + "name": "board_name", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "size_id": { + "name": "size_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "set_ids": { + "name": "set_ids", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "authorized_session_id": { + "name": "authorized_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "esp32_controllers_user_idx": { + "name": "esp32_controllers_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp32_controllers_api_key_idx": { + "name": "esp32_controllers_api_key_idx", + "columns": [ + { + "expression": "api_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp32_controllers_session_idx": { + "name": "esp32_controllers_session_idx", + "columns": [ + { + "expression": "authorized_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp32_controllers_user_id_users_id_fk": { + "name": "esp32_controllers_user_id_users_id_fk", + "tableFrom": "esp32_controllers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp32_controllers_api_key_unique": { + "name": "esp32_controllers_api_key_unique", + "nullsNotDistinct": false, + "columns": [ + "api_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlist_follows": { + "name": "playlist_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "playlist_uuid": { + "name": "playlist_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_playlist_follow": { + "name": "unique_playlist_follow", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "playlist_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlist_follows_follower_idx": { + "name": "playlist_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlist_follows_playlist_idx": { + "name": "playlist_follows_playlist_idx", + "columns": [ + { + "expression": "playlist_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "playlist_follows_follower_id_users_id_fk": { + "name": "playlist_follows_follower_id_users_id_fk", + "tableFrom": "playlist_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.setter_follows": { + "name": "setter_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setter_username": { + "name": "setter_username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_setter_follow": { + "name": "unique_setter_follow", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "setter_username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "setter_follows_follower_idx": { + "name": "setter_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "setter_follows_setter_idx": { + "name": "setter_follows_setter_idx", + "columns": [ + { + "expression": "setter_username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "setter_follows_follower_id_users_id_fk": { + "name": "setter_follows_follower_id_users_id_fk", + "tableFrom": "setter_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_follows": { + "name": "user_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_user_follow": { + "name": "unique_user_follow", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_follows_follower_idx": { + "name": "user_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_follows_following_idx": { + "name": "user_follows_following_idx", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_follows_follower_id_users_id_fk": { + "name": "user_follows_follower_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_follows_following_id_users_id_fk": { + "name": "user_follows_following_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "no_self_follow": { + "name": "no_self_follow", + "value": "\"user_follows\".\"follower_id\" != \"user_follows\".\"following_id\"" + } + }, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "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 + }, + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "comments_entity_created_at_idx": { + "name": "comments_entity_created_at_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comments_user_created_at_idx": { + "name": "comments_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comments_parent_comment_idx": { + "name": "comments_parent_comment_idx", + "columns": [ + { + "expression": "parent_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "comments_uuid_unique": { + "name": "comments_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.votes": { + "name": "votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "votes_unique_user_entity": { + "name": "votes_unique_user_entity", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "votes_entity_idx": { + "name": "votes_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "votes_user_idx": { + "name": "votes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "votes_user_id_users_id_fk": { + "name": "votes_user_id_users_id_fk", + "tableFrom": "votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "vote_value_check": { + "name": "vote_value_check", + "value": "\"votes\".\"value\" IN (1, -1)" + } + }, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_recipient_unread_idx": { + "name": "notifications_recipient_unread_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_recipient_created_at_idx": { + "name": "notifications_recipient_created_at_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_dedup_idx": { + "name": "notifications_dedup_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_at_idx": { + "name": "notifications_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_recipient_id_users_id_fk": { + "name": "notifications_recipient_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "recipient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_actor_id_users_id_fk": { + "name": "notifications_actor_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "notifications_comment_id_comments_id_fk": { + "name": "notifications_comment_id_comments_id_fk", + "tableFrom": "notifications", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notifications_uuid_unique": { + "name": "notifications_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feed_items": { + "name": "feed_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "feed_item_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_uuid": { + "name": "board_uuid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feed_items_recipient_created_at_idx": { + "name": "feed_items_recipient_created_at_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_items_recipient_board_created_at_idx": { + "name": "feed_items_recipient_board_created_at_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_items_actor_created_at_idx": { + "name": "feed_items_actor_created_at_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_items_created_at_idx": { + "name": "feed_items_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feed_items_recipient_id_users_id_fk": { + "name": "feed_items_recipient_id_users_id_fk", + "tableFrom": "feed_items", + "tableTo": "users", + "columnsFrom": [ + "recipient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feed_items_actor_id_users_id_fk": { + "name": "feed_items_actor_id_users_id_fk", + "tableFrom": "feed_items", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.climb_classic_status": { + "name": "climb_classic_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_classic": { + "name": "is_classic", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_proposal_id": { + "name": "last_proposal_id", + "type": "bigserial", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "climb_classic_status_unique_idx": { + "name": "climb_classic_status_unique_idx", + "columns": [ + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "climb_classic_status_last_proposal_id_climb_proposals_id_fk": { + "name": "climb_classic_status_last_proposal_id_climb_proposals_id_fk", + "tableFrom": "climb_classic_status", + "tableTo": "climb_proposals", + "columnsFrom": [ + "last_proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.climb_community_status": { + "name": "climb_community_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "community_grade": { + "name": "community_grade", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_benchmark": { + "name": "is_benchmark", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_proposal_id": { + "name": "last_proposal_id", + "type": "bigserial", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "climb_community_status_unique_idx": { + "name": "climb_community_status_unique_idx", + "columns": [ + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "angle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "climb_community_status_last_proposal_id_climb_proposals_id_fk": { + "name": "climb_community_status_last_proposal_id_climb_proposals_id_fk", + "tableFrom": "climb_community_status", + "tableTo": "climb_proposals", + "columnsFrom": [ + "last_proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.climb_proposals": { + "name": "climb_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "proposer_id": { + "name": "proposer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "proposal_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "proposed_value": { + "name": "proposed_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_value": { + "name": "current_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "proposal_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "climb_proposals_climb_angle_type_idx": { + "name": "climb_proposals_climb_angle_type_idx", + "columns": [ + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "angle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "climb_proposals_status_idx": { + "name": "climb_proposals_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "climb_proposals_proposer_idx": { + "name": "climb_proposals_proposer_idx", + "columns": [ + { + "expression": "proposer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "climb_proposals_board_type_idx": { + "name": "climb_proposals_board_type_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "climb_proposals_created_at_idx": { + "name": "climb_proposals_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "climb_proposals_proposer_id_users_id_fk": { + "name": "climb_proposals_proposer_id_users_id_fk", + "tableFrom": "climb_proposals", + "tableTo": "users", + "columnsFrom": [ + "proposer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "climb_proposals_resolved_by_users_id_fk": { + "name": "climb_proposals_resolved_by_users_id_fk", + "tableFrom": "climb_proposals", + "tableTo": "users", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "climb_proposals_uuid_unique": { + "name": "climb_proposals_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.community_roles": { + "name": "community_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "community_role_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "community_roles_board_type_idx": { + "name": "community_roles_board_type_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "community_roles_user_id_users_id_fk": { + "name": "community_roles_user_id_users_id_fk", + "tableFrom": "community_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "community_roles_granted_by_users_id_fk": { + "name": "community_roles_granted_by_users_id_fk", + "tableFrom": "community_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "community_roles_user_role_board_idx": { + "name": "community_roles_user_role_board_idx", + "nullsNotDistinct": true, + "columns": [ + "user_id", + "role", + "board_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.community_settings": { + "name": "community_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "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": { + "community_settings_scope_key_idx": { + "name": "community_settings_scope_key_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "community_settings_set_by_users_id_fk": { + "name": "community_settings_set_by_users_id_fk", + "tableFrom": "community_settings", + "tableTo": "users", + "columnsFrom": [ + "set_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proposal_votes": { + "name": "proposal_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "proposal_id": { + "name": "proposal_id", + "type": "bigserial", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "proposal_votes_unique_user_proposal": { + "name": "proposal_votes_unique_user_proposal", + "columns": [ + { + "expression": "proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "proposal_votes_proposal_idx": { + "name": "proposal_votes_proposal_idx", + "columns": [ + { + "expression": "proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proposal_votes_proposal_id_climb_proposals_id_fk": { + "name": "proposal_votes_proposal_id_climb_proposals_id_fk", + "tableFrom": "proposal_votes", + "tableTo": "climb_proposals", + "columnsFrom": [ + "proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proposal_votes_user_id_users_id_fk": { + "name": "proposal_votes_user_id_users_id_fk", + "tableFrom": "proposal_votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "proposal_vote_value_check": { + "name": "proposal_vote_value_check", + "value": "\"proposal_votes\".\"value\" IN (1, -1)" + } + }, + "isRLSEnabled": false + }, + "public.new_climb_subscriptions": { + "name": "new_climb_subscriptions", + "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 + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "new_climb_subscriptions_unique_user_board_layout": { + "name": "new_climb_subscriptions_unique_user_board_layout", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "new_climb_subscriptions_user_idx": { + "name": "new_climb_subscriptions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "new_climb_subscriptions_board_layout_idx": { + "name": "new_climb_subscriptions_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": {} + } + }, + "foreignKeys": { + "new_climb_subscriptions_user_id_users_id_fk": { + "name": "new_climb_subscriptions_user_id_users_id_fk", + "tableFrom": "new_climb_subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vote_counts": { + "name": "vote_counts", + "schema": "", + "columns": { + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upvotes": { + "name": "upvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "downvotes": { + "name": "downvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hot_score": { + "name": "hot_score", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vote_counts_score_idx": { + "name": "vote_counts_score_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vote_counts_hot_score_idx": { + "name": "vote_counts_hot_score_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hot_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "vote_counts_entity_type_entity_id_pk": { + "name": "vote_counts_entity_type_entity_id_pk", + "columns": [ + "entity_type", + "entity_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.gym_member_role": { + "name": "gym_member_role", + "schema": "public", + "values": [ + "admin", + "member" + ] + }, + "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" + ] + }, + "public.hold_type": { + "name": "hold_type", + "schema": "public", + "values": [ + "jug", + "sloper", + "pinch", + "crimp", + "pocket" + ] + }, + "public.social_entity_type": { + "name": "social_entity_type", + "schema": "public", + "values": [ + "playlist_climb", + "climb", + "tick", + "comment", + "proposal", + "board", + "gym", + "session" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "new_follower", + "comment_reply", + "comment_on_tick", + "comment_on_climb", + "vote_on_tick", + "vote_on_comment", + "new_climb", + "new_climb_global", + "proposal_approved", + "proposal_rejected", + "proposal_vote", + "proposal_created", + "new_climbs_synced" + ] + }, + "public.feed_item_type": { + "name": "feed_item_type", + "schema": "public", + "values": [ + "ascent", + "new_climb", + "comment", + "proposal_approved", + "session_summary" + ] + }, + "public.community_role_type": { + "name": "community_role_type", + "schema": "public", + "values": [ + "admin", + "community_leader" + ] + }, + "public.proposal_status": { + "name": "proposal_status", + "schema": "public", + "values": [ + "open", + "approved", + "rejected", + "superseded" + ] + }, + "public.proposal_type": { + "name": "proposal_type", + "schema": "public", + "values": [ + "grade", + "classic", + "benchmark" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/0062_snapshot.json b/packages/db/drizzle/meta/0062_snapshot.json new file mode 100644 index 00000000..5728cfd4 --- /dev/null +++ b/packages/db/drizzle/meta/0062_snapshot.json @@ -0,0 +1,7726 @@ +{ + "id": "2f644618-d1f9-4d9d-90e0-6be2d60eb9fc", + "prevId": "155f288c-ffd4-4ec9-a927-67d56bea327c", + "version": "7", + "dialect": "postgresql", + "tables": { + "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": {}, + "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": {}, + "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 + }, + "user_id": { + "name": "user_id", + "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": {} + }, + "board_climbs_setter_username_idx": { + "name": "board_climbs_setter_username_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "setter_username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_climbs_user_id_users_id_fk": { + "name": "board_climbs_user_id_users_id_fk", + "tableFrom": "board_climbs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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.gym_follows": { + "name": "gym_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "gym_id": { + "name": "gym_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "gym_follows_unique_gym_user": { + "name": "gym_follows_unique_gym_user", + "columns": [ + { + "expression": "gym_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gym_follows_gym_id_gyms_id_fk": { + "name": "gym_follows_gym_id_gyms_id_fk", + "tableFrom": "gym_follows", + "tableTo": "gyms", + "columnsFrom": [ + "gym_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gym_follows_user_id_users_id_fk": { + "name": "gym_follows_user_id_users_id_fk", + "tableFrom": "gym_follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gym_members": { + "name": "gym_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "gym_id": { + "name": "gym_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "gym_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "gym_members_unique_gym_user": { + "name": "gym_members_unique_gym_user", + "columns": [ + { + "expression": "gym_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gym_members_gym_id_gyms_id_fk": { + "name": "gym_members_gym_id_gyms_id_fk", + "tableFrom": "gym_members", + "tableTo": "gyms", + "columnsFrom": [ + "gym_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gym_members_user_id_users_id_fk": { + "name": "gym_members_user_id_users_id_fk", + "tableFrom": "gym_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gyms": { + "name": "gyms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_phone": { + "name": "contact_phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "gyms_unique_slug": { + "name": "gyms_unique_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"gyms\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gyms_uuid_idx": { + "name": "gyms_uuid_idx", + "columns": [ + { + "expression": "uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gyms_owner_idx": { + "name": "gyms_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"gyms\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gyms_public_idx": { + "name": "gyms_public_idx", + "columns": [ + { + "expression": "is_public", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"gyms\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gyms_owner_id_users_id_fk": { + "name": "gyms_owner_id_users_id_fk", + "tableFrom": "gyms", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "gyms_uuid_unique": { + "name": "gyms_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_follows": { + "name": "board_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_uuid": { + "name": "board_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_follows_unique_user_board": { + "name": "board_follows_unique_user_board", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_follows_user_idx": { + "name": "board_follows_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_follows_board_uuid_idx": { + "name": "board_follows_board_uuid_idx", + "columns": [ + { + "expression": "board_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_follows_user_id_users_id_fk": { + "name": "board_follows_user_id_users_id_fk", + "tableFrom": "board_follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "board_follows_board_uuid_user_boards_uuid_fk": { + "name": "board_follows_board_uuid_user_boards_uuid_fk", + "tableFrom": "board_follows", + "tableTo": "user_boards", + "columnsFrom": [ + "board_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_boards": { + "name": "user_boards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "size_id": { + "name": "size_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "set_ids": { + "name": "set_ids", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location_name": { + "name": "location_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_owned": { + "name": "is_owned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "angle": { + "name": "angle", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 40 + }, + "is_angle_adjustable": { + "name": "is_angle_adjustable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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()" + }, + "gym_id": { + "name": "gym_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_boards_gym_idx": { + "name": "user_boards_gym_idx", + "columns": [ + { + "expression": "gym_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_unique_owner_config": { + "name": "user_boards_unique_owner_config", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "size_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "set_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_boards\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_owner_owned_idx": { + "name": "user_boards_owner_owned_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_owned", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_boards\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_public_idx": { + "name": "user_boards_public_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_public", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_boards\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_unique_slug": { + "name": "user_boards_unique_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_boards\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_boards_uuid_idx": { + "name": "user_boards_uuid_idx", + "columns": [ + { + "expression": "uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_boards_owner_id_users_id_fk": { + "name": "user_boards_owner_id_users_id_fk", + "tableFrom": "user_boards", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_boards_gym_id_gyms_id_fk": { + "name": "user_boards_gym_id_gyms_id_fk", + "tableFrom": "user_boards", + "tableTo": "gyms", + "columnsFrom": [ + "gym_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_boards_uuid_unique": { + "name": "user_boards_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "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 + }, + "board_id": { + "name": "board_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "goal": { + "name": "goal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_permanent": { + "name": "is_permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "color": { + "name": "color", + "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" + }, + "board_sessions_board_id_user_boards_id_fk": { + "name": "board_sessions_board_id_user_boards_id_fk", + "tableFrom": "board_sessions", + "tableTo": "user_boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_boards": { + "name": "session_boards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_id": { + "name": "board_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "session_boards_session_board_idx": { + "name": "session_boards_session_board_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_boards_session_idx": { + "name": "session_boards_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_boards_board_idx": { + "name": "session_boards_board_idx", + "columns": [ + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_boards_session_id_board_sessions_id_fk": { + "name": "session_boards_session_id_board_sessions_id_fk", + "tableFrom": "session_boards", + "tableTo": "board_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_boards_board_id_user_boards_id_fk": { + "name": "session_boards_board_id_user_boards_id_fk", + "tableFrom": "session_boards", + "tableTo": "user_boards", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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.inferred_sessions": { + "name": "inferred_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_tick_at": { + "name": "first_tick_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_sends": { + "name": "total_sends", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_attempts": { + "name": "total_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_flashes": { + "name": "total_flashes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tick_count": { + "name": "tick_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inferred_sessions_user_idx": { + "name": "inferred_sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inferred_sessions_user_last_tick_idx": { + "name": "inferred_sessions_user_last_tick_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_tick_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inferred_sessions_last_tick_idx": { + "name": "inferred_sessions_last_tick_idx", + "columns": [ + { + "expression": "last_tick_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inferred_sessions_user_id_users_id_fk": { + "name": "inferred_sessions_user_id_users_id_fk", + "tableFrom": "inferred_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_member_overrides": { + "name": "session_member_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_member_overrides_session_idx": { + "name": "session_member_overrides_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_member_overrides_user_idx": { + "name": "session_member_overrides_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_member_overrides_session_id_inferred_sessions_id_fk": { + "name": "session_member_overrides_session_id_inferred_sessions_id_fk", + "tableFrom": "session_member_overrides", + "tableTo": "inferred_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_member_overrides_user_id_users_id_fk": { + "name": "session_member_overrides_user_id_users_id_fk", + "tableFrom": "session_member_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_member_overrides_added_by_user_id_users_id_fk": { + "name": "session_member_overrides_added_by_user_id_users_id_fk", + "tableFrom": "session_member_overrides", + "tableTo": "users", + "columnsFrom": [ + "added_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_member_overrides_session_user_unique": { + "name": "session_member_overrides_session_user_unique", + "nullsNotDistinct": false, + "columns": [ + "session_id", + "user_id" + ] + } + }, + "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 + }, + "inferred_session_id": { + "name": "inferred_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_inferred_session_id": { + "name": "previous_inferred_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_id": { + "name": "board_id", + "type": "bigint", + "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_inferred_session_idx": { + "name": "boardsesh_ticks_inferred_session_idx", + "columns": [ + { + "expression": "inferred_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": {} + }, + "boardsesh_ticks_board_climbed_at_idx": { + "name": "boardsesh_ticks_board_climbed_at_idx", + "columns": [ + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "climbed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "boardsesh_ticks_board_user_idx": { + "name": "boardsesh_ticks_board_user_idx", + "columns": [ + { + "expression": "board_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "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" + }, + "boardsesh_ticks_inferred_session_id_inferred_sessions_id_fk": { + "name": "boardsesh_ticks_inferred_session_id_inferred_sessions_id_fk", + "tableFrom": "boardsesh_ticks", + "tableTo": "inferred_sessions", + "columnsFrom": [ + "inferred_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "boardsesh_ticks_previous_inferred_session_id_inferred_sessions_id_fk": { + "name": "boardsesh_ticks_previous_inferred_session_id_inferred_sessions_id_fk", + "tableFrom": "boardsesh_ticks", + "tableTo": "inferred_sessions", + "columnsFrom": [ + "previous_inferred_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "boardsesh_ticks_board_id_user_boards_id_fk": { + "name": "boardsesh_ticks_board_id_user_boards_id_fk", + "tableFrom": "boardsesh_ticks", + "tableTo": "user_boards", + "columnsFrom": [ + "board_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()" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "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_last_accessed_at_idx": { + "name": "playlists_last_accessed_at_idx", + "columns": [ + { + "expression": "last_accessed_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 + }, + "public.user_hold_classifications": { + "name": "user_hold_classifications", + "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 + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "size_id": { + "name": "size_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hold_type": { + "name": "hold_type", + "type": "hold_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "hand_rating": { + "name": "hand_rating", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "foot_rating": { + "name": "foot_rating", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pull_direction": { + "name": "pull_direction", + "type": "integer", + "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": { + "user_hold_classifications_user_board_idx": { + "name": "user_hold_classifications_user_board_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "size_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_hold_classifications_unique_idx": { + "name": "user_hold_classifications_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "size_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_hold_classifications_hold_idx": { + "name": "user_hold_classifications_hold_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_hold_classifications_user_id_users_id_fk": { + "name": "user_hold_classifications_user_id_users_id_fk", + "tableFrom": "user_hold_classifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.esp32_controllers": { + "name": "esp32_controllers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "board_name": { + "name": "board_name", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "size_id": { + "name": "size_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "set_ids": { + "name": "set_ids", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "authorized_session_id": { + "name": "authorized_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "esp32_controllers_user_idx": { + "name": "esp32_controllers_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp32_controllers_api_key_idx": { + "name": "esp32_controllers_api_key_idx", + "columns": [ + { + "expression": "api_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp32_controllers_session_idx": { + "name": "esp32_controllers_session_idx", + "columns": [ + { + "expression": "authorized_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp32_controllers_user_id_users_id_fk": { + "name": "esp32_controllers_user_id_users_id_fk", + "tableFrom": "esp32_controllers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp32_controllers_api_key_unique": { + "name": "esp32_controllers_api_key_unique", + "nullsNotDistinct": false, + "columns": [ + "api_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playlist_follows": { + "name": "playlist_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "playlist_uuid": { + "name": "playlist_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_playlist_follow": { + "name": "unique_playlist_follow", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "playlist_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlist_follows_follower_idx": { + "name": "playlist_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "playlist_follows_playlist_idx": { + "name": "playlist_follows_playlist_idx", + "columns": [ + { + "expression": "playlist_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "playlist_follows_follower_id_users_id_fk": { + "name": "playlist_follows_follower_id_users_id_fk", + "tableFrom": "playlist_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playlist_follows_playlist_uuid_playlists_uuid_fk": { + "name": "playlist_follows_playlist_uuid_playlists_uuid_fk", + "tableFrom": "playlist_follows", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_uuid" + ], + "columnsTo": [ + "uuid" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.setter_follows": { + "name": "setter_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setter_username": { + "name": "setter_username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_setter_follow": { + "name": "unique_setter_follow", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "setter_username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "setter_follows_follower_idx": { + "name": "setter_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "setter_follows_setter_idx": { + "name": "setter_follows_setter_idx", + "columns": [ + { + "expression": "setter_username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "setter_follows_follower_id_users_id_fk": { + "name": "setter_follows_follower_id_users_id_fk", + "tableFrom": "setter_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_follows": { + "name": "user_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_user_follow": { + "name": "unique_user_follow", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_follows_follower_idx": { + "name": "user_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_follows_following_idx": { + "name": "user_follows_following_idx", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_follows_follower_id_users_id_fk": { + "name": "user_follows_follower_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_follows_following_id_users_id_fk": { + "name": "user_follows_following_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "no_self_follow": { + "name": "no_self_follow", + "value": "\"user_follows\".\"follower_id\" != \"user_follows\".\"following_id\"" + } + }, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "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 + }, + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "comments_entity_created_at_idx": { + "name": "comments_entity_created_at_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comments_user_created_at_idx": { + "name": "comments_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comments_parent_comment_idx": { + "name": "comments_parent_comment_idx", + "columns": [ + { + "expression": "parent_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "comments_uuid_unique": { + "name": "comments_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.votes": { + "name": "votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "votes_unique_user_entity": { + "name": "votes_unique_user_entity", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "votes_entity_idx": { + "name": "votes_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "votes_user_idx": { + "name": "votes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "votes_user_id_users_id_fk": { + "name": "votes_user_id_users_id_fk", + "tableFrom": "votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "vote_value_check": { + "name": "vote_value_check", + "value": "\"votes\".\"value\" IN (1, -1)" + } + }, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_recipient_unread_idx": { + "name": "notifications_recipient_unread_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_recipient_created_at_idx": { + "name": "notifications_recipient_created_at_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_dedup_idx": { + "name": "notifications_dedup_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_at_idx": { + "name": "notifications_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_recipient_id_users_id_fk": { + "name": "notifications_recipient_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "recipient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_actor_id_users_id_fk": { + "name": "notifications_actor_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "notifications_comment_id_comments_id_fk": { + "name": "notifications_comment_id_comments_id_fk", + "tableFrom": "notifications", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notifications_uuid_unique": { + "name": "notifications_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feed_items": { + "name": "feed_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "feed_item_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_uuid": { + "name": "board_uuid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feed_items_recipient_created_at_idx": { + "name": "feed_items_recipient_created_at_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_items_recipient_board_created_at_idx": { + "name": "feed_items_recipient_board_created_at_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_items_actor_created_at_idx": { + "name": "feed_items_actor_created_at_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feed_items_created_at_idx": { + "name": "feed_items_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feed_items_recipient_id_users_id_fk": { + "name": "feed_items_recipient_id_users_id_fk", + "tableFrom": "feed_items", + "tableTo": "users", + "columnsFrom": [ + "recipient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feed_items_actor_id_users_id_fk": { + "name": "feed_items_actor_id_users_id_fk", + "tableFrom": "feed_items", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.climb_classic_status": { + "name": "climb_classic_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_classic": { + "name": "is_classic", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_proposal_id": { + "name": "last_proposal_id", + "type": "bigserial", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "climb_classic_status_unique_idx": { + "name": "climb_classic_status_unique_idx", + "columns": [ + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "climb_classic_status_last_proposal_id_climb_proposals_id_fk": { + "name": "climb_classic_status_last_proposal_id_climb_proposals_id_fk", + "tableFrom": "climb_classic_status", + "tableTo": "climb_proposals", + "columnsFrom": [ + "last_proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.climb_community_status": { + "name": "climb_community_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "community_grade": { + "name": "community_grade", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_benchmark": { + "name": "is_benchmark", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_proposal_id": { + "name": "last_proposal_id", + "type": "bigserial", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "climb_community_status_unique_idx": { + "name": "climb_community_status_unique_idx", + "columns": [ + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "angle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "climb_community_status_last_proposal_id_climb_proposals_id_fk": { + "name": "climb_community_status_last_proposal_id_climb_proposals_id_fk", + "tableFrom": "climb_community_status", + "tableTo": "climb_proposals", + "columnsFrom": [ + "last_proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.climb_proposals": { + "name": "climb_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "climb_uuid": { + "name": "climb_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "angle": { + "name": "angle", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "proposer_id": { + "name": "proposer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "proposal_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "proposed_value": { + "name": "proposed_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_value": { + "name": "current_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "proposal_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "climb_proposals_climb_angle_type_idx": { + "name": "climb_proposals_climb_angle_type_idx", + "columns": [ + { + "expression": "climb_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "angle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "climb_proposals_status_idx": { + "name": "climb_proposals_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "climb_proposals_proposer_idx": { + "name": "climb_proposals_proposer_idx", + "columns": [ + { + "expression": "proposer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "climb_proposals_board_type_idx": { + "name": "climb_proposals_board_type_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "climb_proposals_created_at_idx": { + "name": "climb_proposals_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "climb_proposals_proposer_id_users_id_fk": { + "name": "climb_proposals_proposer_id_users_id_fk", + "tableFrom": "climb_proposals", + "tableTo": "users", + "columnsFrom": [ + "proposer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "climb_proposals_resolved_by_users_id_fk": { + "name": "climb_proposals_resolved_by_users_id_fk", + "tableFrom": "climb_proposals", + "tableTo": "users", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "climb_proposals_uuid_unique": { + "name": "climb_proposals_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.community_roles": { + "name": "community_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "community_role_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "board_type": { + "name": "board_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "community_roles_board_type_idx": { + "name": "community_roles_board_type_idx", + "columns": [ + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "community_roles_user_id_users_id_fk": { + "name": "community_roles_user_id_users_id_fk", + "tableFrom": "community_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "community_roles_granted_by_users_id_fk": { + "name": "community_roles_granted_by_users_id_fk", + "tableFrom": "community_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "community_roles_user_role_board_idx": { + "name": "community_roles_user_role_board_idx", + "nullsNotDistinct": true, + "columns": [ + "user_id", + "role", + "board_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.community_settings": { + "name": "community_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "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": { + "community_settings_scope_key_idx": { + "name": "community_settings_scope_key_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "community_settings_set_by_users_id_fk": { + "name": "community_settings_set_by_users_id_fk", + "tableFrom": "community_settings", + "tableTo": "users", + "columnsFrom": [ + "set_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.proposal_votes": { + "name": "proposal_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "proposal_id": { + "name": "proposal_id", + "type": "bigserial", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "proposal_votes_unique_user_proposal": { + "name": "proposal_votes_unique_user_proposal", + "columns": [ + { + "expression": "proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "proposal_votes_proposal_idx": { + "name": "proposal_votes_proposal_idx", + "columns": [ + { + "expression": "proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proposal_votes_proposal_id_climb_proposals_id_fk": { + "name": "proposal_votes_proposal_id_climb_proposals_id_fk", + "tableFrom": "proposal_votes", + "tableTo": "climb_proposals", + "columnsFrom": [ + "proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proposal_votes_user_id_users_id_fk": { + "name": "proposal_votes_user_id_users_id_fk", + "tableFrom": "proposal_votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "proposal_vote_value_check": { + "name": "proposal_vote_value_check", + "value": "\"proposal_votes\".\"value\" IN (1, -1)" + } + }, + "isRLSEnabled": false + }, + "public.new_climb_subscriptions": { + "name": "new_climb_subscriptions", + "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 + }, + "layout_id": { + "name": "layout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "new_climb_subscriptions_unique_user_board_layout": { + "name": "new_climb_subscriptions_unique_user_board_layout", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "board_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "new_climb_subscriptions_user_idx": { + "name": "new_climb_subscriptions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "new_climb_subscriptions_board_layout_idx": { + "name": "new_climb_subscriptions_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": {} + } + }, + "foreignKeys": { + "new_climb_subscriptions_user_id_users_id_fk": { + "name": "new_climb_subscriptions_user_id_users_id_fk", + "tableFrom": "new_climb_subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vote_counts": { + "name": "vote_counts", + "schema": "", + "columns": { + "entity_type": { + "name": "entity_type", + "type": "social_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upvotes": { + "name": "upvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "downvotes": { + "name": "downvotes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hot_score": { + "name": "hot_score", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vote_counts_score_idx": { + "name": "vote_counts_score_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vote_counts_hot_score_idx": { + "name": "vote_counts_hot_score_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hot_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "vote_counts_entity_type_entity_id_pk": { + "name": "vote_counts_entity_type_entity_id_pk", + "columns": [ + "entity_type", + "entity_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.gym_member_role": { + "name": "gym_member_role", + "schema": "public", + "values": [ + "admin", + "member" + ] + }, + "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" + ] + }, + "public.hold_type": { + "name": "hold_type", + "schema": "public", + "values": [ + "jug", + "sloper", + "pinch", + "crimp", + "pocket" + ] + }, + "public.social_entity_type": { + "name": "social_entity_type", + "schema": "public", + "values": [ + "playlist_climb", + "climb", + "tick", + "comment", + "proposal", + "board", + "gym", + "session" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "new_follower", + "comment_reply", + "comment_on_tick", + "comment_on_climb", + "vote_on_tick", + "vote_on_comment", + "new_climb", + "new_climb_global", + "proposal_approved", + "proposal_rejected", + "proposal_vote", + "proposal_created", + "new_climbs_synced" + ] + }, + "public.feed_item_type": { + "name": "feed_item_type", + "schema": "public", + "values": [ + "ascent", + "new_climb", + "comment", + "proposal_approved", + "session_summary" + ] + }, + "public.community_role_type": { + "name": "community_role_type", + "schema": "public", + "values": [ + "admin", + "community_leader" + ] + }, + "public.proposal_status": { + "name": "proposal_status", + "schema": "public", + "values": [ + "open", + "approved", + "rejected", + "superseded" + ] + }, + "public.proposal_type": { + "name": "proposal_type", + "schema": "public", + "values": [ + "grade", + "classic", + "benchmark" + ] + } + }, + "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 2aa1292e..75bab537 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -428,6 +428,20 @@ "when": 1772150000000, "tag": "0060_backfill_inferred_sessions", "breakpoints": true + }, + { + "idx": 61, + "version": "7", + "when": 1772364755993, + "tag": "0061_silky_cassandra_nova", + "breakpoints": true + }, + { + "idx": 62, + "version": "7", + "when": 1772368267377, + "tag": "0062_add-playlist-follows-uuid-fk", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/package.json b/packages/db/package.json index c5594d56..5a7605f6 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -62,10 +62,10 @@ "ws": "^8.18.3" }, "devDependencies": { + "@faker-js/faker": "^9.0.0", "@types/node": "^25", "@types/ws": "^8.18.1", - "dotenv": "^17.2.3", - "@faker-js/faker": "^9.0.0", + "dotenv": "^17.3.1", "drizzle-kit": "^0.31.8", "tsx": "^4.19.0", "typescript": "^5.9.3" diff --git a/packages/db/src/schema/app/follows.ts b/packages/db/src/schema/app/follows.ts index 4d71f3b0..6331603d 100644 --- a/packages/db/src/schema/app/follows.ts +++ b/packages/db/src/schema/app/follows.ts @@ -1,6 +1,7 @@ import { pgTable, bigserial, text, timestamp, uniqueIndex, index, check } from 'drizzle-orm/pg-core'; import { sql } from 'drizzle-orm'; import { users } from '../auth/users'; +import { playlists } from './playlists'; export const userFollows = pgTable('user_follows', { id: bigserial('id', { mode: 'number' }).primaryKey(), @@ -30,3 +31,17 @@ export const setterFollows = pgTable('setter_follows', { export type SetterFollow = typeof setterFollows.$inferSelect; export type NewSetterFollow = typeof setterFollows.$inferInsert; + +export const playlistFollows = pgTable('playlist_follows', { + id: bigserial('id', { mode: 'number' }).primaryKey(), + followerId: text('follower_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), + playlistUuid: text('playlist_uuid').references(() => playlists.uuid, { onDelete: 'cascade' }).notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), +}, (table) => ({ + uniqueFollow: uniqueIndex('unique_playlist_follow').on(table.followerId, table.playlistUuid), + followerIdx: index('playlist_follows_follower_idx').on(table.followerId), + playlistIdx: index('playlist_follows_playlist_idx').on(table.playlistUuid), +})); + +export type PlaylistFollow = typeof playlistFollows.$inferSelect; +export type NewPlaylistFollow = typeof playlistFollows.$inferInsert; diff --git a/packages/shared-schema/src/schema.ts b/packages/shared-schema/src/schema.ts index 8c99b466..51cd44c3 100644 --- a/packages/shared-schema/src/schema.ts +++ b/packages/shared-schema/src/schema.ts @@ -815,6 +815,10 @@ export const typeDefs = /* GraphQL */ ` climbCount: Int! "Current user's role (owner/editor/viewer)" userRole: String + "Number of users following this playlist" + followerCount: Int! + "Whether the current user follows this playlist" + isFollowedByMe: Boolean! } """ @@ -939,16 +943,16 @@ export const typeDefs = /* GraphQL */ ` input GetPlaylistClimbsInput { "Playlist ID" playlistId: ID! - "Board name for climb lookup" - boardName: String! + "Board name for climb lookup (omit for all-boards mode)" + boardName: String "Layout ID" - layoutId: Int! + layoutId: Int "Size ID" - sizeId: Int! + sizeId: Int "Set IDs" - setIds: String! + setIds: String "Board angle" - angle: Int! + angle: Int "Page number" page: Int "Page size" @@ -2161,6 +2165,14 @@ export const typeDefs = /* GraphQL */ ` setterUsername: String! } + """ + Input for following/unfollowing a playlist. + """ + input FollowPlaylistInput { + "The playlist UUID" + playlistUuid: ID! + } + """ Input for getting a setter profile. """ @@ -3442,6 +3454,16 @@ export const typeDefs = /* GraphQL */ ` """ unfollowSetter(input: FollowSetterInput!): Boolean! + """ + Follow a playlist. Idempotent. Only public playlists can be followed. + """ + followPlaylist(input: FollowPlaylistInput!): Boolean! + + """ + Unfollow a playlist. + """ + unfollowPlaylist(input: FollowPlaylistInput!): Boolean! + """ Subscribe to new climbs for a board type and layout. """ diff --git a/packages/web/app/components/climb-actions/__tests__/use-playlists.test.ts b/packages/web/app/components/climb-actions/__tests__/use-playlists.test.ts index f0b7dcd0..88212e56 100644 --- a/packages/web/app/components/climb-actions/__tests__/use-playlists.test.ts +++ b/packages/web/app/components/climb-actions/__tests__/use-playlists.test.ts @@ -29,6 +29,8 @@ describe('usePlaylists', () => { createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', climbCount: 5, + followerCount: 0, + isFollowedByMe: false, }; const defaultContext = { @@ -98,6 +100,8 @@ describe('usePlaylists', () => { createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', climbCount: 0, + followerCount: 0, + isFollowedByMe: false, }; mockCreatePlaylist.mockResolvedValue(mockPlaylist); @@ -181,6 +185,8 @@ describe('usePlaylists', () => { createdAt: '', updatedAt: '', climbCount: 0, + followerCount: 0, + isFollowedByMe: false, }); }); diff --git a/packages/web/app/components/climb-actions/use-playlists.ts b/packages/web/app/components/climb-actions/use-playlists.ts index 026d06cc..c20adef6 100644 --- a/packages/web/app/components/climb-actions/use-playlists.ts +++ b/packages/web/app/components/climb-actions/use-playlists.ts @@ -36,6 +36,8 @@ const noopCreatePlaylist = async (): Promise => { createdAt: '', updatedAt: '', climbCount: 0, + followerCount: 0, + isFollowedByMe: false, }; }; diff --git a/packages/web/app/components/climb-list/multiboard-climb-list.tsx b/packages/web/app/components/climb-list/multiboard-climb-list.tsx new file mode 100644 index 00000000..b5ac75fe --- /dev/null +++ b/packages/web/app/components/climb-list/multiboard-climb-list.tsx @@ -0,0 +1,201 @@ +'use client'; + +import React, { useMemo, useCallback } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import CircularProgress from '@mui/material/CircularProgress'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import ToggleButton from '@mui/material/ToggleButton'; +import { useMyBoards } from '@/app/hooks/use-my-boards'; +import { useBoardDetailsMap } from '@/app/hooks/use-board-details-map'; +import { useClimbActionsData } from '@/app/hooks/use-climb-actions-data'; +import BoardScrollSection from '@/app/components/board-scroll/board-scroll-section'; +import BoardScrollCard from '@/app/components/board-scroll/board-scroll-card'; +import boardScrollStyles from '@/app/components/board-scroll/board-scroll.module.css'; +import ClimbsList from '@/app/components/board-page/climbs-list'; +import { FavoritesProvider } from '@/app/components/climb-actions/favorites-batch-context'; +import { PlaylistsProvider } from '@/app/components/climb-actions/playlists-batch-context'; +import { getDefaultAngleForBoard } from '@/app/lib/board-config-for-playlist'; +import type { UserBoard } from '@boardsesh/shared-schema'; +import type { Climb } from '@/app/lib/types'; + +export type SortBy = 'popular' | 'new'; + +interface MultiboardClimbListProps { + climbs: Climb[]; + isFetching: boolean; + isLoading: boolean; + hasMore: boolean; + onLoadMore: () => void; + // Board filter + showBoardFilter?: boolean; + /** Board types present in the climbs (used for disabling filter cards) */ + boardTypes?: string[]; + selectedBoard: UserBoard | null; + onBoardSelect: (board: UserBoard | null) => void; + // Sort toggle + showSortToggle?: boolean; + sortBy?: SortBy; + onSortChange?: (sortBy: SortBy) => void; + totalCount?: number; + // Climb interaction + onClimbSelect?: (climb: Climb) => void; + selectedClimbUuid?: string | null; + // Optional header content + header?: React.ReactNode; + hideEndMessage?: boolean; + showBottomSpacer?: boolean; + /** Fallback board types for default board details resolution */ + fallbackBoardTypes?: string[]; +} + +export default function MultiboardClimbList({ + climbs, + isFetching, + isLoading, + hasMore, + onLoadMore, + showBoardFilter = true, + boardTypes, + selectedBoard, + onBoardSelect, + showSortToggle = false, + sortBy = 'popular', + onSortChange, + totalCount, + onClimbSelect, + selectedClimbUuid, + header, + hideEndMessage = true, + showBottomSpacer = true, + fallbackBoardTypes, +}: MultiboardClimbListProps) { + const { boards: myBoards, isLoading: isLoadingBoards } = useMyBoards(true); + + const { boardDetailsMap, defaultBoardDetails, unsupportedClimbs } = useBoardDetailsMap( + climbs, + myBoards, + selectedBoard, + fallbackBoardTypes, + ); + + // Climb action data for favorites/playlists context + const climbUuids = useMemo(() => climbs.map((c) => c.uuid), [climbs]); + const actionsBoardName = selectedBoard?.boardType || (climbs[0]?.boardType ?? 'kilter'); + const actionsLayoutId = selectedBoard?.layoutId || (climbs[0]?.layoutId ?? 1); + const actionsAngle = selectedBoard?.angle || getDefaultAngleForBoard(actionsBoardName); + + const { favoritesProviderProps, playlistsProviderProps } = useClimbActionsData({ + boardName: actionsBoardName, + layoutId: actionsLayoutId, + angle: actionsAngle, + climbUuids, + }); + + // Default climb navigation via redirect API + const defaultNavigateToClimb = useCallback(async (climb: Climb) => { + try { + const bt = climb.boardType || selectedBoard?.boardType; + if (!bt) return; + const params = new URLSearchParams({ boardType: bt, climbUuid: climb.uuid }); + const res = await fetch(`/api/internal/climb-redirect?${params}`); + if (!res.ok) return; + const { url } = await res.json(); + if (url) window.location.href = url; + } catch (error) { + console.error('Failed to navigate to climb:', error); + } + }, [selectedBoard]); + + const handleClimbSelect = onClimbSelect ?? defaultNavigateToClimb; + + const handleSortChange = (_: React.MouseEvent, value: SortBy | null) => { + if (value && onSortChange) { + onSortChange(value); + } + }; + + // Header with sort toggle and count + const headerInline = showSortToggle ? ( + + + Popular + New + + {totalCount != null && totalCount > 0 && ( + + {totalCount} climb{totalCount !== 1 ? 's' : ''} + + )} + + ) : undefined; + + return ( + + {/* Board filter - thumbnail scroll cards */} + {showBoardFilter && (myBoards.length > 0 || isLoadingBoards) && ( + +
onBoardSelect(null)} + > +
+ All +
+
+ All Boards +
+
+ {myBoards.map((board) => ( + onBoardSelect(board)} + /> + ))} +
+ )} + + {isLoading && climbs.length === 0 ? ( + + + + ) : climbs.length === 0 && !isLoading ? ( + + + No climbs found + + + ) : defaultBoardDetails ? ( + + + + + + ) : null} +
+ ); +} diff --git a/packages/web/app/components/climb-list/setter-climb-list.tsx b/packages/web/app/components/climb-list/setter-climb-list.tsx index 39a3b8fb..1a3826dd 100644 --- a/packages/web/app/components/climb-list/setter-climb-list.tsx +++ b/packages/web/app/components/climb-list/setter-climb-list.tsx @@ -1,11 +1,6 @@ 'use client'; import React, { useState, useMemo, useCallback } from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import CircularProgress from '@mui/material/CircularProgress'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import ToggleButton from '@mui/material/ToggleButton'; import { useInfiniteQuery } from '@tanstack/react-query'; import { createGraphQLHttpClient } from '@/app/lib/graphql/client'; import { @@ -13,19 +8,10 @@ import { type GetSetterClimbsFullQueryVariables, type GetSetterClimbsFullQueryResponse, } from '@/app/lib/graphql/operations'; -import { useMyBoards } from '@/app/hooks/use-my-boards'; -import { useClimbActionsData } from '@/app/hooks/use-climb-actions-data'; -import BoardScrollSection from '@/app/components/board-scroll/board-scroll-section'; -import BoardScrollCard from '@/app/components/board-scroll/board-scroll-card'; -import boardScrollStyles from '@/app/components/board-scroll/board-scroll.module.css'; -import ClimbsList from '@/app/components/board-page/climbs-list'; -import { FavoritesProvider } from '@/app/components/climb-actions/favorites-batch-context'; -import { PlaylistsProvider } from '@/app/components/climb-actions/playlists-batch-context'; -import { getBoardDetailsForPlaylist, getDefaultAngleForBoard, getUserBoardDetails } from '@/app/lib/board-config-for-playlist'; +import { getDefaultAngleForBoard } from '@/app/lib/board-config-for-playlist'; import type { UserBoard } from '@boardsesh/shared-schema'; -import type { Climb, BoardDetails } from '@/app/lib/types'; - -type SortBy = 'popular' | 'new'; +import type { Climb } from '@/app/lib/types'; +import MultiboardClimbList, { type SortBy } from './multiboard-climb-list'; interface SetterClimbListProps { username: string; @@ -37,8 +23,6 @@ export default function SetterClimbList({ username, boardTypes, authToken }: Set const [selectedBoard, setSelectedBoard] = useState(null); const [sortBy, setSortBy] = useState('popular'); - const { boards: myBoards, isLoading: isLoadingBoards } = useMyBoards(true); - const { data, fetchNextPage, hasNextPage, isFetching, isLoading } = useInfiniteQuery({ queryKey: ['setterClimbs', username, selectedBoard?.uuid ?? 'all', sortBy], queryFn: async ({ pageParam }) => { @@ -86,187 +70,26 @@ export default function SetterClimbList({ username, boardTypes, authToken }: Set } }, [hasNextPage, fetchNextPage]); - const handleSortChange = (_: React.MouseEvent, value: SortBy | null) => { - if (value) { - setSortBy(value); - } - }; - - const handleBoardSelect = (board: UserBoard) => { + const handleBoardSelect = useCallback((board: UserBoard | null) => { setSelectedBoard(board); - }; - - const handleAllSelect = () => { - setSelectedBoard(null); - }; - - const navigateToClimb = useCallback(async (climb: Climb) => { - try { - const bt = climb.boardType || selectedBoard?.boardType; - if (!bt) return; - const params = new URLSearchParams({ boardType: bt, climbUuid: climb.uuid }); - const res = await fetch(`/api/internal/climb-redirect?${params}`); - if (!res.ok) return; - const { url } = await res.json(); - if (url) window.location.href = url; - } catch (error) { - console.error('Failed to navigate to climb:', error); - } - }, [selectedBoard]); - - // Build boardDetailsMap for multi-board rendering - const { boardDetailsMap, defaultBoardDetails, unsupportedClimbs } = useMemo(() => { - const map: Record = {}; - const unsupported = new Set(); - - const userBoardsByKey = new Map(); - for (const board of myBoards) { - const key = `${board.boardType}:${board.layoutId}`; - if (!userBoardsByKey.has(key)) { - userBoardsByKey.set(key, board); - } - } - - for (const climb of climbs) { - const bt = climb.boardType; - const layoutId = climb.layoutId; - if (!bt || layoutId == null) continue; - - const key = `${bt}:${layoutId}`; - if (map[key]) continue; - - const userBoard = userBoardsByKey.get(key); - if (userBoard) { - const details = getUserBoardDetails(userBoard); - if (details) { - map[key] = details; - continue; - } - } - - const genericDetails = getBoardDetailsForPlaylist(bt, layoutId); - if (genericDetails) { - map[key] = genericDetails; - } - } - - const userBoardTypes = new Set(myBoards.map((b) => b.boardType)); - for (const climb of climbs) { - if (climb.boardType && !userBoardTypes.has(climb.boardType)) { - unsupported.add(climb.uuid); - } - } - - let defaultDetails: BoardDetails | null = null; - if (selectedBoard) { - defaultDetails = getUserBoardDetails(selectedBoard); - } - if (!defaultDetails && myBoards.length > 0) { - defaultDetails = getUserBoardDetails(myBoards[0]); - } - if (!defaultDetails) { - defaultDetails = getBoardDetailsForPlaylist('kilter', 1); - } - - return { - boardDetailsMap: map, - defaultBoardDetails: defaultDetails!, - unsupportedClimbs: unsupported, - }; - }, [climbs, myBoards, selectedBoard]); - - // Climb action data for favorites/playlists context - const climbUuids = useMemo(() => climbs.map((c) => c.uuid), [climbs]); - const actionsBoardName = selectedBoard?.boardType || (climbs[0]?.boardType ?? 'kilter'); - const actionsLayoutId = selectedBoard?.layoutId || (climbs[0]?.layoutId ?? 1); - const actionsAngle = selectedBoard?.angle || getDefaultAngleForBoard(actionsBoardName); - - const { favoritesProviderProps, playlistsProviderProps } = useClimbActionsData({ - boardName: actionsBoardName, - layoutId: actionsLayoutId, - angle: actionsAngle, - climbUuids, - }); - - // Header with sort toggle and count - const headerInline = ( - - - Popular - New - - {totalCount > 0 && ( - - {totalCount} climb{totalCount !== 1 ? 's' : ''} - - )} - - ); + }, []); return ( - - {/* Board filter - thumbnail scroll cards */} - {(myBoards.length > 0 || isLoadingBoards) && ( - -
-
- All -
-
- All Boards -
-
- {myBoards.map((board) => ( - handleBoardSelect(board)} - /> - ))} -
- )} - - {isLoading && climbs.length === 0 ? ( - - - - ) : climbs.length === 0 && !isLoading ? ( - - - No climbs found - - - ) : defaultBoardDetails ? ( - - - - - - ) : null} -
+ ); } diff --git a/packages/web/app/hooks/__tests__/use-board-details-map.test.ts b/packages/web/app/hooks/__tests__/use-board-details-map.test.ts new file mode 100644 index 00000000..ceb849e4 --- /dev/null +++ b/packages/web/app/hooks/__tests__/use-board-details-map.test.ts @@ -0,0 +1,216 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { renderHook } from '@testing-library/react'; + +// Mock dependencies +vi.mock('@/app/lib/board-config-for-playlist', () => ({ + getUserBoardDetails: vi.fn(), + getBoardDetailsForPlaylist: vi.fn(), +})); + +import { useBoardDetailsMap } from '../use-board-details-map'; +import { getUserBoardDetails, getBoardDetailsForPlaylist } from '@/app/lib/board-config-for-playlist'; +import type { UserBoard } from '@boardsesh/shared-schema'; +import type { Climb, BoardDetails } from '@/app/lib/types'; + +const mockGetUserBoardDetails = vi.mocked(getUserBoardDetails); +const mockGetBoardDetailsForPlaylist = vi.mocked(getBoardDetailsForPlaylist); + +function makeClimb(overrides: Partial = {}): Climb { + return { + uuid: 'climb-1', + name: 'Test Climb', + frames: '', + angle: 40, + difficulty: 'V5', + quality_average: '3', + setter_username: 'setter1', + litUpHoldsMap: {}, + description: '', + ascensionist_count: 0, + stars: 3, + difficulty_error: '0', + benchmark_difficulty: null, + boardType: 'kilter', + layoutId: 1, + ...overrides, + }; +} + +function makeUserBoard(overrides: Partial = {}): UserBoard { + return { + uuid: 'board-1', + boardType: 'kilter', + layoutId: 1, + sizeId: 10, + setIds: '1,2,3', + angle: 40, + ...overrides, + } as UserBoard; +} + +function makeBoardDetails(name: string): BoardDetails { + return { + board_name: name, + layout_id: 1, + size_id: 10, + set_ids: [1, 2, 3], + boardWidth: 100, + boardHeight: 150, + holdsData: {}, + litUpHoldsGroupSets: [], + edgeLeft: 0, + edgeRight: 100, + edgeBottom: 0, + edgeTop: 150, + } as unknown as BoardDetails; +} + +describe('useBoardDetailsMap', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return empty map when no climbs provided', () => { + const { result } = renderHook(() => + useBoardDetailsMap([], []), + ); + + expect(Object.keys(result.current.boardDetailsMap)).toHaveLength(0); + expect(result.current.unsupportedClimbs.size).toBe(0); + }); + + it('should build boardDetailsMap keyed by "boardType:layoutId"', () => { + const climb = makeClimb({ boardType: 'kilter', layoutId: 1 }); + const genericDetails = makeBoardDetails('kilter'); + mockGetBoardDetailsForPlaylist.mockReturnValue(genericDetails); + + const { result } = renderHook(() => + useBoardDetailsMap([climb], []), + ); + + expect(result.current.boardDetailsMap['kilter:1']).toBe(genericDetails); + }); + + it('should prefer user board details over generic details', () => { + const climb = makeClimb({ boardType: 'kilter', layoutId: 1 }); + const userBoard = makeUserBoard({ boardType: 'kilter', layoutId: 1 }); + const userDetails = makeBoardDetails('kilter-user'); + const genericDetails = makeBoardDetails('kilter-generic'); + + mockGetUserBoardDetails.mockReturnValue(userDetails); + mockGetBoardDetailsForPlaylist.mockReturnValue(genericDetails); + + const { result } = renderHook(() => + useBoardDetailsMap([climb], [userBoard]), + ); + + expect(result.current.boardDetailsMap['kilter:1']).toBe(userDetails); + // Generic should NOT be called because user board took precedence + expect(mockGetBoardDetailsForPlaylist).not.toHaveBeenCalled(); + }); + + it('should fall back to generic details when user does not own that board', () => { + const climb = makeClimb({ boardType: 'tension', layoutId: 2 }); + const genericDetails = makeBoardDetails('tension'); + + mockGetBoardDetailsForPlaylist.mockReturnValue(genericDetails); + + const { result } = renderHook(() => + useBoardDetailsMap([climb], []), + ); + + expect(result.current.boardDetailsMap['tension:2']).toBe(genericDetails); + expect(mockGetBoardDetailsForPlaylist).toHaveBeenCalledWith('tension', 2); + }); + + it('should populate unsupportedClimbs set for board types user does not own', () => { + const climb1 = makeClimb({ uuid: 'c1', boardType: 'kilter', layoutId: 1 }); + const climb2 = makeClimb({ uuid: 'c2', boardType: 'tension', layoutId: 2 }); + const userBoard = makeUserBoard({ boardType: 'kilter', layoutId: 1 }); + + const kilterDetails = makeBoardDetails('kilter'); + const tensionDetails = makeBoardDetails('tension'); + mockGetUserBoardDetails.mockReturnValue(kilterDetails); + mockGetBoardDetailsForPlaylist.mockReturnValue(tensionDetails); + + const { result } = renderHook(() => + useBoardDetailsMap([climb1, climb2], [userBoard]), + ); + + // Kilter climb is supported (user owns kilter board) + expect(result.current.unsupportedClimbs.has('c1')).toBe(false); + // Tension climb is unsupported (user doesn't own tension board) + expect(result.current.unsupportedClimbs.has('c2')).toBe(true); + }); + + it('should return defaultBoardDetails from selectedBoard when provided', () => { + const selectedBoard = makeUserBoard({ boardType: 'kilter', layoutId: 1 }); + const selectedDetails = makeBoardDetails('kilter-selected'); + + mockGetUserBoardDetails.mockReturnValue(selectedDetails); + + const { result } = renderHook(() => + useBoardDetailsMap([], [], selectedBoard), + ); + + expect(result.current.defaultBoardDetails).toBe(selectedDetails); + }); + + it('should return defaultBoardDetails from first myBoard as fallback', () => { + const myBoard = makeUserBoard({ boardType: 'kilter', layoutId: 1 }); + const myBoardDetails = makeBoardDetails('kilter-mine'); + + mockGetUserBoardDetails.mockReturnValue(myBoardDetails); + + const { result } = renderHook(() => + useBoardDetailsMap([], [myBoard]), + ); + + expect(result.current.defaultBoardDetails).toBe(myBoardDetails); + }); + + it('should use fallbackBoardTypes for default details when no boards available', () => { + const genericDetails = makeBoardDetails('tension'); + mockGetBoardDetailsForPlaylist.mockReturnValue(genericDetails); + + const { result } = renderHook(() => + useBoardDetailsMap([], [], null, ['tension']), + ); + + expect(result.current.defaultBoardDetails).toBe(genericDetails); + expect(mockGetBoardDetailsForPlaylist).toHaveBeenCalledWith('tension', null); + }); + + it('should handle multiple climbs from different board types', () => { + const climb1 = makeClimb({ uuid: 'c1', boardType: 'kilter', layoutId: 1 }); + const climb2 = makeClimb({ uuid: 'c2', boardType: 'tension', layoutId: 2 }); + const climb3 = makeClimb({ uuid: 'c3', boardType: 'kilter', layoutId: 1 }); // duplicate key + + const kilterDetails = makeBoardDetails('kilter'); + const tensionDetails = makeBoardDetails('tension'); + + mockGetBoardDetailsForPlaylist + .mockReturnValueOnce(kilterDetails) + .mockReturnValueOnce(tensionDetails); + + const { result } = renderHook(() => + useBoardDetailsMap([climb1, climb2, climb3], []), + ); + + // Only 2 unique keys in the boardDetailsMap + expect(Object.keys(result.current.boardDetailsMap)).toHaveLength(2); + expect(result.current.boardDetailsMap['kilter:1']).toBe(kilterDetails); + expect(result.current.boardDetailsMap['tension:2']).toBe(tensionDetails); + }); + + it('should skip climbs without boardType or layoutId', () => { + const climb1 = makeClimb({ uuid: 'c1', boardType: undefined, layoutId: 1 }); + const climb2 = makeClimb({ uuid: 'c2', boardType: 'kilter', layoutId: null }); + + const { result } = renderHook(() => + useBoardDetailsMap([climb1, climb2], []), + ); + + expect(Object.keys(result.current.boardDetailsMap)).toHaveLength(0); + }); +}); diff --git a/packages/web/app/hooks/use-board-details-map.ts b/packages/web/app/hooks/use-board-details-map.ts new file mode 100644 index 00000000..e2cf0d1c --- /dev/null +++ b/packages/web/app/hooks/use-board-details-map.ts @@ -0,0 +1,86 @@ +import { useMemo } from 'react'; +import type { UserBoard } from '@boardsesh/shared-schema'; +import type { Climb, BoardDetails } from '@/app/lib/types'; +import { getUserBoardDetails, getBoardDetailsForPlaylist } from '@/app/lib/board-config-for-playlist'; + +interface UseBoardDetailsMapResult { + boardDetailsMap: Record; + defaultBoardDetails: BoardDetails | null; + unsupportedClimbs: Set; +} + +/** + * Shared hook that builds a boardDetailsMap for multi-board climb rendering. + * Maps "boardType:layoutId" keys to BoardDetails objects, preferring user's + * own board details over generic fallbacks. + * + * Used by: SetterClimbList, PlaylistDetailContent, SessionDetailContent + */ +export function useBoardDetailsMap( + climbs: Climb[], + myBoards: UserBoard[], + selectedBoard?: UserBoard | null, + fallbackBoardTypes?: string[], +): UseBoardDetailsMapResult { + return useMemo(() => { + const map: Record = {}; + const unsupported = new Set(); + + // Build user boards keyed by "boardType:layoutId" + const userBoardsByKey = new Map(); + for (const board of myBoards) { + const key = `${board.boardType}:${board.layoutId}`; + if (!userBoardsByKey.has(key)) { + userBoardsByKey.set(key, board); + } + } + + // Resolve BoardDetails for each unique boardType:layoutId + for (const climb of climbs) { + const bt = climb.boardType; + const layoutId = climb.layoutId; + if (!bt || layoutId == null) continue; + + const key = `${bt}:${layoutId}`; + if (map[key]) continue; + + const userBoard = userBoardsByKey.get(key); + if (userBoard) { + const details = getUserBoardDetails(userBoard); + if (details) { + map[key] = details; + continue; + } + } + + const genericDetails = getBoardDetailsForPlaylist(bt, layoutId); + if (genericDetails) { + map[key] = genericDetails; + } + } + + // Mark unsupported climbs (board types the user doesn't have) + const userBoardTypes = new Set(myBoards.map((b) => b.boardType)); + for (const climb of climbs) { + if (climb.boardType && !userBoardTypes.has(climb.boardType)) { + unsupported.add(climb.uuid); + } + } + + // Determine default board details + let defaultDetails: BoardDetails | null = null; + if (selectedBoard) { + defaultDetails = getUserBoardDetails(selectedBoard); + } + if (!defaultDetails && myBoards.length > 0) { + defaultDetails = getUserBoardDetails(myBoards[0]); + } + if (!defaultDetails) { + const fallbackBoardType = fallbackBoardTypes?.[0] || climbs[0]?.boardType || 'kilter'; + const fallbackLayoutId = climbs[0]?.layoutId ?? null; + defaultDetails = getBoardDetailsForPlaylist(fallbackBoardType, fallbackLayoutId); + } + + return { boardDetailsMap: map, defaultBoardDetails: defaultDetails, unsupportedClimbs: unsupported }; + }, [climbs, myBoards, selectedBoard, fallbackBoardTypes]); +} diff --git a/packages/web/app/lib/graphql/operations/playlists.ts b/packages/web/app/lib/graphql/operations/playlists.ts index 241f7924..f09d96fa 100644 --- a/packages/web/app/lib/graphql/operations/playlists.ts +++ b/packages/web/app/lib/graphql/operations/playlists.ts @@ -17,6 +17,8 @@ export const PLAYLIST_FIELDS = gql` lastAccessedAt climbCount userRole + followerCount + isFollowedByMe } `; @@ -112,6 +114,7 @@ export const GET_PLAYLIST_CLIMBS = gql` climbs { uuid layoutId + boardType setter_username name description @@ -148,6 +151,8 @@ export interface Playlist { lastAccessedAt?: string | null; climbCount: number; userRole?: string; + followerCount: number; + isFollowedByMe: boolean; } export interface GetAllUserPlaylistsInput { @@ -275,11 +280,11 @@ export interface RemoveClimbFromPlaylistMutationResponse { export interface GetPlaylistClimbsInput { playlistId: string; - boardName: string; - layoutId: number; - sizeId: number; - setIds: string; - angle: number; + boardName?: string; + layoutId?: number; + sizeId?: number; + setIds?: string; + angle?: number; page?: number; pageSize?: number; } @@ -292,6 +297,7 @@ export interface PlaylistClimbsResult { climbs: Array<{ uuid: string; layoutId?: number | null; + boardType?: string; setter_username: string; name: string; description: string; @@ -474,3 +480,39 @@ export interface SearchPlaylistsQueryResponse { hasMore: boolean; }; } + +// ============================================ +// Playlist Follow Types and Operations +// ============================================ + +export const FOLLOW_PLAYLIST = gql` + mutation FollowPlaylist($input: FollowPlaylistInput!) { + followPlaylist(input: $input) + } +`; + +export const UNFOLLOW_PLAYLIST = gql` + mutation UnfollowPlaylist($input: FollowPlaylistInput!) { + unfollowPlaylist(input: $input) + } +`; + +export interface FollowPlaylistInput { + playlistUuid: string; +} + +export interface FollowPlaylistMutationVariables { + input: FollowPlaylistInput; +} + +export interface FollowPlaylistMutationResponse { + followPlaylist: boolean; +} + +export interface UnfollowPlaylistMutationVariables { + input: FollowPlaylistInput; +} + +export interface UnfollowPlaylistMutationResponse { + unfollowPlaylist: boolean; +} diff --git a/packages/web/app/my-library/playlist/[playlist_uuid]/playlist-detail-content.tsx b/packages/web/app/my-library/playlist/[playlist_uuid]/playlist-detail-content.tsx index 02f6f83d..d0547758 100644 --- a/packages/web/app/my-library/playlist/[playlist_uuid]/playlist-detail-content.tsx +++ b/packages/web/app/my-library/playlist/[playlist_uuid]/playlist-detail-content.tsx @@ -1,7 +1,6 @@ 'use client'; import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import MuiAlert from '@mui/material/Alert'; import MuiButton from '@mui/material/Button'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; @@ -19,19 +18,23 @@ import { ElectricBoltOutlined, EditOutlined, DeleteOutlined, + PeopleOutlined, } from '@mui/icons-material'; import { useInfiniteQuery } from '@tanstack/react-query'; -import { Climb, BoardDetails } from '@/app/lib/types'; +import { Climb } from '@/app/lib/types'; import { executeGraphQL, createGraphQLHttpClient } from '@/app/lib/graphql/client'; import { GET_PLAYLIST, GET_PLAYLIST_CLIMBS, DELETE_PLAYLIST, UPDATE_PLAYLIST_LAST_ACCESSED, + FOLLOW_PLAYLIST, + UNFOLLOW_PLAYLIST, GetPlaylistQueryResponse, GetPlaylistQueryVariables, GetPlaylistClimbsQueryResponse, type GetPlaylistClimbsQueryVariables, + type GetPlaylistClimbsInput, Playlist, UpdatePlaylistLastAccessedMutationVariables, UpdatePlaylistLastAccessedMutationResponse, @@ -41,12 +44,8 @@ import { import { useSnackbar } from '@/app/components/providers/snackbar-provider'; import { LoadingSpinner } from '@/app/components/ui/loading-spinner'; import { EmptyState } from '@/app/components/ui/empty-state'; +import FollowButton from '@/app/components/ui/follow-button'; import { useWsAuthToken } from '@/app/hooks/use-ws-auth-token'; -import { useClimbActionsData } from '@/app/hooks/use-climb-actions-data'; -import { FavoritesProvider } from '@/app/components/climb-actions/favorites-batch-context'; -import { PlaylistsProvider } from '@/app/components/climb-actions/playlists-batch-context'; -import { ClimbCardSkeleton } from '@/app/components/board-page/board-page-skeleton'; -import ClimbsList from '@/app/components/board-page/climbs-list'; import { getBoardDetailsForPlaylist, getDefaultAngleForBoard } from '@/app/lib/board-config-for-playlist'; import { themeTokens } from '@/app/theme/theme-config'; import { useRouter } from 'next/navigation'; @@ -54,6 +53,8 @@ import BackButton from '@/app/components/back-button'; import { PlaylistGeneratorDrawer } from '@/app/components/playlist-generator'; import PlaylistEditDrawer from '@/app/components/library/playlist-edit-drawer'; import CommentSection from '@/app/components/social/comment-section'; +import MultiboardClimbList from '@/app/components/climb-list/multiboard-climb-list'; +import type { UserBoard } from '@boardsesh/shared-schema'; import styles from '@/app/components/library/playlist-view.module.css'; // Validate hex color format @@ -72,18 +73,6 @@ const PLAYLIST_COLORS = [ themeTokens.colors.amber, ]; -const skeletonCardBoxSx = { width: { xs: '100%', lg: '50%' } }; - -const ClimbsListSkeleton = ({ aspectRatio }: { aspectRatio: number }) => ( - - {Array.from({ length: 6 }, (_, i) => ( - - - - ))} - -); - type PlaylistDetailContentProps = { playlistUuid: string; }; @@ -100,14 +89,10 @@ export default function PlaylistDetailContent({ const [generatorOpen, setGeneratorOpen] = useState(false); const [listRefreshKey, setListRefreshKey] = useState(0); const [menuAnchor, setMenuAnchor] = useState(null); - const [selectedClimbUuid, setSelectedClimbUuid] = useState(null); + const [selectedBoard, setSelectedBoard] = useState(null); const lastAccessedUpdatedRef = useRef(false); const { token, isLoading: tokenLoading } = useWsAuthToken(); - // Derived board details from playlist - const [boardDetails, setBoardDetails] = useState(null); - const [angle, setAngle] = useState(40); - const fetchPlaylist = useCallback(async () => { if (tokenLoading) return; @@ -127,14 +112,6 @@ export default function PlaylistDetailContent({ } setPlaylist(response.playlist); - - // Derive board details - const details = getBoardDetailsForPlaylist( - response.playlist.boardType, - response.playlist.layoutId, - ); - setBoardDetails(details); - setAngle(getDefaultAngleForBoard(response.playlist.boardType)); } catch (err) { console.error('Error fetching playlist:', err); setError('Failed to load playlist'); @@ -161,7 +138,7 @@ export default function PlaylistDetailContent({ } }, [playlist, token, playlistUuid]); - // === Playlist climbs data fetching === + // === Playlist climbs data fetching (all-boards mode by default) === const { data: climbsData, @@ -170,38 +147,37 @@ export default function PlaylistDetailContent({ isFetching: isFetchingClimbs, isFetchingNextPage, isLoading: isClimbsLoading, - error: climbsError, } = useInfiniteQuery({ queryKey: [ 'playlistClimbs', playlistUuid, - boardDetails?.board_name, - boardDetails?.layout_id, - boardDetails?.size_id, - angle, + selectedBoard?.uuid ?? 'all', listRefreshKey, ], queryFn: async ({ pageParam = 0 }) => { - if (!boardDetails) throw new Error('Board details not available'); const client = createGraphQLHttpClient(token); + + const input: GetPlaylistClimbsInput = { + playlistId: playlistUuid, + page: pageParam as number, + pageSize: 20, + // Specific-board mode when a board is selected + ...(selectedBoard && { + boardName: selectedBoard.boardType, + layoutId: selectedBoard.layoutId, + sizeId: selectedBoard.sizeId, + setIds: selectedBoard.setIds, + angle: selectedBoard.angle ?? getDefaultAngleForBoard(selectedBoard.boardType), + }), + }; + const response = await client.request( GET_PLAYLIST_CLIMBS, - { - input: { - playlistId: playlistUuid, - boardName: boardDetails.board_name, - layoutId: boardDetails.layout_id, - sizeId: boardDetails.size_id, - setIds: boardDetails.set_ids.join(','), - angle: angle, - page: pageParam, - pageSize: 20, - }, - } satisfies GetPlaylistClimbsQueryVariables, + { input } satisfies GetPlaylistClimbsQueryVariables, ); return response.playlistClimbs; }, - enabled: !tokenLoading && !!token && !!boardDetails, + enabled: !tokenLoading && !!token, initialPageParam: 0, getNextPageParam: (lastPage, allPages) => { if (!lastPage.hasMore) return undefined; @@ -210,42 +186,21 @@ export default function PlaylistDetailContent({ staleTime: 5 * 60 * 1000, }); - const allClimbs: Climb[] = climbsData?.pages.flatMap((page) => page.climbs as Climb[]) ?? []; - - // Filter out cross-layout climbs - const { visibleClimbs, hiddenCount } = useMemo(() => { - const visible: Climb[] = []; - let hidden = 0; + const allClimbs: Climb[] = useMemo( + () => climbsData?.pages.flatMap((page) => page.climbs as Climb[]) ?? [], + [climbsData], + ); + // Collect unique board types for the filter + const boardTypes = useMemo(() => { + const types = new Set(); for (const climb of allClimbs) { - const isCrossLayout = climb.layoutId != null && climb.layoutId !== boardDetails?.layout_id; - if (isCrossLayout) { - hidden++; - } else { - visible.push({ ...climb, angle }); - } + if (climb.boardType) types.add(climb.boardType); } - - return { visibleClimbs: visible, hiddenCount: hidden }; - }, [allClimbs, boardDetails?.layout_id, angle]); - - // Climb UUIDs for favorites/playlists provider - const climbUuids = useMemo( - () => visibleClimbs.map((climb) => climb.uuid), - [visibleClimbs], - ); - - // Favorites and playlists data fetching - const { favoritesProviderProps, playlistsProviderProps } = useClimbActionsData({ - boardName: boardDetails?.board_name ?? '', - layoutId: boardDetails?.layout_id ?? 0, - angle, - climbUuids, - }); - - const handleClimbSelect = useCallback((climb: Climb) => { - setSelectedClimbUuid(climb.uuid); - }, []); + // Also include the playlist's own board type + if (playlist?.boardType) types.add(playlist.boardType); + return Array.from(types); + }, [allClimbs, playlist?.boardType]); const handleLoadMore = useCallback(() => { if (hasNextPage && !isFetchingNextPage) { @@ -281,6 +236,10 @@ export default function PlaylistDetailContent({ } }, [token, playlist, playlistUuid, router, showMessage]); + const handleBoardSelect = useCallback((board: UserBoard | null) => { + setSelectedBoard(board); + }, []); + const isOwner = playlist?.userRole === 'owner'; const getPlaylistColor = () => { @@ -290,6 +249,14 @@ export default function PlaylistDetailContent({ return PLAYLIST_COLORS[0]; }; + // Board details for the generator drawer + const generatorBoardDetails = useMemo(() => { + if (!playlist) return null; + return getBoardDetailsForPlaylist(playlist.boardType, playlist.layoutId); + }, [playlist]); + + const generatorAngle = playlist ? getDefaultAngleForBoard(playlist.boardType) : 40; + if (loading || tokenLoading) { return (
@@ -315,80 +282,6 @@ export default function PlaylistDetailContent({ ); } - // Render the climbs section content - const renderClimbsSection = () => { - if (!boardDetails) { - return ( -
- - Unable to load climb previews for this board configuration. - -
- ); - } - - const aspectRatio = boardDetails.boardWidth / boardDetails.boardHeight; - - if ((isClimbsLoading || tokenLoading) && allClimbs.length === 0) { - return ( -
- -
- ); - } - - if (climbsError) { - return ( -
- -
- ); - } - - if (visibleClimbs.length === 0 && hiddenCount === 0 && !isFetchingClimbs) { - return ( -
- -
- ); - } - - // Build header with hidden-count alert and all-hidden empty state - const climbsHeader = ( - <> - {hiddenCount > 0 && ( - - {`Not showing ${hiddenCount} ${hiddenCount === 1 ? 'climb' : 'climbs'} from other layouts`} - - )} - {visibleClimbs.length === 0 && hiddenCount > 0 && !isFetchingClimbs && ( - - )} - - ); - - return ( -
- - - - - -
- ); - }; - return ( <> {/* Back Button */} @@ -419,6 +312,10 @@ export default function PlaylistDetailContent({ {playlist.climbCount} {playlist.climbCount === 1 ? 'climb' : 'climbs'} + + + {playlist.followerCount} {playlist.followerCount === 1 ? 'follower' : 'followers'} + )} + {/* Follow button for non-owners on public playlists */} + {!isOwner && playlist.isPublic && ( + + ({ input: { playlistUuid: id } })} + onFollowChange={(isFollowing) => { + setPlaylist({ + ...playlist, + followerCount: playlist.followerCount + (isFollowing ? 1 : -1), + isFollowedByMe: isFollowing, + }); + }} + /> + + )}
@@ -474,8 +391,25 @@ export default function PlaylistDetailContent({ - {/* Climbs List */} - {renderClimbsSection()} + {/* Climbs List with multi-board support */} +
+ {allClimbs.length === 0 && !isFetchingClimbs && !isClimbsLoading ? ( + + ) : ( + + )} +
{/* Discussion */} {playlist.isPublic && ( @@ -500,13 +434,13 @@ export default function PlaylistDetailContent({ )} {/* Generator Drawer */} - {boardDetails && ( + {generatorBoardDetails && ( setGeneratorOpen(false)} playlistUuid={playlistUuid} - boardDetails={boardDetails} - angle={angle} + boardDetails={generatorBoardDetails} + angle={generatorAngle} onSuccess={handlePlaylistUpdated} /> )} diff --git a/packages/web/app/session/[sessionId]/session-detail-content.tsx b/packages/web/app/session/[sessionId]/session-detail-content.tsx index 5fdf0fd4..c88c6424 100644 --- a/packages/web/app/session/[sessionId]/session-detail-content.tsx +++ b/packages/web/app/session/[sessionId]/session-detail-content.tsx @@ -37,7 +37,8 @@ import { FavoritesProvider } from '@/app/components/climb-actions/favorites-batc import { PlaylistsProvider } from '@/app/components/climb-actions/playlists-batch-context'; import { useClimbActionsData } from '@/app/hooks/use-climb-actions-data'; import { useMyBoards } from '@/app/hooks/use-my-boards'; -import { getBoardDetailsForPlaylist, getDefaultAngleForBoard, getUserBoardDetails } from '@/app/lib/board-config-for-playlist'; +import { useBoardDetailsMap } from '@/app/hooks/use-board-details-map'; +import { getDefaultAngleForBoard } from '@/app/lib/board-config-for-playlist'; import { convertLitUpHoldsStringToMap } from '@/app/components/board-renderer/util'; import { useWsAuthToken } from '@/app/hooks/use-ws-auth-token'; import { createGraphQLHttpClient } from '@/app/lib/graphql/client'; @@ -48,8 +49,7 @@ import { } from '@/app/lib/graphql/operations/activity-feed'; import { useSnackbar } from '@/app/components/providers/snackbar-provider'; import { themeTokens } from '@/app/theme/theme-config'; -import type { UserBoard } from '@boardsesh/shared-schema'; -import type { Climb, BoardDetails, BoardName } from '@/app/lib/types'; +import type { Climb, BoardName } from '@/app/lib/types'; import UserSearchDialog from './user-search-dialog'; interface SessionDetailContentProps { @@ -270,58 +270,12 @@ export default function SessionDetailContent({ session: initialSession }: Sessio const tickUuids = useMemo(() => ticks.map((t) => t.uuid), [ticks]); // Build boardDetailsMap for multi-board support - const { boardDetailsMap, defaultBoardDetails, unsupportedClimbs } = useMemo(() => { - const map: Record = {}; - const unsupported = new Set(); - - const userBoardsByKey = new Map(); - for (const board of myBoards) { - const key = `${board.boardType}:${board.layoutId}`; - if (!userBoardsByKey.has(key)) { - userBoardsByKey.set(key, board); - } - } - - for (const climb of sessionClimbs) { - const bt = climb.boardType; - const layoutId = climb.layoutId; - if (!bt || layoutId == null) continue; - - const key = `${bt}:${layoutId}`; - if (map[key]) continue; - - const userBoard = userBoardsByKey.get(key); - if (userBoard) { - const details = getUserBoardDetails(userBoard); - if (details) { - map[key] = details; - continue; - } - } - - const genericDetails = getBoardDetailsForPlaylist(bt, layoutId); - if (genericDetails) { - map[key] = genericDetails; - } - } - - const userBoardTypes = new Set(myBoards.map((b) => b.boardType)); - for (const climb of sessionClimbs) { - if (climb.boardType && !userBoardTypes.has(climb.boardType)) { - unsupported.add(climb.uuid); - } - } - - let defaultDetails: BoardDetails | null = null; - if (myBoards.length > 0) { - defaultDetails = getUserBoardDetails(myBoards[0]); - } - if (!defaultDetails && boardTypes[0]) { - defaultDetails = getBoardDetailsForPlaylist(boardTypes[0], null); - } - - return { boardDetailsMap: map, defaultBoardDetails: defaultDetails, unsupportedClimbs: unsupported }; - }, [sessionClimbs, myBoards, boardTypes]); + const { boardDetailsMap, defaultBoardDetails, unsupportedClimbs } = useBoardDetailsMap( + sessionClimbs, + myBoards, + null, + boardTypes, + ); // Climb actions data for favorites/playlists — derive from actual climb data, fall back to session metadata const climbUuids = useMemo(() => sessionClimbs.map((c) => c.uuid), [sessionClimbs]);