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
10 changes: 8 additions & 2 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2
uses: actions/dependency-review-action@v4
with:
# Moderate CVEs in Lerna 9's transitive build-tooling dependencies
# (dev-only; never shipped in any published @apimatic/* package):
# js-yaml GHSA-h67p-54hq-rp68 (quadratic-complexity DoS in merge keys)
# tar GHSA-vmf3-w455-68vh (long-name header parsing differential)
allow-ghsas: GHSA-h67p-54hq-rp68, GHSA-vmf3-w455-68vh
7 changes: 4 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ["14.x", "16.x", "17.x", "18.x", "19.x"]
# Lerna 9 (used by build/test) requires Node ^20.19 || ^22.12 || >=24.
node: ["20.x", "22.x", "24.x"]
os: [ubuntu-latest]
# os: [ubuntu-latest, windows-latest, macOS-latest]

steps:
- name: Checkout repo
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}

Expand Down
44 changes: 22 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
if: github.ref == 'refs/heads/master' && github.event.inputs.version_type != 'prerelease (unstable or experimental changes 0.0.0-alpha.X)'
runs-on: ubuntu-latest
environment: Production
permissions:
id-token: write # required for npm OIDC trusted publishing
contents: write # required for lerna --create-release github (tags + GitHub release)
steps:
- name: Determine version type
id: version_type
Expand All @@ -43,16 +46,20 @@ jobs:
esac

- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
fetch-depth: 0 # Ensure the full history is fetched to push changes back

- name: Set up Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '19'

node-version: '22' # Lerna 9 requires Node ^20.19 || ^22.12 || >=24
registry-url: 'https://registry.npmjs.org'

- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest # OIDC trusted publishing requires npm >= 11.5.1

- name: Install dependencies and build
run: |
yarn install --frozen-lockfile
Expand All @@ -64,13 +71,6 @@ jobs:
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b ci-version-update
git push origin ci-version-update

- name: Configure NPM registry
run: npm config set registry 'https://registry.npmjs.org/'

- name: Authenticate with NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc

- name: Bump versions and publish packages
id: release
Expand Down Expand Up @@ -107,22 +107,29 @@ jobs:
name: Publish the npm package as an alpha release
runs-on: ubuntu-latest
environment: Production
permissions:
id-token: write # required for npm OIDC trusted publishing
contents: write # required for lerna --create-release github (tags + GitHub release)
steps:
- name: Determine version type
id: version_type
run: echo "version=prerelease" >> $GITHUB_ENV

- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
fetch-depth: 0 # Ensure the full history is fetched to push changes back

- name: Set up Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '19'

node-version: '22' # Lerna 9 requires Node ^20.19 || ^22.12 || >=24
registry-url: 'https://registry.npmjs.org'

- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest # OIDC trusted publishing requires npm >= 11.5.1

- name: Install dependencies and build
run: |
yarn install --frozen-lockfile
Expand All @@ -134,13 +141,6 @@ jobs:
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b ci-version-update
git push origin ci-version-update

- name: Configure NPM registry
run: npm config set registry 'https://registry.npmjs.org/'

- name: Authenticate with NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc

- name: Bump versions and publish packages
id: release
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/sonar-qube-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v1
- uses: actions/setup-node@v4
with:
node-version: "18.x"
node-version: "22.x" # Lerna 9 requires Node ^20.19 || ^22.12 || >=24

- name: Install deps
run: yarn
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ build/Release
node_modules/
jspm_packages/

# Nx cache (Lerna is powered by Nx since v7)
.nx/

# TypeScript v1 declaration files
typings/

