@intentsolutionsio/nosql-data-modeler 1.0.0
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.
- package/.claude-plugin/plugin.json +17 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/agents/nosql-agent.md +36 -0
- package/package.json +38 -0
- package/skills/modeling-nosql-data/SKILL.md +86 -0
- package/skills/modeling-nosql-data/assets/README.md +7 -0
- package/skills/modeling-nosql-data/references/README.md +4 -0
- package/skills/modeling-nosql-data/scripts/README.md +7 -0
- package/skills/modeling-nosql-data/scripts/generate_sample_data.py +391 -0
- package/skills/modeling-nosql-data/scripts/migrate_schema.py +455 -0
- package/skills/modeling-nosql-data/scripts/validate_schema.py +492 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Migrates schema from one NoSQL database type to another.
|
|
4
|
+
|
|
5
|
+
This script converts schema definitions between different NoSQL database formats
|
|
6
|
+
(MongoDB, DynamoDB, Firebase, Firestore, etc.) while preserving semantics.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Any, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SchemaTransformer:
|
|
18
|
+
"""Transforms schemas between NoSQL database types."""
|
|
19
|
+
|
|
20
|
+
# Database schema patterns
|
|
21
|
+
DB_PATTERNS = {
|
|
22
|
+
"mongodb": {
|
|
23
|
+
"name": "MongoDB",
|
|
24
|
+
"root": "properties",
|
|
25
|
+
"type_mapping": {
|
|
26
|
+
"string": "string",
|
|
27
|
+
"number": "double",
|
|
28
|
+
"integer": "int",
|
|
29
|
+
"boolean": "boolean",
|
|
30
|
+
"date": "date",
|
|
31
|
+
"object": "object",
|
|
32
|
+
"array": "array",
|
|
33
|
+
"null": "null"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"dynamodb": {
|
|
37
|
+
"name": "DynamoDB",
|
|
38
|
+
"root": "AttributeDefinitions",
|
|
39
|
+
"type_mapping": {
|
|
40
|
+
"string": "S",
|
|
41
|
+
"number": "N",
|
|
42
|
+
"integer": "N",
|
|
43
|
+
"boolean": "BOOL",
|
|
44
|
+
"date": "S",
|
|
45
|
+
"object": "M",
|
|
46
|
+
"array": "L",
|
|
47
|
+
"binary": "B"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"firestore": {
|
|
51
|
+
"name": "Firestore",
|
|
52
|
+
"root": "fields",
|
|
53
|
+
"type_mapping": {
|
|
54
|
+
"string": "stringValue",
|
|
55
|
+
"number": "doubleValue",
|
|
56
|
+
"integer": "integerValue",
|
|
57
|
+
"boolean": "booleanValue",
|
|
58
|
+
"date": "timestampValue",
|
|
59
|
+
"object": "mapValue",
|
|
60
|
+
"array": "arrayValue",
|
|
61
|
+
"null": "nullValue"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"cosmosdb": {
|
|
65
|
+
"name": "Azure Cosmos DB",
|
|
66
|
+
"root": "properties",
|
|
67
|
+
"type_mapping": {
|
|
68
|
+
"string": "string",
|
|
69
|
+
"number": "number",
|
|
70
|
+
"integer": "integer",
|
|
71
|
+
"boolean": "boolean",
|
|
72
|
+
"date": "date",
|
|
73
|
+
"object": "object",
|
|
74
|
+
"array": "array"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def __init__(self, source_type: str, target_type: str):
|
|
80
|
+
"""
|
|
81
|
+
Initialize transformer.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
source_type: Source database type
|
|
85
|
+
target_type: Target database type
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ValueError: If database types are not supported
|
|
89
|
+
"""
|
|
90
|
+
if source_type not in self.DB_PATTERNS:
|
|
91
|
+
raise ValueError(f"Unsupported source type: {source_type}")
|
|
92
|
+
if target_type not in self.DB_PATTERNS:
|
|
93
|
+
raise ValueError(f"Unsupported target type: {target_type}")
|
|
94
|
+
|
|
95
|
+
self.source_type = source_type
|
|
96
|
+
self.target_type = target_type
|
|
97
|
+
self.warnings = []
|
|
98
|
+
|
|
99
|
+
def transform(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Transform schema from source to target format.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
schema: Schema in source format
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Schema in target format
|
|
108
|
+
"""
|
|
109
|
+
self.warnings = []
|
|
110
|
+
|
|
111
|
+
# Parse source schema
|
|
112
|
+
parsed = self._parse_source_schema(schema)
|
|
113
|
+
|
|
114
|
+
# Transform to target format
|
|
115
|
+
transformed = self._transform_to_target(parsed)
|
|
116
|
+
|
|
117
|
+
return transformed
|
|
118
|
+
|
|
119
|
+
def _parse_source_schema(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
120
|
+
"""
|
|
121
|
+
Parse source schema into normalized format.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
schema: Source schema
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Normalized schema representation
|
|
128
|
+
"""
|
|
129
|
+
parsed = {
|
|
130
|
+
"name": schema.get("$id", schema.get("title", "Schema")),
|
|
131
|
+
"description": schema.get("description", ""),
|
|
132
|
+
"fields": {}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Extract fields based on source type
|
|
136
|
+
if self.source_type == "mongodb":
|
|
137
|
+
if "properties" in schema:
|
|
138
|
+
parsed["fields"] = self._extract_mongodb_fields(schema["properties"])
|
|
139
|
+
|
|
140
|
+
elif self.source_type == "dynamodb":
|
|
141
|
+
parsed["fields"] = self._extract_dynamodb_fields(schema)
|
|
142
|
+
|
|
143
|
+
elif self.source_type == "firestore":
|
|
144
|
+
parsed["fields"] = self._extract_firestore_fields(schema)
|
|
145
|
+
|
|
146
|
+
elif self.source_type == "cosmosdb":
|
|
147
|
+
if "properties" in schema:
|
|
148
|
+
parsed["fields"] = self._extract_mongodb_fields(schema["properties"])
|
|
149
|
+
|
|
150
|
+
return parsed
|
|
151
|
+
|
|
152
|
+
def _extract_mongodb_fields(self, properties: Dict[str, Any]) -> Dict[str, Any]:
|
|
153
|
+
"""Extract fields from MongoDB schema."""
|
|
154
|
+
fields = {}
|
|
155
|
+
|
|
156
|
+
for field_name, field_def in properties.items():
|
|
157
|
+
fields[field_name] = {
|
|
158
|
+
"type": field_def.get("type", "string"),
|
|
159
|
+
"description": field_def.get("description", ""),
|
|
160
|
+
"required": "required" in str(field_def),
|
|
161
|
+
"indexed": field_def.get("indexed", False)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return fields
|
|
165
|
+
|
|
166
|
+
def _extract_dynamodb_fields(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
167
|
+
"""Extract fields from DynamoDB schema."""
|
|
168
|
+
fields = {}
|
|
169
|
+
|
|
170
|
+
# DynamoDB representation varies; handle common patterns
|
|
171
|
+
if "AttributeDefinitions" in schema:
|
|
172
|
+
for attr in schema["AttributeDefinitions"]:
|
|
173
|
+
fields[attr["AttributeName"]] = {
|
|
174
|
+
"type": self._map_dynamodb_type(attr["AttributeType"]),
|
|
175
|
+
"required": attr["AttributeName"] in schema.get("KeySchema", [])
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return fields
|
|
179
|
+
|
|
180
|
+
def _extract_firestore_fields(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
181
|
+
"""Extract fields from Firestore schema."""
|
|
182
|
+
fields = {}
|
|
183
|
+
|
|
184
|
+
if "fields" in schema:
|
|
185
|
+
for field_name, field_def in schema["fields"].items():
|
|
186
|
+
field_type = self._extract_firestore_type(field_def)
|
|
187
|
+
fields[field_name] = {
|
|
188
|
+
"type": field_type,
|
|
189
|
+
"indexed": field_def.get("indexed", False)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return fields
|
|
193
|
+
|
|
194
|
+
def _transform_to_target(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
|
|
195
|
+
"""Transform parsed schema to target format."""
|
|
196
|
+
if self.target_type == "mongodb":
|
|
197
|
+
return self._transform_to_mongodb(parsed)
|
|
198
|
+
elif self.target_type == "dynamodb":
|
|
199
|
+
return self._transform_to_dynamodb(parsed)
|
|
200
|
+
elif self.target_type == "firestore":
|
|
201
|
+
return self._transform_to_firestore(parsed)
|
|
202
|
+
elif self.target_type == "cosmosdb":
|
|
203
|
+
return self._transform_to_cosmosdb(parsed)
|
|
204
|
+
|
|
205
|
+
return parsed
|
|
206
|
+
|
|
207
|
+
def _transform_to_mongodb(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
|
|
208
|
+
"""Transform to MongoDB schema format."""
|
|
209
|
+
properties = {}
|
|
210
|
+
|
|
211
|
+
for field_name, field_info in parsed["fields"].items():
|
|
212
|
+
properties[field_name] = {
|
|
213
|
+
"type": field_info.get("type", "string"),
|
|
214
|
+
"description": field_info.get("description", "")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if field_info.get("indexed"):
|
|
218
|
+
properties[field_name]["indexed"] = True
|
|
219
|
+
|
|
220
|
+
schema = {
|
|
221
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
222
|
+
"$id": parsed["name"],
|
|
223
|
+
"title": parsed["name"],
|
|
224
|
+
"type": "object",
|
|
225
|
+
"properties": properties
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return schema
|
|
229
|
+
|
|
230
|
+
def _transform_to_dynamodb(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
|
|
231
|
+
"""Transform to DynamoDB schema format."""
|
|
232
|
+
attributes = []
|
|
233
|
+
|
|
234
|
+
for field_name, field_info in parsed["fields"].items():
|
|
235
|
+
field_type = field_info.get("type", "string")
|
|
236
|
+
dynamo_type = self._map_type_to_dynamodb(field_type)
|
|
237
|
+
|
|
238
|
+
attributes.append({
|
|
239
|
+
"AttributeName": field_name,
|
|
240
|
+
"AttributeType": dynamo_type
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
schema = {
|
|
244
|
+
"TableName": parsed["name"],
|
|
245
|
+
"AttributeDefinitions": attributes,
|
|
246
|
+
"KeySchema": [
|
|
247
|
+
{"AttributeName": "id", "KeyType": "HASH"}
|
|
248
|
+
],
|
|
249
|
+
"BillingMode": "PAY_PER_REQUEST"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return schema
|
|
253
|
+
|
|
254
|
+
def _transform_to_firestore(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
|
|
255
|
+
"""Transform to Firestore schema format."""
|
|
256
|
+
fields = {}
|
|
257
|
+
|
|
258
|
+
for field_name, field_info in parsed["fields"].items():
|
|
259
|
+
field_type = field_info.get("type", "string")
|
|
260
|
+
|
|
261
|
+
fields[field_name] = {
|
|
262
|
+
field_type + "Value": self._get_firestore_default(field_type)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if field_info.get("indexed"):
|
|
266
|
+
fields[field_name]["indexed"] = True
|
|
267
|
+
|
|
268
|
+
schema = {
|
|
269
|
+
"name": f"projects/PROJECT_ID/databases/(default)/documents/{parsed['name']}",
|
|
270
|
+
"fields": fields
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return schema
|
|
274
|
+
|
|
275
|
+
def _transform_to_cosmosdb(self, parsed: Dict[str, Any]) -> Dict[str, Any]:
|
|
276
|
+
"""Transform to Cosmos DB schema format (similar to MongoDB)."""
|
|
277
|
+
return self._transform_to_mongodb(parsed)
|
|
278
|
+
|
|
279
|
+
def _map_type_to_dynamodb(self, source_type: str) -> str:
|
|
280
|
+
"""Map source type to DynamoDB type."""
|
|
281
|
+
type_map = {
|
|
282
|
+
"string": "S",
|
|
283
|
+
"number": "N",
|
|
284
|
+
"integer": "N",
|
|
285
|
+
"boolean": "BOOL",
|
|
286
|
+
"date": "S",
|
|
287
|
+
"object": "M",
|
|
288
|
+
"array": "L"
|
|
289
|
+
}
|
|
290
|
+
return type_map.get(source_type, "S")
|
|
291
|
+
|
|
292
|
+
def _map_dynamodb_type(self, dynamo_type: str) -> str:
|
|
293
|
+
"""Map DynamoDB type to normalized type."""
|
|
294
|
+
type_map = {
|
|
295
|
+
"S": "string",
|
|
296
|
+
"N": "number",
|
|
297
|
+
"B": "binary",
|
|
298
|
+
"SS": "string",
|
|
299
|
+
"NS": "number",
|
|
300
|
+
"BS": "binary",
|
|
301
|
+
"M": "object",
|
|
302
|
+
"L": "array",
|
|
303
|
+
"BOOL": "boolean"
|
|
304
|
+
}
|
|
305
|
+
return type_map.get(dynamo_type, "string")
|
|
306
|
+
|
|
307
|
+
def _extract_firestore_type(self, field_def: Dict) -> str:
|
|
308
|
+
"""Extract type from Firestore field definition."""
|
|
309
|
+
for key in field_def:
|
|
310
|
+
if key.endswith("Value"):
|
|
311
|
+
type_name = key.replace("Value", "")
|
|
312
|
+
type_map = {
|
|
313
|
+
"string": "string",
|
|
314
|
+
"double": "number",
|
|
315
|
+
"integer": "integer",
|
|
316
|
+
"boolean": "boolean",
|
|
317
|
+
"timestamp": "date",
|
|
318
|
+
"map": "object",
|
|
319
|
+
"array": "array"
|
|
320
|
+
}
|
|
321
|
+
return type_map.get(type_name, "string")
|
|
322
|
+
|
|
323
|
+
return "string"
|
|
324
|
+
|
|
325
|
+
def _get_firestore_default(self, field_type: str) -> Any:
|
|
326
|
+
"""Get default value for Firestore field type."""
|
|
327
|
+
defaults = {
|
|
328
|
+
"string": "",
|
|
329
|
+
"number": 0,
|
|
330
|
+
"integer": 0,
|
|
331
|
+
"boolean": False,
|
|
332
|
+
"date": "",
|
|
333
|
+
"object": {},
|
|
334
|
+
"array": []
|
|
335
|
+
}
|
|
336
|
+
return defaults.get(field_type)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def load_schema(filepath: str) -> Dict[str, Any]:
|
|
340
|
+
"""Load schema from JSON file."""
|
|
341
|
+
try:
|
|
342
|
+
with open(filepath, 'r') as f:
|
|
343
|
+
return json.load(f)
|
|
344
|
+
except FileNotFoundError:
|
|
345
|
+
print(f"Error: Schema file not found: {filepath}", file=sys.stderr)
|
|
346
|
+
sys.exit(1)
|
|
347
|
+
except json.JSONDecodeError as e:
|
|
348
|
+
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
|
|
349
|
+
sys.exit(1)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def get_supported_databases() -> List[str]:
|
|
353
|
+
"""Get list of supported database types."""
|
|
354
|
+
return list(SchemaTransformer.DB_PATTERNS.keys())
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def main():
|
|
358
|
+
"""Main entry point for schema migration."""
|
|
359
|
+
parser = argparse.ArgumentParser(
|
|
360
|
+
description="Migrate NoSQL schema between database types",
|
|
361
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
362
|
+
epilog=f"""
|
|
363
|
+
Supported database types: {', '.join(get_supported_databases())}
|
|
364
|
+
|
|
365
|
+
Examples:
|
|
366
|
+
# MongoDB to DynamoDB
|
|
367
|
+
%(prog)s --from mongodb --to dynamodb --schema user.json
|
|
368
|
+
|
|
369
|
+
# DynamoDB to Firestore
|
|
370
|
+
%(prog)s --from dynamodb --to firestore --schema product.json --output firestore-schema.json
|
|
371
|
+
|
|
372
|
+
# Firestore to MongoDB
|
|
373
|
+
%(prog)s --from firestore --to mongodb --schema order.json
|
|
374
|
+
|
|
375
|
+
# List supported types
|
|
376
|
+
%(prog)s --list-types
|
|
377
|
+
"""
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
parser.add_argument(
|
|
381
|
+
"--from",
|
|
382
|
+
dest="source_type",
|
|
383
|
+
help="Source database type"
|
|
384
|
+
)
|
|
385
|
+
parser.add_argument(
|
|
386
|
+
"--to",
|
|
387
|
+
dest="target_type",
|
|
388
|
+
help="Target database type"
|
|
389
|
+
)
|
|
390
|
+
parser.add_argument(
|
|
391
|
+
"--schema",
|
|
392
|
+
help="Path to source schema file"
|
|
393
|
+
)
|
|
394
|
+
parser.add_argument(
|
|
395
|
+
"--output",
|
|
396
|
+
help="Output file for migrated schema"
|
|
397
|
+
)
|
|
398
|
+
parser.add_argument(
|
|
399
|
+
"--list-types",
|
|
400
|
+
action="store_true",
|
|
401
|
+
help="List supported database types"
|
|
402
|
+
)
|
|
403
|
+
parser.add_argument(
|
|
404
|
+
"--verbose",
|
|
405
|
+
action="store_true",
|
|
406
|
+
help="Print detailed output"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
args = parser.parse_args()
|
|
410
|
+
|
|
411
|
+
if args.list_types:
|
|
412
|
+
print("Supported database types:")
|
|
413
|
+
for db_type, info in SchemaTransformer.DB_PATTERNS.items():
|
|
414
|
+
print(f" - {db_type}: {info['name']}")
|
|
415
|
+
sys.exit(0)
|
|
416
|
+
|
|
417
|
+
# Validate required arguments
|
|
418
|
+
if not args.source_type or not args.target_type or not args.schema:
|
|
419
|
+
parser.error("--from, --to, and --schema are required")
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
if args.verbose:
|
|
423
|
+
print(f"Migrating schema from {args.source_type} to {args.target_type}...", file=sys.stderr)
|
|
424
|
+
|
|
425
|
+
# Load source schema
|
|
426
|
+
schema = load_schema(args.schema)
|
|
427
|
+
|
|
428
|
+
# Create transformer
|
|
429
|
+
transformer = SchemaTransformer(args.source_type, args.target_type)
|
|
430
|
+
|
|
431
|
+
# Transform schema
|
|
432
|
+
migrated = transformer.transform(schema)
|
|
433
|
+
|
|
434
|
+
# Output
|
|
435
|
+
output_json = json.dumps(migrated, indent=2)
|
|
436
|
+
print(output_json)
|
|
437
|
+
|
|
438
|
+
# Save to file if requested
|
|
439
|
+
if args.output:
|
|
440
|
+
with open(args.output, 'w') as f:
|
|
441
|
+
f.write(output_json)
|
|
442
|
+
|
|
443
|
+
if args.verbose:
|
|
444
|
+
print(f"✓ Schema migrated successfully", file=sys.stderr)
|
|
445
|
+
print(f"✓ Saved to {args.output}", file=sys.stderr)
|
|
446
|
+
|
|
447
|
+
sys.exit(0)
|
|
448
|
+
|
|
449
|
+
except Exception as e:
|
|
450
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
451
|
+
sys.exit(1)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
if __name__ == "__main__":
|
|
455
|
+
main()
|