From e9e5517bc9b17345743b7438ffdbdf2d8a801b11 Mon Sep 17 00:00:00 2001 From: Andrew McCallum Date: Sat, 31 Oct 2026 13:48:20 +0000 Subject: [PATCH] Remove synced runs that intervals.icu now lists as another sport Sync only ever added runs, so a ride the watch recorded as a run stayed in openRun after being corrected in Garmin or intervals.icu, and its "best efforts" skewed the race predictions. Other sports are never imported, so a run that intervals.icu now calls something else is removed as if it had never been a run: its best efforts, original file and club shares go too. Forgetting it means it's imported again if it turns back into a run. Uploads and runs deleted by hand are untouched. The sync log counts removals. Co-Authored-By: Claude Opus 5.5 --- apps/api/src/integrations.spec.ts | 79 + apps/api/src/integrations/sync.ts | 87 +- .../web/src/components/intervals-settings.tsx | 2 + .../migrations/0009_sync_removed.sql | 1 + .../migrations/meta/0009_snapshot.json | 3147 +++++++++++++++++ .../migrations/meta/_journal.json | 7 + libs/db/src/schema/integrations.ts | 2 + libs/schemas/src/integrations.ts | 2 + 8 files changed, 3324 insertions(+), 3 deletions(-) create mode 100644 libs/db-migrations/migrations/0009_sync_removed.sql create mode 100644 libs/db-migrations/migrations/meta/0009_snapshot.json diff --git a/apps/api/src/integrations.spec.ts b/apps/api/src/integrations.spec.ts index 8baeb16..4221082 100644 --- a/apps/api/src/integrations.spec.ts +++ b/apps/api/src/integrations.spec.ts @@ -1,3 +1,5 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; import { gzipSync } from 'node:zlib'; import { eq } from 'drizzle-orm'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -281,6 +283,83 @@ describe('intervals.icu sync', () => { expect(await t.sync()).toMatchObject({ status: 'ok', importedCount: 1 }); }); + it('removes runs that intervals.icu now lists as another sport', async () => { + const activities: FakeActivity[] = [ + { id: 'i1', start_date: daysAgo(3), type: 'Run', name: 'Not a run' }, + { id: 'i2', start_date: daysAgo(2), type: 'Run', name: 'Deleted' }, + { id: 'i3', start_date: daysAgo(1), type: 'Run', name: 'Real run' }, + ]; + const t = await connected(activities); + await importFit(t.app, t.cookie, { start: new Date(daysAgo(6)) }); + await t.sync(); + const byName = async () => + Object.fromEntries((await t.runs()).map((r) => [r.name, r])); + const before = await byName(); + expect(Object.keys(before)).toHaveLength(4); + + // Shared with a club before anyone noticed it was a ride. + const club = (await ( + await send(t.app, t.cookie, 'POST', '/api/clubs', { + name: 'Parkside', + timezone: 'Europe/London', + }) + ).json()) as { id: string }; + await send(t.app, t.cookie, 'PATCH', `/api/clubs/${club.id}/me`, { + shareLevel: 'summaries', + }); + const ride = before['Not a run']; + await send( + t.app, + t.cookie, + 'PUT', + `/api/activities/${ride.id}/clubs/${club.id}`, + ); + expect(t.db.select().from(schema.clubShare).all()).toHaveLength(1); + const [{ originalFile }] = t.db + .select({ originalFile: schema.activity.originalFile }) + .from(schema.activity) + .where(eq(schema.activity.id, ride.id)) + .all(); + expect( + originalFile && existsSync(join(t.config.dataDir, originalFile)), + ).toBe(true); + + // A run deleted by hand stays deleted, whatever it becomes. + await send( + t.app, + t.cookie, + 'DELETE', + `/api/activities/${before.Deleted.id}`, + ); + activities[0].type = 'Ride'; + activities[1].type = 'Ride'; + const connection = await t.sync(); + expect(connection.syncs[0]).toMatchObject({ imported: 0, removed: 1 }); + // The upload and the real run are left alone. + expect(await t.runs()).toHaveLength(2); + expect(await byName()).toHaveProperty('Real run'); + expect(await byName()).not.toHaveProperty('Not a run'); + expect(t.db.select().from(schema.clubShare).all()).toEqual([]); + expect( + t.db + .select() + .from(schema.personalBest) + .where(eq(schema.personalBest.activityId, ride.id)) + .all(), + ).toEqual([]); + expect(existsSync(join(t.config.dataDir, originalFile ?? ''))).toBe(false); + + // Nothing more to remove next time. + expect((await t.sync()).syncs[0]).toMatchObject({ removed: 0 }); + + // Turned back into a run: imported again. The one deleted by hand isn't. + activities[0].type = 'Run'; + activities[1].type = 'Run'; + expect((await t.sync()).syncs[0]).toMatchObject({ imported: 1 }); + expect(await byName()).toHaveProperty('Not a run'); + expect(await byName()).not.toHaveProperty('Deleted'); + }); + it('keeps imported runs after disconnecting, and one account per person', async () => { const t = await connected([ { id: 'i1', start_date: daysAgo(1), type: 'Run' }, diff --git a/apps/api/src/integrations/sync.ts b/apps/api/src/integrations/sync.ts index d75f9ef..980f46a 100644 --- a/apps/api/src/integrations/sync.ts +++ b/apps/api/src/integrations/sync.ts @@ -1,4 +1,13 @@ -import { and, count, desc, eq, inArray, ne, notInArray } from 'drizzle-orm'; +import { + and, + count, + desc, + eq, + inArray, + isNotNull, + ne, + notInArray, +} from 'drizzle-orm'; import { ActivityFileError, parseActivityFile } from '@openrun/activity-files'; import { type Db, schema } from '@openrun/db'; import { defaultActivityName } from '@openrun/domain'; @@ -11,6 +20,7 @@ import { removeDataFile, writeDataFile, } from '../activity-store.js'; +import { withdrawShares } from '../clubs/shares.js'; import type { Config } from '../config.js'; import { getPrivacySettings } from '../privacy.js'; import { decryptSecret } from '../secrets.js'; @@ -43,7 +53,8 @@ const day = (d: Date) => d.toISOString().slice(0, 10); /** * Fetches runs from intervals.icu that haven't been seen before, oldest first, - * up to BATCH files, and imports them like uploads. Returns false if a sync + * up to BATCH files, and imports them like uploads. Runs that intervals.icu + * now lists as another sport are removed. Returns false if a sync * of this connection was already running. * * Everything since the start date is listed every time: runs can reach @@ -64,7 +75,13 @@ export async function syncIntegration( .values({ integrationId: integration.id, startedAt: now }) .returning() .all(); - const counts = { imported: 0, duplicates: 0, failed: 0, unavailable: 0 }; + const counts = { + imported: 0, + duplicates: 0, + failed: 0, + unavailable: 0, + removed: 0, + }; let more = false; let error: string | null = null; let status: Integration['status'] = 'ok'; @@ -83,6 +100,8 @@ export async function syncIntegration( day(new Date(now.getTime() + DAY_MS)), ); + counts.removed = removeNonRuns(db, config, integration, listed); + const seen = seenIds( db, integration.id, @@ -172,6 +191,67 @@ function seenIds(db: Db, integrationId: string, ids: string[]) { return seen; } +/** + * Removes runs imported earlier that intervals.icu now lists as another + * sport, such as a ride the watch recorded as a run. Other sports are never + * imported, so these go as if they'd never been runs, club shares included. + * Forgetting the item means that if it turns back into a run, the next sync + * imports it again. Runs deleted by hand stay deleted. + */ +function removeNonRuns( + db: Db, + config: Config, + integration: Integration, + listed: IntervalsActivity[], +) { + const notRuns = listed + .filter((a) => a.type && !RUN_TYPES.has(a.type)) + .map((a) => a.id); + let removed = 0; + // SQLite allows a limited number of variables per statement. + for (let i = 0; i < notRuns.length; i += 500) { + const items = db + .select({ + id: schema.integrationItem.id, + activityId: schema.integrationItem.activityId, + }) + .from(schema.integrationItem) + .where( + and( + eq(schema.integrationItem.integrationId, integration.id), + eq(schema.integrationItem.status, 'imported'), + isNotNull(schema.integrationItem.activityId), + inArray(schema.integrationItem.externalId, notRuns.slice(i, i + 500)), + ), + ) + .all(); + for (const item of items) { + const run = db + .select() + .from(schema.activity) + .where( + and( + eq(schema.activity.id, item.activityId ?? ''), + eq(schema.activity.userId, integration.userId), + eq(schema.activity.source, 'intervals'), + ), + ) + .get(); + if (!run) continue; + db.transaction((tx) => { + withdrawShares(db, run.userId, { sourceIds: [run.id] }); + tx.delete(schema.activity).where(eq(schema.activity.id, run.id)).run(); + tx.delete(schema.integrationItem) + .where(eq(schema.integrationItem.id, item.id)) + .run(); + }); + if (run.originalFile) removeDataFile(config.dataDir, run.originalFile); + removed++; + } + } + return removed; +} + async function importOne( db: Db, config: Config, @@ -325,6 +405,7 @@ export function describeConnection( duplicates: s.duplicates, failed: s.failed, unavailable: s.unavailable, + removed: s.removed, more: s.more, error: s.error, })), diff --git a/apps/web/src/components/intervals-settings.tsx b/apps/web/src/components/intervals-settings.tsx index 0dea03a..abcb468 100644 --- a/apps/web/src/components/intervals-settings.tsx +++ b/apps/web/src/components/intervals-settings.tsx @@ -284,6 +284,8 @@ export function describeSync(s: IntegrationSync) { parts.push( `${s.unavailable} from Strava, which intervals.icu can't pass on`, ); + if (s.removed) + parts.push(`${s.removed} removed, now another sport on intervals.icu`); if (s.error) parts.push(s.error); if (!parts.length) parts.push('Nothing new'); if (s.more) parts.push('more next time'); diff --git a/libs/db-migrations/migrations/0009_sync_removed.sql b/libs/db-migrations/migrations/0009_sync_removed.sql new file mode 100644 index 0000000..2015d09 --- /dev/null +++ b/libs/db-migrations/migrations/0009_sync_removed.sql @@ -0,0 +1 @@ +ALTER TABLE `integration_sync` ADD `removed` integer DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/libs/db-migrations/migrations/meta/0009_snapshot.json b/libs/db-migrations/migrations/meta/0009_snapshot.json new file mode 100644 index 0000000..9fb21c1 --- /dev/null +++ b/libs/db-migrations/migrations/meta/0009_snapshot.json @@ -0,0 +1,3147 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "12ca66d0-30f9-4bc6-af29-a6c6c02e1042", + "prevId": "75ff1472-b7c5-4bd5-a690-79a569e2effc", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_token": { + "name": "api_token", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'read'" + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "api_token_token_hash_unique": { + "name": "api_token_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "api_token_user_id_idx": { + "name": "api_token_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_token_user_id_user_id_fk": { + "name": "api_token_user_id_user_id_fk", + "tableFrom": "api_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_by": { + "name": "used_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_code_unique": { + "name": "invite_code_unique", + "columns": [ + "code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_created_by_user_id_fk": { + "name": "invite_created_by_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "invite_used_by_user_id_fk": { + "name": "invite_used_by_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "used_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "passkey": { + "name": "passkey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "passkey_user_id_idx": { + "name": "passkey_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "passkey_credential_id_idx": { + "name": "passkey_credential_id_idx", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "units": { + "name": "units", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'metric'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'UTC'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "activity": { + "name": "activity", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sport": { + "name": "sport", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'run'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distance_m": { + "name": "distance_m", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_s": { + "name": "duration_s", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "elapsed_s": { + "name": "elapsed_s", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "elevation_gain_m": { + "name": "elevation_gain_m", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avg_heart_rate": { + "name": "avg_heart_rate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_heart_rate": { + "name": "max_heart_rate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avg_cadence": { + "name": "avg_cadence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rpe": { + "name": "rpe", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shoe_id": { + "name": "shoe_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "original_file": { + "name": "original_file", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "activity_user_started_idx": { + "name": "activity_user_started_idx", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + }, + "activity_source_external_idx": { + "name": "activity_source_external_idx", + "columns": [ + "user_id", + "source", + "external_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "activity_user_id_user_id_fk": { + "name": "activity_user_id_user_id_fk", + "tableFrom": "activity", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "activity_shoe_id_shoe_id_fk": { + "name": "activity_shoe_id_shoe_id_fk", + "tableFrom": "activity", + "tableTo": "shoe", + "columnsFrom": [ + "shoe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "activity_stream": { + "name": "activity_stream", + "columns": { + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "activity_stream_activity_id_activity_id_fk": { + "name": "activity_stream_activity_id_activity_id_fk", + "tableFrom": "activity_stream", + "tableTo": "activity", + "columnsFrom": [ + "activity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lap": { + "name": "lap", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distance_m": { + "name": "distance_m", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_s": { + "name": "duration_s", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avg_heart_rate": { + "name": "avg_heart_rate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "elevation_gain_m": { + "name": "elevation_gain_m", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "lap_activity_id_idx": { + "name": "lap_activity_id_idx", + "columns": [ + "activity_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "lap_activity_id_activity_id_fk": { + "name": "lap_activity_id_activity_id_fk", + "tableFrom": "lap", + "tableTo": "activity", + "columnsFrom": [ + "activity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "personal_best": { + "name": "personal_best", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distance_m": { + "name": "distance_m", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_s": { + "name": "duration_s", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "achieved_at": { + "name": "achieved_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "personal_best_user_distance_idx": { + "name": "personal_best_user_distance_idx", + "columns": [ + "user_id", + "distance_m" + ], + "isUnique": false + }, + "personal_best_activity_distance_idx": { + "name": "personal_best_activity_distance_idx", + "columns": [ + "activity_id", + "distance_m" + ], + "isUnique": true + } + }, + "foreignKeys": { + "personal_best_user_id_user_id_fk": { + "name": "personal_best_user_id_user_id_fk", + "tableFrom": "personal_best", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "personal_best_activity_id_activity_id_fk": { + "name": "personal_best_activity_id_activity_id_fk", + "tableFrom": "personal_best", + "tableTo": "activity", + "columnsFrom": [ + "activity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shoe": { + "name": "shoe", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "starting_distance_m": { + "name": "starting_distance_m", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "retired_at": { + "name": "retired_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "shoe_user_id_idx": { + "name": "shoe_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shoe_user_id_user_id_fk": { + "name": "shoe_user_id_user_id_fk", + "tableFrom": "shoe", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "privacy_settings": { + "name": "privacy_settings", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "default_visibility": { + "name": "default_visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'private'" + }, + "share_map": { + "name": "share_map", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "privacy_settings_user_id_user_id_fk": { + "name": "privacy_settings_user_id_user_id_fk", + "tableFrom": "privacy_settings", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "privacy_zone": { + "name": "privacy_zone", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lat": { + "name": "lat", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lng": { + "name": "lng", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "radius_m": { + "name": "radius_m", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trim_lat": { + "name": "trim_lat", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trim_lng": { + "name": "trim_lng", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trim_radius_m": { + "name": "trim_radius_m", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "privacy_zone_user_id_idx": { + "name": "privacy_zone_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "privacy_zone_user_id_user_id_fk": { + "name": "privacy_zone_user_id_user_id_fk", + "tableFrom": "privacy_zone", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "club": { + "name": "club", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "join_code": { + "name": "join_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "require_approval": { + "name": "require_approval", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "logo_version": { + "name": "logo_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "club_join_code_unique": { + "name": "club_join_code_unique", + "columns": [ + "join_code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "club_created_by_user_id_fk": { + "name": "club_created_by_user_id_fk", + "tableFrom": "club", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "club_location": { + "name": "club_location", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "club_id": { + "name": "club_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lat": { + "name": "lat", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lng": { + "name": "lng", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "club_location_club_idx": { + "name": "club_location_club_idx", + "columns": [ + "club_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "club_location_club_id_club_id_fk": { + "name": "club_location_club_id_club_id_fk", + "tableFrom": "club_location", + "tableTo": "club", + "columnsFrom": [ + "club_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "club_member": { + "name": "club_member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "club_id": { + "name": "club_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "share_level": { + "name": "share_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'attendance'" + }, + "muted": { + "name": "muted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "highlights": { + "name": "highlights", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "club_member_club_user_idx": { + "name": "club_member_club_user_idx", + "columns": [ + "club_id", + "user_id" + ], + "isUnique": true + }, + "club_member_user_idx": { + "name": "club_member_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "club_member_club_id_club_id_fk": { + "name": "club_member_club_id_club_id_fk", + "tableFrom": "club_member", + "tableTo": "club", + "columnsFrom": [ + "club_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "club_member_user_id_user_id_fk": { + "name": "club_member_user_id_user_id_fk", + "tableFrom": "club_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "club_run": { + "name": "club_run", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "club_id": { + "name": "club_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "series_id": { + "name": "series_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "starts_at": { + "name": "starts_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order_cutoff_min": { + "name": "order_cutoff_min", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'scheduled'" + }, + "cancelled_reason": { + "name": "cancelled_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weather": { + "name": "weather", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weather_updated_at": { + "name": "weather_updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "club_run_club_starts_idx": { + "name": "club_run_club_starts_idx", + "columns": [ + "club_id", + "starts_at" + ], + "isUnique": false + }, + "club_run_series_starts_idx": { + "name": "club_run_series_starts_idx", + "columns": [ + "series_id", + "starts_at" + ], + "isUnique": true + } + }, + "foreignKeys": { + "club_run_club_id_club_id_fk": { + "name": "club_run_club_id_club_id_fk", + "tableFrom": "club_run", + "tableTo": "club", + "columnsFrom": [ + "club_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "club_run_series_id_club_run_series_id_fk": { + "name": "club_run_series_id_club_run_series_id_fk", + "tableFrom": "club_run", + "tableTo": "club_run_series", + "columnsFrom": [ + "series_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "club_run_location_id_club_location_id_fk": { + "name": "club_run_location_id_club_location_id_fk", + "tableFrom": "club_run", + "tableTo": "club_location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "club_run_series": { + "name": "club_run_series", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "club_id": { + "name": "club_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "starts_on": { + "name": "starts_on", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_time": { + "name": "start_time", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_id": { + "name": "location_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order_cutoff_min": { + "name": "order_cutoff_min", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "ended_at": { + "name": "ended_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "club_run_series_club_idx": { + "name": "club_run_series_club_idx", + "columns": [ + "club_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "club_run_series_club_id_club_id_fk": { + "name": "club_run_series_club_id_club_id_fk", + "tableFrom": "club_run_series", + "tableTo": "club", + "columnsFrom": [ + "club_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "club_run_series_location_id_club_location_id_fk": { + "name": "club_run_series_location_id_club_location_id_fk", + "tableFrom": "club_run_series", + "tableTo": "club_location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "club_share": { + "name": "club_share", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "club_id": { + "name": "club_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "club_run_id": { + "name": "club_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distance_m": { + "name": "distance_m", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_s": { + "name": "duration_s", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "track": { + "name": "track", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "club_share_club_source_idx": { + "name": "club_share_club_source_idx", + "columns": [ + "club_id", + "source_id" + ], + "isUnique": true + }, + "club_share_run_idx": { + "name": "club_share_run_idx", + "columns": [ + "club_run_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "club_share_club_id_club_id_fk": { + "name": "club_share_club_id_club_id_fk", + "tableFrom": "club_share", + "tableTo": "club", + "columnsFrom": [ + "club_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "club_share_user_id_user_id_fk": { + "name": "club_share_user_id_user_id_fk", + "tableFrom": "club_share", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "club_share_club_run_id_club_run_id_fk": { + "name": "club_share_club_run_id_club_run_id_fk", + "tableFrom": "club_share", + "tableTo": "club_run", + "columnsFrom": [ + "club_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "coffee_order": { + "name": "coffee_order", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "club_run_id": { + "name": "club_run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "menu_item_id": { + "name": "menu_item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item": { + "name": "item", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "choices": { + "name": "choices", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "coffee_order_run_user_idx": { + "name": "coffee_order_run_user_idx", + "columns": [ + "club_run_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "coffee_order_club_run_id_club_run_id_fk": { + "name": "coffee_order_club_run_id_club_run_id_fk", + "tableFrom": "coffee_order", + "tableTo": "club_run", + "columnsFrom": [ + "club_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coffee_order_user_id_user_id_fk": { + "name": "coffee_order_user_id_user_id_fk", + "tableFrom": "coffee_order", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coffee_order_menu_item_id_menu_item_id_fk": { + "name": "coffee_order_menu_item_id_menu_item_id_fk", + "tableFrom": "coffee_order", + "tableTo": "menu_item", + "columnsFrom": [ + "menu_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "coffee_usual": { + "name": "coffee_usual", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "club_id": { + "name": "club_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "menu_item_id": { + "name": "menu_item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "choices": { + "name": "choices", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "auto_add": { + "name": "auto_add", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "coffee_usual_club_user_idx": { + "name": "coffee_usual_club_user_idx", + "columns": [ + "club_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "coffee_usual_club_id_club_id_fk": { + "name": "coffee_usual_club_id_club_id_fk", + "tableFrom": "coffee_usual", + "tableTo": "club", + "columnsFrom": [ + "club_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coffee_usual_user_id_user_id_fk": { + "name": "coffee_usual_user_id_user_id_fk", + "tableFrom": "coffee_usual", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coffee_usual_menu_item_id_menu_item_id_fk": { + "name": "coffee_usual_menu_item_id_menu_item_id_fk", + "tableFrom": "coffee_usual", + "tableTo": "menu_item", + "columnsFrom": [ + "menu_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "menu_item": { + "name": "menu_item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "club_id": { + "name": "club_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "hidden": { + "name": "hidden", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "menu_item_club_idx": { + "name": "menu_item_club_idx", + "columns": [ + "club_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "menu_item_club_id_club_id_fk": { + "name": "menu_item_club_id_club_id_fk", + "tableFrom": "menu_item", + "tableTo": "club", + "columnsFrom": [ + "club_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rsvp": { + "name": "rsvp", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "club_run_id": { + "name": "club_run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rsvp_run_user_idx": { + "name": "rsvp_run_user_idx", + "columns": [ + "club_run_id", + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rsvp_club_run_id_club_run_id_fk": { + "name": "rsvp_club_run_id_club_run_id_fk", + "tableFrom": "rsvp", + "tableTo": "club_run", + "columnsFrom": [ + "club_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rsvp_user_id_user_id_fk": { + "name": "rsvp_user_id_user_id_fk", + "tableFrom": "rsvp", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_sent": { + "name": "notification_sent", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notification_sent_user_key_idx": { + "name": "notification_sent_user_key_idx", + "columns": [ + "user_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "notification_sent_user_id_user_id_fk": { + "name": "notification_sent_user_id_user_id_fk", + "tableFrom": "notification_sent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "run_reminder": { + "name": "run_reminder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "coffee_nudge": { + "name": "coffee_nudge", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "run_changes": { + "name": "run_changes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "join_requests": { + "name": "join_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_settings_user_id_user_id_fk": { + "name": "notification_settings_user_id_user_id_fk", + "tableFrom": "notification_settings", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "push_subscription": { + "name": "push_subscription", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "push_subscription_endpoint_unique": { + "name": "push_subscription_endpoint_unique", + "columns": [ + "endpoint" + ], + "isUnique": true + } + }, + "foreignKeys": { + "push_subscription_user_id_user_id_fk": { + "name": "push_subscription_user_id_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_name": { + "name": "account_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credentials": { + "name": "credentials", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_from": { + "name": "sync_from", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ok'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_user_provider_idx": { + "name": "integration_user_provider_idx", + "columns": [ + "user_id", + "provider" + ], + "isUnique": true + }, + "integration_account_idx": { + "name": "integration_account_idx", + "columns": [ + "provider", + "account_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "integration_user_id_user_id_fk": { + "name": "integration_user_id_user_id_fk", + "tableFrom": "integration", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_item_external_idx": { + "name": "integration_item_external_idx", + "columns": [ + "integration_id", + "external_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_activity_id_activity_id_fk": { + "name": "integration_item_activity_id_activity_id_fk", + "tableFrom": "integration_item", + "tableTo": "activity", + "columnsFrom": [ + "activity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration_sync": { + "name": "integration_sync", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imported": { + "name": "imported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicates": { + "name": "duplicates", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "failed": { + "name": "failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "unavailable": { + "name": "unavailable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "removed": { + "name": "removed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "more": { + "name": "more", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "integration_sync_idx": { + "name": "integration_sync_idx", + "columns": [ + "integration_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integration_sync_integration_id_integration_id_fk": { + "name": "integration_sync_integration_id_integration_id_fk", + "tableFrom": "integration_sync", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/libs/db-migrations/migrations/meta/_journal.json b/libs/db-migrations/migrations/meta/_journal.json index 21649f0..2574557 100644 --- a/libs/db-migrations/migrations/meta/_journal.json +++ b/libs/db-migrations/migrations/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1793369136922, "tag": "0008_club_logo", "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1793454385270, + "tag": "0009_sync_removed", + "breakpoints": true } ] } \ No newline at end of file diff --git a/libs/db/src/schema/integrations.ts b/libs/db/src/schema/integrations.ts index 4fe0029..40ca449 100644 --- a/libs/db/src/schema/integrations.ts +++ b/libs/db/src/schema/integrations.ts @@ -91,6 +91,8 @@ export const integrationSync = sqliteTable( failed: integer('failed').notNull().default(0), /** Activities the provider won't hand over, e.g. ones that came from Strava. */ unavailable: integer('unavailable').notNull().default(0), + /** Runs removed because the provider now lists them as another sport. */ + removed: integer('removed').notNull().default(0), /** More runs are waiting; the next sync carries on. */ more: integer('more', { mode: 'boolean' }).notNull().default(false), error: text('error'), diff --git a/libs/schemas/src/integrations.ts b/libs/schemas/src/integrations.ts index 58ebd5d..310d2e9 100644 --- a/libs/schemas/src/integrations.ts +++ b/libs/schemas/src/integrations.ts @@ -10,6 +10,8 @@ export const integrationSyncSchema = z.object({ failed: z.number(), /** Activities the provider won't hand over, e.g. ones from Strava. */ unavailable: z.number(), + /** Runs removed because the provider now lists them as another sport. */ + removed: z.number(), /** More runs are waiting; the next sync carries on. */ more: z.boolean(), error: z.string().nullable(),