Add sync_pushes table and granted_scopes column
sync_pushes tracks outbound route pushes to providers, keyed by (user_id, route_id, route_version, provider). Successful rows hold the remote_id; failed rows hold the error and can be retried in place. The unique index makes idempotent push trivial. granted_scopes records the OAuth scope set we requested at exchangeCode time. Wahoo doesn't return a scope field in token responses and grants scopes all-or-nothing, so the requested set is the source of truth. Defaults to an empty array, which means any pre-existing connection will be flagged as scope-mismatched on the first routes_write push — the intended UX for the slice 3 re-auth flow. Adjusts tasks 2.3/2.4 in the spec to match repo reality: this project runs `drizzle-kit push --force` schema-first, with no checked-in migrations directory. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8ba5554a67
commit
c7a09e865d
2 changed files with 47 additions and 4 deletions
|
|
@ -11,10 +11,10 @@
|
|||
|
||||
## 2. Database schema for push tracking
|
||||
|
||||
- [ ] 2.1 Add `sync_pushes` table to `packages/db/src/schema/journal.ts` with columns from design.md §3 and a unique index on `(user_id, route_id, route_version, provider)`
|
||||
- [ ] 2.2 Add a `granted_scopes text[]` column to `sync_connections` in the same schema file, defaulting to an empty array, and backfill existing rows with the legacy scopes (`workouts_read`, `user_read`, `offline_data`)
|
||||
- [ ] 2.3 Generate the Drizzle migration via `pnpm --filter @trails-cool/db db:generate` (migration files land in `packages/db/migrations/`)
|
||||
- [ ] 2.4 Apply locally via `pnpm db:push` and verify with `pnpm db:studio`
|
||||
- [x] 2.1 Add `sync_pushes` table to `packages/db/src/schema/journal.ts` with columns from design.md §3 and a unique index on `(user_id, route_id, route_version, provider)`
|
||||
- [x] 2.2 Add a `granted_scopes text[]` column to `sync_connections` in the same schema file, defaulting to an empty array. (Backfill happens at `exchangeCode` time in slice 3 — pre-existing connections will be recognized as scope-mismatched on first push and trigger re-auth, which is the intended UX.)
|
||||
- [x] 2.3 ~~Generate Drizzle migration files~~ — N/A: this repo runs `drizzle-kit push --force` schema-first, no checked-in `migrations/` directory. The schema edit is the migration source.
|
||||
- [ ] 2.4 Apply locally via `pnpm db:push` against a running dev Postgres and verify with `pnpm db:studio` — deferred: requires `pnpm dev:services` (Docker) which isn't running in this workspace. CI applies on deploy.
|
||||
|
||||
## 3. Wahoo provider scope upgrade
|
||||
|
||||
|
|
|
|||
|
|
@ -196,6 +196,15 @@ export const syncConnections = journalSchema.table("sync_connections", {
|
|||
refreshToken: text("refresh_token").notNull(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
providerUserId: text("provider_user_id"),
|
||||
// Scopes the user actually granted at OAuth time. Wahoo doesn't return a
|
||||
// scope field in token responses and grants scopes all-or-nothing, so we
|
||||
// record the requested scope set on exchangeCode. Used to detect when an
|
||||
// existing connection predates a scope upgrade (e.g. routes_write) and
|
||||
// needs a re-auth before a new push call can succeed.
|
||||
grantedScopes: text("granted_scopes")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
|
|
@ -210,6 +219,40 @@ export const syncImports = journalSchema.table("sync_imports", {
|
|||
importedAt: timestamp("imported_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// Tracks outbound route pushes to external providers (Wahoo today). One row
|
||||
// per (user, route, version, provider) — push idempotency lives here. A row
|
||||
// with `pushedAt` set means we have a confirmed remote_id and won't re-call
|
||||
// the provider; a row with `error` set and no `pushedAt` is a failed attempt
|
||||
// that can be retried in place.
|
||||
export const syncPushes = journalSchema.table(
|
||||
"sync_pushes",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
routeId: text("route_id")
|
||||
.notNull()
|
||||
.references(() => routes.id, { onDelete: "cascade" }),
|
||||
routeVersion: integer("route_version").notNull(),
|
||||
provider: text("provider").notNull(),
|
||||
externalId: text("external_id").notNull(),
|
||||
remoteId: text("remote_id"),
|
||||
pushedAt: timestamp("pushed_at", { withTimezone: true }),
|
||||
error: text("error"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => ({
|
||||
userRouteVersionProviderUnique: uniqueIndex("sync_pushes_user_route_version_provider_unique").on(
|
||||
t.userId,
|
||||
t.routeId,
|
||||
t.routeVersion,
|
||||
t.provider,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
// Social follow relation. Always originates from a local user (`followerId`).
|
||||
// The followed side is keyed by an actor IRI for federation forward-compat —
|
||||
// today every IRI is local (`https://{DOMAIN}/users/{username}`); future
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue