cinchdb 0.1.17__py3-none-any.whl → 0.1.18__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.
- cinchdb/cli/commands/tenant.py +50 -2
- cinchdb/core/connection.py +55 -18
- cinchdb/core/database.py +24 -9
- cinchdb/core/initializer.py +25 -40
- cinchdb/core/path_utils.py +192 -17
- cinchdb/managers/change_applier.py +1 -1
- cinchdb/managers/codegen.py +372 -15
- cinchdb/managers/column.py +7 -4
- cinchdb/managers/data.py +16 -13
- cinchdb/managers/query.py +8 -5
- cinchdb/managers/table.py +8 -5
- cinchdb/managers/tenant.py +227 -71
- cinchdb/utils/name_validator.py +22 -12
- {cinchdb-0.1.17.dist-info → cinchdb-0.1.18.dist-info}/METADATA +35 -1
- {cinchdb-0.1.17.dist-info → cinchdb-0.1.18.dist-info}/RECORD +18 -18
- {cinchdb-0.1.17.dist-info → cinchdb-0.1.18.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.17.dist-info → cinchdb-0.1.18.dist-info}/entry_points.txt +0 -0
- {cinchdb-0.1.17.dist-info → cinchdb-0.1.18.dist-info}/licenses/LICENSE +0 -0
cinchdb/core/path_utils.py
CHANGED
@@ -1,8 +1,15 @@
|
|
1
1
|
"""Path utilities for CinchDB."""
|
2
2
|
|
3
|
+
import hashlib
|
3
4
|
from pathlib import Path
|
4
|
-
from typing import List
|
5
|
+
from typing import List, Dict, Tuple, Optional
|
5
6
|
from cinchdb.infrastructure.metadata_connection_pool import get_metadata_db
|
7
|
+
from cinchdb.utils.name_validator import validate_name
|
8
|
+
|
9
|
+
# Cache for path calculations to avoid repeated string operations
|
10
|
+
_path_cache: Dict[Tuple[str, str, str], Path] = {}
|
11
|
+
_shard_cache: Dict[str, str] = {}
|
12
|
+
_MAX_CACHE_SIZE = 10000 # Limit cache size to prevent unbounded growth
|
6
13
|
|
7
14
|
|
8
15
|
def get_project_root(start_path: Path) -> Path:
|
@@ -41,7 +48,7 @@ def get_database_path(project_root: Path, database: str) -> Path:
|
|
41
48
|
|
42
49
|
|
43
50
|
def get_branch_path(project_root: Path, database: str, branch: str) -> Path:
|
44
|
-
"""Get path to a branch directory.
|
51
|
+
"""Get path to a branch directory (now uses context root).
|
45
52
|
|
46
53
|
Args:
|
47
54
|
project_root: Project root directory
|
@@ -49,15 +56,16 @@ def get_branch_path(project_root: Path, database: str, branch: str) -> Path:
|
|
49
56
|
branch: Branch name
|
50
57
|
|
51
58
|
Returns:
|
52
|
-
Path to branch directory
|
59
|
+
Path to branch directory (context root in tenant-first structure)
|
53
60
|
"""
|
54
|
-
|
61
|
+
# In tenant-first structure, branch path is the context root
|
62
|
+
return get_context_root(project_root, database, branch)
|
55
63
|
|
56
64
|
|
57
65
|
def get_tenant_path(
|
58
66
|
project_root: Path, database: str, branch: str, tenant: str
|
59
67
|
) -> Path:
|
60
|
-
"""Get path to tenant directory.
|
68
|
+
"""Get path to tenant directory (deprecated, use get_context_root instead).
|
61
69
|
|
62
70
|
Args:
|
63
71
|
project_root: Project root directory
|
@@ -66,9 +74,9 @@ def get_tenant_path(
|
|
66
74
|
tenant: Tenant name
|
67
75
|
|
68
76
|
Returns:
|
69
|
-
Path to
|
77
|
+
Path to context root (no longer has /tenants subfolder)
|
70
78
|
"""
|
71
|
-
return get_branch_path(project_root, database, branch)
|
79
|
+
return get_branch_path(project_root, database, branch)
|
72
80
|
|
73
81
|
|
74
82
|
def get_tenant_db_path(
|
@@ -84,18 +92,20 @@ def get_tenant_db_path(
|
|
84
92
|
|
85
93
|
Returns:
|
86
94
|
Path to tenant database file in sharded directory structure
|
95
|
+
|
96
|
+
Raises:
|
97
|
+
InvalidNameError: If any name contains path traversal characters
|
87
98
|
"""
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
99
|
+
# Validate all names to prevent path traversal
|
100
|
+
# Exception: __empty__ is a system tenant and exempt from validation
|
101
|
+
validate_name(database, "database")
|
102
|
+
validate_name(branch, "branch")
|
103
|
+
if tenant != "__empty__": # System tenant exempt from validation
|
104
|
+
validate_name(tenant, "tenant")
|
93
105
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
return shard_dir / f"{tenant}.db"
|
106
|
+
context_root = get_context_root(project_root, database, branch)
|
107
|
+
shard = calculate_shard(tenant)
|
108
|
+
return context_root / shard / f"{tenant}.db"
|
99
109
|
|
100
110
|
|
101
111
|
def ensure_directory(path: Path) -> None:
|
@@ -171,3 +181,168 @@ def list_tenants(project_root: Path, database: str, branch: str) -> List[str]:
|
|
171
181
|
return []
|
172
182
|
tenant_records = metadata_db.list_tenants(branch_info['id'])
|
173
183
|
return sorted(record['name'] for record in tenant_records)
|
184
|
+
|
185
|
+
|
186
|
+
# New tenant-first path utilities
|
187
|
+
|
188
|
+
def get_context_root(project_root: Path, database: str, branch: str) -> Path:
|
189
|
+
"""Get root directory for a database-branch context with caching.
|
190
|
+
|
191
|
+
This is the new tenant-first approach where each database-branch combination
|
192
|
+
gets its own isolated directory with tenants stored as a flat hierarchy.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
project_root: Project root directory
|
196
|
+
database: Database name
|
197
|
+
branch: Branch name
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
Path to context root directory (e.g., .cinchdb/prod-main/)
|
201
|
+
"""
|
202
|
+
cache_key = (str(project_root), database, branch)
|
203
|
+
|
204
|
+
if cache_key not in _path_cache:
|
205
|
+
# Check cache size and clear if needed
|
206
|
+
if len(_path_cache) >= _MAX_CACHE_SIZE:
|
207
|
+
_path_cache.clear()
|
208
|
+
|
209
|
+
_path_cache[cache_key] = project_root / ".cinchdb" / f"{database}-{branch}"
|
210
|
+
|
211
|
+
return _path_cache[cache_key]
|
212
|
+
|
213
|
+
|
214
|
+
def calculate_shard(tenant_name: str) -> str:
|
215
|
+
"""Calculate the shard directory for a tenant using SHA256 hash with caching.
|
216
|
+
|
217
|
+
Args:
|
218
|
+
tenant_name: Name of the tenant
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
Two-character hex string (e.g., "a0", "ff")
|
222
|
+
"""
|
223
|
+
if tenant_name not in _shard_cache:
|
224
|
+
# Check cache size and clear if needed
|
225
|
+
if len(_shard_cache) >= _MAX_CACHE_SIZE:
|
226
|
+
_shard_cache.clear()
|
227
|
+
|
228
|
+
hash_val = hashlib.sha256(tenant_name.encode('utf-8')).hexdigest()
|
229
|
+
_shard_cache[tenant_name] = hash_val[:2]
|
230
|
+
|
231
|
+
return _shard_cache[tenant_name]
|
232
|
+
|
233
|
+
|
234
|
+
def get_tenant_db_path_in_context(context_root: Path, tenant: str) -> Path:
|
235
|
+
"""Get tenant DB path within a context root.
|
236
|
+
|
237
|
+
Uses the tenant-first storage approach where tenants are stored as:
|
238
|
+
{context_root}/{shard}/{tenant}.db
|
239
|
+
|
240
|
+
Args:
|
241
|
+
context_root: Context root directory (from get_context_root)
|
242
|
+
tenant: Tenant name
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
Path to tenant database file within the context
|
246
|
+
"""
|
247
|
+
shard = calculate_shard(tenant)
|
248
|
+
return context_root / shard / f"{tenant}.db"
|
249
|
+
|
250
|
+
|
251
|
+
|
252
|
+
|
253
|
+
def ensure_context_directory(project_root: Path, database: str, branch: str) -> Path:
|
254
|
+
"""Ensure context root directory exists and return it.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
project_root: Project root directory
|
258
|
+
database: Database name
|
259
|
+
branch: Branch name
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
Path to context root directory (created if necessary)
|
263
|
+
"""
|
264
|
+
context_root = get_context_root(project_root, database, branch)
|
265
|
+
context_root.mkdir(parents=True, exist_ok=True)
|
266
|
+
|
267
|
+
return context_root
|
268
|
+
|
269
|
+
|
270
|
+
def ensure_tenant_db_path(project_root: Path, database: str, branch: str, tenant: str) -> Path:
|
271
|
+
"""Ensure tenant database path exists (creates shard directory if needed).
|
272
|
+
|
273
|
+
This is the ONLY place where tenant shard directories should be created,
|
274
|
+
ensuring we have a single source of truth for the directory structure.
|
275
|
+
|
276
|
+
Args:
|
277
|
+
project_root: Project root directory
|
278
|
+
database: Database name
|
279
|
+
branch: Branch name
|
280
|
+
tenant: Tenant name
|
281
|
+
|
282
|
+
Returns:
|
283
|
+
Path to tenant database file (directory created if necessary)
|
284
|
+
|
285
|
+
Raises:
|
286
|
+
InvalidNameError: If any name contains path traversal characters
|
287
|
+
"""
|
288
|
+
# Validation happens inside get_tenant_db_path
|
289
|
+
db_path = get_tenant_db_path(project_root, database, branch, tenant)
|
290
|
+
|
291
|
+
# Ensure the shard directory exists
|
292
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
293
|
+
|
294
|
+
return db_path
|
295
|
+
|
296
|
+
|
297
|
+
def invalidate_cache(database: Optional[str] = None,
|
298
|
+
branch: Optional[str] = None,
|
299
|
+
tenant: Optional[str] = None) -> None:
|
300
|
+
"""Invalidate cache entries (write-through on delete operations).
|
301
|
+
|
302
|
+
This function is called when databases, branches, or tenants are deleted
|
303
|
+
to ensure cached paths don't point to non-existent resources.
|
304
|
+
|
305
|
+
Args:
|
306
|
+
database: Database name to invalidate (invalidates all branches/tenants if provided)
|
307
|
+
branch: Branch name to invalidate (requires database)
|
308
|
+
tenant: Tenant name to invalidate (only invalidates shard cache)
|
309
|
+
"""
|
310
|
+
global _path_cache, _shard_cache
|
311
|
+
|
312
|
+
if database:
|
313
|
+
# Remove all cache entries for this database
|
314
|
+
keys_to_remove = []
|
315
|
+
for key in _path_cache:
|
316
|
+
# key is (project_root, database, branch)
|
317
|
+
if key[1] == database:
|
318
|
+
if branch is None or key[2] == branch:
|
319
|
+
keys_to_remove.append(key)
|
320
|
+
|
321
|
+
for key in keys_to_remove:
|
322
|
+
del _path_cache[key]
|
323
|
+
|
324
|
+
if tenant and tenant in _shard_cache:
|
325
|
+
del _shard_cache[tenant]
|
326
|
+
|
327
|
+
|
328
|
+
def clear_all_caches() -> None:
|
329
|
+
"""Clear all path and shard caches.
|
330
|
+
|
331
|
+
Useful for testing or when major structural changes occur.
|
332
|
+
"""
|
333
|
+
global _path_cache, _shard_cache
|
334
|
+
_path_cache.clear()
|
335
|
+
_shard_cache.clear()
|
336
|
+
|
337
|
+
|
338
|
+
def get_cache_stats() -> Dict[str, int]:
|
339
|
+
"""Get statistics about cache usage.
|
340
|
+
|
341
|
+
Returns:
|
342
|
+
Dictionary with cache statistics
|
343
|
+
"""
|
344
|
+
return {
|
345
|
+
"path_cache_size": len(_path_cache),
|
346
|
+
"shard_cache_size": len(_shard_cache),
|
347
|
+
"max_cache_size": _MAX_CACHE_SIZE
|
348
|
+
}
|
@@ -10,7 +10,7 @@ from cinchdb.models import Change, ChangeType, Tenant
|
|
10
10
|
from cinchdb.managers.change_tracker import ChangeTracker
|
11
11
|
from cinchdb.managers.tenant import TenantManager
|
12
12
|
from cinchdb.core.connection import DatabaseConnection
|
13
|
-
from cinchdb.core.path_utils import get_tenant_db_path, get_branch_path
|
13
|
+
from cinchdb.core.path_utils import get_tenant_db_path as get_tenant_db_path, get_branch_path
|
14
14
|
from cinchdb.infrastructure.metadata_connection_pool import get_metadata_db
|
15
15
|
|
16
16
|
logger = logging.getLogger(__name__)
|
cinchdb/managers/codegen.py
CHANGED
@@ -379,21 +379,58 @@ class CodegenManager:
|
|
379
379
|
results: Dict[str, Any],
|
380
380
|
) -> Dict[str, Any]:
|
381
381
|
"""Generate TypeScript interface models."""
|
382
|
-
#
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
)
|
395
|
-
|
396
|
-
|
382
|
+
# Generate interfaces for tables
|
383
|
+
if include_tables:
|
384
|
+
tables = self.table_manager.list_tables()
|
385
|
+
for table in tables:
|
386
|
+
interface_content = self._generate_typescript_table_interface(table)
|
387
|
+
file_name = f"{self._to_pascal_case(table.name)}.ts"
|
388
|
+
file_path = output_dir / file_name
|
389
|
+
|
390
|
+
with open(file_path, "w") as f:
|
391
|
+
f.write(interface_content)
|
392
|
+
|
393
|
+
results["tables_processed"].append(table.name)
|
394
|
+
results["files_generated"].append(file_name)
|
395
|
+
|
396
|
+
# Generate interfaces for views
|
397
|
+
if include_views:
|
398
|
+
views = self.view_manager.list_views()
|
399
|
+
for view in views:
|
400
|
+
interface_content = self._generate_typescript_view_interface(view)
|
401
|
+
file_name = f"{self._to_pascal_case(view.name)}View.ts"
|
402
|
+
file_path = output_dir / file_name
|
403
|
+
|
404
|
+
with open(file_path, "w") as f:
|
405
|
+
f.write(interface_content)
|
406
|
+
|
407
|
+
results["views_processed"].append(view.name)
|
408
|
+
results["files_generated"].append(file_name)
|
409
|
+
|
410
|
+
# Generate main index file
|
411
|
+
index_content = self._generate_typescript_index(
|
412
|
+
results["tables_processed"],
|
413
|
+
results["views_processed"]
|
414
|
+
)
|
415
|
+
index_path = output_dir / "index.ts"
|
416
|
+
with open(index_path, "w") as f:
|
417
|
+
f.write(index_content)
|
418
|
+
results["files_generated"].append("index.ts")
|
419
|
+
|
420
|
+
# Generate API client
|
421
|
+
client_content = self._generate_typescript_client()
|
422
|
+
client_path = output_dir / "client.ts"
|
423
|
+
with open(client_path, "w") as f:
|
424
|
+
f.write(client_content)
|
425
|
+
results["files_generated"].append("client.ts")
|
426
|
+
|
427
|
+
# Generate types file
|
428
|
+
types_content = self._generate_typescript_types()
|
429
|
+
types_path = output_dir / "types.ts"
|
430
|
+
with open(types_path, "w") as f:
|
431
|
+
f.write(types_content)
|
432
|
+
results["files_generated"].append("types.ts")
|
433
|
+
|
397
434
|
return results
|
398
435
|
|
399
436
|
def _get_view_columns(self, view_name: str) -> Dict[str, str]:
|
@@ -454,6 +491,326 @@ class CodegenManager:
|
|
454
491
|
# Split on underscores and capitalize each part
|
455
492
|
parts = name.replace("-", "_").split("_")
|
456
493
|
return "".join(word.capitalize() for word in parts if word)
|
494
|
+
|
495
|
+
def _sqlite_to_typescript_type(self, sqlite_type: str, column_name: str = "") -> str:
|
496
|
+
"""Convert SQLite type to TypeScript type string."""
|
497
|
+
sqlite_type = sqlite_type.upper()
|
498
|
+
|
499
|
+
# Special case for timestamp fields
|
500
|
+
if column_name in ["created_at", "updated_at"]:
|
501
|
+
return "string" # ISO date strings
|
502
|
+
|
503
|
+
if "INT" in sqlite_type:
|
504
|
+
return "number"
|
505
|
+
elif sqlite_type in ["REAL", "FLOAT", "DOUBLE", "NUMERIC"]:
|
506
|
+
return "number"
|
507
|
+
elif sqlite_type == "BLOB":
|
508
|
+
return "Uint8Array"
|
509
|
+
elif sqlite_type == "BOOLEAN":
|
510
|
+
return "boolean"
|
511
|
+
else:
|
512
|
+
# TEXT, VARCHAR, etc.
|
513
|
+
return "string"
|
514
|
+
|
515
|
+
def _generate_typescript_table_interface(self, table: Table) -> str:
|
516
|
+
"""Generate TypeScript interface for a table."""
|
517
|
+
interface_name = self._to_pascal_case(table.name)
|
518
|
+
|
519
|
+
content = [
|
520
|
+
f"/**",
|
521
|
+
f" * Generated interface for {table.name} table",
|
522
|
+
f" */",
|
523
|
+
f"export interface {interface_name} {{",
|
524
|
+
]
|
525
|
+
|
526
|
+
# Generate properties for each column
|
527
|
+
for column in table.columns:
|
528
|
+
ts_type = self._sqlite_to_typescript_type(column.type, column.name)
|
529
|
+
optional = "?" if column.nullable and column.name not in ["id", "created_at"] else ""
|
530
|
+
content.append(f" {column.name}{optional}: {ts_type};")
|
531
|
+
|
532
|
+
content.append("}")
|
533
|
+
content.append("")
|
534
|
+
|
535
|
+
# Generate input interface (for create/update operations)
|
536
|
+
content.extend([
|
537
|
+
f"/**",
|
538
|
+
f" * Input interface for creating/updating {table.name} records",
|
539
|
+
f" */",
|
540
|
+
f"export interface {interface_name}Input {{",
|
541
|
+
])
|
542
|
+
|
543
|
+
for column in table.columns:
|
544
|
+
# Skip auto-generated fields in input
|
545
|
+
if column.name in ["id", "created_at", "updated_at"]:
|
546
|
+
continue
|
547
|
+
ts_type = self._sqlite_to_typescript_type(column.type, column.name)
|
548
|
+
optional = "?" if column.nullable else ""
|
549
|
+
content.append(f" {column.name}{optional}: {ts_type};")
|
550
|
+
|
551
|
+
content.append("}")
|
552
|
+
content.append("")
|
553
|
+
|
554
|
+
return "\n".join(content)
|
555
|
+
|
556
|
+
def _generate_typescript_view_interface(self, view: View) -> str:
|
557
|
+
"""Generate TypeScript interface for a view."""
|
558
|
+
interface_name = f"{self._to_pascal_case(view.name)}View"
|
559
|
+
|
560
|
+
# Get view columns from PRAGMA
|
561
|
+
columns = self._get_view_columns(view.name)
|
562
|
+
|
563
|
+
content = [
|
564
|
+
f"/**",
|
565
|
+
f" * Generated interface for {view.name} view (read-only)",
|
566
|
+
f" */",
|
567
|
+
f"export interface {interface_name} {{",
|
568
|
+
]
|
569
|
+
|
570
|
+
# Generate properties for each column
|
571
|
+
for column_name, column_type in columns.items():
|
572
|
+
ts_type = self._sqlite_to_typescript_type(column_type, column_name)
|
573
|
+
content.append(f" {column_name}: {ts_type};")
|
574
|
+
|
575
|
+
content.append("}")
|
576
|
+
content.append("")
|
577
|
+
|
578
|
+
return "\n".join(content)
|
579
|
+
|
580
|
+
def _generate_typescript_index(self, tables: List[str], views: List[str]) -> str:
|
581
|
+
"""Generate TypeScript index file exporting all interfaces."""
|
582
|
+
content = [
|
583
|
+
"/**",
|
584
|
+
" * Generated TypeScript models for CinchDB",
|
585
|
+
" */",
|
586
|
+
"",
|
587
|
+
]
|
588
|
+
|
589
|
+
# Export tables
|
590
|
+
for table_name in tables:
|
591
|
+
interface_name = self._to_pascal_case(table_name)
|
592
|
+
content.append(f"export {{ {interface_name}, {interface_name}Input }} from './{interface_name}';")
|
593
|
+
|
594
|
+
# Export views
|
595
|
+
for view_name in views:
|
596
|
+
interface_name = f"{self._to_pascal_case(view_name)}View"
|
597
|
+
content.append(f"export {{ {interface_name} }} from './{interface_name}';")
|
598
|
+
|
599
|
+
# Export client and types
|
600
|
+
content.extend([
|
601
|
+
"",
|
602
|
+
"export { CinchDBClient } from './client';",
|
603
|
+
"export * from './types';",
|
604
|
+
"",
|
605
|
+
])
|
606
|
+
|
607
|
+
return "\n".join(content)
|
608
|
+
|
609
|
+
def _generate_typescript_client(self) -> str:
|
610
|
+
"""Generate TypeScript API client class."""
|
611
|
+
content = [
|
612
|
+
"/**",
|
613
|
+
" * CinchDB TypeScript API Client",
|
614
|
+
" */",
|
615
|
+
"",
|
616
|
+
"import { QueryResult, CreateResult, UpdateResult, DeleteResult } from './types';",
|
617
|
+
"",
|
618
|
+
"export class CinchDBClient {",
|
619
|
+
" private baseUrl: string;",
|
620
|
+
" private headers: HeadersInit;",
|
621
|
+
"",
|
622
|
+
" constructor(baseUrl: string, apiKey: string) {",
|
623
|
+
" this.baseUrl = baseUrl;",
|
624
|
+
" this.headers = {",
|
625
|
+
" 'Content-Type': 'application/json',",
|
626
|
+
" 'X-API-Key': apiKey,",
|
627
|
+
" };",
|
628
|
+
" }",
|
629
|
+
"",
|
630
|
+
" /**",
|
631
|
+
" * Execute a query against the database",
|
632
|
+
" */",
|
633
|
+
" async query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {",
|
634
|
+
" const response = await fetch(`${this.baseUrl}/api/v1/query`, {",
|
635
|
+
" method: 'POST',",
|
636
|
+
" headers: this.headers,",
|
637
|
+
" body: JSON.stringify({ sql, params }),",
|
638
|
+
" });",
|
639
|
+
"",
|
640
|
+
" if (!response.ok) {",
|
641
|
+
" throw new Error(`Query failed: ${response.statusText}`);",
|
642
|
+
" }",
|
643
|
+
"",
|
644
|
+
" return response.json();",
|
645
|
+
" }",
|
646
|
+
"",
|
647
|
+
" /**",
|
648
|
+
" * Select records from a table",
|
649
|
+
" */",
|
650
|
+
" async select<T = any>(",
|
651
|
+
" table: string,",
|
652
|
+
" filters?: Record<string, any>,",
|
653
|
+
" limit?: number,",
|
654
|
+
" offset?: number",
|
655
|
+
" ): Promise<T[]> {",
|
656
|
+
" const params = new URLSearchParams();",
|
657
|
+
" if (filters) {",
|
658
|
+
" Object.entries(filters).forEach(([key, value]) => {",
|
659
|
+
" params.append(key, String(value));",
|
660
|
+
" });",
|
661
|
+
" }",
|
662
|
+
" if (limit !== undefined) params.append('limit', String(limit));",
|
663
|
+
" if (offset !== undefined) params.append('offset', String(offset));",
|
664
|
+
"",
|
665
|
+
" const response = await fetch(",
|
666
|
+
" `${this.baseUrl}/api/v1/tables/${table}/records?${params}`,",
|
667
|
+
" {",
|
668
|
+
" method: 'GET',",
|
669
|
+
" headers: this.headers,",
|
670
|
+
" }",
|
671
|
+
" );",
|
672
|
+
"",
|
673
|
+
" if (!response.ok) {",
|
674
|
+
" throw new Error(`Select failed: ${response.statusText}`);",
|
675
|
+
" }",
|
676
|
+
"",
|
677
|
+
" const result = await response.json();",
|
678
|
+
" return result.records;",
|
679
|
+
" }",
|
680
|
+
"",
|
681
|
+
" /**",
|
682
|
+
" * Create a new record",
|
683
|
+
" */",
|
684
|
+
" async create<T = any>(table: string, data: Partial<T>): Promise<CreateResult<T>> {",
|
685
|
+
" const response = await fetch(`${this.baseUrl}/api/v1/tables/${table}/records`, {",
|
686
|
+
" method: 'POST',",
|
687
|
+
" headers: this.headers,",
|
688
|
+
" body: JSON.stringify(data),",
|
689
|
+
" });",
|
690
|
+
"",
|
691
|
+
" if (!response.ok) {",
|
692
|
+
" throw new Error(`Create failed: ${response.statusText}`);",
|
693
|
+
" }",
|
694
|
+
"",
|
695
|
+
" return response.json();",
|
696
|
+
" }",
|
697
|
+
"",
|
698
|
+
" /**",
|
699
|
+
" * Update a record by ID",
|
700
|
+
" */",
|
701
|
+
" async update<T = any>(",
|
702
|
+
" table: string,",
|
703
|
+
" id: string,",
|
704
|
+
" data: Partial<T>",
|
705
|
+
" ): Promise<UpdateResult<T>> {",
|
706
|
+
" const response = await fetch(",
|
707
|
+
" `${this.baseUrl}/api/v1/tables/${table}/records/${id}`,",
|
708
|
+
" {",
|
709
|
+
" method: 'PUT',",
|
710
|
+
" headers: this.headers,",
|
711
|
+
" body: JSON.stringify(data),",
|
712
|
+
" }",
|
713
|
+
" );",
|
714
|
+
"",
|
715
|
+
" if (!response.ok) {",
|
716
|
+
" throw new Error(`Update failed: ${response.statusText}`);",
|
717
|
+
" }",
|
718
|
+
"",
|
719
|
+
" return response.json();",
|
720
|
+
" }",
|
721
|
+
"",
|
722
|
+
" /**",
|
723
|
+
" * Delete a record by ID",
|
724
|
+
" */",
|
725
|
+
" async delete(table: string, id: string): Promise<DeleteResult> {",
|
726
|
+
" const response = await fetch(",
|
727
|
+
" `${this.baseUrl}/api/v1/tables/${table}/records/${id}`,",
|
728
|
+
" {",
|
729
|
+
" method: 'DELETE',",
|
730
|
+
" headers: this.headers,",
|
731
|
+
" }",
|
732
|
+
" );",
|
733
|
+
"",
|
734
|
+
" if (!response.ok) {",
|
735
|
+
" throw new Error(`Delete failed: ${response.statusText}`);",
|
736
|
+
" }",
|
737
|
+
"",
|
738
|
+
" return response.json();",
|
739
|
+
" }",
|
740
|
+
"",
|
741
|
+
" /**",
|
742
|
+
" * Bulk create multiple records",
|
743
|
+
" */",
|
744
|
+
" async bulkCreate<T = any>(",
|
745
|
+
" table: string,",
|
746
|
+
" records: Partial<T>[]",
|
747
|
+
" ): Promise<CreateResult<T>[]> {",
|
748
|
+
" const response = await fetch(",
|
749
|
+
" `${this.baseUrl}/api/v1/tables/${table}/records/bulk`,",
|
750
|
+
" {",
|
751
|
+
" method: 'POST',",
|
752
|
+
" headers: this.headers,",
|
753
|
+
" body: JSON.stringify({ records }),",
|
754
|
+
" }",
|
755
|
+
" );",
|
756
|
+
"",
|
757
|
+
" if (!response.ok) {",
|
758
|
+
" throw new Error(`Bulk create failed: ${response.statusText}`);",
|
759
|
+
" }",
|
760
|
+
"",
|
761
|
+
" return response.json();",
|
762
|
+
" }",
|
763
|
+
"}",
|
764
|
+
"",
|
765
|
+
]
|
766
|
+
|
767
|
+
return "\n".join(content)
|
768
|
+
|
769
|
+
def _generate_typescript_types(self) -> str:
|
770
|
+
"""Generate TypeScript type definitions."""
|
771
|
+
content = [
|
772
|
+
"/**",
|
773
|
+
" * Common TypeScript type definitions for CinchDB",
|
774
|
+
" */",
|
775
|
+
"",
|
776
|
+
"export interface QueryResult<T = any> {",
|
777
|
+
" success: boolean;",
|
778
|
+
" data: T[];",
|
779
|
+
" rowCount: number;",
|
780
|
+
" error?: string;",
|
781
|
+
"}",
|
782
|
+
"",
|
783
|
+
"export interface CreateResult<T = any> {",
|
784
|
+
" success: boolean;",
|
785
|
+
" data: T;",
|
786
|
+
" error?: string;",
|
787
|
+
"}",
|
788
|
+
"",
|
789
|
+
"export interface UpdateResult<T = any> {",
|
790
|
+
" success: boolean;",
|
791
|
+
" data: T;",
|
792
|
+
" rowsAffected: number;",
|
793
|
+
" error?: string;",
|
794
|
+
"}",
|
795
|
+
"",
|
796
|
+
"export interface DeleteResult {",
|
797
|
+
" success: boolean;",
|
798
|
+
" rowsAffected: number;",
|
799
|
+
" error?: string;",
|
800
|
+
"}",
|
801
|
+
"",
|
802
|
+
"export interface PaginationParams {",
|
803
|
+
" limit?: number;",
|
804
|
+
" offset?: number;",
|
805
|
+
"}",
|
806
|
+
"",
|
807
|
+
"export interface FilterParams {",
|
808
|
+
" [key: string]: any;",
|
809
|
+
"}",
|
810
|
+
"",
|
811
|
+
]
|
812
|
+
|
813
|
+
return "\n".join(content)
|
457
814
|
|
458
815
|
def _generate_cinch_models_class(self) -> str:
|
459
816
|
"""Generate the CinchModels container class."""
|