@enfyra/mcp-server 0.1.31 → 0.1.33
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 +4 -3
- package/dist/lib/mcp-examples.js +14 -27
- package/dist/lib/mcp-examples.js.map +1 -1
- package/dist/lib/mcp-instructions.d.ts +5 -1
- package/dist/lib/mcp-instructions.js +15 -12
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/mcp-usage-telemetry.d.ts +43 -0
- package/dist/lib/mcp-usage-telemetry.js +387 -0
- package/dist/lib/mcp-usage-telemetry.js.map +1 -0
- package/dist/lib/mutation-guards.js +12 -0
- package/dist/lib/mutation-guards.js.map +1 -1
- package/dist/lib/platform-operation-tools.js +353 -6
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.js +5 -4
- package/dist/lib/required-knowledge.js.map +1 -1
- package/dist/lib/response-format.js +12 -2
- package/dist/lib/response-format.js.map +1 -1
- package/dist/lib/runtime-zone-tools.d.ts +44 -0
- package/dist/lib/runtime-zone-tools.js +196 -0
- package/dist/lib/runtime-zone-tools.js.map +1 -1
- package/dist/lib/table-tools.d.ts +2 -0
- package/dist/lib/table-tools.js +168 -20
- package/dist/lib/table-tools.js.map +1 -1
- package/dist/lib/tool-routing.d.ts +1 -1
- package/dist/lib/tool-routing.js +54 -33
- package/dist/lib/tool-routing.js.map +1 -1
- package/dist/lib/toolset-filter.d.ts +9 -0
- package/dist/lib/toolset-filter.js +115 -0
- package/dist/lib/toolset-filter.js.map +1 -0
- package/dist/mcp-server-entry.js +63 -18
- package/dist/mcp-server-entry.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/table-tools.js
CHANGED
|
@@ -44,6 +44,21 @@ const FALLBACK_COLUMN_TYPES = [
|
|
|
44
44
|
'richtext',
|
|
45
45
|
'float',
|
|
46
46
|
];
|
|
47
|
+
const RELATION_TYPE_ALIASES = {
|
|
48
|
+
many_to_one: 'many-to-one',
|
|
49
|
+
manyToOne: 'many-to-one',
|
|
50
|
+
manytoone: 'many-to-one',
|
|
51
|
+
one_to_many: 'one-to-many',
|
|
52
|
+
oneToMany: 'one-to-many',
|
|
53
|
+
onetomany: 'one-to-many',
|
|
54
|
+
one_to_one: 'one-to-one',
|
|
55
|
+
oneToOne: 'one-to-one',
|
|
56
|
+
onetoone: 'one-to-one',
|
|
57
|
+
many_to_many: 'many-to-many',
|
|
58
|
+
manyToMany: 'many-to-many',
|
|
59
|
+
manytomany: 'many-to-many',
|
|
60
|
+
};
|
|
61
|
+
const VALID_RELATION_TYPES = new Set(['many-to-one', 'one-to-many', 'one-to-one', 'many-to-many']);
|
|
47
62
|
const AUTO_MANAGED_COLUMN_NAMES = new Set(['id', '_id', 'createdAt', 'updatedAt']);
|
|
48
63
|
const COLUMN_TYPE_ALIAS_HINTS = [
|
|
49
64
|
'Use varchar for short strings; text or richtext for long prose.',
|
|
@@ -207,6 +222,34 @@ function assertNoDuplicateFieldNames(fieldNames, context) {
|
|
|
207
222
|
throw new Error(`${context} has duplicate field name(s): ${[...duplicates].join(', ')}. Column names and relation propertyName values share one namespace.`);
|
|
208
223
|
}
|
|
209
224
|
}
|
|
225
|
+
function assertNoColumnRelationNameCollision(columnNames, relationNames, context) {
|
|
226
|
+
const relationNameSet = new Set(relationNames.filter(Boolean));
|
|
227
|
+
const collisions = columnNames.filter((name) => relationNameSet.has(name));
|
|
228
|
+
if (collisions.length > 0) {
|
|
229
|
+
throw new Error(`${context} has column/relation namespace collision(s): ${[...new Set(collisions)].join(', ')}. ` +
|
|
230
|
+
`Column names and relation propertyName values share one namespace; remove the scalar column(s) and keep the relation propertyName(s) ${[...new Set(collisions)].join(', ')}. Do not create physical FK columns.`);
|
|
231
|
+
}
|
|
232
|
+
assertNoDuplicateFieldNames([...columnNames, ...relationNames], context);
|
|
233
|
+
}
|
|
234
|
+
function formatConstraintFieldHints(fields, relationNames, logicalFieldNames) {
|
|
235
|
+
const relationNameSet = new Set(relationNames);
|
|
236
|
+
const logicalNameSet = new Set(logicalFieldNames);
|
|
237
|
+
const normalizeName = (value) => value.replace(/[_\-\s]/gu, '').toLowerCase();
|
|
238
|
+
return fields
|
|
239
|
+
.map((field) => {
|
|
240
|
+
const normalized = String(field)
|
|
241
|
+
.replace(/_?ids?$/iu, '')
|
|
242
|
+
.replace(/_id$/iu, '')
|
|
243
|
+
.replace(/Id$/u, '')
|
|
244
|
+
.replace(/Ids$/u, '');
|
|
245
|
+
const match = [...relationNameSet].find((name) => name.toLowerCase() === normalized.toLowerCase());
|
|
246
|
+
if (match)
|
|
247
|
+
return `${field} -> use relation propertyName "${match}" in indexes/uniques, not physical FK "${field}"`;
|
|
248
|
+
const logicalMatch = [...logicalNameSet].find((name) => normalizeName(name) === normalizeName(String(field)));
|
|
249
|
+
return logicalMatch ? `${field} -> did you mean "${logicalMatch}"? Constraint fields must match column/relation names exactly.` : null;
|
|
250
|
+
})
|
|
251
|
+
.filter(Boolean);
|
|
252
|
+
}
|
|
210
253
|
function preflightCreateTableDefinitions(items) {
|
|
211
254
|
items.forEach((item, index) => {
|
|
212
255
|
const columns = Array.isArray(item.columns) ? item.columns : parseJsonArrayParam(`items[${index}].columns`, item.columns || '[]');
|
|
@@ -214,7 +257,7 @@ function preflightCreateTableDefinitions(items) {
|
|
|
214
257
|
const { columns: userColumns } = stripAutoManagedColumns(columns);
|
|
215
258
|
const columnNames = userColumns.map((column) => String(column?.name ?? '')).filter(Boolean);
|
|
216
259
|
const relationNames = relations.map((relation) => String(relation?.propertyName ?? '')).filter(Boolean);
|
|
217
|
-
|
|
260
|
+
assertNoColumnRelationNameCollision(columnNames, relationNames, `create_tables items[${index}] (${item.name || '<unnamed>'})`);
|
|
218
261
|
const logicalFields = new Set([...AUTO_MANAGED_COLUMN_NAMES, ...columnNames, ...relationNames]);
|
|
219
262
|
const indexes = normalizeConstraintGroupsValue(`items[${index}].indexes`, item.indexes ?? []);
|
|
220
263
|
const uniques = normalizeConstraintGroupsValue(`items[${index}].uniques`, item.uniques ?? []);
|
|
@@ -223,11 +266,50 @@ function preflightCreateTableDefinitions(items) {
|
|
|
223
266
|
.flat()
|
|
224
267
|
.filter((field) => !logicalFields.has(field));
|
|
225
268
|
if (unknownConstraintFields.length > 0) {
|
|
226
|
-
|
|
227
|
-
|
|
269
|
+
const uniqueUnknownFields = [...new Set(unknownConstraintFields)];
|
|
270
|
+
const hints = formatConstraintFieldHints(uniqueUnknownFields, relationNames, [...logicalFields]);
|
|
271
|
+
throw new Error(`create_tables items[${index}] (${item.name || '<unnamed>'}) has indexes/uniques referencing undeclared field(s): ${uniqueUnknownFields.join(', ')}. ` +
|
|
272
|
+
'Declare each field as a column or relation propertyName in the same table item. ' +
|
|
273
|
+
(hints.length ? `Hint(s): ${hints.join('; ')}. ` : '') +
|
|
274
|
+
'For relation-based unique pairs added after create, first create the relations, then call update_tables with the unique group.');
|
|
228
275
|
}
|
|
229
276
|
});
|
|
230
277
|
}
|
|
278
|
+
function relationTargetName(relation) {
|
|
279
|
+
const target = relation?.targetTable ?? relation?.targetTableId;
|
|
280
|
+
if (target && typeof target === 'object')
|
|
281
|
+
return target.name ?? target.alias ?? target.id ?? target._id;
|
|
282
|
+
return target;
|
|
283
|
+
}
|
|
284
|
+
export function computeBatchCleanupOrder(items) {
|
|
285
|
+
const tableNames = items.map((item) => String(item?.name || '')).filter(Boolean);
|
|
286
|
+
const tableSet = new Set(tableNames);
|
|
287
|
+
const edges = [];
|
|
288
|
+
for (const item of items) {
|
|
289
|
+
const source = String(item?.name || '');
|
|
290
|
+
if (!source)
|
|
291
|
+
continue;
|
|
292
|
+
const relations = Array.isArray(item.relations) ? item.relations : parseJsonArrayParam(`${source}.relations`, item.relations || '[]');
|
|
293
|
+
for (const relation of relations) {
|
|
294
|
+
const target = String(relationTargetName(relation) || '');
|
|
295
|
+
if (target && tableSet.has(target) && target !== source)
|
|
296
|
+
edges.push([source, target]);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const remaining = new Set(tableNames);
|
|
300
|
+
const ordered = [];
|
|
301
|
+
while (remaining.size > 0) {
|
|
302
|
+
const leaves = [...remaining]
|
|
303
|
+
.filter((name) => !edges.some(([source, target]) => target === name && remaining.has(source) && remaining.has(target)))
|
|
304
|
+
.sort();
|
|
305
|
+
const batch = leaves.length ? leaves : [[...remaining].sort()[0]];
|
|
306
|
+
for (const name of batch) {
|
|
307
|
+
remaining.delete(name);
|
|
308
|
+
ordered.push(name);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return ordered;
|
|
312
|
+
}
|
|
231
313
|
function splitRelationConstraintGroups(groups, relationNames) {
|
|
232
314
|
const immediate = [];
|
|
233
315
|
const deferred = [];
|
|
@@ -268,20 +350,38 @@ function resolveRelationConstraintGroups(table, groups, groupName) {
|
|
|
268
350
|
}));
|
|
269
351
|
}
|
|
270
352
|
export function assertIndexesDoNotReferenceUniqueFields(indexes, uniques) {
|
|
271
|
-
const uniqueFields = new Set(uniques.flat());
|
|
272
353
|
const conflicts = indexes
|
|
273
|
-
.map((
|
|
274
|
-
|
|
275
|
-
|
|
354
|
+
.map((indexGroup) => ({
|
|
355
|
+
indexGroup,
|
|
356
|
+
uniqueGroups: uniques
|
|
357
|
+
.map((uniqueGroup) => ({
|
|
358
|
+
uniqueGroup,
|
|
359
|
+
overlappingFields: indexGroup.filter((field) => uniqueGroup.includes(field)),
|
|
360
|
+
}))
|
|
361
|
+
.filter((conflict) => conflict.overlappingFields.length > 0),
|
|
276
362
|
}))
|
|
277
|
-
.filter((conflict) => conflict.
|
|
363
|
+
.filter((conflict) => conflict.uniqueGroups.length > 0);
|
|
278
364
|
if (conflicts.length > 0) {
|
|
279
365
|
const groups = conflicts
|
|
280
|
-
.map((conflict) => `${JSON.stringify(conflict.
|
|
366
|
+
.map((conflict) => `${JSON.stringify(conflict.indexGroup)} overlaps unique group(s) ${conflict.uniqueGroups.map((item) => `${JSON.stringify(item.uniqueGroup)} via ${JSON.stringify(item.overlappingFields)}`).join(', ')}`)
|
|
281
367
|
.join('; ');
|
|
282
368
|
throw new Error(`Invalid schema constraints: indexes must not include fields that appear in uniques, including composite unique groups. Conflict(s): ${groups}. Unique constraints already create indexed lookups for their fields; remove those fields from indexes and keep them only in uniques.`);
|
|
283
369
|
}
|
|
284
370
|
}
|
|
371
|
+
function pruneIndexesThatOverlapUniques(indexes, uniques) {
|
|
372
|
+
const uniqueFields = new Set(uniques.flat());
|
|
373
|
+
const kept = [];
|
|
374
|
+
const removed = [];
|
|
375
|
+
for (const indexGroup of indexes) {
|
|
376
|
+
if (indexGroup.some((field) => uniqueFields.has(field))) {
|
|
377
|
+
removed.push(indexGroup);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
kept.push(indexGroup);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return { indexes: kept, removed };
|
|
384
|
+
}
|
|
285
385
|
export function normalizeRelationForTablePatch(relation) {
|
|
286
386
|
for (const key of FORBIDDEN_RELATION_KEYS) {
|
|
287
387
|
if (Object.prototype.hasOwnProperty.call(relation, key)) {
|
|
@@ -290,6 +390,12 @@ export function normalizeRelationForTablePatch(relation) {
|
|
|
290
390
|
}
|
|
291
391
|
const { sourceTable, targetTable, targetTableId, mappedBy, fkCol, fkColumn, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, junctionTargetColumn, ...rest } = relation;
|
|
292
392
|
const normalized = { ...rest };
|
|
393
|
+
if (rest.type !== undefined) {
|
|
394
|
+
normalized.type = normalizeRelationType(rest.type);
|
|
395
|
+
}
|
|
396
|
+
if (normalized.type === 'one-to-many' && mappedBy !== undefined && mappedBy !== null && mappedBy !== '') {
|
|
397
|
+
delete normalized.inversePropertyName;
|
|
398
|
+
}
|
|
293
399
|
const resolvedTargetTable = targetTableId ??
|
|
294
400
|
(targetTable && typeof targetTable === 'object'
|
|
295
401
|
? targetTable.id ?? targetTable._id ?? targetTable
|
|
@@ -304,6 +410,14 @@ export function normalizeRelationForTablePatch(relation) {
|
|
|
304
410
|
}
|
|
305
411
|
return normalized;
|
|
306
412
|
}
|
|
413
|
+
export function normalizeRelationType(type) {
|
|
414
|
+
const raw = String(type ?? '').trim();
|
|
415
|
+
const normalized = RELATION_TYPE_ALIASES[raw] ?? raw;
|
|
416
|
+
if (!VALID_RELATION_TYPES.has(normalized)) {
|
|
417
|
+
throw new Error(`Invalid relation type "${raw || '<missing>'}". Use one of many-to-one, one-to-many, one-to-one, or many-to-many. Common aliases such as many_to_one are normalized by the tool.`);
|
|
418
|
+
}
|
|
419
|
+
return normalized;
|
|
420
|
+
}
|
|
307
421
|
function assertNoForbiddenRelationKeys(args) {
|
|
308
422
|
for (const key of FORBIDDEN_RELATION_KEYS) {
|
|
309
423
|
if (Object.prototype.hasOwnProperty.call(args, key)) {
|
|
@@ -554,7 +668,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
554
668
|
assertGlobalRulesAck(args.globalRulesAckKey);
|
|
555
669
|
return withSchemaQueue(async () => {
|
|
556
670
|
assertNoForbiddenRelationKeys(args);
|
|
557
|
-
const { sourceTableId, targetTableId, targetTable
|
|
671
|
+
const { sourceTableId, targetTableId, targetTable } = args;
|
|
672
|
+
const relationPatch = normalizeRelationForTablePatch(args);
|
|
673
|
+
const { type, propertyName, inversePropertyName, mappedBy, isNullable, onDelete, description } = relationPatch;
|
|
558
674
|
const targetRef = targetTableId ?? targetTable;
|
|
559
675
|
if (targetRef === undefined || targetRef === null || targetRef === '') {
|
|
560
676
|
throw new Error('create_relations requires targetTableId or targetTable. Pass an existing table id, name, or alias.');
|
|
@@ -694,7 +810,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
694
810
|
sourceTableId: z.string().describe('Source table id, exact table name, or alias. For many-to-one, this is the table that owns the relation property.'),
|
|
695
811
|
targetTableId: z.string().optional().describe('Target table id, exact table name, or alias. MCP resolves names/aliases to ids before mutation. Optional when targetTable is provided.'),
|
|
696
812
|
targetTable: z.string().optional().describe('Alias for targetTableId when naturally using a target table name/alias such as enfyra_user. Optional when targetTableId is provided.'),
|
|
697
|
-
type: z.
|
|
813
|
+
type: z.string().describe('Relation type. Use many-to-one, one-to-many, one-to-one, or many-to-many. Common aliases such as many_to_one are normalized by the tool.'),
|
|
698
814
|
propertyName: z.string().describe('Property name on source table (e.g., "customer", "items").'),
|
|
699
815
|
inversePropertyName: z.string().optional().describe('Property name on target table for bidirectional relation (e.g., "orders"). Omit unless a concrete response, UI, deep query, aggregate sort/count, or parent-to-child traversal will use the reverse field. Do not add inverses merely because a parent table exists.'),
|
|
700
816
|
mappedBy: z.string().optional().describe('Mapped-by property for inverse relation shapes when required by the backend. Do not use physical FK names.'),
|
|
@@ -736,10 +852,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
736
852
|
const { columns: normalizedUserColumns, normalizations } = normalizeColumnsForLiveMetadata(userColumnsWithoutAuto, supportedTypes);
|
|
737
853
|
const deferredRelations = arrayValue('relations', args.relations).map(normalizeRelationForTablePatch);
|
|
738
854
|
const relationNames = new Set(deferredRelations.map((relation) => relation.propertyName).filter(Boolean));
|
|
739
|
-
|
|
740
|
-
...normalizedUserColumns.map((column) => String(column.name || '')).filter(Boolean),
|
|
741
|
-
...deferredRelations.map((relation) => String(relation.propertyName || '')).filter(Boolean),
|
|
742
|
-
], `create_tables item "${args.name}"`);
|
|
855
|
+
assertNoColumnRelationNameCollision(normalizedUserColumns.map((column) => String(column.name || '')).filter(Boolean), deferredRelations.map((relation) => String(relation.propertyName || '')).filter(Boolean), `create_tables item "${args.name}"`);
|
|
743
856
|
const indexes = normalizeConstraintGroupsValue('indexes', args.indexes ?? []);
|
|
744
857
|
const uniques = normalizeConstraintGroupsValue('uniques', args.uniques ?? []);
|
|
745
858
|
assertIndexesDoNotReferenceUniqueFields(indexes, uniques);
|
|
@@ -815,8 +928,9 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
815
928
|
const mappedUniques = resolveRelationConstraintGroups(tableData, deferredUniques, 'uniques');
|
|
816
929
|
const existingIndexes = normalizeConstraintGroupsValue('indexes', tableData.indexes || []);
|
|
817
930
|
const existingUniques = normalizeConstraintGroupsValue('uniques', tableData.uniques || []);
|
|
818
|
-
const indexes = mergeConstraintGroups(existingIndexes, mappedIndexes);
|
|
819
931
|
const uniques = mergeConstraintGroups(existingUniques, mappedUniques);
|
|
932
|
+
const prunedExistingIndexes = pruneIndexesThatOverlapUniques(existingIndexes, uniques);
|
|
933
|
+
const indexes = mergeConstraintGroups(prunedExistingIndexes.indexes, mappedIndexes);
|
|
820
934
|
assertIndexesDoNotReferenceUniqueFields(indexes, uniques);
|
|
821
935
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { indexes, uniques });
|
|
822
936
|
return {
|
|
@@ -831,11 +945,13 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
831
945
|
indexes: mappedIndexes,
|
|
832
946
|
uniques: mappedUniques,
|
|
833
947
|
},
|
|
948
|
+
prunedExistingIndexes: prunedExistingIndexes.removed,
|
|
834
949
|
result,
|
|
835
950
|
};
|
|
836
951
|
}
|
|
837
952
|
async function updateOneTable(args) {
|
|
838
953
|
const body = {};
|
|
954
|
+
let schemaConstraintNormalization;
|
|
839
955
|
if (args.name !== undefined)
|
|
840
956
|
body.name = args.name;
|
|
841
957
|
if (args.alias !== undefined)
|
|
@@ -860,12 +976,22 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
860
976
|
if (uniques === undefined)
|
|
861
977
|
uniques = normalizeConstraintGroupsValue('uniques', existing.uniques);
|
|
862
978
|
}
|
|
979
|
+
if (args.uniques !== undefined && args.indexes === undefined) {
|
|
980
|
+
const prunedExistingIndexes = pruneIndexesThatOverlapUniques(indexes ?? [], uniques ?? []);
|
|
981
|
+
indexes = prunedExistingIndexes.indexes;
|
|
982
|
+
body.indexes = indexes;
|
|
983
|
+
schemaConstraintNormalization = {
|
|
984
|
+
prunedExistingIndexes: prunedExistingIndexes.removed,
|
|
985
|
+
reason: 'Unique constraints already provide indexed lookups; MCP removed existing non-unique indexes that used fields now covered by uniques.',
|
|
986
|
+
};
|
|
987
|
+
}
|
|
863
988
|
assertIndexesDoNotReferenceUniqueFields(indexes ?? [], uniques ?? []);
|
|
864
989
|
}
|
|
865
990
|
const result = await patchTableAutoConfirm(ENFYRA_API_URL, args.tableId, body);
|
|
866
991
|
return {
|
|
867
992
|
action: 'table_updated',
|
|
868
993
|
tableId: args.tableId,
|
|
994
|
+
schemaConstraintNormalization,
|
|
869
995
|
result,
|
|
870
996
|
};
|
|
871
997
|
}
|
|
@@ -937,14 +1063,17 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
937
1063
|
};
|
|
938
1064
|
}
|
|
939
1065
|
// ─── READ ───
|
|
940
|
-
server.tool('get_all_tables', 'List table definitions from metadata.
|
|
941
|
-
limit: z.number().int().positive().optional().describe('Maximum tables returned after search. Required unless all=true.'),
|
|
1066
|
+
server.tool('get_all_tables', 'List table definitions from metadata. Complete lists must pass either limit or all=true. If search is provided without limit, the tool returns a bounded lookup window of 10 matches.', {
|
|
1067
|
+
limit: z.number().int().positive().optional().describe('Maximum tables returned after search. Required unless all=true or search is provided.'),
|
|
942
1068
|
all: z.boolean().optional().describe('Return all matched tables. Use this when a complete table list is required.'),
|
|
943
1069
|
search: z.string().optional().describe('Optional table name, alias, or description substring filter.'),
|
|
944
1070
|
}, async ({ limit, all, search }) => {
|
|
945
|
-
if (!all && limit === undefined) {
|
|
1071
|
+
if (!all && limit === undefined && !search?.trim()) {
|
|
946
1072
|
throw new Error('get_all_tables requires either limit or all=true. Do not invent arbitrary limits for complete table lists; use all=true.');
|
|
947
1073
|
}
|
|
1074
|
+
if (all && limit !== undefined) {
|
|
1075
|
+
throw new Error('get_all_tables accepts either all=true or limit, not both.');
|
|
1076
|
+
}
|
|
948
1077
|
const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
|
|
949
1078
|
const needle = search?.trim().toLowerCase();
|
|
950
1079
|
const tables = normalizeTablesFromMetadata(metadata)
|
|
@@ -964,13 +1093,16 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
964
1093
|
return [table.name, table.alias, table.description]
|
|
965
1094
|
.some((value) => String(value || '').toLowerCase().includes(needle));
|
|
966
1095
|
});
|
|
967
|
-
const
|
|
1096
|
+
const effectiveLimit = all ? tables.length : (limit ?? 10);
|
|
1097
|
+
const returnedTables = all ? tables : tables.slice(0, effectiveLimit);
|
|
968
1098
|
return jsonContent({
|
|
969
1099
|
action: 'get_all_tables',
|
|
970
1100
|
totalTableCount: normalizeTablesFromMetadata(metadata).length,
|
|
971
1101
|
matchedTableCount: tables.length,
|
|
972
1102
|
returnedTableCount: returnedTables.length,
|
|
973
1103
|
all: Boolean(all),
|
|
1104
|
+
implicitSearchLimit: Boolean(!all && limit === undefined && search?.trim()),
|
|
1105
|
+
hardCap: all ? null : effectiveLimit,
|
|
974
1106
|
search: search || null,
|
|
975
1107
|
tables: returnedTables,
|
|
976
1108
|
detailHint: 'Use inspect_table with a table id/name for columns, relations, indexes, routes, permissions, and GraphQL state.',
|
|
@@ -1071,6 +1203,8 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
1071
1203
|
'Do not include id, _id, createdAt, or updatedAt in columns; Enfyra manages them and create_tables strips them before save.',
|
|
1072
1204
|
'Every field named in indexes/uniques must be a scalar column, auto-managed column, or relation propertyName in the same table item; otherwise the tool rejects the whole batch before creating tables.',
|
|
1073
1205
|
'Use get_schema_design_context first for live column types and relation rules. Do not include physical FK fields.',
|
|
1206
|
+
'The response includes cleanupHints.recordCreateOrder; use it when seeding records so parent/target rows are created before child/source rows.',
|
|
1207
|
+
'The response includes cleanupHints.recordDeleteOrder; use it when deleting seeded test records so child/source rows are removed before parent/target rows.',
|
|
1074
1208
|
].join(' '), {
|
|
1075
1209
|
items: bulkObjectArrayParam(z, 'Table definitions').optional().describe('Native JSON array of table definitions. Pass one object in the array for a single table.'),
|
|
1076
1210
|
tables: bulkObjectArrayParam(z, 'Table definitions').optional().describe('Alias for items when the caller naturally names the batch tables. Pass either items or tables, not both.'),
|
|
@@ -1083,6 +1217,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
1083
1217
|
const parsedItems = parseBulkItemsParam('items', items ?? tables);
|
|
1084
1218
|
assertBulkLimit('create_tables', parsedItems, maxItems);
|
|
1085
1219
|
preflightCreateTableDefinitions(parsedItems);
|
|
1220
|
+
const recordDeleteOrder = computeBatchCleanupOrder(parsedItems);
|
|
1086
1221
|
const created = [];
|
|
1087
1222
|
const deferredRelations = [];
|
|
1088
1223
|
const deferredConstraints = [];
|
|
@@ -1101,6 +1236,11 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
1101
1236
|
}
|
|
1102
1237
|
}
|
|
1103
1238
|
const createdRelations = [];
|
|
1239
|
+
deferredRelations.sort((left, right) => {
|
|
1240
|
+
const leftPriority = normalizeRelationType(left.type) === 'one-to-many' ? 1 : 0;
|
|
1241
|
+
const rightPriority = normalizeRelationType(right.type) === 'one-to-many' ? 1 : 0;
|
|
1242
|
+
return leftPriority - rightPriority;
|
|
1243
|
+
});
|
|
1104
1244
|
for (const relation of deferredRelations) {
|
|
1105
1245
|
const relationResult = await appendRelationToTable({
|
|
1106
1246
|
sourceTableId: relation.sourceTableId,
|
|
@@ -1136,6 +1276,14 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
1136
1276
|
sequential: true,
|
|
1137
1277
|
relationPhaseAfterTables: true,
|
|
1138
1278
|
constraintPhaseAfterRelations: true,
|
|
1279
|
+
cleanupHints: {
|
|
1280
|
+
recordCreateOrder: [...recordDeleteOrder].reverse(),
|
|
1281
|
+
recordDeleteOrder,
|
|
1282
|
+
tableDeleteOrder: recordDeleteOrder,
|
|
1283
|
+
recordCreateRule: 'When seeding sample data, create records sequentially in recordCreateOrder; parent/target rows must exist before child/source rows reference them.',
|
|
1284
|
+
recordRule: 'If you delete seeded records before deleting test tables, delete record batches sequentially in recordDeleteOrder; do not parallelize parent/child deletes.',
|
|
1285
|
+
tableRule: 'For full test cleanup, prefer delete_tables with tableDeleteOrder after deleting custom routes/flows; table deletion removes the remaining table data.',
|
|
1286
|
+
},
|
|
1139
1287
|
created,
|
|
1140
1288
|
createdRelations,
|
|
1141
1289
|
appliedDeferredConstraints,
|