Skip to content

feat: add multi-join support to Data Fabric entity queries#543

Open
aayushuipath wants to merge 4 commits into
mainfrom
feat/entities-multi-join-query
Open

feat: add multi-join support to Data Fabric entity queries#543
aayushuipath wants to merge 4 commits into
mainfrom
feat/entities-multi-join-query

Conversation

@aayushuipath

Copy link
Copy Markdown

Summary

Adds multi-join support to Data Fabric entity queries in the TypeScript SDK, porting the joins capability from the Python SDK (UiPath/uipath-python#1616).

queryRecordsById already supported filterGroup, selectedFields, sortOptions, expansionLevel, aggregates, groupBy, and pagination — but not cross-entity joins. This PR closes that gap so TS reaches parity with Python's structured query.

What changed

  • New type EntityJoin (src/models/data-fabric/entities.types.ts) mirroring the Python EntityJoin model:
    • entityName, joinType, joinFieldName, relatedEntityName, relatedFieldName
    • joinType reuses the existing JoinType enum (LeftJoin) for consistency with SourceJoinCriteria, rather than a loose string.
  • New joins?: EntityJoin[] option on EntityQueryRecordsOptions. Supply one entry per related entity; several entries compose a multi-entity query.
  • queryRecordsById wiring (src/services/data-fabric/entities.ts): joins flows into the POST body and is added to excludeFromPrefix so it is not OData $-prefixed — identical handling to aggregates/groupBy. No endpoint change: the existing …/EntityService/entity/{id}/query (V1) endpoint already accepts joins.
  • JSDoc updated with a multi-join example.
  • Tests (tests/unit/services/data-fabric/entities.test.ts): join pass-through, excludeFromPrefix inclusion, and joinType serializing to the enum string value.

Wire format

POST datafabric_/api/EntityService/entity/{id}/query
{
  "joins": [
    { "entityName": "Order", "joinType": "LeftJoin",
      "joinFieldName": "customerId",
      "relatedEntityName": "Customer", "relatedFieldName": "Id" }
  ]
}

Usage

import { Entities, JoinType } from '@uipath/uipath-typescript/entities';

const entities = new Entities(sdk);

await entities.queryRecordsById("<entityId>", {
  selectedFields: ["Id", "amount"],
  joins: [
    {
      entityName: "Order",
      joinType: JoinType.LeftJoin,
      joinFieldName: "customerId",
      relatedEntityName: "Customer",
      relatedFieldName: "Id",
    },
  ],
});

Scope note

The Python PR #1616 was a broad entities-service expansion. This PR ports only the multi-join feature that was requested. The related V2 binnings clause (the only thing in Python that routes to the v2/.../query endpoint) is intentionally left out and can follow separately if needed.

Backward compatibility

Additive only — one new optional type and one new optional query option. No existing signatures or behavior change.

Test plan

  • npm run typecheck — clean
  • npm run lint (oxlint) — 0 warnings, 0 errors
  • npx vitest run — 1882 unit tests pass (incl. 3 new join tests)
  • npm run build — succeeds; EntityJoin present in dist/entities/index.d.ts
  • Live smoke against a Data Fabric tenant before release tag

🤖 Generated with Claude Code

Port the `joins` query capability from the Python SDK (UiPath/uipath-python#1616)
to queryRecordsById. Adds an EntityJoin type and a `joins` query option that lets
a structured query pull fields from related entities by matching a field on the
base entity to a field on a related entity; supplying several composes a
multi-entity (multi-join) query.

joins are sent in the POST body and added to excludeFromPrefix so they are not
OData $-prefixed, matching the existing aggregates/groupBy handling. joinType
reuses the existing JoinType enum for consistency with SourceJoinCriteria.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aayushuipath aayushuipath requested a review from a team June 20, 2026 09:22
Comment thread src/services/data-fabric/entities.ts Outdated
Comment thread src/models/data-fabric/entities.types.ts
@claude

claude Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review findings

Two inline comments posted + one file missing from the diff:

Inline comments

  1. entities.ts line 617import { JoinType } is placed mid-block inside the ongoing @example code block. Move it to the existing import at line 574 instead.

  2. entities.types.ts lines 254–265 — All EntityJoin fields are ? optional. The three join-key fields (joinFieldName, relatedEntityName, relatedFieldName) must be supplied for the API to execute any join; they should be required.

Not in diff — src/models/data-fabric/entities.models.ts

The EntityServiceModel.queryRecordsById JSDoc was not updated. Per convention: "Keep JSDoc on service class methods in sync with {Entity}ServiceModel — both the models file and the service implementation file must have identical JSDoc for each public method. ServiceModel is the source of truth for docs."

Specifically:

  • @param options still reads "…aggregates, groupBy, and pagination…"joins is missing.
  • The multi-join @example block added to entities.ts is absent from entities.models.ts, which is what TypeDoc renders for the public API docs site.

Mirror the Python SDK's documented constraint (max 3 joins, all of the same
JoinType) in the EntityJoin and joins option JSDoc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread tests/unit/services/data-fabric/entities.test.ts
@claude

claude Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review findings

One new inline comment posted this run:

tests/unit/services/data-fabric/entities.test.ts line 1712 — No integration test covers the new joins parameter. The integration test file is unchanged, and the PR test plan has the live smoke unchecked. A basic integration test calling queryRecordsById with a single join would guard against the endpoint silently ignoring the joins field.

aayushuipath added a commit that referenced this pull request Jun 23, 2026
Address Claude review on PR #543:
- Fix the queryRecordsById @example: move JoinType into the block's top
  import so the multi-join snippet is copy-pasteable (no mid-block import).
- Add a guarded integration test for the joins parameter on queryRecordsById,
  driven by DATA_FABRIC_TEST_JOIN_* fixture env vars; skipped when unset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

✅ No issues found. Checked for bugs and CLAUDE.md compliance.

Address Claude review on PR #543:
- Fix the queryRecordsById @example: move JoinType into the block's top
  import so the multi-join snippet is copy-pasteable (no mid-block import).
- Add a guarded integration test for the joins parameter on queryRecordsById,
  driven by DATA_FABRIC_TEST_JOIN_* fixture env vars; skipped when unset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aayushuipath aayushuipath force-pushed the feat/entities-multi-join-query branch from 33e8ffd to 0479a6e Compare June 24, 2026 04:44
Comment thread src/services/data-fabric/entities.ts
@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review findings

One new inline comment posted this run:

entities.ts line 568 — The @param options was updated in the service class but src/models/data-fabric/entities.models.ts was not updated. That file is the TypeDoc source for the public docs site; without the update, the published docs will show no mention of joins. Three spots need fixing: EntityServiceModel.queryRecordsById @param and @example, and EntityMethods.queryRecords @param and @example (add JoinType to the import and add a multi-join example).

Address Claude re-review on PR #543: the service-class JSDoc had `joins`
but the canonical `entities.models.ts` interface (what TypeDoc renders) did
not. Add `joins` to the @param and a multi-join @example (with JoinType in
the import) for both EntityServiceModel.queryRecordsById and
EntityMethods.queryRecords.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
process.env.DATA_FABRIC_TEST_JOIN_RELATED_FIELD_NAME,
);

