Skip to content

Commit 8b85d6f

Browse files
committed
update based upon changes from rebase
1 parent 04912df commit 8b85d6f

7 files changed

Lines changed: 110 additions & 59 deletions

File tree

packages/graphql/src/components/fields/type-analysis.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Type, type Program, type Union, type Scalar, type ModelProperty, isArrayModelType, getEncode, isUnknownType } from "@typespec/compiler";
22
import { type ModelVariants } from "../../context/index.js";
33
import { getUnionName, getNullableUnionType } from "../../lib/type-utils.js";
4-
import { getScalarMapping } from "../../lib/scalar-mappings.js";
4+
import { getScalarMapping, isGraphQLBuiltinScalar } from "../../lib/scalar-mappings.js";
55

66
/**
77
* Information about a GraphQL type after analysis
@@ -115,9 +115,19 @@ function resolveBaseTypeName(
115115

116116
// Handle scalars (intrinsics)
117117
if (type.kind === "Scalar") {
118-
// Check for scalar mappings FIRST (before builtin checks)
119-
// This handles types like bytes → Bytes, utcDateTime → UTCDateTime, etc.
120-
if (program.checker.isStdType(type)) {
118+
// Check for direct GraphQL builtins FIRST — these always map to built-in GraphQL types
119+
// regardless of any ancestor mappings in the scalar hierarchy.
120+
if (program.checker.isStdType(type, "string")) return "String";
121+
if (program.checker.isStdType(type, "int32")) return "Int";
122+
if (program.checker.isStdType(type, "float32")) return "Float";
123+
if (program.checker.isStdType(type, "float64")) return "Float";
124+
if (program.checker.isStdType(type, "boolean")) return "Boolean";
125+
126+
// Check for scalar mappings — for any std scalar that isn't a GraphQL builtin.
127+
// This handles types like int64 → Long, utcDateTime → UTCDateTime, etc.
128+
// Also handles scalars that get their mapping via the extends chain
129+
// (e.g. unixTimestamp32 → utcDateTime → "UTCDateTimeUnix").
130+
if (program.checker.isStdType(type) && !isGraphQLBuiltinScalar(type as Scalar)) {
121131
// Check for encoding-specific mapping
122132
if (targetType && (targetType.kind === "Scalar" || targetType.kind === "ModelProperty")) {
123133
const encodeData = getEncode(program, targetType as Scalar | ModelProperty);
@@ -128,20 +138,13 @@ function resolveBaseTypeName(
128138
}
129139
}
130140

131-
// Check for default mapping (without encoding)
141+
// Check for default mapping (not all mapping tables have a default)
132142
const mapping = getScalarMapping(program, type);
133143
if (mapping) {
134144
return mapping.graphqlName;
135145
}
136146
}
137147

138-
// Then check for direct GraphQL builtins
139-
if (program.checker.isStdType(type, "string")) return "String";
140-
if (program.checker.isStdType(type, "int32")) return "Int";
141-
if (program.checker.isStdType(type, "float32")) return "Float";
142-
if (program.checker.isStdType(type, "float64")) return "Float";
143-
if (program.checker.isStdType(type, "boolean")) return "Boolean";
144-
145148
// Custom scalar - use the name
146149
return type.name;
147150
}

packages/graphql/src/emitter.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { getOperationKind } from "./lib/operation-kind.js";
2323
import { type GraphQLEmitterOptions, reportDiagnostic } from "./lib.js";
2424
import { resolveTypeUsage, GraphQLTypeUsage, type TypeUsageResolver } from "./type-usage.js";
2525
import { listSchemas } from "./lib/schema.js";
26-
import { createGraphQLMutationEngine } from "./mutation-engine/index.js";
26+
import { createGraphQLMutationEngine, GraphQLTypeContext } from "./mutation-engine/index.js";
2727
import { GraphQLSchema } from "./components/graphql-schema.js";
2828
import {
2929
ScalarVariantTypes,
@@ -36,7 +36,8 @@ import {
3636
import { QueryType, MutationType, SubscriptionType } from "./components/operations/index.js";
3737
import type { ClassifiedTypes, ModelVariants, ScalarVariant } from "./context/index.js";
3838
import { getNullableUnionType } from "./lib/type-utils.js";
39-
import { getScalarMapping } from "./lib/scalar-mappings.js";
39+
import { getScalarMapping, isGraphQLBuiltinScalar } from "./lib/scalar-mappings.js";
40+
import { getSpecifiedBy } from "./lib/specified-by.js";
4041

4142
/**
4243
* Main emitter entry point for GraphQL SDL generation (component-based)
@@ -175,9 +176,10 @@ function mutateTypes(context: EmitContext<GraphQLEmitterOptions>, schema: { type
175176
processedScalars.add(graphqlName);
176177
mutatedScalars.push(mutation.mutatedType);
177178

178-
// Store specification URL if available
179-
if (mutation.specificationUrl) {
180-
scalarSpecifications.set(graphqlName, mutation.specificationUrl);
179+
// Store specification URL if available (set by the scalar mutation via state map)
180+
const specUrl = getSpecifiedBy(context.program, mutation.mutatedType);
181+
if (specUrl) {
182+
scalarSpecifications.set(graphqlName, specUrl);
181183
}
182184
}
183185
};
@@ -195,7 +197,7 @@ function mutateTypes(context: EmitContext<GraphQLEmitterOptions>, schema: { type
195197
}
196198
return;
197199
}
198-
if (target.type.kind === "Scalar" && context.program.checker.isStdType(target.type)) {
200+
if (target.type.kind === "Scalar" && context.program.checker.isStdType(target.type) && !isGraphQLBuiltinScalar(target.type)) {
199201
const encodeData = getEncode(context.program, target);
200202
const encoding = encodeData?.encoding;
201203
const mapping = getScalarMapping(context.program, target.type, encoding);
@@ -212,7 +214,9 @@ function mutateTypes(context: EmitContext<GraphQLEmitterOptions>, schema: { type
212214

213215
navigateTypesInNamespace(schema.type, {
214216
model: (node: Model) => {
215-
const mutation = engine.mutateModel(node);
217+
// Skip array models — they're represented as GraphQL list types, not object types
218+
if (isArrayModelType(context.program, node)) return;
219+
const mutation = engine.mutateModel(node, GraphQLTypeContext.Output);
216220
mutatedModels.push(mutation.mutatedType);
217221
originalToMutated.set(node, mutation.mutatedType);
218222
},

packages/graphql/src/lib/scalar-mappings.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ type MappedScalarName = keyof typeof SCALAR_MAPPINGS;
147147
/**
148148
* Check if a scalar name is a key in the SCALAR_MAPPINGS table.
149149
*/
150-
function isMappedScalarName(name: string): name is MappedScalarName {
150+
export function isMappedScalarName(name: string): name is MappedScalarName {
151151
return name in SCALAR_MAPPINGS;
152152
}
153153

@@ -160,6 +160,35 @@ export function isStdScalar(tk: Typekit, scalar: Scalar): boolean {
160160
return tk.scalar.getStdBase(scalar) === scalar;
161161
}
162162

163+
/**
164+
* The set of TypeSpec std scalar names that map directly to GraphQL built-in scalar types
165+
* (String, Boolean, Int, Float), plus intermediate abstract scalars in the TypeSpec
166+
* hierarchy that should never be renamed or emitted as custom scalar declarations.
167+
*
168+
* Intermediate scalars like `integer` and `float` are included because they appear as
169+
* array indexer keys and other internal contexts, and would otherwise inherit a mapping
170+
* from their ancestor (e.g. integer → numeric → "Numeric").
171+
*/
172+
const GRAPHQL_BUILTIN_SCALAR_NAMES = new Set([
173+
// Direct GraphQL builtins
174+
"string", "boolean", "int32", "float32", "float64",
175+
// Intermediate/abstract std scalars — not normally used as field types,
176+
// but appear in internal contexts like array indexer keys.
177+
// These inherit a mapping from `numeric` but should not be renamed.
178+
"integer", "float",
179+
]);
180+
181+
/**
182+
* Check whether a scalar is a GraphQL built-in or intermediate abstract scalar
183+
* that should never be renamed by the mutation engine or emitted as a custom
184+
* scalar declaration. This includes:
185+
* - Direct builtins: string, boolean, int32, float32, float64
186+
* - Abstract intermediates: integer, float
187+
*/
188+
export function isGraphQLBuiltinScalar(scalar: Scalar): boolean {
189+
return GRAPHQL_BUILTIN_SCALAR_NAMES.has(scalar.name);
190+
}
191+
163192
/**
164193
* Get the GraphQL custom scalar mapping for a scalar by walking its extends chain.
165194
*

packages/graphql/src/mutation-engine/mutations/scalar.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
type SimpleMutationOptions,
77
type SimpleMutations,
88
} from "@typespec/mutator-framework";
9-
import { getScalarMapping, isStdScalar } from "../../lib/scalar-mappings.js";
9+
import { getScalarMapping, isGraphQLBuiltinScalar, isStdScalar } from "../../lib/scalar-mappings.js";
1010
import { getSpecifiedBy, setSpecifiedByUrl } from "../../lib/specified-by.js";
1111
import { sanitizeNameForGraphQL } from "../../lib/type-utils.js";
1212

@@ -28,7 +28,13 @@ export class GraphQLScalarMutation extends SimpleScalarMutation<SimpleMutationOp
2828
const mapping = getScalarMapping(program, this.sourceType);
2929
const isDirectStd = isStdScalar(tk, this.sourceType);
3030

31-
if (mapping && isDirectStd) {
31+
// Skip GraphQL builtins (string, boolean, int32, float32, float64) entirely.
32+
// These map to built-in GraphQL types and must never be renamed, even though
33+
// they may inherit a mapping from an ancestor via the extends chain
34+
// (e.g. float32 → float → numeric → "Numeric").
35+
const isBuiltin = isGraphQLBuiltinScalar(this.sourceType);
36+
37+
if (mapping && isDirectStd && !isBuiltin) {
3238
// Std library scalar that maps to a custom GraphQL scalar (e.g. int64 → Long)
3339
this.mutationNode.mutate((scalar) => {
3440
scalar.name = mapping.graphqlName;

packages/graphql/test/enums.test.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe("enums", () => {
4242
});
4343

4444
describe("numeric values", () => {
45-
it("converts floating point enum values to GraphQL enum names", async () => {
45+
it("uses member names (not values) for enum members with numeric values", async () => {
4646
const code = `
4747
@schema
4848
namespace TestNamespace {
@@ -66,13 +66,14 @@ describe("enums", () => {
6666
const result = await emitSingleSchema(code, {});
6767

6868
strictEqual(result.includes("enum Hour {"), true);
69-
strictEqual(result.includes("_0"), true);
70-
strictEqual(result.includes("_0_25"), true);
71-
strictEqual(result.includes("_0_5"), true);
72-
strictEqual(result.includes("_0_75"), true);
69+
// Enum members use their names, not their numeric values
70+
strictEqual(result.includes("Nothing"), true);
71+
strictEqual(result.includes("HalfofHalf"), true);
72+
strictEqual(result.includes("SweetSpot"), true);
73+
strictEqual(result.includes("AlmostFull"), true);
7374
});
7475

75-
it("converts negative enum values to GraphQL enum names", async () => {
76+
it("uses member names (not values) for enum members with negative values", async () => {
7677
const code = `
7778
@schema
7879
namespace TestNamespace {
@@ -94,9 +95,10 @@ describe("enums", () => {
9495
const result = await emitSingleSchema(code, {});
9596

9697
strictEqual(result.includes("enum Boundary {"), true);
97-
strictEqual(result.includes("_0"), true);
98-
strictEqual(result.includes("_NEGATIVE_1"), true);
99-
strictEqual(result.includes("_1"), true);
98+
// Enum members use their names, not their numeric values
99+
strictEqual(result.includes("zero"), true);
100+
strictEqual(result.includes("negOne"), true);
101+
strictEqual(result.includes("one"), true);
100102
});
101103
});
102104
});

packages/graphql/test/main.tsp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ namespace TestSchema {
1818
// ===================================================================
1919
// 2. Built-in Scalar Mappings
2020
// TypeSpec std-lib scalars map to GraphQL custom scalars automatically:
21-
// int64 → BigInt, numeric → Numeric, decimal/decimal128 → BigDecimal,
21+
// int64 → Long, numeric → Numeric, decimal/decimal128 → BigDecimal,
2222
// url → URL (with @specifiedBy), plainDate → PlainDate,
2323
// plainTime → PlainTime
2424
// ===================================================================
2525

2626
model ScalarShowcase {
27-
/** Maps to GraphQL BigInt custom scalar */
27+
/** Maps to GraphQL Long custom scalar */
2828
bigNumber: int64;
2929
/** Maps to GraphQL Numeric custom scalar */
3030
preciseValue: numeric;

0 commit comments

Comments
 (0)