@inweb/client
Version:
JavaScript REST API client for the Open Cloud Server
577 lines (510 loc) • 19.1 kB
text/typescript
///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2026, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
// This application incorporates Open Design Alliance software pursuant to a
// license agreement with Open Design Alliance.
// Open Design Alliance Copyright (C) 2002-2026 by Open Design Alliance.
// All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////
import { IHttpClient } from "./IHttpClient";
import { Endpoint } from "./Endpoint";
import { IShortUserDesc } from "./IUser";
import { Role } from "./Role";
import { IRoleActions } from "./IRole";
import { Member } from "./Member";
import { File } from "./File";
import { normalizeParam, userFullName, userInitials } from "./Utils";
/**
* Provides properties and methods for obtaining information about a project on the Open Cloud Server and
* managing its {@link Role | roles}, {@link Member | members} and models.
*/
export class Project extends Endpoint {
private _data: any;
/**
* @param data - Raw project data received from the server. For more information, see
* {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
* @param httpClient - HTTP client instance used to send requests to the REST API server.
*/
constructor(data: any, httpClient: IHttpClient) {
super(`/projects/${data.id}`, httpClient);
this.data = data;
}
/**
* Project features the user has access to.
*
* @readonly
*/
get authorization(): {
/**
* Actions are allowed to be performed:
*
* - `update` - The ability to update the project details.
* - `createTopic` - The ability to create a new topic.
* - `createDocument` - The ability to create a new document.
*/
project_actions: string[];
} {
return this.data.authorization;
}
/**
* Project creation time (UTC) in the format specified in
* {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
*
* @readonly
*/
get createdAt(): string {
return this.data.createdAt;
}
/**
* Project custom fields object, to store custom data.
*/
get customFields(): any {
return this.data.customFields;
}
set customFields(value: any) {
this.data.customFields = value;
}
/**
* Raw project data received from the server. For more information, see
* {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
*
* @readonly
*/
get data(): any {
return this._data;
}
private set data(value: any) {
this._data = value;
this._data.owner.fullName = userFullName(this._data.owner);
this._data.owner.initials = userInitials(this._data.owner.fullName);
// owner.avatarUrl & owner.lastModified since 27.6
this._data.owner.avatarUrl ||= "";
this._data.owner.lastModified ||= "";
}
/**
* Project description.
*/
get description(): string {
return this.data.description;
}
set description(value: string) {
this.data.description = value;
}
/**
* Project end date in the format specified in
* {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
*/
get endDate(): string {
return this.data.endDate;
}
set endDate(value: string | Date) {
this.data.endDate = value instanceof Date ? value.toISOString() : value;
}
/**
* Unique project ID.
*
* @readonly
*/
get id(): string {
return this.data.id;
}
/**
* The number of members in the project.
*
* @readonly
*/
get memberCount(): number {
return this.data.memberCount;
}
/**
* The number of models in the project.
*
* @readonly
*/
get modelCount(): number {
return this.data.modelCount;
}
/**
* Project name.
*/
get name(): string {
return this.data.name;
}
set name(value: string) {
this.data.name = value;
}
/**
* Project owner (the user who created the project).
*
* @readonly
*/
get owner(): IShortUserDesc {
return this.data.owner;
}
/**
* Project preview image URL or empty string if the project does not have a preview. Use
* {@link Project.setPreview | setPreview()} to change preview image.
*
* This URL does not change when the underlying image content is updated. Because of this, browsers may
* cache the image, which can prevent the preview from updating visually in web applications (such as
* React, Angular, etc.).
*
* To avoid caching issues, append a dynamic query parameter to the URL, such as the project's
* {@link updatedAt | last modification date}. Since this date changes whenever the preview is updated,
* the browser will correctly reload the new image when required.
*
* @example Usage in a React component
*
* ```js
* function ProjectPreview({ project }) {
* // 'updatedAt' is a timestamp representing when the project was last modified.
* const cacheBuster = project.updatedAt;
*
* // Use a default image if the project does not have a preview
* const imageSrc = project.previewUrl
* ? `${project.previewUrl}?t=${cacheBuster}`
* : "/assets/default-preview.png";
*
* return <img src={imageSrc} alt="Project preview" />;
* }
* ```
*
* @readonly
*/
get previewUrl(): string {
return this._data.avatarUrl || "";
}
/**
* `true` if project is shared project.
*/
get public(): boolean {
return this.data.public;
}
set public(value: boolean) {
this.data.public = value;
}
/**
* Project start date in the format specified in
* {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
*/
get startDate(): string {
return this.data.startDate;
}
set startDate(value: string | Date) {
this.data.startDate = value instanceof Date ? value.toISOString() : value;
}
/**
* The number of topics in the project.
*
* @readonly
*/
get topicCount(): number {
return this.data.topicCount;
}
/**
* Project last update time (UTC) in the format specified in
* {@link https://www.wikipedia.org/wiki/ISO_8601 | ISO 8601}.
*
* @readonly
*/
get updatedAt(): string {
return this.data.updatedAt;
}
/**
* Reloads project data from the server.
*/
async checkout(): Promise<this> {
const response = await this.get("");
this.data = await response.json();
return this;
}
/**
* Updates project data on the server.
*
* @param data - Raw project data. For more information, see
* {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
*/
async update(data: any): Promise<this> {
const response = await this.put("", data);
this.data = await response.json();
return this;
}
/**
* Deletes a project from the server.
*
* @returns Returns the raw data of a deleted project. For more information, see
* {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
*/
override delete(): Promise<any> {
return super
.delete("")
.then((response) => response.text())
.then((text) => {
// deprecated fix for server 23.5 and below
try {
return JSON.parse(text);
} catch {
return { id: this.id };
}
});
}
/**
* Saves project properties changes to the server. Call this method to update project data on the
* server after any property changes.
*/
save(): Promise<this> {
return this.update(this.data);
}
/**
* Sets or removes the project preview.
*
* @param image - Preview image. Can be a
* {@link https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | Data URL} string,
* {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer | ArrayBuffer},
* {@link https://developer.mozilla.org/docs/Web/API/Blob/Blob | Blob} or
* {@link https://developer.mozilla.org/docs/Web/API/File | Web API File} object. Setting the `image`
* to `null` will remove the preview.
*/
async setPreview(image?: BodyInit | null): Promise<this> {
if (!image) {
await this.deletePreview();
} else {
const response = await this.post("/preview", image);
this.data = await response.json();
}
return this;
}
/**
* Removes the project preview.
*/
async deletePreview(): Promise<this> {
const response = await super.delete("/preview");
this.data = await response.json();
return this;
}
/**
* Returns a list of project roles. Project members have different abilities depending on the role they
* have in a project.
*/
getRoles(): Promise<Role[]> {
return this.get("/roles")
.then((response) => response.json())
.then((array) => array.map((data) => new Role(data, this.id, this.httpClient)));
}
/**
* Returns information about the specified project role.
*
* @param name - Role name.
*/
getRole(name: string): Promise<Role> {
return this.get(`/roles/${name}`)
.then((response) => response.json())
.then((data) => new Role(data, this.id, this.httpClient));
}
/**
* Creates a new project role.
*
* @param name - Role name.
* @param description - Role description.
* @param permissions - Actions are allowed to be performed for the role.
*/
createRole(name: string, description: string, permissions: IRoleActions): Promise<Role> {
return this.post("/roles", {
name,
description,
permissions: permissions || {},
})
.then((response) => response.json())
.then((data) => new Role(data, this.id, this.httpClient));
}
/**
* Deletes the specified project role.
*
* @param name - Role name.
* @returns Returns the raw data of a deleted role. For more information, see
* {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
*/
deleteRole(name: string): Promise<any> {
return super.delete(`/roles/${name}`).then((response) => response.json());
}
/**
* Returns a list of project members.
*/
getMembers(): Promise<Member[]> {
return this.get("/members")
.then((response) => response.json())
.then((array) => array.map((data) => new Member(data, this.id, this.httpClient)));
}
/**
* Returns information about the specified project member.
*
* @param memberId - Member ID.
*/
getMember(memberId: string): Promise<Member> {
return this.get(`/members/${memberId}`)
.then((response) => response.json())
.then((data) => new Member(data, this.id, this.httpClient));
}
/**
* Adds a user to the project to become a member and have permission to perform actions.
*
* @param userId - User ID.
* @param role - Role name from the list of project {@link getRoles | roles}.
*/
addMember(userId: string, role: string): Promise<Member> {
return this.post("/members", { userId, role })
.then((response) => response.json())
.then((data) => new Member(data, this.id, this.httpClient));
}
/**
* Removes the specified member from a project.
*
* @param memberId - Member ID.
* @returns Returns the raw data of a deleted member. For more information, see
* {@link https://cloud.opendesign.com/docs//pages/server/api.html#Project | Open Cloud Projects API}.
*/
removeMember(memberId: string): Promise<any> {
return super.delete(`/members/${memberId}`).then((response) => response.json());
}
/**
* Information about the file (model) that can be reference in the project topics.
*
* @typedef {any} FileInformation
* @property {any[]} display_information - The list of fields to allow users to associate the file with
* a server model.
* @property {string} display_information.field_display_name - Field display name.
* @property {string} display_information.field_value - Field value.
* @property {any} file - The file reference object.
* @property {string} file.file_name - File name.
* @property {string} file.reference - File ID.
*/
/**
* Returns a list of project files. For more information, see
* {@link https://cloud.opendesign.com/docs//pages/server/bcf3.html#ProjectFilesInformation | Open Cloud BCF3 API}.
*
* This list contains all files that the project has access to. To add a file to this list, create a
* {@link IGrantedTo.project | project} permission on the file using
* {@link File.createPermission | File.createPermission()}.
*/
getFilesInformation(): Promise<any[]> {
const bcfProjects = new Endpoint("/bcf/3.0/projects", this.httpClient, this.headers);
return bcfProjects
.get(`/${this.id}/files_information`)
.then((response) => response.json())
.then((items) => {
items.forEach((item) => {
const getFieldValue = (displayName: string) => {
return (item.display_information.find((x) => x.field_display_name === displayName) || {}).field_value;
};
const ownerFirstName = getFieldValue("Owner First Name");
const ownerLastName = getFieldValue("Owner Last Name");
const ownerUserName = getFieldValue("Owner User Name");
const ownerFullName = userFullName(ownerFirstName, ownerLastName, ownerUserName);
const ownerInitials = userInitials(ownerFullName);
item.display_information.push({ field_display_name: "Owner Full Name", field_value: ownerFullName });
item.display_information.push({ field_display_name: "Owner Initials", field_value: ownerInitials });
// updatedBy since 24.10
const updatedByFirstName = getFieldValue("Updated By First Name");
const updatedByLastName = getFieldValue("Updated By Last Name");
const updatedByUserName = getFieldValue("Updated By User Name");
const updatedByFullName = userFullName(updatedByFirstName, updatedByLastName, updatedByUserName);
const updatedByInitials = userInitials(updatedByFullName);
item.display_information.push({ field_display_name: "Updated By Full Name", field_value: updatedByFullName });
item.display_information.push({ field_display_name: "Updated By Initials", field_value: updatedByInitials });
// geometryType since 24.12
const geometry = getFieldValue("Geometry Status");
const geometryGltf = getFieldValue("GeometryGltf Status");
const geometryType = geometry === "done" ? "vsfx" : geometryGltf === "done" ? "gltf" : "";
item.display_information.push({ field_display_name: "Geometry Type", field_value: geometryType });
// previewUrl since 27.6
if (getFieldValue("Preview URL") === undefined)
item.display_information.push({ field_display_name: "Preview URL", field_value: "" });
// owner.avatarUrl & owner.lastModified since 27.6
if (getFieldValue("Owner Avatar URL") === undefined)
item.display_information.push({ field_display_name: "Owner Avatar URL", field_value: "" });
if (getFieldValue("Owner Last Modified") === undefined)
item.display_information.push({ field_display_name: "Owner Last Modified", field_value: "" });
// updatedBy.avatarUrl & updatedBy.lastModified since 27.6
if (getFieldValue("Updated By Avatar URL") === undefined)
item.display_information.push({ field_display_name: "Updated By Avatar URL", field_value: "" });
if (getFieldValue("Updated By Last Modified") === undefined)
item.display_information.push({ field_display_name: "Updated By Last Modified", field_value: "" });
});
return items;
});
}
/**
* Returns a list of project files.
*/
getModels(): Promise<File[]> {
return this.getFilesInformation()
.then((filesInformation) => filesInformation.map((item) => item.file.reference))
.then((ids) => {
const files = new Endpoint("/files", this.httpClient, this.headers);
return files.get(`?id=${normalizeParam(ids)}`);
})
.then((response) => response.json())
.then((files) => files.result.map((data) => new File(data, this.httpClient)));
}
/**
* Adds a file to the project with specified permissions.
*
* To change file permissions for the project use {@link Permission.actions}.
*
* @param fileId - File ID.
* @param actions - Actions are allowed to be performed on a file:
*
* - `read` - The ability to read file description, geometry data and properties.
* - `readSourceFile` - The ability to download source file.
* - `write` - The ability to modify file name, description and references.
* - `readViewpoint` - The ability to read file viewpoints.
* - `createViewpoint` - The ability to create file viewpoints.
* - `deleteViewpoint` - The ability to delete file viewpoints.
*
* @returns Returns a file instance added to the project.
*/
async addModel(fileId: string, actions: string | string[]): Promise<File> {
const files = new Endpoint("/files", this.httpClient, this.headers);
const file = await files
.get(`/${fileId}`)
.then((response) => response.json())
.then((data) => new File(data, this.httpClient));
const grantedTo = [{ project: { id: this.id, name: this.name } }];
await file.createPermission(actions, grantedTo);
return file;
}
/**
* Removes the specified file from a project.
*
* @param fileId - File ID.
* @returns Returns a file instance removed from the project.
*/
async removeModel(fileId: string): Promise<File> {
const files = new Endpoint("/files", this.httpClient, this.headers);
const file = await files
.get(`/${fileId}`)
.then((response) => response.json())
.then((data) => new File(data, this.httpClient));
const permissions = await file.getPermissions();
await Promise.allSettled(
permissions
.filter((permission) => permission.grantedTo.some((x) => x.project?.id === this.id))
.map((permission) => permission.delete())
);
return file;
}
}