it.skipIf(!hasJoinFixture)('should query records with a single cross-entity join', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Per convention: "Always throw new Error() when test preconditions are not met — whether it's missing config or missing test data. Never use … silent skips — silent skips hide unrunnable tests and make CI green when tests aren't actually exercised." describe.skip and it.skipIf fall in the same bucket — CI stays green without the test running.

The fix matches the pattern already in this test for missing entityId: remove the hasJoinFixture const + surrounding comment, change it.skipIf(!hasJoinFixture) back to a plain it, and add a throw inside the test body right after the entityId check:

Suggested change
it.skipIf(!hasJoinFixture)('should query records with a single cross-entity join', async () => {
it('should query records with a single cross-entity join', async () => {

Then inside the test body, after the entityId guard:

if (
  !config.dataFabricTestJoinEntityName ||
  !config.dataFabricTestJoinFieldName ||
  !config.dataFabricTestJoinRelatedEntityName ||
  !config.dataFabricTestJoinRelatedFieldName
) {
  throw new Error(
    'DATA_FABRIC_TEST_JOIN_* env vars are required for the join integration test',
  );
}

This makes the test fail (not skip) when the fixture is absent, so a CI run that omits the join env vars is immediately visible as a failure rather than a quiet skip.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Keeping the skip here intentionally. A join needs a second related entity, and the DATA_FABRIC_TEST_JOIN_* fixtures aren't provisioned in the CI tenant, so switching to a throw would fail test-and-build on every run that doesn't set them. This also matches the existing pattern in this file, where tests that can't run under the CI token use describe.skip (the schema-write scope blocks). The test runs and asserts the join round-trip once the fixture vars are set locally or in a tenant that has them. Resolving as won't-fix.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

integration tests run in a pre-defined org+tenant (procodeapps org/integrationtest tenant). If there is anything missing in this org+tenant, can we create that (missing second entity), and remove this skip? @aayushuipath

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we should ideally not skip tests in case of missing test data/required artifacts

@claude

claude Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review findings

One new inline comment posted this run:

tests/integration/shared/data-fabric/entities.integration.test.ts line 975it.skipIf(!hasJoinFixture) violates the convention requiring throw new Error() when test preconditions are not met. Using it.skipIf allows CI to pass with the test absent entirely; the fix is to remove the hasJoinFixture const, use a plain it, and throw inside the test body when the join env vars are missing (same pattern as the existing entityId guard already in the test).

@sonarqubecloud

Copy link
Copy Markdown

*/
export interface EntityJoin {
/** Name of the base (left) entity that owns `joinFieldName`. */
entityName?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

how is this optional? can join exist without entityName? Do same consideration for other fields as well to check if they are truly optional in join context

process.env.DATA_FABRIC_TEST_JOIN_RELATED_FIELD_NAME,
);

it.skipIf(!hasJoinFixture)('should query records with a single cross-entity join', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

integration tests run in a pre-defined org+tenant (procodeapps org/integrationtest tenant). If there is anything missing in this org+tenant, can we create that (missing second entity), and remove this skip? @aayushuipath

process.env.DATA_FABRIC_TEST_JOIN_RELATED_FIELD_NAME,
);

it.skipIf(!hasJoinFixture)('should query records with a single cross-entity join', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we should ideally not skip tests in case of missing test data/required artifacts

*
* @example
* ```typescript
* import { JoinType } from '@uipath/uipath-typescript/entities';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

since we are using existing JoinType, update its JSDoc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants