Skip to content

feat: support for denormalized response from server #196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 17, 2024
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
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged
yarn lint-staged
104 changes: 82 additions & 22 deletions source/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class Session {
schemas?: Schema[];
serverInformation?: QueryServerInformationResponse;
serverVersion?: string;
private ensureSerializableResponse: boolean;
private decodeDatesAsIso: boolean;
private schemasPromise?: Promise<Schema[]>;
private serverInformationPromise?: Promise<ServerInformation>;
Expand All @@ -82,6 +83,7 @@ export class Session {
* @param {object} [options.headers] - Additional headers to send with the request
* @param {object} [options.strictApi] - Turn on strict API mode
* @param {object} [options.decodeDatesAsIso] - Decode dates as ISO strings instead of moment objects
* @param {object} [options.ensureSerializableResponse] - Disable normalization of response data
*
* @constructs Session
*/
Expand All @@ -98,6 +100,7 @@ export class Session {
additionalHeaders = {},
strictApi = false,
decodeDatesAsIso = false,
ensureSerializableResponse = false,
}: SessionOptions = {},
) {
if (!serverUrl || !apiUser || !apiKey) {
Expand Down Expand Up @@ -194,6 +197,16 @@ export class Session {

this.decodeDatesAsIso = decodeDatesAsIso;

/**
* By default the API server will return normalized responses, and we denormalize them in the client.
* This might cause cyclical references in the response data, making it non-JSON serializable.
* This option allows the user to disable normalization of the response data to ensure serializability.
* @memberof Session
* @instance
* @type {Boolean}
*/
this.ensureSerializableResponse = ensureSerializableResponse;

/**
* true if session is initialized
* @memberof Session
Expand Down Expand Up @@ -365,21 +378,36 @@ export class Session {
private decode(
data: any,
identityMap: Data = {},
decodeDatesAsIso: boolean = false,
{
decodeDatesAsIso = false,
ensureSerializableResponse = false,
}: {
decodeDatesAsIso?: boolean;
ensureSerializableResponse?: boolean;
} = {},
): any {
if (Array.isArray(data)) {
return this._decodeArray(data, identityMap, decodeDatesAsIso);
return this._decodeArray(data, identityMap, {
decodeDatesAsIso,
ensureSerializableResponse,
});
}
if (!!data && typeof data === "object") {
if (data.__entity_type__) {
return this._mergeEntity(data, identityMap, decodeDatesAsIso);
if (data.__entity_type__ && !ensureSerializableResponse) {
return this._mergeEntity(data, identityMap, {
decodeDatesAsIso,
ensureSerializableResponse,
});
}
if (data.__type__ === "datetime" && decodeDatesAsIso) {
return this._decodeDateTimeAsIso(data);
} else if (data.__type__ === "datetime") {
return this._decodeDateTimeAsMoment(data);
}
return this._decodePlainObject(data, identityMap, decodeDatesAsIso);
return this._decodePlainObject(data, identityMap, {
decodeDatesAsIso,
ensureSerializableResponse,
});
}
return data;
}
Expand All @@ -396,15 +424,13 @@ export class Session {
let dateValue = data.value;
if (
this.serverInformation &&
this.serverInformation.is_timezone_support_enabled
this.serverInformation.is_timezone_support_enabled &&
!dateValue.endsWith("Z") &&
!dateValue.includes("+")
) {
// Server responds with timezone naive strings, add Z to indicate UTC.
// If the string somehow already contains a timezone offset, do not add Z.
if (!dateValue.endsWith("Z") && !dateValue.includes("+")) {
dateValue += "Z";
}
// Return date as moment object with UTC set to true.
return new Date(dateValue).toISOString();
dateValue += "Z";
}
// Server has no timezone support, return date in ISO format
return new Date(dateValue).toISOString();
Expand Down Expand Up @@ -438,10 +464,19 @@ export class Session {
private _decodePlainObject(
object: Data,
identityMap: Data,
decodeDatesAsIso: boolean,
{
decodeDatesAsIso,
ensureSerializableResponse,
}: {
decodeDatesAsIso?: boolean;
ensureSerializableResponse?: boolean;
} = {},
) {
return Object.keys(object).reduce<Data>((previous, key) => {
previous[key] = this.decode(object[key], identityMap, decodeDatesAsIso);
previous[key] = this.decode(object[key], identityMap, {
decodeDatesAsIso,
ensureSerializableResponse,
});
return previous;
}, {});
}
Expand All @@ -453,10 +488,19 @@ export class Session {
private _decodeArray(
collection: any[],
identityMap: Data,
decodeDatesAsIso: boolean,
{
decodeDatesAsIso = false,
ensureSerializableResponse = false,
}: {
decodeDatesAsIso?: boolean;
ensureSerializableResponse?: boolean;
} = {},
): any[] {
return collection.map((item) =>
this.decode(item, identityMap, decodeDatesAsIso),
this.decode(item, identityMap, {
decodeDatesAsIso,
ensureSerializableResponse,
}),
);
}

Expand All @@ -467,7 +511,13 @@ export class Session {
private _mergeEntity(
entity: Data,
identityMap: Data,
decodeDatesAsIso: boolean,
{
decodeDatesAsIso,
ensureSerializableResponse,
}: {
decodeDatesAsIso?: boolean;
ensureSerializableResponse?: boolean;
} = {},
) {
const identifier = this.getIdentifyingKey(entity);
if (!identifier) {
Expand All @@ -489,11 +539,10 @@ export class Session {

for (const key in entity) {
if (entity.hasOwnProperty(key)) {
mergedEntity[key] = this.decode(
entity[key],
identityMap,
mergedEntity[key] = this.decode(entity[key], identityMap, {
decodeDatesAsIso,
);
ensureSerializableResponse,
});
}
}
return mergedEntity;
Expand Down Expand Up @@ -583,6 +632,7 @@ export class Session {
signal,
additionalHeaders = {},
decodeDatesAsIso = this.decodeDatesAsIso,
ensureSerializableResponse = this.ensureSerializableResponse,
}: CallOptions = {},
): Promise<IsTuple<T> extends true ? T : T[]> {
if (this.initializing) {
Expand All @@ -592,7 +642,6 @@ export class Session {
try {
// Delay call until session is initialized if initialization is in
// progress.

let fetchResponse;
try {
fetchResponse = await fetch(url, {
Expand All @@ -605,6 +654,9 @@ export class Session {
"ftrack-user": this.apiUser,
"ftrack-Clienttoken": this.clientToken,
"ftrack-pushtoken": pushToken,
"ftrack-api-options": ensureSerializableResponse
? "strict:1;denormalize:1"
: undefined,
...this.additionalHeaders,
...additionalHeaders,
} as HeadersInit,
Expand All @@ -627,7 +679,11 @@ export class Session {
throw this.getErrorFromResponse(response);
}
try {
return this.decode(response, {}, decodeDatesAsIso);
return this.decode(
response,
{},
{ decodeDatesAsIso, ensureSerializableResponse },
);
} catch (reason) {
logger.warn("Server reported error in unexpected format. ", reason);
throw this.getErrorFromResponse({
Expand Down Expand Up @@ -781,6 +837,7 @@ export class Session {
* @param {object} options.signal - Abort signal user for aborting requests prematurely
* @param {object} options.headers - Additional headers to send with the request
* @param {object} options.decodeDatesAsIso - Decode dates as ISO strings instead of moment objects
* @param {object} options.ensureSerializableResponse - Disable normalization of response data
* @return {Promise} Promise which will be resolved with an object
* containing action, data and metadata
*/
Expand Down Expand Up @@ -810,6 +867,7 @@ export class Session {
* @param {object} options.signal - Abort signal user for aborting requests prematurely
* @param {object} options.headers - Additional headers to send with the request
* @param {object} options.decodeDatesAsIso - Decode dates as ISO strings instead of moment objects
* @param {object} options.ensureSerializableResponse - Disable normalization of response data
* @return {Promise} Promise which will be resolved with an object
* containing data and metadata
*/
Expand Down Expand Up @@ -855,6 +913,7 @@ export class Session {
* @param {string} options.pushToken - push token to associate with the request
* @param {object} options.headers - Additional headers to send with the request
* @param {object} options.decodeDatesAsIso - Decode dates as ISO strings instead of moment objects
* @param {object} options.ensureSerializableResponse - Disable normalization of response data
* @return {Promise} Promise which will be resolved with the response.
*/
async create<T extends Data = Data>(
Expand All @@ -881,6 +940,7 @@ export class Session {
* @param {string} options.pushToken - push token to associate with the request
* @param {object} options.headers - Additional headers to send with the request
* @param {object} options.decodeDatesAsIso - Decode dates as ISO strings instead of moment objects
* @param {object} options.ensureSerializableResponse - Disable normalization of response data
* @return {Promise} Promise resolved with the response.
*/
async update<T extends Data = Data>(
Expand Down
3 changes: 3 additions & 0 deletions source/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface SessionOptions {
additionalHeaders?: Data;
strictApi?: boolean;
decodeDatesAsIso?: boolean;
ensureSerializableResponse?: boolean;
}

export interface CreateComponentOptions {
Expand Down Expand Up @@ -178,6 +179,7 @@ export interface MutationOptions {
pushToken?: string;
additionalHeaders?: Data;
decodeDatesAsIso?: boolean;
ensureSerializableResponse?: boolean;
}

export type SimpleTypeSchemaProperty = {
Expand Down Expand Up @@ -223,6 +225,7 @@ export interface QueryOptions {
signal?: AbortSignal;
additionalHeaders?: Data;
decodeDatesAsIso?: boolean;
ensureSerializableResponse?: boolean;
}

export interface CallOptions extends MutationOptions, QueryOptions {}
Loading