Skip to content

Commit d62ad32

Browse files
docs(tables): OpenAPI spec for the v2 tables read API
Documents the two public v2 endpoints exactly as implemented — the predicate grammar (full operator enum with per-type and select-cardinality semantics, the strict-node and depth/size bounds), cursor pagination with its null-only termination contract, limit=0 unbounded semantics with the 5MB fail-fast, the camelCase built-in columns, and the flag-off 404 behaviour. Deliberately NOT wired into the docs site loader: the surface is dark behind tables-v2-api, and publishing reference docs for an endpoint that 404s would be premature. The filename matches PR #5273's multi-spec layout so adoption is a one-line loader change at GA. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M563cbFy2S74GvSDf2C3R
1 parent 3dfaaad commit d62ad32

1 file changed

Lines changed: 381 additions & 0 deletions

File tree

apps/docs/openapi-v2-tables.json

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": "Sim Tables API v2",
5+
"version": "2.0.0-preview",
6+
"description": "Read access to Sim tables with the typed predicate filter grammar and opaque cursor pagination. This surface is feature-gated (`tables-v2-api`): when the flag is off for the caller, every endpoint returns 404 as if it does not exist. Filters are predicate trees — `{\"all\": [...]}` (AND) or `{\"any\": [...]}` (OR) groups whose members are `{field, op, value}` conditions or nested groups. Built-in columns `id`, `createdAt`, and `updatedAt` (camelCase) are filterable and sortable alongside user columns."
7+
},
8+
"servers": [{ "url": "https://www.sim.ai" }],
9+
"security": [{ "apiKey": [] }],
10+
"paths": {
11+
"/api/v2/tables": {
12+
"get": {
13+
"operationId": "v2ListTables",
14+
"summary": "List Tables",
15+
"description": "List every table in a workspace with its column schema and row count.",
16+
"tags": ["Tables v2"],
17+
"parameters": [
18+
{
19+
"name": "workspaceId",
20+
"in": "query",
21+
"required": true,
22+
"schema": { "type": "string", "minLength": 1 }
23+
}
24+
],
25+
"responses": {
26+
"200": {
27+
"description": "Tables in the workspace. Served with `Cache-Control: private, no-store`.",
28+
"content": {
29+
"application/json": {
30+
"schema": {
31+
"type": "object",
32+
"required": ["success", "data"],
33+
"properties": {
34+
"success": { "const": true },
35+
"data": {
36+
"type": "object",
37+
"required": ["tables", "totalCount"],
38+
"properties": {
39+
"tables": {
40+
"type": "array",
41+
"items": { "$ref": "#/components/schemas/TableSummary" }
42+
},
43+
"totalCount": { "type": "integer" }
44+
}
45+
}
46+
}
47+
}
48+
}
49+
}
50+
},
51+
"400": { "$ref": "#/components/responses/ValidationError" },
52+
"401": { "$ref": "#/components/responses/Unauthorized" },
53+
"403": { "$ref": "#/components/responses/Forbidden" },
54+
"404": { "$ref": "#/components/responses/NotFoundOrGated" },
55+
"429": { "$ref": "#/components/responses/RateLimited" }
56+
}
57+
}
58+
},
59+
"/api/v2/tables/{tableId}/query": {
60+
"post": {
61+
"operationId": "v2QueryTableRows",
62+
"summary": "Query Rows",
63+
"description": "Query rows with a typed predicate filter, an ordered sort spec, and opaque cursor pagination. Row `data` is keyed by column NAME; `select` cells return option names, and filter operands on select columns accept option names (resolved case-insensitively).\n\n**Pagination contract:** page by passing the previous response's `nextCursor` back as `cursor`, and stop only when it is `null` — a page may return fewer than `limit` rows and still have more behind it, so page fullness is never a termination signal. A cursor encodes a position in the default row order and cannot be combined with `sort` (400 `CURSOR_SORT_CONFLICT`). `totalCount` is computed on the first page only (requests with a `cursor` return `totalCount: null`).",
64+
"tags": ["Tables v2"],
65+
"parameters": [
66+
{
67+
"name": "tableId",
68+
"in": "path",
69+
"required": true,
70+
"schema": { "type": "string", "minLength": 1 }
71+
}
72+
],
73+
"requestBody": {
74+
"required": true,
75+
"description": "Bodies over 1 MB are rejected with 413.",
76+
"content": {
77+
"application/json": {
78+
"schema": {
79+
"type": "object",
80+
"required": ["workspaceId"],
81+
"properties": {
82+
"workspaceId": { "type": "string", "minLength": 1 },
83+
"predicate": { "$ref": "#/components/schemas/Predicate" },
84+
"sort": {
85+
"type": "array",
86+
"maxItems": 16,
87+
"description": "Ordered sort spec, highest priority first.",
88+
"items": {
89+
"type": "object",
90+
"required": ["field", "direction"],
91+
"properties": {
92+
"field": { "type": "string" },
93+
"direction": { "enum": ["asc", "desc"] }
94+
}
95+
}
96+
},
97+
"limit": {
98+
"type": "integer",
99+
"minimum": 0,
100+
"maximum": 1000,
101+
"default": 100,
102+
"description": "Omitted → 100. `1..1000` → page size. `0` → the ENTIRE matching result in one response; fails with 400 `TABLE_QUERY_RESULT_TOO_LARGE` if it exceeds the 5 MB row-data budget (narrow the predicate or page instead)."
103+
},
104+
"cursor": {
105+
"type": "string",
106+
"description": "Opaque token from a previous response's `nextCursor`. Pass back verbatim. Mutually exclusive with `sort`."
107+
}
108+
}
109+
},
110+
"examples": {
111+
"filtered": {
112+
"summary": "Multi-select membership + negated pattern",
113+
"value": {
114+
"workspaceId": "ws_123",
115+
"predicate": {
116+
"all": [
117+
{ "field": "Color", "op": "contains", "value": "Purple" },
118+
{ "field": "name", "op": "nlike", "value": "G*" }
119+
]
120+
},
121+
"limit": 100
122+
}
123+
},
124+
"builtinColumns": {
125+
"summary": "Built-in column range (UTC, timezone-independent)",
126+
"value": {
127+
"workspaceId": "ws_123",
128+
"predicate": {
129+
"all": [
130+
{ "field": "createdAt", "op": "gte", "value": "2026-07-24T03:00:00.000Z" },
131+
{ "field": "createdAt", "op": "lte", "value": "2026-07-25T02:59:59.999Z" }
132+
]
133+
}
134+
}
135+
}
136+
}
137+
}
138+
}
139+
},
140+
"responses": {
141+
"200": {
142+
"description": "A page of rows. Served with `Cache-Control: private, no-store`.",
143+
"content": {
144+
"application/json": {
145+
"schema": {
146+
"type": "object",
147+
"required": ["success", "data"],
148+
"properties": {
149+
"success": { "const": true },
150+
"data": {
151+
"type": "object",
152+
"required": ["rows", "rowCount", "nextCursor"],
153+
"properties": {
154+
"rows": {
155+
"type": "array",
156+
"items": {
157+
"type": "object",
158+
"required": ["id", "data", "createdAt", "updatedAt"],
159+
"properties": {
160+
"id": { "type": "string" },
161+
"data": {
162+
"type": "object",
163+
"description": "Column-NAME-keyed cell values.",
164+
"additionalProperties": true
165+
},
166+
"createdAt": { "type": "string", "format": "date-time" },
167+
"updatedAt": { "type": "string", "format": "date-time" }
168+
}
169+
}
170+
},
171+
"rowCount": { "type": "integer", "description": "Rows in THIS page." },
172+
"totalCount": {
173+
"type": ["integer", "null"],
174+
"description": "Rows matching the predicate across all pages. First page only; null when a cursor was supplied."
175+
},
176+
"limit": { "type": ["integer", "null"] },
177+
"nextCursor": {
178+
"type": ["string", "null"],
179+
"description": "Non-null ⇒ more rows exist. The ONLY termination signal is null."
180+
}
181+
}
182+
}
183+
}
184+
}
185+
}
186+
}
187+
},
188+
"400": {
189+
"description": "Validation failure. Machine-readable `code` values include `INVALID_FILTER` (unknown column, operator/type mismatch, malformed tree), `INVALID_ORDER`, `INVALID_CURSOR`, `CURSOR_SORT_CONFLICT`, and `TABLE_QUERY_RESULT_TOO_LARGE` (unbounded result exceeded the 5 MB budget).",
190+
"content": {
191+
"application/json": {
192+
"schema": { "$ref": "#/components/schemas/ErrorBody" }
193+
}
194+
}
195+
},
196+
"401": { "$ref": "#/components/responses/Unauthorized" },
197+
"403": { "$ref": "#/components/responses/Forbidden" },
198+
"404": { "$ref": "#/components/responses/NotFoundOrGated" },
199+
"413": {
200+
"description": "Request body exceeded the 1 MB cap.",
201+
"content": {
202+
"application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
203+
}
204+
},
205+
"429": { "$ref": "#/components/responses/RateLimited" }
206+
}
207+
}
208+
}
209+
},
210+
"components": {
211+
"securitySchemes": {
212+
"apiKey": { "type": "apiKey", "in": "header", "name": "X-API-Key" }
213+
},
214+
"schemas": {
215+
"Predicate": {
216+
"description": "A predicate tree: exactly one of `all` (every member must match) or `any` (at least one must). Members are conditions or nested groups; nesting expresses mixed AND/OR logic. Groups must be non-empty (1–100 members), trees at most 10 levels deep and 500 nodes total. Nodes are STRICT: unknown keys, or a node carrying both a group key and condition keys, are rejected rather than ignored.",
217+
"oneOf": [
218+
{
219+
"type": "object",
220+
"required": ["all"],
221+
"additionalProperties": false,
222+
"properties": {
223+
"all": {
224+
"type": "array",
225+
"minItems": 1,
226+
"maxItems": 100,
227+
"items": { "$ref": "#/components/schemas/PredicateNode" }
228+
}
229+
}
230+
},
231+
{
232+
"type": "object",
233+
"required": ["any"],
234+
"additionalProperties": false,
235+
"properties": {
236+
"any": {
237+
"type": "array",
238+
"minItems": 1,
239+
"maxItems": 100,
240+
"items": { "$ref": "#/components/schemas/PredicateNode" }
241+
}
242+
}
243+
}
244+
]
245+
},
246+
"PredicateNode": {
247+
"oneOf": [
248+
{ "$ref": "#/components/schemas/Predicate" },
249+
{ "$ref": "#/components/schemas/Condition" }
250+
]
251+
},
252+
"Condition": {
253+
"type": "object",
254+
"required": ["field", "op"],
255+
"additionalProperties": false,
256+
"properties": {
257+
"field": {
258+
"type": "string",
259+
"maxLength": 128,
260+
"description": "Column name, or a built-in: `id`, `createdAt`, `updatedAt` (camelCase — snake_case is treated as a user column and matches nothing)."
261+
},
262+
"op": {
263+
"enum": [
264+
"eq",
265+
"ne",
266+
"gt",
267+
"gte",
268+
"lt",
269+
"lte",
270+
"in",
271+
"nin",
272+
"contains",
273+
"ncontains",
274+
"startsWith",
275+
"endsWith",
276+
"like",
277+
"ilike",
278+
"nlike",
279+
"nilike",
280+
"isEmpty",
281+
"isNotEmpty",
282+
"isNull",
283+
"isNotNull"
284+
],
285+
"description": "`eq`/`ne`/`in`/`nin` are case-sensitive equality/membership. `contains`/`ncontains`/`startsWith`/`endsWith` are case-insensitive text matches — except on a multi-select column, where `contains`/`ncontains` mean set membership by option name. `like`/`nlike` are case-sensitive and `ilike`/`nilike` case-insensitive patterns with `*` as the only wildcard (literal `%`/`_` match themselves). `isEmpty`/`isNotEmpty` treat null and empty string as empty; `isNull`/`isNotNull` are strict null checks. The four `is*` operators take no `value`. Negated text matches retain rows where the cell is absent. `in`/`nin` require a non-empty array of at most 1000 values; other value-taking operators reject arrays. Select columns accept only equality/membership operators appropriate to their cardinality (single: eq/ne/in/nin; multi: contains/ncontains; both: the `is*` checks)."
286+
},
287+
"value": {
288+
"description": "Operand. Omit for the `is*` operators. Ranges on `number` columns require numbers, on `date` columns ISO strings (compared as UTC, independent of any session timezone); ranges on `boolean`/`json` columns are rejected."
289+
}
290+
}
291+
},
292+
"TableSummary": {
293+
"type": "object",
294+
"required": ["id", "name", "schema", "rowCount", "maxRows", "createdAt", "updatedAt"],
295+
"properties": {
296+
"id": { "type": "string" },
297+
"name": { "type": "string" },
298+
"description": { "type": ["string", "null"] },
299+
"schema": {
300+
"type": "object",
301+
"properties": {
302+
"columns": {
303+
"type": "array",
304+
"items": {
305+
"type": "object",
306+
"required": ["name", "type"],
307+
"properties": {
308+
"id": { "type": "string" },
309+
"name": { "type": "string" },
310+
"type": { "enum": ["string", "number", "boolean", "date", "json", "select"] },
311+
"required": { "type": "boolean" },
312+
"unique": { "type": "boolean" },
313+
"options": {
314+
"type": "array",
315+
"description": "Declared choices on a `select` column.",
316+
"items": {
317+
"type": "object",
318+
"properties": { "id": { "type": "string" }, "name": { "type": "string" } }
319+
}
320+
},
321+
"multiple": { "type": "boolean" }
322+
}
323+
}
324+
}
325+
}
326+
},
327+
"rowCount": { "type": "integer" },
328+
"maxRows": { "type": "integer" },
329+
"createdAt": { "type": "string", "format": "date-time" },
330+
"updatedAt": { "type": "string", "format": "date-time" }
331+
}
332+
},
333+
"ErrorBody": {
334+
"type": "object",
335+
"required": ["error"],
336+
"properties": {
337+
"error": {
338+
"type": "string",
339+
"description": "Human-readable message naming the failing field/operator."
340+
},
341+
"code": {
342+
"type": "string",
343+
"description": "Machine-readable code, present on domain validation failures."
344+
}
345+
}
346+
}
347+
},
348+
"responses": {
349+
"ValidationError": {
350+
"description": "Malformed request.",
351+
"content": {
352+
"application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
353+
}
354+
},
355+
"Unauthorized": {
356+
"description": "Missing or invalid API key.",
357+
"content": {
358+
"application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
359+
}
360+
},
361+
"Forbidden": {
362+
"description": "The key's workspace scope does not cover this workspace, or the caller lacks read access.",
363+
"content": {
364+
"application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
365+
}
366+
},
367+
"NotFoundOrGated": {
368+
"description": "Table not found — or the `tables-v2-api` feature flag is off for this caller, in which case the entire surface answers 404. The gate is evaluated after authorization, so a 404 never distinguishes rollout cohort from missing resource for callers without access.",
369+
"content": {
370+
"application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
371+
}
372+
},
373+
"RateLimited": {
374+
"description": "Rate limit exceeded for this key.",
375+
"content": {
376+
"application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
377+
}
378+
}
379+
}
380+
}
381+
}

0 commit comments

Comments
 (0)