Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,62 @@ Or [npm](https://www.npmjs.com/):
npm install @zaiusinc/node-sdk
```

## Migration Guide

### Migrating from v2.x to v3.x

Version 3.0.0 introduces breaking changes related to the removal of the `node-fetch` dependency in favor of Node.js native fetch APIs.

#### Node.js Version Requirement

The minimum Node.js version has been increased:
- **v2.x**: Node.js >= 18.0
- **v3.x**: Node.js >= 22.0

Ensure your environment is running Node.js 22.0 or higher before upgrading.

#### Headers Type Changes

The `Headers` interface is no longer imported from `node-fetch`. Instead, the SDK now uses Node.js native `Headers` type from the global fetch API.

**Before (v2.x):**
```typescript
import { Headers } from 'node-fetch';
import { odp, ODP } from '@zaiusinc/node-sdk';

const response: ODP.ApiV3.HttpResponse<any> = await odp.v3Api.get('...');
const headers: Headers = response.headers; // Headers from node-fetch
```

**After (v3.x):**
```typescript
import { odp, ODP } from '@zaiusinc/node-sdk';

const response: ODP.ApiV3.HttpResponse<any> = await odp.v3Api.get('...');
const headers: Headers = response.headers; // Native Node.js Headers (global)
```

The native `Headers` type is available globally in Node.js 18+ and provides the same standard [Fetch API Headers interface](https://developer.mozilla.org/en-US/docs/Web/API/Headers).

#### Dependency Changes

The `node-fetch` package has been completely removed. If your code was directly importing or using `node-fetch` alongside this SDK, you can:

1. Remove `node-fetch` and `@types/node-fetch` from your dependencies if they're no longer needed
2. Use Node.js native `fetch()` which is available globally in Node.js 18+
3. Use the native `Headers`, `Request`, and `Response` types from the global scope

This is not mandatory, but that'll help get rid of one more dependency from the package.

**Example:**
```typescript
// No need to import fetch or Headers anymore
const response = await fetch('https://api.example.com/data');
const headers = new Headers({
'Content-Type': 'application/json'
});
```

## Usage

To communicate with ODP, you need to obtain and configure an instance of `ODPClient`.
Expand Down
27 changes: 0 additions & 27 deletions jest.config.js

This file was deleted.

26 changes: 7 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zaiusinc/node-sdk",
"version": "2.0.0",
"version": "3.0.0",
"description": "Node SDK for Optimizely Data Platform",
"repository": "https://github.com/ZaiusInc/node-sdk",
"license": "Apache-2.0",
Expand All @@ -26,42 +26,30 @@
"build-watch": "tsc -w",
"lint": "npx eslint src",
"prettier": "prettier --write 'src/**/*.ts'",
"test": "jest --coverage",
"test": "vitest run --coverage",
"test:watch": "vitest",
"prepare-readme-export": "node scripts/prepare_readme_export.js",
"generate-docs": "typedoc && yarn prepare-readme-export"
},
"engines": {
"node": ">=18.0"
},
"dependencies": {
"node-fetch": "^2.7.0"
"node": ">=22.0"
},
"devDependencies": {
"@eslint/compat": "^1.2.7",
"@stylistic/eslint-plugin": "^4.2.0",
"@types/deep-freeze": "^0.1.5",
"@types/jest": "^29.5.14",
"@types/nock": "^11.1.0",
"@types/node": "^22.13.9",
"@types/node-fetch": "^2.6.12",
"@zaiusinc/eslint-config-presets": "2.0.0",
"@vitest/coverage-v8": "^2.1.8",
"@zaiusinc/eslint-config-presets": "^3.1.0",
"deep-freeze": "0.0.1",
"eslint": "^9.21.0",
"eslint-config-love": "^119.0.0",
"eslint-plugin-jsdoc": "^50.6.3",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-react": "^7.37.4",
"jest": "^29.7.0",
"nock": "^14.0.1",
"prettier": "^3.5.3",
"slugify": "^1.6.6",
"ts-jest": "^29.2.6",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typedoc": "^0.28.4",
"typedoc-plugin-frontmatter": "^1.3.0",
"typedoc-plugin-markdown": "^4.6.3",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1"
"vitest": "^2.1.8"
}
}
33 changes: 17 additions & 16 deletions src/Api/Customers/customer.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import {ApiV3} from '../lib/ApiV3';
import {MockInstance, vi} from 'vitest';
import {CustomerPayload} from '../Types';
import {customer} from './customer';
import {InternalConfig} from '../config/configure';

describe('customer', () => {
const mockConfiguration: InternalConfig = {
apiBasePath: 'https://api.zaius.com/v3/',
apiKey: 'api-key'
apiKey: 'api-key',
};
const apiV3: ApiV3.API = new ApiV3.API(mockConfiguration);
let postMock!: jest.SpyInstance;
let postMock!: MockInstance;
beforeEach(() => {
postMock = jest.spyOn(apiV3, 'post').mockReturnValue(Promise.resolve({} as any));
postMock = vi.spyOn(apiV3, 'post').mockReturnValue(Promise.resolve({} as any));
});

it('sends a transformed post with one customer to /profiles', async () => {
Expand All @@ -25,12 +26,12 @@ describe('customer', () => {
const payload: CustomerPayload[] = [
{identifiers: {email: 'test1@optimizely.com'}, attributes: {name: 'Jim Bob'}},
{identifiers: {email: 'test2@optimizely.com'}, attributes: {name: 'Bob Joe'}},
{identifiers: {email: 'test3@optimizely.com'}, attributes: {name: 'Joe Jim'}}
{identifiers: {email: 'test3@optimizely.com'}, attributes: {name: 'Joe Jim'}},
];
const transformedPayload = [
{attributes: {name: 'Jim Bob', email: 'test1@optimizely.com'}},
{attributes: {name: 'Bob Joe', email: 'test2@optimizely.com'}},
{attributes: {name: 'Joe Jim', email: 'test3@optimizely.com'}}
{attributes: {name: 'Joe Jim', email: 'test3@optimizely.com'}},
];
await customer(apiV3, payload);
expect(postMock).toHaveBeenCalledWith('/profiles', transformedPayload);
Expand All @@ -39,13 +40,13 @@ describe('customer', () => {
it('sanitizes the payload', async () => {
const payload: CustomerPayload = {
identifiers: {
email: 'test@optimizely.com'
email: 'test@optimizely.com',
},
attributes: {
name: 'Jim Bob',
blank: ' ',
nullValue: null
}
nullValue: null,
},
};
const transformedPayload = {attributes: {name: 'Jim Bob', email: 'test@optimizely.com'}};
await customer(apiV3, payload);
Expand All @@ -56,12 +57,12 @@ describe('customer', () => {
const payload: CustomerPayload[] = [
{identifiers: {email: 'test1@optimizely.com'}, attributes: {name: 'Jim Bob', blank: ' ', nullValue: null}},
{identifiers: {email: 'test2@optimizely.com'}, attributes: {name: 'Bob Joe', blank: ' ', nullValue: null}},
{identifiers: {email: 'test3@optimizely.com'}, attributes: {name: 'Joe Jim', blank: ' ', nullValue: null}}
{identifiers: {email: 'test3@optimizely.com'}, attributes: {name: 'Joe Jim', blank: ' ', nullValue: null}},
];
const transformedPayload = [
{attributes: {name: 'Jim Bob', email: 'test1@optimizely.com'}},
{attributes: {name: 'Bob Joe', email: 'test2@optimizely.com'}},
{attributes: {name: 'Joe Jim', email: 'test3@optimizely.com'}}
{attributes: {name: 'Joe Jim', email: 'test3@optimizely.com'}},
];
await customer(apiV3, payload);
expect(postMock).toHaveBeenCalledWith('/profiles', transformedPayload);
Expand All @@ -70,16 +71,16 @@ describe('customer', () => {
it('applies PayloadOptions', async () => {
const payload: CustomerPayload = {
identifiers: {
email: 'test@optimizely.com'
email: 'test@optimizely.com',
},
attributes: {
name: 'Jim Bob',
blank: ' ',
nullValue: null
}
nullValue: null,
},
};
const transformedPayload = {
attributes: {name: 'Jim Bob', email: 'test@optimizely.com', blank: null, nullValue: null}
attributes: {name: 'Jim Bob', email: 'test@optimizely.com', blank: null, nullValue: null},
};
await customer(apiV3, payload, {excludeNulls: false});
expect(postMock).toHaveBeenCalledWith('/profiles', transformedPayload);
Expand All @@ -89,12 +90,12 @@ describe('customer', () => {
const payload: CustomerPayload[] = [
{identifiers: {email: 'test1@optimizely.com'}, attributes: {name: 'Jim Bob', blank: ' ', nullValue: null}},
{identifiers: {email: 'test2@optimizely.com'}, attributes: {name: 'Bob Joe', blank: ' ', nullValue: null}},
{identifiers: {email: 'test3@optimizely.com'}, attributes: {name: 'Joe Jim', blank: ' ', nullValue: null}}
{identifiers: {email: 'test3@optimizely.com'}, attributes: {name: 'Joe Jim', blank: ' ', nullValue: null}},
];
const transformedPayload = [
{attributes: {name: 'Jim Bob', email: 'test1@optimizely.com', blank: ' '}},
{attributes: {name: 'Bob Joe', email: 'test2@optimizely.com', blank: ' '}},
{attributes: {name: 'Joe Jim', email: 'test3@optimizely.com', blank: ' '}}
{attributes: {name: 'Joe Jim', email: 'test3@optimizely.com', blank: ' '}},
];
await customer(apiV3, payload, {trimToNull: false});
expect(postMock).toHaveBeenCalledWith('/profiles', transformedPayload);
Expand Down
6 changes: 3 additions & 3 deletions src/Api/Events/event.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import deepFreeze from 'deep-freeze';
import 'jest';
import {MockInstance, vi} from 'vitest';
import {ApiV3} from '../lib/ApiV3';
import {EventPayload} from '../Types';
import {event} from './event';
Expand All @@ -11,9 +11,9 @@ describe('event', () => {
apiKey: 'api-key',
};
const apiV3: ApiV3.API = new ApiV3.API(mockConfiguration);
let postMock!: jest.SpyInstance;
let postMock!: MockInstance;
beforeEach(() => {
postMock = jest.spyOn(apiV3, 'post').mockReturnValue(Promise.resolve({} as any));
postMock = vi.spyOn(apiV3, 'post').mockReturnValue(Promise.resolve({} as any));
});

it('sends a post to /events', async () => {
Expand Down
25 changes: 14 additions & 11 deletions src/Api/GraphQL/graphql.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
import {ApiV3} from '../lib/ApiV3';
import {MockInstance, vi} from 'vitest';
import {graphql} from './graphql';
import {InternalConfig} from '../config/configure';

describe('graphql', () => {
const mockConfiguration: InternalConfig = {
apiBasePath: 'https://api.zaius.com/v3/',
apiKey: 'api-key'
apiKey: 'api-key',
};
const apiV3: ApiV3.API = new ApiV3.API(mockConfiguration);
let postMock!: jest.SpyInstance;
let postMock: MockInstance;
beforeEach(() => {
postMock = jest.spyOn(apiV3, 'post').mockReturnValue(Promise.resolve({} as any));
postMock = vi.spyOn(apiV3, 'post').mockReturnValue(Promise.resolve({} as any));
});

it('sends a query to /graphql', async () => {
const query = '{}';
postMock.mockReturnValue({
success: true,
data: {
data: 'result data'
}
});
postMock.mockReturnValue(
Promise.resolve({
success: true,
data: {
data: 'result data',
},
}) as any,
);
const result = await graphql(apiV3, query);
expect(postMock).toHaveBeenCalledWith('/graphql', {query});
expect(result).toEqual({
success: true,
data: 'result data'
data: 'result data',
});
});

it('sends a query with variables to /graphql', async () => {
const query = '{}';
const variables = {};
postMock.mockReturnValue({ data: { data: 'result data' } });
postMock.mockReturnValue(Promise.resolve({data: {data: 'result data'}}) as any);
await graphql(apiV3, query, variables);
expect(postMock).toHaveBeenCalledWith('/graphql', {query, variables});
});
Expand Down
13 changes: 7 additions & 6 deletions src/Api/IdentifierApi.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IdentifierApi } from './IdentifierApi';
import { vi } from 'vitest';
import { ODPClient } from './index';
import { ConsentUpdate, IdentifierMetadata, ReachabilityUpdate } from './Types';
import * as metadata from './Identifiers/identifiers';
Expand All @@ -16,7 +17,7 @@ describe('IdentifierApi', () => {

it('should get consent',
async () => {
jest.spyOn(consent, 'getConsent').mockReturnValue(Promise.resolve({} as any));
vi.spyOn(consent, 'getConsent').mockReturnValue(Promise.resolve({} as any));

const identifierName = 'testName';
const identifierValue = 'testValue';
Expand All @@ -25,7 +26,7 @@ describe('IdentifierApi', () => {
});

it('should get metadata', async () => {
jest.spyOn(metadata, 'getMetadata').mockReturnValue(Promise.resolve({} as any));
vi.spyOn(metadata, 'getMetadata').mockReturnValue(Promise.resolve({} as any));

const identifierFieldName = 'testField';
const identifierValue = 'testValue';
Expand All @@ -34,7 +35,7 @@ describe('IdentifierApi', () => {
});

it('should get reachability', async () => {
jest.spyOn(reachability, 'getReachability').mockReturnValue(Promise.resolve({} as any));
vi.spyOn(reachability, 'getReachability').mockReturnValue(Promise.resolve({} as any));

const identifierName = 'testName';
const value = 'testValue';
Expand All @@ -43,7 +44,7 @@ describe('IdentifierApi', () => {
});

it('should update consent', async () => {
jest.spyOn(consent, 'updateConsent').mockReturnValue(Promise.resolve({} as any));
vi.spyOn(consent, 'updateConsent').mockReturnValue(Promise.resolve({} as any));

const updates: ConsentUpdate = {
identifier_field_name: '',
Expand All @@ -55,7 +56,7 @@ describe('IdentifierApi', () => {
});

it('should update metadata', async () => {
jest.spyOn(metadata, 'updateMetadata').mockReturnValue(Promise.resolve({} as any));
vi.spyOn(metadata, 'updateMetadata').mockReturnValue(Promise.resolve({} as any));

const updates: IdentifierMetadata = {
identifier_field_name: '',
Expand All @@ -67,7 +68,7 @@ describe('IdentifierApi', () => {
});

it('should update reachability', async () => {
jest.spyOn(reachability, 'updateReachability').mockReturnValue(Promise.resolve({} as any));
vi.spyOn(reachability, 'updateReachability').mockReturnValue(Promise.resolve({} as any));

const updates: ReachabilityUpdate = {
identifier_field_name: '',
Expand Down
Loading