@enfyra/mcp-server 0.0.85 → 0.0.87
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 +122 -47
- package/src/lib/mcp-instructions.js +74 -64
- package/src/lib/mutation-guards.js +18 -18
- package/src/lib/route-permission-tools.js +2 -2
- package/src/lib/table-tools.js +23 -23
- package/src/mcp-server-entry.mjs +148 -154
package/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -155,7 +155,7 @@ onUnmounted(() => {
|
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
name: 'OAuth provider setup values',
|
|
158
|
-
code: `// Enfyra OAuth config row, stored in
|
|
158
|
+
code: `// Enfyra OAuth config row, stored in enfyra_oauth_config.
|
|
159
159
|
{
|
|
160
160
|
"provider": "google",
|
|
161
161
|
"clientId": "<google-client-id>",
|
|
@@ -168,7 +168,7 @@ onUnmounted(() => {
|
|
|
168
168
|
// http://localhost:3000/api/auth/google/callback`,
|
|
169
169
|
notes: [
|
|
170
170
|
'redirectUri is the Enfyra callback URL: {ENFYRA_API_URL}/auth/google/callback.',
|
|
171
|
-
'The provider console callback URL and
|
|
171
|
+
'The provider console callback URL and enfyra_oauth_config.redirectUri must match exactly.',
|
|
172
172
|
'This callback URL is not the app return page; the app return page is sent as the redirect query when starting OAuth.',
|
|
173
173
|
'Use appCallbackUrl only for manual-token apps that intentionally read token query parameters.',
|
|
174
174
|
],
|
|
@@ -210,7 +210,7 @@ window.location.href = url.toString()`,
|
|
|
210
210
|
],
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
|
-
name: 'Create relations directly to
|
|
213
|
+
name: 'Create relations directly to enfyra_user',
|
|
214
214
|
code: `create_table({
|
|
215
215
|
name: "chat_message",
|
|
216
216
|
columns: JSON.stringify([
|
|
@@ -228,7 +228,7 @@ window.location.href = url.toString()`,
|
|
|
228
228
|
{
|
|
229
229
|
propertyName: "sender",
|
|
230
230
|
type: "many-to-one",
|
|
231
|
-
targetTable: { id: "<
|
|
231
|
+
targetTable: { id: "<enfyra_user_id>" },
|
|
232
232
|
isNullable: false,
|
|
233
233
|
onDelete: "CASCADE"
|
|
234
234
|
}
|
|
@@ -238,9 +238,9 @@ window.location.href = url.toString()`,
|
|
|
238
238
|
])
|
|
239
239
|
})`,
|
|
240
240
|
notes: [
|
|
241
|
-
'Use
|
|
242
|
-
'Use table ids for targetTable when already known; MCP can also resolve exact table names such as "
|
|
243
|
-
'Do not add inverse relations on
|
|
241
|
+
'Use enfyra_user as the user table.',
|
|
242
|
+
'Use table ids for targetTable when already known; MCP can also resolve exact table names such as "enfyra_user" before schema mutation.',
|
|
243
|
+
'Do not add inverse relations on enfyra_user unless the user explicitly asks.',
|
|
244
244
|
'createdAt, updatedAt, and custom date/datetime/timestamp fields already get auto-generated single-field indexes; add only compound indexes needed by hot filters.',
|
|
245
245
|
'Do not provide physical FK column names; Enfyra derives them.',
|
|
246
246
|
],
|
|
@@ -253,7 +253,7 @@ window.location.href = url.toString()`,
|
|
|
253
253
|
{
|
|
254
254
|
propertyName: "createdBy",
|
|
255
255
|
type: "many-to-one",
|
|
256
|
-
targetTable: { id: "<
|
|
256
|
+
targetTable: { id: "<enfyra_user_id>" },
|
|
257
257
|
isNullable: true,
|
|
258
258
|
onDelete: "CASCADE"
|
|
259
259
|
},
|
|
@@ -282,7 +282,7 @@ window.location.href = url.toString()`,
|
|
|
282
282
|
relations: JSON.stringify([
|
|
283
283
|
{ propertyName: "message", type: "many-to-one", targetTable: { id: "<chat_message_id>" }, onDelete: "CASCADE" },
|
|
284
284
|
{ propertyName: "conversation", type: "many-to-one", targetTable: { id: "<chat_conversation_id>" }, onDelete: "CASCADE" },
|
|
285
|
-
{ propertyName: "member", type: "many-to-one", targetTable: { id: "<
|
|
285
|
+
{ propertyName: "member", type: "many-to-one", targetTable: { id: "<enfyra_user_id>" }, onDelete: "CASCADE" }
|
|
286
286
|
]),
|
|
287
287
|
uniques: JSON.stringify([["message", "member"]]),
|
|
288
288
|
indexes: JSON.stringify([
|
|
@@ -299,7 +299,7 @@ window.location.href = url.toString()`,
|
|
|
299
299
|
{
|
|
300
300
|
name: 'Add server-owned user verification fields',
|
|
301
301
|
code: `create_column({
|
|
302
|
-
tableId: "<
|
|
302
|
+
tableId: "<enfyra_user_table_id>",
|
|
303
303
|
name: "emailVerifiedAt",
|
|
304
304
|
type: "datetime",
|
|
305
305
|
isNullable: true,
|
|
@@ -308,7 +308,7 @@ window.location.href = url.toString()`,
|
|
|
308
308
|
})
|
|
309
309
|
|
|
310
310
|
create_column({
|
|
311
|
-
tableId: "<
|
|
311
|
+
tableId: "<enfyra_user_table_id>",
|
|
312
312
|
name: "emailVerificationStatus",
|
|
313
313
|
type: "varchar",
|
|
314
314
|
isNullable: false,
|
|
@@ -328,7 +328,7 @@ create_column({
|
|
|
328
328
|
})`,
|
|
329
329
|
notes: [
|
|
330
330
|
'Run schema-changing calls sequentially. Do not parallelize create_column calls.',
|
|
331
|
-
'create_column fetches
|
|
331
|
+
'create_column fetches enfyra_table and patches only real persisted columns with id/_id; generated metadata projections such as createdAt, updatedAt, or relation FK display fields are skipped.',
|
|
332
332
|
'Use isEncrypted=true for encryption at rest. Add isUpdatable=false separately only when the field should be immutable.',
|
|
333
333
|
'Use hooks or field permissions to prevent clients from updating server-owned fields.',
|
|
334
334
|
],
|
|
@@ -339,7 +339,7 @@ create_column({
|
|
|
339
339
|
// 1. Read GET /metadata and find the target table.
|
|
340
340
|
// 2. Keep only persisted column rows with id/_id.
|
|
341
341
|
// 3. Add, change, or remove the intended column.
|
|
342
|
-
// 4. PATCH /
|
|
342
|
+
// 4. PATCH /enfyra_table/:id with the full preserved columns array.
|
|
343
343
|
// 5. If the backend returns requiredConfirmHash, resend with ?schemaConfirmHash=<hash>.
|
|
344
344
|
// 6. Re-read metadata and verify unrelated column ids still exist.
|
|
345
345
|
|
|
@@ -351,8 +351,8 @@ create_column({
|
|
|
351
351
|
isEncrypted: true
|
|
352
352
|
})`,
|
|
353
353
|
notes: [
|
|
354
|
-
'Do not rebuild schema cascade payloads from
|
|
355
|
-
'Generated projections such as createdAt, updatedAt, and relation FK display fields without id/_id are not valid
|
|
354
|
+
'Do not rebuild schema cascade payloads from enfyra_table?fields=columns.*; nested fields can be truncated or relation-derived.',
|
|
355
|
+
'Generated projections such as createdAt, updatedAt, and relation FK display fields without id/_id are not valid enfyra_column rows.',
|
|
356
356
|
'Never delete or omit unrelated persisted columns when adding one field.',
|
|
357
357
|
'Run schema-changing calls sequentially; migration locks are backend-owned.',
|
|
358
358
|
],
|
|
@@ -366,7 +366,7 @@ create_column({
|
|
|
366
366
|
{
|
|
367
367
|
name: 'Minimal MCP query then explicit detail query',
|
|
368
368
|
code: `query_table({
|
|
369
|
-
tableName: "
|
|
369
|
+
tableName: "enfyra_user",
|
|
370
370
|
fields: ["id", "email"],
|
|
371
371
|
filter: "{\\"email\\":{\\"_contains\\":\\"@example.com\\"}}",
|
|
372
372
|
limit: 10
|
|
@@ -443,7 +443,7 @@ GET /enfyra/post?filter={"<primaryKeyFromMetadata>":{"_eq":123}}&limit=1`,
|
|
|
443
443
|
{
|
|
444
444
|
name: 'Exclude large generated fields',
|
|
445
445
|
code: `query_table({
|
|
446
|
-
tableName: "
|
|
446
|
+
tableName: "enfyra_route_handler",
|
|
447
447
|
fields: ["-compiledCode"],
|
|
448
448
|
limit: 20
|
|
449
449
|
})
|
|
@@ -581,7 +581,7 @@ return { ok: true, email }\`
|
|
|
581
581
|
notes: [
|
|
582
582
|
'Use sourceCode, not logic. The server generates compiledCode.',
|
|
583
583
|
'Use method for one handler, or methods only when the same sourceCode should be saved for multiple methods.',
|
|
584
|
-
'Do not pass name to
|
|
584
|
+
'Do not pass name to enfyra_route_handler; one handler is identified by route + method.',
|
|
585
585
|
],
|
|
586
586
|
},
|
|
587
587
|
{
|
|
@@ -591,13 +591,13 @@ const password = @BODY.password
|
|
|
591
591
|
|
|
592
592
|
if (!email || !password) @THROW400("Email and password are required")
|
|
593
593
|
|
|
594
|
-
const existing = await #
|
|
594
|
+
const existing = await #enfyra_user.find({
|
|
595
595
|
filter: { email: { _eq: email } },
|
|
596
596
|
limit: 1
|
|
597
597
|
})
|
|
598
598
|
if (existing.data[0]) @THROW409("Email is already registered")
|
|
599
599
|
|
|
600
|
-
const result = await #
|
|
600
|
+
const result = await #enfyra_user.create({
|
|
601
601
|
data: {
|
|
602
602
|
email,
|
|
603
603
|
password: await @HELPERS.$bcrypt.hash(password)
|
|
@@ -656,7 +656,7 @@ const scope = {
|
|
|
656
656
|
{
|
|
657
657
|
name: 'Pre-hook strips protected body fields silently',
|
|
658
658
|
code: `create_pre_hook({
|
|
659
|
-
routeId: "<
|
|
659
|
+
routeId: "<enfyra_user_patch_route_id>",
|
|
660
660
|
name: "strip_email_verification_fields",
|
|
661
661
|
methods: ["PATCH"],
|
|
662
662
|
priority: -10,
|
|
@@ -665,7 +665,7 @@ delete @BODY.emailVerificationStatus
|
|
|
665
665
|
delete @BODY.emailVerificationSentAt\`
|
|
666
666
|
})`,
|
|
667
667
|
notes: [
|
|
668
|
-
'Use this pattern when clients may send protected user fields through /me or
|
|
668
|
+
'Use this pattern when clients may send protected user fields through /me or enfyra_user PATCH.',
|
|
669
669
|
'Strip fields instead of throwing when the product wants a permissive client contract with server-owned fields.',
|
|
670
670
|
'Use native macros such as @BODY instead of raw $ctx when a macro exists.',
|
|
671
671
|
],
|
|
@@ -724,7 +724,7 @@ ensure_route_access({
|
|
|
724
724
|
{
|
|
725
725
|
name: 'Publish read-only route',
|
|
726
726
|
code: `update_record({
|
|
727
|
-
tableName: "
|
|
727
|
+
tableName: "enfyra_route",
|
|
728
728
|
id: "<route_id>",
|
|
729
729
|
data: {
|
|
730
730
|
publicMethods: [{ id: "<GET_method_id_from_list_methods>" }]
|
|
@@ -736,10 +736,85 @@ ensure_route_access({
|
|
|
736
736
|
'Route permissions apply when the method is not public.',
|
|
737
737
|
],
|
|
738
738
|
},
|
|
739
|
+
{
|
|
740
|
+
name: 'Rate limit anonymous requests by IP',
|
|
741
|
+
code: `create_guard({
|
|
742
|
+
name: "Public signup IP rate limit",
|
|
743
|
+
path: "/newsletter_signup",
|
|
744
|
+
methods: ["POST"],
|
|
745
|
+
position: "pre_auth",
|
|
746
|
+
isEnabled: true,
|
|
747
|
+
description: "Limit anonymous signup attempts by client IP.",
|
|
748
|
+
rules: JSON.stringify([
|
|
749
|
+
{
|
|
750
|
+
type: "rate_limit_by_ip",
|
|
751
|
+
config: { maxRequests: 10, perSeconds: 60 },
|
|
752
|
+
description: "10 signup attempts per minute per IP"
|
|
753
|
+
}
|
|
754
|
+
])
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
inspect_route({ path: "/newsletter_signup" })
|
|
758
|
+
|
|
759
|
+
test_rest_endpoint({
|
|
760
|
+
method: "POST",
|
|
761
|
+
path: "/newsletter_signup",
|
|
762
|
+
body: { email: "test@example.com" }
|
|
763
|
+
})`,
|
|
764
|
+
notes: [
|
|
765
|
+
'Use pre_auth for anonymous/public route protection because no user is available yet.',
|
|
766
|
+
'Rate-limit configs use maxRequests and perSeconds.',
|
|
767
|
+
'Inspect and test after creation so the final behavior is verified through the actual REST route.',
|
|
768
|
+
],
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
name: 'Rate limit authenticated users',
|
|
772
|
+
code: `create_guard({
|
|
773
|
+
name: "Project create per-user limit",
|
|
774
|
+
path: "/projects",
|
|
775
|
+
methods: ["POST"],
|
|
776
|
+
position: "post_auth",
|
|
777
|
+
isEnabled: true,
|
|
778
|
+
description: "Authenticated users can create at most 3 projects per hour.",
|
|
779
|
+
rules: JSON.stringify([
|
|
780
|
+
{
|
|
781
|
+
type: "rate_limit_by_user",
|
|
782
|
+
config: { maxRequests: 3, perSeconds: 3600 }
|
|
783
|
+
}
|
|
784
|
+
])
|
|
785
|
+
})`,
|
|
786
|
+
notes: [
|
|
787
|
+
'Use post_auth for rate_limit_by_user because the server only has user id after auth and RoleGuard.',
|
|
788
|
+
'This does not grant access; users still need route permissions or a public method to reach the route.',
|
|
789
|
+
'Do not put rate_limit_by_user on pre_auth guards; the server drops that rule from pre-auth trees.',
|
|
790
|
+
],
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: 'Restrict an admin-only route to office IPs',
|
|
794
|
+
code: `create_guard({
|
|
795
|
+
name: "Admin reports office allowlist",
|
|
796
|
+
path: "/admin/reports",
|
|
797
|
+
methods: ["GET", "POST"],
|
|
798
|
+
position: "pre_auth",
|
|
799
|
+
isEnabled: false,
|
|
800
|
+
description: "Only office network IPs can reach admin reports.",
|
|
801
|
+
rules: JSON.stringify([
|
|
802
|
+
{
|
|
803
|
+
type: "ip_whitelist",
|
|
804
|
+
config: { ips: ["203.0.113.10", "198.51.100.0/24"] }
|
|
805
|
+
}
|
|
806
|
+
])
|
|
807
|
+
})`,
|
|
808
|
+
notes: [
|
|
809
|
+
'Create risky allowlists disabled first, then inspect the saved guard before enabling it.',
|
|
810
|
+
'IP list configs use ips; exact IPv4 addresses and IPv4 CIDR ranges are supported.',
|
|
811
|
+
'An allowlist is an additional gate, not a replacement for route permissions.',
|
|
812
|
+
],
|
|
813
|
+
},
|
|
739
814
|
{
|
|
740
815
|
name: 'Column rule for email format',
|
|
741
816
|
code: `create_column_rule({
|
|
742
|
-
tableName: "
|
|
817
|
+
tableName: "enfyra_user",
|
|
743
818
|
columnName: "email",
|
|
744
819
|
ruleType: "format",
|
|
745
820
|
value: JSON.stringify({ v: "email" }),
|
|
@@ -810,25 +885,25 @@ const { checkPermissionCondition } = usePermissions()
|
|
|
810
885
|
const canReadReports = computed(() => checkPermissionCondition({
|
|
811
886
|
or: [
|
|
812
887
|
{ route: '/reports', methods: ['GET'] },
|
|
813
|
-
{ route: '/
|
|
888
|
+
{ route: '/report', methods: ['GET'] }
|
|
814
889
|
]
|
|
815
890
|
}))
|
|
816
891
|
|
|
817
892
|
const canCreateReport = computed(() => checkPermissionCondition({
|
|
818
|
-
or: [{ route: '/
|
|
893
|
+
or: [{ route: '/report', methods: ['POST'] }]
|
|
819
894
|
}))
|
|
820
895
|
|
|
821
896
|
const canUpdateReport = computed(() => checkPermissionCondition({
|
|
822
|
-
or: [{ route: '/
|
|
897
|
+
or: [{ route: '/report', methods: ['PATCH'] }]
|
|
823
898
|
}))
|
|
824
899
|
|
|
825
900
|
const canDeleteReport = computed(() => checkPermissionCondition({
|
|
826
|
-
or: [{ route: '/
|
|
901
|
+
or: [{ route: '/report', methods: ['DELETE'] }]
|
|
827
902
|
}))
|
|
828
903
|
</script>`,
|
|
829
904
|
notes: [
|
|
830
905
|
'This is menu/extension visibility, not row-level RLS.',
|
|
831
|
-
'Set
|
|
906
|
+
'Set enfyra_menu.permission on every sensitive admin menu. Example for /reports: { or: [{ route: "/reports", methods: ["GET"] }, { route: "/report", methods: ["GET"] }] }.',
|
|
832
907
|
'Admin pages are sensitive. Use permission gates by default, not as an optional polish step.',
|
|
833
908
|
'Menus should only be visible when the user has at least GET permission for the page route or backing data route.',
|
|
834
909
|
'Inside the extension, gate each action by its own route/method: GET for page visibility, POST for create/flow-trigger buttons, PATCH for normal record edits, DELETE for native delete routes.',
|
|
@@ -976,13 +1051,13 @@ return order && order.total > 1000`,
|
|
|
976
1051
|
notes: [
|
|
977
1052
|
'Prefer operation-sized flow steps with clear keys over one large script that performs SSH, Docker, DB, API, email, and finalization work together.',
|
|
978
1053
|
'Each step should return only ids, booleans, status keys, or small counters that later steps need.',
|
|
979
|
-
'When refactoring an existing flow, add or extract adjacent focused
|
|
1054
|
+
'When refactoring an existing flow, add or extract adjacent focused enfyra_flow_step rows instead of making an oversized sourceCode block longer.',
|
|
980
1055
|
],
|
|
981
1056
|
},
|
|
982
1057
|
{
|
|
983
1058
|
name: 'Flow query step config',
|
|
984
1059
|
code: `{
|
|
985
|
-
"table": "
|
|
1060
|
+
"table": "enfyra_user",
|
|
986
1061
|
"filter": { "email": { "_contains": "@example.com" } },
|
|
987
1062
|
"limit": 50
|
|
988
1063
|
}`,
|
|
@@ -1031,7 +1106,7 @@ return saved`,
|
|
|
1031
1106
|
notes: [
|
|
1032
1107
|
'Use file-specific context only in upload-capable routes.',
|
|
1033
1108
|
'For request uploads, pass file: @UPLOADED_FILE to @STORAGE.$upload/@STORAGE.$update so Enfyra streams from the temp file path.',
|
|
1034
|
-
'Use @STORAGE.$registerFile when an external process already uploaded the object and the script only needs to create the
|
|
1109
|
+
'Use @STORAGE.$registerFile when an external process already uploaded the object and the script only needs to create the enfyra_file record.',
|
|
1035
1110
|
'Do not read @UPLOADED_FILE.path into a Buffer and do not generate examples using @UPLOADED_FILE.buffer.',
|
|
1036
1111
|
'Use buffer only for small generated or transformed files, such as image thumbnails.',
|
|
1037
1112
|
],
|
|
@@ -1058,8 +1133,8 @@ update_method({
|
|
|
1058
1133
|
textColor: "#b45309"
|
|
1059
1134
|
})`,
|
|
1060
1135
|
notes: [
|
|
1061
|
-
'Use dedicated method tools instead of generic CRUD on
|
|
1062
|
-
'The backend stores the method label in
|
|
1136
|
+
'Use dedicated method tools instead of generic CRUD on enfyra_method.',
|
|
1137
|
+
'The backend stores the method label in enfyra_method.name; do not send or filter a `method` field on `enfyra_method`.',
|
|
1063
1138
|
'buttonColor is the badge background and textColor is the badge text color.',
|
|
1064
1139
|
'The Enfyra admin UI is /settings/methods.',
|
|
1065
1140
|
'delete_method is preview-first and should only be used for unused custom methods.',
|
|
@@ -1077,7 +1152,7 @@ update_method({
|
|
|
1077
1152
|
permission: JSON.stringify({
|
|
1078
1153
|
or: [
|
|
1079
1154
|
{ route: "/reports", methods: ["GET"] },
|
|
1080
|
-
{ route: "/
|
|
1155
|
+
{ route: "/report", methods: ["GET"] }
|
|
1081
1156
|
]
|
|
1082
1157
|
})
|
|
1083
1158
|
})
|
|
@@ -1093,7 +1168,7 @@ create_extension({
|
|
|
1093
1168
|
})`,
|
|
1094
1169
|
notes: [
|
|
1095
1170
|
'Menu provides navigation; extension provides content.',
|
|
1096
|
-
'Use
|
|
1171
|
+
'Use enfyra_menu.label, not title.',
|
|
1097
1172
|
'Sensitive admin menus should include a permission condition at creation time.',
|
|
1098
1173
|
'For page extensions, create the menu first and pass menuId to create_extension.',
|
|
1099
1174
|
'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
|
|
@@ -1151,12 +1226,12 @@ create_extension({
|
|
|
1151
1226
|
type: "page",
|
|
1152
1227
|
name: "ReportsPage",
|
|
1153
1228
|
menuId: "<reports-menu-id>",
|
|
1154
|
-
code: "<template><section class=\\"min-h-full w-full space-y-4\\"><Widget :id=\\"<report-status-widget-id>\\" :total=\\"totalReports\\" :rows=\\"reportRows\\" :open-details=\\"openReportDetails\\" @refresh=\\"refresh\\" /><Widget :id=\\"<report-table-widget-id>\\" :rows=\\"reportRows\\" @refresh=\\"refresh\\" /></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'cyan', variant: 'minimal' }); const totalReports = ref(0); const reportRows = ref([]); function refresh() {} function openReportDetails(row) { navigateTo('/data/
|
|
1229
|
+
code: "<template><section class=\\"min-h-full w-full space-y-4\\"><Widget :id=\\"<report-status-widget-id>\\" :total=\\"totalReports\\" :rows=\\"reportRows\\" :open-details=\\"openReportDetails\\" @refresh=\\"refresh\\" /><Widget :id=\\"<report-table-widget-id>\\" :rows=\\"reportRows\\" @refresh=\\"refresh\\" /></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'cyan', variant: 'minimal' }); const totalReports = ref(0); const reportRows = ref([]); function refresh() {} function openReportDetails(row) { navigateTo('/data/report?filter=' + encodeURIComponent(JSON.stringify({ id: { _eq: row.id } }))) }</script>",
|
|
1155
1230
|
isEnabled: true
|
|
1156
1231
|
})`,
|
|
1157
1232
|
notes: [
|
|
1158
1233
|
'Use widgets for bulky or reusable sections such as operation panels, timelines, tables, sidebars, and status cards.',
|
|
1159
|
-
'Embed widgets by their numeric
|
|
1234
|
+
'Embed widgets by their numeric enfyra_extension id, not by extensionId/name.',
|
|
1160
1235
|
'Props and listeners pass through the Widget wrapper. Widget defineProps values update reactively when the parent refs/computed values change.',
|
|
1161
1236
|
'Use kebab-case in the parent template for camelCase widget props, for example :open-details maps to openDetails.',
|
|
1162
1237
|
'Do not mutate widget props. Use computed for derived display state, and use watch only when mirroring a prop into local editable draft state.',
|
|
@@ -1338,11 +1413,11 @@ registerHeaderActions([
|
|
|
1338
1413
|
},
|
|
1339
1414
|
{
|
|
1340
1415
|
name: 'Debug menu or extension changes that do not appear in open Enfyra admin tabs',
|
|
1341
|
-
code: `// Server side:
|
|
1416
|
+
code: `// Server side: enfyra_menu and enfyra_extension are runtime UI definitions.
|
|
1342
1417
|
// They must participate in partial reload, just like metadata/routes.
|
|
1343
1418
|
// Expected server contract:
|
|
1344
|
-
// - cache orchestrator maps
|
|
1345
|
-
// - cache orchestrator maps
|
|
1419
|
+
// - cache orchestrator maps enfyra_menu -> menu reload
|
|
1420
|
+
// - cache orchestrator maps enfyra_extension -> extension reload
|
|
1346
1421
|
// - successful writes emit $system:reload to the admin Socket.IO namespace
|
|
1347
1422
|
|
|
1348
1423
|
// Enfyra admin UI side expected listener behavior:
|
|
@@ -1379,7 +1454,7 @@ create_menu({
|
|
|
1379
1454
|
permission: JSON.stringify({
|
|
1380
1455
|
or: [
|
|
1381
1456
|
{ route: "/operations/jobs", methods: ["GET"] },
|
|
1382
|
-
{ route: "/
|
|
1457
|
+
{ route: "/enfyra_flow_execution", methods: ["GET"] }
|
|
1383
1458
|
]
|
|
1384
1459
|
})
|
|
1385
1460
|
})
|
|
@@ -1391,7 +1466,7 @@ create_menu({
|
|
|
1391
1466
|
// /operations/reports report configuration and delivery history
|
|
1392
1467
|
// /operations/settings system readiness and configuration
|
|
1393
1468
|
// Use UTabs inside large pages instead of placing every section in one dashboard.
|
|
1394
|
-
// For admin record management, link to /data/<table>, e.g. /data/
|
|
1469
|
+
// For admin record management, link to /data/<table>, e.g. /data/report, not public website paths.`,
|
|
1395
1470
|
notes: [
|
|
1396
1471
|
'Design the menu/page split before generating dashboard code.',
|
|
1397
1472
|
'Permission-gate sensitive parent dropdown menus too, using any child page route or backing route that represents read access.',
|
|
@@ -1408,7 +1483,7 @@ create_menu({
|
|
|
1408
1483
|
{
|
|
1409
1484
|
name: 'Extension fetches Enfyra data',
|
|
1410
1485
|
code: `<script setup>
|
|
1411
|
-
const { data, pending, execute: fetchOrders } = useApi('/
|
|
1486
|
+
const { data, pending, execute: fetchOrders } = useApi('/order', {
|
|
1412
1487
|
query: {
|
|
1413
1488
|
limit: 10,
|
|
1414
1489
|
sort: '-createdAt'
|
|
@@ -1510,7 +1585,7 @@ onMounted(async () => {
|
|
|
1510
1585
|
</template>`,
|
|
1511
1586
|
notes: [
|
|
1512
1587
|
'Install browser-side extension dependencies as type: "App".',
|
|
1513
|
-
'Do not use static import statements in
|
|
1588
|
+
'Do not use static import statements in enfyra_extension.code.',
|
|
1514
1589
|
'Load app packages with getPackages([...]) inside the extension runtime.',
|
|
1515
1590
|
'Use onMounted or an explicit action for package loading when the UI can render a loading state.',
|
|
1516
1591
|
],
|
|
@@ -1528,7 +1603,7 @@ const rangeStart = computed(() => {
|
|
|
1528
1603
|
return d.toISOString()
|
|
1529
1604
|
})
|
|
1530
1605
|
|
|
1531
|
-
const flowStats = useApi('/
|
|
1606
|
+
const flowStats = useApi('/enfyra_flow_execution', {
|
|
1532
1607
|
query: computed(() => ({
|
|
1533
1608
|
fields: 'id',
|
|
1534
1609
|
limit: 1,
|
|
@@ -1541,7 +1616,7 @@ const flowStats = useApi('/flow_execution_definition', {
|
|
|
1541
1616
|
}))
|
|
1542
1617
|
})
|
|
1543
1618
|
|
|
1544
|
-
const orderStats = useApi('/
|
|
1619
|
+
const orderStats = useApi('/order', {
|
|
1545
1620
|
query: computed(() => ({
|
|
1546
1621
|
fields: 'id',
|
|
1547
1622
|
limit: 1,
|