Skip to content

feat: Implement asynchronous getSchemas method and prepare for not querying schemas and server information on initialization #124

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 11 commits into from
Jun 28, 2023
Merged
90 changes: 82 additions & 8 deletions source/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ import type {
Schema,
SearchOptions,
SearchResponse,
ServerInformation,
SessionOptions,
UpdateResponse,
} from "./types.js";
import { convertToIsoString } from "./util/convert_to_iso_string.js";
import { Uploader } from "./uploader.js";
import getSchemaMappingFromSchemas from "./util/get_schema_mapping.js";

const logger = loglevel.getLogger("ftrack_api");

Expand All @@ -54,10 +56,14 @@ export class Session {
clientToken: string | null;
initialized: boolean;
initializing: Promise<Session>;
serverInformation?: Data;
additionalHeaders: Data;
schemas?: Schema[];
serverInformation?: QueryServerInformationResponse;
serverVersion?: string;
additionalHeaders: Data;
private schemasPromise?: Promise<Schema[]>;
private serverInformationPromise?: Promise<ServerInformation>;
private serverInformationValues?: string[];
private schemaMapping?: Record<string, Schema>;

/**
* Construct Session instance with API credentials.
Expand Down Expand Up @@ -122,6 +128,8 @@ export class Session {
*/
this.serverUrl = serverUrl;

this.serverInformationValues = serverInformationValues;

/**
* API Endpoint. Specified relative to server URL with leading slash.
* @memberof Session
Expand Down Expand Up @@ -164,13 +172,15 @@ export class Session {
}

// Always include is_timezone_support_enabled as required by API.
// TODO: Remove this in next major.
if (
serverInformationValues &&
!serverInformationValues.includes("is_timezone_support_enabled")
) {
serverInformationValues.push("is_timezone_support_enabled");
}

// TODO: remove these operations from session initialization in next major
const operations: operation.Operation[] = [
{
action: "query_server_information",
Expand All @@ -187,22 +197,36 @@ export class Session {
*/
this.initialized = false;

const initializingPromise =
this.call<[QueryServerInformationResponse, QuerySchemasResponse]>(
operations
);

/**
* Resolved once session is initialized.
* @memberof Session
* @instance
* @type {Promise}
*/
this.initializing = this.call<
[QueryServerInformationResponse, QuerySchemasResponse]
>(operations).then((responses) => {
this.initializing = initializingPromise.then((responses) => {
// TODO: Make this.serverInformation, this.schemas, and this.serverVersion private in next major
// and require calling getServerInformation, getSchemas, and this.getServerVersion instead.
this.serverInformation = responses[0];
this.schemas = responses[1];
this.schemaMapping = getSchemaMappingFromSchemas(this.schemas);
this.serverVersion = this.serverInformation.version;
this.initialized = true;

return Promise.resolve(this);
});

this.serverInformationPromise = initializingPromise
.then((responses) => responses[0])
.catch(() => ({} as ServerInformation));

this.schemasPromise = initializingPromise
.then((responses) => responses[1])
.catch(() => [] as Schema[]);
}

/**
Expand All @@ -211,11 +235,12 @@ export class Session {
* @return {Array|null} List of primary key attributes.
*/
getPrimaryKeyAttributes(entityType: string): string[] | null {
// Todo: make this async in next major
if (!this.schemas) {
logger.warn("Schemas not available.");
return null;
}
const schema = this.schemas.find((item) => item.id === entityType);
const schema = this.schemaMapping?.[entityType];
if (!schema || !schema.primary_key || !schema.primary_key.length) {
logger.warn("Primary key could not be found for: ", entityType);
return null;
Expand Down Expand Up @@ -474,6 +499,52 @@ export class Session {
return JSON.stringify(this.encode(operations));
}

/**
* Returns server information for the session, using serverInformationValues as set on session initialization.
* This is cached after the first call, and assumes that the server information will not change during the session.
* @returns Promise with the server information for the session.
*/
async getServerInformation(): Promise<ServerInformation> {
if (!this.serverInformationPromise) {
this.serverInformationPromise = this.call<QueryServerInformationResponse>(
[
{
action: "query_server_information",
values: this.serverInformationValues,
},
]
).then((responses) => responses[0]);
}

return this.serverInformationPromise;
}

/**
* Returns server version for the session, using serverInformationValues as set on session initialization.
* This is cached after the first call, and assumes that the server information will not change during the session.
* @returns Promise with the server version for the session.
*/
async getServerVersion(): Promise<string> {
return (await this.getServerInformation()).version;
}

/**
* Returns the API schemas for the session.
* This is cached after the first call, and assumes that the schemas will not change during the session.
* @returns Promise with the API schemas for the session.
*/
async getSchemas(): Promise<Schema[]> {
if (!this.schemasPromise) {
this.schemasPromise = this.call<QuerySchemasResponse>([
{ action: "query_schemas" },
]).then((responses) => {
this.schemaMapping = getSchemaMappingFromSchemas(responses[0]);
return responses[0];
});
}
return this.schemasPromise;
}

/**
* Call API with array of operation objects in *operations*.
*
Expand Down Expand Up @@ -509,7 +580,9 @@ export class Session {
decodeDatesAsIso = false,
}: CallOptions = {}
): Promise<IsTuple<T> extends true ? T : T[]> {
await this.initializing;
if (this.initializing) {
await this.initializing;
}
const url = `${this.serverUrl}${this.apiEndpoint}`;

try {
Expand Down Expand Up @@ -688,7 +761,8 @@ export class Session {
* @return {Object|null} Schema definition
*/
getSchema(schemaId: string): Schema | null {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to move the transformation from array to map from data utils here so that it can be re-used by e.g. this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do, would it be as a private attribute? I don't think I want to expand the scope of the api client and make it diverge from the python api more than necessary?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map itself, sure. Could you still replace all it's usage in data-utils by this getSchema method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have now implemented the map. since it's not public, we have to still do the same mapping in the frontend codebase, so no change is necessary. please review.

const schema = this.schemas?.find((s) => s.id === schemaId);
// TODO: make this async in next major
const schema = this.schemaMapping?.[schemaId];
return schema ?? null;
}

Expand Down
6 changes: 4 additions & 2 deletions source/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ export interface ResetRemoteResponse {
data: Data;
}
export type QuerySchemasResponse = Schema[];
export interface QueryServerInformationResponse {

export type QueryServerInformationResponse = ServerInformation;
export interface ServerInformation {
custom_widget?: Data;
default_colors?: string[];
is_nested_subqueries_enabled?: boolean;
Expand All @@ -91,7 +93,7 @@ export interface QueryServerInformationResponse {
username?: string;
};
product?: Data;
version?: string;
version: string;
schema_hash?: string;
storage_scenario?: Data;
[key: string]: any;
Expand Down
11 changes: 11 additions & 0 deletions source/util/get_schema_mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// :copyright: Copyright (c) 2023 ftrack

import { Schema } from "../types.js";

export default function getSchemaMappingFromSchemas(schemas: Schema[]) {
const schemaMapping = {} as Record<string, Schema>;
for (const schema of schemas) {
schemaMapping[schema.id] = schema;
}
return schemaMapping;
}
14 changes: 14 additions & 0 deletions test/session.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,20 @@ describe("Session", () => {
})
).rejects.toThrowError("Code must be provided to enable totp.");
});

it("Should support getting schemas with session.getSchemas()", async () => {
expect(await session.getSchemas()).toEqual(querySchemas);
});

it("Should support getting server information with session.getServerInformation()", async () => {
expect(await session.getServerInformation()).toEqual(
queryServerInformation
);
});

it("Should support getting server version with session.getServerVersion()", async () => {
expect(await session.getServerVersion()).toEqual("dev");
});
});

describe("Encoding entities", () => {
Expand Down