Skip to content

Commit e9bc4a4

Browse files
Add support for JSDoc descriptions from object types (#3)
1 parent 480c327 commit e9bc4a4

File tree

13 files changed

+401
-30
lines changed

13 files changed

+401
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ This changelog documents the changes between release versions.
44
## main
55
Changes to be included in the next upcoming release
66

7+
- Add support for JSDoc descriptions from object types ([#3](https://github.com/hasura/ndc-nodejs-lambda/pull/3))
8+
79
## v0.11.0
810
- Add support for parallel execution of readonly functions ([#2](https://github.com/hasura/ndc-nodejs-lambda/pull/2))
911

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,42 @@ export async function test(statusCode: number): Promise<string> {
166166

167167
Non-readonly functions are not invoked in parallel within the same mutation request to the connector, so it is invalid to use the @paralleldegree JSDoc tag on those functions.
168168

169+
### Documentation
170+
*Note: this feature is still in development.*
171+
172+
JSDoc comments on your functions and types are used to provide descriptions for types exposed in your GraphQL schema. For example:
173+
174+
```typescript
175+
/** Different types of greetings */
176+
interface Greeting {
177+
/** A greeting if you want to be polite */
178+
polite: string
179+
/** A casual-toned greeting */
180+
casual: string
181+
}
182+
183+
/**
184+
* Creates a greeting string using the specified name
185+
*
186+
* @param title The person's title, for example, Mr or Mrs
187+
* @param firstName The person's first name
188+
* @param lastName The person's last name (surname)
189+
* @readonly
190+
*/
191+
export function greet(title: string, firstName: string, lastName: string): Greeting {
192+
return {
193+
polite: `Hello ${name.title} ${name.lastName}`,
194+
casual: `G'day ${name.firstName}`
195+
}
196+
}
197+
```
198+
199+
Descriptions are collected for:
200+
* Functions
201+
* Function parameters
202+
* Types
203+
* Type properties
204+
169205
## Deploying with `hasura3 connector create`
170206

171207
You will need:

ndc-lambda-sdk/src/inference.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ function deriveFunctionSchema(functionDeclaration: ts.FunctionDeclaration, expor
166166
const functionSymbol = context.typeChecker.getSymbolAtLocation(functionIdentifier) ?? throwError(`Function '${exportedFunctionName}' didn't have a symbol`);
167167
const functionType = context.typeChecker.getTypeOfSymbolAtLocation(functionSymbol, functionDeclaration);
168168

169-
const functionDescription = ts.displayPartsToString(functionSymbol.getDocumentationComment(context.typeChecker)).trim();
169+
const functionDescription = getDescriptionFromJsDoc(functionSymbol, context.typeChecker);
170170
const markedReadonlyInJsDoc = functionSymbol.getJsDocTags().find(e => e.name === "readonly") !== undefined;
171171
const parallelDegreeResult = getParallelDegreeFromJsDoc(functionSymbol, markedReadonlyInJsDoc);
172172

@@ -190,14 +190,19 @@ function deriveFunctionSchema(functionDeclaration: ts.FunctionDeclaration, expor
190190

191191
return Result.collectErrors3(functionSchemaArguments, returnTypeResult, parallelDegreeResult)
192192
.map(([functionSchemaArgs, returnType, parallelDegree]) => ({
193-
description: functionDescription ? functionDescription : null,
193+
description: functionDescription,
194194
ndcKind: markedReadonlyInJsDoc ? schema.FunctionNdcKind.Function : schema.FunctionNdcKind.Procedure,
195195
arguments: functionSchemaArgs,
196196
resultType: returnType,
197197
parallelDegree,
198198
}));
199199
}
200200

201+
function getDescriptionFromJsDoc(symbol: ts.Symbol, typeChecker: ts.TypeChecker): string | null {
202+
const description = ts.displayPartsToString(symbol.getDocumentationComment(typeChecker)).trim()
203+
return description ? description : null;
204+
}
205+
201206
function getParallelDegreeFromJsDoc(functionSymbol: ts.Symbol, functionIsReadonly: boolean): Result<number | null, string[]> {
202207
const parallelDegreeTag = functionSymbol.getJsDocTags().find(e => e.name === "paralleldegree");
203208
if (parallelDegreeTag === undefined) {
@@ -422,15 +427,15 @@ function deriveSchemaTypeIfObjectType(tsType: ts.Type, typePath: TypePathSegment
422427
return new Ok({ type: 'named', name: info.generatedTypeName, kind: "object" });
423428
}
424429

425-
context.objectTypeDefinitions[info.generatedTypeName] = { properties: [] }; // Break infinite recursion
430+
context.objectTypeDefinitions[info.generatedTypeName] = { properties: [], description: null }; // Break infinite recursion
426431

427-
const propertyResults = Result.traverseAndCollectErrors(Array.from(info.members), ([propertyName, propertyType]) => {
428-
return deriveSchemaTypeForTsType(propertyType, [...typePath, { segmentType: "ObjectProperty", typeName: info.generatedTypeName, propertyName }], context, recursionDepth + 1)
429-
.map(propertyType => ({ propertyName: propertyName, type: propertyType }));
432+
const propertyResults = Result.traverseAndCollectErrors(Array.from(info.properties), ([propertyName, propertyInfo]) => {
433+
return deriveSchemaTypeForTsType(propertyInfo.tsType, [...typePath, { segmentType: "ObjectProperty", typeName: info.generatedTypeName, propertyName }], context, recursionDepth + 1)
434+
.map(propertyType => ({ propertyName: propertyName, type: propertyType, description: propertyInfo.description }));
430435
});
431436

432437
if (propertyResults instanceof Ok) {
433-
context.objectTypeDefinitions[info.generatedTypeName] = { properties: propertyResults.data }
438+
context.objectTypeDefinitions[info.generatedTypeName] = { properties: propertyResults.data, description: info.description }
434439
return new Ok({ type: 'named', name: info.generatedTypeName, kind: "object" })
435440
} else {
436441
// Remove the recursion short-circuit to ensure errors are raised if this type is encountered again
@@ -475,12 +480,19 @@ function unwrapNullableType(ty: ts.Type): [ts.Type, schema.NullOrUndefinability]
475480
: null;
476481
}
477482

483+
type PropertyTypeInfo = {
484+
tsType: ts.Type,
485+
description: string | null,
486+
}
487+
478488
type ObjectTypeInfo = {
479489
// The name of the type; it may be a generated name if it is an anonymous type, or if it from an external module
480490
generatedTypeName: string,
481-
// The member properties of the object type. The types are
491+
// The properties of the object type. The types are
482492
// concrete types after type parameter resolution
483-
members: Map<string, ts.Type>
493+
properties: Map<string, PropertyTypeInfo>,
494+
// The JSDoc comment on the type
495+
description: string | null,
484496
}
485497

486498
// TODO: This can be vastly simplified when I yeet the name qualification stuff
@@ -490,30 +502,36 @@ function getObjectTypeInfo(tsType: ts.Type, typePath: TypePathSegment[], typeChe
490502
return null;
491503
}
492504

505+
const symbolForDocs = tsType.aliasSymbol ?? tsType.getSymbol();
506+
const description = symbolForDocs ? getDescriptionFromJsDoc(symbolForDocs, typeChecker) : null;
507+
493508
// Anonymous object type - this covers:
494509
// - {a: number, b: string}
495510
// - type Bar = { test: string }
496511
// - type GenericBar<T> = { data: T }
497512
if (tsutils.isObjectType(tsType) && tsutils.isObjectFlagSet(tsType, ts.ObjectFlags.Anonymous)) {
498513
return {
499514
generatedTypeName: qualifyTypeName(tsType, typePath, tsType.aliasSymbol ? typeChecker.typeToString(tsType) : null, functionsFilePath),
500-
members: getMembers(tsType.getProperties(), typeChecker)
515+
properties: getMembers(tsType.getProperties(), typeChecker),
516+
description,
501517
}
502518
}
503519
// Interface type - this covers:
504520
// interface IThing { test: string }
505521
else if (tsutils.isObjectType(tsType) && tsutils.isObjectFlagSet(tsType, ts.ObjectFlags.Interface)) {
506522
return {
507523
generatedTypeName: tsType.getSymbol()?.name ?? generateTypeNameFromTypePath(typePath),
508-
members: getMembers(tsType.getProperties(), typeChecker)
524+
properties: getMembers(tsType.getProperties(), typeChecker),
525+
description,
509526
}
510527
}
511528
// Generic interface type - this covers:
512529
// interface IGenericThing<T> { data: T }
513530
else if (tsutils.isTypeReference(tsType) && tsutils.isObjectFlagSet(tsType.target, ts.ObjectFlags.Interface) && typeChecker.isArrayType(tsType) === false && tsType.getSymbol()?.getName() !== "Promise") {
514531
return {
515532
generatedTypeName: tsType.getSymbol()?.name ?? generateTypeNameFromTypePath(typePath),
516-
members: getMembers(tsType.getProperties(), typeChecker)
533+
properties: getMembers(tsType.getProperties(), typeChecker),
534+
description,
517535
}
518536
}
519537
// Intersection type - this covers:
@@ -523,16 +541,21 @@ function getObjectTypeInfo(tsType: ts.Type, typePath: TypePathSegment[], typeChe
523541
else if (tsutils.isIntersectionType(tsType)) {
524542
return {
525543
generatedTypeName: qualifyTypeName(tsType, typePath, tsType.aliasSymbol ? typeChecker.typeToString(tsType) : null, functionsFilePath),
526-
members: getMembers(tsType.getProperties(), typeChecker)
544+
properties: getMembers(tsType.getProperties(), typeChecker),
545+
description,
527546
}
528547
}
529548

530549
return null;
531550
}
532551

533-
function getMembers(propertySymbols: ts.Symbol[], typeChecker: ts.TypeChecker) {
552+
function getMembers(propertySymbols: ts.Symbol[], typeChecker: ts.TypeChecker): Map<string, PropertyTypeInfo> {
534553
return new Map(
535-
propertySymbols.map(symbol => [symbol.name, typeChecker.getTypeOfSymbol(symbol)])
554+
propertySymbols.map(symbol => {
555+
const tsType = typeChecker.getTypeOfSymbol(symbol);
556+
const description = getDescriptionFromJsDoc(symbol, typeChecker);
557+
return [symbol.name, {tsType, description}]
558+
})
536559
)
537560
}
538561

ndc-lambda-sdk/src/schema.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ export type ObjectTypeDefinitions = {
3535
}
3636

3737
export type ObjectTypeDefinition = {
38+
description: string | null,
3839
properties: ObjectPropertyDefinition[]
3940
}
4041

4142
export type ObjectPropertyDefinition = {
4243
propertyName: string,
44+
description: string | null,
4345
type: TypeDefinition,
4446
}
4547

@@ -193,7 +195,14 @@ export function getNdcSchema(functionsSchema: FunctionsSchema): sdk.SchemaRespon
193195

194196
const objectTypes = mapObjectValues(functionsSchema.objectTypes, objDef => {
195197
return {
196-
fields: Object.fromEntries(objDef.properties.map(propDef => [propDef.propertyName, { type: convertTypeDefinitionToSdkType(propDef.type)}]))
198+
fields: Object.fromEntries(objDef.properties.map(propDef => {
199+
const objField: sdk.ObjectField = {
200+
type: convertTypeDefinitionToSdkType(propDef.type),
201+
...(propDef.description ? { description: propDef.description } : {})
202+
}
203+
return [propDef.propertyName, objField];
204+
})),
205+
...(objDef.description ? { description: objDef.description } : {})
197206
}
198207
});
199208

ndc-lambda-sdk/test/execution/prepare-arguments.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,11 @@ describe("prepare arguments", function() {
168168
}
169169
const objectTypes: ObjectTypeDefinitions = {
170170
"MyObject": {
171+
description: null,
171172
properties: [
172173
{
173174
propertyName: "nullOnlyProp",
175+
description: null,
174176
type: {
175177
type: "nullable",
176178
nullOrUndefinability: NullOrUndefinability.AcceptsNullOnly,
@@ -183,6 +185,7 @@ describe("prepare arguments", function() {
183185
},
184186
{
185187
propertyName: "undefinedOnlyProp",
188+
description: null,
186189
type: {
187190
type: "nullable",
188191
nullOrUndefinability: NullOrUndefinability.AcceptsUndefinedOnly,
@@ -195,6 +198,7 @@ describe("prepare arguments", function() {
195198
},
196199
{
197200
propertyName: "nullOrUndefinedProp",
201+
description: null,
198202
type: {
199203
type: "nullable",
200204
nullOrUndefinability: NullOrUndefinability.AcceptsEither,

ndc-lambda-sdk/test/execution/reshape-result.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,21 @@ describe("reshape result", function() {
8989
describe("projects object types using fields", function() {
9090
const objectTypes: ObjectTypeDefinitions = {
9191
"TestObjectType": {
92+
description: null,
9293
properties: [
9394
{
9495
propertyName: "propA",
96+
description: null,
9597
type: { type: "named", kind: "scalar", name: BuiltInScalarTypeName.String }
9698
},
9799
{
98100
propertyName: "propB",
101+
description: null,
99102
type: { type: "named", kind: "scalar", name: BuiltInScalarTypeName.String }
100103
},
101104
{
102105
propertyName: "nested",
106+
description: null,
103107
type: { type: "nullable", nullOrUndefinability: NullOrUndefinability.AcceptsEither, underlyingType: { type: "named", kind: "object", name: "TestObjectType" } }
104108
}
105109
]
@@ -162,13 +166,16 @@ describe("reshape result", function() {
162166
describe("projects object types using fields through an array type", function() {
163167
const objectTypes: ObjectTypeDefinitions = {
164168
"TestObjectType": {
169+
description: null,
165170
properties: [
166171
{
167172
propertyName: "propA",
173+
description: null,
168174
type: { type: "named", kind: "scalar", name: BuiltInScalarTypeName.String }
169175
},
170176
{
171177
propertyName: "propB",
178+
description: null,
172179
type: { type: "nullable", nullOrUndefinability: NullOrUndefinability.AcceptsEither, underlyingType: { type: "named", kind: "scalar", name: BuiltInScalarTypeName.String } }
173180
}
174181
]

0 commit comments

Comments
 (0)