memuron 0.1.1__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.
- memuron/__init__.py +3 -0
- memuron/actions/__init__.py +12 -0
- memuron/actions/context.py +63 -0
- memuron/actions/helpers.py +88 -0
- memuron/actions/memory.py +340 -0
- memuron/actions/memory_write.py +290 -0
- memuron/actions/nodes.py +340 -0
- memuron/actions/registry.py +5 -0
- memuron/actions/runtime.py +37 -0
- memuron/actions/spaces_documents.py +720 -0
- memuron/actions/sync.py +155 -0
- memuron/application/__init__.py +1 -0
- memuron/application/api.py +206 -0
- memuron/application/app.py +103 -0
- memuron/application/capabilities.py +82 -0
- memuron/application/cli.py +35 -0
- memuron/application/config.py +176 -0
- memuron/application/mcp.py +44 -0
- memuron/application/mcp_oauth.py +290 -0
- memuron/application/registry.py +52 -0
- memuron/context.py +532 -0
- memuron/documents/__init__.py +1 -0
- memuron/documents/link_guardian.py +192 -0
- memuron/documents/linking.py +292 -0
- memuron/documents/parser.py +1152 -0
- memuron/documents/storage.py +151 -0
- memuron/documents/url_ingest.py +375 -0
- memuron/domain/__init__.py +1 -0
- memuron/domain/decoders.py +1 -0
- memuron/domain/encoders.py +185 -0
- memuron/domain/lifecycles.py +8 -0
- memuron/domain/limits.py +6 -0
- memuron/domain/representations.py +56 -0
- memuron/domain/schemas.py +581 -0
- memuron/domain/scope_filter.py +104 -0
- memuron/graphfs/__init__.py +1 -0
- memuron/graphfs/manual.py +635 -0
- memuron/graphfs/projection.py +578 -0
- memuron/graphfs/query.py +1782 -0
- memuron/graphfs/read_model.py +574 -0
- memuron/ingest/__init__.py +1 -0
- memuron/ingest/guardian.py +213 -0
- memuron/ingest/jobs.py +424 -0
- memuron/ingest/prompts.py +147 -0
- memuron/memory/__init__.py +1 -0
- memuron/memory/engine.py +35 -0
- memuron/memory/projections.py +452 -0
- memuron/memory/recipes.py +3247 -0
- memuron/persistence/__init__.py +1 -0
- memuron/persistence/db_pool.py +57 -0
- memuron/persistence/identity_store.py +918 -0
- memuron/persistence/store_helpers.py +16 -0
- memuron/search/__init__.py +1 -0
- memuron/search/fulltext.py +110 -0
- memuron/search/hybrid.py +284 -0
- memuron/search/pgvector.py +252 -0
- memuron/security/__init__.py +1 -0
- memuron/security/auth.py +143 -0
- memuron/security/auth_provider.py +119 -0
- memuron/security/authorization.py +53 -0
- memuron/security/clerk_scopes.py +94 -0
- memuron/security/clerk_webhooks.py +61 -0
- memuron/security/jwt_tokens.py +53 -0
- memuron/security/passwords.py +38 -0
- memuron/security/tenant.py +58 -0
- memuron/spaces/__init__.py +1 -0
- memuron/spaces/model.py +35 -0
- memuron/spaces/service.py +155 -0
- memuron/sync/__init__.py +25 -0
- memuron/sync/folder.py +828 -0
- memuron-0.1.1.dist-info/METADATA +242 -0
- memuron-0.1.1.dist-info/RECORD +74 -0
- memuron-0.1.1.dist-info/WHEEL +4 -0
- memuron-0.1.1.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
"""Users, organizations, and memberships for Memuron auth."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import sqlite3
|
|
8
|
+
import threading
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from time import monotonic
|
|
13
|
+
from typing import Any, Iterator
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
from memuron.spaces.model import compile_space_token, default_personal_space, slugify_space
|
|
17
|
+
|
|
18
|
+
import psycopg
|
|
19
|
+
from artha_engine.store import is_postgres_dsn
|
|
20
|
+
from psycopg.rows import dict_row
|
|
21
|
+
from psycopg_pool import ConnectionPool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _utcnow() -> datetime:
|
|
25
|
+
return datetime.now(UTC).replace(tzinfo=None)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _slugify(value: str) -> str:
|
|
29
|
+
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
30
|
+
return slug or "org"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IdentityStore:
|
|
34
|
+
def __init__(self, database_target: str | Path) -> None:
|
|
35
|
+
self.database_target = str(database_target)
|
|
36
|
+
self.is_postgres = is_postgres_dsn(self.database_target)
|
|
37
|
+
self._space_cache: dict[tuple[str, str], tuple[float, list[dict[str, Any]]]] = {}
|
|
38
|
+
self._space_cache_lock = threading.RLock()
|
|
39
|
+
self._pool: ConnectionPool | None = None
|
|
40
|
+
if self.is_postgres:
|
|
41
|
+
self._pool = ConnectionPool(
|
|
42
|
+
conninfo=self.database_target,
|
|
43
|
+
min_size=1,
|
|
44
|
+
max_size=8,
|
|
45
|
+
kwargs={"row_factory": dict_row},
|
|
46
|
+
open=True,
|
|
47
|
+
)
|
|
48
|
+
self._init_schema()
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_target(cls, database_target: str | Path) -> "IdentityStore":
|
|
52
|
+
return cls(database_target)
|
|
53
|
+
|
|
54
|
+
def _init_schema(self) -> None:
|
|
55
|
+
if self.is_postgres:
|
|
56
|
+
with self._connect_postgres() as conn:
|
|
57
|
+
conn.execute(
|
|
58
|
+
"""
|
|
59
|
+
CREATE TABLE IF NOT EXISTS memuron_users (
|
|
60
|
+
id UUID PRIMARY KEY,
|
|
61
|
+
email TEXT NOT NULL UNIQUE,
|
|
62
|
+
password_hash TEXT NOT NULL,
|
|
63
|
+
name TEXT NOT NULL,
|
|
64
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
65
|
+
)
|
|
66
|
+
"""
|
|
67
|
+
)
|
|
68
|
+
conn.execute(
|
|
69
|
+
"""
|
|
70
|
+
CREATE TABLE IF NOT EXISTS memuron_orgs (
|
|
71
|
+
id UUID PRIMARY KEY,
|
|
72
|
+
slug TEXT NOT NULL UNIQUE,
|
|
73
|
+
name TEXT NOT NULL,
|
|
74
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
75
|
+
)
|
|
76
|
+
"""
|
|
77
|
+
)
|
|
78
|
+
conn.execute(
|
|
79
|
+
"""
|
|
80
|
+
CREATE TABLE IF NOT EXISTS memuron_org_memberships (
|
|
81
|
+
org_id UUID NOT NULL REFERENCES memuron_orgs(id) ON DELETE CASCADE,
|
|
82
|
+
user_id UUID NOT NULL REFERENCES memuron_users(id) ON DELETE CASCADE,
|
|
83
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
84
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
85
|
+
PRIMARY KEY (org_id, user_id)
|
|
86
|
+
)
|
|
87
|
+
"""
|
|
88
|
+
)
|
|
89
|
+
conn.execute(
|
|
90
|
+
"""
|
|
91
|
+
CREATE TABLE IF NOT EXISTS memuron_org_spaces (
|
|
92
|
+
id UUID PRIMARY KEY,
|
|
93
|
+
org_id UUID NOT NULL REFERENCES memuron_orgs(id) ON DELETE CASCADE,
|
|
94
|
+
slug TEXT NOT NULL,
|
|
95
|
+
name TEXT NOT NULL,
|
|
96
|
+
token TEXT NOT NULL,
|
|
97
|
+
description TEXT NOT NULL DEFAULT '',
|
|
98
|
+
guardian_prompt TEXT NOT NULL DEFAULT '',
|
|
99
|
+
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
|
100
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
101
|
+
UNIQUE (org_id, slug),
|
|
102
|
+
UNIQUE (org_id, token)
|
|
103
|
+
)
|
|
104
|
+
"""
|
|
105
|
+
)
|
|
106
|
+
conn.execute(
|
|
107
|
+
"""
|
|
108
|
+
CREATE TABLE IF NOT EXISTS memuron_user_space_prefs (
|
|
109
|
+
user_id UUID NOT NULL REFERENCES memuron_users(id) ON DELETE CASCADE,
|
|
110
|
+
space_id UUID NOT NULL REFERENCES memuron_org_spaces(id) ON DELETE CASCADE,
|
|
111
|
+
enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
112
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
113
|
+
PRIMARY KEY (user_id, space_id)
|
|
114
|
+
)
|
|
115
|
+
"""
|
|
116
|
+
)
|
|
117
|
+
conn.commit()
|
|
118
|
+
self._migrate_clerk_schema(conn)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
with self._connect_sqlite() as conn:
|
|
122
|
+
conn.executescript(
|
|
123
|
+
"""
|
|
124
|
+
CREATE TABLE IF NOT EXISTS memuron_users (
|
|
125
|
+
id TEXT PRIMARY KEY,
|
|
126
|
+
email TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
127
|
+
password_hash TEXT NOT NULL,
|
|
128
|
+
name TEXT NOT NULL,
|
|
129
|
+
created_at TEXT NOT NULL
|
|
130
|
+
);
|
|
131
|
+
CREATE TABLE IF NOT EXISTS memuron_orgs (
|
|
132
|
+
id TEXT PRIMARY KEY,
|
|
133
|
+
slug TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
134
|
+
name TEXT NOT NULL,
|
|
135
|
+
created_at TEXT NOT NULL
|
|
136
|
+
);
|
|
137
|
+
CREATE TABLE IF NOT EXISTS memuron_org_memberships (
|
|
138
|
+
org_id TEXT NOT NULL,
|
|
139
|
+
user_id TEXT NOT NULL,
|
|
140
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
141
|
+
created_at TEXT NOT NULL,
|
|
142
|
+
PRIMARY KEY (org_id, user_id),
|
|
143
|
+
FOREIGN KEY (org_id) REFERENCES memuron_orgs(id) ON DELETE CASCADE,
|
|
144
|
+
FOREIGN KEY (user_id) REFERENCES memuron_users(id) ON DELETE CASCADE
|
|
145
|
+
);
|
|
146
|
+
CREATE TABLE IF NOT EXISTS memuron_org_spaces (
|
|
147
|
+
id TEXT PRIMARY KEY,
|
|
148
|
+
org_id TEXT NOT NULL,
|
|
149
|
+
slug TEXT NOT NULL COLLATE NOCASE,
|
|
150
|
+
name TEXT NOT NULL,
|
|
151
|
+
token TEXT NOT NULL,
|
|
152
|
+
description TEXT NOT NULL DEFAULT '',
|
|
153
|
+
guardian_prompt TEXT NOT NULL DEFAULT '',
|
|
154
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
155
|
+
created_at TEXT NOT NULL,
|
|
156
|
+
UNIQUE (org_id, slug),
|
|
157
|
+
UNIQUE (org_id, token),
|
|
158
|
+
FOREIGN KEY (org_id) REFERENCES memuron_orgs(id) ON DELETE CASCADE
|
|
159
|
+
);
|
|
160
|
+
CREATE TABLE IF NOT EXISTS memuron_user_space_prefs (
|
|
161
|
+
user_id TEXT NOT NULL,
|
|
162
|
+
space_id TEXT NOT NULL,
|
|
163
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
164
|
+
updated_at TEXT NOT NULL,
|
|
165
|
+
PRIMARY KEY (user_id, space_id),
|
|
166
|
+
FOREIGN KEY (user_id) REFERENCES memuron_users(id) ON DELETE CASCADE,
|
|
167
|
+
FOREIGN KEY (space_id) REFERENCES memuron_org_spaces(id) ON DELETE CASCADE
|
|
168
|
+
);
|
|
169
|
+
"""
|
|
170
|
+
)
|
|
171
|
+
conn.commit()
|
|
172
|
+
self._migrate_clerk_schema_sqlite(conn)
|
|
173
|
+
|
|
174
|
+
def _migrate_clerk_schema(self, conn: psycopg.Connection[Any]) -> None:
|
|
175
|
+
conn.execute(
|
|
176
|
+
"ALTER TABLE IF EXISTS memuron_org_spaces "
|
|
177
|
+
"DROP CONSTRAINT IF EXISTS memuron_org_spaces_org_id_fkey"
|
|
178
|
+
)
|
|
179
|
+
conn.execute(
|
|
180
|
+
"ALTER TABLE IF EXISTS memuron_user_space_prefs "
|
|
181
|
+
"DROP CONSTRAINT IF EXISTS memuron_user_space_prefs_user_id_fkey"
|
|
182
|
+
)
|
|
183
|
+
conn.execute(
|
|
184
|
+
"ALTER TABLE IF EXISTS memuron_user_space_prefs "
|
|
185
|
+
"DROP CONSTRAINT IF EXISTS memuron_user_space_prefs_space_id_fkey"
|
|
186
|
+
)
|
|
187
|
+
conn.execute(
|
|
188
|
+
"""
|
|
189
|
+
ALTER TABLE memuron_org_spaces
|
|
190
|
+
ALTER COLUMN org_id TYPE TEXT USING org_id::text
|
|
191
|
+
"""
|
|
192
|
+
)
|
|
193
|
+
conn.execute(
|
|
194
|
+
"""
|
|
195
|
+
ALTER TABLE memuron_user_space_prefs
|
|
196
|
+
ALTER COLUMN user_id TYPE TEXT USING user_id::text
|
|
197
|
+
"""
|
|
198
|
+
)
|
|
199
|
+
conn.commit()
|
|
200
|
+
|
|
201
|
+
def _migrate_clerk_schema_sqlite(self, conn: sqlite3.Connection) -> None:
|
|
202
|
+
_ = conn
|
|
203
|
+
|
|
204
|
+
def ensure_org_spaces(self, org_id: str, user_id: str) -> list[dict[str, Any]]:
|
|
205
|
+
spaces = self.ensure_default_spaces(org_id)
|
|
206
|
+
if user_id != "system":
|
|
207
|
+
for space in spaces:
|
|
208
|
+
if space.get("is_default"):
|
|
209
|
+
self.seed_space_pref_for_user(user_id, space["id"], enabled=True)
|
|
210
|
+
break
|
|
211
|
+
else:
|
|
212
|
+
if spaces:
|
|
213
|
+
self.seed_space_pref_for_user(user_id, spaces[0]["id"], enabled=True)
|
|
214
|
+
self._invalidate_space_cache()
|
|
215
|
+
if user_id == "system":
|
|
216
|
+
return spaces
|
|
217
|
+
return self.list_user_org_spaces(user_id, org_id)
|
|
218
|
+
|
|
219
|
+
@contextmanager
|
|
220
|
+
def _connect_postgres(self) -> Iterator[psycopg.Connection[Any]]:
|
|
221
|
+
if self._pool is None:
|
|
222
|
+
raise RuntimeError("PostgreSQL pool is unavailable")
|
|
223
|
+
with self._pool.connection() as conn:
|
|
224
|
+
yield conn
|
|
225
|
+
|
|
226
|
+
def close(self) -> None:
|
|
227
|
+
with self._space_cache_lock:
|
|
228
|
+
self._space_cache.clear()
|
|
229
|
+
if self._pool is not None:
|
|
230
|
+
self._pool.close()
|
|
231
|
+
self._pool = None
|
|
232
|
+
|
|
233
|
+
def _invalidate_space_cache(self) -> None:
|
|
234
|
+
with self._space_cache_lock:
|
|
235
|
+
self._space_cache.clear()
|
|
236
|
+
|
|
237
|
+
@contextmanager
|
|
238
|
+
def _connect_sqlite(self) -> Iterator[sqlite3.Connection]:
|
|
239
|
+
path = Path(self.database_target)
|
|
240
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
conn = sqlite3.connect(path)
|
|
242
|
+
conn.row_factory = sqlite3.Row
|
|
243
|
+
try:
|
|
244
|
+
yield conn
|
|
245
|
+
finally:
|
|
246
|
+
conn.close()
|
|
247
|
+
|
|
248
|
+
def _public_user(self, row: dict[str, Any]) -> dict[str, Any]:
|
|
249
|
+
return {
|
|
250
|
+
"id": str(row["id"]),
|
|
251
|
+
"email": str(row["email"]),
|
|
252
|
+
"name": str(row["name"]),
|
|
253
|
+
"created_at": str(row["created_at"]),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
def _public_org(self, row: dict[str, Any]) -> dict[str, Any]:
|
|
257
|
+
return {
|
|
258
|
+
"id": str(row["id"]),
|
|
259
|
+
"slug": str(row["slug"]),
|
|
260
|
+
"name": str(row["name"]),
|
|
261
|
+
"created_at": str(row["created_at"]),
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
def create_user(self, *, email: str, password_hash: str, name: str) -> dict[str, Any]:
|
|
265
|
+
user_id = str(uuid4())
|
|
266
|
+
now = _utcnow().isoformat()
|
|
267
|
+
normalized_email = email.strip().lower()
|
|
268
|
+
if self.is_postgres:
|
|
269
|
+
with self._connect_postgres() as conn:
|
|
270
|
+
try:
|
|
271
|
+
row = conn.execute(
|
|
272
|
+
"""
|
|
273
|
+
INSERT INTO memuron_users (id, email, password_hash, name, created_at)
|
|
274
|
+
VALUES (%s, %s, %s, %s, %s)
|
|
275
|
+
RETURNING *
|
|
276
|
+
""",
|
|
277
|
+
(user_id, normalized_email, password_hash, name.strip(), now),
|
|
278
|
+
).fetchone()
|
|
279
|
+
conn.commit()
|
|
280
|
+
except psycopg.errors.UniqueViolation as exc:
|
|
281
|
+
raise ValueError("email already registered") from exc
|
|
282
|
+
return self._public_user(row)
|
|
283
|
+
with self._connect_sqlite() as conn:
|
|
284
|
+
try:
|
|
285
|
+
conn.execute(
|
|
286
|
+
"""
|
|
287
|
+
INSERT INTO memuron_users (id, email, password_hash, name, created_at)
|
|
288
|
+
VALUES (?, ?, ?, ?, ?)
|
|
289
|
+
""",
|
|
290
|
+
(user_id, normalized_email, password_hash, name.strip(), now),
|
|
291
|
+
)
|
|
292
|
+
conn.commit()
|
|
293
|
+
except sqlite3.IntegrityError as exc:
|
|
294
|
+
raise ValueError("email already registered") from exc
|
|
295
|
+
row = conn.execute(
|
|
296
|
+
"SELECT * FROM memuron_users WHERE id = ?",
|
|
297
|
+
(user_id,),
|
|
298
|
+
).fetchone()
|
|
299
|
+
return self._public_user(dict(row))
|
|
300
|
+
|
|
301
|
+
def get_user_by_email(self, email: str) -> dict[str, Any] | None:
|
|
302
|
+
normalized_email = email.strip().lower()
|
|
303
|
+
if self.is_postgres:
|
|
304
|
+
with self._connect_postgres() as conn:
|
|
305
|
+
row = conn.execute(
|
|
306
|
+
"SELECT * FROM memuron_users WHERE lower(email) = %s",
|
|
307
|
+
(normalized_email,),
|
|
308
|
+
).fetchone()
|
|
309
|
+
return dict(row) if row else None
|
|
310
|
+
with self._connect_sqlite() as conn:
|
|
311
|
+
row = conn.execute(
|
|
312
|
+
"SELECT * FROM memuron_users WHERE lower(email) = ?",
|
|
313
|
+
(normalized_email,),
|
|
314
|
+
).fetchone()
|
|
315
|
+
return dict(row) if row else None
|
|
316
|
+
|
|
317
|
+
def get_user_by_id(self, user_id: str) -> dict[str, Any] | None:
|
|
318
|
+
if self.is_postgres:
|
|
319
|
+
with self._connect_postgres() as conn:
|
|
320
|
+
row = conn.execute(
|
|
321
|
+
"SELECT * FROM memuron_users WHERE id = %s",
|
|
322
|
+
(user_id,),
|
|
323
|
+
).fetchone()
|
|
324
|
+
return dict(row) if row else None
|
|
325
|
+
with self._connect_sqlite() as conn:
|
|
326
|
+
row = conn.execute(
|
|
327
|
+
"SELECT * FROM memuron_users WHERE id = ?",
|
|
328
|
+
(user_id,),
|
|
329
|
+
).fetchone()
|
|
330
|
+
return dict(row) if row else None
|
|
331
|
+
|
|
332
|
+
def _unique_org_slug(self, base_slug: str) -> str:
|
|
333
|
+
slug = _slugify(base_slug)
|
|
334
|
+
candidate = slug
|
|
335
|
+
suffix = 1
|
|
336
|
+
while self.get_org_by_slug(candidate) is not None:
|
|
337
|
+
candidate = f"{slug}-{suffix}"
|
|
338
|
+
suffix += 1
|
|
339
|
+
return candidate
|
|
340
|
+
|
|
341
|
+
def create_org(self, *, name: str, slug: str | None = None) -> dict[str, Any]:
|
|
342
|
+
org_id = str(uuid4())
|
|
343
|
+
now = _utcnow().isoformat()
|
|
344
|
+
org_slug = self._unique_org_slug(slug or name)
|
|
345
|
+
if self.is_postgres:
|
|
346
|
+
with self._connect_postgres() as conn:
|
|
347
|
+
row = conn.execute(
|
|
348
|
+
"""
|
|
349
|
+
INSERT INTO memuron_orgs (id, slug, name, created_at)
|
|
350
|
+
VALUES (%s, %s, %s, %s)
|
|
351
|
+
RETURNING *
|
|
352
|
+
""",
|
|
353
|
+
(org_id, org_slug, name.strip(), now),
|
|
354
|
+
).fetchone()
|
|
355
|
+
conn.commit()
|
|
356
|
+
return self._public_org(row)
|
|
357
|
+
with self._connect_sqlite() as conn:
|
|
358
|
+
conn.execute(
|
|
359
|
+
"""
|
|
360
|
+
INSERT INTO memuron_orgs (id, slug, name, created_at)
|
|
361
|
+
VALUES (?, ?, ?, ?)
|
|
362
|
+
""",
|
|
363
|
+
(org_id, org_slug, name.strip(), now),
|
|
364
|
+
)
|
|
365
|
+
conn.commit()
|
|
366
|
+
row = conn.execute(
|
|
367
|
+
"SELECT * FROM memuron_orgs WHERE id = ?",
|
|
368
|
+
(org_id,),
|
|
369
|
+
).fetchone()
|
|
370
|
+
return self._public_org(dict(row))
|
|
371
|
+
|
|
372
|
+
def get_org_by_id(self, org_id: str) -> dict[str, Any] | None:
|
|
373
|
+
if self.is_postgres:
|
|
374
|
+
with self._connect_postgres() as conn:
|
|
375
|
+
row = conn.execute(
|
|
376
|
+
"SELECT * FROM memuron_orgs WHERE id = %s",
|
|
377
|
+
(org_id,),
|
|
378
|
+
).fetchone()
|
|
379
|
+
return self._public_org(row) if row else None
|
|
380
|
+
with self._connect_sqlite() as conn:
|
|
381
|
+
row = conn.execute(
|
|
382
|
+
"SELECT * FROM memuron_orgs WHERE id = ?",
|
|
383
|
+
(org_id,),
|
|
384
|
+
).fetchone()
|
|
385
|
+
return self._public_org(dict(row)) if row else None
|
|
386
|
+
|
|
387
|
+
def get_org_by_slug(self, slug: str) -> dict[str, Any] | None:
|
|
388
|
+
if self.is_postgres:
|
|
389
|
+
with self._connect_postgres() as conn:
|
|
390
|
+
row = conn.execute(
|
|
391
|
+
"SELECT * FROM memuron_orgs WHERE slug = %s",
|
|
392
|
+
(slug,),
|
|
393
|
+
).fetchone()
|
|
394
|
+
return self._public_org(row) if row else None
|
|
395
|
+
with self._connect_sqlite() as conn:
|
|
396
|
+
row = conn.execute(
|
|
397
|
+
"SELECT * FROM memuron_orgs WHERE slug = ?",
|
|
398
|
+
(slug,),
|
|
399
|
+
).fetchone()
|
|
400
|
+
return self._public_org(dict(row)) if row else None
|
|
401
|
+
|
|
402
|
+
def add_membership(self, *, org_id: str, user_id: str, role: str = "member") -> None:
|
|
403
|
+
now = _utcnow().isoformat()
|
|
404
|
+
if self.is_postgres:
|
|
405
|
+
with self._connect_postgres() as conn:
|
|
406
|
+
conn.execute(
|
|
407
|
+
"""
|
|
408
|
+
INSERT INTO memuron_org_memberships (org_id, user_id, role, created_at)
|
|
409
|
+
VALUES (%s, %s, %s, %s)
|
|
410
|
+
ON CONFLICT (org_id, user_id) DO UPDATE SET role = EXCLUDED.role
|
|
411
|
+
""",
|
|
412
|
+
(org_id, user_id, role, now),
|
|
413
|
+
)
|
|
414
|
+
conn.commit()
|
|
415
|
+
return
|
|
416
|
+
with self._connect_sqlite() as conn:
|
|
417
|
+
conn.execute(
|
|
418
|
+
"""
|
|
419
|
+
INSERT INTO memuron_org_memberships (org_id, user_id, role, created_at)
|
|
420
|
+
VALUES (?, ?, ?, ?)
|
|
421
|
+
ON CONFLICT (org_id, user_id) DO UPDATE SET role = excluded.role
|
|
422
|
+
""",
|
|
423
|
+
(org_id, user_id, role, now),
|
|
424
|
+
)
|
|
425
|
+
conn.commit()
|
|
426
|
+
|
|
427
|
+
def get_membership(self, *, org_id: str, user_id: str) -> dict[str, Any] | None:
|
|
428
|
+
if self.is_postgres:
|
|
429
|
+
with self._connect_postgres() as conn:
|
|
430
|
+
row = conn.execute(
|
|
431
|
+
"""
|
|
432
|
+
SELECT role, created_at FROM memuron_org_memberships
|
|
433
|
+
WHERE org_id = %s AND user_id = %s
|
|
434
|
+
""",
|
|
435
|
+
(org_id, user_id),
|
|
436
|
+
).fetchone()
|
|
437
|
+
return dict(row) if row else None
|
|
438
|
+
with self._connect_sqlite() as conn:
|
|
439
|
+
row = conn.execute(
|
|
440
|
+
"""
|
|
441
|
+
SELECT role, created_at FROM memuron_org_memberships
|
|
442
|
+
WHERE org_id = ? AND user_id = ?
|
|
443
|
+
""",
|
|
444
|
+
(org_id, user_id),
|
|
445
|
+
).fetchone()
|
|
446
|
+
return dict(row) if row else None
|
|
447
|
+
|
|
448
|
+
def list_user_orgs(self, user_id: str) -> list[dict[str, Any]]:
|
|
449
|
+
if self.is_postgres:
|
|
450
|
+
with self._connect_postgres() as conn:
|
|
451
|
+
rows = conn.execute(
|
|
452
|
+
"""
|
|
453
|
+
SELECT o.id, o.slug, o.name, o.created_at, m.role
|
|
454
|
+
FROM memuron_org_memberships m
|
|
455
|
+
JOIN memuron_orgs o ON o.id = m.org_id
|
|
456
|
+
WHERE m.user_id = %s
|
|
457
|
+
ORDER BY o.name ASC
|
|
458
|
+
""",
|
|
459
|
+
(user_id,),
|
|
460
|
+
).fetchall()
|
|
461
|
+
else:
|
|
462
|
+
with self._connect_sqlite() as conn:
|
|
463
|
+
rows = conn.execute(
|
|
464
|
+
"""
|
|
465
|
+
SELECT o.id, o.slug, o.name, o.created_at, m.role
|
|
466
|
+
FROM memuron_org_memberships m
|
|
467
|
+
JOIN memuron_orgs o ON o.id = m.org_id
|
|
468
|
+
WHERE m.user_id = ?
|
|
469
|
+
ORDER BY o.name ASC
|
|
470
|
+
""",
|
|
471
|
+
(user_id,),
|
|
472
|
+
).fetchall()
|
|
473
|
+
return [
|
|
474
|
+
{
|
|
475
|
+
**self._public_org(dict(row)),
|
|
476
|
+
"role": str(row["role"]),
|
|
477
|
+
}
|
|
478
|
+
for row in rows
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
def _public_space(self, row: dict[str, Any], *, is_enabled: bool | None = None) -> dict[str, Any]:
|
|
482
|
+
is_default = row.get("is_default")
|
|
483
|
+
if isinstance(is_default, int):
|
|
484
|
+
is_default = bool(is_default)
|
|
485
|
+
payload = {
|
|
486
|
+
"id": str(row["id"]),
|
|
487
|
+
"org_id": str(row["org_id"]),
|
|
488
|
+
"slug": str(row["slug"]),
|
|
489
|
+
"name": str(row["name"]),
|
|
490
|
+
"token": str(row["token"]),
|
|
491
|
+
"description": str(row.get("description") or ""),
|
|
492
|
+
"guardian_prompt": str(row.get("guardian_prompt") or ""),
|
|
493
|
+
"is_default": bool(is_default),
|
|
494
|
+
"created_at": str(row["created_at"]),
|
|
495
|
+
}
|
|
496
|
+
if is_enabled is not None:
|
|
497
|
+
payload["is_enabled"] = is_enabled
|
|
498
|
+
return payload
|
|
499
|
+
|
|
500
|
+
def _unique_space_slug(self, org_id: str, base_slug: str) -> str:
|
|
501
|
+
slug = slugify_space(base_slug)
|
|
502
|
+
candidate = slug
|
|
503
|
+
suffix = 1
|
|
504
|
+
while self.get_space_by_slug(org_id, candidate) is not None:
|
|
505
|
+
candidate = f"{slug}-{suffix}"
|
|
506
|
+
suffix += 1
|
|
507
|
+
return candidate
|
|
508
|
+
|
|
509
|
+
def create_space(
|
|
510
|
+
self,
|
|
511
|
+
*,
|
|
512
|
+
org_id: str,
|
|
513
|
+
name: str,
|
|
514
|
+
slug: str | None = None,
|
|
515
|
+
description: str = "",
|
|
516
|
+
guardian_prompt: str = "",
|
|
517
|
+
is_default: bool = False,
|
|
518
|
+
) -> dict[str, Any]:
|
|
519
|
+
space_id = str(uuid4())
|
|
520
|
+
now = _utcnow().isoformat()
|
|
521
|
+
space_slug = self._unique_space_slug(org_id, slug or name)
|
|
522
|
+
token = compile_space_token(space_slug)
|
|
523
|
+
if is_default:
|
|
524
|
+
self._clear_default_space(org_id)
|
|
525
|
+
if self.is_postgres:
|
|
526
|
+
with self._connect_postgres() as conn:
|
|
527
|
+
row = conn.execute(
|
|
528
|
+
"""
|
|
529
|
+
INSERT INTO memuron_org_spaces (
|
|
530
|
+
id, org_id, slug, name, token, description, guardian_prompt,
|
|
531
|
+
is_default, created_at
|
|
532
|
+
)
|
|
533
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
534
|
+
RETURNING *
|
|
535
|
+
""",
|
|
536
|
+
(
|
|
537
|
+
space_id,
|
|
538
|
+
org_id,
|
|
539
|
+
space_slug,
|
|
540
|
+
name.strip(),
|
|
541
|
+
token,
|
|
542
|
+
description.strip(),
|
|
543
|
+
guardian_prompt.strip(),
|
|
544
|
+
is_default,
|
|
545
|
+
now,
|
|
546
|
+
),
|
|
547
|
+
).fetchone()
|
|
548
|
+
conn.commit()
|
|
549
|
+
created = self._public_space(row)
|
|
550
|
+
self._invalidate_space_cache()
|
|
551
|
+
return created
|
|
552
|
+
with self._connect_sqlite() as conn:
|
|
553
|
+
conn.execute(
|
|
554
|
+
"""
|
|
555
|
+
INSERT INTO memuron_org_spaces (
|
|
556
|
+
id, org_id, slug, name, token, description, guardian_prompt,
|
|
557
|
+
is_default, created_at
|
|
558
|
+
)
|
|
559
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
560
|
+
""",
|
|
561
|
+
(
|
|
562
|
+
space_id,
|
|
563
|
+
org_id,
|
|
564
|
+
space_slug,
|
|
565
|
+
name.strip(),
|
|
566
|
+
token,
|
|
567
|
+
description.strip(),
|
|
568
|
+
guardian_prompt.strip(),
|
|
569
|
+
1 if is_default else 0,
|
|
570
|
+
now,
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
conn.commit()
|
|
574
|
+
row = conn.execute(
|
|
575
|
+
"SELECT * FROM memuron_org_spaces WHERE id = ?",
|
|
576
|
+
(space_id,),
|
|
577
|
+
).fetchone()
|
|
578
|
+
created = self._public_space(dict(row))
|
|
579
|
+
self._invalidate_space_cache()
|
|
580
|
+
return created
|
|
581
|
+
|
|
582
|
+
def _clear_default_space(self, org_id: str) -> None:
|
|
583
|
+
if self.is_postgres:
|
|
584
|
+
with self._connect_postgres() as conn:
|
|
585
|
+
conn.execute(
|
|
586
|
+
"UPDATE memuron_org_spaces SET is_default = FALSE WHERE org_id = %s",
|
|
587
|
+
(org_id,),
|
|
588
|
+
)
|
|
589
|
+
conn.commit()
|
|
590
|
+
return
|
|
591
|
+
with self._connect_sqlite() as conn:
|
|
592
|
+
conn.execute(
|
|
593
|
+
"UPDATE memuron_org_spaces SET is_default = 0 WHERE org_id = ?",
|
|
594
|
+
(org_id,),
|
|
595
|
+
)
|
|
596
|
+
conn.commit()
|
|
597
|
+
|
|
598
|
+
def ensure_default_spaces(self, org_id: str) -> list[dict[str, Any]]:
|
|
599
|
+
spaces = self.list_org_spaces(org_id)
|
|
600
|
+
if spaces:
|
|
601
|
+
return spaces
|
|
602
|
+
defaults = default_personal_space(org_id=org_id)
|
|
603
|
+
created = self.create_space(
|
|
604
|
+
org_id=org_id,
|
|
605
|
+
name=defaults["name"],
|
|
606
|
+
slug=defaults["slug"],
|
|
607
|
+
description=defaults["description"],
|
|
608
|
+
guardian_prompt=defaults["guardian_prompt"],
|
|
609
|
+
is_default=True,
|
|
610
|
+
)
|
|
611
|
+
return [created]
|
|
612
|
+
|
|
613
|
+
def list_org_spaces(self, org_id: str) -> list[dict[str, Any]]:
|
|
614
|
+
if self.is_postgres:
|
|
615
|
+
with self._connect_postgres() as conn:
|
|
616
|
+
rows = conn.execute(
|
|
617
|
+
"""
|
|
618
|
+
SELECT * FROM memuron_org_spaces
|
|
619
|
+
WHERE org_id = %s
|
|
620
|
+
ORDER BY is_default DESC, name ASC
|
|
621
|
+
""",
|
|
622
|
+
(org_id,),
|
|
623
|
+
).fetchall()
|
|
624
|
+
else:
|
|
625
|
+
with self._connect_sqlite() as conn:
|
|
626
|
+
rows = conn.execute(
|
|
627
|
+
"""
|
|
628
|
+
SELECT * FROM memuron_org_spaces
|
|
629
|
+
WHERE org_id = ?
|
|
630
|
+
ORDER BY is_default DESC, name ASC
|
|
631
|
+
""",
|
|
632
|
+
(org_id,),
|
|
633
|
+
).fetchall()
|
|
634
|
+
return [self._public_space(dict(row)) for row in rows]
|
|
635
|
+
|
|
636
|
+
def _load_space_prefs(self, user_id: str, space_ids: list[str]) -> dict[str, bool]:
|
|
637
|
+
if not space_ids:
|
|
638
|
+
return {}
|
|
639
|
+
if self.is_postgres:
|
|
640
|
+
with self._connect_postgres() as conn:
|
|
641
|
+
rows = conn.execute(
|
|
642
|
+
"""
|
|
643
|
+
SELECT space_id, enabled FROM memuron_user_space_prefs
|
|
644
|
+
WHERE user_id = %s AND space_id = ANY(%s)
|
|
645
|
+
""",
|
|
646
|
+
(user_id, space_ids),
|
|
647
|
+
).fetchall()
|
|
648
|
+
else:
|
|
649
|
+
placeholders = ",".join("?" for _ in space_ids)
|
|
650
|
+
with self._connect_sqlite() as conn:
|
|
651
|
+
rows = conn.execute(
|
|
652
|
+
f"""
|
|
653
|
+
SELECT space_id, enabled FROM memuron_user_space_prefs
|
|
654
|
+
WHERE user_id = ? AND space_id IN ({placeholders})
|
|
655
|
+
""",
|
|
656
|
+
(user_id, *space_ids),
|
|
657
|
+
).fetchall()
|
|
658
|
+
prefs: dict[str, bool] = {}
|
|
659
|
+
for row in rows:
|
|
660
|
+
enabled = row["enabled"]
|
|
661
|
+
if isinstance(enabled, int):
|
|
662
|
+
enabled = bool(enabled)
|
|
663
|
+
prefs[str(row["space_id"])] = bool(enabled)
|
|
664
|
+
return prefs
|
|
665
|
+
|
|
666
|
+
def _upsert_space_pref(self, user_id: str, space_id: str, enabled: bool) -> None:
|
|
667
|
+
now = _utcnow().isoformat()
|
|
668
|
+
if self.is_postgres:
|
|
669
|
+
with self._connect_postgres() as conn:
|
|
670
|
+
conn.execute(
|
|
671
|
+
"""
|
|
672
|
+
INSERT INTO memuron_user_space_prefs (user_id, space_id, enabled, updated_at)
|
|
673
|
+
VALUES (%s, %s, %s, %s)
|
|
674
|
+
ON CONFLICT (user_id, space_id)
|
|
675
|
+
DO UPDATE SET enabled = EXCLUDED.enabled, updated_at = EXCLUDED.updated_at
|
|
676
|
+
""",
|
|
677
|
+
(user_id, space_id, enabled, now),
|
|
678
|
+
)
|
|
679
|
+
conn.commit()
|
|
680
|
+
return
|
|
681
|
+
with self._connect_sqlite() as conn:
|
|
682
|
+
conn.execute(
|
|
683
|
+
"""
|
|
684
|
+
INSERT INTO memuron_user_space_prefs (user_id, space_id, enabled, updated_at)
|
|
685
|
+
VALUES (?, ?, ?, ?)
|
|
686
|
+
ON CONFLICT (user_id, space_id)
|
|
687
|
+
DO UPDATE SET enabled = excluded.enabled, updated_at = excluded.updated_at
|
|
688
|
+
""",
|
|
689
|
+
(user_id, space_id, 1 if enabled else 0, now),
|
|
690
|
+
)
|
|
691
|
+
conn.commit()
|
|
692
|
+
|
|
693
|
+
def list_user_org_spaces(self, user_id: str, org_id: str) -> list[dict[str, Any]]:
|
|
694
|
+
cache_key = (user_id, org_id)
|
|
695
|
+
now = monotonic()
|
|
696
|
+
with self._space_cache_lock:
|
|
697
|
+
cached = self._space_cache.get(cache_key)
|
|
698
|
+
if cached is not None and cached[0] > now:
|
|
699
|
+
return [dict(space) for space in cached[1]]
|
|
700
|
+
|
|
701
|
+
if self.is_postgres:
|
|
702
|
+
with self._connect_postgres() as conn:
|
|
703
|
+
rows = conn.execute(
|
|
704
|
+
"""
|
|
705
|
+
SELECT s.*, p.enabled AS pref_enabled
|
|
706
|
+
FROM memuron_org_spaces s
|
|
707
|
+
LEFT JOIN memuron_user_space_prefs p
|
|
708
|
+
ON p.space_id = s.id AND p.user_id = %s
|
|
709
|
+
WHERE s.org_id = %s
|
|
710
|
+
ORDER BY s.is_default DESC, s.name ASC
|
|
711
|
+
""",
|
|
712
|
+
(user_id, org_id),
|
|
713
|
+
).fetchall()
|
|
714
|
+
else:
|
|
715
|
+
with self._connect_sqlite() as conn:
|
|
716
|
+
rows = conn.execute(
|
|
717
|
+
"""
|
|
718
|
+
SELECT s.*, p.enabled AS pref_enabled
|
|
719
|
+
FROM memuron_org_spaces s
|
|
720
|
+
LEFT JOIN memuron_user_space_prefs p
|
|
721
|
+
ON p.space_id = s.id AND p.user_id = ?
|
|
722
|
+
WHERE s.org_id = ?
|
|
723
|
+
ORDER BY s.is_default DESC, s.name ASC
|
|
724
|
+
""",
|
|
725
|
+
(user_id, org_id),
|
|
726
|
+
).fetchall()
|
|
727
|
+
if not rows:
|
|
728
|
+
self.ensure_default_spaces(org_id)
|
|
729
|
+
return self.list_user_org_spaces(user_id, org_id)
|
|
730
|
+
|
|
731
|
+
output: list[dict[str, Any]] = []
|
|
732
|
+
for raw_row in rows:
|
|
733
|
+
space = dict(raw_row)
|
|
734
|
+
enabled = space.pop("pref_enabled", None)
|
|
735
|
+
if enabled is None:
|
|
736
|
+
enabled = bool(space["is_default"])
|
|
737
|
+
self._upsert_space_pref(user_id, str(space["id"]), enabled)
|
|
738
|
+
output.append(self._public_space(space, is_enabled=bool(enabled)))
|
|
739
|
+
with self._space_cache_lock:
|
|
740
|
+
self._space_cache[cache_key] = (
|
|
741
|
+
monotonic() + 30.0,
|
|
742
|
+
[dict(space) for space in output],
|
|
743
|
+
)
|
|
744
|
+
return [dict(space) for space in output]
|
|
745
|
+
|
|
746
|
+
def get_enabled_org_spaces(self, user_id: str, org_id: str) -> list[dict[str, Any]]:
|
|
747
|
+
return [
|
|
748
|
+
space
|
|
749
|
+
for space in self.list_user_org_spaces(user_id, org_id)
|
|
750
|
+
if space.get("is_enabled")
|
|
751
|
+
]
|
|
752
|
+
|
|
753
|
+
def set_space_enabled(
|
|
754
|
+
self,
|
|
755
|
+
user_id: str,
|
|
756
|
+
org_id: str,
|
|
757
|
+
space_id: str,
|
|
758
|
+
*,
|
|
759
|
+
enabled: bool,
|
|
760
|
+
) -> dict[str, Any]:
|
|
761
|
+
space = self.get_space_by_id(space_id)
|
|
762
|
+
if space is None or space["org_id"] != org_id:
|
|
763
|
+
raise ValueError("space not found in organization")
|
|
764
|
+
if not enabled:
|
|
765
|
+
enabled_spaces = self.get_enabled_org_spaces(user_id, org_id)
|
|
766
|
+
if len(enabled_spaces) <= 1 and any(item["id"] == space_id for item in enabled_spaces):
|
|
767
|
+
raise ValueError("at least one space must stay enabled")
|
|
768
|
+
self._upsert_space_pref(user_id, space_id, enabled)
|
|
769
|
+
self._invalidate_space_cache()
|
|
770
|
+
return self._public_space(space, is_enabled=enabled)
|
|
771
|
+
|
|
772
|
+
def seed_space_pref_for_user(
|
|
773
|
+
self,
|
|
774
|
+
user_id: str,
|
|
775
|
+
space_id: str,
|
|
776
|
+
*,
|
|
777
|
+
enabled: bool,
|
|
778
|
+
) -> None:
|
|
779
|
+
prefs = self._load_space_prefs(user_id, [space_id])
|
|
780
|
+
if space_id in prefs:
|
|
781
|
+
return
|
|
782
|
+
self._upsert_space_pref(user_id, space_id, enabled)
|
|
783
|
+
self._invalidate_space_cache()
|
|
784
|
+
|
|
785
|
+
def get_space_by_id(self, space_id: str) -> dict[str, Any] | None:
|
|
786
|
+
if self.is_postgres:
|
|
787
|
+
with self._connect_postgres() as conn:
|
|
788
|
+
row = conn.execute(
|
|
789
|
+
"SELECT * FROM memuron_org_spaces WHERE id = %s",
|
|
790
|
+
(space_id,),
|
|
791
|
+
).fetchone()
|
|
792
|
+
return self._public_space(row) if row else None
|
|
793
|
+
with self._connect_sqlite() as conn:
|
|
794
|
+
row = conn.execute(
|
|
795
|
+
"SELECT * FROM memuron_org_spaces WHERE id = ?",
|
|
796
|
+
(space_id,),
|
|
797
|
+
).fetchone()
|
|
798
|
+
return self._public_space(dict(row)) if row else None
|
|
799
|
+
|
|
800
|
+
def get_space_by_slug(self, org_id: str, slug: str) -> dict[str, Any] | None:
|
|
801
|
+
if self.is_postgres:
|
|
802
|
+
with self._connect_postgres() as conn:
|
|
803
|
+
row = conn.execute(
|
|
804
|
+
"""
|
|
805
|
+
SELECT * FROM memuron_org_spaces
|
|
806
|
+
WHERE org_id = %s AND slug = %s
|
|
807
|
+
""",
|
|
808
|
+
(org_id, slug),
|
|
809
|
+
).fetchone()
|
|
810
|
+
return self._public_space(row) if row else None
|
|
811
|
+
with self._connect_sqlite() as conn:
|
|
812
|
+
row = conn.execute(
|
|
813
|
+
"""
|
|
814
|
+
SELECT * FROM memuron_org_spaces
|
|
815
|
+
WHERE org_id = ? AND slug = ?
|
|
816
|
+
""",
|
|
817
|
+
(org_id, slug),
|
|
818
|
+
).fetchone()
|
|
819
|
+
return self._public_space(dict(row)) if row else None
|
|
820
|
+
|
|
821
|
+
def get_default_space(self, org_id: str) -> dict[str, Any] | None:
|
|
822
|
+
spaces = self.ensure_default_spaces(org_id)
|
|
823
|
+
for space in spaces:
|
|
824
|
+
if space["is_default"]:
|
|
825
|
+
return space
|
|
826
|
+
return spaces[0] if spaces else None
|
|
827
|
+
|
|
828
|
+
def set_default_space(self, org_id: str, space_id: str) -> dict[str, Any]:
|
|
829
|
+
space = self.get_space_by_id(space_id)
|
|
830
|
+
if space is None or space["org_id"] != org_id:
|
|
831
|
+
raise ValueError("space not found in organization")
|
|
832
|
+
self._clear_default_space(org_id)
|
|
833
|
+
if self.is_postgres:
|
|
834
|
+
with self._connect_postgres() as conn:
|
|
835
|
+
row = conn.execute(
|
|
836
|
+
"""
|
|
837
|
+
UPDATE memuron_org_spaces
|
|
838
|
+
SET is_default = TRUE
|
|
839
|
+
WHERE id = %s AND org_id = %s
|
|
840
|
+
RETURNING *
|
|
841
|
+
""",
|
|
842
|
+
(space_id, org_id),
|
|
843
|
+
).fetchone()
|
|
844
|
+
conn.commit()
|
|
845
|
+
updated = self._public_space(row)
|
|
846
|
+
self._invalidate_space_cache()
|
|
847
|
+
return updated
|
|
848
|
+
with self._connect_sqlite() as conn:
|
|
849
|
+
conn.execute(
|
|
850
|
+
"""
|
|
851
|
+
UPDATE memuron_org_spaces
|
|
852
|
+
SET is_default = 1
|
|
853
|
+
WHERE id = ? AND org_id = ?
|
|
854
|
+
""",
|
|
855
|
+
(space_id, org_id),
|
|
856
|
+
)
|
|
857
|
+
conn.commit()
|
|
858
|
+
row = conn.execute(
|
|
859
|
+
"SELECT * FROM memuron_org_spaces WHERE id = ?",
|
|
860
|
+
(space_id,),
|
|
861
|
+
).fetchone()
|
|
862
|
+
updated = self._public_space(dict(row))
|
|
863
|
+
self._invalidate_space_cache()
|
|
864
|
+
return updated
|
|
865
|
+
|
|
866
|
+
def update_space(
|
|
867
|
+
self,
|
|
868
|
+
space_id: str,
|
|
869
|
+
*,
|
|
870
|
+
name: str | None = None,
|
|
871
|
+
description: str | None = None,
|
|
872
|
+
guardian_prompt: str | None = None,
|
|
873
|
+
) -> dict[str, Any]:
|
|
874
|
+
space = self.get_space_by_id(space_id)
|
|
875
|
+
if space is None:
|
|
876
|
+
raise ValueError("space not found")
|
|
877
|
+
updates: dict[str, Any] = {}
|
|
878
|
+
if name is not None:
|
|
879
|
+
updates["name"] = name.strip()
|
|
880
|
+
if description is not None:
|
|
881
|
+
updates["description"] = description.strip()
|
|
882
|
+
if guardian_prompt is not None:
|
|
883
|
+
updates["guardian_prompt"] = guardian_prompt.strip()
|
|
884
|
+
if not updates:
|
|
885
|
+
return space
|
|
886
|
+
if self.is_postgres:
|
|
887
|
+
set_clause = ", ".join(f"{key} = %s" for key in updates)
|
|
888
|
+
with self._connect_postgres() as conn:
|
|
889
|
+
row = conn.execute(
|
|
890
|
+
f"UPDATE memuron_org_spaces SET {set_clause} WHERE id = %s RETURNING *",
|
|
891
|
+
(*updates.values(), space_id),
|
|
892
|
+
).fetchone()
|
|
893
|
+
conn.commit()
|
|
894
|
+
updated = self._public_space(row)
|
|
895
|
+
self._invalidate_space_cache()
|
|
896
|
+
return updated
|
|
897
|
+
set_clause = ", ".join(f"{key} = ?" for key in updates)
|
|
898
|
+
with self._connect_sqlite() as conn:
|
|
899
|
+
conn.execute(
|
|
900
|
+
f"UPDATE memuron_org_spaces SET {set_clause} WHERE id = ?",
|
|
901
|
+
(*updates.values(), space_id),
|
|
902
|
+
)
|
|
903
|
+
conn.commit()
|
|
904
|
+
row = conn.execute(
|
|
905
|
+
"SELECT * FROM memuron_org_spaces WHERE id = ?",
|
|
906
|
+
(space_id,),
|
|
907
|
+
).fetchone()
|
|
908
|
+
updated = self._public_space(dict(row))
|
|
909
|
+
self._invalidate_space_cache()
|
|
910
|
+
return updated
|
|
911
|
+
|
|
912
|
+
def dump_state(self) -> str:
|
|
913
|
+
return json.dumps(
|
|
914
|
+
{
|
|
915
|
+
"database_target": self.database_target,
|
|
916
|
+
"is_postgres": self.is_postgres,
|
|
917
|
+
}
|
|
918
|
+
)
|