diff --git a/apps/cloud/drizzle/0023_groovy_titania.sql b/apps/cloud/drizzle/0023_groovy_titania.sql new file mode 100644 index 0000000000..612c79078b --- /dev/null +++ b/apps/cloud/drizzle/0023_groovy_titania.sql @@ -0,0 +1 @@ +ALTER TABLE "artifact" ADD COLUMN "created_by" text; \ No newline at end of file diff --git a/apps/cloud/drizzle/meta/0023_snapshot.json b/apps/cloud/drizzle/meta/0023_snapshot.json new file mode 100644 index 0000000000..9494b57f86 --- /dev/null +++ b/apps/cloud/drizzle/meta/0023_snapshot.json @@ -0,0 +1,1808 @@ +{ + "id": "151e78d4-1dc9-471b-b44b-df11ee206c2d", + "prevId": "18cfbe2f-8cc3-4c9f-8501-680178204093", + "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 + }, + "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 07930eac04..d499dd5953 100644 --- a/apps/cloud/drizzle/meta/_journal.json +++ b/apps/cloud/drizzle/meta/_journal.json @@ -162,6 +162,13 @@ "when": 1792307915126, "tag": "0022_productive_anthem", "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1792505978902, + "tag": "0023_groovy_titania", + "breakpoints": true } ] } diff --git a/apps/cloud/src/db/executor-schema.ts b/apps/cloud/src/db/executor-schema.ts index 2721636357..a3f2a4a997 100644 --- a/apps/cloud/src/db/executor-schema.ts +++ b/apps/cloud/src/db/executor-schema.ts @@ -245,6 +245,7 @@ export const artifact = pgTable( compiled_fingerprint: text("compiled_fingerprint"), compiled_js: text("compiled_js"), compiled_css: text("compiled_css"), + created_by: text("created_by"), created_at: timestamp("created_at").notNull(), updated_at: timestamp("updated_at").notNull(), row_id: varchar("row_id", { length: 255 }) diff --git a/apps/host-cloudflare/src/db/d1.test.ts b/apps/host-cloudflare/src/db/d1.test.ts index b9a95b0b8e..d4eb08191a 100644 --- a/apps/host-cloudflare/src/db/d1.test.ts +++ b/apps/host-cloudflare/src/db/d1.test.ts @@ -191,6 +191,28 @@ describe("createD1ExecutorDb", () => { await sqlite.close(); }); + it("adds the created_by 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 "created_by"`); + 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(["created_by"])); + + 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 47ac60c89f..950a681b12 100644 --- a/packages/core/api/src/artifacts/api.ts +++ b/packages/core/api/src/artifacts/api.ts @@ -154,6 +154,15 @@ export const ArtifactsApi = HttpApiGroup.make("artifacts") error: [InternalError, ArtifactNotFound], }), ) + .add( + // Moves one of the caller's personal artifacts to the org tier. Another + // member's, an unknown id and an already-shared one all read as 404. + HttpApiEndpoint.post("share", "/artifacts/:artifactId/share", { + params: ArtifactParams, + success: ArtifactResponse, + error: [InternalError, ArtifactNotFound], + }), + ) .add( HttpApiEndpoint.delete("remove", "/artifacts/:artifactId", { params: ArtifactParams, diff --git a/packages/core/api/src/artifacts/artifacts-share.test.ts b/packages/core/api/src/artifacts/artifacts-share.test.ts new file mode 100644 index 0000000000..6a83807d07 --- /dev/null +++ b/packages/core/api/src/artifacts/artifacts-share.test.ts @@ -0,0 +1,93 @@ +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"]), + ); + +describe("artifact share over HTTP", () => { + it.effect("moves a personal artifact to the org tier", () => + Effect.gen(function* () { + const config = makeTestConfig(); + const executor = yield* createExecutor(config); + const context = { tenant: "test-tenant", subject: "test-subject" }; + yield* Effect.promise(() => + config.db.create("artifact", { + ...context, + owner: "user", + id: "art_mine", + title: "Mine", + description: null, + code: "const a = 1;", + bindings: null, + preview: null, + compiled_fingerprint: null, + compiled_js: null, + compiled_css: null, + created_by: null, + created_at: new Date(), + updated_at: new Date(), + }), + ); + const web = yield* webHandlerFor(executor); + + const response = yield* Effect.promise(() => + web.handler( + new Request("http://localhost/artifacts/art_mine/share", { method: "POST" }), + handlerContextFor(executor), + ), + ); + + expect(response.status).toBe(200); + const body = (yield* Effect.promise(() => response.json())) as { owner: string; id: string }; + expect(body.owner).toBe("org"); + expect(body.id).toBe("art_mine"); + }), + ); + + it.effect("answers 404 for an unknown id", () => + Effect.gen(function* () { + const executor = yield* createExecutor(makeTestConfig()); + const web = yield* webHandlerFor(executor); + + const response = yield* Effect.promise(() => + web.handler( + new Request("http://localhost/artifacts/art_missing/share", { method: "POST" }), + handlerContextFor(executor), + ), + ); + + expect(response.status).toBe(404); + }), + ); +}); diff --git a/packages/core/api/src/handlers/artifacts.ts b/packages/core/api/src/handlers/artifacts.ts index 0b74cba015..2c59d5ae87 100644 --- a/packages/core/api/src/handlers/artifacts.ts +++ b/packages/core/api/src/handlers/artifacts.ts @@ -90,6 +90,16 @@ export const ArtifactsHandlers = HttpApiBuilder.group(ExecutorApi, "artifacts", }), ), ) + .handle("share", ({ params: path }) => + capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const shared = yield* executor.artifacts.share({ id: path.artifactId }); + yield* notifyArtifactUsage("updated"); + return artifactToResponse(shared); + }), + ), + ) .handle("remove", ({ params: path }) => capture( Effect.gen(function* () { diff --git a/packages/core/sdk/src/artifact.ts b/packages/core/sdk/src/artifact.ts index 56f9c6ed1c..678ed2a2e5 100644 --- a/packages/core/sdk/src/artifact.ts +++ b/packages/core/sdk/src/artifact.ts @@ -218,6 +218,10 @@ export interface RenameArtifactInput { readonly title: string; } +export interface ShareArtifactInput { + readonly id: string; +} + export interface RemoveArtifactInput { readonly id: string; } diff --git a/packages/core/sdk/src/artifacts.test.ts b/packages/core/sdk/src/artifacts.test.ts index e875f8428b..5a8eb1e147 100644 --- a/packages/core/sdk/src/artifacts.test.ts +++ b/packages/core/sdk/src/artifacts.test.ts @@ -273,7 +273,7 @@ describe("executor.artifacts", () => { const saved = yield* alice.artifacts.save({ title: "Revenue", code: CODE }); - expect((yield* bob.artifacts.list()).map((a) => a.title)).toEqual(["Revenue"]); + expect(yield* bob.artifacts.list()).toEqual([]); expect((yield* bob.artifacts.get(saved.id)).code).toBe(CODE); yield* bob.artifacts.rename({ id: saved.id, title: "Revenue by month" }); @@ -301,6 +301,175 @@ describe("executor.artifacts", () => { }).pipe(Effect.scoped), ); + describe("sharing", () => { + const makeMember = ( + config: Parameters[0] & { + readonly testDb: { readonly db: Parameters[0] }; + }, + name: string, + ) => + Effect.gen(function* () { + const member = yield* createExecutor({ + ...config, + subject: Subject.make(name), + db: withQueryContext(config.testDb.db, { tenant: String(config.tenant), subject: name }), + }); + yield* Effect.addFinalizer(() => member.close().pipe(Effect.ignore)); + return member; + }); + + // Insert a row straight through the storage layer, bypassing `save`, so a + // test can pin the tier and creator that `save` would never produce. + const insertRow = ( + config: { readonly testDb: { readonly db: Parameters[0] } }, + context: { readonly tenant: string; readonly subject: string }, + row: { + readonly id: string; + readonly owner: "org" | "user"; + readonly subject: string; + readonly createdBy: string | null; + }, + ) => + Effect.promise(() => + withQueryContext(config.testDb.db, context).create("artifact", { + tenant: context.tenant, + owner: row.owner, + subject: row.subject, + id: row.id, + title: "Legacy", + description: null, + code: CODE, + bindings: null, + preview: null, + compiled_fingerprint: null, + compiled_js: null, + compiled_css: null, + created_by: row.createdBy, + created_at: new Date(), + updated_at: new Date(), + }), + ); + + it.effect("lists only the artifacts the member created, yet others can still open them", () => + 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 }); + + expect((yield* alice.artifacts.list()).map((a) => a.id)).toEqual([saved.id]); + expect(yield* bob.artifacts.list()).toEqual([]); + expect((yield* bob.artifacts.get(saved.id)).code).toBe(CODE); + }).pipe(Effect.scoped), + ); + + it.effect("lists an org artifact with no recorded creator for every member", () => + 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 }, + ); + + expect((yield* alice.artifacts.list()).map((a) => a.id)).toEqual(["art_legacy"]); + expect((yield* bob.artifacts.list()).map((a) => a.id)).toEqual(["art_legacy"]); + }).pipe(Effect.scoped), + ); + + it.effect("an executor with no subject lists every org artifact", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + const orgOnly = yield* createExecutor({ + ...config, + subject: undefined, + db: withQueryContext(config.testDb.db, { + tenant: String(config.tenant), + subject: null, + }), + }); + yield* Effect.addFinalizer(() => orgOnly.close().pipe(Effect.ignore)); + + const saved = yield* alice.artifacts.save({ title: "Revenue", code: CODE }); + + expect((yield* orgOnly.artifacts.list()).map((a) => a.id)).toEqual([saved.id]); + }).pipe(Effect.scoped), + ); + + it.effect("sharing a personal artifact makes it openable by others but not listed", () => + 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 }, + ); + + const before = yield* Effect.result(bob.artifacts.get("art_mine")); + expect( + Result.isFailure(before) && Predicate.isTagged("ArtifactNotFoundError")(before.failure), + ).toBe(true); + expect((yield* alice.artifacts.get("art_mine")).owner).toBe("user"); + + const shared = yield* alice.artifacts.share({ id: "art_mine" }); + + expect(shared.owner).toBe("org"); + expect((yield* bob.artifacts.get("art_mine")).owner).toBe("org"); + expect((yield* bob.artifacts.list()).map((a) => a.id)).toEqual([]); + expect((yield* alice.artifacts.list()).map((a) => a.id)).toEqual(["art_mine"]); + }).pipe(Effect.scoped), + ); + + it.effect("another member cannot share a personal artifact", () => + 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 }, + ); + + const result = yield* Effect.result(bob.artifacts.share({ id: "art_mine" })); + + expect( + Result.isFailure(result) && Predicate.isTagged("ArtifactNotFoundError")(result.failure), + ).toBe(true); + expect((yield* alice.artifacts.get("art_mine")).owner).toBe("user"); + expect((yield* alice.artifacts.list()).map((a) => a.id)).toEqual(["art_mine"]); + }).pipe(Effect.scoped), + ); + + it.effect("sharing an artifact that is already org-tier fails", () => + Effect.gen(function* () { + const executor = yield* makeTestExecutor(); + const saved = yield* executor.artifacts.save({ title: "Revenue", code: CODE }); + + const result = yield* Effect.result(executor.artifacts.share({ id: saved.id })); + + expect( + Result.isFailure(result) && Predicate.isTagged("ArtifactNotFoundError")(result.failure), + ).toBe(true); + }), + ); + + it.effect("overwriting another member's artifact does not change who lists it", () => + 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 }); + + yield* bob.artifacts.save({ id: saved.id, title: "Revenue v2", code: CODE }); + yield* bob.artifacts.rename({ id: saved.id, title: "Revenue v3" }); + + expect((yield* alice.artifacts.list()).map((a) => a.title)).toEqual(["Revenue v3"]); + expect(yield* bob.artifacts.list()).toEqual([]); + }).pipe(Effect.scoped), + ); + }); + describe("previews", () => { const MARKUP = '

Revenue

'; const SETTLED = '

Revenue

12,480 active

'; diff --git a/packages/core/sdk/src/core-schema.ts b/packages/core/sdk/src/core-schema.ts index 0593c4f2f7..dc80fcd3c4 100644 --- a/packages/core/sdk/src/core-schema.ts +++ b/packages/core/sdk/src/core-schema.ts @@ -434,6 +434,11 @@ export const coreTables = defineTables({ compiled_fingerprint: nullableTextColumn("compiled_fingerprint"), compiled_js: nullableTextColumn("compiled_js"), compiled_css: nullableTextColumn("compiled_css"), + // The subject that first saved it. Org rows carry the empty-string + // `subject` sentinel, so this is what lets a gallery list only the + // caller's own artifacts. Null for org rows saved before it existed + // (creator unknown); never rewritten by an overwrite or rename. + created_by: nullableTextColumn("created_by"), created_at: dateColumn("created_at"), updated_at: dateColumn("updated_at"), }, diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts index f544b97988..b7c17b4feb 100644 --- a/packages/core/sdk/src/executor.ts +++ b/packages/core/sdk/src/executor.ts @@ -98,6 +98,7 @@ import { type ArtifactSummary, type RemoveArtifactInput, type RenameArtifactInput, + type ShareArtifactInput, type SaveArtifactInput, type SetArtifactPreviewInput, } from "./artifact"; @@ -531,6 +532,16 @@ export type Executor = { readonly rename: ( input: RenameArtifactInput, ) => Effect.Effect; + /** + * Move one of the caller's personal (`user`-tier) artifacts to the org + * tier, so every member can open it by id. The creator is recorded and the + * artifact stays in the caller's list. Fails with `ArtifactNotFoundError` + * for an id that is not the caller's own personal artifact (unknown, + * another member's, or already shared). + */ + readonly share: ( + 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`. */ @@ -6713,9 +6724,24 @@ export const createExecutor = b("id", "=", id); + // A member's gallery lists their own artifacts: personal rows (the policy + // already narrows those to this subject), org rows they created, and org + // rows with no recorded creator so legacy ones are not lost. Org rows by + // other members stay reachable by id, just not listed. A pure-org executor + // has no subject to filter by. const artifactsList = (): Effect.Effect => core .findMany("artifact", { + ...(subject == null + ? {} + : { + where: (b: AnyCb) => + b.or( + b("owner", "=", "user"), + b("created_by", "=", subject), + b.isNull("created_by"), + ), + }), // Newest first. `id` breaks ties so two artifacts sharing an // `updated_at` millisecond list in a repeatable order rather than // whatever the storage engine happens to return; which of the two @@ -6851,6 +6877,7 @@ export const createExecutor = => + Effect.gen(function* () { + const where = (b: AnyCb) => b.and(b("id", "=", input.id), b("owner", "=", "user")); + const existing = yield* core.findFirst("artifact", { where }); + if (!existing) { + return yield* new ArtifactNotFoundError({ id: ArtifactId.make(input.id) }); + } + const set = { owner: "org", subject: ORG_SUBJECT, created_by: existing.subject }; + yield* core.updateMany("artifact", { where, set }); + return rowToArtifact({ ...existing, ...set }); + }); + // Hard delete: an artifact is not a connection, so there is no disabled or // archived state to fall back to. const artifactsRemove = (input: RemoveArtifactInput): Effect.Effect => @@ -7895,6 +7940,7 @@ export const createExecutor = { + const exit = await doShare({ + params: { artifactId: props.artifactId }, + reactivityKeys: artifactWriteKeys, + }); + trackEvent("artifact_shared", { success: Exit.isSuccess(exit) }); + if (Exit.isFailure(exit)) { + toast.error("Couldn't share the artifact. Try again."); + return; + } + refresh(); + toast.success("Shared. Anyone in your organization can open this link."); + }; + + const handleCopyLink = () => + navigator.clipboard.writeText(globalThis.window.location.href).then( + () => toast.success("Link copied"), + () => toast.error("Couldn't copy the link. Copy it from the address bar."), + ); + const handleRemove = async () => { const exit = await doRemove({ params: { artifactId: props.artifactId }, @@ -145,6 +167,40 @@ export function ArtifactDetailPage(props: { readonly artifactId: ArtifactId }) { } /> + {artifact.value.owner === "user" ? ( + + + + + + + + Share {artifact.value.title} with your organization? + + + Everyone in your organization will be able to open, edit and delete it from + this link. It won't appear in their artifact list. + + + + Cancel + void handleShare()}>Share + + + + ) : ( + + )}