@enfyra/mcp-server 0.1.69 → 0.1.71

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.
Files changed (248) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/config-local-adapters.d.ts +18 -0
  3. package/dist/lib/config-local-adapters.js +271 -0
  4. package/dist/lib/config-local-adapters.js.map +1 -0
  5. package/dist/lib/config-local-contracts.d.ts +28 -0
  6. package/dist/lib/config-local-contracts.js +200 -0
  7. package/dist/lib/config-local-contracts.js.map +1 -0
  8. package/dist/lib/config-local-prompts.d.ts +10 -0
  9. package/dist/lib/config-local-prompts.js +185 -0
  10. package/dist/lib/config-local-prompts.js.map +1 -0
  11. package/dist/lib/config-local-types.d.ts +23 -0
  12. package/dist/lib/config-local-types.js +2 -0
  13. package/dist/lib/config-local-types.js.map +1 -0
  14. package/dist/lib/config-local.js +3 -649
  15. package/dist/lib/config-local.js.map +1 -1
  16. package/dist/lib/discovery-tools.d.ts +4 -0
  17. package/dist/lib/discovery-tools.js +546 -0
  18. package/dist/lib/discovery-tools.js.map +1 -0
  19. package/dist/lib/dynamic-repository-builder.d.ts +1 -1
  20. package/dist/lib/dynamic-script-context-contract.d.ts +100 -0
  21. package/dist/lib/dynamic-script-context-contract.js +163 -0
  22. package/dist/lib/dynamic-script-context-contract.js.map +1 -0
  23. package/dist/lib/enfyra-mcp-server.d.ts +3 -0
  24. package/dist/lib/enfyra-mcp-server.js +79 -0
  25. package/dist/lib/enfyra-mcp-server.js.map +1 -0
  26. package/dist/lib/enfyra-tool-logic.d.ts +7 -0
  27. package/dist/lib/enfyra-tool-logic.js +8 -0
  28. package/dist/lib/enfyra-tool-logic.js.map +1 -0
  29. package/dist/lib/enfyra-tool-types.d.ts +27 -0
  30. package/dist/lib/enfyra-tool-types.js +2 -0
  31. package/dist/lib/enfyra-tool-types.js.map +1 -0
  32. package/dist/lib/extension-api-builders.d.ts +17 -0
  33. package/dist/lib/extension-api-builders.js +235 -0
  34. package/dist/lib/extension-api-builders.js.map +1 -0
  35. package/dist/lib/extension-component-builders.d.ts +87 -0
  36. package/dist/lib/extension-component-builders.js +623 -0
  37. package/dist/lib/extension-component-builders.js.map +1 -0
  38. package/dist/lib/extension-contract-review.d.ts +26 -0
  39. package/dist/lib/extension-contract-review.js +187 -0
  40. package/dist/lib/extension-contract-review.js.map +1 -0
  41. package/dist/lib/extension-theme-contract.d.ts +63 -0
  42. package/dist/lib/extension-theme-contract.js +417 -0
  43. package/dist/lib/extension-theme-contract.js.map +1 -0
  44. package/dist/lib/identity-tools.d.ts +4 -0
  45. package/dist/lib/identity-tools.js +46 -0
  46. package/dist/lib/identity-tools.js.map +1 -0
  47. package/dist/lib/log-tools.d.ts +4 -0
  48. package/dist/lib/log-tools.js +65 -0
  49. package/dist/lib/log-tools.js.map +1 -0
  50. package/dist/lib/mcp-example-connect.d.ts +9 -0
  51. package/dist/lib/mcp-example-connect.js +442 -0
  52. package/dist/lib/mcp-example-connect.js.map +1 -0
  53. package/dist/lib/mcp-example-extensions.d.ts +9 -0
  54. package/dist/lib/mcp-example-extensions.js +729 -0
  55. package/dist/lib/mcp-example-extensions.js.map +1 -0
  56. package/dist/lib/mcp-example-files.d.ts +9 -0
  57. package/dist/lib/mcp-example-files.js +51 -0
  58. package/dist/lib/mcp-example-files.js.map +1 -0
  59. package/dist/lib/mcp-example-flows.d.ts +9 -0
  60. package/dist/lib/mcp-example-flows.js +76 -0
  61. package/dist/lib/mcp-example-flows.js.map +1 -0
  62. package/dist/lib/mcp-example-graphql.d.ts +9 -0
  63. package/dist/lib/mcp-example-graphql.js +57 -0
  64. package/dist/lib/mcp-example-graphql.js.map +1 -0
  65. package/dist/lib/mcp-example-handlers-hooks.d.ts +9 -0
  66. package/dist/lib/mcp-example-handlers-hooks.js +328 -0
  67. package/dist/lib/mcp-example-handlers-hooks.js.map +1 -0
  68. package/dist/lib/mcp-example-oauth-setup.d.ts +9 -0
  69. package/dist/lib/mcp-example-oauth-setup.js +41 -0
  70. package/dist/lib/mcp-example-oauth-setup.js.map +1 -0
  71. package/dist/lib/mcp-example-permissions-rls.d.ts +9 -0
  72. package/dist/lib/mcp-example-permissions-rls.js +210 -0
  73. package/dist/lib/mcp-example-permissions-rls.js.map +1 -0
  74. package/dist/lib/mcp-example-queries-deep.d.ts +9 -0
  75. package/dist/lib/mcp-example-queries-deep.js +220 -0
  76. package/dist/lib/mcp-example-queries-deep.js.map +1 -0
  77. package/dist/lib/mcp-example-schema-relations.d.ts +9 -0
  78. package/dist/lib/mcp-example-schema-relations.js +148 -0
  79. package/dist/lib/mcp-example-schema-relations.js.map +1 -0
  80. package/dist/lib/mcp-example-websocket.d.ts +9 -0
  81. package/dist/lib/mcp-example-websocket.js +103 -0
  82. package/dist/lib/mcp-example-websocket.js.map +1 -0
  83. package/dist/lib/mcp-examples.d.ts +5 -5
  84. package/dist/lib/mcp-examples.js +22 -2394
  85. package/dist/lib/mcp-examples.js.map +1 -1
  86. package/dist/lib/method-tools.d.ts +4 -0
  87. package/dist/lib/method-tools.js +217 -0
  88. package/dist/lib/method-tools.js.map +1 -0
  89. package/dist/lib/package-tools.d.ts +4 -0
  90. package/dist/lib/package-tools.js +98 -0
  91. package/dist/lib/package-tools.js.map +1 -0
  92. package/dist/lib/platform-data-operations.d.ts +24 -0
  93. package/dist/lib/platform-data-operations.js +71 -0
  94. package/dist/lib/platform-data-operations.js.map +1 -0
  95. package/dist/lib/platform-endpoint-workflow.d.ts +42 -0
  96. package/dist/lib/platform-endpoint-workflow.js +382 -0
  97. package/dist/lib/platform-endpoint-workflow.js.map +1 -0
  98. package/dist/lib/platform-extension-source.d.ts +487 -0
  99. package/dist/lib/platform-extension-source.js +524 -0
  100. package/dist/lib/platform-extension-source.js.map +1 -0
  101. package/dist/lib/platform-extension-tools.d.ts +1 -0
  102. package/dist/lib/platform-extension-tools.js +416 -0
  103. package/dist/lib/platform-extension-tools.js.map +1 -0
  104. package/dist/lib/platform-extension-ui.d.ts +4 -0
  105. package/dist/lib/platform-extension-ui.js +5 -0
  106. package/dist/lib/platform-extension-ui.js.map +1 -0
  107. package/dist/lib/platform-extension-workflow.d.ts +54 -0
  108. package/dist/lib/platform-extension-workflow.js +194 -0
  109. package/dist/lib/platform-extension-workflow.js.map +1 -0
  110. package/dist/lib/platform-flow-operations.d.ts +554 -0
  111. package/dist/lib/platform-flow-operations.js +317 -0
  112. package/dist/lib/platform-flow-operations.js.map +1 -0
  113. package/dist/lib/platform-flow-tools.d.ts +1 -0
  114. package/dist/lib/platform-flow-tools.js +248 -0
  115. package/dist/lib/platform-flow-tools.js.map +1 -0
  116. package/dist/lib/platform-operation-logic.d.ts +10 -0
  117. package/dist/lib/platform-operation-logic.js +11 -0
  118. package/dist/lib/platform-operation-logic.js.map +1 -0
  119. package/dist/lib/platform-operation-tools.d.ts +1 -324
  120. package/dist/lib/platform-operation-tools.js +13 -4620
  121. package/dist/lib/platform-operation-tools.js.map +1 -1
  122. package/dist/lib/platform-operation-types.d.ts +46 -0
  123. package/dist/lib/platform-operation-types.js +2 -0
  124. package/dist/lib/platform-operation-types.js.map +1 -0
  125. package/dist/lib/platform-policy-tools.d.ts +1 -0
  126. package/dist/lib/platform-policy-tools.js +248 -0
  127. package/dist/lib/platform-policy-tools.js.map +1 -0
  128. package/dist/lib/platform-resource-operations.d.ts +124 -0
  129. package/dist/lib/platform-resource-operations.js +109 -0
  130. package/dist/lib/platform-resource-operations.js.map +1 -0
  131. package/dist/lib/platform-resource-tools.d.ts +1 -0
  132. package/dist/lib/platform-resource-tools.js +70 -0
  133. package/dist/lib/platform-resource-tools.js.map +1 -0
  134. package/dist/lib/platform-route-operations.d.ts +222 -0
  135. package/dist/lib/platform-route-operations.js +290 -0
  136. package/dist/lib/platform-route-operations.js.map +1 -0
  137. package/dist/lib/platform-route-tools.d.ts +1 -0
  138. package/dist/lib/platform-route-tools.js +307 -0
  139. package/dist/lib/platform-route-tools.js.map +1 -0
  140. package/dist/lib/platform-shared-operations.d.ts +7 -0
  141. package/dist/lib/platform-shared-operations.js +4 -0
  142. package/dist/lib/platform-shared-operations.js.map +1 -0
  143. package/dist/lib/platform-websocket-tools.d.ts +1 -0
  144. package/dist/lib/platform-websocket-tools.js +70 -0
  145. package/dist/lib/platform-websocket-tools.js.map +1 -0
  146. package/dist/lib/record-tools.d.ts +4 -0
  147. package/dist/lib/record-tools.js +418 -0
  148. package/dist/lib/record-tools.js.map +1 -0
  149. package/dist/lib/required-knowledge.d.ts +1 -1
  150. package/dist/lib/required-knowledge.js +3 -2
  151. package/dist/lib/required-knowledge.js.map +1 -1
  152. package/dist/lib/route-access-tools.d.ts +4 -0
  153. package/dist/lib/route-access-tools.js +172 -0
  154. package/dist/lib/route-access-tools.js.map +1 -0
  155. package/dist/lib/route-definition-tools.d.ts +4 -0
  156. package/dist/lib/route-definition-tools.js +285 -0
  157. package/dist/lib/route-definition-tools.js.map +1 -0
  158. package/dist/lib/route-inspection-tools.d.ts +4 -0
  159. package/dist/lib/route-inspection-tools.js +319 -0
  160. package/dist/lib/route-inspection-tools.js.map +1 -0
  161. package/dist/lib/route-tools.d.ts +1 -0
  162. package/dist/lib/route-tools.js +9 -0
  163. package/dist/lib/route-tools.js.map +1 -0
  164. package/dist/lib/runtime-zone-registry.d.ts +27 -0
  165. package/dist/lib/runtime-zone-registry.js +81 -0
  166. package/dist/lib/runtime-zone-registry.js.map +1 -0
  167. package/dist/lib/runtime-zone-search.d.ts +303 -0
  168. package/dist/lib/runtime-zone-search.js +596 -0
  169. package/dist/lib/runtime-zone-search.js.map +1 -0
  170. package/dist/lib/runtime-zone-tools.d.ts +2 -307
  171. package/dist/lib/runtime-zone-tools.js +2 -674
  172. package/dist/lib/runtime-zone-tools.js.map +1 -1
  173. package/dist/lib/schema-column-tools.d.ts +3 -0
  174. package/dist/lib/schema-column-tools.js +80 -0
  175. package/dist/lib/schema-column-tools.js.map +1 -0
  176. package/dist/lib/schema-mutation-coordinator.d.ts +28 -0
  177. package/dist/lib/schema-mutation-coordinator.js +154 -0
  178. package/dist/lib/schema-mutation-coordinator.js.map +1 -0
  179. package/dist/lib/schema-relation-contracts.d.ts +39 -0
  180. package/dist/lib/schema-relation-contracts.js +160 -0
  181. package/dist/lib/schema-relation-contracts.js.map +1 -0
  182. package/dist/lib/schema-relation-tools.d.ts +3 -0
  183. package/dist/lib/schema-relation-tools.js +61 -0
  184. package/dist/lib/schema-relation-tools.js.map +1 -0
  185. package/dist/lib/schema-table-contracts.d.ts +28 -0
  186. package/dist/lib/schema-table-contracts.js +263 -0
  187. package/dist/lib/schema-table-contracts.js.map +1 -0
  188. package/dist/lib/schema-table-tools.d.ts +3 -0
  189. package/dist/lib/schema-table-tools.js +297 -0
  190. package/dist/lib/schema-table-tools.js.map +1 -0
  191. package/dist/lib/schema-tool-operations.d.ts +150 -0
  192. package/dist/lib/schema-tool-operations.js +468 -0
  193. package/dist/lib/schema-tool-operations.js.map +1 -0
  194. package/dist/lib/schema-tool-types.d.ts +38 -0
  195. package/dist/lib/schema-tool-types.js +2 -0
  196. package/dist/lib/schema-tool-types.js.map +1 -0
  197. package/dist/lib/schema-verification.d.ts +14 -0
  198. package/dist/lib/schema-verification.js +105 -0
  199. package/dist/lib/schema-verification.js.map +1 -0
  200. package/dist/lib/script-tools.d.ts +4 -0
  201. package/dist/lib/script-tools.js +127 -0
  202. package/dist/lib/script-tools.js.map +1 -0
  203. package/dist/lib/system-tools.d.ts +4 -0
  204. package/dist/lib/system-tools.js +41 -0
  205. package/dist/lib/system-tools.js.map +1 -0
  206. package/dist/lib/table-tool-logic.d.ts +5 -0
  207. package/dist/lib/table-tool-logic.js +6 -0
  208. package/dist/lib/table-tool-logic.js.map +1 -0
  209. package/dist/lib/table-tools.d.ts +1 -84
  210. package/dist/lib/table-tools.js +7 -1522
  211. package/dist/lib/table-tools.js.map +1 -1
  212. package/dist/lib/tool-catalog.d.ts +2 -1
  213. package/dist/lib/tool-catalog.js +23 -2
  214. package/dist/lib/tool-catalog.js.map +1 -1
  215. package/dist/lib/tool-metadata-operations.d.ts +27 -0
  216. package/dist/lib/tool-metadata-operations.js +93 -0
  217. package/dist/lib/tool-metadata-operations.js.map +1 -0
  218. package/dist/lib/tool-permission-profile.d.ts +37 -0
  219. package/dist/lib/tool-permission-profile.js +106 -0
  220. package/dist/lib/tool-permission-profile.js.map +1 -0
  221. package/dist/lib/tool-record-operations.d.ts +102 -0
  222. package/dist/lib/tool-record-operations.js +260 -0
  223. package/dist/lib/tool-record-operations.js.map +1 -0
  224. package/dist/lib/tool-route-inspection.d.ts +47 -0
  225. package/dist/lib/tool-route-inspection.js +168 -0
  226. package/dist/lib/tool-route-inspection.js.map +1 -0
  227. package/dist/lib/tool-routing.d.ts +3 -417
  228. package/dist/lib/tool-routing.js +3 -1038
  229. package/dist/lib/tool-routing.js.map +1 -1
  230. package/dist/lib/tool-runtime-config.d.ts +33 -0
  231. package/dist/lib/tool-runtime-config.js +237 -0
  232. package/dist/lib/tool-runtime-config.js.map +1 -0
  233. package/dist/lib/tool-script-operations.d.ts +34 -0
  234. package/dist/lib/tool-script-operations.js +153 -0
  235. package/dist/lib/tool-script-operations.js.map +1 -0
  236. package/dist/lib/workflow-definitions.d.ts +366 -0
  237. package/dist/lib/workflow-definitions.js +703 -0
  238. package/dist/lib/workflow-definitions.js.map +1 -0
  239. package/dist/lib/workflow-planner.d.ts +45 -0
  240. package/dist/lib/workflow-planner.js +338 -0
  241. package/dist/lib/workflow-planner.js.map +1 -0
  242. package/dist/lib/workflow-types.d.ts +39 -0
  243. package/dist/lib/workflow-types.js +2 -0
  244. package/dist/lib/workflow-types.js.map +1 -0
  245. package/dist/mcp-server-entry.d.ts +0 -3
  246. package/dist/mcp-server-entry.js +2 -3249
  247. package/dist/mcp-server-entry.js.map +1 -1
  248. package/package.json +1 -1
