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..8a2d250 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", @@ -30,8 +38,10 @@ "@deno/cache-dir@0.8.0": { "integrity": "e87e80a404958f6350d903e6238b72afb92468378b0b32111f7a1e4916ac7fe7", "dependencies": [ + "jsr:@std/fmt@^0.218.2", "jsr:@std/fs@^0.218.2", - "jsr:@std/io@^0.218.2" + "jsr:@std/io@^0.218.2", + "jsr:@std/path@^0.218.2" ] }, "@deno/dnt@0.41.2": { @@ -57,15 +67,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 +99,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 +130,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 +151,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 +1122,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/scripts/build_npm.ts b/scripts/build_npm.ts index fe53d9c..8a65863 100644 --- a/scripts/build_npm.ts +++ b/scripts/build_npm.ts @@ -17,14 +17,15 @@ await build({ declaration: 'separate', outDir: './npm', importMap: 'deno.jsonc', - skipNpmInstall: true, + skipNpmInstall: false, skipSourceOutput: true, // due to: https://github.com/denoland/dnt/issues/254 tests are disabled for now test: false, scriptModule: false, shims: { // see JS docs for overview and more options - deno: true, + deno: false, + undici: true, }, rootTestDir: './src/v1/tests', filterDiagnostic(diagnostic: ts.Diagnostic) { diff --git a/src/v1/entity_files.ts b/src/v1/entity_files.ts new file mode 100644 index 0000000..cb9275a --- /dev/null +++ b/src/v1/entity_files.ts @@ -0,0 +1,229 @@ +import { assert } from '@std/assert' +import type { AxiosInstance } from 'axios' +import type { Readable } from 'node:stream' +import { defaultTransformers } from './axios_default_transformers.ts' +import { createSearchIteratorFn } from './create_search_iterator_fn.ts' +import { EntityRequestFilter } from './field_value_changes.ts' +import type { PagedRequest } from './paged_request.ts' +import type { PagedResponse } from './paged_response.ts' +import type { DateTime, Replace, RequireOnlyOne } from './types.ts' +import { entityFilesUrl } from './urls.ts' +export type { DateTime } from './types.ts' + +export 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: number + /** 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 +} + +export type EntityFile = Replace + +/** + * Represents the request parameters for retrieving entity files. + */ +export 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 + +export type PagedEntityFileResponseRaw = + & { + entity_files: EntityFileRaw[] + } + & PagedResponse + +export type PagedEntityFileResponse = Replace + +export 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 + } + + /** + * Returns an async iterator that yields all entity files matching the given request + * Each yielded array contains up to the number specified in {@link AllEntityFileRequest.page_size} of entity files. + * Use this method if you want to process the entity files in a streaming fashion. + * + * *Please note:* the yielded entity files array may be empty on the last page. + * + * @example + * ```typescript + * let page = 0 + * for await (const entries of affinity.entityFiles.pagedIterator({ + * person_id: 123, + * page_size: 10 + * })) { + * console.log(`Page ${++page} of entries:`, entries) + * } + * ``` + */ + pagedIterator = createSearchIteratorFn( + this.all.bind(this), + 'entity_files', + ) + + /** + * 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[`entityFiles > can get a single file 1`] = ` +{ + 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, +} +`; + +snapshot[`entityFiles > can upload a file 1`] = ` +{ + file: File { + name: "test.pdf", + size: 16374, + type: "", + }, + person_id: "170614434", +} +`; + +snapshot[`entityFiles > can upload multiple files 1`] = ` +{ + "files[]": [ + File { + name: "test1.pdf", + size: 16374, + type: "", + }, + File { + name: "test2.pdf", + size: 16374, + type: "", + }, + ], + person_id: "170614434", +} +`; + +snapshot[`page 1 of entity files 1`] = ` +[ + { + 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, + }, +] +`; + +snapshot[`page 2 of entity files 1`] = ` +[ + { + created_at: 2019-01-13T20:52:51.539Z, + id: 131, + name: "Import.csv", + opportunity_id: null, + organization_id: null, + person_id: 142, + size: 227224, + uploader_id: 101, + }, +] +`; + +snapshot[`page 3 of entity files 1`] = `[]`; diff --git a/src/v1/tests/entity_files_test.ts b/src/v1/tests/entity_files_test.ts new file mode 100644 index 0000000..3a1ff25 --- /dev/null +++ b/src/v1/tests/entity_files_test.ts @@ -0,0 +1,192 @@ +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 { Buffer } from 'jsr:@std/io/buffer' +import { Readable } from 'node:stream' +import { buffer } from 'node:stream/consumers' +import { AllEntityFileRequest } from '../entity_files.ts' +import { Affinity } from '../index.ts' +import { entityFilesUrl } from '../urls.ts' +import { apiKey, isLiveRun } from './env.ts' +import { getRawFixture, readFixtureFile } from './get_raw_fixture.ts' + +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('entityFiles', () => { + 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({ + person_id: 170614434, + }) + await assertSnapshot(t, res) + }) + + it('can get a single file', async (t) => { + mock?.onGet(entityFilesUrl(131)).reply( + 200, + await getRawFixture('entity_files/get.raw.response.json'), + ) + const res = await affinity.entityFiles.get(131) + 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('can download a file', async (t) => { + const pdfContents = await readFixtureFile('./entity_files/test.pdf') + + mock?.onGet(entityFilesUrl(6534776, true)).reply( + 200, + readableFromArray(pdfContents), + ) + + const stream = await affinity.entityFiles.download(6534776) + const buf: ArrayBuffer = await buffer(stream) + + const expected = new Buffer() + expected.read(pdfContents) + assertEquals(new Buffer(buf), expected) + }) + + it('iterates over all entity files', async (t) => { + const params: AllEntityFileRequest = { + person_id: 142, + page_size: 1, + } + + { + // set up pages sequentially, each referencing the one after + const { default: pages } = await import( + './fixtures/entity_files/paginated.iterator.combined.response.json', + { + with: { + type: 'json', + }, + } + ) + + pages.forEach((page, i) => { + const { next_page_token: previous_page_token } = pages[i - 1] || + {} + const data: AllEntityFileRequest = { + ...params, + } + if (previous_page_token) { + data.page_token = previous_page_token + } + // console.log('Setting up page', params, page.list_entries) + mock?.onGet(entityFilesUrl(), { + params: data, + }).reply( + 200, + page, + ) + }) + } + + let page = 0 + for await ( + const entries of affinity.entityFiles.pagedIterator(params) + ) { + await assertSnapshot(t, entries, { + name: `page ${++page} of entity files`, + }) + } + }) +}) + +function readableFromArray(arr: Uint8Array) { + return new Readable({ + read(size: number) { + this.push(arr) + this.push(null) + }, + }) +} 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/get.raw.response.json b/src/v1/tests/fixtures/entity_files/get.raw.response.json new file mode 100644 index 0000000..2f86d76 --- /dev/null +++ b/src/v1/tests/fixtures/entity_files/get.raw.response.json @@ -0,0 +1,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 +} diff --git a/src/v1/tests/fixtures/entity_files/paginated.iterator.combined.response.json b/src/v1/tests/fixtures/entity_files/paginated.iterator.combined.response.json new file mode 100644 index 0000000..18aecd7 --- /dev/null +++ b/src/v1/tests/fixtures/entity_files/paginated.iterator.combined.response.json @@ -0,0 +1,36 @@ +[ + { + "next_page_token": "B", + "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 + } + ] + }, + { + "next_page_token": "C", + "entity_files": [ + { + "id": 131, + "name": "Import.csv", + "size": 227224, + "person_id": 142, + "organization_id": null, + "opportunity_id": null, + "created_at": "2019-01-13T12:52:51.539-08:00", + "uploader_id": 101 + } + ] + }, + { + "next_page_token": null, + "entity_files": [] + } +] 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 0000000..4be95ce Binary files /dev/null and b/src/v1/tests/fixtures/entity_files/test.pdf differ 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' +} diff --git a/tsconfig.json b/tsconfig.json index f11abde..7e91f44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,9 @@ /* Language and Environment */ "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": [ + "DOM" + ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */