diff --git a/apps/server/migrations/20261112000025_sync_copy_sources_data/migration.sql b/apps/server/migrations/20261112000025_sync_copy_sources_data/migration.sql new file mode 100644 index 000000000..12ef380ed --- /dev/null +++ b/apps/server/migrations/20261112000025_sync_copy_sources_data/migration.sql @@ -0,0 +1,11 @@ +-- Custom SQL: Drizzle cannot generate a data migration (data D5). O Sync de qualquer conector, U3: each +-- copy of today gets its one source, its connection with that connection's account, and the pass, its +-- start, its position (the last key written becomes a KEY position) and its watermark as the copy kept +-- them. A second run inserts nothing. +INSERT INTO copies.copy_source (company_id, copy_id, connector_id, connection_id, account_ref, pass, pass_started_at, position, watermark, created_at) +SELECT c.company_id, c.id, c.connector_id, c.connection_id, n.account_ref, c.pass, c.pass_started_at, + CASE WHEN c.position IS NULL THEN NULL ELSE jsonb_build_object('kind', 'KEY', 'after', c.position) END, + c.watermark, c.created_at +FROM copies.copy c +JOIN connectors.connection n ON n.company_id = c.company_id AND n.connector_id = c.connector_id AND n.id = c.connection_id +WHERE NOT EXISTS (SELECT 1 FROM copies.copy_source s WHERE s.copy_id = c.id); diff --git a/apps/server/migrations/20261112000025_sync_copy_sources_data/snapshot.json b/apps/server/migrations/20261112000025_sync_copy_sources_data/snapshot.json new file mode 100644 index 000000000..e32864442 --- /dev/null +++ b/apps/server/migrations/20261112000025_sync_copy_sources_data/snapshot.json @@ -0,0 +1,9539 @@ +{ + "id": "54851667-1821-40c5-8664-24ec21cf603e", + "prevIds": ["a3baed99-717a-4048-a477-9d35df6a0cee"], + "version": "8", + "dialect": "postgres", + "ddl": [ + { + "name": "builder", + "entityType": "schemas" + }, + { + "name": "apps", + "entityType": "schemas" + }, + { + "name": "connectors", + "entityType": "schemas" + }, + { + "name": "copies", + "entityType": "schemas" + }, + { + "name": "auth", + "entityType": "schemas" + }, + { + "name": "notifications", + "entityType": "schemas" + }, + { + "name": "operations", + "entityType": "schemas" + }, + { + "name": "projects", + "entityType": "schemas" + }, + { + "name": "requests", + "entityType": "schemas" + }, + { + "name": "hub", + "entityType": "schemas" + }, + { + "isRlsEnabled": true, + "name": "agent", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "agent_conversation", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "agent_tool", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "agent_version", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "agent_version_tool", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "assistant_conversation", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "conversation", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "company_account_kind", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "company_model", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "company_model_fallback", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "model_account", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "model_account_assignee", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "model_account_sign_in", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "observational_memory_models", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "person_model_default", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "tool", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "tool_version", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "app", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": false, + "name": "app_session", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": true, + "name": "app_version", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": true, + "name": "blob", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": false, + "name": "handoff", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": true, + "name": "version_file", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": true, + "name": "connection", + "entityType": "tables", + "schema": "connectors" + }, + { + "isRlsEnabled": true, + "name": "connection_sign_in", + "entityType": "tables", + "schema": "connectors" + }, + { + "isRlsEnabled": true, + "name": "connector_write", + "entityType": "tables", + "schema": "connectors" + }, + { + "isRlsEnabled": true, + "name": "project_connection", + "entityType": "tables", + "schema": "connectors" + }, + { + "isRlsEnabled": true, + "name": "copy", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_proposal", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_proposal_source", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_run", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_source", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_table_left", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "project_copy", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "company", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "invitation", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "member", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "organization_role", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": false, + "name": "rate_limit", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "session", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "user", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "notification", + "entityType": "tables", + "schema": "notifications" + }, + { + "isRlsEnabled": true, + "name": "request_notice", + "entityType": "tables", + "schema": "notifications" + }, + { + "isRlsEnabled": true, + "name": "client", + "entityType": "tables", + "schema": "operations" + }, + { + "isRlsEnabled": true, + "name": "app_access", + "entityType": "tables", + "schema": "projects" + }, + { + "isRlsEnabled": true, + "name": "company_database", + "entityType": "tables", + "schema": "projects" + }, + { + "isRlsEnabled": true, + "name": "project", + "entityType": "tables", + "schema": "projects" + }, + { + "isRlsEnabled": true, + "name": "request", + "entityType": "tables", + "schema": "requests" + }, + { + "isRlsEnabled": false, + "name": "starts", + "entityType": "tables", + "schema": "hub" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "draft", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_version", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_by", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_run_at", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "draft", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_version_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "approval", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_revision", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "published_by", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "published_at", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_version_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "approval", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "connections", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "company_account_kind" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "builder", + "table": "company_account_kind" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "company_model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "builder", + "table": "company_model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'off'", + "generated": null, + "identity": null, + "name": "effort", + "entityType": "columns", + "schema": "builder", + "table": "company_model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "chain", + "entityType": "columns", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sealed", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hint", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linked_by", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linked_at", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "limited_until", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_until", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "served_models", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "served_at", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "owner", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sealed", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "statement_timestamp()", + "generated": null, + "identity": null, + "name": "poll_at", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "observer", + "entityType": "columns", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reflector", + "entityType": "columns", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "person_model_default" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "person_model_default" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "builder", + "table": "person_model_default" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "effort", + "entityType": "columns", + "schema": "builder", + "table": "person_model_default" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'PRESET'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operation", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "current_version", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_id", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "live_version_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "published_version_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sending_version_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimed_until", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "database_ready", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_hash", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host_label", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hub_session_id", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failure", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "manifest", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "digest", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sent_by", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "blob" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sha256", + "entityType": "columns", + "schema": "apps", + "table": "blob" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bytes", + "entityType": "columns", + "schema": "apps", + "table": "blob" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code_hash", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "binding_hash", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host_label", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hub_session_id", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version_id", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sha256", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "media_type", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_version", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sealed", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linked_by", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linked_at", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tested_at", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tested_by", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_test", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "notes", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_until", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_ref", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_label", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_revision", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sealed", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thread_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_call_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operation_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input_hash", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version_number", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "answer", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "approved_by", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "exclusive", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "granted_by", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "granted_at", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "definition", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claim_token", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimed_until", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pass", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pass_started_at", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "watermark", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rows", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_run_id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "definition", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proposed_by", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proved_rows", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proved_more", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proved_calls", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proposed_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proposal_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "started_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ended_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outcome", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pages", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "read", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "written", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_ref", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pass", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pass_started_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "watermark", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wait_until", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_table_left" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_table_left" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "copies", + "table": "copy_table_left" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "granted_by", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "granted_at", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleting_since", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nickname", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "app_role", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mailed_at", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "superseded_at", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "replaces", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permission", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "auth", + "table": "rate_limit" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "count", + "entityType": "columns", + "schema": "auth", + "table": "rate_limit" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "auth", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_organization_id", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "impersonated_by", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "banned", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ban_reason", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ban_expires", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automation_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automation_name", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversation_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failure_code", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "run_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "read_at", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_id", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "read_at", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cnpj", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "legal_name", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "registered_at", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "statement_timestamp()", + "generated": null, + "identity": null, + "name": "granted_at", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "neon_project_id", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimed_until", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host_label", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleting_since", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "requested_by", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "decided_by", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "always", + "name": "starts_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "hub", + "table": "starts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "hub", + "table": "starts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "started_at", + "entityType": "columns", + "schema": "hub", + "table": "starts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lower(\"name\" collate \"und-x-icu\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connection_name_key", + "entityType": "indexes", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "account_ref", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"account_ref\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "connection_account_key", + "entityType": "indexes", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"exclusive\"", + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_connection_one_key", + "entityType": "indexes", + "schema": "connectors", + "table": "project_connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"state\" = 'OPEN'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "copy_proposal_open_name_key", + "entityType": "indexes", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "copy_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"ended_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "copy_run_open_key", + "entityType": "indexes", + "schema": "copies", + "table": "copy_run" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "copy_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "started_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "copy_run_copy_started_idx", + "entityType": "indexes", + "schema": "copies", + "table": "copy_run" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_user_id_idx", + "entityType": "indexes", + "schema": "auth", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lower(\"email\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "coalesce(\"project_id\", '')", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_pending_email_key", + "entityType": "indexes", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_user_id_idx", + "entityType": "indexes", + "schema": "auth", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_role_company_id_role_key", + "entityType": "indexes", + "schema": "auth", + "table": "organization_role" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "attempt_key", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_role_company_id_attempt_key_key", + "entityType": "indexes", + "schema": "auth", + "table": "organization_role" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_user_id_idx", + "entityType": "indexes", + "schema": "auth", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "auth", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "person_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_person_created", + "entityType": "indexes", + "schema": "notifications", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "person_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "request_notice_person_created", + "entityType": "indexes", + "schema": "notifications", + "table": "request_notice" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lower(\"name\" collate \"und-x-icu\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_company_name_key", + "entityType": "indexes", + "schema": "projects", + "table": "project" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"state\" = 'PENDING'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "request_open_key", + "entityType": "indexes", + "schema": "requests", + "table": "request" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_project_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id", "active_version"], + "schemaTo": "builder", + "tableTo": "agent_version", + "columnsTo": ["company_id", "agent_id", "number"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "agent_active_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_conversation_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id"], + "schemaTo": "builder", + "tableTo": "agent", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_conversation_agent_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_conversation_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id"], + "schemaTo": "builder", + "tableTo": "agent", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_tool_agent_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_tool" + }, + { + "nameExplicit": true, + "columns": ["tool_id", "tool_version_id"], + "schemaTo": "builder", + "tableTo": "tool_version", + "columnsTo": ["tool_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "agent_tool_tool_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "tool_version_id"], + "schemaTo": "builder", + "tableTo": "tool_version", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "agent_tool_company_tool_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id"], + "schemaTo": "builder", + "tableTo": "agent", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_version_agent_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id", "number"], + "schemaTo": "builder", + "tableTo": "agent_version", + "columnsTo": ["company_id", "agent_id", "number"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_version_tool_agent_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "tool_version_id"], + "schemaTo": "builder", + "tableTo": "tool_version", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "agent_version_tool_tool_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "assistant_conversation_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "assistant_conversation_project_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "assistant_conversation_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "conversation_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "conversation" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "conversation_project_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "conversation_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "company_account_kind_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "company_account_kind" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "company_model_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "company_model" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "company_model_fallback_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account" + }, + { + "nameExplicit": true, + "columns": ["account_id"], + "schemaTo": "builder", + "tableTo": "model_account", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_assignee_account_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_assignee_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_sign_in_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "observational_memory_models_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "person_model_default_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "person_model_default" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tool_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "tool_id"], + "schemaTo": "builder", + "tableTo": "tool", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tool_version_tool_fkey", + "entityType": "fks", + "schema": "builder", + "table": "tool_version" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "app_project_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app" + }, + { + "nameExplicit": true, + "columns": ["project_id", "live_version_id"], + "schemaTo": "apps", + "tableTo": "app_version", + "columnsTo": ["project_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "app_live_version_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app" + }, + { + "nameExplicit": true, + "columns": ["project_id", "published_version_id"], + "schemaTo": "apps", + "tableTo": "app_version", + "columnsTo": ["project_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "app_published_version_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app" + }, + { + "nameExplicit": true, + "columns": ["project_id", "sending_version_id"], + "schemaTo": "apps", + "tableTo": "app_version", + "columnsTo": ["project_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "app_sending_version_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app" + }, + { + "nameExplicit": true, + "columns": ["hub_session_id"], + "schemaTo": "auth", + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "app_session_hub_session_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app_session" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "apps", + "tableTo": "app", + "columnsTo": ["project_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "app_version_project_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app_version" + }, + { + "nameExplicit": true, + "columns": ["hub_session_id"], + "schemaTo": "auth", + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "handoff_hub_session_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "handoff" + }, + { + "nameExplicit": true, + "columns": ["version_id"], + "schemaTo": "apps", + "tableTo": "app_version", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "version_file_version_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "version_file" + }, + { + "nameExplicit": true, + "columns": ["company_id", "sha256"], + "schemaTo": "apps", + "tableTo": "blob", + "columnsTo": ["company_id", "sha256"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "version_file_blob_fkey", + "entityType": "fks", + "schema": "apps", + "table": "version_file" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connection_company_id_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connection_sign_in_member_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connection_sign_in_company_id_connector_id_connection_id_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_write_company_id_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "connector_write" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "project_connection_connection_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "project_connection" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "project_connection_project_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "project_connection" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_company_id_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_connection_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_company_id_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_connection_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_project_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": ["copy_id"], + "schemaTo": "copies", + "tableTo": "copy", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "copy_proposal_copy_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": ["proposal_id"], + "schemaTo": "copies", + "tableTo": "copy_proposal", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_source_proposal_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_source_connection_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "nameExplicit": true, + "columns": ["company_id", "copy_id"], + "schemaTo": "copies", + "tableTo": "copy", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_run_copy_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_run" + }, + { + "nameExplicit": true, + "columns": ["company_id", "copy_id"], + "schemaTo": "copies", + "tableTo": "copy", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_source_copy_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_source" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_source_connection_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_source" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_table_left_company_id_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_table_left" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "project_copy_project_fkey", + "entityType": "fks", + "schema": "copies", + "table": "project_copy" + }, + { + "nameExplicit": true, + "columns": ["company_id", "copy_id"], + "schemaTo": "copies", + "tableTo": "copy", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "project_copy_copy_fkey", + "entityType": "fks", + "schema": "copies", + "table": "project_copy" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "account" + }, + { + "nameExplicit": false, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "invitation_company_id_company_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["inviter_id"], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "invitation_inviter_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "member_company_id_company_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "member_user_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "organization_role_company_id_company_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "organization_role" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "session" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_company_id_fkey", + "entityType": "fks", + "schema": "notifications", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_member_fkey", + "entityType": "fks", + "schema": "notifications", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_notice_member_fkey", + "entityType": "fks", + "schema": "notifications", + "table": "request_notice" + }, + { + "nameExplicit": true, + "columns": ["request_id"], + "schemaTo": "requests", + "tableTo": "request", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_notice_request_id_fkey", + "entityType": "fks", + "schema": "notifications", + "table": "request_notice" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "app_access_company_id_project_id_fkey", + "entityType": "fks", + "schema": "projects", + "table": "app_access" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_company_id_fkey", + "entityType": "fks", + "schema": "requests", + "table": "request" + }, + { + "nameExplicit": true, + "columns": ["company_id", "requested_by"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_member_fkey", + "entityType": "fks", + "schema": "requests", + "table": "request" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_project_id_fkey", + "entityType": "fks", + "schema": "requests", + "table": "request" + }, + { + "columns": ["agent_id", "tool_version_id"], + "nameExplicit": true, + "name": "agent_tool_pkey", + "entityType": "pks", + "schema": "builder", + "table": "agent_tool" + }, + { + "columns": ["agent_id", "number"], + "nameExplicit": true, + "name": "agent_version_pkey", + "entityType": "pks", + "schema": "builder", + "table": "agent_version" + }, + { + "columns": ["agent_id", "number", "tool_version_id"], + "nameExplicit": true, + "name": "agent_version_tool_pkey", + "entityType": "pks", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "columns": ["company_id", "kind"], + "nameExplicit": false, + "name": "company_account_kind_pkey", + "entityType": "pks", + "schema": "builder", + "table": "company_account_kind" + }, + { + "columns": ["account_id", "person_id"], + "nameExplicit": false, + "name": "model_account_assignee_pkey", + "entityType": "pks", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "columns": ["company_id", "person_id"], + "nameExplicit": false, + "name": "person_model_default_pkey", + "entityType": "pks", + "schema": "builder", + "table": "person_model_default" + }, + { + "columns": ["company_id", "sha256"], + "nameExplicit": true, + "name": "blob_pkey", + "entityType": "pks", + "schema": "apps", + "table": "blob" + }, + { + "columns": ["version_id", "path"], + "nameExplicit": true, + "name": "version_file_pkey", + "entityType": "pks", + "schema": "apps", + "table": "version_file" + }, + { + "columns": ["company_id", "thread_id", "tool_call_id"], + "nameExplicit": false, + "name": "connector_write_pkey", + "entityType": "pks", + "schema": "connectors", + "table": "connector_write" + }, + { + "columns": ["project_id", "connection_id"], + "nameExplicit": false, + "name": "project_connection_pkey", + "entityType": "pks", + "schema": "connectors", + "table": "project_connection" + }, + { + "columns": ["proposal_id", "connection_id"], + "nameExplicit": false, + "name": "copy_proposal_source_pkey", + "entityType": "pks", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "columns": ["copy_id", "connection_id"], + "nameExplicit": false, + "name": "copy_source_pkey", + "entityType": "pks", + "schema": "copies", + "table": "copy_source" + }, + { + "columns": ["company_id", "copy_id"], + "nameExplicit": false, + "name": "copy_table_left_pkey", + "entityType": "pks", + "schema": "copies", + "table": "copy_table_left" + }, + { + "columns": ["project_id", "copy_id"], + "nameExplicit": false, + "name": "project_copy_pkey", + "entityType": "pks", + "schema": "copies", + "table": "project_copy" + }, + { + "columns": ["project_id", "person_id"], + "nameExplicit": false, + "name": "app_access_pkey", + "entityType": "pks", + "schema": "projects", + "table": "app_access" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "agent_pkey", + "schema": "builder", + "table": "agent", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "agent_conversation_pkey", + "schema": "builder", + "table": "agent_conversation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "assistant_conversation_pkey", + "schema": "builder", + "table": "assistant_conversation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "conversation_pkey", + "schema": "builder", + "table": "conversation", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "company_model_pkey", + "schema": "builder", + "table": "company_model", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "company_model_fallback_pkey", + "schema": "builder", + "table": "company_model_fallback", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "model_account_pkey", + "schema": "builder", + "table": "model_account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "model_account_sign_in_pkey", + "schema": "builder", + "table": "model_account_sign_in", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "observational_memory_models_pkey", + "schema": "builder", + "table": "observational_memory_models", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "tool_pkey", + "schema": "builder", + "table": "tool", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "tool_version_pkey", + "schema": "builder", + "table": "tool_version", + "entityType": "pks" + }, + { + "columns": ["project_id"], + "nameExplicit": false, + "name": "app_pkey", + "schema": "apps", + "table": "app", + "entityType": "pks" + }, + { + "columns": ["token_hash"], + "nameExplicit": false, + "name": "app_session_pkey", + "schema": "apps", + "table": "app_session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "app_version_pkey", + "schema": "apps", + "table": "app_version", + "entityType": "pks" + }, + { + "columns": ["code_hash"], + "nameExplicit": false, + "name": "handoff_pkey", + "schema": "apps", + "table": "handoff", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "connection_pkey", + "schema": "connectors", + "table": "connection", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "connection_sign_in_pkey", + "schema": "connectors", + "table": "connection_sign_in", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "copy_pkey", + "schema": "copies", + "table": "copy", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "copy_proposal_pkey", + "schema": "copies", + "table": "copy_proposal", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "copy_run_pkey", + "schema": "copies", + "table": "copy_run", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pkey", + "schema": "auth", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "company_pkey", + "schema": "auth", + "table": "company", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "auth", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pkey", + "schema": "auth", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_role_pkey", + "schema": "auth", + "table": "organization_role", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "rate_limit_pkey", + "schema": "auth", + "table": "rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pkey", + "schema": "auth", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "user_pkey", + "schema": "auth", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "auth", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "notification_pkey", + "schema": "notifications", + "table": "notification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "request_notice_pkey", + "schema": "notifications", + "table": "request_notice", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "client_pkey", + "schema": "operations", + "table": "client", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "company_database_pkey", + "schema": "projects", + "table": "company_database", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pkey", + "schema": "projects", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "request_pkey", + "schema": "requests", + "table": "request", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "starts_pkey", + "schema": "hub", + "table": "starts", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "agent_company_id_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "agent" + }, + { + "nameExplicit": true, + "columns": ["agent_id", "tool_id"], + "nullsNotDistinct": false, + "name": "agent_tool_agent_id_tool_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "agent_tool" + }, + { + "nameExplicit": true, + "columns": ["agent_id", "source_revision"], + "nullsNotDistinct": false, + "name": "agent_version_agent_id_source_revision_key", + "entityType": "uniques", + "schema": "builder", + "table": "agent_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id", "number"], + "nullsNotDistinct": false, + "name": "agent_version_company_id_agent_id_number_key", + "entityType": "uniques", + "schema": "builder", + "table": "agent_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "conversation_attempt_key", + "entityType": "uniques", + "schema": "builder", + "table": "conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "model_account_attempt_key", + "entityType": "uniques", + "schema": "builder", + "table": "model_account" + }, + { + "nameExplicit": true, + "columns": ["company_id", "slug"], + "nullsNotDistinct": false, + "name": "tool_company_id_slug_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "tool_company_id_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool" + }, + { + "nameExplicit": true, + "columns": ["tool_id", "number"], + "nullsNotDistinct": false, + "name": "tool_version_tool_id_number_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool_version" + }, + { + "nameExplicit": true, + "columns": ["tool_id", "id"], + "nullsNotDistinct": false, + "name": "tool_version_tool_id_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "tool_version_company_id_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool_version" + }, + { + "nameExplicit": true, + "columns": ["project_id", "number"], + "nullsNotDistinct": false, + "name": "app_version_project_id_number_key", + "entityType": "uniques", + "schema": "apps", + "table": "app_version" + }, + { + "nameExplicit": true, + "columns": ["project_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "app_version_project_id_attempt_key_key", + "entityType": "uniques", + "schema": "apps", + "table": "app_version" + }, + { + "nameExplicit": true, + "columns": ["project_id", "id"], + "nullsNotDistinct": false, + "name": "app_version_project_id_id_key", + "entityType": "uniques", + "schema": "apps", + "table": "app_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "connection_attempt_key", + "entityType": "uniques", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "id"], + "nullsNotDistinct": false, + "name": "connection_company_connector_id_key", + "entityType": "uniques", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": ["company_id", "name"], + "nullsNotDistinct": false, + "name": "copy_company_id_name_key", + "entityType": "uniques", + "schema": "copies", + "table": "copy" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "copy_company_id_attempt_key_key", + "entityType": "uniques", + "schema": "copies", + "table": "copy" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "copy_company_id_id_key", + "entityType": "uniques", + "schema": "copies", + "table": "copy" + }, + { + "nameExplicit": true, + "columns": ["copy_id", "account_ref"], + "nullsNotDistinct": false, + "name": "copy_source_copy_id_account_ref_key", + "entityType": "uniques", + "schema": "copies", + "table": "copy_source" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "invitation_company_id_attempt_key_key", + "entityType": "uniques", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "user_id"], + "nullsNotDistinct": false, + "name": "member_company_id_user_id_key", + "entityType": "uniques", + "schema": "auth", + "table": "member" + }, + { + "nameExplicit": true, + "columns": ["company_id", "run_id"], + "nullsNotDistinct": false, + "name": "notification_run_key", + "entityType": "uniques", + "schema": "notifications", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": ["request_id", "person_id", "kind"], + "nullsNotDistinct": false, + "name": "request_notice_key", + "entityType": "uniques", + "schema": "notifications", + "table": "request_notice" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "project_company_id_attempt_key_key", + "entityType": "uniques", + "schema": "projects", + "table": "project" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "project_company_id_id_key", + "entityType": "uniques", + "schema": "projects", + "table": "project" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "request_attempt_key", + "entityType": "uniques", + "schema": "requests", + "table": "request" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "company_slug_key", + "schema": "auth", + "table": "company", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["attempt_key"], + "nullsNotDistinct": false, + "name": "company_attempt_key_key", + "schema": "auth", + "table": "company", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["nickname"], + "nullsNotDistinct": false, + "name": "company_nickname_key", + "schema": "auth", + "table": "company", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["key"], + "nullsNotDistinct": false, + "name": "rate_limit_key_key", + "schema": "auth", + "table": "rate_limit", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["token"], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "auth", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "auth", + "table": "user", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["cnpj"], + "nullsNotDistinct": false, + "name": "client_cnpj_key", + "schema": "operations", + "table": "client", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["attempt_key"], + "nullsNotDistinct": false, + "name": "client_attempt_key_key", + "schema": "operations", + "table": "client", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["neon_project_id"], + "nullsNotDistinct": false, + "name": "company_database_neon_project_id_key", + "schema": "projects", + "table": "company_database", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["host_label"], + "nullsNotDistinct": false, + "name": "project_host_label_key", + "schema": "projects", + "table": "project", + "entityType": "uniques" + }, + { + "value": "\"revision\" > 0", + "name": "agent_revision_positive", + "entityType": "checks", + "schema": "builder", + "table": "agent" + }, + { + "value": "\"approval\" in ('ASK', 'AUTO')", + "name": "agent_tool_approval", + "entityType": "checks", + "schema": "builder", + "table": "agent_tool" + }, + { + "value": "\"number\" > 0", + "name": "agent_version_number_positive", + "entityType": "checks", + "schema": "builder", + "table": "agent_version" + }, + { + "value": "\"approval\" in ('ASK', 'AUTO')", + "name": "agent_version_tool_approval", + "entityType": "checks", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "value": "\"kind\" IN ('claude_subscription', 'google_cliproxy', 'openai_codex')", + "name": "company_account_kind_kind", + "entityType": "checks", + "schema": "builder", + "table": "company_account_kind" + }, + { + "value": "\"effort\" IN ('off', 'low', 'medium', 'high', 'xhigh', 'max')", + "name": "company_model_effort", + "entityType": "checks", + "schema": "builder", + "table": "company_model" + }, + { + "value": "\"kind\" IN ('api_key', 'claude_subscription', 'google_cliproxy', 'openai_codex')", + "name": "model_account_kind", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "\"state\" IN ('ready', 'paused', 'refused')", + "name": "model_account_state", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "char_length(\"label\") BETWEEN 1 AND 60", + "name": "model_account_label_length", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "\"position\" >= 0", + "name": "model_account_position", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "(\"kind\" = 'api_key') = (\"hint\" IS NOT NULL)", + "name": "model_account_hint", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "\"owner\" IN ('company', 'me')", + "name": "model_account_sign_in_owner", + "entityType": "checks", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "value": "\"kind\" IN ('claude_subscription', 'google_cliproxy', 'openai_codex')", + "name": "model_account_sign_in_kind", + "entityType": "checks", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "value": "char_length(\"label\") BETWEEN 1 AND 60", + "name": "model_account_sign_in_label_length", + "entityType": "checks", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "value": "\"effort\" IN ('off', 'low', 'medium', 'high', 'xhigh', 'max')", + "name": "person_model_default_effort", + "entityType": "checks", + "schema": "builder", + "table": "person_model_default" + }, + { + "value": "\"kind\" in ('PRESET', 'CODE')", + "name": "tool_kind", + "entityType": "checks", + "schema": "builder", + "table": "tool" + }, + { + "value": "(\"kind\" = 'PRESET') = (\"operation\" is not null)", + "name": "tool_operation_by_kind", + "entityType": "checks", + "schema": "builder", + "table": "tool" + }, + { + "value": "(\"sending_version_id\" IS NULL) = (\"claimed_until\" IS NULL)", + "name": "app_sending_has_claim", + "entityType": "checks", + "schema": "apps", + "table": "app" + }, + { + "value": "\"environment\" IN ('preview', 'published')", + "name": "app_session_environment", + "entityType": "checks", + "schema": "apps", + "table": "app_session" + }, + { + "value": "\"state\" IN ('sending', 'ready', 'failed', 'pruning')", + "name": "app_version_state", + "entityType": "checks", + "schema": "apps", + "table": "app_version" + }, + { + "value": "\"state\" <> 'failed' OR \"failure\" IS NOT NULL", + "name": "app_version_failed_has_failure", + "entityType": "checks", + "schema": "apps", + "table": "app_version" + }, + { + "value": "\"number\" > 0", + "name": "app_version_number_positive", + "entityType": "checks", + "schema": "apps", + "table": "app_version" + }, + { + "value": "\"digest\" ~ '^[0-9a-f]{64}$'", + "name": "app_version_digest_form", + "entityType": "checks", + "schema": "apps", + "table": "app_version" + }, + { + "value": "\"sha256\" ~ '^[0-9a-f]{64}$'", + "name": "blob_sha256_form", + "entityType": "checks", + "schema": "apps", + "table": "blob" + }, + { + "value": "octet_length(\"bytes\") <= 10485760", + "name": "blob_size", + "entityType": "checks", + "schema": "apps", + "table": "blob" + }, + { + "value": "\"environment\" IN ('preview', 'published')", + "name": "handoff_environment", + "entityType": "checks", + "schema": "apps", + "table": "handoff" + }, + { + "value": "\"size\" BETWEEN 0 AND 10485760", + "name": "version_file_size", + "entityType": "checks", + "schema": "apps", + "table": "version_file" + }, + { + "value": "char_length(\"name\") BETWEEN 1 AND 60 AND \"name\" = btrim(\"name\")", + "name": "connection_name", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "\"state\" IN ('NEW', 'ACTIVE', 'NEEDS_REAUTH')", + "name": "connection_state", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "(\"account_ref\" IS NULL) = (\"account_label\" IS NULL)\n AND (\"account_label\" IS NULL OR char_length(\"account_label\") BETWEEN 1 AND 120)", + "name": "connection_account", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "jsonb_typeof(\"notes\") = 'object' AND octet_length(\"notes\"::text) <= 32768", + "name": "connection_notes", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "\"connector_id\" ~ '^[a-z][a-z0-9-]{1,31}$'", + "name": "connection_connector_id", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "\"name\" IS NULL OR (char_length(\"name\") BETWEEN 1 AND 60 AND \"name\" = btrim(\"name\"))", + "name": "connection_sign_in_name", + "entityType": "checks", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "value": "(\"connection_id\" IS NULL) <> (\"name\" IS NULL)\n AND (\"connection_id\" IS NULL) = (\"connection_revision\" IS NULL)", + "name": "connection_sign_in_target", + "entityType": "checks", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "value": "(\"agent_id\" is not null and \"version_number\" is not null and \"person_id\" is null) or (\"agent_id\" is null and \"version_number\" is null and \"person_id\" is not null)", + "name": "connector_write_speaker", + "entityType": "checks", + "schema": "connectors", + "table": "connector_write" + }, + { + "value": "\"state\" in ('SENDING', 'DONE', 'REFUSED', 'UNCERTAIN')", + "name": "connector_write_state", + "entityType": "checks", + "schema": "connectors", + "table": "connector_write" + }, + { + "value": "(\"claim_token\" IS NULL) = (\"claimed_until\" IS NULL)", + "name": "copy_claim", + "entityType": "checks", + "schema": "copies", + "table": "copy" + }, + { + "value": "\"name\" ~ '^[a-z][a-z0-9_]{0,39}$'", + "name": "copy_name", + "entityType": "checks", + "schema": "copies", + "table": "copy" + }, + { + "value": "jsonb_typeof(\"definition\") = 'object'", + "name": "copy_definition", + "entityType": "checks", + "schema": "copies", + "table": "copy" + }, + { + "value": "\"name\" ~ '^[a-z][a-z0-9_]{0,39}$'", + "name": "copy_proposal_name", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "jsonb_typeof(\"definition\") = 'object'", + "name": "copy_proposal_definition", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "\"proved_rows\" BETWEEN 0 AND 50", + "name": "copy_proposal_proved_rows", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "\"state\" IN ('OPEN', 'ACCEPTED', 'DISMISSED')", + "name": "copy_proposal_state", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "\"state\" = 'ACCEPTED' OR \"copy_id\" IS NULL", + "name": "copy_proposal_copy", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "(\"ended_at\" IS NULL) = (\"outcome\" IS NULL)", + "name": "copy_run_ended", + "entityType": "checks", + "schema": "copies", + "table": "copy_run" + }, + { + "value": "\"outcome\" IS NULL OR \"outcome\" IN ('COMPLETE', 'PARTIAL', 'ERROR', 'INTERRUPTED')", + "name": "copy_run_outcome", + "entityType": "checks", + "schema": "copies", + "table": "copy_run" + }, + { + "value": "\"position\" IS NULL OR jsonb_typeof(\"position\") = 'object'", + "name": "copy_source_position", + "entityType": "checks", + "schema": "copies", + "table": "copy_source" + }, + { + "value": "\"name\" ~ '^[a-z][a-z0-9_]{0,39}$'", + "name": "copy_table_left_name", + "entityType": "checks", + "schema": "copies", + "table": "copy_table_left" + }, + { + "value": "\"nickname\" ~ '^[a-z0-9]+(-[a-z0-9]+)*$' and char_length(\"nickname\") BETWEEN 3 AND 30", + "name": "company_nickname_form", + "entityType": "checks", + "schema": "auth", + "table": "company" + }, + { + "value": "(\"project_id\" is null) = (\"role\" is not null)", + "name": "invitation_kind", + "entityType": "checks", + "schema": "auth", + "table": "invitation" + }, + { + "value": "\"kind\" in ('AUTOMATION_DONE', 'AUTOMATION_FAILED', 'AUTOMATION_PAUSED')", + "name": "notification_kind", + "entityType": "checks", + "schema": "notifications", + "table": "notification" + }, + { + "value": "\"kind\" <> 'AUTOMATION_DONE' or (\"conversation_id\" is not null and \"failure_code\" is null)", + "name": "notification_done", + "entityType": "checks", + "schema": "notifications", + "table": "notification" + }, + { + "value": "\"kind\" <> 'AUTOMATION_FAILED' or (\"failure_code\" is not null and \"failure_code\" in ('MODEL_NO_ACCOUNT', 'MODEL_CALL_FAILED', 'MODEL_REFUSED', 'MODEL_ANSWER_CUT', 'MACHINE_UNAVAILABLE', 'MACHINE_LOST', 'HUB_UPDATED', 'INTERNAL_UNEXPECTED', 'DATABASE_UNAVAILABLE', 'INTERRUPTED', 'SCHEDULE_PUBLISH_FAILED'))", + "name": "notification_failed", + "entityType": "checks", + "schema": "notifications", + "table": "notification" + }, + { + "value": "\"kind\" <> 'AUTOMATION_PAUSED' or (\"conversation_id\" is null and \"failure_code\" is null)", + "name": "notification_paused", + "entityType": "checks", + "schema": "notifications", + "table": "notification" + }, + { + "value": "\"kind\" in ('REQUEST_OPENED', 'REQUEST_DECIDED')", + "name": "request_notice_kind", + "entityType": "checks", + "schema": "notifications", + "table": "request_notice" + }, + { + "value": "\"cnpj\" ~ '^[0-9A-Z]{12}[0-9]{2}$'", + "name": "client_cnpj_format", + "entityType": "checks", + "schema": "operations", + "table": "client" + }, + { + "value": "char_length(\"legal_name\") BETWEEN 1 AND 150", + "name": "client_legal_name_length", + "entityType": "checks", + "schema": "operations", + "table": "client" + }, + { + "value": "\"revision\" >= 0", + "name": "client_revision_not_negative", + "entityType": "checks", + "schema": "operations", + "table": "client" + }, + { + "value": "\"state\" IN ('creating', 'ready')", + "name": "company_database_state", + "entityType": "checks", + "schema": "projects", + "table": "company_database" + }, + { + "value": "(\"state\" = 'ready') = (\"neon_project_id\" IS NOT NULL)", + "name": "company_database_ready_has_project", + "entityType": "checks", + "schema": "projects", + "table": "company_database" + }, + { + "value": "(\"state\" = 'creating') = (\"claimed_until\" IS NOT NULL)", + "name": "company_database_creating_has_claim", + "entityType": "checks", + "schema": "projects", + "table": "company_database" + }, + { + "value": "\"host_label\" ~ '^[a-z0-9]+(-[a-z0-9]+)*--[a-z0-9]+(-[a-z0-9]+)*$' and char_length(\"host_label\") <= 63", + "name": "project_host_label_form", + "entityType": "checks", + "schema": "projects", + "table": "project" + }, + { + "value": "char_length(\"name\") BETWEEN 1 AND 100", + "name": "project_name_length", + "entityType": "checks", + "schema": "projects", + "table": "project" + }, + { + "value": "\"kind\" in ('PUBLISH_APP_VERSION')", + "name": "request_kind", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "\"number\" > 0", + "name": "request_number", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "char_length(\"message\") <= 280", + "name": "request_message", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "\"state\" in ('PENDING', 'APPROVED', 'DECLINED', 'CANCELLED', 'SUPERSEDED')", + "name": "request_state", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "(\"state\" in ('PENDING', 'CANCELLED')) = (\"decided_by\" is null)", + "name": "request_decided", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "\"reason\" is null or \"state\" = 'DECLINED'", + "name": "request_reason", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "\"version\" ~ '^v[0-9]+\\.[0-9]+\\.[0-9]+$'", + "name": "starts_version_format", + "entityType": "checks", + "schema": "hub", + "table": "starts" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_agent_companies"], + "using": "true", + "withCheck": null, + "name": "agent_companies_select", + "entityType": "policies", + "schema": "builder", + "table": "agent" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent_conversation" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent_tool" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent_version" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "conversation" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "company_select", + "entityType": "policies", + "schema": "builder", + "table": "company_account_kind" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "builder", + "table": "company_account_kind" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "operator_insert", + "entityType": "policies", + "schema": "builder", + "table": "company_account_kind" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "builder", + "table": "company_account_kind" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "company_model" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id is null or person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id is null or person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "model_account" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_model_seals"], + "using": "true", + "withCheck": null, + "name": "model_seals_select", + "entityType": "policies", + "schema": "builder", + "table": "model_account" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_model_seals"], + "using": "true", + "withCheck": null, + "name": "model_seals_select", + "entityType": "policies", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "person_model_default" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "tool" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "tool_version" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "apps", + "table": "app" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "apps", + "table": "app" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "apps", + "table": "app" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_app_companies"], + "using": "true", + "withCheck": null, + "name": "companies_select", + "entityType": "policies", + "schema": "apps", + "table": "app" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "apps", + "table": "app_version" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "apps", + "table": "app_version" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "apps", + "table": "app_version" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "apps", + "table": "blob" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "apps", + "table": "blob" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "apps", + "table": "blob" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "apps", + "table": "version_file" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "apps", + "table": "version_file" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "apps", + "table": "version_file" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "connectors", + "table": "connection" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_connection_seals"], + "using": "true", + "withCheck": null, + "name": "connection_seals_select", + "entityType": "policies", + "schema": "connectors", + "table": "connection" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "connectors", + "table": "connector_write" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "connectors", + "table": "project_connection" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_proposal" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_run" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_source" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_table_left" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "project_copy" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_app_people"], + "using": "true", + "withCheck": null, + "name": "app_people_select", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_person"], + "using": "exists (select 1 from auth.member m where m.company_id = company.id and m.user_id = nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": null, + "name": "person_select", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_invitation"], + "using": "exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = company.id)", + "withCheck": "exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = company.id)", + "name": "invitation_all", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "name": "operator_insert", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and id = nullif(current_setting('conexus.company_id', true), '')", + "name": "operator_update", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_person"], + "using": "inviter_id = nullif(current_setting('conexus.person_id', true), '')\n or lower(email) = (select u.email from auth.\"user\" u where u.id = nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": null, + "name": "person_select", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_invitation"], + "using": "id = nullif(current_setting('conexus.invitation_id', true), '')", + "withCheck": "id = nullif(current_setting('conexus.invitation_id', true), '')", + "name": "invitation_all", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and (role = 'owner' or company_id = nullif(current_setting('conexus.company_id', true), ''))", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and role = 'owner'", + "name": "operator_insert", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and role = 'owner'", + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and role = 'owner'", + "name": "operator_update", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_person"], + "using": "user_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": null, + "name": "person_select", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_invitation"], + "using": "exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = member.company_id)", + "withCheck": null, + "name": "invitation_select", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_invitation"], + "using": null, + "withCheck": "user_id = nullif(current_setting('conexus.person_id', true), '')\n and exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = member.company_id)", + "name": "invitation_insert", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and (role = 'owner' or company_id = nullif(current_setting('conexus.company_id', true), ''))", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "organization_role" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "name": "operator_insert", + "entityType": "policies", + "schema": "auth", + "table": "organization_role" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_runtime"], + "using": "true", + "withCheck": "true", + "name": "runtime_all", + "entityType": "policies", + "schema": "auth", + "table": "session" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "user_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "user_id = nullif(current_setting('conexus.person_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "session" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_invitation"], + "using": "user_id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.invitation i join auth.\"user\" u on u.email = lower(i.email)\n where i.id = nullif(current_setting('conexus.invitation_id', true), '') and u.id = session.user_id)", + "withCheck": "user_id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.invitation i join auth.\"user\" u on u.email = lower(i.email)\n where i.id = nullif(current_setting('conexus.invitation_id', true), '') and u.id = session.user_id)", + "name": "invitation_all", + "entityType": "policies", + "schema": "auth", + "table": "session" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_runtime"], + "using": "true", + "withCheck": "true", + "name": "runtime_sign_in", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.member m where m.user_id = \"user\".id and m.company_id = nullif(current_setting('conexus.company_id', true), ''))\n or exists (select 1 from auth.invitation i where i.inviter_id = \"user\".id and i.company_id = nullif(current_setting('conexus.company_id', true), ''))\n or exists (select 1 from projects.app_access a where a.person_id = \"user\".id and a.company_id = nullif(current_setting('conexus.company_id', true), ''))", + "withCheck": "id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.member m where m.user_id = \"user\".id and m.company_id = nullif(current_setting('conexus.company_id', true), ''))\n or exists (select 1 from auth.invitation i where i.inviter_id = \"user\".id and i.company_id = nullif(current_setting('conexus.company_id', true), ''))\n or exists (select 1 from projects.app_access a where a.person_id = \"user\".id and a.company_id = nullif(current_setting('conexus.company_id', true), ''))", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_person"], + "using": "id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "id = nullif(current_setting('conexus.person_id', true), '')", + "name": "person_all", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_invitation"], + "using": "id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '')\n and (i.inviter_id = \"user\".id or lower(i.email) = \"user\".email))", + "withCheck": "id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '')\n and (i.inviter_id = \"user\".id or lower(i.email) = \"user\".email))", + "name": "invitation_all", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_administrators"], + "using": "role = 'admin'", + "withCheck": null, + "name": "administrators_select", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and (role = 'admin'\n or exists (select 1 from auth.member m where m.user_id = \"user\".id and m.role = 'owner'))", + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and (role = 'admin'\n or exists (select 1 from auth.member m where m.user_id = \"user\".id and m.role = 'owner'))", + "name": "operator_all", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "notifications", + "table": "notification" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": null, + "name": "company_select", + "entityType": "policies", + "schema": "notifications", + "table": "request_notice" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "name": "company_update", + "entityType": "policies", + "schema": "notifications", + "table": "request_notice" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_company"], + "using": null, + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_insert", + "entityType": "policies", + "schema": "notifications", + "table": "request_notice" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "operations", + "table": "client" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "name": "operator_insert", + "entityType": "policies", + "schema": "operations", + "table": "client" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "name": "operator_update", + "entityType": "policies", + "schema": "operations", + "table": "client" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "operations", + "table": "client" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "projects", + "table": "app_access" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_invitation"], + "using": null, + "withCheck": "exists (select 1 from auth.invitation i\n join auth.\"user\" u on lower(u.email) = lower(i.email)\n where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = app_access.company_id\n and i.project_id = app_access.project_id and i.app_role is not distinct from app_access.role\n and u.id = app_access.person_id)", + "name": "invitation_insert", + "entityType": "policies", + "schema": "projects", + "table": "app_access" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_invitation"], + "using": "exists (select 1 from auth.invitation i\n join auth.\"user\" u on lower(u.email) = lower(i.email)\n where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.project_id = app_access.project_id\n and u.id = app_access.person_id)", + "withCheck": null, + "name": "invitation_select", + "entityType": "policies", + "schema": "projects", + "table": "app_access" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_app_people"], + "using": "true", + "withCheck": null, + "name": "app_people_select", + "entityType": "policies", + "schema": "projects", + "table": "app_access" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "projects", + "table": "company_database" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "projects", + "table": "company_database" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "projects", + "table": "company_database" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_hosts"], + "using": "true", + "withCheck": null, + "name": "hosts_select", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_app_people"], + "using": "true", + "withCheck": null, + "name": "app_people_select", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_invitation"], + "using": "exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '')\n and i.company_id = project.company_id and i.project_id = project.id)", + "withCheck": null, + "name": "invitation_select", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "requests", + "table": "request" + } + ], + "renames": [] +} diff --git a/apps/server/migrations/20261112000029_sync_copy_connection_fkey/migration.sql b/apps/server/migrations/20261112000029_sync_copy_connection_fkey/migration.sql new file mode 100644 index 000000000..432cd059b --- /dev/null +++ b/apps/server/migrations/20261112000029_sync_copy_connection_fkey/migration.sql @@ -0,0 +1,5 @@ +-- contract: copy_connection_fkey leaves, the constraint and not a column: no query depends on it, only +-- its cascade, which decision C replaces (O Sync de qualquer conector): a copy ends with its last +-- source in copies.copy_source, not with the first connection it was turned on from, which data D17 +-- says a convenient key must not force. connection_id stays, unread, and leaves later (data D6). +ALTER TABLE "copies"."copy" DROP CONSTRAINT "copy_connection_fkey"; diff --git a/apps/server/migrations/20261112000029_sync_copy_connection_fkey/snapshot.json b/apps/server/migrations/20261112000029_sync_copy_connection_fkey/snapshot.json new file mode 100644 index 000000000..5208a0c5d --- /dev/null +++ b/apps/server/migrations/20261112000029_sync_copy_connection_fkey/snapshot.json @@ -0,0 +1,9526 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "ec2c9bab-5358-4014-ab43-f48ff22f47ca", + "prevIds": ["54851667-1821-40c5-8664-24ec21cf603e"], + "ddl": [ + { + "name": "builder", + "entityType": "schemas" + }, + { + "name": "apps", + "entityType": "schemas" + }, + { + "name": "connectors", + "entityType": "schemas" + }, + { + "name": "copies", + "entityType": "schemas" + }, + { + "name": "auth", + "entityType": "schemas" + }, + { + "name": "notifications", + "entityType": "schemas" + }, + { + "name": "operations", + "entityType": "schemas" + }, + { + "name": "projects", + "entityType": "schemas" + }, + { + "name": "requests", + "entityType": "schemas" + }, + { + "name": "hub", + "entityType": "schemas" + }, + { + "isRlsEnabled": true, + "name": "agent", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "agent_conversation", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "agent_tool", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "agent_version", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "agent_version_tool", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "assistant_conversation", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "conversation", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "company_account_kind", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "company_model", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "company_model_fallback", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "model_account", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "model_account_assignee", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "model_account_sign_in", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "observational_memory_models", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "person_model_default", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "tool", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "tool_version", + "entityType": "tables", + "schema": "builder" + }, + { + "isRlsEnabled": true, + "name": "app", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": false, + "name": "app_session", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": true, + "name": "app_version", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": true, + "name": "blob", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": false, + "name": "handoff", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": true, + "name": "version_file", + "entityType": "tables", + "schema": "apps" + }, + { + "isRlsEnabled": true, + "name": "connection", + "entityType": "tables", + "schema": "connectors" + }, + { + "isRlsEnabled": true, + "name": "connection_sign_in", + "entityType": "tables", + "schema": "connectors" + }, + { + "isRlsEnabled": true, + "name": "connector_write", + "entityType": "tables", + "schema": "connectors" + }, + { + "isRlsEnabled": true, + "name": "project_connection", + "entityType": "tables", + "schema": "connectors" + }, + { + "isRlsEnabled": true, + "name": "copy", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_proposal", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_proposal_source", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_run", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_source", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "copy_table_left", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": true, + "name": "project_copy", + "entityType": "tables", + "schema": "copies" + }, + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "company", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "invitation", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "member", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "organization_role", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": false, + "name": "rate_limit", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "session", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "user", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": true, + "name": "notification", + "entityType": "tables", + "schema": "notifications" + }, + { + "isRlsEnabled": true, + "name": "request_notice", + "entityType": "tables", + "schema": "notifications" + }, + { + "isRlsEnabled": true, + "name": "client", + "entityType": "tables", + "schema": "operations" + }, + { + "isRlsEnabled": true, + "name": "app_access", + "entityType": "tables", + "schema": "projects" + }, + { + "isRlsEnabled": true, + "name": "company_database", + "entityType": "tables", + "schema": "projects" + }, + { + "isRlsEnabled": true, + "name": "project", + "entityType": "tables", + "schema": "projects" + }, + { + "isRlsEnabled": true, + "name": "request", + "entityType": "tables", + "schema": "requests" + }, + { + "isRlsEnabled": false, + "name": "starts", + "entityType": "tables", + "schema": "hub" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "draft", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_version", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_by", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_run_at", + "entityType": "columns", + "schema": "builder", + "table": "agent" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "draft", + "entityType": "columns", + "schema": "builder", + "table": "agent_conversation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_version_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "approval", + "entityType": "columns", + "schema": "builder", + "table": "agent_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_revision", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "published_by", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "published_at", + "entityType": "columns", + "schema": "builder", + "table": "agent_version" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_version_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "approval", + "entityType": "columns", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "connections", + "entityType": "columns", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "builder", + "table": "conversation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "company_account_kind" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "builder", + "table": "company_account_kind" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "company_model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "builder", + "table": "company_model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'off'", + "generated": null, + "identity": null, + "name": "effort", + "entityType": "columns", + "schema": "builder", + "table": "company_model" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "chain", + "entityType": "columns", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sealed", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hint", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linked_by", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linked_at", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "limited_until", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_until", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "served_models", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "served_at", + "entityType": "columns", + "schema": "builder", + "table": "model_account" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "owner", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sealed", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key_id", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "statement_timestamp()", + "generated": null, + "identity": null, + "name": "poll_at", + "entityType": "columns", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "observer", + "entityType": "columns", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reflector", + "entityType": "columns", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "person_model_default" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "builder", + "table": "person_model_default" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "builder", + "table": "person_model_default" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "effort", + "entityType": "columns", + "schema": "builder", + "table": "person_model_default" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'PRESET'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operation", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "current_version", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "builder", + "table": "tool" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_id", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "config", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "builder", + "table": "tool_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "live_version_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "published_version_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sending_version_id", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimed_until", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "database_ready", + "entityType": "columns", + "schema": "apps", + "table": "app" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_hash", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host_label", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hub_session_id", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment", + "entityType": "columns", + "schema": "apps", + "table": "app_session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failure", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "manifest", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "digest", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sent_by", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "apps", + "table": "app_version" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "blob" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sha256", + "entityType": "columns", + "schema": "apps", + "table": "blob" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bytes", + "entityType": "columns", + "schema": "apps", + "table": "blob" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code_hash", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "binding_hash", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host_label", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hub_session_id", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "apps", + "table": "handoff" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version_id", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sha256", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "media_type", + "entityType": "columns", + "schema": "apps", + "table": "version_file" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_version", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sealed", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linked_by", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "linked_at", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tested_at", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tested_by", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_test", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "notes", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_until", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_ref", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_label", + "entityType": "columns", + "schema": "connectors", + "table": "connection" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_revision", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sealed", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key_id", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thread_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tool_call_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operation_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "input_hash", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version_number", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "answer", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "approved_by", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "connectors", + "table": "connector_write" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "exclusive", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "granted_by", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "granted_at", + "entityType": "columns", + "schema": "connectors", + "table": "project_connection" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "model", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "definition", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body_hash", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claim_token", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimed_until", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pass", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pass_started_at", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "watermark", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rows", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_run_id", + "entityType": "columns", + "schema": "copies", + "table": "copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "definition", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proposed_by", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proved_rows", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proved_more", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proved_calls", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proposed_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "proposal_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "started_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ended_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "outcome", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pages", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "read", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "written", + "entityType": "columns", + "schema": "copies", + "table": "copy_run" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connector_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connection_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_ref", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "pass", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pass_started_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "watermark", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "wait_until", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "copies", + "table": "copy_source" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_table_left" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "copy_table_left" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "copies", + "table": "copy_table_left" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "copy_id", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "granted_by", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "granted_at", + "entityType": "columns", + "schema": "copies", + "table": "project_copy" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleting_since", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nickname", + "entityType": "columns", + "schema": "auth", + "table": "company" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "app_role", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mailed_at", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "superseded_at", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "replaces", + "entityType": "columns", + "schema": "auth", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "permission", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "organization_role" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "auth", + "table": "rate_limit" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "count", + "entityType": "columns", + "schema": "auth", + "table": "rate_limit" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "auth", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_organization_id", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "impersonated_by", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "banned", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ban_reason", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ban_expires", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automation_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "automation_name", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "conversation_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "failure_code", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "run_id", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "read_at", + "entityType": "columns", + "schema": "notifications", + "table": "notification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "request_id", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "read_at", + "entityType": "columns", + "schema": "notifications", + "table": "request_notice" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cnpj", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "legal_name", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "revision", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "registered_at", + "entityType": "columns", + "schema": "operations", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "person_id", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "statement_timestamp()", + "generated": null, + "identity": null, + "name": "granted_at", + "entityType": "columns", + "schema": "projects", + "table": "app_access" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "neon_project_id", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "claimed_until", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host", + "entityType": "columns", + "schema": "projects", + "table": "company_database" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "host_label", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleting_since", + "entityType": "columns", + "schema": "projects", + "table": "project" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "attempt_key", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "company_id", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "requested_by", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "project_id", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "decided_by", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reason", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "requests", + "table": "request" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "always", + "name": "starts_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "hub", + "table": "starts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "hub", + "table": "starts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "started_at", + "entityType": "columns", + "schema": "hub", + "table": "starts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lower(\"name\" collate \"und-x-icu\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "connection_name_key", + "entityType": "indexes", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "account_ref", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"account_ref\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "connection_account_key", + "entityType": "indexes", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "connector_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"exclusive\"", + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_connection_one_key", + "entityType": "indexes", + "schema": "connectors", + "table": "project_connection" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"state\" = 'OPEN'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "copy_proposal_open_name_key", + "entityType": "indexes", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "copy_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"ended_at\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "copy_run_open_key", + "entityType": "indexes", + "schema": "copies", + "table": "copy_run" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "copy_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "started_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "copy_run_copy_started_idx", + "entityType": "indexes", + "schema": "copies", + "table": "copy_run" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_user_id_idx", + "entityType": "indexes", + "schema": "auth", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lower(\"email\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "coalesce(\"project_id\", '')", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"status\" = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_pending_email_key", + "entityType": "indexes", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_user_id_idx", + "entityType": "indexes", + "schema": "auth", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_role_company_id_role_key", + "entityType": "indexes", + "schema": "auth", + "table": "organization_role" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "attempt_key", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_role_company_id_attempt_key_key", + "entityType": "indexes", + "schema": "auth", + "table": "organization_role" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_user_id_idx", + "entityType": "indexes", + "schema": "auth", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "auth", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "person_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "notification_person_created", + "entityType": "indexes", + "schema": "notifications", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "person_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "request_notice_person_created", + "entityType": "indexes", + "schema": "notifications", + "table": "request_notice" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "lower(\"name\" collate \"und-x-icu\")", + "isExpression": true, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "project_company_name_key", + "entityType": "indexes", + "schema": "projects", + "table": "project" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "company_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "project_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"state\" = 'PENDING'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "request_open_key", + "entityType": "indexes", + "schema": "requests", + "table": "request" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_project_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id", "active_version"], + "schemaTo": "builder", + "tableTo": "agent_version", + "columnsTo": ["company_id", "agent_id", "number"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "agent_active_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_conversation_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id"], + "schemaTo": "builder", + "tableTo": "agent", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_conversation_agent_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_conversation_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id"], + "schemaTo": "builder", + "tableTo": "agent", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_tool_agent_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_tool" + }, + { + "nameExplicit": true, + "columns": ["tool_id", "tool_version_id"], + "schemaTo": "builder", + "tableTo": "tool_version", + "columnsTo": ["tool_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "agent_tool_tool_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "tool_version_id"], + "schemaTo": "builder", + "tableTo": "tool_version", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "agent_tool_company_tool_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id"], + "schemaTo": "builder", + "tableTo": "agent", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_version_agent_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id", "number"], + "schemaTo": "builder", + "tableTo": "agent_version", + "columnsTo": ["company_id", "agent_id", "number"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "agent_version_tool_agent_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "tool_version_id"], + "schemaTo": "builder", + "tableTo": "tool_version", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "agent_version_tool_tool_version_fkey", + "entityType": "fks", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "assistant_conversation_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "assistant_conversation_project_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "assistant_conversation_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "conversation_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "conversation" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "conversation_project_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "conversation_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "company_account_kind_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "company_account_kind" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "company_model_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "company_model" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "company_model_fallback_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account" + }, + { + "nameExplicit": true, + "columns": ["account_id"], + "schemaTo": "builder", + "tableTo": "model_account", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_assignee_account_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_assignee_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "model_account_sign_in_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "observational_memory_models_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "person_model_default_member_fkey", + "entityType": "fks", + "schema": "builder", + "table": "person_model_default" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tool_company_id_fkey", + "entityType": "fks", + "schema": "builder", + "table": "tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "tool_id"], + "schemaTo": "builder", + "tableTo": "tool", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tool_version_tool_fkey", + "entityType": "fks", + "schema": "builder", + "table": "tool_version" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "app_project_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app" + }, + { + "nameExplicit": true, + "columns": ["project_id", "live_version_id"], + "schemaTo": "apps", + "tableTo": "app_version", + "columnsTo": ["project_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "app_live_version_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app" + }, + { + "nameExplicit": true, + "columns": ["project_id", "published_version_id"], + "schemaTo": "apps", + "tableTo": "app_version", + "columnsTo": ["project_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "app_published_version_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app" + }, + { + "nameExplicit": true, + "columns": ["project_id", "sending_version_id"], + "schemaTo": "apps", + "tableTo": "app_version", + "columnsTo": ["project_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "app_sending_version_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app" + }, + { + "nameExplicit": true, + "columns": ["hub_session_id"], + "schemaTo": "auth", + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "app_session_hub_session_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app_session" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "apps", + "tableTo": "app", + "columnsTo": ["project_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "app_version_project_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "app_version" + }, + { + "nameExplicit": true, + "columns": ["hub_session_id"], + "schemaTo": "auth", + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "handoff_hub_session_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "handoff" + }, + { + "nameExplicit": true, + "columns": ["version_id"], + "schemaTo": "apps", + "tableTo": "app_version", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "version_file_version_id_fkey", + "entityType": "fks", + "schema": "apps", + "table": "version_file" + }, + { + "nameExplicit": true, + "columns": ["company_id", "sha256"], + "schemaTo": "apps", + "tableTo": "blob", + "columnsTo": ["company_id", "sha256"], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "version_file_blob_fkey", + "entityType": "fks", + "schema": "apps", + "table": "version_file" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connection_company_id_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connection_sign_in_member_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connection_sign_in_company_id_connector_id_connection_id_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "connector_write_company_id_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "connector_write" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "project_connection_connection_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "project_connection" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "project_connection_project_fkey", + "entityType": "fks", + "schema": "connectors", + "table": "project_connection" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_company_id_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_company_id_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_connection_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_project_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": ["copy_id"], + "schemaTo": "copies", + "tableTo": "copy", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "copy_proposal_copy_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "nameExplicit": true, + "columns": ["proposal_id"], + "schemaTo": "copies", + "tableTo": "copy_proposal", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_source_proposal_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_proposal_source_connection_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "nameExplicit": true, + "columns": ["company_id", "copy_id"], + "schemaTo": "copies", + "tableTo": "copy", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_run_copy_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_run" + }, + { + "nameExplicit": true, + "columns": ["company_id", "copy_id"], + "schemaTo": "copies", + "tableTo": "copy", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_source_copy_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_source" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "connection_id"], + "schemaTo": "connectors", + "tableTo": "connection", + "columnsTo": ["company_id", "connector_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_source_connection_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_source" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "copy_table_left_company_id_fkey", + "entityType": "fks", + "schema": "copies", + "table": "copy_table_left" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "project_copy_project_fkey", + "entityType": "fks", + "schema": "copies", + "table": "project_copy" + }, + { + "nameExplicit": true, + "columns": ["company_id", "copy_id"], + "schemaTo": "copies", + "tableTo": "copy", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "project_copy_copy_fkey", + "entityType": "fks", + "schema": "copies", + "table": "project_copy" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "account" + }, + { + "nameExplicit": false, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "invitation_company_id_company_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["inviter_id"], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "invitation_inviter_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "member_company_id_company_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "name": "member_user_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "organization_role_company_id_company_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "organization_role" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "session" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_company_id_fkey", + "entityType": "fks", + "schema": "notifications", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "notification_member_fkey", + "entityType": "fks", + "schema": "notifications", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_notice_member_fkey", + "entityType": "fks", + "schema": "notifications", + "table": "request_notice" + }, + { + "nameExplicit": true, + "columns": ["request_id"], + "schemaTo": "requests", + "tableTo": "request", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_notice_request_id_fkey", + "entityType": "fks", + "schema": "notifications", + "table": "request_notice" + }, + { + "nameExplicit": true, + "columns": ["company_id", "project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["company_id", "id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "app_access_company_id_project_id_fkey", + "entityType": "fks", + "schema": "projects", + "table": "app_access" + }, + { + "nameExplicit": true, + "columns": ["company_id"], + "schemaTo": "auth", + "tableTo": "company", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_company_id_fkey", + "entityType": "fks", + "schema": "requests", + "table": "request" + }, + { + "nameExplicit": true, + "columns": ["company_id", "requested_by"], + "schemaTo": "auth", + "tableTo": "member", + "columnsTo": ["company_id", "user_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_member_fkey", + "entityType": "fks", + "schema": "requests", + "table": "request" + }, + { + "nameExplicit": true, + "columns": ["project_id"], + "schemaTo": "projects", + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "request_project_id_fkey", + "entityType": "fks", + "schema": "requests", + "table": "request" + }, + { + "columns": ["agent_id", "tool_version_id"], + "nameExplicit": true, + "name": "agent_tool_pkey", + "entityType": "pks", + "schema": "builder", + "table": "agent_tool" + }, + { + "columns": ["agent_id", "number"], + "nameExplicit": true, + "name": "agent_version_pkey", + "entityType": "pks", + "schema": "builder", + "table": "agent_version" + }, + { + "columns": ["agent_id", "number", "tool_version_id"], + "nameExplicit": true, + "name": "agent_version_tool_pkey", + "entityType": "pks", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "columns": ["company_id", "kind"], + "nameExplicit": false, + "name": "company_account_kind_pkey", + "entityType": "pks", + "schema": "builder", + "table": "company_account_kind" + }, + { + "columns": ["account_id", "person_id"], + "nameExplicit": false, + "name": "model_account_assignee_pkey", + "entityType": "pks", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "columns": ["company_id", "person_id"], + "nameExplicit": false, + "name": "person_model_default_pkey", + "entityType": "pks", + "schema": "builder", + "table": "person_model_default" + }, + { + "columns": ["company_id", "sha256"], + "nameExplicit": true, + "name": "blob_pkey", + "entityType": "pks", + "schema": "apps", + "table": "blob" + }, + { + "columns": ["version_id", "path"], + "nameExplicit": true, + "name": "version_file_pkey", + "entityType": "pks", + "schema": "apps", + "table": "version_file" + }, + { + "columns": ["company_id", "thread_id", "tool_call_id"], + "nameExplicit": false, + "name": "connector_write_pkey", + "entityType": "pks", + "schema": "connectors", + "table": "connector_write" + }, + { + "columns": ["project_id", "connection_id"], + "nameExplicit": false, + "name": "project_connection_pkey", + "entityType": "pks", + "schema": "connectors", + "table": "project_connection" + }, + { + "columns": ["proposal_id", "connection_id"], + "nameExplicit": false, + "name": "copy_proposal_source_pkey", + "entityType": "pks", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "columns": ["copy_id", "connection_id"], + "nameExplicit": false, + "name": "copy_source_pkey", + "entityType": "pks", + "schema": "copies", + "table": "copy_source" + }, + { + "columns": ["company_id", "copy_id"], + "nameExplicit": false, + "name": "copy_table_left_pkey", + "entityType": "pks", + "schema": "copies", + "table": "copy_table_left" + }, + { + "columns": ["project_id", "copy_id"], + "nameExplicit": false, + "name": "project_copy_pkey", + "entityType": "pks", + "schema": "copies", + "table": "project_copy" + }, + { + "columns": ["project_id", "person_id"], + "nameExplicit": false, + "name": "app_access_pkey", + "entityType": "pks", + "schema": "projects", + "table": "app_access" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "agent_pkey", + "schema": "builder", + "table": "agent", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "agent_conversation_pkey", + "schema": "builder", + "table": "agent_conversation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "assistant_conversation_pkey", + "schema": "builder", + "table": "assistant_conversation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "conversation_pkey", + "schema": "builder", + "table": "conversation", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "company_model_pkey", + "schema": "builder", + "table": "company_model", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "company_model_fallback_pkey", + "schema": "builder", + "table": "company_model_fallback", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "model_account_pkey", + "schema": "builder", + "table": "model_account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "model_account_sign_in_pkey", + "schema": "builder", + "table": "model_account_sign_in", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "observational_memory_models_pkey", + "schema": "builder", + "table": "observational_memory_models", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "tool_pkey", + "schema": "builder", + "table": "tool", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "tool_version_pkey", + "schema": "builder", + "table": "tool_version", + "entityType": "pks" + }, + { + "columns": ["project_id"], + "nameExplicit": false, + "name": "app_pkey", + "schema": "apps", + "table": "app", + "entityType": "pks" + }, + { + "columns": ["token_hash"], + "nameExplicit": false, + "name": "app_session_pkey", + "schema": "apps", + "table": "app_session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "app_version_pkey", + "schema": "apps", + "table": "app_version", + "entityType": "pks" + }, + { + "columns": ["code_hash"], + "nameExplicit": false, + "name": "handoff_pkey", + "schema": "apps", + "table": "handoff", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "connection_pkey", + "schema": "connectors", + "table": "connection", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "connection_sign_in_pkey", + "schema": "connectors", + "table": "connection_sign_in", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "copy_pkey", + "schema": "copies", + "table": "copy", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "copy_proposal_pkey", + "schema": "copies", + "table": "copy_proposal", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "copy_run_pkey", + "schema": "copies", + "table": "copy_run", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pkey", + "schema": "auth", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "company_pkey", + "schema": "auth", + "table": "company", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "auth", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pkey", + "schema": "auth", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_role_pkey", + "schema": "auth", + "table": "organization_role", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "rate_limit_pkey", + "schema": "auth", + "table": "rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pkey", + "schema": "auth", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "user_pkey", + "schema": "auth", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "auth", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "notification_pkey", + "schema": "notifications", + "table": "notification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "request_notice_pkey", + "schema": "notifications", + "table": "request_notice", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "client_pkey", + "schema": "operations", + "table": "client", + "entityType": "pks" + }, + { + "columns": ["company_id"], + "nameExplicit": false, + "name": "company_database_pkey", + "schema": "projects", + "table": "company_database", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pkey", + "schema": "projects", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "request_pkey", + "schema": "requests", + "table": "request", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "starts_pkey", + "schema": "hub", + "table": "starts", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "agent_company_id_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "agent" + }, + { + "nameExplicit": true, + "columns": ["agent_id", "tool_id"], + "nullsNotDistinct": false, + "name": "agent_tool_agent_id_tool_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "agent_tool" + }, + { + "nameExplicit": true, + "columns": ["agent_id", "source_revision"], + "nullsNotDistinct": false, + "name": "agent_version_agent_id_source_revision_key", + "entityType": "uniques", + "schema": "builder", + "table": "agent_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "agent_id", "number"], + "nullsNotDistinct": false, + "name": "agent_version_company_id_agent_id_number_key", + "entityType": "uniques", + "schema": "builder", + "table": "agent_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "person_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "conversation_attempt_key", + "entityType": "uniques", + "schema": "builder", + "table": "conversation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "model_account_attempt_key", + "entityType": "uniques", + "schema": "builder", + "table": "model_account" + }, + { + "nameExplicit": true, + "columns": ["company_id", "slug"], + "nullsNotDistinct": false, + "name": "tool_company_id_slug_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "tool_company_id_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool" + }, + { + "nameExplicit": true, + "columns": ["tool_id", "number"], + "nullsNotDistinct": false, + "name": "tool_version_tool_id_number_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool_version" + }, + { + "nameExplicit": true, + "columns": ["tool_id", "id"], + "nullsNotDistinct": false, + "name": "tool_version_tool_id_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "tool_version_company_id_id_key", + "entityType": "uniques", + "schema": "builder", + "table": "tool_version" + }, + { + "nameExplicit": true, + "columns": ["project_id", "number"], + "nullsNotDistinct": false, + "name": "app_version_project_id_number_key", + "entityType": "uniques", + "schema": "apps", + "table": "app_version" + }, + { + "nameExplicit": true, + "columns": ["project_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "app_version_project_id_attempt_key_key", + "entityType": "uniques", + "schema": "apps", + "table": "app_version" + }, + { + "nameExplicit": true, + "columns": ["project_id", "id"], + "nullsNotDistinct": false, + "name": "app_version_project_id_id_key", + "entityType": "uniques", + "schema": "apps", + "table": "app_version" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "connection_attempt_key", + "entityType": "uniques", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": ["company_id", "connector_id", "id"], + "nullsNotDistinct": false, + "name": "connection_company_connector_id_key", + "entityType": "uniques", + "schema": "connectors", + "table": "connection" + }, + { + "nameExplicit": true, + "columns": ["company_id", "name"], + "nullsNotDistinct": false, + "name": "copy_company_id_name_key", + "entityType": "uniques", + "schema": "copies", + "table": "copy" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "copy_company_id_attempt_key_key", + "entityType": "uniques", + "schema": "copies", + "table": "copy" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "copy_company_id_id_key", + "entityType": "uniques", + "schema": "copies", + "table": "copy" + }, + { + "nameExplicit": true, + "columns": ["copy_id", "account_ref"], + "nullsNotDistinct": false, + "name": "copy_source_copy_id_account_ref_key", + "entityType": "uniques", + "schema": "copies", + "table": "copy_source" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "invitation_company_id_attempt_key_key", + "entityType": "uniques", + "schema": "auth", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": ["company_id", "user_id"], + "nullsNotDistinct": false, + "name": "member_company_id_user_id_key", + "entityType": "uniques", + "schema": "auth", + "table": "member" + }, + { + "nameExplicit": true, + "columns": ["company_id", "run_id"], + "nullsNotDistinct": false, + "name": "notification_run_key", + "entityType": "uniques", + "schema": "notifications", + "table": "notification" + }, + { + "nameExplicit": true, + "columns": ["request_id", "person_id", "kind"], + "nullsNotDistinct": false, + "name": "request_notice_key", + "entityType": "uniques", + "schema": "notifications", + "table": "request_notice" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "project_company_id_attempt_key_key", + "entityType": "uniques", + "schema": "projects", + "table": "project" + }, + { + "nameExplicit": true, + "columns": ["company_id", "id"], + "nullsNotDistinct": false, + "name": "project_company_id_id_key", + "entityType": "uniques", + "schema": "projects", + "table": "project" + }, + { + "nameExplicit": true, + "columns": ["company_id", "attempt_key"], + "nullsNotDistinct": false, + "name": "request_attempt_key", + "entityType": "uniques", + "schema": "requests", + "table": "request" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "company_slug_key", + "schema": "auth", + "table": "company", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["attempt_key"], + "nullsNotDistinct": false, + "name": "company_attempt_key_key", + "schema": "auth", + "table": "company", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["nickname"], + "nullsNotDistinct": false, + "name": "company_nickname_key", + "schema": "auth", + "table": "company", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["key"], + "nullsNotDistinct": false, + "name": "rate_limit_key_key", + "schema": "auth", + "table": "rate_limit", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["token"], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "auth", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "auth", + "table": "user", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["cnpj"], + "nullsNotDistinct": false, + "name": "client_cnpj_key", + "schema": "operations", + "table": "client", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["attempt_key"], + "nullsNotDistinct": false, + "name": "client_attempt_key_key", + "schema": "operations", + "table": "client", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["neon_project_id"], + "nullsNotDistinct": false, + "name": "company_database_neon_project_id_key", + "schema": "projects", + "table": "company_database", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": ["host_label"], + "nullsNotDistinct": false, + "name": "project_host_label_key", + "schema": "projects", + "table": "project", + "entityType": "uniques" + }, + { + "value": "\"revision\" > 0", + "name": "agent_revision_positive", + "entityType": "checks", + "schema": "builder", + "table": "agent" + }, + { + "value": "\"approval\" in ('ASK', 'AUTO')", + "name": "agent_tool_approval", + "entityType": "checks", + "schema": "builder", + "table": "agent_tool" + }, + { + "value": "\"number\" > 0", + "name": "agent_version_number_positive", + "entityType": "checks", + "schema": "builder", + "table": "agent_version" + }, + { + "value": "\"approval\" in ('ASK', 'AUTO')", + "name": "agent_version_tool_approval", + "entityType": "checks", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "value": "\"kind\" IN ('claude_subscription', 'google_cliproxy', 'openai_codex')", + "name": "company_account_kind_kind", + "entityType": "checks", + "schema": "builder", + "table": "company_account_kind" + }, + { + "value": "\"effort\" IN ('off', 'low', 'medium', 'high', 'xhigh', 'max')", + "name": "company_model_effort", + "entityType": "checks", + "schema": "builder", + "table": "company_model" + }, + { + "value": "\"kind\" IN ('api_key', 'claude_subscription', 'google_cliproxy', 'openai_codex')", + "name": "model_account_kind", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "\"state\" IN ('ready', 'paused', 'refused')", + "name": "model_account_state", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "char_length(\"label\") BETWEEN 1 AND 60", + "name": "model_account_label_length", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "\"position\" >= 0", + "name": "model_account_position", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "(\"kind\" = 'api_key') = (\"hint\" IS NOT NULL)", + "name": "model_account_hint", + "entityType": "checks", + "schema": "builder", + "table": "model_account" + }, + { + "value": "\"owner\" IN ('company', 'me')", + "name": "model_account_sign_in_owner", + "entityType": "checks", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "value": "\"kind\" IN ('claude_subscription', 'google_cliproxy', 'openai_codex')", + "name": "model_account_sign_in_kind", + "entityType": "checks", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "value": "char_length(\"label\") BETWEEN 1 AND 60", + "name": "model_account_sign_in_label_length", + "entityType": "checks", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "value": "\"effort\" IN ('off', 'low', 'medium', 'high', 'xhigh', 'max')", + "name": "person_model_default_effort", + "entityType": "checks", + "schema": "builder", + "table": "person_model_default" + }, + { + "value": "\"kind\" in ('PRESET', 'CODE')", + "name": "tool_kind", + "entityType": "checks", + "schema": "builder", + "table": "tool" + }, + { + "value": "(\"kind\" = 'PRESET') = (\"operation\" is not null)", + "name": "tool_operation_by_kind", + "entityType": "checks", + "schema": "builder", + "table": "tool" + }, + { + "value": "(\"sending_version_id\" IS NULL) = (\"claimed_until\" IS NULL)", + "name": "app_sending_has_claim", + "entityType": "checks", + "schema": "apps", + "table": "app" + }, + { + "value": "\"environment\" IN ('preview', 'published')", + "name": "app_session_environment", + "entityType": "checks", + "schema": "apps", + "table": "app_session" + }, + { + "value": "\"state\" IN ('sending', 'ready', 'failed', 'pruning')", + "name": "app_version_state", + "entityType": "checks", + "schema": "apps", + "table": "app_version" + }, + { + "value": "\"state\" <> 'failed' OR \"failure\" IS NOT NULL", + "name": "app_version_failed_has_failure", + "entityType": "checks", + "schema": "apps", + "table": "app_version" + }, + { + "value": "\"number\" > 0", + "name": "app_version_number_positive", + "entityType": "checks", + "schema": "apps", + "table": "app_version" + }, + { + "value": "\"digest\" ~ '^[0-9a-f]{64}$'", + "name": "app_version_digest_form", + "entityType": "checks", + "schema": "apps", + "table": "app_version" + }, + { + "value": "\"sha256\" ~ '^[0-9a-f]{64}$'", + "name": "blob_sha256_form", + "entityType": "checks", + "schema": "apps", + "table": "blob" + }, + { + "value": "octet_length(\"bytes\") <= 10485760", + "name": "blob_size", + "entityType": "checks", + "schema": "apps", + "table": "blob" + }, + { + "value": "\"environment\" IN ('preview', 'published')", + "name": "handoff_environment", + "entityType": "checks", + "schema": "apps", + "table": "handoff" + }, + { + "value": "\"size\" BETWEEN 0 AND 10485760", + "name": "version_file_size", + "entityType": "checks", + "schema": "apps", + "table": "version_file" + }, + { + "value": "char_length(\"name\") BETWEEN 1 AND 60 AND \"name\" = btrim(\"name\")", + "name": "connection_name", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "\"state\" IN ('NEW', 'ACTIVE', 'NEEDS_REAUTH')", + "name": "connection_state", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "(\"account_ref\" IS NULL) = (\"account_label\" IS NULL)\n AND (\"account_label\" IS NULL OR char_length(\"account_label\") BETWEEN 1 AND 120)", + "name": "connection_account", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "jsonb_typeof(\"notes\") = 'object' AND octet_length(\"notes\"::text) <= 32768", + "name": "connection_notes", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "\"connector_id\" ~ '^[a-z][a-z0-9-]{1,31}$'", + "name": "connection_connector_id", + "entityType": "checks", + "schema": "connectors", + "table": "connection" + }, + { + "value": "\"name\" IS NULL OR (char_length(\"name\") BETWEEN 1 AND 60 AND \"name\" = btrim(\"name\"))", + "name": "connection_sign_in_name", + "entityType": "checks", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "value": "(\"connection_id\" IS NULL) <> (\"name\" IS NULL)\n AND (\"connection_id\" IS NULL) = (\"connection_revision\" IS NULL)", + "name": "connection_sign_in_target", + "entityType": "checks", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "value": "(\"agent_id\" is not null and \"version_number\" is not null and \"person_id\" is null) or (\"agent_id\" is null and \"version_number\" is null and \"person_id\" is not null)", + "name": "connector_write_speaker", + "entityType": "checks", + "schema": "connectors", + "table": "connector_write" + }, + { + "value": "\"state\" in ('SENDING', 'DONE', 'REFUSED', 'UNCERTAIN')", + "name": "connector_write_state", + "entityType": "checks", + "schema": "connectors", + "table": "connector_write" + }, + { + "value": "(\"claim_token\" IS NULL) = (\"claimed_until\" IS NULL)", + "name": "copy_claim", + "entityType": "checks", + "schema": "copies", + "table": "copy" + }, + { + "value": "\"name\" ~ '^[a-z][a-z0-9_]{0,39}$'", + "name": "copy_name", + "entityType": "checks", + "schema": "copies", + "table": "copy" + }, + { + "value": "jsonb_typeof(\"definition\") = 'object'", + "name": "copy_definition", + "entityType": "checks", + "schema": "copies", + "table": "copy" + }, + { + "value": "\"name\" ~ '^[a-z][a-z0-9_]{0,39}$'", + "name": "copy_proposal_name", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "jsonb_typeof(\"definition\") = 'object'", + "name": "copy_proposal_definition", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "\"proved_rows\" BETWEEN 0 AND 50", + "name": "copy_proposal_proved_rows", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "\"state\" IN ('OPEN', 'ACCEPTED', 'DISMISSED')", + "name": "copy_proposal_state", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "\"state\" = 'ACCEPTED' OR \"copy_id\" IS NULL", + "name": "copy_proposal_copy", + "entityType": "checks", + "schema": "copies", + "table": "copy_proposal" + }, + { + "value": "(\"ended_at\" IS NULL) = (\"outcome\" IS NULL)", + "name": "copy_run_ended", + "entityType": "checks", + "schema": "copies", + "table": "copy_run" + }, + { + "value": "\"outcome\" IS NULL OR \"outcome\" IN ('COMPLETE', 'PARTIAL', 'ERROR', 'INTERRUPTED')", + "name": "copy_run_outcome", + "entityType": "checks", + "schema": "copies", + "table": "copy_run" + }, + { + "value": "\"position\" IS NULL OR jsonb_typeof(\"position\") = 'object'", + "name": "copy_source_position", + "entityType": "checks", + "schema": "copies", + "table": "copy_source" + }, + { + "value": "\"name\" ~ '^[a-z][a-z0-9_]{0,39}$'", + "name": "copy_table_left_name", + "entityType": "checks", + "schema": "copies", + "table": "copy_table_left" + }, + { + "value": "\"nickname\" ~ '^[a-z0-9]+(-[a-z0-9]+)*$' and char_length(\"nickname\") BETWEEN 3 AND 30", + "name": "company_nickname_form", + "entityType": "checks", + "schema": "auth", + "table": "company" + }, + { + "value": "(\"project_id\" is null) = (\"role\" is not null)", + "name": "invitation_kind", + "entityType": "checks", + "schema": "auth", + "table": "invitation" + }, + { + "value": "\"kind\" in ('AUTOMATION_DONE', 'AUTOMATION_FAILED', 'AUTOMATION_PAUSED')", + "name": "notification_kind", + "entityType": "checks", + "schema": "notifications", + "table": "notification" + }, + { + "value": "\"kind\" <> 'AUTOMATION_DONE' or (\"conversation_id\" is not null and \"failure_code\" is null)", + "name": "notification_done", + "entityType": "checks", + "schema": "notifications", + "table": "notification" + }, + { + "value": "\"kind\" <> 'AUTOMATION_FAILED' or (\"failure_code\" is not null and \"failure_code\" in ('MODEL_NO_ACCOUNT', 'MODEL_CALL_FAILED', 'MODEL_REFUSED', 'MODEL_ANSWER_CUT', 'MACHINE_UNAVAILABLE', 'MACHINE_LOST', 'HUB_UPDATED', 'INTERNAL_UNEXPECTED', 'DATABASE_UNAVAILABLE', 'INTERRUPTED', 'SCHEDULE_PUBLISH_FAILED'))", + "name": "notification_failed", + "entityType": "checks", + "schema": "notifications", + "table": "notification" + }, + { + "value": "\"kind\" <> 'AUTOMATION_PAUSED' or (\"conversation_id\" is null and \"failure_code\" is null)", + "name": "notification_paused", + "entityType": "checks", + "schema": "notifications", + "table": "notification" + }, + { + "value": "\"kind\" in ('REQUEST_OPENED', 'REQUEST_DECIDED')", + "name": "request_notice_kind", + "entityType": "checks", + "schema": "notifications", + "table": "request_notice" + }, + { + "value": "\"cnpj\" ~ '^[0-9A-Z]{12}[0-9]{2}$'", + "name": "client_cnpj_format", + "entityType": "checks", + "schema": "operations", + "table": "client" + }, + { + "value": "char_length(\"legal_name\") BETWEEN 1 AND 150", + "name": "client_legal_name_length", + "entityType": "checks", + "schema": "operations", + "table": "client" + }, + { + "value": "\"revision\" >= 0", + "name": "client_revision_not_negative", + "entityType": "checks", + "schema": "operations", + "table": "client" + }, + { + "value": "\"state\" IN ('creating', 'ready')", + "name": "company_database_state", + "entityType": "checks", + "schema": "projects", + "table": "company_database" + }, + { + "value": "(\"state\" = 'ready') = (\"neon_project_id\" IS NOT NULL)", + "name": "company_database_ready_has_project", + "entityType": "checks", + "schema": "projects", + "table": "company_database" + }, + { + "value": "(\"state\" = 'creating') = (\"claimed_until\" IS NOT NULL)", + "name": "company_database_creating_has_claim", + "entityType": "checks", + "schema": "projects", + "table": "company_database" + }, + { + "value": "\"host_label\" ~ '^[a-z0-9]+(-[a-z0-9]+)*--[a-z0-9]+(-[a-z0-9]+)*$' and char_length(\"host_label\") <= 63", + "name": "project_host_label_form", + "entityType": "checks", + "schema": "projects", + "table": "project" + }, + { + "value": "char_length(\"name\") BETWEEN 1 AND 100", + "name": "project_name_length", + "entityType": "checks", + "schema": "projects", + "table": "project" + }, + { + "value": "\"kind\" in ('PUBLISH_APP_VERSION')", + "name": "request_kind", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "\"number\" > 0", + "name": "request_number", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "char_length(\"message\") <= 280", + "name": "request_message", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "\"state\" in ('PENDING', 'APPROVED', 'DECLINED', 'CANCELLED', 'SUPERSEDED')", + "name": "request_state", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "(\"state\" in ('PENDING', 'CANCELLED')) = (\"decided_by\" is null)", + "name": "request_decided", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "\"reason\" is null or \"state\" = 'DECLINED'", + "name": "request_reason", + "entityType": "checks", + "schema": "requests", + "table": "request" + }, + { + "value": "\"version\" ~ '^v[0-9]+\\.[0-9]+\\.[0-9]+$'", + "name": "starts_version_format", + "entityType": "checks", + "schema": "hub", + "table": "starts" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_agent_companies"], + "using": "true", + "withCheck": null, + "name": "agent_companies_select", + "entityType": "policies", + "schema": "builder", + "table": "agent" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent_conversation" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent_tool" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent_version" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "agent_version_tool" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "assistant_conversation" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "conversation" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "company_select", + "entityType": "policies", + "schema": "builder", + "table": "company_account_kind" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "builder", + "table": "company_account_kind" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "operator_insert", + "entityType": "policies", + "schema": "builder", + "table": "company_account_kind" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "builder", + "table": "company_account_kind" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "company_model" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "company_model_fallback" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id is null or person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id is null or person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "model_account" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_model_seals"], + "using": "true", + "withCheck": null, + "name": "model_seals_select", + "entityType": "policies", + "schema": "builder", + "table": "model_account" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "model_account_assignee" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_model_seals"], + "using": "true", + "withCheck": null, + "name": "model_seals_select", + "entityType": "policies", + "schema": "builder", + "table": "model_account_sign_in" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "observational_memory_models" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "person_model_default" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "tool" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "builder", + "table": "tool_version" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "apps", + "table": "app" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "apps", + "table": "app" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "apps", + "table": "app" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_app_companies"], + "using": "true", + "withCheck": null, + "name": "companies_select", + "entityType": "policies", + "schema": "apps", + "table": "app" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "apps", + "table": "app_version" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "apps", + "table": "app_version" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "apps", + "table": "app_version" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "apps", + "table": "blob" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "apps", + "table": "blob" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "apps", + "table": "blob" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "apps", + "table": "version_file" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "apps", + "table": "version_file" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "apps", + "table": "version_file" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "connectors", + "table": "connection" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_connection_seals"], + "using": "true", + "withCheck": null, + "name": "connection_seals_select", + "entityType": "policies", + "schema": "connectors", + "table": "connection" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')\n and (person_id = nullif(current_setting('conexus.person_id', true), '') or nullif(current_setting('conexus.person_id', true), '') is null)", + "name": "company_all", + "entityType": "policies", + "schema": "connectors", + "table": "connection_sign_in" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "connectors", + "table": "connector_write" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "connectors", + "table": "project_connection" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_proposal" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_proposal_source" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_run" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_source" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "copy_table_left" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "copies", + "table": "project_copy" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_app_people"], + "using": "true", + "withCheck": null, + "name": "app_people_select", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_person"], + "using": "exists (select 1 from auth.member m where m.company_id = company.id and m.user_id = nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": null, + "name": "person_select", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_invitation"], + "using": "exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = company.id)", + "withCheck": "exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = company.id)", + "name": "invitation_all", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "name": "operator_insert", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and id = nullif(current_setting('conexus.company_id', true), '')", + "name": "operator_update", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "auth", + "table": "company" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_person"], + "using": "inviter_id = nullif(current_setting('conexus.person_id', true), '')\n or lower(email) = (select u.email from auth.\"user\" u where u.id = nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": null, + "name": "person_select", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_invitation"], + "using": "id = nullif(current_setting('conexus.invitation_id', true), '')", + "withCheck": "id = nullif(current_setting('conexus.invitation_id', true), '')", + "name": "invitation_all", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and (role = 'owner' or company_id = nullif(current_setting('conexus.company_id', true), ''))", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and role = 'owner'", + "name": "operator_insert", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and role = 'owner'", + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and role = 'owner'", + "name": "operator_update", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "auth", + "table": "invitation" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_person"], + "using": "user_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": null, + "name": "person_select", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_invitation"], + "using": "exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = member.company_id)", + "withCheck": null, + "name": "invitation_select", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_invitation"], + "using": null, + "withCheck": "user_id = nullif(current_setting('conexus.person_id', true), '')\n and exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = member.company_id)", + "name": "invitation_insert", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and (role = 'owner' or company_id = nullif(current_setting('conexus.company_id', true), ''))", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "auth", + "table": "member" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "organization_role" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "name": "operator_insert", + "entityType": "policies", + "schema": "auth", + "table": "organization_role" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_runtime"], + "using": "true", + "withCheck": "true", + "name": "runtime_all", + "entityType": "policies", + "schema": "auth", + "table": "session" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "user_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "user_id = nullif(current_setting('conexus.person_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "session" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_invitation"], + "using": "user_id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.invitation i join auth.\"user\" u on u.email = lower(i.email)\n where i.id = nullif(current_setting('conexus.invitation_id', true), '') and u.id = session.user_id)", + "withCheck": "user_id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.invitation i join auth.\"user\" u on u.email = lower(i.email)\n where i.id = nullif(current_setting('conexus.invitation_id', true), '') and u.id = session.user_id)", + "name": "invitation_all", + "entityType": "policies", + "schema": "auth", + "table": "session" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_runtime"], + "using": "true", + "withCheck": "true", + "name": "runtime_sign_in", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.member m where m.user_id = \"user\".id and m.company_id = nullif(current_setting('conexus.company_id', true), ''))\n or exists (select 1 from auth.invitation i where i.inviter_id = \"user\".id and i.company_id = nullif(current_setting('conexus.company_id', true), ''))\n or exists (select 1 from projects.app_access a where a.person_id = \"user\".id and a.company_id = nullif(current_setting('conexus.company_id', true), ''))", + "withCheck": "id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.member m where m.user_id = \"user\".id and m.company_id = nullif(current_setting('conexus.company_id', true), ''))\n or exists (select 1 from auth.invitation i where i.inviter_id = \"user\".id and i.company_id = nullif(current_setting('conexus.company_id', true), ''))\n or exists (select 1 from projects.app_access a where a.person_id = \"user\".id and a.company_id = nullif(current_setting('conexus.company_id', true), ''))", + "name": "company_all", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_person"], + "using": "id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "id = nullif(current_setting('conexus.person_id', true), '')", + "name": "person_all", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_invitation"], + "using": "id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '')\n and (i.inviter_id = \"user\".id or lower(i.email) = \"user\".email))", + "withCheck": "id = nullif(current_setting('conexus.person_id', true), '')\n or exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '')\n and (i.inviter_id = \"user\".id or lower(i.email) = \"user\".email))", + "name": "invitation_all", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_administrators"], + "using": "role = 'admin'", + "withCheck": null, + "name": "administrators_select", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and (role = 'admin'\n or exists (select 1 from auth.member m where m.user_id = \"user\".id and m.role = 'owner'))", + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and (role = 'admin'\n or exists (select 1 from auth.member m where m.user_id = \"user\".id and m.role = 'owner'))", + "name": "operator_all", + "entityType": "policies", + "schema": "auth", + "table": "user" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "notifications", + "table": "notification" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": null, + "name": "company_select", + "entityType": "policies", + "schema": "notifications", + "table": "request_notice" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '') and person_id = nullif(current_setting('conexus.person_id', true), '')", + "name": "company_update", + "entityType": "policies", + "schema": "notifications", + "table": "request_notice" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_company"], + "using": null, + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_insert", + "entityType": "policies", + "schema": "notifications", + "table": "request_notice" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "operations", + "table": "client" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_operator"], + "using": null, + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "name": "operator_insert", + "entityType": "policies", + "schema": "operations", + "table": "client" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "withCheck": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), ''))", + "name": "operator_update", + "entityType": "policies", + "schema": "operations", + "table": "client" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "operations", + "table": "client" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "projects", + "table": "app_access" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": ["hub_invitation"], + "using": null, + "withCheck": "exists (select 1 from auth.invitation i\n join auth.\"user\" u on lower(u.email) = lower(i.email)\n where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.company_id = app_access.company_id\n and i.project_id = app_access.project_id and i.app_role is not distinct from app_access.role\n and u.id = app_access.person_id)", + "name": "invitation_insert", + "entityType": "policies", + "schema": "projects", + "table": "app_access" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_invitation"], + "using": "exists (select 1 from auth.invitation i\n join auth.\"user\" u on lower(u.email) = lower(i.email)\n where i.id = nullif(current_setting('conexus.invitation_id', true), '') and i.project_id = app_access.project_id\n and u.id = app_access.person_id)", + "withCheck": null, + "name": "invitation_select", + "entityType": "policies", + "schema": "projects", + "table": "app_access" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_app_people"], + "using": "true", + "withCheck": null, + "name": "app_people_select", + "entityType": "policies", + "schema": "projects", + "table": "app_access" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "projects", + "table": "company_database" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "projects", + "table": "company_database" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "projects", + "table": "company_database" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_select", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": ["hub_operator"], + "using": "auth.is_installation_administrator(nullif(current_setting('conexus.person_id', true), '')) and company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": null, + "name": "operator_delete", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_hosts"], + "using": "true", + "withCheck": null, + "name": "hosts_select", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_app_people"], + "using": "true", + "withCheck": null, + "name": "app_people_select", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": ["hub_invitation"], + "using": "exists (select 1 from auth.invitation i where i.id = nullif(current_setting('conexus.invitation_id', true), '')\n and i.company_id = project.company_id and i.project_id = project.id)", + "withCheck": null, + "name": "invitation_select", + "entityType": "policies", + "schema": "projects", + "table": "project" + }, + { + "as": "PERMISSIVE", + "for": "ALL", + "roles": ["hub_company"], + "using": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "withCheck": "company_id = nullif(current_setting('conexus.company_id', true), '')", + "name": "company_all", + "entityType": "policies", + "schema": "requests", + "table": "request" + } + ], + "renames": [] +} diff --git a/apps/server/src/hub/connectors/builder-port.ts b/apps/server/src/hub/connectors/builder-port.ts index b0c7ad697..140d56fee 100644 --- a/apps/server/src/hub/connectors/builder-port.ts +++ b/apps/server/src/hub/connectors/builder-port.ts @@ -1,4 +1,4 @@ -import type { CopyDefinition } from '@conexus/connectors' +import type { CopyDefinitionV1 } from '@conexus/connectors' import { connectorOf } from '@conexus/connectors' import type { CompanyId, @@ -92,7 +92,7 @@ export type BuilderConnectors = { /** One page of the copy read, as the person may read the connection: the count, never a row. */ provePage( ids: OfProject, - a: Of & Readonly<{ definition: CopyDefinition; signal: AbortSignal }>, + a: Of & Readonly<{ definition: CopyDefinitionV1; signal: AbortSignal }>, ): Promise /** A copy proposed in the Projeto's conversation, proved by one page; `hub/copies` owns it. */ propose: Propose diff --git a/apps/server/src/hub/connectors/connection-copies.test.ts b/apps/server/src/hub/connectors/connection-copies.test.ts new file mode 100644 index 000000000..48f981dcb --- /dev/null +++ b/apps/server/src/hub/connectors/connection-copies.test.ts @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { ordersCopy, STORE_A, STORE_B, storesOn } from '../copies/test-sources.ts' +import { call, codeOf, personOn, plantCompany } from '../identity/test-hub.ts' + +test('as cópias que o Desligar apaga', async (t) => { + const on = await storesOn(t) + const both = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref, STORE_B.ref]) + const alone = await on.planted('pedidos_a', ordersCopy(true), [STORE_A.ref]) + assert.deepEqual(await on.ended(both), { kind: 'COMPLETE' }) + assert.deepEqual(await on.ended(alone), { kind: 'COMPLETE' }) + function path(ref: string) { + return `${on.base}/${on.stores.get(ref)}/copies` + } + + const listed = await call(on.hub, on.ana, 'GET', path(STORE_A.ref)) + assert.equal(listed.statusCode, 200, listed.body) + assert.deepEqual(Object(listed.json()).copies, [ + { copyId: both, name: 'pedidos', mode: { kind: 'WHOLE' }, last: false, rows: 120 }, + { copyId: alone, name: 'pedidos_a', mode: { kind: 'CHANGES' }, last: true, rows: 120 }, + ]) + const ofB = await call(on.hub, on.ana, 'GET', path(STORE_B.ref)) + assert.deepEqual( + Object(ofB.json()).copies.map((copy: { name: string; last: boolean }) => [ + copy.name, + copy.last, + ]), + [['pedidos', false]], + ) + + const silent = t.mock.method(on.hub.postgres.companyPostgres, 'accountRows', async () => ({ + kind: 'NO_ANSWER' as const, + })) + const quiet = await call(on.hub, on.ana, 'GET', path(STORE_A.ref)) + assert.deepEqual( + Object(quiet.json()).copies.map((copy: { rows: number | null }) => copy.rows), + [null, 120], + 'a contagem de quem segue vem nula; a da última fonte é a da cópia', + ) + silent.mock.restore() + + const carla = await call(on.hub, on.carla, 'GET', path(STORE_A.ref)) + assert.deepEqual([carla.statusCode, codeOf(carla)], [404, 'NOT_FOUND'], 'sem manageConnections') + const davi = await personOn(on.hub, { name: 'Davi Souza', email: 'davi@example.test' }) + const other = await plantCompany(on.hub.test, davi.id, 'Mercearia Lua') + const elsewhere = await call( + on.hub, + davi, + 'GET', + `/api/companies/${other}/connections/${on.stores.get(STORE_A.ref)}/copies`, + ) + assert.deepEqual( + [elsewhere.statusCode, codeOf(elsewhere)], + [404, 'CONNECTION_NOT_FOUND'], + 'uma conexão de outra empresa', + ) +}) + +test('as cópias que o Desligar apaga: uma conexão sem cópia', async (t) => { + const on = await storesOn(t, { stores: [STORE_A] }) + const listed = await call( + on.hub, + on.ana, + 'GET', + `${on.base}/${on.stores.get(STORE_A.ref)}/copies`, + ) + assert.deepEqual([listed.statusCode, Object(listed.json()).copies], [200, []]) +}) diff --git a/apps/server/src/hub/connectors/connection-copies.ts b/apps/server/src/hub/connectors/connection-copies.ts new file mode 100644 index 000000000..b394b4889 --- /dev/null +++ b/apps/server/src/hub/connectors/connection-copies.ts @@ -0,0 +1,13 @@ +import { viewIn } from './connections.ts' +import type { Deps, OfConnection } from './rows.ts' +import { admitted, scopeOf } from './rows.ts' + +/** The copies that read the connection, as Desligar shows them before the click; admitted as Desligar is. */ +export async function copiesOfConnection(deps: Deps, input: OfConnection) { + await deps.database.command(scopeOf(input), async (tx) => { + await admitted(deps, tx, input, 'manageConnections') + await viewIn(tx, input, input.connectionId) + }) + const { companyId, connectionId } = input + return deps.copies().copiesOf({ companyId, personId: input.person.id, connectionId }) +} diff --git a/apps/server/src/hub/connectors/connections-revoke.test.ts b/apps/server/src/hub/connectors/connections-revoke.test.ts index 6d628133e..e2e70368d 100644 --- a/apps/server/src/hub/connectors/connections-revoke.test.ts +++ b/apps/server/src/hub/connectors/connections-revoke.test.ts @@ -25,7 +25,7 @@ test('Desligar pede ao Mercado Livre que tire a autorização, com a chave viva, const sealed = await on.hub.test.asOwner('select count(*)::int as n from connectors.connection') assert.equal(sealed[0]?.n, 1) const gone = await unlinked(on, id) - assert.deepEqual([gone.status, gone.body], [200, { revocation: 'REVOKED' }]) + assert.deepEqual([gone.status, gone.body], [200, { revocation: 'REVOKED', tablesLeft: [] }]) assert.deepEqual(on.fake.revocations(), [ `${on.fake.switches.account.ref}/${on.fake.app.clientId}`, ]) @@ -48,7 +48,7 @@ test('o Mercado Livre recusa: a ligação sai, a pessoa lê REFUSED e o log tem const id = await linked(on) on.fake.switches.refuseRevoke = true const gone = await unlinked(on, id) - assert.deepEqual([gone.status, gone.body], [200, { revocation: 'REFUSED' }]) + assert.deepEqual([gone.status, gone.body], [200, { revocation: 'REFUSED', tablesLeft: [] }]) assert.equal((await on.hub.test.asOwner('select 1 from connectors.connection')).length, 0) const line = lineNamed(on, 'CONNECTION_REVOKE_REFUSED') assert.equal(line?.fields.status, 403) @@ -60,7 +60,7 @@ test('o Mercado Livre fora do ar: a ligação sai, a pessoa lê UNREACHABLE e o const id = await linked(on) await on.served.close() const gone = await unlinked(on, id) - assert.deepEqual([gone.status, gone.body], [200, { revocation: 'UNREACHABLE' }]) + assert.deepEqual([gone.status, gone.body], [200, { revocation: 'UNREACHABLE', tablesLeft: [] }]) assert.equal((await on.hub.test.asOwner('select 1 from connectors.connection')).length, 0) assert.ok(lineNamed(on, 'CONNECTION_REVOKE_UNANSWERED')) }) @@ -72,7 +72,7 @@ test('desligar de novo, e quem não pode: nada é enviado', async (t) => { assert.equal(member.statusCode, 404) assert.equal(on.fake.revocations().length, 0) assert.equal((await unlinked(on, id)).body.revocation, 'REVOKED') - assert.deepEqual((await unlinked(on, id)).body, { revocation: 'NONE' }) + assert.deepEqual((await unlinked(on, id)).body, { revocation: 'NONE', tablesLeft: [] }) assert.equal(on.fake.revocations().length, 1) }) @@ -81,7 +81,7 @@ test('o selo não abre: nada é enviado, a ligação sai, a pessoa lê NOT_SENT const id = await linked(on) await on.hub.test.asOwner(`update connectors.connection set sealed = sealed || '\\x00'::bytea`) const gone = await unlinked(on, id) - assert.deepEqual([gone.status, gone.body], [200, { revocation: 'NOT_SENT' }]) + assert.deepEqual([gone.status, gone.body], [200, { revocation: 'NOT_SENT', tablesLeft: [] }]) assert.equal(on.fake.revocations().length, 0) assert.equal((await on.hub.test.asOwner('select 1 from connectors.connection')).length, 0) assert.ok(lineNamed(on, 'CONNECTION_REVOKE_NOT_SENT')) @@ -94,7 +94,7 @@ test('o vendedor já tirou o Conexus no Mercado Livre: sem chave viva nada é en on.fake.switches.revoke = true on.fake.expireAccess() const gone = await unlinked(on, id) - assert.deepEqual([gone.status, gone.body], [200, { revocation: 'NOT_SENT' }]) + assert.deepEqual([gone.status, gone.body], [200, { revocation: 'NOT_SENT', tablesLeft: [] }]) assert.equal(on.fake.revocations().length, 0) assert.equal((await on.hub.test.asOwner('select 1 from connectors.connection')).length, 0) const line = lineNamed(on, 'CONNECTION_REVOKE_NOT_SENT') diff --git a/apps/server/src/hub/connectors/connections.ts b/apps/server/src/hub/connectors/connections.ts index 1ce3c7bae..2c5903616 100644 --- a/apps/server/src/hub/connectors/connections.ts +++ b/apps/server/src/hub/connectors/connections.ts @@ -21,7 +21,16 @@ import { subjectsOf } from './catalog-subjects.ts' import type { BearerToken, OpenedConnection } from './executor.ts' import { notesOf } from './notes.ts' import type { Deps, OfConnection, Viewer } from './rows.ts' -import { admitted, columns, ofCompany, scopeOf, sealContext, sealedOf, viewOf } from './rows.ts' +import { + admitted, + columns, + connectorIn, + ofCompany, + scopeOf, + sealContext, + sealedOf, + viewOf, +} from './rows.ts' import { connection } from './tables.ts' import { opened } from './testing.ts' import { accessFor, statusOf } from './tokens.ts' @@ -199,7 +208,7 @@ async function sealedText( return open(deps.sealKeys, sealContext(row), await sealedOf(tx, row.id)).reveal() } -async function viewIn(tx: ReadTx, viewer: Viewer, id: ConnectionId): Promise { +export async function viewIn(tx: ReadTx, viewer: Viewer, id: ConnectionId): Promise { const [row] = await tx.select(columns).from(connection).where(ofCompany(viewer, id)) if (row === undefined) throw new Failure('CONNECTION_NOT_FOUND') return viewOf(row) @@ -213,7 +222,7 @@ async function revoked(deps: Deps, input: OfConnection): Promise { .from(connection) .where(ofCompany(input, input.connectionId)), ) - const declared = row === undefined ? undefined : connectorOf(row.connector) + const declared = row === undefined ? undefined : connectorIn(deps, row.connector) if (declared === undefined || !isOAuth(declared) || declared.auth.revoke === undefined) return 'NONE' const where = whereOf(input, declared) @@ -287,26 +296,37 @@ function whereOf(input: OfConnection, connector: Connector) { /** * Removes a connection and its seal; removing one already gone answers the same (criterion 13). - * Before, a connector that declares a revoke is asked to take the authorization back with the - * connection's live token. The provider refusing or being down never stops the removal: the - * company asked to disconnect. What happened comes back, and is logged by name. + * First the account's rows leave the company's copies, so a silent company database refuses before + * anything leaves the Hub or the provider (O Sync de qualquer conector). Then a connector that + * declares a revoke is asked to take the authorization back; the provider refusing or being down + * never stops the removal. Then, in one transaction, the connection's copies locked, the connection + * deleted with its sources and each copy left with no source ended; last their tables dropped. */ export async function unlink(deps: Deps, input: Viewer & { connectionId: ConnectionId }) { await deps.database.command(scopeOf(input), (tx) => admitted(deps, tx, input, 'manageConnections'), ) + const { companyId, connectionId } = input + await deps.copies().leave({ companyId, personId: input.person.id, connectionId }) const revocation = await revoked(deps, input) const removed = await deps.database.command(scopeOf(input), async (tx) => { await admitted(deps, tx, input, 'manageConnections') - return tx + const copyIds = await deps.copies().locked(tx, { companyId, connectionId }) + const rows = await tx .delete(connection) - .where(ofCompany(input, input.connectionId)) + .where(ofCompany(input, connectionId)) .returning({ id: connection.id }) + if (rows.length === 0) return undefined + const ended = await deps.copies().ended(tx, { companyId, copyIds }) + return copyIds.map((copyId) => ({ copyId, ended: ended.includes(copyId) })) }) - await deps.limits.forget(input.connectionId) + await deps.limits.forget(connectionId) + const { tablesLeft } = await deps.copies().dropEnded({ companyId, personId: input.person.id }) return { - removed: removed.length > 0, - revocation: removed.length > 0 ? revocation : ('NONE' as const), + removed: removed !== undefined, + revocation: removed === undefined ? ('NONE' as const) : revocation, + copies: removed ?? [], + tablesLeft, } } diff --git a/apps/server/src/hub/connectors/copy-lane.test.ts b/apps/server/src/hub/connectors/copy-lane.test.ts new file mode 100644 index 000000000..8e2ea80b0 --- /dev/null +++ b/apps/server/src/hub/connectors/copy-lane.test.ts @@ -0,0 +1,144 @@ +import assert from 'node:assert/strict' +import type { TestContext } from 'node:test' +import { test } from 'node:test' +import { setTimeout as sleep } from 'node:timers/promises' +import type { Connector } from '@conexus/connectors' +import type { CopyId } from '@conexus/hub-api' +import { CompanyId, ConnectionId, PersonId } from '@conexus/hub-api' +import type { StoresOn } from '../copies/test-sources.ts' +import { + BY_ACCOUNT, + callsOf, + ITEMS_COPY, + ordersCopy, + STORE_A, + STORE_B, + storesOn, +} from '../copies/test-sources.ts' +import { personOn, plantCompany, post } from '../identity/test-hub.ts' +import { linkMercadoLivre, testOf } from './test-connections.ts' + +const WAIT = { kind: 'PARTIAL', reason: 'WAIT' } + +/** + * Empresa Teste with Loja A, and Mercearia Lua with Loja B on the same Hub. Two companies' copies + * never meet in one database here: every act on it answers done, so only the calls are watched. + */ +async function twoCompanies(t: TestContext, catalog?: readonly Connector[]) { + const on = await storesOn(t, { catalog, stores: [STORE_A], companyDatabase: false }) + const davi = await personOn(on.hub, { name: 'Davi Souza', email: 'davi@example.test' }) + const lua = CompanyId.parse(await plantCompany(on.hub.test, davi.id, 'Mercearia Lua')) + on.fake.switches.account = STORE_B + const linked = await linkMercadoLivre(on.hub, davi, lua, { + connector: 'mercado-livre', + name: 'Loja Lua', + }) + const connectionId = ConnectionId.parse(Object(linked.json()).connection.id) + await testOf(on.hub, davi, `/api/companies/${lua}/connections`, connectionId) + on.stores.set(STORE_B.ref, connectionId) + const made = await post(on.hub, davi, `/api/companies/${lua}/projects`, { name: 'Pedidos' }) + assert.equal(made.statusCode, 201, made.body) + const daviId = PersonId.parse(davi.id) + for (const [companyId, personId] of [ + [on.companyId, on.anaId], + [lua, daviId], + ] as const) + await on.hub.test.database.command({ kind: 'COMPANY', companyId, personId }, (tx) => + on.hub.projects.ports.recordDatabaseHost(tx, { companyId, host: `${companyId}.test` }), + ) + const sql = on.hub.postgres.companyPostgres + t.mock.method(sql, 'syncAddress', async () => on.owner) + t.mock.method(sql, 'writePage', async () => ({ kind: 'DONE' as const, value: [] })) + t.mock.method(sql, 'closePass', async () => ({ kind: 'DONE' as const, rows: 0 })) + t.mock.method(sql, 'dropForeignRows', async () => ({ kind: 'DONE' as const, value: [] })) + on.door.seen.length = 0 + return { + on, + lua: { companyId: lua, personId: daviId }, + run: (companyId: CompanyId, copyId: CopyId) => + on.copies.run({ companyId, copyId }, new AbortController().signal), + } +} + +async function until(holds: () => boolean, what: string) { + for (let waited = 0; !holds(); waited += 10) { + assert.ok(waited < 800, what) + await sleep(10) + } +} + +function holdCalls(on: StoresOn) { + const held = Promise.withResolvers() + on.door.held = held + return () => { + on.door.held = undefined + held.resolve() + } +} + +test('uma chamada de cópia por vez por conector', async (t) => { + const { on, lua, run } = await twoCompanies(t) + const a = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref]) + const b = await on.planted('pedidos', ordersCopy(false), [STORE_B.ref], lua) + const release = holdCalls(on) + const first = run(on.companyId, a) + await until(() => on.door.open === 1, 'a primeira chamada chega ao dublê') + const second = run(lua.companyId, b) + await sleep(300) + assert.equal(on.door.open, 1, 'a outra empresa espera a vez') + assert.equal(callsOf(on.door, STORE_B).length, 0) + release() + assert.deepEqual(await Promise.all([first, second]), [ + { kind: 'ENDED', outcome: { kind: 'COMPLETE' } }, + { kind: 'ENDED', outcome: { kind: 'COMPLETE' } }, + ]) + assert.equal(on.door.most, 1, 'nunca duas chamadas abertas') + assert.ok(callsOf(on.door, STORE_A).length > 1 && callsOf(on.door, STORE_B).length > 1) +}) + +test('uma chamada de cópia por vez por conector: o que conta por conta se sobrepõe', async (t) => { + const { on, lua, run } = await twoCompanies(t, BY_ACCOUNT) + const a = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref]) + const b = await on.planted('pedidos', ordersCopy(false), [STORE_B.ref], lua) + const release = holdCalls(on) + const both = Promise.all([run(on.companyId, a), run(lua.companyId, b)]) + await until(() => on.door.open === 2, 'as duas contas chamam juntas') + release() + assert.deepEqual(await both, [ + { kind: 'ENDED', outcome: { kind: 'COMPLETE' } }, + { kind: 'ENDED', outcome: { kind: 'COMPLETE' } }, + ]) + assert.equal(on.door.most, 2) +}) + +test('uma chamada de cópia por vez por conector: o prazo passa na fila', async (t) => { + const { on, lua, run } = await twoCompanies(t) + const a = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref]) + const b = await on.planted('pedidos', ordersCopy(false), [STORE_B.ref], lua) + const items = await on.planted('anuncios', ITEMS_COPY, [STORE_B.ref], lua) + on.door.busy.set(2, null) + assert.deepEqual(await run(lua.companyId, items), { kind: 'ENDED', outcome: WAIT }) + const [kept] = await on.sourcesOf(items) + assert.equal(Object(kept?.position).kind, 'CURSOR') + const asked = on.door.seen.length + + const release = holdCalls(on) + const first = run(on.companyId, a) + await until(() => on.door.open === 1, 'a primeira chamada chega ao dublê') + const second = run(lua.companyId, b) + await sleep(100) + assert.deepEqual(await run(lua.companyId, items), { kind: 'ENDED', outcome: WAIT }) + assert.equal( + on.door.seen.slice(asked).filter((url) => url.pathname.includes('/items/')).length, + 0, + 'sem chamar', + ) + const [after] = await on.sourcesOf(items) + assert.deepEqual( + [after?.position, after?.pass, after?.wait_until], + [kept?.position, kept?.pass, null], + 'a posição fica, sem wait_until', + ) + release() + await Promise.all([first, second]) +}) diff --git a/apps/server/src/hub/connectors/copy-port.test.ts b/apps/server/src/hub/connectors/copy-port.test.ts index e18de617f..b01fc5544 100644 --- a/apps/server/src/hub/connectors/copy-port.test.ts +++ b/apps/server/src/hub/connectors/copy-port.test.ts @@ -61,13 +61,11 @@ test('a cópia toma a vez da conexão página a página', async (t) => { connectionId, // A member, whose role holds no operation of the connector: the copy speaks as its version. enabledBy: PersonId.parse(on.carla.id), - input: { - definition, - after, - since: '1900-01-01', - windowFrom: null, - limit: 50, - }, + definition, + position: after === null ? null : { kind: 'KEY', after }, + since: null, + windowFrom: null, + limit: 50, signal: new AbortController().signal, }) } diff --git a/apps/server/src/hub/connectors/copy-port.ts b/apps/server/src/hub/connectors/copy-port.ts index f03e13648..7185744a1 100644 --- a/apps/server/src/hub/connectors/copy-port.ts +++ b/apps/server/src/hub/connectors/copy-port.ts @@ -1,16 +1,34 @@ -import type { ConnectorId, CopyModel, CopyPage, PageInput } from '@conexus/connectors' -import { CONNECTORS, CopyPageSchema, connectorOf } from '@conexus/connectors' +import type { + ConnectorId, + CopyDefinition, + CopyDefinitionV1, + CopyDefinitionV2, + CopyModel, + CopyPage, + CopyPosition, + Operation, + PageInput, +} from '@conexus/connectors' +import { + AccountRef, + CopyPageSchema, + connectorOf, + listInputOf, + listPageOf, + Rows, +} from '@conexus/connectors' import type { CompanyId, ConnectionId, PersonId } from '@conexus/hub-api' import { ConnectorId as HubConnectorId, OperationId } from '@conexus/hub-api' import { and, eq, inArray } from 'drizzle-orm' import type { ReadTx } from '../../platform/database.ts' import { Failure } from '../../platform/failure.ts' import { speakerOf } from '../identity/public.ts' +import type { OpenedConnection } from './executor.ts' import type { NotInRole, ReadAnswer } from './read.ts' import { readThrough } from './read.ts' import { operationLabel } from './role-kit.ts' import type { Deps } from './rows.ts' -import { usable } from './rows.ts' +import { connectorIn, usable } from './rows.ts' import { lackedDoorRead } from './speaking.ts' import { connection } from './tables.ts' import { openedIn } from './testing.ts' @@ -21,14 +39,26 @@ type CopySourceFailure = | 'COPY_SOURCE_UNREACHABLE' | 'COPY_SOURCE_UNREADABLE' -/** The read's refusal whole, as `readThrough` gave it: the run keeps the code, the Builder's proof gives it all to the model (C79). */ +/** The read's refusal whole, as `readThrough` gave it: the Builder's proof gives it all to the model (C79). */ export type CopyRefusal = Exclude | NotInRole -export type CopyRead = +/** One page of a version 1 copy through the connector's paged read, for the run and the Builder's proof. */ +type PagedRead = | Readonly<{ kind: 'PAGE'; page: CopyPage }> - | Readonly<{ kind: 'FAILED'; code: CopySourceFailure; refusal: CopyRefusal }> + | Readonly<{ kind: 'REFUSED'; refusal: CopyRefusal }> /** The connection is gone, not ACTIVE, or of a connector that offers no copies. */ | Readonly<{ kind: 'NO_CONNECTION' }> + | InLine + +/** One page of a copy's source as the run reads it. */ +export type CopyRead = + | Readonly<{ kind: 'PAGE'; page: CopyPage; next: CopyPosition | null; total: number | null }> + /** The provider answered 429 (decision 0058), or the connector's lane passed its deadline (`waitMs` null). */ + | Readonly<{ kind: 'WAIT'; waitMs: number | null }> + /** The cursor is older than the read's declared validity: nothing was called, the pass starts again. */ + | Readonly<{ kind: 'STALE' }> + | Readonly<{ kind: 'FAILED'; code: CopySourceFailure | 'COPY_SOURCE_CEILING' }> + | Readonly<{ kind: 'NO_CONNECTION' }> type OfConnection = Readonly<{ companyId: CompanyId; connectionId: ConnectionId }> @@ -39,26 +69,35 @@ function connectorOfRow(deps: Deps, input: OfConnection, personId: PersonId | nu .select({ connector: connection.connectorId }) .from(connection) .where(and(eq(connection.id, input.connectionId), eq(connection.companyId, input.companyId))) - return row === undefined ? undefined : connectorOf(row.connector) + return row === undefined ? undefined : connectorIn(deps, row.connector) }) } /** * The company's connection a copy is turned on from, read in the caller's transaction: its - * connector when it is ACTIVE; none when the company has no such connection. + * connector and its account (none on a connector whose account is `none`) when it is ACTIVE; none + * when the company has no such connection. */ export async function connectionForCopy( tx: ReadTx, input: OfConnection, -): Promise<{ kind: 'ACTIVE'; connectorId: HubConnectorId } | { kind: 'NOT_ACTIVE' } | undefined> { +): Promise< + | { kind: 'ACTIVE'; connectorId: HubConnectorId; accountRef: AccountRef | null } + | { kind: 'NOT_ACTIVE' } + | undefined +> { const [row] = await tx - .select({ state: connection.state, connectorId: connection.connectorId }) + .select({ + state: connection.state, + connectorId: connection.connectorId, + accountRef: connection.accountRef, + }) .from(connection) .where(and(eq(connection.id, input.connectionId), eq(connection.companyId, input.companyId))) if (row === undefined) return undefined - return row.state === 'ACTIVE' - ? { kind: 'ACTIVE', connectorId: HubConnectorId.parse(row.connectorId) } - : { kind: 'NOT_ACTIVE' } + if (row.state !== 'ACTIVE') return { kind: 'NOT_ACTIVE' } + const accountRef = row.accountRef === null ? null : AccountRef.parse(row.accountRef) + return { kind: 'ACTIVE', connectorId: HubConnectorId.parse(row.connectorId), accountRef } } /** @@ -99,10 +138,10 @@ export async function connectionNamesOf( ) } -/** The connection opened for one page when it is ACTIVE and its connector reads copies; none otherwise. */ +/** The connection opened for one page when it is ACTIVE; none otherwise. */ function openedForCopy(deps: Deps, input: OfConnection & { enabledBy: PersonId }) { const scope = { kind: 'COMPANY', companyId: input.companyId, personId: input.enabledBy } as const - return deps.database.read(scope, async (tx) => { + return deps.database.read(scope, async (tx): Promise => { const [row] = await tx .select({ id: connection.id }) .from(connection) @@ -115,9 +154,7 @@ function openedForCopy(deps: Deps, input: OfConnection & { enabledBy: PersonId } ) if (row === undefined) return undefined try { - const opened = await openedIn(deps, tx, input) - const read = opened.connector.copy?.read - return read === undefined ? undefined : { opened, read } + return await openedIn(deps, tx, input) } catch (error) { if (error instanceof Failure && error.code === 'CONNECTION_NOT_FOUND') return undefined throw error @@ -125,37 +162,69 @@ function openedForCopy(deps: Deps, input: OfConnection & { enabledBy: PersonId } }) } +type ReadOf = Readonly<{ + input: OfConnection & { enabledBy: PersonId; signal: AbortSignal } + opened: OpenedConnection + operation: Operation + value: unknown +}> + +/** The connector's copy lane passed its deadline before the call: nothing was asked. */ +type InLine = Readonly<{ kind: 'IN_LINE' }> + +/** + * The read every other reader takes, the connection's turn, its token, the deadline and the log + * line, speaking as a version that declares only the operation; in the connector's copy lane first + * when its provider counts the limit by the app. + */ +async function readAsCopy(deps: Deps, a: ReadOf): Promise { + const { input, opened, operation } = a + function read() { + return readThrough(deps, { + by: 'copy', + ids: { companyId: input.companyId }, + viewer: { person: { id: input.enabledBy }, companyId: input.companyId }, + speaker: { + kind: 'VERSION', + declared: [ + { connectionId: input.connectionId, operation: OperationId.parse(operation.id) }, + ], + }, + opened, + operation, + input: a.value, + deadlineMs: deps.callDeadlineMs, + signal: input.signal, + }) + } + if (opened.connector.callLimit?.countedBy !== 'APP') return read() + const turn = await deps.limits.inCopyTurn( + opened.connector.id, + deps.callDeadlineMs, + async () => ({ kind: 'READ', answer: await read() }) as const, + input.signal, + ) + return turn.kind === 'READ' ? turn.answer : { kind: 'IN_LINE' } +} + /** - * One page of a copy through the read every other reader takes, the connection's turn, its token, - * the deadline and the log line, speaking as a version that declares only the paged read. The one - * way to `readThrough` for a copy's run and for the Builder's proof of a proposal (C26). + * One page of a version 1 copy through the connector's paged read. The one way to `readThrough` + * for a version 1 copy's run and for the Builder's proof of a proposal (C26). */ export async function copyPage( deps: Deps, input: OfConnection & { enabledBy: PersonId; input: PageInput; signal: AbortSignal }, -): Promise { - const found = await openedForCopy(deps, input) - if (found === undefined) return { kind: 'NO_CONNECTION' } - const { opened, read } = found - const answer = await readThrough(deps, { - by: 'copy', - ids: { companyId: input.companyId }, - viewer: { person: { id: input.enabledBy }, companyId: input.companyId }, - speaker: { - kind: 'VERSION', - declared: [{ connectionId: input.connectionId, operation: OperationId.parse(read.id) }], - }, - opened, - operation: read, - input: input.input, - deadlineMs: deps.callDeadlineMs, - signal: input.signal, - }) +): Promise { + const opened = await openedForCopy(deps, input) + const read = opened?.connector.copy?.read + if (opened === undefined || read === undefined) return { kind: 'NO_CONNECTION' } + const answer = await readAsCopy(deps, { input, opened, operation: read, value: input.input }) + if (answer.kind === 'IN_LINE') return answer if (answer.kind === 'OK') return { kind: 'PAGE', page: CopyPageSchema.parse(answer.value) } - return { kind: 'FAILED', code: sourceFailureOf(answer), refusal: answer } + return { kind: 'REFUSED', refusal: answer } } -function sourceFailureOf(answer: CopyRefusal): CopySourceFailure { +function sourceFailureOf(answer: Exclude): CopySourceFailure { switch (answer.kind) { case 'REFUSED': case 'NOT_IN_ROLE': @@ -164,7 +233,6 @@ function sourceFailureOf(answer: CopyRefusal): CopySourceFailure { return 'COPY_SOURCE_NEEDS_REAUTH' case 'UNREACHABLE': case 'DEADLINE': - case 'BUSY': return 'COPY_SOURCE_UNREACHABLE' case 'UNREADABLE': return 'COPY_SOURCE_UNREADABLE' @@ -173,9 +241,106 @@ function sourceFailureOf(answer: CopyRefusal): CopySourceFailure { } } +/** A refusal as the run keeps it: 429 is a wait, every other its code. */ +function refusedRead(refusal: CopyRefusal): CopyRead { + return refusal.kind === 'BUSY' + ? { kind: 'WAIT', waitMs: refusal.waitMs } + : { kind: 'FAILED', code: sourceFailureOf(refusal) } +} + +export type SourcePage = OfConnection & { + enabledBy: PersonId + definition: CopyDefinition + position: CopyPosition | null + /** The watermark less a day: a day in version 1, an instant in version 2; null on a source's first pass (#940). */ + since: string | null + windowFrom: string | null + limit: number + signal: AbortSignal +} + +/** The key of the page's last row, the next page's start; the same position when the page had no row. */ +function lastKey(definition: CopyDefinitionV1, page: CopyPage, position: CopyPosition | null) { + const last = page.rows.at(-1) + if (last === undefined) return position + const { columns, key } = definition + const after = key.map((name) => Number(last[columns.findIndex((column) => column.name === name)])) + return { kind: 'KEY', after } as const +} + +async function pageV1(deps: Deps, a: SourcePage & { definition: CopyDefinitionV1 }) { + const after = a.position?.kind === 'KEY' ? a.position.after : null + const { definition, since, windowFrom, limit } = a + const read = await copyPage(deps, { + ...a, + input: { definition, after, since, windowFrom, limit }, + }) + if (read.kind === 'NO_CONNECTION') return read + if (read.kind === 'IN_LINE') return { kind: 'WAIT', waitMs: null } as const + if (read.kind === 'REFUSED') return refusedRead(read.refusal) + const next = lastKey(definition, read.page, a.position) + return { kind: 'PAGE', page: read.page, next, total: null } as const +} + +function isListRead( + operation: Operation, + id: string, +): operation is Operation { + return operation.id === id && operation.class === 'read' && operation.list !== undefined +} + +async function pageV2( + deps: Deps, + a: SourcePage & { definition: CopyDefinitionV2 }, +): Promise { + const opened = await openedForCopy(deps, a) + const { definition, position } = a + const operation = opened?.connector.operations.find((each) => + isListRead(each, definition.source.operation), + ) + const list = operation?.list + if (opened === undefined || operation === undefined || list === undefined) + return { kind: 'NO_CONNECTION' } + const at = deps.now() + if ( + position?.kind === 'CURSOR' && + list.paging.kind === 'CURSOR' && + at.getTime() - Date.parse(position.at) > list.paging.ttlMs + ) + return { kind: 'STALE' } + const value = listInputOf({ list, definition, position, since: a.since }) + const answer = await readAsCopy(deps, { input: a, opened, operation, value }) + if (answer.kind === 'IN_LINE') return { kind: 'WAIT', waitMs: null } + if (answer.kind !== 'OK') return refusedRead(answer) + const rows = Rows.safeParse(answer.value) + if (!rows.success) return { kind: 'FAILED', code: 'COPY_SOURCE_UNREADABLE' } + const page = listPageOf({ list, definition, position, rows: rows.data, at }) + switch (page.kind) { + case 'PAGE': { + const { rows: cells, more, next, total } = page + return { kind: 'PAGE', page: { rows: cells, more }, next, total } + } + case 'MISFIT': + case 'UNREADABLE': + return { kind: 'FAILED', code: 'COPY_SOURCE_UNREADABLE' } + case 'CEILING': + return { kind: 'FAILED', code: 'COPY_SOURCE_CEILING' } + default: + return page satisfies never + } +} + +/** One page of a copy's source: version 1 through the connector's paged read, version 2 through the list read it names. */ +function readPage(deps: Deps, a: SourcePage): Promise { + const { definition } = a + return definition.version === 1 + ? pageV1(deps, { ...a, definition }) + : pageV2(deps, { ...a, definition }) +} + /** * The copies' door to the outside (decision 0056): the templates of each connector, and one page - * of a copy (`copyPage`). + * of a copy's source (`readPage`). */ export function createCopyPort(deps: Deps) { return Object.freeze({ @@ -183,12 +348,14 @@ export function createCopyPort(deps: Deps) { return (await connectorOfRow(deps, input, null))?.copy?.models }, catalog(): readonly { connector: ConnectorId; model: CopyModel }[] { - return CONNECTORS.flatMap((connector) => + return deps.catalog.flatMap((connector) => (connector.copy?.models ?? []).map((model) => ({ connector: connector.id, model })), ) }, - readPage: ( - input: OfConnection & { enabledBy: PersonId; input: PageInput; signal: AbortSignal }, - ): Promise => copyPage(deps, input), + readPage: (input: SourcePage): Promise => readPage(deps, input), + /** Where the connector's provider counts its call limit; none when it declares no list read. */ + countedBy(connector: string): 'APP' | 'ACCOUNT' | null { + return connectorIn(deps, connector)?.callLimit?.countedBy ?? null + }, }) } diff --git a/apps/server/src/hub/connectors/copy-proof.ts b/apps/server/src/hub/connectors/copy-proof.ts index c0e3b6ab9..3fef7cc18 100644 --- a/apps/server/src/hub/connectors/copy-proof.ts +++ b/apps/server/src/hub/connectors/copy-proof.ts @@ -1,4 +1,4 @@ -import type { CopyDefinition } from '@conexus/connectors' +import type { CopyDefinitionV1 } from '@conexus/connectors' import type { ConnectionId, OfProject, PersonId } from '@conexus/hub-api' import { PROOF_PAGE_ROWS } from '@conexus/hub-api' import { roleOfSpeaker } from '../identity/public.ts' @@ -20,7 +20,7 @@ type ProofAnswer = | Readonly<{ kind: 'CALLS_SPENT' }> /** The day a proof's window starts: today less the window's days, as a run's pass would read it. */ -function windowFromOf(definition: CopyDefinition): string | null { +function windowFromOf(definition: CopyDefinitionV1): string | null { if (definition.window === undefined) return null const from = new Date(Date.now() - definition.window.days * 86_400_000) return from.toISOString().slice(0, 10) @@ -39,7 +39,7 @@ export async function provedPage( opened: OpenedConnection | undefined connectionId: ConnectionId personId: PersonId - definition: CopyDefinition + definition: CopyDefinitionV1 signal: AbortSignal spend: () => boolean }>, @@ -74,6 +74,7 @@ export async function provedPage( }, }) if (page.kind === 'NO_CONNECTION') return { kind: 'NOT_GRANTED' } - if (page.kind === 'FAILED') return page.refusal + if (page.kind === 'REFUSED') return page.refusal + if (page.kind === 'IN_LINE') return { kind: 'DEADLINE' } return { kind: 'PROVED', rows: page.page.rows.length, more: page.page.more } } diff --git a/apps/server/src/hub/connectors/limits.ts b/apps/server/src/hub/connectors/limits.ts index d373de275..0901277d2 100644 --- a/apps/server/src/hub/connectors/limits.ts +++ b/apps/server/src/hub/connectors/limits.ts @@ -1,3 +1,4 @@ +import type { ConnectorId } from '@conexus/connectors' import type { ConnectionId } from '@conexus/hub-api' import type { FailureLog } from '../../platform/failure.ts' import { Failure, logFailure } from '../../platform/failure.ts' @@ -25,6 +26,16 @@ export type Limits = { work: (waitedMs: number) => Promise, signal?: AbortSignal, ): Promise + /** + * The same line for the copies' calls of one connector whose provider counts its limit by the app + * (O Sync de qualquer conector): one copy call at a time across every company, while one process runs (0058). + */ + inCopyTurn( + connector: ConnectorId, + deadlineMs: number, + work: (waitedMs: number) => Promise, + signal?: AbortSignal, + ): Promise /** The live token, or one login (the old token logged out first, while it lives). */ token(opened: OpenedConnection, deadlineMs: number): Promise /** A 401 on `token`: dropped when it is still the current one. */ @@ -47,9 +58,16 @@ type Held = { seat: Seat; state: TokenState } const NONE = { kind: 'NONE' } as const -/** Each connection's calls one at a time, the rest waiting in arrival order until their own deadline. */ -function createLanes(): Pick { - const lanes = new Map() +type InLane = ( + key: K, + deadlineMs: number, + work: (waitedMs: number) => Promise, + signal?: AbortSignal, +) => Promise + +/** Each key's calls one at a time, the rest waiting in arrival order until their own deadline. */ +function createLanes(): InLane { + const lanes = new Map() /** True when the turn is this caller's; false when its deadline passed first, and it left the line. */ function turnOf(lane: Lane, deadlineMs: number, stopped?: AbortSignal): Promise { @@ -74,7 +92,7 @@ function createLanes(): Pick { }) } - function passTurn(id: ConnectionId, lane: Lane): void { + function passTurn(id: K, lane: Lane): void { const [next] = lane.waiting if (next === undefined) { lane.busy = false @@ -85,25 +103,23 @@ function createLanes(): Pick { next() } - return { - async inTurn(connection, deadlineMs, work, signal) { - let lane = lanes.get(connection) - if (lane === undefined) { - lane = { busy: false, waiting: new Set() } - lanes.set(connection, lane) - } - const started = performance.now() - if (!(await turnOf(lane, deadlineMs, signal))) { - // A caller that left an idle lane's entry behind would keep the map's key alive. - if (!lane.busy) lanes.delete(connection) - return { kind: 'DEADLINE' } - } - try { - return await work(performance.now() - started) - } finally { - passTurn(connection, lane) - } - }, + return async (key, deadlineMs, work, signal) => { + let lane = lanes.get(key) + if (lane === undefined) { + lane = { busy: false, waiting: new Set() } + lanes.set(key, lane) + } + const started = performance.now() + if (!(await turnOf(lane, deadlineMs, signal))) { + // A caller that left an idle lane's entry behind would keep the map's key alive. + if (!lane.busy) lanes.delete(key) + return { kind: 'DEADLINE' } + } + try { + return await work(performance.now() - started) + } finally { + passTurn(key, lane) + } } } @@ -158,7 +174,7 @@ function createTokens( executor: Executor, inTurn: Limits['inTurn'], log: FailureLog, -): Omit { +): Omit { const held = new Map() const endings = createEndings(held, executor, inTurn, log) @@ -228,6 +244,10 @@ function createTokens( } export function createLimits(deps: { executor: Executor; log: FailureLog }): Limits { - const lanes = createLanes() - return { ...lanes, ...createTokens(deps.executor, lanes.inTurn, deps.log) } + const inTurn = createLanes() + return { + inTurn, + inCopyTurn: createLanes(), + ...createTokens(deps.executor, inTurn, deps.log), + } } diff --git a/apps/server/src/hub/connectors/public.ts b/apps/server/src/hub/connectors/public.ts index 36c6a8834..6de48ce12 100644 --- a/apps/server/src/hub/connectors/public.ts +++ b/apps/server/src/hub/connectors/public.ts @@ -1,5 +1,5 @@ -import type { ConnectorId } from '@conexus/connectors' -import { connectorOf } from '@conexus/connectors' +import type { Connector, ConnectorId } from '@conexus/connectors' +import { CONNECTORS } from '@conexus/connectors' import { CALL_DEADLINE_MS } from '@conexus/hub-api' import type { FastifyInstance } from 'fastify' import type { Config } from '../../platform/config.ts' @@ -27,10 +27,19 @@ import { registerConnectionRoutes, registerToolCallRoutes, } from './routes.ts' +import type { ConnectionCopies } from './rows.ts' +import { connectorIn } from './rows.ts' import { createToolDoor } from './tool-door.ts' /** The kit's copy contracts, which `hub/copies` parses with and never imports from the kit (decision 0056). */ -export { CopyDefinition, CopyName } from '@conexus/connectors' +export { + AccountRef, + CopyDefinition, + CopyDefinitionV1, + CopyDefinitionV2, + CopyName, + CopyPosition, +} from '@conexus/connectors' export type { ApprovalDeps, WriteAuthorOf, WriteTarget } from './approval.ts' export { mayApprove, mayLeaveAlone, mustAsk } from './approval.ts' export type { KitRead, WriteShape } from './assistant-port.ts' @@ -48,8 +57,15 @@ export type { WriteRequest, Written } from './write-record.ts' export { markOf } from './write-record.ts' export type { Returned } from './wrote.ts' -/** What tests change: a shorter deadline, and the clock a write is dated by (production's is the machine's). */ -export type ConnectorTuning = { callDeadlineMs?: number; now?: (() => Date) | undefined } +/** + * What tests change: a shorter deadline, the clock a write and a cursor are dated by (production's + * is the machine's), and the catalog, with a connector of the test's own beside the kit's. + */ +export type ConnectorTuning = { + callDeadlineMs?: number + now?: (() => Date) | undefined + catalog?: readonly Connector[] | undefined +} export type Connectors = ReturnType @@ -68,11 +84,15 @@ export function createConnectors(deps: { door: AppDoorPort /** `hub/copies`' proposing, handed on to the Builder (Capacidade 3): it is made after the connectors. */ propose: () => Propose + /** What Desligar asks of `hub/copies` (O Sync de qualquer conector), made after the connectors too. */ + copies: () => ConnectionCopies tuning?: ConnectorTuning }) { + const catalog = deps.tuning?.catalog ?? CONNECTORS + const now = deps.tuning?.now ?? (() => new Date()) const origins = new Map() for (const [id, origin] of deps.config.connectorOrigins ?? []) { - const connector = connectorOf(id) + const connector = connectorIn({ catalog }, id) if (connector === undefined) throw new Failure('CONFIG_INVALID', { details: { variables: 'HUB_CONNECTOR_ORIGINS', reason: 'unknown connector' }, @@ -81,7 +101,7 @@ export function createConnectors(deps: { } const apps = new Map() for (const [id, app] of deps.config.connectorApps ?? []) { - const connector = connectorOf(id) + const connector = connectorIn({ catalog }, id) if (connector === undefined || !isOAuth(connector)) throw new Failure('CONFIG_INVALID', { details: { variables: 'HUB_CONNECTOR_APPS_FILE', reason: 'not an OAuth connector' }, @@ -92,7 +112,7 @@ export function createConnectors(deps: { fetch, origins, log: deps.log, - now: deps.tuning?.now ?? (() => new Date()), + now, }) const limits = createLimits({ executor, log: deps.log }) const routeDeps = { @@ -105,6 +125,8 @@ export function createConnectors(deps: { limits, log: deps.log, callDeadlineMs: deps.tuning?.callDeadlineMs ?? CALL_DEADLINE_MS, + catalog, + now, } const appCalls = createAppCalls(routeDeps, deps.door) // One door for the agents and the saved tools they call, so a turn's 50 reads count both. diff --git a/apps/server/src/hub/connectors/routes.ts b/apps/server/src/hub/connectors/routes.ts index a1df0259a..cb57ec96e 100644 --- a/apps/server/src/hub/connectors/routes.ts +++ b/apps/server/src/hub/connectors/routes.ts @@ -7,6 +7,7 @@ import { grantProjectConnection, linkConnection, listAppConnections, + listConnectionCopies, listConnections, listProjectConnections, reconnectConnection, @@ -18,6 +19,7 @@ import { import type { FastifyInstance } from 'fastify' import { answer, serve } from '../../platform/http.ts' import type { AppCalls } from './app-calls.ts' +import { copiesOfConnection } from './connection-copies.ts' import { detail, link, list, reconnect, unlink } from './connections.ts' import { grant, list as listGrants, revoke } from './grants.ts' import { erase } from './notes.ts' @@ -92,11 +94,16 @@ export function registerConnectionRoutes(app: FastifyInstance, deps: Deps): void registerGrantRoutes(app, deps) + serve(app, listConnectionCopies, async ({ person, params }) => + answer({ copies: await copiesOfConnection(deps, { person, ...params }) }), + ) + serve(app, unlinkConnection, async ({ person, params, log }) => { - const { removed, revocation } = await unlink(deps, { person, ...params }) - if (removed) - log.info({ by: person.id, connectionId: params.connectionId }, 'CONNECTION_REMOVED') - return answer({ revocation }) + const { removed, revocation, copies, tablesLeft } = await unlink(deps, { person, ...params }) + const { connectionId } = params + if (removed) log.info({ by: person.id, connectionId }, 'CONNECTION_REMOVED') + for (const { copyId, ended } of copies) log.info({ connectionId, copyId, ended }, 'COPIES_LEFT') + return answer({ revocation, tablesLeft }) }) } diff --git a/apps/server/src/hub/connectors/rows.ts b/apps/server/src/hub/connectors/rows.ts index eae1979b7..20ee1c387 100644 --- a/apps/server/src/hub/connectors/rows.ts +++ b/apps/server/src/hub/connectors/rows.ts @@ -1,9 +1,9 @@ import type { Connector, ConnectorId } from '@conexus/connectors' -import type { CompanyId, Connection, Person } from '@conexus/hub-api' +import type { CompanyId, Connection, CopyId, CopyOfConnection, Person } from '@conexus/hub-api' import { CONNECTION_STATES, ConnectionId, PersonId } from '@conexus/hub-api' import { and, eq, sql } from 'drizzle-orm' import { z } from 'zod' -import type { Database, ReadTx, Scope } from '../../platform/database.ts' +import type { CommandTx, Database, ReadTx, Scope } from '../../platform/database.ts' import type { FailureLog } from '../../platform/failure.ts' import { Failure } from '../../platform/failure.ts' import type { SealKeys } from '../../platform/seal.ts' @@ -14,6 +14,28 @@ import type { Executor } from './executor.ts' import type { Limits } from './limits.ts' import { connection } from './tables.ts' +type CopiesOf = Readonly<{ companyId: CompanyId; personId: PersonId; connectionId: ConnectionId }> + +/** + * What Desligar asks of the company's copies (O Sync de qualquer conector): `hub/copies` answers it, + * made after the connectors, so the port is lazy and this subject never imports that one. + */ +export type ConnectionCopies = Readonly<{ + /** Each copy that reads the connection, as Desligar shows it before the click. */ + copiesOf(input: CopiesOf): Promise + /** Before anything leaves the Hub: the account's rows out of the copies that go on; COPY_DATABASE_UNAVAILABLE when the company's database is silent. */ + leave(input: CopiesOf): Promise + /** In Desligar's transaction, before its DELETE: the copies the connection is a source of, locked. */ + locked(tx: CommandTx, input: Omit): Promise + /** In the same transaction, after it: the locked copies left with no source end, their tables noted. */ + ended( + tx: CommandTx, + input: { companyId: CompanyId; copyIds: readonly CopyId[] }, + ): Promise + /** After the DELETE: the ended copies' tables dropped; those the company's database left, by name. */ + dropEnded(input: Omit): Promise<{ tablesLeft: string[] }> +}> + export type Deps = { database: Database identity: IdentityPorts @@ -27,6 +49,16 @@ export type Deps = { origins: ReadonlyMap /** The Hub's address as the browser sees it: the provider sends the person back to it. */ publicOrigin: string + /** The connectors the Hub knows: the kit's `CONNECTORS`; a test adds its own. */ + catalog: readonly Connector[] + /** The machine's clock; a test injects its own (T20). */ + now: () => Date + copies: () => ConnectionCopies +} + +/** The catalog's connector of an id; none when the catalog has no such connector. */ +export function connectorIn(deps: Pick, id: string): Connector | undefined { + return deps.catalog.find((connector) => connector.id === id) } /** Who asks: for a call the app, the Builder or the Assistant makes, the person is only the scope (the id). */ export type Viewer = { person: Pick; companyId: CompanyId } diff --git a/apps/server/src/hub/connectors/test-app-calls.ts b/apps/server/src/hub/connectors/test-app-calls.ts index 62775d195..746f628db 100644 --- a/apps/server/src/hub/connectors/test-app-calls.ts +++ b/apps/server/src/hub/connectors/test-app-calls.ts @@ -104,7 +104,7 @@ export async function appCalls( const hub = await hubWithLog( t, (config) => ({ ...config, connectorOrigins: new Map([['sankhya', origin]]) }), - a.clock, + { now: a.clock }, ) const ana = await personOn(hub, ANA_OWNER) const companyId = await plantCompany(hub.test, ana.id, 'Empresa A', 'empresa-a') diff --git a/apps/server/src/hub/connectors/test-catalog.ts b/apps/server/src/hub/connectors/test-catalog.ts new file mode 100644 index 000000000..eecdf92db --- /dev/null +++ b/apps/server/src/hub/connectors/test-catalog.ts @@ -0,0 +1,28 @@ +import type { Connector } from '@conexus/connectors' +import { CONNECTORS } from '@conexus/connectors' + +/** The kit's catalog with Mercado Livre's list reads changed as `change` says: the test's own connector. */ +export function catalogWith(change: (connector: Connector) => Connector): readonly Connector[] { + return CONNECTORS.map((connector) => + connector.id === 'mercado-livre' ? change(connector) : connector, + ) +} + +/** Mercado Livre as a provider that counts its call limit by each account. */ +export const BY_ACCOUNT = catalogWith((connector) => ({ + ...connector, + callLimit: { countedBy: 'ACCOUNT' }, +})) + +/** Mercado Livre with `pedidos` read up to 100 rows by its offset. */ +export const CEILED = catalogWith((connector) => ({ + ...connector, + operations: connector.operations.map((operation) => + operation.id === 'pedidos' && operation.list?.paging.kind === 'OFFSET' + ? { + ...operation, + list: { ...operation.list, paging: { ...operation.list.paging, ceiling: 100 } }, + } + : operation, + ), +})) diff --git a/apps/server/src/hub/connectors/test-connections.ts b/apps/server/src/hub/connectors/test-connections.ts index 4cecf89c4..7f52eec69 100644 --- a/apps/server/src/hub/connectors/test-connections.ts +++ b/apps/server/src/hub/connectors/test-connections.ts @@ -66,6 +66,8 @@ export async function mercadoLivreOn( change?: (config: Config) => Config /** The origin the Hub is given for the double, when something stands in front of it. */ through?: (origin: string) => Promise + /** The Hub's clock and catalog, when the test sets its own. */ + tuning?: Parameters[2] } = {}, ) { const fake = mercadoLivreFake({ redirectUri: `${TEST_ORIGIN}${REDIRECT_PATH}` }) @@ -73,12 +75,15 @@ export async function mercadoLivreOn( t.after(() => served.close()) const origin = a.through === undefined ? served.origin : await a.through(served.origin) const change = a.change ?? ((config: Config) => config) - const hub = await hubWithLog(t, (config) => - change({ - ...config, - connectorOrigins: new Map([['mercado-livre', origin]]), - connectorApps: appsOf(fake.app), - }), + const hub = await hubWithLog( + t, + (config) => + change({ + ...config, + connectorOrigins: new Map([['mercado-livre', origin]]), + connectorApps: appsOf(fake.app), + }), + a.tuning, ) const ana = await personOn(hub, ANA_OWNER) const companyId = await plantCompany(hub.test, ana.id) diff --git a/apps/server/src/hub/connectors/testing.ts b/apps/server/src/hub/connectors/testing.ts index 4d4181f11..f2c9f3473 100644 --- a/apps/server/src/hub/connectors/testing.ts +++ b/apps/server/src/hub/connectors/testing.ts @@ -1,4 +1,4 @@ -import { AccountRef, connectorOf } from '@conexus/connectors' +import { AccountRef } from '@conexus/connectors' import type { CompanyId, ConnectionId, Person, TestResult } from '@conexus/hub-api' import { and, eq, sql } from 'drizzle-orm' import type { ReadTx } from '../../platform/database.ts' @@ -8,7 +8,16 @@ import type { Checked } from './check.ts' import { checked } from './check.ts' import type { OpenedConnection } from './executor.ts' import type { Deps, OfConnection } from './rows.ts' -import { admitted, columns, ofCompany, scopeOf, sealContext, sealedOf, viewOf } from './rows.ts' +import { + admitted, + columns, + connectorIn, + ofCompany, + scopeOf, + sealContext, + sealedOf, + viewOf, +} from './rows.ts' import { connection } from './tables.ts' import { accessFor } from './tokens.ts' @@ -40,7 +49,7 @@ export async function openedIn( .from(connection) .where(and(eq(connection.id, input.connectionId), eq(connection.companyId, input.companyId))) if (row === undefined) throw new Failure('CONNECTION_NOT_FOUND') - const connector = connectorOf(row.connector) + const connector = connectorIn(deps, row.connector) // A connector the kit no longer has cannot rebuild its seal's context: the seal does not open. if (connector === undefined) throw new Failure('SEAL_REFUSED', { details: { reason: 'connector gone' } }) diff --git a/apps/server/src/hub/connectors/unlink-copies.test.ts b/apps/server/src/hub/connectors/unlink-copies.test.ts new file mode 100644 index 000000000..99491b6c3 --- /dev/null +++ b/apps/server/src/hub/connectors/unlink-copies.test.ts @@ -0,0 +1,186 @@ +import assert from 'node:assert/strict' +import { randomUUID } from 'node:crypto' +import { test } from 'node:test' +import type { CopyId, ProjectId } from '@conexus/hub-api' +import { appLogin, appLoginPassword, ConnectionId } from '@conexus/hub-api' +import pg from 'pg' +import type { CopiesOn } from '../copies/test-copies.ts' +import { copiesOn, TEST_APP_LOGIN_KEY } from '../copies/test-copies.ts' +import type { StoresOn } from '../copies/test-sources.ts' +import { ordersCopy, STORE_A, STORE_B, storesOn } from '../copies/test-sources.ts' +import { call, codeOf } from '../identity/test-hub.ts' +import { CopyName } from './public.ts' +import { linkSankhya, testOf } from './test-connections.ts' + +type Owner = CopiesOn['owner'] + +/** One read as the Projeto's read login, as the edge reaches it: the rows' count, or the SQLSTATE that refused it. */ +async function asApp(owner: Owner, projectId: ProjectId, text: string) { + const { readLogin } = appLogin(projectId) + const url = new URL(owner.reveal()) + url.username = readLogin + url.password = await appLoginPassword({ key: TEST_APP_LOGIN_KEY, login: readLogin }) + const client = new pg.Client({ connectionString: url.toString() }) + await client.connect() + try { + return { rows: (await client.query(text)).rowCount } + } catch (error) { + if (error instanceof pg.DatabaseError) return { refused: error.code } + throw error + } finally { + await client.end() + } +} + +function unlink(on: Pick, base: string, connectionId: string) { + return call(on.hub, on.ana, 'DELETE', `${base}/${connectionId}`) +} + +async function copiesLeft(on: Pick) { + return (await on.hub.test.asOwner('select name from copies.copy order by name')).map( + (row) => row.name, + ) +} + +/** Turned on, run once and released to the Projeto Pedidos. */ +async function released(on: CopiesOn, model: string) { + const copyId = await on.enable(model) + assert.equal((await on.run(copyId)).kind, 'ENDED') + await on.copies.grant({ + ...{ companyId: on.companyId, personId: on.anaId }, + ...{ projectId: on.projectId, copyId }, + }) + return copyId +} + +/** Another Sankhya connection of the company, linked and tested. */ +async function filial(on: CopiesOn) { + const linked = await linkSankhya( + on.hub, + on.ana, + on.connections, + on.fake.credential, + undefined, + 'Filial', + ) + assert.equal(linked.statusCode, 201, linked.body) + const connectionId = ConnectionId.parse(Object(linked.json()).connection.id) + await testOf(on.hub, on.ana, on.connections, connectionId) + return connectionId +} + +test('desligar tira as linhas e a leitura do app', async (t) => { + const on = await copiesOn(t) + const vendas = await released(on, 'vendas') + const clientes = await released(on, 'clientes') + await on.copies.pause({ companyId: on.companyId, personId: on.anaId, copyId: clientes }) + assert.deepEqual(await asApp(on.owner, on.projectId, 'select * from sync.vendas'), { rows: 183 }) + + const gone = await unlink(on, on.connections, on.connectionId) + assert.deepEqual([gone.statusCode, Object(gone.json()).tablesLeft], [200, []]) + assert.deepEqual(await copiesLeft(on), [], 'a cópia de uma conexão acaba com ela, pausada também') + for (const name of ['vendas', 'clientes']) + assert.deepEqual(await asApp(on.owner, on.projectId, `select * from sync.${name}`), { + refused: '42P01', + }) + + const other = await filial(on) + const again = await on.copies.enable({ + ...{ companyId: on.companyId, personId: on.anaId }, + source: { kind: 'MODEL', connectionId: other, model: CopyName.parse('vendas') }, + interval: '30m', + attemptKey: randomUUID(), + }) + assert.notEqual(again, vendas) + assert.deepEqual( + await asApp(on.owner, on.projectId, 'select * from sync.vendas'), + { refused: '42501' }, + 'a cópia nova com o mesmo nome começa sem a leitura do app', + ) + const [empty] = await on.hub.test.asOwner('select rows from copies.copy where id = $1', [again]) + assert.equal(empty?.rows, null, 'e vazia') +}) + +/** Two stores' copy of orders, run once and released to the Projeto Pedidos. */ +async function storesCopy(on: StoresOn): Promise { + const copyId = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref, STORE_B.ref]) + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + await on.copies.grant({ + ...{ companyId: on.companyId, personId: on.anaId }, + ...{ projectId: on.projectId, copyId }, + }) + return copyId +} + +test('desligar tira as linhas e a leitura do app: duas contas, uma depois da outra', async (t) => { + const on = await storesOn(t) + await storesCopy(on) + const read = 'select * from sync.pedidos' + assert.deepEqual(await asApp(on.owner, on.projectId, read), { rows: 240 }) + const a = await unlink(on, on.base, String(on.stores.get(STORE_A.ref))) + assert.equal(a.statusCode, 200) + assert.deepEqual(await asApp(on.owner, on.projectId, read), { rows: 120 }, 'sem esperar rodada') + assert.deepEqual( + await asApp( + on.owner, + on.projectId, + `select * from sync.pedidos where _account = '${STORE_A.ref}'`, + ), + { rows: 0 }, + ) + assert.deepEqual(await copiesLeft(on), ['pedidos']) + const b = await unlink(on, on.base, String(on.stores.get(STORE_B.ref))) + assert.deepEqual([b.statusCode, Object(b.json()).tablesLeft], [200, []]) + assert.deepEqual(await copiesLeft(on), []) + assert.deepEqual(await asApp(on.owner, on.projectId, read), { refused: '42P01' }) +}) + +test('desligar tira as linhas e a leitura do app: o banco da empresa calado antes do Hub', async (t) => { + const on = await storesOn(t) + await storesCopy(on) + const connectionId = String(on.stores.get(STORE_A.ref)) + const silent = t.mock.method(on.hub.postgres.companyPostgres, 'dropAccountRows', async () => ({ + kind: 'NO_ANSWER' as const, + })) + const refused = await unlink(on, on.base, connectionId) + assert.deepEqual([refused.statusCode, codeOf(refused)], [503, 'COPY_DATABASE_UNAVAILABLE']) + const still = await call(on.hub, on.ana, 'GET', `${on.base}/${connectionId}`) + assert.equal(still.statusCode, 200, 'a conexão continua') + assert.equal(on.fake.revocations().length, 0, 'nada sai do fornecedor') + assert.deepEqual(await copiesLeft(on), ['pedidos']) + silent.mock.restore() + const again = await unlink(on, on.base, connectionId) + assert.deepEqual([again.statusCode, Object(again.json()).tablesLeft], [200, []]) + assert.deepEqual(await asApp(on.owner, on.projectId, 'select * from sync.pedidos'), { + rows: 120, + }) +}) + +test('desligar tira as linhas e a leitura do app: o banco da empresa calado depois do Hub', async (t) => { + const on = await copiesOn(t) + await released(on, 'vendas') + const silent = t.mock.method(on.hub.postgres.companyPostgres, 'dropCopyTable', async () => ({ + kind: 'NO_ANSWER' as const, + })) + const gone = await unlink(on, on.connections, on.connectionId) + assert.deepEqual([gone.statusCode, Object(gone.json()).tablesLeft], [200, ['vendas']]) + const missing = await call(on.hub, on.ana, 'GET', `${on.connections}/${on.connectionId}`) + assert.equal(missing.statusCode, 404, 'a conexão já não existe') + assert.deepEqual(await copiesLeft(on), []) + const left = await on.hub.test.asOwner('select name from copies.copy_table_left') + assert.deepEqual(left, [{ name: 'vendas' }]) + assert.ok(on.hub.lines.some((line) => line.message === 'COPY_TABLES_LEFT')) + silent.mock.restore() + + const other = await filial(on) + await on.copies.enable({ + ...{ companyId: on.companyId, personId: on.anaId }, + source: { kind: 'MODEL', connectionId: other, model: CopyName.parse('clientes') }, + interval: '30m', + attemptKey: randomUUID(), + }) + assert.deepEqual(await on.hub.test.asOwner('select name from copies.copy_table_left'), []) + assert.deepEqual(await asApp(on.owner, on.projectId, 'select * from sync.vendas'), { + refused: '42P01', + }) +}) diff --git a/apps/server/src/hub/copies/accept.ts b/apps/server/src/hub/copies/accept.ts index 1bf10aa53..42b9dee9c 100644 --- a/apps/server/src/hub/copies/accept.ts +++ b/apps/server/src/hub/copies/accept.ts @@ -5,10 +5,11 @@ import { and, eq, sql } from 'drizzle-orm' import type { CommandTx } from '../../platform/database.ts' import { violatedConstraint } from '../../platform/database.ts' import { Failure } from '../../platform/failure.ts' +import type { AccountRef } from '../connectors/public.ts' import { CopyDefinition, connectionForCopy, lackedReadOf } from '../connectors/public.ts' import type { CopiesDeps, InCompany } from './store.ts' import { parsedCopy } from './store.ts' -import { copy, copyProposal, projectCopy } from './tables.ts' +import { copy, copyProposal, copySource, projectCopy } from './tables.ts' export type Accepting = InCompany & { proposalId: CopyProposalId; attemptKey: string } @@ -37,10 +38,11 @@ export async function earlierAttempt( return again } -/** What a new copy row is made of, from a template or a proposal. */ +/** What a new copy row is made of, from a template or a proposal, with its one source's account. */ export type NewCopy = Readonly<{ connectorId: ConnectorId connectionId: ConnectionId + accountRef: AccountRef | null name: string label: string model: string | null @@ -48,7 +50,7 @@ export type NewCopy = Readonly<{ bodyHash: string }> -/** The new copy row under the attempt's key; a name a copy took is `COPY_EXISTS`. */ +/** The new copy row under the attempt's key, and its source; a name a copy took is `COPY_EXISTS`. */ export async function insertedCopy( tx: CommandTx, input: InCompany & { attemptKey: string }, @@ -67,6 +69,11 @@ export async function insertedCopy( }) .returning() if (row === undefined) throw new Failure('COPY_EXISTS') + await tx.insert(copySource).values({ + ...{ companyId: input.companyId, copyId: row.id, connectorId: made.connectorId }, + ...{ connectionId: made.connectionId, accountRef: made.accountRef }, + createdAt: sql`statement_timestamp()`, + }) return row } catch (error) { if (violatedConstraint(error) === 'copy_company_id_name_key') throw new Failure('COPY_EXISTS') @@ -108,7 +115,7 @@ export async function acceptedIn( if (linked.kind !== 'ACTIVE') throw new Failure('COPY_CONNECTION_NOT_READY') const row = await insertedCopy(tx, input, { ...{ connectorId, connectionId, name: proposal.name, label: proposal.label }, - ...{ model: null, bodyHash }, + ...{ accountRef: linked.accountRef, model: null, bodyHash }, definition: CopyDefinition.parse(proposal.definition), }) await tx diff --git a/apps/server/src/hub/copies/enable.ts b/apps/server/src/hub/copies/enable.ts index de37063c3..faf1f414c 100644 --- a/apps/server/src/hub/copies/enable.ts +++ b/apps/server/src/hub/copies/enable.ts @@ -11,9 +11,11 @@ import { Failure } from '../../platform/failure.ts' import { connectionForCopy } from '../connectors/public.ts' import { acceptedIn, acceptedProjectOf, earlierAttempt, insertedCopy } from './accept.ts' import { grantedInDatabase } from './grants.ts' +import { dropEndedCopies } from './leave.ts' import { mayCreate } from './schedule.ts' +import { sourcesOf } from './sources.ts' import type { CopiesDeps, InCompany, StoredCopy } from './store.ts' -import { companyOwner, parsedCopy, personScope, syncOf } from './store.ts' +import { companyOwner, copyTableOf, parsedCopy, personScope, syncOf } from './store.ts' /** What a copy's definition comes from: a template of the connection's connector, or the Builder's proposal (Capacidade 3). */ type CopySource = @@ -66,7 +68,8 @@ async function fromModel( if (made.model === undefined) throw new Failure('COPY_MODEL_UNKNOWN') const { model } = made const row = await insertedCopy(tx, input, { - ...{ connectorId: linked.connectorId, connectionId, name: model.id, label: model.label }, + ...{ connectorId: linked.connectorId, connectionId, accountRef: linked.accountRef }, + ...{ name: model.id, label: model.label }, ...{ model: model.id, definition: model.definition, bodyHash: made.bodyHash }, }) return parsedCopy(row) @@ -136,13 +139,18 @@ export async function enableCopy(deps: CopiesDeps, input: Enabling): Promise + sourcesOf(tx, { companyId, copyId }), + ) const table = answered.kind === 'DONE' - ? await deps.companyPostgres.ensureCopyTable(await deps.companyPostgres.syncAddress(host), { - name: stored.name, - definition: stored.definition, - }) + ? await deps.companyPostgres.ensureCopyTable( + await deps.companyPostgres.syncAddress(host), + copyTableOf(stored, sources), + ) : answered if (table.kind !== 'DONE') { const code = table.kind === 'REFUSED' ? table.code : table.kind diff --git a/apps/server/src/hub/copies/leave.ts b/apps/server/src/hub/copies/leave.ts new file mode 100644 index 000000000..1f5d1969e --- /dev/null +++ b/apps/server/src/hub/copies/leave.ts @@ -0,0 +1,213 @@ +import type { CompanyId, ConnectionId, CopyId, CopyOfConnection } from '@conexus/hub-api' +import { CopyId as CopyIdSchema } from '@conexus/hub-api' +import { and, asc, eq, inArray, notExists, sql } from 'drizzle-orm' +import type { CommandTx, DatabaseUrl, ReadTx } from '../../platform/database.ts' +import { Failure } from '../../platform/failure.ts' +import { AccountRef, CopyName } from '../connectors/public.ts' +import { copyModeOf } from './mode.ts' +import type { CopiesDeps, InCompany } from './store.ts' +import { parsedCopy, personScope, syncOf } from './store.ts' +import { copy, copySource, copyTableLeft } from './tables.ts' + +type OfConnection = InCompany & { connectionId: ConnectionId } + +/** The ids of the copies the connection is a source of. */ +function readingFrom(tx: ReadTx, input: Omit) { + return tx + .select({ copyId: copySource.copyId }) + .from(copySource) + .where( + and( + eq(copySource.companyId, input.companyId), + eq(copySource.connectionId, input.connectionId), + ), + ) +} + +/** The copies that read the connection, each with its sources' accounts, as Desligar weighs them. */ +async function readersOf(tx: ReadTx, input: Omit) { + const ids = (await readingFrom(tx, input)).map((row) => row.copyId) + if (ids.length === 0) return [] + const copies = await tx + .select() + .from(copy) + .where(and(eq(copy.companyId, input.companyId), inArray(copy.id, ids))) + .orderBy(asc(copy.name)) + const sources = await tx + .select({ + copyId: copySource.copyId, + connectionId: copySource.connectionId, + accountRef: copySource.accountRef, + }) + .from(copySource) + .where(and(eq(copySource.companyId, input.companyId), inArray(copySource.copyId, ids))) + return copies.map((row) => { + const stored = parsedCopy(row) + const own = sources.filter((each) => each.copyId === row.id) + const mine = own.find((each) => each.connectionId === input.connectionId) + const ref = mine?.accountRef ?? null + return { stored, ref: ref === null ? null : AccountRef.parse(ref), last: own.length === 1 } + }) +} + +/** Each copy that has the connection as a source, as Desligar shows it before the click. */ +export async function copiesOfConnection( + deps: CopiesDeps, + input: OfConnection, +): Promise { + const readers = await deps.database.read(personScope(input), (tx) => readersOf(tx, input)) + const counts = readers.some(({ last, ref }) => !last && ref !== null) + const sync = counts ? await syncIn(deps, input) : undefined + return Promise.all( + readers.map(async ({ stored, ref, last }) => { + const of = { copyId: stored.id, name: stored.name, mode: copyModeOf(stored.definition), last } + if (last) return { ...of, rows: stored.rows } + const counted = + sync === undefined || ref === null + ? undefined + : await deps.companyPostgres.accountRows(sync, { copy: stored.name, ref }) + return { ...of, rows: counted?.kind === 'DONE' ? counted.rows : null } + }), + ) +} + +/** Where the Hub reaches the sync login, asked only when there is work in the company's database. */ +function syncIn(deps: CopiesDeps, input: InCompany) { + return deps.database.read(personScope(input), (tx) => syncOf(deps, tx, input.companyId)) +} + +/** + * Before the provider's revoke and the Hub's DELETE in `unlink`, outside any transaction (data D22): + * the account's rows out of every copy of the connection that has `_account`. It drops no table: + * which copies end is known only in the DELETE. A silent company database refuses Desligar with + * nothing changed in the Hub; Desligar again finds nothing left to do there. + */ +export async function leaveCopies(deps: CopiesDeps, input: OfConnection): Promise { + const readers = await deps.database.read(personScope(input), (tx) => readersOf(tx, input)) + const accounts = readers.flatMap(({ stored, ref }) => + ref === null ? [] : [{ copy: stored.name, ref }], + ) + if (accounts.length === 0) return + const sync = await syncIn(deps, input) + for (const account of accounts) { + const dropped = + sync === undefined ? undefined : await deps.companyPostgres.dropAccountRows(sync, account) + if (dropped?.kind === 'DONE') continue + const code = + dropped === undefined + ? 'NO_COMPANY_DATABASE' + : dropped.kind === 'REFUSED' + ? dropped.code + : dropped.kind + const { companyId, connectionId } = input + deps.log.error({ companyId, connectionId, code }, 'COPY_DATABASE_FAILED') + throw new Failure('COPY_DATABASE_UNAVAILABLE') + } +} + +/** + * In the transaction of Desligar's DELETE, before it: the rows of the copies the connection is a + * source of, locked (SELECT … FOR UPDATE), so of two connections of one copy deleted together the + * second waits and sees the copy with no source. + */ +export async function lockedCopiesOf( + tx: CommandTx, + input: Omit, +): Promise { + const rows = await tx + .select({ id: copy.id }) + .from(copy) + .where(and(eq(copy.companyId, input.companyId), inArray(copy.id, readingFrom(tx, input)))) + .for('update') + return rows.map((row) => CopyIdSchema.parse(row.id)) +} + +/** + * In the same transaction, after the DELETE took the sources in cascade: each locked copy left with + * no source ends, with its runs and releases (the cascade), and its table is noted in + * `copy_table_left` for `dropEndedCopies`. + */ +export async function endedSourceless( + tx: CommandTx, + input: { companyId: CompanyId; copyIds: readonly CopyId[] }, +): Promise { + if (input.copyIds.length === 0) return [] + const ended = await tx + .delete(copy) + .where( + and( + eq(copy.companyId, input.companyId), + inArray(copy.id, [...input.copyIds]), + notExists( + tx + .select({ one: sql`1` }) + .from(copySource) + .where(and(eq(copySource.companyId, copy.companyId), eq(copySource.copyId, copy.id))), + ), + ), + ) + .returning({ id: copy.id, name: copy.name }) + if (ended.length > 0) + await tx + .insert(copyTableLeft) + .values(ended.map((row) => ({ companyId: input.companyId, copyId: row.id, name: row.name }))) + .onConflictDoNothing() + return ended.map((row) => CopyIdSchema.parse(row.id)) +} + +/** + * One left table dropped under its row's lock: the row taken out (DELETE … RETURNING holds it to the + * end of the transaction, the Hub's role having no UPDATE to lock it otherwise), the table dropped, + * and the row put back when the company's database did not drop it. A drain that waited on the lock + * finds the row gone. + */ +function droppedTable( + deps: CopiesDeps, + input: InCompany & { copyId: CopyId; sync: DatabaseUrl }, +): Promise<{ name: string; dropped: boolean } | undefined> { + return deps.database.command(personScope(input), async (tx) => { + const [row] = await tx + .delete(copyTableLeft) + .where( + and(eq(copyTableLeft.companyId, input.companyId), eq(copyTableLeft.copyId, input.copyId)), + ) + .returning() + if (row === undefined) return undefined + const name = CopyName.parse(row.name) + const answer = await deps.companyPostgres.dropCopyTable(input.sync, { copy: name }) + if (answer.kind === 'DONE') return { name, dropped: true } + await tx.insert(copyTableLeft).values(row) + return { name, dropped: false } + }) +} + +/** + * Drains the company's rows of `copy_table_left`, written in the transaction that ended each copy: + * each table dropped under its row's lock, then the row deleted. No REVOKE: the privileges go with + * the table. Called after every Desligar's DELETE and before `ensureCopyTable` in every Ligar; a + * silent company database leaves the rows for the next call and answers their names. + */ +export async function dropEndedCopies( + deps: CopiesDeps, + input: InCompany, +): Promise<{ tablesLeft: string[] }> { + const left = await deps.database.read(personScope(input), (tx) => + tx + .select() + .from(copyTableLeft) + .where(eq(copyTableLeft.companyId, input.companyId)) + .orderBy(asc(copyTableLeft.name)), + ) + if (left.length === 0) return { tablesLeft: [] } + const sync = await syncIn(deps, input) + const stayed: { copyId: CopyId; name: string }[] = [] + for (const row of left) { + const copyId = CopyIdSchema.parse(row.copyId) + const done = + sync === undefined ? undefined : await droppedTable(deps, { ...input, copyId, sync }) + if (sync === undefined || done?.dropped === false) stayed.push({ copyId, name: row.name }) + } + for (const each of stayed) + deps.log.error({ companyId: input.companyId, copyId: each.copyId }, 'COPY_TABLES_LEFT') + return { tablesLeft: stayed.map((each) => each.name) } +} diff --git a/apps/server/src/hub/copies/list.ts b/apps/server/src/hub/copies/list.ts index 28d37e57d..403f8d32b 100644 --- a/apps/server/src/hub/copies/list.ts +++ b/apps/server/src/hub/copies/list.ts @@ -24,13 +24,13 @@ import { pageKey, pageOf } from '../../platform/pages.ts' import { connectionNamesOf } from '../connectors/public.ts' import { copyModeOf } from './mode.ts' import type { CopiesDeps, CopyState, InCompany, StoredCopy } from './store.ts' -import { parsedCopy, personScope, proposalsOf, storedCopy } from './store.ts' +import { parsedCopy, personScope, proposalsOf, shownOf, storedCopy } from './store.ts' import { copy, copyRun, projectCopy } from './tables.ts' /** The runs of a copy a page holds. */ const RUNS_PAGE = 20 -const Reason = z.enum(['CAP', 'STOPPED']) +const Reason = z.enum(['CAP', 'STOPPED', 'WAIT']) /** A run's row as the contract states it; the reason of each outcome parsed again (code C27). */ function runOf(row: typeof copyRun.$inferSelect): CopyRun { @@ -132,7 +132,6 @@ async function summaries( const schedule: CopySchedule = state?.schedule ?? { kind: 'MISSING' } const interval: CopyInterval | null = state?.interval ?? null const linked = read.names.get(row.connectionId) - const { window } = row.definition return { id: row.id, name: row.name, @@ -148,12 +147,7 @@ async function summaries( rows: row.rows, mode: copyModeOf(row.definition), projects: read.released.get(row.id) ?? [], - shown: { - query: row.definition.source.sql, - key: row.definition.key, - changedSince: row.definition.changedSince, - window: window === undefined ? null : { column: window.column, days: window.days }, - }, + shown: shownOf(row.definition), } }) } diff --git a/apps/server/src/hub/copies/migrate-copy-source.test.ts b/apps/server/src/hub/copies/migrate-copy-source.test.ts new file mode 100644 index 000000000..3d68f44c3 --- /dev/null +++ b/apps/server/src/hub/copies/migrate-copy-source.test.ts @@ -0,0 +1,70 @@ +import assert from 'node:assert/strict' +import { readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import { test } from 'node:test' +import { PADARIA_SOL_MATRIZ } from '../connectors/test-sales.ts' +import type { CopiesOn } from './test-copies.ts' +import { copiesOn, copyRowOf, runsOf } from './test-copies.ts' + +const NOTAS = PADARIA_SOL_MATRIZ.notas +const MIGRATIONS = join(import.meta.dirname, '../../../migrations') +const SOURCES = readdirSync(MIGRATIONS) + .filter((name) => name.endsWith('_sync_copy_sources_data')) + .map((name) => readFileSync(join(MIGRATIONS, name, 'migration.sql'), 'utf8'))[0] + +function sourcesOf(on: CopiesOn) { + return on.hub.test.asOwner( + `select copy_id, connection_id, connector_id, account_ref, pass, position, watermark, pass_started_at + from copies.copy_source order by copy_id`, + ) +} + +test('a fonte de cada cópia de hoje', async (t) => { + assert.ok(SOURCES, 'the data migration') + const on = await copiesOn(t) + const copyId = await on.enable('vendas') + const fiftieth = NOTAS.map((nota) => nota.nunota).sort((a, b) => a - b)[49] + // A copy of before the sources: its pass where the copy kept it, and no source. + await on.hub.test.asOwner('delete from copies.copy_source where copy_id = $1', [copyId]) + await on.hub.test.asOwner( + `update copies.copy set pass = 3, position = $2, watermark = '2026-11-01T12:00:00Z', + pass_started_at = '2026-11-02T12:00:00Z' where id = $1`, + [copyId, JSON.stringify([fiftieth])], + ) + await on.hub.test.asOwner(SOURCES) + const [source] = await sourcesOf(on) + assert.deepEqual( + { + ...source, + watermark: new Date(String(source?.watermark)).toISOString(), + pass_started_at: new Date(String(source?.pass_started_at)).toISOString(), + }, + { + copy_id: copyId, + connection_id: on.connectionId, + connector_id: 'sankhya', + account_ref: null, + pass: 3, + position: { kind: 'KEY', after: [fiftieth] }, + watermark: '2026-11-01T12:00:00.000Z', + pass_started_at: '2026-11-02T12:00:00.000Z', + }, + ) + await on.hub.test.asOwner(SOURCES) + assert.equal((await sourcesOf(on)).length, 1, 'a segunda vez não insere') + // Its first pass, which reads the whole source: the position alone decides where it goes on. + await on.hub.test.asOwner('update copies.copy_source set watermark = null where copy_id = $1', [ + copyId, + ]) + + assert.deepEqual(await on.run(copyId), { kind: 'ENDED', outcome: { kind: 'COMPLETE' } }) + const [run] = await runsOf(on, copyId) + assert.deepEqual( + [run.pages, run.read], + [3, NOTAS.length - 50], + 'a primeira rodada continua da posição guardada', + ) + const row = await copyRowOf(on, copyId) + assert.equal(row.pass, 4) + assert.equal(row.position, null) +}) diff --git a/apps/server/src/hub/copies/mode.ts b/apps/server/src/hub/copies/mode.ts index cca8706a3..2968c7660 100644 --- a/apps/server/src/hub/copies/mode.ts +++ b/apps/server/src/hub/copies/mode.ts @@ -4,11 +4,10 @@ import type { CopyDefinition } from '../connectors/public.ts' /** * How a pass reads the source, the one classification of a definition. Without `changedSince` every * pass reads the whole source and deletes what did not come back, with a `window` or without; with it, - * a pass rereads the changed rows, within the window's days when there is one. + * a pass rereads the changed rows, within the window's days when there is one (version 1 alone has a window). */ export function copyModeOf(definition: CopyDefinition): CopyMode { if (definition.changedSince === null) return { kind: 'WHOLE' } - return definition.window === undefined - ? { kind: 'CHANGES' } - : { kind: 'WINDOW', days: definition.window.days } + const window = definition.version === 1 ? definition.window : undefined + return window === undefined ? { kind: 'CHANGES' } : { kind: 'WINDOW', days: window.days } } diff --git a/apps/server/src/hub/copies/project-view.ts b/apps/server/src/hub/copies/project-view.ts index 01ab2c98f..1978c271f 100644 --- a/apps/server/src/hub/copies/project-view.ts +++ b/apps/server/src/hub/copies/project-view.ts @@ -12,7 +12,7 @@ export type ProjectCopiesView = kind: 'COPIES' released: { name: CopyName - columns: CopyDefinition['columns'] + columns: { name: string; type: CopyDefinition['columns'][number]['type'] }[] mode: CopyMode }[] unreleased: CopyName[] @@ -44,7 +44,7 @@ export async function projectView( kind: 'COPIES', released: released.map(({ copy }) => ({ name: copy.name, - columns: copy.definition.columns, + columns: copy.definition.columns.map(({ name, type }) => ({ name, type })), mode: copyModeOf(copy.definition), })), unreleased: rows.filter(({ granted }) => !granted).map(({ copy }) => copy.name), diff --git a/apps/server/src/hub/copies/propose.ts b/apps/server/src/hub/copies/propose.ts index 5b7583765..ce92468ff 100644 --- a/apps/server/src/hub/copies/propose.ts +++ b/apps/server/src/hub/copies/propose.ts @@ -5,7 +5,7 @@ import { and, eq, sql } from 'drizzle-orm' import type { z } from 'zod' import { violatedConstraint, violatedReference } from '../../platform/database.ts' import type { Proposal, ProposeAnswer } from '../connectors/public.ts' -import { CopyDefinition, CopyName, connectionNamesOf } from '../connectors/public.ts' +import { CopyDefinitionV1, CopyName, connectionNamesOf } from '../connectors/public.ts' import { copyModeOf } from './mode.ts' import type { CopiesDeps } from './store.ts' import { companyScope } from './store.ts' @@ -44,7 +44,7 @@ async function existing(deps: CopiesDeps, ids: OfProject, name: CopyName) { } /** - * Propor (Capacidade 3, U3): the definition by `CopyDefinition` and the name by `CopyName`, the name + * Propor (Capacidade 3, U3): the definition by `CopyDefinitionV1` and the name by `CopyName`, the name * against the company's copies and open proposals, one page of proof through the connectors' port * (the count, never a row), then the row, which the partial unique index guards against a second * open proposal of the name. Nothing is written on any refusal. @@ -55,7 +55,7 @@ export async function propose( a: Proposal, ): Promise { const parsedName = CopyName.safeParse(a.name) - const parsed = CopyDefinition.safeParse(a.definition) + const parsed = CopyDefinitionV1.safeParse(a.definition) const issues = [ ...(parsedName.success ? [] : issuesOf(parsedName.error, 'name')), ...(parsed.success ? [] : issuesOf(parsed.error, 'definition')), diff --git a/apps/server/src/hub/copies/run-v2.test.ts b/apps/server/src/hub/copies/run-v2.test.ts new file mode 100644 index 000000000..d95c7e76d --- /dev/null +++ b/apps/server/src/hub/copies/run-v2.test.ts @@ -0,0 +1,320 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { phraseOf } from '@conexus/hub-api' +import { call } from '../identity/test-hub.ts' +import { asSync } from './test-copies.ts' +import { + BY_ACCOUNT, + CEILED, + callsOf, + ITEMS_COPY, + ordersCopy, + STORE_A, + STORE_B, + storesOn, + tableOf, +} from './test-sources.ts' + +const ORDERS = 120 + +test('a passada de uma leitura de lista', async (t) => { + const on = await storesOn(t, { stores: [STORE_A] }) + const copyId = await on.planted('pedidos', ordersCopy(true), [STORE_A.ref]) + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + const first = await tableOf(on, 'pedidos') + assert.equal(first.length, ORDERS) + assert.ok( + first.every( + ([account, name, , pass]) => account === STORE_A.ref && name === 'Loja A' && pass === 0, + ), + ) + assert.equal(callsOf(on.door, STORE_A).length, 3, '120 pedidos em páginas de 50') + const [source] = await on.sourcesOf(copyId) + assert.equal(source?.pass, 1) + assert.equal(source?.position, null) + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + assert.equal((await tableOf(on, 'pedidos')).length, ORDERS, 'nenhuma linha duplica nem sai') +}) + +test('a primeira passada sem filtro', async (t) => { + const on = await storesOn(t, { stores: [STORE_A] }) + const copyId = await on.planted('pedidos', ordersCopy(true), [STORE_A.ref]) + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + const changedFrom = 'order.date_last_updated.from' + assert.ok(callsOf(on.door, STORE_A).every((url) => !url.searchParams.has(changedFrom))) + const [{ watermark } = {}] = await on.sourcesOf(copyId) + assert.ok(watermark instanceof Date, 'a marca é a hora em que a passada começou') + on.door.seen.length = 0 + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + const since = new Date(watermark.getTime() - 86_400_000).toISOString() + assert.ok( + callsOf(on.door, STORE_A).every((url) => url.searchParams.get(changedFrom) === since), + 'a marca menos um dia', + ) +}) + +test('a passada de uma leitura de lista: a célula que não cabe no tipo termina em erro', async (t) => { + const on = await storesOn(t, { stores: [STORE_A] }) + const misfit = { ...ordersCopy(true) } + misfit.columns = misfit.columns.map((column) => + column.name === 'status' ? { ...column, type: 'bigint' } : column, + ) + const copyId = await on.planted('pedidos', misfit, [STORE_A.ref]) + assert.deepEqual(await on.ended(copyId), { kind: 'ERROR', code: 'COPY_SOURCE_UNREADABLE' }) + const [source] = await on.sourcesOf(copyId) + assert.deepEqual([source?.position, source?.watermark, source?.pass], [null, null, 0]) + assert.deepEqual(await tableOf(on, 'pedidos'), []) +}) + +const WAIT = { kind: 'PARTIAL', reason: 'WAIT' } + +test('o fornecedor pede para esperar', async (t) => { + const on = await storesOn(t, { stores: [STORE_A] }) + const copyId = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref]) + on.door.busy.set(2, '60') + assert.deepEqual(await on.ended(copyId), WAIT) + const [waiting] = await on.sourcesOf(copyId) + assert.deepEqual(waiting?.position, { kind: 'OFFSET', offset: 50 }, 'a posição fica') + assert.equal(waiting?.watermark, null, 'a marca não anda') + const until = waiting?.wait_until + assert.ok(until instanceof Date && until.getTime() > Date.now() + 50_000, 'a hora dita') + assert.equal((await tableOf(on, 'pedidos')).length, 50, 'nada é apagado') + + const asked = on.door.seen.length + assert.deepEqual(await on.ended(copyId), WAIT) + assert.equal(on.door.seen.length, asked, 'antes da hora, nenhuma chamada') + + await on.hub.test.asOwner( + `update copies.copy_source set wait_until = now() - interval '1 second' where copy_id = $1`, + [copyId], + ) + on.door.busy.set(asked + 2, null) + assert.deepEqual(await on.ended(copyId), WAIT) + const [again] = await on.sourcesOf(copyId) + assert.deepEqual(again?.position, { kind: 'OFFSET', offset: 100 }, 'continuou da posição') + assert.equal(again?.wait_until, null, 'sem Retry-After, a próxima pode chamar') + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + const rows = await tableOf(on, 'pedidos') + assert.equal(rows.length, ORDERS) + assert.equal(new Set(rows.map((row) => row[2])).size, ORDERS, 'sem repetição') + assert.equal(on.door.seen.length, asked + 3) +}) + +test('o fornecedor pede para esperar: o limite do aplicativo vale para todas as contas', async (t) => { + const on = await storesOn(t) + const copyId = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref, STORE_B.ref]) + on.door.busy.set(1, '60') + assert.deepEqual(await on.ended(copyId), WAIT) + assert.equal(callsOf(on.door, STORE_B).length, 0, 'a segunda conta não é chamada') + assert.deepEqual(await tableOf(on, 'pedidos'), []) +}) + +test('o fornecedor pede para esperar: o limite de uma conta não diz nada da outra', async (t) => { + const on = await storesOn(t, { catalog: BY_ACCOUNT }) + const copyId = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref, STORE_B.ref]) + on.door.busy.set(1, '60') + assert.deepEqual(await on.ended(copyId), WAIT) + assert.equal(callsOf(on.door, STORE_B).length, 3, 'a segunda conta lê as suas páginas') + const rows = await tableOf(on, 'pedidos') + assert.equal(rows.length, ORDERS) + assert.ok(rows.every(([account]) => account === STORE_B.ref)) + const [a, b] = await on.sourcesOf(copyId) + assert.deepEqual([a?.pass, a?.wait_until instanceof Date, b?.pass], [0, true, 1]) +}) + +test('o cursor vencido recomeça', async (t) => { + let ahead = 0 + const on = await storesOn(t, { stores: [STORE_A], now: () => new Date(Date.now() + ahead) }) + const copyId = await on.planted('anuncios', ITEMS_COPY, [STORE_A.ref]) + on.door.busy.set(3, null) + assert.deepEqual(await on.ended(copyId), WAIT) + const [held] = await on.sourcesOf(copyId) + assert.equal(Object(held?.position).kind, 'CURSOR') + + ahead = 60_000 + let asked = on.door.seen.length + on.door.busy.set(asked + 2, null) + assert.deepEqual(await on.ended(copyId), WAIT) + assert.ok(on.door.seen[asked]?.searchParams.has('scroll_id'), 'dentro da validade, continua') + assert.ok(!on.hub.lines.some((line) => line.message === 'COPY_PASS_RESTARTED')) + + ahead += 6 * 60_000 + asked = on.door.seen.length + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + const restart = on.door.seen[asked] + assert.equal(restart?.searchParams.get('search_type'), 'scan') + assert.ok(!restart?.searchParams.has('scroll_id'), 'recomeça do começo') + const restarted = on.hub.lines.find((line) => line.message === 'COPY_PASS_RESTARTED') + assert.equal(restarted?.fields.pass, 0, 'o mesmo número de passada') + const rows = await tableOf(on, 'anuncios') + assert.equal(rows.length, 1300, 'a passada fecha certa') + assert.ok(rows.every((row) => row[3] === 0)) + assert.equal((await on.sourcesOf(copyId))[0]?.pass, 1) +}) + +test('o teto da leitura', async (t) => { + const on = await storesOn(t, { stores: [STORE_A], catalog: CEILED }) + const copyId = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref]) + assert.deepEqual(await on.ended(copyId), { kind: 'ERROR', code: 'COPY_SOURCE_CEILING' }) + const [source] = await on.sourcesOf(copyId) + assert.deepEqual(source?.position, { kind: 'OFFSET', offset: 50 }, 'a posição guardada') + assert.equal(source?.watermark, null) + assert.equal((await tableOf(on, 'pedidos')).length, 50, 'nada sai') + const listed = await call(on.hub, on.ana, 'GET', `/api/companies/${on.companyId}/copies`) + const [copy] = Object(listed.json()).copies + assert.deepEqual(copy?.run?.outcome, { kind: 'ERROR', code: 'COPY_SOURCE_CEILING' }) + assert.equal( + phraseOf('COPY_SOURCE_CEILING'), + 'A leitura chegou ao máximo de linhas que o sistema entrega.', + ) +}) + +test('duas contas numa tabela', async (t) => { + const on = await storesOn(t) + const copyId = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref, STORE_B.ref]) + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + const rows = await tableOf(on, 'pedidos') + assert.equal(rows.length, 2 * ORDERS, 'o mesmo id em duas contas dá duas linhas') + const sources = await on.sourcesOf(copyId) + assert.deepEqual( + sources.map((source) => [source.account_ref, source.pass, source.position]), + [ + [STORE_A.ref, 1, null], + [STORE_B.ref, 1, null], + ], + ) + + const alone = await on.planted('pedidos_a', ordersCopy(false), [STORE_A.ref]) + await assert.rejects( + on.hub.test.asOwner( + `insert into copies.copy_source (company_id, copy_id, connector_id, connection_id, account_ref, created_at) + select company_id, $1, connector_id, id, $3, now() from connectors.connection where id = $2`, + [alone, on.stores.get(STORE_B.ref), STORE_A.ref], + ), + (error: unknown) => Object(error).constraint === 'copy_source_copy_id_account_ref_key', + ) + + await asSync(on.owner, (client) => + client.query( + `insert into sync.pedidos (id, status, _account, _account_name, _pass) + values (9, 'sumiu', $1, 'Loja A', 0), (9, 'sumiu', $2, 'Loja B', 0)`, + [STORE_A.ref, STORE_B.ref], + ), + ) + on.door.busy.set(on.door.seen.length + 4, '60') + assert.deepEqual(await on.ended(copyId), WAIT) + const after = await tableOf(on, 'pedidos') + function of(ref: string) { + return after.filter(([account]) => account === ref).map((row) => row[2]) + } + assert.equal(of(STORE_A.ref).length, ORDERS, 'a passada de A apaga o que ela não gravou') + assert.ok(!of(STORE_A.ref).includes('9')) + assert.equal(of(STORE_B.ref).length, ORDERS + 1, 'as linhas de B ficam') +}) + +test('o nome da conta segue a conexão', async (t) => { + const on = await storesOn(t, { stores: [STORE_A] }) + const copyId = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref]) + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + await on.hub.test.asOwner(`update connectors.connection set name = 'Loja Centro' where id = $1`, [ + on.stores.get(STORE_A.ref), + ]) + assert.ok((await tableOf(on, 'pedidos')).every((row) => row[1] === 'Loja A')) + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + assert.ok((await tableOf(on, 'pedidos')).every((row) => row[1] === 'Loja Centro')) +}) + +test('a conta desligada', async (t) => { + const on = await storesOn(t) + const copyId = await on.planted('pedidos', ordersCopy(false), [STORE_A.ref, STORE_B.ref]) + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + function unlink(ref: string) { + return call(on.hub, on.ana, 'DELETE', `${on.base}/${on.stores.get(ref)}`) + } + const b = await unlink(STORE_B.ref) + assert.deepEqual([b.statusCode, Object(b.json()).tablesLeft], [200, []]) + assert.deepEqual( + (await on.sourcesOf(copyId)).map((source) => source.account_ref), + [STORE_A.ref], + 'só a fonte da conexão sai', + ) + const left = await tableOf(on, 'pedidos') + assert.ok(left.length === ORDERS && left.every(([account]) => account === STORE_A.ref)) + + await asSync(on.owner, (client) => + client.query( + `insert into sync.pedidos (id, status, _account, _account_name, _pass) values (9, 'tarde', $1, 'Loja B', 0)`, + [STORE_B.ref], + ), + ) + assert.deepEqual(await on.ended(copyId), { kind: 'COMPLETE' }) + const netted = await tableOf(on, 'pedidos') + assert.ok(netted.length === ORDERS && netted.every(([account]) => account === STORE_A.ref)) + + const a = await unlink(STORE_A.ref) + assert.deepEqual([a.statusCode, Object(a.json()).tablesLeft], [200, []]) + assert.deepEqual( + await on.hub.test.asOwner('select count(*)::int as n from copies.copy where id = $1', [copyId]), + [{ n: 0 }], + 'a última conexão encerra a cópia na hora, sem horário nem rodada', + ) + assert.deepEqual( + await on.hub.test.asOwner( + 'select count(*)::int as n from copies.copy c where not exists (select 1 from copies.copy_source s where s.copy_id = c.id)', + ), + [{ n: 0 }], + 'nenhuma cópia sem fonte', + ) + const [gone] = await asSync( + on.owner, + async (client) => (await client.query(`select to_regclass('sync.pedidos') as found`)).rows, + ) + assert.equal(gone?.found, null, 'a tabela saiu') +}) + +test('o registro não leva dado', async (t) => { + let ahead = 0 + const on = await storesOn(t, { stores: [STORE_A], now: () => new Date(Date.now() + ahead) }) + const items = await on.planted('anuncios', ITEMS_COPY, [STORE_A.ref]) + on.door.busy.set(2, '30') + assert.deepEqual(await on.ended(items), WAIT) + await on.hub.test.asOwner('update copies.copy_source set wait_until = null where copy_id = $1', [ + items, + ]) + ahead = 10 * 60_000 + on.door.busy.set(on.door.seen.length + 2, null) + assert.deepEqual(await on.ended(items), WAIT) + const orders = await on.planted('pedidos', ordersCopy(true), [STORE_A.ref]) + assert.deepEqual(await on.ended(orders), { kind: 'COMPLETE' }) + assert.deepEqual(await on.ended(orders), { kind: 'COMPLETE' }) + const copyLines = on.hub.lines.filter((line) => line.message.startsWith('COPY_')) + assert.deepEqual([...new Set(copyLines.map((line) => line.message))].sort(), [ + 'COPY_PASS_RESTARTED', + 'COPY_RUN_ENDED', + ]) + const allowed = new Set( + ['companyId', 'copyId', 'runId', 'connectionId', 'pass', 'outcome', 'reason'].concat([ + 'pages', + 'read', + 'written', + 'ms', + ]), + ) + for (const line of copyLines) + for (const field of Object.keys(line.fields)) + assert.ok(allowed.has(field), `${line.message} ${field}`) + const said = JSON.stringify(on.hub.lines) + for (const data of [ + 'items-', + 'scroll_id', + 'scan', + 'MLB1000000', + 'changedFrom', + 'paid', + '2026-09-10', + ]) + assert.ok(!said.includes(data), `o registro não diz ${data}`) + assert.ok( + on.hub.lines.some((line) => line.message === 'CONNECTOR_CALLED' && line.fields.by === 'copy'), + ) +}) diff --git a/apps/server/src/hub/copies/run.test.ts b/apps/server/src/hub/copies/run.test.ts index 02f0ce382..12b15f00f 100644 --- a/apps/server/src/hub/copies/run.test.ts +++ b/apps/server/src/hub/copies/run.test.ts @@ -8,7 +8,7 @@ import { copiesOn, copyRowOf, holdPage, - plantCopy, + plantSource, runsOf, syncRows, TEST_TUNING, @@ -57,6 +57,22 @@ test('a passada completa', async (t) => { assert.equal((await copyRowOf(on, copyId)).pass, 2) }) +test('a primeira passada sem filtro', async (t) => { + const on = await copiesOn(t) + const copyId = await on.enable('vendas') + assert.deepEqual(await ended(on, copyId), { kind: 'COMPLETE' }) + const first = on.gate.statements.splice(0) + assert.ok( + first.length > 0 && first.every((sent) => !sent.includes('TO_DATE')), + 'sem marca, sem data', + ) + assert.deepEqual(await ended(on, copyId), { kind: 'COMPLETE' }) + assert.ok( + on.gate.statements.every((sent) => sent.includes('DTALTER >= TO_DATE')), + 'com marca', + ) +}) + test('uma rodada por vez', async (t) => { const on = await copiesOn(t) const copyId = await on.enable('vendas') @@ -123,7 +139,7 @@ function notaOf(day: string, seller = 0) { /** The first pass as of 31 October, the second as of 1 November: the window from 1 and 2 October. */ async function twoPasses(on: CopiesOn, model: 'vendas' | 'itens_venda') { const copyId = await on.enable(model) - await plantCopy(on, copyId, 'pass_started_at = $2', ['2026-10-31T12:00:00Z']) + await plantSource(on, copyId, 'pass_started_at = $2', ['2026-10-31T12:00:00Z']) assert.deepEqual(await ended(on, copyId), { kind: 'COMPLETE' }) return copyId } @@ -139,7 +155,7 @@ test('a janela apaga o que sumiu', async (t) => { const before = await syncRows(on.owner, 'vendas', key) on.gate.gone.add(String(notaOf(GONE_IN_WINDOW))) on.gate.gone.add(String(notaOf(GONE_OUTSIDE))) - await plantCopy(on, copyId, 'pass_started_at = $2', ['2026-11-01T12:00:00Z']) + await plantSource(on, copyId, 'pass_started_at = $2', ['2026-11-01T12:00:00Z']) on.fake.switches.refuse.add('consultar') assert.deepEqual(await ended(on, copyId), { kind: 'ERROR', code: 'COPY_SOURCE_REFUSED' }) assert.deepEqual(await syncRows(on.owner, 'vendas', key), before, 'com erro nada sai') @@ -161,7 +177,7 @@ test('a janela relê o que não mudou', async (t) => { model === 'vendas' ? NOTAS.length : ITENS.length, ) on.gate.gone.add(String(notaOf(GONE_IN_WINDOW, 1))) - await plantCopy(on, copyId, 'pass_started_at = $2', ['2026-11-01T12:00:00Z']) + await plantSource(on, copyId, 'pass_started_at = $2', ['2026-11-01T12:00:00Z']) assert.deepEqual(await ended(on, copyId), { kind: 'COMPLETE' }) const rows = await syncRows(on.owner, model, key) const kept = rows.filter((row) => Number(row[0]) === notaOf(KEPT_IN_WINDOW)) diff --git a/apps/server/src/hub/copies/run.ts b/apps/server/src/hub/copies/run.ts index 688ada17c..3e694755b 100644 --- a/apps/server/src/hub/copies/run.ts +++ b/apps/server/src/hub/copies/run.ts @@ -1,10 +1,13 @@ import { randomUUID } from 'node:crypto' -import type { CompanyId, CopyId } from '@conexus/hub-api' import { CopyRunId } from '@conexus/hub-api' import { and, desc, eq, isNull, lt, notInArray, or, sql } from 'drizzle-orm' -import type { DatabaseUrl } from '../../platform/database.ts' -import type { CopiesDeps, CopyOutcome, CopyRunEnd, CopyTuning, StoredCopy } from './store.ts' -import { companyScope, parsedCopy, syncOf } from './store.ts' +import { connectionNamesOf } from '../connectors/public.ts' +import type { Claimed, Counts, Ids, Run, SourceStep } from './source-pass.ts' +import { DATABASE_DOWN, lost, netDropped, sourcePass, WAIT } from './source-pass.ts' +import type { StoredSource } from './sources.ts' +import { datedPasses, sourcesOf } from './sources.ts' +import type { CopiesDeps, CopyOutcome, CopyRunEnd, CopyTuning } from './store.ts' +import { companyScope, copyTableOf, parsedCopy, syncOf } from './store.ts' import { copy, copyRun } from './tables.ts' /** Production's ceilings of one run (code C26): 500 rows a page, 200 pages or 10 minutes, a lease of 10 minutes renewed at each page. */ @@ -17,13 +20,6 @@ export const RUN_DEFAULTS = { /** The newest runs of each copy kept; older ones go when a run ends. */ const KEPT_RUNS = 100 -const DAY_MS = 86_400_000 -/** Before any change date a source keeps: the first pass reads it all. */ -const FIRST_SINCE = '1900-01-01' - -type Ids = { companyId: CompanyId; copyId: CopyId } -type Claimed = { copy: StoredCopy; token: string; runId: CopyRunId; sync: DatabaseUrl | undefined } -type Counts = { pages: number; read: number; written: number } function leaseFrom(deps: CopiesDeps) { return sql`statement_timestamp() + ${deps.tuning.leaseMs} * interval '1 millisecond'` @@ -32,7 +28,7 @@ function leaseFrom(deps: CopiesDeps) { /** * The lease taken in one Hub transaction (data D25): only when none is held or it ran out, decided * by the `UPDATE` itself (code C65). The open run a lost holder left ends `INTERRUPTED`, and the new - * one begins; the pass keeps the instant it began. + * one begins; each source's open pass keeps the instant it began, and one with none begins now. */ async function claim(deps: CopiesDeps, ids: Ids) { const token = randomUUID() @@ -40,11 +36,7 @@ async function claim(deps: CopiesDeps, ids: Ids) { return deps.database.command(companyScope(ids.companyId), async (tx) => { const [row] = await tx .update(copy) - .set({ - claimToken: token, - claimedUntil: leaseFrom(deps), - passStartedAt: sql`coalesce(${copy.passStartedAt}, statement_timestamp())`, - }) + .set({ claimToken: token, claimedUntil: leaseFrom(deps) }) .where( and(same, or(isNull(copy.claimedUntil), lt(copy.claimedUntil, sql`statement_timestamp()`))), ) @@ -59,61 +51,24 @@ async function claim(deps: CopiesDeps, ids: Ids) { .where(and(eq(copyRun.copyId, ids.copyId), isNull(copyRun.endedAt))) const runId = CopyRunId.parse(randomUUID()) await tx.insert(copyRun).values({ id: runId, ...ids, startedAt: sql`statement_timestamp()` }) + await datedPasses(tx, ids) + const sources = await sourcesOf(tx, ids) const claimed: Claimed = { copy: parsedCopy(row), - token, - runId, + holder: { ...ids, token, runId }, sync: await syncOf(deps, tx, ids.companyId), + sources, + names: await connectionNamesOf(tx, { + companyId: ids.companyId, + ids: sources.map((source) => source.connectionId), + }), } return { kind: 'CLAIMED', ...claimed } as const }) } function held(claimed: Claimed) { - return and(eq(copy.id, claimed.copy.id), eq(copy.claimToken, claimed.token)) -} - -/** The page saved in the Hub while the lease is ours: the last key written, the lease renewed, the run's counts. */ -async function saved( - deps: CopiesDeps, - claimed: Claimed, - input: { position: readonly number[] | null; rows: number }, -): Promise { - return deps.database.command(companyScope(claimed.copy.companyId), async (tx) => { - const [row] = await tx - .update(copy) - .set({ position: input.position, claimedUntil: leaseFrom(deps) }) - .where(held(claimed)) - .returning({ id: copy.id }) - if (row === undefined) return false - await tx - .update(copyRun) - .set({ - pages: sql`${copyRun.pages} + 1`, - read: sql`${copyRun.read} + ${input.rows}`, - written: sql`${copyRun.written} + ${input.rows}`, - }) - .where(eq(copyRun.id, claimed.runId)) - return true - }) -} - -/** The pass done in the Hub: the watermark moves to when it began, the next pass starts from nothing. */ -async function passed(deps: CopiesDeps, claimed: Claimed, rows: number): Promise { - return deps.database.command(companyScope(claimed.copy.companyId), async (tx) => { - const [row] = await tx - .update(copy) - .set({ - watermark: sql`${copy.passStartedAt}`, - pass: sql`${copy.pass} + 1`, - passStartedAt: null, - position: null, - rows, - }) - .where(held(claimed)) - .returning({ id: copy.id }) - return row !== undefined - }) + return and(eq(copy.id, claimed.holder.copyId), eq(copy.claimToken, claimed.holder.token)) } function reasonOf(outcome: CopyOutcome): string | null { @@ -130,13 +85,6 @@ function reasonOf(outcome: CopyOutcome): string | null { } } -/** A run that lost its lease to another: it writes nothing more, and the other ends this run's row. */ -function lost(deps: CopiesDeps, claimed: Claimed): CopyRunEnd { - const { companyId, id: copyId } = claimed.copy - deps.log.error({ companyId, copyId, runId: claimed.runId }, 'COPY_CLAIM_LOST') - return { kind: 'ENDED', outcome: { kind: 'INTERRUPTED' } } -} - /** The end: the lease let go, the run's row closed and pointed at, the runs past the newest 100 gone. */ async function ended( deps: CopiesDeps, @@ -148,7 +96,7 @@ async function ended( const done = await deps.database.command(companyScope(companyId), async (tx) => { const [row] = await tx .update(copy) - .set({ claimToken: null, claimedUntil: null, lastRunId: claimed.runId }) + .set({ claimToken: null, claimedUntil: null, lastRunId: claimed.holder.runId }) .where(held(claimed)) .returning({ id: copy.id }) if (row === undefined) return false @@ -159,7 +107,7 @@ async function ended( outcome: outcome.kind, reason: reasonOf(outcome), }) - .where(eq(copyRun.id, claimed.runId)) + .where(eq(copyRun.id, claimed.holder.runId)) const kept = tx .select({ id: copyRun.id }) .from(copyRun) @@ -174,7 +122,7 @@ async function ended( const reason = reasonOf(outcome) deps.log.info( { - ...{ companyId, copyId, runId: claimed.runId, outcome: outcome.kind }, + ...{ companyId, copyId, runId: claimed.holder.runId, outcome: outcome.kind }, ...(reason === null ? {} : { reason }), ...{ pages, read, written, ms: Math.round(performance.now() - counts.started) }, }, @@ -183,115 +131,20 @@ async function ended( return { kind: 'ENDED', outcome } } -function dayOf(instant: number): string { - return new Date(instant).toISOString().slice(0, 10) -} - -/** The two dates of the pass, the same on each of its pages: the watermark less a day, the window from the pass's start. */ -function datesOf(stored: StoredCopy) { - const since = stored.watermark === null ? FIRST_SINCE : dayOf(stored.watermark.getTime() - DAY_MS) - const { window } = stored.definition - const began = stored.passStartedAt?.getTime() ?? Date.now() - const windowFrom = window === undefined ? null : dayOf(began - window.days * DAY_MS) - return { since, windowFrom } -} - -/** The key of the page's last row, the next page's start; the same when the page had no row. */ -function lastKey(stored: StoredCopy, rows: readonly (readonly (string | null)[])[]) { - const last = rows.at(-1) - if (last === undefined) return stored.position - const { columns, key } = stored.definition - return key.map((name) => Number(last[columns.findIndex((column) => column.name === name)])) -} - -const DATABASE_DOWN = { kind: 'ERROR', code: 'COPY_DATABASE_UNAVAILABLE' } as const - -type Pass = { - deps: CopiesDeps - ids: Ids - signal: AbortSignal - claimed: Claimed - sync: DatabaseUrl - counts: Counts & { started: number } - end: (outcome: CopyOutcome) => Promise -} - -/** The Hub's stop, or a ceiling, reached before the next page: the run's end; none when it goes on. */ -async function halted(pass: Pass): Promise { - const { deps, signal, counts } = pass - if (deps.updating() || signal.aborted) return pass.end({ kind: 'PARTIAL', reason: 'STOPPED' }) - const capped = - counts.pages >= deps.tuning.runPages || performance.now() - counts.started >= deps.tuning.runMs - if (!capped) return undefined - const result = await pass.end({ kind: 'PARTIAL', reason: 'CAP' }) - if (result.kind === 'ENDED' && result.outcome.kind === 'PARTIAL') - await deps.clock().runNow(pass.ids.copyId) - return result -} - -type Step = - | { kind: 'END'; end: CopyRunEnd } - | { kind: 'PAGE'; position: StoredCopy['position']; more: boolean } - -/** One page: read from the source, written in the company's database, saved in the Hub. */ -async function pageOf( - pass: Pass, - position: StoredCopy['position'], - dates: ReturnType, -): Promise { - const { deps, ids, signal, claimed, sync, counts } = pass - const stored = claimed.copy - const { definition, name, pass: passId } = stored - const read = await deps.connectors().readPage({ - companyId: ids.companyId, - connectionId: stored.connectionId, - enabledBy: stored.createdBy, - input: { definition, after: position, ...dates, limit: deps.tuning.pageRows }, - signal, - }) - if (read.kind === 'NO_CONNECTION') - return { kind: 'END', end: await pass.end({ kind: 'ERROR', code: 'COPY_SOURCE_UNREACHABLE' }) } - if (read.kind === 'FAILED') { - const outcome: CopyOutcome = signal.aborted - ? { kind: 'PARTIAL', reason: 'STOPPED' } - : { kind: 'ERROR', code: read.code } - return { kind: 'END', end: await pass.end(outcome) } - } - const { rows, more } = read.page - const written = await deps.companyPostgres.writePage( - sync, - { name, definition, pass: passId, rows }, - signal, - ) - if (written.kind !== 'DONE') return { kind: 'END', end: await pass.end(DATABASE_DOWN) } - const next = lastKey(stored, rows) ?? position - if (!(await saved(deps, claimed, { position: next, rows: rows.length }))) - return { kind: 'END', end: lost(deps, claimed) } - counts.pages += 1 - counts.read += rows.length - counts.written += rows.length - return { kind: 'PAGE', position: next, more } -} - -/** The pass's close, once its last page is written: the rows the window no longer holds go, and the pass is counted. */ -async function closedPass(pass: Pass, windowFrom: string | null): Promise { - const { deps, claimed, sync, signal } = pass - const { definition, name, pass: passId } = claimed.copy - const closed = await deps.companyPostgres.closePass( - sync, - { name, definition, pass: passId, windowFrom }, - signal, - ) - if (closed.kind !== 'DONE') return pass.end(DATABASE_DOWN) - if (!(await passed(deps, claimed, closed.rows))) return lost(deps, claimed) - return pass.end({ kind: 'COMPLETE' }) +/** The sources in a run's order: those with a pass open first, then the others as they were made. */ +function inRunOrder(sources: readonly StoredSource[]): StoredSource[] { + return [ + ...sources.filter((source) => source.position !== null), + ...sources.filter((source) => source.position === null), + ] } /** - * One run of one copy, the Factory's shape (`build.ts:139-216`): the lease, then page after page - * read from the source, written in the company's database and saved in the Hub, until the pass - * ends, a ceiling, the Hub's stop or a failure. A failure keeps the position and never moves the - * watermark nor takes a row out. + * One run of one copy, the Factory's shape (`build.ts:139-216`): the lease, then the rows of an + * account that is no source's out of the table (Desligar's net), then each source's pass. A source + * the provider asked to wait ends the run when the provider counts its limit by the app, which every + * account shares; by the account, the run goes on to the next source and ends `PARTIAL` `WAIT`. The + * last source closed ends it `COMPLETE`; the ceilings hold for the whole run. */ export async function runCopy( deps: CopiesDeps, @@ -307,15 +160,18 @@ export async function runCopy( } const { sync } = claimed if (sync === undefined) return end(DATABASE_DOWN) - const pass: Pass = { deps, ids, signal, claimed: held, sync, counts, end } - const { since, windowFrom } = datesOf(held.copy) - let position = held.copy.position - for (;;) { - const stopped = await halted(pass) - if (stopped !== undefined) return stopped - const step = await pageOf(pass, position, { since, windowFrom }) + const table = copyTableOf(held.copy, held.sources) + const run: Run = { deps, ids, signal, claimed: held, sync, table, counts, end } + if (!(await netDropped(run))) return end(DATABASE_DOWN) + const byApp = deps.connectors().countedBy(held.copy.connectorId) === 'APP' + let waited = false + for (const source of inRunOrder(held.sources)) { + const step: SourceStep = source.waits ? { kind: 'WAIT' } : await sourcePass(run, source) if (step.kind === 'END') return step.end - position = step.position - if (!step.more) return closedPass(pass, windowFrom) + if (step.kind === 'WAIT') { + if (byApp) return end(WAIT) + waited = true + } } + return end(waited ? WAIT : { kind: 'COMPLETE' }) } diff --git a/apps/server/src/hub/copies/service.ts b/apps/server/src/hub/copies/service.ts index 419336224..e29939635 100644 --- a/apps/server/src/hub/copies/service.ts +++ b/apps/server/src/hub/copies/service.ts @@ -1,4 +1,11 @@ -import type { CompanyId, CopyId, CopyProposalId, OfProject, ProjectId } from '@conexus/hub-api' +import type { + CompanyId, + ConnectionId, + CopyId, + CopyProposalId, + OfProject, + ProjectId, +} from '@conexus/hub-api' import type { SqlAnswer } from '../../platform/company-postgres.ts' import type { Proposal } from '../connectors/public.ts' import type { Accepting } from './accept.ts' @@ -7,6 +14,13 @@ import type { Enabling } from './enable.ts' import { enableCopy } from './enable.ts' import type { OfRelease } from './grants.ts' import { applyGrants, grantCopy, revokeAllOf, revokeCopy } from './grants.ts' +import { + copiesOfConnection, + dropEndedCopies, + endedSourceless, + leaveCopies, + lockedCopiesOf, +} from './leave.ts' import { listed, models, ofProject, runs } from './list.ts' import { projectView } from './project-view.ts' import { propose } from './propose.ts' @@ -55,6 +69,15 @@ export function createCopyService( /** After an app's logins are made: every release of the Projeto granted again (repair only). */ applyGrants: (input: { companyId: CompanyId; projectId: ProjectId }): Promise => applyGrants(full, input), + /** What Desligar asks of the copies, through the connectors' lazy port (O Sync de qualquer conector). */ + ofConnections: Object.freeze({ + copiesOf: (input: InCompany & { connectionId: ConnectionId }) => + copiesOfConnection(full, input), + leave: (input: InCompany & { connectionId: ConnectionId }) => leaveCopies(full, input), + locked: lockedCopiesOf, + ended: endedSourceless, + dropEnded: (input: InCompany) => dropEndedCopies(full, input), + }), }) } export type CopyService = ReturnType diff --git a/apps/server/src/hub/copies/source-pass.ts b/apps/server/src/hub/copies/source-pass.ts new file mode 100644 index 000000000..4ab91ab61 --- /dev/null +++ b/apps/server/src/hub/copies/source-pass.ts @@ -0,0 +1,215 @@ +import type { CompanyId, CopyId } from '@conexus/hub-api' +import type { CopyAccount, CopyTable } from '../../platform/copy-sql.ts' +import type { DatabaseUrl } from '../../platform/database.ts' +import type { CopyPosition } from '../connectors/public.ts' +import type { Holder, StoredSource } from './sources.ts' +import { passed, restarted, saved, waiting } from './sources.ts' +import type { CopiesDeps, CopyOutcome, CopyRunEnd, StoredCopy } from './store.ts' + +const DAY_MS = 86_400_000 + +export type Ids = { companyId: CompanyId; copyId: CopyId } +export type Claimed = { + copy: StoredCopy + holder: Holder + sync: DatabaseUrl | undefined + sources: StoredSource[] + /** The company's name of each source's connection, the account's name in the table. */ + names: ReadonlyMap +} +export type Counts = { pages: number; read: number; written: number } + +/** A run that lost its lease to another: it writes nothing more, and the other ends this run's row. */ +export function lost(deps: CopiesDeps, claimed: Claimed): CopyRunEnd { + const { companyId, id: copyId } = claimed.copy + deps.log.error({ companyId, copyId, runId: claimed.holder.runId }, 'COPY_CLAIM_LOST') + return { kind: 'ENDED', outcome: { kind: 'INTERRUPTED' } } +} + +function dayOf(instant: number): string { + return new Date(instant).toISOString().slice(0, 10) +} + +/** + * The two dates of a source's pass, the same on each of its pages: the watermark less a day (a day in + * version 1, an instant in version 2; none on its first pass, which reads all, #940), and version 1's + * window from the pass's start. + */ +function datesOf(definition: StoredCopy['definition'], source: StoredSource) { + const from = source.watermark === null ? null : source.watermark.getTime() - DAY_MS + const since = + from === null ? null : definition.version === 2 ? new Date(from).toISOString() : dayOf(from) + const window = definition.version === 1 ? definition.window : undefined + const began = source.passStartedAt?.getTime() ?? Date.now() + const windowFrom = window === undefined ? null : dayOf(began - window.days * DAY_MS) + return { since, windowFrom } +} + +export const DATABASE_DOWN = { kind: 'ERROR', code: 'COPY_DATABASE_UNAVAILABLE' } as const +export const WAIT = { kind: 'PARTIAL', reason: 'WAIT' } as const + +export type Run = { + deps: CopiesDeps + ids: Ids + signal: AbortSignal + claimed: Claimed + sync: DatabaseUrl + table: CopyTable + counts: Counts & { started: number } + end: (outcome: CopyOutcome) => Promise +} + +/** The Hub's stop, or a ceiling of the whole run, reached before the next page: the run's end; none when it goes on. */ +async function halted(run: Run): Promise { + const { deps, signal, counts } = run + if (deps.updating() || signal.aborted) return run.end({ kind: 'PARTIAL', reason: 'STOPPED' }) + const capped = + counts.pages >= deps.tuning.runPages || performance.now() - counts.started >= deps.tuning.runMs + if (!capped) return undefined + const result = await run.end({ kind: 'PARTIAL', reason: 'CAP' }) + if (result.kind === 'ENDED' && result.outcome.kind === 'PARTIAL') + await deps.clock().runNow(run.ids.copyId) + return result +} + +/** How a source's turn in the run ended: the run's end, its pass closed, or the provider asking to wait. */ +export type SourceStep = { kind: 'END'; end: CopyRunEnd } | { kind: 'CLOSED' } | { kind: 'WAIT' } + +function account(run: Run, source: StoredSource): CopyAccount { + if (source.accountRef === null) return null + return { ref: source.accountRef, name: run.claimed.names.get(source.connectionId)?.name ?? '' } +} + +/** The source's pass closed once its last page is written: its account's rows the pass did not write go, and the pass is counted. */ +async function closedPass( + run: Run, + source: StoredSource, + windowFrom: string | null, +): Promise { + const { deps, claimed, sync, signal, table } = run + const closed = await deps.companyPostgres.closePass( + sync, + { ...table, pass: source.pass, windowFrom, account: account(run, source) }, + signal, + ) + if (closed.kind !== 'DONE') return { kind: 'END', end: await run.end(DATABASE_DOWN) } + const done = await passed(deps, claimed.holder, { + connectionId: source.connectionId, + rows: closed.rows, + }) + return done ? { kind: 'CLOSED' } : { kind: 'END', end: lost(deps, claimed) } +} + +/** The run's end on a failed read: the Hub's stop when it stopped the read, the code otherwise. */ +async function failed(run: Run, outcome: CopyOutcome): Promise { + const stopped = run.signal.aborted ? ({ kind: 'PARTIAL', reason: 'STOPPED' } as const) : outcome + return { kind: 'END', end: await run.end(stopped) } +} + +type CopyRead = Awaited['readPage']>> +/** A page's step: the source's turn ended, or the next page to read from `position`. */ +type PageStep = SourceStep | { kind: 'NEXT'; position: CopyPosition | null } + +/** The provider asked to wait: the source keeps when it may call again, the Hub's stop wins. */ +async function waitStep( + run: Run, + source: StoredSource, + waitMs: number | null, +): Promise { + const { deps, claimed, signal } = run + if (signal.aborted) return failed(run, WAIT) + const { connectionId } = source + const kept = waitMs === null || (await waiting(deps, claimed.holder, { connectionId, waitMs })) + return kept ? { kind: 'WAIT' } : { kind: 'END', end: lost(deps, claimed) } +} + +/** The cursor outlived its validity: the source's pass starts again from nothing, logged. */ +async function restartStep(run: Run, source: StoredSource): Promise { + const { deps, ids, claimed } = run + const { connectionId } = source + if (!(await restarted(deps, claimed.holder, { connectionId }))) + return { kind: 'END', end: lost(deps, claimed) } + const fields = { ...ids, runId: claimed.holder.runId, connectionId, pass: source.pass } + deps.log.info(fields, 'COPY_PASS_RESTARTED') + return { kind: 'NEXT', position: null } +} + +/** A page written with the source's account and saved on the source; the last one closes the pass. */ +async function pageStep( + run: Run, + source: StoredSource, + read: Extract, + windowFrom: string | null, +): Promise { + const { deps, claimed, sync, signal, counts, table } = run + const { rows, more } = read.page + const written = await deps.companyPostgres.writePage( + sync, + { ...table, pass: source.pass, rows, account: account(run, source) }, + signal, + ) + if (written.kind !== 'DONE') return { kind: 'END', end: await run.end(DATABASE_DOWN) } + const page = { connectionId: source.connectionId, position: read.next, rows: rows.length } + if (!(await saved(deps, claimed.holder, page))) return { kind: 'END', end: lost(deps, claimed) } + counts.pages += 1 + counts.read += rows.length + counts.written += rows.length + if (!more) return closedPass(run, source, windowFrom) + return { kind: 'NEXT', position: read.next } +} + +/** What one read leads to. */ +function stepOf( + run: Run, + source: StoredSource, + read: CopyRead, + windowFrom: string | null, +): Promise { + switch (read.kind) { + case 'NO_CONNECTION': + return failed(run, { kind: 'ERROR', code: 'COPY_SOURCE_UNREACHABLE' }) + case 'FAILED': + return failed(run, { kind: 'ERROR', code: read.code }) + case 'WAIT': + return waitStep(run, source, read.waitMs) + case 'STALE': + return restartStep(run, source) + case 'PAGE': + return pageStep(run, source, read, windowFrom) + default: + return read satisfies never + } +} + +/** + * One source's turn: page after page read, written in the company's database with its account and + * saved on the source, until its pass ends, the provider asks to wait, a ceiling, the Hub's stop or + * a failure. A failure keeps the position and never moves the watermark nor takes a row out. + */ +export async function sourcePass(run: Run, source: StoredSource): Promise { + const { deps, ids, signal, claimed } = run + const { connectionId } = source + const { definition, createdBy } = claimed.copy + const { since, windowFrom } = datesOf(definition, source) + let position: CopyPosition | null = source.position + for (;;) { + const stopped = await halted(run) + if (stopped !== undefined) return { kind: 'END', end: stopped } + const read = await deps.connectors().readPage({ + ...{ companyId: ids.companyId, connectionId, enabledBy: createdBy, definition, position }, + ...{ since, windowFrom, limit: deps.tuning.pageRows, signal }, + }) + const step = await stepOf(run, source, read, windowFrom) + if (step.kind !== 'NEXT') return step + position = step.position + } +} + +/** Desligar's net before any pass: the rows of an account that is no source's leave a copy of accounts. */ +export async function netDropped(run: Run): Promise { + const { deps, claimed, sync, table, signal } = run + if (!table.accounts) return true + const refs = claimed.sources.flatMap((source) => source.accountRef ?? []) + const net = await deps.companyPostgres.dropForeignRows(sync, { copy: table.name, refs }, signal) + return net.kind === 'DONE' +} diff --git a/apps/server/src/hub/copies/sources.ts b/apps/server/src/hub/copies/sources.ts new file mode 100644 index 000000000..ed25fd906 --- /dev/null +++ b/apps/server/src/hub/copies/sources.ts @@ -0,0 +1,175 @@ +import type { CompanyId, ConnectionId, CopyId, CopyRunId } from '@conexus/hub-api' +import { ConnectionId as ConnectionIdSchema } from '@conexus/hub-api' +import { and, asc, eq, sql } from 'drizzle-orm' +import type { CommandTx, ReadTx } from '../../platform/database.ts' +import { AccountRef, CopyPosition } from '../connectors/public.ts' +import type { CopiesDeps } from './store.ts' +import { companyScope } from './store.ts' +import { copy, copyRun, copySource } from './tables.ts' + +/** One connection a copy reads, where its pass stands and until when it waits (C27: parsed where read). */ +export type StoredSource = Readonly<{ + connectionId: ConnectionId + accountRef: AccountRef | null + pass: number + passStartedAt: Date | null + watermark: Date | null + waitUntil: Date | null + /** The provider's wait is not over by the Hub database's clock: no call this run. */ + waits: boolean + position: CopyPosition | null +}> + +function parsedSource(row: typeof copySource.$inferSelect, waits: boolean): StoredSource { + return { + connectionId: ConnectionIdSchema.parse(row.connectionId), + accountRef: row.accountRef === null ? null : AccountRef.parse(row.accountRef), + pass: row.pass, + passStartedAt: row.passStartedAt, + watermark: row.watermark, + waitUntil: row.waitUntil, + waits, + position: row.position === null ? null : CopyPosition.parse(row.position), + } +} + +type Ids = { companyId: CompanyId; copyId: CopyId } + +function ofCopy(ids: Ids) { + return and(eq(copySource.copyId, ids.copyId), eq(copySource.companyId, ids.companyId)) +} + +/** The copy's sources, in the order they were made. */ +export async function sourcesOf(tx: ReadTx, ids: Ids): Promise { + const rows = await tx + .select({ + row: copySource, + waits: sql`coalesce(${copySource.waitUntil} > statement_timestamp(), false)`, + }) + .from(copySource) + .where(ofCopy(ids)) + .orderBy(asc(copySource.createdAt), asc(copySource.connectionId)) + return rows.map(({ row, waits }) => parsedSource(row, waits)) +} + +/** The pass of each source dated when a run claims the copy and the source has none open: changes from then on are read again. */ +export function datedPasses(tx: CommandTx, ids: Ids) { + return tx + .update(copySource) + .set({ passStartedAt: sql`statement_timestamp()` }) + .where(and(ofCopy(ids), sql`${copySource.passStartedAt} IS NULL`)) +} + +/** A run that holds the copy's lease: what it writes of a source goes in only while it holds it. */ +export type Holder = Readonly<{ + companyId: CompanyId + copyId: CopyId + token: string + runId: CopyRunId +}> + +function held(holder: Holder) { + return and(eq(copy.id, holder.copyId), eq(copy.claimToken, holder.token)) +} + +function leaseFrom(deps: CopiesDeps) { + return sql`statement_timestamp() + ${deps.tuning.leaseMs} * interval '1 millisecond'` +} + +/** + * One source's write while the lease is the run's: the lease renewed in the same transaction, which + * holds the copy's row until the source is written. False when the lease went to another run. + */ +function whileHeld( + deps: CopiesDeps, + holder: Holder, + write: (tx: CommandTx) => Promise, + copyRows?: number, +): Promise { + return deps.database.command(companyScope(holder.companyId), async (tx) => { + const [row] = await tx + .update(copy) + .set({ claimedUntil: leaseFrom(deps), ...(copyRows === undefined ? {} : { rows: copyRows }) }) + .where(held(holder)) + .returning({ id: copy.id }) + if (row === undefined) return false + await write(tx) + return true + }) +} + +function source(holder: Holder, connectionId: ConnectionId) { + return and(ofCopy(holder), eq(copySource.connectionId, connectionId)) +} + +/** A page written: the source's next position, its wait over, and the run's counts. */ +export function saved( + deps: CopiesDeps, + holder: Holder, + input: { connectionId: ConnectionId; position: CopyPosition | null; rows: number }, +): Promise { + return whileHeld(deps, holder, async (tx) => { + await tx + .update(copySource) + .set({ position: input.position, waitUntil: null }) + .where(source(holder, input.connectionId)) + await tx + .update(copyRun) + .set({ + pages: sql`${copyRun.pages} + 1`, + read: sql`${copyRun.read} + ${input.rows}`, + written: sql`${copyRun.written} + ${input.rows}`, + }) + .where(eq(copyRun.id, holder.runId)) + }) +} + +/** The source's pass done: its watermark moves to when the pass began, the next starts from nothing, and the copy counts the table. */ +export function passed( + deps: CopiesDeps, + holder: Holder, + input: { connectionId: ConnectionId; rows: number }, +): Promise { + return whileHeld( + deps, + holder, + (tx) => + tx + .update(copySource) + .set({ + watermark: sql`${copySource.passStartedAt}`, + pass: sql`${copySource.pass} + 1`, + passStartedAt: null, + position: null, + waitUntil: null, + }) + .where(source(holder, input.connectionId)), + input.rows, + ) +} + +/** The provider asked to wait: no call before `waitMs` from now; none said, the next run may call. */ +export function waiting( + deps: CopiesDeps, + holder: Holder, + input: { connectionId: ConnectionId; waitMs: number | null }, +): Promise { + const until = + input.waitMs === null + ? null + : sql`statement_timestamp() + ${input.waitMs} * interval '1 millisecond'` + return whileHeld(deps, holder, (tx) => + tx.update(copySource).set({ waitUntil: until }).where(source(holder, input.connectionId)), + ) +} + +/** The source's cursor outlived its validity: the pass starts again from nothing, with the same number. */ +export function restarted( + deps: CopiesDeps, + holder: Holder, + input: { connectionId: ConnectionId }, +): Promise { + return whileHeld(deps, holder, (tx) => + tx.update(copySource).set({ position: null }).where(source(holder, input.connectionId)), + ) +} diff --git a/apps/server/src/hub/copies/store.ts b/apps/server/src/hub/copies/store.ts index 477d22401..a6a868d9b 100644 --- a/apps/server/src/hub/copies/store.ts +++ b/apps/server/src/hub/copies/store.ts @@ -6,6 +6,7 @@ import type { CopyProposalSummary, CopyRunFailureCode, CopySchedule, + CopySummary, PersonId, ProjectId, } from '@conexus/hub-api' @@ -19,8 +20,8 @@ import { ProjectId as ProjectIdSchema, } from '@conexus/hub-api' import { and, eq, inArray, or } from 'drizzle-orm' -import { z } from 'zod' import type { CompanyPostgres } from '../../platform/company-postgres.ts' +import type { CopyTable } from '../../platform/copy-sql.ts' import type { Database, DatabaseUrl, ReadTx, Scope } from '../../platform/database.ts' import type { FailureLog } from '../../platform/failure.ts' import type { NeonProjects } from '../../platform/neon.ts' @@ -63,7 +64,8 @@ type ProjectDatabase = { export type CopyOutcome = | { kind: 'COMPLETE' } - | { kind: 'PARTIAL'; reason: 'CAP' | 'STOPPED' } + /** WAIT: the provider answered 429, or the connector's lane passed its deadline; each source keeps its position. */ + | { kind: 'PARTIAL'; reason: 'CAP' | 'STOPPED' | 'WAIT' } | { kind: 'ERROR'; code: CopyRunFailureCode } | { kind: 'INTERRUPTED' } @@ -104,9 +106,6 @@ export function personScope(input: InCompany): Scope { return { kind: 'COMPANY', companyId: input.companyId, personId: input.personId } } -/** A position: the last key written, one number per key column (code C27). */ -const Position = z.array(z.int()).nullable() - /** A copy's row as the run and the release read it: the definition parsed again (code C27). */ export async function storedCopy(tx: ReadTx, ids: { companyId: CompanyId; copyId: CopyId }) { const [row] = await tx @@ -125,11 +124,23 @@ export function parsedCopy(row: typeof copy.$inferSelect) { createdBy: PersonIdSchema.parse(row.createdBy), name: CopyName.parse(row.name), definition: CopyDefinition.parse(row.definition), - position: Position.parse(row.position), } } export type StoredCopy = ReturnType +/** The copy's table as the company's database keeps it: with the account's columns when its sources have an account, and a version 2's instants with their offset. */ +export function copyTableOf( + stored: Pick, + sources: readonly { accountRef: string | null }[], +): CopyTable { + return { + name: stored.name, + definition: stored.definition, + accounts: sources.some((source) => source.accountRef !== null), + instants: stored.definition.version === 2 ? 'OFFSET' : 'LOCAL', + } +} + /** * The company's database owner for this act alone, with no transaction open (data D22), the host * the Hub reaches it by recorded once, as `openAppDatabase` does. @@ -171,6 +182,20 @@ export async function syncOf( return ready?.host == null ? undefined : deps.companyPostgres.syncAddress(ready.host) } +/** + * What the list shows of a definition: version 1's query, key and the two date filters; version 2 + * shows no query, its source is an operation of the connector. + */ +export function shownOf(definition: CopyDefinition): CopySummary['shown'] { + const window = definition.version === 1 ? definition.window : undefined + return { + query: definition.version === 1 ? definition.source.sql : '', + key: definition.key, + changedSince: definition.changedSince, + window: window === undefined ? null : { column: window.column, days: window.days }, + } +} + /** * The company's open proposals, and those `named` by id whatever their state (the conversation's * cards), as the list states them; one of a Projeto deleted or marked is left out. @@ -209,7 +234,6 @@ export async function proposalsOf( const definition = CopyDefinition.parse(row.definition) const connectionId = ConnectionIdSchema.parse(row.connectionId) const linked = connections.get(connectionId) - const { window } = definition return [ { id: CopyProposalIdSchema.parse(row.id), @@ -221,12 +245,7 @@ export async function proposalsOf( connector: linked?.connector ?? ConnectorIdSchema.parse(row.connectorId), }, project: { id: projectId, name: project }, - shown: { - query: definition.source.sql, - key: definition.key, - changedSince: definition.changedSince, - window: window === undefined ? null : { column: window.column, days: window.days }, - }, + shown: shownOf(definition), mode: copyModeOf(definition), proved: { rows: row.provedRows, more: row.provedMore }, proposedBy: { id: PersonIdSchema.parse(row.proposedBy), name: proposedByName ?? '' }, diff --git a/apps/server/src/hub/copies/tables.ts b/apps/server/src/hub/copies/tables.ts index ba69d60ef..130d3d89a 100644 --- a/apps/server/src/hub/copies/tables.ts +++ b/apps/server/src/hub/copies/tables.ts @@ -41,7 +41,8 @@ export const copy = copies.table( { id: uuid('id').primaryKey(), companyId: text('company_id').notNull(), - // The connection's connector, so the row's connection is its company's (as project_connection). + // The first connection it was turned on from: the list's connection; each connection the copy + // reads is a row of copy_source. connectorId: text('connector_id').notNull(), connectionId: uuid('connection_id').notNull(), // The table in the company's `sync` schema (`CopyName`). @@ -61,7 +62,8 @@ export const copy = copies.table( // The lease of one run (data D25): who holds it and until when. claimToken: uuid('claim_token'), claimedUntil: instant('claimed_until'), - // The pass: its number, when it began, the last key written, and when the last complete pass began. + // The pass before the sources (copy_source holds each source's): moved there by the migration, read + // by nothing since, dropped later with connection_id (data D6). pass: integer('pass').notNull().default(0), passStartedAt: instant('pass_started_at'), position: jsonb('position'), @@ -80,11 +82,8 @@ export const copy = copies.table( columns: [table.companyId], foreignColumns: [company.id], }).onDelete('cascade'), - foreignKey({ - name: 'copy_connection_fkey', - columns: [table.companyId, table.connectorId, table.connectionId], - foreignColumns: [connection.companyId, connection.connectorId, connection.id], - }).onDelete('cascade'), + // No foreign key to the connection (decision C): a copy reads its connections by copy_source, and + // ends with its last source, not with the connection it was turned on from. check('copy_claim', sql`(${table.claimToken} IS NULL) = (${table.claimedUntil} IS NULL)`), check('copy_name', sql`${table.name} ~ '^[a-z][a-z0-9_]{0,39}$'`), check('copy_definition', sql`jsonb_typeof(${table.definition}) = 'object'`), diff --git a/apps/server/src/hub/copies/test-copies.ts b/apps/server/src/hub/copies/test-copies.ts index 5f0b4580e..17c0506d0 100644 --- a/apps/server/src/hub/copies/test-copies.ts +++ b/apps/server/src/hub/copies/test-copies.ts @@ -35,6 +35,7 @@ import { createCopies } from './public.ts' * row of the answer left out, as a sale gone from the source. */ function gateOf(t: TestContext) { + const statements: string[] = [] const gate = { /** Pages held, by their number among every copy page asked. */ holds: new Map< @@ -45,6 +46,8 @@ function gateOf(t: TestContext) { gone: new Set(), /** Every copy page asked, in order. */ pages: 0, + /** The statement each copy page sent, in order. */ + statements, } return { gate, @@ -56,6 +59,7 @@ function gateOf(t: TestContext) { const isPage = body.toString('utf8').includes(' AS C0_') if (isPage) { gate.pages += 1 + gate.statements.push(body.toString('utf8')) const held = gate.holds.get(gate.pages) if (held !== undefined) { held.arrived.resolve() @@ -94,7 +98,7 @@ function gateOf(t: TestContext) { } /** The clock as a double that notes each call (T20): the run is called directly by the test. */ -function clockDouble() { +export function clockDouble() { const calls: string[] = [] const made = new Set() const clock: CopyClock = { @@ -201,7 +205,7 @@ export async function copiesOn( export type CopiesOn = Awaited> -function hubLog(lines: { fields: Record; message: string }[]) { +export function hubLog(lines: { fields: Record; message: string }[]) { return (fields: Record, message: string) => { lines.push({ fields, message }) } @@ -214,17 +218,33 @@ export function holdPage(on: CopiesOn, nth = 1) { return { arrived: held.arrived.promise, release: () => held.released.resolve() } } -/** As the Hub's owner: a copy's row set as a test needs it (the pass's start, a lease another left). */ -export async function plantCopy(on: CopiesOn, copyId: CopyId, set: string, values: unknown[] = []) { +/** As the Hub's owner: a row of `table` of the copy set as a test needs it. */ +async function plantRow( + on: CopiesOn, + table: 'copy' | 'copy_source', + where: string, + set: string, + values: unknown[], +) { const client = new pg.Client({ connectionString: on.hub.test.ownerUrl.reveal() }) await client.connect() try { - await client.query(`update copies.copy set ${set} where id = $1`, [copyId, ...values]) + await client.query(`update copies.${table} set ${set} where ${where} = $1`, values) } finally { await client.end() } } +/** As the Hub's owner: a copy's row set as a test needs it (a lease another left). */ +export function plantCopy(on: CopiesOn, copyId: CopyId, set: string, values: unknown[] = []) { + return plantRow(on, 'copy', 'id', set, [copyId, ...values]) +} + +/** As the Hub's owner: every source of a copy set as a test needs it (the pass's start, a wait). */ +export function plantSource(on: CopiesOn, copyId: CopyId, set: string, values: unknown[] = []) { + return plantRow(on, 'copy_source', 'copy_id', set, [copyId, ...values]) +} + /** As the sync login, which the owner holds by SET alone: the copies' tables are its own. */ export async function asSync(owner: DatabaseUrl, act: (client: pg.Client) => Promise) { const client = new pg.Client({ connectionString: owner.reveal() }) @@ -252,13 +272,14 @@ export function syncRows(owner: DatabaseUrl, name: string, key: readonly string[ }) } -/** The copy's row in the Hub, as the run left it. */ +/** The copy's row in the Hub with its one source's pass, as the run left them: a key position as its keys. */ export async function copyRowOf(on: CopiesOn, copyId: CopyId) { const result = await on.hub.test.database.read( { kind: 'COMPANY', companyId: on.companyId, personId: null }, (tx) => tx.execute( - sql`select pass, position, watermark, pass_started_at, rows, claim_token, last_run_id from copies.copy where id = ${copyId}`, + sql`select s.pass, s.position->'after' as position, s.watermark, s.pass_started_at, c.rows, c.claim_token, c.last_run_id + from copies.copy c join copies.copy_source s on s.copy_id = c.id where c.id = ${copyId}`, ), ) return Object(result.rows[0]) diff --git a/apps/server/src/hub/copies/test-sources.ts b/apps/server/src/hub/copies/test-sources.ts new file mode 100644 index 000000000..ac7702d8d --- /dev/null +++ b/apps/server/src/hub/copies/test-sources.ts @@ -0,0 +1,316 @@ +import assert from 'node:assert/strict' +import { randomUUID } from 'node:crypto' +import { createServer } from 'node:http' +import type { TestContext } from 'node:test' +import type { CopyId } from '@conexus/hub-api' +import { + CompanyId, + ConnectionId, + CopyId as CopyIdSchema, + PersonId, + ProjectId, +} from '@conexus/hub-api' +import { z } from 'zod' +import { createCompanyDatabase } from '../../platform/test-company-postgres.ts' +import type { ConnectorTuning } from '../connectors/public.ts' +import { CopyDefinitionV2 } from '../connectors/public.ts' +import { + forward, + linkMercadoLivre, + mercadoLivreOn, + testOf, +} from '../connectors/test-connections.ts' +import { post } from '../identity/test-hub.ts' +import type { CopyTuning } from './public.ts' +import { createCopies } from './public.ts' +import { asSync, clockDouble, hubLog, TEST_TUNING } from './test-copies.ts' + +export { BY_ACCOUNT, CEILED, catalogWith } from '../connectors/test-catalog.ts' + +/** Two stores of the double, each its own account. */ +export const STORE_A = { ref: '111000111', label: 'LOJA_A' } +export const STORE_B = { ref: '222000222', label: 'LOJA_B' } + +/** Version 2 over `pedidos`: each order's id, status and the instant it last changed. */ +export function ordersCopy(changed: boolean): CopyDefinitionV2 { + return CopyDefinitionV2.parse({ + version: 2, + source: { kind: 'OPERATION', operation: 'pedidos', input: {} }, + description: 'Os pedidos de cada loja', + columns: [ + { name: 'id', type: 'bigint', path: 'id', description: 'O número do pedido' }, + { name: 'status', type: 'text', path: 'status' }, + { name: 'atualizado', type: 'timestamp', path: 'date_last_updated' }, + ], + key: ['id'], + changedSince: changed ? { column: 'atualizado' } : null, + }) +} + +/** Version 2 over `anuncios`: the scan's ids, read by a cursor. */ +export const ITEMS_COPY = CopyDefinitionV2.parse({ + version: 2, + source: { kind: 'OPERATION', operation: 'anuncios', input: {} }, + description: 'Os anúncios da loja', + columns: [{ name: 'id', type: 'text', path: 'id' }], + key: ['id'], + changedSince: null, +}) + +type Door = { + /** Each data call the Hub made to the double, its path and query, in order. */ + seen: URL[] + /** The calls (counted from 1 among the data calls) answered 429, with the `Retry-After` each carries. */ + busy: Map + /** Data calls open at the double right now, and the most ever open together. */ + open: number + most: number + /** While set, each data call waits for it before it reaches the double. */ + held: PromiseWithResolvers | undefined +} + +const DATA = /^\/(orders\/search|users\/[0-9]+\/items\/search)/ + +/** In front of the double: the data calls noted, counted while open, held, or answered 429 as the test says. */ +export function doorOf(t: TestContext) { + const door: Door = { seen: [], busy: new Map(), open: 0, most: 0, held: undefined } + return { + door, + through: async (origin: string) => { + const server = createServer(async (incoming, outgoing) => { + const chunks: Buffer[] = [] + for await (const chunk of incoming) chunks.push(z.instanceof(Buffer).parse(chunk)) + const url = new URL(incoming.url ?? '/', 'http://double') + const data = DATA.test(url.pathname) + if (data) { + door.seen.push(url) + door.open += 1 + door.most = Math.max(door.most, door.open) + } + try { + if (data && door.held !== undefined) await door.held.promise + const busy = data ? door.busy.get(door.seen.length) : undefined + if (busy !== undefined) { + outgoing.writeHead(429, { + 'content-type': 'application/json', + ...(busy === null ? {} : { 'retry-after': busy }), + }) + outgoing.end(JSON.stringify({ message: 'too many requests', status: 429 })) + return + } + const body = incoming.method === 'GET' ? undefined : Buffer.concat(chunks) + const answered = await forward(origin, incoming, body) + const headers = { ...answered.headers } + delete headers['content-length'] + delete headers['content-encoding'] + outgoing.writeHead(answered.status, headers) + outgoing.end(answered.body) + } finally { + if (data) door.open -= 1 + } + }) + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + t.after(() => { + server.closeAllConnections() + server.close() + }) + const { port } = z.object({ port: z.number() }).parse(server.address()) + return `http://127.0.0.1:${port}` + }, + } +} + +/** The calls the door saw for one store: the orders asked of it, or its scan. */ +export function callsOf(door: Door, store: { ref: string }) { + return door.seen.filter( + (url) => url.searchParams.get('seller') === store.ref || url.pathname.includes(store.ref), + ) +} + +type MercadoLivreOn = Awaited> +type Store = Readonly<{ ref: string; label: string }> + +/** Each store linked and tested on Empresa Teste as its own account: its connection, by its account. */ +async function linkedStores(on: MercadoLivreOn, list: readonly Store[]) { + const stores = new Map() + for (const [at, store] of list.entries()) { + on.fake.switches.account = store + const name = `Loja ${String.fromCharCode(65 + at)}` + const linked = await linkMercadoLivre(on.hub, on.ana, on.companyId, { + connector: 'mercado-livre', + name, + }) + assert.equal(Object(linked.json()).result, 'LINKED', linked.body) + const connectionId = ConnectionId.parse(Object(linked.json()).connection.id) + const tested = await testOf(on.hub, on.ana, on.base, connectionId) + assert.equal(Object(tested.json()).connection?.state, 'ACTIVE', tested.body) + stores.set(store.ref, connectionId) + } + return stores +} + +/** The company's database on the test server with the `sync` schema made, unless `attached` is false. */ +async function companyDatabaseOf( + t: TestContext, + on: MercadoLivreOn, + ids: { companyId: CompanyId; personId: PersonId }, + attached: boolean, +) { + const [neonProject] = on.hub.neon.projects() + assert.ok(neonProject, 'the company database') + const owner = await createCompanyDatabase(t) + if (!attached) return owner + on.hub.neon.ownedBy(neonProject.id, owner) + assert.equal((await on.hub.postgres.companyPostgres.ensureSync(owner)).kind, 'DONE') + const host = new URL(owner.reveal()).hostname + await on.hub.test.database.command({ kind: 'COMPANY', ...ids }, (tx) => + on.hub.projects.ports.recordDatabaseHost(tx, { companyId: ids.companyId, host }), + ) + return owner +} + +/** The copies assembled over the Hub's own subjects, the clock a double. */ +function copiesOver(hub: MercadoLivreOn['hub'], tuning: CopyTuning) { + const { clock, calls } = clockDouble() + const hubState = { updating: false } + const copies = createCopies({ + ...{ database: hub.test.database, identity: hub.identity, projects: hub.projects.ports }, + ...{ neon: hub.neon.neon, companyPostgres: hub.postgres.companyPostgres }, + connectors: () => hub.connectors.copies, + prove: () => hub.connectors.builder.provePage, + apps: () => hub.apps, + clock: () => clock, + log: { info: hubLog(hub.lines), error: hubLog(hub.lines) }, + updating: () => hubState.updating, + tuning, + }) + return { copies, calls, hubState } +} + +type Planting = Readonly<{ + hub: MercadoLivreOn['hub'] + stores: Map + owner: Awaited> + attached: boolean +}> + +/** One more source of the copy, made `at` seconds after the first. */ +function sourceOf(p: Planting, copyId: CopyId, connectionId: ConnectionId, at: number) { + return p.hub.test.asOwner( + `insert into copies.copy_source (company_id, copy_id, connector_id, connection_id, account_ref, created_at) + select company_id, $1, connector_id, id, account_ref, now() + $3 * interval '1 second' + from connectors.connection where id = $2`, + [copyId, connectionId, at], + ) +} + +/** + * A version 2 copy as Ligar a proposal would leave it (U4 makes that path): the row, one source per + * connection in the order given, each with its connection's account, and the table with the + * account's columns. + */ +async function plantedBy( + p: Planting, + by: { companyId: CompanyId; personId: PersonId }, + copy: { name: string; definition: CopyDefinitionV2; refs: readonly string[] }, +) { + const { name, definition } = copy + const copyId = CopyIdSchema.parse(randomUUID()) + const connections = copy.refs.map((ref) => p.stores.get(ref) ?? assert.fail(`the store ${ref}`)) + await p.hub.test.asOwner( + `insert into copies.copy (id, company_id, connector_id, connection_id, name, label, definition, + created_by, created_at, attempt_key, body_hash) + values ($1, $2, 'mercado-livre', $3, $4, $4, $5, $6, now(), $7, 'planted')`, + [ + copyId, + by.companyId, + connections[0], + name, + JSON.stringify(definition), + by.personId, + randomUUID(), + ], + ) + for (const [at, connectionId] of connections.entries()) + await sourceOf(p, copyId, connectionId, at) + if (p.attached) { + const sync = await p.hub.postgres.companyPostgres.syncAddress( + new URL(p.owner.reveal()).hostname, + ) + const table = { name, definition, accounts: true, instants: 'OFFSET' } as const + assert.equal((await p.hub.postgres.companyPostgres.ensureCopyTable(sync, table)).kind, 'DONE') + } + return copyId +} + +/** + * Empresa Teste on a Hub whose Mercado Livre is the kit's double behind `door`, with `stores` linked + * and tested, the Projeto Pedidos and the company's database on the test server, the `sync` schema + * made. The copies are assembled over the Hub's own subjects, the clock a double. + */ +export async function storesOn( + t: TestContext, + a: { + stores?: readonly Store[] + catalog?: ConnectorTuning['catalog'] + now?: () => Date + tuning?: CopyTuning + /** No company database: a test that mocks every act on it, with another company on the same Hub. */ + companyDatabase?: boolean + } = {}, +) { + const { door, through } = doorOf(t) + const on = await mercadoLivreOn(t, { through, tuning: { catalog: a.catalog, now: a.now } }) + const { hub, fake, ana, carla, base } = on + const companyId = CompanyId.parse(on.companyId) + const anaId = PersonId.parse(ana.id) + const stores = await linkedStores(on, a.stores ?? [STORE_A, STORE_B]) + door.seen.length = 0 + const made = await post(hub, ana, `/api/companies/${companyId}/projects`, { name: 'Pedidos' }) + assert.equal(made.statusCode, 201, made.body) + const projectId = ProjectId.parse(Object(made.json()).project.id) + const attached = a.companyDatabase !== false + const owner = await companyDatabaseOf(t, on, { companyId, personId: anaId }, attached) + const { copies, calls, hubState } = copiesOver(hub, a.tuning ?? TEST_TUNING) + const planting = { hub, stores, owner, attached } + function run(copyId: CopyId, signal = new AbortController().signal) { + return copies.run({ companyId, copyId }, signal) + } + return { + ...{ hub, fake, door, ana, anaId, carla, companyId, projectId, owner, stores, base }, + ...{ copies, calls, hubState, run }, + /** Empresa Teste's copy, enabled by Ana, unless `by` names another company. */ + planted: ( + name: string, + definition: CopyDefinitionV2, + refs: readonly string[], + by = { companyId, personId: anaId }, + ) => plantedBy(planting, by, { name, definition, refs }), + addSource: (copyId: CopyId, connectionId: ConnectionId, at: number) => + sourceOf(planting, copyId, connectionId, at), + async ended(copyId: CopyId) { + const end = await run(copyId) + assert.equal(end.kind, 'ENDED') + return end.kind === 'ENDED' ? end.outcome : undefined + }, + /** Each source of the copy, in the order it was made, as the run left it. */ + sourcesOf: (copyId: CopyId) => + hub.test.asOwner( + `select connection_id, account_ref, pass, position, watermark, wait_until, pass_started_at + from copies.copy_source where copy_id = $1 order by created_at`, + [copyId], + ), + } +} + +/** The copy's table: each row's account, its name, its id as text and its pass, in that order. */ +export function tableOf(on: Pick, name: string) { + return asSync(on.owner, async (client) => { + const result = await client.query( + `select _account, _account_name, id::text, _pass from sync."${name}" order by _account, id`, + ) + return result.rows.map((row) => [row._account, row._account_name, row.id, row._pass]) + }) +} + +export type StoresOn = Awaited> diff --git a/apps/server/src/hub/identity/test-hub.ts b/apps/server/src/hub/identity/test-hub.ts index 4d6d44213..1b661a0fe 100644 --- a/apps/server/src/hub/identity/test-hub.ts +++ b/apps/server/src/hub/identity/test-hub.ts @@ -21,6 +21,7 @@ import { } from '../../platform/test-database.ts' import { mailServer, testMail } from '../../platform/test-mail.ts' import { fakeNeon } from '../../platform/test-neon.ts' +import type { ConnectorTuning } from '../connectors/public.ts' import { createStatus } from '../status/public.ts' import { seededRoles } from './company-roles.ts' import { TEST_ROLES, testIdentity } from './test-kit.ts' @@ -40,6 +41,7 @@ export const CARLA = { name: 'Carla Dias', email: 'carla@example.test' } export const INVITEE = 'davi@example.test' type Change = (config: Config) => Config +type HubTuning = Pick /** A connector call's deadline in tests: a silent double is given up on quickly (criterion 11), and a Testar waiting on another's refresh still sees the double's refresh land. */ const TEST_CALL_DEADLINE_MS = 1_000 @@ -55,7 +57,7 @@ async function hubServing( mailApiUrl?: string, log: FailureLog = silent, change?: Change, - clock?: () => Date, + tuning: HubTuning = {}, ) { // Registered before the database's own cleanup, so it runs first (node:test runs them in order): // the Builder's turns drain and end their streams while the server closes, as at shutdown. @@ -80,7 +82,7 @@ async function hubServing( const subjects = subjectsOf({ ...{ database: test.database, mastraPool: test.mastraPool, config, identity, sendMail }, ...{ ...ports, log }, - tuning: { callDeadlineMs: TEST_CALL_DEADLINE_MS, now: clock }, + tuning: { callDeadlineMs: TEST_CALL_DEADLINE_MS, now: tuning.now, catalog: tuning.catalog }, pruneBudgetMs: TEST_PRUNE_BUDGET_MS, appDataDeadlineMs: TEST_CALL_DEADLINE_MS, }) @@ -140,14 +142,17 @@ export function hubOn(t: TestContext) { export type Hub = Awaited> export type MailHub = Hub & { mail: Awaited> } -/** A Hub logging into `lines`, fields and message; `change` sets its config apart from the test one, and `clock` dates the connectors' writes. */ -export async function hubWithLog(t: TestContext, change?: Change, clock?: () => Date) { +/** + * A Hub logging into `lines`, fields and message; `change` sets its config apart from the test one, + * `tuning.now` dates the connectors' writes and cursors, and `tuning.catalog` adds a connector of the test's own. + */ +export async function hubWithLog(t: TestContext, change?: Change, tuning?: HubTuning) { const lines: { fields: Record; message: string }[] = [] const log = { info: (fields: Record, message: string) => lines.push({ fields, message }), error: (fields: Record, message: string) => lines.push({ fields, message }), } - return { ...(await hubServing(t, undefined, log, change, clock)), lines } + return { ...(await hubServing(t, undefined, log, change, tuning)), lines } } /** A Hub whose mail goes to a fake of the provider. */ diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 21449c3e4..bc337ccec 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -193,6 +193,7 @@ export function subjectsOf(deps: { const connectors = createConnectors({ ...{ ...deps, identity, door: apps.connectorCall }, propose: () => copies.propose, + copies: () => copies.ofConnections, }) const copies = createCopies({ ...{ database, identity, neon, log, companyPostgres: deps.companyPostgres }, diff --git a/apps/server/src/platform/company-postgres.ts b/apps/server/src/platform/company-postgres.ts index 92acbff48..efce635e6 100644 --- a/apps/server/src/platform/company-postgres.ts +++ b/apps/server/src/platform/company-postgres.ts @@ -10,9 +10,13 @@ import type { Context, Row, SqlAnswer, SqlRead, SqlStatement, SqlTransport } fro import { existingRoles, LEDGER, missingLogins, quoted, REPLY_CAP_BYTES } from './app-sql.ts' import { cappedText } from './capped-text.ts' import type { AppLoginKey } from './config.ts' -import type { CopyRows, CopyTable } from './copy-sql.ts' +import type { CopyTable } from './copy-sql.ts' import { + accountRows, closePass, + dropAccountRows, + dropCopyTable, + dropForeignRows, ensureCopyTable, ensureSync, grantCopy, @@ -150,6 +154,11 @@ type MigrateAnswer = | { kind: 'REFUSED_BY_RULE'; rule: RuleBreak } | Extract +/** A copy act of `copy-sql.ts` with the context already given. */ +type Bound = F extends (context: Context, ...rest: infer A) => infer R + ? (...rest: A) => R + : never + export type CompanyPostgres = { ensureApp(owner: DatabaseUrl, projectId: ProjectId): Promise /** The read login of an app made before it existed, or whose creation a cut left (step 24.3, U3). */ @@ -174,18 +183,18 @@ export type CompanyPostgres = { ensureCopyTable(sync: DatabaseUrl, table: CopyTable): Promise /** Where the Hub reaches the sync login, from the host it recorded, with no call to Neon. */ syncAddress(host: string): Promise - /** One page in one transaction, each row with the pass. */ - writePage( - sync: DatabaseUrl, - input: CopyTable & { pass: number; rows: CopyRows }, - signal: AbortSignal, - ): Promise + /** One page in one transaction, each row with the pass and, in a copy of accounts, its account. */ + writePage: Bound /** The end of a complete pass: what it did not see leaves, then the count. */ - closePass( - sync: DatabaseUrl, - input: CopyTable & { pass: number; windowFrom: string | null }, - signal: AbortSignal, - ): Promise<{ kind: 'DONE'; rows: number } | Exclude> + closePass: Bound + /** Desligar: an account's rows out of a copy that goes on; repeatable. */ + dropAccountRows: Bound + /** The run's net: the rows of an account that is no source's leave. */ + dropForeignRows: Bound + /** One account's rows in a copy of accounts. */ + accountRows: Bound + /** The table of a copy that ended, with the app logins' read of it; repeatable. */ + dropCopyTable: Bound /** A copy released to a Projeto's run and read logins. */ grantCopy(sync: DatabaseUrl, input: { name: string; projectId: ProjectId }): Promise /** A release taken back; `ALL`, every one of the Projeto and the schema. */ @@ -354,6 +363,10 @@ export function createCompanyPostgres(deps: { syncAddress: (host) => syncAddress(context, host), writePage: (sync, input, signal) => writePage(context, sync, input, signal), closePass: (sync, input, signal) => closePass(context, sync, input, signal), + dropAccountRows: (sync, input) => dropAccountRows(context, sync, input), + dropForeignRows: (sync, input, signal) => dropForeignRows(context, sync, input, signal), + accountRows: (sync, input) => accountRows(context, sync, input), + dropCopyTable: (sync, input) => dropCopyTable(context, sync, input), grantCopy: (sync, input) => grantCopy(context, sync, input), revokeCopy: (sync, input) => revokeCopy(context, sync, input), }) diff --git a/apps/server/src/platform/copy-sql.test.ts b/apps/server/src/platform/copy-sql.test.ts new file mode 100644 index 000000000..06fae2bcc --- /dev/null +++ b/apps/server/src/platform/copy-sql.test.ts @@ -0,0 +1,206 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import pg from 'pg' +import type { CopyTable } from './copy-sql.ts' +import { testAppsConfig } from './test-cloudflare.ts' +import { createCompanyDatabase, syncLoginOf, testCompanyPostgres } from './test-company-postgres.ts' + +type DatabaseUrl = Awaited> + +const KEY = testAppsConfig().appLoginKey +const postgres = testCompanyPostgres(KEY) +const signal = new AbortController().signal + +function syncOf(owner: DatabaseUrl) { + return syncLoginOf(owner, KEY) +} + +async function asSync(sync: DatabaseUrl, text: string) { + const client = new pg.Client({ connectionString: sync.reveal() }) + await client.connect() + try { + return (await client.query(text)).rows + } finally { + await client.end() + } +} + +const PEDIDOS: CopyTable = { + name: 'pedidos', + definition: { + columns: [ + { name: 'id', type: 'text', description: 'O número do pedido' }, + { name: 'itens', type: 'json', description: 'Os itens, como vieram' }, + { name: 'criado', type: 'timestamp' }, + ], + key: ['id'], + changedSince: null, + description: 'Os pedidos de cada loja', + }, + accounts: true, + instants: 'OFFSET', +} + +test('json, chave de texto e descrições', async (t) => { + const owner = await createCompanyDatabase(t) + assert.equal((await postgres.ensureSync(owner)).kind, 'DONE') + const sync = await syncOf(owner) + assert.equal((await postgres.ensureCopyTable(sync, PEDIDOS)).kind, 'DONE') + assert.equal((await postgres.ensureCopyTable(sync, PEDIDOS)).kind, 'DONE', 'repetível') + const rows = [['MLB1', '[{"quantidade":1},{"quantidade":2}]', '2026-11-10T10:00:00-03:00']] + const loja = { ref: '42', name: 'Loja Centro' } + const written = await postgres.writePage( + sync, + { ...PEDIDOS, pass: 0, rows, account: loja }, + signal, + ) + assert.equal(written.kind, 'DONE') + + const [row] = await asSync( + sync, + `select id, _account, _account_name, + (select count(*) from jsonb_array_elements(itens))::int as itens, + criado = '2026-11-10T13:00:00Z'::timestamptz as instante + from sync.pedidos`, + ) + assert.deepEqual(row, { + id: 'MLB1', + _account: '42', + _account_name: 'Loja Centro', + itens: 2, + instante: true, + }) + const types = await asSync( + sync, + `select attname, format_type(atttypid, atttypmod) as type from pg_attribute + where attrelid = 'sync.pedidos'::regclass and attnum > 0 order by attnum`, + ) + assert.deepEqual(Object.fromEntries(types.map((each) => [each.attname, each.type])), { + id: 'text', + itens: 'jsonb', + criado: 'timestamp with time zone', + _account: 'text', + _account_name: 'text', + _pass: 'integer', + }) + const [key] = await asSync( + sync, + `select pg_get_constraintdef(oid) as key from pg_constraint + where conrelid = 'sync.pedidos'::regclass and contype = 'p'`, + ) + assert.equal(key?.key, 'PRIMARY KEY (_account, id)') + const [described] = await asSync( + sync, + `select obj_description('sync.pedidos'::regclass, 'pg_class') as tabela, + col_description('sync.pedidos'::regclass, 1) as id, + col_description('sync.pedidos'::regclass, 2) as itens, + col_description('sync.pedidos'::regclass, 3) as criado`, + ) + assert.deepEqual(described, { + tabela: 'Os pedidos de cada loja', + id: 'O número do pedido', + itens: 'Os itens, como vieram', + criado: null, + }) +}) + +test('uma descrição com aspas e ponto e vírgula fica como texto', async (t) => { + const owner = await createCompanyDatabase(t) + await postgres.ensureSync(owner) + const sync = await syncOf(owner) + const said = "Pedidos'; DROP TABLE sync.pedidos; --" + const table: CopyTable = { ...PEDIDOS, definition: { ...PEDIDOS.definition, description: said } } + assert.equal((await postgres.ensureCopyTable(sync, table)).kind, 'DONE') + const [described] = await asSync( + sync, + `select obj_description('sync.pedidos'::regclass, 'pg_class') as tabela`, + ) + assert.equal(described?.tabela, said) +}) + +test('o fim da passada de uma conta não toca a outra, e o nome da conta segue a conexão', async (t) => { + const owner = await createCompanyDatabase(t) + await postgres.ensureSync(owner) + const sync = await syncOf(owner) + await postgres.ensureCopyTable(sync, PEDIDOS) + const a = { ref: 'A', name: 'Loja A' } + const b = { ref: 'B', name: 'Loja B' } + function row(id: string) { + return [id, null, null] + } + await postgres.writePage( + sync, + { ...PEDIDOS, pass: 0, rows: [row('1'), row('2')], account: a }, + signal, + ) + await postgres.writePage( + sync, + { ...PEDIDOS, pass: 0, rows: [row('1'), row('3')], account: b }, + signal, + ) + await postgres.writePage(sync, { ...PEDIDOS, pass: 1, rows: [row('1')], account: a }, signal) + const renamed = { ref: 'A', name: 'Loja A nova' } + const closed = await postgres.closePass( + sync, + { ...PEDIDOS, pass: 1, windowFrom: null, account: renamed }, + signal, + ) + assert.deepEqual(closed, { kind: 'DONE', rows: 3 }) + assert.deepEqual( + await asSync(sync, 'select _account, id, _account_name from sync.pedidos order by 1, 2'), + [ + { _account: 'A', id: '1', _account_name: 'Loja A nova' }, + { _account: 'B', id: '1', _account_name: 'Loja B' }, + { _account: 'B', id: '3', _account_name: 'Loja B' }, + ], + ) + assert.deepEqual(await postgres.accountRows(sync, { copy: 'pedidos', ref: 'B' }), { + kind: 'DONE', + rows: 2, + }) + assert.equal((await postgres.dropAccountRows(sync, { copy: 'pedidos', ref: 'B' })).kind, 'DONE') + assert.equal((await postgres.dropAccountRows(sync, { copy: 'pedidos', ref: 'B' })).kind, 'DONE') + const net = await postgres.dropForeignRows(sync, { copy: 'pedidos', refs: ['B'] }, signal) + assert.equal(net.kind, 'DONE') + assert.deepEqual(await asSync(sync, 'select count(*)::int as n from sync.pedidos'), [{ n: 0 }]) + assert.equal((await postgres.dropCopyTable(sync, { copy: 'pedidos' })).kind, 'DONE') + assert.equal((await postgres.dropCopyTable(sync, { copy: 'pedidos' })).kind, 'DONE', 'repetível') + assert.equal( + (await postgres.dropAccountRows(sync, { copy: 'pedidos', ref: 'A' })).kind, + 'DONE', + 'a tabela que já saiu não tem linha a tirar', + ) +}) + +test('a versão 1 guarda a hora local, sem as colunas da conta', async (t) => { + const owner = await createCompanyDatabase(t) + await postgres.ensureSync(owner) + const sync = await syncOf(owner) + const table: CopyTable = { + name: 'vendas', + definition: { + columns: [ + { name: 'NUNOTA', type: 'bigint' }, + { name: 'DTNEG', type: 'timestamp' }, + ], + key: ['NUNOTA'], + changedSince: null, + }, + accounts: false, + instants: 'LOCAL', + } + assert.equal((await postgres.ensureCopyTable(sync, table)).kind, 'DONE') + const types = await asSync( + sync, + `select attname, format_type(atttypid, atttypmod) as type from pg_attribute + where attrelid = 'sync.vendas'::regclass and attnum > 0 order by attnum`, + ) + assert.deepEqual( + types.map((each) => [each.attname, each.type]), + [ + ['NUNOTA', 'bigint'], + ['DTNEG', 'timestamp without time zone'], + ['_pass', 'integer'], + ], + ) +}) diff --git a/apps/server/src/platform/copy-sql.ts b/apps/server/src/platform/copy-sql.ts index 404f2eadb..45a25778d 100644 --- a/apps/server/src/platform/copy-sql.ts +++ b/apps/server/src/platform/copy-sql.ts @@ -10,6 +10,9 @@ export const SYNC_LOGIN = 'conexus_sync' const SYNC_SCHEMA = quoted('sync') /** The column that holds the pass that last wrote each row: the engine's own. Only the Hub's catalog leaves it out; a read of the table itself still returns it (issue 946). */ export const COPY_PASS_COLUMN = '_pass' +/** The columns of a copy of accounts, the engine's own beside `_pass`: the provider's account and the company's name for its connection. */ +const COPY_ACCOUNT_COLUMN = '_account' +const COPY_ACCOUNT_NAME_COLUMN = '_account_name' /** * What the company's database keeps of a copy, as the kit's parsed `CopyDefinition` gives it: the @@ -18,14 +21,21 @@ export const COPY_PASS_COLUMN = '_pass' export type CopyTable = Readonly<{ name: string definition: Readonly<{ - columns: readonly Readonly<{ name: string; type: CopyColumnType }>[] + columns: readonly Readonly<{ name: string; type: CopyColumnType; description?: string }>[] key: readonly string[] changedSince: Readonly<{ column: string }> | null window?: Readonly<{ column: string }> + description?: string }> + /** The connector's account is not `none`: each row carries `_account` and `_account_name`, and the key starts with `_account`. */ + accounts: boolean + /** OFFSET (version 2): a timestamp is `timestamptz`, a provider's instant with its offset; LOCAL (version 1) keeps a database's local hour. */ + instants: 'LOCAL' | 'OFFSET' }> -type CopyColumnType = 'text' | 'numeric' | 'bigint' | 'date' | 'timestamp' | 'boolean' +type CopyColumnType = 'text' | 'numeric' | 'bigint' | 'date' | 'timestamp' | 'boolean' | 'json' export type CopyRows = readonly (readonly (string | null)[])[] +/** The account a page or a pass belongs to, in a copy of accounts; null in one whose connector has none. */ +export type CopyAccount = Readonly<{ ref: string; name: string }> | null const SQL_TYPE: Readonly> = { text: 'text', @@ -34,6 +44,47 @@ const SQL_TYPE: Readonly> = { date: 'date', timestamp: 'timestamp', boolean: 'boolean', + json: 'jsonb', +} + +function sqlTypeOf(table: CopyTable, type: CopyColumnType): string { + return type === 'timestamp' && table.instants === 'OFFSET' ? 'timestamptz' : SQL_TYPE[type] +} + +function keyOf(table: CopyTable): string[] { + const key = table.definition.key.map(quoted) + return table.accounts ? [COPY_ACCOUNT_COLUMN, ...key] : key +} + +/** + * The descriptions as `COMMENT ON`, whose text cannot be a bound parameter: the descriptions travel + * bound in a setting of the transaction, and `format('%L')` quotes them (data D20). + */ +function commentsOf(table: CopyTable): SqlStatement[] { + const columns = Object.fromEntries( + table.definition.columns.flatMap((c) => (c.description ? [[c.name, c.description]] : [])), + ) + const described = table.definition.description + if (described === undefined && Object.keys(columns).length === 0) return [] + const comments = { name: table.name, table: described ?? null, columns } + return [ + { + text: `SELECT set_config('conexus.copy_comments', $1, true)`, + values: [JSON.stringify(comments)], + }, + { + text: `DO $$ + DECLARE c jsonb := current_setting('conexus.copy_comments')::jsonb; e record; + BEGIN + IF c->>'table' IS NOT NULL THEN + EXECUTE format('COMMENT ON TABLE sync.%I IS %L', c->>'name', c->>'table'); + END IF; + FOR e IN SELECT * FROM jsonb_each_text(c->'columns') LOOP + EXECUTE format('COMMENT ON COLUMN sync.%I.%I IS %L', c->>'name', e.key, e.value); + END LOOP; + END $$`, + }, + ] } function tableOf(name: string): string { @@ -81,18 +132,27 @@ export async function syncAddress(context: Context, host: string): Promise { - const columns = definition.columns.map((c) => `${quoted(c.name)} ${SQL_TYPE[c.type]}`) + const { name, definition } = table + const columns = definition.columns.map((c) => `${quoted(c.name)} ${sqlTypeOf(table, c.type)}`) + const accounts = table.accounts + ? [`${COPY_ACCOUNT_COLUMN} text NOT NULL`, `${COPY_ACCOUNT_NAME_COLUMN} text NOT NULL`] + : [] const statements: SqlStatement[] = [ { - text: `CREATE TABLE IF NOT EXISTS ${tableOf(name)} (${columns.join(', ')}, - ${COPY_PASS_COLUMN} integer NOT NULL, PRIMARY KEY (${definition.key.map(quoted).join(', ')}))`, + text: `CREATE TABLE IF NOT EXISTS ${tableOf(name)} (${[...columns, ...accounts].join(', ')}, + ${COPY_PASS_COLUMN} integer NOT NULL, PRIMARY KEY (${keyOf(table).join(', ')}))`, }, + ...commentsOf(table), ] if (definition.window !== undefined) { statements.push({ @@ -104,73 +164,170 @@ export function ensureCopyTable( /** * One page in one transaction: the rows as a JSON array of objects read by `jsonb_to_recordset` - * with the copy's types, each written with the pass, a row of the same key written over. + * with the copy's types, each written with the pass (and, in a copy of accounts, the page's account + * and its name), a row of the same key written over. */ export async function writePage( context: Context, sync: DatabaseUrl, - input: CopyTable & { pass: number; rows: CopyRows }, + input: CopyTable & { pass: number; rows: CopyRows; account: CopyAccount }, signal: AbortSignal, ): Promise { if (input.rows.length === 0) return { kind: 'DONE', value: [] } const { columns, key } = input.definition const names = columns.map((c) => quoted(c.name)) - const record = columns.map((c) => `${quoted(c.name)} ${SQL_TYPE[c.type]}`) + // A json cell travels as its text and is cast in the SELECT: the database parses it, not the Hub. + const record = columns.map( + (c) => `${quoted(c.name)} ${c.type === 'json' ? 'text' : sqlTypeOf(input, c.type)}`, + ) + const picked = columns.map((c, at) => `r.${names[at]}${c.type === 'json' ? '::jsonb' : ''}`) const keys = new Set(key) + const own = input.account === null ? [] : [COPY_ACCOUNT_COLUMN, COPY_ACCOUNT_NAME_COLUMN] const updated = [ ...columns.filter((c) => !keys.has(c.name)).map((c) => quoted(c.name)), + ...own.slice(1), COPY_PASS_COLUMN, ].map((column) => `${column} = EXCLUDED.${column}`) const objects = input.rows.map((row) => Object.fromEntries(columns.map((c, at) => [c.name, row[at] ?? null])), ) + const accountValues = input.account === null ? [] : [input.account.ref, input.account.name] return context.transport( sync, [ { - text: `INSERT INTO ${tableOf(input.name)} (${names.join(', ')}, ${COPY_PASS_COLUMN}) - SELECT ${names.map((n) => `r.${n}`).join(', ')}, $2::integer + text: `INSERT INTO ${tableOf(input.name)} (${[...names, ...own, COPY_PASS_COLUMN].join(', ')}) + SELECT ${[...picked, ...own.map((_, at) => `$${at + 3}::text`)].join(', ')}, $2::integer FROM jsonb_to_recordset($1::jsonb) AS r(${record.join(', ')}) - ON CONFLICT (${key.map(quoted).join(', ')}) DO UPDATE SET ${updated.join(', ')}`, - values: [JSON.stringify(objects), input.pass], + ON CONFLICT (${keyOf(input).join(', ')}) DO UPDATE SET ${updated.join(', ')}`, + values: [JSON.stringify(objects), input.pass, ...accountValues], }, ], budget(signal), ) } +/** In a copy of accounts, what a pass's close touches is its own account's rows alone. */ +function ofAccount( + account: CopyAccount, + at: number, +): Readonly<{ where: string; values: string[] }> { + return account === null + ? { where: '', values: [] } + : { where: ` AND ${COPY_ACCOUNT_COLUMN} = $${at}`, values: [account.ref] } +} + /** - * The end of a complete pass: a copy with no change date leaves every row the pass did not write; - * one with a window, every row of the window the pass did not write; then the count. + * The end of a complete pass of one source: a copy with no change date leaves every row of the + * source's account the pass did not write; one with a window, every such row of the window; the + * account's rows take the connection's name as it is now; then the count of the whole table. */ export async function closePass( context: Context, sync: DatabaseUrl, - input: CopyTable & { pass: number; windowFrom: string | null }, + input: CopyTable & { pass: number; windowFrom: string | null; account: CopyAccount }, signal: AbortSignal, ): Promise<{ kind: 'DONE'; rows: number } | Exclude> { const { changedSince, window } = input.definition + const { account } = input const table = tableOf(input.name) + const whole = ofAccount(account, 2) + const windowed = ofAccount(account, 3) const gone: SqlStatement[] = changedSince === null - ? [{ text: `DELETE FROM ${table} WHERE ${COPY_PASS_COLUMN} <> $1`, values: [input.pass] }] + ? [ + { + text: `DELETE FROM ${table} WHERE ${COPY_PASS_COLUMN} <> $1${whole.where}`, + values: [input.pass, ...whole.values], + }, + ] : window !== undefined && input.windowFrom !== null ? [ { - text: `DELETE FROM ${table} WHERE ${quoted(window.column)} >= $1::date AND ${COPY_PASS_COLUMN} <> $2`, - values: [input.windowFrom, input.pass], + text: `DELETE FROM ${table} WHERE ${quoted(window.column)} >= $1::date AND ${COPY_PASS_COLUMN} <> $2${windowed.where}`, + values: [input.windowFrom, input.pass, ...windowed.values], }, ] : [] + const named: SqlStatement[] = + account === null + ? [] + : [ + { + text: `UPDATE ${table} SET ${COPY_ACCOUNT_NAME_COLUMN} = $2 + WHERE ${COPY_ACCOUNT_COLUMN} = $1 AND ${COPY_ACCOUNT_NAME_COLUMN} IS DISTINCT FROM $2`, + values: [account.ref, account.name], + }, + ] const answer = await context.transport( sync, - [...gone, { text: `SELECT count(*)::text AS n FROM ${table}` }], + [...gone, ...named, { text: `SELECT count(*)::text AS n FROM ${table}` }], budget(signal), ) if (answer.kind !== 'DONE') return answer return { kind: 'DONE', rows: Number(answer.value.at(-1)?.[0]?.n) } } +/** SQLSTATE of no such table: a copy's table already dropped, or never made. */ +const NO_TABLE = '42P01' + +/** One statement as the sync login, within the call's budget. */ +function sent(context: Context, sync: DatabaseUrl, statement: SqlStatement, signal?: AbortSignal) { + return context.transport(sync, [statement], budget(signal)) +} + +/** An answer that found no table: nothing left to take out. */ +function doneWithoutTable(answer: SqlAnswer): SqlAnswer { + return answer.kind === 'REFUSED' && answer.code === NO_TABLE + ? { kind: 'DONE', value: [] } + : answer +} + +/** Desligar: the account's rows out of a copy that goes on with other accounts; repeatable. */ +export async function dropAccountRows( + context: Context, + sync: DatabaseUrl, + input: { copy: string; ref: string }, +): Promise { + const text = `DELETE FROM ${tableOf(input.copy)} WHERE ${COPY_ACCOUNT_COLUMN} = $1` + return doneWithoutTable(await sent(context, sync, { text, values: [input.ref] })) +} + +/** + * The run's net under Desligar: the rows whose account is no source's leave, a page a run wrote while + * its connection was being removed; repeatable. + */ +export async function dropForeignRows( + context: Context, + sync: DatabaseUrl, + input: { copy: string; refs: readonly string[] }, + signal: AbortSignal, +): Promise { + const text = `DELETE FROM ${tableOf(input.copy)} WHERE ${COPY_ACCOUNT_COLUMN} <> ALL($1::text[])` + return sent(context, sync, { text, values: [input.refs] }, signal) +} + +/** The rows of one account in a copy of accounts, as Desligar tells them before the click. */ +export async function accountRows( + context: Context, + sync: DatabaseUrl, + input: { copy: string; ref: string }, +): Promise<{ kind: 'DONE'; rows: number } | Exclude> { + const text = `SELECT count(*)::text AS n FROM ${tableOf(input.copy)} WHERE ${COPY_ACCOUNT_COLUMN} = $1` + const answer = await sent(context, sync, { text, values: [input.ref] }) + if (answer.kind !== 'DONE') return answer + return { kind: 'DONE', rows: Number(answer.value.at(-1)?.[0]?.n) } +} + +/** The table of a copy that ended: the app logins' `SELECT` goes with it; repeatable. */ +export function dropCopyTable( + context: Context, + sync: DatabaseUrl, + input: { copy: string }, +): Promise { + return sent(context, sync, { text: `DROP TABLE IF EXISTS ${tableOf(input.copy)}` }) +} + /** What the sync login gives a Projeto's run and read logins, which its owner made first. */ export function grantCopy( context: Context, diff --git a/apps/server/src/platform/test-app-ports.ts b/apps/server/src/platform/test-app-ports.ts index c87f53385..829991fdb 100644 --- a/apps/server/src/platform/test-app-ports.ts +++ b/apps/server/src/platform/test-app-ports.ts @@ -75,6 +75,10 @@ export function heldCompanyPostgres(config: Config) { }, writePage: (sync, input, signal) => inner.writePage(sync, input, signal), closePass: (sync, input, signal) => inner.closePass(sync, input, signal), + dropAccountRows: (sync, input) => inner.dropAccountRows(sync, input), + dropForeignRows: (sync, input, signal) => inner.dropForeignRows(sync, input, signal), + accountRows: (sync, input) => inner.accountRows(sync, input), + dropCopyTable: (sync, input) => inner.dropCopyTable(sync, input), grantCopy: (sync, input) => inner.grantCopy(sync, input), revokeCopy: (sync, input) => inner.revokeCopy(sync, input), } diff --git a/apps/server/src/platform/test-company-postgres.ts b/apps/server/src/platform/test-company-postgres.ts index 1ee761ce2..4b600eb18 100644 --- a/apps/server/src/platform/test-company-postgres.ts +++ b/apps/server/src/platform/test-company-postgres.ts @@ -12,6 +12,7 @@ import type { import { answerOf, createCompanyPostgres, errorName } from './company-postgres.ts' import type { AppLoginKey } from './config.ts' import { readTestDatabase } from './config.ts' +import { SYNC_LOGIN } from './copy-sql.ts' import { DatabaseUrl } from './database.ts' import { loadMigrationRule } from './migration-rule.ts' import type { Lifetime } from './test-database.ts' @@ -315,3 +316,16 @@ export async function forgetReadLogin(owner: DatabaseUrl, projectId: ProjectId) await client.end() } } + +/** The Hub's acts on a company's database over the test server, with the test's app login key. */ +export function testCompanyPostgres(key: AppLoginKey) { + return createCompanyPostgres({ transport: pgTransport, appLoginKey: key }) +} + +/** The sync login's address on the company's database: the owner's, with the login and its password. */ +export async function syncLoginOf(owner: DatabaseUrl, key: AppLoginKey): Promise { + const url = new URL(owner.reveal()) + url.username = SYNC_LOGIN + url.password = await appLoginPassword({ key: key.reveal(), login: SYNC_LOGIN }) + return DatabaseUrl.parse(url.toString()) +} diff --git a/apps/web/src/hub/connectors/integrations-states.test.tsx b/apps/web/src/hub/connectors/integrations-states.test.tsx index e4637493a..e95c71c34 100644 --- a/apps/web/src/hub/connectors/integrations-states.test.tsx +++ b/apps/web/src/hub/connectors/integrations-states.test.tsx @@ -180,7 +180,7 @@ test('Desligar confirma pelo nome e volta à lista; Cancelar não manda nada', a test('Desligar que o fornecedor não desfez: a frase diz, e Fechar leva à lista', async () => { const connection = connectionOf({ name: 'Financeiro' }) open(viewOf({ connections: [connection] }), { - unlink: () => HttpResponse.json({ revocation: 'REFUSED' }), + unlink: () => HttpResponse.json({ revocation: 'REFUSED', tablesLeft: [] }), }) fireEvent.click(await screen.findByRole('button', { name: 'Mais ações' })) fireEvent.click(await screen.findByRole('menuitem', { name: 'Desligar' })) diff --git a/apps/web/src/hub/connectors/test-hub.tsx b/apps/web/src/hub/connectors/test-hub.tsx index a8906d551..2f4047207 100644 --- a/apps/web/src/hub/connectors/test-hub.tsx +++ b/apps/web/src/hub/connectors/test-hub.tsx @@ -135,7 +135,7 @@ function writes( const replaced = answers.unlink?.() if (replaced) return replaced view.connections = view.connections.filter((one) => one.id !== params.connectionId) - return HttpResponse.json({ revocation: 'NONE' }) + return HttpResponse.json({ revocation: 'NONE', tablesLeft: [] }) }), ] } diff --git a/apps/web/src/hub/copies/copy-list.test.tsx b/apps/web/src/hub/copies/copy-list.test.tsx index c1f436af2..79b98bd5c 100644 --- a/apps/web/src/hub/copies/copy-list.test.tsx +++ b/apps/web/src/hub/copies/copy-list.test.tsx @@ -83,6 +83,18 @@ test('Cópias: um erro mostra "Com erro" e a frase do código; pausada e sem rel expect(within(produtos).queryByText('30 min')).toBeNull() }) +test('a rodada que esperou', async () => { + openCopies( + server, + { copies: [copy({ run: copyRun({ outcome: { kind: 'PARTIAL', reason: 'WAIT' } }) })] }, + LIST, + ) + const vendas = await rowOf('Vendas') + expect( + within(vendas).getByText('Esperando o fornecedor; a próxima rodada continua de onde parou'), + ).toBeTruthy() +}) + test('Cópias: sem cópias, a frase de primeira vez e Ligar uma cópia', async () => { openCopies(server, { copies: [] }, LIST) expect( diff --git a/apps/web/src/hub/copies/text.ts b/apps/web/src/hub/copies/text.ts index 2e11b64b8..dc35ee4e3 100644 --- a/apps/web/src/hub/copies/text.ts +++ b/apps/web/src/hub/copies/text.ts @@ -68,6 +68,19 @@ export function stateOf(copy: Pick): CopyState return { label: 'Aguardando a próxima rodada', tone: 'neutral' } } +type PartialReason = Extract['reason'] + +function partialText(reason: PartialReason): string { + switch (reason) { + case 'CAP': + return 'Parcial; a próxima continua' + case 'WAIT': + return 'Esperando o fornecedor; a próxima rodada continua de onde parou' + case 'STOPPED': + return 'O Conexus reiniciou no meio; a próxima continua de onde parou' + } +} + /** How a run ended, in a sentence: a failure by its code's phrase (code C48), never "0 linhas". */ export function outcomeText(run: CopyRun): string { const { outcome } = run @@ -77,9 +90,7 @@ export function outcomeText(run: CopyRun): string { case 'COMPLETE': return 'Completa' case 'PARTIAL': - return outcome.reason === 'CAP' - ? 'Parcial; a próxima continua' - : 'O Conexus reiniciou no meio; a próxima continua de onde parou' + return partialText(outcome.reason) case 'INTERRUPTED': return 'O Conexus reiniciou no meio; a próxima continua de onde parou' case 'ERROR': diff --git a/docs/reference/boundaries.md b/docs/reference/boundaries.md index 973e0634e..b208487f6 100644 --- a/docs/reference/boundaries.md +++ b/docs/reference/boundaries.md @@ -1341,8 +1341,10 @@ moves the watermark or takes a row out. (`dist/docs/references/docs-workflows-workflow-state.md`); the schedule only knows when to fire (`dist/scheduler-Me4rCjO2.js:340-372`). Nothing in `@neondatabase/serverless` 1.2.0 or in PostgreSQL 18 knows what a source's last read reached. -- **Hand-made piece.** `position` (the last key written) and `watermark` (when the last complete pass - began) on `copies.copy`, with `passStartedAt` (`hub/copies/tables.ts`). `saved` writes the position +- **Hand-made piece.** `position` (where the read stopped: a key, an offset or the provider's cursor, + `CopyPosition` in `packages/connectors/src/kernel/copy.ts`) and `watermark` (when the last complete + pass began) on each source of the copy, `copies.copy_source`, with `pass_started_at` + (`hub/copies/tables.ts`). `saved` in `hub/copies/sources.ts` writes the position after each page; `passed` moves the watermark to the instant the pass began, only at the end of a complete one (`run.ts`). The paged read adds the dates to the guarded query (`packages/connectors/src/sankhya/copy-read.ts`): the watermark less a day and the window from the @@ -1404,8 +1406,66 @@ moves the watermark or takes a row out. (`KEPT_RUNS`, `run.ts`). - **Proof.** `run.test.ts` "a passada completa" and "o registro não leva dado" (only ids, counts, codes and milliseconds, never a query, a column read or a row). -- **Revisit when** a Mastra release records a manual run, or when a company turns off a connection - with copies: the foreign key's cascade removes the copy's rows and leaves its tables in `sync`. +- **Revisit when** a Mastra release records a manual run. + +### One source per account (O Sync de qualquer conector) + +- **Library line.** `@mastra/core` 1.82.0 keeps a workflow's state inside one run + (`dist/docs/references/docs-workflows-workflow-state.md`), and `drizzle-orm` 1.0.0-rc.4 only writes + what it is given; nothing installed keeps a read's point per connection of one definition. Nango + keeps one checkpoint per execution of one sync + (`nango@e6c6131` `docs/guides/functions/syncs/sync-partitioning.mdx:7-17`). +- **Hand-made piece.** `copies.copy_source` (`hub/copies/tables.ts`), one row per connection of a + copy with its account (`account_ref`), its pass, position, watermark and wait; the copy no longer + hangs from a connection. A version 2 copy's table carries `_account` and `_account_name` + (`platform/copy-sql.ts`); `closePass` deletes only the rows of the account whose pass ended, and + the run's net (`dropForeignRows`) the rows of an account that is no source's. Desligar takes the + account's rows out of each copy that goes on and ends, with its table, a copy whose last source it + was (`hub/copies/sources.ts`, `leaveCopies` and `dropEndedCopies`); a table the company's database + did not drop is noted in `copies.copy_table_left` and dropped on the company's next Desligar or + Ligar. +- **Why.** One definition, the state per connection, as Nango partitions a sync; the founder's + decision to remove the rows at once (#953). +- **Proof.** `hub/copies/run-v2.test.ts` "duas contas numa tabela" and "a conta desligada"; + `platform/copy-sql.test.ts` "o fim da passada de uma conta não toca a outra, e o nome da conta segue + a conexão"; `hub/connectors/unlink-copies.test.ts` "desligar tira as linhas e a leitura do app" and + its three cases; `hub/copies/migrate-copy-source.test.ts` "a fonte de cada cópia de hoje". + +### The provider's wait and the cursor that expires (O Sync de qualquer conector) + +- **Library line.** Nothing in `@mastra/core` 1.82.0 or `drizzle-orm` 1.0.0-rc.4 carries a wait or a + cursor from one run to the next. Airbyte and Nango wait on a 429 by its `Retry-After` and go on + (`airbyte@8aed3123` `connector-builder-ui/error-handling.md:15,107`; `nango@e6c6131` + `docs/guides/functions/rate-limits.mdx:15-38`). The Mercado Livre `scroll_id` lives 5 minutes. +- **Hand-made piece.** A 429 left after the executor's own wait inside the call's deadline (0058) + ends the run `PARTIAL` with the reason `WAIT`: the source keeps its position, and `wait_until` + when the provider said how long (`run.ts`, `sources.ts` `waiting`); the next run skips a source + still waiting. A list read declares its cursor's life in `paging.ttlMs`; a saved cursor older than + that (by the injected clock) is never sent: the source's pass starts again (`restarted`), logged + `COPY_PASS_RESTARTED`. +- **Why.** The platforms wait and resume; no one recorded how a provider refuses an expired cursor, + so the run does not depend on reading that refusal. +- **Proof.** `hub/copies/run-v2.test.ts` "o fornecedor pede para esperar" and its two cases, and "o + cursor vencido recomeça". + +### One copy call at a time per connector counted by the app (O Sync de qualquer conector) + +- **Library line.** No limiter is installed: `node_modules` has no `p-limit`, `p-queue` or + `bottleneck`, only `p-retry` 7.1.1 from another package, which repeats and does not limit. + Airbyte's `HTTPAPIBudget` limits one connection's calls per window + (`airbyte@8aed3123` `config-based/understanding-the-yaml-file/rate-limit-api-budget.md:3-30`); + nothing sums the calls of many companies' connections on one OAuth app. +- **Hand-made piece.** `callLimit` (`countedBy` `APP` or `ACCOUNT`) on the connector's declaration + (`packages/connectors/src/kernel/definition.ts`; Mercado Livre `APP`, Sankhya `ACCOUNT`), and + `inCopyTurn` in `hub/connectors/limits.ts`: the same in-process line as each connection's calls, + keyed by the connector, which `copy-port.ts` takes for every copy read of a connector that counts + by the app. A read whose deadline passes in the line asks nothing, and the run ends `PARTIAL` + `WAIT` with the position kept and no `wait_until`. No number is invented: Mercado Livre publishes + none. +- **Why.** Mercado Livre counts its limit "per Client ID (application)", one app for every company. +- **Proof.** `hub/connectors/copy-lane.test.ts` "uma chamada de cópia por vez por conector" and its + two cases; the first and third fail when `copy-port.ts` skips the line. +- **Revisit when** the Hub runs in more than one process (0058). ### The clock of each copy diff --git a/packages/connectors/src/index.ts b/packages/connectors/src/index.ts index fdec834a1..c8c6d08bd 100644 --- a/packages/connectors/src/index.ts +++ b/packages/connectors/src/index.ts @@ -30,6 +30,7 @@ export type { export { CANONICAL_CELL, CopyDefinition, + CopyDefinitionV1, CopyDefinitionV2, CopyName, CopyPageSchema, @@ -68,7 +69,7 @@ export type { } from './kernel/operation.ts' export { AccountRef, deniesAccess, locateOf, requestOf } from './kernel/operation.ts' export { toRequest } from './kernel/request.ts' -export { CONCURRENT_CALL } from './kernel/rows.ts' +export { CONCURRENT_CALL, Rows } from './kernel/rows.ts' export { mercadoLivreFake } from './mercado-livre/fake.ts' export { sankhyaFake } from './sankhya/fake.ts' export type { SalesSet } from './sankhya/fake-sales.ts' diff --git a/packages/connectors/src/kernel/copy.test.ts b/packages/connectors/src/kernel/copy.test.ts index 70c4f4ac4..7a52df4c7 100644 --- a/packages/connectors/src/kernel/copy.test.ts +++ b/packages/connectors/src/kernel/copy.test.ts @@ -6,6 +6,7 @@ import type { ConnectorCopy, CopyModel } from './copy.ts' import { CANONICAL_CELL, CopyDefinition, + CopyDefinitionV1, CopyDefinitionV2, CopyName, CopyPosition, @@ -131,7 +132,7 @@ const copyRead = defineOperation({ const MODEL: CopyModel = { id: CopyName.parse('modelo'), label: 'Modelo', - definition: CopyDefinition.parse(DEFINITION), + definition: CopyDefinitionV1.parse(DEFINITION), } /** A copy the types would refuse, to meet the run-time checks of `defineConnector`. */ @@ -251,7 +252,7 @@ function versionsAndPositions() { false, 'json não é da versão 1', ) - assert.equal(CopyDefinition.safeParse(V2).success, false, 'a versão 2 não passa pela 1') + assert.equal(CopyDefinitionV1.safeParse(V2).success, false, 'a versão 2 não passa pela 1') for (const [kind, cell] of [ ['timestamp', '2026-11-10T12:30:45.000-03:00'], @@ -284,3 +285,23 @@ test('a versão 2', () => { acceptsAndRefusesV2() versionsAndPositions() }) + +test('as duas versões', () => { + const v1 = CopyDefinition.parse(DEFINITION) + assert.equal(v1.version, 1) + assert.deepEqual(v1, CopyDefinitionV1.parse(DEFINITION), 'a versão 1 de hoje lida igual') + const v2 = CopyDefinition.parse(V2) + assert.equal(v2.version, 2) + assert.deepEqual(v2, CopyDefinitionV2.parse(V2)) + assert.equal(CopyDefinition.safeParse({ ...V2, version: 3 }).success, false, 'versão 3') + assert.equal( + CopyDefinition.safeParse({ ...V2, window: { column: 'x', days: 1 } }).success, + false, + 'a janela não é da versão 2', + ) + assert.equal( + CopyDefinition.safeParse({ ...DEFINITION, key: ['NOME'] }).success, + false, + 'as regras da versão 1 valem na união', + ) +}) diff --git a/packages/connectors/src/kernel/copy.ts b/packages/connectors/src/kernel/copy.ts index 018c3ba42..9c3b8f161 100644 --- a/packages/connectors/src/kernel/copy.ts +++ b/packages/connectors/src/kernel/copy.ts @@ -53,7 +53,7 @@ function datedColumn( } /** What a copy reads, the company's data from the moment it is turned on (version 1, one kind of source). */ -export const CopyDefinition = z +export const CopyDefinitionV1 = z .object({ version: z.literal(1), // No date filter in it: the paged read adds the dates of each pass. @@ -80,7 +80,7 @@ export const CopyDefinition = z for (const field of ['changedSince', 'window'] as const) datedColumn(field, definition[field]?.column, types, context) }) -export type CopyDefinition = z.infer +export type CopyDefinitionV1 = z.infer /** Version 2 adds `json`, the cell of a nested value as its JSON text. */ export const CopyColumnTypeV2 = z.enum([...CopyColumnType.options, 'json']) @@ -158,6 +158,10 @@ export const CopyDefinitionV2 = z }) export type CopyDefinitionV2 = z.infer +/** Either version, told apart by `version`: what a copy's and a proposal's row hold, parsed again where read (code C27). */ +export const CopyDefinition = z.discriminatedUnion('version', [CopyDefinitionV1, CopyDefinitionV2]) +export type CopyDefinition = z.infer + /** How far a pass has read, as the source's own paging gives it: after a key (version 1), at an offset, or at a cursor and the instant it was given. */ export const CopyPosition = z.discriminatedUnion('kind', [ z.object({ kind: z.literal('KEY'), after: z.array(z.int()) }), @@ -171,12 +175,12 @@ export const CopyPosition = z.discriminatedUnion('kind', [ export type CopyPosition = z.infer /** A starting point a connector offers; turning a copy on copies `definition` into the company's row. */ -export type CopyModel = Readonly<{ id: CopyName; label: string; definition: CopyDefinition }> +export type CopyModel = Readonly<{ id: CopyName; label: string; definition: CopyDefinitionV1 }> /** One page: the definition, the last key read (none on a pass's first page), the two dates of the pass, and the size. */ export const PageInput = z .object({ - definition: CopyDefinition, + definition: CopyDefinitionV1, // a connector's own paged read serves version 1 alone after: z.array(z.int()).nullable(), // one per key column since: z.iso.date().nullable(), // the watermark less one day; null on the first pass, which reads all (#940) windowFrom: z.iso.date().nullable(), // the pass's start less the window's days diff --git a/packages/connectors/src/kernel/definition.ts b/packages/connectors/src/kernel/definition.ts index 969788b6f..abc254b17 100644 --- a/packages/connectors/src/kernel/definition.ts +++ b/packages/connectors/src/kernel/definition.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import type { Auth } from './auth.ts' import type { ConnectorCopy } from './copy.ts' -import { CopyDefinition } from './copy.ts' +import { CopyDefinitionV1 } from './copy.ts' import type { AccountOf, Failure, Operation, OperationId, WriteOperation } from './operation.ts' export const ConnectorId = z @@ -78,7 +78,7 @@ const Origin = z URL.canParse(value) && new URL(value).origin === value && value.startsWith('https://'), ) -/** Takes `id` as a plain string and brands it; throws on a `perProject` other than 'one' or 'many', a reported account with a login that is not OAuth, a repeated operation id, a bad version or origin, a door entry that is not 'read', a copy read that is not 'read' or repeats an operation id, a repeated copy template id or a template the `CopyDefinition` refuses, a list read in a connector with no `callLimit`, a write that is not 'write' or repeats an id, an operation of a subject the connector lacks, a subject with no operation, a `from` that is not an earlier operation, or a check that is not one of its operations or needs an earlier one. */ +/** Takes `id` as a plain string and brands it; throws on a `perProject` other than 'one' or 'many', a reported account with a login that is not OAuth, a repeated operation id, a bad version or origin, a door entry that is not 'read', a copy read that is not 'read' or repeats an operation id, a repeated copy template id or a template the `CopyDefinitionV1` refuses, a list read in a connector with no `callLimit`, a write that is not 'write' or repeats an id, an operation of a subject the connector lacks, a subject with no operation, a `from` that is not an earlier operation, or a check that is not one of its operations or needs an earlier one. */ export function defineConnector( connector: Omit & { id: string; auth: A }, ): Connector & Readonly<{ auth: A }> { @@ -135,7 +135,7 @@ function assertCopy(copy: ConnectorCopy | undefined, ids: ReadonlySet): if (ids.has(copy.read.id)) throw new Error(`Copy read repeats operation id ${copy.read.id}`) const models = copy.models.map((model) => model.id) if (new Set(models).size !== models.length) throw new Error('Repeated copy template id') - for (const model of copy.models) CopyDefinition.parse(model.definition) + for (const model of copy.models) CopyDefinitionV1.parse(model.definition) } function assertCheckStandsAlone(connector: Pick): void { diff --git a/packages/connectors/src/mercado-livre/fake-data.ts b/packages/connectors/src/mercado-livre/fake-data.ts index d9992b673..99039e836 100644 --- a/packages/connectors/src/mercado-livre/fake-data.ts +++ b/packages/connectors/src/mercado-livre/fake-data.ts @@ -9,9 +9,10 @@ export const REDIRECT_URI = 'https://hub.exemplo.test/integrations/return' export const SELLER = Object.freeze({ ref: '123456789', label: 'PADARIASOL' }) +/** More than the 1,000 an offset search reaches: only a scan reads them all. */ export const ITEMS = Array.from( - { length: 12 }, - (_, index) => `MLB10000000${String(index + 1).padStart(2, '0')}`, + { length: 1300 }, + (_, index) => `MLB1000000${String(index + 1).padStart(4, '0')}`, ) export const ORDER = 2000000000001 export const PACK = 2000000000099 @@ -49,6 +50,33 @@ export const ORDER_BODY = { ], } +/** 120 orders, the first `ORDER_BODY`; each changed a minute after the one before, so a changed-since filter keeps the newest. */ +export const ORDERS = Array.from({ length: 120 }, (_, index) => ({ + ...ORDER_BODY, + id: ORDER + index, + date_last_updated: new Date(Date.UTC(2026, 8, 10, 13, index)).toISOString(), +})) + +/** The rows of a scan's page: its `scroll_id` carries where the next page starts (`-`). */ +export function scanOf(rows: readonly T[], query: URLSearchParams, route: string) { + const scroll = query.get('scroll_id') + const offset = scroll === null ? 0 : Number(scroll.slice(route.length + 1)) + const limit = Number(query.get('limit') ?? 50) + const page = rows.slice(offset, offset + limit) + return { rows: page, scrollId: `${route}-${offset + page.length}`, total: rows.length, limit } +} + +/** A scan's cursor the double never gave. */ +export function badScroll(query: URLSearchParams, route: string): boolean { + const scroll = query.get('scroll_id') + return scroll !== null && !new RegExp(`^${route}-\\d+$`).test(scroll) +} + +export const BAD_SCROLL = { + status: 400, + body: { message: 'invalid scroll_id', error: 'bad_request', status: 400 }, +} + export function itemBody(id: string, index: number) { return { id, diff --git a/packages/connectors/src/mercado-livre/fake-routes-more.ts b/packages/connectors/src/mercado-livre/fake-routes-more.ts index 56324cd09..65b301295 100644 --- a/packages/connectors/src/mercado-livre/fake-routes-more.ts +++ b/packages/connectors/src/mercado-livre/fake-routes-more.ts @@ -1,6 +1,8 @@ import type { Route } from './fake-data.ts' import { + BAD_SCROLL, BUYER, + badScroll, CLAIM, ITEMS, ORDER, @@ -9,6 +11,7 @@ import { paging, SELLER, SELLER_PATH, + scanOf, } from './fake-data.ts' const QUESTIONS = Array.from({ length: 12 }, (_, index) => ({ @@ -50,6 +53,18 @@ export const MORE_ROUTES: readonly Route[] = [ group: 'perguntas', pattern: /^\/questions\/search$/, answer: ({ query }) => { + if (query.get('search_type') === 'scan') { + if (badScroll(query, 'questions')) return BAD_SCROLL + const scan = scanOf(QUESTIONS, query, 'questions') + return { + body: { + total: scan.total, + limit: scan.limit, + questions: scan.rows, + scroll_id: scan.scrollId, + }, + } + } const page = pageOf(QUESTIONS, query, 50) return { body: { diff --git a/packages/connectors/src/mercado-livre/fake-routes.ts b/packages/connectors/src/mercado-livre/fake-routes.ts index 68b02c55d..d3f9cf232 100644 --- a/packages/connectors/src/mercado-livre/fake-routes.ts +++ b/packages/connectors/src/mercado-livre/fake-routes.ts @@ -1,15 +1,19 @@ import type { Route } from './fake-data.ts' import { + BAD_SCROLL, BUYER, + badScroll, ITEMS, itemBody, ORDER, ORDER_BODY, + ORDERS, pageOf, paging, SELLER, SELLER_PATH, SHIPMENT, + scanOf, } from './fake-data.ts' import { MORE_ROUTES } from './fake-routes-more.ts' @@ -20,6 +24,18 @@ export const ROUTES: readonly Route[] = [ group: 'anuncios', pattern: new RegExp(`^/users/${SELLER_PATH}/items/search$`), answer: ({ query }) => { + if (query.get('search_type') === 'scan') { + if (badScroll(query, 'items')) return BAD_SCROLL + const scan = scanOf(ITEMS, query, 'items') + return { + body: { + seller_id: SELLER.ref, + results: scan.rows, + scroll_id: scan.scrollId, + paging: { total: scan.total, limit: scan.limit }, + }, + } + } const page = pageOf(ITEMS, query, 50) return { body: { @@ -84,9 +100,21 @@ export const ROUTES: readonly Route[] = [ name: 'pedidos', group: 'vendas', pattern: /^\/orders\/search$/, - answer: ({ query }) => ({ - body: { results: [ORDER_BODY], paging: paging(1, query), sort: { id: 'date_desc' } }, - }), + answer: ({ query }) => { + const from = query.get('order.date_last_updated.from') + const changed = + from === null + ? ORDERS + : ORDERS.filter((order) => Date.parse(order.date_last_updated) >= Date.parse(from)) + const page = pageOf(changed, query, 50) + return { + body: { + results: page.rows, + paging: paging(page.total, query), + sort: { id: 'date_desc' }, + }, + } + }, }, { name: 'pedido', diff --git a/packages/connectors/src/mercado-livre/operations-list.test.ts b/packages/connectors/src/mercado-livre/operations-list.test.ts new file mode 100644 index 000000000..66756633d --- /dev/null +++ b/packages/connectors/src/mercado-livre/operations-list.test.ts @@ -0,0 +1,262 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { SENT_AT } from '../kernel/conformance-declared.ts' +import { answerOf } from '../kernel/conformance-session.ts' +import type { CopyDefinitionV2, CopyPosition } from '../kernel/copy.ts' +import { CopyDefinitionV2 as CopyDefinitionV2Schema } from '../kernel/copy.ts' +import { listInputOf, listPageOf } from '../kernel/list.ts' +import type { Operation } from '../kernel/operation.ts' +import { requestOf } from '../kernel/operation.ts' +import { toRequest } from '../kernel/request.ts' +import { Rows } from '../kernel/rows.ts' +import { mercadoLivre } from './definition.ts' +import { mercadoLivreFake } from './fake.ts' +import { mensagensDoPacote } from './operations/mensagens.ts' +import { ACCOUNT, authorize } from './test-session.ts' + +function scanned(results: unknown[], extra: Record) { + return operationOf('anuncios').answer({ status: 200, body: { results, ...extra } }) +} + +function operationOf(id: string) { + const found = mercadoLivre.operations.find((operation) => operation.id === id) + assert.ok(found, id) + return found +} + +test('pedidos pelo que mudou', () => { + const pedidos = operationOf('pedidos') + const changedFrom = '2026-11-09T10:00:00.000-03:00' + const query = requestOf(pedidos, { offset: 0, limit: 50, changedFrom }, ACCOUNT, SENT_AT).query + assert.equal(query['order.date_last_updated.from'], changedFrom) + assert.equal(query.offset, '0') + assert.equal(query.limit, '50') + assert.equal( + 'order.date_last_updated.from' in + requestOf(pedidos, { offset: 0, limit: 5 }, ACCOUNT, SENT_AT).query, + false, + 'sem a entrada, a leitura de hoje sai igual', + ) + assert.equal( + pedidos.input.safeParse({ offset: 0, limit: 5, changedFrom: '2026-11-09' }).success, + false, + ) +}) + +test('a varredura: a primeira página manda scan, as seguintes o scroll_id', () => { + for (const id of ['anuncios', 'perguntas']) { + const read = operationOf(id) + const first = requestOf(read, { scan: true, limit: 50 }, ACCOUNT, SENT_AT).query + assert.equal(first.search_type, 'scan', id) + assert.equal(first.scroll_id, undefined, id) + assert.equal(first.offset, undefined, id) + const next = requestOf(read, { scrollId: 'abc', limit: 50 }, ACCOUNT, SENT_AT).query + assert.equal(next.scroll_id, 'abc', id) + assert.equal(next.search_type, 'scan', id) + assert.equal( + 'search_type' in requestOf(read, { offset: 0, limit: 50 }, ACCOUNT, SENT_AT).query, + false, + `${id}: por deslocamento, como hoje`, + ) + assert.equal(read.input.safeParse({ scan: false, limit: 50 }).success, false, id) + assert.equal(read.input.safeParse({ scrollId: '', limit: 50 }).success, false, id) + } +}) + +test('o cursor da resposta de uma varredura vai em seguinte, e a página sem linhas é o fim', () => { + assert.deepEqual(scanned(['a', 'b'], { scroll_id: 'cursor-1', paging: { total: 9, limit: 2 } }), { + kind: 'OK', + value: { linhas: [{ id: 'a' }, { id: 'b' }], temMais: true, seguinte: 'cursor-1', total: 9 }, + }) + assert.deepEqual(scanned([], { scroll_id: 'cursor-2', paging: { total: 9, limit: 2 } }), { + kind: 'OK', + value: { linhas: [], temMais: false, total: 9 }, + }) + assert.deepEqual(scanned(['a'], { paging: { total: 1, offset: 0, limit: 5 } }), { + kind: 'OK', + value: { linhas: [{ id: 'a' }], temMais: false, total: 1 }, + }) +}) + +test('o total e o temMais saem das mesmas contagens: sem deslocamento só o total, e um total quebrado nenhum dos dois', () => { + assert.deepEqual(scanned(['a'], { paging: { total: 9 } }), { + kind: 'OK', + value: { linhas: [{ id: 'a' }], temMais: null, total: 9 }, + }) + assert.deepEqual(scanned(['a'], { paging: { total: 9.5, offset: 0, limit: 1 } }), { + kind: 'OK', + value: { linhas: [{ id: 'a' }], temMais: null }, + }) +}) + +/** Every page of a list read of the double, through the copy's own input and page: the cells of each row. */ +async function walked( + operation: Operation, + definition: CopyDefinitionV2, + since: string | null = null, +) { + const list = operation.list ?? assert.fail(`${operation.id} declares no list`) + const fake = mercadoLivreFake() + const granted = await authorize(fake) + if (granted.kind !== 'OK') return assert.fail('authorize') + const cells: (string | null)[][] = [] + let position: CopyPosition | null = null + for (let pages = 0; pages < 100; pages++) { + const input = operation.input.parse(listInputOf({ list, definition, position, since })) + const request = requestOf(operation, input, ACCOUNT, SENT_AT) + const answered = operation.answer( + await answerOf(await fake.handle(toRequest(mercadoLivre, request, granted.accessToken))), + ) + if (answered.kind !== 'OK') return assert.fail(`${operation.id}: ${answered.kind}`) + const rows = Rows.parse(answered.value) + const page = listPageOf({ list, definition, position, rows, at: SENT_AT }) + if (page.kind !== 'PAGE') return assert.fail(`${operation.id}: ${page.kind}`) + cells.push(...page.rows.map((row) => [...row])) + if (!page.more) return { cells, pages: pages + 1 } + position = page.next + } + return assert.fail(`${operation.id} never ends`) +} + +function definitionOf( + operation: Operation, + columns: { name: string; type: string; path: string }[], + changed: string | null = null, +) { + return CopyDefinitionV2Schema.parse({ + version: 2, + source: { kind: 'OPERATION', operation: operation.id, input: {} }, + description: 'Teste.', + columns, + key: [columns[0]?.name], + changedSince: changed === null ? null : { column: changed }, + }) +} + +test('pedidos declara a lista por deslocamento e o filtro do que mudou', async () => { + const pedidos = operationOf('pedidos') + assert.deepEqual(pedidos.list, { + paging: { kind: 'OFFSET', offset: 'offset', limit: 'limit', max: 50, ceiling: null }, + changed: { input: 'changedFrom', field: 'date_last_updated' }, + }) + const definition = definitionOf( + pedidos, + [ + { name: 'id', type: 'bigint', path: 'id' }, + { name: 'alterado', type: 'timestamp', path: 'date_last_updated' }, + ], + 'alterado', + ) + const all = await walked(pedidos, definition) + assert.equal(all.cells.length, 120, 'os 120 pedidos do dublê') + assert.equal(all.pages, 3) + const changed = await walked(pedidos, definition, '2026-09-10T14:50:00.000Z') + assert.deepEqual( + changed.cells.map((row) => row[1]), + all.cells + .map((row) => row[1]) + .filter((at) => Date.parse(at ?? '') >= Date.parse('2026-09-10T14:50:00.000Z')), + 'só o que mudou desde o instante', + ) + assert.equal(changed.cells.length, 10) +}) + +test('a varredura passa de mil', async () => { + for (const [id, max] of [ + ['anuncios', 100], + ['perguntas', 50], + ] as const) { + assert.deepEqual( + operationOf(id).list, + { + paging: { + kind: 'CURSOR', + cursor: 'scrollId', + limit: 'limit', + max, + start: { scan: true }, + ttlMs: 300_000, + }, + changed: null, + }, + id, + ) + } + const anuncios = operationOf('anuncios') + const all = await walked( + anuncios, + definitionOf(anuncios, [{ name: 'id', type: 'text', path: 'id' }]), + ) + assert.equal(all.cells.length, 1300, 'os 1.300 anúncios, passando de mil') + assert.equal(new Set(all.cells.map((row) => row[0])).size, 1300, 'nenhum repete') + const list = anuncios.list ?? assert.fail('list') + const definition = definitionOf(anuncios, [{ name: 'id', type: 'text', path: 'id' }]) + function pageOf(body: Record) { + const answered = scanned(['MLB1'], body) + if (answered.kind !== 'OK') return assert.fail(answered.kind) + return listPageOf({ + list, + definition, + position: null, + rows: Rows.parse(answered.value), + at: SENT_AT, + }) + } + assert.deepEqual( + pageOf({ paging: { total: 9, offset: 0, limit: 1 } }), + { kind: 'UNREADABLE', reason: 'NO_CURSOR' }, + 'linhas, mais por vir e sem scroll_id', + ) + assert.deepEqual( + pageOf({ paging: { total: 9, limit: 1 } }), + { kind: 'UNREADABLE', reason: 'NO_MORE_SIGNAL' }, + 'linhas sem scroll_id e sem deslocamento: nenhum sinal de fim', + ) +}) + +test('as outras listas por deslocamento, e as mensagens de um pacote sem lista', () => { + for (const [id, max] of [ + ['reclamacoes', 30], + ['periodosDeFaturamento', 12], + ] as const) + assert.deepEqual( + operationOf(id).list, + { + paging: { kind: 'OFFSET', offset: 'offset', limit: 'limit', max, ceiling: null }, + changed: null, + }, + id, + ) + assert.equal(mensagensDoPacote.list, undefined, 'pede o packId de um pacote') + const listed = mercadoLivre.operations.filter((operation) => operation.list !== undefined) + assert.deepEqual(listed.map((operation) => operation.id).sort(), [ + 'anuncios', + 'pedidos', + 'perguntas', + 'periodosDeFaturamento', + 'reclamacoes', + ]) +}) + +test('o comprador não chega à cópia', async () => { + const personal = ['maria.ficticia', '988776655', '52998224725', 'Fictícia', 'COMPRADORFICTICIO'] + for (const id of ['pedidos', 'perguntas']) { + const operation = operationOf(id) + const fields = ['id', 'buyer', 'from', 'status', 'order_items', 'text', 'date_created'] + const columns = [ + { name: 'id', type: 'bigint', path: 'id' }, + ...fields.slice(1).map((field) => ({ name: field, type: 'json', path: field })), + ] + const all = await walked(operation, definitionOf(operation, columns)) + assert.ok(all.cells.length > 0, id) + const text = JSON.stringify(all.cells) + for (const value of personal) assert.ok(!text.includes(value), `${id} copia ${value}`) + function at(name: string) { + return columns.findIndex((column) => column.name === name) + } + assert.ok( + all.cells.every((row) => row[at(id === 'pedidos' ? 'buyer' : 'from')] === null), + `${id}: o objeto cortado é nulo`, + ) + } +}) diff --git a/packages/connectors/src/mercado-livre/operations.test.ts b/packages/connectors/src/mercado-livre/operations.test.ts index 3f3c39e9f..ff36dc3fb 100644 --- a/packages/connectors/src/mercado-livre/operations.test.ts +++ b/packages/connectors/src/mercado-livre/operations.test.ts @@ -210,7 +210,7 @@ async function twoPages( } for (const [id, total] of [ - ['anuncios', 12], + ['anuncios', 1300], ['perguntas', 12], ['periodosDeFaturamento', 14], ] as const) { @@ -240,78 +240,3 @@ test('as visitas mandam as datas como a documentação mostra, só o dia', () => const probe = visitas.probe(new Map()) assert.ok(visitas.input.safeParse(probe).success) }) - -function scanned(results: unknown[], extra: Record) { - return operationOf('anuncios').answer({ status: 200, body: { results, ...extra } }) -} - -function operationOf(id: string) { - const found = mercadoLivre.operations.find((operation) => operation.id === id) - assert.ok(found, id) - return found -} - -test('pedidos pelo que mudou', () => { - const pedidos = operationOf('pedidos') - const changedFrom = '2026-11-09T10:00:00.000-03:00' - const query = requestOf(pedidos, { offset: 0, limit: 50, changedFrom }, ACCOUNT, SENT_AT).query - assert.equal(query['order.date_last_updated.from'], changedFrom) - assert.equal(query.offset, '0') - assert.equal(query.limit, '50') - assert.equal( - 'order.date_last_updated.from' in - requestOf(pedidos, { offset: 0, limit: 5 }, ACCOUNT, SENT_AT).query, - false, - 'sem a entrada, a leitura de hoje sai igual', - ) - assert.equal( - pedidos.input.safeParse({ offset: 0, limit: 5, changedFrom: '2026-11-09' }).success, - false, - ) -}) - -test('a varredura: a primeira página manda scan, as seguintes o scroll_id', () => { - for (const id of ['anuncios', 'perguntas']) { - const read = operationOf(id) - const first = requestOf(read, { scan: true, limit: 50 }, ACCOUNT, SENT_AT).query - assert.equal(first.search_type, 'scan', id) - assert.equal(first.scroll_id, undefined, id) - assert.equal(first.offset, undefined, id) - const next = requestOf(read, { scrollId: 'abc', limit: 50 }, ACCOUNT, SENT_AT).query - assert.equal(next.scroll_id, 'abc', id) - assert.equal(next.search_type, 'scan', id) - assert.equal( - 'search_type' in requestOf(read, { offset: 0, limit: 50 }, ACCOUNT, SENT_AT).query, - false, - `${id}: por deslocamento, como hoje`, - ) - assert.equal(read.input.safeParse({ scan: false, limit: 50 }).success, false, id) - assert.equal(read.input.safeParse({ scrollId: '', limit: 50 }).success, false, id) - } -}) - -test('o cursor da resposta de uma varredura vai em seguinte, e a página sem linhas é o fim', () => { - assert.deepEqual(scanned(['a', 'b'], { scroll_id: 'cursor-1', paging: { total: 9, limit: 2 } }), { - kind: 'OK', - value: { linhas: [{ id: 'a' }, { id: 'b' }], temMais: true, seguinte: 'cursor-1', total: 9 }, - }) - assert.deepEqual(scanned([], { scroll_id: 'cursor-2', paging: { total: 9, limit: 2 } }), { - kind: 'OK', - value: { linhas: [], temMais: false, total: 9 }, - }) - assert.deepEqual(scanned(['a'], { paging: { total: 1, offset: 0, limit: 5 } }), { - kind: 'OK', - value: { linhas: [{ id: 'a' }], temMais: false, total: 1 }, - }) -}) - -test('o total e o temMais saem das mesmas contagens: sem deslocamento só o total, e um total quebrado nenhum dos dois', () => { - assert.deepEqual(scanned(['a'], { paging: { total: 9 } }), { - kind: 'OK', - value: { linhas: [{ id: 'a' }], temMais: null, total: 9 }, - }) - assert.deepEqual(scanned(['a'], { paging: { total: 9.5, offset: 0, limit: 1 } }), { - kind: 'OK', - value: { linhas: [{ id: 'a' }], temMais: null }, - }) -}) diff --git a/packages/connectors/src/mercado-livre/operations/anuncios.ts b/packages/connectors/src/mercado-livre/operations/anuncios.ts index f108e4f67..96fc98491 100644 --- a/packages/connectors/src/mercado-livre/operations/anuncios.ts +++ b/packages/connectors/src/mercado-livre/operations/anuncios.ts @@ -7,6 +7,7 @@ import { Offset, PathId, readOf, + SCROLL_TTL_MS, Scan, searchTypeOf, sellerOf, @@ -29,6 +30,13 @@ export const anuncios = readOf({ limit: Limit(100), }), list: 'results', + paged: { + paging: { + ...{ kind: 'CURSOR', cursor: 'scrollId', limit: 'limit', max: 100 }, + ...{ start: { scan: true }, ttlMs: SCROLL_TTL_MS }, + }, + changed: null, + }, probe: () => ({ offset: 0, limit: 5 }), request: (input, account) => get(`/users/${sellerOf(account)}/items/search`, { diff --git a/packages/connectors/src/mercado-livre/operations/faturamento.ts b/packages/connectors/src/mercado-livre/operations/faturamento.ts index 05a5cee4d..b013e9eab 100644 --- a/packages/connectors/src/mercado-livre/operations/faturamento.ts +++ b/packages/connectors/src/mercado-livre/operations/faturamento.ts @@ -19,6 +19,10 @@ export const periodosDeFaturamento = readOf({ limit: Limit(12).optional(), }), list: 'results', + paged: { + paging: { kind: 'OFFSET', offset: 'offset', limit: 'limit', max: 12, ceiling: null }, + changed: null, + }, probe: () => ({}), request: (input) => get('/billing/integration/monthly/periods', { diff --git a/packages/connectors/src/mercado-livre/operations/perguntas.ts b/packages/connectors/src/mercado-livre/operations/perguntas.ts index 570d54693..fb49fa2f5 100644 --- a/packages/connectors/src/mercado-livre/operations/perguntas.ts +++ b/packages/connectors/src/mercado-livre/operations/perguntas.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { get } from '../../kernel/operation.ts' -import { Limit, Offset, readOf, Scan, searchTypeOf, sellerOf } from './shared.ts' +import { Limit, Offset, readOf, SCROLL_TTL_MS, Scan, searchTypeOf, sellerOf } from './shared.ts' export const perguntas = readOf({ id: 'perguntas', @@ -26,6 +26,13 @@ export const perguntas = readOf({ limit: Limit(50), }), list: 'questions', + paged: { + paging: { + ...{ kind: 'CURSOR', cursor: 'scrollId', limit: 'limit', max: 50 }, + ...{ start: { scan: true }, ttlMs: SCROLL_TTL_MS }, + }, + changed: null, + }, omit: ['from'], // the asker: the question route adds their name and contact probe: () => ({ offset: 0, limit: 5 }), request: (input, account) => diff --git a/packages/connectors/src/mercado-livre/operations/reclamacoes.ts b/packages/connectors/src/mercado-livre/operations/reclamacoes.ts index 7cb1db4d5..32fa4eb37 100644 --- a/packages/connectors/src/mercado-livre/operations/reclamacoes.ts +++ b/packages/connectors/src/mercado-livre/operations/reclamacoes.ts @@ -15,6 +15,10 @@ export const reclamacoes = readOf({ limit: Limit(30), }), list: 'data', + paged: { + paging: { kind: 'OFFSET', offset: 'offset', limit: 'limit', max: 30, ceiling: null }, + changed: null, + }, probe: () => ({ status: 'opened' as const, offset: 0, limit: 5 }), request: (input, account) => get('/post-purchase/v1/claims/search', { diff --git a/packages/connectors/src/mercado-livre/operations/shared.ts b/packages/connectors/src/mercado-livre/operations/shared.ts index c4c54bdd4..37c43e88c 100644 --- a/packages/connectors/src/mercado-livre/operations/shared.ts +++ b/packages/connectors/src/mercado-livre/operations/shared.ts @@ -1,5 +1,11 @@ import { z } from 'zod' -import type { Account, Operation, OperationId, ProviderRequest } from '../../kernel/operation.ts' +import type { + Account, + ListRead, + Operation, + OperationId, + ProviderRequest, +} from '../../kernel/operation.ts' import { defineOperation, valueAt } from '../../kernel/operation.ts' import type { Rows } from '../../kernel/rows.ts' import { Rows as RowsOut } from '../../kernel/rows.ts' @@ -19,6 +25,9 @@ export const Scan = { scrollId: z.string().min(1).max(4000).optional(), } +/** A scan's `scroll_id` lives 5 minutes (itens-e-buscas): the copy never sends an older one. */ +export const SCROLL_TTL_MS = 300_000 + /** The `search_type` the route wants while a scan runs: from its first page on. */ export function searchTypeOf(input: Readonly<{ scan?: true; scrollId?: string }>) { return input.scan === true || input.scrollId !== undefined ? 'scan' : undefined @@ -44,6 +53,7 @@ type ReadSpec = Readonly<{ label: string input: z.ZodType list: string | null // where the rows are: a key of the body, or the body itself + paged?: ListRead // how the list pages and asks only what changed: the copy engine reads it as the operation's `list` omit?: readonly string[] // the buyer's personal data, by dotted path emptyNotFound?: string // the start of the message of the 404 that means the seller has no data yet from?: readonly string[] @@ -53,9 +63,10 @@ type ReadSpec = Readonly<{ /** A read of Mercado Livre: idempotent by class, answered as rows by the API's own table (`rows`). */ export function readOf(spec: ReadSpec): Operation { - const { docs, list, omit, emptyNotFound, ...operation } = spec + const { docs, list, omit, emptyNotFound, paged, ...operation } = spec const defined = defineOperation({ ...operation, + ...(paged === undefined ? {} : { list: paged }), class: 'read' as const, idempotent: true, ...rows({ list, omit, emptyNotFound }), diff --git a/packages/connectors/src/mercado-livre/operations/vendas.ts b/packages/connectors/src/mercado-livre/operations/vendas.ts index 5e26f51b9..782813ceb 100644 --- a/packages/connectors/src/mercado-livre/operations/vendas.ts +++ b/packages/connectors/src/mercado-livre/operations/vendas.ts @@ -26,6 +26,10 @@ export const pedidos = readOf({ limit: Limit(50), }), list: 'results', + paged: { + paging: { kind: 'OFFSET', offset: 'offset', limit: 'limit', max: 50, ceiling: null }, + changed: { input: 'changedFrom', field: 'date_last_updated' }, + }, omit: BUYER, probe: () => ({ offset: 0, limit: 5 }), request: (input, account) => diff --git a/packages/connectors/src/mercado-livre/skill/SKILL.md b/packages/connectors/src/mercado-livre/skill/SKILL.md index 1e424dc8d..426ae6c4f 100644 --- a/packages/connectors/src/mercado-livre/skill/SKILL.md +++ b/packages/connectors/src/mercado-livre/skill/SKILL.md @@ -12,7 +12,8 @@ The connection is read only. There is no operation that writes, and a refusal is ## Rules for every read - Every read answers `{ linhas, temMais }`: the rows as Mercado Livre sent them (a field the kit does not know passes, and one object comes as a list of one), and whether another page follows (`temMais` is `null` when the route does not say). The app parses the fields it uses at its own boundary, never the kit. -- One page per call. A list ends when `offset + limit` reaches `total` (`paging`, or the same counts at the top of the answer), and `temMais` says so. Read the next page only when the person's question needs it. +- One page per call. A list ends when `offset + limit` reaches `total` (`paging`, or the same counts at the top of the answer), and `temMais` says so; a scan (`anuncios`, `perguntas`) gives the next page's cursor in `seguinte` and ends on a page with no rows. Read the next page only when the person's question needs it. +- `pedidos`, `anuncios`, `perguntas`, `reclamacoes` and `periodosDeFaturamento` declare how their list pages (and `pedidos`, how it asks only what changed), so each can become a copy the Conexus keeps in the company's database: for data an app reads again and again, propose the copy instead of paging live. - Each list has its own page size cap and its own window; the part for the subject gives them. - A refusal comes with what Mercado Livre answered. A permission the app lacks (`PA_UNAUTHORIZED_RESULT_FROM_POLICIES`) refuses only that group of reads; the others still work. - An answer Mercado Livre could not fill whole (a partial `206`, on an order) is read, and carries `parcial: true`: some fields are missing, and Mercado Livre says the rest usually suffices. Say so when a missing field matters to the question. diff --git a/packages/connectors/src/mercado-livre/skill/references/anuncios.md b/packages/connectors/src/mercado-livre/skill/references/anuncios.md index 0784b0f1f..b6405d595 100644 --- a/packages/connectors/src/mercado-livre/skill/references/anuncios.md +++ b/packages/connectors/src/mercado-livre/skill/references/anuncios.md @@ -1,6 +1,6 @@ # Anúncios -- `anuncios`: the ids of the seller's listings, filtered by `status` or `sku`. Up to 100 per page (`offset`, `limit`). The search reads at most 1000 listings (Mercado Livre's offset cap): a seller with more narrows it with `status` or `sku`; there is no way past 1000. +- `anuncios`: the ids of the seller's listings, filtered by `status` or `sku`. Up to 100 per page. By `offset` the search reads at most 1000 listings (Mercado Livre's offset cap); a scan reads them all: the first page sends `scan: true`, each answer gives the next page's cursor in `seguinte`, sent back as `scrollId` (it lives 5 minutes), and a page with no rows is the end. - `anunciosDetalhe`: up to 20 listings per call by id (title, price, stock, sold, status, category, listing type). Each item has its own `code`: a 404 there is that id's, not the call's. - `descricao`: the plain-text description of one listing. - `estoque`: the stock of one user product by warehouse. Limit of 100 calls per minute. diff --git a/packages/connectors/src/mercado-livre/skill/references/perguntas.md b/packages/connectors/src/mercado-livre/skill/references/perguntas.md index 8c056edb0..d402e1951 100644 --- a/packages/connectors/src/mercado-livre/skill/references/perguntas.md +++ b/packages/connectors/src/mercado-livre/skill/references/perguntas.md @@ -1,4 +1,4 @@ # Perguntas -- `perguntas`: questions on the seller's listings with the answer when there is one, up to 50 per page, filtered by `status`. Unanswered questions stay visible for 7 months. The asker is not in the answer. The search reads at most 1000 questions (Mercado Livre's offset cap): with more, narrow it with `status`; there is no way past 1000. +- `perguntas`: questions on the seller's listings with the answer when there is one, up to 50 per page, filtered by `status`. Unanswered questions stay visible for 7 months. The asker is not in the answer. By `offset` the search reads at most 1000 questions (Mercado Livre's offset cap); a scan reads them all: the first page sends `scan: true`, each answer gives the next page's cursor in `seguinte`, sent back as `scrollId` (it lives 5 minutes), and a page with no rows is the end. - `tempoDeResposta`: the seller's average answer time, in minutes. diff --git a/packages/connectors/src/mercado-livre/skill/references/vendas.md b/packages/connectors/src/mercado-livre/skill/references/vendas.md index 03c834edd..647167436 100644 --- a/packages/connectors/src/mercado-livre/skill/references/vendas.md +++ b/packages/connectors/src/mercado-livre/skill/references/vendas.md @@ -1,6 +1,6 @@ # Vendas -- `pedidos`: the seller's orders, up to 50 per page. Orders are kept for 12 months; filter by `status` and by `dateFrom`/`dateTo` (ISO 8601 with offset) and sort with `date_asc` or `date_desc`. An order can come partial (`206`): it is read, and `parcial: true` says some of its fields are missing. +- `pedidos`: the seller's orders, up to 50 per page. Orders are kept for 12 months; filter by `status` and by `dateFrom`/`dateTo` (ISO 8601 with offset) and sort with `date_asc` or `date_desc`. `changedFrom` (ISO 8601 with offset) asks only the orders changed since that instant (`date_last_updated`). An order can come partial (`206`): it is read, and `parcial: true` says some of its fields are missing. - `pedido`: one order by id, with its items, its `pack_id` and its shipping id. - `pacote`: one pack (several orders of one buyer shipped together). Only orders with a `pack_id` have one. diff --git a/packages/connectors/src/sankhya/copies.ts b/packages/connectors/src/sankhya/copies.ts index 0702246fa..c1fa61eb6 100644 --- a/packages/connectors/src/sankhya/copies.ts +++ b/packages/connectors/src/sankhya/copies.ts @@ -1,5 +1,5 @@ import type { CopyColumnType, CopyModel } from '../kernel/copy.ts' -import { CopyDefinition, CopyName } from '../kernel/copy.ts' +import { CopyDefinitionV1, CopyName } from '../kernel/copy.ts' /* * The five templates, with the columns and types decision 0063 measured on a real Sankhya. No query @@ -25,7 +25,7 @@ function model( return Object.freeze({ id: CopyName.parse(id), label, - definition: CopyDefinition.parse({ + definition: CopyDefinitionV1.parse({ version: 1, source: { kind: 'QUERY', sql: definition.sql }, columns: definition.columns.map(([name, type]) => ({ name, type })), diff --git a/packages/connectors/src/sankhya/copy-read.test.ts b/packages/connectors/src/sankhya/copy-read.test.ts index bc89b6828..54e771c38 100644 --- a/packages/connectors/src/sankhya/copy-read.test.ts +++ b/packages/connectors/src/sankhya/copy-read.test.ts @@ -3,7 +3,7 @@ import { test } from 'node:test' import { z } from 'zod' import { readToken, tokenRequest } from '../kernel/auth.ts' import { SENT_AT } from '../kernel/conformance-declared.ts' -import type { CopyDefinition, CopyPage, PageInput } from '../kernel/copy.ts' +import type { CopyDefinitionV1, CopyPage, PageInput } from '../kernel/copy.ts' import { CANONICAL_CELL, PAGE_ROWS } from '../kernel/copy.ts' import type { Answered } from '../kernel/operation.ts' import { requestOf } from '../kernel/operation.ts' @@ -45,7 +45,7 @@ function pageOf(answered: Answered): CopyPage { return answered.value } -function model(id: string): CopyDefinition { +function model(id: string): CopyDefinitionV1 { const found = sankhyaCopyModels.find((each) => each.id === id) assert.ok(found) return found.definition @@ -140,7 +140,7 @@ async function cellsOf(read: Read) { assert.equal(at('DTALTER'), changedOn(nota.dtneg).replace(' ', 'T'), 'carimbo ISO') assert.match(at('VLRNOTA') ?? '', /^\d+(\.\d+)?$/, 'decimal com ponto') assert.equal(at('TIPMOV'), 'V') - const flags: CopyDefinition = { + const flags: CopyDefinitionV1 = { version: 1, source: { kind: 'QUERY', sql: 'SELECT CODPARC, CGC_CPF, ATIVO FROM TGFPAR' }, columns: [ diff --git a/packages/hub-api/src/connection-copies.ts b/packages/hub-api/src/connection-copies.ts new file mode 100644 index 000000000..23490b2b2 --- /dev/null +++ b/packages/hub-api/src/connection-copies.ts @@ -0,0 +1,34 @@ +import { z } from 'zod' +import { CompanyId } from './companies.ts' +import { ConnectionId } from './connectors.ts' +import { CopyId, CopyMode } from './copies.ts' +import { defineOperation } from './operation.ts' + +const OfConnection = z.object({ companyId: CompanyId, connectionId: ConnectionId }) + +/** + * A copy that reads the connection, as Desligar shows it before the click: `last` when the connection + * is its only source (Desligar ends the copy), and that account's rows (null when the company's + * database did not answer). + */ +export const CopyOfConnection = z.object({ + copyId: CopyId, + name: z.string(), + mode: CopyMode, + last: z.boolean(), + rows: z.int().min(0).nullable(), +}) +export type CopyOfConnection = z.infer + +export const listConnectionCopies = defineOperation({ + id: 'listConnectionCopies', + summary: + "Lists the copies that read a connection, with what Desligar would take from each: the account's rows, or the whole copy when it is the copy's last source.", + access: 'session', + method: 'GET', + path: '/api/companies/{companyId}/connections/{connectionId}/copies', + bodyLimit: 1024, + input: { params: OfConnection }, + success: { status: 200, body: z.object({ copies: z.array(CopyOfConnection) }) }, + failures: ['NOT_FOUND', 'SESSION_REQUIRED', 'DATABASE_UNAVAILABLE', 'CONNECTION_NOT_FOUND'], +}) diff --git a/packages/hub-api/src/connectors.ts b/packages/hub-api/src/connectors.ts index 006fbf7c0..f003cedd2 100644 --- a/packages/hub-api/src/connectors.ts +++ b/packages/hub-api/src/connectors.ts @@ -261,14 +261,20 @@ export type Revocation = z.infer export const unlinkConnection = defineOperation({ id: 'unlinkConnection', summary: - 'Removes a connection and its sealed fields, after asking the provider to take the authorization back when the connector can; removing it again answers the same.', + "Removes a connection and its sealed fields, after taking its account's rows out of the company's copies and asking the provider to take the authorization back when the connector can; a copy left with no source ends with its table; removing it again answers the same.", access: 'session', method: 'DELETE', path: '/api/companies/{companyId}/connections/{connectionId}', bodyLimit: 1024, input: { params: OfConnection }, - success: { status: 200, body: z.object({ revocation: Revocation }) }, - failures: READ_FAILURES, + // tablesLeft: the tables of the copies Desligar ended that the company's database did not drop + // yet; the connection is gone all the same, and the next Desligar or Ligar drops them. + success: { + status: 200, + body: z.object({ revocation: Revocation, tablesLeft: z.array(z.string()) }), + }, + // COPY_DATABASE_UNAVAILABLE: the company's database did not answer before anything left the Hub. + failures: [...READ_FAILURES, 'COPY_DATABASE_UNAVAILABLE'], }) export const startConnectionSignIn = defineOperation({ diff --git a/packages/hub-api/src/copies.ts b/packages/hub-api/src/copies.ts index 1169dad8e..f278dd2e4 100644 --- a/packages/hub-api/src/copies.ts +++ b/packages/hub-api/src/copies.ts @@ -28,12 +28,13 @@ export const CopySchedule = z.discriminatedUnion('kind', [ ]) export type CopySchedule = z.infer -/** Why a run ended in error: the source's four failures, or the company's database. */ +/** Why a run ended in error: the source's failures, its read's ceiling, or the company's database. */ export const CopyRunFailureCode = z.enum([ 'COPY_SOURCE_REFUSED', 'COPY_SOURCE_NEEDS_REAUTH', 'COPY_SOURCE_UNREACHABLE', 'COPY_SOURCE_UNREADABLE', + 'COPY_SOURCE_CEILING', 'COPY_DATABASE_UNAVAILABLE', ] as const satisfies readonly FailureCode[]) export type CopyRunFailureCode = z.infer @@ -45,7 +46,8 @@ export const CopyRun = z.object({ outcome: z.discriminatedUnion('kind', [ z.object({ kind: z.literal('RUNNING') }), z.object({ kind: z.literal('COMPLETE') }), - z.object({ kind: z.literal('PARTIAL'), reason: z.enum(['CAP', 'STOPPED']) }), + // WAIT: the provider asked to wait; the next run goes on from where the source stopped. + z.object({ kind: z.literal('PARTIAL'), reason: z.enum(['CAP', 'STOPPED', 'WAIT']) }), z.object({ kind: z.literal('ERROR'), code: CopyRunFailureCode }), z.object({ kind: z.literal('INTERRUPTED') }), ]), diff --git a/packages/hub-api/src/copy-failures.ts b/packages/hub-api/src/copy-failures.ts index 31e062fbe..e30e938a5 100644 --- a/packages/hub-api/src/copy-failures.ts +++ b/packages/hub-api/src/copy-failures.ts @@ -82,6 +82,14 @@ export const COPY_FAILURES = { action: 'A próxima rodada tenta de novo.', actionKind: 'NONE', }, + COPY_SOURCE_CEILING: { + category: 'THIRD_PARTY', + settles: true, + status: 502, + phrase: 'A leitura chegou ao máximo de linhas que o sistema entrega.', + action: 'A cópia precisa de um filtro que leia menos linhas.', + actionKind: 'NONE', + }, COPY_SOURCE_UNREADABLE: { category: 'THIRD_PARTY', settles: true, diff --git a/packages/hub-api/src/index.ts b/packages/hub-api/src/index.ts index 82408b1ee..c3a3ceaf3 100644 --- a/packages/hub-api/src/index.ts +++ b/packages/hub-api/src/index.ts @@ -116,6 +116,7 @@ export { VersionCheck, VersionData, } from './conexus-parts.ts' +export * from './connection-copies.ts' export { CALL_DEADLINE_MS, CONNECTION_STATES,