Remove shared endpoints

This commit is contained in:
Michael S. Molina
2026-04-08 17:05:31 -03:00
parent 66e8094823
commit 24f994b5c2
3 changed files with 60 additions and 193 deletions

View File

@@ -30,11 +30,11 @@ Each extension receives its own isolated storage namespace. When Superset loads
## Storage Tiers
| Tier | Storage Type | Context Property | Use Case |
|------|--------------|------------------|----------|
| 1 | Browser storage | `ctx.storage.local`, `ctx.storage.session` | UI state, wizard progress, draft forms |
| 2 | Server-side cache | `ctx.storage.ephemeral` | Job progress, temporary results |
| 3 | Database | `ctx.storage.persistent` | User preferences, extension config (coming soon) |
| Tier | Storage Type | Context Property | Use Case |
| ---- | ----------------- | ------------------------------------------ | ------------------------------------------------ |
| 1 | Browser storage | `ctx.storage.local`, `ctx.storage.session` | UI state, wizard progress, draft forms |
| 2 | Server-side cache | `ctx.storage.ephemeral` | Job progress, temporary results |
| 3 | Database | `ctx.storage.persistent` | User preferences, extension config (coming soon) |
## Tier 1: Local State
@@ -128,7 +128,11 @@ const ctx = getContext();
await ctx.storage.ephemeral.set('job_progress', { pct: 42, status: 'running' });
// Store with custom TTL (5 minutes)
await ctx.storage.ephemeral.set('quick_cache', { results: [1, 2, 3] }, { ttl: 300 });
await ctx.storage.ephemeral.set(
'quick_cache',
{ results: [1, 2, 3] },
{ ttl: 300 },
);
// Retrieve
const progress = await ctx.storage.ephemeral.get('job_progress');
@@ -194,25 +198,17 @@ result = ctx.storage.ephemeral.shared.get('shared_result')
Coming soon.
## Choosing the Right Tier
| Need | Recommended Tier |
|------|------------------|
| UI state (sidebar collapsed, panel sizes) | `ctx.storage.local` |
| Wizard/form progress within a session | `ctx.storage.session` |
| Background job progress | `ctx.storage.ephemeral` |
| Temporary computation cache | `ctx.storage.ephemeral` |
## Key Patterns
All storage keys are automatically namespaced:
| Scope | Key Pattern |
|-------|-------------|
| Scope | Key Pattern |
| ----------- | -------------------------------------------------- |
| User-scoped | `superset-ext:{extension_id}:user:{user_id}:{key}` |
| Shared | `superset-ext:{extension_id}:shared:{key}` |
| Shared | `superset-ext:{extension_id}:shared:{key}` |
This ensures:
- Extensions cannot accidentally access each other's data
- Users cannot see other users' data (by default)
- Clean prefix-based deletion on uninstall

View File

@@ -28,13 +28,12 @@ const DEFAULT_TTL = 3600;
export function createEphemeralState(
extensionId: string,
): typeof StorageTypes.ephemeralState {
const buildUrl = (key: string, isShared: boolean): string => {
const buildUrl = (key: string, shared?: boolean): string => {
const basePath = '/api/v1/extensions/storage/ephemeral';
const encodedId = encodeURIComponent(extensionId);
const encodedKey = encodeURIComponent(key);
return isShared
? `${basePath}/shared/${encodedId}/${encodedKey}`
: `${basePath}/${encodedId}/${encodedKey}`;
const url = `${basePath}/${encodedId}/${encodedKey}`;
return shared ? `${url}?shared=true` : url;
};
const shared: StorageTypes.ephemeralState.EphemeralStateAccessor = {
@@ -63,9 +62,7 @@ export function createEphemeralState(
return {
DEFAULT_TTL,
get: async (key: string) => {
const response = await SupersetClient.get({
endpoint: buildUrl(key, false),
});
const response = await SupersetClient.get({ endpoint: buildUrl(key) });
return response.json?.result ?? null;
},
set: async (
@@ -74,13 +71,13 @@ export function createEphemeralState(
options?: StorageTypes.ephemeralState.SetOptions,
) => {
await SupersetClient.put({
endpoint: buildUrl(key, false),
endpoint: buildUrl(key),
body: JSON.stringify({ value, ttl: options?.ttl ?? DEFAULT_TTL }),
headers: { 'Content-Type': 'application/json' },
});
},
remove: async (key: string) => {
await SupersetClient.delete({ endpoint: buildUrl(key, false) });
await SupersetClient.delete({ endpoint: buildUrl(key) });
},
shared,
};

View File

@@ -21,8 +21,8 @@ REST API for extension storage.
Provides HTTP endpoints for frontend extensions to access server-side
ephemeral storage without direct backend code.
By default, all operations are user-scoped (private to the current user).
Use the /shared/ endpoints to access state visible to all users.
All operations are user-scoped by default. Use `?shared=true` query param
to access shared state visible to all users.
"""
from __future__ import annotations
@@ -73,6 +73,14 @@ def _parse_ttl(body: dict[str, Any]) -> tuple[int | None, str | None]:
return ttl, None
def _build_storage_key(extension_id: str, key: str, shared: bool) -> str:
"""Build the cache key based on scope (user or shared)."""
if shared:
return _build_cache_key(KEY_PREFIX, extension_id, "shared", key)
user_id = g.user.id
return _build_cache_key(KEY_PREFIX, extension_id, "user", user_id, key)
class ExtensionStorageRestApi(BaseApi):
"""REST API for extension ephemeral state storage."""
@@ -97,18 +105,14 @@ class ExtensionStorageRestApi(BaseApi):
return jsonify({"message": message}), 400
# =========================================================================
# User-Scoped Ephemeral State Endpoints (Default)
# =========================================================================
@protect()
@safe
@expose("/ephemeral/<extension_id>/<key>", methods=("GET",))
def get_ephemeral(self, extension_id: str, key: str, **kwargs: Any) -> Response:
"""Get a value from user-scoped ephemeral state.
"""Get a value from ephemeral state.
---
get:
summary: Get a value from user-scoped ephemeral state (default)
summary: Get a value from ephemeral state
parameters:
- in: path
name: extension_id
@@ -122,6 +126,12 @@ class ExtensionStorageRestApi(BaseApi):
type: string
required: true
description: Storage key
- in: query
name: shared
schema:
type: boolean
required: false
description: If true, read from shared state visible to all users
responses:
200:
description: Value retrieved successfully
@@ -139,8 +149,8 @@ class ExtensionStorageRestApi(BaseApi):
if not extension:
return self.response_404("Extension not found")
user_id = g.user.id
cache_key = _build_cache_key(KEY_PREFIX, extension_id, "user", user_id, key)
shared = request.args.get("shared", "false").lower() == "true"
cache_key = _build_storage_key(extension_id, key, shared)
value = cache_manager.extension_ephemeral_state_cache.get(cache_key)
return self.response(200, result=value)
@@ -149,10 +159,10 @@ class ExtensionStorageRestApi(BaseApi):
@safe
@expose("/ephemeral/<extension_id>/<key>", methods=("PUT",))
def set_ephemeral(self, extension_id: str, key: str, **kwargs: Any) -> Response:
"""Set a value in user-scoped ephemeral state.
"""Set a value in ephemeral state.
---
put:
summary: Set a value in user-scoped ephemeral state (default)
summary: Set a value in ephemeral state
parameters:
- in: path
name: extension_id
@@ -166,6 +176,12 @@ class ExtensionStorageRestApi(BaseApi):
type: string
required: true
description: Storage key
- in: query
name: shared
schema:
type: boolean
required: false
description: If true, store as shared state visible to all users
requestBody:
required: true
content:
@@ -199,8 +215,8 @@ class ExtensionStorageRestApi(BaseApi):
if error:
return self.response_400(error)
user_id = g.user.id
cache_key = _build_cache_key(KEY_PREFIX, extension_id, "user", user_id, key)
shared = request.args.get("shared", "false").lower() == "true"
cache_key = _build_storage_key(extension_id, key, shared)
cache_manager.extension_ephemeral_state_cache.set(cache_key, value, timeout=ttl)
return self.response(200, message="Value stored successfully")
@@ -209,10 +225,10 @@ class ExtensionStorageRestApi(BaseApi):
@safe
@expose("/ephemeral/<extension_id>/<key>", methods=("DELETE",))
def delete_ephemeral(self, extension_id: str, key: str, **kwargs: Any) -> Response:
"""Delete a value from user-scoped ephemeral state.
"""Delete a value from ephemeral state.
---
delete:
summary: Delete a value from user-scoped ephemeral state (default)
summary: Delete a value from ephemeral state
parameters:
- in: path
name: extension_id
@@ -226,6 +242,12 @@ class ExtensionStorageRestApi(BaseApi):
type: string
required: true
description: Storage key
- in: query
name: shared
schema:
type: boolean
required: false
description: If true, delete from shared state
responses:
200:
description: Value deleted successfully
@@ -236,156 +258,8 @@ class ExtensionStorageRestApi(BaseApi):
if not extension:
return self.response_404("Extension not found")
user_id = g.user.id
cache_key = _build_cache_key(KEY_PREFIX, extension_id, "user", user_id, key)
cache_manager.extension_ephemeral_state_cache.delete(cache_key)
return self.response(200, message="Value deleted successfully")
# =========================================================================
# Shared (Global) Ephemeral State Endpoints
# =========================================================================
@protect()
@safe
@expose("/ephemeral/shared/<extension_id>/<key>", methods=("GET",))
def get_ephemeral_shared(
self, extension_id: str, key: str, **kwargs: Any
) -> Response:
"""Get a value from shared ephemeral state.
---
get:
summary: Get a value from shared (global) ephemeral state
parameters:
- in: path
name: extension_id
schema:
type: string
required: true
description: Extension ID (publisher.name)
- in: path
name: key
schema:
type: string
required: true
description: Storage key
responses:
200:
description: Value retrieved successfully
content:
application/json:
schema:
type: object
properties:
result:
description: The stored value
404:
description: Extension not found
"""
extension = _get_extension_or_404(extension_id)
if not extension:
return self.response_404("Extension not found")
cache_key = _build_cache_key(KEY_PREFIX, extension_id, "shared", key)
value = cache_manager.extension_ephemeral_state_cache.get(cache_key)
return self.response(200, result=value)
@protect()
@safe
@expose("/ephemeral/shared/<extension_id>/<key>", methods=("PUT",))
def set_ephemeral_shared(
self, extension_id: str, key: str, **kwargs: Any
) -> Response:
"""Set a value in shared ephemeral state.
---
put:
summary: Set a value in shared (global) ephemeral state
parameters:
- in: path
name: extension_id
schema:
type: string
required: true
description: Extension ID (publisher.name)
- in: path
name: key
schema:
type: string
required: true
description: Storage key
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
value:
description: The value to store
ttl:
type: integer
description: Time-to-live in seconds (default 3600)
responses:
200:
description: Value stored successfully
400:
description: Invalid request body
404:
description: Extension not found
"""
extension = _get_extension_or_404(extension_id)
if not extension:
return self.response_404("Extension not found")
body = request.get_json(silent=True) or {}
if "value" not in body:
return self.response_400("Request body must contain 'value' field")
value = body["value"]
ttl, error = _parse_ttl(body)
if error:
return self.response_400(error)
cache_key = _build_cache_key(KEY_PREFIX, extension_id, "shared", key)
cache_manager.extension_ephemeral_state_cache.set(cache_key, value, timeout=ttl)
return self.response(200, message="Value stored successfully")
@protect()
@safe
@expose("/ephemeral/shared/<extension_id>/<key>", methods=("DELETE",))
def delete_ephemeral_shared(
self, extension_id: str, key: str, **kwargs: Any
) -> Response:
"""Delete a value from shared ephemeral state.
---
delete:
summary: Delete a value from shared (global) ephemeral state
parameters:
- in: path
name: extension_id
schema:
type: string
required: true
description: Extension ID (publisher.name)
- in: path
name: key
schema:
type: string
required: true
description: Storage key
responses:
200:
description: Value deleted successfully
404:
description: Extension not found
"""
extension = _get_extension_or_404(extension_id)
if not extension:
return self.response_404("Extension not found")
cache_key = _build_cache_key(KEY_PREFIX, extension_id, "shared", key)
shared = request.args.get("shared", "false").lower() == "true"
cache_key = _build_storage_key(extension_id, key, shared)
cache_manager.extension_ephemeral_state_cache.delete(cache_key)
return self.response(200, message="Value deleted successfully")