diff --git a/apps/cloud/drizzle/0024_typical_ultimates.sql b/apps/cloud/drizzle/0024_typical_ultimates.sql new file mode 100644 index 0000000000..49ced88724 --- /dev/null +++ b/apps/cloud/drizzle/0024_typical_ultimates.sql @@ -0,0 +1 @@ +ALTER TABLE "artifact" ADD COLUMN "preview_capture" text; \ No newline at end of file diff --git a/apps/cloud/drizzle/meta/0024_snapshot.json b/apps/cloud/drizzle/meta/0024_snapshot.json new file mode 100644 index 0000000000..0526e3c5fc --- /dev/null +++ b/apps/cloud/drizzle/meta/0024_snapshot.json @@ -0,0 +1,1814 @@ +{ + "id": "ff33d550-f9c5-4793-adb5-42d498a0415c", + "prevId": "151e78d4-1dc9-471b-b44b-df11ee206c2d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workos_updated_at": { + "name": "workos_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sign_in_at": { + "name": "last_sign_in_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_email_lower_idx": { + "name": "accounts_email_lower_idx", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.membership_tombstones": { + "name": "membership_tombstones", + "schema": "", + "columns": { + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "membership_tombstones_organization_id_idx": { + "name": "membership_tombstones_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "membership_tombstones_account_id_accounts_id_fk": { + "name": "membership_tombstones_account_id_accounts_id_fk", + "tableFrom": "membership_tombstones", + "tableTo": "accounts", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "membership_tombstones_organization_id_organizations_id_fk": { + "name": "membership_tombstones_organization_id_organizations_id_fk", + "tableFrom": "membership_tombstones", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "workos_updated_at": { + "name": "workos_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memberships_membership_id_unique": { + "name": "memberships_membership_id_unique", + "columns": [ + { + "expression": "membership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memberships_organization_id_idx": { + "name": "memberships_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memberships_account_id_organization_id_pk": { + "name": "memberships_account_id_organization_id_pk", + "columns": ["account_id", "organization_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backfilled_at": { + "name": "backfilled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "workos_updated_at": { + "name": "workos_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workos_sync": { + "name": "workos_sync", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "range_start": { + "name": "range_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "backfill_completed_at": { + "name": "backfill_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "drained_at": { + "name": "drained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bindings": { + "name": "bindings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "preview": { + "name": "preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_capture": { + "name": "preview_capture", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compiled_fingerprint": { + "name": "compiled_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compiled_js": { + "name": "compiled_js", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compiled_css": { + "name": "compiled_css", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "artifact_uidx": { + "name": "artifact_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob": { + "name": "blob", + "schema": "", + "columns": { + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "blob_id_uidx": { + "name": "blob_id_uidx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection": { + "name": "connection", + "schema": "", + "columns": { + "integration": { + "name": "integration", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_ids": { + "name": "item_ids", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "credential_write": { + "name": "credential_write", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_health": { + "name": "last_health", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "tools_synced_at": { + "name": "tools_synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tools_sync_started_at": { + "name": "tools_sync_started_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "oauth_client": { + "name": "oauth_client", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_owner": { + "name": "oauth_client_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_item_id": { + "name": "refresh_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "refresh_lease_until": { + "name": "refresh_lease_until", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "refresh_lease_holder": { + "name": "refresh_lease_holder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_refreshed_at": { + "name": "oauth_refreshed_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "oauth_scope": { + "name": "oauth_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_token_url": { + "name": "oauth_token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_state": { + "name": "provider_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connection_uidx": { + "name": "connection_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.definition": { + "name": "definition", + "schema": "", + "columns": { + "integration": { + "name": "integration", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection": { + "name": "connection", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "definition_uidx": { + "name": "definition_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "health_check": { + "name": "health_check", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "config_revised_at": { + "name": "config_revised_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "can_remove": { + "name": "can_remove", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "can_refresh": { + "name": "can_refresh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "integration_uidx": { + "name": "integration_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant": { + "name": "grant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_item_id": { + "name": "client_secret_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_write": { + "name": "credential_write", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_integration": { + "name": "origin_integration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_issuer": { + "name": "origin_issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_redirect_uri": { + "name": "origin_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_client_uidx": { + "name": "oauth_client_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_session": { + "name": "oauth_session", + "schema": "", + "columns": { + "state": { + "name": "state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "client_slug": { + "name": "client_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration": { + "name": "integration", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pkce_verifier": { + "name": "pkce_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_session_uidx": { + "name": "oauth_session_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "collection": { + "name": "collection", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "plugin_storage_uidx": { + "name": "plugin_storage_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "collection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.private_executor_cloud_settings": { + "name": "private_executor_cloud_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subject": { + "name": "subject", + "schema": "", + "columns": { + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "subject_uidx": { + "name": "subject_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool": { + "name": "tool", + "schema": "", + "columns": { + "integration": { + "name": "integration", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection": { + "name": "connection", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_schema": { + "name": "input_schema", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "output_schema": { + "name": "output_schema", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "annotations": { + "name": "annotations", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_uidx": { + "name": "tool_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_policy": { + "name": "tool_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_policy_uidx": { + "name": "tool_policy_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/cloud/drizzle/meta/_journal.json b/apps/cloud/drizzle/meta/_journal.json index d499dd5953..0c827e76de 100644 --- a/apps/cloud/drizzle/meta/_journal.json +++ b/apps/cloud/drizzle/meta/_journal.json @@ -169,6 +169,13 @@ "when": 1792505978902, "tag": "0023_groovy_titania", "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1792517905833, + "tag": "0024_typical_ultimates", + "breakpoints": true } ] } diff --git a/apps/cloud/src/db/executor-schema.ts b/apps/cloud/src/db/executor-schema.ts index a3f2a4a997..f3ea64eeb9 100644 --- a/apps/cloud/src/db/executor-schema.ts +++ b/apps/cloud/src/db/executor-schema.ts @@ -242,6 +242,7 @@ export const artifact = pgTable( code: text("code").notNull(), bindings: json("bindings"), preview: text("preview"), + preview_capture: text("preview_capture"), compiled_fingerprint: text("compiled_fingerprint"), compiled_js: text("compiled_js"), compiled_css: text("compiled_css"), diff --git a/apps/host-cloudflare/src/db/d1.test.ts b/apps/host-cloudflare/src/db/d1.test.ts index d4eb08191a..5b6fa6d731 100644 --- a/apps/host-cloudflare/src/db/d1.test.ts +++ b/apps/host-cloudflare/src/db/d1.test.ts @@ -213,6 +213,28 @@ describe("createD1ExecutorDb", () => { await sqlite.close(); }); + it("adds the preview_capture artifact column to a database that predates it", async () => { + const sqlite = await createSqliteTestFumaDb({ tables: collectTables() }); + const { db } = makeRecordingD1(sqlite.client); + const first = await createD1ExecutorDb(db, undefined); + await first.close(); + await sqlite.client.execute(`ALTER TABLE "artifact" DROP COLUMN "preview_capture"`); + await sqlite.client.execute( + `UPDATE private_executor_cloudflare_schema_fingerprint + SET fingerprint = 'stale' + WHERE id = 'runtime-schema'`, + ); + + const second = await createD1ExecutorDb(db, undefined); + await second.close(); + + const columns = await sqlite.client.execute(`PRAGMA table_info("artifact")`); + const names = columns.rows.map((row) => String(row.name)); + expect(names).toEqual(expect.arrayContaining(["preview_capture"])); + + await sqlite.close(); + }); + it("converges safely when concurrent opens see the same stale fingerprint", async () => { const sqlite = await createSqliteTestFumaDb({ tables: collectTables() }); const { db } = makeRecordingD1(sqlite.client); diff --git a/packages/core/api/src/artifacts/api.ts b/packages/core/api/src/artifacts/api.ts index 950a681b12..a71371d7f3 100644 --- a/packages/core/api/src/artifacts/api.ts +++ b/packages/core/api/src/artifacts/api.ts @@ -89,25 +89,14 @@ export const ARTIFACT_PREVIEW_IMAGE_LIMIT = 512 * 1024; /** * A snapshot of the artifact as the viewer just saw it, WITH its data. * - * ## Data safety — read before widening who may call this + * ## Data safety * - * Unlike the layout preview, this is captured after the artifact's queries - * resolved, so it contains whatever the capturing viewer was allowed to see. - * That makes it per-viewer state that happens to be stored on a shared-shaped - * row. - * - * It is correct today only because of a property that holds by circumstance and - * not by construction: artifacts are viewer-owned, so the only person who can - * read the row is the person whose data is in the snapshot. When org-tier - * sharing lands — the `owner` column already anticipates it — that stops being - * true, and this preview would leak one member's numbers into another member's - * gallery. - * - * At that point it must either move to per-viewer storage keyed by (artifact, - * viewer), or be excluded from any view its capturer does not own. The LAYOUT - * preview, written at create time from a render with no data in it, is the one - * that stays shareable — which is why the column carries both kinds rather than - * being replaced by this one. + * Captured after the artifact's queries resolved, so it contains whatever the + * capturing viewer was allowed to see. It is stored apart from the shared + * layout preview, and only the artifact's creator may write it or is shown it. + * A call from anyone else is answered `{ stored: false }`; every other member + * keeps seeing the layout preview, which is derived from the source with no + * data in it. */ const SetArtifactPreviewPayload = Schema.Struct({ /** Markup from the settled render. Sanitized by the handler through the same diff --git a/packages/core/api/src/artifacts/artifacts-preview.test.ts b/packages/core/api/src/artifacts/artifacts-preview.test.ts new file mode 100644 index 0000000000..d4e8738324 --- /dev/null +++ b/packages/core/api/src/artifacts/artifacts-preview.test.ts @@ -0,0 +1,108 @@ +import { HttpApiBuilder } from "effect/unstable/httpapi"; +import { HttpRouter, HttpServer } from "effect/unstable/http"; +import { describe, expect, it } from "@effect/vitest"; +import { Context, Effect, Layer } from "effect"; + +import { createExecutor, type Executor } from "@executor-js/sdk"; +import { makeTestConfig } from "@executor-js/sdk/testing"; + +import { ExecutorApi } from "../api"; +import { observabilityMiddleware } from "../observability"; +import { CoreHandlers, ExecutionEngineService, ExecutorService } from "../server"; + +const webHandlerFor = (executor: Executor) => + Effect.acquireRelease( + Effect.sync(() => + HttpRouter.toWebHandler( + HttpApiBuilder.layer(ExecutorApi).pipe( + Layer.provide(CoreHandlers), + Layer.provide(observabilityMiddleware(ExecutorApi)), + Layer.provide(Layer.succeed(ExecutorService)(executor)), + Layer.provide( + Layer.succeed(ExecutionEngineService)({} as ExecutionEngineService["Service"]), + ), + Layer.provideMerge(HttpServer.layerServices), + Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })), + ), + { disableLogger: true }, + ), + ), + (web) => Effect.promise(() => web.dispose()), + ); + +const handlerContextFor = (executor: Executor) => + Context.make(ExecutorService, executor).pipe( + Context.add(ExecutionEngineService, {} as ExecutionEngineService["Service"]), + ); + +const CAPTURE = '

12,480 active

'; + +const putPreview = (executor: Executor, id: string) => + Effect.gen(function* () { + const web = yield* webHandlerFor(executor); + const response = yield* Effect.promise(() => + web.handler( + new Request(`http://localhost/artifacts/${id}/preview`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ preview: CAPTURE }), + }), + handlerContextFor(executor), + ), + ); + return { + status: response.status, + body: yield* Effect.promise(() => response.json()), + }; + }); + +const insertOrgArtifact = ( + config: ReturnType, + id: string, + createdBy: string, +) => + Effect.promise(() => + config.db.create("artifact", { + tenant: "test-tenant", + subject: "", + owner: "org", + id, + title: "Revenue", + description: null, + code: "const a = 1;", + bindings: null, + preview: null, + compiled_fingerprint: null, + compiled_js: null, + compiled_css: null, + created_by: createdBy, + created_at: new Date(), + updated_at: new Date(), + }), + ); + +describe("artifact preview capture over HTTP", () => { + it.effect("answers stored: false when the caller did not create the artifact", () => + Effect.gen(function* () { + const config = makeTestConfig(); + const executor = yield* createExecutor(config); + yield* insertOrgArtifact(config, "art_theirs", "someone-else"); + + const result = yield* putPreview(executor, "art_theirs"); + + expect(result).toEqual({ status: 200, body: { stored: false } }); + }), + ); + + it.effect("answers stored: true when the caller created the artifact", () => + Effect.gen(function* () { + const config = makeTestConfig(); + const executor = yield* createExecutor(config); + yield* insertOrgArtifact(config, "art_mine", "test-subject"); + + const result = yield* putPreview(executor, "art_mine"); + + expect(result).toEqual({ status: 200, body: { stored: true } }); + }), + ); +}); diff --git a/packages/core/api/src/handlers/artifacts.ts b/packages/core/api/src/handlers/artifacts.ts index 2c59d5ae87..e19dd36f31 100644 --- a/packages/core/api/src/handlers/artifacts.ts +++ b/packages/core/api/src/handlers/artifacts.ts @@ -118,17 +118,17 @@ export const ArtifactsHandlers = HttpApiBuilder.group(ExecutorApi, "artifacts", // same allowlist the create-time preview goes through, which is what // makes the console safe to render either kind identically. // - // A payload that sanitizes to nothing is reported as not stored - // rather than as an error: the caller is a viewer who merely opened - // an artifact, and the card simply keeps its layout preview. + // A payload that sanitizes to nothing, or one from a caller who did + // not create the artifact, is reported as not stored rather than as + // an error: the caller is a viewer who merely opened an artifact, and + // the card simply keeps its layout preview. const sanitized = sanitizeArtifactPreviewMarkup(payload.preview); if (sanitized === null) return { stored: false }; const executor = yield* ExecutorService; - yield* executor.artifacts.setPreview({ + return yield* executor.artifacts.setPreview({ id: path.artifactId, preview: sanitized, }); - return { stored: true }; }), ), ), diff --git a/packages/core/sdk/src/artifact-preview.ts b/packages/core/sdk/src/artifact-preview.ts index ca89cde388..b9fc7d8e88 100644 --- a/packages/core/sdk/src/artifact-preview.ts +++ b/packages/core/sdk/src/artifact-preview.ts @@ -48,6 +48,13 @@ */ export const ARTIFACT_PREVIEW_MARKUP_LIMIT = 32 * 1024; +/** + * Whether a stored preview is a raster data URL rather than markup. Markup is + * all this host writes now, but an image snapshot captured by an older build + * can still sit in a row, and it carries its capturer's data. + */ +export const isArtifactPreviewImage = (value: string): boolean => value.startsWith("data:image/"); + /** * Elements that may appear in a preview. * diff --git a/packages/core/sdk/src/artifact.ts b/packages/core/sdk/src/artifact.ts index 678ed2a2e5..27c3fc77f5 100644 --- a/packages/core/sdk/src/artifact.ts +++ b/packages/core/sdk/src/artifact.ts @@ -12,6 +12,7 @@ import { Option, Schema, type Effect } from "effect"; import type { ArtifactFullRow, ArtifactRow, ArtifactSummaryRow } from "./core-schema"; +import { isArtifactPreviewImage } from "./artifact-preview"; import { ArtifactId, ConnectionName, IntegrationSlug, Owner } from "./ids"; /** @@ -122,14 +123,10 @@ export interface Artifact { * What the gallery draws for this artifact, or `null` to fall back to the * schematic. * - * DATA SAFETY. A `layout` preview is derived from `code` alone and is safe to - * treat as shared artifact metadata. An `image` preview is NOT: it is pixels - * of the artifact rendered with whatever data the VIEWER could see. Today - * every artifact is viewer-owned, so the row and the viewer are the same - * person and storing it here is correct. When org-tier sharing lands, an - * image preview must move to per-viewer storage or be excluded from shared - * views — otherwise one member's numbers become another's thumbnail. The - * `layout` half stays shareable either way, which is why both kinds exist. + * DATA SAFETY. The create-time layout preview is derived from `code` alone + * and is shared with every member. A settled capture holds its capturer's + * data, so it is stored apart and only its creator is shown it; everyone else + * gets the layout preview. */ readonly preview: ArtifactPreview | null; readonly createdAt: Date; @@ -244,18 +241,46 @@ export interface SetArtifactPreviewInput { const asDate = (value: Date | number | string): Date => value instanceof Date ? value : new Date(value); -export const rowToArtifactSummary = (row: ArtifactSummaryRow): ArtifactSummary => ({ +/** + * The preview this viewer may see. The creator gets their settled capture, else + * the layout preview. Anyone else gets the layout preview only, and nothing + * when that column holds an image snapshot written before captures were stored + * apart. + */ +const previewForViewer = ( + row: Pick, + viewer: string | null, +): ArtifactPreview | null => { + if (isArtifactCreator(row, viewer)) { + return previewFromColumn(row.preview_capture) ?? previewFromColumn(row.preview); + } + return typeof row.preview === "string" && isArtifactPreviewImage(row.preview) + ? null + : previewFromColumn(row.preview); +}; + +/** Whether `viewer` created the row: any personal row the policy let them + * read, or an org row recorded against their subject. */ +export const isArtifactCreator = ( + row: Pick, + viewer: string | null, +): boolean => row.owner === "user" || (viewer !== null && row.created_by === viewer); + +export const rowToArtifactSummary = ( + row: ArtifactSummaryRow, + viewer: string | null, +): ArtifactSummary => ({ id: ArtifactId.make(row.id), owner: row.owner as Owner, title: row.title, description: row.description ?? null, - preview: previewFromColumn(row.preview), + preview: previewForViewer(row, viewer), createdAt: asDate(row.created_at), updatedAt: asDate(row.updated_at), }); -export const rowToArtifact = (row: ArtifactFullRow): Artifact => ({ - ...rowToArtifactSummary(row), +export const rowToArtifact = (row: ArtifactFullRow, viewer: string | null): Artifact => ({ + ...rowToArtifactSummary(row, viewer), code: row.code, bindings: bindingsFromColumn(row.bindings), }); diff --git a/packages/core/sdk/src/artifacts.test.ts b/packages/core/sdk/src/artifacts.test.ts index 5a8eb1e147..4404ed1ed8 100644 --- a/packages/core/sdk/src/artifacts.test.ts +++ b/packages/core/sdk/src/artifacts.test.ts @@ -328,6 +328,7 @@ describe("executor.artifacts", () => { readonly owner: "org" | "user"; readonly subject: string; readonly createdBy: string | null; + readonly preview?: string | null; }, ) => Effect.promise(() => @@ -340,7 +341,8 @@ describe("executor.artifacts", () => { description: null, code: CODE, bindings: null, - preview: null, + preview: row.preview ?? null, + preview_capture: null, compiled_fingerprint: null, compiled_js: null, compiled_css: null, @@ -468,6 +470,178 @@ describe("executor.artifacts", () => { expect(yield* bob.artifacts.list()).toEqual([]); }).pipe(Effect.scoped), ); + + describe("capture privacy", () => { + const LAYOUT = '

Revenue

'; + const LAYOUT_V2 = '

Revenue v2

'; + const CAPTURE = '

Revenue

12,480 active

'; + const BOB_CAPTURE = '

Revenue

3 active

'; + const IMAGE = "data:image/png;base64,AAAA"; + const markup = (value: string) => ({ kind: "layout", markup: value }); + + it.effect("the creator sees their capture while another member keeps the layout", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + const bob = yield* makeMember(config, "bob"); + const saved = yield* alice.artifacts.save({ + title: "Revenue", + code: CODE, + preview: LAYOUT, + }); + + const result = yield* alice.artifacts.setPreview({ id: saved.id, preview: CAPTURE }); + + expect(result).toEqual({ stored: true }); + expect((yield* alice.artifacts.get(saved.id)).preview).toEqual(markup(CAPTURE)); + expect((yield* bob.artifacts.get(saved.id)).preview).toEqual(markup(LAYOUT)); + }).pipe(Effect.scoped), + ); + + it.effect("another member's capture is not stored and changes nobody's preview", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + const bob = yield* makeMember(config, "bob"); + const saved = yield* alice.artifacts.save({ + title: "Revenue", + code: CODE, + preview: LAYOUT, + }); + + const result = yield* bob.artifacts.setPreview({ id: saved.id, preview: BOB_CAPTURE }); + + expect(result).toEqual({ stored: false }); + expect((yield* alice.artifacts.get(saved.id)).preview).toEqual(markup(LAYOUT)); + expect((yield* bob.artifacts.get(saved.id)).preview).toEqual(markup(LAYOUT)); + }).pipe(Effect.scoped), + ); + + it.effect("a capture on an org artifact with no recorded creator is not stored", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + const bob = yield* makeMember(config, "bob"); + yield* insertRow( + config, + { tenant: String(config.tenant), subject: "bob" }, + { id: "art_legacy", owner: "org", subject: "", createdBy: null, preview: LAYOUT }, + ); + + expect(yield* alice.artifacts.setPreview({ id: "art_legacy", preview: CAPTURE })).toEqual( + { stored: false }, + ); + expect(yield* bob.artifacts.setPreview({ id: "art_legacy", preview: CAPTURE })).toEqual({ + stored: false, + }); + expect((yield* alice.artifacts.get("art_legacy")).preview).toEqual(markup(LAYOUT)); + }).pipe(Effect.scoped), + ); + + it.effect("the owner of a personal artifact stores and sees their capture", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + yield* insertRow( + config, + { tenant: String(config.tenant), subject: "test-subject" }, + { + id: "art_mine", + owner: "user", + subject: "test-subject", + createdBy: null, + preview: LAYOUT, + }, + ); + + const result = yield* alice.artifacts.setPreview({ id: "art_mine", preview: CAPTURE }); + + expect(result).toEqual({ stored: true }); + expect((yield* alice.artifacts.get("art_mine")).preview).toEqual(markup(CAPTURE)); + }).pipe(Effect.scoped), + ); + + it.effect("sharing a personal artifact keeps the owner's capture", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + const bob = yield* makeMember(config, "bob"); + yield* insertRow( + config, + { tenant: String(config.tenant), subject: "test-subject" }, + { + id: "art_mine", + owner: "user", + subject: "test-subject", + createdBy: null, + preview: LAYOUT, + }, + ); + yield* alice.artifacts.setPreview({ id: "art_mine", preview: CAPTURE }); + + yield* alice.artifacts.share({ id: "art_mine" }); + + expect((yield* alice.artifacts.get("art_mine")).preview).toEqual(markup(CAPTURE)); + expect((yield* bob.artifacts.get("art_mine")).preview).toEqual(markup(LAYOUT)); + }).pipe(Effect.scoped), + ); + + it.effect("an overwrite clears the capture and falls back to the new layout", () => + Effect.gen(function* () { + const { executor: alice } = yield* makeTestWorkspaceHarness(); + const saved = yield* alice.artifacts.save({ + title: "Revenue", + code: CODE, + preview: LAYOUT, + }); + yield* alice.artifacts.setPreview({ id: saved.id, preview: CAPTURE }); + + yield* alice.artifacts.save({ + id: saved.id, + title: "Revenue", + code: CODE, + preview: LAYOUT_V2, + }); + + expect((yield* alice.artifacts.get(saved.id)).preview).toEqual(markup(LAYOUT_V2)); + }).pipe(Effect.scoped), + ); + + it.effect("an image snapshot left in the layout column is shown to its creator only", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + const bob = yield* makeMember(config, "bob"); + const saved = yield* alice.artifacts.save({ + title: "Revenue", + code: CODE, + preview: IMAGE, + }); + + expect((yield* alice.artifacts.get(saved.id)).preview).toEqual(markup(IMAGE)); + expect((yield* bob.artifacts.get(saved.id)).preview).toBe(null); + }).pipe(Effect.scoped), + ); + + it.effect("lists the creator's capture, and hides an image from non-creators", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + const bob = yield* makeMember(config, "bob"); + const saved = yield* alice.artifacts.save({ + title: "Revenue", + code: CODE, + preview: LAYOUT, + }); + yield* alice.artifacts.setPreview({ id: saved.id, preview: CAPTURE }); + yield* insertRow( + config, + { tenant: String(config.tenant), subject: "bob" }, + { id: "art_legacy", owner: "org", subject: "", createdBy: null, preview: IMAGE }, + ); + + const aliceList = yield* alice.artifacts.list(); + const bobList = yield* bob.artifacts.list(); + + expect(aliceList.find((a) => a.id === saved.id)?.preview).toEqual(markup(CAPTURE)); + expect(bobList.map((a) => a.id)).toEqual(["art_legacy"]); + expect(bobList[0]?.preview).toBe(null); + }).pipe(Effect.scoped), + ); + }); }); describe("previews", () => { diff --git a/packages/core/sdk/src/core-schema.ts b/packages/core/sdk/src/core-schema.ts index dc80fcd3c4..efc1115858 100644 --- a/packages/core/sdk/src/core-schema.ts +++ b/packages/core/sdk/src/core-schema.ts @@ -425,6 +425,11 @@ export const coreTables = defineTables({ // column existed have none, and so does one whose render was too large, // failed open, or has never been opened. preview: nullableTextColumn("preview"), + // The settled snapshot a viewer captured after opening the artifact. It + // holds whatever data that viewer could see, so it is written and read + // only for the artifact's creator and is never part of what other + // members get. Cleared by an overwrite; kept by a share. + preview_capture: nullableTextColumn("preview_capture"), // The server-side precompile of `code`: the fingerprint of the compiler // that produced it, the JSX transformed to plain JS, and the Tailwind // CSS for the classes the source uses. All nullable: never compiled, a @@ -503,6 +508,9 @@ export type ArtifactRow = FumaRow; * The columns a list projects — everything except the JSX source, which only a * full read needs. * + * `created_by` and `preview_capture` are selected only so the reader can pick + * which preview the viewer gets; neither reaches the projection. + * * `preview` is here on purpose, and it is the one heavy-ish column that earns * its place: the gallery IS the list, so a preview fetched anywhere else would * be a second round trip per card to render the thing the card is made of. @@ -516,6 +524,8 @@ export const ARTIFACT_SUMMARY_COLUMNS = [ "title", "description", "preview", + "preview_capture", + "created_by", "created_at", "updated_at", ] as const satisfies readonly (keyof ArtifactRow)[]; diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts index b7c17b4feb..c8ff8ccf87 100644 --- a/packages/core/sdk/src/executor.ts +++ b/packages/core/sdk/src/executor.ts @@ -89,6 +89,7 @@ import { export type { OnElicitation, InvokeOptions } from "./elicitation"; import { compiledFromRow, + isArtifactCreator, rowToArtifact, rowToArtifactSummary, type Artifact, @@ -543,11 +544,13 @@ export type Executor = { input: ShareArtifactInput, ) => Effect.Effect; readonly remove: (input: RemoveArtifactInput) => Effect.Effect; - /** Upgrade the stored preview to a snapshot of a settled render. Touches - * only `preview`, and never `updated_at`. */ + /** Store a snapshot of a settled render as the creator's preview. Touches + * only `preview_capture`, and never `updated_at`. `stored` is false when + * the caller is not the artifact's creator: the snapshot holds their data, + * so it is dropped rather than shown to anyone else. */ readonly setPreview: ( input: SetArtifactPreviewInput, - ) => Effect.Effect; + ) => Effect.Effect<{ readonly stored: boolean }, ArtifactNotFoundError | StorageFailure>; }; /** @@ -6752,7 +6755,7 @@ export const createExecutor = rows.map(rowToArtifactSummary))); + .pipe(Effect.map((rows) => rows.map((row) => rowToArtifactSummary(row, subject)))); const artifactsGet = ( id: string, @@ -6763,7 +6766,7 @@ export const createExecutor = ({ @@ -6791,7 +6794,7 @@ export const createExecutor = ownedKeys("org"), @@ -6881,31 +6885,39 @@ export const createExecutor = => + ): Effect.Effect<{ readonly stored: boolean }, ArtifactNotFoundError | StorageFailure> => Effect.gen(function* () { const where = artifactById(input.id); - const existing = yield* core.findFirst("artifact", { where }); + const existing = yield* core.findFirst("artifact", { + where, + select: ARTIFACT_SUMMARY_COLUMNS, + }); if (!existing) { return yield* new ArtifactNotFoundError({ id: ArtifactId.make(input.id), }); } + if (!isArtifactCreator(existing, subject)) return { stored: false }; yield* core.updateMany("artifact", { where, - set: { preview: input.preview }, + set: { preview_capture: input.preview }, }); + return { stored: true }; }); const artifactsRename = ( @@ -6924,7 +6936,7 @@ export const createExecutor =