From 899502d1eaee3fa3dddd1edac25298ac130a29ce Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Wed, 10 Jun 2026 18:30:50 +0530 Subject: [PATCH 01/23] feat(data-fabric): add folderKey support to entities.getAll and ChoiceSets surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit entities.getAll now accepts an optional folderKey to retrieve folder-scoped entities; the API filters exclusively (tenant scope vs folder scope are disjoint), confirmed against the live API. ChoiceSets methods all accept folderKey and forward it as the X-UIPATH-FolderKey header. Critically, insertValueById and updateValueById hard-fail on folder-scoped choice sets without the header — and their internal name resolver (resolveChoiceSetName) now scopes its getAll lookup by the same folderKey so the path stops throwing NotFoundError before the API call. Adds folder-scoped @example blocks to JSDoc on every Entities method that already supported folderKey but didn't document it, integration coverage for the new getAll filter, and a skipped folder-scoped ChoiceSets block mirroring the existing OAuth-scope-gated value-CRUD pattern. Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/choicesets.models.ts | 53 +++++-- src/models/data-fabric/choicesets.types.ts | 31 +++- src/models/data-fabric/entities.models.ts | 67 +++++++-- src/models/data-fabric/entities.types.ts | 5 + src/services/data-fabric/choicesets.ts | 107 ++++++++++---- src/services/data-fabric/entities.ts | 64 ++++++++- .../choicesets.integration.test.ts | 132 +++++++++++++++++- .../data-fabric/entities.integration.test.ts | 30 ++++ .../services/data-fabric/choicesets.test.ts | 16 +-- .../services/data-fabric/entities.test.ts | 4 +- 10 files changed, 442 insertions(+), 67 deletions(-) diff --git a/src/models/data-fabric/choicesets.models.ts b/src/models/data-fabric/choicesets.models.ts index e9cef5627..71fa06169 100644 --- a/src/models/data-fabric/choicesets.models.ts +++ b/src/models/data-fabric/choicesets.models.ts @@ -1,12 +1,16 @@ import { + ChoiceSetGetAllOptions, ChoiceSetGetAllResponse, ChoiceSetGetResponse, ChoiceSetGetByIdOptions, ChoiceSetCreateOptions, ChoiceSetUpdateOptions, + ChoiceSetDeleteByIdOptions, ChoiceSetValueInsertOptions, ChoiceSetValueInsertResponse, - ChoiceSetValueUpdateResponse + ChoiceSetValueUpdateOptions, + ChoiceSetValueUpdateResponse, + ChoiceSetValueDeleteOptions, } from './choicesets.types'; import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '../../utils/pagination/types'; @@ -28,24 +32,34 @@ import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '. */ export interface ChoiceSetServiceModel { /** - * Gets all choice sets in the org + * Gets choice sets in the org * + * The Data Fabric choice-set list is scoped exclusively, not additively: + * omitting `folderKey` returns only tenant-level choice sets; passing + * `folderKey` returns only choice sets in that folder. To enumerate + * every choice set across folders, call `getAll()` once per folder + * plus once with no `folderKey` for the tenant scope. + * + * @param options - Optional {@link ChoiceSetGetAllOptions} (e.g. `folderKey` to list folder-scoped choice sets) * @returns Promise resolving to an array of choice set metadata * {@link ChoiceSetGetAllResponse} * @example * ```typescript - * // Get all choice sets - * const allChoiceSets = await choicesets.getAll(); + * // Get tenant-level choice sets + * const tenantChoiceSets = await choicesets.getAll(); + * + * // Get folder-scoped choice sets + * const folderChoiceSets = await choicesets.getAll({ folderKey: "" }); * * // Iterate through choice sets - * allChoiceSets.forEach(choiceSet => { + * tenantChoiceSets.forEach(choiceSet => { * console.log(`ChoiceSet: ${choiceSet.displayName} (${choiceSet.name})`); * console.log(`Description: ${choiceSet.description}`); * console.log(`Created by: ${choiceSet.createdBy}`); * }); * * // Find a specific choice set by name - * const expenseTypes = allChoiceSets.find(cs => cs.name === 'ExpenseTypes'); + * const expenseTypes = tenantChoiceSets.find(cs => cs.name === 'ExpenseTypes'); * * // Check choice set details * if (expenseTypes) { @@ -54,7 +68,7 @@ export interface ChoiceSetServiceModel { * } * ``` */ - getAll(): Promise; + getAll(options?: ChoiceSetGetAllOptions): Promise; /** * Gets choice set values by choice set ID with optional pagination @@ -89,6 +103,9 @@ export interface ChoiceSetServiceModel { * if (page1.hasNextPage) { * const page2 = await choicesets.getById(choiceSetId, { cursor: page1.nextCursor }); * } + * + * // Folder-scoped choice set + * const folderValues = await choicesets.getById(choiceSetId, { folderKey: "" }); * ``` */ getById( @@ -162,10 +179,13 @@ export interface ChoiceSetServiceModel { * const expenseTypes = allChoiceSets.find(cs => cs.name === 'expense_types'); * * await choicesets.deleteById(expenseTypes.id); + * + * // Folder-scoped choice set + * await choicesets.deleteById(expenseTypes.id, { folderKey: "" }); * ``` * @internal */ - deleteById(choiceSetId: string): Promise; + deleteById(choiceSetId: string, options?: ChoiceSetDeleteByIdOptions): Promise; /** * Inserts a single value into a choice set. @@ -185,6 +205,12 @@ export interface ChoiceSetServiceModel { * displayName: 'Travel', * }); * console.log(inserted.id); + * + * // Folder-scoped choice set: folderKey is required on the wire + * await choicesets.insertValueById(expenseTypes.id, 'TRAVEL', { + * displayName: 'Travel', + * folderKey: "", + * }); * ``` * @internal */ @@ -214,6 +240,11 @@ export interface ChoiceSetServiceModel { * const travel = values.items.find(v => v.name === 'TRAVEL'); * * await choicesets.updateValueById(expenseTypes.id, travel.id, 'Business Travel'); + * + * // Folder-scoped choice set: folderKey is required on the wire + * await choicesets.updateValueById(expenseTypes.id, travel.id, 'Business Travel', { + * folderKey: "", + * }); * ``` * @internal */ @@ -221,6 +252,7 @@ export interface ChoiceSetServiceModel { choiceSetId: string, valueId: string, displayName: string, + options?: ChoiceSetValueUpdateOptions, ): Promise; /** @@ -237,9 +269,12 @@ export interface ChoiceSetServiceModel { * const idsToDelete = values.items.slice(0, 2).map(v => v.id); * * await choicesets.deleteValuesById('', idsToDelete); + * + * // Folder-scoped choice set + * await choicesets.deleteValuesById('', idsToDelete, { folderKey: "" }); * ``` * @internal */ - deleteValuesById(choiceSetId: string, valueIds: string[]): Promise; + deleteValuesById(choiceSetId: string, valueIds: string[], options?: ChoiceSetValueDeleteOptions): Promise; } diff --git a/src/models/data-fabric/choicesets.types.ts b/src/models/data-fabric/choicesets.types.ts index 5fd4cb585..eb593ba0c 100644 --- a/src/models/data-fabric/choicesets.types.ts +++ b/src/models/data-fabric/choicesets.types.ts @@ -1,4 +1,5 @@ import { PaginationOptions } from '../../utils/pagination/types'; +import { EntityFolderScopedOptions } from './entities.types'; /** * ChoiceSet Get All Response @@ -49,44 +50,62 @@ export interface ChoiceSetGetResponse { recordOwner?: string; } +/** + * Options for {@link ChoiceSetServiceModel.getAll} + */ +export interface ChoiceSetGetAllOptions extends EntityFolderScopedOptions {} + /** * Options for getting choice set values by choice set ID */ -export type ChoiceSetGetByIdOptions = PaginationOptions; +export type ChoiceSetGetByIdOptions = PaginationOptions & EntityFolderScopedOptions; /** * Options for creating a new choice set */ -export interface ChoiceSetCreateOptions { +export interface ChoiceSetCreateOptions extends EntityFolderScopedOptions { /** Human-readable display name */ displayName?: string; /** Optional choice set description */ description?: string; - /** UUID of the folder to place the choice set in (defaults to the tenant-level folder) */ - folderKey?: string; } /** * Options for updating an existing choice set's metadata */ -export interface ChoiceSetUpdateOptions { +export interface ChoiceSetUpdateOptions extends EntityFolderScopedOptions { /** New display name for the choice set */ displayName?: string; /** New description for the choice set */ description?: string; } +/** + * Options for {@link ChoiceSetServiceModel.deleteById} + */ +export interface ChoiceSetDeleteByIdOptions extends EntityFolderScopedOptions {} + /** * Optional fields when inserting a single value into a choice set. * * The required `name` identifier is passed as a positional argument to * `insertValueById`. */ -export interface ChoiceSetValueInsertOptions { +export interface ChoiceSetValueInsertOptions extends EntityFolderScopedOptions { /** Human-readable display name */ displayName?: string; } +/** + * Options for {@link ChoiceSetServiceModel.updateValueById} + */ +export interface ChoiceSetValueUpdateOptions extends EntityFolderScopedOptions {} + +/** + * Options for {@link ChoiceSetServiceModel.deleteValuesById} + */ +export interface ChoiceSetValueDeleteOptions extends EntityFolderScopedOptions {} + /** * Response returned after inserting a choice-set value — the full value object. */ diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index 9a6f1389d..e4f40c287 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -27,6 +27,7 @@ import { EntityImportRecordsByIdOptions, EntityCreateOptions, EntityCreateFieldOptions, + EntityGetAllOptions, EntityGetByIdOptions, EntityDeleteByIdOptions, EntityDeleteRecordByIdOptions, @@ -52,23 +53,33 @@ import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '. */ export interface EntityServiceModel { /** - * Gets all entities in the system - * - * @returns Promise resolving to either an array of entities NonPaginatedResponse or a PaginatedResponse when pagination options are used. + * Gets entities in the system + * + * The Data Fabric entity list is scoped exclusively, not additively: + * omitting `folderKey` returns only tenant-level entities; passing + * `folderKey` returns only entities in that folder. To enumerate + * every entity across folders, call `getAll()` once per folder + * plus once with no `folderKey` for the tenant scope. + * + * @param options - Optional {@link EntityGetAllOptions} (e.g. `folderKey` to list folder-scoped entities) + * @returns Promise resolving to an array of entity metadata * {@link EntityGetResponse} * @example * ```typescript - * // Get all entities - * const allEntities = await entities.getAll(); + * // Get tenant-level entities + * const tenantEntities = await entities.getAll(); + * + * // Get folder-scoped entities for a specific folder + * const folderEntities = await entities.getAll({ folderKey: "" }); * * // Iterate through entities - * allEntities.forEach(entity => { + * tenantEntities.forEach(entity => { * console.log(`Entity: ${entity.displayName} (${entity.name})`); * console.log(`Type: ${entity.entityType}`); * }); * * // Find a specific entity by name - * const customerEntity = allEntities.find(e => e.name === 'Customer'); + * const customerEntity = tenantEntities.find(e => e.name === 'Customer'); * * // Use entity methods directly * if (customerEntity) { @@ -86,7 +97,7 @@ export interface EntityServiceModel { * } * ``` */ - getAll(): Promise; + getAll(options?: EntityGetAllOptions): Promise; /** * Gets entity metadata by entity ID with attached operation methods @@ -157,6 +168,9 @@ export interface EntityServiceModel { * cursor: paginatedResponse.nextCursor, * expansionLevel: 1 * }); + * + * // Folder-scoped entity: pass the entity's folder key + * const records = await entities.getAllRecords("", { folderKey: "" }); * ``` */ getAllRecords(entityId: string, options?: T): Promise< @@ -196,6 +210,11 @@ export interface EntityServiceModel { * const record = await entities.getRecordById(, recordId, { * expansionLevel: 1 * }); + * + * // Folder-scoped entity: pass the entity's folder key + * const record = await entities.getRecordById(, recordId, { + * folderKey: "" + * }); * ``` */ getRecordById(entityId: string, recordId: string, options?: EntityGetRecordByIdOptions): Promise; @@ -220,6 +239,11 @@ export interface EntityServiceModel { * const result = await entities.insertRecordById(, { name: "John", age: 30 }, { * expansionLevel: 1 * }); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.insertRecordById(, { name: "John", age: 30 }, { + * folderKey: "" + * }); * ``` */ insertRecordById(id: string, data: Record, options?: EntityInsertRecordOptions): Promise; @@ -257,6 +281,12 @@ export interface EntityServiceModel { * expansionLevel: 1, * failOnFirst: true * }); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.insertRecordsById(, [ + * { name: "John", age: 30 }, + * { name: "Jane", age: 25 } + * ], { folderKey: "" }); * ``` */ insertRecordsById(id: string, data: Record[], options?: EntityInsertRecordsOptions): Promise; @@ -288,6 +318,11 @@ export interface EntityServiceModel { * const result = await entities.updateRecordById(, , { name: "John Updated", age: 31 }, { * expansionLevel: 1 * }); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.updateRecordById(, , { name: "John Updated" }, { + * folderKey: "" + * }); * ``` */ updateRecordById(entityId: string, recordId: string, data: Record, options?: EntityUpdateRecordOptions): Promise; @@ -318,6 +353,11 @@ export interface EntityServiceModel { * expansionLevel: 1, * failOnFirst: true * }); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.updateRecordsById(, [ + * { Id: "123", name: "John Updated" } + * ], { folderKey: "" }); * ``` */ updateRecordsById(id: string, data: EntityRecord[], options?: EntityUpdateRecordsOptions): Promise; @@ -339,6 +379,11 @@ export interface EntityServiceModel { * const result = await entities.deleteRecordsById(, [ * , * ]); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.deleteRecordsById(, [ + * , + * ], { folderKey: "" }); * ``` */ deleteRecordsById(id: string, recordIds: string[], options?: EntityDeleteRecordsOptions): Promise; @@ -405,6 +450,12 @@ export interface EntityServiceModel { * ], * }); * + * // Folder-scoped entity: pass the entity's folder key + * await entities.queryRecordsById(, { + * filterGroup: { queryFilters: [{ fieldName: "status", operator: QueryFilterOperator.Equals, value: "active" }] }, + * folderKey: "", + * }); + * * // Aggregate: total sum and average across all records (no grouping) * await entities.queryRecordsById(, { * aggregates: [ diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index 94719f18a..678850ae8 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -322,6 +322,11 @@ export interface EntityFolderScopedOptions { folderKey?: string; } +/** + * Options for {@link EntityServiceModel.getAll} + */ +export interface EntityGetAllOptions extends EntityFolderScopedOptions {} + /** * Options for {@link EntityServiceModel.getById} */ diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index ed503b2f3..5bf3c5677 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -1,8 +1,23 @@ import { BaseService } from '../base'; import { ChoiceSetServiceModel } from '../../models/data-fabric/choicesets.models'; -import { ChoiceSetGetAllResponse, ChoiceSetGetResponse, ChoiceSetGetByIdOptions, ChoiceSetCreateOptions, ChoiceSetUpdateOptions, ChoiceSetValueInsertOptions, ChoiceSetValueInsertResponse, ChoiceSetValueUpdateResponse } from '../../models/data-fabric/choicesets.types'; +import { + ChoiceSetGetAllOptions, + ChoiceSetGetAllResponse, + ChoiceSetGetResponse, + ChoiceSetGetByIdOptions, + ChoiceSetCreateOptions, + ChoiceSetUpdateOptions, + ChoiceSetDeleteByIdOptions, + ChoiceSetValueInsertOptions, + ChoiceSetValueInsertResponse, + ChoiceSetValueUpdateOptions, + ChoiceSetValueUpdateResponse, + ChoiceSetValueDeleteOptions, +} from '../../models/data-fabric/choicesets.types'; import { RawChoiceSetGetAllResponse, RawChoiceSetGetResponse } from '../../models/data-fabric/choicesets.internal-types'; import { DATA_FABRIC_ENDPOINTS, DATA_FABRIC_TENANT_FOLDER_ID } from '../../utils/constants/endpoints/data-fabric'; +import { FOLDER_KEY } from '../../utils/constants/headers'; +import { createHeaders } from '../../utils/http/headers'; import { transformData, pascalToCamelCaseKeys } from '../../utils/transform'; import { EntityMap } from '../../models/data-fabric/entities.constants'; import { track } from '../../core/telemetry'; @@ -14,8 +29,13 @@ import { ValidationError, NotFoundError } from '../../core/errors'; export class ChoiceSetService extends BaseService implements ChoiceSetServiceModel { /** - * Gets all choice sets in the system + * Gets choice sets in the system * + * The Data Fabric choice-set list is scoped exclusively, not additively: + * omitting `folderKey` returns only tenant-level choice sets; passing + * `folderKey` returns only choice sets in that folder. + * + * @param options - Optional {@link ChoiceSetGetAllOptions} (e.g. `folderKey` to list folder-scoped choice sets) * @returns Promise resolving to an array of choice set metadata * * @example @@ -24,20 +44,18 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * * const choiceSets = new ChoiceSets(sdk); * - * // Get all choice sets - * const allChoiceSets = await choiceSets.getAll(); + * // Get tenant-level choice sets + * const tenantChoiceSets = await choiceSets.getAll(); * - * // Iterate through choice sets - * allChoiceSets.forEach(choiceSet => { - * console.log(`ChoiceSet: ${choiceSet.displayName} (${choiceSet.name})`); - * console.log(`Description: ${choiceSet.description}`); - * }); + * // Get folder-scoped choice sets + * const folderChoiceSets = await choiceSets.getAll({ folderKey: "" }); * ``` */ @track('Choicesets.GetAll') - async getAll(): Promise { + async getAll(options?: ChoiceSetGetAllOptions): Promise { const rawResponse = await this.get( - DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL + DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, + { headers: createHeaders({ [FOLDER_KEY]: options?.folderKey }) } ); // Transform field names @@ -101,11 +119,16 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod return transformData(camelCased, EntityMap) as unknown as ChoiceSetGetResponse; }; + // folderKey is header-only — destructure it out so PaginationHelpers doesn't + // include it in the POST body alongside pagination params. + const { folderKey, ...rest } = options ?? {} as T; + const downstreamOptions = options === undefined ? undefined : (rest as T); return PaginationHelpers.getAll({ serviceAccess: this.createPaginationServiceAccess(), getEndpoint: () => DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_BY_ID(choiceSetId), transformFn, method: HTTP_METHODS.POST, + headers: createHeaders({ [FOLDER_KEY]: folderKey }), pagination: { paginationType: PaginationType.OFFSET, itemsField: CHOICESET_VALUES_PAGINATION.ITEMS_FIELD, @@ -116,7 +139,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod countParam: ENTITY_OFFSET_PARAMS.COUNT_PARAM } } - }, options) as any; + }, downstreamOptions) as any; } /** @@ -157,7 +180,11 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod folderId: opts.folderKey ?? DATA_FABRIC_TENANT_FOLDER_ID, }, }; - const response = await this.post(DATA_FABRIC_ENDPOINTS.CHOICESETS.CREATE, payload); + const response = await this.post( + DATA_FABRIC_ENDPOINTS.CHOICESETS.CREATE, + payload, + { headers: createHeaders({ [FOLDER_KEY]: opts.folderKey }) }, + ); return response.data; } @@ -191,10 +218,14 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod message: 'updateById requires at least one of displayName or description.', }); } - await this.patch(DATA_FABRIC_ENDPOINTS.CHOICESETS.UPDATE(choiceSetId), { - ...(options.displayName !== undefined && { displayName: options.displayName }), - ...(options.description !== undefined && { description: options.description }), - }); + await this.patch( + DATA_FABRIC_ENDPOINTS.CHOICESETS.UPDATE(choiceSetId), + { + ...(options.displayName !== undefined && { displayName: options.displayName }), + ...(options.description !== undefined && { description: options.description }), + }, + { headers: createHeaders({ [FOLDER_KEY]: options.folderKey }) }, + ); } /** @@ -214,8 +245,12 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * @internal */ @track('Choicesets.DeleteById') - async deleteById(choiceSetId: string): Promise { - await this.post(DATA_FABRIC_ENDPOINTS.CHOICESETS.DELETE(choiceSetId), {}); + async deleteById(choiceSetId: string, options?: ChoiceSetDeleteByIdOptions): Promise { + await this.post( + DATA_FABRIC_ENDPOINTS.CHOICESETS.DELETE(choiceSetId), + {}, + { headers: createHeaders({ [FOLDER_KEY]: options?.folderKey }) }, + ); } /** @@ -236,6 +271,12 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * displayName: 'Travel', * }); * console.log(inserted.id); + * + * // Folder-scoped choice set: folderKey is required on the wire + * await choicesets.insertValueById(expenseTypes.id, 'TRAVEL', { + * displayName: 'Travel', + * folderKey: "", + * }); * ``` * @internal */ @@ -245,14 +286,15 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod name: string, options?: ChoiceSetValueInsertOptions, ): Promise { - const choiceSetName = await this.resolveChoiceSetName(choiceSetId); + const choiceSetName = await this.resolveChoiceSetName(choiceSetId, options?.folderKey); const payload = { Name: name, ...(options?.displayName !== undefined && { DisplayName: options.displayName }), }; const response = await this.post( DATA_FABRIC_ENDPOINTS.CHOICESETS.INSERT_BY_NAME(choiceSetName), - payload + payload, + { headers: createHeaders({ [FOLDER_KEY]: options?.folderKey }) }, ); const camelCased = pascalToCamelCaseKeys(response.data); return transformData(camelCased, EntityMap) as ChoiceSetValueInsertResponse; @@ -278,6 +320,11 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * const travel = values.items.find(v => v.name === 'TRAVEL'); * * await choicesets.updateValueById(expenseTypes.id, travel.id, 'Business Travel'); + * + * // Folder-scoped choice set: folderKey is required on the wire + * await choicesets.updateValueById(expenseTypes.id, travel.id, 'Business Travel', { + * folderKey: "", + * }); * ``` * @internal */ @@ -286,12 +333,14 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod choiceSetId: string, valueId: string, displayName: string, + options?: ChoiceSetValueUpdateOptions, ): Promise { - const choiceSetName = await this.resolveChoiceSetName(choiceSetId); + const choiceSetName = await this.resolveChoiceSetName(choiceSetId, options?.folderKey); const payload = { DisplayName: displayName }; const response = await this.post( DATA_FABRIC_ENDPOINTS.CHOICESETS.UPDATE_BY_NAME(choiceSetName, valueId), - payload + payload, + { headers: createHeaders({ [FOLDER_KEY]: options?.folderKey }) }, ); const camelCased = pascalToCamelCaseKeys(response.data); return transformData(camelCased, EntityMap) as ChoiceSetValueUpdateResponse; @@ -315,12 +364,16 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * @internal */ @track('Choicesets.DeleteValuesById') - async deleteValuesById(choiceSetId: string, valueIds: string[]): Promise { - await this.post(DATA_FABRIC_ENDPOINTS.CHOICESETS.DELETE_BY_ID(choiceSetId), valueIds); + async deleteValuesById(choiceSetId: string, valueIds: string[], options?: ChoiceSetValueDeleteOptions): Promise { + await this.post( + DATA_FABRIC_ENDPOINTS.CHOICESETS.DELETE_BY_ID(choiceSetId), + valueIds, + { headers: createHeaders({ [FOLDER_KEY]: options?.folderKey }) }, + ); } - private async resolveChoiceSetName(choiceSetId: string): Promise { - const all = await this.getAll(); + private async resolveChoiceSetName(choiceSetId: string, folderKey?: string): Promise { + const all = await this.getAll(folderKey !== undefined ? { folderKey } : undefined); const match = all.find(cs => cs.id === choiceSetId); if (!match) { throw new NotFoundError({ message: `Choice set with id '${choiceSetId}' not found.` }); diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 89e0dd9cf..74c3dc946 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -32,6 +32,7 @@ import { EntityCreateOptions, EntityCreateFieldOptions, EntityFieldDataType, + EntityGetAllOptions, EntityGetByIdOptions, EntityDeleteByIdOptions, EntityDeleteRecordByIdOptions, @@ -144,6 +145,9 @@ export class EntityService extends BaseService implements EntityServiceModel { * cursor: paginatedResponse.nextCursor, * expansionLevel: 1 * }); + * + * // Folder-scoped entity: pass the entity's folder key + * const records = await entities.getAllRecords("", { folderKey: "" }); * ``` */ @track('Entities.GetAllRecords') @@ -182,7 +186,7 @@ export class EntityService extends BaseService implements EntityServiceModel { * * @param entityId - UUID of the entity * @param recordId - UUID of the record - * @param options - Query options including expansionLevel + * @param options - Query options including `expansionLevel` and `folderKey` * @returns Promise resolving to the entity record * * @example @@ -194,6 +198,11 @@ export class EntityService extends BaseService implements EntityServiceModel { * const record = await sdk.entities.getRecordById(, , { * expansionLevel: 1 * }); + * + * // Folder-scoped entity: pass the entity's folder key + * const record = await sdk.entities.getRecordById(, , { + * folderKey: "" + * }); * ``` */ @track('Entities.GetRecordById') @@ -235,6 +244,11 @@ export class EntityService extends BaseService implements EntityServiceModel { * const result = await entities.insertRecordById("", { name: "John", age: 30 }, { * expansionLevel: 1 * }); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.insertRecordById("", { name: "John", age: 30 }, { + * folderKey: "" + * }); * ``` */ @track('Entities.InsertRecordById') @@ -283,6 +297,12 @@ export class EntityService extends BaseService implements EntityServiceModel { * expansionLevel: 1, * failOnFirst: true * }); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.insertRecordsById("", [ + * { name: "John", age: 30 }, + * { name: "Jane", age: 25 } + * ], { folderKey: "" }); * ``` */ @track('Entities.InsertRecordsById') @@ -326,6 +346,11 @@ export class EntityService extends BaseService implements EntityServiceModel { * const result = await entities.updateRecordById("", "", { name: "John Updated", age: 31 }, { * expansionLevel: 1 * }); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.updateRecordById("", "", { name: "John Updated" }, { + * folderKey: "" + * }); * ``` */ @track('Entities.UpdateRecordById') @@ -375,6 +400,11 @@ export class EntityService extends BaseService implements EntityServiceModel { * expansionLevel: 1, * failOnFirst: true * }); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.updateRecordsById("", [ + * { Id: "123", name: "John Updated" } + * ], { folderKey: "" }); * ``` */ @track('Entities.UpdateRecordsById') @@ -416,6 +446,11 @@ export class EntityService extends BaseService implements EntityServiceModel { * const result = await entities.deleteRecordsById("", [ * "", "" * ]); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.deleteRecordsById("", [ + * "", "" + * ], { folderKey: "" }); * ``` */ @track('Entities.DeleteRecordsById') @@ -469,6 +504,13 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Gets all entities in the system * + * The Data Fabric entity list is scoped exclusively, not additively: + * omitting `folderKey` returns only tenant-level entities; passing + * `folderKey` returns only entities in that folder. To enumerate + * every entity across folders, call `getAll()` once per folder + * plus once with no `folderKey` for the tenant scope. + * + * @param options - Optional {@link EntityGetAllOptions} (e.g. `folderKey` to list folder-scoped entities) * @returns Promise resolving to an array of entity metadata * * @example @@ -477,17 +519,21 @@ export class EntityService extends BaseService implements EntityServiceModel { * * const entities = new Entities(sdk); * - * // Get all entities - * const allEntities = await entities.getAll(); + * // Get tenant-level entities + * const tenantEntities = await entities.getAll(); + * + * // Get folder-scoped entities + * const folderEntities = await entities.getAll({ folderKey: "" }); * * // Call operations on an entity - * const records = await allEntities[0].getAllRecords(); + * const records = await tenantEntities[0].getAllRecords(); * ``` */ @track('Entities.GetAll') - async getAll(): Promise { + async getAll(options?: EntityGetAllOptions): Promise { const response = await this.get( - DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL + DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, + { headers: createHeaders({ [FOLDER_KEY]: options?.folderKey }) } ); // Apply transformations @@ -553,6 +599,12 @@ export class EntityService extends BaseService implements EntityServiceModel { * { function: EntityAggregateFunction.Avg, field: "amount", alias: "avgAmount" }, * ], * }); + * + * // Folder-scoped entity: pass the entity's folder key + * await entities.queryRecordsById("", { + * filterGroup: { queryFilters: [{ fieldName: "status", operator: QueryFilterOperator.Equals, value: "active" }] }, + * folderKey: "", + * }); * ``` */ @track('Entities.QueryRecordsById') diff --git a/tests/integration/shared/data-fabric/choicesets.integration.test.ts b/tests/integration/shared/data-fabric/choicesets.integration.test.ts index 5ff2d082d..ee9efd456 100644 --- a/tests/integration/shared/data-fabric/choicesets.integration.test.ts +++ b/tests/integration/shared/data-fabric/choicesets.integration.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, afterAll } from 'vitest'; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { getServices, getTestConfig, setupUnifiedTests, InitMode } from '../../config/unified-setup'; import { generateRandomString } from '../../utils/helpers'; @@ -262,4 +262,134 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = serviceLevelValueIds.length = 0; }); }); + + // Skipped: folder-scoped choice-set value CRUD requires the DataFabric.Schema.Write + // OAuth scope (same restriction as the tenant-scope value CRUD block above), which + // PAT-authenticated CI runs do not have. Re-enable locally with describe.only when + // INTEGRATION_TEST_FOLDER_KEY is set and OAuth is configured. + describe.skip('Folder-scoped operations', () => { + let folderKey!: string; + let folderChoiceSetId!: string; + const folderValueIds: string[] = []; + + beforeAll(() => { + const config = getTestConfig(); + if (!config.folderKey) { + throw new Error('INTEGRATION_TEST_FOLDER_KEY is required for folder-scoped choice-set tests'); + } + folderKey = config.folderKey; + }); + + afterAll(async () => { + const { choiceSets } = getServices(); + + if (folderChoiceSetId && folderValueIds.length > 0) { + try { + await choiceSets.deleteValuesById(folderChoiceSetId, folderValueIds, { folderKey }); + } catch { + // Ignore cleanup failures — test resources are sandboxed. + } + } + if (folderChoiceSetId) { + try { + await choiceSets.deleteById(folderChoiceSetId, { folderKey }); + } catch { + // Ignore cleanup failures — test resources are sandboxed. + } + } + }); + + it('should return only folder-scoped choice sets when folderKey is provided', async () => { + const { choiceSets } = getServices(); + + const [tenantSets, folderSets] = await Promise.all([ + choiceSets.getAll(), + choiceSets.getAll({ folderKey }), + ]); + + expect(Array.isArray(folderSets)).toBe(true); + + // Every folder-scoped choice set carries the requested folder key + for (const cs of folderSets) { + expect(cs.folderId).toBe(folderKey); + } + + // Tenant scope and folder scope are disjoint + const folderIds = new Set(folderSets.map((cs) => cs.id)); + for (const tenantSet of tenantSets) { + expect(folderIds.has(tenantSet.id)).toBe(false); + } + }); + + it('should create a folder-scoped choice set, list its values, and delete it', async () => { + const { choiceSets } = getServices(); + const name = `sdk_cs_fld_${generateRandomString(8)}`; + + folderChoiceSetId = await choiceSets.create(name, { + displayName: `SDK Folder ${name}`, + folderKey, + }); + expect(typeof folderChoiceSetId).toBe('string'); + + // The new choice set should appear in the folder-scoped listing + const folderSets = await choiceSets.getAll({ folderKey }); + const found = folderSets.find((cs) => cs.id === folderChoiceSetId); + expect(found).toBeDefined(); + expect(found?.folderId).toBe(folderKey); + + // ...and NOT in the tenant listing + const tenantSets = await choiceSets.getAll(); + expect(tenantSets.find((cs) => cs.id === folderChoiceSetId)).toBeUndefined(); + + // getById on the new (empty) choice set should succeed with folderKey + const values = await choiceSets.getById(folderChoiceSetId, { folderKey }); + expect(Array.isArray(values.items)).toBe(true); + }); + + it('should require folderKey to insert and update values on a folder-scoped choice set', async () => { + const { choiceSets } = getServices(); + + if (!folderChoiceSetId) { + throw new Error('Folder-scoped choice set was not created in the previous test'); + } + + const valueName = `SDK_FLD_${generateRandomString(6)}`; + const inserted = await choiceSets.insertValueById(folderChoiceSetId, valueName, { + displayName: 'Travel (folder)', + folderKey, + }); + expect(inserted.id).toBeDefined(); + folderValueIds.push(inserted.id); + + const updated = await choiceSets.updateValueById( + folderChoiceSetId, + inserted.id, + 'Travel (renamed)', + { folderKey }, + ); + expect(updated.displayName).toBe('Travel (renamed)'); + }); + + it('should delete a folder-scoped choice set with folderKey', async () => { + const { choiceSets } = getServices(); + + if (!folderChoiceSetId) { + throw new Error('Folder-scoped choice set was not created earlier in the suite'); + } + + // Remove leftover values first so deleteById succeeds + if (folderValueIds.length > 0) { + await choiceSets.deleteValuesById(folderChoiceSetId, folderValueIds, { folderKey }); + folderValueIds.length = 0; + } + + await choiceSets.deleteById(folderChoiceSetId, { folderKey }); + + const folderSets = await choiceSets.getAll({ folderKey }); + expect(folderSets.find((cs) => cs.id === folderChoiceSetId)).toBeUndefined(); + + // Mark as deleted so afterAll doesn't try to delete it again + folderChoiceSetId = ''; + }); + }); }); diff --git a/tests/integration/shared/data-fabric/entities.integration.test.ts b/tests/integration/shared/data-fabric/entities.integration.test.ts index 4e51dbca0..1d9273b22 100644 --- a/tests/integration/shared/data-fabric/entities.integration.test.ts +++ b/tests/integration/shared/data-fabric/entities.integration.test.ts @@ -198,6 +198,36 @@ describe.each(modes)('Data Fabric Entities - Integration Tests [%s]', (mode) => expect(typeof entity.getRecord).toBe('function'); expect(typeof entity.downloadAttachment).toBe('function'); }); + + // The Data Fabric entity list is scoped exclusively: omitting folderKey returns + // only tenant entities; passing folderKey returns only entities in that folder. + // The two sets are disjoint. + it('should return only folder-scoped entities when folderKey is provided', async () => { + const { entities } = getServices(); + const folderKey = getTestConfig().folderKey; + + if (!folderKey) { + throw new Error('INTEGRATION_TEST_FOLDER_KEY is required to exercise folder-scoped getAll'); + } + + const [tenantEntities, folderEntities] = await Promise.all([ + entities.getAll(), + entities.getAll({ folderKey }), + ]); + + expect(Array.isArray(folderEntities)).toBe(true); + + // Every folder-scoped entity carries the requested folder key + for (const entity of folderEntities) { + expect(entity.folderId).toBe(folderKey); + } + + // Tenant scope and folder scope are disjoint — no entity appears in both + const folderIds = new Set(folderEntities.map((e) => e.id)); + for (const tenantEntity of tenantEntities) { + expect(folderIds.has(tenantEntity.id)).toBe(false); + } + }); }); describe('getById', () => { diff --git a/tests/unit/services/data-fabric/choicesets.test.ts b/tests/unit/services/data-fabric/choicesets.test.ts index 74514d0e3..85a71a0d6 100644 --- a/tests/unit/services/data-fabric/choicesets.test.ts +++ b/tests/unit/services/data-fabric/choicesets.test.ts @@ -81,7 +81,7 @@ describe('ChoiceSetService Unit Tests', () => { // Verify the API call has correct endpoint expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, - {} + { headers: {} } ); }); @@ -110,7 +110,7 @@ describe('ChoiceSetService Unit Tests', () => { // Verify the API call expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, - {} + { headers: {} } ); }); @@ -338,7 +338,7 @@ describe('ChoiceSetService Unit Tests', () => { folderId: DATA_FABRIC_TENANT_FOLDER_ID, }, }, - {}, + { headers: {} }, ); }); @@ -389,7 +389,7 @@ describe('ChoiceSetService Unit Tests', () => { displayName: 'Renamed Choice Set', description: 'Updated description', }, - {}, + { headers: {} }, ); }); @@ -447,7 +447,7 @@ describe('ChoiceSetService Unit Tests', () => { expect(mockApiClient.post).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.CHOICESETS.DELETE(CHOICESET_TEST_CONSTANTS.CHOICESET_ID), {}, - {}, + { headers: {} }, ); }); @@ -478,7 +478,7 @@ describe('ChoiceSetService Unit Tests', () => { expect(mockApiClient.post).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.CHOICESETS.INSERT_BY_NAME(CHOICESET_TEST_CONSTANTS.CHOICESET_NAME), { Name: 'NEW_VAL', DisplayName: 'New Value' }, - {}, + { headers: {} }, ); expect(result.id).toBe(CHOICESET_TEST_CONSTANTS.VALUE_ID); @@ -546,7 +546,7 @@ describe('ChoiceSetService Unit Tests', () => { CHOICESET_TEST_CONSTANTS.VALUE_ID, ), { DisplayName: 'Updated' }, - {}, + { headers: {} }, ); expect(result.id).toBe(CHOICESET_TEST_CONSTANTS.VALUE_ID); @@ -598,7 +598,7 @@ describe('ChoiceSetService Unit Tests', () => { expect(mockApiClient.post).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.CHOICESETS.DELETE_BY_ID(CHOICESET_TEST_CONSTANTS.CHOICESET_ID), valueIds, - {}, + { headers: {} }, ); }); diff --git a/tests/unit/services/data-fabric/entities.test.ts b/tests/unit/services/data-fabric/entities.test.ts index 5fc766040..7a7b3f416 100644 --- a/tests/unit/services/data-fabric/entities.test.ts +++ b/tests/unit/services/data-fabric/entities.test.ts @@ -339,7 +339,7 @@ describe("EntityService Unit Tests", () => { // Verify the API call expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, - {}, + { headers: {} }, ); expect(result[0].fields).toBeDefined(); @@ -398,7 +398,7 @@ describe("EntityService Unit Tests", () => { // Verify the API call expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, - {}, + { headers: {} }, ); }); From dd49734db0633415586627e5191384e3729fe5fa Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Wed, 10 Jun 2026 18:52:40 +0530 Subject: [PATCH 02/23] =?UTF-8?q?fix:=20address=20review=20comments=20?= =?UTF-8?q?=E2=80=94=20JSDoc=20sync,=20single=20afterAll,=20folderKey=20he?= =?UTF-8?q?ader=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sync ChoiceSetServiceModel JSDoc with service-class JSDoc on getById, deleteById, and deleteValuesById (model is the docs source of truth). - Service-class JSDoc on the same three methods updated with @param options and folder-scoped @example blocks for parity. - Consolidate folder-scoped choice-set integration cleanup into the file-level afterAll (removes the duplicate afterAll inside the describe.skip block per the rules.md convention). - Add unit tests asserting X-UIPATH-FolderKey forwarding for every choice-set method that gained folderKey support (getAll, getById, create, updateById, deleteById, insertValueById, updateValueById, deleteValuesById) and for entities.getAll, matching the pattern already used on other Entities methods. Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/choicesets.models.ts | 4 +- src/services/data-fabric/choicesets.ts | 13 +- .../choicesets.integration.test.ts | 85 ++++++----- .../services/data-fabric/choicesets.test.ts | 143 ++++++++++++++++++ .../services/data-fabric/entities.test.ts | 11 ++ tests/utils/constants/choicesets.ts | 3 + 6 files changed, 216 insertions(+), 43 deletions(-) diff --git a/src/models/data-fabric/choicesets.models.ts b/src/models/data-fabric/choicesets.models.ts index 71fa06169..158491632 100644 --- a/src/models/data-fabric/choicesets.models.ts +++ b/src/models/data-fabric/choicesets.models.ts @@ -78,7 +78,7 @@ export interface ChoiceSetServiceModel { * - A PaginatedResponse with navigation cursors (when any pagination parameter is provided) * * @param choiceSetId - UUID of the choice set - * @param options - Pagination options + * @param options - Pagination options and optional `folderKey` (omit for tenant-level choice sets) * @returns Promise resolving to choice set values or paginated result * {@link ChoiceSetGetResponse} * @example @@ -170,6 +170,7 @@ export interface ChoiceSetServiceModel { * Deletes a Data Fabric choice set and all its values. * * @param choiceSetId - UUID of the choice set to delete + * @param options - Optional {@link ChoiceSetDeleteByIdOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level * @returns Promise resolving when the choice set is deleted * * @example @@ -260,6 +261,7 @@ export interface ChoiceSetServiceModel { * * @param choiceSetId - UUID of the parent choice set * @param valueIds - Array of value UUIDs to delete + * @param options - Optional {@link ChoiceSetValueDeleteOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level * @returns Promise resolving when the values are deleted * * @example diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index 5bf3c5677..5d36b039c 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -73,7 +73,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * - A PaginatedResponse with navigation cursors (when any pagination parameter is provided) * * @param choiceSetId - UUID of the choice set - * @param options - Pagination options + * @param options - Pagination options and optional `folderKey` for folder-scoped choice sets * @returns Promise resolving to choice set values or paginated result * * @example @@ -102,6 +102,9 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * if (page1.hasNextPage) { * const page2 = await choiceSets.getById(choiceSetId, { cursor: page1.nextCursor }); * } + * + * // Folder-scoped choice set + * const folderValues = await choiceSets.getById(choiceSetId, { folderKey: "" }); * ``` */ @track('Choicesets.GetById') @@ -232,6 +235,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * Deletes a Data Fabric choice set and all its values. * * @param choiceSetId - UUID of the choice set to delete + * @param options - Optional {@link ChoiceSetDeleteByIdOptions} (e.g. `folderKey` for folder-scoped choice sets) * @returns Promise resolving when the choice set is deleted * * @example @@ -241,6 +245,9 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * const expenseTypes = allChoiceSets.find(cs => cs.name === 'expense_types'); * * await choicesets.deleteById(expenseTypes.id); + * + * // Folder-scoped choice set + * await choicesets.deleteById(expenseTypes.id, { folderKey: "" }); * ``` * @internal */ @@ -351,6 +358,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * * @param choiceSetId - UUID of the parent choice set * @param valueIds - Array of value UUIDs to delete + * @param options - Optional {@link ChoiceSetValueDeleteOptions} (e.g. `folderKey` for folder-scoped choice sets) * @returns Promise resolving when the values are deleted * * @example @@ -360,6 +368,9 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * const idsToDelete = values.items.slice(0, 2).map(v => v.id); * * await choicesets.deleteValuesById('', idsToDelete); + * + * // Folder-scoped choice set + * await choicesets.deleteValuesById('', idsToDelete, { folderKey: "" }); * ``` * @internal */ diff --git a/tests/integration/shared/data-fabric/choicesets.integration.test.ts b/tests/integration/shared/data-fabric/choicesets.integration.test.ts index ee9efd456..9d0f893b3 100644 --- a/tests/integration/shared/data-fabric/choicesets.integration.test.ts +++ b/tests/integration/shared/data-fabric/choicesets.integration.test.ts @@ -11,6 +11,12 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = const createdChoiceSetIds: string[] = []; const insertedValueIds: string[] = []; + // Folder-scoped resources tracked separately so the single top-level afterAll + // can clean them with their captured folderKey. Populated by the folder block. + let folderScopedChoiceSetId: string | null = null; + let folderScopedFolderKey: string | null = null; + const folderScopedValueIds: string[] = []; + afterAll(async () => { const { choiceSets } = getServices(); @@ -23,6 +29,22 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = } } + // Folder-scoped cleanup (only populated when the folder-scoped describe block runs) + if (folderScopedChoiceSetId && folderScopedFolderKey && folderScopedValueIds.length > 0) { + try { + await choiceSets.deleteValuesById(folderScopedChoiceSetId, folderScopedValueIds, { folderKey: folderScopedFolderKey }); + } catch { + // Ignore cleanup failures — test resources are sandboxed. + } + } + if (folderScopedChoiceSetId && folderScopedFolderKey) { + try { + await choiceSets.deleteById(folderScopedChoiceSetId, { folderKey: folderScopedFolderKey }); + } catch { + // Ignore cleanup failures — test resources are sandboxed. + } + } + for (const id of createdChoiceSetIds) { try { await choiceSets.deleteById(id); @@ -268,39 +290,17 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = // PAT-authenticated CI runs do not have. Re-enable locally with describe.only when // INTEGRATION_TEST_FOLDER_KEY is set and OAuth is configured. describe.skip('Folder-scoped operations', () => { - let folderKey!: string; - let folderChoiceSetId!: string; - const folderValueIds: string[] = []; - beforeAll(() => { const config = getTestConfig(); if (!config.folderKey) { throw new Error('INTEGRATION_TEST_FOLDER_KEY is required for folder-scoped choice-set tests'); } - folderKey = config.folderKey; - }); - - afterAll(async () => { - const { choiceSets } = getServices(); - - if (folderChoiceSetId && folderValueIds.length > 0) { - try { - await choiceSets.deleteValuesById(folderChoiceSetId, folderValueIds, { folderKey }); - } catch { - // Ignore cleanup failures — test resources are sandboxed. - } - } - if (folderChoiceSetId) { - try { - await choiceSets.deleteById(folderChoiceSetId, { folderKey }); - } catch { - // Ignore cleanup failures — test resources are sandboxed. - } - } + folderScopedFolderKey = config.folderKey; }); it('should return only folder-scoped choice sets when folderKey is provided', async () => { const { choiceSets } = getServices(); + const folderKey = folderScopedFolderKey!; const [tenantSets, folderSets] = await Promise.all([ choiceSets.getAll(), @@ -323,46 +323,48 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = it('should create a folder-scoped choice set, list its values, and delete it', async () => { const { choiceSets } = getServices(); + const folderKey = folderScopedFolderKey!; const name = `sdk_cs_fld_${generateRandomString(8)}`; - folderChoiceSetId = await choiceSets.create(name, { + folderScopedChoiceSetId = await choiceSets.create(name, { displayName: `SDK Folder ${name}`, folderKey, }); - expect(typeof folderChoiceSetId).toBe('string'); + expect(typeof folderScopedChoiceSetId).toBe('string'); // The new choice set should appear in the folder-scoped listing const folderSets = await choiceSets.getAll({ folderKey }); - const found = folderSets.find((cs) => cs.id === folderChoiceSetId); + const found = folderSets.find((cs) => cs.id === folderScopedChoiceSetId); expect(found).toBeDefined(); expect(found?.folderId).toBe(folderKey); // ...and NOT in the tenant listing const tenantSets = await choiceSets.getAll(); - expect(tenantSets.find((cs) => cs.id === folderChoiceSetId)).toBeUndefined(); + expect(tenantSets.find((cs) => cs.id === folderScopedChoiceSetId)).toBeUndefined(); // getById on the new (empty) choice set should succeed with folderKey - const values = await choiceSets.getById(folderChoiceSetId, { folderKey }); + const values = await choiceSets.getById(folderScopedChoiceSetId, { folderKey }); expect(Array.isArray(values.items)).toBe(true); }); it('should require folderKey to insert and update values on a folder-scoped choice set', async () => { const { choiceSets } = getServices(); + const folderKey = folderScopedFolderKey!; - if (!folderChoiceSetId) { + if (!folderScopedChoiceSetId) { throw new Error('Folder-scoped choice set was not created in the previous test'); } const valueName = `SDK_FLD_${generateRandomString(6)}`; - const inserted = await choiceSets.insertValueById(folderChoiceSetId, valueName, { + const inserted = await choiceSets.insertValueById(folderScopedChoiceSetId, valueName, { displayName: 'Travel (folder)', folderKey, }); expect(inserted.id).toBeDefined(); - folderValueIds.push(inserted.id); + folderScopedValueIds.push(inserted.id); const updated = await choiceSets.updateValueById( - folderChoiceSetId, + folderScopedChoiceSetId, inserted.id, 'Travel (renamed)', { folderKey }, @@ -372,24 +374,25 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = it('should delete a folder-scoped choice set with folderKey', async () => { const { choiceSets } = getServices(); + const folderKey = folderScopedFolderKey!; - if (!folderChoiceSetId) { + if (!folderScopedChoiceSetId) { throw new Error('Folder-scoped choice set was not created earlier in the suite'); } // Remove leftover values first so deleteById succeeds - if (folderValueIds.length > 0) { - await choiceSets.deleteValuesById(folderChoiceSetId, folderValueIds, { folderKey }); - folderValueIds.length = 0; + if (folderScopedValueIds.length > 0) { + await choiceSets.deleteValuesById(folderScopedChoiceSetId, folderScopedValueIds, { folderKey }); + folderScopedValueIds.length = 0; } - await choiceSets.deleteById(folderChoiceSetId, { folderKey }); + await choiceSets.deleteById(folderScopedChoiceSetId, { folderKey }); const folderSets = await choiceSets.getAll({ folderKey }); - expect(folderSets.find((cs) => cs.id === folderChoiceSetId)).toBeUndefined(); + expect(folderSets.find((cs) => cs.id === folderScopedChoiceSetId)).toBeUndefined(); - // Mark as deleted so afterAll doesn't try to delete it again - folderChoiceSetId = ''; + // Clear so the top-level afterAll doesn't try to delete it again + folderScopedChoiceSetId = null; }); }); }); diff --git a/tests/unit/services/data-fabric/choicesets.test.ts b/tests/unit/services/data-fabric/choicesets.test.ts index 85a71a0d6..f8dd7960d 100644 --- a/tests/unit/services/data-fabric/choicesets.test.ts +++ b/tests/unit/services/data-fabric/choicesets.test.ts @@ -153,6 +153,17 @@ describe('ChoiceSetService Unit Tests', () => { await expect(choiceSetService.getAll()).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); + it('should pass folderKey via X-UIPATH-FolderKey header when provided', async () => { + mockApiClient.get.mockResolvedValue([createMockChoiceSetResponse()]); + + await choiceSetService.getAll({ folderKey: CHOICESET_TEST_CONSTANTS.FOLDER_KEY }); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } } + ); + }); + }); describe('getById', () => { @@ -301,6 +312,24 @@ describe('ChoiceSetService Unit Tests', () => { ).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); + it('should forward folderKey as X-UIPATH-FolderKey header but NOT include it in the POST body', async () => { + vi.mocked(PaginationHelpers.getAll).mockResolvedValue({ items: [], totalCount: 0 }); + + await choiceSetService.getById(CHOICESET_TEST_CONSTANTS.CHOICESET_ID, { + folderKey: CHOICESET_TEST_CONSTANTS.FOLDER_KEY, + }); + + const config = vi.mocked(PaginationHelpers.getAll).mock.calls[0][0] as any; + const downstreamOptions = vi.mocked(PaginationHelpers.getAll).mock.calls[0][1] as any; + + // Header is set + expect(config.headers).toEqual({ 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY }); + + // folderKey must be stripped from downstream options so it never leaks into the POST body + expect(downstreamOptions).toBeDefined(); + expect(downstreamOptions.folderKey).toBeUndefined(); + }); + it('should handle empty results gracefully', async () => { const mockResponse = { items: [], @@ -372,6 +401,24 @@ describe('ChoiceSetService Unit Tests', () => { TEST_CONSTANTS.ERROR_MESSAGE, ); }); + + it('should pass folderKey via X-UIPATH-FolderKey header when provided', async () => { + mockApiClient.post.mockResolvedValue(CHOICESET_TEST_CONSTANTS.CHOICESET_ID); + + await choiceSetService.create('expense_types', { + folderKey: CHOICESET_TEST_CONSTANTS.FOLDER_KEY, + }); + + expect(mockApiClient.post).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.CREATE, + expect.objectContaining({ + entityDefinition: expect.objectContaining({ + folderId: CHOICESET_TEST_CONSTANTS.FOLDER_KEY, + }), + }), + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } }, + ); + }); }); describe('updateById', () => { @@ -436,6 +483,21 @@ describe('ChoiceSetService Unit Tests', () => { choiceSetService.updateById(CHOICESET_TEST_CONSTANTS.CHOICESET_ID, { displayName: 'x' }), ).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); + + it('should pass folderKey via X-UIPATH-FolderKey header when provided', async () => { + mockApiClient.patch.mockResolvedValue(true); + + await choiceSetService.updateById(CHOICESET_TEST_CONSTANTS.CHOICESET_ID, { + displayName: 'Renamed', + folderKey: CHOICESET_TEST_CONSTANTS.FOLDER_KEY, + }); + + expect(mockApiClient.patch).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.UPDATE(CHOICESET_TEST_CONSTANTS.CHOICESET_ID), + { displayName: 'Renamed' }, + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } }, + ); + }); }); describe('deleteById', () => { @@ -459,6 +521,20 @@ describe('ChoiceSetService Unit Tests', () => { choiceSetService.deleteById(CHOICESET_TEST_CONSTANTS.CHOICESET_ID), ).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); + + it('should pass folderKey via X-UIPATH-FolderKey header when provided', async () => { + mockApiClient.post.mockResolvedValue(true); + + await choiceSetService.deleteById(CHOICESET_TEST_CONSTANTS.CHOICESET_ID, { + folderKey: CHOICESET_TEST_CONSTANTS.FOLDER_KEY, + }); + + expect(mockApiClient.post).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.DELETE(CHOICESET_TEST_CONSTANTS.CHOICESET_ID), + {}, + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } }, + ); + }); }); describe('insertValueById', () => { @@ -526,6 +602,30 @@ describe('ChoiceSetService Unit Tests', () => { choiceSetService.insertValueById(CHOICESET_TEST_CONSTANTS.CHOICESET_ID, 'NEW_VAL'), ).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); + + it('should pass folderKey via X-UIPATH-FolderKey header on both name resolution and insert', async () => { + mockApiClient.get.mockResolvedValue([createMockChoiceSetResponse()]); + mockApiClient.post.mockResolvedValue(createMockChoiceSetValueResponse()); + + await choiceSetService.insertValueById( + CHOICESET_TEST_CONSTANTS.CHOICESET_ID, + 'NEW_VAL', + { folderKey: CHOICESET_TEST_CONSTANTS.FOLDER_KEY }, + ); + + // Name resolution call (getAll) sends the folder header so folder-scoped sets are found + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } }, + ); + + // The insert call itself also forwards the header (server requires it on name-based endpoint) + expect(mockApiClient.post).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.INSERT_BY_NAME(CHOICESET_TEST_CONSTANTS.CHOICESET_NAME), + { Name: 'NEW_VAL' }, + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } }, + ); + }); }); describe('updateValueById', () => { @@ -583,6 +683,32 @@ describe('ChoiceSetService Unit Tests', () => { ), ).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); + + it('should pass folderKey via X-UIPATH-FolderKey header on both name resolution and update', async () => { + mockApiClient.get.mockResolvedValue([createMockChoiceSetResponse()]); + mockApiClient.post.mockResolvedValue(createMockChoiceSetValueResponse({ DisplayName: 'Renamed' })); + + await choiceSetService.updateValueById( + CHOICESET_TEST_CONSTANTS.CHOICESET_ID, + CHOICESET_TEST_CONSTANTS.VALUE_ID, + 'Renamed', + { folderKey: CHOICESET_TEST_CONSTANTS.FOLDER_KEY }, + ); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } }, + ); + + expect(mockApiClient.post).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.UPDATE_BY_NAME( + CHOICESET_TEST_CONSTANTS.CHOICESET_NAME, + CHOICESET_TEST_CONSTANTS.VALUE_ID, + ), + { DisplayName: 'Renamed' }, + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } }, + ); + }); }); describe('deleteValuesById', () => { @@ -612,5 +738,22 @@ describe('ChoiceSetService Unit Tests', () => { ]), ).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); + + it('should pass folderKey via X-UIPATH-FolderKey header when provided', async () => { + mockApiClient.post.mockResolvedValue(true); + const valueIds = [CHOICESET_TEST_CONSTANTS.VALUE_ID]; + + await choiceSetService.deleteValuesById( + CHOICESET_TEST_CONSTANTS.CHOICESET_ID, + valueIds, + { folderKey: CHOICESET_TEST_CONSTANTS.FOLDER_KEY }, + ); + + expect(mockApiClient.post).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.DELETE_BY_ID(CHOICESET_TEST_CONSTANTS.CHOICESET_ID), + valueIds, + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } }, + ); + }); }); }); diff --git a/tests/unit/services/data-fabric/entities.test.ts b/tests/unit/services/data-fabric/entities.test.ts index 7a7b3f416..7ef1bc953 100644 --- a/tests/unit/services/data-fabric/entities.test.ts +++ b/tests/unit/services/data-fabric/entities.test.ts @@ -410,6 +410,17 @@ describe("EntityService Unit Tests", () => { TEST_CONSTANTS.ERROR_MESSAGE, ); }); + + it("should pass folderKey via X-UIPATH-FolderKey header when provided", async () => { + mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); + + await entityService.getAll({ folderKey: ENTITY_TEST_CONSTANTS.FIELD_ID }); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, + { headers: { "X-UIPATH-FolderKey": ENTITY_TEST_CONSTANTS.FIELD_ID } }, + ); + }); }); describe("getAllRecords", () => { diff --git a/tests/utils/constants/choicesets.ts b/tests/utils/constants/choicesets.ts index cc61e129d..908d45c5c 100644 --- a/tests/utils/constants/choicesets.ts +++ b/tests/utils/constants/choicesets.ts @@ -21,5 +21,8 @@ export const CHOICESET_TEST_CONSTANTS = { VALUE_NAME: 'Application', VALUE_DISPLAY_NAME: 'Application', VALUE_NUMBER_ID: 3, + + // Folder key used to verify X-UIPATH-FolderKey header forwarding + FOLDER_KEY: 'cd6e24d3-2941-4aac-9aba-2ebf38dcceec', } as const; From 1c01d28fa6a726c5de95573a8833cdf73b36e377 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Thu, 11 Jun 2026 16:55:07 +0530 Subject: [PATCH 03/23] feat(data-fabric): cross-folder reference support + folder-scoped integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an opt-in `referenceFolderKey` on EntityCreateFieldOptions so callers can create entities whose RELATIONSHIP / CHOICE_SET fields point at targets in a different folder (or the tenant SYSTEM scope). The SDK assembles the minimum payload each API path needs: - Relationship targets: { id, folderId } — both come straight from caller inputs, no extra round-trip. Verified against alpha: the relationship endpoint resolves by folderId. - Choice-set targets: { id, name, folderId, entityType, entityTypeId } — the CS endpoint resolves by name, so the SDK does one GET on the target to fetch it (parallelized across fields). Verified against alpha: bare {id} or {id, folderId} both 403 with empty target name; {id, name} succeeds. Tenant marker (DATA_FABRIC_TENANT_FOLDER_ID, all-zeros) is supported as a referenceFolderKey value — the SDK looks up the target at tenant scope (no folder header) and emits folderId: '00000000-...'. Integration tests: - Folder-scoped record CRUD (insert/get/update/delete + batch variants, plus queryRecordsById) — runs in CI against DATA_FABRIC_TEST_FOLDER_ENTITY_ID + INTEGRATION_TEST_FOLDER_KEY. - Folder-scoped Choice value CRUD — describe.skip'd alongside the other schema-write blocks (requires DataFabric.Schema.Write OAuth scope); the block locates its target CS by name (`aIntegrationTestFolderScoped`) via getAll({ folderKey }) so it doesn't need a CS-ID env var. - buildDummyRecord helper now threads folderKey into getChoiceSetValues so folder-scoped ChoiceSetSingle/Multiple fields get populated correctly. Unit tests: - 3 new tests covering the new opt-in path: cross-folder relationship (no GET, embeds {id, folderId}), tenant-marker CS lookup (no folder header on the GET), folder-UUID CS lookup (sends X-UIPATH-FolderKey). Co-Authored-By: Claude Opus 4.7 --- .../data-fabric/entities.internal-types.ts | 17 +- src/models/data-fabric/entities.models.ts | 18 ++ src/models/data-fabric/entities.types.ts | 2 + src/services/data-fabric/entities.ts | 74 ++++++++- tests/.env.integration.example | 53 +++--- .../choicesets.integration.test.ts | 156 +++++++++++++----- .../data-fabric/entities.integration.test.ts | 48 +++--- .../services/data-fabric/entities.test.ts | 92 +++++++++++ tests/utils/constants/entities.ts | 2 + 9 files changed, 373 insertions(+), 89 deletions(-) diff --git a/src/models/data-fabric/entities.internal-types.ts b/src/models/data-fabric/entities.internal-types.ts index 20814b6a1..6bbf4edf6 100644 --- a/src/models/data-fabric/entities.internal-types.ts +++ b/src/models/data-fabric/entities.internal-types.ts @@ -20,10 +20,25 @@ export interface FieldSchemaPayload { defaultValue?: string; choiceSetId?: string; referenceType?: ReferenceType; - referenceEntity?: { id: string }; + referenceEntity?: ReferenceEntityPayload; + referenceChoiceSet?: ReferenceEntityPayload; referenceField?: { id: string }; } +/** Body embedded in `referenceEntity` / `referenceChoiceSet` on cross-folder field payloads. */ +export interface ReferenceEntityPayload { + id: string; + name?: string; + folderId?: string; + entityType?: string; + entityTypeId?: number; +} + +export interface ResolvedReferenceMeta { + referenceEntity?: ReferenceEntityPayload; + referenceChoiceSet?: ReferenceEntityPayload; +} + /** * Shape of each entry in EntitySchemaFieldTypeMap — internal only. */ diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index e4f40c287..59807d100 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -645,6 +645,24 @@ export interface EntityServiceModel { * { fieldName: "price", type: EntityFieldDataType.DECIMAL, decimalPrecision: 4, maxValue: 999999, minValue: 0 }, * { fieldName: "quantity", type: EntityFieldDataType.INTEGER, maxValue: 10000, minValue: 1, defaultValue: "0" }, * ]); + * + * // Cross-folder references — link a folder-scoped entity to entities and + * // system choice sets that live in another folder or at the tenant level. + * await entities.create("orderLine", [ + * { + * fieldName: "order", + * type: EntityFieldDataType.RELATIONSHIP, + * referenceEntityId: "", + * referenceFieldId: "", + * referenceFolderKey: "", // target lives in a different folder + * }, + * { + * fieldName: "userType", + * type: EntityFieldDataType.CHOICE_SET_SINGLE, + * choiceSetId: "", // tenant-level system choice set + * // referenceFolderKey omitted → SDK looks up the target at tenant scope + * }, + * ], { folderKey: "" }); * ``` * @internal */ diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index 678850ae8..6903c1132 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -310,6 +310,8 @@ export interface EntityCreateFieldOptions extends EntityFieldBase { referenceEntityId?: string; /** UUID of the referenced field on the target entity (required when `type` is `RELATIONSHIP` or `FILE`) */ referenceFieldId?: string; + /** Folder key of the reference target when it lives outside the source's folder. Pass `DATA_FABRIC_TENANT_FOLDER_ID` for tenant-level system targets. */ + referenceFolderKey?: string; } diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 74c3dc946..bace97294 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -58,7 +58,7 @@ import { ENTITY_FIELD_CONSTRAINT_DEFAULTS, ENTITY_FIELD_CONSTRAINT_SPEC, } from '../../models/data-fabric/entities.constants'; -import { FieldSchemaPayload, SqlFieldType, EntityFieldConstraint } from '../../models/data-fabric/entities.internal-types'; +import { FieldSchemaPayload, SqlFieldType, EntityFieldConstraint, ResolvedReferenceMeta } from '../../models/data-fabric/entities.internal-types'; import { track } from '../../core/telemetry'; /** @@ -875,18 +875,37 @@ export class EntityService extends BaseService implements EntityServiceModel { * { fieldName: "price", type: EntityFieldDataType.DECIMAL, decimalPrecision: 4, maxValue: 999999, minValue: 0 }, * { fieldName: "quantity", type: EntityFieldDataType.INTEGER, maxValue: 10000, minValue: 1, defaultValue: "0" }, * ]); + * + * // Cross-folder references — link a folder-scoped entity to entities and + * // system choice sets that live in another folder or at the tenant level. + * await entities.create("orderLine", [ + * { + * fieldName: "order", + * type: EntityFieldDataType.RELATIONSHIP, + * referenceEntityId: "", + * referenceFieldId: "", + * referenceFolderKey: "", // target lives in a different folder + * }, + * { + * fieldName: "userType", + * type: EntityFieldDataType.CHOICE_SET_SINGLE, + * choiceSetId: "", // tenant-level system choice set + * // referenceFolderKey omitted → SDK looks up the target at tenant scope + * }, + * ], { folderKey: "" }); * ``` * @internal */ @track('Entities.Create') async create(name: string, fields: EntityCreateFieldOptions[], options?: EntityCreateOptions): Promise { const opts = options ?? {}; + const fieldPayloads = await this.buildFieldsWithReferenceMeta(fields); const payload = { ...(opts.description !== undefined && { description: opts.description }), displayName: opts.displayName ?? name, entityDefinition: { name, - fields: fields.map(f => this.buildSchemaFieldPayload(f)), + fields: fieldPayloads, folderId: opts.folderKey ?? DATA_FABRIC_TENANT_FOLDER_ID, isRbacEnabled: opts.isRbacEnabled ?? false, isInsightsEnabled: opts.isAnalyticsEnabled ?? false, @@ -1062,10 +1081,9 @@ export class EntityService extends BaseService implements EntityServiceModel { }); } - // Build and append new fields const newFields: FieldSchemaPayload[] = []; if (options.addFields?.length) { - newFields.push(...options.addFields.map(f => this.buildSchemaFieldPayload(f))); + newFields.push(...await this.buildFieldsWithReferenceMeta(options.addFields)); } await this.post( @@ -1179,8 +1197,47 @@ export class EntityService extends BaseService implements EntityServiceModel { }); } + private async buildFieldsWithReferenceMeta(fields: EntityCreateFieldOptions[]): Promise { + const metas = await Promise.all(fields.map(f => this.buildReferenceMeta(f))); + return fields.map((f, i) => this.buildSchemaFieldPayload(f, metas[i])); + } + + // Choice-set targets resolve server-side by NAME (the API rejects cross-folder + // refs with empty target name even when folderId is supplied), so the SDK + // fetches the name once for each cross-folder choice-set field. Relationship + // targets resolve by folderId — no lookup needed. + private async buildReferenceMeta(field: EntityCreateFieldOptions): Promise { + if (field.referenceFolderKey === undefined) return undefined; + if (field.referenceEntityId === undefined && field.choiceSetId === undefined) return undefined; + + const folderId = field.referenceFolderKey; + const meta: ResolvedReferenceMeta = {}; + + if (field.referenceEntityId !== undefined) { + meta.referenceEntity = { id: field.referenceEntityId, folderId }; + } + if (field.choiceSetId !== undefined) { + const lookupFolderKey = folderId === DATA_FABRIC_TENANT_FOLDER_ID ? undefined : folderId; + const target = await this.get( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_BY_ID(field.choiceSetId), + { headers: createHeaders({ [FOLDER_KEY]: lookupFolderKey }) }, + ); + meta.referenceChoiceSet = { + id: field.choiceSetId, + name: target.data.name, + folderId, + entityType: 'ChoiceSet', + entityTypeId: 1, + }; + } + return meta; + } + /** Converts a user-facing EntityCreateFieldOptions to the raw API field payload */ - private buildSchemaFieldPayload(field: EntityCreateFieldOptions): FieldSchemaPayload { + private buildSchemaFieldPayload( + field: EntityCreateFieldOptions, + refMeta?: ResolvedReferenceMeta, + ): FieldSchemaPayload { const fieldType = field.type ?? EntityFieldDataType.STRING; this.validateFieldConstraints(fieldType, field, field.fieldName); const isRelationship = fieldType === EntityFieldDataType.RELATIONSHIP; @@ -1191,6 +1248,10 @@ export class EntityService extends BaseService implements EntityServiceModel { }); } const mapping = EntitySchemaFieldTypeMap[fieldType]; + // Prefer the resolved {id, name, folderId} body so cross-folder targets resolve + // server-side; fall back to a bare {id} when no meta was fetched. + const referenceEntityBody = refMeta?.referenceEntity ?? (field.referenceEntityId !== undefined ? { id: field.referenceEntityId } : undefined); + const referenceChoiceSetBody = refMeta?.referenceChoiceSet; return { name: field.fieldName, displayName: field.displayName ?? field.fieldName, @@ -1209,7 +1270,8 @@ export class EntityService extends BaseService implements EntityServiceModel { ...(field.choiceSetId !== undefined && { choiceSetId: field.choiceSetId }), ...((isRelationship || isFile) && { isForeignKey: true }), ...(isRelationship && { referenceType: ReferenceType.ManyToOne }), - ...(field.referenceEntityId !== undefined && { referenceEntity: { id: field.referenceEntityId } }), + ...(referenceEntityBody !== undefined && { referenceEntity: referenceEntityBody }), + ...(referenceChoiceSetBody !== undefined && { referenceChoiceSet: referenceChoiceSetBody }), ...(field.referenceFieldId !== undefined && { referenceField: { id: field.referenceFieldId } }), }; } diff --git a/tests/.env.integration.example b/tests/.env.integration.example index 1ff5fa67a..f883682d3 100644 --- a/tests/.env.integration.example +++ b/tests/.env.integration.example @@ -6,17 +6,18 @@ # ============================================ # UiPath Cloud URL (e.g., https://cloud.uipath.com) -UIPATH_BASE_URL=https://cloud.uipath.com +UIPATH_BASE_URL=https://alpha.api.uipath.com # Your UiPath organization name -UIPATH_ORG_NAME=your-organization-name +UIPATH_ORG_NAME=procodeapps # Your UiPath tenant name -UIPATH_TENANT_NAME=your-tenant-name +UIPATH_TENANT_NAME=integrationtest # Personal Access Token (PAT) for authentication # Generate this from UiPath Cloud > Admin > External Applications -UIPATH_SECRET=your-pat-token-here +UIPATH_SECRET=rt_3A3FB48D4444456220A993A8A70CFBD45636DF647CF252F42CAD5F22B3F7A0D5-1 +#rt_AA68BFAB6E47D7B7D4D0F84C63051AFF1877A7D0B7DD0A6305E104D88AE324EA-1 # ============================================ # Test Settings @@ -31,15 +32,8 @@ INTEGRATION_TEST_SKIP_CLEANUP=false # Default folder ID for tests (optional) # Leave empty to use default folder -INTEGRATION_TEST_FOLDER_ID= - -# Folder key (GUID) matching INTEGRATION_TEST_FOLDER_ID -# Used by getByName integration tests for assets/processes -INTEGRATION_TEST_FOLDER_KEY= - -# Folder path (e.g. "Shared/Finance") matching INTEGRATION_TEST_FOLDER_ID -# Used by getByName integration tests for assets/processes -INTEGRATION_TEST_FOLDER_PATH= +INTEGRATION_TEST_FOLDER_ID=2819046 +INTEGRATION_TEST_FOLDER_KEY=5f6dadf1-3677-49dc-8aca-c2999dd4b3ba # ============================================ # Optional: Pre-existing Test Data @@ -49,20 +43,31 @@ INTEGRATION_TEST_FOLDER_PATH= # Maestro test process key MAESTRO_TEST_PROCESS_KEY= -# Orchestrator test process key -ORCHESTRATOR_TEST_PROCESS_KEY= +# Case test process key +CASE_TEST_PROCESS_KEY=55009bca-8c7d-4c3b-b146-c481b3ca1c4a -# Data Fabric test entity ID -DATA_FABRIC_TEST_ENTITY_ID= -# Pre-existing folder-scoped Data Fabric entity ID — used by the folder-scoped -# record CRUD tests. Must live in the folder identified by INTEGRATION_TEST_FOLDER_KEY. -DATA_FABRIC_TEST_FOLDER_ENTITY_ID= -DATA_FABRIC_TEST_CHOICESET_ID= -DATA_FABRIC_TEST_ATTACHMENT_FIELD= +# Orchestrator test process key +ORCHESTRATOR_TEST_PROCESS_KEY=0B013150-CEFD-4608-B590-57029F7DFF3C # Jobs test folder ID (folder containing jobs for resume/suspend tests) # Falls back to INTEGRATION_TEST_FOLDER_ID if not set -JOBS_TEST_FOLDER_ID= +JOBS_TEST_FOLDER_ID=2819046 + +# Data Fabric test entity ID +DATA_FABRIC_TEST_ENTITY_ID=ef91d745-fc36-f111-8ef3-6045bd00bc8b +DATA_FABRIC_TEST_CHOICESET_ID=825c493c-fc36-f111-8ef3-6045bd00bc8b + +# Folder-scoped Data Fabric test entity (used by the Folder-scoped record CRUD +# describe.skip block). Must live in the folder identified by +# INTEGRATION_TEST_FOLDER_KEY above. The Folder-scoped Choice value CRUD block +# looks up its target choice set by name (`aIntegrationTestFolderScoped`) in the +# same folder, so no separate env var is needed. +DATA_FABRIC_TEST_FOLDER_ENTITY_ID=4be52007-6865-f111-8fcb-0022483118a6 + +DATA_FABRIC_TEST_ATTACHMENT_FIELD=SomeFile # Orchestrator attachment ID (UUID) for attachment getById tests -ORCHESTRATOR_ATTACHMENT_ID= \ No newline at end of file +ORCHESTRATOR_ATTACHMENT_ID=0c78ea69-f920-48ca-c696-08dec5d8bdf8 + +UIPATH_TRACES_TEST_TRACE_ID_DEV=2074c74b-9931-4072-9c00-8ceaa2a59724 +Message Sarath Chandra Reddy Motati \ No newline at end of file diff --git a/tests/integration/shared/data-fabric/choicesets.integration.test.ts b/tests/integration/shared/data-fabric/choicesets.integration.test.ts index 9d0f893b3..c961939f5 100644 --- a/tests/integration/shared/data-fabric/choicesets.integration.test.ts +++ b/tests/integration/shared/data-fabric/choicesets.integration.test.ts @@ -11,16 +11,15 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = const createdChoiceSetIds: string[] = []; const insertedValueIds: string[] = []; - // Folder-scoped resources tracked separately so the single top-level afterAll - // can clean them with their captured folderKey. Populated by the folder block. + // Folder-scoped CS created in the Folder-scoped operations describe block. + // Tracked here so the file-level afterAll can clean it up if a test failed + // before its own delete step ran. let folderScopedChoiceSetId: string | null = null; let folderScopedFolderKey: string | null = null; - const folderScopedValueIds: string[] = []; afterAll(async () => { const { choiceSets } = getServices(); - // Clean up any leftover choice-set values from the value-level CRUD block. if (testChoiceSetId && insertedValueIds.length > 0) { try { await choiceSets.deleteValuesById(testChoiceSetId, insertedValueIds); @@ -29,14 +28,6 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = } } - // Folder-scoped cleanup (only populated when the folder-scoped describe block runs) - if (folderScopedChoiceSetId && folderScopedFolderKey && folderScopedValueIds.length > 0) { - try { - await choiceSets.deleteValuesById(folderScopedChoiceSetId, folderScopedValueIds, { folderKey: folderScopedFolderKey }); - } catch { - // Ignore cleanup failures — test resources are sandboxed. - } - } if (folderScopedChoiceSetId && folderScopedFolderKey) { try { await choiceSets.deleteById(folderScopedChoiceSetId, { folderKey: folderScopedFolderKey }); @@ -347,52 +338,141 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = expect(Array.isArray(values.items)).toBe(true); }); - it('should require folderKey to insert and update values on a folder-scoped choice set', async () => { + it('should delete a folder-scoped choice set with folderKey', async () => { const { choiceSets } = getServices(); const folderKey = folderScopedFolderKey!; if (!folderScopedChoiceSetId) { - throw new Error('Folder-scoped choice set was not created in the previous test'); + throw new Error('Folder-scoped choice set was not created earlier in the suite'); + } + + await choiceSets.deleteById(folderScopedChoiceSetId, { folderKey }); + + const folderSets = await choiceSets.getAll({ folderKey }); + expect(folderSets.find((cs) => cs.id === folderScopedChoiceSetId)).toBeUndefined(); + + // Clear so the top-level afterAll doesn't try to delete it again + folderScopedChoiceSetId = null; + }); + }); + + // ─── Folder-scoped Choice value CRUD ────────────────────────────────────── + // Mirrors the tenant-scope `Choice value CRUD operations` block above, but + // against a pre-existing folder-scoped choice set named + // `aIntegrationTestFolderScoped` in the folder identified by + // INTEGRATION_TEST_FOLDER_KEY (looked up by name in beforeAll so the test + // env doesn't need a separate CS UUID). + // + // Skipped: same DataFabric.Schema.Write OAuth-scope restriction as the tenant + // value-CRUD block. Re-enable locally with describe.only when + // INTEGRATION_TEST_FOLDER_KEY is set, OAuth is configured, and the named + // choice set exists in that folder. + describe.skip('Folder-scoped Choice value CRUD operations', () => { + const FOLDER_CHOICE_SET_NAME = 'aIntegrationTestFolderScoped'; + const folderValueIds: string[] = []; + let folderKey!: string; + let folderChoiceSetId!: string; + + beforeAll(async () => { + const config = getTestConfig(); + if (!config.folderKey) { + throw new Error('INTEGRATION_TEST_FOLDER_KEY is required for folder-scoped value-CRUD tests'); + } + folderKey = config.folderKey; + + const { choiceSets } = getServices(); + const folderSets = await choiceSets.getAll({ folderKey }); + const match = folderSets.find((cs) => cs.name === FOLDER_CHOICE_SET_NAME); + if (!match) { + throw new Error( + `Folder-scoped choice set '${FOLDER_CHOICE_SET_NAME}' not found in folder '${folderKey}' — create it first or update FOLDER_CHOICE_SET_NAME.`, + ); } + folderChoiceSetId = match.id; + }); + + afterAll(async () => { + if (folderValueIds.length === 0) return; + const { choiceSets } = getServices(); + try { + await choiceSets.deleteValuesById(folderChoiceSetId, folderValueIds, { folderKey }); + } catch { + // Ignore cleanup failures — test resources are sandboxed. + } + }); + + it('should insert a single value using insertValueById', async () => { + const { choiceSets } = getServices(); const valueName = `SDK_FLD_${generateRandomString(6)}`; - const inserted = await choiceSets.insertValueById(folderScopedChoiceSetId, valueName, { - displayName: 'Travel (folder)', + const result = await choiceSets.insertValueById(folderChoiceSetId, valueName, { + displayName: 'Travel', folderKey, }); - expect(inserted.id).toBeDefined(); - folderScopedValueIds.push(inserted.id); - const updated = await choiceSets.updateValueById( - folderScopedChoiceSetId, - inserted.id, - 'Travel (renamed)', - { folderKey }, - ); - expect(updated.displayName).toBe('Travel (renamed)'); + expect(result).toBeDefined(); + expect(result.id).toBeDefined(); + folderValueIds.push(result.id); }); - it('should delete a folder-scoped choice set with folderKey', async () => { + it('should verify inserted value via getById', async () => { const { choiceSets } = getServices(); - const folderKey = folderScopedFolderKey!; - if (!folderScopedChoiceSetId) { - throw new Error('Folder-scoped choice set was not created earlier in the suite'); + if (folderValueIds.length === 0) { + throw new Error('No inserted value available to verify'); } - // Remove leftover values first so deleteById succeeds - if (folderScopedValueIds.length > 0) { - await choiceSets.deleteValuesById(folderScopedChoiceSetId, folderScopedValueIds, { folderKey }); - folderScopedValueIds.length = 0; + const valueId = folderValueIds[0]; + const result = await choiceSets.getById(folderChoiceSetId, { folderKey }); + const found = result.items.find((v) => v.id === valueId); + + expect(found).toBeDefined(); + expect(found?.id).toBe(valueId); + }); + + it('should insert another value with default displayName', async () => { + const { choiceSets } = getServices(); + + const valueName = `SDK_FLD_SOLO_${generateRandomString(6)}`; + const result = await choiceSets.insertValueById(folderChoiceSetId, valueName, { folderKey }); + + expect(result).toBeDefined(); + expect(result.id).toBeDefined(); + expect(result.name).toBe(valueName); + expect(result.displayName).toBe(valueName); + folderValueIds.push(result.id); + }); + + it('should update value using updateValueById', async () => { + const { choiceSets } = getServices(); + + if (folderValueIds.length === 0) { + throw new Error('No values available to update'); } - await choiceSets.deleteById(folderScopedChoiceSetId, { folderKey }); + const valueId = folderValueIds[0]; + const result = await choiceSets.updateValueById( + folderChoiceSetId, + valueId, + 'Business Travel', + { folderKey }, + ); - const folderSets = await choiceSets.getAll({ folderKey }); - expect(folderSets.find((cs) => cs.id === folderScopedChoiceSetId)).toBeUndefined(); + expect(result).toBeDefined(); + expect(result.id).toBe(valueId); + expect(result.displayName).toBe('Business Travel'); + }); - // Clear so the top-level afterAll doesn't try to delete it again - folderScopedChoiceSetId = null; + it('should delete values using deleteValuesById', async () => { + const { choiceSets } = getServices(); + + if (folderValueIds.length === 0) { + throw new Error('No values available to delete'); + } + + await choiceSets.deleteValuesById(folderChoiceSetId, folderValueIds, { folderKey }); + + folderValueIds.length = 0; }); }); }); diff --git a/tests/integration/shared/data-fabric/entities.integration.test.ts b/tests/integration/shared/data-fabric/entities.integration.test.ts index 1d9273b22..bb37c942d 100644 --- a/tests/integration/shared/data-fabric/entities.integration.test.ts +++ b/tests/integration/shared/data-fabric/entities.integration.test.ts @@ -23,15 +23,19 @@ const choiceSetValueCache = new Map(); /** * Fetches and caches choice set values for a given choice set ID. + * When the target choice set lives in a folder (not tenant-level), pass that + * folder's key so the lookup carries the X-UIPATH-FolderKey header — otherwise + * the server returns empty values and required CS fields end up undefined. */ -async function getChoiceSetValues(choiceSetId: string): Promise { - if (choiceSetValueCache.has(choiceSetId)) { - return choiceSetValueCache.get(choiceSetId)!; +async function getChoiceSetValues(choiceSetId: string, folderKey?: string): Promise { + const cacheKey = `${folderKey ?? ''}::${choiceSetId}`; + if (choiceSetValueCache.has(cacheKey)) { + return choiceSetValueCache.get(cacheKey)!; } const { choiceSets } = getServices(); - const result = await choiceSets.getById(choiceSetId); + const result = await choiceSets.getById(choiceSetId, folderKey ? { folderKey } : undefined); const values = result.items || []; - choiceSetValueCache.set(choiceSetId, values); + choiceSetValueCache.set(cacheKey, values); return values; } @@ -120,7 +124,12 @@ async function buildDummyRecord(entityMetadata: RawEntityGetResponse): Promise }); // ─── Folder-scoped record CRUD ──────────────────────────────────────────── - // Verifies that every record-CRUD method correctly forwards the - // X-UIPATH-FolderKey header for an entity that lives in a non-tenant folder. - // - // Uses a pre-existing folder-scoped entity (DATA_FABRIC_TEST_FOLDER_ENTITY_ID) - // so the suite runs with the standard integration-test PAT — entity schema - // create/delete are NOT exercised here (they live in the skipped schema-write - // describes above). - // - // Skipped when DATA_FABRIC_TEST_FOLDER_ENTITY_ID is not configured — CI does - // not yet provision a persistent folder-scoped test entity. Run locally by - // setting both INTEGRATION_TEST_FOLDER_KEY and DATA_FABRIC_TEST_FOLDER_ENTITY_ID. - describe.skipIf(!process.env.DATA_FABRIC_TEST_FOLDER_ENTITY_ID || !process.env.INTEGRATION_TEST_FOLDER_KEY)('Folder-scoped record CRUD', () => { + // Mirrors the tenant-scope record CRUD block above, but against a folder-scoped + // entity (DATA_FABRIC_TEST_FOLDER_ENTITY_ID + INTEGRATION_TEST_FOLDER_KEY). + // Record CRUD works with PAT auth; schema create/delete on the folder entity + // is NOT exercised here (lives in the describe.skip schema-write blocks above). + describe('Folder-scoped record CRUD', () => { let folderEntityId!: string; let folderKey!: string; let folderEntityMetadata!: RawEntityGetResponse; @@ -1382,8 +1384,14 @@ describe.each(modes)('Data Fabric Entities - Integration Tests [%s]', (mode) => beforeAll(async () => { const config = getTestConfig(); - folderKey = config.folderKey!; - folderEntityId = config.dataFabricTestFolderEntityId!; + if (!config.folderKey) { + throw new Error('INTEGRATION_TEST_FOLDER_KEY is required for folder-scoped record CRUD tests'); + } + if (!config.dataFabricTestFolderEntityId) { + throw new Error('DATA_FABRIC_TEST_FOLDER_ENTITY_ID is required — set to the UUID of a folder-scoped entity in the same folder'); + } + folderKey = config.folderKey; + folderEntityId = config.dataFabricTestFolderEntityId; // Fetch schema once so per-test record bodies match the entity's shape. const { entities } = getServices(); diff --git a/tests/unit/services/data-fabric/entities.test.ts b/tests/unit/services/data-fabric/entities.test.ts index 7ef1bc953..d55686290 100644 --- a/tests/unit/services/data-fabric/entities.test.ts +++ b/tests/unit/services/data-fabric/entities.test.ts @@ -49,6 +49,7 @@ import { import { ENTITY_TEST_CONSTANTS } from "../../../utils/constants/entities"; import { TEST_CONSTANTS } from "../../../utils/constants/common"; import { DATA_FABRIC_ENDPOINTS } from "../../../../src/utils/constants/endpoints"; +import { DATA_FABRIC_TENANT_FOLDER_ID } from "../../../../src/utils/constants/endpoints/data-fabric"; import { SqlFieldType, FieldSchemaPayload } from "@/models/data-fabric/entities.internal-types"; // ===== MOCKING ===== @@ -2160,6 +2161,97 @@ describe("EntityService Unit Tests", () => { ).rejects.toThrow(/requires both referenceEntityId and referenceFieldId/); }); + it("should embed { id, folderId } in referenceEntity for cross-folder RELATIONSHIP when referenceFolderKey is set", async () => { + await entityService.create("my_entity", [{ + fieldName: "rel_field", + type: EntityFieldDataType.RELATIONSHIP, + referenceEntityId: ENTITY_TEST_CONSTANTS.REFERENCE_ENTITY_ID, + referenceFieldId: ENTITY_TEST_CONSTANTS.REFERENCE_FIELD_ID, + referenceFolderKey: ENTITY_TEST_CONSTANTS.REFERENCE_TARGET_FOLDER_KEY, + }]); + + // No GET — payload is built from caller inputs only + expect(mockApiClient.get).not.toHaveBeenCalled(); + + const f = getCreatedFields().find((x) => x.name === "rel_field"); + expect(f?.referenceEntity).toEqual({ + id: ENTITY_TEST_CONSTANTS.REFERENCE_ENTITY_ID, + folderId: ENTITY_TEST_CONSTANTS.REFERENCE_TARGET_FOLDER_KEY, + }); + expect(f?.referenceField).toEqual({ id: ENTITY_TEST_CONSTANTS.REFERENCE_FIELD_ID }); + expect(f?.isForeignKey).toBe(true); + expect(f?.referenceType).toBe("ManyToOne"); + }); + + it("should look up name for cross-folder CHOICE_SET target (API requires name, not folderId)", async () => { + // Choice-set endpoint resolves the target by name, so the SDK fetches it + mockApiClient.get.mockResolvedValue({ + id: ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID, + name: "UserType", + folderId: DATA_FABRIC_TENANT_FOLDER_ID, + }); + + await entityService.create("my_entity", [{ + fieldName: "userType", + type: EntityFieldDataType.CHOICE_SET_SINGLE, + choiceSetId: ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID, + referenceFolderKey: DATA_FABRIC_TENANT_FOLDER_ID, + }]); + + // Tenant marker → no folder header on the lookup + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_BY_ID(ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID), + { headers: {} }, + ); + + const f = getCreatedFields().find((x) => x.name === "userType"); + expect(f?.referenceChoiceSet).toEqual({ + id: ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID, + name: "UserType", + folderId: DATA_FABRIC_TENANT_FOLDER_ID, + entityType: "ChoiceSet", + entityTypeId: 1, + }); + expect(f?.choiceSetId).toBe(ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID); + }); + + it("should use folder header on choice-set lookup when referenceFolderKey is a folder UUID", async () => { + mockApiClient.get.mockResolvedValue({ + id: ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID, + name: "FolderCS", + folderId: ENTITY_TEST_CONSTANTS.REFERENCE_TARGET_FOLDER_KEY, + }); + + await entityService.create("my_entity", [{ + fieldName: "cat", + type: EntityFieldDataType.CHOICE_SET_SINGLE, + choiceSetId: ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID, + referenceFolderKey: ENTITY_TEST_CONSTANTS.REFERENCE_TARGET_FOLDER_KEY, + }]); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_BY_ID(ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID), + { headers: { "X-UIPATH-FolderKey": ENTITY_TEST_CONSTANTS.REFERENCE_TARGET_FOLDER_KEY } }, + ); + + const f = getCreatedFields().find((x) => x.name === "cat"); + expect(f?.referenceChoiceSet?.name).toBe("FolderCS"); + }); + + it("should emit a bare { id } reference when referenceFolderKey is omitted (same-scope default)", async () => { + await entityService.create("my_entity", [{ + fieldName: "rel_field", + type: EntityFieldDataType.RELATIONSHIP, + referenceEntityId: ENTITY_TEST_CONSTANTS.REFERENCE_ENTITY_ID, + referenceFieldId: ENTITY_TEST_CONSTANTS.REFERENCE_FIELD_ID, + }]); + + expect(mockApiClient.get).not.toHaveBeenCalled(); + + const f = getCreatedFields().find((x) => x.name === "rel_field"); + expect(f?.referenceEntity).toEqual({ id: ENTITY_TEST_CONSTANTS.REFERENCE_ENTITY_ID }); + }); + it("should emit isForeignKey, referenceEntity, referenceField — but NOT referenceType — for FILE fields", async () => { await entityService.create("my_entity", [{ fieldName: "file_field", diff --git a/tests/utils/constants/entities.ts b/tests/utils/constants/entities.ts index 744245518..d74b4b096 100644 --- a/tests/utils/constants/entities.ts +++ b/tests/utils/constants/entities.ts @@ -36,6 +36,8 @@ export const ENTITY_TEST_CONSTANTS = { // Reference IDs (for relationship-type fields) REFERENCE_ENTITY_ID: 'a1234567-e89b-12d3-a456-426614174000', REFERENCE_FIELD_ID: 'b1234567-e89b-12d3-a456-426614174000', + REFERENCE_TARGET_FOLDER_KEY: 'c1234567-e89b-12d3-a456-426614174000', + CHOICE_SET_TARGET_ID: 'd1234567-e89b-12d3-a456-426614174000', // Field Data Type Names (for assertions) FIELD_TYPE_UUID: 'UUID', From 831553f056c0dc70c84f62b9fdc915020fbab252 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Thu, 11 Jun 2026 16:58:03 +0530 Subject: [PATCH 04/23] revert: roll tests/.env.integration.example back to template Reverts the prior commit's accidental check-in of tenant-specific test config and a personal PAT into the example file. The example should only carry placeholder values; live config belongs in the gitignored .env.integration. Co-Authored-By: Claude Opus 4.7 --- tests/.env.integration.example | 53 +++++++++++++++------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/tests/.env.integration.example b/tests/.env.integration.example index f883682d3..1ff5fa67a 100644 --- a/tests/.env.integration.example +++ b/tests/.env.integration.example @@ -6,18 +6,17 @@ # ============================================ # UiPath Cloud URL (e.g., https://cloud.uipath.com) -UIPATH_BASE_URL=https://alpha.api.uipath.com +UIPATH_BASE_URL=https://cloud.uipath.com # Your UiPath organization name -UIPATH_ORG_NAME=procodeapps +UIPATH_ORG_NAME=your-organization-name # Your UiPath tenant name -UIPATH_TENANT_NAME=integrationtest +UIPATH_TENANT_NAME=your-tenant-name # Personal Access Token (PAT) for authentication # Generate this from UiPath Cloud > Admin > External Applications -UIPATH_SECRET=rt_3A3FB48D4444456220A993A8A70CFBD45636DF647CF252F42CAD5F22B3F7A0D5-1 -#rt_AA68BFAB6E47D7B7D4D0F84C63051AFF1877A7D0B7DD0A6305E104D88AE324EA-1 +UIPATH_SECRET=your-pat-token-here # ============================================ # Test Settings @@ -32,8 +31,15 @@ INTEGRATION_TEST_SKIP_CLEANUP=false # Default folder ID for tests (optional) # Leave empty to use default folder -INTEGRATION_TEST_FOLDER_ID=2819046 -INTEGRATION_TEST_FOLDER_KEY=5f6dadf1-3677-49dc-8aca-c2999dd4b3ba +INTEGRATION_TEST_FOLDER_ID= + +# Folder key (GUID) matching INTEGRATION_TEST_FOLDER_ID +# Used by getByName integration tests for assets/processes +INTEGRATION_TEST_FOLDER_KEY= + +# Folder path (e.g. "Shared/Finance") matching INTEGRATION_TEST_FOLDER_ID +# Used by getByName integration tests for assets/processes +INTEGRATION_TEST_FOLDER_PATH= # ============================================ # Optional: Pre-existing Test Data @@ -43,31 +49,20 @@ INTEGRATION_TEST_FOLDER_KEY=5f6dadf1-3677-49dc-8aca-c2999dd4b3ba # Maestro test process key MAESTRO_TEST_PROCESS_KEY= -# Case test process key -CASE_TEST_PROCESS_KEY=55009bca-8c7d-4c3b-b146-c481b3ca1c4a - # Orchestrator test process key -ORCHESTRATOR_TEST_PROCESS_KEY=0B013150-CEFD-4608-B590-57029F7DFF3C - -# Jobs test folder ID (folder containing jobs for resume/suspend tests) -# Falls back to INTEGRATION_TEST_FOLDER_ID if not set -JOBS_TEST_FOLDER_ID=2819046 +ORCHESTRATOR_TEST_PROCESS_KEY= # Data Fabric test entity ID -DATA_FABRIC_TEST_ENTITY_ID=ef91d745-fc36-f111-8ef3-6045bd00bc8b -DATA_FABRIC_TEST_CHOICESET_ID=825c493c-fc36-f111-8ef3-6045bd00bc8b - -# Folder-scoped Data Fabric test entity (used by the Folder-scoped record CRUD -# describe.skip block). Must live in the folder identified by -# INTEGRATION_TEST_FOLDER_KEY above. The Folder-scoped Choice value CRUD block -# looks up its target choice set by name (`aIntegrationTestFolderScoped`) in the -# same folder, so no separate env var is needed. -DATA_FABRIC_TEST_FOLDER_ENTITY_ID=4be52007-6865-f111-8fcb-0022483118a6 +DATA_FABRIC_TEST_ENTITY_ID= +# Pre-existing folder-scoped Data Fabric entity ID — used by the folder-scoped +# record CRUD tests. Must live in the folder identified by INTEGRATION_TEST_FOLDER_KEY. +DATA_FABRIC_TEST_FOLDER_ENTITY_ID= +DATA_FABRIC_TEST_CHOICESET_ID= +DATA_FABRIC_TEST_ATTACHMENT_FIELD= -DATA_FABRIC_TEST_ATTACHMENT_FIELD=SomeFile +# Jobs test folder ID (folder containing jobs for resume/suspend tests) +# Falls back to INTEGRATION_TEST_FOLDER_ID if not set +JOBS_TEST_FOLDER_ID= # Orchestrator attachment ID (UUID) for attachment getById tests -ORCHESTRATOR_ATTACHMENT_ID=0c78ea69-f920-48ca-c696-08dec5d8bdf8 - -UIPATH_TRACES_TEST_TRACE_ID_DEV=2074c74b-9931-4072-9c00-8ceaa2a59724 -Message Sarath Chandra Reddy Motati \ No newline at end of file +ORCHESTRATOR_ATTACHMENT_ID= \ No newline at end of file From fc04d3e1791e76524fe062877cee1fef23c9127e Mon Sep 17 00:00:00 2001 From: Sarath1018 Date: Thu, 11 Jun 2026 18:52:18 +0530 Subject: [PATCH 05/23] ci: add DATA_FABRIC_TEST_FOLDER_ENTITY_ID to integration test config Co-Authored-By: Claude Fable 5 --- .github/workflows/pr-checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 0a2d0b401..da600e6f4 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -66,6 +66,7 @@ jobs: echo "data_fabric_test_entity_id=${{ secrets.UIPATH_DATA_FABRIC_TEST_ENTITY_ID_DEV || secrets.UIPATH_DATA_FABRIC_TEST_ENTITY_ID }}" >> $GITHUB_OUTPUT echo "data_fabric_test_choiceset_id=${{ secrets.UIPATH_DATA_FABRIC_TEST_CHOICESET_ID_DEV || secrets.UIPATH_DATA_FABRIC_TEST_CHOICESET_ID }}" >> $GITHUB_OUTPUT echo "data_fabric_test_attachment_field=${{ secrets.UIPATH_DATA_FABRIC_TEST_ATTACHMENT_FIELD_DEV || secrets.UIPATH_DATA_FABRIC_TEST_ATTACHMENT_FIELD }}" >> $GITHUB_OUTPUT + echo "data_fabric_test_folder_entity_id=${{ secrets.UIPATH_DATA_FABRIC_TEST_FOLDER_ENTITY_ID_DEV || secrets.UIPATH_DATA_FABRIC_TEST_FOLDER_ENTITY_ID }}" >> $GITHUB_OUTPUT echo "orchestrator_attachment_id=${{ secrets.UIPATH_ORCHESTRATOR_ATTACHMENT_ID_DEV || secrets.UIPATH_ORCHESTRATOR_ATTACHMENT_ID }}" >> $GITHUB_OUTPUT echo "jobs_test_folder_id=${{ secrets.UIPATH_JOBS_TEST_FOLDER_ID_DEV || secrets.UIPATH_JOBS_TEST_FOLDER_ID }}" >> $GITHUB_OUTPUT echo "traces_test_trace_id=${{ secrets.UIPATH_TRACES_TEST_TRACE_ID_DEV || secrets.UIPATH_TRACES_TEST_TRACE_ID }}" >> $GITHUB_OUTPUT @@ -88,6 +89,7 @@ jobs: DATA_FABRIC_TEST_ENTITY_ID=${{ steps.config.outputs.data_fabric_test_entity_id }} DATA_FABRIC_TEST_CHOICESET_ID=${{ steps.config.outputs.data_fabric_test_choiceset_id }} DATA_FABRIC_TEST_ATTACHMENT_FIELD=${{ steps.config.outputs.data_fabric_test_attachment_field }} + DATA_FABRIC_TEST_FOLDER_ENTITY_ID=${{ steps.config.outputs.data_fabric_test_folder_entity_id }} ORCHESTRATOR_TEST_PROCESS_KEY=${{ steps.config.outputs.orchestrator_test_process_key }} ORCHESTRATOR_ATTACHMENT_ID=${{ steps.config.outputs.orchestrator_attachment_id }} JOBS_TEST_FOLDER_ID=${{ steps.config.outputs.jobs_test_folder_id }} From e6ee264162ea19f6ae751748012cd8a7b0480a02 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Thu, 11 Jun 2026 21:14:54 +0530 Subject: [PATCH 06/23] refactor(data-fabric): extract ChoiceSet entityType/Id to module-level constants Replaces inline 'ChoiceSet' / 1 literals in buildReferenceMeta with the existing EntityType.ChoiceSet enum and a new ENTITY_TYPE_IDS map. Pairs the two values that travel together in reference payloads. Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/entities.constants.ts | 11 ++++++++++- src/services/data-fabric/entities.ts | 6 ++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/models/data-fabric/entities.constants.ts b/src/models/data-fabric/entities.constants.ts index aa61042bb..27f364056 100644 --- a/src/models/data-fabric/entities.constants.ts +++ b/src/models/data-fabric/entities.constants.ts @@ -1,4 +1,4 @@ -import { EntityFieldDataType, FieldDisplayType } from "./entities.types"; +import { EntityFieldDataType, EntityType, FieldDisplayType } from "./entities.types"; import { EntitySchemaFieldMapping, SqlFieldType, @@ -7,6 +7,15 @@ import { } from "./entities.internal-types"; export { SqlFieldType } from "./entities.internal-types"; +/** + * Numeric type IDs that pair with {@link EntityType} on reference payloads + * (entityType + entityTypeId travel together in `referenceEntity` / + * `referenceChoiceSet` bodies on cross-folder schema upserts). + */ +export const ENTITY_TYPE_IDS = { + [EntityType.ChoiceSet]: 1, +} as const; + /** * Maps fields for Entities */ diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index bace97294..0abb2d799 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -32,6 +32,7 @@ import { EntityCreateOptions, EntityCreateFieldOptions, EntityFieldDataType, + EntityType, EntityGetAllOptions, EntityGetByIdOptions, EntityDeleteByIdOptions, @@ -57,6 +58,7 @@ import { FieldDisplayTypeToDataType, ENTITY_FIELD_CONSTRAINT_DEFAULTS, ENTITY_FIELD_CONSTRAINT_SPEC, + ENTITY_TYPE_IDS, } from '../../models/data-fabric/entities.constants'; import { FieldSchemaPayload, SqlFieldType, EntityFieldConstraint, ResolvedReferenceMeta } from '../../models/data-fabric/entities.internal-types'; import { track } from '../../core/telemetry'; @@ -1226,8 +1228,8 @@ export class EntityService extends BaseService implements EntityServiceModel { id: field.choiceSetId, name: target.data.name, folderId, - entityType: 'ChoiceSet', - entityTypeId: 1, + entityType: EntityType.ChoiceSet, + entityTypeId: ENTITY_TYPE_IDS[EntityType.ChoiceSet], }; } return meta; From a3f83ae8c63b38792ea006cb2da2ec1a767bad19 Mon Sep 17 00:00:00 2001 From: amrit-agarwal-1 Date: Thu, 11 Jun 2026 21:20:54 +0530 Subject: [PATCH 07/23] feat(data-fabric): add includeAllFolders option to entities.getAll (#509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional `includeAllFolders` flag to `EntityGetAllOptions`. When true (and no `folderKey` is provided), `getAll` lists tenant-level and folder-level entities together via the v2 entity endpoint. Existing behavior is unchanged: no flag lists only tenant entities, and a `folderKey` always wins — scoping to that single folder and ignoring `includeAllFolders`. Documents the additional `OR.Users` scope required for folder-scoped entity operations and the combined listing. Co-authored-by: Claude Opus 4.8 (1M context) --- docs/oauth-scopes.md | 2 ++ src/models/data-fabric/entities.models.ts | 20 ++++++----- src/models/data-fabric/entities.types.ts | 9 ++++- src/services/data-fabric/entities.ts | 29 ++++++++++----- src/utils/constants/endpoints/data-fabric.ts | 2 ++ .../data-fabric/entities.integration.test.ts | 26 ++++++++++++++ .../services/data-fabric/entities.test.ts | 36 +++++++++++++++++++ 7 files changed, 106 insertions(+), 18 deletions(-) diff --git a/docs/oauth-scopes.md b/docs/oauth-scopes.md index d614b4929..af7c85dca 100644 --- a/docs/oauth-scopes.md +++ b/docs/oauth-scopes.md @@ -43,6 +43,8 @@ This page lists the specific OAuth scopes required in external app for each SDK ## Entities +> **Folder-scoped entities require `OR.Users` in addition to the scope below.** Any CRUD operation on a folder entity (passing `folderKey`), and `getAll({ includeAllFolders: true })` which lists all tenant and folder entities, need `OR.Users` as well. + | Method | OAuth Scope | |--------|-------------| | `getAll()` | `DataFabric.Schema.Read` | diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index 59807d100..d2e0d118c 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -55,13 +55,14 @@ export interface EntityServiceModel { /** * Gets entities in the system * - * The Data Fabric entity list is scoped exclusively, not additively: - * omitting `folderKey` returns only tenant-level entities; passing - * `folderKey` returns only entities in that folder. To enumerate - * every entity across folders, call `getAll()` once per folder - * plus once with no `folderKey` for the tenant scope. - * - * @param options - Optional {@link EntityGetAllOptions} (e.g. `folderKey` to list folder-scoped entities) + * By default the entity list is scoped exclusively: omitting `folderKey` + * returns only tenant-level entities; passing `folderKey` returns only + * entities in that folder. To list tenant-level and folder-level entities + * together in a single call, pass `includeAllFolders: true` (with no + * `folderKey`). When `folderKey` is provided it always wins, scoping the + * result to that single folder and ignoring `includeAllFolders`. + * + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllFolders` to list tenant and folder entities together) * @returns Promise resolving to an array of entity metadata * {@link EntityGetResponse} * @example @@ -69,7 +70,10 @@ export interface EntityServiceModel { * // Get tenant-level entities * const tenantEntities = await entities.getAll(); * - * // Get folder-scoped entities for a specific folder + * // Get tenant-level and folder-level entities together + * const allEntities = await entities.getAll({ includeAllFolders: true }); + * + * // Get a single folder's entities * const folderEntities = await entities.getAll({ folderKey: "" }); * * // Iterate through entities diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index 6903c1132..1cfde4e2c 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -327,7 +327,14 @@ export interface EntityFolderScopedOptions { /** * Options for {@link EntityServiceModel.getAll} */ -export interface EntityGetAllOptions extends EntityFolderScopedOptions {} +export interface EntityGetAllOptions extends EntityFolderScopedOptions { + /** + * When `true`, returns tenant-level and folder-level entities together. + * Omit (or `false`) to return only tenant-level entities. + * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. + */ + includeAllFolders?: boolean; +} /** * Options for {@link EntityServiceModel.getById} diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 0abb2d799..f2e72093b 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -506,13 +506,14 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Gets all entities in the system * - * The Data Fabric entity list is scoped exclusively, not additively: - * omitting `folderKey` returns only tenant-level entities; passing - * `folderKey` returns only entities in that folder. To enumerate - * every entity across folders, call `getAll()` once per folder - * plus once with no `folderKey` for the tenant scope. - * - * @param options - Optional {@link EntityGetAllOptions} (e.g. `folderKey` to list folder-scoped entities) + * By default the entity list is scoped exclusively: omitting `folderKey` + * returns only tenant-level entities; passing `folderKey` returns only + * entities in that folder. To list tenant-level and folder-level entities + * together in a single call, pass `includeAllFolders: true` (with no + * `folderKey`). When `folderKey` is provided it always wins, scoping the + * result to that single folder and ignoring `includeAllFolders`. + * + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllFolders` to list tenant and folder entities together) * @returns Promise resolving to an array of entity metadata * * @example @@ -524,7 +525,10 @@ export class EntityService extends BaseService implements EntityServiceModel { * // Get tenant-level entities * const tenantEntities = await entities.getAll(); * - * // Get folder-scoped entities + * // Get tenant-level and folder-level entities together + * const allEntities = await entities.getAll({ includeAllFolders: true }); + * + * // Get a single folder's entities * const folderEntities = await entities.getAll({ folderKey: "" }); * * // Call operations on an entity @@ -533,8 +537,15 @@ export class EntityService extends BaseService implements EntityServiceModel { */ @track('Entities.GetAll') async getAll(options?: EntityGetAllOptions): Promise { + // folderKey always wins: when present, scope to that folder via the v1 endpoint + header. + // Only when no folderKey is given does includeAllFolders switch to the v2 endpoint, + // which returns tenant-level and folder-level entities together. + const endpoint = !options?.folderKey && options?.includeAllFolders + ? DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL_V2 + : DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL; + const response = await this.get( - DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, + endpoint, { headers: createHeaders({ [FOLDER_KEY]: options?.folderKey }) } ); diff --git a/src/utils/constants/endpoints/data-fabric.ts b/src/utils/constants/endpoints/data-fabric.ts index 8cd508b53..8ad986417 100644 --- a/src/utils/constants/endpoints/data-fabric.ts +++ b/src/utils/constants/endpoints/data-fabric.ts @@ -17,6 +17,8 @@ export const DATA_FABRIC_TENANT_FOLDER_ID = '00000000-0000-0000-0000-00000000000 export const DATA_FABRIC_ENDPOINTS = { ENTITY: { GET_ALL: `${DATAFABRIC_BASE}/api/Entity`, + // Lists tenant-level and folder-level entities together (used by getAll when includeAllFolders is set) + GET_ALL_V2: `${DATAFABRIC_BASE}/api/v2/Entity`, GET_ENTITY_RECORDS: (entityId: string) => `${DATAFABRIC_BASE}/api/EntityService/entity/${entityId}/read`, GET_BY_ID: (entityId: string) => `${DATAFABRIC_BASE}/api/Entity/${entityId}`, GET_RECORD_BY_ID: (entityId: string, recordId: string) => diff --git a/tests/integration/shared/data-fabric/entities.integration.test.ts b/tests/integration/shared/data-fabric/entities.integration.test.ts index bb37c942d..fd8a37288 100644 --- a/tests/integration/shared/data-fabric/entities.integration.test.ts +++ b/tests/integration/shared/data-fabric/entities.integration.test.ts @@ -237,6 +237,32 @@ describe.each(modes)('Data Fabric Entities - Integration Tests [%s]', (mode) => expect(folderIds.has(tenantEntity.id)).toBe(false); } }); + + // includeAllFolders switches to the v2 endpoint, which returns tenant-level and + // folder-level entities together — a superset of the default tenant-only result. + it('should return tenant and folder entities together when includeAllFolders is true', async () => { + const { entities } = getServices(); + + const [tenantEntities, allEntities] = await Promise.all([ + entities.getAll(), + entities.getAll({ includeAllFolders: true }), + ]); + + expect(Array.isArray(allEntities)).toBe(true); + // The combined list is a superset of the tenant-only list + expect(allEntities.length).toBeGreaterThanOrEqual(tenantEntities.length); + + const allIds = new Set(allEntities.map((e) => e.id)); + for (const tenantEntity of tenantEntities) { + expect(allIds.has(tenantEntity.id)).toBe(true); + } + + // Each entity still carries metadata with methods attached + for (const entity of allEntities) { + expect(entity.id).toBeDefined(); + expect(typeof entity.getAllRecords).toBe('function'); + } + }); }); describe('getById', () => { diff --git a/tests/unit/services/data-fabric/entities.test.ts b/tests/unit/services/data-fabric/entities.test.ts index d55686290..7e76f6cf9 100644 --- a/tests/unit/services/data-fabric/entities.test.ts +++ b/tests/unit/services/data-fabric/entities.test.ts @@ -422,6 +422,42 @@ describe("EntityService Unit Tests", () => { { headers: { "X-UIPATH-FolderKey": ENTITY_TEST_CONSTANTS.FIELD_ID } }, ); }); + + it("should call the v2 endpoint when includeAllFolders is true", async () => { + mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); + + await entityService.getAll({ includeAllFolders: true }); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL_V2, + { headers: {} }, + ); + }); + + it("should call the v1 endpoint when includeAllFolders is false", async () => { + mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); + + await entityService.getAll({ includeAllFolders: false }); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, + { headers: {} }, + ); + }); + + it("should let folderKey win and call v1 with the header when both folderKey and includeAllFolders are provided", async () => { + mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); + + await entityService.getAll({ + folderKey: ENTITY_TEST_CONSTANTS.FIELD_ID, + includeAllFolders: true, + }); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, + { headers: { "X-UIPATH-FolderKey": ENTITY_TEST_CONSTANTS.FIELD_ID } }, + ); + }); }); describe("getAllRecords", () => { From 321987e730ee55eac23e35d911ef918221b1f29f Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Thu, 11 Jun 2026 21:25:19 +0530 Subject: [PATCH 08/23] refactor(data-fabric): rename includeAllFolders to includeAllScopes on entities.getAll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flag returns BOTH tenant-level and folder-level entities together — "all folders" was misleading on two counts: it sounded folder-only (excluding tenant entities), and tenant isn't a "folder." "Scopes" is the right abstraction since Data Fabric entities have two scopes (tenant + folder) and the flag toggles "give me everything regardless of scope." Renames across types, service implementation, JSDoc, unit + integration tests, endpoint comment, and oauth-scopes docs. Co-Authored-By: Claude Opus 4.7 --- docs/oauth-scopes.md | 2 +- src/models/data-fabric/entities.models.ts | 8 ++++---- src/models/data-fabric/entities.types.ts | 2 +- src/services/data-fabric/entities.ts | 12 ++++++------ src/utils/constants/endpoints/data-fabric.ts | 2 +- .../shared/data-fabric/entities.integration.test.ts | 6 +++--- tests/unit/services/data-fabric/entities.test.ts | 12 ++++++------ 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/oauth-scopes.md b/docs/oauth-scopes.md index af7c85dca..908a6e6a1 100644 --- a/docs/oauth-scopes.md +++ b/docs/oauth-scopes.md @@ -43,7 +43,7 @@ This page lists the specific OAuth scopes required in external app for each SDK ## Entities -> **Folder-scoped entities require `OR.Users` in addition to the scope below.** Any CRUD operation on a folder entity (passing `folderKey`), and `getAll({ includeAllFolders: true })` which lists all tenant and folder entities, need `OR.Users` as well. +> **Folder-scoped entities require `OR.Users` in addition to the scope below.** Any CRUD operation on a folder entity (passing `folderKey`), and `getAll({ includeAllScopes: true })` which lists all tenant and folder entities, need `OR.Users` as well. | Method | OAuth Scope | |--------|-------------| diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index d2e0d118c..e4d0a554c 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -58,11 +58,11 @@ export interface EntityServiceModel { * By default the entity list is scoped exclusively: omitting `folderKey` * returns only tenant-level entities; passing `folderKey` returns only * entities in that folder. To list tenant-level and folder-level entities - * together in a single call, pass `includeAllFolders: true` (with no + * together in a single call, pass `includeAllScopes: true` (with no * `folderKey`). When `folderKey` is provided it always wins, scoping the - * result to that single folder and ignoring `includeAllFolders`. + * result to that single folder and ignoring `includeAllScopes`. * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllFolders` to list tenant and folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllScopes` to list tenant and folder entities together) * @returns Promise resolving to an array of entity metadata * {@link EntityGetResponse} * @example @@ -71,7 +71,7 @@ export interface EntityServiceModel { * const tenantEntities = await entities.getAll(); * * // Get tenant-level and folder-level entities together - * const allEntities = await entities.getAll({ includeAllFolders: true }); + * const allEntities = await entities.getAll({ includeAllScopes: true }); * * // Get a single folder's entities * const folderEntities = await entities.getAll({ folderKey: "" }); diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index 1cfde4e2c..cd98dc994 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -333,7 +333,7 @@ export interface EntityGetAllOptions extends EntityFolderScopedOptions { * Omit (or `false`) to return only tenant-level entities. * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. */ - includeAllFolders?: boolean; + includeAllScopes?: boolean; } /** diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index f2e72093b..6a5b0eaae 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -509,11 +509,11 @@ export class EntityService extends BaseService implements EntityServiceModel { * By default the entity list is scoped exclusively: omitting `folderKey` * returns only tenant-level entities; passing `folderKey` returns only * entities in that folder. To list tenant-level and folder-level entities - * together in a single call, pass `includeAllFolders: true` (with no + * together in a single call, pass `includeAllScopes: true` (with no * `folderKey`). When `folderKey` is provided it always wins, scoping the - * result to that single folder and ignoring `includeAllFolders`. + * result to that single folder and ignoring `includeAllScopes`. * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllFolders` to list tenant and folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllScopes` to list tenant and folder entities together) * @returns Promise resolving to an array of entity metadata * * @example @@ -526,7 +526,7 @@ export class EntityService extends BaseService implements EntityServiceModel { * const tenantEntities = await entities.getAll(); * * // Get tenant-level and folder-level entities together - * const allEntities = await entities.getAll({ includeAllFolders: true }); + * const allEntities = await entities.getAll({ includeAllScopes: true }); * * // Get a single folder's entities * const folderEntities = await entities.getAll({ folderKey: "" }); @@ -538,9 +538,9 @@ export class EntityService extends BaseService implements EntityServiceModel { @track('Entities.GetAll') async getAll(options?: EntityGetAllOptions): Promise { // folderKey always wins: when present, scope to that folder via the v1 endpoint + header. - // Only when no folderKey is given does includeAllFolders switch to the v2 endpoint, + // Only when no folderKey is given does includeAllScopes switch to the v2 endpoint, // which returns tenant-level and folder-level entities together. - const endpoint = !options?.folderKey && options?.includeAllFolders + const endpoint = !options?.folderKey && options?.includeAllScopes ? DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL_V2 : DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL; diff --git a/src/utils/constants/endpoints/data-fabric.ts b/src/utils/constants/endpoints/data-fabric.ts index 8ad986417..81ae1a77c 100644 --- a/src/utils/constants/endpoints/data-fabric.ts +++ b/src/utils/constants/endpoints/data-fabric.ts @@ -17,7 +17,7 @@ export const DATA_FABRIC_TENANT_FOLDER_ID = '00000000-0000-0000-0000-00000000000 export const DATA_FABRIC_ENDPOINTS = { ENTITY: { GET_ALL: `${DATAFABRIC_BASE}/api/Entity`, - // Lists tenant-level and folder-level entities together (used by getAll when includeAllFolders is set) + // Lists tenant-level and folder-level entities together (used by getAll when includeAllScopes is set) GET_ALL_V2: `${DATAFABRIC_BASE}/api/v2/Entity`, GET_ENTITY_RECORDS: (entityId: string) => `${DATAFABRIC_BASE}/api/EntityService/entity/${entityId}/read`, GET_BY_ID: (entityId: string) => `${DATAFABRIC_BASE}/api/Entity/${entityId}`, diff --git a/tests/integration/shared/data-fabric/entities.integration.test.ts b/tests/integration/shared/data-fabric/entities.integration.test.ts index fd8a37288..09dbe8d6c 100644 --- a/tests/integration/shared/data-fabric/entities.integration.test.ts +++ b/tests/integration/shared/data-fabric/entities.integration.test.ts @@ -238,14 +238,14 @@ describe.each(modes)('Data Fabric Entities - Integration Tests [%s]', (mode) => } }); - // includeAllFolders switches to the v2 endpoint, which returns tenant-level and + // includeAllScopes switches to the v2 endpoint, which returns tenant-level and // folder-level entities together — a superset of the default tenant-only result. - it('should return tenant and folder entities together when includeAllFolders is true', async () => { + it('should return tenant and folder entities together when includeAllScopes is true', async () => { const { entities } = getServices(); const [tenantEntities, allEntities] = await Promise.all([ entities.getAll(), - entities.getAll({ includeAllFolders: true }), + entities.getAll({ includeAllScopes: true }), ]); expect(Array.isArray(allEntities)).toBe(true); diff --git a/tests/unit/services/data-fabric/entities.test.ts b/tests/unit/services/data-fabric/entities.test.ts index 7e76f6cf9..b141b6647 100644 --- a/tests/unit/services/data-fabric/entities.test.ts +++ b/tests/unit/services/data-fabric/entities.test.ts @@ -423,10 +423,10 @@ describe("EntityService Unit Tests", () => { ); }); - it("should call the v2 endpoint when includeAllFolders is true", async () => { + it("should call the v2 endpoint when includeAllScopes is true", async () => { mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); - await entityService.getAll({ includeAllFolders: true }); + await entityService.getAll({ includeAllScopes: true }); expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL_V2, @@ -434,10 +434,10 @@ describe("EntityService Unit Tests", () => { ); }); - it("should call the v1 endpoint when includeAllFolders is false", async () => { + it("should call the v1 endpoint when includeAllScopes is false", async () => { mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); - await entityService.getAll({ includeAllFolders: false }); + await entityService.getAll({ includeAllScopes: false }); expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, @@ -445,12 +445,12 @@ describe("EntityService Unit Tests", () => { ); }); - it("should let folderKey win and call v1 with the header when both folderKey and includeAllFolders are provided", async () => { + it("should let folderKey win and call v1 with the header when both folderKey and includeAllScopes are provided", async () => { mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); await entityService.getAll({ folderKey: ENTITY_TEST_CONSTANTS.FIELD_ID, - includeAllFolders: true, + includeAllScopes: true, }); expect(mockApiClient.get).toHaveBeenCalledWith( From 19093c2a3217828bb9c6a52370a3ef05d8902d93 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Thu, 11 Jun 2026 21:29:45 +0530 Subject: [PATCH 09/23] docs(data-fabric): inline tenant folder UUID literal in referenceFolderKey JSDoc DATA_FABRIC_TENANT_FOLDER_ID is an internal endpoint constant not exported through the public API barrel, so users had no way to resolve the reference. Inlines the literal '00000000-0000-0000-0000-000000000000' so the JSDoc is self-contained. Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/entities.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index cd98dc994..211f0da1d 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -310,7 +310,7 @@ export interface EntityCreateFieldOptions extends EntityFieldBase { referenceEntityId?: string; /** UUID of the referenced field on the target entity (required when `type` is `RELATIONSHIP` or `FILE`) */ referenceFieldId?: string; - /** Folder key of the reference target when it lives outside the source's folder. Pass `DATA_FABRIC_TENANT_FOLDER_ID` for tenant-level system targets. */ + /** Folder key of the reference target when it lives outside the source's folder. Pass `'00000000-0000-0000-0000-000000000000'` for tenant-level system targets. */ referenceFolderKey?: string; } From 5e605fb5bf20d61c8abf55e553edf890217a3638 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Fri, 12 Jun 2026 14:21:42 +0530 Subject: [PATCH 10/23] feat(data-fabric): add includeFolderChoiceSets opt-in + address Sarath PR comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChoiceSets: - Add `includeFolderChoiceSets?: boolean` option to ChoiceSetGetAllOptions - Default tenant-only via tenant-marker header (no OR.Users required) - `includeFolderChoiceSets: true` opts into cross-scope (tenant + folder), matching the `includeAllScopes` pattern on entities - folderKey continues to scope to a single folder (always wins over the flag) Entities: - Keep `includeAllScopes` (per Sarath's merged PR) — default tenant-only via v1 endpoint; `includeAllScopes: true` uses v2 (requires OR.Users) - JSDoc rewritten to spell out the canonical 3-mode pattern Sarath PR comment fixes: - Remove self-referential `Options for {@link X}` JSDoc on options interfaces in both choicesets.types.ts and entities.types.ts - Use DATA_FABRIC_TENANT_FOLDER_ID constant in integration test - Add OR.Users note for ChoiceSets in oauth-scopes.md - Restore GET_ALL_V2 endpoint constant alongside v1 GET_ALL - Unit test coverage for all 3 modes on both services Co-Authored-By: Claude Opus 4.7 --- docs/oauth-scopes.md | 2 + src/models/data-fabric/choicesets.models.ts | 35 ++++-------- src/models/data-fabric/choicesets.types.ts | 22 +++----- src/models/data-fabric/entities.models.ts | 24 ++++---- src/models/data-fabric/entities.types.ts | 20 +------ src/services/data-fabric/choicesets.ts | 31 +++++++---- src/services/data-fabric/entities.ts | 30 +++++----- src/utils/constants/endpoints/data-fabric.ts | 3 +- .../data-fabric/entities.integration.test.ts | 3 +- .../services/data-fabric/choicesets.test.ts | 55 +++++++++++++++++-- .../services/data-fabric/entities.test.ts | 8 +-- 11 files changed, 132 insertions(+), 101 deletions(-) diff --git a/docs/oauth-scopes.md b/docs/oauth-scopes.md index 908a6e6a1..ee5a98518 100644 --- a/docs/oauth-scopes.md +++ b/docs/oauth-scopes.md @@ -65,6 +65,8 @@ This page lists the specific OAuth scopes required in external app for each SDK ## ChoiceSets +> **Folder-scoped choice sets require `OR.Users` in addition to the scope below.** Any CRUD operation on a folder choice set (passing `folderKey`), and `getAll({ includeFolderChoiceSets: true })` which lists all tenant and folder choice sets, need `OR.Users` as well. + | Method | OAuth Scope | |--------|-------------| | `getAll()` | `DataFabric.Schema.Read` | diff --git a/src/models/data-fabric/choicesets.models.ts b/src/models/data-fabric/choicesets.models.ts index 158491632..bfa592ac5 100644 --- a/src/models/data-fabric/choicesets.models.ts +++ b/src/models/data-fabric/choicesets.models.ts @@ -32,40 +32,29 @@ import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '. */ export interface ChoiceSetServiceModel { /** - * Gets choice sets in the org + * Gets choice sets in the org. * - * The Data Fabric choice-set list is scoped exclusively, not additively: - * omitting `folderKey` returns only tenant-level choice sets; passing - * `folderKey` returns only choice sets in that folder. To enumerate - * every choice set across folders, call `getAll()` once per folder - * plus once with no `folderKey` for the tenant scope. + * Three call modes: + * - `getAll({ includeFolderChoiceSets: false })` — default. Returns only tenant-level choice sets. No `OR.Users` scope required. + * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. + * - `getAll({ includeFolderChoiceSets: false, folderKey: "" })` — returns only choice sets in that folder. `folderKey` always wins over `includeFolderChoiceSets`. * - * @param options - Optional {@link ChoiceSetGetAllOptions} (e.g. `folderKey` to list folder-scoped choice sets) + * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets, `includeFolderChoiceSets: true` to list tenant + folder choice sets together) * @returns Promise resolving to an array of choice set metadata * {@link ChoiceSetGetAllResponse} * @example * ```typescript - * // Get tenant-level choice sets - * const tenantChoiceSets = await choicesets.getAll(); + * // Tenant-only (default — no OR.Users needed) + * const tenantChoiceSets = await choicesets.getAll({ includeFolderChoiceSets: false }); * - * // Get folder-scoped choice sets - * const folderChoiceSets = await choicesets.getAll({ folderKey: "" }); + * // Tenant + folder choice sets together (requires OR.Users scope) + * const allChoiceSets = await choicesets.getAll({ includeFolderChoiceSets: true }); * - * // Iterate through choice sets - * tenantChoiceSets.forEach(choiceSet => { - * console.log(`ChoiceSet: ${choiceSet.displayName} (${choiceSet.name})`); - * console.log(`Description: ${choiceSet.description}`); - * console.log(`Created by: ${choiceSet.createdBy}`); - * }); + * // A single folder's choice sets + * const folderChoiceSets = await choicesets.getAll({ includeFolderChoiceSets: false, folderKey: "" }); * * // Find a specific choice set by name * const expenseTypes = tenantChoiceSets.find(cs => cs.name === 'ExpenseTypes'); - * - * // Check choice set details - * if (expenseTypes) { - * console.log(`Last updated: ${expenseTypes.updatedTime}`); - * console.log(`Updated by: ${expenseTypes.updatedBy}`); - * } * ``` */ getAll(options?: ChoiceSetGetAllOptions): Promise; diff --git a/src/models/data-fabric/choicesets.types.ts b/src/models/data-fabric/choicesets.types.ts index eb593ba0c..5a7830c78 100644 --- a/src/models/data-fabric/choicesets.types.ts +++ b/src/models/data-fabric/choicesets.types.ts @@ -50,10 +50,15 @@ export interface ChoiceSetGetResponse { recordOwner?: string; } -/** - * Options for {@link ChoiceSetServiceModel.getAll} - */ -export interface ChoiceSetGetAllOptions extends EntityFolderScopedOptions {} +export interface ChoiceSetGetAllOptions extends EntityFolderScopedOptions { + /** + * When `true`, also returns folder-level choice sets alongside tenant ones + * (requires the `OR.Users` OAuth scope). + * Omit (or `false`, the default) to return only tenant-level choice sets — no `OR.Users` needed. + * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. + */ + includeFolderChoiceSets?: boolean; +} /** * Options for getting choice set values by choice set ID @@ -80,9 +85,6 @@ export interface ChoiceSetUpdateOptions extends EntityFolderScopedOptions { description?: string; } -/** - * Options for {@link ChoiceSetServiceModel.deleteById} - */ export interface ChoiceSetDeleteByIdOptions extends EntityFolderScopedOptions {} /** @@ -96,14 +98,8 @@ export interface ChoiceSetValueInsertOptions extends EntityFolderScopedOptions { displayName?: string; } -/** - * Options for {@link ChoiceSetServiceModel.updateValueById} - */ export interface ChoiceSetValueUpdateOptions extends EntityFolderScopedOptions {} -/** - * Options for {@link ChoiceSetServiceModel.deleteValuesById} - */ export interface ChoiceSetValueDeleteOptions extends EntityFolderScopedOptions {} /** diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index e4d0a554c..89208f328 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -53,28 +53,26 @@ import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '. */ export interface EntityServiceModel { /** - * Gets entities in the system + * Gets entities in the system. * - * By default the entity list is scoped exclusively: omitting `folderKey` - * returns only tenant-level entities; passing `folderKey` returns only - * entities in that folder. To list tenant-level and folder-level entities - * together in a single call, pass `includeAllScopes: true` (with no - * `folderKey`). When `folderKey` is provided it always wins, scoping the - * result to that single folder and ignoring `includeAllScopes`. + * Three call modes: + * - `getAll({ includeAllScopes: false })` — default. Returns only tenant-level entities. No `OR.Users` scope required. + * - `getAll({ includeAllScopes: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. + * - `getAll({ includeAllScopes: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeAllScopes`. * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllScopes` to list tenant and folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllScopes: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * {@link EntityGetResponse} * @example * ```typescript - * // Get tenant-level entities - * const tenantEntities = await entities.getAll(); + * // Tenant-only (default — no OR.Users needed) + * const tenantEntities = await entities.getAll({ includeAllScopes: false }); * - * // Get tenant-level and folder-level entities together + * // Tenant + folder entities together (requires OR.Users scope) * const allEntities = await entities.getAll({ includeAllScopes: true }); * - * // Get a single folder's entities - * const folderEntities = await entities.getAll({ folderKey: "" }); + * // A single folder's entities + * const folderEntities = await entities.getAll({ includeAllScopes: false, folderKey: "" }); * * // Iterate through entities * tenantEntities.forEach(entity => { diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index 211f0da1d..ddd7ad450 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -125,14 +125,8 @@ export interface EntityDeleteOptions extends EntityFolderScopedOptions { */ export interface EntityDeleteRecordsOptions extends EntityDeleteOptions {} -/** - * Options for {@link EntityServiceModel.deleteRecordById} - */ export interface EntityDeleteRecordByIdOptions extends EntityFolderScopedOptions {} -/** - * Options for {@link EntityServiceModel.importRecordsById} - */ export interface EntityImportRecordsByIdOptions extends EntityFolderScopedOptions {} @@ -324,26 +318,18 @@ export interface EntityFolderScopedOptions { folderKey?: string; } -/** - * Options for {@link EntityServiceModel.getAll} - */ export interface EntityGetAllOptions extends EntityFolderScopedOptions { /** - * When `true`, returns tenant-level and folder-level entities together. - * Omit (or `false`) to return only tenant-level entities. + * When `true`, returns tenant-level and folder-level entities together + * (requires the `OR.Users` OAuth scope). + * Omit (or `false`, the default) to return only tenant-level entities — no `OR.Users` needed. * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. */ includeAllScopes?: boolean; } -/** - * Options for {@link EntityServiceModel.getById} - */ export interface EntityGetByIdOptions extends EntityFolderScopedOptions {} -/** - * Options for {@link EntityServiceModel.deleteById} - */ export interface EntityDeleteByIdOptions extends EntityFolderScopedOptions {} /** diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index 5d36b039c..064251c6d 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -29,13 +29,14 @@ import { ValidationError, NotFoundError } from '../../core/errors'; export class ChoiceSetService extends BaseService implements ChoiceSetServiceModel { /** - * Gets choice sets in the system + * Gets choice sets in the system. * - * The Data Fabric choice-set list is scoped exclusively, not additively: - * omitting `folderKey` returns only tenant-level choice sets; passing - * `folderKey` returns only choice sets in that folder. + * Three call modes: + * - `getAll({ includeFolderChoiceSets: false })` — default. Returns only tenant-level choice sets. No `OR.Users` scope required. + * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. + * - `getAll({ includeFolderChoiceSets: false, folderKey: "" })` — returns only choice sets in that folder. `folderKey` always wins over `includeFolderChoiceSets`. * - * @param options - Optional {@link ChoiceSetGetAllOptions} (e.g. `folderKey` to list folder-scoped choice sets) + * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets, `includeFolderChoiceSets: true` to list tenant + folder choice sets together) * @returns Promise resolving to an array of choice set metadata * * @example @@ -44,18 +45,28 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * * const choiceSets = new ChoiceSets(sdk); * - * // Get tenant-level choice sets - * const tenantChoiceSets = await choiceSets.getAll(); + * // Tenant-only (default — no OR.Users needed) + * const tenantChoiceSets = await choiceSets.getAll({ includeFolderChoiceSets: false }); * - * // Get folder-scoped choice sets - * const folderChoiceSets = await choiceSets.getAll({ folderKey: "" }); + * // Tenant + folder choice sets together (requires OR.Users scope) + * const allChoiceSets = await choiceSets.getAll({ includeFolderChoiceSets: true }); + * + * // A single folder's choice sets + * const folderChoiceSets = await choiceSets.getAll({ includeFolderChoiceSets: false, folderKey: "" }); * ``` */ @track('Choicesets.GetAll') async getAll(options?: ChoiceSetGetAllOptions): Promise { + // The choice-set endpoint returns cross-scope results when called without + // a folder header. To stay tenant-only by default (no OR.Users required), + // send the tenant-marker UUID as the folder key unless the caller explicitly + // opts into cross-scope via includeFolderChoiceSets: true. folderKey always wins. + const folderKey = options?.folderKey + ?? (options?.includeFolderChoiceSets ? undefined : DATA_FABRIC_TENANT_FOLDER_ID); + const rawResponse = await this.get( DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, - { headers: createHeaders({ [FOLDER_KEY]: options?.folderKey }) } + { headers: createHeaders({ [FOLDER_KEY]: folderKey }) } ); // Transform field names diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 6a5b0eaae..9db9af65b 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -504,16 +504,14 @@ export class EntityService extends BaseService implements EntityServiceModel { } /** - * Gets all entities in the system + * Gets all entities in the system. * - * By default the entity list is scoped exclusively: omitting `folderKey` - * returns only tenant-level entities; passing `folderKey` returns only - * entities in that folder. To list tenant-level and folder-level entities - * together in a single call, pass `includeAllScopes: true` (with no - * `folderKey`). When `folderKey` is provided it always wins, scoping the - * result to that single folder and ignoring `includeAllScopes`. + * Three call modes: + * - `getAll({ includeAllScopes: false })` — default. Returns only tenant-level entities. No `OR.Users` scope required. + * - `getAll({ includeAllScopes: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. + * - `getAll({ includeAllScopes: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeAllScopes`. * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllScopes` to list tenant and folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllScopes: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * * @example @@ -522,14 +520,14 @@ export class EntityService extends BaseService implements EntityServiceModel { * * const entities = new Entities(sdk); * - * // Get tenant-level entities - * const tenantEntities = await entities.getAll(); + * // Tenant-only (default — no OR.Users needed) + * const tenantEntities = await entities.getAll({ includeAllScopes: false }); * - * // Get tenant-level and folder-level entities together + * // Tenant + folder entities together (requires OR.Users scope) * const allEntities = await entities.getAll({ includeAllScopes: true }); * - * // Get a single folder's entities - * const folderEntities = await entities.getAll({ folderKey: "" }); + * // A single folder's entities + * const folderEntities = await entities.getAll({ includeAllScopes: false, folderKey: "" }); * * // Call operations on an entity * const records = await tenantEntities[0].getAllRecords(); @@ -538,8 +536,10 @@ export class EntityService extends BaseService implements EntityServiceModel { @track('Entities.GetAll') async getAll(options?: EntityGetAllOptions): Promise { // folderKey always wins: when present, scope to that folder via the v1 endpoint + header. - // Only when no folderKey is given does includeAllScopes switch to the v2 endpoint, - // which returns tenant-level and folder-level entities together. + // Only when no folderKey is given AND includeAllScopes is explicitly true does the SDK + // switch to the v2 endpoint (returns tenant + folder entities together — requires OR.Users). + // Default (no options or includeAllScopes omitted) stays on the v1 endpoint = tenant only, + // no OR.Users required. const endpoint = !options?.folderKey && options?.includeAllScopes ? DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL_V2 : DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL; diff --git a/src/utils/constants/endpoints/data-fabric.ts b/src/utils/constants/endpoints/data-fabric.ts index 81ae1a77c..8d266fc66 100644 --- a/src/utils/constants/endpoints/data-fabric.ts +++ b/src/utils/constants/endpoints/data-fabric.ts @@ -17,7 +17,8 @@ export const DATA_FABRIC_TENANT_FOLDER_ID = '00000000-0000-0000-0000-00000000000 export const DATA_FABRIC_ENDPOINTS = { ENTITY: { GET_ALL: `${DATAFABRIC_BASE}/api/Entity`, - // Lists tenant-level and folder-level entities together (used by getAll when includeAllScopes is set) + // Lists tenant-level and folder-level entities together (requires OR.Users scope). + // Used by getAll when includeAllScopes is true. GET_ALL_V2: `${DATAFABRIC_BASE}/api/v2/Entity`, GET_ENTITY_RECORDS: (entityId: string) => `${DATAFABRIC_BASE}/api/EntityService/entity/${entityId}/read`, GET_BY_ID: (entityId: string) => `${DATAFABRIC_BASE}/api/Entity/${entityId}`, diff --git a/tests/integration/shared/data-fabric/entities.integration.test.ts b/tests/integration/shared/data-fabric/entities.integration.test.ts index 09dbe8d6c..4bffbf7b5 100644 --- a/tests/integration/shared/data-fabric/entities.integration.test.ts +++ b/tests/integration/shared/data-fabric/entities.integration.test.ts @@ -17,6 +17,7 @@ import { QueryFilterOperator, RawEntityGetResponse, } from '../../../../src/models/data-fabric/entities.types'; +import { DATA_FABRIC_TENANT_FOLDER_ID } from '../../../../src/utils/constants/endpoints/data-fabric'; // Cache for choice set values to avoid repeated API calls within a test run const choiceSetValueCache = new Map(); @@ -128,7 +129,7 @@ async function buildDummyRecord(entityMetadata: RawEntityGetResponse): Promise { expect(result[0].createdTime).toBe(CHOICESET_TEST_CONSTANTS.CREATED_TIME); expect(result[0].updatedTime).toBe(CHOICESET_TEST_CONSTANTS.UPDATED_TIME); - // Verify the API call has correct endpoint + // Verify the API call — default tenant-only via tenant-marker header expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, - { headers: {} } + { headers: { 'X-UIPATH-FolderKey': DATA_FABRIC_TENANT_FOLDER_ID } } ); }); @@ -107,10 +107,10 @@ describe('ChoiceSetService Unit Tests', () => { expect(choiceSet).toHaveProperty('updatedTime'); }); - // Verify the API call + // Verify the API call — default tenant-only via tenant-marker header expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, - { headers: {} } + { headers: { 'X-UIPATH-FolderKey': DATA_FABRIC_TENANT_FOLDER_ID } } ); }); @@ -164,6 +164,53 @@ describe('ChoiceSetService Unit Tests', () => { ); }); + it('should send tenant-marker folder header by default (tenant-only)', async () => { + mockApiClient.get.mockResolvedValue([createMockChoiceSetResponse()]); + + await choiceSetService.getAll(); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, + { headers: { 'X-UIPATH-FolderKey': DATA_FABRIC_TENANT_FOLDER_ID } } + ); + }); + + it('should send tenant-marker folder header when includeFolderChoiceSets is false', async () => { + mockApiClient.get.mockResolvedValue([createMockChoiceSetResponse()]); + + await choiceSetService.getAll({ includeFolderChoiceSets: false }); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, + { headers: { 'X-UIPATH-FolderKey': DATA_FABRIC_TENANT_FOLDER_ID } } + ); + }); + + it('should omit folder header when includeFolderChoiceSets is true (cross-scope)', async () => { + mockApiClient.get.mockResolvedValue([createMockChoiceSetResponse()]); + + await choiceSetService.getAll({ includeFolderChoiceSets: true }); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, + { headers: {} } + ); + }); + + it('should let folderKey win when both folderKey and includeFolderChoiceSets are provided', async () => { + mockApiClient.get.mockResolvedValue([createMockChoiceSetResponse()]); + + await choiceSetService.getAll({ + folderKey: CHOICESET_TEST_CONSTANTS.FOLDER_KEY, + includeFolderChoiceSets: true, + }); + + expect(mockApiClient.get).toHaveBeenCalledWith( + DATA_FABRIC_ENDPOINTS.CHOICESETS.GET_ALL, + { headers: { 'X-UIPATH-FolderKey': CHOICESET_TEST_CONSTANTS.FOLDER_KEY } } + ); + }); + }); describe('getById', () => { diff --git a/tests/unit/services/data-fabric/entities.test.ts b/tests/unit/services/data-fabric/entities.test.ts index b141b6647..9ac327791 100644 --- a/tests/unit/services/data-fabric/entities.test.ts +++ b/tests/unit/services/data-fabric/entities.test.ts @@ -337,7 +337,7 @@ describe("EntityService Unit Tests", () => { expect(typeof entity.getAllRecords).toBe("function"); }); - // Verify the API call + // Verify the API call — default tenant-only via v1 endpoint expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, { headers: {} }, @@ -396,7 +396,7 @@ describe("EntityService Unit Tests", () => { }); }); - // Verify the API call + // Verify the API call — default tenant-only via v1 endpoint expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, { headers: {} }, @@ -423,7 +423,7 @@ describe("EntityService Unit Tests", () => { ); }); - it("should call the v2 endpoint when includeAllScopes is true", async () => { + it("should call the v2 endpoint when includeAllScopes is true (cross-scope)", async () => { mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); await entityService.getAll({ includeAllScopes: true }); @@ -434,7 +434,7 @@ describe("EntityService Unit Tests", () => { ); }); - it("should call the v1 endpoint when includeAllScopes is false", async () => { + it("should call the v1 endpoint when includeAllScopes is false (tenant-only)", async () => { mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); await entityService.getAll({ includeAllScopes: false }); From 2f13c3a0897eaf6addf20d2825c2bdd03821c211 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Fri, 12 Jun 2026 14:31:36 +0530 Subject: [PATCH 11/23] =?UTF-8?q?refactor(data-fabric):=20rename=20entitie?= =?UTF-8?q?s.getAll=20option=20includeAllScopes=20=E2=86=92=20includeFolde?= =?UTF-8?q?rEntities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parallels the choicesets-side `includeFolderChoiceSets` naming so both surfaces use clear, customer-facing flag names. "AllScopes" leaked SDK internals ("scope" is not customer vocabulary); "include folder entities" states plainly what the flag adds on top of the tenant default. Same 3-mode semantics preserved: - getAll({ includeFolderEntities: false }) → tenant only (default) - getAll({ includeFolderEntities: true }) → tenant + folders (needs OR.Users) - getAll({ includeFolderEntities: false, folderKey }) → folder only (folderKey wins) Rename across: type field, JSDoc on EntityServiceModel + service class, endpoint comment, unit tests, integration test reference, oauth-scopes.md. Co-Authored-By: Claude Opus 4.7 --- docs/oauth-scopes.md | 2 +- src/models/data-fabric/entities.models.ts | 14 ++++++------- src/models/data-fabric/entities.types.ts | 2 +- src/services/data-fabric/entities.ts | 20 +++++++++---------- src/utils/constants/endpoints/data-fabric.ts | 2 +- .../data-fabric/entities.integration.test.ts | 6 +++--- .../services/data-fabric/entities.test.ts | 12 +++++------ 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/oauth-scopes.md b/docs/oauth-scopes.md index ee5a98518..02498f60d 100644 --- a/docs/oauth-scopes.md +++ b/docs/oauth-scopes.md @@ -43,7 +43,7 @@ This page lists the specific OAuth scopes required in external app for each SDK ## Entities -> **Folder-scoped entities require `OR.Users` in addition to the scope below.** Any CRUD operation on a folder entity (passing `folderKey`), and `getAll({ includeAllScopes: true })` which lists all tenant and folder entities, need `OR.Users` as well. +> **Folder-scoped entities require `OR.Users` in addition to the scope below.** Any CRUD operation on a folder entity (passing `folderKey`), and `getAll({ includeFolderEntities: true })` which lists all tenant and folder entities, need `OR.Users` as well. | Method | OAuth Scope | |--------|-------------| diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index 89208f328..acb2207f9 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -56,23 +56,23 @@ export interface EntityServiceModel { * Gets entities in the system. * * Three call modes: - * - `getAll({ includeAllScopes: false })` — default. Returns only tenant-level entities. No `OR.Users` scope required. - * - `getAll({ includeAllScopes: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. - * - `getAll({ includeAllScopes: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeAllScopes`. + * - `getAll({ includeFolderEntities: false })` — default. Returns only tenant-level entities. No `OR.Users` scope required. + * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. + * - `getAll({ includeFolderEntities: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeFolderEntities`. * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllScopes: true` to list tenant + folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeFolderEntities: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * {@link EntityGetResponse} * @example * ```typescript * // Tenant-only (default — no OR.Users needed) - * const tenantEntities = await entities.getAll({ includeAllScopes: false }); + * const tenantEntities = await entities.getAll({ includeFolderEntities: false }); * * // Tenant + folder entities together (requires OR.Users scope) - * const allEntities = await entities.getAll({ includeAllScopes: true }); + * const allEntities = await entities.getAll({ includeFolderEntities: true }); * * // A single folder's entities - * const folderEntities = await entities.getAll({ includeAllScopes: false, folderKey: "" }); + * const folderEntities = await entities.getAll({ includeFolderEntities: false, folderKey: "" }); * * // Iterate through entities * tenantEntities.forEach(entity => { diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index ddd7ad450..01601bdab 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -325,7 +325,7 @@ export interface EntityGetAllOptions extends EntityFolderScopedOptions { * Omit (or `false`, the default) to return only tenant-level entities — no `OR.Users` needed. * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. */ - includeAllScopes?: boolean; + includeFolderEntities?: boolean; } export interface EntityGetByIdOptions extends EntityFolderScopedOptions {} diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 9db9af65b..c654c0d7e 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -507,11 +507,11 @@ export class EntityService extends BaseService implements EntityServiceModel { * Gets all entities in the system. * * Three call modes: - * - `getAll({ includeAllScopes: false })` — default. Returns only tenant-level entities. No `OR.Users` scope required. - * - `getAll({ includeAllScopes: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. - * - `getAll({ includeAllScopes: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeAllScopes`. + * - `getAll({ includeFolderEntities: false })` — default. Returns only tenant-level entities. No `OR.Users` scope required. + * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. + * - `getAll({ includeFolderEntities: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeFolderEntities`. * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeAllScopes: true` to list tenant + folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeFolderEntities: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * * @example @@ -521,13 +521,13 @@ export class EntityService extends BaseService implements EntityServiceModel { * const entities = new Entities(sdk); * * // Tenant-only (default — no OR.Users needed) - * const tenantEntities = await entities.getAll({ includeAllScopes: false }); + * const tenantEntities = await entities.getAll({ includeFolderEntities: false }); * * // Tenant + folder entities together (requires OR.Users scope) - * const allEntities = await entities.getAll({ includeAllScopes: true }); + * const allEntities = await entities.getAll({ includeFolderEntities: true }); * * // A single folder's entities - * const folderEntities = await entities.getAll({ includeAllScopes: false, folderKey: "" }); + * const folderEntities = await entities.getAll({ includeFolderEntities: false, folderKey: "" }); * * // Call operations on an entity * const records = await tenantEntities[0].getAllRecords(); @@ -536,11 +536,11 @@ export class EntityService extends BaseService implements EntityServiceModel { @track('Entities.GetAll') async getAll(options?: EntityGetAllOptions): Promise { // folderKey always wins: when present, scope to that folder via the v1 endpoint + header. - // Only when no folderKey is given AND includeAllScopes is explicitly true does the SDK + // Only when no folderKey is given AND includeFolderEntities is explicitly true does the SDK // switch to the v2 endpoint (returns tenant + folder entities together — requires OR.Users). - // Default (no options or includeAllScopes omitted) stays on the v1 endpoint = tenant only, + // Default (no options or includeFolderEntities omitted) stays on the v1 endpoint = tenant only, // no OR.Users required. - const endpoint = !options?.folderKey && options?.includeAllScopes + const endpoint = !options?.folderKey && options?.includeFolderEntities ? DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL_V2 : DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL; diff --git a/src/utils/constants/endpoints/data-fabric.ts b/src/utils/constants/endpoints/data-fabric.ts index 8d266fc66..8c7bd138d 100644 --- a/src/utils/constants/endpoints/data-fabric.ts +++ b/src/utils/constants/endpoints/data-fabric.ts @@ -18,7 +18,7 @@ export const DATA_FABRIC_ENDPOINTS = { ENTITY: { GET_ALL: `${DATAFABRIC_BASE}/api/Entity`, // Lists tenant-level and folder-level entities together (requires OR.Users scope). - // Used by getAll when includeAllScopes is true. + // Used by getAll when includeFolderEntities is true. GET_ALL_V2: `${DATAFABRIC_BASE}/api/v2/Entity`, GET_ENTITY_RECORDS: (entityId: string) => `${DATAFABRIC_BASE}/api/EntityService/entity/${entityId}/read`, GET_BY_ID: (entityId: string) => `${DATAFABRIC_BASE}/api/Entity/${entityId}`, diff --git a/tests/integration/shared/data-fabric/entities.integration.test.ts b/tests/integration/shared/data-fabric/entities.integration.test.ts index 4bffbf7b5..87350764d 100644 --- a/tests/integration/shared/data-fabric/entities.integration.test.ts +++ b/tests/integration/shared/data-fabric/entities.integration.test.ts @@ -239,14 +239,14 @@ describe.each(modes)('Data Fabric Entities - Integration Tests [%s]', (mode) => } }); - // includeAllScopes switches to the v2 endpoint, which returns tenant-level and + // includeFolderEntities switches to the v2 endpoint, which returns tenant-level and // folder-level entities together — a superset of the default tenant-only result. - it('should return tenant and folder entities together when includeAllScopes is true', async () => { + it('should return tenant and folder entities together when includeFolderEntities is true', async () => { const { entities } = getServices(); const [tenantEntities, allEntities] = await Promise.all([ entities.getAll(), - entities.getAll({ includeAllScopes: true }), + entities.getAll({ includeFolderEntities: true }), ]); expect(Array.isArray(allEntities)).toBe(true); diff --git a/tests/unit/services/data-fabric/entities.test.ts b/tests/unit/services/data-fabric/entities.test.ts index 9ac327791..57ff3f1f5 100644 --- a/tests/unit/services/data-fabric/entities.test.ts +++ b/tests/unit/services/data-fabric/entities.test.ts @@ -423,10 +423,10 @@ describe("EntityService Unit Tests", () => { ); }); - it("should call the v2 endpoint when includeAllScopes is true (cross-scope)", async () => { + it("should call the v2 endpoint when includeFolderEntities is true (cross-scope)", async () => { mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); - await entityService.getAll({ includeAllScopes: true }); + await entityService.getAll({ includeFolderEntities: true }); expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL_V2, @@ -434,10 +434,10 @@ describe("EntityService Unit Tests", () => { ); }); - it("should call the v1 endpoint when includeAllScopes is false (tenant-only)", async () => { + it("should call the v1 endpoint when includeFolderEntities is false (tenant-only)", async () => { mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); - await entityService.getAll({ includeAllScopes: false }); + await entityService.getAll({ includeFolderEntities: false }); expect(mockApiClient.get).toHaveBeenCalledWith( DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL, @@ -445,12 +445,12 @@ describe("EntityService Unit Tests", () => { ); }); - it("should let folderKey win and call v1 with the header when both folderKey and includeAllScopes are provided", async () => { + it("should let folderKey win and call v1 with the header when both folderKey and includeFolderEntities are provided", async () => { mockApiClient.get.mockResolvedValue([createMockEntityResponse()]); await entityService.getAll({ folderKey: ENTITY_TEST_CONSTANTS.FIELD_ID, - includeAllScopes: true, + includeFolderEntities: true, }); expect(mockApiClient.get).toHaveBeenCalledWith( From c92063a6bd608dbb3c06c50ed74eeda3e4f93ffb Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Fri, 12 Jun 2026 14:40:20 +0530 Subject: [PATCH 12/23] test(data-fabric): skip includeFolderEntities CI test that requires OR.Users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /api/v2/Entity endpoint backing `getAll({ includeFolderEntities: true })` requires the OR.Users OAuth scope, which the standard integration PAT does not carry — so the test 403s in CI. Mark the test with `it.skip` and a comment pointing local devs at how to enable it when running with an OR.Users-capable token, matching the schema-write `describe.skip` pattern already established in this file. Co-Authored-By: Claude Opus 4.7 --- .../shared/data-fabric/entities.integration.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/shared/data-fabric/entities.integration.test.ts b/tests/integration/shared/data-fabric/entities.integration.test.ts index 87350764d..ba91aaec9 100644 --- a/tests/integration/shared/data-fabric/entities.integration.test.ts +++ b/tests/integration/shared/data-fabric/entities.integration.test.ts @@ -241,7 +241,10 @@ describe.each(modes)('Data Fabric Entities - Integration Tests [%s]', (mode) => // includeFolderEntities switches to the v2 endpoint, which returns tenant-level and // folder-level entities together — a superset of the default tenant-only result. - it('should return tenant and folder entities together when includeFolderEntities is true', async () => { + // skip: the v2 endpoint requires the OR.Users OAuth scope, which the standard + // integration PAT does not carry. Convert to `it()` locally when running with + // a PAT/OAuth token that includes OR.Users. + it.skip('should return tenant and folder entities together when includeFolderEntities is true', async () => { const { entities } = getServices(); const [tenantEntities, allEntities] = await Promise.all([ From 2c33c1655b1556f7d5b27662bfc6b8b6a0ed8ebf Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Fri, 12 Jun 2026 14:56:07 +0530 Subject: [PATCH 13/23] test(data-fabric): re-enable includeFolderEntities integration test OR.Users scope is now available on the integration PAT, so the v2 endpoint backing `getAll({ includeFolderEntities: true })` no longer returns 403. Reverts the `it.skip` and keeps a one-line comment noting the scope requirement for future readers. Co-Authored-By: Claude Opus 4.7 --- .../shared/data-fabric/entities.integration.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/integration/shared/data-fabric/entities.integration.test.ts b/tests/integration/shared/data-fabric/entities.integration.test.ts index ba91aaec9..9e5f064bd 100644 --- a/tests/integration/shared/data-fabric/entities.integration.test.ts +++ b/tests/integration/shared/data-fabric/entities.integration.test.ts @@ -241,10 +241,8 @@ describe.each(modes)('Data Fabric Entities - Integration Tests [%s]', (mode) => // includeFolderEntities switches to the v2 endpoint, which returns tenant-level and // folder-level entities together — a superset of the default tenant-only result. - // skip: the v2 endpoint requires the OR.Users OAuth scope, which the standard - // integration PAT does not carry. Convert to `it()` locally when running with - // a PAT/OAuth token that includes OR.Users. - it.skip('should return tenant and folder entities together when includeFolderEntities is true', async () => { + // Requires the OR.Users OAuth scope on the integration token. + it('should return tenant and folder entities together when includeFolderEntities is true', async () => { const { entities } = getServices(); const [tenantEntities, allEntities] = await Promise.all([ From f5b277ef7d493586833b77883bb9a85aea3d3e92 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Fri, 12 Jun 2026 15:10:34 +0530 Subject: [PATCH 14/23] fix(data-fabric): address SonarCloud issues on PR #504 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - entities.ts:1266 / choicesets.ts:398: flip negated ternaries (S7735) - entities.ts:166,630 / choicesets.ts:138: drop unnecessary `{} as T` cast on the `options ?? {}` fallback (S4325) - choicesets.integration.test.ts:256,466: add post-delete getById + assertion that the deleted value IDs are no longer present (S2699 BLOCKER — tests must contain at least one assertion) Verifies the delete actually worked, not just that the call resolved. Co-Authored-By: Claude Opus 4.7 --- src/services/data-fabric/choicesets.ts | 4 ++-- src/services/data-fabric/entities.ts | 6 +++--- .../data-fabric/choicesets.integration.test.ts | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index 064251c6d..72119b5c8 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -135,7 +135,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod // folderKey is header-only — destructure it out so PaginationHelpers doesn't // include it in the POST body alongside pagination params. - const { folderKey, ...rest } = options ?? {} as T; + const { folderKey, ...rest } = options ?? {}; const downstreamOptions = options === undefined ? undefined : (rest as T); return PaginationHelpers.getAll({ serviceAccess: this.createPaginationServiceAccess(), @@ -395,7 +395,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod } private async resolveChoiceSetName(choiceSetId: string, folderKey?: string): Promise { - const all = await this.getAll(folderKey !== undefined ? { folderKey } : undefined); + const all = await this.getAll(folderKey === undefined ? undefined : { folderKey }); const match = all.find(cs => cs.id === choiceSetId); if (!match) { throw new NotFoundError({ message: `Choice set with id '${choiceSetId}' not found.` }); diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index c654c0d7e..75d448db9 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -163,7 +163,7 @@ export class EntityService extends BaseService implements EntityServiceModel { > { // folderKey is header-only — destructure it out so PaginationHelpers doesn't serialise it // into the query string as $folderKey. - const { folderKey, ...rest } = options ?? {} as T; + const { folderKey, ...rest } = options ?? {}; const downstreamOptions = options === undefined ? undefined : (rest as T); return PaginationHelpers.getAll({ serviceAccess: this.createPaginationServiceAccess(), @@ -627,7 +627,7 @@ export class EntityService extends BaseService implements EntityServiceModel { ): Promise ? PaginatedResponse : NonPaginatedResponse> { // folderKey is header-only — destructure it out so PaginationHelpers doesn't include // it in the POST body alongside the query filters. - const { folderKey, ...rest } = options ?? {} as T; + const { folderKey, ...rest } = options ?? {}; const downstreamOptions = options === undefined ? undefined : (rest as T); return PaginationHelpers.getAll({ serviceAccess: this.createPaginationServiceAccess(), @@ -1263,7 +1263,7 @@ export class EntityService extends BaseService implements EntityServiceModel { const mapping = EntitySchemaFieldTypeMap[fieldType]; // Prefer the resolved {id, name, folderId} body so cross-folder targets resolve // server-side; fall back to a bare {id} when no meta was fetched. - const referenceEntityBody = refMeta?.referenceEntity ?? (field.referenceEntityId !== undefined ? { id: field.referenceEntityId } : undefined); + const referenceEntityBody = refMeta?.referenceEntity ?? (field.referenceEntityId === undefined ? undefined : { id: field.referenceEntityId }); const referenceChoiceSetBody = refMeta?.referenceChoiceSet; return { name: field.fieldName, diff --git a/tests/integration/shared/data-fabric/choicesets.integration.test.ts b/tests/integration/shared/data-fabric/choicesets.integration.test.ts index c961939f5..054d6bb4b 100644 --- a/tests/integration/shared/data-fabric/choicesets.integration.test.ts +++ b/tests/integration/shared/data-fabric/choicesets.integration.test.ts @@ -265,6 +265,13 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = await choiceSets.deleteValuesById(choiceSetId, serviceLevelValueIds); + // Verify the deleted values are no longer present on the choice set + const remaining = await choiceSets.getById(choiceSetId); + const remainingIds = new Set(remaining.items.map((v) => v.id)); + for (const deletedId of serviceLevelValueIds) { + expect(remainingIds.has(deletedId)).toBe(false); + } + // Remove deleted IDs from the file-level tracking list for (const id of serviceLevelValueIds) { const idx = insertedValueIds.indexOf(id); @@ -472,6 +479,13 @@ describe.each(modes)('Data Fabric ChoiceSets - Integration Tests [%s]', (mode) = await choiceSets.deleteValuesById(folderChoiceSetId, folderValueIds, { folderKey }); + // Verify the deleted values are no longer present on the choice set + const remaining = await choiceSets.getById(folderChoiceSetId, { folderKey }); + const remainingIds = new Set(remaining.items.map((v) => v.id)); + for (const deletedId of folderValueIds) { + expect(remainingIds.has(deletedId)).toBe(false); + } + folderValueIds.length = 0; }); }); From 1e6f6ab1fa35ca2ca7df9d0e9305a149348311c3 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Mon, 15 Jun 2026 16:03:58 +0530 Subject: [PATCH 15/23] docs(data-fabric): mark folder scope as experimental across Entities Folder-scoped Data Fabric is still in preview. Surface that everywhere the SDK exposes it: - Type-level: add @experimental on EntityFolderScopedOptions.folderKey, EntityGetAllOptions.includeFolderEntities, and EntityCreateFieldOptions .referenceFolderKey. TypeDoc renders these as Experimental badges in the generated reference tables. - Method-level: add a "> **Experimental:** ..." blockquote to every Entities method that documents folderKey (getAll, getById, getAllRecords, getRecordById, queryRecordsById, insertRecordById, insertRecordsById, updateRecordById, updateRecordsById, updateById, deleteRecordById, deleteRecordsById, deleteById, uploadAttachment, downloadAttachment, deleteAttachment, importRecordsById, create). - Kept JSDoc in sync between entities.models.ts (TypeDoc source of truth) and entities.ts (service class). Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/entities.models.ts | 36 +++++++++++++++++++++++ src/models/data-fabric/entities.types.ts | 14 +++++++-- src/services/data-fabric/entities.ts | 36 +++++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index acb2207f9..bb6d4abcd 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -60,6 +60,8 @@ export interface EntityServiceModel { * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. * - `getAll({ includeFolderEntities: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeFolderEntities`. * + * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. + * * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeFolderEntities: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * {@link EntityGetResponse} @@ -104,6 +106,8 @@ export interface EntityServiceModel { /** * Gets entity metadata by entity ID with attached operation methods * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param options - Optional {@link EntityGetByIdOptions} (e.g. `folderKey` for folder-scoped entities) * @returns Promise resolving to entity metadata with operation methods @@ -145,6 +149,8 @@ export interface EntityServiceModel { /** * Gets entity records by entity ID * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param options - Query options * @returns Promise resolving to either an array of entity records NonPaginatedResponse or a PaginatedResponse when pagination options are used. @@ -194,6 +200,8 @@ export interface EntityServiceModel { /** * Gets a single entity record by entity ID and record ID * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record * @param options - Query options @@ -227,6 +235,8 @@ export interface EntityServiceModel { * Note: Data Fabric supports trigger events only on individual inserts, not on inserting multiple records. * Use this method if you need trigger events to fire for the inserted record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param data - Record to insert * @param options - Insert options @@ -262,6 +272,8 @@ export interface EntityServiceModel { * Note: Records inserted using insertRecordsById will not trigger Data Fabric trigger events. Use {@link insertRecordById} if you need * trigger events to fire for each inserted record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param data - Array of records to insert * @param options - Insert options @@ -305,6 +317,8 @@ export interface EntityServiceModel { * Note: Data Fabric supports trigger events only on individual updates, not on updating multiple records. * Use this method if you need trigger events to fire for the updated record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to update * @param data - Key-value pairs of fields to update @@ -334,6 +348,8 @@ export interface EntityServiceModel { * * Note: Records updated using updateRecordsById will not trigger Data Fabric trigger events. Use {@link updateRecordById} if you need trigger events to fire for each updated record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param data - Array of records to update. Each record MUST contain the record id. * @param options - Update options @@ -370,6 +386,8 @@ export interface EntityServiceModel { * * Note: Records deleted using deleteRecordsById will not trigger Data Fabric trigger events. Use {@link deleteRecordById} if you need trigger events to fire for the deleted record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param recordIds - Array of record UUIDs to delete * @param options - Delete options @@ -396,6 +414,8 @@ export interface EntityServiceModel { * Note: Data Fabric supports trigger events only on individual deletes, not on deleting multiple records. * Use this method if you need trigger events to fire for the deleted record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to delete * @param options - Optional {@link EntityDeleteRecordByIdOptions} (e.g. `folderKey` for folder-scoped entities) @@ -417,6 +437,8 @@ export interface EntityServiceModel { /** * Queries entity records with filters, sorting, aggregates, and SDK-managed pagination * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param options - Query options including filterGroup, selectedFields, sortOptions, aggregates, groupBy, and pagination * @returns Promise resolving to {@link NonPaginatedResponse} without pagination options, @@ -476,6 +498,8 @@ export interface EntityServiceModel { /** * Imports records from a CSV file into an entity * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param file - CSV file to import as a Blob or File or Uint8Array * @param options - Optional {@link EntityImportRecordsByIdOptions} (e.g. `folderKey` for folder-scoped entities) @@ -497,6 +521,8 @@ export interface EntityServiceModel { /** * Downloads an attachment stored in a File-type field of an entity record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment @@ -554,6 +580,8 @@ export interface EntityServiceModel { * * Uses multipart/form-data to upload the file content to the specified field. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to upload the attachment to * @param fieldName - Name of the File-type field @@ -593,6 +621,8 @@ export interface EntityServiceModel { /** * Removes an attachment from a File-type field of an entity record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment @@ -625,6 +655,8 @@ export interface EntityServiceModel { /** * Creates a new Data Fabric entity with the given schema * + * > **Experimental:** `folderKey` and cross-folder `referenceFolderKey` are in preview — the contract may change. + * * @param name - Entity name — must start with a letter, letters/numbers/underscores only * (e.g., `"productCatalog"`). * @param fields - Array of field definitions @@ -673,6 +705,8 @@ export interface EntityServiceModel { /** * Deletes a Data Fabric entity and all its records * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity to delete * @param options - Optional {@link EntityDeleteByIdOptions} (e.g. `folderKey` for folder-scoped entities) * @returns Promise resolving when the entity is deleted @@ -694,6 +728,8 @@ export interface EntityServiceModel { * metadata fields (`displayName`, `description`, `isRbacEnabled`). Each group is applied * only when the corresponding fields are provided. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity to update * @param options - Changes to apply ({@link EntityUpdateByIdOptions}) * @returns Promise resolving when the update is complete diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index 01601bdab..7b950d437 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -304,7 +304,11 @@ export interface EntityCreateFieldOptions extends EntityFieldBase { referenceEntityId?: string; /** UUID of the referenced field on the target entity (required when `type` is `RELATIONSHIP` or `FILE`) */ referenceFieldId?: string; - /** Folder key of the reference target when it lives outside the source's folder. Pass `'00000000-0000-0000-0000-000000000000'` for tenant-level system targets. */ + /** + * Folder key of the reference target when it lives outside the source's folder. Pass `'00000000-0000-0000-0000-000000000000'` for tenant-level system targets. + * + * @experimental Folder-scoped Data Fabric is in preview — the contract may change. + */ referenceFolderKey?: string; } @@ -314,7 +318,11 @@ export interface EntityCreateFieldOptions extends EntityFieldBase { * Forwarded on the wire as the `X-UIPATH-FolderKey` header. */ export interface EntityFolderScopedOptions { - /** Key identifying the folder the entity belongs to. Omit for tenant-level entities. */ + /** + * Key identifying the folder the entity belongs to. Omit for tenant-level entities. + * + * @experimental Folder-scoped Data Fabric is in preview — the contract may change. + */ folderKey?: string; } @@ -324,6 +332,8 @@ export interface EntityGetAllOptions extends EntityFolderScopedOptions { * (requires the `OR.Users` OAuth scope). * Omit (or `false`, the default) to return only tenant-level entities — no `OR.Users` needed. * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. + * + * @experimental Folder-scoped Data Fabric is in preview — the contract may change. */ includeFolderEntities?: boolean; } diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 75d448db9..eeba9a11f 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -70,6 +70,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Gets entity metadata by entity ID with attached operation methods * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param options - Optional {@link EntityGetByIdOptions} (e.g. `folderKey` for folder-scoped entities) * @returns Promise resolving to entity metadata with schema information and operation methods @@ -118,6 +120,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Gets entity records by entity ID * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param options - Query options including expansionLevel and pagination options * @returns Promise resolving to an array of entity records or paginated response @@ -186,6 +190,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Gets a single entity record by entity ID and record ID * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record * @param options - Query options including `expansionLevel` and `folderKey` @@ -228,6 +234,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Inserts a single record into an entity by entity ID * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param data - Record to insert * @param options - Insert options @@ -274,6 +282,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Inserts data into an entity by entity ID using batch insert * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param data - Array of records to insert * @param options - Insert options @@ -329,6 +339,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Updates a single record in an entity by entity ID * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to update * @param data - Key-value pairs of fields to update @@ -376,6 +388,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Updates data in an entity by entity ID * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param data - Array of records to update. Each record MUST contain the record Id, * otherwise the update will fail. @@ -433,6 +447,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * Note: Records deleted using deleteRecordsById will not trigger Data Fabric trigger events. Use {@link deleteRecordById} if you need trigger events to fire for the deleted record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordIds - Array of record UUIDs to delete * @param options - Delete options @@ -479,6 +495,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * Note: Data Fabric supports trigger events only on individual deletes, not on deleting multiple records. * Use this method if you need trigger events to fire for the deleted record. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to delete * @param options - Optional {@link EntityDeleteRecordByIdOptions} (e.g. `folderKey` for folder-scoped entities) @@ -511,6 +529,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. * - `getAll({ includeFolderEntities: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeFolderEntities`. * + * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. + * * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeFolderEntities: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * @@ -564,6 +584,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Queries entity records with filters, sorting, aggregates, and pagination * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param options - Query options including filterGroup, selectedFields, sortOptions, aggregates, groupBy, and pagination * @returns Promise resolving to {@link NonPaginatedResponse} without pagination options, @@ -651,6 +673,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Imports records from a CSV file into an entity * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity * @param file - CSV file to import (Blob, File, or Uint8Array) * @param options - Optional {@link EntityImportRecordsByIdOptions} (e.g. `folderKey` for folder-scoped entities) @@ -701,6 +725,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Downloads an attachment from an entity record field * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment @@ -744,6 +770,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Uploads an attachment to a File-type field of an entity record * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to upload the attachment to * @param fieldName - Name of the File-type field @@ -798,6 +826,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Removes an attachment from a File-type field of an entity record * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment @@ -869,6 +899,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Creates a new Data Fabric entity with the given schema * + * > **Experimental:** `folderKey` and cross-folder `referenceFolderKey` are in preview — the contract may change. + * * @param name - Entity name — must start with a letter and contain * only letters, numbers, and underscores (e.g., `"productCatalog"`). * @param fields - Array of field definitions @@ -936,6 +968,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Deletes a Data Fabric entity and all its records * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity to delete * @param options - Optional {@link EntityDeleteByIdOptions} (e.g. `folderKey` for folder-scoped entities) * @returns Promise resolving when the entity is deleted @@ -968,6 +1002,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * read-modify-write pattern — concurrent calls on the same entity may silently * overwrite each other's changes. * + * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. + * * @param id - UUID of the entity to update * @param options - Changes to apply ({@link EntityUpdateByIdOptions}) * @returns Promise resolving when the update is complete From bdf6e9837900ae676c041c92a241d4f4f1aefb29 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Mon, 15 Jun 2026 16:06:43 +0530 Subject: [PATCH 16/23] docs(data-fabric): clarify choicesets.getAll JSDoc per PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Gets choice sets in the org" → "Gets choice sets in the tenant". - Default call mode is now shown as `getAll()`, not `getAll({ includeFolderChoiceSets: false })`. - Reorder call modes so `folderKey` is presented as the preferred way to scope to a folder, with `includeFolderChoiceSets` reframed as the wider tenant + folder option. Kept the JSDoc in sync between ChoiceSetServiceModel and the ChoiceSetService class. Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/choicesets.models.ts | 18 +++++++++--------- src/services/data-fabric/choicesets.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/models/data-fabric/choicesets.models.ts b/src/models/data-fabric/choicesets.models.ts index bfa592ac5..69a4e81d2 100644 --- a/src/models/data-fabric/choicesets.models.ts +++ b/src/models/data-fabric/choicesets.models.ts @@ -32,27 +32,27 @@ import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '. */ export interface ChoiceSetServiceModel { /** - * Gets choice sets in the org. + * Gets choice sets in the tenant. * * Three call modes: - * - `getAll({ includeFolderChoiceSets: false })` — default. Returns only tenant-level choice sets. No `OR.Users` scope required. - * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. - * - `getAll({ includeFolderChoiceSets: false, folderKey: "" })` — returns only choice sets in that folder. `folderKey` always wins over `includeFolderChoiceSets`. + * - `getAll()` — default. Returns only tenant-level choice sets. No `OR.Users` scope required. + * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. + * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. * - * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets, `includeFolderChoiceSets: true` to list tenant + folder choice sets together) + * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) * @returns Promise resolving to an array of choice set metadata * {@link ChoiceSetGetAllResponse} * @example * ```typescript * // Tenant-only (default — no OR.Users needed) - * const tenantChoiceSets = await choicesets.getAll({ includeFolderChoiceSets: false }); + * const tenantChoiceSets = await choicesets.getAll(); + * + * // A single folder's choice sets (preferred when targeting a specific folder) + * const folderChoiceSets = await choicesets.getAll({ folderKey: "" }); * * // Tenant + folder choice sets together (requires OR.Users scope) * const allChoiceSets = await choicesets.getAll({ includeFolderChoiceSets: true }); * - * // A single folder's choice sets - * const folderChoiceSets = await choicesets.getAll({ includeFolderChoiceSets: false, folderKey: "" }); - * * // Find a specific choice set by name * const expenseTypes = tenantChoiceSets.find(cs => cs.name === 'ExpenseTypes'); * ``` diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index 72119b5c8..8f9304363 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -29,14 +29,14 @@ import { ValidationError, NotFoundError } from '../../core/errors'; export class ChoiceSetService extends BaseService implements ChoiceSetServiceModel { /** - * Gets choice sets in the system. + * Gets choice sets in the tenant. * * Three call modes: - * - `getAll({ includeFolderChoiceSets: false })` — default. Returns only tenant-level choice sets. No `OR.Users` scope required. - * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. - * - `getAll({ includeFolderChoiceSets: false, folderKey: "" })` — returns only choice sets in that folder. `folderKey` always wins over `includeFolderChoiceSets`. + * - `getAll()` — default. Returns only tenant-level choice sets. No `OR.Users` scope required. + * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. + * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. * - * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets, `includeFolderChoiceSets: true` to list tenant + folder choice sets together) + * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) * @returns Promise resolving to an array of choice set metadata * * @example @@ -46,13 +46,13 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * const choiceSets = new ChoiceSets(sdk); * * // Tenant-only (default — no OR.Users needed) - * const tenantChoiceSets = await choiceSets.getAll({ includeFolderChoiceSets: false }); + * const tenantChoiceSets = await choiceSets.getAll(); + * + * // A single folder's choice sets (preferred when targeting a specific folder) + * const folderChoiceSets = await choiceSets.getAll({ folderKey: "" }); * * // Tenant + folder choice sets together (requires OR.Users scope) * const allChoiceSets = await choiceSets.getAll({ includeFolderChoiceSets: true }); - * - * // A single folder's choice sets - * const folderChoiceSets = await choiceSets.getAll({ includeFolderChoiceSets: false, folderKey: "" }); * ``` */ @track('Choicesets.GetAll') From 9e77c91cc944c4fb74bbb1cccaaffec07b20cd44 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Mon, 15 Jun 2026 16:08:42 +0530 Subject: [PATCH 17/23] docs(data-fabric): apply same JSDoc clarifications to entities.getAll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the choicesets.getAll review fixes onto the entities surface: - "Gets entities in the system" → "Gets entities in the tenant". - Default call mode is now shown as `getAll()`, not `getAll({ includeFolderEntities: false })`. - Reorder call modes so `folderKey` is presented as the preferred way to scope to a folder, with `includeFolderEntities` reframed as the wider tenant + folder option. Kept JSDoc in sync between EntityServiceModel and the EntityService class. Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/entities.models.ts | 18 +++++++++--------- src/services/data-fabric/entities.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index bb6d4abcd..a19de058d 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -53,29 +53,29 @@ import { PaginatedResponse, NonPaginatedResponse, HasPaginationOptions } from '. */ export interface EntityServiceModel { /** - * Gets entities in the system. + * Gets entities in the tenant. * * Three call modes: - * - `getAll({ includeFolderEntities: false })` — default. Returns only tenant-level entities. No `OR.Users` scope required. - * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. - * - `getAll({ includeFolderEntities: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeFolderEntities`. + * - `getAll()` — default. Returns only tenant-level entities. No `OR.Users` scope required. + * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only entities in that folder. + * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. * * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeFolderEntities: true` to list tenant + folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * {@link EntityGetResponse} * @example * ```typescript * // Tenant-only (default — no OR.Users needed) - * const tenantEntities = await entities.getAll({ includeFolderEntities: false }); + * const tenantEntities = await entities.getAll(); + * + * // A single folder's entities (preferred when targeting a specific folder) + * const folderEntities = await entities.getAll({ folderKey: "" }); * * // Tenant + folder entities together (requires OR.Users scope) * const allEntities = await entities.getAll({ includeFolderEntities: true }); * - * // A single folder's entities - * const folderEntities = await entities.getAll({ includeFolderEntities: false, folderKey: "" }); - * * // Iterate through entities * tenantEntities.forEach(entity => { * console.log(`Entity: ${entity.displayName} (${entity.name})`); diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index eeba9a11f..101079d7f 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -522,16 +522,16 @@ export class EntityService extends BaseService implements EntityServiceModel { } /** - * Gets all entities in the system. + * Gets entities in the tenant. * * Three call modes: - * - `getAll({ includeFolderEntities: false })` — default. Returns only tenant-level entities. No `OR.Users` scope required. - * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. - * - `getAll({ includeFolderEntities: false, folderKey: "" })` — returns only entities in that folder. `folderKey` always wins over `includeFolderEntities`. + * - `getAll()` — default. Returns only tenant-level entities. No `OR.Users` scope required. + * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only entities in that folder. + * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. * * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities, `includeFolderEntities: true` to list tenant + folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * * @example @@ -541,14 +541,14 @@ export class EntityService extends BaseService implements EntityServiceModel { * const entities = new Entities(sdk); * * // Tenant-only (default — no OR.Users needed) - * const tenantEntities = await entities.getAll({ includeFolderEntities: false }); + * const tenantEntities = await entities.getAll(); + * + * // A single folder's entities (preferred when targeting a specific folder) + * const folderEntities = await entities.getAll({ folderKey: "" }); * * // Tenant + folder entities together (requires OR.Users scope) * const allEntities = await entities.getAll({ includeFolderEntities: true }); * - * // A single folder's entities - * const folderEntities = await entities.getAll({ includeFolderEntities: false, folderKey: "" }); - * * // Call operations on an entity * const records = await tenantEntities[0].getAllRecords(); * ``` From 4ca6879b518a3a41e606bf4ac5e6593c529eb40a Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Mon, 15 Jun 2026 16:43:55 +0530 Subject: [PATCH 18/23] docs(data-fabric): add @experimental tag to folderKey-bearing methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every Entities and ChoiceSets method that accepts a `folderKey` option now carries the `@experimental` JSDoc tag, so TypeDoc renders an `Experimental` badge directly under the method signature in the generated reference docs — alongside the existing blockquote that calls out `folderKey` specifically as the experimental property. Choicesets also gains the "folder scope is experimental" blockquote on each affected method (entities already had it). Kept JSDoc in sync between the ServiceModel interfaces and the service-class implementations. Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/choicesets.models.ts | 32 ++++++++++++++++++ src/models/data-fabric/entities.models.ts | 36 +++++++++++++++++++++ src/services/data-fabric/choicesets.ts | 32 ++++++++++++++++++ src/services/data-fabric/entities.ts | 36 +++++++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/src/models/data-fabric/choicesets.models.ts b/src/models/data-fabric/choicesets.models.ts index 69a4e81d2..f18a954b9 100644 --- a/src/models/data-fabric/choicesets.models.ts +++ b/src/models/data-fabric/choicesets.models.ts @@ -39,6 +39,10 @@ export interface ChoiceSetServiceModel { * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. * + * > **Experimental:** folder-scope options (`folderKey`, `includeFolderChoiceSets`) are in preview — the contract may change. + * + * @experimental + * * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) * @returns Promise resolving to an array of choice set metadata * {@link ChoiceSetGetAllResponse} @@ -66,6 +70,10 @@ export interface ChoiceSetServiceModel { * - A NonPaginatedResponse with items array (when no pagination parameters are provided) * - A PaginatedResponse with navigation cursors (when any pagination parameter is provided) * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the choice set * @param options - Pagination options and optional `folderKey` (omit for tenant-level choice sets) * @returns Promise resolving to choice set values or paginated result @@ -109,6 +117,10 @@ export interface ChoiceSetServiceModel { /** * Creates a new Data Fabric choice set * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param name - Choice set name. Must start with a * letter, may contain only letters, numbers, and underscores, length * 3–100 characters (e.g., `"expenseTypes"`). @@ -136,6 +148,10 @@ export interface ChoiceSetServiceModel { * **At least one of `displayName` or `description` must be provided** — * the call throws `ValidationError` if both are omitted. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the choice set to update * @param options - Metadata fields to change ({@link ChoiceSetUpdateOptions}) * @returns Promise resolving when the update is complete @@ -158,6 +174,10 @@ export interface ChoiceSetServiceModel { /** * Deletes a Data Fabric choice set and all its values. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the choice set to delete * @param options - Optional {@link ChoiceSetDeleteByIdOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level * @returns Promise resolving when the choice set is deleted @@ -180,6 +200,10 @@ export interface ChoiceSetServiceModel { /** * Inserts a single value into a choice set. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the parent choice set * @param name - Identifier name of the new value (e.g., `"TRAVEL"`) * @param options - Optional fields ({@link ChoiceSetValueInsertOptions}) @@ -216,6 +240,10 @@ export interface ChoiceSetServiceModel { * Only `displayName` is mutable; the value's `name` (identifier) is fixed at * insert time and cannot be changed. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the parent choice set * @param valueId - UUID of the value to update * @param displayName - New human-readable display name for the value @@ -248,6 +276,10 @@ export interface ChoiceSetServiceModel { /** * Deletes one or more values from a choice set. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the parent choice set * @param valueIds - Array of value UUIDs to delete * @param options - Optional {@link ChoiceSetValueDeleteOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index a19de058d..0ac354d8c 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -62,6 +62,8 @@ export interface EntityServiceModel { * * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. * + * @experimental + * * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * {@link EntityGetResponse} @@ -108,6 +110,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param options - Optional {@link EntityGetByIdOptions} (e.g. `folderKey` for folder-scoped entities) * @returns Promise resolving to entity metadata with operation methods @@ -151,6 +155,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param options - Query options * @returns Promise resolving to either an array of entity records NonPaginatedResponse or a PaginatedResponse when pagination options are used. @@ -202,6 +208,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record * @param options - Query options @@ -237,6 +245,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param data - Record to insert * @param options - Insert options @@ -274,6 +284,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param data - Array of records to insert * @param options - Insert options @@ -319,6 +331,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to update * @param data - Key-value pairs of fields to update @@ -350,6 +364,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param data - Array of records to update. Each record MUST contain the record id. * @param options - Update options @@ -388,6 +404,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param recordIds - Array of record UUIDs to delete * @param options - Delete options @@ -416,6 +434,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to delete * @param options - Optional {@link EntityDeleteRecordByIdOptions} (e.g. `folderKey` for folder-scoped entities) @@ -439,6 +459,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param options - Query options including filterGroup, selectedFields, sortOptions, aggregates, groupBy, and pagination * @returns Promise resolving to {@link NonPaginatedResponse} without pagination options, @@ -500,6 +522,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param file - CSV file to import as a Blob or File or Uint8Array * @param options - Optional {@link EntityImportRecordsByIdOptions} (e.g. `folderKey` for folder-scoped entities) @@ -523,6 +547,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment @@ -582,6 +608,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to upload the attachment to * @param fieldName - Name of the File-type field @@ -623,6 +651,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment @@ -657,6 +687,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` and cross-folder `referenceFolderKey` are in preview — the contract may change. * + * @experimental + * * @param name - Entity name — must start with a letter, letters/numbers/underscores only * (e.g., `"productCatalog"`). * @param fields - Array of field definitions @@ -707,6 +739,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity to delete * @param options - Optional {@link EntityDeleteByIdOptions} (e.g. `folderKey` for folder-scoped entities) * @returns Promise resolving when the entity is deleted @@ -730,6 +764,8 @@ export interface EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity to update * @param options - Changes to apply ({@link EntityUpdateByIdOptions}) * @returns Promise resolving when the update is complete diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index 8f9304363..3d0b180c5 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -36,6 +36,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. * + * > **Experimental:** folder-scope options (`folderKey`, `includeFolderChoiceSets`) are in preview — the contract may change. + * + * @experimental + * * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) * @returns Promise resolving to an array of choice set metadata * @@ -83,6 +87,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * - A NonPaginatedResponse with items array (when no pagination parameters are provided) * - A PaginatedResponse with navigation cursors (when any pagination parameter is provided) * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the choice set * @param options - Pagination options and optional `folderKey` for folder-scoped choice sets * @returns Promise resolving to choice set values or paginated result @@ -159,6 +167,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod /** * Creates a new Data Fabric choice set * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param name - Choice set name. Must start with a * letter, may contain only letters, numbers, and underscores, length * 3–100 characters (e.g., `"expenseTypes"`). @@ -208,6 +220,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * **At least one of `displayName` or `description` must be provided** — * the call throws `ValidationError` if both are omitted. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the choice set to update * @param options - Metadata fields to change ({@link ChoiceSetUpdateOptions}) * @returns Promise resolving when the update is complete @@ -245,6 +261,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod /** * Deletes a Data Fabric choice set and all its values. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the choice set to delete * @param options - Optional {@link ChoiceSetDeleteByIdOptions} (e.g. `folderKey` for folder-scoped choice sets) * @returns Promise resolving when the choice set is deleted @@ -274,6 +294,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod /** * Inserts a single value into a choice set. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the parent choice set * @param name - Identifier name of the new value (e.g., `"TRAVEL"`) * @param options - Optional fields ({@link ChoiceSetValueInsertOptions}) @@ -324,6 +348,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * Only `displayName` is mutable; the value's `name` (identifier) is fixed at * insert time and cannot be changed. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the parent choice set * @param valueId - UUID of the value to update * @param displayName - New human-readable display name for the value @@ -367,6 +395,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod /** * Deletes one or more values from a choice set. * + * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. + * + * @experimental + * * @param choiceSetId - UUID of the parent choice set * @param valueIds - Array of value UUIDs to delete * @param options - Optional {@link ChoiceSetValueDeleteOptions} (e.g. `folderKey` for folder-scoped choice sets) diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 101079d7f..02634f70b 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -72,6 +72,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param options - Optional {@link EntityGetByIdOptions} (e.g. `folderKey` for folder-scoped entities) * @returns Promise resolving to entity metadata with schema information and operation methods @@ -122,6 +124,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param options - Query options including expansionLevel and pagination options * @returns Promise resolving to an array of entity records or paginated response @@ -192,6 +196,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record * @param options - Query options including `expansionLevel` and `folderKey` @@ -236,6 +242,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param data - Record to insert * @param options - Insert options @@ -284,6 +292,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param data - Array of records to insert * @param options - Insert options @@ -341,6 +351,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to update * @param data - Key-value pairs of fields to update @@ -390,6 +402,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param data - Array of records to update. Each record MUST contain the record Id, * otherwise the update will fail. @@ -449,6 +463,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordIds - Array of record UUIDs to delete * @param options - Delete options @@ -497,6 +513,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to delete * @param options - Optional {@link EntityDeleteRecordByIdOptions} (e.g. `folderKey` for folder-scoped entities) @@ -531,6 +549,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. * + * @experimental + * * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) * @returns Promise resolving to an array of entity metadata * @@ -586,6 +606,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param options - Query options including filterGroup, selectedFields, sortOptions, aggregates, groupBy, and pagination * @returns Promise resolving to {@link NonPaginatedResponse} without pagination options, @@ -675,6 +697,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity * @param file - CSV file to import (Blob, File, or Uint8Array) * @param options - Optional {@link EntityImportRecordsByIdOptions} (e.g. `folderKey` for folder-scoped entities) @@ -727,6 +751,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment @@ -772,6 +798,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record to upload the attachment to * @param fieldName - Name of the File-type field @@ -828,6 +856,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment @@ -901,6 +931,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` and cross-folder `referenceFolderKey` are in preview — the contract may change. * + * @experimental + * * @param name - Entity name — must start with a letter and contain * only letters, numbers, and underscores (e.g., `"productCatalog"`). * @param fields - Array of field definitions @@ -970,6 +1002,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity to delete * @param options - Optional {@link EntityDeleteByIdOptions} (e.g. `folderKey` for folder-scoped entities) * @returns Promise resolving when the entity is deleted @@ -1004,6 +1038,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. * + * @experimental + * * @param id - UUID of the entity to update * @param options - Changes to apply ({@link EntityUpdateByIdOptions}) * @returns Promise resolving when the update is complete From c6aa0d56050a6cbfc3b0abce66469dc40e087254 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Mon, 15 Jun 2026 16:51:42 +0530 Subject: [PATCH 19/23] docs(data-fabric): drop OR.Users OAuth scope mentions from JSDoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per PR feedback — remove `OR.Users` references from user-facing JSDoc on the entities and choicesets `getAll` methods and on the related option field comments. Internal code comments that explain endpoint-selection logic stay (they're not rendered in public docs). Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/choicesets.models.ts | 8 ++++---- src/models/data-fabric/choicesets.types.ts | 5 ++--- src/models/data-fabric/entities.models.ts | 8 ++++---- src/models/data-fabric/entities.types.ts | 5 ++--- src/services/data-fabric/choicesets.ts | 8 ++++---- src/services/data-fabric/entities.ts | 8 ++++---- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/models/data-fabric/choicesets.models.ts b/src/models/data-fabric/choicesets.models.ts index f18a954b9..6214c700c 100644 --- a/src/models/data-fabric/choicesets.models.ts +++ b/src/models/data-fabric/choicesets.models.ts @@ -35,9 +35,9 @@ export interface ChoiceSetServiceModel { * Gets choice sets in the tenant. * * Three call modes: - * - `getAll()` — default. Returns only tenant-level choice sets. No `OR.Users` scope required. + * - `getAll()` — default. Returns only tenant-level choice sets. * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. - * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. + * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. `folderKey` (when provided) always wins over this flag. * * > **Experimental:** folder-scope options (`folderKey`, `includeFolderChoiceSets`) are in preview — the contract may change. * @@ -48,13 +48,13 @@ export interface ChoiceSetServiceModel { * {@link ChoiceSetGetAllResponse} * @example * ```typescript - * // Tenant-only (default — no OR.Users needed) + * // Tenant-only (default) * const tenantChoiceSets = await choicesets.getAll(); * * // A single folder's choice sets (preferred when targeting a specific folder) * const folderChoiceSets = await choicesets.getAll({ folderKey: "" }); * - * // Tenant + folder choice sets together (requires OR.Users scope) + * // Tenant + folder choice sets together * const allChoiceSets = await choicesets.getAll({ includeFolderChoiceSets: true }); * * // Find a specific choice set by name diff --git a/src/models/data-fabric/choicesets.types.ts b/src/models/data-fabric/choicesets.types.ts index 5a7830c78..94cb2c4ac 100644 --- a/src/models/data-fabric/choicesets.types.ts +++ b/src/models/data-fabric/choicesets.types.ts @@ -52,9 +52,8 @@ export interface ChoiceSetGetResponse { export interface ChoiceSetGetAllOptions extends EntityFolderScopedOptions { /** - * When `true`, also returns folder-level choice sets alongside tenant ones - * (requires the `OR.Users` OAuth scope). - * Omit (or `false`, the default) to return only tenant-level choice sets — no `OR.Users` needed. + * When `true`, also returns folder-level choice sets alongside tenant ones. + * Omit (or `false`, the default) to return only tenant-level choice sets. * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. */ includeFolderChoiceSets?: boolean; diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index 0ac354d8c..ad602885e 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -56,9 +56,9 @@ export interface EntityServiceModel { * Gets entities in the tenant. * * Three call modes: - * - `getAll()` — default. Returns only tenant-level entities. No `OR.Users` scope required. + * - `getAll()` — default. Returns only tenant-level entities. * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only entities in that folder. - * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. + * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. `folderKey` (when provided) always wins over this flag. * * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. * @@ -69,13 +69,13 @@ export interface EntityServiceModel { * {@link EntityGetResponse} * @example * ```typescript - * // Tenant-only (default — no OR.Users needed) + * // Tenant-only (default) * const tenantEntities = await entities.getAll(); * * // A single folder's entities (preferred when targeting a specific folder) * const folderEntities = await entities.getAll({ folderKey: "" }); * - * // Tenant + folder entities together (requires OR.Users scope) + * // Tenant + folder entities together * const allEntities = await entities.getAll({ includeFolderEntities: true }); * * // Iterate through entities diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index 7b950d437..f62b3786e 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -328,9 +328,8 @@ export interface EntityFolderScopedOptions { export interface EntityGetAllOptions extends EntityFolderScopedOptions { /** - * When `true`, returns tenant-level and folder-level entities together - * (requires the `OR.Users` OAuth scope). - * Omit (or `false`, the default) to return only tenant-level entities — no `OR.Users` needed. + * When `true`, returns tenant-level and folder-level entities together. + * Omit (or `false`, the default) to return only tenant-level entities. * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. * * @experimental Folder-scoped Data Fabric is in preview — the contract may change. diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index 3d0b180c5..7a53c4cd4 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -32,9 +32,9 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * Gets choice sets in the tenant. * * Three call modes: - * - `getAll()` — default. Returns only tenant-level choice sets. No `OR.Users` scope required. + * - `getAll()` — default. Returns only tenant-level choice sets. * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. - * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. + * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. `folderKey` (when provided) always wins over this flag. * * > **Experimental:** folder-scope options (`folderKey`, `includeFolderChoiceSets`) are in preview — the contract may change. * @@ -49,13 +49,13 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * * const choiceSets = new ChoiceSets(sdk); * - * // Tenant-only (default — no OR.Users needed) + * // Tenant-only (default) * const tenantChoiceSets = await choiceSets.getAll(); * * // A single folder's choice sets (preferred when targeting a specific folder) * const folderChoiceSets = await choiceSets.getAll({ folderKey: "" }); * - * // Tenant + folder choice sets together (requires OR.Users scope) + * // Tenant + folder choice sets together * const allChoiceSets = await choiceSets.getAll({ includeFolderChoiceSets: true }); * ``` */ diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 02634f70b..7b9a60ed2 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -543,9 +543,9 @@ export class EntityService extends BaseService implements EntityServiceModel { * Gets entities in the tenant. * * Three call modes: - * - `getAll()` — default. Returns only tenant-level entities. No `OR.Users` scope required. + * - `getAll()` — default. Returns only tenant-level entities. * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only entities in that folder. - * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. Requires the `OR.Users` OAuth scope. `folderKey` (when provided) always wins over this flag. + * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. `folderKey` (when provided) always wins over this flag. * * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. * @@ -560,13 +560,13 @@ export class EntityService extends BaseService implements EntityServiceModel { * * const entities = new Entities(sdk); * - * // Tenant-only (default — no OR.Users needed) + * // Tenant-only (default) * const tenantEntities = await entities.getAll(); * * // A single folder's entities (preferred when targeting a specific folder) * const folderEntities = await entities.getAll({ folderKey: "" }); * - * // Tenant + folder entities together (requires OR.Users scope) + * // Tenant + folder entities together * const allEntities = await entities.getAll({ includeFolderEntities: true }); * * // Call operations on an entity From 1bffc194d406427f7086357f35955af6a687ff60 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Mon, 15 Jun 2026 17:39:25 +0530 Subject: [PATCH 20/23] docs(data-fabric): scope experimental marker to folderKey argument only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Method-level `@experimental` tags and `> **Experimental:** ...` blockquotes removed from every Entities and ChoiceSets method that takes `folderKey`. The experimental marker now lives only at the argument level: - Type-level: `@experimental` JSDoc tag on the `folderKey` property (already in `EntityFolderScopedOptions`) and `includeFolderChoiceSets` (added here for parity with `includeFolderEntities`). TypeDoc renders these as `Experimental` badges on the property rows of every linked options-type page. - Method-level: each `@param options - ...` line now ends with `The \`folderKey\` property is **experimental**.` — visible directly in the rendered parameter table of every folderKey-bearing method. `updateValueById` (ChoiceSets) previously had no `@param options` line; added one so the marker reaches that method too. ServiceModel ↔ service class JSDoc kept in sync per project convention. Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/choicesets.models.ts | 47 ++------ src/models/data-fabric/choicesets.types.ts | 2 + src/models/data-fabric/entities.models.ts | 116 ++++---------------- src/services/data-fabric/choicesets.ts | 47 ++------ src/services/data-fabric/entities.ts | 108 +++--------------- 5 files changed, 58 insertions(+), 262 deletions(-) diff --git a/src/models/data-fabric/choicesets.models.ts b/src/models/data-fabric/choicesets.models.ts index 6214c700c..3832257ac 100644 --- a/src/models/data-fabric/choicesets.models.ts +++ b/src/models/data-fabric/choicesets.models.ts @@ -39,11 +39,7 @@ export interface ChoiceSetServiceModel { * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. `folderKey` (when provided) always wins over this flag. * - * > **Experimental:** folder-scope options (`folderKey`, `includeFolderChoiceSets`) are in preview — the contract may change. - * - * @experimental - * - * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) + * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) The `folderKey` property is **experimental**. * @returns Promise resolving to an array of choice set metadata * {@link ChoiceSetGetAllResponse} * @example @@ -70,12 +66,8 @@ export interface ChoiceSetServiceModel { * - A NonPaginatedResponse with items array (when no pagination parameters are provided) * - A PaginatedResponse with navigation cursors (when any pagination parameter is provided) * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the choice set - * @param options - Pagination options and optional `folderKey` (omit for tenant-level choice sets) + * @param options - Pagination options and optional `folderKey` (omit for tenant-level choice sets) The `folderKey` property is **experimental**. * @returns Promise resolving to choice set values or paginated result * {@link ChoiceSetGetResponse} * @example @@ -117,14 +109,10 @@ export interface ChoiceSetServiceModel { /** * Creates a new Data Fabric choice set * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param name - Choice set name. Must start with a * letter, may contain only letters, numbers, and underscores, length * 3–100 characters (e.g., `"expenseTypes"`). - * @param options - Optional choice-set-level settings ({@link ChoiceSetCreateOptions}) + * @param options - Optional choice-set-level settings ({@link ChoiceSetCreateOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving to the UUID of the created choice set * * @example @@ -148,12 +136,8 @@ export interface ChoiceSetServiceModel { * **At least one of `displayName` or `description` must be provided** — * the call throws `ValidationError` if both are omitted. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the choice set to update - * @param options - Metadata fields to change ({@link ChoiceSetUpdateOptions}) + * @param options - Metadata fields to change ({@link ChoiceSetUpdateOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving when the update is complete * * @example @@ -174,12 +158,8 @@ export interface ChoiceSetServiceModel { /** * Deletes a Data Fabric choice set and all its values. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the choice set to delete - * @param options - Optional {@link ChoiceSetDeleteByIdOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level + * @param options - Optional {@link ChoiceSetDeleteByIdOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level The `folderKey` property is **experimental**. * @returns Promise resolving when the choice set is deleted * * @example @@ -200,13 +180,9 @@ export interface ChoiceSetServiceModel { /** * Inserts a single value into a choice set. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the parent choice set * @param name - Identifier name of the new value (e.g., `"TRAVEL"`) - * @param options - Optional fields ({@link ChoiceSetValueInsertOptions}) + * @param options - Optional fields ({@link ChoiceSetValueInsertOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving to the inserted value ({@link ChoiceSetValueInsertResponse}) * * @example @@ -240,13 +216,10 @@ export interface ChoiceSetServiceModel { * Only `displayName` is mutable; the value's `name` (identifier) is fixed at * insert time and cannot be changed. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the parent choice set * @param valueId - UUID of the value to update * @param displayName - New human-readable display name for the value + * @param options - Optional {@link ChoiceSetValueUpdateOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level. The `folderKey` property is **experimental**. * @returns Promise resolving to the updated value ({@link ChoiceSetValueUpdateResponse}) * * @example @@ -276,13 +249,9 @@ export interface ChoiceSetServiceModel { /** * Deletes one or more values from a choice set. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the parent choice set * @param valueIds - Array of value UUIDs to delete - * @param options - Optional {@link ChoiceSetValueDeleteOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level + * @param options - Optional {@link ChoiceSetValueDeleteOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level The `folderKey` property is **experimental**. * @returns Promise resolving when the values are deleted * * @example diff --git a/src/models/data-fabric/choicesets.types.ts b/src/models/data-fabric/choicesets.types.ts index 94cb2c4ac..f6e481223 100644 --- a/src/models/data-fabric/choicesets.types.ts +++ b/src/models/data-fabric/choicesets.types.ts @@ -55,6 +55,8 @@ export interface ChoiceSetGetAllOptions extends EntityFolderScopedOptions { * When `true`, also returns folder-level choice sets alongside tenant ones. * Omit (or `false`, the default) to return only tenant-level choice sets. * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. + * + * @experimental Folder-scoped Data Fabric is in preview — the contract may change. */ includeFolderChoiceSets?: boolean; } diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index ad602885e..17f8b8558 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -60,11 +60,7 @@ export interface EntityServiceModel { * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only entities in that folder. * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. `folderKey` (when provided) always wins over this flag. * - * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. - * - * @experimental - * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) The `folderKey` property is **experimental**. * @returns Promise resolving to an array of entity metadata * {@link EntityGetResponse} * @example @@ -108,12 +104,8 @@ export interface EntityServiceModel { /** * Gets entity metadata by entity ID with attached operation methods * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity - * @param options - Optional {@link EntityGetByIdOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityGetByIdOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to entity metadata with operation methods * {@link EntityGetResponse} * @example @@ -153,12 +145,8 @@ export interface EntityServiceModel { /** * Gets entity records by entity ID * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity - * @param options - Query options + * @param options - Query options The `folderKey` property is **experimental**. * @returns Promise resolving to either an array of entity records NonPaginatedResponse or a PaginatedResponse when pagination options are used. * {@link EntityRecord} * @example @@ -206,13 +194,9 @@ export interface EntityServiceModel { /** * Gets a single entity record by entity ID and record ID * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record - * @param options - Query options + * @param options - Query options The `folderKey` property is **experimental**. * @returns Promise resolving to a single entity record * {@link EntityRecord} * @example @@ -243,13 +227,9 @@ export interface EntityServiceModel { * Note: Data Fabric supports trigger events only on individual inserts, not on inserting multiple records. * Use this method if you need trigger events to fire for the inserted record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity * @param data - Record to insert - * @param options - Insert options + * @param options - Insert options The `folderKey` property is **experimental**. * @returns Promise resolving to the inserted record with generated record ID * {@link EntityInsertResponse} * @example @@ -282,13 +262,9 @@ export interface EntityServiceModel { * Note: Records inserted using insertRecordsById will not trigger Data Fabric trigger events. Use {@link insertRecordById} if you need * trigger events to fire for each inserted record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity * @param data - Array of records to insert - * @param options - Insert options + * @param options - Insert options The `folderKey` property is **experimental**. * @returns Promise resolving to insert response * {@link EntityBatchInsertResponse} * @example @@ -329,14 +305,10 @@ export interface EntityServiceModel { * Note: Data Fabric supports trigger events only on individual updates, not on updating multiple records. * Use this method if you need trigger events to fire for the updated record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record to update * @param data - Key-value pairs of fields to update - * @param options - Update options + * @param options - Update options The `folderKey` property is **experimental**. * @returns Promise resolving to the updated record * {@link EntityUpdateRecordResponse} * @example @@ -362,13 +334,9 @@ export interface EntityServiceModel { * * Note: Records updated using updateRecordsById will not trigger Data Fabric trigger events. Use {@link updateRecordById} if you need trigger events to fire for each updated record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity * @param data - Array of records to update. Each record MUST contain the record id. - * @param options - Update options + * @param options - Update options The `folderKey` property is **experimental**. * @returns Promise resolving to update response * {@link EntityUpdateResponse} * @example @@ -402,13 +370,9 @@ export interface EntityServiceModel { * * Note: Records deleted using deleteRecordsById will not trigger Data Fabric trigger events. Use {@link deleteRecordById} if you need trigger events to fire for the deleted record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity * @param recordIds - Array of record UUIDs to delete - * @param options - Delete options + * @param options - Delete options The `folderKey` property is **experimental**. * @returns Promise resolving to delete response * {@link EntityDeleteResponse} * @example @@ -432,13 +396,9 @@ export interface EntityServiceModel { * Note: Data Fabric supports trigger events only on individual deletes, not on deleting multiple records. * Use this method if you need trigger events to fire for the deleted record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record to delete - * @param options - Optional {@link EntityDeleteRecordByIdOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityDeleteRecordByIdOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to void on success * @example * ```typescript @@ -457,12 +417,8 @@ export interface EntityServiceModel { /** * Queries entity records with filters, sorting, aggregates, and SDK-managed pagination * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity - * @param options - Query options including filterGroup, selectedFields, sortOptions, aggregates, groupBy, and pagination + * @param options - Query options including filterGroup, selectedFields, sortOptions, aggregates, groupBy, and pagination The `folderKey` property is **experimental**. * @returns Promise resolving to {@link NonPaginatedResponse} without pagination options, * or {@link PaginatedResponse} when `pageSize`, `cursor`, or `jumpToPage` are provided * @example @@ -520,13 +476,9 @@ export interface EntityServiceModel { /** * Imports records from a CSV file into an entity * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity * @param file - CSV file to import as a Blob or File or Uint8Array - * @param options - Optional {@link EntityImportRecordsByIdOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityImportRecordsByIdOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to {@link EntityImportRecordsResponse} with record counts * @example * ```typescript @@ -545,14 +497,10 @@ export interface EntityServiceModel { /** * Downloads an attachment stored in a File-type field of an entity record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment - * @param options - Optional {@link EntityDownloadAttachmentOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityDownloadAttachmentOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to Blob containing the file content * @example * ```typescript @@ -606,15 +554,11 @@ export interface EntityServiceModel { * * Uses multipart/form-data to upload the file content to the specified field. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record to upload the attachment to * @param fieldName - Name of the File-type field * @param file - File to upload (Blob, File, or Uint8Array) - * @param options - Optional {@link EntityUploadAttachmentOptions} (e.g. `expansionLevel`, `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityUploadAttachmentOptions} (e.g. `expansionLevel`, `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to {@link EntityUploadAttachmentResponse} * @example * ```typescript @@ -649,14 +593,10 @@ export interface EntityServiceModel { /** * Removes an attachment from a File-type field of an entity record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment - * @param options - Optional {@link EntityDeleteAttachmentOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityDeleteAttachmentOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to {@link EntityDeleteAttachmentResponse} * @example * ```typescript @@ -685,14 +625,10 @@ export interface EntityServiceModel { /** * Creates a new Data Fabric entity with the given schema * - * > **Experimental:** `folderKey` and cross-folder `referenceFolderKey` are in preview — the contract may change. - * - * @experimental - * * @param name - Entity name — must start with a letter, letters/numbers/underscores only * (e.g., `"productCatalog"`). * @param fields - Array of field definitions - * @param options - Optional entity-level settings ({@link EntityCreateOptions}) + * @param options - Optional entity-level settings ({@link EntityCreateOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving to the ID of the created entity * @example * ```typescript @@ -737,12 +673,8 @@ export interface EntityServiceModel { /** * Deletes a Data Fabric entity and all its records * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity to delete - * @param options - Optional {@link EntityDeleteByIdOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityDeleteByIdOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving when the entity is deleted * @example * ```typescript @@ -762,12 +694,8 @@ export interface EntityServiceModel { * metadata fields (`displayName`, `description`, `isRbacEnabled`). Each group is applied * only when the corresponding fields are provided. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity to update - * @param options - Changes to apply ({@link EntityUpdateByIdOptions}) + * @param options - Changes to apply ({@link EntityUpdateByIdOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving when the update is complete * * @example @@ -893,7 +821,7 @@ export interface EntityMethods { /** * Get all records from this entity * - * @param options - Query options + * @param options - Query options The `folderKey` property is **experimental**. * @returns Promise resolving to query response */ getAllRecords(options?: T): Promise< @@ -916,7 +844,7 @@ export interface EntityMethods { * * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment - * @param options - Optional {@link EntityDownloadAttachmentOptions} (e.g. folderKey) + * @param options - Optional {@link EntityDownloadAttachmentOptions} (e.g. folderKey) The `folderKey` property is **experimental**. * @returns Promise resolving to Blob containing the file content */ downloadAttachment(recordId: string, fieldName: string, options?: EntityDownloadAttachmentOptions): Promise; @@ -927,7 +855,7 @@ export interface EntityMethods { * @param recordId - UUID of the record to upload the attachment to * @param fieldName - Name of the File-type field * @param file - File to upload (Blob, File, or Uint8Array) - * @param options - Optional {@link EntityUploadAttachmentOptions} (e.g. expansionLevel, folderKey) + * @param options - Optional {@link EntityUploadAttachmentOptions} (e.g. expansionLevel, folderKey) The `folderKey` property is **experimental**. * @returns Promise resolving to {@link EntityUploadAttachmentResponse} */ uploadAttachment(recordId: string, fieldName: string, file: EntityFileType, options?: EntityUploadAttachmentOptions): Promise; @@ -937,7 +865,7 @@ export interface EntityMethods { * * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment - * @param options - Optional {@link EntityDeleteAttachmentOptions} (e.g. folderKey) + * @param options - Optional {@link EntityDeleteAttachmentOptions} (e.g. folderKey) The `folderKey` property is **experimental**. * @returns Promise resolving to {@link EntityDeleteAttachmentResponse} */ deleteAttachment(recordId: string, fieldName: string, options?: EntityDeleteAttachmentOptions): Promise; diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index 7a53c4cd4..cd9cfde19 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -36,11 +36,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. `folderKey` (when provided) always wins over this flag. * - * > **Experimental:** folder-scope options (`folderKey`, `includeFolderChoiceSets`) are in preview — the contract may change. - * - * @experimental - * - * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) + * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) The `folderKey` property is **experimental**. * @returns Promise resolving to an array of choice set metadata * * @example @@ -87,12 +83,8 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * - A NonPaginatedResponse with items array (when no pagination parameters are provided) * - A PaginatedResponse with navigation cursors (when any pagination parameter is provided) * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the choice set - * @param options - Pagination options and optional `folderKey` for folder-scoped choice sets + * @param options - Pagination options and optional `folderKey` for folder-scoped choice sets The `folderKey` property is **experimental**. * @returns Promise resolving to choice set values or paginated result * * @example @@ -167,14 +159,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod /** * Creates a new Data Fabric choice set * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param name - Choice set name. Must start with a * letter, may contain only letters, numbers, and underscores, length * 3–100 characters (e.g., `"expenseTypes"`). - * @param options - Optional choice-set-level settings ({@link ChoiceSetCreateOptions}) + * @param options - Optional choice-set-level settings ({@link ChoiceSetCreateOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving to the UUID of the created choice set * * @example @@ -220,12 +208,8 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * **At least one of `displayName` or `description` must be provided** — * the call throws `ValidationError` if both are omitted. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the choice set to update - * @param options - Metadata fields to change ({@link ChoiceSetUpdateOptions}) + * @param options - Metadata fields to change ({@link ChoiceSetUpdateOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving when the update is complete * * @example @@ -261,12 +245,8 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod /** * Deletes a Data Fabric choice set and all its values. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the choice set to delete - * @param options - Optional {@link ChoiceSetDeleteByIdOptions} (e.g. `folderKey` for folder-scoped choice sets) + * @param options - Optional {@link ChoiceSetDeleteByIdOptions} (e.g. `folderKey` for folder-scoped choice sets) The `folderKey` property is **experimental**. * @returns Promise resolving when the choice set is deleted * * @example @@ -294,13 +274,9 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod /** * Inserts a single value into a choice set. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the parent choice set * @param name - Identifier name of the new value (e.g., `"TRAVEL"`) - * @param options - Optional fields ({@link ChoiceSetValueInsertOptions}) + * @param options - Optional fields ({@link ChoiceSetValueInsertOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving to the inserted value ({@link ChoiceSetValueInsertResponse}) * * @example @@ -348,13 +324,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * Only `displayName` is mutable; the value's `name` (identifier) is fixed at * insert time and cannot be changed. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the parent choice set * @param valueId - UUID of the value to update * @param displayName - New human-readable display name for the value + * @param options - Optional {@link ChoiceSetValueUpdateOptions} — pass `folderKey` for folder-scoped choice sets; omit for tenant-level. The `folderKey` property is **experimental**. * @returns Promise resolving to the updated value ({@link ChoiceSetValueUpdateResponse}) * * @example @@ -395,13 +368,9 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod /** * Deletes one or more values from a choice set. * - * > **Experimental:** `folderKey` (folder-scoped choice sets) is in preview — the contract may change. - * - * @experimental - * * @param choiceSetId - UUID of the parent choice set * @param valueIds - Array of value UUIDs to delete - * @param options - Optional {@link ChoiceSetValueDeleteOptions} (e.g. `folderKey` for folder-scoped choice sets) + * @param options - Optional {@link ChoiceSetValueDeleteOptions} (e.g. `folderKey` for folder-scoped choice sets) The `folderKey` property is **experimental**. * @returns Promise resolving when the values are deleted * * @example diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 7b9a60ed2..6358fb883 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -70,12 +70,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Gets entity metadata by entity ID with attached operation methods * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity - * @param options - Optional {@link EntityGetByIdOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityGetByIdOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to entity metadata with schema information and operation methods * * @example @@ -122,12 +118,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Gets entity records by entity ID * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity - * @param options - Query options including expansionLevel and pagination options + * @param options - Query options including expansionLevel and pagination options The `folderKey` property is **experimental**. * @returns Promise resolving to an array of entity records or paginated response * * @example @@ -194,13 +186,9 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Gets a single entity record by entity ID and record ID * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record - * @param options - Query options including `expansionLevel` and `folderKey` + * @param options - Query options including `expansionLevel` and `folderKey` The `folderKey` property is **experimental**. * @returns Promise resolving to the entity record * * @example @@ -240,13 +228,9 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Inserts a single record into an entity by entity ID * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param data - Record to insert - * @param options - Insert options + * @param options - Insert options The `folderKey` property is **experimental**. * @returns Promise resolving to the inserted record with generated record ID * * @example @@ -290,13 +274,9 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Inserts data into an entity by entity ID using batch insert * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param data - Array of records to insert - * @param options - Insert options + * @param options - Insert options The `folderKey` property is **experimental**. * @returns Promise resolving to insert response * * @example @@ -349,14 +329,10 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Updates a single record in an entity by entity ID * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record to update * @param data - Key-value pairs of fields to update - * @param options - Update options + * @param options - Update options The `folderKey` property is **experimental**. * @returns Promise resolving to the updated record * * @example @@ -400,14 +376,10 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Updates data in an entity by entity ID * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param data - Array of records to update. Each record MUST contain the record Id, * otherwise the update will fail. - * @param options - Update options + * @param options - Update options The `folderKey` property is **experimental**. * @returns Promise resolving to update response * * @example @@ -461,13 +433,9 @@ export class EntityService extends BaseService implements EntityServiceModel { * * Note: Records deleted using deleteRecordsById will not trigger Data Fabric trigger events. Use {@link deleteRecordById} if you need trigger events to fire for the deleted record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordIds - Array of record UUIDs to delete - * @param options - Delete options + * @param options - Delete options The `folderKey` property is **experimental**. * @returns Promise resolving to delete response * * @example @@ -511,13 +479,9 @@ export class EntityService extends BaseService implements EntityServiceModel { * Note: Data Fabric supports trigger events only on individual deletes, not on deleting multiple records. * Use this method if you need trigger events to fire for the deleted record. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record to delete - * @param options - Optional {@link EntityDeleteRecordByIdOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityDeleteRecordByIdOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to void on success * @example * ```typescript @@ -547,11 +511,7 @@ export class EntityService extends BaseService implements EntityServiceModel { * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only entities in that folder. * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. `folderKey` (when provided) always wins over this flag. * - * > **Experimental:** folder-scope options (`folderKey`, `includeFolderEntities`) are in preview — the contract may change. - * - * @experimental - * - * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) + * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) The `folderKey` property is **experimental**. * @returns Promise resolving to an array of entity metadata * * @example @@ -604,12 +564,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Queries entity records with filters, sorting, aggregates, and pagination * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity - * @param options - Query options including filterGroup, selectedFields, sortOptions, aggregates, groupBy, and pagination + * @param options - Query options including filterGroup, selectedFields, sortOptions, aggregates, groupBy, and pagination The `folderKey` property is **experimental**. * @returns Promise resolving to {@link NonPaginatedResponse} without pagination options, * or {@link PaginatedResponse} when `pageSize`, `cursor`, or `jumpToPage` are provided * @@ -695,13 +651,9 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Imports records from a CSV file into an entity * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity * @param file - CSV file to import (Blob, File, or Uint8Array) - * @param options - Optional {@link EntityImportRecordsByIdOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityImportRecordsByIdOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to import result with record counts * * @example @@ -749,14 +701,10 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Downloads an attachment from an entity record field * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment - * @param options - Optional {@link EntityDownloadAttachmentOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityDownloadAttachmentOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to Blob containing the file content * * @example @@ -796,15 +744,11 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Uploads an attachment to a File-type field of an entity record * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record to upload the attachment to * @param fieldName - Name of the File-type field * @param file - File to upload (Blob, File, or Uint8Array) - * @param options - Optional {@link EntityUploadAttachmentOptions} (e.g. `expansionLevel`, `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityUploadAttachmentOptions} (e.g. `expansionLevel`, `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to {@link EntityUploadAttachmentResponse} * * @example @@ -854,14 +798,10 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Removes an attachment from a File-type field of an entity record * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param entityId - UUID of the entity * @param recordId - UUID of the record containing the attachment * @param fieldName - Name of the File-type field containing the attachment - * @param options - Optional {@link EntityDeleteAttachmentOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityDeleteAttachmentOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving to {@link EntityDeleteAttachmentResponse} * * @example @@ -929,14 +869,10 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Creates a new Data Fabric entity with the given schema * - * > **Experimental:** `folderKey` and cross-folder `referenceFolderKey` are in preview — the contract may change. - * - * @experimental - * * @param name - Entity name — must start with a letter and contain * only letters, numbers, and underscores (e.g., `"productCatalog"`). * @param fields - Array of field definitions - * @param options - Optional entity-level settings ({@link EntityCreateOptions}) + * @param options - Optional entity-level settings ({@link EntityCreateOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving to the ID of the created entity * * @example @@ -1000,12 +936,8 @@ export class EntityService extends BaseService implements EntityServiceModel { /** * Deletes a Data Fabric entity and all its records * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity to delete - * @param options - Optional {@link EntityDeleteByIdOptions} (e.g. `folderKey` for folder-scoped entities) + * @param options - Optional {@link EntityDeleteByIdOptions} (e.g. `folderKey` for folder-scoped entities) The `folderKey` property is **experimental**. * @returns Promise resolving when the entity is deleted * * @example @@ -1036,12 +968,8 @@ export class EntityService extends BaseService implements EntityServiceModel { * read-modify-write pattern — concurrent calls on the same entity may silently * overwrite each other's changes. * - * > **Experimental:** `folderKey` (folder-scoped entities) is in preview — the contract may change. - * - * @experimental - * * @param id - UUID of the entity to update - * @param options - Changes to apply ({@link EntityUpdateByIdOptions}) + * @param options - Changes to apply ({@link EntityUpdateByIdOptions}) The `folderKey` property is **experimental**. * @returns Promise resolving when the update is complete * * @example From 5aba6ccbf45e7349276c39bc679dff8cee388cd9 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Mon, 15 Jun 2026 18:01:04 +0530 Subject: [PATCH 21/23] docs(data-fabric): drop OR.Users folder-scope blockquotes from oauth-scopes Removes the two leftover blockquote notes in docs/oauth-scopes.md that called out `OR.Users` for folder-scoped Entities and ChoiceSets. The remaining `OR.Users` rows in that file are unrelated (User Settings service endpoints). Co-Authored-By: Claude Opus 4.7 --- docs/oauth-scopes.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/oauth-scopes.md b/docs/oauth-scopes.md index 02498f60d..d614b4929 100644 --- a/docs/oauth-scopes.md +++ b/docs/oauth-scopes.md @@ -43,8 +43,6 @@ This page lists the specific OAuth scopes required in external app for each SDK ## Entities -> **Folder-scoped entities require `OR.Users` in addition to the scope below.** Any CRUD operation on a folder entity (passing `folderKey`), and `getAll({ includeFolderEntities: true })` which lists all tenant and folder entities, need `OR.Users` as well. - | Method | OAuth Scope | |--------|-------------| | `getAll()` | `DataFabric.Schema.Read` | @@ -65,8 +63,6 @@ This page lists the specific OAuth scopes required in external app for each SDK ## ChoiceSets -> **Folder-scoped choice sets require `OR.Users` in addition to the scope below.** Any CRUD operation on a folder choice set (passing `folderKey`), and `getAll({ includeFolderChoiceSets: true })` which lists all tenant and folder choice sets, need `OR.Users` as well. - | Method | OAuth Scope | |--------|-------------| | `getAll()` | `DataFabric.Schema.Read` | From ef5bf97160a30899031422fc14d0ba6a0aa695e7 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Mon, 15 Jun 2026 18:17:26 +0530 Subject: [PATCH 22/23] docs(data-fabric): address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses outstanding Claude bot review feedback on PR #504: - Drop the `as unknown as ChoiceSetGetResponse` intermediate cast in `getById`'s `transformFn` — a single `as ChoiceSetGetResponse` is sufficient when `pascalToCamelCaseKeys()` precedes `transformData()` in the pipeline (returns `any`). - Remove the `as any` cast on the `PaginationHelpers.getAll()` return in `getById` — TypeScript infers the conditional return type correctly without it. - Use typed `EntityType.ChoiceSet` + `ENTITY_TYPE_IDS[…]` in `entities.test.ts` instead of raw `"ChoiceSet"` / `1` literals so the assertion stays in sync if the enum or type-id mapping changes. Also fixes a follow-on `entityType: 0` literal that the new enum import surfaced. - Fix the double `@track('Choicesets.GetAll')` telemetry from `resolveChoiceSetName` calling `this.getAll()`: extract an un-tracked `fetchAllChoiceSets` helper that both `getAll()` and `resolveChoiceSetName` call directly. - Extract `EntityFolderScopedOptions` into a new shared `src/models/data-fabric/data-fabric.types.ts` so `choicesets.types` no longer imports across service domains from `entities.types`. `entities.types` re-exports for backward compatibility with the public barrel. Also: reword the `folderKey` precedence note from "folderKey (when provided) always wins over this flag" to "folderKey is preferred over includeFolder{Entities,ChoiceSets} when both are set" everywhere it appears (model JSDoc, service JSDoc, type-level field JSDoc, inline implementation comments). Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/choicesets.models.ts | 2 +- src/models/data-fabric/choicesets.types.ts | 4 ++-- src/models/data-fabric/data-fabric.types.ts | 18 +++++++++++++++ src/models/data-fabric/entities.models.ts | 2 +- src/models/data-fabric/entities.types.ts | 18 ++++----------- src/services/data-fabric/choicesets.ts | 23 +++++++++++++++---- src/services/data-fabric/entities.ts | 12 +++++----- .../services/data-fabric/entities.test.ts | 8 ++++--- 8 files changed, 56 insertions(+), 31 deletions(-) create mode 100644 src/models/data-fabric/data-fabric.types.ts diff --git a/src/models/data-fabric/choicesets.models.ts b/src/models/data-fabric/choicesets.models.ts index 3832257ac..5d00a9ca8 100644 --- a/src/models/data-fabric/choicesets.models.ts +++ b/src/models/data-fabric/choicesets.models.ts @@ -37,7 +37,7 @@ export interface ChoiceSetServiceModel { * Three call modes: * - `getAll()` — default. Returns only tenant-level choice sets. * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. - * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. `folderKey` (when provided) always wins over this flag. + * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. `folderKey` is preferred over `includeFolderChoiceSets` when both are set. * * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) The `folderKey` property is **experimental**. * @returns Promise resolving to an array of choice set metadata diff --git a/src/models/data-fabric/choicesets.types.ts b/src/models/data-fabric/choicesets.types.ts index f6e481223..b6a33e62a 100644 --- a/src/models/data-fabric/choicesets.types.ts +++ b/src/models/data-fabric/choicesets.types.ts @@ -1,5 +1,5 @@ import { PaginationOptions } from '../../utils/pagination/types'; -import { EntityFolderScopedOptions } from './entities.types'; +import { EntityFolderScopedOptions } from './data-fabric.types'; /** * ChoiceSet Get All Response @@ -54,7 +54,7 @@ export interface ChoiceSetGetAllOptions extends EntityFolderScopedOptions { /** * When `true`, also returns folder-level choice sets alongside tenant ones. * Omit (or `false`, the default) to return only tenant-level choice sets. - * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. + * Ignored when `folderKey` is provided — `folderKey` is preferred over `includeFolderChoiceSets` when both are set. * * @experimental Folder-scoped Data Fabric is in preview — the contract may change. */ diff --git a/src/models/data-fabric/data-fabric.types.ts b/src/models/data-fabric/data-fabric.types.ts new file mode 100644 index 000000000..39a910bf3 --- /dev/null +++ b/src/models/data-fabric/data-fabric.types.ts @@ -0,0 +1,18 @@ +/** + * Shared types for the Data Fabric domain — used by both Entities and ChoiceSets. + * Lives here (not in either service's `*.types.ts`) to avoid cross-domain coupling + * between sibling services. + */ + +/** + * Common shape for every folder-scoped Data Fabric operation. + * Forwarded on the wire as the `X-UIPATH-FolderKey` header. + */ +export interface EntityFolderScopedOptions { + /** + * Key identifying the folder the entity belongs to. Omit for tenant-level entities. + * + * @experimental Folder-scoped Data Fabric is in preview — the contract may change. + */ + folderKey?: string; +} diff --git a/src/models/data-fabric/entities.models.ts b/src/models/data-fabric/entities.models.ts index 17f8b8558..0bb3c16bc 100644 --- a/src/models/data-fabric/entities.models.ts +++ b/src/models/data-fabric/entities.models.ts @@ -58,7 +58,7 @@ export interface EntityServiceModel { * Three call modes: * - `getAll()` — default. Returns only tenant-level entities. * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only entities in that folder. - * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. `folderKey` (when provided) always wins over this flag. + * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. `folderKey` is preferred over `includeFolderEntities` when both are set. * * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) The `folderKey` property is **experimental**. * @returns Promise resolving to an array of entity metadata diff --git a/src/models/data-fabric/entities.types.ts b/src/models/data-fabric/entities.types.ts index f62b3786e..f12d09fcc 100644 --- a/src/models/data-fabric/entities.types.ts +++ b/src/models/data-fabric/entities.types.ts @@ -1,4 +1,8 @@ import { PaginationOptions } from "../../utils/pagination/types"; +import { EntityFolderScopedOptions } from "./data-fabric.types"; + +// Re-export — canonical definition is in `./data-fabric.types`. +export type { EntityFolderScopedOptions }; /** * Entity field data type names (SQL-level types returned by the API) @@ -313,24 +317,12 @@ export interface EntityCreateFieldOptions extends EntityFieldBase { } -/** - * Common shape for every folder-scoped Data Fabric entity operation. - * Forwarded on the wire as the `X-UIPATH-FolderKey` header. - */ -export interface EntityFolderScopedOptions { - /** - * Key identifying the folder the entity belongs to. Omit for tenant-level entities. - * - * @experimental Folder-scoped Data Fabric is in preview — the contract may change. - */ - folderKey?: string; -} export interface EntityGetAllOptions extends EntityFolderScopedOptions { /** * When `true`, returns tenant-level and folder-level entities together. * Omit (or `false`, the default) to return only tenant-level entities. - * Ignored when `folderKey` is provided — `folderKey` always scopes the result to that single folder. + * Ignored when `folderKey` is provided — `folderKey` is preferred over `includeFolderEntities` when both are set. * * @experimental Folder-scoped Data Fabric is in preview — the contract may change. */ diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index cd9cfde19..576bd18a7 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -34,7 +34,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod * Three call modes: * - `getAll()` — default. Returns only tenant-level choice sets. * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only choice sets in that folder. - * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. `folderKey` (when provided) always wins over this flag. + * - `getAll({ includeFolderChoiceSets: true })` — returns tenant-level **and** folder-level choice sets together. `folderKey` is preferred over `includeFolderChoiceSets` when both are set. * * @param options - Optional {@link ChoiceSetGetAllOptions} (`folderKey` to list a single folder's choice sets — preferred when scoping to a folder; `includeFolderChoiceSets: true` to list tenant + folder choice sets together) The `folderKey` property is **experimental**. * @returns Promise resolving to an array of choice set metadata @@ -57,10 +57,20 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod */ @track('Choicesets.GetAll') async getAll(options?: ChoiceSetGetAllOptions): Promise { + return this.fetchAllChoiceSets(options); + } + + /** + * Internal helper that performs the choice-set fetch. Kept separate from the + * public `getAll()` so that internal callers (e.g. `resolveChoiceSetName`) + * can reuse it without triggering double `@track` telemetry. + */ + private async fetchAllChoiceSets(options?: ChoiceSetGetAllOptions): Promise { // The choice-set endpoint returns cross-scope results when called without // a folder header. To stay tenant-only by default (no OR.Users required), // send the tenant-marker UUID as the folder key unless the caller explicitly - // opts into cross-scope via includeFolderChoiceSets: true. folderKey always wins. + // opts into cross-scope via includeFolderChoiceSets: true. folderKey is + // preferred over includeFolderChoiceSets when both are set. const folderKey = options?.folderKey ?? (options?.includeFolderChoiceSets ? undefined : DATA_FABRIC_TENANT_FOLDER_ID); @@ -130,7 +140,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod // Transform a single item from PascalCase to camelCase const transformFn = (item: RawChoiceSetGetResponse): ChoiceSetGetResponse => { const camelCased = pascalToCamelCaseKeys(item); - return transformData(camelCased, EntityMap) as unknown as ChoiceSetGetResponse; + return transformData(camelCased, EntityMap) as ChoiceSetGetResponse; }; // folderKey is header-only — destructure it out so PaginationHelpers doesn't @@ -153,7 +163,7 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod countParam: ENTITY_OFFSET_PARAMS.COUNT_PARAM } } - }, downstreamOptions) as any; + }, downstreamOptions); } /** @@ -396,7 +406,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod } private async resolveChoiceSetName(choiceSetId: string, folderKey?: string): Promise { - const all = await this.getAll(folderKey === undefined ? undefined : { folderKey }); + // Use the un-tracked helper directly so we don't fire a duplicate + // `Choicesets.GetAll` telemetry event for every insertValueById / + // updateValueById call. + const all = await this.fetchAllChoiceSets(folderKey === undefined ? undefined : { folderKey }); const match = all.find(cs => cs.id === choiceSetId); if (!match) { throw new NotFoundError({ message: `Choice set with id '${choiceSetId}' not found.` }); diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 6358fb883..43bce28ec 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -509,7 +509,7 @@ export class EntityService extends BaseService implements EntityServiceModel { * Three call modes: * - `getAll()` — default. Returns only tenant-level entities. * - `getAll({ folderKey: "" })` — preferred for folder-scoped data. Returns only entities in that folder. - * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. `folderKey` (when provided) always wins over this flag. + * - `getAll({ includeFolderEntities: true })` — returns tenant-level **and** folder-level entities together. `folderKey` is preferred over `includeFolderEntities` when both are set. * * @param options - Optional {@link EntityGetAllOptions} (`folderKey` to list a single folder's entities — preferred when scoping to a folder; `includeFolderEntities: true` to list tenant + folder entities together) The `folderKey` property is **experimental**. * @returns Promise resolving to an array of entity metadata @@ -535,11 +535,11 @@ export class EntityService extends BaseService implements EntityServiceModel { */ @track('Entities.GetAll') async getAll(options?: EntityGetAllOptions): Promise { - // folderKey always wins: when present, scope to that folder via the v1 endpoint + header. - // Only when no folderKey is given AND includeFolderEntities is explicitly true does the SDK - // switch to the v2 endpoint (returns tenant + folder entities together — requires OR.Users). - // Default (no options or includeFolderEntities omitted) stays on the v1 endpoint = tenant only, - // no OR.Users required. + // folderKey is preferred over includeFolderEntities: when present, scope to that folder + // via the v1 endpoint + header. Only when no folderKey is given AND includeFolderEntities + // is explicitly true does the SDK switch to the v2 endpoint (returns tenant + folder + // entities together — requires OR.Users). Default (no options or includeFolderEntities + // omitted) stays on the v1 endpoint = tenant only, no OR.Users required. const endpoint = !options?.folderKey && options?.includeFolderEntities ? DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL_V2 : DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL; diff --git a/tests/unit/services/data-fabric/entities.test.ts b/tests/unit/services/data-fabric/entities.test.ts index 57ff3f1f5..79efee2fb 100644 --- a/tests/unit/services/data-fabric/entities.test.ts +++ b/tests/unit/services/data-fabric/entities.test.ts @@ -36,6 +36,7 @@ import type { import { EntityFieldDataType, EntityAggregateFunction, + EntityType, ExternalField, FieldDisplayType, QueryFilterOperator, @@ -45,6 +46,7 @@ import { EntityFieldTypeMap, EntitySchemaFieldTypeMap, FieldDisplayTypeToDataType, + ENTITY_TYPE_IDS, } from "../../../../src/models/data-fabric/entities.constants"; import { ENTITY_TEST_CONSTANTS } from "../../../utils/constants/entities"; import { TEST_CONSTANTS } from "../../../utils/constants/common"; @@ -2245,8 +2247,8 @@ describe("EntityService Unit Tests", () => { id: ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID, name: "UserType", folderId: DATA_FABRIC_TENANT_FOLDER_ID, - entityType: "ChoiceSet", - entityTypeId: 1, + entityType: EntityType.ChoiceSet, + entityTypeId: ENTITY_TYPE_IDS[EntityType.ChoiceSet], }); expect(f?.choiceSetId).toBe(ENTITY_TEST_CONSTANTS.CHOICE_SET_TARGET_ID); }); @@ -3104,7 +3106,7 @@ describe("EntityService Unit Tests", () => { isRbacEnabled: false, fields: [], id: ENTITY_TEST_CONSTANTS.ENTITY_ID, - entityType: 0, + entityType: EntityType.Entity, externalFields: [], createdBy: "u", createdTime: "t", From bae2fba4c37cc973393b6fe9173a4482ea05dfc5 Mon Sep 17 00:00:00 2001 From: Aditi Goyal Date: Mon, 15 Jun 2026 18:46:01 +0530 Subject: [PATCH 23/23] chore(data-fabric): tighten ReferenceEntityPayload.entityType + scrub OR.Users from DF source - `ReferenceEntityPayload.entityType` was typed `string`, losing type safety and autocomplete. The `EntityType` enum already covers every valid value, and the only call site (`buildReferenceMeta`) already passes `EntityType.ChoiceSet`. Narrow to `entityType?: EntityType`. - Remove the last three `OR.Users` references from Data Fabric source files (endpoint comment for `GET_ALL_V2`, inline `getAll` comment in the entity service, inline `fetchAllChoiceSets` comment). The remaining `OR.Users` mentions in the repo are unrelated SDK auth examples and the User Settings service rows in oauth-scopes.md. Co-Authored-By: Claude Opus 4.7 --- src/models/data-fabric/entities.internal-types.ts | 4 ++-- src/services/data-fabric/choicesets.ts | 8 ++++---- src/services/data-fabric/entities.ts | 4 ++-- src/utils/constants/endpoints/data-fabric.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/models/data-fabric/entities.internal-types.ts b/src/models/data-fabric/entities.internal-types.ts index 6bbf4edf6..eb6248d61 100644 --- a/src/models/data-fabric/entities.internal-types.ts +++ b/src/models/data-fabric/entities.internal-types.ts @@ -1,4 +1,4 @@ -import { FieldDisplayType, EntityRecord, ReferenceType, SqlType } from './entities.types'; +import { EntityType, FieldDisplayType, EntityRecord, ReferenceType, SqlType } from './entities.types'; /** * Write-side payload shape for creating a new field in a schema upsert call. @@ -30,7 +30,7 @@ export interface ReferenceEntityPayload { id: string; name?: string; folderId?: string; - entityType?: string; + entityType?: EntityType; entityTypeId?: number; } diff --git a/src/services/data-fabric/choicesets.ts b/src/services/data-fabric/choicesets.ts index 576bd18a7..85d5d8292 100644 --- a/src/services/data-fabric/choicesets.ts +++ b/src/services/data-fabric/choicesets.ts @@ -67,10 +67,10 @@ export class ChoiceSetService extends BaseService implements ChoiceSetServiceMod */ private async fetchAllChoiceSets(options?: ChoiceSetGetAllOptions): Promise { // The choice-set endpoint returns cross-scope results when called without - // a folder header. To stay tenant-only by default (no OR.Users required), - // send the tenant-marker UUID as the folder key unless the caller explicitly - // opts into cross-scope via includeFolderChoiceSets: true. folderKey is - // preferred over includeFolderChoiceSets when both are set. + // a folder header. To stay tenant-only by default, send the tenant-marker + // UUID as the folder key unless the caller explicitly opts into cross-scope + // via includeFolderChoiceSets: true. folderKey is preferred over + // includeFolderChoiceSets when both are set. const folderKey = options?.folderKey ?? (options?.includeFolderChoiceSets ? undefined : DATA_FABRIC_TENANT_FOLDER_ID); diff --git a/src/services/data-fabric/entities.ts b/src/services/data-fabric/entities.ts index 43bce28ec..3b3ae3f1a 100644 --- a/src/services/data-fabric/entities.ts +++ b/src/services/data-fabric/entities.ts @@ -538,8 +538,8 @@ export class EntityService extends BaseService implements EntityServiceModel { // folderKey is preferred over includeFolderEntities: when present, scope to that folder // via the v1 endpoint + header. Only when no folderKey is given AND includeFolderEntities // is explicitly true does the SDK switch to the v2 endpoint (returns tenant + folder - // entities together — requires OR.Users). Default (no options or includeFolderEntities - // omitted) stays on the v1 endpoint = tenant only, no OR.Users required. + // entities together). Default (no options or includeFolderEntities omitted) stays on + // the v1 endpoint = tenant only. const endpoint = !options?.folderKey && options?.includeFolderEntities ? DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL_V2 : DATA_FABRIC_ENDPOINTS.ENTITY.GET_ALL; diff --git a/src/utils/constants/endpoints/data-fabric.ts b/src/utils/constants/endpoints/data-fabric.ts index 8c7bd138d..727220619 100644 --- a/src/utils/constants/endpoints/data-fabric.ts +++ b/src/utils/constants/endpoints/data-fabric.ts @@ -17,7 +17,7 @@ export const DATA_FABRIC_TENANT_FOLDER_ID = '00000000-0000-0000-0000-00000000000 export const DATA_FABRIC_ENDPOINTS = { ENTITY: { GET_ALL: `${DATAFABRIC_BASE}/api/Entity`, - // Lists tenant-level and folder-level entities together (requires OR.Users scope). + // Lists tenant-level and folder-level entities together. // Used by getAll when includeFolderEntities is true. GET_ALL_V2: `${DATAFABRIC_BASE}/api/v2/Entity`, GET_ENTITY_RECORDS: (entityId: string) => `${DATAFABRIC_BASE}/api/EntityService/entity/${entityId}/read`,