@baasix/baasix 0.1.69 → 0.1.70

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.
@@ -16,6 +16,7 @@
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
17
  import { z } from "zod";
18
18
  import env from "../utils/env.js";
19
+ const SUPPORTED_SCHEMA_FIELD_TYPES_DESCRIPTION = "Supported field types: String, Text, TEXT, CiText, HTML, Integer, BigInt, Decimal, Float, Real, Double, DOUBLE, Boolean, Date, DateTime, DateTime_NO_TZ, Time, Time_NO_TZ, UUID, SUID, TOKEN, JSON, JSONB, Enum, ENUM, VIRTUAL, Array_Integer, Array_String, Array_Double, Array_Decimal, Array_DateTime, Array_DateTime_NO_TZ, Array_Date, Array_Time, Array_Time_NO_TZ, Array_UUID, Array_Boolean, Range_Integer, Range_Double, Range_Decimal, Range_Date, Range_DateTime, Range_DateTime_NO_TZ, Range_Time, Range_Time_NO_TZ, Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, Geography.";
19
20
  // ==================== Helper Functions ====================
20
21
  // Session storage for authenticated MCP clients
21
22
  const mcpSessions = new Map();
@@ -94,6 +95,9 @@ const TOOL_ACTION_MAP = {
94
95
  baasix_add_index: "create",
95
96
  baasix_create_relationship: "create",
96
97
  baasix_update_schema: "update",
98
+ baasix_add_schema_field: "create",
99
+ baasix_update_schema_field: "update",
100
+ baasix_delete_schema_field: "delete",
97
101
  baasix_update_relationship: "update",
98
102
  baasix_delete_schema: "delete",
99
103
  baasix_remove_index: "delete",
@@ -139,6 +143,7 @@ const TOOL_ACTION_MAP = {
139
143
  baasix_list_permissions: "read",
140
144
  baasix_get_permission: "read",
141
145
  baasix_get_permissions: "read",
146
+ baasix_get_role_collection_permissions: "read",
142
147
  baasix_create_permission: "create",
143
148
  baasix_update_permission: "update",
144
149
  baasix_delete_permission: "delete",
@@ -190,8 +195,10 @@ KEY CONCEPTS:
190
195
 
191
196
  COMMON TASK MAPPING:
192
197
  - "Create a table" or "create a collection" → use baasix_create_schema (this creates both the schema definition AND the database table)
193
- - "Add a column/field" → use baasix_update_schema (⚠️ MUST first call baasix_get_schema to get ALL existing fields, then send complete schema with additions)
194
- - "Remove a column/field" → use baasix_update_schema (retrieve full schema, remove the field, send complete schema without it)
198
+ - "Add a single column/field" → use baasix_add_schema_field
199
+ - "Update a single column/field" → use baasix_update_schema_field
200
+ - "Remove a single column/field" → use baasix_delete_schema_field
201
+ - "Bulk schema edits across many fields" → use baasix_update_schema (⚠️ full replacement)
195
202
  - "Insert/add data" or "create a record" → use baasix_create_item
196
203
  - "Query/list/fetch data" or "get rows" → use baasix_list_items
197
204
  - "Sum", "count", "average", "total", "report", "stats", "analytics", "dashboard", "min/max" → use baasix_generate_report (NOT baasix_list_items)
@@ -214,7 +221,15 @@ WORKFLOW FOR CREATING A NEW TABLE:
214
221
  3. baasix_add_index — (optional) add indexes for performance
215
222
  4. baasix_create_permission — (optional) set role-based access
216
223
 
217
- SCHEMA FIELD TYPES: String (VARCHAR), Text (unlimited), Integer, BigInt, Decimal (precision/scale), Float, Real, Double, Boolean, Date, DateTime, Time, UUID, SUID (short ID), JSONB, Array, Enum
224
+ SCHEMA FIELD TYPES:
225
+ - String/Text: String, Text, TEXT, CiText, HTML
226
+ - Numbers: Integer, BigInt, Decimal, Float, Real, Double, DOUBLE
227
+ - Date/time: Date, DateTime, DateTime_NO_TZ, Time, Time_NO_TZ
228
+ - IDs/tokens: UUID, SUID, TOKEN
229
+ - JSON/enums/virtual: JSON, JSONB, Enum, ENUM, VIRTUAL
230
+ - Arrays: Array_Integer, Array_String, Array_Double, Array_Decimal, Array_DateTime, Array_DateTime_NO_TZ, Array_Date, Array_Time, Array_Time_NO_TZ, Array_UUID, Array_Boolean
231
+ - Ranges: Range_Integer, Range_Double, Range_Decimal, Range_Date, Range_DateTime, Range_DateTime_NO_TZ, Range_Time, Range_Time_NO_TZ
232
+ - PostGIS: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, Geography
218
233
 
219
234
  DEFAULT VALUE TYPES: { type: "UUIDV4" }, { type: "SUID" }, { type: "NOW" }, { type: "AUTOINCREMENT" }, { type: "SQL", value: "..." }
220
235
 
@@ -318,18 +333,20 @@ The update_schema tool performs a full replacement, so you need the complete cur
318
333
  This creates both the schema definition AND the actual PostgreSQL table with all specified columns.
319
334
 
320
335
  FIELD TYPES:
321
- - String: VARCHAR — requires values.length (e.g., { "type": "String", "values": { "length": 255 } })
322
- - Text: Unlimited length text
323
- - Integer, BigInt: Whole numbers
324
- - Decimal: requires values.precision & values.scale (e.g., { "type": "Decimal", "values": { "precision": 10, "scale": 2 } })
325
- - Float, Real, Double: Floating point numbers
326
- - Boolean: true/false
327
- - Date, DateTime, Time: Date/time values
328
- - UUID: Use with defaultValue { "type": "UUIDV4" } for auto-generated IDs
329
- - SUID: Short unique ID with defaultValue { "type": "SUID" }
330
- - JSONB: JSON data with indexing support
331
- - Array: Specify element type via values.type (e.g., { "type": "Array", "values": { "type": "String" } })
332
- - Enum: Specify allowed values via values.values (e.g., { "type": "Enum", "values": { "values": ["active", "inactive"] } })
336
+ - String/Text: String, Text, TEXT, CiText, HTML
337
+ - Numbers: Integer, BigInt, Decimal, Float, Real, Double, DOUBLE
338
+ - Date/time: Date, DateTime, DateTime_NO_TZ, Time, Time_NO_TZ
339
+ - IDs/tokens: UUID, SUID, TOKEN
340
+ - JSON/enums/virtual: JSON, JSONB, Enum, ENUM, VIRTUAL
341
+ - Arrays: Array_Integer, Array_String, Array_Double, Array_Decimal, Array_DateTime, Array_DateTime_NO_TZ, Array_Date, Array_Time, Array_Time_NO_TZ, Array_UUID, Array_Boolean
342
+ - Ranges: Range_Integer, Range_Double, Range_Decimal, Range_Date, Range_DateTime, Range_DateTime_NO_TZ, Range_Time, Range_Time_NO_TZ
343
+ - PostGIS: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, Geography
344
+
345
+ COMMON TYPE-SPECIFIC RULES:
346
+ - String: requires values.length (e.g., { "type": "String", "values": { "length": 255 } })
347
+ - Decimal: requires values.precision and values.scale
348
+ - Enum/ENUM: requires values.values array
349
+ - Array_* / Range_* / PostGIS types use explicit type names (recommended over generic aliases)
333
350
 
334
351
  FIELD OPTIONS: allowNull (boolean), unique (boolean), primaryKey (boolean), defaultValue (value or { type: "UUIDV4"|"SUID"|"NOW"|"AUTOINCREMENT" })
335
352
 
@@ -338,13 +355,31 @@ ALWAYS include an "id" field as primary key. Add "timestamps": true in the schem
338
355
  EXAMPLE — Create a "products" table:
339
356
  collection: "products"
340
357
  schema: { "timestamps": true, "fields": { "id": { "type": "UUID", "primaryKey": true, "defaultValue": { "type": "UUIDV4" } }, "name": { "type": "String", "allowNull": false, "values": { "length": 255 } }, "price": { "type": "Decimal", "values": { "precision": 10, "scale": 2 }, "defaultValue": 0 }, "inStock": { "type": "Boolean", "defaultValue": true } } }`, {
341
- collection: z.string().describe("Collection name"),
358
+ collection: z.string().describe("Name of the new collection/table. Must be a valid PostgreSQL identifier (letters, numbers, underscores; cannot start with a number). Max 63 chars."),
342
359
  schema: z
343
360
  .object({
344
- fields: z.record(z.any()).describe("Field definitions"),
361
+ timestamps: z.boolean().optional().describe("Set true to auto-add createdAt and updatedAt DateTime columns managed by Baasix. Recommended for most tables."),
362
+ paranoid: z.boolean().optional().describe("Set true to enable soft deletes: adds a deletedAt column. Deleted records are hidden from queries but not physically removed."),
363
+ sortEnabled: z.boolean().optional().describe("Set true to add a 'sort' Integer column for manual ordering of records."),
364
+ usertrack: z.boolean().optional().describe("Set true to track which user created/updated each record. Adds userCreated and userUpdated M2O fields to baasix_User."),
365
+ fields: z.record(z.object({
366
+ type: z.string().describe(`${SUPPORTED_SCHEMA_FIELD_TYPES_DESCRIPTION} Type-specific requirements: String needs values.length, Decimal needs values.precision+scale, enum-like types need values.values.`),
367
+ allowNull: z.boolean().optional().describe("Allow NULL values. Default: true. Set false to add a NOT NULL constraint."),
368
+ unique: z.boolean().optional().describe("Enforce a UNIQUE constraint on this column."),
369
+ primaryKey: z.boolean().optional().describe("Mark as the primary key column."),
370
+ defaultValue: z.union([z.string(), z.number(), z.boolean(), z.null(), z.object({ type: z.string(), value: z.string().optional() })]).optional().describe("Default value: a literal (e.g. 0, 'active', true) or { type: 'UUIDV4' | 'SUID' | 'NOW' | 'AUTOINCREMENT' | 'SQL', value?: 'expr' }."),
371
+ values: z.object({
372
+ length: z.number().optional().describe("Max character length — required for String/VARCHAR (e.g. 255)"),
373
+ precision: z.number().optional().describe("Total digits — required for Decimal (e.g. 10)"),
374
+ scale: z.number().optional().describe("Decimal places — required for Decimal (e.g. 2)"),
375
+ type: z.string().optional().describe("Element type — required for Array (e.g. 'String', 'Integer')"),
376
+ values: z.array(z.string()).optional().describe("Allowed values — required for Enum (e.g. ['active','inactive'])"),
377
+ }).optional().describe("Type-specific config. String→{length}, Decimal→{precision,scale}, Array→{type}, Enum→{values:[...]}"),
378
+ description: z.string().optional().describe("Human-readable description stored in schema metadata only."),
379
+ }).passthrough()).describe("Field definitions keyed by column name. ALWAYS include an 'id' primary key field (type UUID with defaultValue {type:'UUIDV4'})."),
345
380
  })
346
381
  .passthrough()
347
- .describe("Schema definition"),
382
+ .describe("Schema definition for the new table"),
348
383
  }, async (args, extra) => {
349
384
  const { collection, schema } = args;
350
385
  const res = await callRoute('POST', '/schemas', extra, { collectionName: collection, schema });
@@ -359,12 +394,17 @@ schema: { "timestamps": true, "fields": { "id": { "type": "UUID", "primaryKey":
359
394
  registerTool("baasix_update_schema", `Modify an existing database table — add new columns, change column types, or remove columns.
360
395
  Use this when asked to 'add a field', 'add a column', or 'alter a table'.
361
396
 
397
+ ✅ PREFERRED for single-column changes:
398
+ - Add one column: use baasix_add_schema_field
399
+ - Update one column: use baasix_update_schema_field
400
+ - Delete one column: use baasix_delete_schema_field
401
+
362
402
  ⚠️ CRITICAL: This performs a FULL REPLACEMENT of the schema definition, NOT a merge.
363
403
  You MUST first call baasix_get_schema to retrieve the current schema, then include ALL existing fields
364
404
  and add/modify/remove only the fields you need. If you send only new fields, ALL existing fields will
365
405
  be LOST from the schema definition.
366
406
 
367
- CORRECT WORKFLOW to add a column:
407
+ CORRECT WORKFLOW to add a column using full-replacement (baasix_update_schema):
368
408
  1. Call baasix_get_schema for the collection
369
409
  2. Copy the entire existing schema (including name, timestamps, paranoid, and ALL fields)
370
410
  3. Add/modify/remove the desired fields while keeping all other fields intact
@@ -397,10 +437,28 @@ EXAMPLE — Adding a "description" field to an existing "products" table that ha
397
437
  collection: z.string().describe("Collection name"),
398
438
  schema: z
399
439
  .object({
400
- fields: z.record(z.any()).optional().describe("The COMPLETE field definitions including ALL existing fields plus any additions/modifications. This REPLACES the entire schema — do NOT send partial fields."),
440
+ timestamps: z.boolean().optional().describe("true=keep/add createdAt+updatedAt columns. false=remove them. Omit to leave unchanged."),
441
+ paranoid: z.boolean().optional().describe("true=keep/add deletedAt soft-delete column. false=remove it. Omit to leave unchanged."),
442
+ sortEnabled: z.boolean().optional().describe("true=keep/add 'sort' Integer column. Omit to leave unchanged."),
443
+ usertrack: z.boolean().optional().describe("true=keep/add userCreated+userUpdated tracking fields. Omit to leave unchanged."),
444
+ fields: z.record(z.object({
445
+ type: z.string().describe(SUPPORTED_SCHEMA_FIELD_TYPES_DESCRIPTION),
446
+ allowNull: z.boolean().optional().describe("Allow NULL. Default true. false=NOT NULL constraint."),
447
+ unique: z.boolean().optional().describe("Enforce UNIQUE constraint."),
448
+ primaryKey: z.boolean().optional().describe("Primary key column."),
449
+ defaultValue: z.union([z.string(), z.number(), z.boolean(), z.null(), z.object({ type: z.string(), value: z.string().optional() })]).optional().describe("Literal default or { type: 'UUIDV4'|'SUID'|'NOW'|'AUTOINCREMENT'|'SQL', value?: 'expr' }."),
450
+ values: z.object({
451
+ length: z.number().optional().describe("Required for String/VARCHAR"),
452
+ precision: z.number().optional().describe("Required for Decimal"),
453
+ scale: z.number().optional().describe("Required for Decimal"),
454
+ type: z.string().optional().describe("Required for Array element type"),
455
+ values: z.array(z.string()).optional().describe("Required for Enum allowed values"),
456
+ }).optional(),
457
+ description: z.string().optional().describe("Field description (metadata only)"),
458
+ }).passthrough()).optional().describe("The COMPLETE field definitions — ALL existing fields plus your changes. This REPLACES the entire fields object. Missing fields will be removed from schema definition."),
401
459
  })
402
460
  .passthrough()
403
- .describe("The COMPLETE schema definition. This REPLACES the entire schema include ALL fields, not just changes."),
461
+ .describe("The COMPLETE schema definition. Replaces the entire schema. Fetch current schema with baasix_get_schema first, then include ALL fields plus your additions/modifications."),
404
462
  }, async (args, extra) => {
405
463
  const { collection, schema } = args;
406
464
  const res = await callRoute('PATCH', `/schemas/${encodeURIComponent(collection)}`, extra, { schema });
@@ -408,6 +466,113 @@ EXAMPLE — Adding a "description" field to an existing "products" table that ha
408
466
  return errorResult(res.error || `Failed to update collection '${collection}'`);
409
467
  return successResult({ success: true, message: `Collection '${collection}' updated successfully` });
410
468
  });
