s3db.js 11.2.6 → 11.3.2
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/README.md +138 -0
- package/dist/s3db.cjs.js +2 -2
- package/dist/s3db.es.js +2 -2
- package/mcp/.env.example +117 -0
- package/mcp/{server.js → entrypoint.js} +1941 -683
- package/mcp/tools/bulk.js +112 -0
- package/mcp/tools/connection.js +228 -0
- package/mcp/tools/crud.js +579 -0
- package/mcp/tools/debugging.js +299 -0
- package/mcp/tools/export-import.js +281 -0
- package/mcp/tools/index.js +67 -0
- package/mcp/tools/partitions.js +223 -0
- package/mcp/tools/query.js +150 -0
- package/mcp/tools/resources.js +96 -0
- package/mcp/tools/stats.js +281 -0
- package/package.json +17 -7
- package/src/database.class.js +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partition Management Tools
|
|
3
|
+
* Handles partition listing, inspection, and orphaned partition recovery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const partitionTools = [
|
|
7
|
+
{
|
|
8
|
+
name: 'resourceListPartitions',
|
|
9
|
+
description: 'List all partitions defined for a resource',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
resourceName: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Name of the resource'
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
required: ['resourceName']
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'resourceListPartitionValues',
|
|
23
|
+
description: 'List unique values for a specific partition field',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
resourceName: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Name of the resource'
|
|
30
|
+
},
|
|
31
|
+
partitionName: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Name of the partition'
|
|
34
|
+
},
|
|
35
|
+
limit: {
|
|
36
|
+
type: 'number',
|
|
37
|
+
description: 'Maximum number of values to return',
|
|
38
|
+
default: 1000
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: ['resourceName', 'partitionName']
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'dbFindOrphanedPartitions',
|
|
46
|
+
description: 'Find partitions that reference fields no longer in the schema',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
resourceName: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Name of specific resource to check (optional - checks all if not provided)'
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
required: []
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'dbRemoveOrphanedPartitions',
|
|
60
|
+
description: 'Remove orphaned partitions from resource configuration',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
resourceName: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'Name of the resource'
|
|
67
|
+
},
|
|
68
|
+
dryRun: {
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
description: 'Preview changes without applying them',
|
|
71
|
+
default: true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
required: ['resourceName']
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
export function createPartitionHandlers(server) {
|
|
80
|
+
return {
|
|
81
|
+
async resourceListPartitions(args, database) {
|
|
82
|
+
server.ensureConnected(database);
|
|
83
|
+
const { resourceName } = args;
|
|
84
|
+
const resource = server.getResource(database, resourceName);
|
|
85
|
+
|
|
86
|
+
const partitions = resource.config.partitions || {};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
resource: resourceName,
|
|
91
|
+
partitions: Object.keys(partitions),
|
|
92
|
+
count: Object.keys(partitions).length,
|
|
93
|
+
details: partitions
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async resourceListPartitionValues(args, database) {
|
|
98
|
+
server.ensureConnected(database);
|
|
99
|
+
const { resourceName, partitionName, limit = 1000 } = args;
|
|
100
|
+
const resource = server.getResource(database, resourceName);
|
|
101
|
+
|
|
102
|
+
if (!resource.config.partitions || !resource.config.partitions[partitionName]) {
|
|
103
|
+
throw new Error(`Partition '${partitionName}' not found in resource '${resourceName}'`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// List all objects with this partition prefix
|
|
108
|
+
const prefix = `${database.keyPrefix}resource=${resourceName}/partition=${partitionName}/`;
|
|
109
|
+
|
|
110
|
+
const response = await database.client.listObjectsV2({
|
|
111
|
+
Bucket: database.bucket,
|
|
112
|
+
Prefix: prefix,
|
|
113
|
+
MaxKeys: limit
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Extract unique partition values from keys
|
|
117
|
+
const partitionValues = new Set();
|
|
118
|
+
|
|
119
|
+
for (const obj of response.Contents || []) {
|
|
120
|
+
// Parse partition values from key
|
|
121
|
+
const keyParts = obj.Key.split('/');
|
|
122
|
+
const partitionPart = keyParts.find(part => part.startsWith('partition='));
|
|
123
|
+
if (partitionPart) {
|
|
124
|
+
const valuesPart = keyParts.slice(keyParts.indexOf(partitionPart) + 1).find(part => !part.startsWith('id='));
|
|
125
|
+
if (valuesPart) {
|
|
126
|
+
partitionValues.add(valuesPart);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
resource: resourceName,
|
|
134
|
+
partition: partitionName,
|
|
135
|
+
values: Array.from(partitionValues),
|
|
136
|
+
count: partitionValues.size
|
|
137
|
+
};
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: error.message,
|
|
142
|
+
resource: resourceName,
|
|
143
|
+
partition: partitionName
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async dbFindOrphanedPartitions(args, database) {
|
|
149
|
+
server.ensureConnected(database);
|
|
150
|
+
const { resourceName } = args;
|
|
151
|
+
|
|
152
|
+
const orphanedByResource = {};
|
|
153
|
+
const resourcesToCheck = resourceName
|
|
154
|
+
? [resourceName]
|
|
155
|
+
: Object.keys(database.resources || {});
|
|
156
|
+
|
|
157
|
+
for (const name of resourcesToCheck) {
|
|
158
|
+
const resource = database.resources[name];
|
|
159
|
+
if (resource && resource.findOrphanedPartitions) {
|
|
160
|
+
const orphaned = resource.findOrphanedPartitions();
|
|
161
|
+
if (Object.keys(orphaned).length > 0) {
|
|
162
|
+
orphanedByResource[name] = orphaned;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
orphanedPartitions: orphanedByResource,
|
|
170
|
+
affectedResources: Object.keys(orphanedByResource),
|
|
171
|
+
count: Object.keys(orphanedByResource).length,
|
|
172
|
+
hasIssues: Object.keys(orphanedByResource).length > 0
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async dbRemoveOrphanedPartitions(args, database) {
|
|
177
|
+
server.ensureConnected(database);
|
|
178
|
+
const { resourceName, dryRun = true } = args;
|
|
179
|
+
const resource = server.getResource(database, resourceName);
|
|
180
|
+
|
|
181
|
+
if (!resource.removeOrphanedPartitions) {
|
|
182
|
+
throw new Error(`Resource '${resourceName}' does not support removeOrphanedPartitions method`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Find orphaned partitions first
|
|
186
|
+
const orphaned = resource.findOrphanedPartitions();
|
|
187
|
+
|
|
188
|
+
if (Object.keys(orphaned).length === 0) {
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
message: 'No orphaned partitions found',
|
|
192
|
+
resource: resourceName,
|
|
193
|
+
dryRun
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (dryRun) {
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
message: 'Dry run - no changes made',
|
|
201
|
+
resource: resourceName,
|
|
202
|
+
orphanedPartitions: orphaned,
|
|
203
|
+
wouldRemove: Object.keys(orphaned),
|
|
204
|
+
dryRun: true
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Actually remove
|
|
209
|
+
const removed = resource.removeOrphanedPartitions();
|
|
210
|
+
|
|
211
|
+
// Save metadata
|
|
212
|
+
await database.uploadMetadataFile();
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
success: true,
|
|
216
|
+
message: `Removed ${Object.keys(removed).length} orphaned partition(s)`,
|
|
217
|
+
resource: resourceName,
|
|
218
|
+
removedPartitions: removed,
|
|
219
|
+
dryRun: false
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query & Filtering Tools
|
|
3
|
+
* Provides advanced query and search capabilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const queryTools = [
|
|
7
|
+
{
|
|
8
|
+
name: 'resourceQuery',
|
|
9
|
+
description: 'Query documents with complex filters and conditions',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
resourceName: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Name of the resource'
|
|
16
|
+
},
|
|
17
|
+
filters: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
description: 'Query filters (e.g., {status: "active", age: {$gt: 18}})'
|
|
20
|
+
},
|
|
21
|
+
limit: {
|
|
22
|
+
type: 'number',
|
|
23
|
+
description: 'Maximum number of results',
|
|
24
|
+
default: 100
|
|
25
|
+
},
|
|
26
|
+
offset: {
|
|
27
|
+
type: 'number',
|
|
28
|
+
description: 'Number of results to skip',
|
|
29
|
+
default: 0
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
required: ['resourceName', 'filters']
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'resourceSearch',
|
|
37
|
+
description: 'Search for documents by text in specific fields',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
resourceName: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Name of the resource'
|
|
44
|
+
},
|
|
45
|
+
searchText: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Text to search for'
|
|
48
|
+
},
|
|
49
|
+
fields: {
|
|
50
|
+
type: 'array',
|
|
51
|
+
items: { type: 'string' },
|
|
52
|
+
description: 'Fields to search in (if not specified, searches all string fields)'
|
|
53
|
+
},
|
|
54
|
+
caseSensitive: {
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
description: 'Case-sensitive search',
|
|
57
|
+
default: false
|
|
58
|
+
},
|
|
59
|
+
limit: {
|
|
60
|
+
type: 'number',
|
|
61
|
+
description: 'Maximum number of results',
|
|
62
|
+
default: 100
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
required: ['resourceName', 'searchText']
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
export function createQueryHandlers(server) {
|
|
71
|
+
return {
|
|
72
|
+
async resourceQuery(args, database) {
|
|
73
|
+
server.ensureConnected(database);
|
|
74
|
+
const { resourceName, filters, limit = 100, offset = 0 } = args;
|
|
75
|
+
const resource = server.getResource(database, resourceName);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Use the query method from resource
|
|
79
|
+
const results = await resource.query(filters, { limit, offset });
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
data: results,
|
|
84
|
+
count: results.length,
|
|
85
|
+
filters,
|
|
86
|
+
pagination: {
|
|
87
|
+
limit,
|
|
88
|
+
offset,
|
|
89
|
+
hasMore: results.length === limit
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: error.message,
|
|
96
|
+
filters
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async resourceSearch(args, database) {
|
|
102
|
+
server.ensureConnected(database);
|
|
103
|
+
const { resourceName, searchText, fields, caseSensitive = false, limit = 100 } = args;
|
|
104
|
+
const resource = server.getResource(database, resourceName);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Get all documents and filter in memory
|
|
108
|
+
const allDocs = await resource.list({ limit: limit * 2 }); // Fetch more to ensure we have enough after filtering
|
|
109
|
+
|
|
110
|
+
const searchString = caseSensitive ? searchText : searchText.toLowerCase();
|
|
111
|
+
|
|
112
|
+
// Determine fields to search
|
|
113
|
+
let searchFields = fields;
|
|
114
|
+
if (!searchFields || searchFields.length === 0) {
|
|
115
|
+
// Auto-detect string fields
|
|
116
|
+
searchFields = Object.keys(resource.attributes || {}).filter(key => {
|
|
117
|
+
const attr = resource.attributes[key];
|
|
118
|
+
const type = typeof attr === 'string' ? attr.split('|')[0] : attr.type;
|
|
119
|
+
return type === 'string';
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Filter documents
|
|
124
|
+
const results = allDocs.filter(doc => {
|
|
125
|
+
return searchFields.some(field => {
|
|
126
|
+
const value = doc[field];
|
|
127
|
+
if (!value) return false;
|
|
128
|
+
const valueString = caseSensitive ? String(value) : String(value).toLowerCase();
|
|
129
|
+
return valueString.includes(searchString);
|
|
130
|
+
});
|
|
131
|
+
}).slice(0, limit);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
data: results,
|
|
136
|
+
count: results.length,
|
|
137
|
+
searchText,
|
|
138
|
+
searchFields,
|
|
139
|
+
caseSensitive
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
error: error.message,
|
|
145
|
+
searchText
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Management Tools
|
|
3
|
+
* Handles resource creation and listing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const resourceManagementTools = [
|
|
7
|
+
{
|
|
8
|
+
name: 'dbCreateResource',
|
|
9
|
+
description: 'Create a new resource (collection/table) in the database',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
name: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Resource name'
|
|
16
|
+
},
|
|
17
|
+
attributes: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
description: 'Schema attributes definition (e.g., {"name": "string|required", "age": "number"})'
|
|
20
|
+
},
|
|
21
|
+
behavior: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Resource behavior',
|
|
24
|
+
enum: ['user-managed', 'body-only', 'body-overflow', 'enforce-limits', 'truncate-data'],
|
|
25
|
+
default: 'user-managed'
|
|
26
|
+
},
|
|
27
|
+
timestamps: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description: 'Enable automatic timestamps',
|
|
30
|
+
default: false
|
|
31
|
+
},
|
|
32
|
+
partitions: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
description: 'Partition configuration'
|
|
35
|
+
},
|
|
36
|
+
paranoid: {
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
description: 'Enable paranoid mode (soft deletes)',
|
|
39
|
+
default: true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
required: ['name', 'attributes']
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'dbListResources',
|
|
47
|
+
description: 'List all resources in the database',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {},
|
|
51
|
+
required: []
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export function createResourceManagementHandlers(server) {
|
|
57
|
+
return {
|
|
58
|
+
async dbCreateResource(args, database) {
|
|
59
|
+
server.ensureConnected(database);
|
|
60
|
+
|
|
61
|
+
const { name, attributes, behavior = 'user-managed', timestamps = false, partitions, paranoid = true } = args;
|
|
62
|
+
|
|
63
|
+
const resource = await database.createResource({
|
|
64
|
+
name,
|
|
65
|
+
attributes,
|
|
66
|
+
behavior,
|
|
67
|
+
timestamps,
|
|
68
|
+
partitions,
|
|
69
|
+
paranoid
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
resource: {
|
|
75
|
+
name: resource.name,
|
|
76
|
+
behavior: resource.behavior,
|
|
77
|
+
attributes: resource.attributes,
|
|
78
|
+
partitions: resource.config.partitions,
|
|
79
|
+
timestamps: resource.config.timestamps
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async dbListResources(args, database) {
|
|
85
|
+
server.ensureConnected(database);
|
|
86
|
+
|
|
87
|
+
const resourceList = await database.listResources();
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
resources: resourceList,
|
|
92
|
+
count: resourceList.length
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|