Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions apps/docs/content/docs/tables/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,13 @@ Every column has a type, which decides how its values are stored and validated.
| **Currency** | An amount in a currency you pick per column | `$1,234.56` |
| **Boolean** | `true` or `false` | `true` |
| **Date** | A date | `2026-03-16` |
| **Expiration** | An absolute row expiration time, stored as Unix epoch seconds (seconds since January 1, 1970 UTC) | `1773671400` |
| **JSON** | An object or array | `{ "tier": "pro" }` |
| **Select** | One of a fixed set of options, or several | `Pro` |

Types are enforced as you enter values, so a Number column only takes numbers.

A Currency column stores a plain number and renders it in the currency you choose for that column, so filters, sorts, and exports all see the amount itself. Changing a column's currency relabels it — it does not convert the amounts.

A table can have one Expiration column. Adding it enables row expiration; rows with a non-empty expiration value become eligible for deletion after that time passes. Cleanup runs periodically, so actual row removal may happen after the expiration timestamp rather than exactly at it. Deleting the Expiration column disables expiration for the table. Expiration cells use the date editor, while APIs and workflows read and write integer Unix epoch seconds.

## Editing a table

Open the **Tables** section in the sidebar and click **New table** to create one. Add columns from the column header, type into a cell to edit it, and paste rows from a spreadsheet to bulk-load. Filter and sort from the toolbar without changing the underlying data. The editor has full keyboard support; see [keyboard shortcuts](/keyboard-shortcuts).
Expand Down
90 changes: 9 additions & 81 deletions apps/docs/openapi-v2-tables.json
Original file line number Diff line number Diff line change
Expand Up @@ -4979,16 +4979,7 @@
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"currency",
"boolean",
"date",
"ttl",
"json",
"select"
],
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
"description": "Data type of values stored in the column."
},
"required": {
Expand Down Expand Up @@ -5266,16 +5257,7 @@
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"currency",
"boolean",
"date",
"ttl",
"json",
"select"
],
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
"description": "Column data type."
},
"required": {
Expand Down Expand Up @@ -5454,16 +5436,7 @@
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"currency",
"boolean",
"date",
"ttl",
"json",
"select"
],
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
"description": "Data type of values stored in the column."
},
"required": {
Expand Down Expand Up @@ -5563,16 +5536,7 @@
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"currency",
"boolean",
"date",
"ttl",
"json",
"select"
],
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
"description": "Column data type."
},
"required": {
Expand Down Expand Up @@ -5669,7 +5633,7 @@
"type": {
"description": "Replacement column data type.",
"type": "string",
"enum": ["string", "number", "currency", "boolean", "date", "ttl", "json", "select"]
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"]
},
"required": {
"description": "Whether inserts must supply a value for this column.",
Expand Down Expand Up @@ -7433,16 +7397,7 @@
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"currency",
"boolean",
"date",
"ttl",
"json",
"select"
],
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
"description": "Data type of values stored in the column."
},
"required": {
Expand Down Expand Up @@ -7642,16 +7597,7 @@
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"currency",
"boolean",
"date",
"ttl",
"json",
"select"
],
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
"description": "Output column data type."
},
"required": {
Expand Down Expand Up @@ -7792,16 +7738,7 @@
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"currency",
"boolean",
"date",
"ttl",
"json",
"select"
],
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
"description": "Output column data type."
},
"required": {
Expand Down Expand Up @@ -7919,16 +7856,7 @@
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"currency",
"boolean",
"date",
"ttl",
"json",
"select"
],
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
"description": "Data type of values stored in the column."
},
"required": {
Expand Down
4 changes: 3 additions & 1 deletion apps/sim/lib/api/contracts/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export const domainObjectSchema = <T>() => z.custom<T>(isRecordLike)
* Column types are a fixed enum derived from `COLUMN_TYPES` so callers cannot
* send arbitrary strings the server would reject downstream.
*/
export const columnTypeSchema = z.enum(COLUMN_TYPES)
export const columnTypeSchema = z
.enum(COLUMN_TYPES)
.meta({ omitEnumValuesFromOpenApi: ['ttl'] as const })

/** One choice in a `select` column. `id` is the stable cell key. */
export const selectOptionSchema = z.object({
Expand Down
4 changes: 4 additions & 0 deletions scripts/openapi/documents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ describe('generated OpenAPI documents', () => {
expect(tableProperties.ownerEmail).toMatchObject({ type: 'string', format: 'email' })
})

it('omits feature-flagged table column types', () => {
expect(JSON.stringify(generatedDocument(tablesOpenApiDocument))).not.toContain('"ttl"')
})

it('keeps billing as its own API reference group', () => {
const spec = generatedDocument(billingOpenApiDocument)
expect((spec.tags as JsonObject[]).map((tag) => tag.name)).toEqual(['Billing'])
Expand Down
29 changes: 29 additions & 0 deletions scripts/openapi/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,35 @@ describe('OpenAPI generator', () => {
})
})

it('omits feature-flagged enum values from generated schemas', () => {
const columnType = z.enum(['string', 'ttl']).meta({ omitEnumValuesFromOpenApi: ['ttl'] })
const body = z
.object({ type: columnType.describe('Column data type.') })
.meta({ id: 'HiddenEnumRequest', title: 'Hidden enum request', description: 'Request body.' })
const response = z
.object({ ok: z.boolean().describe('Whether the request succeeded.') })
.meta({ id: 'HiddenEnumResponse', title: 'Hidden enum response', description: 'Response.' })
const contract = defineRouteContract({
method: 'POST',
path: '/hidden-enum',
body,
response: { mode: 'json', schema: response },
})
const route = defineOpenApiRoute(
contract,
operation('hiddenEnum', { description: 'Response.' }),
{ body, response }
)
const spec = generateOpenApiDocument(document([route]))
const schemas = (spec.components as JsonObject).schemas as JsonObject
const requestProperties = (schemas.HiddenEnumRequest as JsonObject).properties as JsonObject
const documentedColumnType = requestProperties.type as JsonObject

expect(columnType.safeParse('ttl').success).toBe(true)
expect(documentedColumnType.enum).toEqual(['string'])
expect(documentedColumnType).not.toHaveProperty('omitEnumValuesFromOpenApi')
})

it('handles every route response mode and media type', () => {
const emptyContract = defineRouteContract({
method: 'DELETE',
Expand Down
37 changes: 30 additions & 7 deletions scripts/openapi/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,31 @@ function stripLegacySchemaIds(value: unknown): unknown {
)
}

function omitEnumValuesFromOpenApi(
metadata: z.core.GlobalMeta | undefined,
schema: JsonObject,
label: string
): void {
const omittedValues = metadata?.omitEnumValuesFromOpenApi
if (omittedValues === undefined) return

invariant(
Array.isArray(omittedValues) && omittedValues.length > 0,
`${label} omitEnumValuesFromOpenApi must be a non-empty array`
)
invariant(
Array.isArray(schema.enum),
`${label} omitEnumValuesFromOpenApi requires an enum schema`
)
for (const value of omittedValues) {
invariant(schema.enum.includes(value), `${label} omits an enum value that does not exist`)
}

schema.enum = schema.enum.filter((value) => !omittedValues.includes(value))
invariant(schema.enum.length > 0, `${label} cannot omit every enum value`)
Reflect.deleteProperty(schema, 'omitEnumValuesFromOpenApi')
}

function comparableSchema(schema: ApiSchema, io: SchemaIo): unknown {
const cached = comparableSchemaCache.get(schema)?.get(io)
if (cached) return cached
Expand Down Expand Up @@ -206,14 +231,12 @@ function generateSchema(
unrepresentable: 'any',
cycles: 'ref',
reused: 'inline',
override: ({ zodSchema, path }) => {
override: ({ zodSchema, jsonSchema, path }) => {
const current = zodSchema as ApiSchema
validateExamples(
current,
z.globalRegistry.get(current)?.examples,
io,
`${label} at ${path.join('.') || '<root>'}`
)
const metadata = z.globalRegistry.get(current)
const schemaLabel = `${label} at ${path.join('.') || '<root>'}`
validateExamples(current, metadata?.examples, io, schemaLabel)
omitEnumValuesFromOpenApi(metadata, jsonSchema as JsonObject, schemaLabel)
},
}) as JsonObject
const byIo = generatedSchemaCache.get(schema) ?? new Map<SchemaIo, JsonObject>()
Expand Down
Loading