469
+ registerTool("baasix_add_schema_field", `Add a single field/column to an existing collection without sending the full schema.
470
+ Use this instead of baasix_update_schema when you only need to add one column.
471
+ Do NOT use for relation fields — use baasix_create_relationship for foreign keys.
472
+
473
+ FIELD TYPES:
474
+ - String/Text: String, Text, TEXT, CiText, HTML
475
+ - Numbers: Integer, BigInt, Decimal, Float, Real, Double, DOUBLE
476
+ - Date/time: Date, DateTime, DateTime_NO_TZ, Time, Time_NO_TZ
477
+ - IDs/tokens: UUID, SUID, TOKEN
478
+ - JSON/enums/virtual: JSON, JSONB, Enum, ENUM, VIRTUAL
479
+ - Arrays: Array_Integer, Array_String, Array_Double, Array_Decimal, Array_DateTime, Array_DateTime_NO_TZ, Array_Date, Array_Time, Array_Time_NO_TZ, Array_UUID, Array_Boolean
480
+ - Ranges: Range_Integer, Range_Double, Range_Decimal, Range_Date, Range_DateTime, Range_DateTime_NO_TZ, Range_Time, Range_Time_NO_TZ
481
+ - PostGIS: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, Geography
482
+
483
+ COMMON TYPE-SPECIFIC RULES:
484
+ - String: requires values.length (e.g. 255)
485
+ - Decimal: requires values.precision and values.scale
486
+ - Enum/ENUM: requires values.values array
487
+ - Array_* / Range_* / PostGIS types use explicit type names (recommended over generic aliases)
488
+
489
+ EXAMPLE — Add a nullable "bio" text column:
490
+ { collection: "users", fieldName: "bio", field: { type: "Text", allowNull: true } }
491
+
492
+ EXAMPLE — Add a "status" enum with default:
493
+ { collection: "orders", fieldName: "status", field: { type: "Enum", allowNull: false, defaultValue: "pending", values: { values: ["pending","processing","shipped","delivered"] } } }`, {
494
+ collection: z.string().describe("Name of the collection/table to add the field to"),
495
+ fieldName: z.string().describe("Column name to create. Must be a valid PostgreSQL identifier (letters, numbers, underscores; cannot start with a number). Max 63 chars."),
496
+ field: z
497
+ .object({
498
+ type: z.string().describe(`${SUPPORTED_SCHEMA_FIELD_TYPES_DESCRIPTION} Prefer explicit array/range/postgis types (for example Array_UUID, Range_DateTime, Point) over generic aliases.`),
499
+ allowNull: z.boolean().optional().describe("Whether the column can be NULL. Default: true. Set false to enforce NOT NULL."),
500
+ unique: z.boolean().optional().describe("Set true to add a UNIQUE constraint — no two rows can have the same value."),
501
+ primaryKey: z.boolean().optional().describe("Mark this column as the primary key. Rarely needed for new fields; typically only 'id' is the PK."),
502
+ defaultValue: z.union([
503
+ z.string(), z.number(), z.boolean(), z.null(),
504
+ z.object({ type: z.enum(["UUIDV4", "SUID", "NOW", "AUTOINCREMENT", "SQL"]), value: z.string().optional() })
505
+ ]).optional().describe("Default value for the column. Use a literal (e.g. 0, 'active', true) or a special object: { type: 'UUIDV4' } for auto UUID, { type: 'SUID' } for short ID, { type: 'NOW' } for current timestamp, { type: 'AUTOINCREMENT' } for auto-increment integer, { type: 'SQL', value: 'expr' } for raw SQL expression."),
506
+ values: z.object({
507
+ length: z.number().optional().describe("Max character length — required for String/VARCHAR (e.g. 255)"),
508
+ precision: z.number().optional().describe("Total number of digits — required for Decimal (e.g. 10)"),
509
+ scale: z.number().optional().describe("Number of decimal places — required for Decimal (e.g. 2)"),
510
+ type: z.string().optional().describe("Element data type — required for Array (e.g. 'String', 'Integer', 'UUID')"),
511
+ values: z.array(z.string()).optional().describe("Allowed enum values — required for Enum (e.g. ['active','inactive','pending'])"),
512
+ }).optional().describe("Type-specific configuration: String→{length}, Decimal→{precision,scale}, Array→{type}, Enum→{values:[...]}"),
513
+ description: z.string().optional().describe("Human-readable description stored in schema metadata only, not in the DB column."),
514
+ })
515
+ .passthrough()
516
+ .describe("Field/column definition object"),
517
+ }, async (args, extra) => {
518
+ const { collection, fieldName, field } = args;
519
+ const res = await callRoute('POST', `/schemas/${encodeURIComponent(collection)}/fields`, extra, { fieldName, field });
520
+ if (!res.ok)
521
+ return errorResult(res.error || `Failed to add field '${fieldName}' to '${collection}'`);
522
+ return successResult({ success: true, message: `Field '${fieldName}' added to '${collection}' successfully` });
523
+ });
524
+ registerTool("baasix_update_schema_field", `Update properties of a single existing field/column in a collection.
525
+ This is a PARTIAL update — only the properties you send are merged into the existing field definition.
526
+ Do NOT use for relation fields — use baasix_update_relationship instead.
527
+
528
+ EXAMPLE — Make a field non-nullable:
529
+ { collection: "products", fieldName: "name", field: { allowNull: false } }
530
+
531
+ EXAMPLE — Change a String field's max length:
532
+ { collection: "users", fieldName: "username", field: { type: "String", values: { length: 100 } } }
533
+
534
+ EXAMPLE — Add a default value to an existing field:
535
+ { collection: "orders", fieldName: "status", field: { defaultValue: "pending" } }`, {
536
+ collection: z.string().describe("Name of the collection/table that contains the field"),
537
+ fieldName: z.string().describe("Name of the existing field/column to update. Use baasix_get_schema to see all fields."),
538
+ field: z
539
+ .object({
540
+ type: z.string().optional().describe(`${SUPPORTED_SCHEMA_FIELD_TYPES_DESCRIPTION} Changing type on a populated column may require data migration.`),
541
+ allowNull: z.boolean().optional().describe("Set false to add a NOT NULL constraint, true to allow NULLs."),
542
+ unique: z.boolean().optional().describe("Set true to add a UNIQUE constraint, false to remove it."),
543
+ defaultValue: z.union([
544
+ z.string(), z.number(), z.boolean(), z.null(),
545
+ z.object({ type: z.enum(["UUIDV4", "SUID", "NOW", "AUTOINCREMENT", "SQL"]), value: z.string().optional() })
546
+ ]).optional().describe("New default value. Literal or special object: { type: 'UUIDV4'|'SUID'|'NOW'|'AUTOINCREMENT'|'SQL', value?: 'expr' }."),
547
+ values: z.object({
548
+ length: z.number().optional().describe("Max character length for String/VARCHAR"),
549
+ precision: z.number().optional().describe("Total digits for Decimal"),
550
+ scale: z.number().optional().describe("Decimal places for Decimal"),
551
+ type: z.string().optional().describe("Element type for Array"),
552
+ values: z.array(z.string()).optional().describe("Allowed values for Enum"),
553
+ }).optional().describe("Type-specific settings to update"),
554
+ description: z.string().optional().describe("Human-readable description stored in schema metadata"),
555
+ })
556
+ .passthrough()
557
+ .describe("Partial field definition — only the properties you include are merged into the existing field"),
558
+ }, async (args, extra) => {
559
+ const { collection, fieldName, field } = args;
560
+ const res = await callRoute('PATCH', `/schemas/${encodeURIComponent(collection)}/fields/${encodeURIComponent(fieldName)}`, extra, { field });
561
+ if (!res.ok)
562
+ return errorResult(res.error || `Failed to update field '${fieldName}' in '${collection}'`);
563
+ return successResult({ success: true, message: `Field '${fieldName}' updated in '${collection}' successfully` });
564
+ });
565
+ registerTool("baasix_delete_schema_field", `Delete a single field/column from a collection.
566
+ This updates schema definition only via schema manager logic and does not directly execute DROP COLUMN SQL.`, {
567
+ collection: z.string().describe("Collection name"),
568
+ fieldName: z.string().describe("Field/column name to delete"),
569
+ }, async (args, extra) => {
570
+ const { collection, fieldName } = args;
571
+ const res = await callRoute('DELETE', `/schemas/${encodeURIComponent(collection)}/fields/${encodeURIComponent(fieldName)}`, extra);
572
+ if (!res.ok)
573
+ return errorResult(res.error || `Failed to delete field '${fieldName}' from '${collection}'`);
574
+ return successResult({ success: true, message: `Field '${fieldName}' deleted from '${collection}' successfully` });
575
+ });
411
576
  registerTool("baasix_delete_schema", `DROP/DELETE an entire database table and all its data permanently.
412
577
  Use this when asked to 'drop a table', 'delete a collection', or 'remove a table'.
413
578
 
@@ -481,21 +646,22 @@ PARAMETERS:
481
646
  - target: the related table (e.g., "categories")
482
647
  - alias: reverse-access name on the target table (e.g., "products" so categories.products works)
483
648
  - onDelete: CASCADE | RESTRICT | SET NULL
649
+ - through: optional custom junction table for M2M/M2A. Recommendation: include "_junction" suffix (for example "products_tags_junction") for naming consistency.
484
650
 
485
651
  EXAMPLE — products belongsTo categories:
486
652
  sourceCollection: "products"
487
653
  relationshipData: { "name": "category", "type": "M2O", "target": "categories", "alias": "products", "onDelete": "CASCADE" }`, {
488
- sourceCollection: z.string().describe("Source collection name"),
654
+ sourceCollection: z.string().describe("Name of the collection/table that will own the relationship (the table that gets the FK column for M2O/O2O)."),
489
655
  relationshipData: z
490
656
  .object({
491
- name: z.string().describe("Relationship field name"),
492
- type: z.enum(["M2O", "O2M", "O2O", "M2M", "M2A"]).describe("Relationship type"),
493
- target: z.string().optional().describe("Target collection name"),
494
- alias: z.string().optional().describe("Alias for reverse relationship"),
495
- onDelete: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Delete behavior"),
496
- onUpdate: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Update behavior"),
497
- tables: z.array(z.string()).optional().describe("Target tables for M2A relationships"),
498
- through: z.string().optional().describe("Custom junction table name for M2M/M2A"),
657
+ name: z.string().describe("Field name for the relationship in the source collection (e.g. 'category'). For M2O/O2O this creates a '{name}_Id' FK column. Used to access the relation (e.g. product.category). Keep it lowercase camelCase."),
658
+ type: z.enum(["M2O", "O2M", "O2O", "M2M", "M2A"]).describe("Relationship type: M2O=BelongsTo (adds FK column to source), O2M=HasMany (virtual reverse, no column), O2O=HasOne (unique FK), M2M=Many-to-Many (auto-creates junction table), M2A=Polymorphic (one source to many different target types)."),
659
+ target: z.string().optional().describe("Name of the target collection/table being referenced. Required for M2O, O2M, O2O, M2M. For M2A use 'tables' instead."),
660
+ alias: z.string().optional().describe("Reverse-access name added to the target collection (e.g. if products M2O categories with alias='products', then categories.products returns all related products). Defaults to the source collection name."),
661
+ onDelete: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("What happens to the source record when the referenced target record is deleted. CASCADE=delete source too, RESTRICT=prevent target deletion if source exists, SET NULL=set FK to NULL."),
662
+ onUpdate: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("What happens to the FK when the target PK changes. CASCADE=update FK automatically, RESTRICT=block update, SET NULL=set FK to NULL."),
663
+ tables: z.array(z.string()).optional().describe("Required for M2A only. Array of target collection names this collection can relate to polymorphically (e.g. ['images','documents','videos'])."),
664
+ through: z.string().optional().describe("Custom name for the auto-created junction table in M2M/M2A. Recommended naming: include '_junction' suffix (example: 'products_tags_junction'). If omitted, Baasix auto-generates '{source}_{target}_junction'."),
499
665
  })
500
666
  .describe("Relationship configuration"),
501
667
  }, async (args, extra) => {
@@ -849,7 +1015,8 @@ Each permission defines: role_Id (which role), collection (which table), action
849
1015
  Filter examples:
850
1016
  All permissions for a role: {"role_Id": {"eq": "<role-uuid>"}}
851
1017
  All read permissions: {"action": {"eq": "read"}}
852
- Permissions for a table: {"collection": {"eq": "products"}}`, {
1018
+ Permissions for a table: {"collection": {"eq": "products"}}
1019
+ Single targeted permission (role + collection + optional action): {"AND": [{"role_Id": {"eq": "<role-uuid>"}}, {"collection": {"eq": "products"}}, {"action": {"eq": "read"}}]}`, {
853
1020
  filter: z.record(z.any()).optional().describe("Filter: {\"role_Id\": {\"eq\": \"<uuid>\"}}, {\"collection\": {\"eq\": \"products\"}}, {\"action\": {\"eq\": \"read\"}}"),
854
1021
  sort: z.string().optional().describe("Sort as 'field:asc' or 'field:desc'"),
855
1022
  page: z.number().optional().default(1).describe("Page number"),
@@ -1417,6 +1584,56 @@ Accepts either the role name (e.g., "editor", "public") or the role UUID.`, {
1417
1584
  return errorResult(error);
1418
1585
  }
1419
1586
  });
1587
+ registerTool("baasix_get_role_collection_permissions", `Get permissions for one specific role and collection, with optional action filter.
1588
+ Use this when you want a targeted RBAC check instead of listing all permissions.
1589
+
1590
+ Accepts role name (e.g., "editor", "public") or role UUID.
1591
+ Returns matching permission rows for that role+collection (and action if provided).`, {
1592
+ role: z.string().describe("Role name (e.g., 'editor', 'public') or role UUID"),
1593
+ collection: z.string().describe("Collection/table name to filter permissions for"),
1594
+ action: z.enum(["create", "read", "update", "delete"]).optional().describe("Optional action filter. If omitted, returns all actions for the role+collection."),
1595
+ }, async (args, extra) => {
1596
+ const { role, collection, action } = args;
1597
+ try {
1598
+ // Find role by name or id
1599
+ const roleParams = new URLSearchParams();
1600
+ roleParams.append('filter', JSON.stringify({ OR: [{ name: { eq: role } }, { id: { eq: role } }] }));
1601
+ roleParams.append('limit', '1');
1602
+ const rolesRes = await callRoute('GET', `/items/baasix_Role?${roleParams}`, extra);
1603
+ if (!rolesRes.ok)
1604
+ return errorResult(rolesRes.error || `Failed to look up role '${role}'`);
1605
+ const rolesData = rolesRes.data?.data || rolesRes.data;
1606
+ if (!Array.isArray(rolesData) || !rolesData.length) {
1607
+ return errorResult(`Role '${role}' not found`);
1608
+ }
1609
+ const roleId = rolesData[0].id;
1610
+ const filter = {
1611
+ role_Id: { eq: roleId },
1612
+ collection: { eq: collection },
1613
+ };
1614
+ if (action) {
1615
+ filter.action = { eq: action };
1616
+ }
1617
+ const permParams = new URLSearchParams();
1618
+ permParams.append('filter', JSON.stringify(filter));
1619
+ permParams.append('limit', '-1');
1620
+ const permRes = await callRoute('GET', `/permissions?${permParams}`, extra);
1621
+ if (!permRes.ok)
1622
+ return errorResult(permRes.error || 'Failed to get targeted permissions');
1623
+ const data = permRes.data?.data || permRes.data;
1624
+ const permissions = Array.isArray(data) ? data : [];
1625
+ return successResult({
1626
+ role: rolesData[0],
1627
+ collection,
1628
+ action: action || null,
1629
+ permissions,
1630
+ totalCount: permissions.length,
1631
+ });
1632
+ }
1633
+ catch (error) {
1634
+ return errorResult(error);
1635
+ }
1636
+ });
1420
1637
  // ==================== Update Permissions for Role Tool ====================
1421
1638
  registerTool("baasix_update_permissions", `Bulk set/update access control permissions for a role — define which tables a role can create, read, update, or delete. Creates new permissions or updates existing ones. Automatically reloads the permission cache.
1422
1639
 
@@ -1759,17 +1976,19 @@ Returns authenticated: false if no user is logged in (public/anonymous access).`
1759
1976
  }
1760
1977
  });
1761
1978
  // ==================== Update Relationship Tool ====================
1762
- registerTool("baasix_update_relationship", "Modify an existing foreign key / relationship between two database tables (change delete behavior, alias, etc.).", {
1763
- sourceCollection: z.string().describe("Source collection name"),
1764
- relationshipName: z.string().describe("Relationship field name to update"),
1979
+ registerTool("baasix_update_relationship", `Modify configuration of an existing foreign key / relationship (e.g. change delete behavior or alias).
1980
+ This does NOT change the relationship type or target — to do that, delete and recreate the relationship.
1981
+ Use baasix_get_schema first to see the current relationship configuration.`, {
1982
+ sourceCollection: z.string().describe("Name of the collection that owns the relationship (the table with the FK column)."),
1983
+ relationshipName: z.string().describe("Name of the relationship field to update. Use baasix_get_schema to see existing relationships and their exact names."),
1765
1984
  relationshipData: z
1766
1985
  .object({
1767
- alias: z.string().optional().describe("Alias for reverse relationship"),
1768
- onDelete: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Delete behavior"),
1769
- onUpdate: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("Update behavior"),
1770
- description: z.string().optional().describe("Relationship description"),
1986
+ alias: z.string().optional().describe("New reverse-access name on the target collection (e.g. 'orders' on baasix_User so user.orders returns all related orders)."),
1987
+ onDelete: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("What happens to the source record when the referenced target is deleted. CASCADE=delete source too, RESTRICT=block deletion, SET NULL=set FK to NULL."),
1988
+ onUpdate: z.enum(["CASCADE", "RESTRICT", "SET NULL"]).optional().describe("What happens to the FK when the target PK changes. CASCADE=update FK, RESTRICT=block update, SET NULL=set FK to NULL."),
1989
+ description: z.string().optional().describe("Human-readable description stored in schema metadata only."),
1771
1990
  })
1772
- .describe("Updated relationship configuration"),
1991
+ .describe("Updated relationship configuration — only include the properties you want to change"),
1773
1992
  }, async (args, extra) => {
1774
1993
  const { sourceCollection, relationshipName, relationshipData } = args;
1775
1994
  const res = await callRoute('PATCH', `/schemas/${encodeURIComponent(sourceCollection)}/relationships/${encodeURIComponent(relationshipName)}`, extra, relationshipData);