From efa43a60cacc557494ee5fec518cbbce91f94aa7 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Fri, 22 May 2026 16:00:36 +0545 Subject: [PATCH] feat(OUT-3645): add pending_action tombstone columns to file_folder_sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additive migration. Adds pending_action, pending_action_target, pending_action_attempts, pending_action_last_attempt_at, and pending_action_last_error columns plus a CHECK constraint enforcing that pending_action and pending_action_target are both set or both null. Also gitignores docs/ for spec and plan artifacts. Subsequent PRs populate these columns from per-file handlers and read from the resync sweeper. PR 1 is observation-only — no code reads or writes the new columns yet. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 2 + src/db/constants.ts | 14 + ...ing_action_columns_to_file_folder_sync.sql | 8 + .../meta/20260522104036_snapshot.json | 570 ++++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + src/db/schema/fileFolderSync.schema.ts | 66 +- 6 files changed, 648 insertions(+), 19 deletions(-) create mode 100644 src/db/migrations/20260522104036_add_pending_action_columns_to_file_folder_sync.sql create mode 100644 src/db/migrations/meta/20260522104036_snapshot.json diff --git a/.gitignore b/.gitignore index 0fea7c3..2c253e1 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ next-env.d.ts /.vscode/ # Sentry Config File .env.sentry-build-plugin + +docs/ diff --git a/src/db/constants.ts b/src/db/constants.ts index da90054..f735f03 100644 --- a/src/db/constants.ts +++ b/src/db/constants.ts @@ -9,3 +9,17 @@ export enum DropboxClientType { NAMESPACE_ID = 'namespace_id', } export type DropboxClientTypeValue = (typeof DropboxClientType)[keyof typeof DropboxClientType] + +export enum PendingAction { + DELETE = 'delete', + CREATE = 'create', + UPDATE = 'update', +} +export type PendingActionValue = (typeof PendingAction)[keyof typeof PendingAction] + +export enum PendingActionTarget { + ASSEMBLY = 'assembly', + DROPBOX = 'dropbox', +} +export type PendingActionTargetValue = + (typeof PendingActionTarget)[keyof typeof PendingActionTarget] diff --git a/src/db/migrations/20260522104036_add_pending_action_columns_to_file_folder_sync.sql b/src/db/migrations/20260522104036_add_pending_action_columns_to_file_folder_sync.sql new file mode 100644 index 0000000..6e675cb --- /dev/null +++ b/src/db/migrations/20260522104036_add_pending_action_columns_to_file_folder_sync.sql @@ -0,0 +1,8 @@ +CREATE TYPE "public"."pending_action_enum" AS ENUM('delete', 'create', 'update'); +CREATE TYPE "public"."pending_action_target_enum" AS ENUM('assembly', 'dropbox'); +ALTER TABLE "file_folder_sync" ADD COLUMN "pending_action" "pending_action_enum"; +ALTER TABLE "file_folder_sync" ADD COLUMN "pending_action_target" "pending_action_target_enum"; +ALTER TABLE "file_folder_sync" ADD COLUMN "pending_action_attempts" integer DEFAULT 0 NOT NULL; +ALTER TABLE "file_folder_sync" ADD COLUMN "pending_action_last_attempt_at" timestamp with time zone; +ALTER TABLE "file_folder_sync" ADD COLUMN "pending_action_last_error" text; +ALTER TABLE "file_folder_sync" ADD CONSTRAINT "file_folder_sync_pending_action_target_consistency" CHECK (("file_folder_sync"."pending_action" IS NULL) = ("file_folder_sync"."pending_action_target" IS NULL)); \ No newline at end of file diff --git a/src/db/migrations/meta/20260522104036_snapshot.json b/src/db/migrations/meta/20260522104036_snapshot.json new file mode 100644 index 0000000..8a2d0df --- /dev/null +++ b/src/db/migrations/meta/20260522104036_snapshot.json @@ -0,0 +1,570 @@ +{ + "id": "537b0c47-fe56-4863-8ab3-cd96ee95c925", + "prevId": "21203760-221f-4020-bf52-7d6249e36430", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.assembly_webhook_records": { + "name": "assembly_webhook_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portal_id": { + "name": "portal_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "assembly_webhook_events_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "assembly_channel_id": { + "name": "assembly_channel_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_all_columns_combined": { + "name": "uq_all_columns_combined", + "columns": [ + { + "expression": "portal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assembly_channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "triggered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channel_sync": { + "name": "channel_sync", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portal_id": { + "name": "portal_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "dbx_account_id": { + "name": "dbx_account_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "assembly_channel_id": { + "name": "assembly_channel_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "dbx_root_path": { + "name": "dbx_root_path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "dbx_root_id": { + "name": "dbx_root_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "dbx_cursor": { + "name": "dbx_cursor", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "total_files_count": { + "name": "total_files_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "synced_files_count": { + "name": "synced_files_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_channel_sync_portal_id_dbxAccount_id_deleted_at": { + "name": "idx_channel_sync_portal_id_dbxAccount_id_deleted_at", + "columns": [ + { + "expression": "portal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dbx_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_channel_sync__channel_id_dbx_root_path": { + "name": "uq_channel_sync__channel_id_dbx_root_path", + "columns": [ + { + "expression": "assembly_channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dbx_root_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"channel_sync\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_channel_sync_portal_id_deleted_at_created_at": { + "name": "idx_channel_sync_portal_id_deleted_at_created_at", + "columns": [ + { + "expression": "portal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dropbox_connections": { + "name": "dropbox_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portal_id": { + "name": "portal_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "root_namespace_id": { + "name": "root_namespace_id", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pending_webhook": { + "name": "pending_webhook", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_webhook_synced_at": { + "name": "last_webhook_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_webhook_sync_started_at": { + "name": "last_webhook_sync_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "initiated_by": { + "name": "initiated_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_dropbox_connections_portal_id": { + "name": "uq_dropbox_connections_portal_id", + "columns": [ + { + "expression": "portal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_folder_sync": { + "name": "file_folder_sync", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portal_id": { + "name": "portal_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "channel_sync_id": { + "name": "channel_sync_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "item_path": { + "name": "item_path", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "object": { + "name": "object", + "type": "object_types", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'file'" + }, + "content_hash": { + "name": "content_hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "dbx_file_id": { + "name": "dbx_file_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "assembly_file_id": { + "name": "assembly_file_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pending_action": { + "name": "pending_action", + "type": "pending_action_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "pending_action_target": { + "name": "pending_action_target", + "type": "pending_action_target_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "pending_action_attempts": { + "name": "pending_action_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pending_action_last_attempt_at": { + "name": "pending_action_last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pending_action_last_error": { + "name": "pending_action_last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "file_folder_sync_channel_sync_id_channel_sync_id_fk": { + "name": "file_folder_sync_channel_sync_id_channel_sync_id_fk", + "tableFrom": "file_folder_sync", + "tableTo": "channel_sync", + "columnsFrom": ["channel_sync_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_folder_sync_pending_action_target_consistency": { + "name": "file_folder_sync_pending_action_target_consistency", + "value": "(\"file_folder_sync\".\"pending_action\" IS NULL) = (\"file_folder_sync\".\"pending_action_target\" IS NULL)" + } + }, + "isRLSEnabled": false + } + }, + "enums": { + "public.assembly_webhook_events_enum": { + "name": "assembly_webhook_events_enum", + "schema": "public", + "values": [ + "file.created", + "file.updated", + "file.deleted", + "folder.created", + "folder.updated", + "folder.deleted" + ] + }, + "public.object_types": { + "name": "object_types", + "schema": "public", + "values": ["file", "folder"] + }, + "public.pending_action_enum": { + "name": "pending_action_enum", + "schema": "public", + "values": ["delete", "create", "update"] + }, + "public.pending_action_target_enum": { + "name": "pending_action_target_enum", + "schema": "public", + "values": ["assembly", "dropbox"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 2aeffc7..8dc11cf 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1774267912585, "tag": "20260323121152_add_webhook_debounce_columns_to_dropbox_connections", "breakpoints": false + }, + { + "idx": 11, + "version": "7", + "when": 1779446436504, + "tag": "20260522104036_add_pending_action_columns_to_file_folder_sync", + "breakpoints": false } ] } diff --git a/src/db/schema/fileFolderSync.schema.ts b/src/db/schema/fileFolderSync.schema.ts index 41d811c..83c1ee9 100644 --- a/src/db/schema/fileFolderSync.schema.ts +++ b/src/db/schema/fileFolderSync.schema.ts @@ -1,29 +1,57 @@ -import { type InferInsertModel, type InferSelectModel, relations } from 'drizzle-orm' -import { pgEnum, pgTable, uuid, varchar } from 'drizzle-orm/pg-core' +import { type InferInsertModel, type InferSelectModel, relations, sql } from 'drizzle-orm' +import { + check, + integer, + pgEnum, + pgTable, + text, + timestamp, + uuid, + varchar, +} from 'drizzle-orm/pg-core' import { createInsertSchema, createUpdateSchema } from 'drizzle-zod' import type z from 'zod' -import { ObjectType } from '@/db/constants' +import { ObjectType, PendingAction, PendingActionTarget } from '@/db/constants' import { enumToPgEnum, timestampsWithSoftDelete } from '@/db/db.helpers' import { channelSync } from '@/db/schema/channelSync.schema' export const ObjectEnum = pgEnum('object_types', enumToPgEnum(ObjectType)) +export const PendingActionEnum = pgEnum('pending_action_enum', enumToPgEnum(PendingAction)) +export const PendingActionTargetEnum = pgEnum( + 'pending_action_target_enum', + enumToPgEnum(PendingActionTarget), +) -export const fileFolderSync = pgTable('file_folder_sync', { - id: uuid().primaryKey().notNull().defaultRandom(), - portalId: varchar({ length: 32 }).notNull(), // Workspace ID / Portal ID in Copilot - channelSyncId: uuid() - .notNull() - .references(() => channelSync.id, { - onDelete: 'cascade', - onUpdate: 'cascade', - }), - itemPath: varchar(), - object: ObjectEnum().default(ObjectType.FILE).notNull(), - contentHash: varchar(), - dbxFileId: varchar(), - assemblyFileId: uuid(), - ...timestampsWithSoftDelete, -}) +export const fileFolderSync = pgTable( + 'file_folder_sync', + { + id: uuid().primaryKey().notNull().defaultRandom(), + portalId: varchar({ length: 32 }).notNull(), // Workspace ID / Portal ID in Copilot + channelSyncId: uuid() + .notNull() + .references(() => channelSync.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), + itemPath: varchar(), + object: ObjectEnum().default(ObjectType.FILE).notNull(), + contentHash: varchar(), + dbxFileId: varchar(), + assemblyFileId: uuid(), + pendingAction: PendingActionEnum(), + pendingActionTarget: PendingActionTargetEnum(), + pendingActionAttempts: integer().notNull().default(0), + pendingActionLastAttemptAt: timestamp({ withTimezone: true, mode: 'date' }), + pendingActionLastError: text(), + ...timestampsWithSoftDelete, + }, + (table) => [ + check( + 'file_folder_sync_pending_action_target_consistency', + sql`(${table.pendingAction} IS NULL) = (${table.pendingActionTarget} IS NULL)`, + ), + ], +) export const FileSyncRelations = relations(fileFolderSync, ({ one }) => ({ channel: one(channelSync, {