dashtro 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cms_backend/__init__.py +0 -0
- cms_backend/api/__init__.py +0 -0
- cms_backend/api/utils/__init__.py +49 -0
- cms_backend/api/utils/actor.py +11 -0
- cms_backend/api/utils/api_key_auth.py +31 -0
- cms_backend/api/utils/audit_client.py +237 -0
- cms_backend/api/utils/mongodb_client.py +183 -0
- cms_backend/api/utils/postgres_audit_client.py +207 -0
- cms_backend/api/utils/postgres_client.py +1258 -0
- cms_backend/api/utils/schema.py +69 -0
- cms_backend/api/utils/sqlite_client.py +1236 -0
- cms_backend/api/utils/workspace_diff.py +44 -0
- cms_backend/config.py +13 -0
- cms_backend/main.py +78 -0
- cms_backend/models/__init__.py +0 -0
- cms_backend/models/collection.py +48 -0
- cms_backend/models/field_types.py +75 -0
- cms_backend/models/project.py +23 -0
- cms_backend/models/rich_text_component.py +22 -0
- cms_backend/models/schema.py +213 -0
- cms_backend/scripts/__init__.py +0 -0
- cms_backend/scripts/cms_schema.py +1036 -0
- cms_backend/scripts/migrate_image_keys.py +54 -0
- cms_mcp/__init__.py +0 -0
- cms_mcp/server.py +316 -0
- dashtro-0.1.0.dist-info/METADATA +247 -0
- dashtro-0.1.0.dist-info/RECORD +31 -0
- dashtro-0.1.0.dist-info/WHEEL +5 -0
- dashtro-0.1.0.dist-info/entry_points.txt +3 -0
- dashtro-0.1.0.dist-info/licenses/LICENSE +21 -0
- dashtro-0.1.0.dist-info/top_level.txt +2 -0
cms_backend/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from decouple import config
|
|
2
|
+
|
|
3
|
+
from .audit_client import SqliteAuditClient
|
|
4
|
+
from .postgres_audit_client import PostgresAuditClient
|
|
5
|
+
from .postgres_client import PostgresAuth, PostgresData
|
|
6
|
+
from .sqlite_client import SqliteAuth, SqliteData
|
|
7
|
+
|
|
8
|
+
# mongodb is not yet migrated to the FastAPI data API, so it stays unsupported.
|
|
9
|
+
_DB_TYPE = config("DB_TYPE", default="sqlite")
|
|
10
|
+
_SUPPORTED_DB_TYPES = ("sqlite", "postgres")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_data_client():
|
|
14
|
+
if _DB_TYPE == "postgres":
|
|
15
|
+
return PostgresData()
|
|
16
|
+
if _DB_TYPE == "sqlite":
|
|
17
|
+
return SqliteData()
|
|
18
|
+
raise NotImplementedError(
|
|
19
|
+
f"DB_TYPE={_DB_TYPE!r} is not supported yet; use one of {_SUPPORTED_DB_TYPES}"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_auth_client():
|
|
24
|
+
if _DB_TYPE == "postgres":
|
|
25
|
+
return PostgresAuth()
|
|
26
|
+
if _DB_TYPE == "sqlite":
|
|
27
|
+
return SqliteAuth()
|
|
28
|
+
raise NotImplementedError(
|
|
29
|
+
f"DB_TYPE={_DB_TYPE!r} is not supported yet; use one of {_SUPPORTED_DB_TYPES}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_audit_client() -> SqliteAuditClient | PostgresAuditClient:
|
|
34
|
+
if _DB_TYPE == "postgres":
|
|
35
|
+
return PostgresAuditClient()
|
|
36
|
+
return SqliteAuditClient()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"SqliteData",
|
|
41
|
+
"SqliteAuth",
|
|
42
|
+
"SqliteAuditClient",
|
|
43
|
+
"PostgresData",
|
|
44
|
+
"PostgresAuth",
|
|
45
|
+
"PostgresAuditClient",
|
|
46
|
+
"get_data_client",
|
|
47
|
+
"get_auth_client",
|
|
48
|
+
"get_audit_client",
|
|
49
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
def get_actor(request) -> dict:
|
|
2
|
+
"""Return the actor verified by CMSAuthMiddleware for this request."""
|
|
3
|
+
return request.state.actor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_client_ip(request) -> str:
|
|
7
|
+
"""Extract the real client IP from a FastAPI Request."""
|
|
8
|
+
forwarded_for = request.headers.get("x-forwarded-for")
|
|
9
|
+
if forwarded_for:
|
|
10
|
+
return forwarded_for.split(",")[0].strip()
|
|
11
|
+
return request.client.host if request.client else ""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from fastapi import Header, HTTPException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def require_api_key(operation: str):
|
|
5
|
+
"""Dependency factory: verifies X-API-Key and that it carries `operation` ('read'/'write')."""
|
|
6
|
+
|
|
7
|
+
def _dependency(x_api_key: str = Header(default=None)) -> dict:
|
|
8
|
+
if not x_api_key:
|
|
9
|
+
raise HTTPException(status_code=401, detail="X-API-Key header missing")
|
|
10
|
+
from api.utils import get_auth_client
|
|
11
|
+
|
|
12
|
+
db_auth = get_auth_client()
|
|
13
|
+
key_info = db_auth.verify_api_key(x_api_key)
|
|
14
|
+
if not key_info:
|
|
15
|
+
raise HTTPException(status_code=401, detail="Invalid or revoked API key")
|
|
16
|
+
if operation not in (key_info.get("scopes") or []):
|
|
17
|
+
raise HTTPException(
|
|
18
|
+
status_code=403, detail=f"API key does not have '{operation}' access"
|
|
19
|
+
)
|
|
20
|
+
return key_info
|
|
21
|
+
|
|
22
|
+
return _dependency
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_key_scope(key_info: dict, project_id: str, collection_name: str):
|
|
26
|
+
"""Raises 403 if the key is scoped to a different project or collection set."""
|
|
27
|
+
if key_info.get("project_id") and key_info["project_id"] != project_id:
|
|
28
|
+
raise HTTPException(status_code=403, detail="API key is not scoped to this project")
|
|
29
|
+
collections = key_info.get("collections") or []
|
|
30
|
+
if collections and collection_name not in collections:
|
|
31
|
+
raise HTTPException(status_code=403, detail="API key is not scoped to this collection")
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sqlite3
|
|
3
|
+
import threading
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
from decouple import config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SqliteAuditClient:
|
|
11
|
+
_instance = None
|
|
12
|
+
|
|
13
|
+
def __new__(cls, *args, **kwargs):
|
|
14
|
+
if not cls._instance:
|
|
15
|
+
cls._instance = super().__new__(cls)
|
|
16
|
+
cls._instance._local = threading.local()
|
|
17
|
+
cls._instance._ensure_table()
|
|
18
|
+
return cls._instance
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _create_connection():
|
|
22
|
+
db_path = config("SQLITE_DB_PATH", default="db.sqlite3")
|
|
23
|
+
conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
24
|
+
conn.row_factory = sqlite3.Row
|
|
25
|
+
return conn
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def connection(self):
|
|
29
|
+
local = self._local
|
|
30
|
+
if not hasattr(local, "conn") or local.conn is None:
|
|
31
|
+
local.conn = self._create_connection()
|
|
32
|
+
return local.conn
|
|
33
|
+
|
|
34
|
+
@connection.setter
|
|
35
|
+
def connection(self, value):
|
|
36
|
+
self._local.conn = value
|
|
37
|
+
|
|
38
|
+
def get_cursor(self):
|
|
39
|
+
try:
|
|
40
|
+
self.connection.execute("SELECT 1")
|
|
41
|
+
except (sqlite3.ProgrammingError, sqlite3.InterfaceError):
|
|
42
|
+
self.connection = self._create_connection()
|
|
43
|
+
return self.connection.cursor()
|
|
44
|
+
|
|
45
|
+
def _ensure_table(self):
|
|
46
|
+
cursor = self.get_cursor()
|
|
47
|
+
cursor.execute("""
|
|
48
|
+
CREATE TABLE IF NOT EXISTS cms_audit_logs (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
user_id TEXT NOT NULL DEFAULT 'anonymous',
|
|
51
|
+
user_email TEXT NOT NULL DEFAULT 'anonymous',
|
|
52
|
+
action TEXT NOT NULL,
|
|
53
|
+
resource_type TEXT NOT NULL,
|
|
54
|
+
resource_id TEXT DEFAULT '',
|
|
55
|
+
resource_name TEXT DEFAULT '',
|
|
56
|
+
project_id TEXT DEFAULT '',
|
|
57
|
+
workspace_name TEXT DEFAULT '',
|
|
58
|
+
details TEXT DEFAULT '{}',
|
|
59
|
+
ip_address TEXT DEFAULT '',
|
|
60
|
+
created_at TEXT NOT NULL
|
|
61
|
+
);
|
|
62
|
+
""")
|
|
63
|
+
cursor.execute(
|
|
64
|
+
"CREATE INDEX IF NOT EXISTS idx_audit_created_at ON cms_audit_logs(created_at DESC);"
|
|
65
|
+
)
|
|
66
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_audit_action ON cms_audit_logs(action);")
|
|
67
|
+
cursor.execute(
|
|
68
|
+
"CREATE INDEX IF NOT EXISTS idx_audit_resource_type ON cms_audit_logs(resource_type);"
|
|
69
|
+
)
|
|
70
|
+
self.connection.commit()
|
|
71
|
+
cursor.close()
|
|
72
|
+
|
|
73
|
+
def log(
|
|
74
|
+
self,
|
|
75
|
+
action: str,
|
|
76
|
+
resource_type: str,
|
|
77
|
+
user_id: str = "anonymous",
|
|
78
|
+
user_email: str = "anonymous",
|
|
79
|
+
resource_id: str = "",
|
|
80
|
+
resource_name: str = "",
|
|
81
|
+
project_id: str = "",
|
|
82
|
+
workspace_name: str = "",
|
|
83
|
+
details: dict | None = None,
|
|
84
|
+
ip_address: str = "",
|
|
85
|
+
):
|
|
86
|
+
log_id = str(uuid.uuid4().hex)
|
|
87
|
+
created_at = datetime.now(tz=UTC).isoformat()
|
|
88
|
+
details_json = json.dumps(details or {})
|
|
89
|
+
cursor = self.get_cursor()
|
|
90
|
+
cursor.execute(
|
|
91
|
+
"""INSERT INTO cms_audit_logs
|
|
92
|
+
(id, user_id, user_email, action, resource_type, resource_id, resource_name,
|
|
93
|
+
project_id, workspace_name, details, ip_address, created_at)
|
|
94
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
95
|
+
(
|
|
96
|
+
log_id,
|
|
97
|
+
user_id,
|
|
98
|
+
user_email,
|
|
99
|
+
action,
|
|
100
|
+
resource_type,
|
|
101
|
+
resource_id,
|
|
102
|
+
resource_name,
|
|
103
|
+
project_id,
|
|
104
|
+
workspace_name,
|
|
105
|
+
details_json,
|
|
106
|
+
ip_address,
|
|
107
|
+
created_at,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
self.connection.commit()
|
|
111
|
+
cursor.close()
|
|
112
|
+
|
|
113
|
+
def get_logs(
|
|
114
|
+
self,
|
|
115
|
+
action: str | None = None,
|
|
116
|
+
resource_type: str | None = None,
|
|
117
|
+
project_id: str | None = None,
|
|
118
|
+
user_id: str | None = None,
|
|
119
|
+
from_date: str | None = None,
|
|
120
|
+
to_date: str | None = None,
|
|
121
|
+
limit: int = 100,
|
|
122
|
+
offset: int = 0,
|
|
123
|
+
) -> dict:
|
|
124
|
+
where_clauses = []
|
|
125
|
+
params = []
|
|
126
|
+
|
|
127
|
+
if action:
|
|
128
|
+
where_clauses.append("action = ?")
|
|
129
|
+
params.append(action)
|
|
130
|
+
if resource_type:
|
|
131
|
+
where_clauses.append("resource_type = ?")
|
|
132
|
+
params.append(resource_type)
|
|
133
|
+
if project_id:
|
|
134
|
+
where_clauses.append("project_id = ?")
|
|
135
|
+
params.append(project_id)
|
|
136
|
+
if user_id:
|
|
137
|
+
where_clauses.append("user_id = ?")
|
|
138
|
+
params.append(user_id)
|
|
139
|
+
if from_date:
|
|
140
|
+
where_clauses.append("created_at >= ?")
|
|
141
|
+
params.append(from_date)
|
|
142
|
+
if to_date:
|
|
143
|
+
where_clauses.append("created_at <= ?")
|
|
144
|
+
params.append(to_date)
|
|
145
|
+
|
|
146
|
+
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
|
147
|
+
|
|
148
|
+
cursor = self.get_cursor()
|
|
149
|
+
cursor.execute(f"SELECT COUNT(*) FROM cms_audit_logs {where_sql}", params)
|
|
150
|
+
total = cursor.fetchone()[0]
|
|
151
|
+
|
|
152
|
+
cursor.execute(
|
|
153
|
+
f"SELECT * FROM cms_audit_logs {where_sql} ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
|
154
|
+
[*params, limit, offset],
|
|
155
|
+
)
|
|
156
|
+
rows = cursor.fetchall()
|
|
157
|
+
cursor.close()
|
|
158
|
+
|
|
159
|
+
logs = []
|
|
160
|
+
for r in rows:
|
|
161
|
+
logs.append(
|
|
162
|
+
{
|
|
163
|
+
"id": r["id"],
|
|
164
|
+
"user_id": r["user_id"],
|
|
165
|
+
"user_email": r["user_email"],
|
|
166
|
+
"action": r["action"],
|
|
167
|
+
"resource_type": r["resource_type"],
|
|
168
|
+
"resource_id": r["resource_id"],
|
|
169
|
+
"resource_name": r["resource_name"],
|
|
170
|
+
"project_id": r["project_id"],
|
|
171
|
+
"workspace_name": r["workspace_name"],
|
|
172
|
+
"details": json.loads(r["details"] or "{}"),
|
|
173
|
+
"ip_address": r["ip_address"],
|
|
174
|
+
"created_at": r["created_at"],
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return {"total": total, "logs": logs}
|
|
179
|
+
|
|
180
|
+
def get_heatmap_data(self, year: int, month: int | None = None) -> list[dict]:
|
|
181
|
+
"""Return per-day operation counts and top action for a year or a single month."""
|
|
182
|
+
import calendar as cal
|
|
183
|
+
|
|
184
|
+
if month:
|
|
185
|
+
days_in_month = cal.monthrange(year, month)[1]
|
|
186
|
+
start = f"{year:04d}-{month:02d}-01"
|
|
187
|
+
end = f"{year:04d}-{month:02d}-{days_in_month:02d}"
|
|
188
|
+
else:
|
|
189
|
+
start = f"{year:04d}-01-01"
|
|
190
|
+
end = f"{year:04d}-12-31"
|
|
191
|
+
|
|
192
|
+
cursor = self.get_cursor()
|
|
193
|
+
cursor.execute(
|
|
194
|
+
"""
|
|
195
|
+
SELECT date(created_at) AS day, action, COUNT(*) AS cnt
|
|
196
|
+
FROM cms_audit_logs
|
|
197
|
+
WHERE date(created_at) BETWEEN ? AND ?
|
|
198
|
+
GROUP BY day, action
|
|
199
|
+
ORDER BY day, cnt DESC
|
|
200
|
+
""",
|
|
201
|
+
(start, end),
|
|
202
|
+
)
|
|
203
|
+
rows = cursor.fetchall()
|
|
204
|
+
cursor.close()
|
|
205
|
+
|
|
206
|
+
# Aggregate: total count per day + top action
|
|
207
|
+
day_map: dict[str, dict] = {}
|
|
208
|
+
for r in rows:
|
|
209
|
+
day = r["day"]
|
|
210
|
+
if day not in day_map:
|
|
211
|
+
day_map[day] = {"count": 0, "top_action": r["action"]}
|
|
212
|
+
day_map[day]["count"] += r["cnt"]
|
|
213
|
+
|
|
214
|
+
# Fill in every calendar day (zero counts included)
|
|
215
|
+
result = []
|
|
216
|
+
if month:
|
|
217
|
+
days_in_month = cal.monthrange(year, month)[1]
|
|
218
|
+
for d in range(1, days_in_month + 1):
|
|
219
|
+
day_str = f"{year:04d}-{month:02d}-{d:02d}"
|
|
220
|
+
entry = day_map.get(day_str, {"count": 0, "top_action": None})
|
|
221
|
+
result.append(
|
|
222
|
+
{"date": day_str, "count": entry["count"], "top_action": entry["top_action"]}
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
import datetime
|
|
226
|
+
|
|
227
|
+
current = datetime.date(year, 1, 1)
|
|
228
|
+
end_date = datetime.date(year, 12, 31)
|
|
229
|
+
while current <= end_date:
|
|
230
|
+
day_str = current.isoformat()
|
|
231
|
+
entry = day_map.get(day_str, {"count": 0, "top_action": None})
|
|
232
|
+
result.append(
|
|
233
|
+
{"date": day_str, "count": entry["count"], "top_action": entry["top_action"]}
|
|
234
|
+
)
|
|
235
|
+
current += datetime.timedelta(days=1)
|
|
236
|
+
|
|
237
|
+
return result
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import UTC, datetime, timedelta
|
|
5
|
+
|
|
6
|
+
import certifi
|
|
7
|
+
import jwt
|
|
8
|
+
from api.utils.schema import Schema
|
|
9
|
+
from decouple import config
|
|
10
|
+
from pymongo import MongoClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MongoDBClient:
|
|
14
|
+
_instance = None
|
|
15
|
+
|
|
16
|
+
def __new__(cls, *args, **kwargs):
|
|
17
|
+
if not cls._instance:
|
|
18
|
+
cls._instance = super().__new__(cls, *args, **kwargs)
|
|
19
|
+
mongo_uri = config("MONGO_URI")
|
|
20
|
+
db_name = config("MONGO_DB_NAME")
|
|
21
|
+
client = MongoClient(mongo_uri, tlsCAFile=certifi.where())
|
|
22
|
+
cls._instance.db = client[db_name]
|
|
23
|
+
return cls._instance
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MongoDBAuth(MongoDBClient):
|
|
27
|
+
def __init__(self):
|
|
28
|
+
MongoDBClient.__init__(self)
|
|
29
|
+
|
|
30
|
+
def create_user(self, email: str, password: str):
|
|
31
|
+
user_id = str(uuid.uuid4().hex)
|
|
32
|
+
salt = os.urandom(16).hex()
|
|
33
|
+
hashed = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 260000).hex()
|
|
34
|
+
self.db["cms_users"].insert_one(
|
|
35
|
+
{
|
|
36
|
+
"_id": user_id,
|
|
37
|
+
"email": email,
|
|
38
|
+
"password": f"{salt}${hashed}",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
return user_id
|
|
42
|
+
|
|
43
|
+
def verify_id_token(self, id_token):
|
|
44
|
+
secret = config("JWT_SECRET_KEY")
|
|
45
|
+
try:
|
|
46
|
+
payload = jwt.decode(id_token, secret, algorithms=["HS256"])
|
|
47
|
+
return {
|
|
48
|
+
"uid": payload["uid"],
|
|
49
|
+
"email": payload["email"],
|
|
50
|
+
"email_verified": payload.get("email_verified", True),
|
|
51
|
+
}
|
|
52
|
+
except jwt.ExpiredSignatureError:
|
|
53
|
+
raise Exception("Token verification failed: Token has expired") from None
|
|
54
|
+
except jwt.InvalidTokenError as e:
|
|
55
|
+
raise Exception(f"Token verification failed: {str(e)}") from e
|
|
56
|
+
|
|
57
|
+
def login_user(self, email: str, password: str):
|
|
58
|
+
user = self.db["cms_users"].find_one({"email": email})
|
|
59
|
+
stored = user.get("password", "")
|
|
60
|
+
salt, _, hashed = stored.partition("$")
|
|
61
|
+
check = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 260000).hex()
|
|
62
|
+
if not user or check != hashed:
|
|
63
|
+
raise Exception("Login failed: INVALID_CREDENTIALS")
|
|
64
|
+
|
|
65
|
+
secret = config("JWT_SECRET_KEY")
|
|
66
|
+
now = datetime.now(tz=UTC)
|
|
67
|
+
payload = {
|
|
68
|
+
"uid": str(user["_id"]),
|
|
69
|
+
"email": user["email"],
|
|
70
|
+
"email_verified": True,
|
|
71
|
+
"exp": now + timedelta(hours=1),
|
|
72
|
+
"iat": now,
|
|
73
|
+
}
|
|
74
|
+
id_token = jwt.encode(payload, secret, algorithm="HS256")
|
|
75
|
+
refresh_payload = {**payload, "exp": now + timedelta(days=30)}
|
|
76
|
+
refresh_token = jwt.encode(refresh_payload, secret, algorithm="HS256")
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"idToken": id_token,
|
|
80
|
+
"refreshToken": refresh_token,
|
|
81
|
+
"localId": str(user["_id"]),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def get_admin_token_id(self):
|
|
85
|
+
admin_email = config("ADMIN_EMAIL")
|
|
86
|
+
admin_password = config("ADMIN_PASSWORD")
|
|
87
|
+
return self.login_user(admin_email, admin_password).get("idToken")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MongoDBData(MongoDBClient, Schema):
|
|
91
|
+
def __init__(self):
|
|
92
|
+
MongoDBClient.__init__(self)
|
|
93
|
+
schema_data = list(self.db["cms_schema"].find())
|
|
94
|
+
schema_collections_data = list(self.db["cms_schema_collections"].find())
|
|
95
|
+
Schema.__init__(self, schema_data, schema_collections_data)
|
|
96
|
+
|
|
97
|
+
def get_schema(self, raw_schema_data: list):
|
|
98
|
+
return {str(doc["_id"]): doc.get("data", {}) for doc in raw_schema_data}
|
|
99
|
+
|
|
100
|
+
async def fetch_document_data(self, workspace_name, collection_id, document_id):
|
|
101
|
+
doc = self.db["cms_workspace_data"].find_one(
|
|
102
|
+
{
|
|
103
|
+
"workspace_name": workspace_name,
|
|
104
|
+
"collection_id": collection_id,
|
|
105
|
+
"document_id": document_id,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
return doc["data"] if doc else None
|
|
109
|
+
|
|
110
|
+
def set_document_data(self, path, data):
|
|
111
|
+
parts = path.strip("/").split("/")
|
|
112
|
+
|
|
113
|
+
if parts[0] == "_schema" and len(parts) == 2:
|
|
114
|
+
self.db["cms_schema"].replace_one(
|
|
115
|
+
{"_id": parts[1]}, {"_id": parts[1], "data": data}, upsert=True
|
|
116
|
+
)
|
|
117
|
+
elif parts[0] == "_schema_collections" and len(parts) == 2:
|
|
118
|
+
self.db["cms_schema_collections"].replace_one(
|
|
119
|
+
{"_id": parts[1]}, {"_id": parts[1], "data": data}, upsert=True
|
|
120
|
+
)
|
|
121
|
+
elif parts[0] == "_workspace" and len(parts) == 4:
|
|
122
|
+
workspace_name, collection_id, document_id = parts[1], parts[2], parts[3]
|
|
123
|
+
self.db["cms_workspace_data"].replace_one(
|
|
124
|
+
{
|
|
125
|
+
"workspace_name": workspace_name,
|
|
126
|
+
"collection_id": collection_id,
|
|
127
|
+
"document_id": document_id,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"workspace_name": workspace_name,
|
|
131
|
+
"collection_id": collection_id,
|
|
132
|
+
"document_id": document_id,
|
|
133
|
+
"data": data,
|
|
134
|
+
},
|
|
135
|
+
upsert=True,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def delete_document_data(self, path):
|
|
139
|
+
parts = path.strip("/").split("/")
|
|
140
|
+
|
|
141
|
+
if parts[0] == "_schema" and len(parts) == 2:
|
|
142
|
+
self.db["cms_schema"].delete_one({"_id": parts[1]})
|
|
143
|
+
elif parts[0] == "_schema_collections" and len(parts) == 2:
|
|
144
|
+
self.db["cms_schema_collections"].delete_one({"_id": parts[1]})
|
|
145
|
+
elif parts[0] == "_workspace" and len(parts) == 4:
|
|
146
|
+
workspace_name, collection_id, document_id = parts[1], parts[2], parts[3]
|
|
147
|
+
self.db["cms_workspace_data"].delete_one(
|
|
148
|
+
{
|
|
149
|
+
"workspace_name": workspace_name,
|
|
150
|
+
"collection_id": collection_id,
|
|
151
|
+
"document_id": document_id,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def delete_collection_data(self, path):
|
|
156
|
+
parts = path.strip("/").split("/")
|
|
157
|
+
if parts[0] != "_workspace" or len(parts) != 3:
|
|
158
|
+
return
|
|
159
|
+
workspace_name, collection_id = parts[1], parts[2]
|
|
160
|
+
self.db["cms_workspace_data"].delete_many(
|
|
161
|
+
{
|
|
162
|
+
"workspace_name": workspace_name,
|
|
163
|
+
"collection_id": collection_id,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def get_realtime_content(path):
|
|
169
|
+
client = MongoDBClient()
|
|
170
|
+
doc = client.db["cms_realtime"].find_one({"_id": path})
|
|
171
|
+
return doc["data"] if doc else None
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def set_realtime_content(path, value):
|
|
175
|
+
client = MongoDBClient()
|
|
176
|
+
client.db["cms_realtime"].replace_one(
|
|
177
|
+
{"_id": path}, {"_id": path, "data": value}, upsert=True
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def delete_realtime_content(path):
|
|
182
|
+
client = MongoDBClient()
|
|
183
|
+
client.db["cms_realtime"].delete_one({"_id": path})
|