From b01509be1c6fd9c6a3734b536fb9649820887314 Mon Sep 17 00:00:00 2001 From: Joscha Feth Date: Mon, 29 Jul 2024 18:11:08 +0100 Subject: [PATCH] style: fix --- README.md | 2 +- deno.jsonc | 4 +- deno.lock | 54 ++++- flake.lock | 6 +- src/v1/entity_files.ts | 207 ++++++++++++++++++ src/v1/field_value_changes.ts | 11 +- src/v1/field_values.ts | 5 +- src/v1/index.ts | 5 + src/v1/list_entries.ts | 14 +- src/v1/paged_response.ts | 4 + .../__snapshots__/entity_files_test.ts.snap | 58 +++++ src/v1/tests/entity_files_test.ts | 121 ++++++++++ .../entity_files/all.raw.response.json | 25 +++ src/v1/tests/fixtures/entity_files/test.pdf | Bin 0 -> 16374 bytes src/v1/tests/get_raw_fixture.ts | 4 + src/v1/urls.ts | 14 ++ 16 files changed, 512 insertions(+), 22 deletions(-) create mode 100644 src/v1/entity_files.ts create mode 100644 src/v1/tests/__snapshots__/entity_files_test.ts.snap create mode 100644 src/v1/tests/entity_files_test.ts create mode 100644 src/v1/tests/fixtures/entity_files/all.raw.response.json create mode 100644 src/v1/tests/fixtures/entity_files/test.pdf diff --git a/README.md b/README.md index 7ca9375..6127e24 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ examples, etc. - ❌ Interactions - ❌ Relationship Strengths - ❌ Notes -- ❌ Entity Files +- ✅ [Entity Files](src/v1/entity_files.ts) - ❌ Reminders - ❌ Webhooks - ✅ [Whoami](src/v1/auth.ts) diff --git a/deno.jsonc b/deno.jsonc index d912c91..09f8930 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -4,6 +4,7 @@ "@std/assert": "jsr:@std/assert@^0.226.0", "@std/fs": "jsr:@std/fs@^0.229.1", "@std/path": "jsr:@std/path@^0.225.2", + "@std/streams": "jsr:@std/streams@^1.0.0", "@std/testing": "jsr:@std/testing@^0.225.0", "axios": "npm:axios@^1.7.2", "axios-mock-adapter": "https://esm.sh/axios-mock-adapter@1.22.0", @@ -12,7 +13,8 @@ "tasks": { "build": "deno run -A scripts/build_npm.ts", "check": "deno check src/**/*.ts", - "test": "deno test --allow-env=API_KEY,NODE_EXTRA_CA_CERTS --allow-net=api.affinity.co --doc --allow-read=./src/v1/tests/ src/**/tests/", + "test": "deno test --allow-env=API_KEY,NODE_EXTRA_CA_CERTS --allow-net --doc --allow-read=./src/v1/tests/ src/**/tests/", + "test:single": "deno test --allow-env=API_KEY,NODE_EXTRA_CA_CERTS --allow-net --doc --allow-read=./src/v1/tests/", "test:coverage": "deno task test --coverage=cov_profile && deno coverage cov_profile --html", "watch": "deno task test --no-clear-screen --watch --shuffle --parallel", "snapshot-update": "deno task test --allow-write=./src/v1/tests/__snapshots__ -- --update", diff --git a/deno.lock b/deno.lock index edfff99..ba643f1 100644 --- a/deno.lock +++ b/deno.lock @@ -4,20 +4,28 @@ "specifiers": { "jsr:@deno/cache-dir@^0.8.0": "jsr:@deno/cache-dir@0.8.0", "jsr:@deno/dnt@^0.41.2": "jsr:@deno/dnt@0.41.2", + "jsr:@std/assert@1.0.0-rc.2": "jsr:@std/assert@1.0.0-rc.2", "jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2", "jsr:@std/assert@^0.225.2": "jsr:@std/assert@0.225.3", "jsr:@std/assert@^0.226.0": "jsr:@std/assert@0.226.0", "jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2", + "jsr:@std/bytes@^1.0.2-rc.3": "jsr:@std/bytes@1.0.2", "jsr:@std/fmt@^0.218.2": "jsr:@std/fmt@0.218.2", "jsr:@std/fmt@^0.225.3": "jsr:@std/fmt@0.225.3", + "jsr:@std/fmt@^0.225.4": "jsr:@std/fmt@0.225.6", "jsr:@std/fs@^0.218.2": "jsr:@std/fs@0.218.2", "jsr:@std/fs@^0.229.1": "jsr:@std/fs@0.229.1", - "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.0", + "jsr:@std/fs@^1.0.0-rc.1": "jsr:@std/fs@1.0.0", + "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.1", + "jsr:@std/io": "jsr:@std/io@0.218.2", "jsr:@std/io@^0.218.2": "jsr:@std/io@0.218.2", + "jsr:@std/path@1.0.0-rc.2": "jsr:@std/path@1.0.0-rc.2", "jsr:@std/path@^0.218.2": "jsr:@std/path@0.218.2", "jsr:@std/path@^0.225.1": "jsr:@std/path@0.225.2", "jsr:@std/path@^0.225.2": "jsr:@std/path@0.225.2", - "jsr:@std/testing@^0.225.0": "jsr:@std/testing@0.225.0", + "jsr:@std/path@^1.0.2": "jsr:@std/path@1.0.2", + "jsr:@std/streams@^1.0.0": "jsr:@std/streams@1.0.0", + "jsr:@std/testing@^0.225.0": "jsr:@std/testing@0.225.3", "npm:@openapitools/openapi-generator-cli@2.13.4": "npm:@openapitools/openapi-generator-cli@2.13.4_@nestjs+common@10.3.0__reflect-metadata@0.1.13__rxjs@7.8.1_axios@1.6.8_rxjs@7.8.1_reflect-metadata@0.1.13", "npm:@ts-morph/bootstrap@0.22": "npm:@ts-morph/bootstrap@0.22.0", "npm:@types/node": "npm:@types/node@18.16.19", @@ -57,15 +65,24 @@ "jsr:@std/internal@^1.0.0" ] }, + "@std/assert@1.0.0-rc.2": { + "integrity": "0484eab1d76b55fca1c3beaff485a274e67dd3b9f065edcbe70030dfc0b964d3" + }, "@std/bytes@0.218.2": { "integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670" }, + "@std/bytes@1.0.2": { + "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" + }, "@std/fmt@0.218.2": { "integrity": "99526449d2505aa758b6cbef81e7dd471d8b28ec0dcb1491d122b284c548788a" }, "@std/fmt@0.225.3": { "integrity": "cb6ea567155f9865b80b502b2dde7671803eddd6dad743d8851d0de2c40bd349" }, + "@std/fmt@0.225.6": { + "integrity": "aba6aea27f66813cecfd9484e074a9e9845782ab0685c030e453a8a70b37afc8" + }, "@std/fs@0.218.2": { "integrity": "dd9431453f7282e8c577cc22c9e6d036055a9a980b5549f887d6012969fabcca", "dependencies": [ @@ -80,12 +97,22 @@ "jsr:@std/path@^0.225.1" ] }, + "@std/fs@1.0.0": { + "integrity": "d72e4a125af7168d717a2ed1dca77a728b422b0d138fd20579e3fa41a77da943", + "dependencies": [ + "jsr:@std/path@^1.0.2" + ] + }, "@std/internal@1.0.0": { "integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a" }, + "@std/internal@1.0.1": { + "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" + }, "@std/io@0.218.2": { "integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f", "dependencies": [ + "jsr:@std/assert@^0.218.2", "jsr:@std/bytes@^0.218.2" ] }, @@ -101,6 +128,18 @@ "jsr:@std/assert@^0.226.0" ] }, + "@std/path@1.0.0-rc.2": { + "integrity": "39f20d37a44d1867abac8d91c169359ea6e942237a45a99ee1e091b32b921c7d" + }, + "@std/path@1.0.2": { + "integrity": "a452174603f8c620bd278a380c596437a9eef50c891c64b85812f735245d9ec7" + }, + "@std/streams@1.0.0": { + "integrity": "350242b8fad9874ed45f3c42df3d132bd0a958f8a8bae9bbfa1ff039716aa6fb", + "dependencies": [ + "jsr:@std/bytes@^1.0.2-rc.3" + ] + }, "@std/testing@0.225.0": { "integrity": "53ca3c47eb121acabda633fd50699c4b33da6a7ce3e5bb34d7277d15df5f0a6e", "dependencies": [ @@ -110,6 +149,16 @@ "jsr:@std/internal@^1.0.0", "jsr:@std/path@^0.225.2" ] + }, + "@std/testing@0.225.3": { + "integrity": "348c24d0479d44ab3dbb4f26170f242e19f24051b45935d4a9e7ca0ab7e37780", + "dependencies": [ + "jsr:@std/assert@1.0.0-rc.2", + "jsr:@std/fmt@^0.225.4", + "jsr:@std/fs@^1.0.0-rc.1", + "jsr:@std/internal@^1.0.0", + "jsr:@std/path@1.0.0-rc.2" + ] } }, "npm": { @@ -1071,6 +1120,7 @@ "jsr:@std/assert@^0.226.0", "jsr:@std/fs@^0.229.1", "jsr:@std/path@^0.225.2", + "jsr:@std/streams@^1.0.0", "jsr:@std/testing@^0.225.0", "npm:axios@^1.7.2", "npm:typescript@^5.4.5" diff --git a/flake.lock b/flake.lock index c85667a..f562e72 100644 --- a/flake.lock +++ b/flake.lock @@ -57,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1722126295, - "narHash": "sha256-K9ia1zVOgi2a75aivdPC5pVr4BlTqHv4h8KMh4utrzI=", + "lastModified": 1722267763, + "narHash": "sha256-E22vXGsS/NK1T0oZWDbI+E34+oGJHk9YUkszDYInUIU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4b4a3bf7f651861551b04380dc4a099d9bb35d20", + "rev": "f4f322d1424aa547eba9cb092f905f5ceb9b639c", "type": "github" }, "original": { diff --git a/src/v1/entity_files.ts b/src/v1/entity_files.ts new file mode 100644 index 0000000..f1541ad --- /dev/null +++ b/src/v1/entity_files.ts @@ -0,0 +1,207 @@ +import type { AxiosInstance } from 'axios' +import { defaultTransformers } from './axios_default_transformers.ts' +import { FieldBase } from './fields.ts' +import { FieldValueType } from './lists.ts' +import type { DateTime, Replace, RequireOnlyOne } from './types.ts' +import { entityFilesUrl, fieldsUrl } from './urls.ts' +import type { PagedRequest } from './paged_request.ts' +import type { PagedResponse } from './paged_response.ts' +export type { DateTime } from './types.ts' +import { Readable } from 'node:stream' +import { EntityRequestFilter } from './field_value_changes.ts' +import { assert } from '@std/assert' + +type EntityFileRaw = { + /** The unique identifier of the entity file object. */ + id: number + /** The name of the file. */ + name: string + /** The size of the file in bytes. */ + size: string + /** The unique identifier of the person corresponding to the entity file. */ + person_id: number | null + /** The unique identifier of the organization corresponding to the entity file. */ + organization_id: number | null + /** The unique identifier of the opportunity corresponding to the entity file. */ + opportunity_id: number | null + /** The unique identifier of the user who created the entity file. */ + uploader_id: number + /** The time when the entity file was created. */ + created_at: DateTime +} + +type EntityFile = Replace + +/** + * Represents the request parameters for retrieving entity files. + */ +type AllEntityFileRequest = + & { + /** + * A unique ID that represents a Person whose associated files should be retrieved. + */ + person_id?: number + /** + * A unique ID that represents an Organization whose associated files should be retrieved. + */ + organization_id?: number + /** + * A unique ID that represents an Opportunity whose associated files should be retrieved. + */ + opportunity_id?: number + } + & PagedRequest + +type PagedEntityFileResponseRaw = + & { + entity_files: EntityFileRaw[] + } + & PagedResponse + +type PagedEntityFileResponse = Replace + +type UploadEntityFileRequest = + & { + files: File[] + } + & RequireOnlyOne + +/** + * Entity files are files uploaded to a relevant entity. + * Possible files, for example, would be a pitch deck for an opportunity or a physical mail correspondence for a person. + */ +export class EntityFiles { + /** @hidden */ + constructor(private readonly axios: AxiosInstance) { + } + + private static transformEntityFile(file: EntityFileRaw): EntityFile { + return { + ...file, + created_at: new Date(file.created_at), + } + } + + /** + * Returns all entity files within your organization. + */ + async all(params?: AllEntityFileRequest): Promise { + const response = await this.axios.get( + entityFilesUrl(), + { + params, + transformResponse: [ + ...defaultTransformers(), + (json: PagedEntityFileResponseRaw) => { + return { + ...json, + entity_files: json.entity_files.map( + EntityFiles.transformEntityFile, + ), + } + }, + ], + }, + ) + return response.data + } + + /** + * Fetches an entity with a specified `entity_file_id`. + */ + async get(entity_file_id: EntityFile['id']): Promise { + const response = await this.axios.get( + entityFilesUrl(entity_file_id), + { + transformResponse: [ + ...defaultTransformers(), + EntityFiles.transformEntityFile, + ], + }, + ) + return response.data + } + + /** + * Downloads the entity file with the specified `entity_file_id`. + * + * @example + * ```typescript + * import { promises as fsPromises } from 'node:fs'; + * const fileResponseStream = affinity.entityFiles.download(123); + * await fsPromises.writeFile(filePath, fileResponseStream); + * ``` + */ + async download(entity_file_id: EntityFile['id']): Promise { + const response = await this.axios.get( + entityFilesUrl(entity_file_id, true), + { + responseType: 'stream', + // The download location of entity files is provided via a redirect from Affinity + maxRedirects: 5, + }, + ) + return response.data + } + + /** + * Uploads files attached to the entity with the given id. + * + * The file will display on the entity's profile, provided that the entity is not a person internal to the user's organization. + * + * @example + * ```typescript + * const file = fs.createReadStream('example.pdf'); + * const entityFile = await affinity.entityFiles.upload({ + * files: [file], + * person_id: 123, + * }); + * ``` + */ + async upload(params: UploadEntityFileRequest): Promise { + 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) + }) + } + if (params.person_id) { + formData.append('person_id', params.person_id.toString()) + } else if (params.organization_id) { + formData.append( + 'organization_id', + params.organization_id.toString(), + ) + } else if (params.opportunity_id) { + formData.append('opportunity_id', params.opportunity_id.toString()) + } else { + throw new Error( + 'One of person_id, organization_id or opportunity_id must be provided', + ) + } + + const response = await this.axios.post<{ success: boolean }>( + entityFilesUrl(), + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ) + return response.data.success === true + } +} diff --git a/src/v1/field_value_changes.ts b/src/v1/field_value_changes.ts index 7ab250a..0411392 100644 --- a/src/v1/field_value_changes.ts +++ b/src/v1/field_value_changes.ts @@ -85,10 +85,7 @@ export type FieldValueChange = Replace can list all files 1`] = ` +{ + entity_files: [ + { + created_at: 2011-01-25T17:59:35.288Z, + id: 43212, + name: "JohnDoeFriends.csv", + opportunity_id: null, + organization_id: null, + person_id: 142, + size: 993, + uploader_id: 10, + }, + { + created_at: 2019-01-13T20:52:51.539Z, + id: 131, + name: "Import.csv", + opportunity_id: null, + organization_id: null, + person_id: 38654, + size: 227224, + uploader_id: 101, + }, + ], + next_page_token: null, +} +`; + +snapshot[`persons > can upload a file 1`] = ` +{ + file: File { + name: "test.pdf", + size: 16374, + type: "", + }, + person_id: "170614434", +} +`; + +snapshot[`persons > can upload multiple files 1`] = ` +{ + "files[]": [ + File { + name: "test1.pdf", + size: 16374, + type: "", + }, + File { + name: "test2.pdf", + size: 16374, + type: "", + }, + ], + person_id: "170614434", +} +`; diff --git a/src/v1/tests/entity_files_test.ts b/src/v1/tests/entity_files_test.ts new file mode 100644 index 0000000..5989c26 --- /dev/null +++ b/src/v1/tests/entity_files_test.ts @@ -0,0 +1,121 @@ +import { assert, assertEquals } from '@std/assert' +import { afterEach, beforeEach, describe, it } from '@std/testing/bdd' +import { assertSnapshot } from '@std/testing/snapshot' +import axios from 'axios' +import MockAdapter from 'axios-mock-adapter' +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 { buffer } from 'node:stream/consumers' +import { Buffer } from 'jsr:@std/io/buffer' + +const multipartFormDataHeaderMatcher = { + asymmetricMatch: (headers: Record) => { + assertEquals(headers['Content-Type'], 'multipart/form-data') + return true + }, +} + +const createSnapshotBodyMatcher = (t: Deno.TestContext) => ({ + asymmetricMatch: async (reqBody: FormData) => { + const data: Record = Object.fromEntries( + reqBody.entries(), + ) + if (reqBody.has('files[]')) { + // normal serialization overwrites duplicated keys, so we need to handle this case + data['files[]'] = reqBody.getAll('files[]') + } + await assertSnapshot(t, data) + return true + }, +}) + +describe('persons', () => { + let mock: MockAdapter + let affinity: Affinity + + beforeEach(() => { + if (!isLiveRun()) { + mock = new MockAdapter(axios, { onNoMatch: 'throwException' }) + } + affinity = new Affinity(apiKey() || 'api_key') + }) + afterEach(() => { + mock?.reset() + }) + + it('can list all files', async (t) => { + mock?.onGet(entityFilesUrl()).reply( + 200, + await getRawFixture('entity_files/all.raw.response.json'), + ) + const res = await affinity.entityFiles.all() + await assertSnapshot(t, res) + }) + + it('can upload a file', async (t) => { + mock + ?.onPost( + entityFilesUrl(), + createSnapshotBodyMatcher(t), + multipartFormDataHeaderMatcher, + ) + .reply( + 200, + { success: true }, + ) + const res = await affinity.entityFiles.upload({ + person_id: 170614434, + files: [ + new File( + [ + await readFixtureFile('./entity_files/test.pdf'), + ], + 'test.pdf', + ), + ], + }) + assert(res) + }) + + it('can upload multiple files', async (t) => { + mock + ?.onPost( + entityFilesUrl(), + createSnapshotBodyMatcher(t), + multipartFormDataHeaderMatcher, + ) + .reply( + 200, + { success: true }, + ) + const res = await affinity.entityFiles.upload({ + person_id: 170614434, + files: [ + new File( + [await readFixtureFile('./entity_files/test.pdf')], + 'test1.pdf', + ), + new File( + [await readFixtureFile('./entity_files/test.pdf')], + 'test2.pdf', + ), + ], + }) + assert(res) + }) + + it.only('can download a file', async (t) => { + // mock?.onGet(entityFilesUrl(6534776, true)).reply( + // 200, + // await readFixtureFile('./entity_files/test.pdf'), + // ) + const stream = await affinity.entityFiles.download(6534776) + const buf: ArrayBuffer = await buffer(stream) + const pdfContents = await readFixtureFile('./entity_files/test.pdf') + const expected = new Buffer() + expected.read(pdfContents) + assertEquals(new Buffer(buf), expected) + }) +}) diff --git a/src/v1/tests/fixtures/entity_files/all.raw.response.json b/src/v1/tests/fixtures/entity_files/all.raw.response.json new file mode 100644 index 0000000..174e118 --- /dev/null +++ b/src/v1/tests/fixtures/entity_files/all.raw.response.json @@ -0,0 +1,25 @@ +{ + "entity_files": [ + { + "id": 43212, + "name": "JohnDoeFriends.csv", + "size": 993, + "person_id": 142, + "organization_id": null, + "opportunity_id": null, + "created_at": "2011-01-25T09:59:35.288-08:00", + "uploader_id": 10 + }, + { + "id": 131, + "name": "Import.csv", + "size": 227224, + "person_id": 38654, + "organization_id": null, + "opportunity_id": null, + "created_at": "2019-01-13T12:52:51.539-08:00", + "uploader_id": 101 + } + ], + "next_page_token": null +} diff --git a/src/v1/tests/fixtures/entity_files/test.pdf b/src/v1/tests/fixtures/entity_files/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4be95ce1766b0386295c0bb5b12cd38fdb0104d6 GIT binary patch literal 16374 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 literal 0 HcmV?d00001 diff --git a/src/v1/tests/get_raw_fixture.ts b/src/v1/tests/get_raw_fixture.ts index 8595a15..66bb09f 100644 --- a/src/v1/tests/get_raw_fixture.ts +++ b/src/v1/tests/get_raw_fixture.ts @@ -5,3 +5,7 @@ const __dirname = path.dirname(path.fromFileUrl(import.meta.url)) export async function getRawFixture(filePath: string) { return await Deno.readTextFile(path.join(__dirname, 'fixtures', filePath)) } + +export async function readFixtureFile(filePath: string) { + return await Deno.readFile(path.join(__dirname, 'fixtures', filePath)) +} diff --git a/src/v1/urls.ts b/src/v1/urls.ts index 021afa6..fb23fc5 100644 --- a/src/v1/urls.ts +++ b/src/v1/urls.ts @@ -80,3 +80,17 @@ export const personsUrl = (person_id?: number | 'fields') => { * See [here](https://api-docs.affinity.co/#get-global-person-fields) for more info. */ export const personFieldsUrl = () => personsUrl('fields') + +/** + * @hidden + * See [here](https://api-docs.affinity.co/#entity-files) for more info. + */ +export const entityFilesUrl = ( + entity_file_id?: number, + is_download: boolean = false, +) => { + return entity_file_id + ? `/entity-files` + (is_download ? '/download' : '') + + `/${encodeURIComponent(entity_file_id)}` + : '/entity-files' +}