diff --git a/backend/drizzle/20260710092603_uneven_dakota_north/migration.sql b/backend/drizzle/20260710092603_uneven_dakota_north/migration.sql new file mode 100644 index 000000000..3fb8a4783 --- /dev/null +++ b/backend/drizzle/20260710092603_uneven_dakota_north/migration.sql @@ -0,0 +1 @@ +DROP INDEX "attachments_organization_id_created_at_index"; \ No newline at end of file diff --git a/backend/drizzle/20260710092603_uneven_dakota_north/snapshot.json b/backend/drizzle/20260710092603_uneven_dakota_north/snapshot.json new file mode 100644 index 000000000..add298d6d --- /dev/null +++ b/backend/drizzle/20260710092603_uneven_dakota_north/snapshot.json @@ -0,0 +1,5325 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "2be899a2-c3e4-455d-adb6-eea7232d159d", + "prevIds": [ + "2612cce1-6377-4fe5-8ced-d528b8b0a1e5" + ], + "ddl": [ + { + "isRlsEnabled": false, + "name": "activities", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "attachments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "oauth_accounts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "passkeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limits", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tokens", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "totps", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "domains", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "context_counters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "product_counters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "inactive_memberships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "memberships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organizations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "requests", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "seen_by", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "system_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tenants", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "emails", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "unsubscribe_tokens", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user_counters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "users", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "yjs_documents", + "entityType": "tables", + "schema": "public" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resource_type", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "table_name", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subject_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "changed_fields", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stx", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'attachment'", + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'New attachment'", + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stx", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(1000000)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(1000000)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "keywords", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleted_at", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleted_by", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "seq", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "public", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bucket_name", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "group_id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "filename", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_type", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "converted_content_type", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "original_key", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "converted_key", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_key", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_user_id", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified_at", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credential_id", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "public_key", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_name", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'desktop'", + "generated": null, + "identity": null, + "name": "device_type", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_os", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "browser", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name_on_device", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limits" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "points", + "entityType": "columns", + "schema": "public", + "table": "rate_limits" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expire", + "entityType": "columns", + "schema": "public", + "table": "rate_limits" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'regular'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_name", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'desktop'", + "generated": null, + "identity": null, + "name": "device_type", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_os", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "browser", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth_strategy", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_hash", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_subnet_hash", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(2)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_country", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_asn", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "single_use_token", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "oauth_account_id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inactive_membership_id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "invoked_at", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "domain", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verification_token", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified_at", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_checked_at", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_key", + "entityType": "columns", + "schema": "public", + "table": "context_counters" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "counts", + "entityType": "columns", + "schema": "public", + "table": "context_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "context_counters" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "view_count", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_viewed_at", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_type", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rejected_at", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_type", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "archived", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "muted", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "display_order", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'organization'", + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banner_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "short_name", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "country", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "default_language", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[\"en\"]'", + "generated": null, + "identity": null, + "name": "languages", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notification_email", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "website_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(1000000)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "welcome_text", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "auth_strategies", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "chat_support", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_id", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"quotas\":{\"user\":1000,\"organization\":5,\"attachment\":100},\"rateLimits\":{\"apiPointsPerHour\":1000}}'", + "generated": null, + "identity": null, + "name": "restrictions", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscription_id", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'none'", + "generated": null, + "identity": null, + "name": "subscription_status", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscription_plan", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscription_data", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_id", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified_at", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_seen_at", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_started_at", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_sign_in_at", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'user'", + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_url", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banner_url", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "mfa_required", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "first_name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "newsletter", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "user_flags", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_edited_by", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "activities_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "activities_org_id_index", + "entityType": "indexes", + "schema": "public", + "table": "activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "activities_entity_type_subject_id_index", + "entityType": "indexes", + "schema": "public", + "table": "activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_organization_id_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_tenant_id_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_created_by_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updated_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_updated_by_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "group_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_group_id_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "oauth_accounts_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "oauth_accounts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkeys_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "credential_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkeys_credential_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "secret", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_secret_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "ip_hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_user_id_ip_hash_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ip_subnet_hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_ip_subnet_hash_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "secret", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_secret_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "single_use_token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_single_use_token_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "totps_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "totps" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "domains_tenant_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "domains" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "domain", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "domains_domain_idx", + "entityType": "indexes", + "schema": "public", + "table": "domains" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_tenant_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "rejected_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_org_pending_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updated_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_updated_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_tenant_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "context_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_context_org_role_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_org_user_tenant_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_name_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_tenant_id_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_created_by_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updated_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_updated_by_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "requests_emails", + "entityType": "indexes", + "schema": "public", + "table": "requests" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "requests_created_at", + "entityType": "indexes", + "schema": "public", + "table": "requests" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "context_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "entity_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "seen_by_user_context_type_index", + "entityType": "indexes", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "seen_by_entity_id_index", + "entityType": "indexes", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "seen_by_tenant_id_index", + "entityType": "indexes", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_status_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_created_by_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "subscription_status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_subscription_status_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "emails_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "emails" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "secret", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "unsubscribe_tokens_secret_idx", + "entityType": "indexes", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "unsubscribe_tokens_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_name_index", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_email_index", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_yjs_docs_tenant", + "entityType": "indexes", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_yjs_docs_org", + "entityType": "indexes", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "attachments_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "attachments_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "attachments_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "deleted_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "attachments_deleted_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "attachments_Ytb3N4m0pWsH_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "oauth_accounts_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "oauth_accounts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "passkeys_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "passkeys" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sessions_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tokens_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": false, + "columns": [ + "oauth_account_id" + ], + "schemaTo": "public", + "tableTo": "oauth_accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tokens_oauth_account_id_oauth_accounts_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tokens_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "totps_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "totps" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "domains_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "domains" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "inactive_memberships_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "inactive_memberships_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "inactive_memberships_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "inactive_memberships_gmmCA2ACknk4_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "memberships_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "memberships_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "memberships_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "memberships_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "memberships_Yta3VHtyCTj4_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "organizations_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "organizations_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "organizations_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "seen_by_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "system_roles_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "system_roles" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "tenants_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "emails_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "emails" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "unsubscribe_tokens_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "users_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "yjs_documents_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "yjs_documents_HY8MgE0sbYNM_fkey", + "entityType": "fks", + "schema": "public", + "table": "yjs_documents" + }, + { + "columns": [ + "id", + "created_at" + ], + "nameExplicit": false, + "name": "activities_pkey", + "entityType": "pks", + "schema": "public", + "table": "activities" + }, + { + "columns": [ + "id", + "expires_at" + ], + "nameExplicit": false, + "name": "sessions_pkey", + "entityType": "pks", + "schema": "public", + "table": "sessions" + }, + { + "columns": [ + "id", + "expires_at" + ], + "nameExplicit": false, + "name": "tokens_pkey", + "entityType": "pks", + "schema": "public", + "table": "tokens" + }, + { + "columns": [ + "id", + "created_at" + ], + "nameExplicit": false, + "name": "seen_by_pkey", + "entityType": "pks", + "schema": "public", + "table": "seen_by" + }, + { + "columns": [ + "id", + "created_at" + ], + "nameExplicit": false, + "name": "unsubscribe_tokens_pkey", + "entityType": "pks", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "columns": [ + "entity_type", + "entity_id" + ], + "nameExplicit": false, + "name": "yjs_documents_pkey", + "entityType": "pks", + "schema": "public", + "table": "yjs_documents" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "attachments_pkey", + "schema": "public", + "table": "attachments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "oauth_accounts_pkey", + "schema": "public", + "table": "oauth_accounts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "passkeys_pkey", + "schema": "public", + "table": "passkeys", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "rate_limits_pkey", + "schema": "public", + "table": "rate_limits", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "totps_pkey", + "schema": "public", + "table": "totps", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "domains_pkey", + "schema": "public", + "table": "domains", + "entityType": "pks" + }, + { + "columns": [ + "context_key" + ], + "nameExplicit": false, + "name": "context_counters_pkey", + "schema": "public", + "table": "context_counters", + "entityType": "pks" + }, + { + "columns": [ + "entity_id" + ], + "nameExplicit": false, + "name": "product_counters_pkey", + "schema": "public", + "table": "product_counters", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "inactive_memberships_pkey", + "schema": "public", + "table": "inactive_memberships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "memberships_pkey", + "schema": "public", + "table": "memberships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organizations_pkey", + "schema": "public", + "table": "organizations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "requests_pkey", + "schema": "public", + "table": "requests", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "system_roles_pkey", + "schema": "public", + "table": "system_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tenants_pkey", + "schema": "public", + "table": "tenants", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "emails_pkey", + "schema": "public", + "table": "emails", + "entityType": "pks" + }, + { + "columns": [ + "user_id" + ], + "nameExplicit": false, + "name": "user_counters_pkey", + "schema": "public", + "table": "user_counters", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "users_pkey", + "schema": "public", + "table": "users", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "provider", + "provider_user_id", + "email" + ], + "nullsNotDistinct": false, + "name": "oauth_accounts_provider_provider_user_id_email_unique", + "entityType": "uniques", + "schema": "public", + "table": "oauth_accounts" + }, + { + "nameExplicit": true, + "columns": [ + "tenant_id", + "email", + "context_id" + ], + "nullsNotDistinct": false, + "name": "inactive_memberships_tenant_email_ctx", + "entityType": "uniques", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + "tenant_id", + "user_id", + "context_id" + ], + "nullsNotDistinct": false, + "name": "memberships_unique_context", + "entityType": "uniques", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + "tenant_id", + "id" + ], + "nullsNotDistinct": false, + "name": "organizations_tenant_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + "user_id", + "entity_id" + ], + "nullsNotDistinct": false, + "name": "seen_by_user_entity_unique", + "entityType": "uniques", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": false, + "columns": [ + "domain" + ], + "nullsNotDistinct": false, + "name": "domains_domain_key", + "schema": "public", + "table": "domains", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "organizations_slug_key", + "schema": "public", + "table": "organizations", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "nullsNotDistinct": false, + "name": "system_roles_user_id_key", + "schema": "public", + "table": "system_roles", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "emails_email_key", + "schema": "public", + "table": "emails", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "users_slug_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "users_email_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": [ + "public" + ], + "using": "\n \n COALESCE(current_setting('app.tenant_id', true), '') != ''\n AND \"attachments\".\"tenant_id\" = current_setting('app.tenant_id', true)::text\n\n AND (\"attachments\".\"deleted_at\" IS NULL OR current_setting('app.include_deleted', true) = 'true')\n ", + "withCheck": null, + "name": "attachments_select_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": [ + "public" + ], + "using": null, + "withCheck": "true", + "name": "attachments_insert_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "attachments_update_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "attachments_delete_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": [ + "public" + ], + "using": "\n \n COALESCE(current_setting('app.tenant_id', true), '') != ''\n AND \"yjs_documents\".\"tenant_id\" = current_setting('app.tenant_id', true)::text\n\n \n ", + "withCheck": null, + "name": "yjs_documents_select_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": [ + "public" + ], + "using": null, + "withCheck": "true", + "name": "yjs_documents_insert_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "yjs_documents_update_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "yjs_documents_delete_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/backend/drizzle/20260710092608_rls_setup/migration.sql b/backend/drizzle/20260710092608_rls_setup/migration.sql new file mode 100644 index 000000000..f2a626d33 --- /dev/null +++ b/backend/drizzle/20260710092608_rls_setup/migration.sql @@ -0,0 +1,67 @@ +-- ⚠️ AUTO-GENERATED — DO NOT EDIT THIS FILE DIRECTLY +-- This migration is generated by a script in backend/scripts/migrations/. +-- To make changes, edit the generator script and re-run it. +-- The user will handle migration generation and application. +-- +-- RLS (Row-Level Security) Setup +-- Configures FORCE RLS, table ownership, and grants. +-- Policies are defined in Drizzle schema files using pgPolicy(). +-- RLS enforces tenant-level isolation only; org-level isolation is application-layer (orgGuard). +-- Gracefully skips if required roles are not yet created. + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'runtime_role') THEN + RAISE NOTICE 'Roles not available - skipping RLS setup.'; + RETURN; + END IF; + + BEGIN + -- Table ownership and FORCE RLS + ALTER TABLE attachments OWNER TO admin_role; + ALTER TABLE yjs_documents OWNER TO admin_role; + ALTER TABLE activities OWNER TO admin_role; + + ALTER TABLE attachments FORCE ROW LEVEL SECURITY; + ALTER TABLE yjs_documents FORCE ROW LEVEL SECURITY; + + -- Grants: runtime_role (subject to RLS) + GRANT SELECT, INSERT, UPDATE, DELETE ON attachments TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON yjs_documents TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON organizations TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON memberships TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON inactive_memberships TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON users TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON sessions TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON user_counters TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON tokens TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON passkeys TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON oauth_accounts TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON totps TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON requests TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON unsubscribe_tokens TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON emails TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON rate_limits TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON context_counters TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON seen_by TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON product_counters TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON domains TO runtime_role; + GRANT SELECT, INSERT, UPDATE, DELETE ON tenants TO runtime_role; + GRANT SELECT ON system_roles TO runtime_role; + GRANT SELECT ON activities TO runtime_role; + + -- Grants: admin_role (full access; also used by the CDC worker) + GRANT ALL ON ALL TABLES IN SCHEMA public TO admin_role; + GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO admin_role; + + -- Grants: pg_catalog usage for JSONB operators + GRANT USAGE ON SCHEMA pg_catalog TO runtime_role; + + RAISE NOTICE 'RLS setup complete.'; + EXCEPTION WHEN OTHERS THEN + -- Fail LOUDLY: swallowing this rolled back ownership, FORCE RLS and every grant in + -- one silent NOTICE — the app then boots with no table grants (every request 403s) + -- or, worse, without enforced RLS. + RAISE EXCEPTION 'RLS setup failed: % (SQLSTATE: %)', SQLERRM, SQLSTATE; + END; +END $$; diff --git a/backend/drizzle/20260710092608_rls_setup/snapshot.json b/backend/drizzle/20260710092608_rls_setup/snapshot.json new file mode 100644 index 000000000..add298d6d --- /dev/null +++ b/backend/drizzle/20260710092608_rls_setup/snapshot.json @@ -0,0 +1,5325 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "2be899a2-c3e4-455d-adb6-eea7232d159d", + "prevIds": [ + "2612cce1-6377-4fe5-8ced-d528b8b0a1e5" + ], + "ddl": [ + { + "isRlsEnabled": false, + "name": "activities", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "attachments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "oauth_accounts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "passkeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limits", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tokens", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "totps", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "domains", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "context_counters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "product_counters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "inactive_memberships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "memberships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organizations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "requests", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "seen_by", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "system_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tenants", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "emails", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "unsubscribe_tokens", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user_counters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "users", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "yjs_documents", + "entityType": "tables", + "schema": "public" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resource_type", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "table_name", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subject_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "changed_fields", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stx", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'attachment'", + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'New attachment'", + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stx", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(1000000)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(1000000)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "keywords", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleted_at", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleted_by", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "seq", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "public", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bucket_name", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "group_id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "filename", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_type", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "converted_content_type", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "original_key", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "converted_key", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_key", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_user_id", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified_at", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credential_id", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "public_key", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_name", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'desktop'", + "generated": null, + "identity": null, + "name": "device_type", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_os", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "browser", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name_on_device", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limits" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "points", + "entityType": "columns", + "schema": "public", + "table": "rate_limits" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expire", + "entityType": "columns", + "schema": "public", + "table": "rate_limits" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'regular'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_name", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'desktop'", + "generated": null, + "identity": null, + "name": "device_type", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_os", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "browser", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth_strategy", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_hash", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_subnet_hash", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(2)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_country", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_asn", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "single_use_token", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "oauth_account_id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inactive_membership_id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "invoked_at", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "domain", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verification_token", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified_at", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_checked_at", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_key", + "entityType": "columns", + "schema": "public", + "table": "context_counters" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "counts", + "entityType": "columns", + "schema": "public", + "table": "context_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "context_counters" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "view_count", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_viewed_at", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_type", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rejected_at", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_type", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "archived", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "muted", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "display_order", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'organization'", + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banner_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "short_name", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "country", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "default_language", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[\"en\"]'", + "generated": null, + "identity": null, + "name": "languages", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notification_email", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "website_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(1000000)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "welcome_text", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "auth_strategies", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "chat_support", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_id", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"quotas\":{\"user\":1000,\"organization\":5,\"attachment\":100},\"rateLimits\":{\"apiPointsPerHour\":1000}}'", + "generated": null, + "identity": null, + "name": "restrictions", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscription_id", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'none'", + "generated": null, + "identity": null, + "name": "subscription_status", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscription_plan", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscription_data", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_id", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified_at", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_seen_at", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_started_at", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_sign_in_at", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'user'", + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_url", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banner_url", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "mfa_required", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "first_name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "newsletter", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "user_flags", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_edited_by", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "activities_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "activities_org_id_index", + "entityType": "indexes", + "schema": "public", + "table": "activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "activities_entity_type_subject_id_index", + "entityType": "indexes", + "schema": "public", + "table": "activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_organization_id_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_tenant_id_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_created_by_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updated_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_updated_by_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "group_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_group_id_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "oauth_accounts_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "oauth_accounts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkeys_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "credential_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkeys_credential_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "secret", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_secret_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "ip_hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_user_id_ip_hash_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ip_subnet_hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_ip_subnet_hash_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "secret", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_secret_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "single_use_token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_single_use_token_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "totps_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "totps" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "domains_tenant_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "domains" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "domain", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "domains_domain_idx", + "entityType": "indexes", + "schema": "public", + "table": "domains" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_tenant_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "rejected_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_org_pending_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updated_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_updated_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_tenant_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "context_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_context_org_role_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_org_user_tenant_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_name_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_tenant_id_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_created_by_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updated_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_updated_by_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "requests_emails", + "entityType": "indexes", + "schema": "public", + "table": "requests" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "requests_created_at", + "entityType": "indexes", + "schema": "public", + "table": "requests" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "context_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "entity_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "seen_by_user_context_type_index", + "entityType": "indexes", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "seen_by_entity_id_index", + "entityType": "indexes", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "seen_by_tenant_id_index", + "entityType": "indexes", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_status_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_created_by_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "subscription_status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_subscription_status_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "emails_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "emails" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "secret", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "unsubscribe_tokens_secret_idx", + "entityType": "indexes", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "unsubscribe_tokens_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_name_index", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_email_index", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_yjs_docs_tenant", + "entityType": "indexes", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_yjs_docs_org", + "entityType": "indexes", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "attachments_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "attachments_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "attachments_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "deleted_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "attachments_deleted_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "attachments_Ytb3N4m0pWsH_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "oauth_accounts_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "oauth_accounts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "passkeys_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "passkeys" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sessions_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tokens_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": false, + "columns": [ + "oauth_account_id" + ], + "schemaTo": "public", + "tableTo": "oauth_accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tokens_oauth_account_id_oauth_accounts_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tokens_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "totps_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "totps" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "domains_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "domains" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "inactive_memberships_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "inactive_memberships_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "inactive_memberships_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "inactive_memberships_gmmCA2ACknk4_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "memberships_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "memberships_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "memberships_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "memberships_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "memberships_Yta3VHtyCTj4_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "organizations_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "organizations_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "organizations_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "seen_by_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "system_roles_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "system_roles" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "tenants_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "emails_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "emails" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "unsubscribe_tokens_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "users_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "yjs_documents_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "yjs_documents_HY8MgE0sbYNM_fkey", + "entityType": "fks", + "schema": "public", + "table": "yjs_documents" + }, + { + "columns": [ + "id", + "created_at" + ], + "nameExplicit": false, + "name": "activities_pkey", + "entityType": "pks", + "schema": "public", + "table": "activities" + }, + { + "columns": [ + "id", + "expires_at" + ], + "nameExplicit": false, + "name": "sessions_pkey", + "entityType": "pks", + "schema": "public", + "table": "sessions" + }, + { + "columns": [ + "id", + "expires_at" + ], + "nameExplicit": false, + "name": "tokens_pkey", + "entityType": "pks", + "schema": "public", + "table": "tokens" + }, + { + "columns": [ + "id", + "created_at" + ], + "nameExplicit": false, + "name": "seen_by_pkey", + "entityType": "pks", + "schema": "public", + "table": "seen_by" + }, + { + "columns": [ + "id", + "created_at" + ], + "nameExplicit": false, + "name": "unsubscribe_tokens_pkey", + "entityType": "pks", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "columns": [ + "entity_type", + "entity_id" + ], + "nameExplicit": false, + "name": "yjs_documents_pkey", + "entityType": "pks", + "schema": "public", + "table": "yjs_documents" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "attachments_pkey", + "schema": "public", + "table": "attachments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "oauth_accounts_pkey", + "schema": "public", + "table": "oauth_accounts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "passkeys_pkey", + "schema": "public", + "table": "passkeys", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "rate_limits_pkey", + "schema": "public", + "table": "rate_limits", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "totps_pkey", + "schema": "public", + "table": "totps", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "domains_pkey", + "schema": "public", + "table": "domains", + "entityType": "pks" + }, + { + "columns": [ + "context_key" + ], + "nameExplicit": false, + "name": "context_counters_pkey", + "schema": "public", + "table": "context_counters", + "entityType": "pks" + }, + { + "columns": [ + "entity_id" + ], + "nameExplicit": false, + "name": "product_counters_pkey", + "schema": "public", + "table": "product_counters", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "inactive_memberships_pkey", + "schema": "public", + "table": "inactive_memberships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "memberships_pkey", + "schema": "public", + "table": "memberships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organizations_pkey", + "schema": "public", + "table": "organizations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "requests_pkey", + "schema": "public", + "table": "requests", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "system_roles_pkey", + "schema": "public", + "table": "system_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tenants_pkey", + "schema": "public", + "table": "tenants", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "emails_pkey", + "schema": "public", + "table": "emails", + "entityType": "pks" + }, + { + "columns": [ + "user_id" + ], + "nameExplicit": false, + "name": "user_counters_pkey", + "schema": "public", + "table": "user_counters", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "users_pkey", + "schema": "public", + "table": "users", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "provider", + "provider_user_id", + "email" + ], + "nullsNotDistinct": false, + "name": "oauth_accounts_provider_provider_user_id_email_unique", + "entityType": "uniques", + "schema": "public", + "table": "oauth_accounts" + }, + { + "nameExplicit": true, + "columns": [ + "tenant_id", + "email", + "context_id" + ], + "nullsNotDistinct": false, + "name": "inactive_memberships_tenant_email_ctx", + "entityType": "uniques", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + "tenant_id", + "user_id", + "context_id" + ], + "nullsNotDistinct": false, + "name": "memberships_unique_context", + "entityType": "uniques", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + "tenant_id", + "id" + ], + "nullsNotDistinct": false, + "name": "organizations_tenant_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + "user_id", + "entity_id" + ], + "nullsNotDistinct": false, + "name": "seen_by_user_entity_unique", + "entityType": "uniques", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": false, + "columns": [ + "domain" + ], + "nullsNotDistinct": false, + "name": "domains_domain_key", + "schema": "public", + "table": "domains", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "organizations_slug_key", + "schema": "public", + "table": "organizations", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "nullsNotDistinct": false, + "name": "system_roles_user_id_key", + "schema": "public", + "table": "system_roles", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "emails_email_key", + "schema": "public", + "table": "emails", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "users_slug_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "users_email_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": [ + "public" + ], + "using": "\n \n COALESCE(current_setting('app.tenant_id', true), '') != ''\n AND \"attachments\".\"tenant_id\" = current_setting('app.tenant_id', true)::text\n\n AND (\"attachments\".\"deleted_at\" IS NULL OR current_setting('app.include_deleted', true) = 'true')\n ", + "withCheck": null, + "name": "attachments_select_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": [ + "public" + ], + "using": null, + "withCheck": "true", + "name": "attachments_insert_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "attachments_update_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "attachments_delete_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": [ + "public" + ], + "using": "\n \n COALESCE(current_setting('app.tenant_id', true), '') != ''\n AND \"yjs_documents\".\"tenant_id\" = current_setting('app.tenant_id', true)::text\n\n \n ", + "withCheck": null, + "name": "yjs_documents_select_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": [ + "public" + ], + "using": null, + "withCheck": "true", + "name": "yjs_documents_insert_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "yjs_documents_update_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "yjs_documents_delete_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/backend/drizzle/20260710093820_jazzy_colossus/migration.sql b/backend/drizzle/20260710093820_jazzy_colossus/migration.sql new file mode 100644 index 000000000..47e242db6 --- /dev/null +++ b/backend/drizzle/20260710093820_jazzy_colossus/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "inactive_memberships" ADD COLUMN "reminded_at" timestamp;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "published_at" timestamp DEFAULT now(); \ No newline at end of file diff --git a/backend/drizzle/20260710093820_jazzy_colossus/snapshot.json b/backend/drizzle/20260710093820_jazzy_colossus/snapshot.json new file mode 100644 index 000000000..c949c8949 --- /dev/null +++ b/backend/drizzle/20260710093820_jazzy_colossus/snapshot.json @@ -0,0 +1,5351 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "52bb50e2-6b5a-41e7-899e-42ee4bcf341a", + "prevIds": [ + "2be899a2-c3e4-455d-adb6-eea7232d159d" + ], + "ddl": [ + { + "isRlsEnabled": false, + "name": "activities", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "attachments", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "oauth_accounts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "passkeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limits", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tokens", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "totps", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "domains", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "context_counters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "product_counters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "inactive_memberships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "memberships", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organizations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "requests", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "seen_by", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "system_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "tenants", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "emails", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "unsubscribe_tokens", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user_counters", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "users", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "yjs_documents", + "entityType": "tables", + "schema": "public" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "resource_type", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "table_name", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subject_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "changed_fields", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stx", + "entityType": "columns", + "schema": "public", + "table": "activities" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'attachment'", + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'New attachment'", + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "stx", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(1000000)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(1000000)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "keywords", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleted_at", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deleted_by", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "seq", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "public", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bucket_name", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "group_id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "filename", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content_type", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "converted_content_type", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "original_key", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "converted_key", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_key", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "attachments" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_user_id", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified_at", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "oauth_accounts" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credential_id", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "public_key", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_name", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'desktop'", + "generated": null, + "identity": null, + "name": "device_type", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_os", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "browser", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name_on_device", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "passkeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limits" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "points", + "entityType": "columns", + "schema": "public", + "table": "rate_limits" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expire", + "entityType": "columns", + "schema": "public", + "table": "rate_limits" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'regular'", + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_name", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'desktop'", + "generated": null, + "identity": null, + "name": "device_type", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_os", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "browser", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "auth_strategy", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_hash", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_subnet_hash", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "varchar(2)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_country", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_asn", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "sessions" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "single_use_token", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "oauth_account_id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inactive_membership_id", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "invoked_at", + "entityType": "columns", + "schema": "public", + "table": "tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "totps" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "domain", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verification_token", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified_at", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_checked_at", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "domains" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_key", + "entityType": "columns", + "schema": "public", + "table": "context_counters" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "counts", + "entityType": "columns", + "schema": "public", + "table": "context_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "context_counters" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "view_count", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_viewed_at", + "entityType": "columns", + "schema": "public", + "table": "product_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_type", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rejected_at", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reminded_at", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "inactive_memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_type", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "archived", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "muted", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "display_order", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "memberships" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'organization'", + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banner_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "published_at", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "short_name", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "country", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "default_language", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[\"en\"]'", + "generated": null, + "identity": null, + "name": "languages", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notification_email", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "website_url", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "varchar(1000000)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "welcome_text", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "auth_strategies", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "chat_support", + "entityType": "columns", + "schema": "public", + "table": "organizations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_id", + "entityType": "columns", + "schema": "public", + "table": "requests" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "context_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "seen_by" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "system_roles" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'active'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "json", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{\"quotas\":{\"user\":1000,\"organization\":5,\"attachment\":100},\"rateLimits\":{\"apiPointsPerHour\":1000}}'", + "generated": null, + "identity": null, + "name": "restrictions", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscription_id", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'none'", + "generated": null, + "identity": null, + "name": "subscription_status", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscription_plan", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "subscription_data", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "tenants" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "verified", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_id", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "verified_at", + "entityType": "columns", + "schema": "public", + "table": "emails" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "secret", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_seen_at", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_started_at", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_sign_in_at", + "entityType": "columns", + "schema": "public", + "table": "user_counters" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'user'", + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbnail_url", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "banner_url", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "mfa_required", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "first_name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_name", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "newsletter", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "user_flags", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_by", + "entityType": "columns", + "schema": "public", + "table": "users" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_type", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "entity_id", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "varchar(24)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tenant_id", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_edited_by", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "activities_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "activities_org_id_index", + "entityType": "indexes", + "schema": "public", + "table": "activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "activities_entity_type_subject_id_index", + "entityType": "indexes", + "schema": "public", + "table": "activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_organization_id_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_tenant_id_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_created_by_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updated_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_updated_by_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "group_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "attachments_group_id_index", + "entityType": "indexes", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "oauth_accounts_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "oauth_accounts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkeys_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "credential_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "passkeys_credential_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "secret", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_secret_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "ip_hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_user_id_ip_hash_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ip_subnet_hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "sessions_ip_subnet_hash_idx", + "entityType": "indexes", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "secret", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_secret_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "single_use_token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tokens_single_use_token_idx", + "entityType": "indexes", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "totps_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "totps" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "domains_tenant_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "domains" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "domain", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "domains_domain_idx", + "entityType": "indexes", + "schema": "public", + "table": "domains" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_tenant_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "rejected_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "inactive_memberships_org_pending_idx", + "entityType": "indexes", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updated_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_updated_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_tenant_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "context_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "role", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_context_org_role_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "memberships_org_user_tenant_idx", + "entityType": "indexes", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_name_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_tenant_id_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_created_by_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updated_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organizations_updated_by_index", + "entityType": "indexes", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "requests_emails", + "entityType": "indexes", + "schema": "public", + "table": "requests" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "requests_created_at", + "entityType": "indexes", + "schema": "public", + "table": "requests" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "context_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "entity_type", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "seen_by_user_context_type_index", + "entityType": "indexes", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "entity_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "seen_by_entity_id_index", + "entityType": "indexes", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "seen_by_tenant_id_index", + "entityType": "indexes", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_status_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_by", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_created_by_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "subscription_status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tenants_subscription_status_index", + "entityType": "indexes", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "emails_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "emails" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "secret", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "unsubscribe_tokens_secret_idx", + "entityType": "indexes", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "unsubscribe_tokens_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_name_index", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_email_index", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "users_created_at_index", + "entityType": "indexes", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tenant_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_yjs_docs_tenant", + "entityType": "indexes", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_yjs_docs_org", + "entityType": "indexes", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "attachments_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "attachments_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "attachments_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "deleted_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "attachments_deleted_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "attachments_Ytb3N4m0pWsH_fkey", + "entityType": "fks", + "schema": "public", + "table": "attachments" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "oauth_accounts_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "oauth_accounts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "passkeys_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "passkeys" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sessions_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "sessions" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tokens_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": false, + "columns": [ + "oauth_account_id" + ], + "schemaTo": "public", + "tableTo": "oauth_accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tokens_oauth_account_id_oauth_accounts_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tokens_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tokens" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "totps_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "totps" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "domains_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "domains" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "inactive_memberships_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "inactive_memberships_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "inactive_memberships_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "inactive_memberships_gmmCA2ACknk4_fkey", + "entityType": "fks", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "memberships_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "memberships_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "memberships_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "memberships_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "memberships_Yta3VHtyCTj4_fkey", + "entityType": "fks", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "organizations_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "organizations_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "organizations_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "seen_by_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "system_roles_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "system_roles" + }, + { + "nameExplicit": false, + "columns": [ + "created_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "tenants_created_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "tenants" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "emails_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "emails" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "unsubscribe_tokens_user_id_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "nameExplicit": false, + "columns": [ + "updated_by" + ], + "schemaTo": "public", + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "users_updated_by_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "users" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id" + ], + "schemaTo": "public", + "tableTo": "tenants", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "yjs_documents_tenant_id_tenants_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "yjs_documents" + }, + { + "nameExplicit": false, + "columns": [ + "tenant_id", + "organization_id" + ], + "schemaTo": "public", + "tableTo": "organizations", + "columnsTo": [ + "tenant_id", + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "yjs_documents_HY8MgE0sbYNM_fkey", + "entityType": "fks", + "schema": "public", + "table": "yjs_documents" + }, + { + "columns": [ + "id", + "created_at" + ], + "nameExplicit": false, + "name": "activities_pkey", + "entityType": "pks", + "schema": "public", + "table": "activities" + }, + { + "columns": [ + "id", + "expires_at" + ], + "nameExplicit": false, + "name": "sessions_pkey", + "entityType": "pks", + "schema": "public", + "table": "sessions" + }, + { + "columns": [ + "id", + "expires_at" + ], + "nameExplicit": false, + "name": "tokens_pkey", + "entityType": "pks", + "schema": "public", + "table": "tokens" + }, + { + "columns": [ + "id", + "created_at" + ], + "nameExplicit": false, + "name": "seen_by_pkey", + "entityType": "pks", + "schema": "public", + "table": "seen_by" + }, + { + "columns": [ + "id", + "created_at" + ], + "nameExplicit": false, + "name": "unsubscribe_tokens_pkey", + "entityType": "pks", + "schema": "public", + "table": "unsubscribe_tokens" + }, + { + "columns": [ + "entity_type", + "entity_id" + ], + "nameExplicit": false, + "name": "yjs_documents_pkey", + "entityType": "pks", + "schema": "public", + "table": "yjs_documents" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "attachments_pkey", + "schema": "public", + "table": "attachments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "oauth_accounts_pkey", + "schema": "public", + "table": "oauth_accounts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "passkeys_pkey", + "schema": "public", + "table": "passkeys", + "entityType": "pks" + }, + { + "columns": [ + "key" + ], + "nameExplicit": false, + "name": "rate_limits_pkey", + "schema": "public", + "table": "rate_limits", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "totps_pkey", + "schema": "public", + "table": "totps", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "domains_pkey", + "schema": "public", + "table": "domains", + "entityType": "pks" + }, + { + "columns": [ + "context_key" + ], + "nameExplicit": false, + "name": "context_counters_pkey", + "schema": "public", + "table": "context_counters", + "entityType": "pks" + }, + { + "columns": [ + "entity_id" + ], + "nameExplicit": false, + "name": "product_counters_pkey", + "schema": "public", + "table": "product_counters", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "inactive_memberships_pkey", + "schema": "public", + "table": "inactive_memberships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "memberships_pkey", + "schema": "public", + "table": "memberships", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "organizations_pkey", + "schema": "public", + "table": "organizations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "requests_pkey", + "schema": "public", + "table": "requests", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "system_roles_pkey", + "schema": "public", + "table": "system_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tenants_pkey", + "schema": "public", + "table": "tenants", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "emails_pkey", + "schema": "public", + "table": "emails", + "entityType": "pks" + }, + { + "columns": [ + "user_id" + ], + "nameExplicit": false, + "name": "user_counters_pkey", + "schema": "public", + "table": "user_counters", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "users_pkey", + "schema": "public", + "table": "users", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "provider", + "provider_user_id", + "email" + ], + "nullsNotDistinct": false, + "name": "oauth_accounts_provider_provider_user_id_email_unique", + "entityType": "uniques", + "schema": "public", + "table": "oauth_accounts" + }, + { + "nameExplicit": true, + "columns": [ + "tenant_id", + "email", + "context_id" + ], + "nullsNotDistinct": false, + "name": "inactive_memberships_tenant_email_ctx", + "entityType": "uniques", + "schema": "public", + "table": "inactive_memberships" + }, + { + "nameExplicit": true, + "columns": [ + "tenant_id", + "user_id", + "context_id" + ], + "nullsNotDistinct": false, + "name": "memberships_unique_context", + "entityType": "uniques", + "schema": "public", + "table": "memberships" + }, + { + "nameExplicit": true, + "columns": [ + "tenant_id", + "id" + ], + "nullsNotDistinct": false, + "name": "organizations_tenant_id_unique", + "entityType": "uniques", + "schema": "public", + "table": "organizations" + }, + { + "nameExplicit": true, + "columns": [ + "user_id", + "entity_id" + ], + "nullsNotDistinct": false, + "name": "seen_by_user_entity_unique", + "entityType": "uniques", + "schema": "public", + "table": "seen_by" + }, + { + "nameExplicit": false, + "columns": [ + "domain" + ], + "nullsNotDistinct": false, + "name": "domains_domain_key", + "schema": "public", + "table": "domains", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "organizations_slug_key", + "schema": "public", + "table": "organizations", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "nullsNotDistinct": false, + "name": "system_roles_user_id_key", + "schema": "public", + "table": "system_roles", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "emails_email_key", + "schema": "public", + "table": "emails", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "users_slug_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "users_email_key", + "schema": "public", + "table": "users", + "entityType": "uniques" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": [ + "public" + ], + "using": "\n \n COALESCE(current_setting('app.tenant_id', true), '') != ''\n AND \"attachments\".\"tenant_id\" = current_setting('app.tenant_id', true)::text\n\n AND (\"attachments\".\"deleted_at\" IS NULL OR current_setting('app.include_deleted', true) = 'true')\n ", + "withCheck": null, + "name": "attachments_select_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": [ + "public" + ], + "using": null, + "withCheck": "true", + "name": "attachments_insert_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "attachments_update_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "attachments_delete_policy", + "entityType": "policies", + "schema": "public", + "table": "attachments" + }, + { + "as": "PERMISSIVE", + "for": "SELECT", + "roles": [ + "public" + ], + "using": "\n \n COALESCE(current_setting('app.tenant_id', true), '') != ''\n AND \"yjs_documents\".\"tenant_id\" = current_setting('app.tenant_id', true)::text\n\n \n ", + "withCheck": null, + "name": "yjs_documents_select_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + }, + { + "as": "PERMISSIVE", + "for": "INSERT", + "roles": [ + "public" + ], + "using": null, + "withCheck": "true", + "name": "yjs_documents_insert_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + }, + { + "as": "PERMISSIVE", + "for": "UPDATE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "yjs_documents_update_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + }, + { + "as": "PERMISSIVE", + "for": "DELETE", + "roles": [ + "public" + ], + "using": "true", + "withCheck": null, + "name": "yjs_documents_delete_policy", + "entityType": "policies", + "schema": "public", + "table": "yjs_documents" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/backend/scripts/migrations/10-rls.migration.ts b/backend/scripts/migrations/10-rls.migration.ts index 95c90b938..5c109ea29 100644 --- a/backend/scripts/migrations/10-rls.migration.ts +++ b/backend/scripts/migrations/10-rls.migration.ts @@ -29,8 +29,12 @@ async function run() { }); const membershipTableNames = [getTableName(membershipsTable), getTableName(inactiveMembershipsTable)]; - // Pages have no RLS: parentless, always public, and protected by sysAdminGuard. - const noRlsProductEntityNames = ['pages']; + // Products excluded from RLS (parentless, always public, protected by sysAdminGuard). + // Filtered by the tables that actually exist: a fork without these modules must not + // emit grants for nonexistent tables — one failed statement rolls back the whole + // grant block (see the exception handler below). + const noRlsCandidates = ['pages']; + const noRlsProductEntityNames = noRlsCandidates.filter((name) => (entityTableNames as string[]).includes(name)); // Only product entity tables + yjs_documents still use RLS (excluding pages) const additionalRlsTables = [getTableName(yjsDocumentsTable)]; @@ -101,7 +105,10 @@ ${readOnlyTables.map((t) => ` GRANT SELECT ON ${t} TO runtime_role;`).join('\ RAISE NOTICE 'RLS setup complete.'; EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'RLS setup failed: %. Skipping - RLS will not be enforced.', SQLERRM; + -- Fail LOUDLY: swallowing this rolled back ownership, FORCE RLS and every grant in + -- one silent NOTICE — the app then boots with no table grants (every request 403s) + -- or, worse, without enforced RLS. + RAISE EXCEPTION 'RLS setup failed: % (SQLSTATE: %)', SQLERRM, SQLSTATE; END; END $$; `; diff --git a/backend/src/db/utils/context-entity-columns.ts b/backend/src/db/utils/context-entity-columns.ts index 748100a25..5549b93e4 100644 --- a/backend/src/db/utils/context-entity-columns.ts +++ b/backend/src/db/utils/context-entity-columns.ts @@ -1,4 +1,4 @@ -import { uuid, varchar } from 'drizzle-orm/pg-core'; +import { timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import type { ContextEntityType } from 'shared'; import { maxLength } from '#/db/utils/constraints'; import { tenantEntityColumns } from '#/db/utils/tenant-entity-columns'; @@ -14,4 +14,11 @@ export const contextEntityColumns = (entityType: T) bannerUrl: varchar({ length: maxLength.url }), createdBy: uuid().references(() => usersTable.id, { onDelete: 'set null' }), updatedBy: uuid().references(() => usersTable.id, { onDelete: 'set null' }), + /** + * When the context went live for its members: null = draft (invites against it are + * deferred, see membership dispatch). Defaults to creation time, so forks without + * draft flows never see null and the mechanism stays dormant. Distinct from + * `publicAt` (public readability). + */ + publishedAt: timestamp({ mode: 'string' }).defaultNow(), }); diff --git a/backend/src/db/utils/host-relation-columns.ts b/backend/src/db/utils/host-relation-columns.ts deleted file mode 100644 index a571ad4ab..000000000 --- a/backend/src/db/utils/host-relation-columns.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { uuid } from 'drizzle-orm/pg-core'; -import { - appConfig, - type EntityIdColumns, - type EntityType, - type HostEntityType, - hierarchy, - type ProductEntityType, -} from 'shared'; - -/** Column builder type for a nullable uuid host id column. */ -type NullableUuid = ReturnType; - -/** - * Host-entity id column generated for a hosted product (see `host:` in the hierarchy - * builder): e.g. `attachment` with `host: 'task'` gets a nullable `taskId` column. - * Example: `HostRelationColumns<'attachment'>` → `{ taskId: NullableUuid }`. - * - * Nullable by design: a hosted row may exist unhosted (raak: project-level attachments - * without a task). Forks that want strictly-hosted products (e.g. comment → item) add a - * NOT NULL constraint in their migration; the mechanism (cascade, counters) treats null - * as "not hosted". - * - * No FK is generated, the reference is soft, like embedding id arrays: the CDC cascade - * owns lifecycle consistency, and a hard FK would force cross-module table imports. - * Indexes still live in the table definition. - */ -export type HostRelationColumns = EntityIdColumns & EntityType, NullableUuid>; - -/** - * Generates the host id column for a hosted product entity from the hierarchy config. - * Returns an empty object for products without a declared host, so tables can spread it - * unconditionally. - */ -export const hostRelationColumns = (entityType: E): HostRelationColumns => { - const hostType = hierarchy.getHostType(entityType); - const columns: Record = {}; - if (hostType) columns[appConfig.entityIdColumnKeys[hostType]] = uuid(); - return columns as HostRelationColumns; -}; diff --git a/backend/src/lib/activity-bus.ts b/backend/src/lib/activity-bus.ts index 545ee7213..2e4ef5155 100644 --- a/backend/src/lib/activity-bus.ts +++ b/backend/src/lib/activity-bus.ts @@ -25,9 +25,17 @@ const allEventTypes = new Set(activityEventTypes); * passed through to StreamNotification for the client sync engine. * `trace` is backend-internal only (OTel span correlation). */ +/** Per-row batch payload (permission-relevant fields only), mirrored from the CDC wire. */ +export interface ActivityBatchRow { + seq?: number; + rowData: Record; +} + export interface ActivityEvent extends Omit { type: ActivityEventType; rowData: unknown; + /** Per-row permission fields for batch events — dispatch decides per subscriber per row. */ + batchRows?: ActivityBatchRow[] | null; // Sync fields from CDC worker cacheToken: string | null; seq: number | null; diff --git a/backend/src/lib/cdc-websocket.ts b/backend/src/lib/cdc-websocket.ts index 86c6d397e..97c7c3ee5 100644 --- a/backend/src/lib/cdc-websocket.ts +++ b/backend/src/lib/cdc-websocket.ts @@ -31,6 +31,15 @@ const cdcMessageSchema = z.object({ batchUntilSeq: z.number().optional(), }), rowData: z.record(z.string(), z.unknown()), + // Per-row permission fields for batches: dispatch decides per subscriber across all rows + batchRows: z + .array( + z.object({ + seq: z.number().optional(), + rowData: z.record(z.string(), z.unknown()), + }), + ) + .optional(), // Cache token generated by CDC for realtime entities (batch token for batches) cacheToken: z.string().nullable(), // Batch cache reservations: individual entity tokens for backend entity cache @@ -322,6 +331,7 @@ class CdcWebSocketServer { ...message.activity, type, rowData: message.rowData, + batchRows: message.batchRows ?? null, seq: message.activity.seq ?? null, cacheToken: message.cacheToken, batchUntilSeq: message.activity.batchUntilSeq ?? null, diff --git a/backend/src/modules/attachment/attachment-db.ts b/backend/src/modules/attachment/attachment-db.ts index f23b979da..14e61c8ea 100644 --- a/backend/src/modules/attachment/attachment-db.ts +++ b/backend/src/modules/attachment/attachment-db.ts @@ -2,7 +2,6 @@ import { boolean, foreignKey, index, snakeCase, uuid, varchar } from 'drizzle-or import { tenantSelectPolicy, writeThroughPolicies } from '#/db/rls-helpers'; import { maxLength } from '#/db/utils/constraints'; import { contextRelationColumns } from '#/db/utils/context-relation-columns'; -import { hostRelationColumns } from '#/db/utils/host-relation-columns'; import { productEntityColumns } from '#/db/utils/product-entity-columns'; import { organizationsTable } from '#/modules/organization/organization-db'; @@ -14,10 +13,6 @@ export const attachmentsTable = snakeCase.table( 'attachments', { ...productEntityColumns('attachment'), - // Host relation columns (hierarchy `host:`): a nullable Id column when a host is - // declared (raak: attachment -> task); empty in the template hierarchy. Hosted rows - // cascade with their host and feed the host counter. - ...hostRelationColumns('attachment'), public: boolean().notNull().default(false), bucketName: varchar({ length: maxLength.field }).notNull(), /** Upload batch grouping (multi-file uploads shown as one carousel), not ownership. */ diff --git a/backend/src/modules/entities/entities-listeners.ts b/backend/src/modules/entities/entities-listeners.ts index ba87365e7..3cd85e1bf 100644 --- a/backend/src/modules/entities/entities-listeners.ts +++ b/backend/src/modules/entities/entities-listeners.ts @@ -1,7 +1,9 @@ import { appConfig } from 'shared'; -import { activityBus } from '#/lib/activity-bus'; -import { dispatchToAppStream } from '#/modules/entities/helpers/dispatch-to-stream'; +import { activityBus, getEventData } from '#/lib/activity-bus'; +import { type AppStreamSubscriber, dispatchToAppStream } from '#/modules/entities/helpers/dispatch-to-stream'; +import { toMembershipBase } from '#/modules/memberships/helpers/select'; import { log } from '#/utils/logger'; +import { streamSubscriberManager } from './stream'; import type { AppStreamEvent } from './stream/types'; /** @@ -28,6 +30,22 @@ for (const entityType of appConfig.productEntityTypes) { for (const action of ['created', 'updated', 'deleted'] as const) { activityBus.on(`membership.${action}`, async (event) => { if (!event.organizationId) return; + + // Keep connected subscribers' membership snapshots fresh BEFORE dispatch: the + // dispatch decision must mirror the API, which reads live memberships per request. + // (A membership in a NEW org still requires a reconnect — channel registration is + // connect-time — but grants within already-subscribed orgs update live.) + const membership = getEventData(event, 'membership'); + if (membership?.userId) { + const subscribers = streamSubscriberManager.getByChannel(`org:${event.organizationId}`); + for (const subscriber of subscribers) { + if (subscriber.userId !== membership.userId) continue; + const remaining = subscriber.memberships.filter((existing) => existing.id !== membership.id); + subscriber.memberships = + action === 'deleted' ? remaining : [...remaining, toMembershipBase(membership as Record)]; + } + } + try { await dispatchToAppStream(event as AppStreamEvent); } catch (error) { diff --git a/backend/src/modules/entities/helpers/cascade-hosted.ts b/backend/src/modules/entities/helpers/cascade-hosted.ts deleted file mode 100644 index 5c549e3b3..000000000 --- a/backend/src/modules/entities/helpers/cascade-hosted.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { and, eq, inArray, isNull } from 'drizzle-orm'; -import type { AnyPgTable, PgColumn } from 'drizzle-orm/pg-core'; -import { hierarchy, type ProductEntityType } from 'shared'; -import type { AuthContext } from '#/core/context'; -import { entityTables } from '#/tables'; - -/** Structural shape a hosted product table must have for the cascade update. */ -type HostedTable = AnyPgTable & { - id: PgColumn; - deletedAt: PgColumn; - organizationId: PgColumn; -}; - -/** - * Soft-delete all hosted rows of the given host rows (hierarchy `host:` relationships, - * e.g. deleting tasks soft-deletes their attachments). The API-layer half of the - * host lifecycle cascade. The CDC pipeline handles hard-delete suppression and host - * counter deltas from the resulting tombstone events. - * - * Tombstones intentionally flow per hosted row (no event suppression here): delta sync - * clients need each one to drop cached rows. - * - * @returns The soft-deleted row ids per hosted entity type. - */ -export const cascadeSoftDeleteHosted = async ( - ctx: AuthContext, - { - hostType, - hostIds, - deletedAt, - deletedBy, - }: { hostType: ProductEntityType; hostIds: string[]; deletedAt: string; deletedBy: string }, -): Promise>> => { - const { db, organizationId } = ctx.var; - const deleted: Partial> = {}; - if (hostIds.length === 0) return deleted; - - for (const relation of hierarchy.getHostRelations()) { - if (relation.hostType !== hostType) continue; - - const table = entityTables[relation.hostedType as keyof typeof entityTables] as HostedTable; - const hostIdColumn = (table as unknown as Record)[relation.hostIdColumn]; - if (!hostIdColumn) { - throw new Error( - `[HostEntity] Hosted table for "${relation.hostedType}" is missing its host id column "${relation.hostIdColumn}"`, - ); - } - - const rows = await db - .update(table) - .set({ deletedAt, deletedBy, updatedAt: deletedAt, updatedBy: deletedBy } as never) - .where(and(inArray(hostIdColumn, hostIds), eq(table.organizationId, organizationId), isNull(table.deletedAt))) - .returning({ id: table.id }); - - deleted[relation.hostedType as ProductEntityType] = rows.map(({ id }) => id as string); - } - - return deleted; -}; diff --git a/backend/src/modules/entities/helpers/dispatch-to-stream.ts b/backend/src/modules/entities/helpers/dispatch-to-stream.ts index 361831344..ef81a106f 100644 --- a/backend/src/modules/entities/helpers/dispatch-to-stream.ts +++ b/backend/src/modules/entities/helpers/dispatch-to-stream.ts @@ -1,4 +1,5 @@ -import { getEventData } from '#/lib/activity-bus'; +import { appConfig } from 'shared'; +import { type ActivityBatchRow, getEventData } from '#/lib/activity-bus'; import { signCacheToken } from '#/middlewares/entity-cache/token-signer'; import type { MembershipBaseModel } from '#/modules/memberships/helpers/select'; import { checkPermission } from '#/permissions'; @@ -7,7 +8,7 @@ import { log } from '#/utils/logger'; import type { CursoredSubscriber } from '../stream'; import { isMembershipEvent } from '../stream/build-message'; import { createStreamDispatcher } from '../stream/dispatcher'; -import type { AppStreamEvent } from '../stream/types'; +import type { AppStreamEvent, AppStreamProductEvent } from '../stream/types'; /** * App stream subscriber (authenticated). @@ -21,12 +22,75 @@ export interface AppStreamSubscriber extends CursoredSubscriber { memberships: MembershipBaseModel[]; } +/** The membership + admin state a dispatch decision needs (test-friendly subset). */ +export type SubscriberAccess = Pick; + +/** + * SSE must mirror API read visibility: can this subscriber read the event's row? + * + * Runs the SAME engine as API reads (`checkPermission`) with the SAME inputs — the + * event carries the full row (REPLICA IDENTITY FULL), which row conditions and public + * grants evaluate per subscriber. Rows are self-describing, so no second row is ever + * needed. Fail-closed on malformed events. + * + * This decision doubles as cacheToken issuance (a signed token is a read capability + * `appCache` honors without re-running predicates), so over-notifying is never OK. + * + * Forks may add product rules here (e.g. author-only draft rows) before the engine + * check — concepts the engine has no policy vocabulary for. + * + * Exported so the parity property test can assert: SQL predicate ≍ checkPermission ≍ + * this function. + */ +export function canReceiveEntityEvent(subscriber: SubscriberAccess, event: AppStreamProductEvent): boolean { + const row = (event.rowData ?? undefined) as Record | undefined; + + try { + const subject = buildSubject(event.entityType, event, { + id: event.subjectId ?? undefined, + createdBy: (row?.createdBy as string | null | undefined) ?? undefined, + row, + }); + + return checkPermission(subscriber.memberships, 'read', subject, { + isSystemAdmin: subscriber.isSystemAdmin, + userId: subscriber.userId, + }).isAllowed; + } catch { + log.error('Malformed stream event: missing ancestor scope', { + entityType: event.entityType, + subjectId: event.subjectId, + }); + return false; + } +} + +/** + * Rebase a product event onto one of its batch rows: subjectId and every CONTEXT id + * column come from the row itself — rows are self-describing, which also makes + * re-parenting evaluate correctly. + */ +const rowScopedEvent = (event: AppStreamProductEvent, rowData: Record): AppStreamProductEvent => { + const overrides: Record = { rowData }; + if (typeof rowData.id === 'string') overrides.subjectId = rowData.id; + for (const contextType of appConfig.contextEntityTypes) { + const columnKey = appConfig.entityIdColumnKeys[contextType]; + if (columnKey in rowData) overrides[columnKey] = rowData[columnKey]; + } + return { ...event, ...overrides } as AppStreamProductEvent; +}; + +/** The rows an event speaks for: per-row batch payloads, or the single event row. */ +const eventRows = (event: AppStreamProductEvent): ActivityBatchRow[] => + event.batchRows?.length ? event.batchRows : [{ rowData: event.rowData as Record }]; + /** * Dispatch activity events to matching app stream subscribers. * * All events route through org channels: * - Membership events → filtered to affected user - * - Product entity events → filtered by read permission + * - Product entity events → per-ROW read permission (mirrors the API): a subscriber is + * pinged iff they can read at least one row the event speaks for. */ export const dispatchToAppStream = createStreamDispatcher({ getChannel: (event) => `org:${event.organizationId}`, @@ -37,22 +101,11 @@ export const dispatchToAppStream = createStreamDispatcher + canReceiveEntityEvent(subscriber, rowData === event.rowData ? event : rowScopedEvent(event, rowData)), + ); }, transformNotification: (notification, subscriber) => { // Sign cache token per subscriber session diff --git a/backend/src/modules/entities/helpers/recalculate-counters.ts b/backend/src/modules/entities/helpers/recalculate-counters.ts index f19b78d43..cf06ed518 100644 --- a/backend/src/modules/entities/helpers/recalculate-counters.ts +++ b/backend/src/modules/entities/helpers/recalculate-counters.ts @@ -178,25 +178,6 @@ export const recalculateCounters = async (db: DbOrTx) => { ); } - // 4c: Hosted-row counters → context_counters, keyed by host id (hierarchy `host:`, - // e.g. e:attachment per owning task). Live rows only: the CDC deltas decrement on - // soft-delete transitions, so recalculation must exclude tombstones to agree. - for (const relation of hierarchy.getHostRelations()) { - const src = tbl(relation.hostedType as EntityType); - const key = `e:${relation.hostedType}`; - const hostFk = fkCol(relation.hostType); - - await upsertContextCounters( - db, - ` - SELECT h.${hostFk}, jsonb_build_object('${key}', COUNT(*)::int), NOW() - FROM ${src} h - WHERE h.${hostFk} IS NOT NULL AND h.deleted_at IS NULL - GROUP BY h.${hostFk} - `, - ); - } - // Return row counts const [{ contextRows }] = await db .select({ contextRows: sql`count(*)`.mapWith(Number) }) diff --git a/backend/src/modules/entities/stream/dispatch-mirror.test.ts b/backend/src/modules/entities/stream/dispatch-mirror.test.ts new file mode 100644 index 000000000..8da4d9b7e --- /dev/null +++ b/backend/src/modules/entities/stream/dispatch-mirror.test.ts @@ -0,0 +1,171 @@ +import type { SSEStreamingApi } from 'hono/streaming'; +import { afterEach, describe, expect, it } from 'vitest'; +import type { ActivityEvent } from '#/lib/activity-bus'; +import type { AppStreamSubscriber } from '#/modules/entities/helpers/dispatch-to-stream'; +import { dispatchToAppStream } from '#/modules/entities/helpers/dispatch-to-stream'; +import type { MembershipBaseModel } from '#/modules/memberships/helpers/select'; +import type { StreamNotification } from '#/schemas'; +import { streamSubscriberManager } from './subscriber-manager'; +import type { AppStreamEvent } from './types'; + +/** + * Dispatch-mirror behavior on the real dispatcher: pings go to exactly the subscribers + * who can read the event's row(s) — org membership under cella's 2-level config, batches + * per row. Row-level permission parity itself is covered by the three-way property test + * in permissions/row-predicates.test.ts. + */ + +const ORG_A = 'org-dispatch-a'; +const ORG_B = 'org-dispatch-b'; + +const membership = (organizationId: string, role: string, userId: string): MembershipBaseModel => + ({ + id: `mem-organization-${organizationId}-${role}-${userId}`, + userId, + contextType: 'organization', + contextId: organizationId, + organizationId, + role, + }) as unknown as MembershipBaseModel; + +/** Fake SSE subscriber capturing every notification written to its stream. */ +const fakeSubscriber = ( + memberships: MembershipBaseModel[], + userId: string, + organizationIds: string[], + channelOrg: string, +) => { + const received: StreamNotification[] = []; + const stream = { + writeSSE: async ({ data }: { data: string }) => { + received.push(JSON.parse(data)); + }, + } as unknown as SSEStreamingApi; + + const subscriber: AppStreamSubscriber = { + id: crypto.randomUUID(), + channel: `org:${channelOrg}`, + stream, + userId, + sessionToken: 'test-session-token', + organizationIds: new Set(organizationIds), + isSystemAdmin: false, + memberships, + cursor: null, + }; + return { subscriber, received }; +}; + +const attachmentRow = (id: string, organizationId: string, extra: Record = {}) => ({ + id, + organizationId, + createdBy: 'author-user', + ...extra, +}); + +const attachmentEvent = (organizationId: string, overrides: Record): ActivityEvent => + ({ + id: 'activity-1', + type: 'attachment.created', + action: 'create', + entityType: 'attachment', + resourceType: null, + tableName: 'attachments', + subjectId: 'attachment-1', + tenantId: 'tenant-1', + organizationId, + rowData: null, + cacheToken: null, + seq: 7, + batchUntilSeq: null, + propagation: null, + trace: null, + stx: null, + ...overrides, + }) as unknown as ActivityEvent; + +afterEach(() => { + // Fake subscribers are registered per test; drop them so tests stay isolated + for (const org of [ORG_A, ORG_B]) { + for (const subscriber of streamSubscriberManager.getByChannel(`org:${org}`)) { + streamSubscriberManager.unregister(subscriber.id); + } + } +}); + +describe('dispatch mirror: org membership, live snapshots, batches', () => { + it('pings org members; a subscriber whose membership is gone gets nothing despite channel registration', async () => { + const member = fakeSubscriber([membership(ORG_A, 'member', 'member-user')], 'member-user', [ORG_A], ORG_A); + const admin = fakeSubscriber([membership(ORG_A, 'admin', 'admin-user')], 'admin-user', [ORG_A], ORG_A); + // Membership deleted after connect: the listener refreshed the snapshot to empty, + // but channel registration is connect-time — the engine must deny per event. + const stale = fakeSubscriber([], 'stale-user', [ORG_A], ORG_A); + const otherOrg = fakeSubscriber([membership(ORG_B, 'member', 'other-user')], 'other-user', [ORG_B], ORG_B); + for (const { subscriber } of [member, admin, stale, otherOrg]) { + streamSubscriberManager.register(subscriber); + } + + await dispatchToAppStream( + attachmentEvent(ORG_A, { rowData: attachmentRow('attachment-1', ORG_A) }) as AppStreamEvent, + ); + + expect(member.received).toHaveLength(1); // org member: read granted + expect(admin.received).toHaveLength(1); // org admin: read granted + expect(stale.received).toHaveLength(0); // no live membership: engine denies + expect(otherOrg.received).toHaveLength(0); // different org channel entirely + }); + + it('pings a subscriber who can read only a non-representative batch row', async () => { + // Subscriber connected while a member of both orgs (registered on org:B), but the + // org-B membership is gone from the live snapshot. CDC splits batch messages per seq + // context (per org here), so mixed-org rows in one message should not occur on the + // wire — dispatch still evaluates per row and must not assume it. + const { subscriber, received } = fakeSubscriber( + [membership(ORG_A, 'member', 'moved-user')], + 'moved-user', + [ORG_A, ORG_B], + ORG_B, + ); + streamSubscriberManager.register(subscriber); + + // Representative (first) row lives in org B (unreadable); the second row is in org A — + // under representative-row dispatch this subscriber would have been skipped + await dispatchToAppStream( + attachmentEvent(ORG_B, { + seq: 20, + batchUntilSeq: 21, + rowData: attachmentRow('attachment-a', ORG_B), + batchRows: [ + { seq: 20, rowData: attachmentRow('attachment-a', ORG_B) }, + { seq: 21, rowData: attachmentRow('attachment-b', ORG_A) }, + ], + }) as AppStreamEvent, + ); + + expect(received).toHaveLength(1); + }); + + it('does not ping anyone for a batch with no readable rows', async () => { + const { subscriber, received } = fakeSubscriber( + [membership(ORG_A, 'member', 'moved-user')], + 'moved-user', + [ORG_A, ORG_B], + ORG_B, + ); + streamSubscriberManager.register(subscriber); + + await dispatchToAppStream( + attachmentEvent(ORG_B, { + seq: 30, + batchUntilSeq: 31, + rowData: attachmentRow('attachment-a', ORG_B), + batchRows: [ + { seq: 30, rowData: attachmentRow('attachment-a', ORG_B) }, + { seq: 31, rowData: attachmentRow('attachment-b', ORG_B) }, + ], + }) as AppStreamEvent, + ); + + expect(received).toHaveLength(0); + }); +}); diff --git a/backend/src/modules/me/me-queries.ts b/backend/src/modules/me/me-queries.ts index e441d9b87..f7c4c761d 100644 --- a/backend/src/modules/me/me-queries.ts +++ b/backend/src/modules/me/me-queries.ts @@ -1,4 +1,4 @@ -import { and, eq, getColumns, inArray, isNull, sql } from 'drizzle-orm'; +import { and, eq, getColumns, inArray, isNotNull, isNull, sql } from 'drizzle-orm'; import { appConfig } from 'shared'; import type { AuthContext, DbContext } from '#/core/context'; import { sessionsTable } from '#/modules/auth/sessions-db'; @@ -142,6 +142,9 @@ export const findPendingInvitations = async (ctx: DbContext, { userId }: FindPen eq(inactiveMembershipsTable.contextType, entityType), eq(inactiveMembershipsTable.userId, userId), isNull(inactiveMembershipsTable.rejectedAt), + // Invites against an unpublished (draft) context are deferred: hidden from the + // invitee until the context is published and the invite is dispatched. + isNotNull(cols.publishedAt), ), ); }), diff --git a/backend/src/modules/memberships/helpers/deferred-invites.ts b/backend/src/modules/memberships/helpers/deferred-invites.ts new file mode 100644 index 000000000..03d25f065 --- /dev/null +++ b/backend/src/modules/memberships/helpers/deferred-invites.ts @@ -0,0 +1,114 @@ +import { appConfig, type ContextEntityType } from 'shared'; +import { nanoid } from 'shared/utils/nanoid'; +import type { AuthContext } from '#/core/context'; +import { mailer } from '#/lib/mailer'; +import { resolveEntity } from '#/modules/entities/entities-queries'; +import { + findPendingInactiveMembershipsByContexts, + insertTokens, + stampInactiveMembershipsReminded, + updateInactiveMembershipToken, +} from '#/modules/memberships/memberships-queries'; +import { log } from '#/utils/logger'; +import { encodeLowerCased } from '#/utils/oslo'; +import { slugFromEmail } from '#/utils/slug-from-email'; +import { createDate, TimeSpan } from '#/utils/time-span'; +import { memberInviteEmail, memberInviteWithTokenEmail } from '../../../../emails'; + +interface DispatchDeferredInvitesOpts { + /** Context entity ids whose pending invites should be dispatched (e.g. a published course + descendants). */ + contextIds: string[]; +} + +/** + * Dispatch invites that were deferred while their context was unpublished: send the held + * emails through the normal invitation flow and stamp `remindedAt`. Invitation tokens are + * rotated (fresh secret + expiry) since raw tokens are unrecoverable and may have expired + * while the context was a draft. Rows emailed within the last 7 days are skipped (legacy + * re-invite throttle). + */ +export async function dispatchDeferredInvites(ctx: AuthContext, { contextIds }: DispatchDeferredInvitesOpts) { + const user = ctx.var.user; + + const pendingRows = await findPendingInactiveMembershipsByContexts(ctx, { contextIds }); + + // 7-day throttle on last dispatch; deferred rows have remindedAt null → always due + const throttleBefore = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + const dueRows = pendingRows.filter((row) => !row.remindedAt || new Date(row.remindedAt) < throttleBefore); + if (!dueRows.length) return { dispatched: 0 }; + + const lng = appConfig.defaultLanguage; + const senderName = user.name; + const senderThumbnailUrl = user.thumbnailUrl; + + // Group per context+role: each email batch shares entityName + role static props + const groups = new Map(); + for (const row of dueRows) { + const key = `${row.contextType}:${row.contextId}:${row.role}`; + const group = groups.get(key) ?? []; + group.push(row); + groups.set(key, group); + } + + const dispatchedIds: string[] = []; + + for (const group of groups.values()) { + const { contextType, contextId, role } = group[0]; + const entity = await resolveEntity(ctx, contextType as ContextEntityType, contextId); + if (!entity) continue; + + const staticProps = { senderName, senderThumbnailUrl, role, entityName: entity.name }; + const entityLink = `${appConfig.frontendUrl}/${contextType}/${entity.slug}`; + + const withTokenRecipients: Array<{ email: string; lng: string; name: string; inviteLink: string }> = []; + const noTokenRecipients: Array<{ email: string; lng: string; name: string; memberInviteLink: string }> = []; + + for (const row of group) { + if (row.tokenId) { + // Rotate the invitation token: fresh secret + expiry, re-pointed from the invite row + const raw = nanoid(40); + const [token] = await insertTokens(ctx, { + tokens: [ + { + secret: encodeLowerCased(raw), + type: 'invitation' as const, + email: row.email, + createdBy: row.createdBy, + expiresAt: createDate(new TimeSpan(7, 'd')), + inactiveMembershipId: row.id, + }, + ], + }); + await updateInactiveMembershipToken(ctx, { id: row.id, tokenId: token.id }); + + withTokenRecipients.push({ + email: row.email, + lng, + name: slugFromEmail(row.email), + inviteLink: `${appConfig.backendAuthUrl}/invoke-token/invitation/${raw}`, + }); + } else { + noTokenRecipients.push({ + email: row.email, + lng, + name: slugFromEmail(row.email), + memberInviteLink: entityLink, + }); + } + dispatchedIds.push(row.id); + } + + if (withTokenRecipients.length > 0) { + await mailer.prepareEmails(memberInviteWithTokenEmail, staticProps, withTokenRecipients, user.email); + } + if (noTokenRecipients.length > 0) { + await mailer.prepareEmails(memberInviteEmail, staticProps, noTokenRecipients, user.email); + } + } + + await stampInactiveMembershipsReminded(ctx, { ids: dispatchedIds, remindedAt: new Date().toISOString() }); + + log.info('Deferred invites dispatched', { count: dispatchedIds.length, contexts: contextIds.length }); + + return { dispatched: dispatchedIds.length }; +} diff --git a/backend/src/modules/memberships/helpers/membership-helpers.test.ts b/backend/src/modules/memberships/helpers/membership-helpers.test.ts new file mode 100644 index 000000000..4a67659a6 --- /dev/null +++ b/backend/src/modules/memberships/helpers/membership-helpers.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { resolveParentMembershipRole } from './membership-helpers'; + +/** + * Auto-created parent/associated membership roles (multi-vocabulary support): the + * least-privileged fitting role by default — 'member' when the vocabulary has it, + * identical to the previous hardcoded behavior — or the invited role carried over + * when carryRole is set and valid. Multi-vocabulary branches (no 'member' in the + * vocabulary, invalid carried roles) are exercised end-to-end by forks with nested + * contexts; cella's single vocabulary covers the default paths here. + */ +describe('resolveParentMembershipRole', () => { + it("defaults to 'member' when the vocabulary has it (previous hardcoded behavior)", () => { + expect(resolveParentMembershipRole('organization', 'admin')).toBe('member'); + }); + + it('carries the invited role over when carryRole is set and valid', () => { + expect(resolveParentMembershipRole('organization', 'admin', true)).toBe('admin'); + }); + + it('ignores carryRole for the default path when the invited role equals member', () => { + expect(resolveParentMembershipRole('organization', 'member', true)).toBe('member'); + }); +}); diff --git a/backend/src/modules/memberships/helpers/membership-helpers.ts b/backend/src/modules/memberships/helpers/membership-helpers.ts index e37691d66..022709478 100644 --- a/backend/src/modules/memberships/helpers/membership-helpers.ts +++ b/backend/src/modules/memberships/helpers/membership-helpers.ts @@ -1,5 +1,6 @@ import { inArray, max } from 'drizzle-orm'; import { appConfig, type ContextEntityType, hierarchy } from 'shared'; +import type { MenuStructureItem } from 'shared/config-builder'; import { defaultOrder, orderGap } from 'shared/utils/display-order'; import type { DbContext } from '#/core/context'; import type { EntityModel } from '#/modules/entities/entities-queries'; @@ -19,6 +20,23 @@ import { log } from '#/utils/logger'; const rootContextType = hierarchy.contextTypes.find((t) => hierarchy.getParent(t) === null)!; const rootIdColumnKey = appConfig.entityIdColumnKeys[rootContextType]; +/** + * Role for an auto-created parent/associated membership. Defaults to the least-privileged + * fitting role: `member` when the target vocabulary has it, else the vocabulary's last role + * (roles are declared most → least privileged). With `carryRole` (menuStructure), the invited + * role carries over when valid in the target vocabulary. + */ +export const resolveParentMembershipRole = ( + contextType: ContextEntityType, + invitedRole: MembershipModel['role'], + carryRole = false, +): MembershipModel['role'] => { + const contextRoles = hierarchy.getRoles(contextType) as readonly MembershipModel['role'][]; + if (carryRole && contextRoles.includes(invitedRole)) return invitedRole; + if (contextRoles.includes('member' as MembershipModel['role'])) return 'member' as MembershipModel['role']; + return contextRoles[contextRoles.length - 1]; +}; + type BaseEntityModel = EntityModel & { [key: string]: unknown; tenantId: string; // Required for RLS @@ -142,7 +160,8 @@ export const insertMemberships = async ( return { ...baseMembership, tenantId: entity.tenantId, - role: 'member', // parent membership is always 'member' + // parent membership defaults to the least-privileged fitting role ('member' in cella) + role: resolveParentMembershipRole(rootContextType, baseMembership.role), [rootIdColumnKey]: targetEntitiesIdColumnKeys[rootIdColumnKey], contextType: rootContextType, contextId: targetEntitiesIdColumnKeys[rootIdColumnKey], @@ -170,7 +189,13 @@ export const insertMemberships = async ( return { ...baseMembership, tenantId: entity.tenantId, - role: 'member', // parent/associated membership is always 'member' + // associated membership role: least-privileged fit, or carried over when carryRole is set + role: resolveParentMembershipRole( + associatedType as ContextEntityType, + baseMembership.role, + // Config literals only carry the property when a fork sets it + 'carryRole' in relation ? (relation as MenuStructureItem).carryRole : undefined, + ), ...remainingIdColumnKeys, contextType: associatedType, contextId: associatedField, diff --git a/backend/src/modules/memberships/inactive-memberships-db.ts b/backend/src/modules/memberships/inactive-memberships-db.ts index de59a183e..56c27b3ba 100644 --- a/backend/src/modules/memberships/inactive-memberships-db.ts +++ b/backend/src/modules/memberships/inactive-memberships-db.ts @@ -26,6 +26,12 @@ export const inactiveMembershipsTable = snakeCase.table( tokenId: uuid(), // References tokens.id logically (no FK due to partitioning) role: varchar({ enum: roleEnum }).notNull().default('member'), rejectedAt: timestamp({ mode: 'string' }), + /** + * Last invite/reminder email dispatch for this row; null when never dispatched (e.g. + * deferred while the target context was unpublished). Reminder emails are throttled + * against remindedAt ?? createdAt. + */ + remindedAt: timestamp({ mode: 'string' }), createdBy: uuid() .notNull() .references(() => usersTable.id, { onDelete: 'cascade' }), diff --git a/backend/src/modules/memberships/memberships-mocks.ts b/backend/src/modules/memberships/memberships-mocks.ts index e63ab5973..76780a8d6 100644 --- a/backend/src/modules/memberships/memberships-mocks.ts +++ b/backend/src/modules/memberships/memberships-mocks.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { appConfig, type ContextEntityType, type EntityRole, roles } from 'shared'; +import { appConfig, type ContextEntityType, type EntityRole, hierarchy, roles } from 'shared'; import { generateMockContextIdColumns, MOCK_REF_DATE, @@ -76,7 +76,8 @@ export const mockContextMembership = ( ...contextEntityColumns, [appConfig.entityIdColumnKeys[contextType]]: contextEntity.id, // Set the correct context entity ID ...overrideIds, - role: faker.helpers.arrayElement(roles.all), + // Pick from the context's own role vocabulary (e.g. course → staff/student/guest) + role: faker.helpers.arrayElement(hierarchy.getRoles(contextType)), displayOrder: getMembershipOrderOffset(contextEntity.id) * 10, createdAt: mockPastIsoDate(), createdBy: userId, @@ -157,6 +158,7 @@ export const mockInactiveMembership = (key = 'inactive-membership:default'): Ina tokenId, role: faker.helpers.arrayElement(roles.all), rejectedAt: null, + remindedAt: null, createdAt, createdBy: mockUuid(), ...contextEntityColumns, diff --git a/backend/src/modules/memberships/memberships-queries.ts b/backend/src/modules/memberships/memberships-queries.ts index 27d4fe8db..d89893ebb 100644 --- a/backend/src/modules/memberships/memberships-queries.ts +++ b/backend/src/modules/memberships/memberships-queries.ts @@ -1,4 +1,4 @@ -import { and, count, eq, ilike, inArray, or, type SQL, sql } from 'drizzle-orm'; +import { and, count, eq, ilike, inArray, isNull, or, type SQL, sql } from 'drizzle-orm'; import { alias } from 'drizzle-orm/pg-core'; import type { ContextEntityType, EntityRole } from 'shared'; import type { AuthContext, DbContext } from '#/core/context'; @@ -73,6 +73,9 @@ export const findMembershipAwareRows = async ( language: usersTable.language, membershipId: membershipsTable.id, inactiveMembershipId: inactiveMembershipsTable.id, + // Last dispatch timestamps for the reminder throttle (remindedAt ?? createdAt) + inactiveMembershipCreatedAt: inactiveMembershipsTable.createdAt, + inactiveMembershipRemindedAt: inactiveMembershipsTable.remindedAt, orgMembershipId: orgMemberships.id, tokenId: tokensTable.id, }) @@ -109,6 +112,38 @@ export const findMembershipAwareRows = async ( .where(and(inArray(emailsTable.email, emails))); }; +/** Pending (not rejected) inactive memberships for a set of contexts (deferred-invite dispatch). */ +export const findPendingInactiveMembershipsByContexts = async ( + ctx: DbContext, + { contextIds }: { contextIds: string[] }, +) => { + const { db } = ctx.var; + if (!contextIds.length) return []; + return db + .select() + .from(inactiveMembershipsTable) + .where(and(inArray(inactiveMembershipsTable.contextId, contextIds), isNull(inactiveMembershipsTable.rejectedAt))); +}; + +/** Stamp remindedAt (last email dispatch) on inactive memberships. */ +export const stampInactiveMembershipsReminded = async ( + ctx: DbContext, + { ids, remindedAt }: { ids: string[]; remindedAt: string }, +) => { + const { db } = ctx.var; + if (!ids.length) return; + return db.update(inactiveMembershipsTable).set({ remindedAt }).where(inArray(inactiveMembershipsTable.id, ids)); +}; + +/** Point an inactive membership at a fresh invitation token (rotation at deferred dispatch). */ +export const updateInactiveMembershipToken = async ( + ctx: DbContext, + { id, tokenId }: { id: string; tokenId: string }, +) => { + const { db } = ctx.var; + return db.update(inactiveMembershipsTable).set({ tokenId }).where(eq(inactiveMembershipsTable.id, id)); +}; + interface FindMembershipByIdInOrgOpts { membershipId: string; } diff --git a/backend/src/modules/memberships/operations/create-memberships.ts b/backend/src/modules/memberships/operations/create-memberships.ts index 85509bb5b..f8ae0a4b5 100644 --- a/backend/src/modules/memberships/operations/create-memberships.ts +++ b/backend/src/modules/memberships/operations/create-memberships.ts @@ -12,6 +12,7 @@ import { findMembershipAwareRows, insertInactiveMemberships, insertTokens, + stampInactiveMembershipsReminded, } from '#/modules/memberships/memberships-queries'; import { getValidContextEntity } from '#/permissions/get-context-entity'; import { log } from '#/utils/logger'; @@ -40,10 +41,24 @@ export async function createMembershipsOp(ctx: AuthContext, input: CreateMembers const normalizedEmails = [...new Set(emails.map((e: string) => e.toLowerCase().trim()))]; if (!normalizedEmails.length) throw new AppError(400, 'no_recipients', 'warn'); + // The invited role must exist in the target context's vocabulary (e.g. no org 'member' on a course) + if (!hierarchy.getRoles(entityType).includes(role)) { + throw new AppError(400, 'invalid_role', 'warn', { entityType }); + } + const { entity } = await getValidContextEntity(ctx, entityId, entityType, 'update'); const { slug: entitySlug, name: entityName } = entity; + /** + * Draft context (publishedAt null): invites are recorded (inactive memberships + tokens) + * but email dispatch is held and existing users are not added directly — everything is + * released when the context is published. The context's most-privileged role (first in + * its vocabulary, e.g. admin/staff/owner) stays live so staff can collaborate in drafts. + */ + const contextIsDraft = entity.publishedAt === null; + const deferDispatch = contextIsDraft && role !== hierarchy.getRoles(entityType)[0]; + const currentOrgMemberships = await countMembershipsByContext(ctx, { contextType: rootContextType, contextId: organization.id, @@ -84,6 +99,10 @@ export async function createMembershipsOp(ctx: AuthContext, input: CreateMembers for (const e of normalizedEmails) rowsByEmail.set(e, []); for (const r of membershipAwareRows) rowsByEmail.get(r.email)?.push(r); + // Reminder throttle: a pending invite is re-emailed at most once per 7 days + const reminderThrottleBefore = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + const remindedInactiveMembershipIds: string[] = []; + for (const email of normalizedEmails) { const rows = rowsByEmail.get(email)!; @@ -97,7 +116,15 @@ export async function createMembershipsOp(ctx: AuthContext, input: CreateMembers } if (hasUserInactiveMembership || hasTokenInvite) { - reminderEmails.push(email); + // No reminders against a draft context; otherwise throttle on last dispatch + const inactiveRow = rows.find((r) => r.inactiveMembershipId); + const lastDispatch = inactiveRow?.inactiveMembershipRemindedAt ?? inactiveRow?.inactiveMembershipCreatedAt; + const throttled = !!lastDispatch && new Date(lastDispatch) >= reminderThrottleBefore; + + if (!deferDispatch && !throttled) { + reminderEmails.push(email); + if (inactiveRow?.inactiveMembershipId) remindedInactiveMembershipIds.push(inactiveRow.inactiveMembershipId); + } continue; } @@ -110,7 +137,8 @@ export async function createMembershipsOp(ctx: AuthContext, input: CreateMembers } else { const hasActiveOrgMembership = entityType !== rootContextType && !!rows.find((r) => r.orgMembershipId); - if (hasActiveOrgMembership) { + // Draft context: existing users are deferred too — no membership, no nav entry, no email + if (hasActiveOrgMembership && !deferDispatch) { existingUsersToDirectAdd.push({ userId: userRow.userId, email }); } else { existingUsersToActivate.push({ userId: userRow.userId, email }); @@ -230,7 +258,8 @@ export async function createMembershipsOp(ctx: AuthContext, input: CreateMembers const staticProps = { senderName, senderThumbnailUrl, role, entityName }; - if (noTokenRecipients.length > 0) { + // Draft context: hold every email — deferred invites are dispatched at publish + if (!deferDispatch && noTokenRecipients.length > 0) { await mailer.prepareEmails(memberInviteEmail, staticProps, noTokenRecipients, user.email); } @@ -242,14 +271,22 @@ export async function createMembershipsOp(ctx: AuthContext, input: CreateMembers entityLink, })); - if (directAdditionRecipients.length > 0) { + if (!deferDispatch && directAdditionRecipients.length > 0) { await mailer.prepareEmails(memberAddedEmail, staticProps, directAdditionRecipients, user.email); } - if (withTokenRecipients.length > 0) { + if (!deferDispatch && withTokenRecipients.length > 0) { await mailer.prepareEmails(memberInviteWithTokenEmail, staticProps, withTokenRecipients, user.email); } + // Track reminder dispatch for the 7-day throttle + if (!deferDispatch && remindedInactiveMembershipIds.length > 0) { + await stampInactiveMembershipsReminded(ctx, { + ids: remindedInactiveMembershipIds, + remindedAt: new Date().toISOString(), + }); + } + const invitesSentCount = insertedInactiveMemberships.length; log.info('Users invited on entity level', { diff --git a/backend/src/modules/memberships/operations/update-membership.ts b/backend/src/modules/memberships/operations/update-membership.ts index e636ceefd..092775d82 100644 --- a/backend/src/modules/memberships/operations/update-membership.ts +++ b/backend/src/modules/memberships/operations/update-membership.ts @@ -1,4 +1,4 @@ -import type { EntityRole } from 'shared'; +import { type EntityRole, hierarchy } from 'shared'; import { getEdgeOrder } from 'shared/utils/display-order'; import type { AuthContext } from '#/core/context'; import { AppError } from '#/core/error'; @@ -31,6 +31,11 @@ export async function updateMembershipOp(ctx: AuthContext, membershipId: string, const updatedType = membershipToUpdate.contextType; + // The new role must exist in the context's vocabulary (e.g. no org 'member' on a course) + if (role !== undefined && !hierarchy.getRoles(updatedType).includes(role)) { + throw new AppError(400, 'invalid_role', 'warn', { entityType: updatedType }); + } + await getValidContextEntity(ctx, membershipToUpdate.contextId, updatedType, role ? 'update' : 'read'); if (archived !== undefined && archived !== membershipToUpdate.archived) { diff --git a/backend/src/modules/organization/organization-mocks.ts b/backend/src/modules/organization/organization-mocks.ts index 23f11bc2c..090a4079d 100644 --- a/backend/src/modules/organization/organization-mocks.ts +++ b/backend/src/modules/organization/organization-mocks.ts @@ -59,6 +59,7 @@ const generateOrganizationBase = (id: string, tenantId: string, name: string, cr welcomeText: `Welcome to ${name}!`, authStrategies: ['passkey'] as AuthStrategy[], chatSupport: faker.datatype.boolean(), + publishedAt: createdAt, createdAt, createdBy: null, updatedAt: createdAt, diff --git a/backend/src/modules/seen/operations/get-unseen-counts.ts b/backend/src/modules/seen/operations/get-unseen-counts.ts index 898163374..101405d4c 100644 --- a/backend/src/modules/seen/operations/get-unseen-counts.ts +++ b/backend/src/modules/seen/operations/get-unseen-counts.ts @@ -1,8 +1,26 @@ -import { hierarchy } from 'shared'; +import { getColumns, type SQL } from 'drizzle-orm'; +import type { PgColumn } from 'drizzle-orm/pg-core'; +import { appConfig, hierarchy, type SeenTrackedEntityType } from 'shared'; import type { AuthContext } from '#/core/context'; import { tenantRead } from '#/db/tenant-context'; import { groupingContextTypes, seenWindowMs, trackedEntityTypes } from '#/modules/seen/operations/mark-seen'; import { findUnseenCountsByUser } from '#/modules/seen/seen-queries'; +import { resolveCollectionReadFilter } from '#/permissions/collection-scope'; +import { buildCollectionReadWhere } from '#/permissions/row-predicates'; +import { getEntityTable } from '#/tables'; + +/** Sub-context column for the read predicate: the parent-level id column, org fallback. */ +const subContextColumn = (entityType: SeenTrackedEntityType): PgColumn => { + const table = getEntityTable(entityType); + const columns = getColumns(table) as Record; + const parent = hierarchy.getParent(entityType); + const parentColumn = parent + ? columns[appConfig.entityIdColumnKeys[parent as keyof typeof appConfig.entityIdColumnKeys]] + : undefined; + const column = parentColumn ?? columns.organizationId; + if (!column) throw new Error(`[Seen] No sub-context column for "${entityType}"`); + return column; +}; export async function getUnseenCountsOp(ctx: AuthContext) { const user = ctx.var.user; @@ -16,38 +34,52 @@ export async function getUnseenCountsOp(ctx: AuthContext) { // Any tracked type with no parent groups by org → org ids join the context id set. const needsOrgFallback = trackedEntityTypes.some((t) => !hierarchy.getParent(t)); - // Group the user's context ids by tenant. Entity tables have FORCE ROW LEVEL SECURITY with a - // tenant-scoped policy, so the count must run inside tenantRead (sets app.tenant_id) — a plain - // baseDb read returns zero rows. A user's memberships can span tenants, so we read per tenant. - const contextIdsByTenant = new Map>(); - const addContextId = (tenantId: string, contextId: string) => { - const set = contextIdsByTenant.get(tenantId) ?? new Set(); - set.add(contextId); - contextIdsByTenant.set(tenantId, set); - }; + // Group the user's context ids by ORG (mirror rule: read scopes are org-scoped, so the + // count runs per org with that org's predicate). Entity tables have FORCE ROW LEVEL + // SECURITY with a tenant-scoped policy, so each count runs inside tenantRead. + const orgGroups = new Map }>(); for (const m of memberships) { - if (groupingContextTypes.has(m.contextType)) addContextId(m.tenantId, m.contextId); - if (needsOrgFallback) addContextId(m.tenantId, m.organizationId); + const group = orgGroups.get(m.organizationId) ?? { tenantId: m.tenantId, contextIds: new Set() }; + if (groupingContextTypes.has(m.contextType)) group.contextIds.add(m.contextId); + if (needsOrgFallback) group.contextIds.add(m.organizationId); + orgGroups.set(m.organizationId, group); } - if (contextIdsByTenant.size === 0) { + if (orgGroups.size === 0) { return {}; } const windowCutoff = new Date(Date.now() - seenWindowMs).toISOString(); const results: Record> = {}; - // Count tracked entities created within the rolling window that this user has not seen, grouped - // by home context. A single NOT EXISTS per type — exact, no total-minus-seen skew. Rows older - // than seen_by retention never participate: they cannot be marked seen, so scoping the count to - // the same window is what lets badges reach zero and stay there. - for (const [tenantId, contextIdSet] of contextIdsByTenant) { + // Per org: compose each tracked type's collection read predicate (same compiler as + // list endpoints) so badges only count rows this user can actually fetch — the seen + // counter is a change signal that must mirror the feed, never a wider number. + for (const [organizationId, { tenantId, contextIds }] of orgGroups) { + const scopeWhereByType: Partial> = {}; + const readableTypes: SeenTrackedEntityType[] = []; + + for (const entityType of trackedEntityTypes) { + const readFilter = resolveCollectionReadFilter(memberships, entityType, organizationId); + const scopeWhere = buildCollectionReadWhere( + readFilter, + getEntityTable(entityType), + subContextColumn(entityType), + user.id, + ); + if (scopeWhere.kind === 'none') continue; + readableTypes.push(entityType); + if (scopeWhere.kind === 'where') scopeWhereByType[entityType] = scopeWhere.where; + } + if (readableTypes.length === 0) continue; + const unseenRows = await tenantRead({ var: { ...ctx.var, tenantId } } as AuthContext, (readCtx) => findUnseenCountsByUser(readCtx, { userId: user.id, - contextIds: [...contextIdSet], - entityTypes: trackedEntityTypes, + contextIds: [...contextIds], + entityTypes: readableTypes, cutoff: windowCutoff, + scopeWhereByType, }), ); diff --git a/backend/src/modules/seen/seen-queries.ts b/backend/src/modules/seen/seen-queries.ts index 6ec5f0d8b..6bb49da03 100644 --- a/backend/src/modules/seen/seen-queries.ts +++ b/backend/src/modules/seen/seen-queries.ts @@ -17,15 +17,24 @@ interface FindUnseenCountsByUserOpts { contextIds: string[]; entityTypes: readonly SeenTrackedEntityType[]; cutoff: string; + /** + * Per-type collection read predicate (same compiler as list endpoints), so badges + * only count rows the user can actually fetch. `undefined` value = unrestricted + * scope for that type; a type absent from the record is counted unrestricted too + * (callers should pre-drop types whose scope resolved to `none`). + */ + scopeWhereByType?: Partial>; } /** * Count tracked entities the user has NOT seen, grouped by their home context. * * "Unseen" = live tracked rows created within the seen window (`cutoff`) that have no seen_by - * record for this user. Computed per entity type as a single NOT EXISTS query — exact per-entity, - * so no total-minus-seen aggregate skew (a recently-viewed row that just aged out of the window - * can no longer cancel a different in-window row). + * record for this user, restricted to rows the user's read scope can fetch (mirror rule: the + * badge counts exactly what the corresponding list endpoint would return). Computed per entity + * type as a single NOT EXISTS query — exact per-entity, so no total-minus-seen aggregate skew + * (a recently-viewed row that just aged out of the window can no longer cancel a different + * in-window row). * * The window lives only here, on entity.createdAt. seen_by is retention-bounded (pg_partman) to * the same 90 days on view time, and because viewTime >= createdAt, a seen row can only be dropped @@ -33,7 +42,7 @@ interface FindUnseenCountsByUserOpts { */ export const findUnseenCountsByUser = async ( ctx: DbContext, - { userId, contextIds, entityTypes, cutoff }: FindUnseenCountsByUserOpts, + { userId, contextIds, entityTypes, cutoff, scopeWhereByType }: FindUnseenCountsByUserOpts, ) => { const { db } = ctx.var; const rows: { contextId: string; entityType: string; unseenCount: number }[] = []; @@ -58,6 +67,9 @@ export const findUnseenCountsByUser = async ( sql`NOT EXISTS (SELECT 1 FROM ${seenByTable} WHERE ${seenByTable.userId} = ${userId} AND ${seenByTable.entityId} = ${orgTable.id})`, ]; if (columns.deletedAt) filters.push(isNull(columns.deletedAt)); + // Forks add feed-parity filters here (e.g. excluding draft rows the feed hides). + const scopeWhere = scopeWhereByType?.[entityType]; + if (scopeWhere) filters.push(scopeWhere); const entityRows = await db .select({ diff --git a/backend/src/permissions/collection-scope.ts b/backend/src/permissions/collection-scope.ts index 9618bca4d..0f8406f00 100644 --- a/backend/src/permissions/collection-scope.ts +++ b/backend/src/permissions/collection-scope.ts @@ -2,16 +2,15 @@ import { type AccessPolicies, accessPolicies, type ContextEntityType, + elevatedRoles as configuredElevatedRoles, getPolicyPermissions, getSubjectPolicies, hierarchy, isRowCondition, type NormalizedPermissionValue, + type PermissionTopology, type ProductEntityType, type RowCondition, - type RowRestriction, - type RowRestrictions, - rowRestrictions, } from 'shared'; import { AppError } from '#/core/error'; import type { MembershipBaseModel } from '#/modules/memberships/helpers/select'; @@ -32,40 +31,60 @@ const roleReadValue = ( * A row-conditional slice of the caller's readable scope: rows within `subContextIds` * (undefined = org-wide) are readable only where `condition` matches. * Compiled to SQL by `buildCollectionReadWhere` (`row-predicates.ts`). + * `contextType` is the grant's level; absent = the entity's home context (sub-context + * column) — the shape every pre-deep-chain caller produced. */ export interface ConditionalScope { condition: RowCondition; subContextIds: string[] | undefined; + contextType?: ContextEntityType; + /** Home-scoped grant (elevatedRoles): these levels' columns must be NULL as well. */ + deeperContexts?: ContextEntityType[]; } /** - * A restricted membership grant: rows within `subContextIds` (undefined = org-wide) are - * readable only where the entity's row restriction qualifies THIS grant (its context - * level vs `visibilityDepth`, its role vs `audienceRoles`). Non-exempt grants of a - * restricted entity always resolve here instead of into the merged unconditional scope. + * An unconditional grant at an intermediate ancestor level (deep chains only, e.g. + * course/courseSection between organization and an item's home project): rows are + * scoped by THAT level's own id column. Root grants stay org-wide and home grants + * stay in `subContextIds`, so two-level forks never produce these. With `elevatedRoles` + * configured, only subtree-scoped roles land here. */ -export interface RestrictedGrantScope { +export interface AncestorScope { contextType: ContextEntityType; - role: string; - subContextIds: string[] | undefined; + subContextIds: string[]; } -/** Restriction context for SQL compilation, resolved once per filter. */ -export interface RestrictedScope { - restriction: RowRestriction; - /** The entity's ancestor chain (most specific first) for depth qualification. */ - orderedContexts: ContextEntityType[]; - grants: RestrictedGrantScope[]; +/** + * A HOME-scoped unconditional grant (elevatedRoles): rows homed exactly at this level — + * that level's id column matches AND every more-specific ancestor column is NULL. + * Produced only when `elevatedRoles` is configured, for roles outside the list. + */ +export interface HomeScope { + contextType: ContextEntityType; + subContextIds: string[]; + /** The chain levels more specific than `contextType` (their columns must be NULL). */ + deeperContexts: ContextEntityType[]; } /** Accumulator for scope resolution: unconditional ids + per-condition ids, org-wide flags. */ interface ScopeAccumulator { unconditionalOrgWide: boolean; unconditionalIds: Set; - /** Keyed by condition name; conditions sharing a name must be the same rule. */ - conditional: Map }>; - /** Keyed by `${contextType}:${role}`; only populated when the entity has a row restriction. */ - restrictedGrants: Map }>; + /** Unconditional grants at intermediate ancestor levels (deep chains), keyed by context type. */ + ancestorUnconditional: Map>; + /** HOME-scoped unconditional grants (elevatedRoles), keyed by context type. */ + homeScoped: Map>; + /** Keyed by `${condition name}:${level}:${homeOnly}`; conditions sharing a name must be the same rule. */ + conditional: Map< + string, + { + condition: RowCondition; + contextType?: ContextEntityType; + homeOnly: boolean; + orgWide: boolean; + ids: Set; + } + >; } /** @@ -82,64 +101,95 @@ const resolveScopes = ( memberships: MembershipBaseModel[], entityType: ProductEntityType, organizationId: string, - restriction: RowRestriction | undefined, + elevatedRoles: readonly string[] | undefined, + ancestors: readonly ContextEntityType[], // most-specific → root, e.g. [project, course, organization] ): ScopeAccumulator => { - const ancestors = hierarchy.getOrderedAncestors(entityType); // most-specific → root, e.g. [project, organization] const rootContext = ancestors.at(-1) ?? null; // organization - const subContextType = ancestors.find((context) => context !== rootContext) ?? null; // project + const subContextType = ancestors.find((context) => context !== rootContext) ?? null; // home context, e.g. project + + // Grant scoping: with elevatedRoles configured, a non-elevated role's grant speaks only + // for rows HOMED at its level. Grants at the deepest level are home-exact by + // construction; root/intermediate grants of non-elevated roles become home-scoped. + const isHomeScopedGrant = (contextType: ContextEntityType, role: string): boolean => + elevatedRoles !== undefined && !elevatedRoles.includes(role) && contextType !== subContextType; const acc: ScopeAccumulator = { unconditionalOrgWide: false, unconditionalIds: new Set(), + ancestorUnconditional: new Map(), + homeScoped: new Map(), conditional: new Map(), - restrictedGrants: new Map(), - }; - - const addConditional = (condition: RowCondition, contextId: string | null) => { - const entry = acc.conditional.get(condition.name) ?? { condition, orgWide: false, ids: new Set() }; - if (contextId === null) entry.orgWide = true; - else entry.ids.add(contextId); - acc.conditional.set(condition.name, entry); }; - const addRestrictedGrant = (contextType: ContextEntityType, role: string, contextId: string | null) => { - const key = `${contextType}:${role}`; - const entry = acc.restrictedGrants.get(key) ?? { contextType, role, orgWide: false, ids: new Set() }; + const addConditional = ( + condition: RowCondition, + contextId: string | null, + contextType?: ContextEntityType, + homeOnly = false, + ) => { + const key = `${condition.name}:${contextType ?? ''}:${homeOnly}`; + const entry = acc.conditional.get(key) ?? { + condition, + contextType, + homeOnly, + orgWide: false, + ids: new Set(), + }; if (contextId === null) entry.orgWide = true; else entry.ids.add(contextId); - acc.restrictedGrants.set(key, entry); + acc.conditional.set(key, entry); }; - // Unconditional membership grants of a restricted entity qualify PER ROW (depth/roles) - // unless the role is exempt, route them to restricted grants instead of merged scope. - // Row-conditional grants (e.g. 'own') are never narrowed by restrictions. const addUnconditional = (contextType: ContextEntityType, role: string, contextId: string | null) => { - if (restriction && !restriction.exemptRoles.includes(role)) { - addRestrictedGrant(contextType, role, contextId); + // Non-elevated roles above the deepest level scope to rows HOMED at their grant level + if (isHomeScopedGrant(contextType, role)) { + const ids = acc.homeScoped.get(contextType) ?? new Set(); + ids.add(contextId ?? organizationId); + acc.homeScoped.set(contextType, ids); return; } if (contextId === null) acc.unconditionalOrgWide = true; - else acc.unconditionalIds.add(contextId); + else if (contextType === subContextType) acc.unconditionalIds.add(contextId); + else { + // Intermediate ancestor level (deep chains): scoped by that level's own id column. + const ids = acc.ancestorUnconditional.get(contextType) ?? new Set(); + ids.add(contextId); + acc.ancestorUnconditional.set(contextType, ids); + } }; for (const membership of memberships) { - // Root-context (e.g. organization) grant → org-wide scope. + // Root-context (e.g. organization) grant → org-wide scope (or org-homed rows only, + // for non-elevated roles). if (rootContext && membership.contextType === rootContext && membership.contextId === organizationId) { const value = roleReadValue(policies, entityType, rootContext, membership.role); if (value === 1) addUnconditional(rootContext, membership.role, null); - else if (isRowCondition(value)) addConditional(value, null); + else if (isRowCondition(value)) + addConditional(value, null, undefined, isHomeScopedGrant(rootContext, membership.role)); + continue; } - // Sub-context (e.g. project) grant → scope to those ids within this organization. + // Any non-root ancestor grant (home context or, in deep chains, an intermediate + // level like course/courseSection) → scope to those ids within this organization. + // Each grant is later filtered by its OWN level's id column; on tables with + // denormalized ancestor columns an intermediate id covers every row physically + // below it (single-row checks walk the same chain — see getAllDecisions). if ( - subContextType && - membership.contextType === subContextType && membership.organizationId === organizationId && - membership.contextId + membership.contextId && + membership.contextType !== rootContext && + ancestors.includes(membership.contextType) ) { - const value = roleReadValue(policies, entityType, subContextType, membership.role); - if (value === 1) addUnconditional(subContextType, membership.role, membership.contextId); - else if (isRowCondition(value)) addConditional(value, membership.contextId); + const grantLevel = membership.contextType as ContextEntityType; + const value = roleReadValue(policies, entityType, grantLevel, membership.role); + if (value === 1) addUnconditional(grantLevel, membership.role, membership.contextId); + else if (isRowCondition(value)) + addConditional( + value, + membership.contextId, + grantLevel === subContextType ? undefined : grantLevel, + isHomeScopedGrant(grantLevel, membership.role), + ); } } @@ -159,17 +209,23 @@ const resolveScopes = ( * carry no row-conditional read grants, existing call sites that only consume * `subContextIds` keep their exact previous behavior. * - * Restricted scope (`restricted`): present only for entities with a declared row - * restriction. Each entry is one membership grant whose rows must additionally satisfy - * the restriction's depth/role predicates for THAT grant (OR-ed with everything else). - * - * A read is empty only when `subContextIds` is `[]`, `conditionalScopes` is empty AND - * no restricted grants exist. + * A read is empty only when `subContextIds` is `[]` AND `conditionalScopes`, + * `ancestorScopes` and `homeScopes` are all empty. */ export interface CollectionReadFilter { subContextIds: string[] | undefined; conditionalScopes: ConditionalScope[]; - restricted?: RestrictedScope; + /** + * Unconditional grants at intermediate ancestor levels (deep chains; aggregate reads + * only — `requested` narrowing stays home-level). OR-ed with everything else, each + * scoped by its own level's id column. + */ + ancestorScopes?: AncestorScope[]; + /** + * HOME-scoped grants (elevatedRoles; aggregate reads only): rows homed exactly at + * the grant's level. OR-ed with everything else. + */ + homeScopes?: HomeScope[]; } /** Whether the resolved filter yields no readable rows at all (op should return an empty list). */ @@ -178,18 +234,43 @@ export const hasNoReadScope = (filter: CollectionReadFilter): boolean => { filter.subContextIds !== undefined && filter.subContextIds.length === 0 && filter.conditionalScopes.length === 0 && - (filter.restricted?.grants.length ?? 0) === 0 + (filter.ancestorScopes?.length ?? 0) === 0 && + (filter.homeScopes?.length ?? 0) === 0 ); }; -const toConditionalScopes = (acc: ScopeAccumulator): ConditionalScope[] => { +/** Chain levels more specific than `contextType` within the entity's ancestor chain. */ +const deeperContextsOf = (orderedContexts: readonly ContextEntityType[], contextType: ContextEntityType) => { + const index = orderedContexts.indexOf(contextType); + return index > 0 ? [...orderedContexts.slice(0, index)] : []; +}; + +const toConditionalScopes = ( + acc: ScopeAccumulator, + orderedContexts: readonly ContextEntityType[], +): ConditionalScope[] => { // Org-wide unconditional scope subsumes every conditional slice. if (acc.unconditionalOrgWide) return []; const scopes: ConditionalScope[] = []; - for (const { condition, orgWide, ids } of acc.conditional.values()) { + for (const { condition, contextType, homeOnly, orgWide, ids } of acc.conditional.values()) { + // Home-scoped conditional slices additionally require the deeper columns NULL + const deeper = homeOnly + ? deeperContextsOf(orderedContexts, contextType ?? (orderedContexts.at(-1) as ContextEntityType)) + : undefined; if (orgWide) { - scopes.push({ condition, subContextIds: undefined }); + scopes.push({ condition, subContextIds: undefined, ...(deeper?.length && { deeperContexts: deeper }) }); + continue; + } + // Intermediate-level slices keep their own id space (scoped by their own column). + if (contextType) { + if (ids.size > 0) + scopes.push({ + condition, + subContextIds: [...ids], + contextType, + ...(deeper?.length && { deeperContexts: deeper }), + }); continue; } // Ids already unconditionally readable don't need the conditional slice. @@ -199,19 +280,29 @@ const toConditionalScopes = (acc: ScopeAccumulator): ConditionalScope[] => { return scopes; }; -const toRestrictedGrantScopes = (acc: ScopeAccumulator): RestrictedGrantScope[] => { - // Org-wide unconditional (exempt) scope subsumes every restricted slice. +const toAncestorScopes = (acc: ScopeAccumulator): AncestorScope[] => { + // Org-wide unconditional scope subsumes every ancestor slice. if (acc.unconditionalOrgWide) return []; - const scopes: RestrictedGrantScope[] = []; - for (const { contextType, role, orgWide, ids } of acc.restrictedGrants.values()) { - if (orgWide) { - scopes.push({ contextType, role, subContextIds: undefined }); - continue; - } - // Ids already unconditionally readable (via exempt-role grants) don't need the slice. - const remaining = [...ids].filter((id) => !acc.unconditionalIds.has(id)); - if (remaining.length > 0) scopes.push({ contextType, role, subContextIds: remaining }); + const scopes: AncestorScope[] = []; + for (const [contextType, ids] of acc.ancestorUnconditional) { + if (ids.size > 0) scopes.push({ contextType, subContextIds: [...ids] }); + } + return scopes; +}; + +const toHomeScopes = (acc: ScopeAccumulator, orderedContexts: readonly ContextEntityType[]): HomeScope[] => { + // Org-wide unconditional scope subsumes every home slice. + if (acc.unconditionalOrgWide) return []; + + const scopes: HomeScope[] = []; + for (const [contextType, ids] of acc.homeScoped) { + if (ids.size > 0) + scopes.push({ + contextType, + subContextIds: [...ids], + deeperContexts: deeperContextsOf(orderedContexts, contextType), + }); } return scopes; }; @@ -242,7 +333,7 @@ export const resolveCollectionReadFilter = ( entityType, organizationId, requested, - rowRestrictions, + configuredElevatedRoles, ); }; @@ -250,6 +341,12 @@ export const resolveCollectionReadFilter = ( * Same as {@link resolveCollectionReadFilter} but against an explicit policy set, * mirroring `getAllDecisions(policies, …)`. Used by the check/SQL parity property test * to exercise synthetic policies; handlers use the bound wrapper above. + * + * @param elevatedRoles - Grant scoping role list (see `shared/config/permissions-config.ts`); + * the bound wrapper injects the configured value. + * @param topology - Hierarchy override, the same seam `getAllDecisions(…, { topology })` + * exposes. Defaults to the app's real hierarchy; parity tests pass a synthetic one to + * exercise deep chains a 2-level config structurally cannot reach. */ export const resolveCollectionReadFilterForPolicies = ( policies: AccessPolicies, @@ -257,40 +354,51 @@ export const resolveCollectionReadFilterForPolicies = ( entityType: ProductEntityType, organizationId: string, requested?: { subContextId?: string; subContextIds?: string[] }, - restrictions?: RowRestrictions, + elevatedRoles?: readonly string[], + topology?: PermissionTopology, ): CollectionReadFilter => { - const restriction = restrictions?.[entityType]; - const acc = resolveScopes(policies, memberships, entityType, organizationId, restriction); - const conditionalScopes = toConditionalScopes(acc); - const restrictedGrantScopes = toRestrictedGrantScopes(acc); - const orderedContexts = hierarchy.getOrderedAncestors(entityType) as ContextEntityType[]; + const topoHierarchy = topology?.hierarchy ?? hierarchy; + const orderedContexts = topoHierarchy.getOrderedAncestors(entityType) as ContextEntityType[]; + const acc = resolveScopes(policies, memberships, entityType, organizationId, elevatedRoles, orderedContexts); + const conditionalScopes = toConditionalScopes(acc, orderedContexts); + const rootContext = orderedContexts.at(-1) ?? null; + const homeContext = orderedContexts.find((context) => context !== rootContext) ?? null; + const ancestorScopes = toAncestorScopes(acc); + const homeScopes = toHomeScopes(acc, orderedContexts); - const withRestricted = (filter: Omit, grants: RestrictedGrantScope[]) => { - if (!restriction || grants.length === 0) return filter as CollectionReadFilter; - return { ...filter, restricted: { restriction, orderedContexts, grants } }; + const withScopes = ( + filter: Omit, + ancestors: AncestorScope[] = ancestorScopes, + homes: HomeScope[] = homeScopes, + ): CollectionReadFilter => { + let base: CollectionReadFilter = ancestors.length > 0 ? { ...filter, ancestorScopes: ancestors } : filter; + if (homes.length > 0) base = { ...base, homeScopes: homes }; + return base; }; const unconditionallyReadable = (id: string): boolean => acc.unconditionalOrgWide || acc.unconditionalIds.has(id); + /** Is this conditional entry scoped by an intermediate level's own column? */ + const isIntermediate = (contextType: ContextEntityType | undefined): boolean => + contextType !== undefined && contextType !== homeContext && contextType !== rootContext; + + // `requested` narrowing stays strictly home-level (pre-deep-chain semantics): + // intermediate-level entries are DROPPED here — a requested-id read widened by an + // intermediate grant would leak rows outside the requested set unless every caller + // also ANDs its own placement filter. Deep-chain list ops therefore skip `requested` + // and apply placement as an explicit filter on top of the aggregate WHERE. const conditionalScopesFor = (ids: string[]): ConditionalScope[] => { const remaining = ids.filter((id) => !unconditionallyReadable(id)); if (remaining.length === 0) return []; - return conditionalScopes - .map(({ condition, subContextIds }) => ({ - condition, - subContextIds: subContextIds === undefined ? remaining : remaining.filter((id) => subContextIds.includes(id)), - })) - .filter((scope) => scope.subContextIds.length > 0); - }; - const restrictedGrantsFor = (ids: string[]): RestrictedGrantScope[] => { - const remaining = ids.filter((id) => !unconditionallyReadable(id)); - if (remaining.length === 0) return []; - return restrictedGrantScopes - .map(({ contextType, role, subContextIds }) => ({ - contextType, - role, - subContextIds: subContextIds === undefined ? remaining : remaining.filter((id) => subContextIds.includes(id)), - })) - .filter((scope) => scope.subContextIds.length > 0); + return ( + conditionalScopes + // Intermediate + home-scoped slices are dropped (requested narrowing is home-level) + .filter((scope) => !isIntermediate(scope.contextType) && !scope.deeperContexts) + .map(({ condition, subContextIds }) => ({ + condition, + subContextIds: subContextIds === undefined ? remaining : remaining.filter((id) => subContextIds.includes(id)), + })) + .filter((scope) => scope.subContextIds.length > 0) + ); }; // Explicit single id (e.g. ?projectId=…): must be within the caller's readable scope. @@ -299,28 +407,22 @@ export const resolveCollectionReadFilterForPolicies = ( if (unconditionallyReadable(id)) return { subContextIds: [id], conditionalScopes: [] }; const scopes = conditionalScopesFor([id]); - const restrictedScopes = restrictedGrantsFor([id]); - if (scopes.length === 0 && restrictedScopes.length === 0) { + if (scopes.length === 0) { throw new AppError(403, 'forbidden', 'warn', { entityType }); } - return withRestricted({ subContextIds: [], conditionalScopes: scopes }, restrictedScopes); + return { subContextIds: [], conditionalScopes: scopes }; } // Explicit set (e.g. all projects of a workspace): intersect with the caller's scope. if (requested?.subContextIds !== undefined) { const unconditional = requested.subContextIds.filter((id) => unconditionallyReadable(id)); - return withRestricted( - { subContextIds: unconditional, conditionalScopes: conditionalScopesFor(requested.subContextIds) }, - restrictedGrantsFor(requested.subContextIds), - ); + return { subContextIds: unconditional, conditionalScopes: conditionalScopesFor(requested.subContextIds) }; } - // Aggregate read: org-wide for ancestor-level grants, otherwise the caller's readable sub-contexts. - return withRestricted( - { - subContextIds: acc.unconditionalOrgWide ? undefined : [...acc.unconditionalIds], - conditionalScopes, - }, - restrictedGrantScopes, - ); + // Aggregate read: org-wide for root-level grants, otherwise the caller's readable + // sub-contexts plus any intermediate ancestor / home scopes. + return withScopes({ + subContextIds: acc.unconditionalOrgWide ? undefined : [...acc.unconditionalIds], + conditionalScopes, + }); }; diff --git a/backend/src/permissions/get-product-entity.ts b/backend/src/permissions/get-product-entity.ts index f738f4755..6df1528c2 100644 --- a/backend/src/permissions/get-product-entity.ts +++ b/backend/src/permissions/get-product-entity.ts @@ -1,4 +1,4 @@ -import { appConfig, type EntityActionType, hierarchy, hostDelegation, type ProductEntityType } from 'shared'; +import type { EntityActionType, ProductEntityType } from 'shared'; import type { AuthContext } from '#/core/context'; import { AppError } from '#/core/error'; import { baseDb } from '#/db/db'; @@ -7,29 +7,6 @@ import { type EntityModel, resolveEntity } from '#/modules/entities/entities-que import { checkPermission } from '#/permissions'; import { buildSubject } from '#/permissions/build-subject'; -/** - * Resolve the host row for a hosted entity when its type delegates actions to the host - * (`delegateToHost`). Returns undefined for non-delegated types and unhosted rows. - */ -const resolveHostRow = async ( - ctx: AuthContext, - entityType: ProductEntityType, - entity: Record, -): Promise | undefined> => { - if (!hostDelegation[entityType]?.length) return undefined; - const hostType = hierarchy.getHostType(entityType); - if (!hostType) return undefined; - - const hostId = entity[appConfig.entityIdColumnKeys[hostType]]; - if (typeof hostId !== 'string') return undefined; - - const hostRow = - ctx.var.db === baseDb && ctx.var.tenantId - ? await tenantRead(ctx, (readCtx) => resolveEntity(readCtx, hostType, hostId)) - : await resolveEntity(ctx, hostType, hostId); - return hostRow ?? undefined; -}; - /** * Result type for product entity validation including the can object. */ @@ -71,16 +48,13 @@ export const getValidProductEntity = async ( : await resolveEntity(ctx, entityType, id); if (!entity) throw new AppError(404, 'not_found', 'warn', { entityType }); - // Host delegation (hierarchy `host:` + delegateToHost): resolve the host row once so the - // engine can union the host's decision into this row's (load-at-check, the engine never - // loads rows itself). Only for hosted rows of delegated entity types. - const hostRow = await resolveHostRow(ctx, entityType, entity); - - // Step 2: Check permission for the requested action. + // Step 2: Check permission for the requested action. The entity doubles as `row` so + // 'own' row conditions and public read grants (e.g. `publicSelf`) evaluate from real + // row data — without it, row-dependent grants fail closed on every single-row check. const subject = buildSubject(entityType, entity, { id: entity.id, createdBy: 'createdBy' in entity ? (entity.createdBy as string | null) : undefined, - ...(hostRow && { hostRow }), + row: entity as Record, }); const { isAllowed } = checkPermission(memberships, action, subject, { isSystemAdmin, userId }); diff --git a/backend/src/permissions/index.ts b/backend/src/permissions/index.ts index 3c5686a82..48ae7ebdb 100644 --- a/backend/src/permissions/index.ts +++ b/backend/src/permissions/index.ts @@ -6,11 +6,11 @@ export { type PermissionResult, } from './check-permission'; export { + type AncestorScope, type CollectionReadFilter, type ConditionalScope, + type HomeScope, hasNoReadScope, - type RestrictedGrantScope, - type RestrictedScope, resolveCollectionReadFilter, } from './collection-scope'; export { getValidContextEntity, type ValidContextEntityResult } from './get-context-entity'; diff --git a/backend/src/permissions/row-predicates.test.ts b/backend/src/permissions/row-predicates.test.ts index 435d7d661..627e5ec34 100644 --- a/backend/src/permissions/row-predicates.test.ts +++ b/backend/src/permissions/row-predicates.test.ts @@ -6,19 +6,26 @@ import { type ContextEntityType, computeCan, configureAccessPolicies, + createEntityHierarchy, + createRoleRegistry, + type EntityType, getAllDecisions, getContextRoles, hierarchy, + type PermissionTopology, type PermissionValue, - type RowRestrictions, + type ProductEntityType, resolvePermission, type SubjectForPermission, toColumnName, } from 'shared'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { seedDb } from '#/db/db'; +import { canReceiveEntityEvent } from '#/modules/entities/helpers/dispatch-to-stream'; +import type { AppStreamProductEvent } from '#/modules/entities/stream/types'; import type { MembershipBaseModel } from '#/modules/memberships/helpers/select'; -import { resolveCollectionReadFilterForPolicies } from './collection-scope'; +import { checkPermission } from './check-permission'; +import { resolveCollectionReadFilter, resolveCollectionReadFilterForPolicies } from './collection-scope'; import { buildCollectionReadWhere } from './row-predicates'; /** @@ -47,11 +54,6 @@ const SUB = CHAIN.length > 1 ? CHAIN[0] : null; // the narrowable sub-context (p const ROOT_ID = 'org1'; const SUB_INSTANCES = SUB ? ['s1', 's2', 's3'] : []; // sub-context instance ids (empty when there is no sub-context) -// Depth values a row's `visibilityDepth` can take: the sub-context (or root when none), the root, -// and one context guaranteed to be outside attachment's chain (never qualifies). -const DEPTH_SUB = SUB ?? ROOT; -const UNKNOWN_DEPTH = 'nonexistent-context'; - // Sub-context id column on the scratch table (raak: `project_id`), derived from config. Null on an // org-only fork, where the read is org-wide and no sub-context column is ever referenced. const subIdKey = SUB ? appConfig.entityIdColumnKeys[SUB] : null; // 'projectId' | null @@ -61,8 +63,6 @@ const subColumnName = subIdKey ? toColumnName(subIdKey) : null; // 'project_id' const baseColumns = { id: varchar('id').primaryKey(), createdBy: varchar('created_by'), - visibilityDepth: varchar('visibility_depth'), - audienceRoles: varchar('audience_roles').array(), }; const parityTable = pgTable( 'test_permission_parity_rows', @@ -80,32 +80,16 @@ interface ParityRow { id: string; subContextId: string | null; createdBy: string | null; - visibilityDepth: string | null; - audienceRoles: string[] | null; } -/** Depth × audience combinations, incl. an unknown depth outside attachment's chain. */ -const VISIBILITY_COMBOS: Array<{ key: string; visibilityDepth: string | null; audienceRoles: string[] | null }> = [ - { key: 'open', visibilityDepth: null, audienceRoles: null }, - { key: 'sub', visibilityDepth: DEPTH_SUB, audienceRoles: null }, - { key: 'root-member', visibilityDepth: ROOT, audienceRoles: ['member'] }, - { key: 'admin-only', visibilityDepth: null, audienceRoles: ['admin'] }, - { key: 'unknown-depth', visibilityDepth: UNKNOWN_DEPTH, audienceRoles: ['guest', 'member'] }, - { key: 'empty-roles', visibilityDepth: DEPTH_SUB, audienceRoles: [] }, -]; - -/** Fixed row set: every sub-context (or the single org) × creator × visibility combination. */ +/** Fixed row set: every sub-context (or the single org) × creator. */ const SUB_SLOTS: (string | null)[] = SUB ? SUB_INSTANCES : [null]; const ROWS: ParityRow[] = SUB_SLOTS.flatMap((subContextId) => - [...USERS, null].flatMap((createdBy) => - VISIBILITY_COMBOS.map(({ key, visibilityDepth, audienceRoles }) => ({ - id: `${subContextId ?? 'root'}:${createdBy ?? 'nobody'}:${key}`, - subContextId, - createdBy, - visibilityDepth, - audienceRoles, - })), - ), + [...USERS, null].map((createdBy) => ({ + id: `${subContextId ?? 'root'}:${createdBy ?? 'nobody'}`, + subContextId, + createdBy, + })), ); /** Deterministic PRNG (mulberry32) so failures reproduce exactly. */ @@ -145,23 +129,8 @@ interface Scenario { policies: AccessPolicies; memberships: MembershipBaseModel[]; userId: string | undefined; - restrictions: RowRestrictions; } -/** Random restriction config: none, depth-only, roles-only or both; exemption on or off. */ -const randomRestrictions = (random: () => number): RowRestrictions => { - const shape = pick(random, ['none', 'none', 'depth', 'roles', 'both', 'both'] as const); - if (shape === 'none') return {}; - const exemptRoles = random() < 0.5 ? (['admin'] as const) : ([] as const); - return { - attachment: { - ...(shape !== 'roles' && { depthColumn: 'visibilityDepth' }), - ...(shape !== 'depth' && { rolesColumn: 'audienceRoles' }), - exemptRoles, - }, - }; -}; - const membership = (contextType: ContextEntityType, contextId: string, role: string): MembershipBaseModel => ({ id: `mem-${contextType}-${contextId}-${role}`, @@ -179,7 +148,6 @@ const randomScenario = (random: () => number): Scenario => { policies: randomPolicies(random), memberships: [], userId: undefined, - restrictions: randomRestrictions(random), }; } @@ -194,7 +162,6 @@ const randomScenario = (random: () => number): Scenario => { policies: randomPolicies(random), memberships, userId: pick(random, USERS), - restrictions: randomRestrictions(random), }; }; @@ -203,7 +170,6 @@ const rowSubject = (row: ParityRow): SubjectForPermission => ({ id: row.id, createdBy: row.createdBy, contextIds: { [ROOT]: ROOT_ID, ...(SUB && row.subContextId !== null ? { [SUB]: row.subContextId } : {}) }, - row: { visibilityDepth: row.visibilityDepth, audienceRoles: row.audienceRoles }, }); /** Path 1: the engine's per-row read decision. */ @@ -212,7 +178,6 @@ const engineReadableIds = (scenario: Scenario): Set => { for (const row of ROWS) { const { can } = getAllDecisions(scenario.policies, scenario.memberships, rowSubject(row), { userId: scenario.userId, - restrictions: scenario.restrictions, }); if (can.read) readable.add(row.id); } @@ -221,14 +186,7 @@ const engineReadableIds = (scenario: Scenario): Set => { /** Path 2: the compiled SQL predicate executed against Postgres. */ const sqlReadableIds = async (scenario: Scenario): Promise> => { - const filter = resolveCollectionReadFilterForPolicies( - scenario.policies, - scenario.memberships, - 'attachment', - ROOT_ID, - undefined, - scenario.restrictions, - ); + const filter = resolveCollectionReadFilterForPolicies(scenario.policies, scenario.memberships, 'attachment', ROOT_ID); const where = buildCollectionReadWhere(filter, parityTable, subContextColumn, scenario.userId); if (where.kind === 'none') return new Set(); @@ -243,17 +201,13 @@ beforeAll(async () => { sql.raw(` create table test_permission_parity_rows ( id varchar primary key, - created_by varchar, - visibility_depth varchar, - audience_roles varchar[]${subColumnName ? `,\n ${subColumnName} varchar not null` : ''} + created_by varchar${subColumnName ? `,\n ${subColumnName} varchar not null` : ''} ) `), ); const values = ROWS.map((r) => ({ id: r.id, createdBy: r.createdBy, - visibilityDepth: r.visibilityDepth, - audienceRoles: r.audienceRoles, ...(subIdKey && r.subContextId !== null ? { [subIdKey]: r.subContextId } : {}), })); await seedDb.insert(parityTable).values(values as (typeof parityTable.$inferInsert)[]); @@ -280,10 +234,6 @@ describe('row-condition parity: engine check ⊆⊇ compiled SQL ⊆⊇ compute- // Engine ⊆⊇ compute-can: per single membership, the frontend-resolved state must // match the engine's decision for every row in that membership's scope. - // computeCan is restriction-blind (context-level, no row data). - // restricted entities must resolve per row via the decision API, so parity with - // compute-can is only asserted when no restriction is declared. - if (scenario.restrictions.attachment) continue; for (const m of scenario.memberships) { const canMap = computeCan(m.contextType as ContextEntityType, m, scenario.policies); const state = canMap.attachment?.read ?? false; @@ -317,8 +267,9 @@ describe('row-condition parity: engine check ⊆⊇ compiled SQL ⊆⊇ compute- scenario.memberships, 'attachment', ROOT_ID, - { subContextId: requestedSubContext }, - scenario.restrictions, + { + subContextId: requestedSubContext, + }, ); } catch { // 403: no scope at all for the requested sub-context. The engine must agree that @@ -351,3 +302,379 @@ describe('row-condition parity: engine check ⊆⊇ compiled SQL ⊆⊇ compute- }); }); }); + +/****************************************************************************** + * Deep-chain parity: 4-level SYNTHETIC hierarchy (organization > course > + * courseSection > project, product `item` parented to project with nullable + * ancestors) — exercises intermediate ancestor-level grants (course / + * courseSection) that a 2-level config structurally cannot reach. Both paths + * run on the same topology seam: the engine via `getAllDecisions(…, { topology })`, + * the scope compiler via `resolveCollectionReadFilterForPolicies(…, topology)`. + * The casts naming synthetic entities are contained in the fixtures below, + * mirroring `shared/src/testing/wide-fixture.ts`. + ******************************************************************************/ + +const deepRoles = createRoleRegistry(['admin', 'member', 'staff', 'student', 'owner', 'follower'] as const); +const deepHierarchy = createEntityHierarchy(deepRoles) + .user() + .context('organization', { parent: null, roles: ['admin', 'member'] }) + .context('course', { parent: 'organization', roles: ['staff', 'student'] }) + .context('courseSection', { parent: 'course', roles: ['staff', 'student'] }) + .context('project', { parent: 'courseSection', roles: ['owner', 'follower'] }) + .product('item', { parent: 'project', nullableAncestors: ['project', 'courseSection', 'course'] }) + .build(); +const deepTopology: PermissionTopology = { hierarchy: deepHierarchy }; + +type DeepContextType = 'organization' | 'course' | 'courseSection' | 'project'; +const DEEP_ENTITY_TYPES = ['user', 'organization', 'course', 'courseSection', 'project', 'item'] as const; +const DEEP_CONTEXT_ROLES = { + organization: ['admin', 'member'], + course: ['staff', 'student'], + courseSection: ['staff', 'student'], + project: ['owner', 'follower'], +} as const satisfies Record; +const DEEP_ITEM = 'item' as unknown as ProductEntityType; + +// Column keys follow the `${contextType}Id` convention `buildCollectionReadWhere` falls +// back to for topology levels absent from `appConfig.entityIdColumnKeys`. +const deepParityTable = pgTable('test_permission_parity_deep_rows', { + id: varchar('id').primaryKey(), + organizationId: varchar('organization_id').notNull(), + courseId: varchar('course_id'), + courseSectionId: varchar('course_section_id'), + projectId: varchar('project_id'), + createdBy: varchar('created_by'), +}); + +interface DeepParityRow { + id: string; + organizationId: string; + courseId: string | null; + courseSectionId: string | null; + projectId: string | null; + createdBy: string | null; +} + +/** Placements across all four depths; p1 sits under s1/c1, p2 under c2, p3 is org-level. */ +const DEEP_PLACEMENTS = [ + { key: 'org', courseId: null, courseSectionId: null, projectId: null }, + { key: 'c1', courseId: 'c1', courseSectionId: null, projectId: null }, + { key: 's1', courseId: 'c1', courseSectionId: 's1', projectId: null }, + { key: 'p1', courseId: 'c1', courseSectionId: 's1', projectId: 'p1' }, + { key: 'p2', courseId: 'c2', courseSectionId: null, projectId: 'p2' }, + { key: 'p3', courseId: null, courseSectionId: null, projectId: 'p3' }, +] as const; + +/** Fixed row set: every placement × creator. */ +const DEEP_ROWS: DeepParityRow[] = [...USERS, null].flatMap((createdBy) => + DEEP_PLACEMENTS.map((placement) => ({ + id: `${createdBy ?? 'nobody'}:${placement.key}`, + organizationId: ROOT_ID, + courseId: placement.courseId, + courseSectionId: placement.courseSectionId, + projectId: placement.projectId, + createdBy, + })), +); + +/** Policies for `item` over the synthetic topology, one read cell per context × role. */ +const deepPolicies = (readValue: (contextType: DeepContextType, role: string) => PermissionValue): AccessPolicies => + configureAccessPolicies( + DEEP_ENTITY_TYPES as unknown as readonly EntityType[], + ({ subject, contexts }) => { + if ((subject.name as string) !== 'item') return; + const builders = contexts as unknown as Record< + DeepContextType, + Record void> + >; + for (const [contextType, roles] of Object.entries(DEEP_CONTEXT_ROLES) as [DeepContextType, readonly string[]][]) { + for (const role of roles) builders[contextType][role]({ read: readValue(contextType, role) }); + } + }, + deepTopology, + ); + +const deepMembership = (contextType: DeepContextType, contextId: string, role: string): MembershipBaseModel => + ({ + id: `mem-${contextType}-${contextId}-${role}`, + userId: 'actor', + contextType, + contextId, + organizationId: ROOT_ID, + role, + }) as unknown as MembershipBaseModel; + +interface DeepScenario { + policies: AccessPolicies; + memberships: MembershipBaseModel[]; + userId: string | undefined; +} + +const randomDeepScenario = (random: () => number): DeepScenario => { + const memberships: MembershipBaseModel[] = []; + if (random() < 0.5) + memberships.push(deepMembership('organization', ROOT_ID, pick(random, DEEP_CONTEXT_ROLES.organization))); + if (random() < 0.5) memberships.push(deepMembership('course', 'c1', pick(random, DEEP_CONTEXT_ROLES.course))); + if (random() < 0.3) memberships.push(deepMembership('course', 'c2', pick(random, DEEP_CONTEXT_ROLES.course))); + if (random() < 0.5) + memberships.push(deepMembership('courseSection', 's1', pick(random, DEEP_CONTEXT_ROLES.courseSection))); + if (random() < 0.5) memberships.push(deepMembership('project', 'p1', pick(random, DEEP_CONTEXT_ROLES.project))); + if (random() < 0.3) memberships.push(deepMembership('project', 'p3', pick(random, DEEP_CONTEXT_ROLES.project))); + return { + policies: deepPolicies(() => randomReadValue(random)), + memberships, + userId: random() < 0.9 ? pick(random, USERS) : undefined, + }; +}; + +const deepRowSubject = (row: DeepParityRow): SubjectForPermission => + ({ + entityType: 'item', + id: row.id, + createdBy: row.createdBy, + contextIds: { + organization: ROOT_ID, + course: row.courseId, + courseSection: row.courseSectionId, + project: row.projectId, + }, + }) as unknown as SubjectForPermission; + +/** Path 1: the engine's per-row read decision, over the synthetic topology. */ +const deepEngineReadableIds = (scenario: DeepScenario, elevatedRoles?: readonly string[]): Set => { + const readable = new Set(); + for (const row of DEEP_ROWS) { + const { can } = getAllDecisions(scenario.policies, scenario.memberships, deepRowSubject(row), { + userId: scenario.userId, + topology: deepTopology, + ...(elevatedRoles && { elevatedRoles }), + }); + if (can.read) readable.add(row.id); + } + return readable; +}; + +/** Path 2: the compiled SQL predicate executed against Postgres, same topology. */ +const deepSqlReadableIds = async (scenario: DeepScenario, elevatedRoles?: readonly string[]): Promise> => { + const filter = resolveCollectionReadFilterForPolicies( + scenario.policies, + scenario.memberships, + DEEP_ITEM, + ROOT_ID, + undefined, + elevatedRoles, + deepTopology, + ); + const where = buildCollectionReadWhere(filter, deepParityTable, deepParityTable.projectId, scenario.userId); + + if (where.kind === 'none') return new Set(); + const query = seedDb.select({ id: deepParityTable.id }).from(deepParityTable); + const rows = where.kind === 'all' ? await query : await query.where(where.where); + return new Set(rows.map((r) => r.id)); +}; + +const deepScenarioLabel = (seed: string, i: number, scenario: DeepScenario): string => + `seed ${seed} scenario ${i} (memberships: ${scenario.memberships + .map((m) => `${m.contextType}:${m.contextId}:${m.role}`) + .join(', ')}; user: ${scenario.userId ?? 'anonymous'})`; + +beforeAll(async () => { + await seedDb.execute(sql`drop table if exists test_permission_parity_deep_rows`); + await seedDb.execute(sql` + create table test_permission_parity_deep_rows ( + id varchar primary key, + organization_id varchar not null, + course_id varchar, + course_section_id varchar, + project_id varchar, + created_by varchar + ) + `); + await seedDb.insert(deepParityTable).values(DEEP_ROWS); +}); + +afterAll(async () => { + await seedDb.execute(sql`drop table if exists test_permission_parity_deep_rows`); +}); + +describe('deep-chain parity: intermediate ancestor grants agree between engine and SQL', () => { + it('agrees on every row across random policies, multi-level memberships and actors', async () => { + const random = mulberry32(0xdeec); + + for (let i = 0; i < 250; i++) { + const scenario = randomDeepScenario(random); + const label = deepScenarioLabel('0xdeec', i, scenario); + + const fromEngine = deepEngineReadableIds(scenario); + const fromSql = await deepSqlReadableIds(scenario); + expect(fromSql, label).toEqual(fromEngine); + } + }); +}); + +/****************************************************************************** + * elevatedRoles parity (same synthetic topology): with `elevatedRoles` configured, + * a product grant of a non-listed role speaks only for rows HOMED at its own + * context level, while listed roles (admin/staff) keep subtree scope. Engine + * (`getAllDecisions(…, { elevatedRoles })`) ≍ compiled SQL, row for row. + ******************************************************************************/ + +const SUBTREE_ROLES = ['admin', 'staff'] as const; + +describe('elevatedRoles parity: home-scoped grants agree between engine and SQL', () => { + it('agrees on every row across random policies and memberships with elevatedRoles configured', async () => { + const random = mulberry32(0x50b7); + + for (let i = 0; i < 100; i++) { + const scenario = randomDeepScenario(random); + const label = deepScenarioLabel('0x50b7', i, scenario); + + const fromEngine = deepEngineReadableIds(scenario, SUBTREE_ROLES); + const fromSql = await deepSqlReadableIds(scenario, SUBTREE_ROLES); + expect(fromSql, label).toEqual(fromEngine); + } + }); + + it('scopes a non-elevated course grant to course-HOMED rows only', async () => { + const scenario: DeepScenario = { + policies: deepPolicies((contextType, role) => (contextType === 'course' && role === 'student' ? 1 : 0)), + memberships: [deepMembership('course', 'c1', 'student')], + userId: 'u1', + }; + // Rows homed at c1 itself — NOT the section/project rows physically below it. + const expected = new Set( + DEEP_ROWS.filter((r) => r.courseId === 'c1' && r.courseSectionId === null && r.projectId === null).map( + (r) => r.id, + ), + ); + expect(deepEngineReadableIds(scenario, SUBTREE_ROLES)).toEqual(expected); + expect(await deepSqlReadableIds(scenario, SUBTREE_ROLES)).toEqual(expected); + }); + + it('keeps subtree scope for a listed (staff) course grant', async () => { + const scenario: DeepScenario = { + policies: deepPolicies((contextType, role) => (contextType === 'course' && role === 'staff' ? 1 : 0)), + memberships: [deepMembership('course', 'c1', 'staff')], + userId: 'u1', + }; + // Everything physically below c1: the course row plus its section and project rows. + const expected = new Set(DEEP_ROWS.filter((r) => r.courseId === 'c1').map((r) => r.id)); + expect(deepEngineReadableIds(scenario, SUBTREE_ROLES)).toEqual(expected); + expect(await deepSqlReadableIds(scenario, SUBTREE_ROLES)).toEqual(expected); + }); + + it("matches a home-scoped 'own' grant only on the author's home-level rows", async () => { + const scenario: DeepScenario = { + policies: deepPolicies((contextType, role) => (contextType === 'course' && role === 'student' ? 'own' : 0)), + memberships: [deepMembership('course', 'c1', 'student')], + userId: 'u1', + }; + const expected = new Set( + DEEP_ROWS.filter( + (r) => r.courseId === 'c1' && r.courseSectionId === null && r.projectId === null && r.createdBy === 'u1', + ).map((r) => r.id), + ); + expect(deepEngineReadableIds(scenario, SUBTREE_ROLES)).toEqual(expected); + expect(await deepSqlReadableIds(scenario, SUBTREE_ROLES)).toEqual(expected); + }); +}); + +/****************************************************************************** + * Three-way mirror parity under the REAL app config: collection SQL ≍ single-row + * engine check ≍ SSE dispatch predicate (`canReceiveEntityEvent`). The dispatch + * decision doubles as cacheToken issuance (a signed cache token is a read + * capability `appCache` honors without re-running predicates), so any divergence + * is a security bug. + * + * Config note: `canReceiveEntityEvent` evaluates through `checkPermission`, which + * binds the app's REAL policies — there is no policy-injection seam, and adding + * one only for tests would fork the code path under test. So unlike the + * synthetic-topology blocks above, this block runs the real config (cella: + * 2-level, unconditional org grants; a fork with a nested chain or `'own'` read + * cells exercises those automatically via the CHAIN-derived scenario space). + * Deep-chain and elevatedRoles decisions are asserted engine ≍ SQL above and reach + * dispatch through the same `checkPermission` call. Rows carry no publicAt, + * keeping public grants out of scope (collection SQL compiles no public clause). + ******************************************************************************/ + +const realMembership = ( + contextType: ContextEntityType, + contextId: string, + role: string, + organizationId: string, +): MembershipBaseModel => + ({ + id: `mem-${contextType}-${contextId}-${role}`, + userId: 'actor', + contextType, + contextId, + organizationId, + role, + }) as unknown as MembershipBaseModel; + +const randomRealScenario = (random: () => number): { memberships: MembershipBaseModel[]; userId: string } => { + const memberships: MembershipBaseModel[] = []; + if (random() < 0.5) memberships.push(realMembership(ROOT, ROOT_ID, pick(random, getContextRoles(ROOT)), ROOT_ID)); + // A grant in a DIFFERENT org must contribute nothing to this org's collection + if (random() < 0.3) + memberships.push(realMembership(ROOT, 'org-other', pick(random, getContextRoles(ROOT)), 'org-other')); + if (SUB) { + for (const subId of SUB_INSTANCES) { + if (random() < 0.4) memberships.push(realMembership(SUB, subId, pick(random, getContextRoles(SUB)), ROOT_ID)); + } + } + // SSE subscribers are always authenticated; 'outsider' stands in for a user with no rows + return { memberships, userId: random() < 0.85 ? pick(random, USERS) : 'outsider' }; +}; + +/** Context id columns as they appear on an activity event (and its row). */ +const rowContextColumns = (row: ParityRow): Record => ({ + [appConfig.entityIdColumnKeys[ROOT]]: ROOT_ID, + ...(subIdKey ? { [subIdKey]: row.subContextId } : {}), +}); + +const dispatchEvent = (row: ParityRow): AppStreamProductEvent => + ({ + entityType: 'attachment', + subjectId: row.id, + ...rowContextColumns(row), + rowData: { id: row.id, createdBy: row.createdBy, ...rowContextColumns(row) }, + }) as unknown as AppStreamProductEvent; + +describe('three-way mirror parity: SQL ≍ engine ≍ dispatch under the real app config', () => { + it('agrees on every row for random membership sets and actors', async () => { + const random = mulberry32(0x3a11); + + for (let i = 0; i < 100; i++) { + const { memberships, userId } = randomRealScenario(random); + const label = `seed 0x3a11 scenario ${i} (memberships: ${memberships + .map((m) => `${m.contextType}:${m.contextId}:${m.role}`) + .join(', ')}; user: ${userId})`; + + const filter = resolveCollectionReadFilter(memberships, 'attachment', ROOT_ID); + const where = buildCollectionReadWhere(filter, parityTable, subContextColumn, userId); + const query = seedDb.select({ id: parityTable.id }).from(parityTable); + const fromSql = new Set( + where.kind === 'none' + ? [] + : (where.kind === 'all' ? await query : await query.where(where.where)).map((r) => r.id), + ); + + for (const row of ROWS) { + // Same subject shape dispatch builds: ancestor scope + createdBy + the row itself + const subject = { ...rowSubject(row), row: { createdBy: row.createdBy } }; + const engineAllowed = checkPermission(memberships, 'read', subject, { + isSystemAdmin: false, + userId, + }).isAllowed; + const dispatchAllowed = canReceiveEntityEvent( + { userId, isSystemAdmin: false, memberships }, + dispatchEvent(row), + ); + + expect(dispatchAllowed, `${label} → row ${row.id} dispatch-vs-engine`).toBe(engineAllowed); + expect(fromSql.has(row.id), `${label} → row ${row.id} sql-vs-engine`).toBe(engineAllowed); + } + } + }); +}); diff --git a/backend/src/permissions/row-predicates.ts b/backend/src/permissions/row-predicates.ts index cdefec6ec..d936a872f 100644 --- a/backend/src/permissions/row-predicates.ts +++ b/backend/src/permissions/row-predicates.ts @@ -1,7 +1,7 @@ import { and, eq, inArray, isNull, or, type SQL, sql } from 'drizzle-orm'; import type { AnyPgTable, PgColumn } from 'drizzle-orm/pg-core'; -import { qualifyingDepths, type RowCondition, type RowConditionSqlForm } from 'shared'; -import type { CollectionReadFilter, RestrictedScope } from './collection-scope'; +import { appConfig, type ContextEntityType, type RowCondition, type RowConditionSqlForm } from 'shared'; +import type { CollectionReadFilter } from './collection-scope'; /** * Compiles row-condition SQL forms (declared ORM-free in `shared`, see @@ -49,45 +49,18 @@ export type CollectionReadWhere = | { kind: 'none' } // no readable scope: op should return an empty list without querying | { kind: 'where'; where: SQL }; -/** - * SQL-form of a row restriction for ONE membership grant (see - * `shared/src/permissions/row-restrictions.ts` for semantics; this must agree with - * `membershipGrantQualifies`, asserted by the parity property test): - * - depth: row depth is NULL or among the depths this grant's context level qualifies for - * - roles: row audience is NULL/empty or contains this grant's role - */ -const restrictionPredicates = ( - restricted: RestrictedScope, - grant: { contextType: (typeof restricted.orderedContexts)[number]; role: string }, - table: AnyPgTable, -): SQL[] => { - const predicates: SQL[] = []; - const { restriction, orderedContexts } = restricted; - - if (restriction.depthColumn) { - const column = resolveColumn(table, restriction.depthColumn, 'visibilityDepth restriction'); - const depths = qualifyingDepths(orderedContexts, grant.contextType); - const predicate = depths.length > 0 ? or(isNull(column), inArray(column, depths)) : isNull(column); - if (predicate) predicates.push(predicate); - } - - if (restriction.rolesColumn) { - const column = resolveColumn(table, restriction.rolesColumn, 'audienceRoles restriction'); - const predicate = or(isNull(column), sql`cardinality(${column}) = 0`, sql`${grant.role} = any(${column})`); - if (predicate) predicates.push(predicate); - } - - return predicates; -}; - /** * Build the row-scope WHERE clause for a collection read from a resolved filter: - * `(subContext IN unconditional ids) OR (condition SQL AND subContext IN conditional ids) - * OR (restriction predicates for grant AND subContext IN grant ids) OR …`. + * `(subContext IN unconditional ids) OR (condition SQL AND subContext IN conditional ids) OR …`. + * + * Deep chains: entries tagged with an intermediate `contextType` (e.g. course grants on + * a project-homed entity) are scoped by THAT level's own id column, resolved via + * `appConfig.entityIdColumnKeys` — on tables with denormalized ancestor columns an + * intermediate id covers every row physically below it. * * @param filter - Resolved scope filter (`resolveCollectionReadFilter`). * @param table - The product table being queried. - * @param subContextColumn - The table's sub-context id column (e.g. `tasks.projectId`). + * @param subContextColumn - The table's home sub-context id column (e.g. `tasks.projectId`). * @param userId - The acting user id; undefined for anonymous actors. */ export const buildCollectionReadWhere = ( @@ -99,34 +72,55 @@ export const buildCollectionReadWhere = ( // Org-wide unconditional read (conditional scopes are subsumed and already dropped). if (filter.subContextIds === undefined) return { kind: 'all' }; + /** + * The id column a scope entry filters by: its own level's column, or the home column. + * Column keys come from `appConfig.entityIdColumnKeys`; a synthetic topology level + * (parity tests) is absent there and falls back to the `${contextType}Id` convention + * the config validator enforces for real entities. + */ + const scopeColumn = (contextType: ContextEntityType | undefined): PgColumn => + contextType + ? resolveColumn( + table, + (appConfig.entityIdColumnKeys as Partial>)[contextType] ?? `${contextType}Id`, + `${contextType} scope`, + ) + : subContextColumn; + const clauses: SQL[] = []; if (filter.subContextIds.length > 0) { clauses.push(inArray(subContextColumn, filter.subContextIds)); } - for (const { condition, subContextIds } of filter.conditionalScopes) { - const conditionSql = compileRowConditionSql(condition, table, userId); - if (subContextIds === undefined) { - // Org-wide conditional grant: condition alone bounds the rows. - clauses.push(conditionSql); - continue; - } + for (const { contextType, subContextIds } of filter.ancestorScopes ?? []) { + if (subContextIds.length === 0) continue; + clauses.push(inArray(scopeColumn(contextType), subContextIds)); + } + + // HOME-scoped grants (elevatedRoles): the grant level's column matches AND every + // more-specific ancestor column is NULL — rows homed exactly at that level. + for (const { contextType, subContextIds, deeperContexts } of filter.homeScopes ?? []) { if (subContextIds.length === 0) continue; - const scoped = and(inArray(subContextColumn, subContextIds), conditionSql); + const scoped = and( + inArray(scopeColumn(contextType), subContextIds), + ...deeperContexts.map((deeper) => isNull(scopeColumn(deeper))), + ); if (scoped) clauses.push(scoped); } - if (filter.restricted) { - for (const grant of filter.restricted.grants) { - const predicates = restrictionPredicates(filter.restricted, grant, table); - if (grant.subContextIds !== undefined) { - if (grant.subContextIds.length === 0) continue; - predicates.unshift(inArray(subContextColumn, grant.subContextIds)); - } - const scoped = predicates.length === 1 ? predicates[0] : and(...predicates); + for (const { condition, subContextIds, contextType, deeperContexts } of filter.conditionalScopes) { + const conditionSql = compileRowConditionSql(condition, table, userId); + const homeNulls = (deeperContexts ?? []).map((deeper) => isNull(scopeColumn(deeper))); + if (subContextIds === undefined) { + // Org-wide conditional grant: condition (plus home NULLs, if home-scoped) bounds the rows. + const scoped = homeNulls.length > 0 ? and(conditionSql, ...homeNulls) : conditionSql; if (scoped) clauses.push(scoped); + continue; } + if (subContextIds.length === 0) continue; + const scoped = and(inArray(scopeColumn(contextType), subContextIds), conditionSql, ...homeNulls); + if (scoped) clauses.push(scoped); } if (clauses.length === 0) return { kind: 'none' }; diff --git a/backend/src/permissions/split-by-permission.ts b/backend/src/permissions/split-by-permission.ts index 3a361b9cc..d5f3af04b 100644 --- a/backend/src/permissions/split-by-permission.ts +++ b/backend/src/permissions/split-by-permission.ts @@ -1,11 +1,11 @@ -import type { ContextEntityType, EntityActionType, ProductEntityType } from 'shared'; +import type { ContextEntityIdColumns, ContextEntityType, EntityActionType, ProductEntityType } from 'shared'; import type { AuthContext } from '#/core/context'; import { AppError } from '#/core/error'; import { baseDb } from '#/db/db'; import { tenantRead } from '#/db/tenant-context'; import { resolveEntities } from '#/modules/entities/entities-queries'; import { checkPermission } from '#/permissions'; -import { buildSubjectFromEntity } from '#/permissions/build-subject'; +import { buildSubject } from '#/permissions/build-subject'; /** * Splits entity IDs into allowed and disallowed based on the user's permissions. @@ -38,8 +38,15 @@ export const splitByPermission = async ( : await resolveEntities(ctx, entityType, ids); // Check permissions for all entities in a single batch operation. - // userId enables 'own' policy evaluation per entity. - const subjects = entities.map((entity) => buildSubjectFromEntity(entityType, entity)); + // userId enables 'own' policy evaluation per entity; the entity doubles as `row` so + // row conditions and public read grants evaluate from real row data. + const subjects = entities.map((entity) => + buildSubject(entityType, entity as Partial, { + id: entity.id, + createdBy: 'createdBy' in entity ? (entity.createdBy as string | null) : undefined, + row: entity as Record, + }), + ); const { results } = checkPermission(memberships, action, subjects, { isSystemAdmin, userId }); // Partition into allowed and disallowed diff --git a/backend/src/schemas/schema-naming-conventions.test.ts b/backend/src/schemas/schema-naming-conventions.test.ts index 2fc10bd5d..25c86a6b5 100644 --- a/backend/src/schemas/schema-naming-conventions.test.ts +++ b/backend/src/schemas/schema-naming-conventions.test.ts @@ -22,6 +22,13 @@ describe('yjs schema-naming conventions match the drizzle schema', () => { } }); + it('snake_cases camelCase entity types (multi-word forks)', () => { + // No cella entity type is camelCase, but forks add them (e.g. courseSection); + // the yjs relay would derive a wrong physical name without snake_casing here. + expect(toTableName('courseSection')).toBe('course_sections'); + expect(toTableName('task')).toBe('tasks'); + }); + it('each entity table name and read-columns follow the convention', () => { for (const [entityType, table] of Object.entries(entityTables)) { expect(toTableName(entityType), `table name for "${entityType}"`).toBe(getTableName(table)); diff --git a/backend/tests/integration/rls-security.test.ts b/backend/tests/integration/rls-security.test.ts index 2800e162c..b24e7a32d 100644 --- a/backend/tests/integration/rls-security.test.ts +++ b/backend/tests/integration/rls-security.test.ts @@ -896,7 +896,7 @@ const rlsSuiteReady = await (async () => { const tableName = getTableName(entityTables[entityType as keyof typeof entityTables]); const rowId = seededContextRowIdsByTable.get(tableName); if (!rowId) return []; - return baseImmutableColumns.map((col) => [tableName, col, entityType, rowId]); + return baseImmutableColumns.map((col): ImmutableEntityCase => [tableName, col, entityType, rowId]); }); const seededProductRowIdsByTable = new Map( @@ -908,7 +908,9 @@ const rlsSuiteReady = await (async () => { const tableName = getTableName(entityTables[entityType as keyof typeof entityTables]); const rowId = seededProductRowIdsByTable.get(tableName); if (!rowId) return []; - return [...baseImmutableColumns, 'organization_id'].map((col) => [tableName, col, entityType, rowId]); + return [...baseImmutableColumns, 'organization_id'].map( + (col): ImmutableEntityCase => [tableName, col, entityType, rowId], + ); }); const membershipCases: [string, string][] = membershipImmutableColumns.map((col) => ['memberships', col]); diff --git a/backend/tests/invitations/draft-invites.test.ts b/backend/tests/invitations/draft-invites.test.ts new file mode 100644 index 000000000..18959deb4 --- /dev/null +++ b/backend/tests/invitations/draft-invites.test.ts @@ -0,0 +1,189 @@ +import { eq } from 'drizzle-orm'; +import { getMyInvitations, membershipInvite } from 'sdk'; +import type { EntityRole } from 'shared'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import type { AuthContext } from '#/core/context'; +import { baseDb as db } from '#/db/db'; +import { dispatchDeferredInvites } from '#/modules/memberships/helpers/deferred-invites'; +import { inactiveMembershipsTable } from '#/modules/memberships/inactive-memberships-db'; +import { membershipsTable } from '#/modules/memberships/memberships-db'; +import { organizationsTable } from '#/modules/organization/organization-db'; +import { defaultHeaders } from '../fixtures'; +import { createOrganizationAdminUser, createTestOrganization, createTestSession, createTestUser } from '../helpers'; +import { createAppClient } from '../test-client'; +import { clearDatabase, mockFetchRequest, setTestConfig } from '../test-utils'; + +setTestConfig({ + enabledAuthStrategies: ['passkey'], + selfRegistration: true, +}); + +beforeAll(async () => { + mockFetchRequest(); +}); + +afterEach(async () => await clearDatabase()); + +/** + * Draft-mode invite deferral (context `publishedAt` mechanism): invites against an + * unpublished context are recorded but not dispatched; the context's most-privileged + * role stays live so staff can collaborate in drafts; `dispatchDeferredInvites` + * (called by a fork's publish flow) releases held invites with token rotation and the + * 7-day reminder throttle. Cella's template never nulls publishedAt, so everything + * here is dormant by default — these tests null it explicitly. + */ +describe('Draft context invite deferral', async () => { + const call = await createAppClient(); + + const createDraftOrgWorld = async () => { + const organization = await createTestOrganization(); + const admin = await createOrganizationAdminUser( + 'admin@example.com', + organization.id, + 'admin', + true, + organization.tenantId, + ); + const sessionCookie = await createTestSession(admin); + // The template always publishes at creation; draft state is a fork flow + await db.update(organizationsTable).set({ publishedAt: null }).where(eq(organizationsTable.id, organization.id)); + return { organization, admin, sessionCookie }; + }; + + const invite = async ( + organization: { id: string; tenantId: string }, + emails: string[], + role: EntityRole, + sessionCookie: string, + ) => { + return await call(membershipInvite, { + path: { tenantId: organization.tenantId, organizationId: organization.id }, + body: { emails, role }, + query: { entityId: organization.id, entityType: 'organization' as const }, + headers: { ...defaultHeaders, Cookie: sessionCookie }, + }); + }; + + const getInactiveRows = async (contextId: string) => { + return await db.select().from(inactiveMembershipsTable).where(eq(inactiveMembershipsTable.contextId, contextId)); + }; + + it('defers member invites against a draft context (row created, no dispatch, no membership)', async () => { + const { organization, sessionCookie } = await createDraftOrgWorld(); + + const { response } = await invite(organization, ['new-member@example.com'], 'member', sessionCookie); + expect(response.status).toBe(200); + + const [row] = await getInactiveRows(organization.id); + expect(row).toBeDefined(); + expect(row.role).toBe('member'); + expect(row.tokenId).toBeTruthy(); // token minted for the new user + expect(row.remindedAt).toBeNull(); // but email dispatch was held + + const memberships = await db.select().from(membershipsTable).where(eq(membershipsTable.contextId, organization.id)); + expect(memberships).toHaveLength(1); // only the inviting admin + }); + + it('keeps the most-privileged role live: admin invites dispatch on a draft context', async () => { + const { organization, sessionCookie } = await createDraftOrgWorld(); + + const { response } = await invite(organization, ['co-admin@example.com'], 'admin', sessionCookie); + expect(response.status).toBe(200); + + const [row] = await getInactiveRows(organization.id); + expect(row).toBeDefined(); + expect(row.role).toBe('admin'); + expect(row.remindedAt).toBeNull(); // initial invite email is not a reminder stamp + }); + + it('hides deferred invites from the invitee until dispatch', async () => { + const { organization, admin, sessionCookie } = await createDraftOrgWorld(); + const invitee = await createTestUser('invitee@example.com'); + + await invite(organization, [invitee.email], 'member', sessionCookie); + + const inviteeCookie = await createTestSession(invitee); + const myInvitations = () => call(getMyInvitations, { headers: { ...defaultHeaders, Cookie: inviteeCookie } }); + + const before = await myInvitations(); + expect(before.response.status).toBe(200); + expect((before.data as { items: unknown[] }).items).toHaveLength(0); + + // A fork's publish flow: stamp publishedAt, then release the held invites + await db + .update(organizationsTable) + .set({ publishedAt: new Date().toISOString() }) + .where(eq(organizationsTable.id, organization.id)); + await dispatchDeferredInvites({ var: { db, user: admin } } as unknown as AuthContext, { + contextIds: [organization.id], + }); + + const after = await myInvitations(); + expect((after.data as { items: unknown[] }).items).toHaveLength(1); + }); + + it('dispatch rotates tokens, stamps remindedAt, and honors the 7-day throttle', async () => { + const { organization, admin, sessionCookie } = await createDraftOrgWorld(); + + await invite(organization, ['deferred@example.com'], 'member', sessionCookie); + const [beforeRow] = await getInactiveRows(organization.id); + const originalTokenId = beforeRow.tokenId; + expect(beforeRow.remindedAt).toBeNull(); + + const ctx = { var: { db, user: admin } } as unknown as AuthContext; + const first = await dispatchDeferredInvites(ctx, { contextIds: [organization.id] }); + expect(first.dispatched).toBe(1); + + const [afterRow] = await getInactiveRows(organization.id); + expect(afterRow.remindedAt).not.toBeNull(); + expect(afterRow.tokenId).toBeTruthy(); + expect(afterRow.tokenId).not.toBe(originalTokenId); // raw secrets are unrecoverable → rotate + + // Second dispatch inside the throttle window: no re-send, no token churn + const second = await dispatchDeferredInvites(ctx, { contextIds: [organization.id] }); + expect(second.dispatched).toBe(0); + const [afterSecond] = await getInactiveRows(organization.id); + expect(afterSecond.remindedAt).toBe(afterRow.remindedAt); + expect(afterSecond.tokenId).toBe(afterRow.tokenId); + }); + + it('throttles reminder emails to once per 7 days on published contexts', async () => { + const organization = await createTestOrganization(); + const admin = await createOrganizationAdminUser( + 'admin@example.com', + organization.id, + 'admin', + true, + organization.tenantId, + ); + const sessionCookie = await createTestSession(admin); + // Reminders apply to invitees with a known email (existing users). Truly-new token + // invitees re-enter the new-user path, where duplicate invites are already + // conflict-suppressed without email. + const invitee = await createTestUser('pending@example.com'); + + await invite(organization, [invitee.email], 'member', sessionCookie); + const [initial] = await getInactiveRows(organization.id); + expect(initial.remindedAt).toBeNull(); // initial invite email is not a reminder + + // Re-invite immediately: pending invite was dispatched at creation → reminder suppressed + await invite(organization, [invitee.email], 'member', sessionCookie); + const [afterEarlyReinvite] = await getInactiveRows(organization.id); + expect(afterEarlyReinvite.remindedAt).toBeNull(); + + // Age the invite past the throttle window (remindedAt takes precedence over the + // immutable createdAt in the throttle check) → re-invite sends and re-stamps + const eightDaysAgo = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString(); + await db + .update(inactiveMembershipsTable) + .set({ remindedAt: eightDaysAgo }) + .where(eq(inactiveMembershipsTable.id, initial.id)); + const [aged] = await getInactiveRows(organization.id); + + await invite(organization, [invitee.email], 'member', sessionCookie); + const [afterDueReinvite] = await getInactiveRows(organization.id); + expect(afterDueReinvite.remindedAt).not.toBeNull(); + expect(afterDueReinvite.remindedAt).not.toBe(aged.remindedAt); + expect(new Date(afterDueReinvite.remindedAt!).getTime()).toBeGreaterThan(Date.now() - 24 * 60 * 60 * 1000); + }); +}); diff --git a/cdc/src/services/activity-service.ts b/cdc/src/services/activity-service.ts index 8a2c10d9a..5913fcecb 100644 --- a/cdc/src/services/activity-service.ts +++ b/cdc/src/services/activity-service.ts @@ -5,6 +5,8 @@ import type { TraceContext } from '../lib/tracing'; import type { CdcRowData } from '../types'; import { wsClient } from '../network/websocket-client'; import { nanoid } from 'shared/utils/nanoid'; +import { resolveContextKey } from '../utils/compute-unified-deltas'; +import { pickPermissionRowData } from '../utils/permission-row-data'; /** An individual entity cache-reservation token, sent with batch payloads. */ export interface CdcBatchReservation { @@ -13,16 +15,27 @@ export interface CdcBatchReservation { entityId: string; } +/** Per-row payload for batch messages: permission-relevant fields only (see pickPermissionRowData). */ +export interface CdcBatchRow { + seq?: number; + rowData: CdcRowData; +} + /** * Outbound activity + row-data message the CDC worker sends to the API server. * This is the producing end of the wire contract; the backend independently validates * the same shape with `cdcMessageSchema` (see backend/src/lib/cdc-websocket.ts, `CdcMessage`). * Keep both in sync: a field added here needs a matching field there, or the backend * will reject the message at runtime. + * + * `batchRows` carries per-row permission fields (context ids, createdBy, publicAt) so + * mixed-visibility batches dispatch per subscriber per row instead of deciding on the + * first row alone. */ export interface CdcOutboundMessage { activity: InsertActivityModel & { id?: string; seq?: number; batchUntilSeq?: number }; rowData: CdcRowData; + batchRows?: CdcBatchRow[]; cacheToken: string | null; batchReservations?: CdcBatchReservation[]; _trace: TraceContext; @@ -89,8 +102,23 @@ export interface BatchEventInfo { } /** - * Send a single batch CDC message to the API server. - * Uses the first event's activity as the representative, enriched with batch fields. + * The seq-context key of a batch event, mirroring seq allocation: seqs are counters per + * (contextKey, entityType) — see `computeBatchUnifiedDeltas`. Resource events (no + * entityType) never carry seqs; grouping them by org matches their dispatch channel. + */ +function batchContextKey({ activity, rowData }: BatchEventInfo): string { + if (!activity.entityType) return activity.organizationId ?? 'none'; + return resolveContextKey(activity.entityType, rowData, activity); +} + +/** + * Send batch CDC message(s) to the API server. + * + * Seqs are per-context counters, so one message can only describe one seq context: a + * transaction batch spanning contexts (e.g. one bulk create with mixed placements) is + * split into one message per seq context — the same contextKey seq allocation groups + * by — each with its own contiguous seq..batchUntilSeq range, batch cacheToken and + * reservations. Single-context batches send exactly one message, as before. */ export function sendBatchMessageToApi( events: BatchEventInfo[], @@ -98,6 +126,24 @@ export function sendBatchMessageToApi( ): void { if (events.length === 0) return; + const groups = new Map(); + for (const event of events) { + const key = batchContextKey(event); + const group = groups.get(key); + if (group) group.push(event); + else groups.set(key, [event]); + } + + for (const group of groups.values()) { + sendBatchGroupToApi(group, traceContext); + } +} + +/** Send one per-context batch group as a single message, using the first event as representative. */ +function sendBatchGroupToApi( + events: BatchEventInfo[], + traceContext: TraceContext, +): void { const first = events[0]; // Collect seqs for min/max range (create/update batches) @@ -105,8 +151,11 @@ export function sendBatchMessageToApi( const batchUntilSeq = seqs.length > 0 ? Math.max(...seqs) : undefined; const minSeq = seqs.length > 0 ? Math.min(...seqs) : undefined; + // Within one seq context the range is contiguous by construction (ranges are reserved + // per context, and the per-context split above matches that grouping) — if this fires, + // seq allocation itself is broken. if (minSeq !== undefined && batchUntilSeq !== undefined && batchUntilSeq - minSeq + 1 !== seqs.length) { - log.error('Non-contiguous seqs in batch — sync integrity at risk', { + log.error('Non-contiguous seqs within one seq context — sync integrity at risk', { minSeq, batchUntilSeq, seqCount: seqs.length, expected: batchUntilSeq - minSeq + 1, }); } @@ -126,13 +175,16 @@ export function sendBatchMessageToApi( const base = buildActivityPayload(first.activity, first.rowData, traceContext, minSeq); const activity = { ...base.activity, batchUntilSeq }; - const payload: CdcOutboundMessage = { ...base, activity, cacheToken: batchToken, batchReservations }; + // Per-row permission fields: dispatch decides per subscriber across ALL rows of the + // batch (the representative first row alone would mis-dispatch mixed-visibility batches) + const batchRows: CdcBatchRow[] = events.map((event) => ({ + seq: event.seq, + rowData: pickPermissionRowData(event.rowData) as CdcRowData, + })); + + const payload: CdcOutboundMessage = { ...base, activity, batchRows, cacheToken: batchToken, batchReservations }; if (!wsClient.send(payload)) { log.warn('Failed to send batch message to API', { batchSize: events.length }); } } - - - - diff --git a/cdc/src/services/transaction-buffer.ts b/cdc/src/services/transaction-buffer.ts index 727ddde45..cfdb6ee07 100644 --- a/cdc/src/services/transaction-buffer.ts +++ b/cdc/src/services/transaction-buffer.ts @@ -1,5 +1,5 @@ import type { Pgoutput } from 'pg-logical-replication'; -import { isContextEntity, appConfig, hierarchy } from 'shared'; +import { isContextEntity, appConfig } from 'shared'; import type { ContextEntityIdColumns } from 'shared'; import type { ParseMessageResult } from '../pipeline/parse-message'; import type { PendingEvent } from '../types'; @@ -15,14 +15,6 @@ for (const { embeddedEntity, hostEntity } of appConfig.entityEmbeddings) { softCascadeTargets.set(hostEntity, sources); } -/** Host relationships (hierarchy `host:`): product types that host others, and each hosted type's host id column. */ -const hostTypes = new Set(); -const hostIdColumnByHostedType = new Map(); -for (const { hostedType, hostType, hostIdColumn } of hierarchy.getHostRelations()) { - hostTypes.add(hostType); - hostIdColumnByHostedType.set(hostedType, hostIdColumn); -} - const { transactionTimeoutMs } = RESOURCE_LIMITS.buffers; /** @@ -46,9 +38,6 @@ export class TransactionBuffer { /** Context entity IDs deleted in the current transaction (streaming suppression). */ private deletedContextIds = new Set(); - /** Host product IDs deleted in the current transaction (hosted-row cascade suppression). */ - private deletedHostIds = new Set(); - /** Count of events suppressed in the current transaction. */ private suppressedCount = 0; @@ -78,7 +67,6 @@ export class TransactionBuffer { this.activeXid = msg.xid; this.pendingEvents = []; this.deletedContextIds.clear(); - this.deletedHostIds.clear(); this.suppressedCount = 0; this.startTimeout(); } @@ -102,13 +90,8 @@ export class TransactionBuffer { this.deletedContextIds.add(activity.subjectId); } - // Track host product deletes: hosted-row deletes in the same tx are cascades - if (activity.action === 'delete' && activity.entityType && hostTypes.has(activity.entityType) && activity.subjectId) { - this.deletedHostIds.add(activity.subjectId); - } - // Drop cascaded child deletes inline, never buffer them - if ((this.deletedContextIds.size > 0 || this.deletedHostIds.size > 0) && this.isCascadedDelete(result)) { + if (this.deletedContextIds.size > 0 && this.isCascadedDelete(result)) { this.suppressedCount++; return; } @@ -127,24 +110,21 @@ export class TransactionBuffer { let events = this.pendingEvents; let suppressedCount = this.suppressedCount; const deletedContextIds = this.deletedContextIds.size > 0 ? [...this.deletedContextIds] : null; - const deletedHostIds = this.deletedHostIds.size > 0 ? [...this.deletedHostIds] : null; this.activeXid = null; this.pendingEvents = []; this.deletedContextIds.clear(); - this.deletedHostIds.clear(); this.suppressedCount = 0; - // Second pass: catch child deletes that arrived before their parent context entity or - // host delete. Most children are dropped inline in onEvent(), but WAL order isn't always + // Second pass: catch child deletes that arrived before their parent context entity + // delete. Most children are dropped inline in onEvent(), but WAL order isn't always // parent-first (e.g., application-level batch deletes). This pass is cheap since only // surviving events remain (typically context entity deletes + non-delete mutations). - if ((deletedContextIds || deletedHostIds) && events.length > 1) { - const deletedContextSet = new Set(deletedContextIds ?? []); - const deletedHostSet = new Set(deletedHostIds ?? []); + if (deletedContextIds && events.length > 1) { + const deletedContextSet = new Set(deletedContextIds); const filtered: PendingEvent[] = []; for (const event of events) { - if (this.isCascadedDeleteByIds(event.result, deletedContextSet, deletedHostSet)) { + if (this.isCascadedDeleteByIds(event.result, deletedContextSet)) { suppressedCount++; } else { filtered.push(event); @@ -207,19 +187,14 @@ export class TransactionBuffer { * Used in onEvent() for streaming suppression. */ private isCascadedDelete(result: ParseMessageResult): boolean { - return this.isCascadedDeleteByIds(result, this.deletedContextIds, this.deletedHostIds); + return this.isCascadedDeleteByIds(result, this.deletedContextIds); } /** - * Check if an event is a cascaded delete from a deleted context entity or a deleted host. - * Context cascades match via the activity's context entity ID columns; host cascades match - * via the hosted row's host id column in rowData (host ids are product ids, not on the activity). + * Check if an event is a cascaded delete from a deleted context entity, matched via the + * activity's context entity ID columns. */ - private isCascadedDeleteByIds( - result: ParseMessageResult, - deletedContextIds: Set, - deletedHostIds: Set, - ): boolean { + private isCascadedDeleteByIds(result: ParseMessageResult, deletedContextIds: Set): boolean { const { activity } = result; if (activity.action !== 'delete') return false; @@ -227,24 +202,10 @@ export class TransactionBuffer { if (activity.entityType && isContextEntity(activity.entityType)) return false; // Check all context entity ID columns on this activity - if (deletedContextIds.size > 0) { - for (const idColumn of contextIdColumnKeys) { - const value = (activity as Partial)[idColumn]; - if (typeof value === 'string' && deletedContextIds.has(value)) { - return true; - } - } - } - - // Hosted-row cascade: the row's host id references a host deleted in this transaction - if (deletedHostIds.size > 0 && activity.entityType) { - const hostIdColumn = hostIdColumnByHostedType.get(activity.entityType); - if (hostIdColumn) { - const row = result.rowData ?? result.oldRowData; - const hostId = row?.[hostIdColumn]; - if (typeof hostId === 'string' && deletedHostIds.has(hostId)) { - return true; - } + for (const idColumn of contextIdColumnKeys) { + const value = (activity as Partial)[idColumn]; + if (typeof value === 'string' && deletedContextIds.has(value)) { + return true; } } diff --git a/cdc/src/tests/activity-service.test.ts b/cdc/src/tests/activity-service.test.ts index 748858277..f10961ce2 100644 --- a/cdc/src/tests/activity-service.test.ts +++ b/cdc/src/tests/activity-service.test.ts @@ -58,17 +58,71 @@ describe('sendBatchMessageToApi', () => { expect(log.error).not.toHaveBeenCalled(); }); - it('logs error for non-contiguous seqs (gap detection invariant)', () => { - // seq 10, 12: gap at 11 + it('logs error for non-contiguous seqs within one context (gap detection invariant)', () => { + // seq 10, 12 in the same seq context: gap at 11 — seq allocation itself broke const events = [mockBatchEvent(10), mockBatchEvent(12)]; sendBatchMessageToApi(events, { traceId: 'test', spanId: 'test' } as never); expect(log.error).toHaveBeenCalledWith( - 'Non-contiguous seqs in batch — sync integrity at risk', + 'Non-contiguous seqs within one seq context — sync integrity at risk', expect.objectContaining({ minSeq: 10, batchUntilSeq: 12, seqCount: 2, expected: 3 }), ); }); + it('splits a cross-context batch into per-context messages with contiguous ranges', () => { + // Seqs are per-context counters: org-a holds 10-11, org-b holds 5-7. Pre-split this + // was ONE message with seq 5..11 — a non-contiguous, semantically wrong range. + const inOrg = (org: string, seq: number): ReturnType => { + const event = mockBatchEvent(seq, `entity-${org}-${seq}`); + return { ...event, rowData: { ...event.rowData, organizationId: org } }; + }; + // Interleaved on purpose: grouping must not depend on input order + const events = [inOrg('org-a', 10), inOrg('org-b', 5), inOrg('org-a', 11), inOrg('org-b', 6), inOrg('org-b', 7)]; + sendBatchMessageToApi(events, { traceId: 'test', spanId: 'test' } as never); + + expect(wsClient.send).toHaveBeenCalledTimes(2); + const payloads = vi.mocked(wsClient.send).mock.calls.map((call) => call[0] as never as { + activity: { seq?: number; batchUntilSeq?: number }; + rowData: Record; + batchRows: { seq?: number }[]; + batchReservations?: unknown[]; + }); + const orgA = payloads.find((p) => p.rowData.organizationId === 'org-a'); + const orgB = payloads.find((p) => p.rowData.organizationId === 'org-b'); + + expect(orgA?.activity.seq).toBe(10); + expect(orgA?.activity.batchUntilSeq).toBe(11); + expect(orgB?.activity.seq).toBe(5); + expect(orgB?.activity.batchUntilSeq).toBe(7); + // Each message speaks only for its own context's rows and reservations + expect(orgA?.batchRows.map((row) => row.seq)).toEqual([10, 11]); + expect(orgB?.batchRows.map((row) => row.seq)).toEqual([5, 6, 7]); + expect(orgA?.batchReservations).toHaveLength(2); + expect(orgB?.batchReservations).toHaveLength(3); + // Both ranges are contiguous: the integrity invariant holds per context + expect(log.error).not.toHaveBeenCalled(); + }); + + it('slims batch rows to permission-relevant fields only', () => { + const event = mockBatchEvent(10); + const second = mockBatchEvent(11); + const events = [ + { ...event, rowData: { ...event.rowData, organizationId: 'org-a', createdBy: 'u1', name: 'secret' } }, + { ...second, rowData: { ...second.rowData, organizationId: 'org-a' } }, + ]; + sendBatchMessageToApi(events, { traceId: 'test', spanId: 'test' } as never); + + const payload = vi.mocked(wsClient.send).mock.calls[0][0] as never as { + batchRows: { seq?: number; rowData: Record }[]; + }; + // Context ids + identity/audit fields stay; content fields (name) never hit the wire + expect(payload.batchRows[0].rowData).toEqual({ + id: event.rowData.id, + organizationId: 'org-a', + createdBy: 'u1', + }); + }); + it('handles single-event batch without error', () => { const events = [mockBatchEvent(42)]; sendBatchMessageToApi(events, { traceId: 'test', spanId: 'test' } as never); diff --git a/cdc/src/utils/permission-row-data.ts b/cdc/src/utils/permission-row-data.ts new file mode 100644 index 000000000..33140d271 --- /dev/null +++ b/cdc/src/utils/permission-row-data.ts @@ -0,0 +1,31 @@ +import { appConfig } from 'shared'; +import type { CdcRowData } from '../types'; + +/** + * Row columns the API's SSE dispatch needs to evaluate read visibility per subscriber + * ("SSE mirrors the API"): identity/audit basics, every context id column, and the + * public marker (`publicAt`, read by `publicRead` grants). + * + * Forks that evaluate extra row fields at dispatch — custom row conditions or product + * rules in `canReceiveEntityEvent` (e.g. an author-only `draft` column) — must add + * those columns to the base list here so batch rows carry them on the wire. + * + * Used to slim per-row batch data on the wire — permission evaluation only, never content. + */ +const permissionRowKeys: Set = (() => { + const keys = new Set(['id', 'createdBy', 'deletedAt', 'publicAt']); + for (const contextType of appConfig.contextEntityTypes) { + keys.add(appConfig.entityIdColumnKeys[contextType]); + } + return keys; +})(); + +/** Pick the permission-relevant subset of a row (null-safe passthrough). */ +export function pickPermissionRowData(rowData: CdcRowData | null | undefined): CdcRowData | null { + if (!rowData) return null; + const slim: Record = {}; + for (const [key, value] of Object.entries(rowData)) { + if (permissionRowKeys.has(key)) slim[key] = value; + } + return slim as CdcRowData; +} diff --git a/cdc/src/utils/update-counts.ts b/cdc/src/utils/update-counts.ts index 3905f389c..f02c9cd30 100644 --- a/cdc/src/utils/update-counts.ts +++ b/cdc/src/utils/update-counts.ts @@ -1,5 +1,5 @@ import { appConfig, hierarchy, resolveNonNullAncestors } from 'shared'; -import type { ActivityAction, AncestorSource, HostRelation } from 'shared'; +import type { ActivityAction, AncestorSource } from 'shared'; import type { ActivityWithoutId } from '../pipeline/parse-message'; import type { TableMeta } from '../types'; import type { CdcRowData } from '../types'; @@ -16,7 +16,6 @@ export interface CountDelta { /** Hierarchy surface the counter machinery needs; injectable for synthetic-hierarchy tests. */ export type CountsHierarchy = AncestorSource & { isProduct(entityType: string): boolean; - getHostRelations(): readonly HostRelation[]; }; /** @@ -87,31 +86,6 @@ export function getCountDeltas( } } - // Host-relation counters: e: per host row (hierarchy `host:`, e.g. e:attachment - // per owning task). Live rows only, mirroring recalculation phase 4c; soft-delete/restore - // transitions arrive here remapped to delete/create via countAction. - for (const relation of h.getHostRelations()) { - if (relation.hostedType !== tableMeta.type) continue; - const counterKey = `e:${relation.hostedType}`; - const col = relation.hostIdColumn; - - if (countAction === 'create') { - const hostId = getStringValue(newRow, col); - if (hostId) deltas.push({ contextKey: hostId, deltas: { [counterKey]: 1 } }); - } else if (countAction === 'delete') { - const hostId = getStringValue(oldRow ?? newRow, col); - if (hostId) deltas.push({ contextKey: hostId, deltas: { [counterKey]: -1 } }); - } else if (countAction === 'update' && oldRow && newRow.deletedAt == null && oldRow.deletedAt == null) { - // Re-host: -1 on the old host, +1 on the new one (live→live only) - const oldHostId = getStringValue(oldRow, col); - const newHostId = getStringValue(newRow, col); - if (oldHostId !== newHostId) { - if (oldHostId) deltas.push({ contextKey: oldHostId, deltas: { [counterKey]: -1 } }); - if (newHostId) deltas.push({ contextKey: newHostId, deltas: { [counterKey]: 1 } }); - } - } - } - return deltas; } diff --git a/cella/ADD_ENTITY.md b/cella/ADD_ENTITY.md index e8b5cc971..67adc5b8c 100644 --- a/cella/ADD_ENTITY.md +++ b/cella/ADD_ENTITY.md @@ -49,17 +49,16 @@ export const hierarchy = createEntityHierarchy(roles) .product('task', { parent: 'project' }) .product('label', { parent: 'project' }) .product('note', { parent: 'project' }) // ← new - .product('attachment', { parent: 'project', host: 'task' }) + .product('attachment', { parent: 'project' }) .build(); ``` `.product(name, options)` options: - **`parent`** (required) — the entity's **home context**. Becomes the non-null `Id` column and the most-specific link in its ancestor chain. Products *must* have a home; the builder throws otherwise. -- **`host`** (optional) — product-to-product ownership (e.g. `attachment` hosted by `task`). Generates a nullable `Id` column; host deletes cascade to hosted rows; CDC maintains per-host `e:` counts. The host must be a previously-declared product sharing the **same home context**. See [§ Optional capabilities](#optional-capabilities). - **`relatedContexts`** (optional) — non-ancestor context references (nullable id columns). -The builder ([`entity-hierarchy.ts`](../shared/src/config-builder/entity-hierarchy.ts)) validates at construction: parents/hosts must be declared **before** children, parents must be contexts, hosts must be products sharing the home context, roles must exist, `organization` + `user()` are mandatory. **Order matters** — declare parents and hosts earlier in the chain. +The builder ([`entity-hierarchy.ts`](../shared/src/config-builder/entity-hierarchy.ts)) validates at construction: parents must be declared **before** children, parents must be contexts, roles must exist, `organization` + `user()` are mandatory. **Order matters** — declare parents earlier in the chain. > **Public readability is not declared here.** It is a permission concern — see Step 3. @@ -88,7 +87,7 @@ You do **not** edit [`app-config.ts`](../shared/src/config-builder/app-config.ts ### Step 3 — 🔴 Declare access policies -Add a `case` to the `switch` in [`shared/config/permissions-config.ts`](../shared/config/permissions-config.ts). The callback gives you `contexts` (CRUD cells per role×context), `publicRead`, `delegateToHost`, and `restrict`. Model it on `task`: +Add a `case` to the `switch` in [`shared/config/permissions-config.ts`](../shared/config/permissions-config.ts). The callback gives you `contexts` (CRUD cells per role×context) and `publicRead`. Model it on `task`: ```ts case 'note': @@ -106,8 +105,6 @@ Cell values: `1` allowed, `0`/omitted denied, `'own'` = built-in "actor is creat - **Product entities have no "self" rows** — their context rows are *home* rows where `create` is meaningful. (Context entities distinguish *elevation* rows on an ancestor, which carry `create`, from *self* rows on the same context, which omit it. See the header comment in `permissions-config.ts`.) - **`publicRead(mode)`** — `'publicSelf'` (row's own `publicAt`), `'publicParent'` (parent's `publicAt`), or `'publicParentOrSelf'`. A `publicParent` grant requires the parent to declare a self-publication grant, or configuration throws. -- **`delegateToHost(actions)`** — hosted row inherits the action if its host row allows it; requires `host:` in the hierarchy (Step 1). -- **`restrict(restriction)`** — subject-level row restriction. ### Step 4 — 🔴 Create the database table @@ -146,7 +143,7 @@ Mandatory columns come from the spread helpers — **do not hand-write them**: | `productEntityColumns('')` | `stx` (jsonb, sync metadata), `seq` (bigint, CDC delta cursor), `description`, `keywords`, `createdBy/updatedBy`, `deletedAt/deletedBy` (soft delete) | | ↳ `tenantEntityColumns` (nested) | `id` (uuid v7 PK via `generateId`), `entityType` (enum locked to the type), `tenantId` (FK → tenants), `name`, `createdAt`, `updatedAt` | -Conventions: `snakeCase.table(...)` maps camelCase fields → snake_case columns automatically; table name is the pluralized type (`note` → `notes`); ids are plain **UUID v7** (time-ordered, no prefixes). You may replace the explicit `organizationId`/`projectId` columns with `...contextRelationColumns('note')` (emits NOT-NULL ancestor id columns from the hierarchy) and, for hosted entities, `...hostRelationColumns('note')` — see [`attachment-db.ts`](../backend/src/modules/attachment/attachment-db.ts). +Conventions: `snakeCase.table(...)` maps camelCase fields → snake_case columns automatically; table name is the pluralized type (`note` → `notes`); ids are plain **UUID v7** (time-ordered, no prefixes). You may replace the explicit `organizationId`/`projectId` columns with `...contextRelationColumns('note')` (emits NOT-NULL ancestor id columns from the hierarchy) — see [`attachment-db.ts`](../backend/src/modules/attachment/attachment-db.ts). Which tables get RLS, immutability triggers, the CDC publication, and `REPLICA IDENTITY FULL` is derived from the registry in the next step — 🟢 no per-table wiring for any of those. @@ -308,7 +305,6 @@ Context entities do **not** go through the CDC/SSE product pipeline or the wire- ## Optional capabilities -- **Host (product-to-product ownership)** — declare `host: 'task'` in Step 1; the hosted table gets a nullable `Id` column (`...hostRelationColumns`), host deletes cascade (API + CDC), CDC maintains per-host `e:` counts, and you can `delegateToHost([...])` in Step 3. Host and hosted must share the same home context. Reference: `attachment` hosted by `task`. - **Public read** — `publicRead(mode)` in Step 3 (see modes above). Product entities typically use `'publicParent'`; the parent must itself declare a self-publication grant. - **Unseen-count badges** — add the type to `seenTrackedEntityTypes` (Step 2); tracking in [`app-stream-handler.ts`](../frontend/src/query/realtime/app-stream-handler.ts) and the `seen` module then covers it automatically, grouping badges by the parent context. - **Embedded id-arrays** — if the entity is referenced as an id array on another entity (like `label` in `task.labels`), add an `entityEmbeddings` entry (Step 2) so CDC ref-counting, cache patching, and cascade suppression handle it. diff --git a/frontend/src/modules/common/form-fields/select-role-radio.tsx b/frontend/src/modules/common/form-fields/select-role-radio.tsx index c755c1b92..a86ea2656 100644 --- a/frontend/src/modules/common/form-fields/select-role-radio.tsx +++ b/frontend/src/modules/common/form-fields/select-role-radio.tsx @@ -1,27 +1,31 @@ import { useTranslation } from 'react-i18next'; -import { type EntityRole, roles } from 'shared'; +import { type ContextEntityType, type EntityRole, hierarchy, roles } from 'shared'; import { RadioGroup, RadioGroupItem } from '~/modules/ui/radio-group'; import { cn } from '~/utils/cn'; interface Props { onValueChange: (value?: string) => void; value?: EntityRole; + /** Restrict options to this context entity's role vocabulary (e.g. course → staff/student/guest). */ + entityType?: ContextEntityType; className?: string; } /** * Radio group for selecting a single entity role. */ -export function SelectRoleRadio({ onValueChange, value, className }: Props) { +export function SelectRoleRadio({ onValueChange, value, entityType, className }: Props) { const { t } = useTranslation(); + const roleOptions = entityType ? hierarchy.getRoles(entityType) : roles.all; + return ( onValueChange(v as string)} className={cn('inline-flex items-center gap-4', className)} > - {roles.all.map((role) => ( + {roleOptions.map((role) => ( // biome-ignore lint/a11y/noLabelWithoutControl: label is for visual grouping only, no input needed