@baasix/baasix 0.1.3 → 0.1.5

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 (258) hide show
  1. package/README.md +50 -15
  2. package/dist/README.md +50 -15
  3. package/dist/app/404/index.html +1 -1
  4. package/dist/app/404.html +1 -1
  5. package/dist/app/_next/static/chunks/{3817-e20c8f0a0810fc95.js → 3817-5170706432487eeb.js} +1 -1
  6. package/dist/app/_next/static/chunks/{9599-a7e572bb88c3392b.js → 9599-c979a80a3f8557a2.js} +1 -1
  7. package/dist/app/_next/static/chunks/app/(authenticated)/settings/schema/page-12bec5e776a0974d.js +1 -0
  8. package/dist/app/_next/static/chunks/app/(authenticated)/workflows/page-5c2330ee5dc13ab1.js +1 -0
  9. package/dist/app/_next/static/chunks/{webpack-97b8127545c29856.js → webpack-c0a542d3562cc14f.js} +1 -1
  10. package/dist/app/activity-log/all-activity/index.html +1 -1
  11. package/dist/app/activity-log/all-activity/index.txt +2 -2
  12. package/dist/app/activity-log/email-log/index.html +1 -1
  13. package/dist/app/activity-log/email-log/index.txt +2 -2
  14. package/dist/app/activity-log/index.html +1 -1
  15. package/dist/app/activity-log/index.txt +1 -1
  16. package/dist/app/activity-log/notifications/index.html +1 -1
  17. package/dist/app/activity-log/notifications/index.txt +1 -1
  18. package/dist/app/activity-log/sessions/index.html +1 -1
  19. package/dist/app/activity-log/sessions/index.txt +1 -1
  20. package/dist/app/activity-log/workflow-executions/index.html +1 -1
  21. package/dist/app/activity-log/workflow-executions/index.txt +1 -1
  22. package/dist/app/activity-log/workflow-logs/index.html +1 -1
  23. package/dist/app/activity-log/workflow-logs/index.txt +1 -1
  24. package/dist/app/change-password/index.html +1 -1
  25. package/dist/app/change-password/index.txt +1 -1
  26. package/dist/app/dashboard/index.html +1 -1
  27. package/dist/app/dashboard/index.txt +1 -1
  28. package/dist/app/data-browser/index.html +1 -1
  29. package/dist/app/data-browser/index.txt +1 -1
  30. package/dist/app/file-manager/index.html +1 -1
  31. package/dist/app/file-manager/index.txt +2 -2
  32. package/dist/app/forgot-password/index.html +1 -1
  33. package/dist/app/forgot-password/index.txt +1 -1
  34. package/dist/app/index.html +1 -1
  35. package/dist/app/index.txt +1 -1
  36. package/dist/app/login/index.html +1 -1
  37. package/dist/app/login/index.txt +1 -1
  38. package/dist/app/register/index.html +1 -1
  39. package/dist/app/register/index.txt +1 -1
  40. package/dist/app/settings/migrations/index.html +1 -1
  41. package/dist/app/settings/migrations/index.txt +1 -1
  42. package/dist/app/settings/permissions/index.html +1 -1
  43. package/dist/app/settings/permissions/index.txt +2 -2
  44. package/dist/app/settings/project/index.html +1 -1
  45. package/dist/app/settings/project/index.txt +2 -2
  46. package/dist/app/settings/roles/index.html +1 -1
  47. package/dist/app/settings/roles/index.txt +2 -2
  48. package/dist/app/settings/schema/index.html +1 -1
  49. package/dist/app/settings/schema/index.txt +2 -2
  50. package/dist/app/settings/tasks/index.html +1 -1
  51. package/dist/app/settings/tasks/index.txt +1 -1
  52. package/dist/app/settings/templates/edit/index.html +1 -1
  53. package/dist/app/settings/templates/edit/index.txt +1 -1
  54. package/dist/app/settings/templates/index.html +1 -1
  55. package/dist/app/settings/templates/index.txt +2 -2
  56. package/dist/app/settings/tenants/index.html +1 -1
  57. package/dist/app/settings/tenants/index.txt +2 -2
  58. package/dist/app/users/index.html +1 -1
  59. package/dist/app/users/index.txt +2 -2
  60. package/dist/app/users/invites/index.html +1 -1
  61. package/dist/app/users/invites/index.txt +1 -1
  62. package/dist/app/users/list/index.html +1 -1
  63. package/dist/app/users/list/index.txt +2 -2
  64. package/dist/app/users/preferences/index.html +1 -1
  65. package/dist/app/users/preferences/index.txt +1 -1
  66. package/dist/app/users/user-roles/index.html +1 -1
  67. package/dist/app/users/user-roles/index.txt +1 -1
  68. package/dist/app/workflows/detail/index.html +1 -1
  69. package/dist/app/workflows/detail/index.txt +1 -1
  70. package/dist/app/workflows/edit/index.html +1 -1
  71. package/dist/app/workflows/edit/index.txt +1 -1
  72. package/dist/app/workflows/execution/index.html +1 -1
  73. package/dist/app/workflows/execution/index.txt +1 -1
  74. package/dist/app/workflows/index.html +1 -1
  75. package/dist/app/workflows/index.txt +2 -2
  76. package/dist/auth/routes.d.ts +1 -1
  77. package/dist/auth/routes.d.ts.map +1 -1
  78. package/dist/routes/auth.route.d.ts +1 -1
  79. package/dist/routes/auth.route.d.ts.map +1 -1
  80. package/dist/routes/file.route.d.ts +1 -1
  81. package/dist/routes/file.route.d.ts.map +1 -1
  82. package/dist/routes/items.route.d.ts +1 -1
  83. package/dist/routes/items.route.d.ts.map +1 -1
  84. package/dist/routes/mcp.route.d.ts +23 -0
  85. package/dist/routes/mcp.route.d.ts.map +1 -0
  86. package/dist/routes/mcp.route.js +318 -0
  87. package/dist/routes/mcp.route.js.map +1 -0
  88. package/dist/routes/migration.route.d.ts +1 -1
  89. package/dist/routes/migration.route.d.ts.map +1 -1
  90. package/dist/routes/notification.route.d.ts +1 -1
  91. package/dist/routes/notification.route.d.ts.map +1 -1
  92. package/dist/routes/openapi.route.d.ts +1 -1
  93. package/dist/routes/openapi.route.d.ts.map +1 -1
  94. package/dist/routes/openapi.route.js +1 -1
  95. package/dist/routes/openapi.route.js.map +1 -1
  96. package/dist/routes/permission.route.d.ts +1 -1
  97. package/dist/routes/permission.route.d.ts.map +1 -1
  98. package/dist/routes/realtime.route.d.ts +1 -1
  99. package/dist/routes/realtime.route.d.ts.map +1 -1
  100. package/dist/routes/reports.route.d.ts +1 -1
  101. package/dist/routes/reports.route.d.ts.map +1 -1
  102. package/dist/routes/schema.route.d.ts +1 -1
  103. package/dist/routes/schema.route.d.ts.map +1 -1
  104. package/dist/routes/schema.route.js +10 -6
  105. package/dist/routes/schema.route.js.map +1 -1
  106. package/dist/routes/settings.route.d.ts +1 -1
  107. package/dist/routes/settings.route.d.ts.map +1 -1
  108. package/dist/routes/templates.route.d.ts +1 -1
  109. package/dist/routes/templates.route.d.ts.map +1 -1
  110. package/dist/routes/utils.route.d.ts +1 -1
  111. package/dist/routes/utils.route.d.ts.map +1 -1
  112. package/dist/routes/workflow.route.d.ts +1 -1
  113. package/dist/routes/workflow.route.d.ts.map +1 -1
  114. package/dist/services/MCPService.d.ts +39 -0
  115. package/dist/services/MCPService.d.ts.map +1 -0
  116. package/dist/services/MCPService.js +1413 -0
  117. package/dist/services/MCPService.js.map +1 -0
  118. package/dist/services/PluginManager.d.ts +3 -4
  119. package/dist/services/PluginManager.d.ts.map +1 -1
  120. package/dist/services/PluginManager.js.map +1 -1
  121. package/dist/services/TasksService.d.ts +2 -2
  122. package/dist/services/TasksService.d.ts.map +1 -1
  123. package/dist/types/drizzle.d.ts +22 -0
  124. package/dist/types/drizzle.d.ts.map +1 -0
  125. package/dist/types/drizzle.js +6 -0
  126. package/dist/types/drizzle.js.map +1 -0
  127. package/dist/types/external.d.ts +69 -0
  128. package/dist/types/external.d.ts.map +1 -0
  129. package/dist/types/external.js +6 -0
  130. package/dist/types/external.js.map +1 -0
  131. package/dist/types/files.d.ts +2 -2
  132. package/dist/types/index.d.ts +28 -23
  133. package/dist/types/index.d.ts.map +1 -1
  134. package/dist/types/index.js +22 -50
  135. package/dist/types/index.js.map +1 -1
  136. package/dist/types/internal.d.ts +57 -0
  137. package/dist/types/internal.d.ts.map +1 -0
  138. package/dist/types/internal.js +6 -0
  139. package/dist/types/internal.js.map +1 -0
  140. package/dist/types/plugin.d.ts +6 -344
  141. package/dist/types/plugin.d.ts.map +1 -1
  142. package/dist/types/plugin.js +2 -2
  143. package/dist/types/query.d.ts +5 -30
  144. package/dist/types/query.d.ts.map +1 -1
  145. package/dist/types/query.js +3 -1
  146. package/dist/types/query.js.map +1 -1
  147. package/dist/types/relations.d.ts +1 -1
  148. package/dist/types/relations.d.ts.map +1 -1
  149. package/dist/types/services.d.ts +13 -29
  150. package/dist/types/services.d.ts.map +1 -1
  151. package/dist/types/services.js +1 -0
  152. package/dist/types/services.js.map +1 -1
  153. package/dist/utils/auth.d.ts.map +1 -1
  154. package/dist/utils/auth.js +27 -5
  155. package/dist/utils/auth.js.map +1 -1
  156. package/dist/utils/db.d.ts +1 -1
  157. package/dist/utils/fieldUtils.d.ts +2 -2
  158. package/dist/utils/fieldUtils.d.ts.map +1 -1
  159. package/dist/utils/router.d.ts +1 -1
  160. package/dist/utils/router.d.ts.map +1 -1
  161. package/dist/utils/schemaManager.d.ts +27 -3
  162. package/dist/utils/schemaManager.d.ts.map +1 -1
  163. package/dist/utils/schemaManager.js +37 -0
  164. package/dist/utils/schemaManager.js.map +1 -1
  165. package/dist/utils/sortUtils.d.ts +1 -1
  166. package/dist/utils/sortUtils.d.ts.map +1 -1
  167. package/dist/utils/typeMapper.d.ts +2 -1
  168. package/dist/utils/typeMapper.d.ts.map +1 -1
  169. package/dist/utils/typeMapper.js.map +1 -1
  170. package/package.json +3 -1
  171. package/dist/app/_next/static/chunks/app/(authenticated)/settings/schema/page-899574f35091dd58.js +0 -1
  172. package/dist/app/_next/static/chunks/app/(authenticated)/workflows/page-1a7a4305ef892fa8.js +0 -1
  173. package/dist/types/aggregation.d.ts +0 -40
  174. package/dist/types/aggregation.d.ts.map +0 -1
  175. package/dist/types/aggregation.js +0 -6
  176. package/dist/types/aggregation.js.map +0 -1
  177. package/dist/types/assets.d.ts +0 -32
  178. package/dist/types/assets.d.ts.map +0 -1
  179. package/dist/types/assets.js +0 -6
  180. package/dist/types/assets.js.map +0 -1
  181. package/dist/types/auth.d.ts +0 -50
  182. package/dist/types/auth.d.ts.map +0 -1
  183. package/dist/types/auth.js +0 -6
  184. package/dist/types/auth.js.map +0 -1
  185. package/dist/types/cache.d.ts +0 -47
  186. package/dist/types/cache.d.ts.map +0 -1
  187. package/dist/types/cache.js +0 -6
  188. package/dist/types/cache.js.map +0 -1
  189. package/dist/types/database.d.ts +0 -16
  190. package/dist/types/database.d.ts.map +0 -1
  191. package/dist/types/database.js +0 -6
  192. package/dist/types/database.js.map +0 -1
  193. package/dist/types/fields.d.ts +0 -71
  194. package/dist/types/fields.d.ts.map +0 -1
  195. package/dist/types/fields.js +0 -6
  196. package/dist/types/fields.js.map +0 -1
  197. package/dist/types/import-export.d.ts +0 -62
  198. package/dist/types/import-export.d.ts.map +0 -1
  199. package/dist/types/import-export.js +0 -6
  200. package/dist/types/import-export.js.map +0 -1
  201. package/dist/types/mail.d.ts +0 -34
  202. package/dist/types/mail.d.ts.map +0 -1
  203. package/dist/types/mail.js +0 -6
  204. package/dist/types/mail.js.map +0 -1
  205. package/dist/types/notifications.d.ts +0 -16
  206. package/dist/types/notifications.d.ts.map +0 -1
  207. package/dist/types/notifications.js +0 -6
  208. package/dist/types/notifications.js.map +0 -1
  209. package/dist/types/reports.d.ts +0 -17
  210. package/dist/types/reports.d.ts.map +0 -1
  211. package/dist/types/reports.js +0 -6
  212. package/dist/types/reports.js.map +0 -1
  213. package/dist/types/schema.d.ts +0 -26
  214. package/dist/types/schema.d.ts.map +0 -1
  215. package/dist/types/schema.js +0 -6
  216. package/dist/types/schema.js.map +0 -1
  217. package/dist/types/seed.d.ts +0 -27
  218. package/dist/types/seed.d.ts.map +0 -1
  219. package/dist/types/seed.js +0 -6
  220. package/dist/types/seed.js.map +0 -1
  221. package/dist/types/settings.d.ts +0 -36
  222. package/dist/types/settings.d.ts.map +0 -1
  223. package/dist/types/settings.js +0 -6
  224. package/dist/types/settings.js.map +0 -1
  225. package/dist/types/sockets.d.ts +0 -26
  226. package/dist/types/sockets.d.ts.map +0 -1
  227. package/dist/types/sockets.js +0 -6
  228. package/dist/types/sockets.js.map +0 -1
  229. package/dist/types/sort.d.ts +0 -25
  230. package/dist/types/sort.d.ts.map +0 -1
  231. package/dist/types/sort.js +0 -6
  232. package/dist/types/sort.js.map +0 -1
  233. package/dist/types/spatial.d.ts +0 -19
  234. package/dist/types/spatial.d.ts.map +0 -1
  235. package/dist/types/spatial.js +0 -6
  236. package/dist/types/spatial.js.map +0 -1
  237. package/dist/types/stats.d.ts +0 -21
  238. package/dist/types/stats.d.ts.map +0 -1
  239. package/dist/types/stats.js +0 -6
  240. package/dist/types/stats.js.map +0 -1
  241. package/dist/types/storage.d.ts +0 -19
  242. package/dist/types/storage.d.ts.map +0 -1
  243. package/dist/types/storage.js +0 -6
  244. package/dist/types/storage.js.map +0 -1
  245. package/dist/types/tasks.d.ts +0 -14
  246. package/dist/types/tasks.d.ts.map +0 -1
  247. package/dist/types/tasks.js +0 -6
  248. package/dist/types/tasks.js.map +0 -1
  249. package/dist/types/utils.d.ts +0 -54
  250. package/dist/types/utils.d.ts.map +0 -1
  251. package/dist/types/utils.js +0 -6
  252. package/dist/types/utils.js.map +0 -1
  253. package/dist/types/workflow.d.ts +0 -17
  254. package/dist/types/workflow.d.ts.map +0 -1
  255. package/dist/types/workflow.js +0 -6
  256. package/dist/types/workflow.js.map +0 -1
  257. /package/dist/app/_next/static/{IrfWLqQW-v_VRpZ550bv5 → FlPSV3QluMHtyypp36AiX}/_buildManifest.js +0 -0
  258. /package/dist/app/_next/static/{IrfWLqQW-v_VRpZ550bv5 → FlPSV3QluMHtyypp36AiX}/_ssgManifest.js +0 -0