Expand Down
1 change: 0 additions & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "independent",
"packages": [
"packages/*"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"jest-watch-typeahead": "^0.6.1",
"jsdom": "^19.0.0",
"jsdom-global": "^3.0.2",
"lerna": "^6.5.1",
Comment thread
hamzamahmood marked this conversation as resolved.
"lerna": "^9.0.7",
"lint-staged": "^12.5.0",
"prettier": "^2.0.2",
"spawn-command": "0.0.2-1",
Expand Down
24 changes: 21 additions & 3 deletions packages/core/src/http/requestBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@
) => (pointer: string | null, setter: (value: any) => any) => this
) => PagedAsyncIterable<TItem, TPagedResponse>
): PagedAsyncIterable<TItem, TPagedResponse>;
callAsEventStream<T>(
createEventStream: (
req: this,
streamReadTimeout: number | undefined
) => Promise<ApiResponse<T>>
): Promise<ApiResponse<T>>;
call(requestOptions?: RequestOptions): Promise<ApiResponse<void>>;
callAsJson<T>(
schema: Schema<T, any>,
Expand Down Expand Up @@ -240,6 +246,7 @@
protected _httpMethod: HttpMethod,
protected _xmlSerializer: XmlSerializerInterface,
protected _retryConfig: RetryConfiguration,
protected _streamReadTimeout: number | undefined,
protected _path?: string,
protected _apiLogger?: ApiLoggerInterface
) {
Expand Down Expand Up @@ -566,6 +573,14 @@
req.updateByJsonPointer.bind(req)
);
}
public callAsEventStream<T>(
createEventStream: (
req: this,
streamReadTimeout: number | undefined
) => Promise<ApiResponse<T>>
): Promise<ApiResponse<T>> {
return createEventStream(this, this._streamReadTimeout);
}
public updateByJsonPointer(
pointer: string | null,
updater: (value: any) => any
Expand Down Expand Up @@ -616,6 +631,7 @@
this._httpMethod,
this._xmlSerializer,
this._retryConfig,
this._streamReadTimeout,
this._path,
this._apiLogger
);
Expand Down Expand Up @@ -868,14 +884,15 @@
}
}

export function createRequestBuilderFactory<BaseUrlParamType, AuthParams>(

Check warning on line 887 in packages/core/src/http/requestBuilder.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Function 'createRequestBuilderFactory' has too many parameters (8). Maximum allowed is 7.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRovr_qP4NW85AYwv&open=AZ9FRovr_qP4NW85AYwv&pullRequest=322
httpClient: HttpClientInterface,
baseUrlProvider: (arg?: BaseUrlParamType) => string,
apiErrorConstructor: ApiErrorConstructor,
authenticationProvider: AuthenticatorInterface<AuthParams>,
retryConfig: RetryConfiguration,
xmlSerializer: XmlSerializerInterface = new XmlSerialization(),
apiLogger?: ApiLoggerInterface
xmlSerializer?: XmlSerializerInterface,
apiLogger?: ApiLoggerInterface,
streamReadTimeout?: number
): RequestBuilderFactory<BaseUrlParamType, AuthParams> {
return (httpMethod, path?) => {
return new DefaultRequestBuilder(
Expand All @@ -884,8 +901,9 @@
apiErrorConstructor,
authenticationProvider,
httpMethod,
xmlSerializer,
xmlSerializer ?? new XmlSerialization(),
retryConfig,
streamReadTimeout,
path,
apiLogger
);
Expand Down
81 changes: 72 additions & 9 deletions packages/core/test/http/requestBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,60 @@
}

describe('test default request builder behavior with succesful responses', () => {
it('callAsEventStream hands the builder to the factory and returns its ApiResponse', async () => {
const reqBuilder = defaultRequestBuilder('/test/requestBuilder?text=true');
reqBuilder.text('testBody');
const response = await reqBuilder.callAsEventStream(async (req) => {
// the builder itself is handed in (like paginate's `req`); the
// factory drives it, including re-issuing for reconnection
expect(req).toBe(reqBuilder);
return req.callAsText();
});
// the hook returns the ApiResponse<T> the factory produced, as-is
expect(response.statusCode).toBe(200);
expect(response.result).toBe('testBody result');
});

it('forwards the constructor-supplied stream read timeout to the factory', async () => {
// Supplied to the factory (like retryConfig), not via a setter.
const reqBuilder = createRequestBuilderFactory<string, boolean>(
mockHttpClientAdapter(),
(server) => mockBaseURIProvider(server),
ApiError,
basicAuth,
retryConfig,
undefined,
undefined,
1234
)('GET', '/test/requestBuilder');
reqBuilder.baseUrl('default');
const response = await reqBuilder.callAsEventStream(
async (req, streamReadTimeout) => {
expect(streamReadTimeout).toBe(1234);
return req.callAsText();
}
);
expect(response.statusCode).toBe(200);
});

it('forwards undefined when no stream read timeout is configured', async () => {
const reqBuilder = createRequestBuilderFactory<string, boolean>(
mockHttpClientAdapter(),
(server) => mockBaseURIProvider(server),
ApiError,
basicAuth,
retryConfig
)('GET', '/test/requestBuilder');
reqBuilder.baseUrl('default');
const response = await reqBuilder.callAsEventStream(
async (req, streamReadTimeout) => {
expect(streamReadTimeout).toBeUndefined();
return req.callAsText();
}
);
expect(response.statusCode).toBe(200);
});

function setupTextRequestTest() {
const expectedRequest: HttpRequest = {
method: 'GET',
Expand Down Expand Up @@ -470,10 +524,10 @@
noContentResponse
);
const nullableString = await reqBuilder.callAsJson(nullable(string()));
expect(nullableString.result).toEqual(null);

Check warning on line 527 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(nullableString.result).toBeNull()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYww&open=AZ9FRowt_qP4NW85AYww&pullRequest=322

const optionalString = await reqBuilder.callAsJson(optional(string()));
expect(optionalString.result).toEqual(undefined);

Check warning on line 530 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(optionalString.result).toBeUndefined()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYwx&open=AZ9FRowt_qP4NW85AYwx&pullRequest=322
});
it('should test response with whitespace content string cases', async () => {
const reqBuilder = defaultRequestBuilder(
Expand All @@ -481,10 +535,10 @@
whitespacedResponse
);
const nullableString = await reqBuilder.callAsJson(nullable(string()));
expect(nullableString.result).toEqual(null);

Check warning on line 538 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(nullableString.result).toBeNull()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYwy&open=AZ9FRowt_qP4NW85AYwy&pullRequest=322

const optionalString = await reqBuilder.callAsJson(optional(string()));
expect(optionalString.result).toEqual(undefined);

Check warning on line 541 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(optionalString.result).toBeUndefined()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYwz&open=AZ9FRowt_qP4NW85AYwz&pullRequest=322
});
it('should test response with no content boolean cases', async () => {
const reqBuilder = defaultRequestBuilder(
Expand All @@ -492,10 +546,10 @@
noContentResponse
);
const nullableString = await reqBuilder.callAsJson(nullable(boolean()));
expect(nullableString.result).toEqual(null);

Check warning on line 549 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(nullableString.result).toBeNull()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw0&open=AZ9FRowt_qP4NW85AYw0&pullRequest=322

const optionalString = await reqBuilder.callAsJson(optional(boolean()));
expect(optionalString.result).toEqual(undefined);

Check warning on line 552 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(optionalString.result).toBeUndefined()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw1&open=AZ9FRowt_qP4NW85AYw1&pullRequest=322
});
it('should test response with whitespace content boolean cases', async () => {
const reqBuilder = defaultRequestBuilder(
Expand All @@ -503,10 +557,10 @@
whitespacedResponse
);
const nullableString = await reqBuilder.callAsJson(nullable(boolean()));
expect(nullableString.result).toEqual(null);

Check warning on line 560 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(nullableString.result).toBeNull()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw2&open=AZ9FRowt_qP4NW85AYw2&pullRequest=322

const optionalString = await reqBuilder.callAsJson(optional(boolean()));
expect(optionalString.result).toEqual(undefined);

Check warning on line 563 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(optionalString.result).toBeUndefined()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw3&open=AZ9FRowt_qP4NW85AYw3&pullRequest=322
});
it('should test response with no content object cases', async () => {
const reqBuilder = defaultRequestBuilder(
Expand All @@ -516,12 +570,12 @@
const nullableString = await reqBuilder.callAsJson(
nullable(employeeSchema)
);
expect(nullableString.result).toEqual(null);

Check warning on line 573 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(nullableString.result).toBeNull()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw4&open=AZ9FRowt_qP4NW85AYw4&pullRequest=322

const optionalString = await reqBuilder.callAsJson(
optional(employeeSchema)
);
expect(optionalString.result).toEqual(undefined);

Check warning on line 578 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(optionalString.result).toBeUndefined()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw5&open=AZ9FRowt_qP4NW85AYw5&pullRequest=322
});
it('should test response with whitespace content object cases', async () => {
const reqBuilder = defaultRequestBuilder(
Expand All @@ -531,12 +585,12 @@
const nullableString = await reqBuilder.callAsJson(
nullable(employeeSchema)
);
expect(nullableString.result).toEqual(null);

Check warning on line 588 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(nullableString.result).toBeNull()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw6&open=AZ9FRowt_qP4NW85AYw6&pullRequest=322

const optionalString = await reqBuilder.callAsJson(
optional(employeeSchema)
);
expect(optionalString.result).toEqual(undefined);

Check warning on line 593 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(optionalString.result).toBeUndefined()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw7&open=AZ9FRowt_qP4NW85AYw7&pullRequest=322
});
it('should test request builder configured with all kind of headers', async () => {
const reqBuilder = defaultRequestBuilder();
Expand Down Expand Up @@ -950,9 +1004,9 @@
'$request.body#/new',
() => fail()
);
expect(
async () => await extractRequestBody(newRequestBuilder)
).rejects.toThrow('Body is undefined');
await expect(extractRequestBody(newRequestBuilder)).rejects.toThrow(
'Body is undefined'
);
});

it('should not add new field as request body plain', async () => {
Expand Down Expand Up @@ -1048,22 +1102,31 @@
);
});

it('should not update body when not set', async () => {
it('should not update body when not set', () => {
const requestBuilder = defaultRequestBuilder();
const updater = jest.fn();

requestBuilder.updateByJsonPointer('$request.body#/user', updater);

requestBuilder.updateByJsonPointer('$request.body#/user', () => fail());
expect(updater).not.toHaveBeenCalled();
});

it('should not update for null json pointer', async () => {
it('should not update for null json pointer', () => {
const requestBuilder = defaultRequestBuilder();
const updater = jest.fn();

requestBuilder.updateByJsonPointer(null, () => fail());
requestBuilder.updateByJsonPointer(null, updater);

expect(updater).not.toHaveBeenCalled();
});

it('should not update for invalid pointer prefix', async () => {
it('should not update for invalid pointer prefix', () => {
const requestBuilder = defaultRequestBuilder();
const updater = jest.fn();

requestBuilder.updateByJsonPointer('$invalid.prefix#/key', updater);

requestBuilder.updateByJsonPointer('$invalid.prefix#/key', () => fail());
expect(updater).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -1114,7 +1177,7 @@
await reqBuilder.callAsText();
} catch (error) {
if (error instanceof ApiError) {
expect(error.result).toBe(undefined);

Check warning on line 1180 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(error.result).toBeUndefined()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw8&open=AZ9FRowt_qP4NW85AYw8&pullRequest=322
expect(error.statusCode).toEqual(400);
expect(error.message).toEqual(
'Single status Error: 400, accept => , body => {"key":"value"}.'
Expand All @@ -1133,7 +1196,7 @@
await reqBuilder.callAsText();
} catch (error) {
if (error instanceof ApiError) {
expect(error.result).toBe(undefined);

Check warning on line 1199 in packages/core/test/http/requestBuilder.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(error.result).toBeUndefined()" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-js-runtime&issues=AZ9FRowt_qP4NW85AYw9&open=AZ9FRowt_qP4NW85AYw9&pullRequest=322
expect(error.statusCode).toEqual(400);
expect(error.message).toEqual('Error with range');
return;
Expand Down
Loading
Loading