Comparing version 0.0.0-main-b7bb4612 to 0.0.0-main-ba6da9cd
{ | ||
"name": "grats", | ||
"version": "0.0.4", | ||
"version": "0.0.5", | ||
"main": "dist/src/index.js", | ||
@@ -12,3 +12,3 @@ "bin": "dist/src/cli.js", | ||
"scripts": { | ||
"test": "ts-node --esm src/tests/test.ts", | ||
"test": "ts-node src/tests/test.ts", | ||
"integration-tests": "node src/tests/integration.mjs", | ||
@@ -38,3 +38,7 @@ "build": "tsc --build", | ||
}, | ||
"packageManager": "pnpm@8.1.1" | ||
"packageManager": "pnpm@8.1.1", | ||
"engines": { | ||
"node": ">=16 <=21", | ||
"pnpm": "^8" | ||
} | ||
} |
@@ -34,4 +34,4 @@ /** | ||
export declare function inputFieldUntyped(): string; | ||
export declare function typeTagOnUnamedClass(): string; | ||
export declare function typeTagOnAliasOfNonObject(): string; | ||
export declare function typeTagOnUnnamedClass(): string; | ||
export declare function typeTagOnAliasOfNonObjectOrUnknown(): string; | ||
export declare function typeNameNotDeclaration(): string; | ||
@@ -66,3 +66,3 @@ export declare function typeNameMissingInitializer(): string; | ||
export declare function pluralTypeMissingParameter(): string; | ||
export declare function expectedIdentifer(): string; | ||
export declare function expectedIdentifier(): string; | ||
export declare function killsParentOnExceptionWithWrongConfig(): string; | ||
@@ -83,1 +83,7 @@ export declare function killsParentOnExceptionOnNullable(): string; | ||
export declare function unresolvedTypeReference(): string; | ||
export declare function expectedTypeAnnotationOnContext(): string; | ||
export declare function expectedTypeAnnotationOfReferenceOnContext(): string; | ||
export declare function expectedTypeAnnotationOnContextToBeResolvable(): string; | ||
export declare function expectedTypeAnnotationOnContextToHaveDeclaration(): string; | ||
export declare function unexpectedParamSpreadForContextParam(): string; | ||
export declare function multipleContextTypes(): string; |
"use strict"; | ||
exports.__esModule = true; | ||
exports.defaultArgPropertyMissingName = exports.defaultArgElementIsNotAssignment = exports.defaultValueIsNotLiteral = exports.ambiguousNumberType = exports.expectedOneNonNullishType = exports.propertyFieldMissingType = exports.cannotResolveSymbolForDescription = exports.promiseMissingTypeArg = exports.methodMissingType = exports.gqlEntityMissingName = exports.enumVariantMissingInitializer = exports.enumVariantNotStringLiteral = exports.enumTagOnInvalidNode = exports.argNotTyped = exports.argNameNotLiteral = exports.argIsNotProperty = exports.argumentParamIsNotObject = exports.argumentParamIsMissingType = exports.typeNameDoesNotMatchExpected = exports.typeNameTypeNotStringLiteral = exports.typeNameMissingTypeAnnotation = exports.typeNameInitializerWrong = exports.typeNameInitializeNotString = exports.typeNameMissingInitializer = exports.typeNameNotDeclaration = exports.typeTagOnAliasOfNonObject = exports.typeTagOnUnamedClass = exports.inputFieldUntyped = exports.inputTypeFieldNotProperty = exports.inputTypeNotLiteral = exports.functionFieldNotNamedExport = exports.functionFieldDefaultExport = exports.functionFieldNotNamed = exports.functionFieldParentTypeNotValid = exports.functionFieldParentTypeMissing = exports.functionFieldNotTopLevel = exports.invalidReturnTypeForFunctionField = exports.invalidParentArgForFunctionField = exports.expectedUnionTypeReference = exports.expectedUnionTypeNode = exports.invalidUnionTagUsage = exports.invalidInputTagUsage = exports.invalidEnumTagUsage = exports.invalidInterfaceTagUsage = exports.invalidScalarTagUsage = exports.invalidTypeTagUsage = exports.invalidGratsTag = exports.wrongCasingForGratsTag = exports.killsParentOnExceptionOnWrongNode = exports.fieldTagOnWrongNode = void 0; | ||
exports.unresolvedTypeReference = exports.invalidTypePassedToFieldFunction = exports.parameterPropertyMissingType = exports.parameterPropertyNotPublic = exports.parameterWithoutModifiers = exports.duplicateInterfaceTag = exports.duplicateTag = exports.implementsTagOnTypeAlias = exports.implementsTagOnInterface = exports.implementsTagOnClass = exports.implementsTagMissingValue = exports.mergedInterfaces = exports.nonNullTypeCannotBeOptional = exports.killsParentOnExceptionOnNullable = exports.killsParentOnExceptionWithWrongConfig = exports.expectedIdentifer = exports.pluralTypeMissingParameter = exports.unknownGraphQLType = exports.unsupportedTypeLiteral = exports.defaultArgPropertyMissingInitializer = void 0; | ||
exports.defaultArgPropertyMissingName = exports.defaultArgElementIsNotAssignment = exports.defaultValueIsNotLiteral = exports.ambiguousNumberType = exports.expectedOneNonNullishType = exports.propertyFieldMissingType = exports.cannotResolveSymbolForDescription = exports.promiseMissingTypeArg = exports.methodMissingType = exports.gqlEntityMissingName = exports.enumVariantMissingInitializer = exports.enumVariantNotStringLiteral = exports.enumTagOnInvalidNode = exports.argNotTyped = exports.argNameNotLiteral = exports.argIsNotProperty = exports.argumentParamIsNotObject = exports.argumentParamIsMissingType = exports.typeNameDoesNotMatchExpected = exports.typeNameTypeNotStringLiteral = exports.typeNameMissingTypeAnnotation = exports.typeNameInitializerWrong = exports.typeNameInitializeNotString = exports.typeNameMissingInitializer = exports.typeNameNotDeclaration = exports.typeTagOnAliasOfNonObjectOrUnknown = exports.typeTagOnUnnamedClass = exports.inputFieldUntyped = exports.inputTypeFieldNotProperty = exports.inputTypeNotLiteral = exports.functionFieldNotNamedExport = exports.functionFieldDefaultExport = exports.functionFieldNotNamed = exports.functionFieldParentTypeNotValid = exports.functionFieldParentTypeMissing = exports.functionFieldNotTopLevel = exports.invalidReturnTypeForFunctionField = exports.invalidParentArgForFunctionField = exports.expectedUnionTypeReference = exports.expectedUnionTypeNode = exports.invalidUnionTagUsage = exports.invalidInputTagUsage = exports.invalidEnumTagUsage = exports.invalidInterfaceTagUsage = exports.invalidScalarTagUsage = exports.invalidTypeTagUsage = exports.invalidGratsTag = exports.wrongCasingForGratsTag = exports.killsParentOnExceptionOnWrongNode = exports.fieldTagOnWrongNode = void 0; | ||
exports.multipleContextTypes = exports.unexpectedParamSpreadForContextParam = exports.expectedTypeAnnotationOnContextToHaveDeclaration = exports.expectedTypeAnnotationOnContextToBeResolvable = exports.expectedTypeAnnotationOfReferenceOnContext = exports.expectedTypeAnnotationOnContext = exports.unresolvedTypeReference = exports.invalidTypePassedToFieldFunction = exports.parameterPropertyMissingType = exports.parameterPropertyNotPublic = exports.parameterWithoutModifiers = exports.duplicateInterfaceTag = exports.duplicateTag = exports.implementsTagOnTypeAlias = exports.implementsTagOnInterface = exports.implementsTagOnClass = exports.implementsTagMissingValue = exports.mergedInterfaces = exports.nonNullTypeCannotBeOptional = exports.killsParentOnExceptionOnNullable = exports.killsParentOnExceptionWithWrongConfig = exports.expectedIdentifier = exports.pluralTypeMissingParameter = exports.unknownGraphQLType = exports.unsupportedTypeLiteral = exports.defaultArgPropertyMissingInitializer = void 0; | ||
var Extractor_1 = require("./Extractor"); | ||
@@ -115,10 +115,10 @@ // TODO: Move these to short URLS that are easier to keep from breaking. | ||
exports.inputFieldUntyped = inputFieldUntyped; | ||
function typeTagOnUnamedClass() { | ||
function typeTagOnUnnamedClass() { | ||
return "Unexpected `@".concat(Extractor_1.TYPE_TAG, "` annotation on unnamed class declaration."); | ||
} | ||
exports.typeTagOnUnamedClass = typeTagOnUnamedClass; | ||
function typeTagOnAliasOfNonObject() { | ||
return "Expected `@".concat(Extractor_1.TYPE_TAG, "` type to be a type literal. For example: `type Foo = { bar: string }`"); | ||
exports.typeTagOnUnnamedClass = typeTagOnUnnamedClass; | ||
function typeTagOnAliasOfNonObjectOrUnknown() { | ||
return "Expected `@".concat(Extractor_1.TYPE_TAG, "` type to be a type literal or `unknown`. For example: `type Foo = { bar: string }` or `type Query = unknown`."); | ||
} | ||
exports.typeTagOnAliasOfNonObject = typeTagOnAliasOfNonObject; | ||
exports.typeTagOnAliasOfNonObjectOrUnknown = typeTagOnAliasOfNonObjectOrUnknown; | ||
function typeNameNotDeclaration() { | ||
@@ -153,7 +153,7 @@ return "Expected `__typename` to be a property declaration."; | ||
function argumentParamIsMissingType() { | ||
return "Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: never`."; | ||
return "Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: unknown`."; | ||
} | ||
exports.argumentParamIsMissingType = argumentParamIsMissingType; | ||
function argumentParamIsNotObject() { | ||
return "Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`."; | ||
return "Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`. If there are no arguments, you can use `args: unknown`."; | ||
} | ||
@@ -241,6 +241,6 @@ exports.argumentParamIsNotObject = argumentParamIsNotObject; | ||
exports.pluralTypeMissingParameter = pluralTypeMissingParameter; | ||
function expectedIdentifer() { | ||
function expectedIdentifier() { | ||
return "Expected an identifier."; | ||
} | ||
exports.expectedIdentifer = expectedIdentifer; | ||
exports.expectedIdentifier = expectedIdentifier; | ||
function killsParentOnExceptionWithWrongConfig() { | ||
@@ -318,1 +318,27 @@ return "Unexpected `@".concat(Extractor_1.KILLS_PARENT_ON_EXCEPTION_TAG, "` tag. `@").concat(Extractor_1.KILLS_PARENT_ON_EXCEPTION_TAG, "` is only supported when the Grats config `nullableByDefault` is enabled."); | ||
exports.unresolvedTypeReference = unresolvedTypeReference; | ||
function expectedTypeAnnotationOnContext() { | ||
return "Expected context parameter to have a type annotation. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration."; | ||
} | ||
exports.expectedTypeAnnotationOnContext = expectedTypeAnnotationOnContext; | ||
function expectedTypeAnnotationOfReferenceOnContext() { | ||
return "Expected context parameter's type to be a type reference Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration."; | ||
} | ||
exports.expectedTypeAnnotationOfReferenceOnContext = expectedTypeAnnotationOfReferenceOnContext; | ||
function expectedTypeAnnotationOnContextToBeResolvable() { | ||
// TODO: Provide guidance? | ||
// TODO: I don't think we have a test case that triggers this error. | ||
return "Unable to resolve context parameter type. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration."; | ||
} | ||
exports.expectedTypeAnnotationOnContextToBeResolvable = expectedTypeAnnotationOnContextToBeResolvable; | ||
function expectedTypeAnnotationOnContextToHaveDeclaration() { | ||
return "Unable to locate the declaration of the context parameter's type. Grats validates that your context parameter is type-safe by checking all context values reference the same type declaration. Did you forget to import or define this type?"; | ||
} | ||
exports.expectedTypeAnnotationOnContextToHaveDeclaration = expectedTypeAnnotationOnContextToHaveDeclaration; | ||
function unexpectedParamSpreadForContextParam() { | ||
return "Unexpected spread parameter in context parameter position. Grats expects the context parameter to be a single, explicitly typed, argument."; | ||
} | ||
exports.unexpectedParamSpreadForContextParam = unexpectedParamSpreadForContextParam; | ||
function multipleContextTypes() { | ||
return "Context argument's type does not match. Grats expects all resolvers that read the context argument to use the same type for that argument. Did you use the incorrect type in one of your resolvers?"; | ||
} | ||
exports.multipleContextTypes = multipleContextTypes; |
@@ -84,3 +84,3 @@ import { FieldDefinitionNode, InputValueDefinitionNode, NamedTypeNode, NameNode, TypeNode, StringValueNode, ConstValueNode, ConstDirectiveNode, EnumValueDefinitionNode, ConstObjectFieldNode, ConstObjectValueNode, ConstListValueNode } from "graphql"; | ||
collectArrayLiteral(node: ts.ArrayLiteralExpression): ConstListValueNode | null; | ||
cellectObjectLiteral(node: ts.ObjectLiteralExpression): ConstObjectValueNode | null; | ||
collectObjectLiteral(node: ts.ObjectLiteralExpression): ConstObjectValueNode | null; | ||
collectObjectField(node: ts.ObjectLiteralElementLike): ConstObjectFieldNode | null; | ||
@@ -93,2 +93,3 @@ collectArg(node: ts.TypeElement, defaults?: Map<string, ts.Expression> | null): InputValueDefinitionNode | null; | ||
entityName(node: ts.ClassDeclaration | ts.MethodDeclaration | ts.MethodSignature | ts.PropertyDeclaration | ts.InterfaceDeclaration | ts.PropertySignature | ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.FunctionDeclaration | ts.ParameterDeclaration, tag: ts.JSDocTag): NameNode | null; | ||
validateContextParameter(node: ts.ParameterDeclaration): null | undefined; | ||
methodDeclaration(node: ts.MethodDeclaration | ts.MethodSignature): FieldDefinitionNode | null; | ||
@@ -95,0 +96,0 @@ collectMethodType(node: ts.TypeNode): TypeNode | null; |
@@ -313,2 +313,6 @@ "use strict"; | ||
} | ||
var context = node.parameters[2]; | ||
if (context != null) { | ||
this.validateContextParameter(context); | ||
} | ||
var description = this.collectDescription(funcName); | ||
@@ -427,3 +431,3 @@ if (!ts.isSourceFile(node.parent)) { | ||
if (node.name == null) { | ||
return this.report(node, E.typeTagOnUnamedClass()); | ||
return this.report(node, E.typeTagOnUnnamedClass()); | ||
} | ||
@@ -455,11 +459,19 @@ var name = this.entityName(node, tag); | ||
return null; | ||
if (!ts.isTypeLiteralNode(node.type)) { | ||
this.reportUnhandled(node.type, "type", E.typeTagOnAliasOfNonObject()); | ||
return; | ||
var fields = []; | ||
var interfaces = null; | ||
if (ts.isTypeLiteralNode(node.type)) { | ||
fields = this.collectFields(node.type); | ||
interfaces = this.collectInterfaces(node); | ||
this.checkForTypenameProperty(node.type, name.value); | ||
} | ||
else if (node.type.kind === ts.SyntaxKind.UnknownKeyword) { | ||
// This is fine, we just don't know what it is. This should be the expected | ||
// case for operation types such as `Query`, `Mutation`, and `Subscription` | ||
// where there is not strong convention around. | ||
} | ||
else { | ||
return this.report(node.type, E.typeTagOnAliasOfNonObjectOrUnknown()); | ||
} | ||
var description = this.collectDescription(node.name); | ||
var fields = this.collectFields(node.type); | ||
var interfaces = this.collectInterfaces(node); | ||
this.ctx.recordTypeName(node.name, name, "TYPE"); | ||
this.checkForTypenameProperty(node.type, name.value); | ||
this.definitions.push(this.gql.objectTypeDefinition(node, name, fields, interfaces, description)); | ||
@@ -721,3 +733,3 @@ }; | ||
} | ||
if (argsType.kind === ts.SyntaxKind.NeverKeyword) { | ||
if (argsType.kind === ts.SyntaxKind.UnknownKeyword) { | ||
return []; | ||
@@ -791,3 +803,3 @@ } | ||
else if (ts.isObjectLiteralExpression(node)) { | ||
return this.cellectObjectLiteral(node); | ||
return this.collectObjectLiteral(node); | ||
} | ||
@@ -827,3 +839,3 @@ else if (ts.isArrayLiteralExpression(node)) { | ||
}; | ||
Extractor.prototype.cellectObjectLiteral = function (node) { | ||
Extractor.prototype.collectObjectLiteral = function (node) { | ||
var e_8, _a; | ||
@@ -1033,2 +1045,41 @@ var fields = []; | ||
}; | ||
// Ensure the type of the ctx param resolves to the declaration | ||
// annotated with `@gqlContext`. | ||
Extractor.prototype.validateContextParameter = function (node) { | ||
if (node.type == null) { | ||
return this.report(node, E.expectedTypeAnnotationOnContext()); | ||
} | ||
if (node.type.kind === ts.SyntaxKind.UnknownKeyword) { | ||
// If the user just needs to define the argument to get to a later parameter, | ||
// they can use `ctx: unknown` to safely avoid triggering a Grats error. | ||
return; | ||
} | ||
if (!ts.isTypeReferenceNode(node.type)) { | ||
return this.report(node.type, E.expectedTypeAnnotationOfReferenceOnContext()); | ||
} | ||
// Check for ... | ||
if (node.dotDotDotToken != null) { | ||
return this.report(node.dotDotDotToken, E.unexpectedParamSpreadForContextParam()); | ||
} | ||
var symbol = this.ctx.checker.getSymbolAtLocation(node.type.typeName); | ||
if (symbol == null) { | ||
return this.report(node.type.typeName, E.expectedTypeAnnotationOnContextToBeResolvable()); | ||
} | ||
var declaration = this.ctx.findSymbolDeclaration(symbol); | ||
if (declaration == null) { | ||
return this.report(node.type.typeName, E.expectedTypeAnnotationOnContextToHaveDeclaration()); | ||
} | ||
if (this.ctx.gqlContext == null) { | ||
// This is the first typed context value we've seen... | ||
this.ctx.gqlContext = { | ||
declaration: declaration, | ||
firstReference: node.type.typeName | ||
}; | ||
} | ||
else if (this.ctx.gqlContext.declaration !== declaration) { | ||
return this.report(node.type.typeName, E.multipleContextTypes(), [ | ||
this.related(this.ctx.gqlContext.firstReference, "A different type reference was used here"), | ||
]); | ||
} | ||
}; | ||
Extractor.prototype.methodDeclaration = function (node) { | ||
@@ -1053,2 +1104,6 @@ var tag = this.findTag(node, exports.FIELD_TAG); | ||
} | ||
var context = node.parameters[1]; | ||
if (context != null) { | ||
this.validateContextParameter(context); | ||
} | ||
var description = this.collectDescription(node.name); | ||
@@ -1252,3 +1307,3 @@ var id = this.expectIdentifier(node.name); | ||
} | ||
return this.report(node, E.expectedIdentifer()); | ||
return this.report(node, E.expectedIdentifier()); | ||
}; | ||
@@ -1255,0 +1310,0 @@ Extractor.prototype.findTag = function (node, tagName) { |
@@ -44,2 +44,3 @@ "use strict"; | ||
var serverDirectives_1 = require("./serverDirectives"); | ||
var helpers_1 = require("./utils/helpers"); | ||
var serverDirectives_2 = require("./serverDirectives"); | ||
@@ -91,2 +92,3 @@ __createBinding(exports, serverDirectives_2, "applyServerDirectives"); | ||
var definitions = Array.from(serverDirectives_1.DIRECTIVES_AST.definitions); | ||
var errors = []; | ||
try { | ||
@@ -103,3 +105,4 @@ for (var _c = __values(program.getSourceFiles()), _d = _c.next(); !_d.done; _d = _c.next()) { | ||
if (typeErrors.length > 0) { | ||
return (0, DiagnosticError_1.err)(typeErrors); | ||
(0, helpers_1.extend)(errors, typeErrors); | ||
continue; | ||
} | ||
@@ -114,3 +117,4 @@ } | ||
// the first one. | ||
return (0, DiagnosticError_1.err)([syntaxErrors[0]]); | ||
errors.push(syntaxErrors[0]); | ||
continue; | ||
} | ||
@@ -120,4 +124,6 @@ } | ||
var extractedResult = extractor.extract(); | ||
if (extractedResult.kind === "ERROR") | ||
return extractedResult; | ||
if (extractedResult.kind === "ERROR") { | ||
(0, helpers_1.extend)(errors, extractedResult.err); | ||
continue; | ||
} | ||
try { | ||
@@ -145,2 +151,5 @@ for (var _e = (e_2 = void 0, __values(extractedResult.value)), _f = _e.next(); !_f.done; _f = _e.next()) { | ||
} | ||
if (errors.length > 0) { | ||
return (0, DiagnosticError_1.err)(errors); | ||
} | ||
// If you define a field on an interface using the functional style, we need to add | ||
@@ -147,0 +156,0 @@ // that field to each concrete type as well. This must be done after all types are created, |
@@ -122,2 +122,3 @@ "use strict"; | ||
}); | ||
var gratsDir = path.join(__dirname, "../.."); | ||
var fixturesDir = path.join(__dirname, "fixtures"); | ||
@@ -188,5 +189,2 @@ var integrationFixturesDir = path.join(__dirname, "integrationFixtures"); | ||
} | ||
if (server.Query == null || typeof server.Query !== "function") { | ||
throw new Error("Expected `".concat(filePath, "` to export a Query type as `Query`")); | ||
} | ||
options = { | ||
@@ -197,3 +195,8 @@ nullableByDefault: true | ||
parsedOptions = { | ||
options: {}, | ||
options: { | ||
// Required to enable ts-node to locate function exports | ||
rootDir: gratsDir, | ||
outDir: "dist", | ||
configFilePath: "tsconfig.json" | ||
}, | ||
raw: { | ||
@@ -213,3 +216,3 @@ grats: options | ||
source: server.query, | ||
rootValue: new server.Query() | ||
rootValue: server.Query != null ? new server.Query() : null | ||
})]; | ||
@@ -216,0 +219,0 @@ case 2: |
@@ -18,2 +18,10 @@ import { DefinitionNode, DocumentNode, FieldDefinitionNode, Location, NameNode } from "graphql"; | ||
/** | ||
* Information about the GraphQL context type. We track the first value we see, | ||
* and then validate that any other values we see are the same. | ||
*/ | ||
type GqlContext = { | ||
declaration: ts.Node; | ||
firstReference: ts.Node; | ||
}; | ||
/** | ||
* Used to track TypeScript references. | ||
@@ -36,2 +44,3 @@ * | ||
_unresolvedTypes: Map<NameNode, ts.Symbol>; | ||
gqlContext: GqlContext | null; | ||
hasTypename: Set<string>; | ||
@@ -38,0 +47,0 @@ constructor(options: ts.ParsedCommandLine, checker: ts.TypeChecker, host: ts.CompilerHost); |
@@ -52,2 +52,5 @@ "use strict"; | ||
this._unresolvedTypes = new Map(); | ||
// The resolver context declaration, if it has been encountered. | ||
// Gets mutated by Extractor. | ||
this.gqlContext = null; | ||
this.hasTypename = new Set(); | ||
@@ -54,0 +57,0 @@ this._options = options; |
@@ -7,2 +7,2 @@ export declare class DefaultMap<K, V> { | ||
} | ||
export declare function extend<T>(a: T[], b: T[]): void; | ||
export declare function extend<T>(a: T[], b: readonly T[]): void; |
{ | ||
"name": "grats", | ||
"version": "0.0.0-main-b7bb4612", | ||
"version": "0.0.0-main-ba6da9cd", | ||
"main": "dist/src/index.js", | ||
@@ -32,4 +32,8 @@ "bin": "dist/src/cli.js", | ||
"packageManager": "pnpm@8.1.1", | ||
"engines": { | ||
"node": ">=16 <=21", | ||
"pnpm": "^8" | ||
}, | ||
"scripts": { | ||
"test": "ts-node --esm src/tests/test.ts", | ||
"test": "ts-node src/tests/test.ts", | ||
"integration-tests": "node src/tests/integration.mjs", | ||
@@ -36,0 +40,0 @@ "build": "tsc --build", |
@@ -17,3 +17,3 @@ # -=[ ALPHA SOFTWARE ]=- | ||
By making your TypeScript implementation the source of truth, you never have to | ||
worry about valuating that your implementation matches your schema. Your | ||
worry about validating that your implementation matches your schema. Your | ||
implementation _is_ your schema! | ||
@@ -20,0 +20,0 @@ |
209038
49
4324
242
80