Skip to content

Commit c0fff8a

Browse files
committed
Add @schema decorator to mark namespaces as GraphQL schemas
Using the `TypeSpec.GraphQL.@schema` decorator on a namespace indicates that the decorated namespace represents a GraphQL schema that should be generated by the GraphQL emitter. Because this allows for multiple schemas to be specified in a TypeSpec source, our test host is reworked to provide a `GraphQLSchemaRecord` corresponding to each schema produced. This commit does not actually implement any emitter functionality, but populates a state map that will be used by the emitter in the future.
1 parent fee344a commit c0fff8a

File tree

15 files changed

+301
-21
lines changed

15 files changed

+301
-21
lines changed

packages/graphql/lib/main.tsp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "./schema.tsp";

packages/graphql/lib/schema.tsp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import "../dist/src/lib/schema.js";
2+
3+
using TypeSpec.Reflection;
4+
5+
namespace TypeSpec.GraphQL;
6+
7+
namespace Schema {
8+
model SchemaOptions {
9+
name?: string;
10+
}
11+
}
12+
13+
/**
14+
* Mark this namespace as describing a GraphQL schema and configure schema properties.
15+
*
16+
* @example
17+
*
18+
* ```typespec
19+
* @schema(name: "MySchema")
20+
* namespace MySchema {};
21+
* ```
22+
*/
23+
extern dec schema(target: Namespace, options?: Schema.SchemaOptions);

