plero-addepar
v0.1.0
Published
NestJS client package for calling Addepar APIs from plero_v3.
Downloads
89
Readme
plero-addepar
NestJS client package for calling Addepar APIs from plero_v3.
The package provides:
- A NestJS dynamic module.
- API key/secret Basic Auth and OAuth bearer-token support.
- Addepar JSON:API headers and
Addepar-Firmhandling. - Generic HTTP and JSON:API resource helpers.
- Named helpers for the major Addepar docs areas.
Install Locally
Build the package tarball from this repo:
pnpm packInstall it in plero_v3:
pnpm add /Users/ismailawa/addepar/plero-addepar-0.1.0.tgzImport from:
import { AddeparModule, AddeparService } from 'plero-addepar';NestJS Setup
import { Module } from '@nestjs/common';
import { AddeparModule } from 'plero-addepar';
@Module({
imports: [
AddeparModule.forRoot({
baseUrl: process.env.ADDEPAR_BASE_URL,
apiKey: process.env.ADDEPAR_API_KEY,
apiSecret: process.env.ADDEPAR_API_SECRET,
firmId: process.env.ADDEPAR_FIRM_ID,
timeoutMs: 30_000,
retryCount: 1,
}),
],
})
export class AppModule {}Async configuration:
AddeparModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
baseUrl: config.getOrThrow('ADDEPAR_BASE_URL'),
apiKey: config.get('ADDEPAR_API_KEY'),
apiSecret: config.get('ADDEPAR_API_SECRET'),
accessToken: config.get('ADDEPAR_ACCESS_TOKEN'),
firmId: config.getOrThrow('ADDEPAR_FIRM_ID'),
}),
});Configuration
baseUrl should include the Addepar API base path where appropriate:
https://firmdomain.addepar.com/api/https://api.addepar.com/https://firmdomain.clientdev.addepar.com/api/https://partnername.sandbox.addepar.com/api/
Supported options:
| Option | Env fallback | Description |
| --- | --- | --- |
| baseUrl | ADDEPAR_BASE_URL | Addepar API base URL. Required. |
| firmId | ADDEPAR_FIRM_ID | Value sent in Addepar-Firm. Required. |
| apiKey | ADDEPAR_API_KEY | Basic Auth username/API key. |
| apiSecret | ADDEPAR_API_SECRET | Basic Auth password/API secret. |
| accessToken | ADDEPAR_ACCESS_TOKEN | OAuth bearer token. Used instead of Basic Auth when present. |
| oauthClientId | ADDEPAR_OAUTH_CLIENT_ID | OAuth client ID. |
| oauthClientSecret | ADDEPAR_OAUTH_CLIENT_SECRET | OAuth client secret. |
| oauthRedirectUri | ADDEPAR_OAUTH_REDIRECT_URI | OAuth redirect URI. |
| oauthAuthorizeUrl | ADDEPAR_OAUTH_AUTHORIZE_URL | Optional custom authorization URL. |
| oauthTokenUrl | ADDEPAR_OAUTH_TOKEN_URL | Optional custom token URL. |
| timeoutMs | none | Request timeout. Defaults to 30000. |
| retryCount | none | Retries for network, timeout, 429, and 5xx. Defaults to 0. |
| retryDelayMs | none | Initial exponential backoff delay. Defaults to 250. |
| maxRetryDelayMs | none | Maximum retry delay. Defaults to 30000. |
| defaultHeaders | none | Headers added to every request. |
At least one auth mode is required:
apiKeyandapiSecret- or
accessToken
Auth
Basic Auth:
AddeparModule.forRoot({
baseUrl: 'https://firmdomain.addepar.com/api',
firmId: '1',
apiKey: 'key',
apiSecret: 'secret',
});Bearer Auth:
AddeparModule.forRoot({
baseUrl: 'https://api.addepar.com',
firmId: '1',
accessToken: 'oauth-access-token',
});OAuth authorization URL:
const url = addepar.getOAuthAuthorizationUrl({
scope: ['PORTFOLIO', 'USERS_WRITE'],
state: 'csrf-token',
});OAuth token exchange:
const token = await addepar.exchangeAuthorizationCode({
code: 'authorization-code',
});
const refreshed = await addepar.refreshAccessToken({
refreshToken: token.refresh_token!,
});Generic HTTP Client
Use these for endpoints that do not need a named helper yet:
await addepar.request<T>({
method: 'GET',
path: '/v1/entities',
query: { 'page[limit]': 100 },
});
await addepar.get<T>('/v1/entities/123');
await addepar.post<T>('/v1/jobs', body);
await addepar.put<T>('/v1/fee_schedules/1', body);
await addepar.patch<T>('/v1/entities/123', body);
await addepar.delete<void>('/v1/entities/123');Each response is:
{
data: T;
metadata: {
status: number;
statusText: string;
headers: Headers;
location?: string;
retryAfterSeconds?: number;
};
}JSON:API Helpers
import { addeparDocument, addeparResource } from 'plero-addepar';
const body = addeparDocument(
addeparResource('entities', {
original_name: 'New entity',
model_type: 'PERSON_NODE',
}),
);Generic resource helpers:
await addepar.listResources('/v1/groups');
await addepar.getResource('/v1/groups', '100');
await addepar.createResource('/v1/groups', 'groups', { name: 'Smith Family' });
await addepar.updateResource('/v1/groups', 'groups', '100', { name: 'Updated' });
await addepar.deleteResource('/v1/groups', '100');
await addepar.listAllPages('/v1/groups', { query: { 'page[limit]': 100 } });Relationship helpers:
await addepar.getRelationship('/v1/teams', '2', 'members');
await addepar.addRelationship('/v1/teams', '2', 'members', [
{ type: 'users', id: '80' },
]);
await addepar.replaceRelationship('/v1/teams', '2', 'members', [
{ type: 'users', id: '80' },
]);
await addepar.removeRelationship('/v1/teams', '2', 'members', [
{ type: 'users', id: '80' },
]);Entities
await addepar.listEntities({ 'page[limit]': 100 });
await addepar.listAllEntities();
await addepar.getEntity('1111');
await addepar.createEntity({
original_name: 'New entity',
model_type: 'PERSON_NODE',
currency_factor: 'USD',
});
await addepar.createEntities([{ original_name: 'One' }, { original_name: 'Two' }]);
await addepar.updateEntity('1111', { original_name: 'Updated entity' });
await addepar.updateEntities([
{ id: '1111', attributes: { original_name: 'Updated entity' } },
]);
await addepar.deleteEntity('1111');
await addepar.deleteEntities(['1111', '2222']);Attributes And Arguments
await addepar.listAttributes();
await addepar.getAttribute('value');
await addepar.queryAttributes({ data: { type: 'attribute_query', attributes: {} } });
await addepar.getAttributeArguments('value');
await addepar.listArguments();
await addepar.getArgument('time_point');Portfolio
await addepar.listPortfolioViews();
await addepar.getPortfolioView('2');
await addepar.getPortfolioViewResults('2', {
portfolio_type: 'entity',
portfolio_id: 22,
output_type: 'json',
start_date: '2020-01-01',
end_date: '2020-12-31',
});
await addepar.queryPortfolio({
columns: [{ key: 'value' }],
groupings: [{ key: 'asset_class' }],
portfolio_type: 'ENTITY',
portfolio_id: [22],
start_date: '2020-02-18',
end_date: '2020-03-18',
});Jobs
await addepar.createPortfolioViewJob({
view_id: '2',
portfolio_type: 'entity',
portfolio_id: '22',
output_type: 'json',
start_date: '2011-12-31',
end_date: '2013-01-15',
});
await addepar.createPortfolioQueryJob({
columns: [{ key: 'value' }],
groupings: [{ key: 'asset_class' }],
portfolio_type: 'entity',
portfolio_id: [22],
start_date: '2020-02-18',
end_date: '2020-03-18',
});
await addepar.listJobs();
await addepar.listAllJobs();
await addepar.getJob('job-id');
await addepar.downloadJob('job-id');
await addepar.deleteJob('job-id');Transaction jobs:
await addepar.createTransactionViewJob({
view_id: 5,
portfolio_id: 193,
portfolio_type: 'entity',
output_type: 'xlsx',
start_date: '2024-01-01',
end_date: '2024-06-30',
});
await addepar.createTransactionJob('transaction_view_results', params);
await addepar.listTransactionJobs();
await addepar.getTransactionJob('job-id');
await addepar.downloadTransactionJob('job-id');
await addepar.deleteTransactionJob('job-id');Ownership Graph
Groups:
await addepar.listGroups();
await addepar.listAllGroups();
await addepar.getGroup('100');
await addepar.queryGroups({ data: { type: 'groups_query', attributes: {} } });
await addepar.createGroup({ name: 'Smith Family' });
await addepar.updateGroup('100', { name: 'Smith Family Updated' });
await addepar.deleteGroup('100');
await addepar.getGroupMembers('100');
await addepar.getGroupChildGroups('100');
await addepar.addGroupMembers('100', [{ type: 'entities', id: '200' }]);
await addepar.replaceGroupMembers('100', [{ type: 'entities', id: '200' }]);
await addepar.removeGroupMembers('100', [{ type: 'entities', id: '200' }]);
await addepar.addGroupChildGroups('100', [{ type: 'groups', id: '101' }]);
await addepar.replaceGroupChildGroups('100', [{ type: 'groups', id: '101' }]);Positions:
await addepar.listPositions();
await addepar.listAllPositions();
await addepar.getPosition('123');
await addepar.createPosition(
{ name: 'GOOG' },
{
owner: { data: { type: 'entities', id: '10' } },
owned: { data: { type: 'entities', id: '11' } },
},
);
await addepar.updatePosition('123', { name: 'GOOG Updated' });
await addepar.deletePosition('123');
await addepar.getPositionOwner('123');
await addepar.getPositionOwned('123');External IDs:
await addepar.listExternalIds();
await addepar.getExternalId('1');
await addepar.createExternalId({ external_id: 'crm-123' });
await addepar.updateExternalId('1', { external_id: 'crm-456' });
await addepar.deleteExternalId('1');Transactions And Snapshots
Transactions:
await addepar.listTransactions();
await addepar.getTransaction('1000');
await addepar.createTransaction(
{
type: 'buy',
currency: 'USD',
trade_date: '2020-09-25',
units: 24.48,
},
{
owner: { data: { type: 'entities', id: '2138776' } },
owned: { data: { type: 'entities', id: '2363267' } },
},
);
await addepar.createTransactions([{ attributes: { type: 'buy', currency: 'USD' } }]);
await addepar.updateTransaction('1000', { comment: 'Updated' });
await addepar.updateTransactions([
{ id: '1000', attributes: { comment: 'Updated' } },
]);
await addepar.deleteTransaction('1000');
await addepar.deleteTransactions(['1000', '1001']);
await addepar.getTransactionOwner('1000');
await addepar.getTransactionOwned('1000');
await addepar.getTransactionCashPosition('1000');Snapshots:
await addepar.listSnapshots();
await addepar.getSnapshot('37483253353524');
await addepar.createSnapshot({ type: 'snapshot', currency: 'USD', trade_date: '2023-01-25' });
await addepar.updateSnapshot('37483253353524', { comment: 'Updated' });
await addepar.deleteSnapshot('37483253353524');Historical prices:
await addepar.listHistoricalPrices('60');
await addepar.saveHistoricalPrices('60', [
{ date: '2012-06-29', nodeId: 60, value: 101.0 },
]);
await addepar.deleteHistoricalPrice('60', '2012-06-29');Files And Portal
await addepar.listFiles();
await addepar.listAllFiles();
await addepar.getFile('37');
await addepar.downloadFile('37');
await addepar.getArchivedFile('37');
await addepar.downloadArchivedFile('37');
await addepar.createFile({ name: 'Sample File.txt' });
await addepar.updateFile('37', { name: 'Renamed File.txt' });
await addepar.deleteFile('37');
await addepar.addFileAssociatedGroups('37', [{ type: 'groups', id: '20' }]);
await addepar.replaceFileAssociatedGroups('37', [{ type: 'groups', id: '20' }]);
await addepar.removeFileAssociatedGroups('37', [{ type: 'groups', id: '20' }]);
await addepar.addFileAssociatedEntities('37', [{ type: 'entities', id: '100' }]);
await addepar.replaceFileAssociatedEntities('37', [{ type: 'entities', id: '100' }]);
await addepar.removeFileAssociatedEntities('37', [{ type: 'entities', id: '100' }]);
await addepar.publishFilesToPortal({
files_id: [37, 38],
portal_publishing: 'PUBLISH',
contact_notification: 'DO_NOT_NOTIFY',
});Users, Roles, Teams, Contacts
Users:
await addepar.listUsers();
await addepar.listAllUsers();
await addepar.getUser('2000');
await addepar.getCurrentUser();
await addepar.createUser({ email: '[email protected]', first_name: 'Ada' });
await addepar.updateUser('2000', { first_name: 'Ada' });
await addepar.deleteUser('2000');
await addepar.queryUsersByEmail('[email protected]');
await addepar.queryUsersByExternalUserId('employee-123');
await addepar.replaceUserAssignedRole('2000', { type: 'roles', id: '80' });
await addepar.addUserPermissionedEntities('2000', [{ type: 'entities', id: '10000' }]);
await addepar.addUserPermissionedGroups('2000', [{ type: 'groups', id: '20000' }]);Roles:
await addepar.listRoles();
await addepar.getRole('80');
await addepar.getRoleAssignedUsers('80');
await addepar.addRoleAssignedUsers('80', [{ type: 'users', id: '2000' }]);
await addepar.replaceRoleAssignedUsers('80', [{ type: 'users', id: '2000' }]);
await addepar.removeRoleAssignedUsers('80', [{ type: 'users', id: '2000' }]);Teams:
await addepar.listTeams();
await addepar.listAllTeams();
await addepar.getTeam('2');
await addepar.createTeam({ name: 'Advisor Team' });
await addepar.updateTeam('2', { name: 'Updated Team' });
await addepar.deleteTeam('2');
await addepar.getTeamMembers('2');
await addepar.addTeamMembers('2', [{ type: 'users', id: '80' }]);
await addepar.replaceTeamMembers('2', [{ type: 'users', id: '80' }]);
await addepar.removeTeamMembers('2', [{ type: 'users', id: '80' }]);Contacts:
await addepar.listContacts();
await addepar.listAllContacts();
await addepar.getContact('1');
await addepar.createContact({ first_name: 'Adam', last_name: 'Smith' });
await addepar.updateContact('1', { first_name: 'Adam' });
await addepar.deleteContact('1');
await addepar.inviteContact('1');
await addepar.restoreContact('1');
await addepar.revokeContact('1');
await addepar.exemptContactFromTwoFactor('1');
await addepar.requireContactTwoFactor('1');
await addepar.enableContactSaml('1');
await addepar.disableContactSaml('1');View sets:
await addepar.listViewSets();
await addepar.getViewSet('2000');Reports And Audit
Reports:
await addepar.listReports({ 'page[limit]': 100 });
await addepar.listAllReports();
await addepar.createReportGenerationJob({
report_id: 4,
portfolios: [{ portfolio_type: 'entity', portfolio_id: '22' }],
start_date: '2023-07-01',
end_date: '2023-08-01',
});
await addepar.listGeneratedReports();
await addepar.getGeneratedReport('job-id');
await addepar.getGeneratedReportCreator('job-id');
await addepar.getGeneratedReportZippedFile('job-id');
await addepar.downloadGeneratedReportZippedFile('job-id');Audit:
await addepar.queryAuditTrail({
object_type: 'login_attempt',
start_date: '2021-03-26',
end_date: '2021-03-30',
});
await addepar.getAuditTrailEntry('entry-id');Billing
await addepar.listFees();
await addepar.getFee('1');
await addepar.createFee({ name: 'Management Fee' });
await addepar.updateFee('1', { name: 'Updated Fee' });
await addepar.deleteFee('1');
await addepar.listFeeSchedules();
await addepar.getFeeSchedule('1');
await addepar.createFeeSchedule({
name: 'Quarterly Fee Schedule',
currency: 'USD',
interval: 'QUARTERLY',
});
await addepar.updateFeeSchedule('1', { name: 'Updated Schedule' });
await addepar.deleteFeeSchedule('1');
await addepar.listBillablePortfolios();
await addepar.getBillablePortfolio('1');Benchmarks And Target Allocations
Benchmarks:
await addepar.listBenchmarks();
await addepar.getBenchmark('524');
await addepar.createBenchmark({ benchmark_type: 'fixed_return', fixed_return: 0.15 });
await addepar.updateBenchmark('524', { display_name: 'Updated Benchmark' });
await addepar.deleteBenchmark('524');Target allocations:
await addepar.listAllocationModels();
await addepar.getAllocationModel('123');
await addepar.createAllocationModel({
name: 'Balanced Growth Model',
attribute_ids: ['asset_class', 'sector'],
});
await addepar.updateAllocationModel('123', { name: 'Updated Model' });
await addepar.deleteAllocationModel('123');
await addepar.listAllocationTemplates();
await addepar.getAllocationTemplate('123');
await addepar.createAllocationTemplate({ name: 'Template' });
await addepar.updateAllocationTemplate('123', { name: 'Updated Template' });
await addepar.deleteAllocationTemplate('123');Errors
The package throws structured errors:
import {
AddeparConfigError,
AddeparHttpError,
AddeparNetworkError,
AddeparTimeoutError,
} from 'plero-addepar';AddeparConfigError: missing or invalid package configuration.AddeparHttpError: non-2xx Addepar response. Includesstatus,statusText,responseBody, andretryAfterSeconds.AddeparNetworkError: fetch/network failure.AddeparTimeoutError: request exceededtimeoutMs.
Retry Behavior
Retries are attempted for:
- network errors
- timeout errors
- HTTP
429 - HTTP
5xx
X-RateLimit-Retry-After and Retry-After are respected when present.
Development
pnpm install
pnpm typecheck
pnpm test --runInBand
pnpm lint
pnpm build
pnpm pack