@enfyra/mcp-server 0.0.32 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/lib/mcp-examples.js +110 -28
- package/src/lib/mcp-instructions.js +8 -2
- package/src/lib/table-tools.js +3 -3
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -90,12 +90,12 @@ window.location.href = url.toString()`,
|
|
|
90
90
|
columns: JSON.stringify([
|
|
91
91
|
{ name: "kind", type: "varchar", isNullable: false, defaultValue: "dm" },
|
|
92
92
|
{ name: "title", type: "varchar", isNullable: true },
|
|
93
|
-
{ name: "
|
|
94
|
-
{ name: "last_message_at", type: "datetime", isNullable: true }
|
|
93
|
+
{ name: "description", type: "text", isNullable: true }
|
|
95
94
|
])
|
|
96
95
|
})`,
|
|
97
96
|
notes: [
|
|
98
97
|
'create_table creates the default route for /chat_conversation.',
|
|
98
|
+
'Keep the latest message as a relation named lastMessage after chat_message exists; do not duplicate last message text/date columns.',
|
|
99
99
|
'Do not create tables just to get custom paths; use create_route for that.',
|
|
100
100
|
],
|
|
101
101
|
},
|
|
@@ -105,7 +105,7 @@ window.location.href = url.toString()`,
|
|
|
105
105
|
name: "chat_message",
|
|
106
106
|
columns: JSON.stringify([
|
|
107
107
|
{ name: "text", type: "text", isNullable: false },
|
|
108
|
-
{ name: "
|
|
108
|
+
{ name: "persistStatus", type: "varchar", defaultValue: "persisted" }
|
|
109
109
|
]),
|
|
110
110
|
relations: JSON.stringify([
|
|
111
111
|
{
|
|
@@ -134,13 +134,39 @@ window.location.href = url.toString()`,
|
|
|
134
134
|
'Do not provide physical FK column names; Enfyra derives them.',
|
|
135
135
|
],
|
|
136
136
|
},
|
|
137
|
+
{
|
|
138
|
+
name: 'Add chat_conversation.lastMessage after chat_message exists',
|
|
139
|
+
code: `update_table({
|
|
140
|
+
tableId: "<chat_conversation_id>",
|
|
141
|
+
relations: JSON.stringify([
|
|
142
|
+
{
|
|
143
|
+
propertyName: "createdBy",
|
|
144
|
+
type: "many-to-one",
|
|
145
|
+
targetTable: { id: "<user_definition_id>" },
|
|
146
|
+
isNullable: true,
|
|
147
|
+
onDelete: "CASCADE"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
propertyName: "lastMessage",
|
|
151
|
+
type: "many-to-one",
|
|
152
|
+
targetTable: { id: "<chat_message_id>" },
|
|
153
|
+
isNullable: true,
|
|
154
|
+
onDelete: "SET NULL"
|
|
155
|
+
}
|
|
156
|
+
])
|
|
157
|
+
})`,
|
|
158
|
+
notes: [
|
|
159
|
+
'Use relation fields such as lastMessage.id,lastMessage.text,lastMessage.createdAt when loading conversation lists.',
|
|
160
|
+
'When deleting the current last message, a post-hook should set lastMessage to the newest remaining message or null.',
|
|
161
|
+
],
|
|
162
|
+
},
|
|
137
163
|
{
|
|
138
164
|
name: 'Unread/read table with unique and indexes',
|
|
139
165
|
code: `create_table({
|
|
140
166
|
name: "chat_message_read",
|
|
141
167
|
columns: JSON.stringify([
|
|
142
|
-
{ name: "
|
|
143
|
-
{ name: "
|
|
168
|
+
{ name: "isRead", type: "boolean", defaultValue: false },
|
|
169
|
+
{ name: "readAt", type: "datetime", isNullable: true }
|
|
144
170
|
]),
|
|
145
171
|
relations: JSON.stringify([
|
|
146
172
|
{ propertyName: "message", type: "many-to-one", targetTable: { id: "<chat_message_id>" }, onDelete: "CASCADE" },
|
|
@@ -149,8 +175,8 @@ window.location.href = url.toString()`,
|
|
|
149
175
|
]),
|
|
150
176
|
uniques: JSON.stringify([["message", "member"]]),
|
|
151
177
|
indexes: JSON.stringify([
|
|
152
|
-
["member", "
|
|
153
|
-
["conversation", "member", "
|
|
178
|
+
["member", "isRead", "conversation"],
|
|
179
|
+
["conversation", "member", "isRead"]
|
|
154
180
|
])
|
|
155
181
|
})`,
|
|
156
182
|
notes: [
|
|
@@ -165,15 +191,12 @@ window.location.href = url.toString()`,
|
|
|
165
191
|
useWhen: 'Use when fetching records, filtering by relations, loading nested data, or counting efficiently.',
|
|
166
192
|
examples: [
|
|
167
193
|
{
|
|
168
|
-
name: 'List current user conversations through
|
|
169
|
-
code: `GET /enfyra/
|
|
170
|
-
"member": { "id": { "_eq": "<currentUserId>" } }
|
|
171
|
-
}&deep={
|
|
172
|
-
"conversation": { "fields": "id,kind,title,last_message_text,last_message_at" }
|
|
173
|
-
}&limit=0`,
|
|
194
|
+
name: 'List current user conversations through RLS',
|
|
195
|
+
code: `GET /enfyra/chat_conversation?fields=id,kind,title,lastMessage.id,lastMessage.text,lastMessage.createdAt&limit=0`,
|
|
174
196
|
notes: [
|
|
175
|
-
'Use
|
|
176
|
-
'
|
|
197
|
+
'Use a conversation read pre-hook/RLS boundary so the route only returns conversations visible to @USER.',
|
|
198
|
+
'lastMessage is a relation to chat_message; do not duplicate preview fields on chat_conversation.',
|
|
199
|
+
'limit=0 means load all matching conversation rows.',
|
|
177
200
|
'Do not fetch messages for every conversation on initial list load; load messages after selecting a conversation.',
|
|
178
201
|
],
|
|
179
202
|
},
|
|
@@ -189,7 +212,7 @@ window.location.href = url.toString()`,
|
|
|
189
212
|
name: 'Count without loading all rows',
|
|
190
213
|
code: `GET /enfyra/chat_message_read?fields=id&limit=1&meta=filterCount&filter={
|
|
191
214
|
"member": { "id": { "_eq": "<currentUserId>" } },
|
|
192
|
-
"
|
|
215
|
+
"isRead": { "_eq": false }
|
|
193
216
|
}`,
|
|
194
217
|
notes: [
|
|
195
218
|
'Use meta=totalCount with no filter and meta=filterCount with a filter.',
|
|
@@ -372,7 +395,7 @@ const socket = io("/chat", {
|
|
|
372
395
|
code: `const conversationId = @BODY.conversationId
|
|
373
396
|
if (!conversationId) @THROW400("conversationId is required")
|
|
374
397
|
|
|
375
|
-
const membership = await
|
|
398
|
+
const membership = await @REPOS.chat_conversation_member.find({
|
|
376
399
|
filter: {
|
|
377
400
|
conversation: { id: { _eq: conversationId } },
|
|
378
401
|
member: { id: { _eq: @USER.id } }
|
|
@@ -394,7 +417,7 @@ if (!membership.data[0]) @THROW403("Not a conversation member")
|
|
|
394
417
|
code: `const { conversationId, text, clientId } = @BODY
|
|
395
418
|
if (!conversationId || !text) @THROW400("conversationId and text are required")
|
|
396
419
|
|
|
397
|
-
const membership = await
|
|
420
|
+
const membership = await @REPOS.chat_conversation_member.find({
|
|
398
421
|
filter: {
|
|
399
422
|
conversation: { id: { _eq: conversationId } },
|
|
400
423
|
member: { id: { _eq: @USER.id } }
|
|
@@ -403,16 +426,22 @@ const membership = await #chat_conversation_member.find({
|
|
|
403
426
|
})
|
|
404
427
|
if (!membership.data[0]) @THROW403("Not a conversation member")
|
|
405
428
|
|
|
406
|
-
const created = await
|
|
429
|
+
const created = await @REPOS.chat_message.create({
|
|
407
430
|
data: {
|
|
408
431
|
conversation: { id: conversationId },
|
|
409
432
|
sender: { id: @USER.id },
|
|
410
433
|
text,
|
|
411
|
-
|
|
434
|
+
persistStatus: "persisted"
|
|
412
435
|
}
|
|
413
436
|
})
|
|
414
437
|
|
|
415
438
|
const message = created.data?.[0] ?? null
|
|
439
|
+
if (message?.id) {
|
|
440
|
+
await @REPOS.chat_conversation.update({
|
|
441
|
+
id: conversationId,
|
|
442
|
+
data: { lastMessage: { id: message.id }, updatedAt: message.createdAt || new Date().toISOString() }
|
|
443
|
+
})
|
|
444
|
+
}
|
|
416
445
|
@SOCKET.emitToRoom(\`conversation:\${conversationId}\`, "chat:message", {
|
|
417
446
|
clientId,
|
|
418
447
|
message
|
|
@@ -510,42 +539,95 @@ return {
|
|
|
510
539
|
{
|
|
511
540
|
name: 'Create menu then extension',
|
|
512
541
|
code: `create_menu({
|
|
513
|
-
|
|
542
|
+
label: "Reports",
|
|
543
|
+
type: "Menu",
|
|
514
544
|
path: "/reports",
|
|
515
|
-
icon: "
|
|
516
|
-
order: 20
|
|
545
|
+
icon: "lucide:bar-chart-3",
|
|
546
|
+
order: 20,
|
|
547
|
+
isEnabled: true
|
|
517
548
|
})
|
|
518
549
|
|
|
550
|
+
// Read the created menu id from the tool response, then:
|
|
519
551
|
create_extension({
|
|
552
|
+
type: "page",
|
|
520
553
|
name: "ReportsPage",
|
|
521
|
-
|
|
522
|
-
|
|
554
|
+
description: "Reports dashboard",
|
|
555
|
+
menuId: "<created-menu-id>",
|
|
556
|
+
code: "<template><section class=\\"min-h-full w-full p-4 sm:p-6 lg:p-8\\">Reports</section></template><script setup>useHeaderActionRegistry({ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', onClick: () => {}, order: 0 })</script>",
|
|
557
|
+
isEnabled: true
|
|
523
558
|
})`,
|
|
524
559
|
notes: [
|
|
525
560
|
'Menu provides navigation; extension provides content.',
|
|
526
|
-
'
|
|
561
|
+
'Use menu_definition.label, not title.',
|
|
562
|
+
'For page extensions, create the menu first and pass menuId to create_extension.',
|
|
563
|
+
'Page extensions should be full-bleed by default and responsive from the first version.',
|
|
564
|
+
],
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: 'Plan a Cloud admin dashboard as multiple pages',
|
|
568
|
+
code: `// Recommended menu shape for an operations surface:
|
|
569
|
+
create_menu({
|
|
570
|
+
type: "Dropdown Menu",
|
|
571
|
+
label: "Cloud",
|
|
572
|
+
path: "/cloud",
|
|
573
|
+
icon: "lucide:cloud",
|
|
574
|
+
order: 2,
|
|
575
|
+
isEnabled: true
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
// Child page extensions should be focused:
|
|
579
|
+
// /dashboard overview with time range KPIs
|
|
580
|
+
// /cloud/projects project status and drill-downs
|
|
581
|
+
// /cloud/provisioning provisioning timeline/failures/slow steps
|
|
582
|
+
// /cloud/billing orders/subscriptions/refunds
|
|
583
|
+
// /cloud/infrastructure hosts/capacity/plans/system credential readiness
|
|
584
|
+
// /cloud/readiness legal/Paddle/landing launch checklist
|
|
585
|
+
// Use UTabs inside large pages instead of placing every section in one dashboard.`,
|
|
586
|
+
notes: [
|
|
587
|
+
'Design the menu/page split before generating dashboard code.',
|
|
588
|
+
'Keep /dashboard as overview and use focused pages for operational domains.',
|
|
589
|
+
'UTabs is available in eApp extension runtime for page-level sections.',
|
|
527
590
|
],
|
|
528
591
|
},
|
|
529
592
|
{
|
|
530
593
|
name: 'Extension fetches Enfyra data',
|
|
531
594
|
code: `<script setup>
|
|
532
|
-
const { data, pending,
|
|
595
|
+
const { data, pending, execute: fetchOrders } = useApi('/order_definition', {
|
|
533
596
|
query: {
|
|
534
597
|
limit: 10,
|
|
535
598
|
sort: '-createdAt'
|
|
536
599
|
}
|
|
537
600
|
})
|
|
601
|
+
|
|
602
|
+
onMounted(() => fetchOrders())
|
|
538
603
|
</script>
|
|
539
604
|
|
|
540
605
|
<template>
|
|
541
|
-
<UButton :loading="pending" @click="
|
|
606
|
+
<UButton :loading="pending" @click="fetchOrders">Refresh</UButton>
|
|
542
607
|
<pre>{{ data }}</pre>
|
|
543
608
|
</template>`,
|
|
544
609
|
notes: [
|
|
545
610
|
'Use app-provided composables in extensions.',
|
|
611
|
+
'useApi does not auto-run; call execute() on mounted or through an action.',
|
|
546
612
|
'Keep extension UI focused; move backend logic into handlers/hooks when needed.',
|
|
547
613
|
],
|
|
548
614
|
},
|
|
615
|
+
{
|
|
616
|
+
name: 'Extension can use modern browser APIs',
|
|
617
|
+
code: `<script setup lang="ts">
|
|
618
|
+
const statuses = ['active', 'ready']
|
|
619
|
+
const ok = statuses.includes('active')
|
|
620
|
+
const requiredTerms = new Set(['cloud-terms', 'privacy-policy', 'refund-policy'])
|
|
621
|
+
const loaded = await Promise.all([Promise.resolve(1), Promise.resolve(2)])
|
|
622
|
+
const label = String('pending_payment').replace(/_/g, ' ')
|
|
623
|
+
const date = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(new Date())
|
|
624
|
+
console.log(ok, requiredTerms.has('cloud-terms'), loaded, label, date)
|
|
625
|
+
</script>`,
|
|
626
|
+
notes: [
|
|
627
|
+
'Do not rewrite extension code to ES5 when tooling rejects modern APIs.',
|
|
628
|
+
'If diagnostics complain about these APIs, fix eApp extension TypeScript lib/runtime contract.',
|
|
629
|
+
],
|
|
630
|
+
},
|
|
549
631
|
],
|
|
550
632
|
},
|
|
551
633
|
};
|
|
@@ -78,7 +78,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
78
78
|
'### After a new table is created',
|
|
79
79
|
'- MCP **`create_table` supports creating columns and relations in the same call**: pass `columns` and `relations` as JSON arrays. Use `create_relation` only when adding a relation to an existing table later.',
|
|
80
80
|
'- MCP **`create_table` supports `isSingleRecord` directly**. Set `isSingleRecord: true` in the create call for settings/config tables that should keep only one record; do not create first and then patch only for this flag.',
|
|
81
|
-
'- MCP **`create_table` and `update_table` support `indexes` and `uniques`** as JSON arrays of logical field groups. Use compound indexes for hot filters and unread/read state, e.g. `indexes: [["member","
|
|
81
|
+
'- MCP **`create_table` and `update_table` support `indexes` and `uniques`** as JSON arrays of logical field groups. Use compound indexes for hot filters and unread/read state, e.g. `indexes: [["member","isRead","conversation"],["conversation","member","isRead"]]` and `uniques: [["message","member"]]`. Relation property names are allowed; Enfyra resolves them to physical FK columns for SQL and Mongo.',
|
|
82
82
|
'- MCP **`create_table` does not accept `alias`**. Do not invent or send alias during table creation; default route/schema behavior is based on `name`. Use `update_table` later only when alias truly needs to change.',
|
|
83
83
|
'- In `create_table.relations`, each relation uses `targetTable` (table id or `{id}`), `type`, `propertyName`, optional `inversePropertyName` or `mappedBy`, `isNullable`, `onDelete`, and `description`. The target table must already exist.',
|
|
84
84
|
'- **Use `user_definition` as the only user table.** Do not create app-specific user/profile mapping tables such as `chat_profile`, `app_user`, `customer_user`, or tables that only mirror/link Enfyra users. If an app needs extra user fields or user relations, add columns/relations directly on `user_definition`.',
|
|
@@ -89,7 +89,8 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
89
89
|
'- If the user asks to add a foreign key field, interpret it as a relation request unless they explicitly say they need a plain scalar column. Do not create both a relation and a duplicate scalar FK column for the same concept.',
|
|
90
90
|
'- **Never ask for or provide physical FK column names** when creating/updating relations. Do not include `fkCol`, `fkColumn`, `foreignKeyColumn`, `sourceColumn`, `targetColumn`, `junctionSourceColumn`, or `junctionTargetColumn` in create/update payloads unless you are only displaying existing metadata. Enfyra relation cascade derives physical FK/junction names from `propertyName` and table metadata, then hides FK columns from app form/schema definition.',
|
|
91
91
|
'- For relation CRUD payloads, the public interface is the relation `propertyName`: example create body uses `"author": {"id": 1}`, not `"authorId"` or a physical FK column. Query/deep/filter keys also use relation `propertyName`.',
|
|
92
|
-
'- **Realtime/chat unread modeling:** unread/read is per user and per message. Do not put `read` or `lastRead` on `chat_conversation` globally. Prefer a join table such as `chat_message_read` with relations `message`, `conversation`, `member`, boolean `
|
|
92
|
+
'- **Realtime/chat unread modeling:** unread/read is per user and per message. Do not put `read` or `lastRead` on `chat_conversation` globally. Prefer a join table such as `chat_message_read` with relations `message`, `conversation`, `member`, boolean `isRead`, nullable `readAt`, unique `["message","member"]`, and indexes `["member","isRead","conversation"]` plus `["conversation","member","isRead"]`. The UI can render a dot by checking existence of unread rows instead of counting every unread message.',
|
|
93
|
+
'- **Realtime/chat latest message modeling:** keep `chat_conversation.lastMessage` as a nullable many-to-one relation to `chat_message`. Do not duplicate latest message text/date onto `chat_conversation`. Load conversation lists with relation fields such as `lastMessage.id,lastMessage.text,lastMessage.createdAt`, update `lastMessage` after the message is persisted, and repair it in a `DELETE /chat_message` post-hook when deleting the current latest message.',
|
|
93
94
|
'- **Chat deletion modeling:** user-level delete/leave should remove the user from `chat_conversation_member` or otherwise make membership inactive. Do not add duplicated `deleted_at` state to both conversation and membership unless the product explicitly needs restore/audit behavior. A DM deleted for both sides is a membership operation for both members; a group is physically deleted only when no memberships remain.',
|
|
94
95
|
'- **Chat conversation title:** for DMs, compute the display title from the other visible member on the server/script response when possible. Do not trust the client to rename a DM from the current user perspective. Group titles can be generated from member display names until the product adds a custom group name.',
|
|
95
96
|
'- **Chat unread UI:** default to a boolean unread dot. Do not show unread counts unless the user explicitly asks for exact counts; exact counts require more query work and are not the default chat-list UX.',
|
|
@@ -270,6 +271,9 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
270
271
|
'- **CRITICAL:** MUST call `create_record` or `update_record` on `extension_definition` — outputting Vue code in chat does NOT save it. User will NOT see it.',
|
|
271
272
|
'- **Code format:** Vue SFC only. Structure: `<template>...</template>` + `<script setup>...</script>`. Server auto-compiles; if compile fails, fix and retry.',
|
|
272
273
|
'- **NO import statements.** All APIs are injected globally (see full list below).',
|
|
274
|
+
'- **Design first for dashboards:** before creating/updating a dashboard extension, define the menu/page split, time range controls, tabs, and drill-down links. Keep `/dashboard` as overview; create focused menu pages for projects, provisioning, billing, infrastructure, and readiness when the surface grows.',
|
|
275
|
+
'- **Page layout default:** page extensions should render full-bleed inside the app shell by default. Do not wrap the entire page in a centered card/container unless explicitly requested. Use responsive grids/stacks from the first version so the page works on desktop, tablet, and mobile.',
|
|
276
|
+
'- **Do not downgrade extension code to ES5 to appease tooling.** eApp extension runtime should support normal browser/runtime APIs such as `Array.includes`, `Set`, `Promise.all`, `String.replace`, `Intl.DateTimeFormat`, and `Intl.NumberFormat`. If diagnostics reject these, fix eApp extension checker/runtime contract instead of rewriting generated extension code around the limitation.',
|
|
273
277
|
'',
|
|
274
278
|
'#### Injected Vue API functions:',
|
|
275
279
|
'- Reactivity: `ref`, `reactive`, `computed`, `readonly`, `shallowRef`, `shallowReactive`',
|
|
@@ -311,6 +315,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
311
315
|
'- **File Manager:** `FileManager`, `FileView`, `FileGridCard`, `CreateFolderModal`',
|
|
312
316
|
'- **Menu:** `MenuRenderer`, `MenuItemEditor`',
|
|
313
317
|
'- **UI:** `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
|
|
318
|
+
'- **Tabs:** `UTabs` is available in current eApp extension runtime. Use it for page-level sections when a page would otherwise become too long.',
|
|
314
319
|
'- **Extension:** `Widget` — embed widget extension via `<Widget :id="extensionId" />`',
|
|
315
320
|
'- **WebSocket:** `WebSocketManager`',
|
|
316
321
|
'- **Permission:** `PermissionGate`, `PermissionManager`',
|
|
@@ -323,6 +328,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
323
328
|
'- **FormEditor field-map:** Customize fields via `:field-map`. Options: `label`, `description`, `hideLabel`, `hideDescription`, `component`, `componentProps`, `type`, `disabled`, `placeholder`, `permission`, `excludedOptions`/`includedOptions`, `fieldProps` (e.g. grid `class: \'md:col-span-2\'` when `layout=\'grid\'`), `booleanWrapperClass`, `fieldWrapperClass`. Optional `:sections` — array of `{ id, title?, hideHeading?, headingClass?, class?, rootClass?, fields: string[] }`; field order follows `fields`; unlisted columns render after. Custom input component: `modelValue` / `update:modelValue`.',
|
|
324
329
|
'- **type "page":** Full-page extension. Requires `menu: { id }` — create menu first (`create_menu` or `create_record` on `menu_definition`), find by path/label, then create extension with `menu: { id: menuId }`. `menu_definition` uses **label** not name — filter by `label` or `path`.',
|
|
325
330
|
'- **type "widget":** Widget extension. No menu required. Embed via `<Widget :id="extensionId" />` in other extensions or pages.',
|
|
331
|
+
'- **Existing pages:** if a menu already has a page extension, update that `extension_definition` record instead of creating a duplicate menu/extension. For example `/dashboard` is menu-driven and may already have an extension attached.',
|
|
326
332
|
'',
|
|
327
333
|
'#### NPM packages (install via MCP):',
|
|
328
334
|
'- **Use `install_package` tool** — just pass the package name and type. The tool auto-fetches version from NPM, checks if already installed, and creates the record.',
|
package/src/lib/table-tools.js
CHANGED
|
@@ -112,7 +112,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
112
112
|
'Create a new table definition with an auto-included `id` primary key column.',
|
|
113
113
|
'**Not** for adding a custom API path or handler only — for that use **`create_route`** with an existing `mainTableId`. Use **`create_table`** when the user needs new stored data (new entity).',
|
|
114
114
|
'PREFERRED: pass `columns` and `relations` params as JSON arrays to create a table WITH columns and relations in one call (cascade). Only use create_column/create_relation separately when adding to an existing table later.',
|
|
115
|
-
'Indexes and uniques are first-class table metadata. Use `indexes` for query performance and `uniques` for data integrity. Each entry is a logical field group such as [["member","
|
|
115
|
+
'Indexes and uniques are first-class table metadata. Use `indexes` for query performance and `uniques` for data integrity. Each entry is a logical field group such as [["member","isRead","conversation"]] or [{"value":["message","member"]}]. Relation property names are allowed; Enfyra resolves them to physical FK columns.',
|
|
116
116
|
'Relations are supported in this same create_table call when the target table already exists. Each relation uses { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete? }; targetTable may be a table id or {id}.',
|
|
117
117
|
'Do NOT provide physical FK/junction columns. Never include fkCol, fkColumn, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn. Enfyra derives and hides those physical columns from relation propertyName/table metadata.',
|
|
118
118
|
'Schema operations (create/update/delete table, add column) must run one at a time — migration locks DB; parallel calls will fail.',
|
|
@@ -130,7 +130,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
130
130
|
isSingleRecord: z.boolean().optional().describe('Set to true for single-record tables such as settings/config. This is passed directly to table_definition create.'),
|
|
131
131
|
columns: z.string().optional().describe('JSON array of column definitions to create with the table (cascade). Each column: { name, type, isNullable?, isUnique?, defaultValue?, description?, options? }. The `id` column is always auto-included. Example: [{"name":"title","type":"varchar"},{"name":"status","type":"enum","options":["draft","published"]}]'),
|
|
132
132
|
relations: z.string().optional().describe('JSON array of relation definitions to create with the table in the same cascade call. Each relation: { targetTable, type, propertyName, inversePropertyName?, mappedBy?, isNullable?, onDelete?, description? }. targetTable can be an id or {"id": <id>}. Do not include physical FK/junction columns such as fkCol, foreignKeyColumn, sourceColumn, targetColumn, junctionSourceColumn, or junctionTargetColumn; Enfyra derives them and hides FK columns from app schema. Example: [{"targetTable":2,"type":"many-to-one","propertyName":"author","inversePropertyName":"posts","isNullable":false,"onDelete":"CASCADE"}]'),
|
|
133
|
-
indexes: z.string().optional().describe('JSON array of logical index field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Relation property names are allowed. Example: [["member","
|
|
133
|
+
indexes: z.string().optional().describe('JSON array of logical index field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Relation property names are allowed. Example: [["member","isRead","conversation"],["conversation","member","isRead"]]'),
|
|
134
134
|
uniques: z.string().optional().describe('JSON array of logical unique field groups. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Example: [["message","member"]]'),
|
|
135
135
|
},
|
|
136
136
|
async ({ name, description, isSingleRecord, columns: columnsJson, relations: relationsJson, indexes: indexesJson, uniques: uniquesJson }) => {
|
|
@@ -178,7 +178,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
178
178
|
[
|
|
179
179
|
'Update table properties: name (rename), alias, description, isSingleRecord, graphqlEnabled, indexes, and uniques.',
|
|
180
180
|
'Does NOT modify columns or relations — use create_column, update_column, delete_column, create_relation for those.',
|
|
181
|
-
'When passing `indexes` or `uniques`, pass the complete desired array of logical field groups; omitted fields are preserved. Relation property names are allowed and are resolved by Enfyra. Example indexes: [["member","
|
|
181
|
+
'When passing `indexes` or `uniques`, pass the complete desired array of logical field groups; omitted fields are preserved. Relation property names are allowed and are resolved by Enfyra. Example indexes: [["member","isRead","conversation"],["conversation","member","isRead"]].',
|
|
182
182
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
183
183
|
].join(' '),
|
|
184
184
|
{
|