forked from microsoft/typespec
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement
@operationFields
decorator
The `@operationFields` decorator is used to specify one or more operations that should be placed onto a GraphQL type as fields with arguments. This is our solution for representing [GraphQL field arguments](https://spec.graphql.org/October2021/#sec-Field-Arguments) in TypeSpec, as TypeSpec does not support arguments on model properties.
- Loading branch information
Showing
7 changed files
with
387 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
import "./operation-fields.tsp"; | ||
import "./schema.tsp"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import "../dist/src/lib/operation-fields.js"; | ||
|
||
using TypeSpec.Reflection; | ||
|
||
namespace TypeSpec.GraphQL; | ||
|
||
alias OperationOrInterface = Operation | Interface; | ||
|
||
/** | ||
* Assign one or more operations or interfaces to act as fields with arguments on a model. | ||
* | ||
* @example | ||
* | ||
* ```typespec | ||
* op followers(query: string): Person[]; | ||
* | ||
* @operationFields(followers) | ||
* model Person {} | ||
*/ | ||
extern dec operationFields(target: Model, ...operations: OperationOrInterface[]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { | ||
walkPropertiesInherited, | ||
type DecoratorContext, | ||
type DecoratorFunction, | ||
type Interface, | ||
type Model, | ||
type Operation, | ||
type Program, | ||
} from "@typespec/compiler"; | ||
|
||
// import { createTypeRelationChecker } from "../../../compiler/dist/src/core/type-relation-checker.js"; | ||
|
||
import { GraphQLKeys, NAMESPACE, reportDiagnostic } from "../lib.js"; | ||
import { useStateMap } from "./state-map.js"; | ||
import { operationsEqual } from "./utils.js"; | ||
|
||
// This will set the namespace for decorators implemented in this file | ||
export const namespace = NAMESPACE; | ||
|
||
const [getOperationFieldsInternal, setOperationFields, _getOperationFieldsMap] = useStateMap< | ||
Model, | ||
Set<Operation> | ||
>(GraphQLKeys.operationFields); | ||
|
||
/** | ||
* Get the operation fields for a given model | ||
* @param program Program | ||
* @param model Model | ||
* @returns Set of operations defined for the model | ||
*/ | ||
export function getOperationFields(program: Program, model: Model): Set<Operation> { | ||
return getOperationFieldsInternal(program, model) || new Set<Operation>(); | ||
} | ||
|
||
function validateDuplicateProperties( | ||
context: DecoratorContext, | ||
model: Model, | ||
operation: Operation, | ||
) { | ||
const operationFields = getOperationFields(context.program, model); | ||
if (operationFields.has(operation)) { | ||
reportDiagnostic(context.program, { | ||
code: "operation-field-duplicate", | ||
format: { operation: operation.name, model: model.name }, | ||
target: context.getArgumentTarget(0)!, | ||
}); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
function validateNoConflictWithProperties( | ||
context: DecoratorContext, | ||
model: Model, | ||
operation: Operation, | ||
) { | ||
const conflictTypes = []; | ||
if ([...walkPropertiesInherited(model)].some((prop) => prop.name === operation.name)) { | ||
conflictTypes.push("property"); // an operation and a property is always a conflict | ||
} | ||
const existingOperation = [...getOperationFields(context.program, model)].find( | ||
(op) => op.name === operation.name, | ||
); | ||
|
||
if (existingOperation && !operationsEqual(existingOperation, operation)) { | ||
conflictTypes.push("operation"); | ||
} | ||
for (const conflictType of conflictTypes) { | ||
reportDiagnostic(context.program, { | ||
code: "operation-field-conflict", | ||
format: { operation: operation.name, model: model.name, conflictType }, | ||
target: context.getArgumentTarget(0)!, | ||
}); | ||
} | ||
return conflictTypes.length === 0; | ||
} | ||
|
||
/** | ||
* Add this operation to the model's operation fields. | ||
* @param context DecoratorContext | ||
* @param model Model | ||
* @param operation Operation | ||
*/ | ||
export function addOperationField( | ||
context: DecoratorContext, | ||
model: Model, | ||
operation: Operation, | ||
): void { | ||
const operationFields = getOperationFields(context.program, model); | ||
if (!validateDuplicateProperties(context, model, operation)) { | ||
return; | ||
} | ||
if (!validateNoConflictWithProperties(context, model, operation)) { | ||
return; | ||
} | ||
operationFields.add(operation); | ||
setOperationFields(context.program, model, operationFields); | ||
} | ||
|
||
export const $operationFields: DecoratorFunction = ( | ||
context: DecoratorContext, | ||
target: Model, | ||
...operationOrInterfaces: (Operation | Interface)[] | ||
): void => { | ||
for (const operationOrInterface of operationOrInterfaces) { | ||
if (operationOrInterface.kind === "Operation") { | ||
addOperationField(context, target, operationOrInterface); | ||
} else { | ||
for (const [_, operation] of operationOrInterface.operations) { | ||
addOperationField(context, target, operation); | ||
} | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { | ||
walkPropertiesInherited, | ||
type Model, | ||
type ModelProperty, | ||
type Operation, | ||
} from "@typespec/compiler"; | ||
|
||
export function propertiesEqual( | ||
prop1: ModelProperty, | ||
prop2: ModelProperty, | ||
ignoreNames: boolean = false, | ||
): boolean { | ||
if (!ignoreNames && prop1.name !== prop2.name) { | ||
return false; | ||
} | ||
return prop1.type === prop2.type && prop1.optional === prop2.optional; | ||
} | ||
|
||
export function modelsEqual(model1: Model, model2: Model, ignoreNames: boolean = false): boolean { | ||
if (!ignoreNames && model1.name !== model2.name) { | ||
return false; | ||
} | ||
const model1Properties = new Set(walkPropertiesInherited(model1)); | ||
const model2Properties = new Set(walkPropertiesInherited(model2)); | ||
if (model1Properties.size !== model2Properties.size) { | ||
return false; | ||
} | ||
if ( | ||
[...model1Properties].some( | ||
(prop) => ![...model2Properties].some((p) => propertiesEqual(prop, p, false)), | ||
) | ||
) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
export function operationsEqual( | ||
op1: Operation, | ||
op2: Operation, | ||
ignoreNames: boolean = false, | ||
): boolean { | ||
if (!ignoreNames && op1.name !== op2.name) { | ||
return false; | ||
} | ||
return op1.returnType === op2.returnType && modelsEqual(op1.parameters, op2.parameters, true); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import type { DecoratorImplementations } from "@typespec/compiler"; | ||
import { NAMESPACE } from "./lib.js"; | ||
import { $operationFields } from "./lib/operation-fields.js"; | ||
import { $schema } from "./lib/schema.js"; | ||
|
||
export const $decorators: DecoratorImplementations = { | ||
[NAMESPACE]: { | ||
operationFields: $operationFields, | ||
schema: $schema, | ||
}, | ||
}; |
Oops, something went wrong.