Skip to content

Commit

Permalink
feat: simplify file upload (#24)
Browse files Browse the repository at this point in the history
* chore: update flake.lock

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/f4f322d1424aa547eba9cb092f905f5ceb9b639c' (2024-07-29)
  → 'github:NixOS/nixpkgs/3563397b2f10ffa1891e1a6ce99d13d960d73acd' (2024-07-30)

* chore: update flake.lock

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/3563397b2f10ffa1891e1a6ce99d13d960d73acd' (2024-07-30)
  → 'github:NixOS/nixpkgs/772f92db90637e4560a1d72f86099ccbce359cce' (2024-07-31)

* feat: simplify file upload
  • Loading branch information
joscha authored Jul 31, 2024
1 parent 449c7a2 commit 3568bc8
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 23 deletions.
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
deno
nixpkgs-fmt
# can't use slim here as long as we still publish with npm.
# TODO(@joscha) change this once we only publish to jsr
# TODO(@joscha) change this once we only publish to jsr only
nodejs_20
# For openapi-generator
# If more dependencies are needed, investigate whether to load https://github.com/OpenAPITools/openapi-generator/blob/master/flake.nix
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@
"author": "Planet A Ventures <[email protected]>",
"license": "MIT",
"private": true,
"type": "module"
"type": "module",
"engines": {
"node": ">=20"
}
}
3 changes: 3 additions & 0 deletions scripts/build_npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ await build({
access: 'public',
},
keywords: ['affinity', 'crm', 'node', 'api', 'sdk'],
engines: {
node: '>=20',
},
},
postBuild() {
// steps to run after building and before running the tests
Expand Down
50 changes: 37 additions & 13 deletions src/v1/entity_files.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { assert } from '@std/assert'
import type { AxiosInstance } from 'axios'
import fs from 'node:fs'
import path from 'node:path'
import type { Readable } from 'node:stream'
import { defaultTransformers } from './axios_default_transformers.ts'
import { createSearchIteratorFn } from './create_search_iterator_fn.ts'
Expand Down Expand Up @@ -63,9 +65,11 @@ export type PagedEntityFileResponse = Replace<PagedEntityFileResponseRaw, {
entity_files: EntityFile[]
}>

export type SupportedFileType = File | string

export type UploadEntityFileRequest =
& {
files: File[]
files: SupportedFileType[]
}
& RequireOnlyOne<EntityRequestFilter, keyof EntityRequestFilter>

Expand Down Expand Up @@ -188,18 +192,9 @@ export class EntityFiles {
const formData = new FormData()
const { files } = params
assert(files.length, 'At least one file must be provided')
if (files.length === 1) {
// Append the file as 'file' if only one file is provided
// it's a bit odd that the Affinity API expects the file to be sent in a different
// parameter, but maybe there is an implementation detail that treats multiple files
// differently to a single one, so we're complying with the API here
const [file] = files
formData.append('file', file, file.name)
} else {
files.forEach((file) => {
formData.append('files[]', file, file.name)
})
}

await Promise.all(files.map((file) => appendToFormData(formData, file)))

if (params.person_id) {
formData.append('person_id', params.person_id.toString())
} else if (params.organization_id) {
Expand Down Expand Up @@ -227,3 +222,32 @@ export class EntityFiles {
return response.data.success === true
}
}

function isFile(file: unknown): file is File {
return file instanceof File
}

async function appendToFormData(formData: FormData, file: SupportedFileType) {
if (typeof file === 'string') {
formData.append('files[]', await openAsBlob(file), path.basename(file))
} else if (isFile(file)) {
formData.append('files[]', file)
} else {
throw new Error('Unsupported file type')
}
}

// TODO(@joscha): replace with `import { openAsBlob } from "node:fs";` ASAP
function openAsBlob(filePath: string): Promise<Blob> {
return new Promise((resolve, reject) => {
const fileStream = fs.createReadStream(filePath)
const chunks: (string | ArrayBuffer)[] = []
fileStream.on('data', (chunk) => {
chunks.push(typeof chunk === 'string' ? chunk : chunk.buffer)
})
fileStream.on('end', () => {
resolve(new Blob(chunks))
})
fileStream.on('error', reject)
})
}
25 changes: 20 additions & 5 deletions src/v1/tests/__snapshots__/entity_files_test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,28 @@ snapshot[`entityFiles > can get a single file 1`] = `
}
`;

snapshot[`entityFiles > can upload files from path 1`] = `
{
"files[]": [
File {
name: "test.pdf",
size: 16374,
type: "",
},
],
person_id: "170614434",
}
`;

snapshot[`entityFiles > can upload a file 1`] = `
{
file: File {
name: "test.pdf",
size: 16374,
type: "",
},
"files[]": [
File {
name: "test.pdf",
size: 16374,
type: "",
},
],
person_id: "170614434",
}
`;
Expand Down
30 changes: 30 additions & 0 deletions src/v1/tests/entity_files_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { Affinity } from '../index.ts'
import { entityFilesUrl } from '../urls.ts'
import { apiKey, isLiveRun } from './env.ts'
import { getRawFixture, readFixtureFile } from './get_raw_fixture.ts'
import * as path from '@std/path'
import fs from 'node:fs'

const __dirname = path.dirname(path.fromFileUrl(import.meta.url))

const multipartFormDataHeaderMatcher = {
asymmetricMatch: (headers: Record<string, string>) => {
Expand Down Expand Up @@ -67,6 +71,32 @@ describe('entityFiles', () => {
await assertSnapshot(t, res)
})

it('can upload files from path', async (t) => {
mock
?.onPost(
entityFilesUrl(),
createSnapshotBodyMatcher(t),
multipartFormDataHeaderMatcher,
)
.reply(
200,
{ success: true },
)
const localPath = path.join(
__dirname,
'fixtures',
'entity_files',
'test.pdf',
)
const res = await affinity.entityFiles.upload({
person_id: 170614434,
files: [
localPath,
],
})
assert(res)
})

it('can upload a file', async (t) => {
mock
?.onPost(
Expand Down

0 comments on commit 3568bc8

Please sign in to comment.