diff --git a/apps/docs/content/docs/tables/index.mdx b/apps/docs/content/docs/tables/index.mdx index 41b821d995e..e44ca483464 100644 --- a/apps/docs/content/docs/tables/index.mdx +++ b/apps/docs/content/docs/tables/index.mdx @@ -24,7 +24,6 @@ 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` | @@ -32,8 +31,6 @@ 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). diff --git a/apps/docs/openapi-v2-tables.json b/apps/docs/openapi-v2-tables.json index d91748c097d..fb9688e5f6c 100644 --- a/apps/docs/openapi-v2-tables.json +++ b/apps/docs/openapi-v2-tables.json @@ -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": { @@ -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": { @@ -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": { @@ -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": { @@ -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.", @@ -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": { @@ -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": { @@ -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": { @@ -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": { diff --git a/apps/sim/lib/api/contracts/tables.ts b/apps/sim/lib/api/contracts/tables.ts index 9d3a95e206e..30dd03797e3 100644 --- a/apps/sim/lib/api/contracts/tables.ts +++ b/apps/sim/lib/api/contracts/tables.ts @@ -53,7 +53,9 @@ export const domainObjectSchema = () => z.custom(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({ diff --git a/scripts/openapi/documents.test.ts b/scripts/openapi/documents.test.ts index d2c31bd694b..c7c3b510584 100644 --- a/scripts/openapi/documents.test.ts +++ b/scripts/openapi/documents.test.ts @@ -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']) diff --git a/scripts/openapi/generator.test.ts b/scripts/openapi/generator.test.ts index b6b9b453fb9..14fbd811396 100644 --- a/scripts/openapi/generator.test.ts +++ b/scripts/openapi/generator.test.ts @@ -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', diff --git a/scripts/openapi/generator.ts b/scripts/openapi/generator.ts index 550113a1ce8..93fb3bb762b 100644 --- a/scripts/openapi/generator.ts +++ b/scripts/openapi/generator.ts @@ -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 @@ -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('.') || ''}` - ) + const metadata = z.globalRegistry.get(current) + const schemaLabel = `${label} at ${path.join('.') || ''}` + validateExamples(current, metadata?.examples, io, schemaLabel) + omitEnumValuesFromOpenApi(metadata, jsonSchema as JsonObject, schemaLabel) }, }) as JsonObject const byIo = generatedSchemaCache.get(schema) ?? new Map()