@@ -0,0 +1,1413 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * MCPService - Model Context Protocol Server for Baasix
4
+ *
5
+ * This service provides MCP tools that directly call Baasix services,
6
+ * eliminating HTTP round-trips and providing better performance.
7
+ *
8
+ * Enable via environment variable: MCP_ENABLED=true
9
+ * Access at: http://localhost:8056/mcp (or custom MCP_PATH)
10
+ *
11
+ * Note: @ts-nocheck is used because the MCP SDK + Zod combination causes
12
+ * TypeScript to hang during compilation due to complex generic type inference.
13
+ * The explicit type annotations are kept for documentation and IDE support.
14
+ */
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { z } from "zod";
17
+ import { schemaManager } from "../utils/schemaManager.js";
18
+ import { ItemsService } from "./ItemsService.js";
19
+ import permissionService from "./PermissionService.js";
20
+ import realtimeService from "./RealtimeService.js";
21
+ import settingsService from "./SettingsService.js";
22
+ import env from "../utils/env.js";
23
+ // ==================== Helper Functions ====================
24
+ // Session storage for authenticated MCP clients
25
+ const mcpSessions = new Map();
26
+ /**
27
+ * Store accountability info for an MCP session
28
+ */
29
+ export function setMCPSession(sessionId, accountability) {
30
+ mcpSessions.set(sessionId, accountability);
31
+ }
32
+ /**
33
+ * Get accountability info from session
34
+ */
35
+ function getAccountabilityFromSession(sessionId) {
36
+ return mcpSessions.get(sessionId);
37
+ }
38
+ /**
39
+ * Get default accountability (admin for now, can be configured)
40
+ */
41
+ function getDefaultAccountability() {
42
+ return {
43
+ user: null,
44
+ role: "administrator",
45
+ admin: true,
46
+ ip: "127.0.0.1",
47
+ };
48
+ }
49
+ /**
50
+ * Remove an MCP session
51
+ */
52
+ export function removeMCPSession(sessionId) {
53
+ mcpSessions.delete(sessionId);
54
+ }
55
+ /**
56
+ * Get accountability from extra context
57
+ */
58
+ function getAccountability(extra) {
59
+ if (extra.sessionId) {
60
+ const session = getAccountabilityFromSession(extra.sessionId);
61
+ if (session)
62
+ return session;
63
+ }
64
+ return getDefaultAccountability();
65
+ }
66
+ /**
67
+ * Create success response
68
+ */
69
+ function successResult(data) {
70
+ return {
71
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
72
+ };
73
+ }
74
+ /**
75
+ * Create error response
76
+ */
77
+ function errorResult(error) {
78
+ const message = typeof error === "string" ? error : error.message;
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
81
+ isError: true,
82
+ };
83
+ }
84
+ // ==================== MCP Server Creation ====================
85
+ /**
86
+ * Create and configure the MCP server with all Baasix tools
87
+ */
88
+ export function createMCPServer() {
89
+ const server = new McpServer({
90
+ name: "baasix-mcp-server",
91
+ version: "0.1.0",
92
+ });
93
+ // ==================== Schema Management Tools ====================
94
+ server.tool("baasix_list_schemas", "Get all available collections/schemas in Baasix with optional search and pagination", {
95
+ search: z.string().optional().describe("Search term to filter schemas by collection name"),
96
+ page: z.number().optional().default(1).describe("Page number for pagination"),
97
+ limit: z.number().optional().default(10).describe("Number of schemas per page"),
98
+ sort: z.string().optional().default("collectionName:asc").describe("Sort field and direction"),
99
+ }, async (args, extra) => {
100
+ const { search, page, limit, sort } = args;
101
+ const accountability = getAccountability(extra);
102
+ const schemaService = new ItemsService("baasix_SchemaDefinition", { accountability });
103
+ const query = {
104
+ fields: ["collectionName", "schema"],
105
+ page,
106
+ limit,
107
+ };
108
+ if (sort && sort.includes(":")) {
109
+ const [field, direction] = sort.split(":");
110
+ query.sort = [direction?.toLowerCase() === "desc" ? `-${field}` : field];
111
+ }
112
+ if (search) {
113
+ query.search = search;
114
+ query.searchFields = ["collectionName"];
115
+ }
116
+ const result = await schemaService.readByQuery(query, true);
117
+ return successResult({
118
+ data: result.data,
119
+ totalCount: result.totalCount,
120
+ page,
121
+ limit,
122
+ });
123
+ });
124
+ server.tool("baasix_get_schema", "Get detailed schema information for a specific collection", {
125
+ collection: z.string().describe("Collection name"),
126
+ }, async (args, _extra) => {
127
+ const { collection } = args;
128
+ const schemaDef = schemaManager.getSchemaDefinition(collection);
129
+ if (!schemaDef) {
130
+ return errorResult(`Schema '${collection}' not found`);
131
+ }
132
+ return successResult({
133
+ collectionName: collection,
134
+ schema: schemaDef,
135
+ });
136
+ });
137
+ server.tool("baasix_create_schema", `Create a new collection schema in Baasix.
138
+
139
+ FIELD TYPES:
140
+ - String: VARCHAR with values.length (e.g., 255)
141
+ - Text: Unlimited text
142
+ - Integer, BigInt: Whole numbers
143
+ - Decimal: values.precision & values.scale
144
+ - Float, Real, Double: Floating point
145
+ - Boolean: true/false
146
+ - Date, DateTime, Time: Date/time
147
+ - UUID: With defaultValue.type: "UUIDV4"
148
+ - SUID: Short unique ID with defaultValue.type: "SUID"
149
+ - JSONB: JSON with indexing
150
+ - Array: values.type specifies element type
151
+ - Enum: values.values array
152
+
153
+ DEFAULT VALUE TYPES:
154
+ - { type: "UUIDV4" } - Random UUID v4
155
+ - { type: "SUID" } - Short unique ID
156
+ - { type: "NOW" } - Current timestamp
157
+ - { type: "AUTOINCREMENT" } - Auto-incrementing integer
158
+ - { type: "SQL", value: "..." } - Custom SQL expression`, {
159
+ collection: z.string().describe("Collection name"),
160
+ schema: z
161
+ .object({
162
+ fields: z.record(z.any()).describe("Field definitions"),
163
+ })
164
+ .passthrough()
165
+ .describe("Schema definition"),
166
+ }, async (args, _extra) => {
167
+ const { collection, schema } = args;
168
+ try {
169
+ await schemaManager.createCollection(collection, schema);
170
+ return successResult({
171
+ success: true,
172
+ message: `Collection '${collection}' created successfully`,
173
+ collectionName: collection,
174
+ });
175
+ }
176
+ catch (error) {
177
+ return errorResult(error);
178
+ }
179
+ });
180
+ server.tool("baasix_update_schema", "Update an existing collection schema (add/modify/remove fields)", {
181
+ collection: z.string().describe("Collection name"),
182
+ schema: z
183
+ .object({
184
+ fields: z.record(z.any()).optional().describe("Updated field definitions"),
185
+ })
186
+ .passthrough()
187
+ .describe("Schema updates"),
188
+ }, async (args, _extra) => {
189
+ const { collection, schema } = args;
190
+ try {
191
+ await schemaManager.updateCollection(collection, schema);
192
+ return successResult({
193
+ success: true,
194
+ message: `Collection '${collection}' updated successfully`,
195
+ });
196
+ }
197
+ catch (error) {
198
+ return errorResult(error);
199
+ }
200
+ });
201
+ server.tool("baasix_delete_schema", "Delete a collection and all its data", {
202
+ collection: z.string().describe("Collection name to delete"),
203
+ }, async (args, _extra) => {
204
+ const { collection } = args;
205
+ try {
206
+ await schemaManager.deleteCollection(collection);
207
+ return successResult({
208
+ success: true,
209
+ message: `Collection '${collection}' deleted successfully`,
210
+ });
211
+ }
212
+ catch (error) {
213
+ return errorResult(error);
214
+ }
215
+ });
216
+ server.tool("baasix_add_index", "Add an index to a collection for better query performance", {
217
+ collection: z.string().describe("Collection name"),
218
+ indexDefinition: z
219
+ .object({
220
+ name: z.string().optional().describe("Index name (auto-generated if not provided)"),
221
+ fields: z.array(z.string()).describe("Fields to index"),
222
+ unique: z.boolean().optional().describe("Whether the index should be unique"),
223
+ type: z.enum(["btree", "hash", "gin", "gist"]).optional().describe("Index type"),
224
+ })
225
+ .describe("Index definition"),
226
+ }, async (args, _extra) => {
227
+ const { collection, indexDefinition } = args;
228
+ try {
229
+ await schemaManager.addIndex(collection, indexDefinition);
230
+ return successResult({
231
+ success: true,
232
+ message: `Index added to '${collection}' successfully`,
233
+ });
234
+ }
235
+ catch (error) {
236
+ return errorResult(error);
237
+ }
238
+ });
239
+ server.tool("baasix_remove_index", "Remove an index from a collection", {
240
+ collection: z.string().describe("Collection name"),
241
+ indexName: z.string().describe("Name of the index to remove"),
242
+ }, async (args, _extra) => {
243
+ const { collection, indexName } = args;
244
+ try {
245
+ await schemaManager.removeIndex(collection, indexName);
246
+ return successResult({
247
+ success: true,
248
+ message: `Index '${indexName}' removed from '${collection}'`,
249
+ });
250
+ }
251
+ catch (error) {
252
+ return errorResult(error);
253
+ }
254
+ });
255
+ server.tool("baasix_create_relationship", `Create a relationship between collections.
256
+
257
+ RELATIONSHIP TYPES:
258
+ - M2O (Many-to-One): Creates foreign key with auto-index. products.category → categories
259
+ - O2M (One-to-Many): Virtual reverse of M2O. categories.products → products
260
+ - O2O (One-to-One): Creates foreign key with auto-index. user.profile → profiles
261
+ - M2M (Many-to-Many): Creates junction table with auto-indexed FKs. products ↔ tags
262
+ - M2A (Many-to-Any): Polymorphic junction table. comments → posts OR products
263
+
264
+ EXAMPLE M2O:
265
+ {
266
+ "name": "category",
267
+ "type": "M2O",
268
+ "target": "categories",
269
+ "alias": "products",
270
+ "onDelete": "CASCADE"
271
+ }`, {
272
+ sourceCollection: z.string().describe("Source collection name"),
273
+ relationshipData: z
274
+ .object({
275
+ name: z.string().describe("Relationship field name"),
276
+ type: z.enum(["M2O", "O2M", "O2O", "M2M", "M2A"]).describe("Relationship type"),
277
+ target: z.string().optional().describe("Target collection name"),
278
+ alias: z.string().optional().describe("Alias for reverse relationship"),
279
+ onDelete: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Delete behavior"),
280
+ onUpdate: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Update behavior"),
281
+ tables: z.array(z.string()).optional().describe("Target tables for M2A relationships"),
282
+ through: z.string().optional().describe("Custom junction table name for M2M/M2A"),
283
+ })
284
+ .describe("Relationship configuration"),
285
+ }, async (args, _extra) => {
286
+ const { sourceCollection, relationshipData } = args;
287
+ try {
288
+ await schemaManager.addRelationship(sourceCollection, relationshipData);
289
+ return successResult({
290
+ success: true,
291
+ message: `Relationship '${relationshipData.name}' created on '${sourceCollection}'`,
292
+ });
293
+ }
294
+ catch (error) {
295
+ return errorResult(error);
296
+ }
297
+ });
298
+ server.tool("baasix_delete_relationship", "Delete a relationship from a collection", {
299
+ sourceCollection: z.string().describe("Source collection name"),
300
+ fieldName: z.string().describe("Relationship field name"),
301
+ }, async (args, _extra) => {
302
+ const { sourceCollection, fieldName } = args;
303
+ try {
304
+ await schemaManager.removeRelationship(sourceCollection, fieldName);
305
+ return successResult({
306
+ success: true,
307
+ message: `Relationship '${fieldName}' removed from '${sourceCollection}'`,
308
+ });
309
+ }
310
+ catch (error) {
311
+ return errorResult(error);
312
+ }
313
+ });
314
+ server.tool("baasix_export_schemas", "Export all schemas as JSON for backup or migration", {}, async (_args, _extra) => {
315
+ try {
316
+ const schemas = schemaManager.exportSchemas();
317
+ return successResult(schemas);
318
+ }
319
+ catch (error) {
320
+ return errorResult(error);
321
+ }
322
+ });
323
+ server.tool("baasix_import_schemas", "Import schemas from JSON data", {
324
+ schemas: z.record(z.any()).describe("Schema data to import"),
325
+ }, async (args, _extra) => {
326
+ const { schemas } = args;
327
+ try {
328
+ await schemaManager.importSchemas(schemas);
329
+ return successResult({
330
+ success: true,
331
+ message: "Schemas imported successfully",
332
+ });
333
+ }
334
+ catch (error) {
335
+ return errorResult(error);
336
+ }
337
+ });
338
+ // ==================== Item Management Tools ====================
339
+ server.tool("baasix_list_items", `Query items from a collection with powerful filtering, sorting, pagination, relations, and aggregation.
340
+
341
+ FILTER OPERATORS (50+):
342
+ - Comparison: eq, neq, gt, gte, lt, lte
343
+ - String: contains, icontains, startswith, endswith, like, ilike, regex
344
+ - Null: isNull (true/false), empty (true/false)
345
+ - List: in, nin, between, nbetween
346
+ - Array: arraycontains, arraycontainsany, arraylength, arrayempty
347
+ - JSONB: jsoncontains, jsonhaskey, jsonhasanykeys, jsonhasallkeys, jsonpath
348
+ - Geospatial: dwithin, intersects, contains, within, overlaps
349
+ - Logical: AND, OR, NOT
350
+
351
+ DYNAMIC VARIABLES:
352
+ - $CURRENT_USER: Current user's ID
353
+ - $NOW: Current timestamp
354
+ - $NOW-DAYS_7: 7 days ago
355
+ - $NOW+MONTHS_1: 1 month from now
356
+
357
+ FILTER EXAMPLES:
358
+ - {"status": {"eq": "active"}}
359
+ - {"AND": [{"price": {"gte": 10}}, {"price": {"lte": 100}}]}
360
+ - {"tags": {"arraycontains": ["featured"]}}`, {
361
+ collection: z.string().describe("Collection name"),
362
+ filter: z.record(z.any()).optional().describe("Filter criteria"),
363
+ fields: z.array(z.string()).optional().describe("Fields to return"),
364
+ sort: z.string().optional().describe("Sort field and direction (e.g., 'createdAt:desc')"),
365
+ page: z.number().optional().default(1).describe("Page number"),
366
+ limit: z.number().optional().default(10).describe("Items per page (-1 for all)"),
367
+ search: z.string().optional().describe("Full-text search query"),
368
+ searchFields: z.array(z.string()).optional().describe("Fields to search in"),
369
+ aggregate: z.record(z.any()).optional().describe("Aggregation functions"),
370
+ groupBy: z.array(z.string()).optional().describe("Fields to group by"),
371
+ relConditions: z.record(z.any()).optional().describe("Filter conditions for related records"),
372
+ }, async (args, extra) => {
373
+ const { collection, filter, fields, sort, page, limit, search, searchFields, aggregate, groupBy, relConditions } = args;
374
+ try {
375
+ const accountability = getAccountability(extra);
376
+ const itemsService = new ItemsService(collection, { accountability });
377
+ const query = { page, limit };
378
+ if (filter)
379
+ query.filter = filter;
380
+ if (fields)
381
+ query.fields = fields;
382
+ if (sort) {
383
+ const [field, direction] = sort.split(":");
384
+ query.sort = [direction?.toLowerCase() === "desc" ? `-${field}` : field];
385
+ }
386
+ if (search)
387
+ query.search = search;
388
+ if (searchFields)
389
+ query.searchFields = searchFields;
390
+ if (aggregate)
391
+ query.aggregate = aggregate;
392
+ if (groupBy)
393
+ query.groupBy = groupBy;
394
+ if (relConditions)
395
+ query.relConditions = relConditions;
396
+ const result = await itemsService.readByQuery(query);
397
+ return successResult({
398
+ data: result.data,
399
+ totalCount: result.totalCount,
400
+ page,
401
+ limit,
402
+ });
403
+ }
404
+ catch (error) {
405
+ return errorResult(error);
406
+ }
407
+ });
408
+ server.tool("baasix_get_item", "Get a specific item by ID from a collection, optionally including related data", {
409
+ collection: z.string().describe("Collection name"),
410
+ id: z.string().describe("Item ID (UUID)"),
411
+ fields: z.array(z.string()).optional().describe("Fields to return"),
412
+ }, async (args, extra) => {
413
+ const { collection, id, fields } = args;
414
+ try {
415
+ const accountability = getAccountability(extra);
416
+ const itemsService = new ItemsService(collection, { accountability });
417
+ const query = {};
418
+ if (fields)
419
+ query.fields = fields;
420
+ const result = await itemsService.readOne(id, query);
421
+ return successResult({ data: result });
422
+ }
423
+ catch (error) {
424
+ return errorResult(error);
425
+ }
426
+ });
427
+ server.tool("baasix_create_item", "Create a new item in a collection", {
428
+ collection: z.string().describe("Collection name"),
429
+ data: z.record(z.any()).describe("Item data"),
430
+ }, async (args, extra) => {
431
+ const { collection, data } = args;
432
+ try {
433
+ const accountability = getAccountability(extra);
434
+ const itemsService = new ItemsService(collection, { accountability });
435
+ const result = await itemsService.createOne(data);
436
+ return successResult({ data: result });
437
+ }
438
+ catch (error) {
439
+ return errorResult(error);
440
+ }
441
+ });
442
+ server.tool("baasix_update_item", "Update an existing item in a collection", {
443
+ collection: z.string().describe("Collection name"),
444
+ id: z.string().describe("Item ID"),
445
+ data: z.record(z.any()).describe("Updated item data"),
446
+ }, async (args, extra) => {
447
+ const { collection, id, data } = args;
448
+ try {
449
+ const accountability = getAccountability(extra);
450
+ const itemsService = new ItemsService(collection, { accountability });
451
+ const result = await itemsService.updateOne(id, data);
452
+ return successResult({ data: result });
453
+ }
454
+ catch (error) {
455
+ return errorResult(error);
456
+ }
457
+ });
458
+ server.tool("baasix_delete_item", "Delete an item from a collection", {
459
+ collection: z.string().describe("Collection name"),
460
+ id: z.string().describe("Item ID"),
461
+ }, async (args, extra) => {
462
+ const { collection, id } = args;
463
+ try {
464
+ const accountability = getAccountability(extra);
465
+ const itemsService = new ItemsService(collection, { accountability });
466
+ await itemsService.deleteOne(id);
467
+ return successResult({ success: true, message: `Item '${id}' deleted` });
468
+ }
469
+ catch (error) {
470
+ return errorResult(error);
471
+ }
472
+ });
473
+ // ==================== File Management Tools ====================
474
+ server.tool("baasix_list_files", "List files with metadata and optional filtering", {
475
+ filter: z.record(z.any()).optional().describe("Filter criteria"),
476
+ page: z.number().optional().default(1).describe("Page number"),
477
+ limit: z.number().optional().default(10).describe("Files per page"),
478
+ }, async (args, extra) => {
479
+ const { filter, page, limit } = args;
480
+ try {
481
+ const accountability = getAccountability(extra);
482
+ const filesService = new ItemsService("baasix_File", { accountability });
483
+ const query = { page, limit };
484
+ if (filter)
485
+ query.filter = filter;
486
+ const result = await filesService.readByQuery(query);
487
+ return successResult({
488
+ data: result.data,
489
+ totalCount: result.totalCount,
490
+ page,
491
+ limit,
492
+ });
493
+ }
494
+ catch (error) {
495
+ return errorResult(error);
496
+ }
497
+ });
498
+ server.tool("baasix_get_file_info", "Get detailed information about a specific file", {
499
+ id: z.string().describe("File ID"),
500
+ }, async (args, extra) => {
501
+ const { id } = args;
502
+ try {
503
+ const accountability = getAccountability(extra);
504
+ const filesService = new ItemsService("baasix_File", { accountability });
505
+ const result = await filesService.readOne(id);
506
+ return successResult({ data: result });
507
+ }
508
+ catch (error) {
509
+ return errorResult(error);
510
+ }
511
+ });
512
+ server.tool("baasix_delete_file", "Delete a file", {
513
+ id: z.string().describe("File ID"),
514
+ }, async (args, extra) => {
515
+ const { id } = args;
516
+ try {
517
+ const accountability = getAccountability(extra);
518
+ const filesService = new ItemsService("baasix_File", { accountability });
519
+ await filesService.deleteOne(id);
520
+ return successResult({ success: true, message: `File '${id}' deleted` });
521
+ }
522
+ catch (error) {
523
+ return errorResult(error);
524
+ }
525
+ });
526
+ // ==================== Permission Tools ====================
527
+ server.tool("baasix_list_roles", "List all available roles", {}, async (_args, extra) => {
528
+ try {
529
+ const accountability = getAccountability(extra);
530
+ const rolesService = new ItemsService("baasix_Role", { accountability });
531
+ const result = await rolesService.readByQuery({ limit: -1 }, true);
532
+ return successResult({ data: result.data });
533
+ }
534
+ catch (error) {
535
+ return errorResult(error);
536
+ }
537
+ });
538
+ server.tool("baasix_list_permissions", "List all permissions with optional filtering", {
539
+ filter: z.record(z.any()).optional().describe("Filter criteria"),
540
+ sort: z.string().optional().describe("Sort field and direction"),
541
+ page: z.number().optional().default(1).describe("Page number"),
542
+ limit: z.number().optional().default(10).describe("Permissions per page"),
543
+ }, async (args, extra) => {
544
+ const { filter, sort, page, limit } = args;
545
+ try {
546
+ const accountability = getAccountability(extra);
547
+ const permService = new ItemsService("baasix_Permission", { accountability });
548
+ const query = { page, limit };
549
+ if (filter)
550
+ query.filter = filter;
551
+ if (sort) {
552
+ const [field, direction] = sort.split(":");
553
+ query.sort = [direction?.toLowerCase() === "desc" ? `-${field}` : field];
554
+ }
555
+ const result = await permService.readByQuery(query, true);
556
+ return successResult({
557
+ data: result.data,
558
+ totalCount: result.totalCount,
559
+ page,
560
+ limit,
561
+ });
562
+ }
563
+ catch (error) {
564
+ return errorResult(error);
565
+ }
566
+ });
567
+ server.tool("baasix_create_permission", `Create a new permission for role-based access control.
568
+
569
+ ACTIONS: create, read, update, delete
570
+
571
+ FIELDS:
572
+ - ["*"] for all fields
573
+ - ["name", "price"] for specific fields
574
+
575
+ CONDITIONS (Row-level security):
576
+ - Uses same filter operators as queries
577
+ - {"published": {"eq": true}} - only published records
578
+ - {"author_Id": {"eq": "$CURRENT_USER"}} - only own records`, {
579
+ role_Id: z.string().describe("Role ID (UUID)"),
580
+ collection: z.string().describe("Collection name"),
581
+ action: z.enum(["create", "read", "update", "delete"]).describe("Permission action"),
582
+ fields: z.array(z.string()).optional().describe("Allowed fields"),
583
+ conditions: z.record(z.any()).optional().describe("Row-level security conditions"),
584
+ defaultValues: z.record(z.any()).optional().describe("Default values for creation"),
585
+ relConditions: z.record(z.any()).optional().describe("Relationship conditions"),
586
+ }, async (args, extra) => {
587
+ const { role_Id, collection, action, fields, conditions, defaultValues, relConditions } = args;
588
+ try {
589
+ const accountability = getAccountability(extra);
590
+ const permService = new ItemsService("baasix_Permission", { accountability });
591
+ const data = { role_Id, collection, action };
592
+ if (fields)
593
+ data.fields = fields;
594
+ if (conditions)
595
+ data.conditions = conditions;
596
+ if (defaultValues)
597
+ data.defaultValues = defaultValues;
598
+ if (relConditions)
599
+ data.relConditions = relConditions;
600
+ const result = await permService.createOne(data);
601
+ // Reload permissions cache
602
+ await permissionService.loadPermissions();
603
+ return successResult({ data: result });
604
+ }
605
+ catch (error) {
606
+ return errorResult(error);
607
+ }
608
+ });
609
+ server.tool("baasix_update_permission", "Update an existing permission", {
610
+ id: z.string().describe("Permission ID"),
611
+ role_Id: z.string().optional().describe("Role ID"),
612
+ collection: z.string().optional().describe("Collection name"),
613
+ action: z.enum(["create", "read", "update", "delete"]).optional().describe("Permission action"),
614
+ fields: z.array(z.string()).optional().describe("Allowed fields"),
615
+ conditions: z.record(z.any()).optional().describe("Permission conditions"),
616
+ defaultValues: z.record(z.any()).optional().describe("Default values"),
617
+ relConditions: z.record(z.any()).optional().describe("Relationship conditions"),
618
+ }, async (args, extra) => {
619
+ const { id, ...updateData } = args;
620
+ try {
621
+ const accountability = getAccountability(extra);
622
+ const permService = new ItemsService("baasix_Permission", { accountability });
623
+ const result = await permService.updateOne(id, updateData);
624
+ // Reload permissions cache
625
+ await permissionService.loadPermissions();
626
+ return successResult({ data: result });
627
+ }
628
+ catch (error) {
629
+ return errorResult(error);
630
+ }
631
+ });
632
+ server.tool("baasix_delete_permission", "Delete a permission", {
633
+ id: z.string().describe("Permission ID"),
634
+ }, async (args, extra) => {
635
+ const { id } = args;
636
+ try {
637
+ const accountability = getAccountability(extra);
638
+ const permService = new ItemsService("baasix_Permission", { accountability });
639
+ await permService.deleteOne(id);
640
+ // Reload permissions cache
641
+ await permissionService.loadPermissions();
642
+ return successResult({ success: true, message: `Permission '${id}' deleted` });
643
+ }
644
+ catch (error) {
645
+ return errorResult(error);
646
+ }
647
+ });
648
+ server.tool("baasix_reload_permissions", "Reload the permission cache", {}, async (_args, _extra) => {
649
+ try {
650
+ await permissionService.loadPermissions();
651
+ return successResult({ success: true, message: "Permissions reloaded" });
652
+ }
653
+ catch (error) {
654
+ return errorResult(error);
655
+ }
656
+ });
657
+ // ==================== Realtime Tools ====================
658
+ server.tool("baasix_realtime_status", "Get the status of the realtime service including WAL configuration", {}, async (_args, _extra) => {
659
+ try {
660
+ const status = await realtimeService.getStatus();
661
+ return successResult(status);
662
+ }
663
+ catch (error) {
664
+ return errorResult(error);
665
+ }
666
+ });
667
+ server.tool("baasix_realtime_config", "Check PostgreSQL replication configuration for WAL-based realtime", {}, async (_args, _extra) => {
668
+ try {
669
+ const config = await realtimeService.getConfig();
670
+ return successResult(config);
671
+ }
672
+ catch (error) {
673
+ return errorResult(error);
674
+ }
675
+ });
676
+ server.tool("baasix_realtime_collections", "Get list of collections with realtime enabled", {}, async (_args, _extra) => {
677
+ try {
678
+ const collections = realtimeService.getRealtimeCollections();
679
+ return successResult({ collections });
680
+ }
681
+ catch (error) {
682
+ return errorResult(error);
683
+ }
684
+ });
685
+ server.tool("baasix_realtime_enable", "Enable realtime for a collection", {
686
+ collection: z.string().describe("Collection name"),
687
+ actions: z.array(z.enum(["insert", "update", "delete"])).optional().describe("Actions to broadcast"),
688
+ replicaIdentityFull: z.boolean().optional().describe("Set REPLICA IDENTITY FULL for old values"),
689
+ }, async (args, _extra) => {
690
+ const { collection, actions, replicaIdentityFull } = args;
691
+ try {
692
+ await realtimeService.enableRealtime(collection, {
693
+ actions: actions || ["insert", "update", "delete"],
694
+ replicaIdentityFull: replicaIdentityFull || false,
695
+ });
696
+ return successResult({
697
+ success: true,
698
+ message: `Realtime enabled for '${collection}'`,
699
+ });
700
+ }
701
+ catch (error) {
702
+ return errorResult(error);
703
+ }
704
+ });
705
+ server.tool("baasix_realtime_disable", "Disable realtime for a collection", {
706
+ collection: z.string().describe("Collection name"),
707
+ }, async (args, _extra) => {
708
+ const { collection } = args;
709
+ try {
710
+ await realtimeService.disableRealtime(collection);
711
+ return successResult({
712
+ success: true,
713
+ message: `Realtime disabled for '${collection}'`,
714
+ });
715
+ }
716
+ catch (error) {
717
+ return errorResult(error);
718
+ }
719
+ });
720
+ // ==================== Settings Tools ====================
721
+ server.tool("baasix_get_settings", "Get application settings", {
722
+ key: z.string().optional().describe("Specific setting key to retrieve"),
723
+ }, async (args, _extra) => {
724
+ const { key } = args;
725
+ try {
726
+ const settings = key ? await settingsService.getSetting(key) : await settingsService.getAllSettings();
727
+ return successResult({ data: settings });
728
+ }
729
+ catch (error) {
730
+ return errorResult(error);
731
+ }
732
+ });
733
+ server.tool("baasix_update_settings", "Update application settings", {
734
+ settings: z.record(z.any()).describe("Settings object to update"),
735
+ }, async (args, _extra) => {
736
+ const { settings } = args;
737
+ try {
738
+ await settingsService.updateSettings(settings);
739
+ return successResult({
740
+ success: true,
741
+ message: "Settings updated",
742
+ });
743
+ }
744
+ catch (error) {
745
+ return errorResult(error);
746
+ }
747
+ });
748
+ // ==================== Email Template Tools ====================
749
+ server.tool("baasix_list_templates", "List all email templates with optional filtering", {
750
+ filter: z.record(z.any()).optional().describe("Filter criteria"),
751
+ page: z.number().optional().default(1).describe("Page number"),
752
+ limit: z.number().optional().default(10).describe("Templates per page"),
753
+ }, async (args, extra) => {
754
+ const { filter, page, limit } = args;
755
+ try {
756
+ const accountability = getAccountability(extra);
757
+ const templateService = new ItemsService("baasix_Template", { accountability });
758
+ const query = { page, limit };
759
+ if (filter)
760
+ query.filter = filter;
761
+ const result = await templateService.readByQuery(query, true);
762
+ return successResult({
763
+ data: result.data,
764
+ totalCount: result.totalCount,
765
+ page,
766
+ limit,
767
+ });
768
+ }
769
+ catch (error) {
770
+ return errorResult(error);
771
+ }
772
+ });
773
+ server.tool("baasix_get_template", "Get a specific email template by ID", {
774
+ id: z.string().describe("Template ID (UUID)"),
775
+ }, async (args, extra) => {
776
+ const { id } = args;
777
+ try {
778
+ const accountability = getAccountability(extra);
779
+ const templateService = new ItemsService("baasix_Template", { accountability });
780
+ const result = await templateService.readOne(id);
781
+ return successResult({ data: result });
782
+ }
783
+ catch (error) {
784
+ return errorResult(error);
785
+ }
786
+ });
787
+ server.tool("baasix_update_template", `Update an email template's subject, description, or body content.
788
+
789
+ TEMPLATE TYPES:
790
+ - magic_link: Magic link authentication emails
791
+ - invite: User invitation emails
792
+ - password_reset: Password reset emails
793
+ - welcome: Welcome emails
794
+ - verification: Email verification emails
795
+
796
+ AVAILABLE VARIABLES:
797
+ - User: {{user.firstName}}, {{user.lastName}}, {{user.fullName}}, {{user.email}}
798
+ - Tenant: {{tenant.name}}, {{tenant.logo}}, {{tenant.website}}
799
+ - Auth: {{magicLink}}, {{magicCode}}, {{resetPasswordLink}}, {{inviteLink}}`, {
800
+ id: z.string().describe("Template ID (UUID)"),
801
+ subject: z.string().optional().describe("Email subject line"),
802
+ description: z.string().optional().describe("Template description"),
803
+ body: z.string().optional().describe("Template body as HTML"),
804
+ isActive: z.boolean().optional().describe("Whether the template is active"),
805
+ }, async (args, extra) => {
806
+ const { id, ...updateData } = args;
807
+ try {
808
+ const accountability = getAccountability(extra);
809
+ const templateService = new ItemsService("baasix_Template", { accountability });
810
+ const result = await templateService.updateOne(id, updateData);
811
+ return successResult({ data: result });
812
+ }
813
+ catch (error) {
814
+ return errorResult(error);
815
+ }
816
+ });
817
+ // ==================== Notification Tools ====================
818
+ server.tool("baasix_list_notifications", "List notifications for the authenticated user", {
819
+ page: z.number().optional().default(1).describe("Page number"),
820
+ limit: z.number().optional().default(10).describe("Notifications per page"),
821
+ seen: z.boolean().optional().describe("Filter by seen status"),
822
+ }, async (args, extra) => {
823
+ const { page, limit, seen } = args;
824
+ try {
825
+ const accountability = getAccountability(extra);
826
+ const notifService = new ItemsService("baasix_Notification", { accountability });
827
+ const query = { page, limit };
828
+ if (seen !== undefined) {
829
+ query.filter = { seen: { eq: seen } };
830
+ }
831
+ const result = await notifService.readByQuery(query);
832
+ return successResult({
833
+ data: result.data,
834
+ totalCount: result.totalCount,
835
+ page,
836
+ limit,
837
+ });
838
+ }
839
+ catch (error) {
840
+ return errorResult(error);
841
+ }
842
+ });
843
+ server.tool("baasix_mark_notification_seen", "Mark a notification as seen", {
844
+ id: z.string().describe("Notification ID"),
845
+ }, async (args, extra) => {
846
+ const { id } = args;
847
+ try {
848
+ const accountability = getAccountability(extra);
849
+ const notifService = new ItemsService("baasix_Notification", { accountability });
850
+ const result = await notifService.updateOne(id, { seen: true });
851
+ return successResult({ data: result });
852
+ }
853
+ catch (error) {
854
+ return errorResult(error);
855
+ }
856
+ });
857
+ // ==================== Utility Tools ====================
858
+ server.tool("baasix_server_info", "Get Baasix server information and health status", {}, async (_args, _extra) => {
859
+ try {
860
+ const info = {
861
+ name: "baasix",
862
+ version: env.get("npm_package_version") || "0.1.0",
863
+ environment: env.get("NODE_ENV") || "development",
864
+ timestamp: new Date().toISOString(),
865
+ uptime: process.uptime(),
866
+ memory: process.memoryUsage(),
867
+ nodejs: process.version,
868
+ mcp: {
869
+ enabled: true,
870
+ version: "0.1.0",
871
+ },
872
+ };
873
+ return successResult(info);
874
+ }
875
+ catch (error) {
876
+ return errorResult(error);
877
+ }
878
+ });
879
+ server.tool("baasix_sort_items", "Sort items within a collection (move item before/after another)", {
880
+ collection: z.string().describe("Collection name"),
881
+ item: z.string().describe("ID of item to move"),
882
+ to: z.string().describe("ID of target item to move before"),
883
+ }, async (args, extra) => {
884
+ const { collection, item, to } = args;
885
+ try {
886
+ const accountability = getAccountability(extra);
887
+ const itemsService = new ItemsService(collection, { accountability });
888
+ // Get schema to check if sortEnabled
889
+ const schemaDef = schemaManager.getSchemaDefinition(collection);
890
+ if (!schemaDef?.sortEnabled) {
891
+ throw new Error(`Collection '${collection}' does not have sorting enabled`);
892
+ }
893
+ // Get both items to swap sort values
894
+ const [sourceItem, targetItem] = await Promise.all([itemsService.readOne(item), itemsService.readOne(to)]);
895
+ // Swap sort values
896
+ await Promise.all([
897
+ itemsService.updateOne(item, { sort: targetItem.sort }),
898
+ itemsService.updateOne(to, { sort: sourceItem.sort }),
899
+ ]);
900
+ return successResult({
901
+ success: true,
902
+ message: `Item '${item}' moved before '${to}'`,
903
+ });
904
+ }
905
+ catch (error) {
906
+ return errorResult(error);
907
+ }
908
+ });
909
+ server.tool("baasix_generate_report", "Generate reports with grouping and aggregation for a collection", {
910
+ collection: z.string().describe("Collection name"),
911
+ groupBy: z.string().optional().describe("Field to group by"),
912
+ filter: z.record(z.any()).optional().describe("Filter criteria"),
913
+ dateRange: z
914
+ .object({
915
+ start: z.string().optional(),
916
+ end: z.string().optional(),
917
+ })
918
+ .optional()
919
+ .describe("Date range filter"),
920
+ }, async (args, extra) => {
921
+ const { collection, groupBy, filter, dateRange } = args;
922
+ try {
923
+ const accountability = getAccountability(extra);
924
+ const itemsService = new ItemsService(collection, { accountability });
925
+ const query = { limit: -1 };
926
+ // Build filter with date range
927
+ const filters = [];
928
+ if (filter)
929
+ filters.push(filter);
930
+ if (dateRange) {
931
+ if (dateRange.start) {
932
+ filters.push({ createdAt: { gte: dateRange.start } });
933
+ }
934
+ if (dateRange.end) {
935
+ filters.push({ createdAt: { lte: dateRange.end } });
936
+ }
937
+ }
938
+ if (filters.length > 0) {
939
+ query.filter = filters.length === 1 ? filters[0] : { AND: filters };
940
+ }
941
+ // Add aggregation if groupBy is specified
942
+ if (groupBy) {
943
+ query.groupBy = [groupBy];
944
+ query.aggregate = {
945
+ count: { function: "count", field: "*" },
946
+ };
947
+ }
948
+ const result = await itemsService.readByQuery(query);
949
+ return successResult({
950
+ data: result.data,
951
+ totalCount: result.totalCount,
952
+ groupBy,
953
+ dateRange,
954
+ });
955
+ }
956
+ catch (error) {
957
+ return errorResult(error);
958
+ }
959
+ });
960
+ // ==================== Collection Stats Tool ====================
961
+ server.tool("baasix_collection_stats", "Get collection statistics and analytics", {
962
+ collections: z.array(z.string()).optional().describe("Specific collections to get stats for"),
963
+ timeframe: z.string().optional().describe('Timeframe for stats (e.g., "24h", "7d", "30d")'),
964
+ }, async (args, extra) => {
965
+ const { collections, timeframe } = args;
966
+ try {
967
+ const accountability = getAccountability(extra);
968
+ // Get all schemas if no specific collections provided
969
+ const allSchemas = schemaManager.getSchemas();
970
+ const targetCollections = collections || Object.keys(allSchemas).filter((name) => !name.startsWith("baasix_"));
971
+ const stats = {};
972
+ for (const collection of targetCollections) {
973
+ try {
974
+ const itemsService = new ItemsService(collection, { accountability });
975
+ // Get total count
976
+ const countResult = await itemsService.readByQuery({
977
+ aggregate: { total: { function: "count", field: "*" } },
978
+ limit: 1,
979
+ });
980
+ // Get recent count if timeframe specified
981
+ let recentCount = null;
982
+ if (timeframe) {
983
+ const now = new Date();
984
+ const startDate = new Date();
985
+ if (timeframe === "24h")
986
+ startDate.setHours(now.getHours() - 24);
987
+ else if (timeframe === "7d")
988
+ startDate.setDate(now.getDate() - 7);
989
+ else if (timeframe === "30d")
990
+ startDate.setDate(now.getDate() - 30);
991
+ const recentResult = await itemsService.readByQuery({
992
+ filter: { createdAt: { gte: startDate.toISOString() } },
993
+ aggregate: { recent: { function: "count", field: "*" } },
994
+ limit: 1,
995
+ });
996
+ recentCount = recentResult.data?.[0]?.recent || 0;
997
+ }
998
+ stats[collection] = {
999
+ totalCount: countResult.data?.[0]?.total || 0,
1000
+ ...(recentCount !== null && { recentCount, timeframe }),
1001
+ };
1002
+ }
1003
+ catch {
1004
+ stats[collection] = { error: "Could not fetch stats" };
1005
+ }
1006
+ }
1007
+ return successResult(stats);
1008
+ }
1009
+ catch (error) {
1010
+ return errorResult(error);
1011
+ }
1012
+ });
1013
+ // ==================== Send Notification Tool ====================
1014
+ server.tool("baasix_send_notification", "Send a notification to specified users", {
1015
+ recipients: z.array(z.string()).describe("Array of user IDs to send notification to"),
1016
+ title: z.string().describe("Notification title"),
1017
+ message: z.string().describe("Notification message"),
1018
+ type: z.string().optional().default("info").describe("Notification type"),
1019
+ }, async (args, extra) => {
1020
+ const { recipients, title, message, type } = args;
1021
+ try {
1022
+ const accountability = getAccountability(extra);
1023
+ const itemsService = new ItemsService("baasix_Notification", { accountability });
1024
+ const notifications = await Promise.all(recipients.map((userId) => itemsService.createOne({
1025
+ user_Id: userId,
1026
+ title,
1027
+ message,
1028
+ type: type || "info",
1029
+ seen: false,
1030
+ })));
1031
+ return successResult({
1032
+ success: true,
1033
+ sent: notifications.length,
1034
+ notifications,
1035
+ });
1036
+ }
1037
+ catch (error) {
1038
+ return errorResult(error);
1039
+ }
1040
+ });
1041
+ // ==================== Get Permission Tool ====================
1042
+ server.tool("baasix_get_permission", "Get a specific permission by ID", {
1043
+ id: z.string().describe("Permission ID"),
1044
+ }, async (args, extra) => {
1045
+ const { id } = args;
1046
+ try {
1047
+ const accountability = getAccountability(extra);
1048
+ const itemsService = new ItemsService("baasix_Permission", { accountability });
1049
+ const permission = await itemsService.readOne(id);
1050
+ return successResult(permission);
1051
+ }
1052
+ catch (error) {
1053
+ return errorResult(error);
1054
+ }
1055
+ });
1056
+ // ==================== Get Permissions for Role Tool ====================
1057
+ server.tool("baasix_get_permissions", "Get permissions for a specific role", {
1058
+ role: z.string().describe("Role name or ID"),
1059
+ }, async (args, extra) => {
1060
+ const { role } = args;
1061
+ try {
1062
+ const accountability = getAccountability(extra);
1063
+ // First try to find role by name
1064
+ const rolesService = new ItemsService("baasix_Role", { accountability });
1065
+ const rolesResult = await rolesService.readByQuery({
1066
+ filter: { OR: [{ name: { eq: role } }, { id: { eq: role } }] },
1067
+ limit: 1,
1068
+ });
1069
+ if (!rolesResult.data?.length) {
1070
+ return errorResult(`Role '${role}' not found`);
1071
+ }
1072
+ const roleId = rolesResult.data[0].id;
1073
+ // Get permissions for this role
1074
+ const permissionsService = new ItemsService("baasix_Permission", { accountability });
1075
+ const permissions = await permissionsService.readByQuery({
1076
+ filter: { role_Id: { eq: roleId } },
1077
+ limit: -1,
1078
+ });
1079
+ return successResult({
1080
+ role: rolesResult.data[0],
1081
+ permissions: permissions.data,
1082
+ totalCount: permissions.totalCount,
1083
+ });
1084
+ }
1085
+ catch (error) {
1086
+ return errorResult(error);
1087
+ }
1088
+ });
1089
+ // ==================== Update Permissions for Role Tool ====================
1090
+ server.tool("baasix_update_permissions", "Update permissions for a role (bulk update)", {
1091
+ role: z.string().describe("Role name or ID"),
1092
+ permissions: z
1093
+ .array(z.object({
1094
+ collection: z.string(),
1095
+ action: z.enum(["create", "read", "update", "delete"]),
1096
+ fields: z.array(z.string()).optional(),
1097
+ conditions: z.record(z.any()).optional(),
1098
+ }))
1099
+ .describe("Array of permission objects to set for the role"),
1100
+ }, async (args, extra) => {
1101
+ const { role, permissions } = args;
1102
+ try {
1103
+ const accountability = getAccountability(extra);
1104
+ // Find role
1105
+ const rolesService = new ItemsService("baasix_Role", { accountability });
1106
+ const rolesResult = await rolesService.readByQuery({
1107
+ filter: { OR: [{ name: { eq: role } }, { id: { eq: role } }] },
1108
+ limit: 1,
1109
+ });
1110
+ if (!rolesResult.data?.length) {
1111
+ return errorResult(`Role '${role}' not found`);
1112
+ }
1113
+ const roleData = rolesResult.data[0];
1114
+ const roleId = roleData.id;
1115
+ const permissionsService = new ItemsService("baasix_Permission", { accountability });
1116
+ // Create/update permissions
1117
+ const results = await Promise.all(permissions.map(async (perm) => {
1118
+ // Check if permission exists
1119
+ const existing = await permissionsService.readByQuery({
1120
+ filter: {
1121
+ AND: [{ role_Id: { eq: roleId } }, { collection: { eq: perm.collection } }, { action: { eq: perm.action } }],
1122
+ },
1123
+ limit: 1,
1124
+ });
1125
+ if (existing.data?.length) {
1126
+ // Update existing
1127
+ return permissionsService.updateOne(existing.data[0].id, {
1128
+ fields: perm.fields || ["*"],
1129
+ conditions: perm.conditions || {},
1130
+ });
1131
+ }
1132
+ else {
1133
+ // Create new
1134
+ return permissionsService.createOne({
1135
+ role_Id: roleId,
1136
+ collection: perm.collection,
1137
+ action: perm.action,
1138
+ fields: perm.fields || ["*"],
1139
+ conditions: perm.conditions || {},
1140
+ });
1141
+ }
1142
+ }));
1143
+ // Reload permissions cache
1144
+ await permissionService.loadPermissions();
1145
+ return successResult({
1146
+ success: true,
1147
+ role: roleData.name,
1148
+ updated: results.length,
1149
+ });
1150
+ }
1151
+ catch (error) {
1152
+ return errorResult(error);
1153
+ }
1154
+ });
1155
+ // ==================== Auth Tools ====================
1156
+ server.tool("baasix_get_current_user", "Get current user information with role and permissions", {
1157
+ fields: z.array(z.string()).optional().describe("Specific fields to retrieve"),
1158
+ }, async (args, extra) => {
1159
+ const { fields } = args;
1160
+ try {
1161
+ const accountability = getAccountability(extra);
1162
+ if (!accountability.user) {
1163
+ return successResult({
1164
+ authenticated: false,
1165
+ role: accountability.role,
1166
+ admin: accountability.admin,
1167
+ });
1168
+ }
1169
+ const usersService = new ItemsService("baasix_User", { accountability });
1170
+ const user = await usersService.readOne(accountability.user, {
1171
+ fields: fields || ["*", "role.*"],
1172
+ });
1173
+ return successResult({
1174
+ authenticated: true,
1175
+ user,
1176
+ role: accountability.role,
1177
+ admin: accountability.admin,
1178
+ });
1179
+ }
1180
+ catch (error) {
1181
+ return errorResult(error);
1182
+ }
1183
+ });
1184
+ server.tool("baasix_register_user", "Register a new user", {
1185
+ email: z.string().email().describe("User email address"),
1186
+ password: z.string().describe("User password"),
1187
+ firstName: z.string().optional().describe("User first name"),
1188
+ lastName: z.string().optional().describe("User last name"),
1189
+ roleName: z.string().optional().describe("Role name to assign"),
1190
+ }, async (args, _extra) => {
1191
+ const { email, password, firstName, lastName, roleName } = args;
1192
+ try {
1193
+ const { default: axios } = await import("axios");
1194
+ const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
1195
+ const response = await axios.post(`${baseUrl}/auth/register`, {
1196
+ email,
1197
+ password,
1198
+ firstName,
1199
+ lastName,
1200
+ roleName,
1201
+ });
1202
+ return successResult(response.data);
1203
+ }
1204
+ catch (error) {
1205
+ const axiosError = error;
1206
+ return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1207
+ }
1208
+ });
1209
+ server.tool("baasix_send_invite", "Send an invitation to a user", {
1210
+ email: z.string().email().describe("Email address to invite"),
1211
+ role_Id: z.string().describe("Role ID to assign"),
1212
+ tenant_Id: z.string().optional().describe("Tenant ID"),
1213
+ link: z.string().url().describe("Application URL for the invitation link"),
1214
+ }, async (args, _extra) => {
1215
+ const { email, role_Id, tenant_Id, link } = args;
1216
+ try {
1217
+ const { default: axios } = await import("axios");
1218
+ const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
1219
+ const response = await axios.post(`${baseUrl}/auth/invite`, {
1220
+ email,
1221
+ role_Id,
1222
+ tenant_Id,
1223
+ link,
1224
+ });
1225
+ return successResult(response.data);
1226
+ }
1227
+ catch (error) {
1228
+ const axiosError = error;
1229
+ return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1230
+ }
1231
+ });
1232
+ server.tool("baasix_verify_invite", "Verify an invitation token", {
1233
+ token: z.string().describe("Invitation token"),
1234
+ link: z.string().url().optional().describe("Application URL to validate"),
1235
+ }, async (args, _extra) => {
1236
+ const { token, link } = args;
1237
+ try {
1238
+ const { default: axios } = await import("axios");
1239
+ const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
1240
+ const params = new URLSearchParams();
1241
+ if (link)
1242
+ params.append("link", link);
1243
+ const response = await axios.get(`${baseUrl}/auth/verify-invite/${token}?${params}`);
1244
+ return successResult(response.data);
1245
+ }
1246
+ catch (error) {
1247
+ const axiosError = error;
1248
+ return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1249
+ }
1250
+ });
1251
+ server.tool("baasix_send_magic_link", "Send magic link or code for authentication", {
1252
+ email: z.string().email().describe("User email address"),
1253
+ link: z.string().url().optional().describe("Application URL for magic link"),
1254
+ mode: z.enum(["link", "code"]).optional().default("link").describe("Magic authentication mode"),
1255
+ }, async (args, _extra) => {
1256
+ const { email, link, mode } = args;
1257
+ try {
1258
+ const { default: axios } = await import("axios");
1259
+ const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
1260
+ const response = await axios.post(`${baseUrl}/auth/magiclink`, {
1261
+ email,
1262
+ link,
1263
+ mode,
1264
+ });
1265
+ return successResult(response.data);
1266
+ }
1267
+ catch (error) {
1268
+ const axiosError = error;
1269
+ return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1270
+ }
1271
+ });
1272
+ server.tool("baasix_get_user_tenants", "Get available tenants for the current user", {}, async (_args, _extra) => {
1273
+ try {
1274
+ const { default: axios } = await import("axios");
1275
+ const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
1276
+ const response = await axios.get(`${baseUrl}/auth/tenants`);
1277
+ return successResult(response.data);
1278
+ }
1279
+ catch (error) {
1280
+ const axiosError = error;
1281
+ return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1282
+ }
1283
+ });
1284
+ server.tool("baasix_switch_tenant", "Switch to a different tenant context", {
1285
+ tenant_Id: z.string().describe("Tenant ID to switch to"),
1286
+ }, async (args, _extra) => {
1287
+ const { tenant_Id } = args;
1288
+ try {
1289
+ const { default: axios } = await import("axios");
1290
+ const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
1291
+ const response = await axios.post(`${baseUrl}/auth/switch-tenant`, {
1292
+ tenant_Id,
1293
+ });
1294
+ return successResult(response.data);
1295
+ }
1296
+ catch (error) {
1297
+ const axiosError = error;
1298
+ return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1299
+ }
1300
+ });
1301
+ // ==================== Auth Status & Session Tools ====================
1302
+ server.tool("baasix_auth_status", "Check authentication status and session validity", {}, async (_args, extra) => {
1303
+ try {
1304
+ const accountability = getAccountability(extra);
1305
+ return successResult({
1306
+ authenticated: !!accountability.user,
1307
+ userId: accountability.user,
1308
+ role: accountability.role,
1309
+ admin: accountability.admin,
1310
+ sessionId: extra.sessionId || null,
1311
+ });
1312
+ }
1313
+ catch (error) {
1314
+ return errorResult(error);
1315
+ }
1316
+ });
1317
+ server.tool("baasix_login", "Login with email and password", {
1318
+ email: z.string().email().describe("User email address"),
1319
+ password: z.string().describe("User password"),
1320
+ }, async (args, _extra) => {
1321
+ const { email, password } = args;
1322
+ try {
1323
+ const { default: axios } = await import("axios");
1324
+ const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
1325
+ const response = await axios.post(`${baseUrl}/auth/login`, {
1326
+ email,
1327
+ password,
1328
+ });
1329
+ return successResult(response.data);
1330
+ }
1331
+ catch (error) {
1332
+ const axiosError = error;
1333
+ return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1334
+ }
1335
+ });
1336
+ server.tool("baasix_logout", "Logout and invalidate current session", {}, async (_args, extra) => {
1337
+ try {
1338
+ const { default: axios } = await import("axios");
1339
+ const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
1340
+ await axios.post(`${baseUrl}/auth/logout`);
1341
+ // Remove MCP session if exists
1342
+ if (extra.sessionId) {
1343
+ removeMCPSession(extra.sessionId);
1344
+ }
1345
+ return successResult({ success: true, message: "Logged out successfully" });
1346
+ }
1347
+ catch (error) {
1348
+ const axiosError = error;
1349
+ return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1350
+ }
1351
+ });
1352
+ server.tool("baasix_refresh_auth", "Refresh authentication token", {
1353
+ refreshToken: z.string().optional().describe("Refresh token (if not using cookies)"),
1354
+ }, async (args, _extra) => {
1355
+ const { refreshToken } = args;
1356
+ try {
1357
+ const { default: axios } = await import("axios");
1358
+ const baseUrl = `http://localhost:${env.get("PORT") || "8056"}`;
1359
+ const response = await axios.post(`${baseUrl}/auth/refresh`, {
1360
+ refreshToken,
1361
+ });
1362
+ return successResult(response.data);
1363
+ }
1364
+ catch (error) {
1365
+ const axiosError = error;
1366
+ return errorResult(axiosError.response?.data?.message || axiosError.message || "Unknown error");
1367
+ }
1368
+ });
1369
+ // ==================== Update Relationship Tool ====================
1370
+ server.tool("baasix_update_relationship", "Update an existing relationship between collections", {
1371
+ sourceCollection: z.string().describe("Source collection name"),
1372
+ relationshipName: z.string().describe("Relationship field name to update"),
1373
+ relationshipData: z
1374
+ .object({
1375
+ alias: z.string().optional().describe("Alias for reverse relationship"),
1376
+ onDelete: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Delete behavior"),
1377
+ onUpdate: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Update behavior"),
1378
+ description: z.string().optional().describe("Relationship description"),
1379
+ })
1380
+ .describe("Updated relationship configuration"),
1381
+ }, async (args, _extra) => {
1382
+ const { sourceCollection, relationshipName, relationshipData } = args;
1383
+ try {
1384
+ const schema = schemaManager.getSchema(sourceCollection);
1385
+ if (!schema) {
1386
+ return errorResult(`Collection '${sourceCollection}' not found`);
1387
+ }
1388
+ const schemaWithRelationships = schema;
1389
+ const relationship = schemaWithRelationships.relationships?.find((r) => r.name === relationshipName);
1390
+ if (!relationship) {
1391
+ return errorResult(`Relationship '${relationshipName}' not found in collection '${sourceCollection}'`);
1392
+ }
1393
+ // Update the relationship
1394
+ const updatedRelationship = { ...relationship, ...relationshipData };
1395
+ const relationships = schemaWithRelationships.relationships?.map((r) => r.name === relationshipName ? updatedRelationship : r) || [];
1396
+ await schemaManager.updateSchema(sourceCollection, {
1397
+ ...schema,
1398
+ relationships,
1399
+ });
1400
+ return successResult({
1401
+ success: true,
1402
+ collection: sourceCollection,
1403
+ relationship: updatedRelationship,
1404
+ });
1405
+ }
1406
+ catch (error) {
1407
+ return errorResult(error);
1408
+ }
1409
+ });
1410
+ return server;
1411
+ }
1412
+ export default { createMCPServer, setMCPSession, removeMCPSession };
1413
+ //# sourceMappingURL=MCPService.js.map