@salesforce/graphiti 10.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +424 -0
- package/CHANGELOG.md +448 -0
- package/LICENSE.txt +82 -0
- package/README.md +204 -0
- package/TASK.md +249 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +683 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/args.d.ts +13 -0
- package/dist/commands/args.js +207 -0
- package/dist/commands/args.js.map +1 -0
- package/dist/commands/build.d.ts +11 -0
- package/dist/commands/build.js +209 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/connect.d.ts +8 -0
- package/dist/commands/connect.js +55 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/describe.d.ts +6 -0
- package/dist/commands/describe.js +229 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/meta.d.ts +9 -0
- package/dist/commands/meta.js +140 -0
- package/dist/commands/meta.js.map +1 -0
- package/dist/commands/navigate.d.ts +14 -0
- package/dist/commands/navigate.js +105 -0
- package/dist/commands/navigate.js.map +1 -0
- package/dist/commands/query-helpers.d.ts +80 -0
- package/dist/commands/query-helpers.js +865 -0
- package/dist/commands/query-helpers.js.map +1 -0
- package/dist/commands/query.d.ts +26 -0
- package/dist/commands/query.js +901 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/review.d.ts +18 -0
- package/dist/commands/review.js +533 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/session-mgmt.d.ts +25 -0
- package/dist/commands/session-mgmt.js +206 -0
- package/dist/commands/session-mgmt.js.map +1 -0
- package/dist/commands/type.d.ts +6 -0
- package/dist/commands/type.js +342 -0
- package/dist/commands/type.js.map +1 -0
- package/dist/commands/validate-input.d.ts +6 -0
- package/dist/commands/validate-input.js +32 -0
- package/dist/commands/validate-input.js.map +1 -0
- package/dist/intent/build-aggregate.d.ts +33 -0
- package/dist/intent/build-aggregate.js +134 -0
- package/dist/intent/build-aggregate.js.map +1 -0
- package/dist/intent/build-create.d.ts +14 -0
- package/dist/intent/build-create.js +16 -0
- package/dist/intent/build-create.js.map +1 -0
- package/dist/intent/build-delete.d.ts +30 -0
- package/dist/intent/build-delete.js +53 -0
- package/dist/intent/build-delete.js.map +1 -0
- package/dist/intent/build-detail.d.ts +32 -0
- package/dist/intent/build-detail.js +80 -0
- package/dist/intent/build-detail.js.map +1 -0
- package/dist/intent/build-discover.d.ts +30 -0
- package/dist/intent/build-discover.js +149 -0
- package/dist/intent/build-discover.js.map +1 -0
- package/dist/intent/build-list.d.ts +28 -0
- package/dist/intent/build-list.js +75 -0
- package/dist/intent/build-list.js.map +1 -0
- package/dist/intent/build-mutation.d.ts +23 -0
- package/dist/intent/build-mutation.js +54 -0
- package/dist/intent/build-mutation.js.map +1 -0
- package/dist/intent/build-output.d.ts +27 -0
- package/dist/intent/build-output.js +60 -0
- package/dist/intent/build-output.js.map +1 -0
- package/dist/intent/build-raw.d.ts +23 -0
- package/dist/intent/build-raw.js +54 -0
- package/dist/intent/build-raw.js.map +1 -0
- package/dist/intent/build-update.d.ts +14 -0
- package/dist/intent/build-update.js +16 -0
- package/dist/intent/build-update.js.map +1 -0
- package/dist/intent/get-schema-with-priming.d.ts +26 -0
- package/dist/intent/get-schema-with-priming.js +32 -0
- package/dist/intent/get-schema-with-priming.js.map +1 -0
- package/dist/intent/select-child-relationship.d.ts +19 -0
- package/dist/intent/select-child-relationship.js +38 -0
- package/dist/intent/select-child-relationship.js.map +1 -0
- package/dist/intent/types.d.ts +159 -0
- package/dist/intent/types.js +21 -0
- package/dist/intent/types.js.map +1 -0
- package/dist/lib/apply-command.d.ts +15 -0
- package/dist/lib/apply-command.js +238 -0
- package/dist/lib/apply-command.js.map +1 -0
- package/dist/lib/auth.d.ts +38 -0
- package/dist/lib/auth.js +113 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/codegen.d.ts +32 -0
- package/dist/lib/codegen.js +700 -0
- package/dist/lib/codegen.js.map +1 -0
- package/dist/lib/command-registry.d.ts +59 -0
- package/dist/lib/command-registry.js +366 -0
- package/dist/lib/command-registry.js.map +1 -0
- package/dist/lib/formatter.d.ts +76 -0
- package/dist/lib/formatter.js +419 -0
- package/dist/lib/formatter.js.map +1 -0
- package/dist/lib/fs-utils.d.ts +23 -0
- package/dist/lib/fs-utils.js +46 -0
- package/dist/lib/fs-utils.js.map +1 -0
- package/dist/lib/graphql-name.d.ts +27 -0
- package/dist/lib/graphql-name.js +32 -0
- package/dist/lib/graphql-name.js.map +1 -0
- package/dist/lib/interactive.d.ts +6 -0
- package/dist/lib/interactive.js +562 -0
- package/dist/lib/interactive.js.map +1 -0
- package/dist/lib/introspect.d.ts +40 -0
- package/dist/lib/introspect.js +280 -0
- package/dist/lib/introspect.js.map +1 -0
- package/dist/lib/object-info.d.ts +79 -0
- package/dist/lib/object-info.js +313 -0
- package/dist/lib/object-info.js.map +1 -0
- package/dist/lib/path-selection.d.ts +50 -0
- package/dist/lib/path-selection.js +146 -0
- package/dist/lib/path-selection.js.map +1 -0
- package/dist/lib/prime-schema.d.ts +59 -0
- package/dist/lib/prime-schema.js +158 -0
- package/dist/lib/prime-schema.js.map +1 -0
- package/dist/lib/query-builder.d.ts +10 -0
- package/dist/lib/query-builder.js +168 -0
- package/dist/lib/query-builder.js.map +1 -0
- package/dist/lib/query-commands.d.ts +19 -0
- package/dist/lib/query-commands.js +262 -0
- package/dist/lib/query-commands.js.map +1 -0
- package/dist/lib/session.d.ts +205 -0
- package/dist/lib/session.js +1075 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/tokenize.d.ts +12 -0
- package/dist/lib/tokenize.js +48 -0
- package/dist/lib/tokenize.js.map +1 -0
- package/dist/lib/uiapi.d.ts +109 -0
- package/dist/lib/uiapi.js +159 -0
- package/dist/lib/uiapi.js.map +1 -0
- package/dist/lib/validator.d.ts +27 -0
- package/dist/lib/validator.js +100 -0
- package/dist/lib/validator.js.map +1 -0
- package/dist/lib/variable-promotion.d.ts +69 -0
- package/dist/lib/variable-promotion.js +95 -0
- package/dist/lib/variable-promotion.js.map +1 -0
- package/dist/lib/walker.d.ts +147 -0
- package/dist/lib/walker.js +723 -0
- package/dist/lib/walker.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +34 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +7 -0
- package/dist/mcp/stdio.js +25 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools/echo.d.ts +7 -0
- package/dist/mcp/tools/echo.js +17 -0
- package/dist/mcp/tools/echo.js.map +1 -0
- package/dist/mcp/tools/sf-gql-aggregate.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-aggregate.js +75 -0
- package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -0
- package/dist/mcp/tools/sf-gql-create.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-create.js +35 -0
- package/dist/mcp/tools/sf-gql-create.js.map +1 -0
- package/dist/mcp/tools/sf-gql-delete.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-delete.js +31 -0
- package/dist/mcp/tools/sf-gql-delete.js.map +1 -0
- package/dist/mcp/tools/sf-gql-detail.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-detail.js +58 -0
- package/dist/mcp/tools/sf-gql-detail.js.map +1 -0
- package/dist/mcp/tools/sf-gql-discover.d.ts +9 -0
- package/dist/mcp/tools/sf-gql-discover.js +51 -0
- package/dist/mcp/tools/sf-gql-discover.js.map +1 -0
- package/dist/mcp/tools/sf-gql-list.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-list.js +53 -0
- package/dist/mcp/tools/sf-gql-list.js.map +1 -0
- package/dist/mcp/tools/sf-gql-raw.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-raw.js +39 -0
- package/dist/mcp/tools/sf-gql-raw.js.map +1 -0
- package/dist/mcp/tools/sf-gql-update.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-update.js +35 -0
- package/dist/mcp/tools/sf-gql-update.js.map +1 -0
- package/dist/mcp/tools/shared/zod-schemas.d.ts +49 -0
- package/dist/mcp/tools/shared/zod-schemas.js +46 -0
- package/dist/mcp/tools/shared/zod-schemas.js.map +1 -0
- package/package.json +36 -0
- package/ralph-loop.sh +120 -0
- package/scripts/smoke-orderby.sh +190 -0
- package/src/__tests__/helpers/prime-deps.ts +46 -0
- package/src/__tests__/helpers/schema.ts +73 -0
- package/src/__tests__/helpers/stdout.ts +33 -0
- package/src/__tests__/setup.ts +19 -0
- package/src/cli.ts +764 -0
- package/src/commands/__tests__/query.spec.ts +137 -0
- package/src/commands/args.ts +306 -0
- package/src/commands/build.ts +288 -0
- package/src/commands/connect.ts +60 -0
- package/src/commands/describe.ts +246 -0
- package/src/commands/meta.ts +171 -0
- package/src/commands/navigate.ts +134 -0
- package/src/commands/query-helpers.ts +1202 -0
- package/src/commands/query.ts +1085 -0
- package/src/commands/review.ts +670 -0
- package/src/commands/session-mgmt.ts +256 -0
- package/src/commands/type.ts +437 -0
- package/src/commands/validate-input.ts +38 -0
- package/src/intent/__tests__/build-aggregate.spec.ts +931 -0
- package/src/intent/__tests__/build-create-validation.spec.ts +135 -0
- package/src/intent/__tests__/build-delete.spec.ts +121 -0
- package/src/intent/__tests__/build-detail.spec.ts +333 -0
- package/src/intent/__tests__/build-discover.spec.ts +432 -0
- package/src/intent/__tests__/build-list.spec.ts +284 -0
- package/src/intent/__tests__/build-mutation.spec.ts +108 -0
- package/src/intent/__tests__/build-output.spec.ts +55 -0
- package/src/intent/__tests__/build-raw.spec.ts +153 -0
- package/src/intent/__tests__/build-update-validation.spec.ts +134 -0
- package/src/intent/build-aggregate.ts +182 -0
- package/src/intent/build-create.ts +19 -0
- package/src/intent/build-delete.ts +62 -0
- package/src/intent/build-detail.ts +95 -0
- package/src/intent/build-discover.ts +199 -0
- package/src/intent/build-list.ts +91 -0
- package/src/intent/build-mutation.ts +75 -0
- package/src/intent/build-output.ts +72 -0
- package/src/intent/build-raw.ts +61 -0
- package/src/intent/build-update.ts +19 -0
- package/src/intent/get-schema-with-priming.ts +43 -0
- package/src/intent/select-child-relationship.ts +48 -0
- package/src/intent/types.ts +181 -0
- package/src/lib/__tests__/apply-command.parity.spec.ts +97 -0
- package/src/lib/__tests__/apply-command.spec.ts +171 -0
- package/src/lib/__tests__/auth.spec.ts +292 -0
- package/src/lib/__tests__/formatter.spec.ts +86 -0
- package/src/lib/__tests__/graphql-name.spec.ts +64 -0
- package/src/lib/__tests__/introspect.spec.ts +399 -0
- package/src/lib/__tests__/object-info.spec.ts +124 -0
- package/src/lib/__tests__/path-selection.spec.ts +219 -0
- package/src/lib/__tests__/prime-schema.spec.ts +269 -0
- package/src/lib/__tests__/query-builder.spec.ts +95 -0
- package/src/lib/__tests__/query-commands.spec.ts +74 -0
- package/src/lib/__tests__/session.spec.ts +292 -0
- package/src/lib/__tests__/tokenize.spec.ts +33 -0
- package/src/lib/__tests__/uiapi.spec.ts +192 -0
- package/src/lib/__tests__/variable-promotion.spec.ts +211 -0
- package/src/lib/__tests__/walker.spec.ts +250 -0
- package/src/lib/apply-command.ts +286 -0
- package/src/lib/auth.ts +157 -0
- package/src/lib/codegen.ts +899 -0
- package/src/lib/command-registry.ts +434 -0
- package/src/lib/formatter.ts +587 -0
- package/src/lib/fs-utils.ts +46 -0
- package/src/lib/graphql-name.ts +35 -0
- package/src/lib/interactive.ts +634 -0
- package/src/lib/introspect.ts +320 -0
- package/src/lib/object-info.ts +409 -0
- package/src/lib/path-selection.ts +162 -0
- package/src/lib/prime-schema.ts +195 -0
- package/src/lib/query-builder.ts +193 -0
- package/src/lib/query-commands.ts +290 -0
- package/src/lib/session.ts +1304 -0
- package/src/lib/tokenize.ts +43 -0
- package/src/lib/uiapi.ts +176 -0
- package/src/lib/validator.ts +143 -0
- package/src/lib/variable-promotion.ts +145 -0
- package/src/lib/walker.ts +975 -0
- package/src/mcp/__tests__/server.spec.ts +155 -0
- package/src/mcp/server.ts +38 -0
- package/src/mcp/stdio.ts +29 -0
- package/src/mcp/tools/__tests__/sf-gql-aggregate.spec.ts +173 -0
- package/src/mcp/tools/__tests__/sf-gql-create.spec.ts +235 -0
- package/src/mcp/tools/__tests__/sf-gql-delete.spec.ts +194 -0
- package/src/mcp/tools/__tests__/sf-gql-detail.spec.ts +246 -0
- package/src/mcp/tools/__tests__/sf-gql-discover.spec.ts +320 -0
- package/src/mcp/tools/__tests__/sf-gql-list.spec.ts +128 -0
- package/src/mcp/tools/__tests__/sf-gql-raw.spec.ts +141 -0
- package/src/mcp/tools/__tests__/sf-gql-update.spec.ts +207 -0
- package/src/mcp/tools/echo.ts +24 -0
- package/src/mcp/tools/sf-gql-aggregate.ts +102 -0
- package/src/mcp/tools/sf-gql-create.ts +55 -0
- package/src/mcp/tools/sf-gql-delete.ts +49 -0
- package/src/mcp/tools/sf-gql-detail.ts +85 -0
- package/src/mcp/tools/sf-gql-discover.ts +67 -0
- package/src/mcp/tools/sf-gql-list.ts +73 -0
- package/src/mcp/tools/sf-gql-raw.ts +56 -0
- package/src/mcp/tools/sf-gql-update.ts +55 -0
- package/src/mcp/tools/shared/zod-schemas.ts +55 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { buildSchema } from "graphql";
|
|
8
|
+
import { describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { makeNoopPrimeDeps } from "../../__tests__/helpers/prime-deps.js";
|
|
10
|
+
import {
|
|
11
|
+
type ObjectInfoResult,
|
|
12
|
+
clearObjectInfoCache,
|
|
13
|
+
setCachedObjectInfo,
|
|
14
|
+
} from "../../lib/object-info.js";
|
|
15
|
+
import * as sessionModule from "../../lib/session.js";
|
|
16
|
+
import { primeSchemaCache } from "../../lib/walker.js";
|
|
17
|
+
import { buildAggregate } from "../build-aggregate.js";
|
|
18
|
+
|
|
19
|
+
vi.mock("../../lib/session.js", async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<typeof sessionModule>();
|
|
21
|
+
return { ...actual, createSession: vi.fn(actual.createSession) };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const SCHEMA_SDL = `
|
|
25
|
+
type Query { uiapi: UIAPI! }
|
|
26
|
+
type UIAPI {
|
|
27
|
+
query: RecordQuery!
|
|
28
|
+
aggregate: RecordQueryAggregate!
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type RecordQuery {
|
|
32
|
+
Account(first: Int, after: String, where: Account_Filter, orderBy: Account_OrderBy): AccountConnection!
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type RecordQueryAggregate {
|
|
36
|
+
Account(first: Int, after: String, where: Account_Filter, orderBy: Account_OrderBy, groupBy: Account_GroupBy): AccountAggregateConnection
|
|
37
|
+
Order(first: Int, after: String, where: Order_Filter, orderBy: Order_OrderBy, groupBy: Order_GroupBy): OrderAggregateConnection
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
input Account_Filter { Industry: PicklistOperators, AnnualRevenue: DoubleOperators }
|
|
41
|
+
input Account_OrderBy { Name: OrderByClause, Industry: OrderByClause, Description: AggregateOrderByStringClause }
|
|
42
|
+
input Order_OrderBy { Status: OrderByClause, Amount: AggregateOrderByNumberClause, CreatedDate: OrderByClause }
|
|
43
|
+
input Order_Filter { Status: PicklistOperators, Amount: DoubleOperators, CreatedDate: DateTimeOperators }
|
|
44
|
+
input Account_GroupBy { Industry: GroupByClause, Name: GroupByClause }
|
|
45
|
+
input Order_GroupBy { Status: GroupByClause, CreatedDate: GroupByDateFunction }
|
|
46
|
+
|
|
47
|
+
input DateTimeOperators { gte: String, lte: String }
|
|
48
|
+
input GroupByDateFunction { function: GroupByFunction }
|
|
49
|
+
enum GroupByFunction { CALENDAR_MONTH CALENDAR_QUARTER CALENDAR_YEAR DAY_IN_MONTH DAY_IN_WEEK DAY_IN_YEAR FISCAL_MONTH FISCAL_QUARTER FISCAL_YEAR HOUR_IN_DAY CALENDAR_MONTH_IN_YEAR FISCAL_MONTH_IN_YEAR WEEK_IN_YEAR }
|
|
50
|
+
|
|
51
|
+
input PicklistOperators { eq: String, ne: String, in: [String!] }
|
|
52
|
+
input DoubleOperators { eq: Float, gt: Float, lt: Float }
|
|
53
|
+
input OrderByClause { order: Order!, nulls: NullsOrder }
|
|
54
|
+
input AggregateOrderByStringClause { function: AggregateOrderByStringFunction!, order: ResultsOrder!, nulls: NullsOrder }
|
|
55
|
+
input AggregateOrderByNumberClause { function: AggregateOrderByNumberFunction!, order: ResultsOrder!, nulls: NullsOrder }
|
|
56
|
+
enum AggregateOrderByStringFunction { COUNT COUNT_DISTINCT MAX MIN }
|
|
57
|
+
enum AggregateOrderByNumberFunction { AVG COUNT COUNT_DISTINCT MAX MIN SUM }
|
|
58
|
+
enum ResultsOrder { ASC DESC }
|
|
59
|
+
input GroupByClause { group: Boolean }
|
|
60
|
+
enum Order { ASC DESC }
|
|
61
|
+
enum NullsOrder { FIRST LAST }
|
|
62
|
+
|
|
63
|
+
type AccountConnection { edges: [AccountEdge!]!, pageInfo: PageInfo! }
|
|
64
|
+
type AccountEdge { node: Account! }
|
|
65
|
+
|
|
66
|
+
type AccountAggregateConnection {
|
|
67
|
+
edges: [AccountAggregateEdge!]!
|
|
68
|
+
pageInfo: PageInfo!
|
|
69
|
+
}
|
|
70
|
+
type AccountAggregateEdge { node: AccountResult!, cursor: String! }
|
|
71
|
+
type AccountResult { aggregate: AccountAggregate }
|
|
72
|
+
|
|
73
|
+
type AccountAggregate {
|
|
74
|
+
Id: IDAggregate
|
|
75
|
+
Name: StringAggregate
|
|
76
|
+
Industry: PicklistAggregate
|
|
77
|
+
AnnualRevenue: DoubleAggregate
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type OrderAggregateConnection {
|
|
81
|
+
edges: [OrderAggregateEdge!]!
|
|
82
|
+
pageInfo: PageInfo!
|
|
83
|
+
}
|
|
84
|
+
type OrderAggregateEdge { node: OrderResult!, cursor: String! }
|
|
85
|
+
type OrderResult { aggregate: OrderAggregate }
|
|
86
|
+
|
|
87
|
+
type OrderAggregate {
|
|
88
|
+
Id: IDAggregate
|
|
89
|
+
Status: PicklistAggregate
|
|
90
|
+
Amount: DoubleAggregate
|
|
91
|
+
CreatedDate: DateTimeAggregate
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type DateTimeAggregate {
|
|
95
|
+
value: String
|
|
96
|
+
count: LongValue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type IDAggregate {
|
|
100
|
+
value: ID
|
|
101
|
+
count: LongValue
|
|
102
|
+
countDistinct: LongValue
|
|
103
|
+
min: IDValue
|
|
104
|
+
max: IDValue
|
|
105
|
+
}
|
|
106
|
+
type StringAggregate {
|
|
107
|
+
value: String
|
|
108
|
+
count: LongValue
|
|
109
|
+
countDistinct: LongValue
|
|
110
|
+
min: StringValue
|
|
111
|
+
max: StringValue
|
|
112
|
+
}
|
|
113
|
+
type DoubleAggregate {
|
|
114
|
+
value: Float
|
|
115
|
+
count: LongValue
|
|
116
|
+
countDistinct: LongValue
|
|
117
|
+
sum: DoubleValue
|
|
118
|
+
avg: DoubleValue
|
|
119
|
+
min: DoubleValue
|
|
120
|
+
max: DoubleValue
|
|
121
|
+
}
|
|
122
|
+
type PicklistAggregate {
|
|
123
|
+
value: String
|
|
124
|
+
count: LongValue
|
|
125
|
+
countDistinct: LongValue
|
|
126
|
+
min: PicklistValue
|
|
127
|
+
max: PicklistValue
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type Account {
|
|
131
|
+
Id: ID!
|
|
132
|
+
Name: StringValue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type LongValue { value: Float }
|
|
136
|
+
type IDValue { value: ID }
|
|
137
|
+
type StringValue { value: String }
|
|
138
|
+
type DoubleValue { value: Float }
|
|
139
|
+
type PicklistValue { value: String }
|
|
140
|
+
type PageInfo { hasNextPage: Boolean!, endCursor: String }
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const ORG = "test-aggregate";
|
|
144
|
+
const ORG_URL = "https://test-aggregate.my.salesforce.com";
|
|
145
|
+
const SCHEMA = buildSchema(SCHEMA_SDL);
|
|
146
|
+
primeSchemaCache(ORG, SCHEMA);
|
|
147
|
+
primeSchemaCache(ORG_URL, SCHEMA);
|
|
148
|
+
|
|
149
|
+
const noopPrimeDeps = () => makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
150
|
+
|
|
151
|
+
describe("intent/build-aggregate", () => {
|
|
152
|
+
it("default aggregation is count over Id (FR-8.2)", async () => {
|
|
153
|
+
const out = await buildAggregate(
|
|
154
|
+
{
|
|
155
|
+
org: ORG,
|
|
156
|
+
object: "Account",
|
|
157
|
+
groupBy: [],
|
|
158
|
+
aggregations: [{ function: "count" }],
|
|
159
|
+
},
|
|
160
|
+
noopPrimeDeps(),
|
|
161
|
+
);
|
|
162
|
+
expect(out.query).toMatch(/\bquery\s+AccountAggregate\b/);
|
|
163
|
+
// path: uiapi.aggregate.Account.edges.node.aggregate.Id.count.value
|
|
164
|
+
// aliased as countId on Id (FR-8.5 default key)
|
|
165
|
+
expect(out.query).toMatch(
|
|
166
|
+
/aggregate\s*\{[^}]*countId\s*:\s*Id\s*\{\s*count\s*\{\s*value\s*\}\s*\}/s,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("two aggregations on same field render as separate aliased projections", async () => {
|
|
171
|
+
const out = await buildAggregate(
|
|
172
|
+
{
|
|
173
|
+
org: ORG,
|
|
174
|
+
object: "Order",
|
|
175
|
+
groupBy: [],
|
|
176
|
+
aggregations: [
|
|
177
|
+
{ function: "sum", field: "Amount" },
|
|
178
|
+
{ function: "avg", field: "Amount" },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
noopPrimeDeps(),
|
|
182
|
+
);
|
|
183
|
+
expect(out.query).toMatch(/sumAmount\s*:\s*Amount\s*\{\s*sum\s*\{\s*value/s);
|
|
184
|
+
expect(out.query).toMatch(/avgAmount\s*:\s*Amount\s*\{\s*avg\s*\{\s*value/s);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("respects custom operationName", async () => {
|
|
188
|
+
const out = await buildAggregate(
|
|
189
|
+
{
|
|
190
|
+
org: ORG,
|
|
191
|
+
object: "Account",
|
|
192
|
+
groupBy: [],
|
|
193
|
+
aggregations: [{ function: "count" }],
|
|
194
|
+
operationName: "MyAccountAgg",
|
|
195
|
+
},
|
|
196
|
+
noopPrimeDeps(),
|
|
197
|
+
);
|
|
198
|
+
expect(out.query).toMatch(/\bquery\s+MyAccountAgg\b/);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("countDistinct over default Id renders as countDistinctId", async () => {
|
|
202
|
+
const out = await buildAggregate(
|
|
203
|
+
{
|
|
204
|
+
org: ORG,
|
|
205
|
+
object: "Account",
|
|
206
|
+
groupBy: [],
|
|
207
|
+
aggregations: [{ function: "countDistinct" }],
|
|
208
|
+
},
|
|
209
|
+
noopPrimeDeps(),
|
|
210
|
+
);
|
|
211
|
+
expect(out.query).toMatch(
|
|
212
|
+
/countDistinctId\s*:\s*Id\s*\{\s*countDistinct\s*\{\s*value\s*\}\s*\}/s,
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it.each([
|
|
217
|
+
["sum", "sumAmount"],
|
|
218
|
+
["avg", "avgAmount"],
|
|
219
|
+
["min", "minAmount"],
|
|
220
|
+
["max", "maxAmount"],
|
|
221
|
+
] as const)("%s over Amount renders under its function segment", async (fn, alias) => {
|
|
222
|
+
const out = await buildAggregate(
|
|
223
|
+
{
|
|
224
|
+
org: ORG,
|
|
225
|
+
object: "Order",
|
|
226
|
+
groupBy: [],
|
|
227
|
+
aggregations: [{ function: fn, field: "Amount" }],
|
|
228
|
+
},
|
|
229
|
+
noopPrimeDeps(),
|
|
230
|
+
);
|
|
231
|
+
const re = new RegExp(`${alias}\\s*:\\s*Amount\\s*\\{\\s*${fn}\\s*\\{\\s*value`, "s");
|
|
232
|
+
expect(out.query).toMatch(re);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it.each(["sum", "avg", "min", "max"] as const)("%s without field throws (FR-8.3)", async (fn) => {
|
|
236
|
+
await expect(
|
|
237
|
+
buildAggregate(
|
|
238
|
+
{
|
|
239
|
+
org: ORG,
|
|
240
|
+
object: "Account",
|
|
241
|
+
groupBy: [],
|
|
242
|
+
aggregations: [{ function: fn }],
|
|
243
|
+
},
|
|
244
|
+
noopPrimeDeps(),
|
|
245
|
+
),
|
|
246
|
+
).rejects.toThrow(/requires.*field/i);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("explicit alias overrides default key", async () => {
|
|
250
|
+
const out = await buildAggregate(
|
|
251
|
+
{
|
|
252
|
+
org: ORG,
|
|
253
|
+
object: "Order",
|
|
254
|
+
groupBy: [],
|
|
255
|
+
aggregations: [{ function: "sum", field: "Amount", alias: "totalRev" }],
|
|
256
|
+
},
|
|
257
|
+
noopPrimeDeps(),
|
|
258
|
+
);
|
|
259
|
+
expect(out.query).toMatch(/totalRev\s*:\s*Amount\s*\{\s*sum\s*\{\s*value/s);
|
|
260
|
+
expect(out.query).not.toMatch(/sumAmount/);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("default-key collision throws (FR-8.4)", async () => {
|
|
264
|
+
await expect(
|
|
265
|
+
buildAggregate(
|
|
266
|
+
{
|
|
267
|
+
org: ORG,
|
|
268
|
+
object: "Order",
|
|
269
|
+
groupBy: [],
|
|
270
|
+
aggregations: [
|
|
271
|
+
{ function: "sum", field: "Amount" },
|
|
272
|
+
{ function: "sum", field: "Amount" },
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
noopPrimeDeps(),
|
|
276
|
+
),
|
|
277
|
+
).rejects.toThrow(/duplicate/i);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("collision avoided when each entry has unique alias", async () => {
|
|
281
|
+
const out = await buildAggregate(
|
|
282
|
+
{
|
|
283
|
+
org: ORG,
|
|
284
|
+
object: "Order",
|
|
285
|
+
groupBy: [],
|
|
286
|
+
aggregations: [
|
|
287
|
+
{ function: "sum", field: "Amount", alias: "a" },
|
|
288
|
+
{ function: "sum", field: "Amount", alias: "b" },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
noopPrimeDeps(),
|
|
292
|
+
);
|
|
293
|
+
expect(out.query).toMatch(/\ba\s*:\s*Amount\s*\{\s*sum/s);
|
|
294
|
+
expect(out.query).toMatch(/\bb\s*:\s*Amount\s*\{\s*sum/s);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("explicit-alias collision also throws", async () => {
|
|
298
|
+
await expect(
|
|
299
|
+
buildAggregate(
|
|
300
|
+
{
|
|
301
|
+
org: ORG,
|
|
302
|
+
object: "Order",
|
|
303
|
+
groupBy: [],
|
|
304
|
+
aggregations: [
|
|
305
|
+
{ function: "sum", field: "Amount", alias: "x" },
|
|
306
|
+
{ function: "avg", field: "Amount", alias: "x" },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
noopPrimeDeps(),
|
|
310
|
+
),
|
|
311
|
+
).rejects.toThrow(/duplicate/i);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("PascalCase strips __c suffix in default key", async () => {
|
|
315
|
+
const out = await buildAggregate(
|
|
316
|
+
{
|
|
317
|
+
org: ORG,
|
|
318
|
+
object: "Custom",
|
|
319
|
+
groupBy: [],
|
|
320
|
+
aggregations: [{ function: "count", field: "Foo_Bar__c" }],
|
|
321
|
+
},
|
|
322
|
+
noopPrimeDeps(),
|
|
323
|
+
).catch(() => null);
|
|
324
|
+
// We just want the key derivation; if the schema lacks Custom.aggregate, that's fine —
|
|
325
|
+
// this is a placeholder until a __c-bearing fixture is added in groupBy step.
|
|
326
|
+
// Skip when schema rejects.
|
|
327
|
+
if (out) {
|
|
328
|
+
expect(out.query).toMatch(/countFooBar/);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('groupBy ["Industry"] sets groupBy arg and selects Industry { value }', async () => {
|
|
333
|
+
const out = await buildAggregate(
|
|
334
|
+
{
|
|
335
|
+
org: ORG,
|
|
336
|
+
object: "Account",
|
|
337
|
+
groupBy: ["Industry"],
|
|
338
|
+
aggregations: [{ function: "count" }],
|
|
339
|
+
},
|
|
340
|
+
noopPrimeDeps(),
|
|
341
|
+
);
|
|
342
|
+
expect(out.query).toMatch(/groupBy\s*:\s*\{\s*Industry\s*:\s*\{\s*group\s*:\s*true\s*\}\s*\}/s);
|
|
343
|
+
expect(out.query).toMatch(/aggregate\s*\{[^}]*\bIndustry\s*\{\s*value\s*\}/s);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("groupBy with multiple fields sets each key and selects each value", async () => {
|
|
347
|
+
const out = await buildAggregate(
|
|
348
|
+
{
|
|
349
|
+
org: ORG,
|
|
350
|
+
object: "Account",
|
|
351
|
+
groupBy: ["Industry", "Name"],
|
|
352
|
+
aggregations: [{ function: "count" }],
|
|
353
|
+
},
|
|
354
|
+
noopPrimeDeps(),
|
|
355
|
+
);
|
|
356
|
+
expect(out.query).toMatch(/Industry\s*:\s*\{\s*group\s*:\s*true\s*\}/s);
|
|
357
|
+
expect(out.query).toMatch(/Name\s*:\s*\{\s*group\s*:\s*true\s*\}/s);
|
|
358
|
+
expect(out.query).toMatch(/\bIndustry\s*\{\s*value\s*\}/s);
|
|
359
|
+
expect(out.query).toMatch(/\bName\s*\{\s*value\s*\}/s);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("empty groupBy does not emit groupBy arg", async () => {
|
|
363
|
+
const out = await buildAggregate(
|
|
364
|
+
{
|
|
365
|
+
org: ORG,
|
|
366
|
+
object: "Account",
|
|
367
|
+
groupBy: [],
|
|
368
|
+
aggregations: [{ function: "count" }],
|
|
369
|
+
},
|
|
370
|
+
noopPrimeDeps(),
|
|
371
|
+
);
|
|
372
|
+
expect(out.query).not.toMatch(/groupBy\s*:/);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("date-function groupBy emits { function: CALENDAR_MONTH }", async () => {
|
|
376
|
+
const out = await buildAggregate(
|
|
377
|
+
{
|
|
378
|
+
org: ORG,
|
|
379
|
+
object: "Order",
|
|
380
|
+
groupBy: [{ field: "CreatedDate", function: "CALENDAR_MONTH" }],
|
|
381
|
+
aggregations: [{ function: "count" }],
|
|
382
|
+
},
|
|
383
|
+
noopPrimeDeps(),
|
|
384
|
+
);
|
|
385
|
+
expect(out.query).toMatch(
|
|
386
|
+
/groupBy\s*:\s*\{\s*CreatedDate\s*:\s*\{\s*function\s*:\s*CALENDAR_MONTH\s*\}\s*\}/s,
|
|
387
|
+
);
|
|
388
|
+
expect(out.query).not.toMatch(/CreatedDate.*group.*true/s);
|
|
389
|
+
expect(out.query).toMatch(/CreatedDate\s*\{\s*value\s*\}/s);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("mixed groupBy: scalar string + date-function object", async () => {
|
|
393
|
+
const out = await buildAggregate(
|
|
394
|
+
{
|
|
395
|
+
org: ORG,
|
|
396
|
+
object: "Order",
|
|
397
|
+
groupBy: ["Status", { field: "CreatedDate", function: "CALENDAR_QUARTER" }],
|
|
398
|
+
aggregations: [{ function: "count" }],
|
|
399
|
+
},
|
|
400
|
+
noopPrimeDeps(),
|
|
401
|
+
);
|
|
402
|
+
expect(out.query).toMatch(/Status\s*:\s*\{\s*group\s*:\s*true\s*\}/s);
|
|
403
|
+
expect(out.query).toMatch(/CreatedDate\s*:\s*\{\s*function\s*:\s*CALENDAR_QUARTER\s*\}/s);
|
|
404
|
+
expect(out.query).toMatch(/Status\s*\{\s*value\s*\}/s);
|
|
405
|
+
expect(out.query).toMatch(/CreatedDate\s*\{\s*value\s*\}/s);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("date-function groupBy field collides with aggregation alias (FR-8.4)", async () => {
|
|
409
|
+
await expect(
|
|
410
|
+
buildAggregate(
|
|
411
|
+
{
|
|
412
|
+
org: ORG,
|
|
413
|
+
object: "Order",
|
|
414
|
+
groupBy: [{ field: "CreatedDate", function: "CALENDAR_MONTH" }],
|
|
415
|
+
aggregations: [{ function: "count", alias: "CreatedDate" }],
|
|
416
|
+
},
|
|
417
|
+
noopPrimeDeps(),
|
|
418
|
+
),
|
|
419
|
+
).rejects.toThrow(/duplicate aggregation key 'CreatedDate'/);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("duplicate groupBy field rejected", async () => {
|
|
423
|
+
await expect(
|
|
424
|
+
buildAggregate(
|
|
425
|
+
{
|
|
426
|
+
org: ORG,
|
|
427
|
+
object: "Order",
|
|
428
|
+
groupBy: ["Status", "Status"],
|
|
429
|
+
aggregations: [{ function: "count" }],
|
|
430
|
+
},
|
|
431
|
+
noopPrimeDeps(),
|
|
432
|
+
),
|
|
433
|
+
).rejects.toThrow(/duplicate groupBy field 'Status'/);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("duplicate groupBy field rejected (mixed scalar + date-function)", async () => {
|
|
437
|
+
await expect(
|
|
438
|
+
buildAggregate(
|
|
439
|
+
{
|
|
440
|
+
org: ORG,
|
|
441
|
+
object: "Order",
|
|
442
|
+
groupBy: [
|
|
443
|
+
{ field: "CreatedDate", function: "CALENDAR_MONTH" },
|
|
444
|
+
{ field: "CreatedDate", function: "CALENDAR_YEAR" },
|
|
445
|
+
],
|
|
446
|
+
aggregations: [{ function: "count" }],
|
|
447
|
+
},
|
|
448
|
+
noopPrimeDeps(),
|
|
449
|
+
),
|
|
450
|
+
).rejects.toThrow(/duplicate groupBy field 'CreatedDate'/);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("dotted groupBy field rejected (flat-only v1)", async () => {
|
|
454
|
+
await expect(
|
|
455
|
+
buildAggregate(
|
|
456
|
+
{
|
|
457
|
+
org: ORG,
|
|
458
|
+
object: "Account",
|
|
459
|
+
groupBy: ["Owner.Name"],
|
|
460
|
+
aggregations: [{ function: "count" }],
|
|
461
|
+
},
|
|
462
|
+
noopPrimeDeps(),
|
|
463
|
+
),
|
|
464
|
+
).rejects.toThrow(/dotted/i);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("filter promotes $vars and emits where arg (FR-8.6)", async () => {
|
|
468
|
+
const out = await buildAggregate(
|
|
469
|
+
{
|
|
470
|
+
org: ORG,
|
|
471
|
+
object: "Order",
|
|
472
|
+
groupBy: [],
|
|
473
|
+
aggregations: [{ function: "sum", field: "Amount" }],
|
|
474
|
+
filter: { Status: { eq: "$status" }, Amount: { gt: "$min" } },
|
|
475
|
+
},
|
|
476
|
+
noopPrimeDeps(),
|
|
477
|
+
);
|
|
478
|
+
expect(out.query).toMatch(/where\s*:\s*\{\s*Status\s*:\s*\{\s*eq\s*:\s*\$status\s*\}/s);
|
|
479
|
+
expect(out.query).toMatch(/Amount\s*:\s*\{\s*gt\s*:\s*\$min\s*\}/s);
|
|
480
|
+
// promoted as typed query variables
|
|
481
|
+
expect(out.query).toMatch(/\$status\s*:\s*String/);
|
|
482
|
+
expect(out.query).toMatch(/\$min\s*:\s*Float/);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("filter without $vars renders inline values", async () => {
|
|
486
|
+
const out = await buildAggregate(
|
|
487
|
+
{
|
|
488
|
+
org: ORG,
|
|
489
|
+
object: "Order",
|
|
490
|
+
groupBy: [],
|
|
491
|
+
aggregations: [{ function: "count" }],
|
|
492
|
+
filter: { Status: { eq: "Open" } },
|
|
493
|
+
},
|
|
494
|
+
noopPrimeDeps(),
|
|
495
|
+
);
|
|
496
|
+
expect(out.query).toMatch(/where\s*:\s*\{\s*Status\s*:\s*\{\s*eq\s*:\s*"Open"\s*\}/s);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("orderBy emits on the aggregate connection", async () => {
|
|
500
|
+
const out = await buildAggregate(
|
|
501
|
+
{
|
|
502
|
+
org: ORG,
|
|
503
|
+
object: "Order",
|
|
504
|
+
groupBy: ["Status"],
|
|
505
|
+
aggregations: [{ function: "count" }],
|
|
506
|
+
orderBy: { Status: { order: "DESC" } },
|
|
507
|
+
},
|
|
508
|
+
noopPrimeDeps(),
|
|
509
|
+
);
|
|
510
|
+
expect(out.query).toMatch(/orderBy\s*:\s*\{\s*Status\s*:\s*\{\s*order\s*:\s*DESC\s*\}\s*\}/s);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("orderBy array collapsed to first element with warning", async () => {
|
|
514
|
+
const out = await buildAggregate(
|
|
515
|
+
{
|
|
516
|
+
org: ORG,
|
|
517
|
+
object: "Order",
|
|
518
|
+
groupBy: ["Status"],
|
|
519
|
+
aggregations: [{ function: "count" }],
|
|
520
|
+
orderBy: [{ Status: { order: "ASC" } }, { Amount: { order: "DESC" } }],
|
|
521
|
+
},
|
|
522
|
+
noopPrimeDeps(),
|
|
523
|
+
);
|
|
524
|
+
expect(out.query).toMatch(/orderBy\s*:\s*\{\s*Status\s*:\s*\{\s*order\s*:\s*ASC\s*\}\s*\}/s);
|
|
525
|
+
expect(out.query).not.toMatch(/Amount.*order/s);
|
|
526
|
+
expect(out.warnings.some((w) => w.includes("array collapsed to first element"))).toBe(true);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("single-element orderBy array does not warn", async () => {
|
|
530
|
+
const out = await buildAggregate(
|
|
531
|
+
{
|
|
532
|
+
org: ORG,
|
|
533
|
+
object: "Order",
|
|
534
|
+
groupBy: ["Status"],
|
|
535
|
+
aggregations: [{ function: "count" }],
|
|
536
|
+
orderBy: [{ Status: { order: "DESC" } }],
|
|
537
|
+
},
|
|
538
|
+
noopPrimeDeps(),
|
|
539
|
+
);
|
|
540
|
+
expect(out.query).toMatch(/orderBy/);
|
|
541
|
+
expect(out.warnings.some((w) => w.includes("array collapsed"))).toBe(false);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("first emits on the aggregate connection", async () => {
|
|
545
|
+
const out = await buildAggregate(
|
|
546
|
+
{
|
|
547
|
+
org: ORG,
|
|
548
|
+
object: "Account",
|
|
549
|
+
groupBy: ["Industry"],
|
|
550
|
+
aggregations: [{ function: "count" }],
|
|
551
|
+
first: 5,
|
|
552
|
+
},
|
|
553
|
+
noopPrimeDeps(),
|
|
554
|
+
);
|
|
555
|
+
expect(out.query).toMatch(/first\s*:\s*5/);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("pageInfo { hasNextPage endCursor } always emitted", async () => {
|
|
559
|
+
const out = await buildAggregate(
|
|
560
|
+
{
|
|
561
|
+
org: ORG,
|
|
562
|
+
object: "Account",
|
|
563
|
+
groupBy: ["Industry"],
|
|
564
|
+
aggregations: [{ function: "count" }],
|
|
565
|
+
},
|
|
566
|
+
noopPrimeDeps(),
|
|
567
|
+
);
|
|
568
|
+
expect(out.query).toMatch(/pageInfo\s*\{\s*hasNextPage\s*\n?\s*endCursor\s*\}/s);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("$after variable always declared", async () => {
|
|
572
|
+
const out = await buildAggregate(
|
|
573
|
+
{
|
|
574
|
+
org: ORG,
|
|
575
|
+
object: "Account",
|
|
576
|
+
aggregations: [{ function: "count" }],
|
|
577
|
+
},
|
|
578
|
+
noopPrimeDeps(),
|
|
579
|
+
);
|
|
580
|
+
expect(out.query).toMatch(/\$after\s*:\s*String/);
|
|
581
|
+
expect(out.query).toMatch(/after\s*:\s*\$after/);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("top-N composition: first + orderBy + groupBy", async () => {
|
|
585
|
+
const out = await buildAggregate(
|
|
586
|
+
{
|
|
587
|
+
org: ORG,
|
|
588
|
+
object: "Account",
|
|
589
|
+
groupBy: ["Industry"],
|
|
590
|
+
aggregations: [{ function: "count" }],
|
|
591
|
+
orderBy: { Name: { order: "DESC" } },
|
|
592
|
+
first: 5,
|
|
593
|
+
},
|
|
594
|
+
noopPrimeDeps(),
|
|
595
|
+
);
|
|
596
|
+
expect(out.query).toMatch(/first\s*:\s*5/);
|
|
597
|
+
expect(out.query).toMatch(/orderBy\s*:\s*\{/s);
|
|
598
|
+
expect(out.query).toMatch(/groupBy\s*:\s*\{/s);
|
|
599
|
+
expect(out.query).toMatch(/pageInfo\s*\{/s);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("aggregate orderBy with function (AggregateOrderByStringClause)", async () => {
|
|
603
|
+
const out = await buildAggregate(
|
|
604
|
+
{
|
|
605
|
+
org: ORG,
|
|
606
|
+
object: "Account",
|
|
607
|
+
groupBy: ["Industry"],
|
|
608
|
+
aggregations: [{ function: "count" }],
|
|
609
|
+
orderBy: { Description: { function: "COUNT", order: "DESC" } },
|
|
610
|
+
},
|
|
611
|
+
noopPrimeDeps(),
|
|
612
|
+
);
|
|
613
|
+
expect(out.query).toMatch(
|
|
614
|
+
/orderBy\s*:\s*\{\s*Description\s*:\s*\{\s*function\s*:\s*COUNT\s*,\s*order\s*:\s*DESC\s*\}\s*\}/s,
|
|
615
|
+
);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("aggregate orderBy with number function (AggregateOrderByNumberClause)", async () => {
|
|
619
|
+
const out = await buildAggregate(
|
|
620
|
+
{
|
|
621
|
+
org: ORG,
|
|
622
|
+
object: "Order",
|
|
623
|
+
groupBy: ["Status"],
|
|
624
|
+
aggregations: [{ function: "sum", field: "Amount" }],
|
|
625
|
+
orderBy: { Amount: { function: "SUM", order: "DESC" } },
|
|
626
|
+
},
|
|
627
|
+
noopPrimeDeps(),
|
|
628
|
+
);
|
|
629
|
+
expect(out.query).toMatch(
|
|
630
|
+
/orderBy\s*:\s*\{\s*Amount\s*:\s*\{\s*function\s*:\s*SUM\s*,\s*order\s*:\s*DESC\s*\}\s*\}/s,
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("top-N with date-function groupBy + aggregate orderBy", async () => {
|
|
635
|
+
const out = await buildAggregate(
|
|
636
|
+
{
|
|
637
|
+
org: ORG,
|
|
638
|
+
object: "Order",
|
|
639
|
+
groupBy: [{ field: "CreatedDate", function: "CALENDAR_MONTH" }],
|
|
640
|
+
aggregations: [{ function: "sum", field: "Amount" }],
|
|
641
|
+
orderBy: { Amount: { function: "SUM", order: "DESC" } },
|
|
642
|
+
first: 5,
|
|
643
|
+
},
|
|
644
|
+
noopPrimeDeps(),
|
|
645
|
+
);
|
|
646
|
+
expect(out.query).toMatch(/first\s*:\s*5/);
|
|
647
|
+
expect(out.query).toMatch(/CreatedDate\s*:\s*\{\s*function\s*:\s*CALENDAR_MONTH\s*\}/s);
|
|
648
|
+
expect(out.query).toMatch(/Amount\s*:\s*\{\s*function\s*:\s*SUM\s*,\s*order\s*:\s*DESC\s*\}/s);
|
|
649
|
+
expect(out.query).toMatch(/pageInfo\s*\{/s);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("multi-field groupBy with aggregate orderBy", async () => {
|
|
653
|
+
const out = await buildAggregate(
|
|
654
|
+
{
|
|
655
|
+
org: ORG,
|
|
656
|
+
object: "Account",
|
|
657
|
+
groupBy: ["Industry", "Name"],
|
|
658
|
+
aggregations: [{ function: "count" }],
|
|
659
|
+
orderBy: { Description: { function: "COUNT", order: "DESC" } },
|
|
660
|
+
},
|
|
661
|
+
noopPrimeDeps(),
|
|
662
|
+
);
|
|
663
|
+
expect(out.query).toMatch(/Industry\s*:\s*\{\s*group\s*:\s*true\s*\}/s);
|
|
664
|
+
expect(out.query).toMatch(/Name\s*:\s*\{\s*group\s*:\s*true\s*\}/s);
|
|
665
|
+
expect(out.query).toMatch(
|
|
666
|
+
/Description\s*:\s*\{\s*function\s*:\s*COUNT\s*,\s*order\s*:\s*DESC\s*\}/s,
|
|
667
|
+
);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("no aggregations → empty aggregate selection (no error)", async () => {
|
|
671
|
+
const out = await buildAggregate(
|
|
672
|
+
{
|
|
673
|
+
org: ORG,
|
|
674
|
+
object: "Account",
|
|
675
|
+
groupBy: ["Industry"],
|
|
676
|
+
},
|
|
677
|
+
noopPrimeDeps(),
|
|
678
|
+
);
|
|
679
|
+
// groupBy still applies; no count entries
|
|
680
|
+
expect(out.query).toMatch(/Industry\s*:\s*\{\s*group\s*:\s*true\s*\}/s);
|
|
681
|
+
expect(out.query).not.toMatch(/count\s*\{\s*value/);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("no aggregations + no groupBy → defaults to count over Id (FR-8.2)", async () => {
|
|
685
|
+
const out = await buildAggregate(
|
|
686
|
+
{
|
|
687
|
+
org: ORG,
|
|
688
|
+
object: "Account",
|
|
689
|
+
},
|
|
690
|
+
noopPrimeDeps(),
|
|
691
|
+
);
|
|
692
|
+
expect(out.query).toMatch(
|
|
693
|
+
/aggregate\s*\{[^}]*countId\s*:\s*Id\s*\{\s*count\s*\{\s*value\s*\}\s*\}/s,
|
|
694
|
+
);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("no aggregations + empty groupBy array → defaults to count over Id (FR-8.2)", async () => {
|
|
698
|
+
const out = await buildAggregate(
|
|
699
|
+
{
|
|
700
|
+
org: ORG,
|
|
701
|
+
object: "Account",
|
|
702
|
+
groupBy: [],
|
|
703
|
+
aggregations: [],
|
|
704
|
+
},
|
|
705
|
+
noopPrimeDeps(),
|
|
706
|
+
);
|
|
707
|
+
expect(out.query).toMatch(
|
|
708
|
+
/aggregate\s*\{[^}]*countId\s*:\s*Id\s*\{\s*count\s*\{\s*value\s*\}\s*\}/s,
|
|
709
|
+
);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("alias colliding with a groupBy field throws (FR-8.4 cross-source)", async () => {
|
|
713
|
+
await expect(
|
|
714
|
+
buildAggregate(
|
|
715
|
+
{
|
|
716
|
+
org: ORG,
|
|
717
|
+
object: "Account",
|
|
718
|
+
groupBy: ["Industry"],
|
|
719
|
+
aggregations: [{ function: "count", alias: "Industry" }],
|
|
720
|
+
},
|
|
721
|
+
noopPrimeDeps(),
|
|
722
|
+
),
|
|
723
|
+
).rejects.toThrow(/duplicate aggregation key 'Industry'/);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it.each(["", "1foo", "}}__schema{types{name", "foo-bar", "foo bar"] as const)(
|
|
727
|
+
"alias '%s' is rejected as not a valid GraphQL Name",
|
|
728
|
+
async (alias) => {
|
|
729
|
+
await expect(
|
|
730
|
+
buildAggregate(
|
|
731
|
+
{
|
|
732
|
+
org: ORG,
|
|
733
|
+
object: "Order",
|
|
734
|
+
groupBy: [],
|
|
735
|
+
aggregations: [{ function: "sum", field: "Amount", alias }],
|
|
736
|
+
},
|
|
737
|
+
noopPrimeDeps(),
|
|
738
|
+
),
|
|
739
|
+
).rejects.toThrow(/not a valid GraphQL Name/);
|
|
740
|
+
},
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
it.each([" Industry", "Industry ", "", "1Industry"] as const)(
|
|
744
|
+
"groupBy entry '%s' is rejected as not a valid GraphQL Name",
|
|
745
|
+
async (field) => {
|
|
746
|
+
await expect(
|
|
747
|
+
buildAggregate(
|
|
748
|
+
{
|
|
749
|
+
org: ORG,
|
|
750
|
+
object: "Account",
|
|
751
|
+
groupBy: [field],
|
|
752
|
+
aggregations: [{ function: "count" }],
|
|
753
|
+
},
|
|
754
|
+
noopPrimeDeps(),
|
|
755
|
+
),
|
|
756
|
+
).rejects.toThrow(/not a valid GraphQL Name/);
|
|
757
|
+
},
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
it("rejects an operationName that is not a valid GraphQL Name", async () => {
|
|
761
|
+
await expect(
|
|
762
|
+
buildAggregate(
|
|
763
|
+
{
|
|
764
|
+
org: ORG,
|
|
765
|
+
object: "Account",
|
|
766
|
+
groupBy: [],
|
|
767
|
+
aggregations: [{ function: "count" }],
|
|
768
|
+
operationName: "has spaces",
|
|
769
|
+
},
|
|
770
|
+
noopPrimeDeps(),
|
|
771
|
+
),
|
|
772
|
+
).rejects.toThrow(/buildAggregate: operationName 'has spaces' is not a valid GraphQL Name/);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("rejects an object that is not a valid GraphQL Name", async () => {
|
|
776
|
+
await expect(
|
|
777
|
+
buildAggregate(
|
|
778
|
+
{ org: ORG, object: "Order Item", groupBy: [], aggregations: [{ function: "count" }] },
|
|
779
|
+
noopPrimeDeps(),
|
|
780
|
+
),
|
|
781
|
+
).rejects.toThrow(/buildAggregate: object 'Order Item' is not a valid GraphQL Name/);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("malformed $-prefix in filter surfaces a Variable: warning (no silent failure)", async () => {
|
|
785
|
+
const out = await buildAggregate(
|
|
786
|
+
{
|
|
787
|
+
org: ORG,
|
|
788
|
+
object: "Account",
|
|
789
|
+
groupBy: [],
|
|
790
|
+
aggregations: [{ function: "count" }],
|
|
791
|
+
filter: { Industry: { eq: "$1var" } },
|
|
792
|
+
},
|
|
793
|
+
noopPrimeDeps(),
|
|
794
|
+
);
|
|
795
|
+
expect(out.warnings.some((w) => w.startsWith("Variable:") && w.includes("$1var"))).toBe(true);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// FR-10.2 + FR-10.5: picklist `min`/`max` must emit the picklist literal
|
|
799
|
+
// union under each aggregator wrapper, not `string | null`. Documented as
|
|
800
|
+
// e2e Gap 4 — `tryEnrichPicklist` previously bailed when it found
|
|
801
|
+
// `min`/`max` children on a `PicklistAggregate` and codegen fell back to
|
|
802
|
+
// the default machinery (string).
|
|
803
|
+
//
|
|
804
|
+
// This test seeds ObjectInfo for `Account.Industry` directly via
|
|
805
|
+
// `setCachedObjectInfo` because the MCP intent layer does not yet pre-warm
|
|
806
|
+
// ObjectInfo on its own (W-22694063, follow-up PR). With prewarming in
|
|
807
|
+
// place this test would assert the same expectation without the manual
|
|
808
|
+
// seed.
|
|
809
|
+
describe("FR-10.2 picklist literal unions on aggregate min/max", () => {
|
|
810
|
+
const ACCOUNT_INDUSTRY_VALUES = ["Banking", "Technology", "Energy"];
|
|
811
|
+
|
|
812
|
+
function seedAccountIndustry(): void {
|
|
813
|
+
const info: ObjectInfoResult = {
|
|
814
|
+
apiName: "Account",
|
|
815
|
+
label: "Account",
|
|
816
|
+
labelPlural: "Accounts",
|
|
817
|
+
createable: true,
|
|
818
|
+
deletable: true,
|
|
819
|
+
updateable: true,
|
|
820
|
+
queryable: true,
|
|
821
|
+
searchable: true,
|
|
822
|
+
custom: false,
|
|
823
|
+
keyPrefix: "001",
|
|
824
|
+
nameFields: ["Name"],
|
|
825
|
+
defaultRecordTypeId: null,
|
|
826
|
+
fields: [],
|
|
827
|
+
childRelationships: [],
|
|
828
|
+
recordTypeInfos: [],
|
|
829
|
+
picklists: [
|
|
830
|
+
{
|
|
831
|
+
apiName: "Industry",
|
|
832
|
+
label: "Industry",
|
|
833
|
+
required: false,
|
|
834
|
+
values: ACCOUNT_INDUSTRY_VALUES.map((v) => ({ value: v, label: v })),
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
fetchedAt: new Date().toISOString(),
|
|
838
|
+
};
|
|
839
|
+
setCachedObjectInfo(ORG, "Account", info);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Aggregate aliases wrap the function-named child, which itself wraps the
|
|
843
|
+
// `value`. So picklist min/max emits, e.g.
|
|
844
|
+
// minIndustry: { min: { value: AccountIndustry | null } | null } | null;
|
|
845
|
+
// per FR-10.5's "underlying field's wrapper type" combined with FR-10.2's
|
|
846
|
+
// picklist literal union mandate.
|
|
847
|
+
it("min/max on Industry emit `{ <fn>: { value: AccountIndustry | null } | null } | null`", async () => {
|
|
848
|
+
clearObjectInfoCache(ORG);
|
|
849
|
+
seedAccountIndustry();
|
|
850
|
+
try {
|
|
851
|
+
const out = await buildAggregate(
|
|
852
|
+
{
|
|
853
|
+
org: ORG,
|
|
854
|
+
object: "Account",
|
|
855
|
+
groupBy: [],
|
|
856
|
+
aggregations: [
|
|
857
|
+
{ function: "min", field: "Industry" },
|
|
858
|
+
{ function: "max", field: "Industry" },
|
|
859
|
+
],
|
|
860
|
+
},
|
|
861
|
+
noopPrimeDeps(),
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
expect(out.types).toMatch(
|
|
865
|
+
/AccountIndustry\s*=\s*"Banking"\s*\|\s*"Technology"\s*\|\s*"Energy"/,
|
|
866
|
+
);
|
|
867
|
+
expect(out.types).toMatch(
|
|
868
|
+
/minIndustry\s*:\s*\{\s*min\s*:\s*\{\s*value\s*:\s*AccountIndustry\s*\|\s*null\s*\}\s*\|\s*null;\s*\}\s*\|\s*null;/,
|
|
869
|
+
);
|
|
870
|
+
expect(out.types).toMatch(
|
|
871
|
+
/maxIndustry\s*:\s*\{\s*max\s*:\s*\{\s*value\s*:\s*AccountIndustry\s*\|\s*null\s*\}\s*\|\s*null;\s*\}\s*\|\s*null;/,
|
|
872
|
+
);
|
|
873
|
+
expect(out.types).not.toMatch(/min\s*:\s*\{\s*value\s*:\s*string\s*\|\s*null/);
|
|
874
|
+
} finally {
|
|
875
|
+
clearObjectInfoCache(ORG);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it("count on a picklist-typed field stays number-shaped", async () => {
|
|
880
|
+
clearObjectInfoCache(ORG);
|
|
881
|
+
seedAccountIndustry();
|
|
882
|
+
try {
|
|
883
|
+
const out = await buildAggregate(
|
|
884
|
+
{
|
|
885
|
+
org: ORG,
|
|
886
|
+
object: "Account",
|
|
887
|
+
groupBy: [],
|
|
888
|
+
aggregations: [{ function: "count", field: "Industry" }],
|
|
889
|
+
},
|
|
890
|
+
noopPrimeDeps(),
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
expect(out.types).toMatch(
|
|
894
|
+
/countIndustry\s*:\s*\{\s*count\s*:\s*\{\s*value\s*:\s*number\s*\|\s*null\s*\}\s*\|\s*null;\s*\}\s*\|\s*null;/,
|
|
895
|
+
);
|
|
896
|
+
} finally {
|
|
897
|
+
clearObjectInfoCache(ORG);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it("ObjectInfo missing → falls back to string (no throw, no union)", async () => {
|
|
902
|
+
clearObjectInfoCache(ORG);
|
|
903
|
+
const out = await buildAggregate(
|
|
904
|
+
{
|
|
905
|
+
org: ORG,
|
|
906
|
+
object: "Account",
|
|
907
|
+
groupBy: [],
|
|
908
|
+
aggregations: [{ function: "min", field: "Industry" }],
|
|
909
|
+
},
|
|
910
|
+
noopPrimeDeps(),
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
expect(out.types).not.toMatch(/AccountIndustry\s*=/);
|
|
914
|
+
// Degraded shape — value typed as string under the wrapper, not the
|
|
915
|
+
// picklist literal union. Confirms the fix is dormant without
|
|
916
|
+
// ObjectInfo and that there's no throw/regression in the absence of
|
|
917
|
+
// the planned prewarm helper (W-22694063).
|
|
918
|
+
expect(out.types).toMatch(/min\s*:\s*\{\s*value\s*:\s*string\s*\|\s*null/);
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it("threads instanceUrl as 3rd arg to createSession", async () => {
|
|
923
|
+
const spy = vi.mocked(sessionModule.createSession);
|
|
924
|
+
spy.mockClear();
|
|
925
|
+
await buildAggregate(
|
|
926
|
+
{ org: ORG, object: "Account", groupBy: [], aggregations: [{ function: "count" }] },
|
|
927
|
+
noopPrimeDeps(),
|
|
928
|
+
);
|
|
929
|
+
expect(spy).toHaveBeenCalledWith(ORG, "aggregate", ORG_URL);
|
|
930
|
+
});
|
|
931
|
+
});
|