Skip to content
Merged
2 changes: 1 addition & 1 deletion .codegen.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "engineHash": "abf4f7d", "specHash": "c303afc", "version": "1.14.0" }
{ "engineHash": "d69fdc9", "specHash": "c303afc", "version": "1.14.0" }
39 changes: 39 additions & 0 deletions docs/working-with-nulls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Handling null values in Box Typescript SDK Gen

While using Box Typescript SDK it's important to understand how null values behave. This document provides a general overview of null value behaviour in Box Typescript SDK to help developers manage data consistently and predictably.

## Understanding null behaviour

The Box Typescript SDK follows a consistent pattern when handling null values in update operations. This behaviour applies to most endpoints that modify resources such as users, files, folders and metadata. The updating field behaves differently depending on weather you omit it, set it to null, or provide a value:

- Omitting the field: The field won't be included in request and the value will remain unchanged
- Setting it to null: Setting a field to null, will cause sending HTTP request with field value set to null, what will result in removing its current value or disassociates it from the resource.
- Providing a value: Providing a non-null value assigns or updates the field to that value.

## Example Usage

The client.files.updateFileById() method demonstrates null handling when modifying the lock field while updating the file:

```ts
async function create_update_file(client) {
fileId = '12345';

// locking the file
const fileWithLock = await client.files.updateFileById(fileId, {
requestBody: { lock: { access: 'lock' } },
queryParams: { fields: ['lock'] },
});
console.log('File with lock ', fileWithLock);

// unlocking the file using lock value as null
const fileWithoutLock = await client.files.updateFileById(fileId, {
requestBody: { lock: null },
queryParams: { fields: ['lock'] },
});
console.log('File without lock ', fileWithoutLock);
}
```

## Summary

To summarize, if you omit the field, the field remains unchanged. If you set it to null, it clears/removes the value. If you provide a value to that field, the field gets updated to that specified value.
50 changes: 25 additions & 25 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 45 additions & 1 deletion src/box/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { SerializedData } from '../serialization/json.js';
import { GeneratedCodeError } from '../internal/errors.js';
import util from 'util';
import { DataSanitizer } from '../internal/logging.generated';

export class BoxSdkError extends GeneratedCodeError {
readonly timestamp?: string;
Expand Down Expand Up @@ -38,14 +40,56 @@ export interface ResponseInfo {
export class BoxApiError extends BoxSdkError {
readonly requestInfo!: RequestInfo;
readonly responseInfo!: ResponseInfo;
readonly dataSanitizer: DataSanitizer = new DataSanitizer({});
constructor(
fields: Pick<
BoxApiError,
'message' | 'timestamp' | 'error' | 'requestInfo' | 'responseInfo'
| 'message'
| 'timestamp'
| 'error'
| 'requestInfo'
| 'responseInfo'
| 'dataSanitizer'
>,
) {
super(fields);
this.name = 'BoxApiError';
if (fields.dataSanitizer) {
this.dataSanitizer = fields.dataSanitizer;
}
Object.setPrototypeOf(this, BoxApiError.prototype);
}

[util.inspect.custom]() {
return this.toString();
}

toString(): string {
return JSON.stringify(this.toJSON(), null, 2);
}

toJSON() {
return {
name: this.name,
message: this.message,
timestamp: this.timestamp,
error: this.error,
requestInfo: {
method: this.requestInfo.method,
url: this.requestInfo.url,
queryParams: this.requestInfo.queryParams,
headers: this.dataSanitizer.sanitizeHeaders(this.requestInfo.headers),
body: this.requestInfo.body,
},
responseInfo: {
statusCode: this.responseInfo.statusCode,
headers: this.dataSanitizer.sanitizeHeaders(this.responseInfo.headers),
body: this.dataSanitizer.sanitizeBody(this.responseInfo.body),
code: this.responseInfo.code,
contextInfo: this.responseInfo.contextInfo,
requestId: this.responseInfo.requestId,
helpUrl: this.responseInfo.helpUrl,
},
};
}
}
48 changes: 48 additions & 0 deletions src/internal/logging.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SerializedData } from '../serialization/json.js';
import { sanitizeMap } from './utils.js';
import { sanitizeSerializedData } from '../serialization/json.js';
export class DataSanitizer {
private readonly keysToSanitize: {
readonly [key: string]: string;
} = {
['authorization']: '',
['access_token']: '',
['refresh_token']: '',
['subject_token']: '',
['token']: '',
['client_id']: '',
['client_secret']: '',
['shared_link']: '',
['download_url']: '',
['jwt_private_key']: '',
['jwt_private_key_passphrase']: '',
['password']: '',
};
constructor(
fields: Omit<
DataSanitizer,
'keysToSanitize' | 'sanitizeHeaders' | 'sanitizeBody'
>,
) {}
/**
* @param {{
readonly [key: string]: string;
}} headers
* @returns {{
readonly [key: string]: string;
}}
*/
sanitizeHeaders(headers: { readonly [key: string]: string }): {
readonly [key: string]: string;
} {
return sanitizeMap(headers, this.keysToSanitize);
}
/**
* @param {SerializedData} body
* @returns {SerializedData}
*/
sanitizeBody(body: SerializedData): SerializedData {
return sanitizeSerializedData(body, this.keysToSanitize);
}
}
export interface DataSanitizerInput {}
17 changes: 17 additions & 0 deletions src/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
FormData,
generateReadableStreamFromFile,
} from './utilsNode';
import { sanitizedValue } from '../serialization/json';

export type HashName = 'sha1';
export type DigestHashType = 'base64';
Expand Down Expand Up @@ -239,3 +240,19 @@ export function createCancellationController(): CancellationController {
export function random(min: number, max: number): number {
return Math.random() * (max - min) + min;
}

/**
* Sanitize a map by replacing sensitive values with a placeholder.
* @param dictionary The map to sanitize
* @param keysToSanitize Keys to sanitize
*/
export function sanitizeMap(
dictionary: Record<string, string>,
keysToSanitize: Record<string, string>,
): Record<string, string> {
return Object.fromEntries(
Object.entries(dictionary).map(([k, v]) =>
k.toLowerCase() in keysToSanitize ? [k, sanitizedValue()] : [k, v],
),
);
}
8 changes: 6 additions & 2 deletions src/networking/boxNetworkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,19 +248,22 @@ export class BoxNetworkClient implements NetworkClient {
return fetchResponse;
}

const [code, contextInfo, requestId, helpUrl] = sdIsMap(fetchResponse.data)
const [code, contextInfo, requestId, helpUrl, message] = sdIsMap(
fetchResponse.data,
)
? [
sdToJson(fetchResponse.data['code']),
sdIsMap(fetchResponse.data['context_info'])
? fetchResponse.data['context_info']
: undefined,
sdToJson(fetchResponse.data['request_id']),
sdToJson(fetchResponse.data['help_url']),
sdToJson(fetchResponse.data['message']),
]
: [];

throw new BoxApiError({
message: `${fetchResponse.status}`,
message: `${fetchResponse.status} ${message}; Request ID: ${requestId}`,
timestamp: `${Date.now()}`,
requestInfo: {
method: requestInit.method!,
Expand All @@ -280,6 +283,7 @@ export class BoxNetworkClient implements NetworkClient {
requestId: requestId,
helpUrl: helpUrl,
},
dataSanitizer: networkSession.dataSanitizer,
});
}
}
Expand Down
Loading