Skip to content

Commit 4c562c8

Browse files
waleedlatif1claude
andauthored
feat(tables): column operations, row ordering, V1 API (#3488)
* feat(tables): add column operations, row ordering, V1 columns API, and OpenAPI spec Adds column rename/delete/type change/constraint updates to the tables module, row ordering via position column, UI metadata schema, V1 public API for column operations with rate limiting and audit logging, and OpenAPI documentation. Key changes: - Service-layer column operations with validation (name pattern, type compatibility, unique/required constraints) - Position column on user_table_rows with composite index for efficient ordering - V1 /api/v1/tables/{tableId}/columns endpoint (POST/PATCH/DELETE) with rate limiting and audit - Shared Zod schemas extracted to table/utils.ts using COLUMN_TYPES constant - Targeted React Query invalidation (row vs schema mutations) with consistent onSettled usage - OpenAPI 3.1.0 spec for columns endpoint with code samples - Position field added to all row response mappings for consistency - Sort fallback to position ordering when buildSortClause returns null Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): use specific error prefixes instead of broad "Cannot" match Prevents internal TypeErrors (e.g. "Cannot read properties of undefined") from leaking as 400 responses. Now matches only domain-specific errors: "Cannot delete the last column" and "Cannot set column". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): reject Infinity and NaN in number type compatibility check Number.isFinite rejects Infinity, -Infinity, and NaN, preventing non-finite values from passing column type validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): invalidate table list on row create/delete for stale rowCount Row create and delete mutations now invalidate the table list cache since it includes a computed rowCount. Row updates (which don't change count) continue to only invalidate row queries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tables): add column name length check, deduplicate name gen, reset pagination on clear - Add MAX_COLUMN_NAME_LENGTH validation to addTableColumn (was missing, renameColumn already had it) - Extract generateColumnName helper to eliminate triplicated logic across handleAddColumn, handleInsertColumnLeft, handleInsertColumnRight - Reset pagination to page 0 when clearing sort/filter to prevent showing empty pages after narrowing filters are removed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: hoist tableId above try block in V1 columns route, add detail invalidation to invalidateRowCount - V1 columns route: `tableId` was declared inside `try` but referenced in `catch` logger.error, causing undefined in error logs. Hoisted `await params` above try in all three handlers (POST, PATCH, DELETE). - invalidateRowCount: added `tableKeys.detail(tableId)` invalidation since the single-table GET response includes `rowCount`, which becomes stale after row create/delete without this. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add position to all row mutation responses, remove dead filter code - Add `position` field to POST (single + batch) and PATCH row responses across both internal and V1 routes, matching GET responses and OpenAPI spec. - Remove unused `filterConfig`, `handleFilterToggle`, `handleFilterClear`, and `activeFilters` — dead code left over from merge conflict resolution. `handleFilterApply` (the one actually wired to JSX) is preserved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: invalidateTableSchema now also invalidates table list cache Column add/rename/delete/update mutations now invalidate tableKeys.list() since the list endpoint returns schema.columns for each table. Without this, the sidebar table list would show stale column schemas until staleTime expires. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace window.prompt/confirm with emcn Modal dialogs Replace non-standard browser dialogs with proper emcn Modal components to match the existing codebase pattern (e.g. delete table confirmation). - Column rename: Modal with Input field + Enter key support - Column delete: Modal with destructive confirmation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d4eb25d commit 4c562c8

File tree

20 files changed

+14550
-55
lines changed

20 files changed

+14550
-55
lines changed

apps/docs/openapi.json

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,288 @@
13621362
}
13631363
}
13641364
},
1365+
"/api/v1/tables/{tableId}/columns": {
1366+
"post": {
1367+
"operationId": "addColumn",
1368+
"summary": "Add Column",
1369+
"description": "Add a new column to the table schema. Optionally specify a position to insert the column at a specific index.",
1370+
"tags": ["Tables"],
1371+
"x-codeSamples": [
1372+
{
1373+
"lang": "curl",
1374+
"source": "curl -X POST \\\n \"https://www.sim.ai/api/v1/tables/{tableId}/columns\" \\\n -H \"X-API-Key: YOUR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"workspaceId\": \"YOUR_WORKSPACE_ID\",\n \"column\": {\n \"name\": \"email\",\n \"type\": \"string\",\n \"required\": true,\n \"unique\": true\n }\n }'"
1375+
}
1376+
],
1377+
"parameters": [
1378+
{
1379+
"name": "tableId",
1380+
"in": "path",
1381+
"required": true,
1382+
"schema": { "type": "string" },
1383+
"description": "The table ID"
1384+
}
1385+
],
1386+
"requestBody": {
1387+
"required": true,
1388+
"content": {
1389+
"application/json": {
1390+
"schema": {
1391+
"type": "object",
1392+
"required": ["workspaceId", "column"],
1393+
"properties": {
1394+
"workspaceId": {
1395+
"type": "string",
1396+
"description": "The workspace ID"
1397+
},
1398+
"column": {
1399+
"type": "object",
1400+
"required": ["name", "type"],
1401+
"properties": {
1402+
"name": {
1403+
"type": "string",
1404+
"description": "Column name (alphanumeric and underscores, must start with letter or underscore)"
1405+
},
1406+
"type": {
1407+
"type": "string",
1408+
"enum": ["string", "number", "boolean", "date", "json"],
1409+
"description": "Column data type"
1410+
},
1411+
"required": {
1412+
"type": "boolean",
1413+
"description": "Whether the column requires a value"
1414+
},
1415+
"unique": {
1416+
"type": "boolean",
1417+
"description": "Whether column values must be unique"
1418+
},
1419+
"position": {
1420+
"type": "integer",
1421+
"minimum": 0,
1422+
"description": "Position index to insert the column at (0-based). Appends if omitted."
1423+
}
1424+
}
1425+
}
1426+
}
1427+
}
1428+
}
1429+
}
1430+
},
1431+
"responses": {
1432+
"200": {
1433+
"description": "Column added successfully",
1434+
"content": {
1435+
"application/json": {
1436+
"schema": {
1437+
"type": "object",
1438+
"properties": {
1439+
"success": { "type": "boolean" },
1440+
"data": {
1441+
"type": "object",
1442+
"properties": {
1443+
"columns": {
1444+
"type": "array",
1445+
"items": { "$ref": "#/components/schemas/ColumnDefinition" }
1446+
}
1447+
}
1448+
}
1449+
}
1450+
}
1451+
}
1452+
}
1453+
},
1454+
"400": {
1455+
"$ref": "#/components/responses/BadRequest"
1456+
},
1457+
"401": {
1458+
"$ref": "#/components/responses/Unauthorized"
1459+
},
1460+
"404": {
1461+
"$ref": "#/components/responses/NotFound"
1462+
},
1463+
"429": {
1464+
"$ref": "#/components/responses/RateLimited"
1465+
}
1466+
}
1467+
},
1468+
"patch": {
1469+
"operationId": "updateColumn",
1470+
"summary": "Update Column",
1471+
"description": "Update a column's name, type, or constraints. Multiple updates can be applied in a single request. When renaming, subsequent updates (type, constraints) use the new name.",
1472+
"tags": ["Tables"],
1473+
"x-codeSamples": [
1474+
{
1475+
"lang": "curl",
1476+
"source": "curl -X PATCH \\\n \"https://www.sim.ai/api/v1/tables/{tableId}/columns\" \\\n -H \"X-API-Key: YOUR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"workspaceId\": \"YOUR_WORKSPACE_ID\",\n \"columnName\": \"old_name\",\n \"updates\": {\n \"name\": \"new_name\",\n \"type\": \"number\"\n }\n }'"
1477+
}
1478+
],
1479+
"parameters": [
1480+
{
1481+
"name": "tableId",
1482+
"in": "path",
1483+
"required": true,
1484+
"schema": { "type": "string" },
1485+
"description": "The table ID"
1486+
}
1487+
],
1488+
"requestBody": {
1489+
"required": true,
1490+
"content": {
1491+
"application/json": {
1492+
"schema": {
1493+
"type": "object",
1494+
"required": ["workspaceId", "columnName", "updates"],
1495+
"properties": {
1496+
"workspaceId": {
1497+
"type": "string",
1498+
"description": "The workspace ID"
1499+
},
1500+
"columnName": {
1501+
"type": "string",
1502+
"description": "Current name of the column to update"
1503+
},
1504+
"updates": {
1505+
"type": "object",
1506+
"properties": {
1507+
"name": {
1508+
"type": "string",
1509+
"description": "New column name"
1510+
},
1511+
"type": {
1512+
"type": "string",
1513+
"enum": ["string", "number", "boolean", "date", "json"],
1514+
"description": "New column data type"
1515+
},
1516+
"required": {
1517+
"type": "boolean",
1518+
"description": "Whether the column requires a value"
1519+
},
1520+
"unique": {
1521+
"type": "boolean",
1522+
"description": "Whether column values must be unique"
1523+
}
1524+
}
1525+
}
1526+
}
1527+
}
1528+
}
1529+
}
1530+
},
1531+
"responses": {
1532+
"200": {
1533+
"description": "Column updated successfully",
1534+
"content": {
1535+
"application/json": {
1536+
"schema": {
1537+
"type": "object",
1538+
"properties": {
1539+
"success": { "type": "boolean" },
1540+
"data": {
1541+
"type": "object",
1542+
"properties": {
1543+
"columns": {
1544+
"type": "array",
1545+
"items": { "$ref": "#/components/schemas/ColumnDefinition" }
1546+
}
1547+
}
1548+
}
1549+
}
1550+
}
1551+
}
1552+
}
1553+
},
1554+
"400": {
1555+
"$ref": "#/components/responses/BadRequest"
1556+
},
1557+
"401": {
1558+
"$ref": "#/components/responses/Unauthorized"
1559+
},
1560+
"404": {
1561+
"$ref": "#/components/responses/NotFound"
1562+
},
1563+
"429": {
1564+
"$ref": "#/components/responses/RateLimited"
1565+
}
1566+
}
1567+
},
1568+
"delete": {
1569+
"operationId": "deleteColumn",
1570+
"summary": "Delete Column",
1571+
"description": "Delete a column from the table schema. This removes the column definition and strips the corresponding key from all existing row data. Cannot delete the last remaining column.",
1572+
"tags": ["Tables"],
1573+
"x-codeSamples": [
1574+
{
1575+
"lang": "curl",
1576+
"source": "curl -X DELETE \\\n \"https://www.sim.ai/api/v1/tables/{tableId}/columns\" \\\n -H \"X-API-Key: YOUR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"workspaceId\": \"YOUR_WORKSPACE_ID\",\n \"columnName\": \"old_column\"\n }'"
1577+
}
1578+
],
1579+
"parameters": [
1580+
{
1581+
"name": "tableId",
1582+
"in": "path",
1583+
"required": true,
1584+
"schema": { "type": "string" },
1585+
"description": "The table ID"
1586+
}
1587+
],
1588+
"requestBody": {
1589+
"required": true,
1590+
"content": {
1591+
"application/json": {
1592+
"schema": {
1593+
"type": "object",
1594+
"required": ["workspaceId", "columnName"],
1595+
"properties": {
1596+
"workspaceId": {
1597+
"type": "string",
1598+
"description": "The workspace ID"
1599+
},
1600+
"columnName": {
1601+
"type": "string",
1602+
"description": "Name of the column to delete"
1603+
}
1604+
}
1605+
}
1606+
}
1607+
}
1608+
},
1609+
"responses": {
1610+
"200": {
1611+
"description": "Column deleted successfully",
1612+
"content": {
1613+
"application/json": {
1614+
"schema": {
1615+
"type": "object",
1616+
"properties": {
1617+
"success": { "type": "boolean" },
1618+
"data": {
1619+
"type": "object",
1620+
"properties": {
1621+
"columns": {
1622+
"type": "array",
1623+
"items": { "$ref": "#/components/schemas/ColumnDefinition" }
1624+
}
1625+
}
1626+
}
1627+
}
1628+
}
1629+
}
1630+
}
1631+
},
1632+
"400": {
1633+
"$ref": "#/components/responses/BadRequest"
1634+
},
1635+
"401": {
1636+
"$ref": "#/components/responses/Unauthorized"
1637+
},
1638+
"404": {
1639+
"$ref": "#/components/responses/NotFound"
1640+
},
1641+
"429": {
1642+
"$ref": "#/components/responses/RateLimited"
1643+
}
1644+
}
1645+
}
1646+
},
13651647
"/api/v1/tables/{tableId}/rows": {
13661648
"get": {
13671649
"operationId": "listRows",
@@ -3774,6 +4056,10 @@
37744056
"additionalProperties": true,
37754057
"description": "Row data as key-value pairs matching the table schema."
37764058
},
4059+
"position": {
4060+
"type": "integer",
4061+
"description": "Row's position/order in the table."
4062+
},
37774063
"createdAt": {
37784064
"type": "string",
37794065
"format": "date-time",

0 commit comments

Comments
 (0)