packages/graphql/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"main": "dist/src/index.js",
2121
"exports": {
2222
".": {
23+
"typespec": "./lib/main.tsp",
2324
"types": "./dist/src/index.d.ts",
2425
"default": "./dist/src/index.js"
2526
},
@@ -31,6 +32,12 @@
3132
"engines": {
3233
"node": ">=18.0.0"
3334
},
35+
"graphql": {
36+
"documents": "test/**/*.{js,ts}"
37+
},
38+
"dependencies": {
39+
"graphql": "^16.9.0"
40+
},
3441
"scripts": {
3542
"clean": "rimraf ./dist ./temp",
3643
"build": "tsc",

packages/graphql/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { $onEmit } from "./emitter.js";
22
export { $lib } from "./lib.js";
3+
export { $decorators } from "./tsp-index.js";

packages/graphql/src/lib.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { createTypeSpecLibrary, type JSONSchemaType } from "@typespec/compiler";
22

3+
export const NAMESPACE = "TypeSpec.GraphQL";
4+
35
export interface GraphQLEmitterOptions {
46
/**
57
* Name of the output file.
@@ -99,4 +101,4 @@ export const libDef = {
99101

100102
export const $lib = createTypeSpecLibrary(libDef);
101103

102-
export const { reportDiagnostic, createDiagnostic } = $lib;
104+
export const { reportDiagnostic, createDiagnostic, createStateSymbol } = $lib;

packages/graphql/src/lib/schema.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
type DecoratorContext,
3+
type DecoratorFunction,
4+
type Namespace,
5+
type Program,
6+
type Type,
7+
getTypeName,
8+
validateDecoratorUniqueOnNode,
9+
} from "@typespec/compiler";
10+
11+
import { createStateSymbol, NAMESPACE, reportDiagnostic } from "../lib.js";
12+
import { useStateMap } from "./state-map.js";
13+
14+
// This will set the namespace for decorators implemented in this file
15+
export const namespace = NAMESPACE;
16+
17+
export interface SchemaDetails {
18+
name?: string;
19+
}
20+
21+
export interface Schema extends SchemaDetails {
22+
type: Namespace;
23+
}
24+
25+
const [getSchema, setSchema, getSchemaMap] = useStateMap<Namespace, Schema>(
26+
createStateSymbol("schemas"),
27+
);
28+
29+
/**
30+
* List all the schemas defined in the TypeSpec program
31+
* @param program Program
32+
* @returns List of schemas.
33+
*/
34+
export function listSchemas(program: Program): Schema[] {
35+
return [...getSchemaMap(program).values()];
36+
}
37+
38+
export {
39+
/**
40+
* Get the schema information for the given namespace.
41+
* @param program Program
42+
* @param namespace Schema namespace
43+
* @returns Schema information or undefined if namespace is not a schema namespace.
44+
*/
45+
getSchema,
46+
};
47+
48+
/**
49+
* Check if the namespace is defined as a schema.
50+
* @param program Program
51+
* @param namespace Namespace
52+
* @returns Boolean
53+
*/
54+
export function isSchema(program: Program, namespace: Namespace): boolean {
55+
return getSchemaMap(program).has(namespace);
56+
}
57+
58+
/**
59+
* Mark the given namespace as a schema.
60+
* @param program Program
61+
* @param namespace Namespace
62+
* @param details Schema details
63+
*/
64+
export function addSchema(
65+
program: Program,
66+
namespace: Namespace,
67+
details: SchemaDetails = {},
68+
): void {
69+
const schemaMap = getSchemaMap(program);
70+
const existing = schemaMap.get(namespace) ?? {};
71+
setSchema(program, namespace, { ...existing, ...details, type: namespace });
72+
}
73+
74+
export const $schema: DecoratorFunction = (
75+
context: DecoratorContext,
76+
target: Namespace,
77+
options: Type,
78+
) => {
79+
validateDecoratorUniqueOnNode(context, target, $schema);
80+
81+
if (options && options.kind !== "Model") {
82+
reportDiagnostic(context.program, {
83+
code: "invalid-argument",
84+
format: { value: options.kind, expected: "Model" },
85+
target: context.getArgumentTarget(0)!,
86+
});
87+
return;
88+
}
89+
90+
const schemaDetails: SchemaDetails = {};
91+
const name = options?.properties.get("name")?.type;
92+
if (name) {
93+
if (name.kind === "String") {
94+
schemaDetails.name = name.value;
95+
} else {
96+
reportDiagnostic(context.program, {
97+
code: "unassignable",
98+
format: { sourceType: getTypeName(name), targetType: "String" },
99+
target: context.getArgumentTarget(0)!,
100+
});
101+
}
102+
}
103+
104+
addSchema(context.program, target, schemaDetails);
105+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Type } from "@typespec/compiler";
2+
import { unsafe_useStateMap, unsafe_useStateSet } from "@typespec/compiler/experimental";
3+
4+
/**
5+
* This is a copy of the experimental state-accessor lib from @typespec/compiler
6+
*/
7+
8+
function createStateSymbol(name: string) {
9+
return Symbol.for(`TypeSpec.${name}`);
10+
}
11+
12+
export function useStateMap<K extends Type, V>(key: string | symbol) {
13+
return unsafe_useStateMap<K, V>(typeof key === "string" ? createStateSymbol(key) : key);
14+
}
15+
16+
export function useStateSet<K extends Type>(key: string | symbol) {
17+
return unsafe_useStateSet<K>(typeof key === "string" ? createStateSymbol(key) : key);
18+
}

packages/graphql/src/schema-emitter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function createGraphQLEmitter(
1818
const filePath = interpolatePath(options.outputFile, { "schema-name": "schema" });
1919
await emitFile(program, {
2020
path: filePath,
21-
content: "Hello world",
21+
content: "",
2222
newLine: options.newLine,
2323
});
2424
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { GraphQLSchema } from "graphql";
2+
3+
export const EMPTY_SCHEMA = new GraphQLSchema({});
4+
5+
export const EMPTY_SCHEMA_OUTPUT = `#graphql
6+
type Query {
7+
"""
8+
A placeholder field. If you are seeing this, it means no operations were defined that could be emitted.
9+
"""
10+
_: Boolean
11+
}
12+
`;

packages/graphql/src/tsp-index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { DecoratorImplementations } from "@typespec/compiler";
2+
import { NAMESPACE } from "./lib.js";
3+
import { $schema } from "./lib/schema.js";
4+
5+
export const $decorators: DecoratorImplementations = {
6+
[NAMESPACE]: {
7+
schema: $schema,
8+
},
9+
};

0 commit comments

Comments
 (0)