Skip to content

Commit 39a4a4f

Browse files
feat(cli): sim tables import
Imports a CSV into a new or existing table, driving the same presigned handshake `files upload` uses — the two are the same protocol against different paths, so they now share one implementation. What made this more than a wrapper is that the import carries decisions the handshake does not: the source is a local file or one already in the workspace, the target is a new table or an existing one to append to or replace, and mapping/createColumns are rejected unless the target is existing. Both choices are required rather than inferred — defaulting to a new table would turn a forgotten --to-table into a silent second copy of the data — and the conditional flags are checked here so the error names the flag instead of arriving as a complaint about the request body. The transfer only queues the work; rows are parsed afterwards, so returning at `complete` would report success for an import that goes on to fail on a bad row. It polls to a settled status and reports the rows written, with progress on a terminal only. --no-wait opts out. The handshake steps are hidden now that a command drives them; `imports get` and `imports cancel` stay, being useful against an import already running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
1 parent a0fa865 commit 39a4a4f

3 files changed

Lines changed: 333 additions & 72 deletions

File tree

‎packages/sim-cli/src/commands/hand-written.test.ts‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { createWriteStream, existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
22
import { tmpdir } from 'node:os'
33
import { join } from 'node:path'
4-
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5-
import { streamToFile } from './hand-written.js'
4+
import { Command } from 'commander'
5+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6+
import { attachHandWritten, streamToFile } from './hand-written.js'
7+
8+
vi.mock('../context.js', () => ({
9+
clientFrom: () => ({
10+
client: { request: vi.fn(), requireWorkspace: () => 'ws_local' },
11+
profile: { workspaceId: 'ws_local', output: 'json', name: 'default', apiKey: 'k' },
12+
}),
13+
}))
614

715
let dir: string
816

@@ -59,3 +67,48 @@ describe('streamToFile', () => {
5967
}
6068
)
6169
})
70+
71+
describe('tables import argument guards', () => {
72+
function importCommand(): Command {
73+
const root = new Command('sim').exitOverride()
74+
attachHandWritten(root)
75+
const walk = (command: Command) => {
76+
command.exitOverride()
77+
command.commands.forEach(walk)
78+
}
79+
walk(root)
80+
return root
81+
}
82+
83+
async function run(argv: string[]) {
84+
await importCommand().parseAsync(['node', 'sim', 'tables', 'import', ...argv])
85+
}
86+
87+
it('refuses to guess the target', async () => {
88+
// Defaulting to a new table would turn a forgotten `--to-table` into a
89+
// silent second copy of the data.
90+
await expect(run(['f.csv'])).rejects.toThrow(/exactly one of --new-table/)
91+
await expect(run(['f.csv', '--new-table', 'a', '--to-table', 't'])).rejects.toThrow(
92+
/exactly one of --new-table/
93+
)
94+
})
95+
96+
it('refuses to guess the source', async () => {
97+
await expect(run(['--new-table', 'a'])).rejects.toThrow(/exactly one of <path>/)
98+
await expect(run(['f.csv', '--new-table', 'a', '--file-id', 'w_1'])).rejects.toThrow(
99+
/exactly one of <path>/
100+
)
101+
})
102+
103+
it('names the flag when mapping is paired with a new table', async () => {
104+
// The server rejects this too, but as a message about the request body.
105+
await expect(run(['f.csv', '--new-table', 'a', '--mapping', '{}'])).rejects.toThrow(
106+
/--to-table only/
107+
)
108+
})
109+
110+
it('checks all of that before touching the filesystem', async () => {
111+
// `f.csv` does not exist; a "cannot read" error would mean a guard ran late.
112+
await expect(run(['f.csv'])).rejects.toThrow(/exactly one of/)
113+
})
114+
})

0 commit comments

Comments
 (0)