Skip to content

Commit f6f5084

Browse files
improvement(api): make v2 table import and export async-only
Drops the three synchronous entry points: POST /tables/[tableId]/import, POST /tables/import-csv, and GET /tables/[tableId]/export. Sync import tied a write to the lifetime of an HTTP request. The body *was* the data, so it carried a 10 MB cap that Next silently truncates past — a partial import reporting success. It also had no job, so a timeout mid-write left rows in place with nothing to poll and nothing to cancel. The async path reads the file from storage instead: upload via POST /api/v2/files for a key, start with POST /import-async, watch GET /tables/[tableId] -> job, stop with POST /job/cancel. Sync export carried no such hazard, but one shape per operation beats two: with both removed the surface has exactly one way to move a table in or out, and the CLI wraps the extra calls. This also removes the last multipart handling in v2 tables. Those were the only routes bypassing parseRequest — form fields were parsed by hand against separate form schemas, outside the contract system every other v2 write goes through. Create-a-table-from-CSV is now two calls: POST /tables, then /import-async with createColumns. csvImportModeSchema is append|replace, so there is no single-call create. Route baseline 1064 -> 1061.
1 parent 03e6ee9 commit f6f5084

9 files changed

Lines changed: 9 additions & 1473 deletions

File tree

apps/docs/openapi-v2-tables.json

Lines changed: 0 additions & 370 deletions
Original file line numberDiff line numberDiff line change
@@ -3076,77 +3076,6 @@
30763076
}
30773077
}
30783078
},
3079-
"/api/v2/tables/import-csv": {
3080-
"post": {
3081-
"operationId": "createTableFromCsv",
3082-
"summary": "Create Table From CSV",
3083-
"description": "Create a table from a CSV or TSV file. The column schema is inferred from the file’s first rows and the table is named after the file.\n\nSend `multipart/form-data` with `workspaceId` **before** the file part, so an unauthorized upload is rejected before its bytes are read. Rows stream in as they are parsed, so a file larger than memory still imports; a failure part way through drops the half-populated table rather than leaving it behind.",
3084-
"tags": ["Tables"],
3085-
"x-codeSamples": [
3086-
{
3087-
"id": "curl",
3088-
"label": "cURL",
3089-
"lang": "bash",
3090-
"source": "curl -X POST \\\n \"https://www.sim.ai/api/v2/tables/import-csv\" \\\n -H \"X-API-Key: YOUR_API_KEY\" \\\n -F \"workspaceId=YOUR_WORKSPACE_ID\" \\\n -F \"file=@contacts.csv\""
3091-
}
3092-
],
3093-
"requestBody": {
3094-
"required": true,
3095-
"description": "Bodies over 10 MB are rejected with 413 — use the async import instead.",
3096-
"content": {
3097-
"multipart/form-data": {
3098-
"schema": {
3099-
"$ref": "#/components/schemas/CreateTableFromCsvForm"
3100-
}
3101-
}
3102-
}
3103-
},
3104-
"responses": {
3105-
"201": {
3106-
"description": "The created table.",
3107-
"headers": {
3108-
"X-RateLimit-Limit": {
3109-
"$ref": "#/components/headers/RateLimitLimit"
3110-
},
3111-
"X-RateLimit-Remaining": {
3112-
"$ref": "#/components/headers/RateLimitRemaining"
3113-
},
3114-
"X-RateLimit-Reset": {
3115-
"$ref": "#/components/headers/RateLimitReset"
3116-
}
3117-
},
3118-
"content": {
3119-
"application/json": {
3120-
"schema": {
3121-
"$ref": "#/components/schemas/TableEnvelope"
3122-
}
3123-
}
3124-
}
3125-
},
3126-
"400": {
3127-
"$ref": "#/components/responses/BadRequest"
3128-
},
3129-
"401": {
3130-
"$ref": "#/components/responses/Unauthorized"
3131-
},
3132-
"403": {
3133-
"$ref": "#/components/responses/Forbidden"
3134-
},
3135-
"404": {
3136-
"$ref": "#/components/responses/NotFound"
3137-
},
3138-
"413": {
3139-
"$ref": "#/components/responses/PayloadTooLarge"
3140-
},
3141-
"429": {
3142-
"$ref": "#/components/responses/RateLimited"
3143-
},
3144-
"500": {
3145-
"$ref": "#/components/responses/InternalError"
3146-
}
3147-
}
3148-
}
3149-
},
31503079
"/api/v2/tables/jobs": {
31513080
"get": {
31523081
"operationId": "listTableJobs",
@@ -3224,99 +3153,6 @@
32243153
}
32253154
}
32263155
},
3227-
"/api/v2/tables/{tableId}/import": {
3228-
"post": {
3229-
"operationId": "importTableCsv",
3230-
"summary": "Import CSV",
3231-
"description": "Import a CSV or TSV into an existing table, appending or replacing its rows.\n\nSend `multipart/form-data` with `workspaceId` **before** the file part. Omit `mapping` to auto-map CSV headers to same-named columns; pass `createColumns` to have unmatched headers created as new columns, with types inferred from the file. The response reports what was written AND what was not (`skippedHeaders`, `unmappedColumns`), so a partial mapping is visible without diffing the schema.\n\nThe table’s single write-job slot is held for the whole import, so a concurrent import or delete gets 409. Files over 10 MB must use `POST /import-async`.",
3232-
"tags": ["Tables"],
3233-
"x-codeSamples": [
3234-
{
3235-
"id": "curl",
3236-
"label": "cURL",
3237-
"lang": "bash",
3238-
"source": "curl -X POST \\\n \"https://www.sim.ai/api/v2/tables/{tableId}/import\" \\\n -H \"X-API-Key: YOUR_API_KEY\" \\\n -F \"workspaceId=YOUR_WORKSPACE_ID\" \\\n -F \"mode=append\" \\\n -F \"file=@contacts.csv\""
3239-
}
3240-
],
3241-
"parameters": [
3242-
{
3243-
"$ref": "#/components/parameters/TableId"
3244-
}
3245-
],
3246-
"requestBody": {
3247-
"required": true,
3248-
"description": "Bodies over 10 MB are rejected with 413 — use the async import instead.",
3249-
"content": {
3250-
"multipart/form-data": {
3251-
"schema": {
3252-
"$ref": "#/components/schemas/ImportTableForm"
3253-
}
3254-
}
3255-
}
3256-
},
3257-
"responses": {
3258-
"200": {
3259-
"description": "The import summary.",
3260-
"headers": {
3261-
"X-RateLimit-Limit": {
3262-
"$ref": "#/components/headers/RateLimitLimit"
3263-
},
3264-
"X-RateLimit-Remaining": {
3265-
"$ref": "#/components/headers/RateLimitRemaining"
3266-
},
3267-
"X-RateLimit-Reset": {
3268-
"$ref": "#/components/headers/RateLimitReset"
3269-
}
3270-
},
3271-
"content": {
3272-
"application/json": {
3273-
"schema": {
3274-
"$ref": "#/components/schemas/ImportTableEnvelope"
3275-
},
3276-
"example": {
3277-
"data": {
3278-
"tableId": "tbl_92e4c6a8b0d24f1e8a3c5d7b9f0e2a14",
3279-
"mode": "append",
3280-
"insertedCount": 250,
3281-
"mappedColumns": ["Email", "Full Name"],
3282-
"skippedHeaders": ["Notes"],
3283-
"unmappedColumns": ["created_by"],
3284-
"sourceFile": "contacts.csv"
3285-
}
3286-
}
3287-
}
3288-
}
3289-
},
3290-
"400": {
3291-
"$ref": "#/components/responses/BadRequest"
3292-
},
3293-
"401": {
3294-
"$ref": "#/components/responses/Unauthorized"
3295-
},
3296-
"403": {
3297-
"$ref": "#/components/responses/Forbidden"
3298-
},
3299-
"404": {
3300-
"$ref": "#/components/responses/NotFound"
3301-
},
3302-
"409": {
3303-
"$ref": "#/components/responses/Conflict"
3304-
},
3305-
"413": {
3306-
"$ref": "#/components/responses/PayloadTooLarge"
3307-
},
3308-
"423": {
3309-
"$ref": "#/components/responses/Locked"
3310-
},
3311-
"429": {
3312-
"$ref": "#/components/responses/RateLimited"
3313-
},
3314-
"500": {
3315-
"$ref": "#/components/responses/InternalError"
3316-
}
3317-
}
3318-
}
3319-
},
33203156
"/api/v2/tables/{tableId}/import-async": {
33213157
"post": {
33223158
"operationId": "importTableCsvAsync",
@@ -3407,89 +3243,6 @@
34073243
}
34083244
}
34093245
},
3410-
"/api/v2/tables/{tableId}/export": {
3411-
"get": {
3412-
"operationId": "exportTable",
3413-
"summary": "Export Table",
3414-
"description": "Stream the whole table as a CSV or JSON file attachment.\n\nThe only endpoint whose success body is the file itself rather than the `{ data }` envelope. Rows are written as they are read, so nothing is buffered — but once the stream has started a failure can only tear the connection down. Large tables should use `POST /export-async`, which survives a dropped connection and leaves a re-downloadable result.",
3415-
"tags": ["Tables"],
3416-
"x-codeSamples": [
3417-
{
3418-
"id": "curl",
3419-
"label": "cURL",
3420-
"lang": "bash",
3421-
"source": "curl -X GET \\\n \"https://www.sim.ai/api/v2/tables/{tableId}/export?workspaceId=YOUR_WORKSPACE_ID&format=csv\" \\\n -H \"X-API-Key: YOUR_API_KEY\" \\\n -o table.csv"
3422-
}
3423-
],
3424-
"parameters": [
3425-
{
3426-
"$ref": "#/components/parameters/TableId"
3427-
},
3428-
{
3429-
"$ref": "#/components/parameters/WorkspaceIdQuery"
3430-
},
3431-
{
3432-
"$ref": "#/components/parameters/ExportFormatQuery"
3433-
}
3434-
],
3435-
"responses": {
3436-
"200": {
3437-
"description": "The table contents. CSV carries a header row of column names; JSON is an array of name-keyed row objects.",
3438-
"headers": {
3439-
"X-RateLimit-Limit": {
3440-
"$ref": "#/components/headers/RateLimitLimit"
3441-
},
3442-
"X-RateLimit-Remaining": {
3443-
"$ref": "#/components/headers/RateLimitRemaining"
3444-
},
3445-
"X-RateLimit-Reset": {
3446-
"$ref": "#/components/headers/RateLimitReset"
3447-
},
3448-
"Content-Disposition": {
3449-
"description": "Attachment filename, derived from the table name.",
3450-
"schema": {
3451-
"type": "string",
3452-
"example": "attachment; filename=\"customers.csv\""
3453-
}
3454-
}
3455-
},
3456-
"content": {
3457-
"text/csv": {
3458-
"schema": {
3459-
"type": "string"
3460-
}
3461-
},
3462-
"application/json": {
3463-
"schema": {
3464-
"type": "array",
3465-
"items": {
3466-
"type": "object"
3467-
}
3468-
}
3469-
}
3470-
}
3471-
},
3472-
"400": {
3473-
"$ref": "#/components/responses/BadRequest"
3474-
},
3475-
"401": {
3476-
"$ref": "#/components/responses/Unauthorized"
3477-
},
3478-
"403": {
3479-
"$ref": "#/components/responses/Forbidden"
3480-
},
3481-
"404": {
3482-
"$ref": "#/components/responses/NotFound"
3483-
},
3484-
"429": {
3485-
"$ref": "#/components/responses/RateLimited"
3486-
},
3487-
"500": {
3488-
"$ref": "#/components/responses/InternalError"
3489-
}
3490-
}
3491-
}
3492-
},
34933246
"/api/v2/tables/{tableId}/export-async": {
34943247
"post": {
34953248
"operationId": "exportTableAsync",
@@ -5422,129 +5175,6 @@
54225175
}
54235176
}
54245177
},
5425-
"ImportTableForm": {
5426-
"type": "object",
5427-
"description": "Multipart form for a synchronous import. `mapping` and `createColumns` are JSON-encoded strings, since every multipart field arrives as text.",
5428-
"required": ["workspaceId", "file"],
5429-
"properties": {
5430-
"workspaceId": {
5431-
"type": "string",
5432-
"minLength": 1,
5433-
"description": "The workspace that owns the table. Must appear BEFORE the file part — the server rejects an unauthorized upload before reading its bytes."
5434-
},
5435-
"file": {
5436-
"type": "string",
5437-
"format": "binary",
5438-
"description": "The .csv or .tsv file."
5439-
},
5440-
"mode": {
5441-
"enum": ["append", "replace"],
5442-
"default": "append",
5443-
"description": "`append` adds rows; `replace` deletes every existing row first."
5444-
},
5445-
"mapping": {
5446-
"type": "string",
5447-
"description": "JSON object mapping each CSV header to a column name, or null to skip that header. Omit to auto-map headers to same-named columns.",
5448-
"example": "{\"Email\":\"email\",\"Full Name\":\"name\",\"Notes\":null}"
5449-
},
5450-
"createColumns": {
5451-
"type": "string",
5452-
"description": "JSON array of CSV headers to create as new columns before importing. Their types are inferred from the file.",
5453-
"example": "[\"Phone\"]"
5454-
},
5455-
"timezone": {
5456-
"type": "string",
5457-
"description": "IANA zone used to read naive datetimes (Excel and Sheets exports carry no offset). Defaults to the API key owner’s saved timezone, else UTC.",
5458-
"example": "America/New_York"
5459-
}
5460-
}
5461-
},
5462-
"CreateTableFromCsvForm": {
5463-
"type": "object",
5464-
"description": "Multipart form for creating a table from a file.",
5465-
"required": ["workspaceId", "file"],
5466-
"properties": {
5467-
"workspaceId": {
5468-
"type": "string",
5469-
"minLength": 1,
5470-
"description": "The workspace to create the table in. Must appear BEFORE the file part — the server rejects an unauthorized upload before reading its bytes."
5471-
},
5472-
"file": {
5473-
"type": "string",
5474-
"format": "binary",
5475-
"description": "The .csv or .tsv file."
5476-
},
5477-
"folderId": {
5478-
"type": "string",
5479-
"description": "Folder to create the table in. Omit to create it at the workspace root."
5480-
},
5481-
"timezone": {
5482-
"type": "string",
5483-
"description": "IANA zone used to read naive datetimes. Defaults to the API key owner’s saved timezone, else UTC.",
5484-
"example": "America/New_York"
5485-
}
5486-
}
5487-
},
5488-
"ImportTableEnvelope": {
5489-
"type": "object",
5490-
"description": "Synchronous-import summary wrapped in the v2 data envelope.",
5491-
"required": ["data"],
5492-
"properties": {
5493-
"data": {
5494-
"type": "object",
5495-
"required": [
5496-
"tableId",
5497-
"mode",
5498-
"insertedCount",
5499-
"mappedColumns",
5500-
"skippedHeaders",
5501-
"unmappedColumns",
5502-
"sourceFile"
5503-
],
5504-
"properties": {
5505-
"tableId": {
5506-
"type": "string"
5507-
},
5508-
"mode": {
5509-
"enum": ["append", "replace"]
5510-
},
5511-
"insertedCount": {
5512-
"type": "integer",
5513-
"description": "Rows written."
5514-
},
5515-
"deletedCount": {
5516-
"type": "integer",
5517-
"description": "Rows removed first. Present only for `mode: \"replace\"`."
5518-
},
5519-
"mappedColumns": {
5520-
"type": "array",
5521-
"items": {
5522-
"type": "string"
5523-
},
5524-
"description": "CSV headers that were written to a column."
5525-
},
5526-
"skippedHeaders": {
5527-
"type": "array",
5528-
"items": {
5529-
"type": "string"
5530-
},
5531-
"description": "CSV headers the mapping explicitly skipped."
5532-
},
5533-
"unmappedColumns": {
5534-
"type": "array",
5535-
"items": {
5536-
"type": "string"
5537-
},
5538-
"description": "Table columns no CSV header supplied — left at their existing values."
5539-
},
5540-
"sourceFile": {
5541-
"type": "string",
5542-
"description": "Uploaded filename, echoed back."
5543-
}
5544-
}
5545-
}
5546-
}
5547-
},
55485178
"ImportAsyncEnvelope": {
55495179
"type": "object",
55505180
"description": "Background-import kickoff acknowledgement.",

0 commit comments

Comments
 (0)