@@ -1,1525 +1,10 @@
1
- /**
2
- * Table & Column tools for Enfyra MCP Server
3
- */
4
- import { z } from 'zod';
5
- import { fetchAPI } from './fetch.js';
6
- import { fetchMetadataContext, fetchTableCatalog, fetchTableMetadata, resolveTableCatalogEntry, } from './metadata-client.js';
7
- import { jsonContent } from './response-format.js';
8
- import { assertGlobalRulesAck, globalRulesAckParam } from './required-knowledge.js';
9
- import { normalizeTableName } from './tool-input-normalization.js';
10
- function bulkObjectArrayParam(z, label) {
11
- return z.array(z.record(z.any())).describe(`${label} as a native JSON array of objects. Pass one object in the array for a single mutation.`);
12
- }
13
- let schemaQueue = Promise.resolve();
14
- export function assertColumnContractBroadening(existingColumn, requested, toolset = 'guided') {
15
- const broadened = [];
16
- if (existingColumn?.isUpdatable === false && requested?.isUpdatable === true)
17
- broadened.push('isUpdatable false→true');
18
- if (existingColumn?.isPublished === false && requested?.isPublished === true)
19
- broadened.push('isPublished false→true');
20
- if (broadened.length > 0 && (toolset !== 'full' || requested?.allowContractBroadening !== true)) {
21
- throw new Error(`Column contract broadening is blocked in the guided toolset: ${broadened.join(', ')}. Do not broaden canonical metadata merely for a custom action or E2E fixture; use an exact trusted internal write after authorization instead. Expert full-toolset changes additionally require allowContractBroadening=true.`);
22
- }
23
- return broadened;
24
- }
25
- function withSchemaQueue(operation) {
26
- const run = schemaQueue.then(operation, operation);
27
- schemaQueue = run.catch(() => { });
28
- return run;
29
- }
30
- const FORBIDDEN_RELATION_KEYS = [
31
- 'fkCol',
32
- 'fkColumn',
33
- 'foreignKeyColumn',
34
- 'referencedColumn',
35
- 'constraintName',
36
- 'sourceColumn',
37
- 'targetColumn',
38
- 'junctionTableName',
39
- 'junctionSourceColumn',
40
- 'junctionTargetColumn',
41
- ];
42
- const FALLBACK_COLUMN_TYPES = [
43
- 'int',
44
- 'varchar',
45
- 'text',
46
- 'boolean',
47
- 'uuid',
48
- 'ObjectId',
49
- 'bigint',
50
- 'date',
51
- 'datetime',
52
- 'timestamp',
53
- 'enum',
54
- 'simple-json',
55
- 'code',
56
- 'array-select',
57
- 'richtext',
58
- 'float',
59
- ];
60
- const RELATION_TYPE_ALIASES = {
61
- many_to_one: 'many-to-one',
62
- manyToOne: 'many-to-one',
63
- manytoone: 'many-to-one',
64
- one_to_many: 'one-to-many',
65
- oneToMany: 'one-to-many',
66
- onetomany: 'one-to-many',
67
- one_to_one: 'one-to-one',
68
- oneToOne: 'one-to-one',
69
- onetoone: 'one-to-one',
70
- many_to_many: 'many-to-many',
71
- manyToMany: 'many-to-many',
72
- manytomany: 'many-to-many',
73
- };
74
- const VALID_RELATION_TYPES = new Set(['many-to-one', 'one-to-many', 'one-to-one', 'many-to-many']);
75
- const AUTO_MANAGED_COLUMN_NAMES = new Set(['id', '_id', 'createdAt', 'updatedAt']);
76
- export function buildPrimaryColumnForDbType(dbType) {
77
- return dbType === 'mongodb'
78
- ? { name: '_id', type: 'ObjectId', isPrimary: true, isGenerated: true, isNullable: false }
79
- : { name: 'id', type: 'int', isPrimary: true, isGenerated: true, isNullable: false };
80
- }
81
- const COLUMN_TYPE_ALIAS_HINTS = [
82
- 'Use varchar for short strings; text or richtext for long prose.',
83
- 'Use float for prices, money, percentages, ratings, and decimal-like numbers unless the live instance explicitly lists decimal.',
84
- 'Use simple-json for structured objects/arrays when the live instance lists it; do not use json/jsonb as column types.',
85
- 'Use relations for links to other records; do not create userId/course_id/categoryIds columns for normalized relationships.',
86
- ];
87
- export function normalizeTablesFromMetadata(metadata) {
88
- if (Array.isArray(metadata))
89
- return metadata;
90
- if (metadata?.data?.name && Array.isArray(metadata.data.columns))
91
- return [metadata.data];
92
- if (metadata?.name && Array.isArray(metadata.columns))
93
- return [metadata];
94
- const tablesSource = metadata?.data?.tables || metadata?.tables || metadata?.data || [];
95
- return Array.isArray(tablesSource)
96
- ? tablesSource
97
- : Object.values(tablesSource || {});
98
- }
99
- export function resolveTableFromMetadata(metadata, tableId) {
100
- return normalizeTablesFromMetadata(metadata)
101
- .find((table) => String(getId(table)) === String(tableId)) || null;
102
- }
103
- export function resolveTableFromMetadataByName(metadata, tableName) {
104
- if (!tableName)
105
- return null;
106
- return normalizeTablesFromMetadata(metadata)
107
- .find((table) => table?.name === tableName || table?.alias === tableName) || null;
108
- }
109
- export function resolveTableIdentifierFromMetadata(metadata, tableRef, label = 'table') {
110
- const resolvedTable = normalizeTablesFromMetadata(metadata)
111
- .find((table) => (String(getId(table)) === String(tableRef) ||
112
- table?.name === tableRef ||
113
- table?.alias === tableRef));
114
- if (!resolvedTable) {
115
- throw new Error(`${label} "${tableRef}" was not found in metadata. Pass an existing table id, name, or alias from get_all_tables/inspect_table.`);
116
- }
117
- return getId(resolvedTable);
118
- }
119
- /**
120
- * Helper: fetch table with full columns and relations.
121
- * Schema cascade tools resolve the table catalog entry first, then request the
122
- * complete permission-projected schema from /metadata/:name.
123
- */
124
- export async function fetchTableWithDetails(ENFYRA_API_URL, tableId) {
125
- const catalog = await fetchTableCatalog(ENFYRA_API_URL);
126
- const tableData = resolveTableCatalogEntry(catalog, tableId);
127
- if (!tableData) {
128
- throw new Error(`Full metadata for table ${tableId} was not found; refusing schema cascade patch.`);
129
- }
130
- const metadataTable = await fetchTableMetadata(ENFYRA_API_URL, tableData.name);
131
- if (!Array.isArray(metadataTable.columns)) {
132
- throw new Error(`Full metadata for table ${tableId} did not include columns; refusing schema cascade patch.`);
133
- }
134
- return {
135
- ...tableData,
136
- ...metadataTable,
137
- columns: metadataTable.columns,
138
- relations: Array.isArray(metadataTable.relations) ? metadataTable.relations : [],
139
- };
140
- }
141
- /**
142
- * PATCH enfyra_table with auto-confirm for schema changes.
143
- * First PATCH returns preview + requiredConfirmHash; this helper
144
- * automatically resends with ?schemaConfirmHash= to apply.
145
- */
146
- async function patchTableAutoConfirm(ENFYRA_API_URL, tableId, body) {
147
- const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_table/${tableId}`, {
148
- method: 'PATCH',
149
- body: JSON.stringify(body),
150
- });
151
- const preview = Array.isArray(result?.data) ? result.data[0] : result?.data;
152
- if (preview?._preview && preview?.requiredConfirmHash) {
153
- return fetchAPI(ENFYRA_API_URL, `/enfyra_table/${tableId}?schemaConfirmHash=${preview.requiredConfirmHash}`, {
154
- method: 'PATCH',
155
- body: JSON.stringify(body),
156
- });
157
- }
158
- return result;
159
- }
160
- function parseJsonArrayParam(name, value) {
161
- if (!value)
162
- return [];
163
- const parsed = JSON.parse(value);
164
- if (!Array.isArray(parsed)) {
165
- throw new Error(`${name} must be a JSON array.`);
166
- }
167
- return parsed;
168
- }
169
- function parseBulkItemsParam(name, value) {
170
- if (value === undefined || value === null || value === '') {
171
- throw new Error(`${name} must be a native JSON array. Pass one object in the array for a single mutation.`);
172
- }
173
- const parsed = typeof value === 'string' ? JSON.parse(value) : value;
174
- if (!Array.isArray(parsed)) {
175
- throw new Error(`${name} must be a JSON array. Pass one object in the array for a single mutation.`);
176
- }
177
- if (parsed.length === 0) {
178
- throw new Error(`${name} must include at least one item.`);
179
- }
180
- parsed.forEach((item, index) => {
181
- if (!item || typeof item !== 'object' || Array.isArray(item)) {
182
- throw new Error(`${name}[${index}] must be an object.`);
183
- }
184
- });
185
- return parsed;
186
- }
187
- function assertBulkLimit(name, items, maxItems) {
188
- if (items.length > maxItems) {
189
- throw new Error(`${name} received ${items.length} items, above maxItems=${maxItems}. Split the batch deliberately.`);
190
- }
191
- }
192
- function normalizeConstraintGroups(name, groups) {
193
- return groups.map((group, index) => {
194
- const value = Array.isArray(group) ? group : group?.value;
195
- if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string' || !item.trim())) {
196
- throw new Error(`${name}[${index}] must be a non-empty string array or { "value": [...] }.`);
197
- }
198
- return value;
199
- });
200
- }
201
- function parseConstraintGroupsParam(name, value) {
202
- return normalizeConstraintGroups(name, parseJsonArrayParam(name, value));
203
- }
204
- function normalizeConstraintGroupsValue(name, value) {
205
- if (value == null)
206
- return [];
207
- const parsed = typeof value === 'string' ? JSON.parse(value) : value;
208
- if (!Array.isArray(parsed)) {
209
- throw new Error(`${name} must be a JSON array.`);
210
- }
211
- return normalizeConstraintGroups(name, parsed);
212
- }
213
- function stripAutoManagedColumns(columns) {
214
- const skippedAutoColumns = [];
215
- const filtered = columns.filter((column) => {
216
- const name = String(column?.name ?? '');
217
- if (!AUTO_MANAGED_COLUMN_NAMES.has(name))
218
- return true;
219
- skippedAutoColumns.push({
220
- name,
221
- reason: 'Enfyra manages id/createdAt/updatedAt automatically during table creation.',
222
- });
223
- return false;
224
- });
225
- return { columns: filtered, skippedAutoColumns };
226
- }
227
- function assertColumnNameCanBeCreated(name, context) {
228
- const columnName = String(name ?? '');
229
- if (AUTO_MANAGED_COLUMN_NAMES.has(columnName)) {
230
- throw new Error(`${context} "${columnName}" is auto-managed by Enfyra. Do not create id, _id, createdAt, or updatedAt columns manually.`);
231
- }
232
- }
233
- function assertNoDuplicateFieldNames(fieldNames, context) {
234
- const seen = new Set();
235
- const duplicates = new Set();
236
- for (const name of fieldNames.filter(Boolean)) {
237
- if (seen.has(name))
238
- duplicates.add(name);
239
- seen.add(name);
240
- }
241
- if (duplicates.size > 0) {
242
- throw new Error(`${context} has duplicate field name(s): ${[...duplicates].join(', ')}. Column names and relation propertyName values share one namespace.`);
243
- }
244
- }
245
- function assertNoColumnRelationNameCollision(columnNames, relationNames, context) {
246
- const relationNameSet = new Set(relationNames.filter(Boolean));
247
- const collisions = columnNames.filter((name) => relationNameSet.has(name));
248
- if (collisions.length > 0) {
249
- throw new Error(`${context} has column/relation namespace collision(s): ${[...new Set(collisions)].join(', ')}. ` +
250
- `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.`);
251
- }
252
- assertNoDuplicateFieldNames([...columnNames, ...relationNames], context);
253
- }
254
- function formatConstraintFieldHints(fields, relationNames, logicalFieldNames) {
255
- const relationNameSet = new Set(relationNames);
256
- const logicalNameSet = new Set(logicalFieldNames);
257
- const normalizeName = (value) => value.replace(/[_\-\s]/gu, '').toLowerCase();
258
- return fields
259
- .map((field) => {
260
- const normalized = String(field)
261
- .replace(/_?ids?$/iu, '')
262
- .replace(/_id$/iu, '')
263
- .replace(/Id$/u, '')
264
- .replace(/Ids$/u, '');
265
- const match = [...relationNameSet].find((name) => name.toLowerCase() === normalized.toLowerCase());
266
- if (match)
267
- return `${field} -> use relation propertyName "${match}" in indexes/uniques, not physical FK "${field}"`;
268
- const logicalMatch = [...logicalNameSet].find((name) => normalizeName(name) === normalizeName(String(field)));
269
- return logicalMatch ? `${field} -> did you mean "${logicalMatch}"? Constraint fields must match column/relation names exactly.` : null;
270
- })
271
- .filter(Boolean);
272
- }
273
- function preflightCreateTableDefinitions(items) {
274
- items.forEach((item, index) => {
275
- const columns = Array.isArray(item.columns) ? item.columns : parseJsonArrayParam(`items[${index}].columns`, item.columns || '[]');
276
- const relations = Array.isArray(item.relations) ? item.relations : parseJsonArrayParam(`items[${index}].relations`, item.relations || '[]');
277
- const { columns: userColumns } = stripAutoManagedColumns(columns);
278
- const columnNames = userColumns.map((column) => String(column?.name ?? '')).filter(Boolean);
279
- const relationNames = relations.map((relation) => String(relation?.propertyName ?? '')).filter(Boolean);
280
- assertNoColumnRelationNameCollision(columnNames, relationNames, `create_tables items[${index}] (${item.name || '<unnamed>'})`);
281
- const logicalFields = new Set([...AUTO_MANAGED_COLUMN_NAMES, ...columnNames, ...relationNames]);
282
- const indexes = normalizeConstraintGroupsValue(`items[${index}].indexes`, item.indexes ?? []);
283
- const uniques = normalizeConstraintGroupsValue(`items[${index}].uniques`, item.uniques ?? []);
284
- assertIndexesDoNotReferenceUniqueFields(indexes, uniques);
285
- const unknownConstraintFields = [...indexes, ...uniques]
286
- .flat()
287
- .filter((field) => !logicalFields.has(field));
288
- if (unknownConstraintFields.length > 0) {
289
- const uniqueUnknownFields = [...new Set(unknownConstraintFields)];
290
- const hints = formatConstraintFieldHints(uniqueUnknownFields, relationNames, [...logicalFields]);
291
- throw new Error(`create_tables items[${index}] (${item.name || '<unnamed>'}) has indexes/uniques referencing undeclared field(s): ${uniqueUnknownFields.join(', ')}. ` +
292
- 'Declare each field as a column or relation propertyName in the same table item. ' +
293
- (hints.length ? `Hint(s): ${hints.join('; ')}. ` : '') +
294
- 'For relation-based unique pairs added after create, first create the relations, then call update_tables with the unique group.');
295
- }
296
- });
297
- }
298
- export function normalizeCreateTableDefinitions(items) {
299
- const batchNameMap = new Map(items
300
- .map((item) => String(item?.name ?? ''))
301
- .filter(Boolean)
302
- .map((name) => [name.toLowerCase(), normalizeTableName(name)]));
303
- return items.map((item) => {
304
- const originalName = String(item?.name ?? '');
305
- const name = normalizeTableName(originalName);
306
- const relations = Array.isArray(item?.relations)
307
- ? item.relations.map((relation) => {
308
- const target = relation?.targetTable ?? relation?.targetTableId;
309
- if (typeof target !== 'string')
310
- return relation;
311
- const normalizedTarget = batchNameMap.get(target.toLowerCase());
312
- if (!normalizedTarget || normalizedTarget === target)
313
- return relation;
314
- return relation?.targetTable !== undefined
315
- ? { ...relation, targetTable: normalizedTarget }
316
- : { ...relation, targetTableId: normalizedTarget };
317
- })
318
- : item?.relations;
319
- return {
320
- ...item,
321
- name,
322
- ...(relations !== undefined ? { relations } : {}),
323
- ...(name !== originalName ? { _requestedTableName: originalName } : {}),
324
- };
325
- });
326
- }
327
- function relationTargetName(relation) {
328
- const target = relation?.targetTable ?? relation?.targetTableId;
329
- if (target && typeof target === 'object')
330
- return target.name ?? target.alias ?? target.id ?? target._id;
331
- return target;
332
- }
333
- export function computeBatchCleanupOrder(items) {
334
- const tableNames = items.map((item) => String(item?.name || '')).filter(Boolean);
335
- const tableSet = new Set(tableNames);
336
- const edges = [];
337
- for (const item of items) {
338
- const source = String(item?.name || '');
339
- if (!source)
340
- continue;
341
- const relations = Array.isArray(item.relations) ? item.relations : parseJsonArrayParam(`${source}.relations`, item.relations || '[]');
342
- for (const relation of relations) {
343
- const target = String(relationTargetName(relation) || '');
344
- if (target && tableSet.has(target) && target !== source)
345
- edges.push([source, target]);
346
- }
347
- }
348
- const remaining = new Set(tableNames);
349
- const ordered = [];
350
- while (remaining.size > 0) {
351
- const leaves = [...remaining]
352
- .filter((name) => !edges.some(([source, target]) => target === name && remaining.has(source) && remaining.has(target)))
353
- .sort();
354
- const batch = leaves.length ? leaves : [[...remaining].sort()[0]];
355
- for (const name of batch) {
356
- remaining.delete(name);
357
- ordered.push(name);
358
- }
359
- }
360
- return ordered;
361
- }
362
- function splitRelationConstraintGroups(groups, relationNames) {
363
- const immediate = [];
364
- const deferred = [];
365
- for (const group of groups) {
366
- if (group.some((field) => relationNames.has(field)))
367
- deferred.push(group);
368
- else
369
- immediate.push(group);
370
- }
371
- return { immediate, deferred };
372
- }
373
- function mergeConstraintGroups(existing, additions) {
374
- const seen = new Set(existing.map((group) => JSON.stringify(group)));
375
- const merged = [...existing];
376
- for (const group of additions) {
377
- const key = JSON.stringify(group);
378
- if (!seen.has(key)) {
379
- seen.add(key);
380
- merged.push(group);
381
- }
382
- }
383
- return merged;
384
- }
385
- function resolveRelationConstraintGroups(table, groups, groupName) {
386
- const relationByProperty = new Map((table.relations || [])
387
- .filter((relation) => relation?.propertyName)
388
- .map((relation) => [relation.propertyName, relation]));
389
- return groups.map((group) => group.map((field) => {
390
- const relation = relationByProperty.get(field);
391
- if (!relation)
392
- return field;
393
- const physicalColumn = relation.foreignKeyColumn || relation.fkColumn || relation.fkCol;
394
- if (!physicalColumn) {
395
- throw new Error(`${groupName} uses relation propertyName "${field}", but the created relation did not expose a physical FK column for indexing. ` +
396
- 'This usually means the relation is not a direct many-to-one/one-to-one relation. Keep relation indexes/uniques only on direct owning relations.');
397
- }
398
- return physicalColumn;
399
- }));
400
- }
401
- export function assertIndexesDoNotReferenceUniqueFields(indexes, uniques) {
402
- const conflicts = indexes
403
- .map((indexGroup) => ({
404
- indexGroup,
405
- uniqueGroups: uniques
406
- .map((uniqueGroup) => ({
407
- uniqueGroup,
408
- overlappingFields: indexGroup.filter((field) => uniqueGroup.includes(field)),
409
- }))
410
- .filter((conflict) => conflict.overlappingFields.length > 0),
411
- }))
412
- .filter((conflict) => conflict.uniqueGroups.length > 0);
413
- if (conflicts.length > 0) {
414
- const groups = conflicts
415
- .map((conflict) => `${JSON.stringify(conflict.indexGroup)} overlaps unique group(s) ${conflict.uniqueGroups.map((item) => `${JSON.stringify(item.uniqueGroup)} via ${JSON.stringify(item.overlappingFields)}`).join(', ')}`)
416
- .join('; ');
417
- 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.`);
418
- }
419
- }
420
- function pruneIndexesThatOverlapUniques(indexes, uniques) {
421
- const uniqueFields = new Set(uniques.flat());
422
- const kept = [];
423
- const removed = [];
424
- for (const indexGroup of indexes) {
425
- if (indexGroup.some((field) => uniqueFields.has(field))) {
426
- removed.push(indexGroup);
427
- }
428
- else {
429
- kept.push(indexGroup);
430
- }
431
- }
432
- return { indexes: kept, removed };
433
- }
434
- export function normalizeRelationForTablePatch(relation) {
435
- for (const key of FORBIDDEN_RELATION_KEYS) {
436
- if (Object.prototype.hasOwnProperty.call(relation, key)) {
437
- throw new Error(`Relation schema must not include physical column field "${key}". Use propertyName/targetTable only; Enfyra derives FK and junction columns.`);
438
- }
439
- }
440
- const { sourceTable, targetTable, targetTableId, mappedBy, fkCol, fkColumn, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, junctionTargetColumn, ...rest } = relation;
441
- const normalized = { ...rest };
442
- if (rest.type !== undefined) {
443
- normalized.type = normalizeRelationType(rest.type);
444
- }
445
- if (normalized.type === 'one-to-many' && mappedBy !== undefined && mappedBy !== null && mappedBy !== '') {
446
- delete normalized.inversePropertyName;
447
- }
448
- const resolvedTargetTable = targetTableId ??
449
- (targetTable && typeof targetTable === 'object'
450
- ? targetTable.id ?? targetTable._id ?? targetTable
451
- : targetTable);
452
- if (resolvedTargetTable !== undefined && resolvedTargetTable !== null) {
453
- normalized.targetTable = resolvedTargetTable;
454
- }
455
- if (mappedBy !== undefined && mappedBy !== null && mappedBy !== '') {
456
- normalized.mappedBy = typeof mappedBy === 'object'
457
- ? mappedBy.propertyName ?? mappedBy.name ?? mappedBy.id ?? mappedBy._id
458
- : mappedBy;
459
- }
460
- return normalized;
461
- }
462
- export function normalizeRelationType(type) {
463
- const raw = String(type ?? '').trim();
464
- const normalized = RELATION_TYPE_ALIASES[raw] ?? raw;
465
- if (!VALID_RELATION_TYPES.has(normalized)) {
466
- 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.`);
467
- }
468
- return normalized;
469
- }
470
- function assertNoForbiddenRelationKeys(args) {
471
- for (const key of FORBIDDEN_RELATION_KEYS) {
472
- if (Object.prototype.hasOwnProperty.call(args, key)) {
473
- throw new Error(`create_relations must not include physical column field "${key}". Use sourceTableId/targetTableId and relation propertyName only; Enfyra derives FK and junction columns.`);
474
- }
475
- }
476
- }
477
- export function sanitizeExistingRelationForTablePatch(relation) {
478
- const { fkCol, fkColumn, foreignKeyColumn, referencedColumn, constraintName, sourceColumn, targetColumn, junctionTableName, junctionSourceColumn, junctionTargetColumn, ...rest } = relation;
479
- return normalizeRelationForTablePatch(rest);
480
- }
481
- export function resolveRelationTargetsFromMetadata(metadata, relations) {
482
- return relations.map((relation) => {
483
- const targetTable = relation.targetTable;
484
- if (typeof targetTable !== 'string' || !targetTable.trim())
485
- return relation;
486
- const resolvedTable = resolveTableFromMetadataByName(metadata, targetTable);
487
- if (!resolvedTable)
488
- return relation;
489
- return { ...relation, targetTable: getId(resolvedTable) };
490
- });
491
- }
492
- function getId(record) {
493
- return record?.id ?? record?._id ?? null;
494
- }
495
- function normalizeColumnForTablePatch(column) {
496
- const { table, ...rest } = column;
497
- return rest;
498
- }
499
- function parseColumnTypeOptions(options) {
500
- if (Array.isArray(options))
501
- return options.map(String);
502
- if (typeof options !== 'string')
503
- return [];
504
- const trimmed = options.trim();
505
- if (!trimmed)
506
- return [];
507
- try {
508
- const parsed = JSON.parse(trimmed);
509
- if (Array.isArray(parsed))
510
- return parsed.map(String);
511
- }
512
- catch {
513
- // Some Enfyra enum metadata is stored as {"a","b"} rather than JSON.
514
- }
515
- const braceMatch = trimmed.match(/^\{(.+)\}$/);
516
- if (!braceMatch)
517
- return [];
518
- return braceMatch[1]
519
- .split(',')
520
- .map((item) => item.trim().replace(/^"|"$/g, ''))
521
- .filter(Boolean);
522
- }
523
- function normalizeColumnOptionsValue(options) {
524
- if (options === undefined)
525
- return undefined;
526
- return typeof options === 'string' ? JSON.parse(options) : options;
527
- }
528
- export function getSupportedColumnTypesFromMetadata(metadata) {
529
- const columnTable = normalizeTablesFromMetadata(metadata).find((table) => table?.name === 'enfyra_column');
530
- const typeColumn = columnTable?.columns?.find((column) => column?.name === 'type');
531
- const options = parseColumnTypeOptions(typeColumn?.options);
532
- return options.length ? options : FALLBACK_COLUMN_TYPES;
533
- }
534
- function chooseFirstSupported(supported, candidates) {
535
- return candidates.find((candidate) => supported.has(candidate));
536
- }
537
- export function normalizeColumnTypeForLiveMetadata(type, supportedTypes = FALLBACK_COLUMN_TYPES) {
538
- const raw = String(type ?? '').trim();
539
- if (!raw) {
540
- throw new Error(`Column type is required. Valid live types: ${supportedTypes.join(', ')}.`);
541
- }
542
- const supported = new Set(supportedTypes);
543
- if (supported.has(raw))
544
- return { type: raw, changed: false, originalType: raw };
545
- const normalized = raw.toLowerCase();
546
- const alias = normalized === 'string' || normalized === 'char' || normalized === 'character varying'
547
- ? chooseFirstSupported(supported, ['varchar', 'text'])
548
- : normalized === 'integer'
549
- ? chooseFirstSupported(supported, ['int', 'bigint', 'float'])
550
- : normalized === 'bool'
551
- ? chooseFirstSupported(supported, ['boolean'])
552
- : ['decimal', 'numeric', 'number', 'double', 'money', 'currency'].includes(normalized)
553
- ? chooseFirstSupported(supported, ['decimal', 'float'])
554
- : ['longtext', 'mediumtext', 'large-text'].includes(normalized)
555
- ? chooseFirstSupported(supported, ['text', 'richtext', 'varchar'])
556
- : ['json', 'jsonb', 'object', 'array'].includes(normalized)
557
- ? chooseFirstSupported(supported, ['simple-json', 'text'])
558
- : ['date-time', 'timestamptz'].includes(normalized)
559
- ? chooseFirstSupported(supported, ['datetime', 'timestamp'])
560
- : null;
561
- if (!alias) {
562
- throw new Error(`Unsupported column type "${raw}" for this live Enfyra instance. Valid live types: ${supportedTypes.join(', ')}. ` +
563
- `Guidance: ${COLUMN_TYPE_ALIAS_HINTS.join(' ')}`);
564
- }
565
- return { type: alias, changed: true, originalType: raw };
566
- }
567
- export function normalizeColumnsForLiveMetadata(columns, supportedTypes) {
568
- const normalizations = [];
569
- const normalizedColumns = columns.map((column) => {
570
- const normalized = normalizeColumnTypeForLiveMetadata(column.type, supportedTypes);
571
- if (!normalized.changed)
572
- return column;
573
- normalizations.push({ column: String(column.name ?? '<unnamed>'), from: normalized.originalType, to: normalized.type });
574
- return { ...column, type: normalized.type };
575
- });
576
- return { columns: normalizedColumns, normalizations };
577
- }
578
- function findMetadataTable(metadata, tableName) {
579
- return normalizeTablesFromMetadata(metadata).find((table) => table?.name === tableName) || null;
580
- }
581
- function metadataColumnNames(metadata, tableName) {
582
- return (findMetadataTable(metadata, tableName)?.columns || [])
583
- .map((column) => column?.name)
584
- .filter(Boolean);
585
- }
586
- function metadataColumnOptions(metadata, tableName, columnName) {
587
- const column = (findMetadataTable(metadata, tableName)?.columns || [])
588
- .find((item) => item?.name === columnName);
589
- return parseColumnTypeOptions(column?.options);
590
- }
591
- function summarizeCreatedTableSchema(table, fallbackName) {
592
- if (!table)
593
- return null;
594
- const columns = (table.columns || [])
595
- .map((column) => column?.name)
596
- .filter(Boolean);
597
- const relations = (table.relations || [])
598
- .map((relation) => relation?.propertyName)
599
- .filter(Boolean);
600
- return {
601
- tableName: table.name || fallbackName,
602
- primaryKey: (table.columns || []).find((column) => column?.isPrimary)?.name || null,
603
- fields: [...columns, ...relations].sort(),
604
- columns,
605
- relations,
606
- };
607
- }
608
- function getPatchableColumns(columns = []) {
609
- return (columns || [])
610
- .filter((column) => getId(column) !== null)
611
- .map(normalizeColumnForTablePatch);
612
- }
613
- function getMissingIds(beforeIds, afterIds, excludedIds = []) {
614
- const afterSet = new Set(afterIds.map(String));
615
- const excludedSet = new Set(excludedIds.map(String));
616
- return beforeIds
617
- .map(String)
618
- .filter((id) => !excludedSet.has(id) && !afterSet.has(id));
619
- }
620
- async function verifyColumnCascade(ENFYRA_API_URL, tableId, beforeIds, { action, columnId, columnName, }) {
621
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
622
- const afterColumns = getPatchableColumns(tableData.columns);
623
- const afterIds = afterColumns.map((column) => String(getId(column)));
624
- const excludedIds = action === 'delete' ? [columnId] : [];
625
- const missingIds = getMissingIds(beforeIds, afterIds, excludedIds);
626
- if (missingIds.length > 0) {
627
- throw new Error(`Schema cascade verification failed: unrelated column ids disappeared: ${missingIds.join(', ')}`);
628
- }
629
- if (action === 'create' && !afterColumns.some((column) => column.name === columnName)) {
630
- throw new Error(`Schema cascade verification failed: column "${columnName}" was not found after create.`);
631
- }
632
- if (action === 'delete' && afterIds.includes(String(columnId))) {
633
- throw new Error(`Schema cascade verification failed: column ${columnId} still exists after delete.`);
634
- }
635
- if (action === 'update' && !afterIds.includes(String(columnId))) {
636
- throw new Error(`Schema cascade verification failed: column ${columnId} was not found after update.`);
637
- }
638
- return afterColumns;
639
- }
640
- async function verifyRelationCascade(ENFYRA_API_URL, tableId, beforeIds, { action, relationId, propertyName, }) {
641
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
642
- const afterRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
643
- const afterIds = afterRelations.map((relation) => String(getId(relation))).filter((id) => id !== 'null');
644
- const excludedIds = action === 'delete' ? [relationId] : [];
645
- const missingIds = getMissingIds(beforeIds, afterIds, excludedIds);
646
- if (missingIds.length > 0) {
647
- throw new Error(`Schema cascade verification failed: unrelated relation ids disappeared: ${missingIds.join(', ')}`);
648
- }
649
- if (action === 'create' && !afterRelations.some((relation) => relation.propertyName === propertyName)) {
650
- throw new Error(`Schema cascade verification failed: relation "${propertyName}" was not found after create.`);
651
- }
652
- if (action === 'delete' && afterIds.includes(String(relationId))) {
653
- throw new Error(`Schema cascade verification failed: relation ${relationId} still exists after delete.`);
654
- }
655
- return afterRelations;
656
- }
657
- export function buildColumnDefinition({ name, type, supportedTypes, isNullable, isUnique, isPublished, isUpdatable, isEncrypted, isPrimary, isGenerated, isSystem, defaultValue, description, options, }) {
658
- const normalizedType = normalizeColumnTypeForLiveMetadata(type, supportedTypes).type;
659
- const column = {
660
- name,
661
- type: normalizedType,
662
- isNullable: isNullable ?? true,
663
- isPrimary: isPrimary ?? false,
664
- isGenerated: isGenerated ?? false,
665
- isSystem: isSystem ?? false,
666
- isPublished: isPublished ?? true,
667
- isUpdatable: isUpdatable ?? true,
668
- isEncrypted: isEncrypted ?? false,
669
- };
670
- if (isUnique !== undefined)
671
- column.isUnique = isUnique;
672
- if (defaultValue !== undefined)
673
- column.defaultValue = defaultValue;
674
- if (description !== undefined)
675
- column.description = description;
676
- if (options !== undefined)
677
- column.options = normalizeColumnOptionsValue(options);
678
- return column;
679
- }
680
- /**
681
- * Register table tools with MCP server
682
- */
1
+ export * from './table-tool-logic.js';
2
+ import { registerSchemaColumnTools } from './schema-column-tools.js';
3
+ import { registerSchemaRelationTools } from './schema-relation-tools.js';
4
+ import { registerSchemaTableTools } from './schema-table-tools.js';
683
5
  export function registerTableTools(server, ENFYRA_API_URL, options = {}) {
684
- const toolset = options.toolset || 'guided';
685
- const apiBase = ENFYRA_API_URL.replace(/\/$/, '');
686
- async function appendColumnToTable(args) {
687
- assertGlobalRulesAck(args.globalRulesAckKey);
688
- return withSchemaQueue(async () => {
689
- const [tableData, columnMetadata] = await Promise.all([
690
- fetchTableWithDetails(ENFYRA_API_URL, args.tableId),
691
- fetchTableMetadata(ENFYRA_API_URL, 'enfyra_column'),
692
- ]);
693
- if (!tableData) {
694
- throw new Error(`Table with ID ${args.tableId} not found.`);
695
- }
696
- const supportedTypes = getSupportedColumnTypesFromMetadata(columnMetadata);
697
- const normalized = normalizeColumnTypeForLiveMetadata(args.type, supportedTypes);
698
- assertColumnNameCanBeCreated(args.name, 'create_columns');
699
- const existingColumns = getPatchableColumns(tableData.columns);
700
- const beforeIds = existingColumns.map((column) => String(getId(column)));
701
- const newCol = buildColumnDefinition({ ...args, supportedTypes });
702
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, args.tableId, { columns: [...existingColumns, newCol] });
703
- await verifyColumnCascade(ENFYRA_API_URL, args.tableId, beforeIds, {
704
- action: 'create',
705
- columnName: args.name,
706
- });
707
- return jsonContent({
708
- action: 'column_created',
709
- tableId: args.tableId,
710
- columnName: args.name,
711
- schemaNormalization: normalized.changed ? [{ column: args.name, from: normalized.originalType, to: normalized.type }] : [],
712
- supportedColumnTypes: supportedTypes,
713
- result,
714
- });
715
- });
716
- }
717
- async function appendRelationToTable(args) {
718
- assertGlobalRulesAck(args.globalRulesAckKey);
719
- return withSchemaQueue(async () => {
720
- assertNoForbiddenRelationKeys(args);
721
- const { sourceTableId, targetTableId, targetTable } = args;
722
- const relationPatch = normalizeRelationForTablePatch(args);
723
- const { type, propertyName, inversePropertyName, mappedBy, isNullable, onDelete, description } = relationPatch;
724
- const targetRef = targetTableId ?? targetTable;
725
- if (targetRef === undefined || targetRef === null || targetRef === '') {
726
- throw new Error('create_relations requires targetTableId or targetTable. Pass an existing table id, name, or alias.');
727
- }
728
- const catalog = await fetchTableCatalog(ENFYRA_API_URL);
729
- const resolvedSourceTableId = resolveTableIdentifierFromMetadata(catalog, sourceTableId, 'sourceTableId');
730
- const resolvedTargetTableId = resolveTableIdentifierFromMetadata(catalog, targetRef, 'targetTableId');
731
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, resolvedSourceTableId);
732
- if (!tableData) {
733
- throw new Error(`Table ${sourceTableId} not found.`);
734
- }
735
- const existingRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
736
- const beforeIds = existingRelations.map((relation) => String(getId(relation))).filter((id) => id !== 'null');
737
- const newRelation = { targetTable: resolvedTargetTableId, type, propertyName };
738
- if (inversePropertyName !== undefined)
739
- newRelation.inversePropertyName = inversePropertyName || null;
740
- if (mappedBy !== undefined)
741
- newRelation.mappedBy = mappedBy;
742
- if (isNullable !== undefined)
743
- newRelation.isNullable = isNullable;
744
- if (onDelete !== undefined)
745
- newRelation.onDelete = onDelete;
746
- if (description !== undefined)
747
- newRelation.description = description;
748
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, resolvedSourceTableId, { relations: [...existingRelations, newRelation] });
749
- await verifyRelationCascade(ENFYRA_API_URL, resolvedSourceTableId, beforeIds, {
750
- action: 'create',
751
- propertyName,
752
- });
753
- return jsonContent({
754
- action: 'relation_created',
755
- relation: { propertyName, type, sourceTableId: resolvedSourceTableId, targetTableId: resolvedTargetTableId },
756
- result,
757
- });
758
- });
759
- }
760
- async function removeColumnFromTable({ tableId, columnId, confirm, globalRulesAckKey }) {
761
- return withSchemaQueue(async () => {
762
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
763
- if (!tableData) {
764
- throw new Error(`Table with ID ${tableId} not found.`);
765
- }
766
- const existingColumns = getPatchableColumns(tableData.columns);
767
- const beforeIds = existingColumns.map((column) => String(getId(column)));
768
- if (!beforeIds.includes(String(columnId))) {
769
- throw new Error(`Column ${columnId} was not found on table ${tableId}; refusing schema cascade patch.`);
770
- }
771
- if (!confirm) {
772
- const target = existingColumns.find((column) => String(getId(column)) === String(columnId));
773
- return {
774
- content: [{ type: 'text', text: JSON.stringify({
775
- action: 'delete_column_preview',
776
- tableId,
777
- columnId,
778
- targetColumn: target,
779
- preservedColumnIds: beforeIds.filter((id) => id !== String(columnId)),
780
- destructive: true,
781
- next: 'Call delete_columns again with the same one-item array and confirm=true to drop the physical column and metadata.',
782
- }, null, 2) }],
783
- };
784
- }
785
- assertGlobalRulesAck(globalRulesAckKey);
786
- const columns = existingColumns
787
- .filter(col => String(getId(col)) !== String(columnId));
788
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
789
- await verifyColumnCascade(ENFYRA_API_URL, tableId, beforeIds, {
790
- action: 'delete',
791
- columnId,
792
- });
793
- return jsonContent({
794
- action: 'column_deleted',
795
- tableId,
796
- columnId,
797
- result,
798
- });
799
- });
800
- }
801
- async function removeRelationFromTable({ tableId, relationId, confirm, globalRulesAckKey }) {
802
- return withSchemaQueue(async () => {
803
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
804
- if (!tableData) {
805
- throw new Error(`Table with ID ${tableId} not found.`);
806
- }
807
- const existingRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
808
- const beforeIds = existingRelations.map((relation) => String(getId(relation))).filter((id) => id !== 'null');
809
- if (!beforeIds.includes(String(relationId))) {
810
- throw new Error(`Relation ${relationId} was not found on table ${tableId}; refusing schema cascade patch.`);
811
- }
812
- if (!confirm) {
813
- const target = existingRelations.find((relation) => String(getId(relation)) === String(relationId));
814
- return {
815
- content: [{ type: 'text', text: JSON.stringify({
816
- action: 'delete_relation_preview',
817
- tableId,
818
- relationId,
819
- targetRelation: target,
820
- preservedRelationIds: beforeIds.filter((id) => id !== String(relationId)),
821
- destructive: true,
822
- next: 'Call delete_relations again with the same one-item array and confirm=true to drop relation metadata and any derived FK/junction structures.',
823
- }, null, 2) }],
824
- };
825
- }
826
- assertGlobalRulesAck(globalRulesAckKey);
827
- const relations = existingRelations
828
- .filter(rel => String(getId(rel)) !== String(relationId));
829
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { relations });
830
- await verifyRelationCascade(ENFYRA_API_URL, tableId, beforeIds, {
831
- action: 'delete',
832
- relationId,
833
- });
834
- return jsonContent({
835
- action: 'relation_deleted',
836
- tableId,
837
- relationId,
838
- result,
839
- });
840
- });
841
- }
842
- const columnCreateSchema = {
843
- tableId: z.string().describe('Table definition ID (from get_all_tables or create_tables).'),
844
- name: z.string().describe('Column name (e.g., "title", "webhook_secret"). Lowercase with underscores. Do not create id, _id, createdAt, or updatedAt; Enfyra manages them automatically.'),
845
- type: z.string().describe('Column type from the live enfyra_column.type enum. Common valid types are int, varchar, text, boolean, uuid, ObjectId, bigint, date, datetime, timestamp, enum, simple-json, code, array-select, richtext, and float. The tool normalizes common aliases before sending: decimal/numeric/money/number -> float when decimal is not live-supported, longtext -> text, json/jsonb/object/array -> simple-json when live-supported, string -> varchar. Prefer relations instead of *_id columns.'),
846
- isNullable: z.boolean().optional().default(true).describe('Set to false if column cannot be null.'),
847
- isUnique: z.boolean().optional().default(false).describe('Set to true for unique constraint.'),
848
- isPublished: z.boolean().optional().describe('Set column visibility baseline. Use false for secrets and internal fields.'),
849
- isUpdatable: z.boolean().optional().describe('Set false for immutable fields that cannot be updated after creation. Independent from isEncrypted.'),
850
- isEncrypted: z.boolean().optional().describe('Set true to encrypt this column at the Enfyra database-query layer. This does not change isUpdatable. Encrypted fields cannot be filtered or sorted.'),
851
- isPrimary: z.boolean().optional().describe('Set true only for primary key columns; normally only create_tables auto id uses this.'),
852
- isGenerated: z.boolean().optional().describe('Set true only for generated columns such as auto id.'),
853
- isSystem: z.boolean().optional().describe('Set true only for system-managed columns. Avoid for normal app fields.'),
854
- defaultValue: z.string().optional().describe('Default value as JSON string or backend-supported literal.'),
855
- description: z.string().optional().describe('Column description.'),
856
- options: z.union([z.array(z.string()), z.string()]).optional().describe('Column options as a native array such as ["draft","published"] or a JSON string for older clients.'),
857
- globalRulesAckKey: globalRulesAckParam(z),
858
- };
859
- const relationCreateSchema = {
860
- 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.'),
861
- 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.'),
862
- 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.'),
863
- 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.'),
864
- propertyName: z.string().describe('Property name on source table (e.g., "customer", "items").'),
865
- 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.'),
866
- mappedBy: z.string().optional().describe('Mapped-by property for inverse relation shapes when required by the backend. Do not use physical FK names.'),
867
- isNullable: z.boolean().optional().default(true).describe('Whether the relation is nullable.'),
868
- onDelete: z.enum(['CASCADE', 'SET NULL', 'RESTRICT']).optional().default('SET NULL').describe('On delete behavior.'),
869
- description: z.string().optional().describe('Relation description.'),
870
- fkCol: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
871
- fkColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
872
- foreignKeyColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
873
- sourceColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
874
- targetColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
875
- junctionSourceColumn: z.never().optional().describe('Forbidden. Use relation property names only; Enfyra derives junction columns.'),
876
- junctionTargetColumn: z.never().optional().describe('Forbidden. Use relation property names only; Enfyra derives junction columns.'),
877
- globalRulesAckKey: globalRulesAckParam(z),
878
- };
879
- const columnDeleteSchema = {
880
- tableId: z.string().describe('Table definition ID.'),
881
- columnId: z.string().describe('Column definition ID to delete.'),
882
- confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
883
- globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
884
- };
885
- const relationDeleteSchema = {
886
- tableId: z.string().describe('Table definition ID (source table of the relation).'),
887
- relationId: z.string().describe('Relation definition ID to delete.'),
888
- confirm: z.boolean().optional().default(false).describe('Required true to apply the destructive delete. Omit/false returns a preview only.'),
889
- globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
890
- };
891
- function arrayValue(name, value) {
892
- if (value === undefined || value === null || value === '')
893
- return [];
894
- return Array.isArray(value) ? value : parseJsonArrayParam(name, value);
895
- }
896
- async function createOneTable(args) {
897
- const userColumns = arrayValue('columns', args.columns);
898
- const [metadataContext, columnMetadata] = await Promise.all([
899
- fetchMetadataContext(ENFYRA_API_URL),
900
- fetchTableMetadata(ENFYRA_API_URL, 'enfyra_column'),
901
- ]);
902
- const supportedTypes = getSupportedColumnTypesFromMetadata(columnMetadata);
903
- const idColumn = buildPrimaryColumnForDbType(metadataContext.dbType);
904
- const { columns: userColumnsWithoutAuto, skippedAutoColumns } = stripAutoManagedColumns(userColumns);
905
- const { columns: normalizedUserColumns, normalizations } = normalizeColumnsForLiveMetadata(userColumnsWithoutAuto, supportedTypes);
906
- const schemaNormalizations = [
907
- ...(args._requestedTableName ? [{ field: 'name', from: args._requestedTableName, to: args.name, reason: 'Enfyra table names are lowercase.' }] : []),
908
- ...normalizations,
909
- ];
910
- const deferredRelations = arrayValue('relations', args.relations).map(normalizeRelationForTablePatch);
911
- const relationNames = new Set(deferredRelations.map((relation) => relation.propertyName).filter(Boolean));
912
- assertNoColumnRelationNameCollision(normalizedUserColumns.map((column) => String(column.name || '')).filter(Boolean), deferredRelations.map((relation) => String(relation.propertyName || '')).filter(Boolean), `create_tables item "${args.name}"`);
913
- const indexes = normalizeConstraintGroupsValue('indexes', args.indexes ?? []);
914
- const uniques = normalizeConstraintGroupsValue('uniques', args.uniques ?? []);
915
- assertIndexesDoNotReferenceUniqueFields(indexes, uniques);
916
- const splitIndexes = splitRelationConstraintGroups(indexes, relationNames);
917
- const splitUniques = splitRelationConstraintGroups(uniques, relationNames);
918
- const body = { name: args.name, description: args.description, columns: [idColumn, ...normalizedUserColumns], relations: [] };
919
- if (args.isSingleRecord !== undefined)
920
- body.isSingleRecord = args.isSingleRecord;
921
- if (args.indexes !== undefined)
922
- body.indexes = splitIndexes.immediate;
923
- if (args.uniques !== undefined)
924
- body.uniques = splitUniques.immediate;
925
- const result = await fetchAPI(ENFYRA_API_URL, '/enfyra_table', {
926
- method: 'POST',
927
- body: JSON.stringify(body),
928
- });
929
- const createdTable = Array.isArray(result?.data) ? result.data[0] : result;
930
- const createdTableId = createdTable?.id ?? createdTable?._id;
931
- const liveMetadataAfterCreate = await fetchAPI(ENFYRA_API_URL, `/metadata/${encodeURIComponent(args.name)}`)
932
- .catch((error) => ({ error: error?.message || String(error) }));
933
- const liveTableAfterCreate = liveMetadataAfterCreate?.error
934
- ? null
935
- : liveMetadataAfterCreate?.data?.table || liveMetadataAfterCreate?.data || liveMetadataAfterCreate?.table || liveMetadataAfterCreate;
936
- const liveSchema = summarizeCreatedTableSchema(liveTableAfterCreate, args.name);
937
- const routePath = `/${args.name}`;
938
- return {
939
- action: 'table_created',
940
- table: { id: createdTableId, name: args.name, routePath },
941
- summary: {
942
- columnCount: normalizedUserColumns.length + 1,
943
- createdColumnCount: normalizedUserColumns.length,
944
- deferredRelationCount: deferredRelations.length,
945
- indexGroupCount: indexes.length,
946
- uniqueGroupCount: uniques.length,
947
- deferredConstraintCount: splitIndexes.deferred.length + splitUniques.deferred.length,
948
- },
949
- schemaNormalization: schemaNormalizations,
950
- skippedAutoColumns,
951
- schema: {
952
- intended: {
953
- tableName: args.name,
954
- primaryKey: idColumn.name,
955
- fields: [idColumn.name, ...normalizedUserColumns.map((column) => column.name), ...deferredRelations.map((relation) => relation.propertyName)].filter(Boolean).sort(),
956
- columns: [idColumn.name, ...normalizedUserColumns.map((column) => column.name)].filter(Boolean),
957
- relations: deferredRelations.map((relation) => relation.propertyName).filter(Boolean),
958
- },
959
- live: liveSchema,
960
- liveMetadataAvailable: Boolean(liveSchema),
961
- liveMetadataError: liveMetadataAfterCreate?.error || undefined,
962
- },
963
- supportedColumnTypes: supportedTypes,
964
- rest: {
965
- base: apiBase,
966
- routePath,
967
- operations: ['GET /<table>', 'POST /<table>', 'PATCH /<table>/:id', 'DELETE /<table>/:id'],
968
- noGetById: true,
969
- },
970
- deferredRelations,
971
- deferredConstraints: {
972
- indexes: splitIndexes.deferred,
973
- uniques: splitUniques.deferred,
974
- },
975
- result,
976
- };
977
- }
978
- async function applyDeferredConstraints(tableId, deferredConstraints) {
979
- const deferredIndexes = deferredConstraints?.indexes || [];
980
- const deferredUniques = deferredConstraints?.uniques || [];
981
- if (deferredIndexes.length === 0 && deferredUniques.length === 0)
982
- return null;
983
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
984
- const mappedIndexes = resolveRelationConstraintGroups(tableData, deferredIndexes, 'indexes');
985
- const mappedUniques = resolveRelationConstraintGroups(tableData, deferredUniques, 'uniques');
986
- const existingIndexes = normalizeConstraintGroupsValue('indexes', tableData.indexes || []);
987
- const existingUniques = normalizeConstraintGroupsValue('uniques', tableData.uniques || []);
988
- const uniques = mergeConstraintGroups(existingUniques, mappedUniques);
989
- const prunedExistingIndexes = pruneIndexesThatOverlapUniques(existingIndexes, uniques);
990
- const indexes = mergeConstraintGroups(prunedExistingIndexes.indexes, mappedIndexes);
991
- assertIndexesDoNotReferenceUniqueFields(indexes, uniques);
992
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { indexes, uniques });
993
- return {
994
- action: 'deferred_constraints_applied',
995
- tableId,
996
- tableName: tableData.name,
997
- requested: {
998
- indexes: deferredIndexes,
999
- uniques: deferredUniques,
1000
- },
1001
- applied: {
1002
- indexes: mappedIndexes,
1003
- uniques: mappedUniques,
1004
- },
1005
- prunedExistingIndexes: prunedExistingIndexes.removed,
1006
- result,
1007
- };
1008
- }
1009
- async function updateOneTable(args) {
1010
- const body = {};
1011
- let schemaConstraintNormalization;
1012
- if (args.name !== undefined)
1013
- body.name = args.name;
1014
- if (args.alias !== undefined)
1015
- body.alias = args.alias;
1016
- if (args.description !== undefined)
1017
- body.description = args.description;
1018
- if (args.isSingleRecord !== undefined)
1019
- body.isSingleRecord = args.isSingleRecord;
1020
- if (args.graphqlEnabled !== undefined)
1021
- body.graphqlEnabled = args.graphqlEnabled;
1022
- if (args.indexes !== undefined)
1023
- body.indexes = normalizeConstraintGroupsValue('indexes', args.indexes);
1024
- if (args.uniques !== undefined)
1025
- body.uniques = normalizeConstraintGroupsValue('uniques', args.uniques);
1026
- if (args.indexes !== undefined || args.uniques !== undefined) {
1027
- let indexes = body.indexes;
1028
- let uniques = body.uniques;
1029
- if (indexes === undefined || uniques === undefined) {
1030
- const existing = await fetchTableWithDetails(ENFYRA_API_URL, args.tableId);
1031
- if (indexes === undefined)
1032
- indexes = normalizeConstraintGroupsValue('indexes', existing.indexes);
1033
- if (uniques === undefined)
1034
- uniques = normalizeConstraintGroupsValue('uniques', existing.uniques);
1035
- }
1036
- if (args.uniques !== undefined && args.indexes === undefined) {
1037
- const prunedExistingIndexes = pruneIndexesThatOverlapUniques(indexes ?? [], uniques ?? []);
1038
- indexes = prunedExistingIndexes.indexes;
1039
- body.indexes = indexes;
1040
- schemaConstraintNormalization = {
1041
- prunedExistingIndexes: prunedExistingIndexes.removed,
1042
- reason: 'Unique constraints already provide indexed lookups; MCP removed existing non-unique indexes that used fields now covered by uniques.',
1043
- };
1044
- }
1045
- assertIndexesDoNotReferenceUniqueFields(indexes ?? [], uniques ?? []);
1046
- }
1047
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, args.tableId, body);
1048
- return {
1049
- action: 'table_updated',
1050
- tableId: args.tableId,
1051
- schemaConstraintNormalization,
1052
- result,
1053
- };
1054
- }
1055
- async function deleteOneTable({ tableId, tableName, confirm }) {
1056
- const resolvedTableId = tableId ?? resolveTableIdentifierFromMetadata(await fetchTableCatalog(ENFYRA_API_URL), tableName, 'delete_tables item tableName');
1057
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, resolvedTableId);
1058
- if (!confirm) {
1059
- return {
1060
- action: 'delete_table_preview',
1061
- tableId: resolvedTableId,
1062
- tableName: tableData.name,
1063
- columnCount: (tableData.columns || []).length,
1064
- relationCount: (tableData.relations || []).length,
1065
- destructive: true,
1066
- };
1067
- }
1068
- const result = await fetchAPI(ENFYRA_API_URL, `/enfyra_table/${resolvedTableId}`, {
1069
- method: 'DELETE',
1070
- });
1071
- return {
1072
- action: 'table_deleted',
1073
- tableId: resolvedTableId,
1074
- tableName: tableData.name,
1075
- result,
1076
- };
1077
- }
1078
- async function updateOneColumn({ tableId, columnId, name, type, isNullable, isPublished, isUpdatable, defaultValue, description, options, allowContractBroadening }) {
1079
- const tableData = await fetchTableWithDetails(ENFYRA_API_URL, tableId);
1080
- if (!tableData) {
1081
- throw new Error(`Table with ID ${tableId} not found.`);
1082
- }
1083
- const existingColumns = getPatchableColumns(tableData.columns);
1084
- const beforeIds = existingColumns.map((column) => String(getId(column)));
1085
- if (!beforeIds.includes(String(columnId))) {
1086
- throw new Error(`Column ${columnId} was not found on table ${tableId}; refusing schema cascade patch.`);
1087
- }
1088
- const existingColumn = existingColumns.find((column) => String(getId(column)) === String(columnId));
1089
- const contractBroadening = assertColumnContractBroadening(existingColumn || {}, {
1090
- isPublished,
1091
- isUpdatable,
1092
- allowContractBroadening,
1093
- }, toolset);
1094
- const columns = existingColumns.map(col => {
1095
- const rest = normalizeColumnForTablePatch(col);
1096
- if (String(getId(col)) === String(columnId)) {
1097
- if (name !== undefined)
1098
- rest.name = name;
1099
- if (type !== undefined)
1100
- rest.type = type;
1101
- if (isNullable !== undefined)
1102
- rest.isNullable = isNullable;
1103
- if (isPublished !== undefined)
1104
- rest.isPublished = isPublished;
1105
- if (isUpdatable !== undefined)
1106
- rest.isUpdatable = isUpdatable;
1107
- if (defaultValue !== undefined)
1108
- rest.defaultValue = defaultValue;
1109
- if (description !== undefined)
1110
- rest.description = description;
1111
- if (options !== undefined)
1112
- rest.options = normalizeColumnOptionsValue(options);
1113
- }
1114
- return rest;
1115
- });
1116
- const result = await patchTableAutoConfirm(ENFYRA_API_URL, tableId, { columns });
1117
- await verifyColumnCascade(ENFYRA_API_URL, tableId, beforeIds, {
1118
- action: 'update',
1119
- columnId,
1120
- });
1121
- return {
1122
- action: 'column_updated',
1123
- tableId,
1124
- columnId,
1125
- contractBroadening,
1126
- result,
1127
- };
1128
- }
1129
- // ─── READ ───
1130
- 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.', {
1131
- limit: z.number().int().positive().optional().describe('Maximum tables returned after search. Required unless all=true or search is provided.'),
1132
- all: z.boolean().optional().describe('Return all matched tables. Use this when a complete table list is required.'),
1133
- search: z.string().optional().describe('Optional table name, alias, or description substring filter.'),
1134
- }, async ({ limit, all, search }) => {
1135
- if (!all && limit === undefined && !search?.trim()) {
1136
- throw new Error('get_all_tables requires either limit or all=true. Do not invent arbitrary limits for complete table lists; use all=true.');
1137
- }
1138
- if (all && limit !== undefined) {
1139
- throw new Error('get_all_tables accepts either all=true or limit, not both.');
1140
- }
1141
- const catalog = await fetchTableCatalog(ENFYRA_API_URL);
1142
- const needle = search?.trim().toLowerCase();
1143
- const tables = catalog
1144
- .map((table) => ({
1145
- id: getId(table),
1146
- name: table.name ?? null,
1147
- alias: table.alias ?? null,
1148
- description: table.description ?? null,
1149
- isSingleRecord: table.isSingleRecord ?? null,
1150
- columnCount: null,
1151
- relationCount: null,
1152
- routeBacked: null,
1153
- }))
1154
- .filter((table) => {
1155
- if (!needle)
1156
- return true;
1157
- return [table.name, table.alias, table.description]
1158
- .some((value) => String(value || '').toLowerCase().includes(needle));
1159
- });
1160
- const effectiveLimit = all ? tables.length : (limit ?? 10);
1161
- const returnedTables = all ? tables : tables.slice(0, effectiveLimit);
1162
- return jsonContent({
1163
- action: 'get_all_tables',
1164
- totalTableCount: catalog.length,
1165
- matchedTableCount: tables.length,
1166
- returnedTableCount: returnedTables.length,
1167
- all: Boolean(all),
1168
- implicitSearchLimit: Boolean(!all && limit === undefined && search?.trim()),
1169
- hardCap: all ? null : effectiveLimit,
1170
- search: search || null,
1171
- tables: returnedTables,
1172
- detailHint: 'Use inspect_table with a table id/name for columns, relations, indexes, routes, permissions, and GraphQL state.',
1173
- });
1174
- });
1175
- server.tool('get_schema_design_context', [
1176
- 'Step-zero schema design guide for Enfyra table creation.',
1177
- 'Call this before creating a new app schema or multiple tables.',
1178
- 'It returns live column types, supported metadata attributes, relation types, constraint shape, and the exact creation sequence so the model does not guess SQL types or physical FK fields.',
1179
- ].join(' '), {}, async () => {
1180
- const [metadataContext, tableMetadata, columnMetadata, relationMetadata] = await Promise.all([
1181
- fetchMetadataContext(ENFYRA_API_URL),
1182
- fetchTableMetadata(ENFYRA_API_URL, 'enfyra_table'),
1183
- fetchTableMetadata(ENFYRA_API_URL, 'enfyra_column'),
1184
- fetchTableMetadata(ENFYRA_API_URL, 'enfyra_relation'),
1185
- ]);
1186
- const liveColumnTypes = getSupportedColumnTypesFromMetadata(columnMetadata);
1187
- const relationTypes = metadataColumnOptions(relationMetadata, 'enfyra_relation', 'type');
1188
- const onDeleteOptions = metadataColumnOptions(relationMetadata, 'enfyra_relation', 'onDelete');
1189
- const tableAttributes = metadataColumnNames(tableMetadata, 'enfyra_table');
1190
- const columnAttributes = metadataColumnNames(columnMetadata, 'enfyra_column');
1191
- const relationAttributes = metadataColumnNames(relationMetadata, 'enfyra_relation');
1192
- const primaryColumnNames = [metadataContext.dbType === 'mongodb' ? '_id' : 'id'];
1193
- const primaryColumnTypes = [metadataContext.dbType === 'mongodb' ? 'ObjectId' : 'int'];
1194
- return jsonContent({
1195
- action: 'schema_design_context',
1196
- stepZero: 'Read this response before create_tables/create_columns/create_relations. Use these live attributes and types, not SQL dialect guesses.',
1197
- liveColumnTypes,
1198
- primaryKeyContext: {
1199
- observedPrimaryColumnNames: primaryColumnNames,
1200
- observedPrimaryColumnTypes: primaryColumnTypes,
1201
- createTableDefault: metadataContext.dbType === 'mongodb'
1202
- ? 'create_tables auto-includes the Mongo _id/ObjectId primary key.'
1203
- : 'create_tables auto-includes the SQL id/int primary key.',
1204
- },
1205
- createTableInput: {
1206
- directFields: ['name', 'description', 'isSingleRecord'],
1207
- cascadeFields: ['columns', 'relations'],
1208
- constraintFields: ['indexes', 'uniques'],
1209
- notAcceptedAtCreate: ['alias', 'graphqlEnabled'],
1210
- autoManagedColumns: ['id', 'createdAt', 'updatedAt'],
1211
- idColumn: metadataContext.dbType === 'mongodb'
1212
- ? 'create_tables auto-includes _id/ObjectId; do not include your own primary key.'
1213
- : 'create_tables auto-includes id/int; do not include your own primary key.',
1214
- reservedColumnRule: 'Do not declare id, _id, createdAt, or updatedAt in create_tables/create_columns. create_tables strips them before save and reports skippedAutoColumns; create_columns rejects them.',
1215
- },
1216
- columnDefinitionInput: {
1217
- allowedFields: ['name', 'type', 'isNullable', 'isUnique', 'isPublished', 'isUpdatable', 'isEncrypted', 'defaultValue', 'description', 'options'],
1218
- liveTypes: liveColumnTypes,
1219
- typeSelection: [
1220
- 'varchar: short text, labels, slugs, status strings.',
1221
- 'text/richtext: long prose.',
1222
- 'float: prices, amounts, ratings, percentages, decimal-like numbers unless liveTypes contains decimal.',
1223
- 'int/bigint: counts, ordering, integer quantities.',
1224
- 'boolean: true/false flags.',
1225
- 'date/datetime/timestamp: temporal fields.',
1226
- 'enum: constrained option strings when options are provided.',
1227
- 'simple-json: structured snapshots/arrays only when liveTypes contains simple-json.',
1228
- 'code: source-code fields.',
1229
- ],
1230
- forbiddenGuesses: ['json', 'jsonb', 'longtext', 'decimal'].filter((type) => !liveColumnTypes.includes(type)),
1231
- aliasNormalization: 'Schema tools normalize common aliases where possible and return schemaNormalization, but models should choose from liveTypes directly.',
1232
- namespaceRule: 'Column names and relation propertyName values share one table namespace. Do not define a scalar column and a relation with the same name in one table.',
1233
- },
1234
- relationDefinitionInput: {
1235
- allowedFields: ['targetTableId', 'targetTable', 'type', 'propertyName', 'inversePropertyName', 'mappedBy', 'isNullable', 'onDelete', 'description'],
1236
- relationTypes: relationTypes.length ? relationTypes : ['many-to-one', 'one-to-many', 'one-to-one', 'many-to-many'],
1237
- onDeleteOptions: onDeleteOptions.length ? onDeleteOptions : ['CASCADE', 'SET NULL', 'RESTRICT'],
1238
- forbiddenPhysicalFields: FORBIDDEN_RELATION_KEYS,
1239
- rule: 'Use relations for links between records. Do not create scalar FK columns such as userId, owner_id, categoryIds, or courseId unless the user explicitly wants denormalized snapshot data.',
1240
- namespaceRule: 'Relation propertyName must be unique among both relation names and scalar column names on the same table. If a relation is named owner, do not also create ownerId/owner as a scalar field.',
1241
- inverseDesignRule: 'If a parent detail/read must deep-load a child collection (for example application.reviewAssignments or assignment.scorecards), create the owning child many-to-one relation with inversePropertyName immediately. Without the inverse, parent-to-child deep reads will fail with unknown relation.',
1242
- },
1243
- constraints: {
1244
- indexes: 'JSON array of non-unique logical field groups, e.g. [["status","createdAt"]]. Relation propertyName values are allowed.',
1245
- uniques: 'JSON array of unique logical field groups, e.g. [["record","actor"]].',
1246
- uniqueIndexRule: 'Any field that appears in any uniques group, including composite unique groups such as ["event","attendee"], must not appear in indexes because unique constraints already create indexed lookups for their fields.',
1247
- createTablesPreflight: 'create_tables rejects constraints that reference fields not declared as scalar columns, auto-managed columns, or relation propertyName values in that same table item before it creates anything.',
1248
- relationBasedUniques: 'For one-pass schema creation, put the owning relations in the same create_tables item as the relation-based unique group. If relations already exist, add relation-based uniques later with update_tables.',
1249
- },
1250
- recommendedSequence: [
1251
- '1. Name domain entities and decide which existing tables are reused, especially enfyra_user for users/owners/actors.',
1252
- '2. Create independent lookup/base tables first with scalar columns only.',
1253
- '3. Create dependent tables with scalar columns and relations whose target tables already exist.',
1254
- '4. Use create_relations after both tables exist when a relation could not be included during table creation.',
1255
- '5. Add relation-based unique groups in the same create_tables item when the relations are declared there, or via update_tables after relations exist.',
1256
- '6. Before deep parent detail queries, confirm every child collection relation exists as an inversePropertyName on the owning child relation.',
1257
- '7. Insert records using column names and relation propertyName values, never hidden FK columns.',
1258
- '8. Re-inspect each table with inspect_table before writing records or adding query examples.',
1259
- ],
1260
- liveMetadataAttributes: {
1261
- enfyra_table: tableAttributes,
1262
- enfyra_column: columnAttributes,
1263
- enfyra_relation: relationAttributes,
1264
- },
1265
- });
1266
- });
1267
- // ─── TABLE MUTATIONS ───
1268
- server.tool('create_tables', [
1269
- 'Create one or more table definitions. Always pass items as a native JSON array; for one table, pass one item.',
1270
- 'The tool creates tables sequentially, creates columns with each table, then creates all requested relations after every table in the batch exists. This avoids relation target races for weak agents.',
1271
- 'Each item supports { name, description?, isSingleRecord?, columns?, relations?, indexes?, uniques? }. columns/relations/indexes/uniques may be arrays inside the item.',
1272
- 'Do not include id, _id, createdAt, or updatedAt in columns; Enfyra manages them and create_tables strips them before save.',
1273
- '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.',
1274
- 'Use get_schema_design_context first for live column types and relation rules. Do not include physical FK fields.',
1275
- 'The response includes cleanupHints.recordCreateOrder; use it when seeding records so parent/target rows are created before child/source rows.',
1276
- 'The response includes cleanupHints.recordDeleteOrder; use it when deleting seeded test records so child/source rows are removed before parent/target rows.',
1277
- ].join(' '), {
1278
- items: bulkObjectArrayParam(z, 'Table definitions').optional().describe('Native JSON array of table definitions. Pass one object in the array for a single table.'),
1279
- 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.'),
1280
- maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one schema batch. Default/max is 100; operations still run sequentially.'),
1281
- globalRulesAckKey: globalRulesAckParam(z),
1282
- }, async ({ items, tables, maxItems, globalRulesAckKey }) => {
1283
- assertGlobalRulesAck(globalRulesAckKey);
1284
- if (items !== undefined && tables !== undefined)
1285
- throw new Error('Pass either items or tables to create_tables, not both.');
1286
- const parsedItems = normalizeCreateTableDefinitions(parseBulkItemsParam('items', items ?? tables));
1287
- assertBulkLimit('create_tables', parsedItems, maxItems);
1288
- preflightCreateTableDefinitions(parsedItems);
1289
- const recordDeleteOrder = computeBatchCleanupOrder(parsedItems);
1290
- const created = [];
1291
- const deferredRelations = [];
1292
- const deferredConstraints = [];
1293
- for (const [index, item] of parsedItems.entries()) {
1294
- const result = await withSchemaQueue(() => createOneTable(item));
1295
- created.push({ index, ...result, deferredRelations: undefined });
1296
- for (const relation of result.deferredRelations || []) {
1297
- deferredRelations.push({ index, sourceTableId: result.table.id || result.table.name, ...relation });
1298
- }
1299
- if ((result.deferredConstraints?.indexes || []).length || (result.deferredConstraints?.uniques || []).length) {
1300
- deferredConstraints.push({
1301
- index,
1302
- tableId: result.table.id || result.table.name,
1303
- ...result.deferredConstraints,
1304
- });
1305
- }
1306
- }
1307
- const createdRelations = [];
1308
- deferredRelations.sort((left, right) => {
1309
- const leftPriority = normalizeRelationType(left.type) === 'one-to-many' ? 1 : 0;
1310
- const rightPriority = normalizeRelationType(right.type) === 'one-to-many' ? 1 : 0;
1311
- return leftPriority - rightPriority;
1312
- });
1313
- for (const relation of deferredRelations) {
1314
- const relationResult = await appendRelationToTable({
1315
- sourceTableId: relation.sourceTableId,
1316
- targetTableId: relation.targetTable,
1317
- type: relation.type,
1318
- propertyName: relation.propertyName,
1319
- inversePropertyName: relation.inversePropertyName,
1320
- mappedBy: relation.mappedBy,
1321
- isNullable: relation.isNullable,
1322
- onDelete: relation.onDelete,
1323
- description: relation.description,
1324
- globalRulesAckKey,
1325
- });
1326
- createdRelations.push({ index: relation.index, ...JSON.parse(relationResult.content[0].text) });
1327
- }
1328
- const appliedDeferredConstraints = [];
1329
- for (const constraints of deferredConstraints) {
1330
- const constraintResult = await withSchemaQueue(() => applyDeferredConstraints(constraints.tableId, {
1331
- indexes: constraints.indexes,
1332
- uniques: constraints.uniques,
1333
- }));
1334
- if (constraintResult)
1335
- appliedDeferredConstraints.push({ index: constraints.index, ...constraintResult });
1336
- }
1337
- return jsonContent({
1338
- action: 'tables_created',
1339
- requested: parsedItems.length,
1340
- createdCount: created.length,
1341
- deferredRelationCount: deferredRelations.length,
1342
- createdRelationCount: createdRelations.length,
1343
- deferredConstraintCount: deferredConstraints.length,
1344
- appliedDeferredConstraintCount: appliedDeferredConstraints.length,
1345
- sequential: true,
1346
- relationPhaseAfterTables: true,
1347
- constraintPhaseAfterRelations: true,
1348
- cleanupHints: {
1349
- recordCreateOrder: [...recordDeleteOrder].reverse(),
1350
- recordDeleteOrder,
1351
- tableDeleteOrder: recordDeleteOrder,
1352
- recordCreateRule: 'When seeding sample data, create records sequentially in recordCreateOrder; parent/target rows must exist before child/source rows reference them.',
1353
- recordRule: 'If you delete seeded records before deleting test tables, delete record batches sequentially in recordDeleteOrder; do not parallelize parent/child deletes.',
1354
- tableRule: 'For full test cleanup, prefer delete_tables with tableDeleteOrder after deleting custom routes/flows; table deletion removes the remaining table data.',
1355
- },
1356
- created: toolset === 'full' ? created : created.map(({ result, supportedColumnTypes, schema, ...item }) => ({
1357
- ...item,
1358
- schema: {
1359
- intended: schema?.intended,
1360
- liveMetadataAvailable: schema?.liveMetadataAvailable,
1361
- liveMetadataError: schema?.liveMetadataError,
1362
- },
1363
- })),
1364
- createdRelations: toolset === 'full' ? createdRelations : createdRelations.map(({ result, responseFormat, ...item }) => item),
1365
- appliedDeferredConstraints: toolset === 'full' ? appliedDeferredConstraints : appliedDeferredConstraints.map(({ result, ...item }) => item),
1366
- });
1367
- });
1368
- server.tool('update_tables', 'Update one or more table definitions. Always pass items as a native JSON array; for one table, pass one item. Items run sequentially through the schema queue.', {
1369
- items: bulkObjectArrayParam(z, 'Table update items').describe('Native JSON array of table update items: [{ tableId, name?, alias?, description?, isSingleRecord?, graphqlEnabled?, indexes?, uniques? }]. indexes/uniques may be arrays.'),
1370
- maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one schema batch. Default/max is 100.'),
1371
- globalRulesAckKey: globalRulesAckParam(z),
1372
- }, async ({ items, maxItems, globalRulesAckKey }) => {
1373
- assertGlobalRulesAck(globalRulesAckKey);
1374
- const parsedItems = parseBulkItemsParam('items', items);
1375
- assertBulkLimit('update_tables', parsedItems, maxItems);
1376
- const updated = [];
1377
- for (const [index, item] of parsedItems.entries()) {
1378
- if (!item.tableId)
1379
- throw new Error(`items[${index}].tableId is required.`);
1380
- const result = await withSchemaQueue(() => updateOneTable(item));
1381
- updated.push({ index, ...result });
1382
- }
1383
- return jsonContent({ action: 'tables_updated', requested: parsedItems.length, updatedCount: updated.length, sequential: true, updated });
1384
- });
1385
- server.tool('delete_tables', 'Delete one or more table definitions. Always pass items as a native JSON array; for one table, pass one item. confirm=false previews every target; confirm=true deletes sequentially.', {
1386
- items: bulkObjectArrayParam(z, 'Table delete items').describe('Native JSON array of delete items: [{ tableId }] or [{ tableName }]. Names are resolved through live metadata before preview/delete.'),
1387
- maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one schema batch. Default/max is 100.'),
1388
- confirm: z.boolean().optional().default(false).describe('Required true to apply destructive deletes. Omit/false returns previews only.'),
1389
- globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1390
- }, async ({ items, maxItems, confirm, globalRulesAckKey }) => {
1391
- const parsedItems = parseBulkItemsParam('items', items);
1392
- assertBulkLimit('delete_tables', parsedItems, maxItems);
1393
- if (confirm)
1394
- assertGlobalRulesAck(globalRulesAckKey);
1395
- const results = [];
1396
- for (const [index, item] of parsedItems.entries()) {
1397
- if (!item.tableId && !item.tableName)
1398
- throw new Error(`items[${index}] requires tableId or tableName.`);
1399
- const result = await withSchemaQueue(() => deleteOneTable({ tableId: item.tableId, tableName: item.tableName, confirm }));
1400
- results.push({ index, ...result });
1401
- }
1402
- return jsonContent({
1403
- action: confirm ? 'tables_deleted' : 'delete_tables_preview',
1404
- requested: parsedItems.length,
1405
- sequential: true,
1406
- destructive: true,
1407
- results,
1408
- next: confirm ? undefined : 'Call delete_tables again with the same items and confirm=true to delete sequentially.',
1409
- });
1410
- });
1411
- // ─── COLUMN MUTATIONS ───
1412
- server.tool('create_columns', 'Create one or more columns. Always pass items as a native JSON array; for one column, pass one item. Items run sequentially through the schema queue.', {
1413
- items: bulkObjectArrayParam(z, 'Column definitions').optional().describe('Native JSON array of column definitions. Each item uses create_columns fields: { tableId, name, type, isNullable?, isUnique?, isPublished?, isUpdatable?, isEncrypted?, isPrimary?, isGenerated?, isSystem?, defaultValue?, description?, options? }.'),
1414
- columns: bulkObjectArrayParam(z, 'Column definitions').optional().describe('Alias for items when the caller naturally names the batch columns. Pass either items or columns, not both.'),
1415
- maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one schema batch. Default/max is 100.'),
1416
- globalRulesAckKey: globalRulesAckParam(z),
1417
- }, async ({ items, columns, maxItems, globalRulesAckKey }) => {
1418
- assertGlobalRulesAck(globalRulesAckKey);
1419
- if (items !== undefined && columns !== undefined)
1420
- throw new Error('Pass either items or columns to create_columns, not both.');
1421
- const parsedItems = parseBulkItemsParam('items', items ?? columns);
1422
- assertBulkLimit('create_columns', parsedItems, maxItems);
1423
- const created = [];
1424
- for (const [index, item] of parsedItems.entries()) {
1425
- const result = await appendColumnToTable({ ...item, globalRulesAckKey });
1426
- created.push({ index, ...JSON.parse(result.content[0].text) });
1427
- }
1428
- return jsonContent({ action: 'columns_created', requested: parsedItems.length, createdCount: created.length, sequential: true, created });
1429
- });
1430
- server.tool('update_columns', 'Update one or more columns. Always pass items as a native JSON array; for one column, pass one item. Items run sequentially through the schema queue. Guided mode blocks isUpdatable/isPublished false→true broadening. Do not set isUpdatable=true merely to seed E2E data or let a custom action change a server-owned field; use an exact trusted internal write after authorization and preserve the canonical metadata contract.', {
1431
- items: bulkObjectArrayParam(z, 'Column update items').describe('Native JSON array of column update items: [{ tableId, columnId, name?, type?, isNullable?, isPublished?, isUpdatable?, defaultValue?, description?, options?, allowContractBroadening? }]. Guided mode blocks false→true isUpdatable/isPublished changes. Expert full mode requires allowContractBroadening=true; never use broadening only for custom-action writes or test fixtures.'),
1432
- maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one schema batch. Default/max is 100.'),
1433
- globalRulesAckKey: globalRulesAckParam(z),
1434
- }, async ({ items, maxItems, globalRulesAckKey }) => {
1435
- assertGlobalRulesAck(globalRulesAckKey);
1436
- const parsedItems = parseBulkItemsParam('items', items);
1437
- assertBulkLimit('update_columns', parsedItems, maxItems);
1438
- const updated = [];
1439
- for (const [index, item] of parsedItems.entries()) {
1440
- if (!item.tableId)
1441
- throw new Error(`items[${index}].tableId is required.`);
1442
- if (!item.columnId)
1443
- throw new Error(`items[${index}].columnId is required.`);
1444
- const result = await withSchemaQueue(() => updateOneColumn(item));
1445
- updated.push({ index, ...result });
1446
- }
1447
- return jsonContent({ action: 'columns_updated', requested: parsedItems.length, updatedCount: updated.length, sequential: true, updated });
1448
- });
1449
- server.tool('delete_columns', 'Delete one or more columns. Always pass items as a native JSON array; for one column, pass one item. confirm=false previews every target; confirm=true deletes sequentially.', {
1450
- items: bulkObjectArrayParam(z, 'Column delete items').describe('Native JSON array of delete items: [{ tableId, columnId }].'),
1451
- maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one schema batch. Default/max is 100.'),
1452
- confirm: z.boolean().optional().default(false).describe('Required true to apply destructive deletes. Omit/false returns previews only.'),
1453
- globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1454
- }, async ({ items, maxItems, confirm, globalRulesAckKey }) => {
1455
- const parsedItems = parseBulkItemsParam('items', items);
1456
- assertBulkLimit('delete_columns', parsedItems, maxItems);
1457
- if (confirm)
1458
- assertGlobalRulesAck(globalRulesAckKey);
1459
- const results = [];
1460
- for (const [index, item] of parsedItems.entries()) {
1461
- if (!item.tableId)
1462
- throw new Error(`items[${index}].tableId is required.`);
1463
- if (!item.columnId)
1464
- throw new Error(`items[${index}].columnId is required.`);
1465
- const result = await removeColumnFromTable({ ...item, confirm, globalRulesAckKey });
1466
- results.push({ index, ...JSON.parse(result.content[0].text) });
1467
- }
1468
- return jsonContent({
1469
- action: confirm ? 'columns_deleted' : 'delete_columns_preview',
1470
- requested: parsedItems.length,
1471
- sequential: true,
1472
- destructive: true,
1473
- results,
1474
- next: confirm ? undefined : 'Call delete_columns again with the same items and confirm=true to delete sequentially.',
1475
- });
1476
- });
1477
- // ─── RELATION MUTATIONS ───
1478
- server.tool('create_relations', 'Create one or more relations. Always pass items as a native JSON array; for one relation, pass one item. Items run sequentially through the schema queue and table names/aliases are resolved internally.', {
1479
- items: bulkObjectArrayParam(z, 'Relation definitions').optional().describe('Native JSON array of relation definitions. Each item uses { sourceTableId, targetTableId or targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. Do not send physical FK fields.'),
1480
- relations: bulkObjectArrayParam(z, 'Relation definitions').optional().describe('Alias for items when the caller naturally names the batch relations. Pass either items or relations, not both.'),
1481
- maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one schema batch. Default/max is 100.'),
1482
- globalRulesAckKey: globalRulesAckParam(z),
1483
- }, async ({ items, relations, maxItems, globalRulesAckKey }) => {
1484
- assertGlobalRulesAck(globalRulesAckKey);
1485
- if (items !== undefined && relations !== undefined)
1486
- throw new Error('Pass either items or relations to create_relations, not both.');
1487
- const parsedItems = parseBulkItemsParam('items', items ?? relations);
1488
- assertBulkLimit('create_relations', parsedItems, maxItems);
1489
- const created = [];
1490
- for (const [index, item] of parsedItems.entries()) {
1491
- const result = await appendRelationToTable({ ...item, globalRulesAckKey });
1492
- created.push({ index, ...JSON.parse(result.content[0].text) });
1493
- }
1494
- return jsonContent({ action: 'relations_created', requested: parsedItems.length, createdCount: created.length, sequential: true, created });
1495
- });
1496
- server.tool('delete_relations', 'Delete one or more relations. Always pass items as a native JSON array; for one relation, pass one item. confirm=false previews every target; confirm=true deletes sequentially.', {
1497
- items: bulkObjectArrayParam(z, 'Relation delete items').describe('Native JSON array of delete items: [{ tableId, relationId }].'),
1498
- maxItems: z.number().int().min(1).max(100).optional().default(100).describe('Safety cap for one schema batch. Default/max is 100.'),
1499
- confirm: z.boolean().optional().default(false).describe('Required true to apply destructive deletes. Omit/false returns previews only.'),
1500
- globalRulesAckKey: globalRulesAckParam(z).optional().describe('Required when confirm=true. Use globalRulesAckKey from get_enfyra_required_knowledge.'),
1501
- }, async ({ items, maxItems, confirm, globalRulesAckKey }) => {
1502
- const parsedItems = parseBulkItemsParam('items', items);
1503
- assertBulkLimit('delete_relations', parsedItems, maxItems);
1504
- if (confirm)
1505
- assertGlobalRulesAck(globalRulesAckKey);
1506
- const results = [];
1507
- for (const [index, item] of parsedItems.entries()) {
1508
- if (!item.tableId)
1509
- throw new Error(`items[${index}].tableId is required.`);
1510
- if (!item.relationId)
1511
- throw new Error(`items[${index}].relationId is required.`);
1512
- const result = await removeRelationFromTable({ ...item, confirm, globalRulesAckKey });
1513
- results.push({ index, ...JSON.parse(result.content[0].text) });
1514
- }
1515
- return jsonContent({
1516
- action: confirm ? 'relations_deleted' : 'delete_relations_preview',
1517
- requested: parsedItems.length,
1518
- sequential: true,
1519
- destructive: true,
1520
- results,
1521
- next: confirm ? undefined : 'Call delete_relations again with the same items and confirm=true to delete sequentially.',
1522
- });
1523
- });
6
+ registerSchemaTableTools(server, ENFYRA_API_URL, options);
7
+ registerSchemaColumnTools(server, ENFYRA_API_URL, options);
8
+ registerSchemaRelationTools(server, ENFYRA_API_URL, options);
1524
9
  }
1525
10
  //# sourceMappingURL=table-tools.js.map